acdream/docs/research/deepdives/r02-combat-system.md
Erik 3f913f1999 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.
2026-04-18 10:32:44 +02:00

1090 lines
45 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# R2 — Retail AC Combat System (Deep Dive)
**Scope:** The complete physical combat system as shipped in retail Asheron's
Call: attack modes, attack types, attack heights, the power/accuracy bar,
the full damage formula, crit mechanics, evasion, body-part targeting,
resistances, wire format, and PvP differences. This is the authoritative
reference for porting combat into **acdream**.
**Sources used:**
- `acclient.exe` decompilation, primarily `chunk_00560000.c` (combat HUD +
notification handlers) — FUN_0056AC80 family and FUN_0056D370 family
have the "You hit / You evaded / Critical!" text formatting and the
AttackConditions bit-decode. Client-side combat is mostly *presentation*;
the math lives on the server, but the wire format and the constants the
client bakes in are ground truth.
- **ACE** server-side port: `DamageEvent.cs`, `Player_Combat.cs`,
`Player_Melee.cs`, `Creature_Combat.cs`, `Creature_BodyPart.cs`,
`BodyPartTable.cs`, `WorldObject_Weapon.cs`, `SkillCheck.cs`,
`SkillFormula.cs`, `Armor.cs`, `AttackQueue.cs`. ACE's formulas are
the most complete open-source reconstruction; they match retail pcaps
and data-mined formulas.
- **Chorizite.ACProtocol** generated types for the wire definitions of
C2S `Combat_TargetedMeleeAttack`/`TargetedMissileAttack` and S2C
`Combat_HandleAttackerNotificationEvent`/`HandleDefenderNotificationEvent`
plus evasion + death.
- `docs/research/acclient_function_map.md` for correlating decompiled FUNs.
> **A note on retail vs ACE.** The retail client does **not** calculate
> damage — it sends intent (`target_guid`, `attack_height`, `power_level`)
> and receives the resulting damage number + flags. The formulas below
> are the server's job; for acdream (a client), we implement them so we
> can (a) predict outcomes for the power bar preview, (b) verify what we
> receive against what we'd expect, and (c) so the plugin API can surface
> "why did my attack do 47?" telemetry. The actual authority is whatever
> the ACE server sends back.
---
## 1. Attack modes (CombatMode enum)
Retail has five values, bit-packed. Source:
`ACE.Entity.Enum.CombatMode`:
| Bit | Name | Purpose |
|--------|-------------|-----------------------------------------------------|
| 0x00 | `Undef` | Uninitialized / server-side only |
| 0x01 | `NonCombat` | "Peace mode" — weapon sheathed, cannot attack |
| 0x02 | `Melee` | Weapon drawn, melee swing animations available |
| 0x04 | `Missile` | Bow/crossbow/atlatl drawn, arrow nocked |
| 0x08 | `Magic` | Wand/orb ready, casting stance |
Helpers: `ValidCombat = NonCombat|Melee|Missile|Magic`,
`CombatCombat = Melee|Missile|Magic` (anything that actually swings).
**Transitions.** The client sends `Combat_ChangeCombatMode` (GameAction
0x0053) carrying a single `uint32 Mode`. The server plays the stance-swap
animations and responds with `PrivateUpdatePropertyInt(CombatMode, newMode)`.
The swap takes real time — you cannot attack during it. ACE's
`SetCombatMode` calls `MotionTable.GetAnimationLength(MotionTableId,
Stance, MotionCommand.Ready, targetMotion)` and queues the attack
window to fire after that animLength. For **weapon swap** (e.g. bow →
sword), the client does `currentStance → NonCombat → HandCombat → NonCombat
→ newStance`, with each hop incurring its own animLength; ACE's
`SwitchCombatStyles()` computes the sum.
**MotionStance** is the fine-grained counterpart tied to held-item
geometry. Values: `NonCombat`, `HandCombat` (unarmed), `SwordCombat`,
`SwordShieldCombat`, `TwoHandedSwordCombat`, `BowCombat`, `CrossbowCombat`,
`AtlatlCombat`, `SlingCombat`, `ThrownWeaponCombat`, `ThrownShieldCombat`,
`DualWieldCombat`, `Magic`. The translation from weapon to stance lives
in `Creature_Combat.GetWeaponStance(WorldObject weapon)`:
```
CombatStyle.OneHanded → SwordCombat
CombatStyle.OneHandedAndShield→ SwordShieldCombat
CombatStyle.TwoHanded → TwoHandedSwordCombat
CombatStyle.Bow → BowCombat
CombatStyle.Crossbow → CrossbowCombat
CombatStyle.Atlatl → AtlatlCombat
CombatStyle.Sling → SlingCombat
CombatStyle.ThrownWeapon → ThrownWeaponCombat
CombatStyle.ThrownShield → ThrownShieldCombat
CombatStyle.DualWield → DualWieldCombat
CombatStyle.Unarmed → HandCombat
CombatStyle.Magic → Magic
```
If a shield is also equipped and the stance is SwordCombat or
ThrownWeaponCombat, it is promoted to the `…ShieldCombat` variant.
---
## 2. Attack types (AttackType enum + weapon styles)
Retail stores attack type as a bitflag because a single weapon can offer
multiple animations (thrust OR slash, pickable with the power bar). From
`AttackType.cs`:
| Bit | Flag | Notes |
|---------|--------------------|----------------------------------------|
| 0x0001 | `Punch` | Unarmed jab |
| 0x0002 | `Thrust` | Single-weapon thrust |
| 0x0004 | `Slash` | Single-weapon slash |
| 0x0008 | `Kick` | High-power unarmed |
| 0x0010 | `OffhandPunch` | Dual-wield left-hand punch |
| 0x0020 | `DoubleSlash` | Rapier / scimitar style |
| 0x0040 | `TripleSlash` | Quickness / fast swords |
| 0x0080 | `DoubleThrust` | Stiletto style |
| 0x0100 | `TripleThrust` | Spears / jambiya |
| 0x0200 | `OffhandThrust` | |
| 0x0400 | `OffhandSlash` | |
| 0x0800 | `OffhandDoubleSlash`| |
| 0x1000 | `OffhandTripleSlash`| |
| 0x2000 | `OffhandDoubleThrust`| |
| 0x4000 | `OffhandTripleThrust`| |
Composites: `Unarmed = Punch|Kick|OffhandPunch`;
`DoubleStrike`, `TripleStrike`, `MultiStrike = Double|Triple`;
`Offhand = <all offhand bits>`;
`Thrusts`, `Slashes`, `Punches` (the single-technique groupings).
**How a weapon picks its animation.** `WorldObject.GetAttackType(stance,
powerLevel, offhand)` (ACE `WorldObject_Weapon.cs:1050`):
1. If `offhand` is true, return the offhand variant.
2. Start from the weapon's `W_AttackType` bitfield.
3. Apply stance-specific overrides. The key rule: many weapons have both
Thrust and Slash. The **ThrustThreshold** (= `0.33f`, sourced from the
Dark Majesty Strategy Guide p.150) gates which one is used:
```
powerLevel < 0.33 → use Thrust
powerLevel >= 0.33 → use Slash
```
Some weapons use `DoubleThrust|DoubleSlash` or
`TripleThrust|TripleSlash` pairs; the threshold still applies.
4. Shield stance forces thrust on multi-strike weapons; sword stance
(no shield) forces slash.
**Multi-strike to single-strike reduction.** `AttackType.ReduceMultiStrike`
collapses `Double/Triple{Thrust,Slash}` → `{Thrust,Slash}` for the
damage-type decision, because multi-strike only affects the animation
loop and swing count, not what body-parts it hits or what damage type
it applies.
**GetNumStrikes.** Returns 1, 2, or 3 based on the flag. The player's
`Attack()` function divides the anim length by numStrikes and fires a
`DamageTarget()` call per sub-strike (each one rolling its own damage,
its own crit, its own evasion). One power-bar commit = N damage rolls.
**From the decompiled client side** (`chunk_00560000.c:8186`), the
formatted hit message is literally:
```c
FUN_00406500(&puStack_58, "You %s %s for %d point%s of %sdamage!",
verb, targetName, damage, pluralS, damageTypeStr);
```
Where `verb` is produced from `local_50 + 5` — a table keyed off attack
type (e.g. "slash", "thrust", "pierce"). So the retail HUD verbifies the
attack type.
---
## 3. Attack heights (AttackHeight enum)
From `AttackHeight.cs`:
```
High = 1
Medium = 2
Low = 3
```
The client sends this as a `uint32` in both melee and missile attack
packets. **Its effect on damage** flows through *body-part selection*,
not a direct damage multiplier. The flow:
1. Retail builds a **Quadrant** by OR-ing the height with the attacker's
relative direction:
```
quadrant = attackHeight.ToQuadrant() | attacker.GetRelativeDir(defender);
```
2. The defender's `BodyPartTable` has 12 buckets, one per quadrant:
`[HLF MLF LLF | HRF MRF LRF | HLB MLB LLB | HRB MRB LRB]`
(where H/M/L = High/Medium/Low, L/R = Left/Right, F/B = Front/Back).
3. Each creature weenie's `PropertiesBodyPart` has per-quadrant
probabilities; `BodyPartTable.RollBodyPart(quadrant)` weighted-random
selects one.
4. The selected `CombatBodyPart` has its own armor and its own crit
multiplier.
**For player defenders** (the retail client fills this in differently)
the flow is simpler because player body-parts are not in a full weenie
table; `BodyParts.GetBodyPart(AttackHeight)` picks uniformly from:
```
High → Head, Chest, UpperArm
Medium → Chest, Abdomen, UpperArm, LowerArm, Hand, UpperLeg
Low → Foot, LowerLeg
```
The selected body part then maps to a `CoverageMask` which selects which
armor/clothing pieces resist the hit. Medium attacks hit the meatiest
torso zones and therefore usually connect with chest armor, which is
typically the best-AL piece — this is *why* players who want to mitigate
incoming damage should buff chest pieces. Low attacks hit feet/legs
which often have lower AL — **low attacks are typically more damaging**
on well-geared players.
**On monsters,** height × quadrant distributes across 27 possible
`CombatBodyPart` values (Head, Chest, Abdomen, UpperArm, LowerArm, Hand,
UpperLeg, LowerLeg, Foot, Horn, FrontLeg, FrontFoot, RearLeg, RearFoot,
Torso, Tail, Arm, Leg, Claw, Wings, Breath, Tentacle, UpperTentacle,
LowerTentacle, Cloak — enum `CombatBodyPart`). Different monsters have
different coverage; a drudge has limbs, an olthoi has mandibles + abdomen
+ legs. The weenie table encodes this.
---
## 4. Power bar / accuracy bar
**The power bar is a user-charged attack intensity meter**, 0.0 → 1.0.
The client holds the attack key to fill it; releasing fires an attack
packet carrying the current value in the `Power` or `Accuracy` float
field. Player_Melee.cs:
```csharp
public float PowerLevel { get; set; } // 0.0..1.0
public float AccuracyLevel{ get; set; } // 0.0..1.0 (bows)
public float GetPowerAccuracyBar() =>
GetCombatType() == CombatType.Missile ? AccuracyLevel : PowerLevel;
public PowerAccuracy GetPowerRange() =>
PowerLevel < 0.33f ? Low :
PowerLevel < 0.66f ? Medium :
High;
```
**How long does it take to fill?** ACE's refill model:
`NextRefillTime = now + PowerLevel * refillMod` after an attack lands,
where `refillMod = 0.8f` for dual-wield (20% faster) else 1.0. Retail
uses approximately 1 second for full power from a hard release; charging
to 50% bar = ~0.5s, full = ~1.0s. The exact tick rate is handled by the
**client's** UI animation; the server only sees the final `PowerLevel`
float the client sends and trusts it (this is why power-bar-hacks
historically existed — no server-side verification).
### Effect of power on damage math
Player_Combat.cs:
```csharp
public override float GetPowerMod(WorldObject weapon)
{
if (weapon == null || !weapon.IsRanged)
return PowerLevel + 0.5f; // → [0.5, 1.5] range for melee/thrown
else
return 1.0f; // melee power-bar inert for bows
}
public override float GetAccuracyMod(WorldObject weapon)
{
if (weapon != null && weapon.IsRanged)
return AccuracyLevel + 0.6f;// → [0.6, 1.6] range for bows
else
return 1.0f;
}
```
So the **effective multiplier ranges**:
- **Melee / thrown:** `PowerMod ∈ [0.5, 1.5]`, applied as a *damage*
multiplier. Zero-bar does half damage, full-bar does 1.5×.
- **Bows / crossbows / atlatls:** `AccuracyMod ∈ [0.6, 1.6]`, applied
as an *attack-skill* multiplier (makes you more likely to *hit*, not
hit harder). Bow damage is power-bar-inert; bows have a separate
`DamageMod` attribute on the launcher (e.g. Yumi = 2.13).
**Stamina cost.** `Player.GetAttackStamina(PowerAccuracy)` from
`Player_Combat.cs:621` interpolates a cost from a 3×3 table keyed by
bucket (Low/Med/High) and held-item burden, then scales down by an
Endurance bonus (caps at 50% reduction at ~290 Endurance). Low-bar
thrust costs 1 stam per 700 burden; high-bar full swing costs 2 per 700,
4 per 1200, 6 per 1600. Running out of stamina drops defense to 0 AND
halves your weapon skill.
---
## 5. Damage formula — the full retail expression
**Canonical form, exactly as implemented in `DamageEvent.DoCalculateDamage`:**
```
// PHASE 1 — hit?
evasionChance = 1 - SkillCheck.GetSkillChance(
effectiveAttackSkill,
effectiveDefenseSkill,
0.03f);
if (rng() < evasionChance) → EVADE, damage = 0, exit.
// PHASE 2 — base damage roll
baseDamage = rng(weapon.MinDamage, weapon.MaxDamage) // uniform
// PHASE 3 — pre-mitigation multipliers
damageBefore =
baseDamage
* attributeMod // [1.0 … 5.5] based on Str/Coord
* powerMod // [0.5 … 1.5] melee, 1.0 missile
* slayerMod // 1.0 base, ×bonus vs slayer type
* damageRatingMod // additive combine (see §6)
// PHASE 4 — CRITICAL override (rolled after non-crit compute)
if (rng() < criticalChance) {
damageBefore = weapon.MaxDamage
* attributeMod
* powerMod
* slayerMod
* damageRatingMod // recomputed, no Recklessness in crits
* criticalDamageMod; // 1 + wepMultiplier
}
// PHASE 5 — mitigation
damage = damageBefore
* armorMod // AL → 200/3 / (AL + 200/3)
* shieldMod // shield SL absorption
* resistanceMod // natural + buff resists
* damageResistanceRatingMod; // DRR from augmentations
return round(damage);
```
Every factor is a multiplicative `float`, applied in order. `AdditiveCombine`
is used **within** the `damageRatingMod` slot — that's where DR, recklessness,
sneak attack, and heritage stack additively (see §6). Everything else is
pure multiplication.
### Component details
**`baseDamage`** — `BaseDamageMod` on the weapon wraps a `BaseDamage(max,
variance)` plus enchantments. Actual min = `max × (1 variance × VarianceMod)`.
`ThreadSafeRandom.Next(min, max)` uniformly samples. Blood Drinker
enchantments add a flat `DamageBonus`. Missile launchers (bows) add
`ElementalBonus` (additive int) when the ammo damage type matches the bow's
imbued element.
**`attributeMod`** — from `SkillFormula.GetAttributeMod`:
```csharp
public const float DefaultMod = 0.011f; // melee, finesse, thrown, atlatl
public const float BowMod = 0.008f; // bows, crossbows
attributeMod = max(1.0f, 1.0f + (currentSkill - 55) * factor)
```
Note: despite the name this is *not* a direct attribute stat — it's the
current weapon skill being damage-scaled. The name comes from the fact
that in pre-MoA AC it WAS tied to the primary attribute. Post-MoA, it's
the combat skill. A 400 Heavy Weapons skill gives
`1 + (400-55)*0.011 = 1 + 3.795 = 4.795`. A 200 bow skill gives
`1 + 145*0.008 = 2.16`.
**`powerMod`** — see §4. `PowerLevel + 0.5` melee, `1.0` missile.
**`slayerMod`** — `WorldObject.GetWeaponCreatureSlayerModifier`. If the
weapon has a `SlayerCreatureType` matching the target's `CreatureType`,
returns the stored `SlayerDamageBonus` (values like 1.53.0 in data). Else
1.0.
**`damageRatingMod`** — `Creature.AdditiveCombine(damageRatingBaseMod,
recklessnessMod, sneakAttackMod, heritageMod [, pkDamageMod])`. Each
input is something like `1.xx`, and `AdditiveCombine` does:
```
(x1 1) + (x2 1) + … + 1
```
i.e. additive-in-delta. So `1.10 × 1.20` ≠ `AdditiveCombine(1.10, 1.20)
= 1.30`.
**`armorMod`** — see §9.
**`shieldMod`** — `GetShieldMod`. If the attacker is within the shield's
effective arc (default 180° front cone), compute
`effectiveSL = baseSL + impenMods`,
`effectiveRL = baseRL + baneMods` (clamped [-2, +2]),
`effectiveLevel = effectiveSL × effectiveRL`, capped at Shield skill
(`skill.Current` if specialized, else `skill.Current / 2`). Final
`shieldMod = SkillFormula.CalcArmorMod(effectiveLevel)`.
**`resistanceMod`** — §10.
**`damageResistanceRatingMod`** — rating-based mitigation from
augmentations/buffs/imbues, converted via `GetNegativeRatingMod`.
### Worked examples
**Example A — low-level Chorizon, swords, full bar, hits a drudge:**
- Heavy Weapons skill 125, Strength 70, no enchantments
- Weapon: Iron Sword (max 12, variance 0.5 → min 6)
- Target: Drudge Skulker — BaseArmor 24 on chest, natural 0 resists
- PowerLevel 1.0, no crit, no aug DR, front-left medium quadrant hit
Steps:
1. `baseDamage = rng(6, 12) = 9` (sample)
2. `attributeMod = 1 + (125 55)×0.011 = 1 + 0.77 = 1.77`
3. `powerMod = 1.0 + 0.5 = 1.5`
4. `slayerMod = 1.0`, `damageRatingMod = 1.0`
5. `damageBefore = 9 × 1.77 × 1.5 × 1.0 × 1.0 = 23.895`
6. Drudge chest: `effectiveAL = 24 × 1.0 = 24`, `armorMod =
(200/3) / (24 + 200/3) = 66.67 / 90.67 = 0.735`
7. `resistanceMod = 1.0` (drudge: slash neutral)
8. `damage = 23.895 × 0.735 × 1.0 × 1.0 × 1.0 = 17.56`
9. Final: `18 damage (slash)`
**Example B — mid-level with crit:**
- Light Weapons skill 300, Coordination 120, Biting Strike rapier
- Weapon: Bone Rapier (max 32, variance 0.4, CritFrequency 0.15)
- Target: Banderling — BaseArmor 50 on chest, 1.3× pierce vulnerability
- PowerLevel 0.3 → thrust, crit rolls TRUE
Steps:
1. `attributeMod = 1 + (300 55)×0.011 = 1 + 2.695 = 3.695`
2. `powerMod = 0.3 + 0.5 = 0.8`
3. On crit, `baseDamage` = weapon max = 32 (not rolled)
4. `critDmgMod = 1.0 + 1.0 = 2.0` (default weapon multiplier)
5. `damageBefore = 32 × 3.695 × 0.8 × 1.0 × 1.0 × 2.0 = 189.1`
6. Banderling chest: `effectiveAL = 50`, `armorMod = 66.67 / 116.67 =
0.571`
7. `resistanceMod = 1.3` (pierce vuln)
8. `damage = 189.1 × 0.571 × 1.3 × 1.0 = 140.4`
9. Final: `140 damage (pierce)` — "Critical hit!"
**Example C — bow vs naked PvP target:**
- Missile Weapons skill 350, Coord 160
- Weapon: Yumi (max 45, var 0.3, DamageMod 2.13 → "max" = 45 × 2.13 = 96)
- Ammo: Cold-imbued arrows (+20 elemental bonus)
- Target: player, 0 natural Cold resist, Prot Cold 6 (res 0.56)
- AccuracyLevel 1.0, no crit, no sneak, NPK so no PK scale
Steps:
1. `maxDamage = (45 + 0 + 20) × 2.13 = 138.45`, `minDamage = 138.45 ×
0.7 = 96.9`
2. `baseDamage = rng(96.9, 138.45) = 118` sampled
3. `attributeMod = 1 + (350 55)×0.008 = 1 + 2.36 = 3.36`
4. `powerMod = 1.0` (bows ignore power bar for damage)
5. `damageBefore = 118 × 3.36 × 1.0 × 1.0 × 1.0 = 396.5`
6. `attackSkill = skill × accuracyMod × offenseMod = 350 × 1.6 × 1.0 =
560` (accuracy boosts *hit chance*, not damage)
7. Target chest clothes only: `effectiveAL = 12 + 0 = 12`, `armorMod =
66.67 / 78.67 = 0.847`
8. `resistanceMod = 0.56` (Prot Cold 6)
9. `damage = 396.5 × 0.847 × 0.56 = 188.1`
10. Final: `188 damage (cold)`
---
## 6. Critical hits
**Base crit chance:**
- **Physical:** 10% (`defaultPhysicalCritFrequency = 0.1f` in
`WorldObject_Weapon.cs:291`).
- **Magic:** 5% (`defaultMagicCritFrequency = 0.05f`, post-Iron Coast
release; was 2% pre-Atonement).
Per-weapon override via `PropertyFloat.CriticalFrequency` (e.g.
Biting Strike quest weapons 15%).
**Critical Strike imbue:** `Math.Max(critRate, GetCriticalStrikeMod(skill))`
— a scaling function that rewards high attack skill. At low skill it's
10%, at 400+ skill it can push physical critical chance past 30%.
**Crit Rating:** `critRate += wielder.GetCritRating() * 0.01f`. Each
point of Crit Rating = +1% flat.
**Crit Resist Rating:** mitigates incoming crits as a **multiplicative
reduction** via `GetNegativeRatingMod` — rating 20 → critRate ×= 0.833.
**Critical damage multiplier:** `CriticalDamageMod = 1.0 +
weapon.CriticalMultiplier` where `CriticalMultiplier` defaults to
`1.0f` (so default crit is 2× max damage). Crippling Blow imbue
replaces this when higher via `Math.Max`.
**Crit damage bookkeeping:**
1. On crit, `DamageBeforeMitigation = weapon.MaxDamage × attributeMod ×
powerMod × slayerMod × damageRatingMod × critDamageMod`. Note
baseDamage is replaced by **max** damage, not re-rolled.
2. Recklessness is **set to 1.0** on crits (doesn't stack with crit
multiplier — retail design decision explicit in the code comment
`// recklessness excluded from crits`).
3. `CriticalDamageRatingMod` (from Crit Damage Rating augmentation)
replaces Recklessness in the rating stack for the crit calculation.
**Critical Defense augmentation:** if the defender has the augmentation
and a crit is rolled, there's a secondary roll:
```
criticalDefenseChance = augRank * 0.05f // vs player attacker
criticalDefenseChance = augRank * 0.25f // vs monster attacker
```
If that succeeds, `CriticalDefended = true`; the attacker still hits
but with normal (non-crit) damage, and the AttackConditions bit 0x01 is
set. The hit message adds: `"Your target's Critical Protection
augmentation allows them to avoid your critical hit!"` (the literal
string lives at 0x00560000.c:8191).
**Logoff auto-crit:** if the defender is logging out OR in a PK logout
freeze window (2 min post-PvP), `criticalChance = 1.0f`. From the dev
notes (ACE comments cite 2002/08 Atonement): "any time a character is
logging off, PK or not, all physical attacks against them become
automatically critical. (Note that spells do not share this behavior.)"
---
## 7. Defense formula — attack skill vs defense skill
The core mechanic uses a **logistic curve** on skill difference.
`SkillCheck.GetSkillChance`:
```csharp
public static double GetSkillChance(int skill, int difficulty,
float factor = 0.03f)
{
var chance = 1.0 - (1.0 / (1.0 + Math.Exp(factor * (skill - difficulty))));
return Math.Clamp(chance, 0.0, 1.0);
}
```
- **Physical combat:** `factor = 0.03f`. Equal skill = 50% hit. +50 skill
advantage → ~81.8% hit. +100 → ~95.3%. +200 → ~99.75%. 50 skill →
~18%. 100 → ~4.7%.
- **Magic:** `factor = 0.07f` — the same curve but **steeper**. Equal
skill is still 50% resist, but +50 skill advantage is already 97%.
This is why magic defense drops off fast when underskilled.
**EvadeChance** = `1 hitChance`. If `rng() < EvadeChance`, the attack
is evaded (no damage, no stamina cost to attacker, 1 stamina cost to
defender in combat mode).
### Effective skills
**Attacker (player) effective attack skill:**
```
effAttack = round(weaponSkill * accuracyMod * offenseMod)
```
- `weaponSkill` = current skill (post-enchantments) of the equipped
weapon's skill category (LightWeapons / HeavyWeapons / FinesseWeapons
/ MissileWeapons / TwoHandedCombat / DualWield).
- `accuracyMod` = `AccuracyLevel + 0.6` for bows, else 1.0.
- `offenseMod` = weapon enchantment like Heart Seeker's attack-skill
mod (e.g. 1.12 for HS VII).
Off-hand attacks: if `DualWield.Current < weapon.Current`, the DualWield
skill replaces the weapon skill. (That's why specialized dual-wield
builds spec DW — you don't want your main-hand skill to leak down into
off-hand hits.)
**Attacker (monster) effective attack skill:**
```
effAttack = round(weaponSkill * offenseMod) // no accuracy mod
```
**Defender effective defense skill:**
```
effDefense = round(defenseSkill * defenseMod * burdenMod * stanceMod
+ defenseImbues)
```
- `defenseSkill` = MeleeDefense vs melee/missile-close-range,
MissileDefense vs missile, MagicDefense vs spells.
- `defenseMod` = Defender weapon enchantment (e.g. Defender V = 1.25).
- `burdenMod` = scales with encumbrance-vs-capacity.
- `stanceMod` from `Player.GetDefenseStanceMod()`:
- `IsJumping` → 0.5
- `IsLoggingOut` → 0.8
- In combat mode (not NonCombat) → 1.0
- NonCombat + Crouch → 0.4
- NonCombat + Sitting → 0.3
- NonCombat + Sleeping → 0.2
- `defenseImbues` = count of equipped items with the matching
MeleeDefense/MissileDefense imbued effect (flat additive).
**Exhausted (Stamina ≤ 0):** `effDefense = 0` → always hit. Also attacker
weapon skill is halved internally. Stamina management **is** a combat
mechanic.
**Overpower.** Some elite monsters (olthoi queens, Virindi lords) have
an `Overpower` property. On attack, an Overpower roll happens **before**
the evasion roll: if successful, the hit is auto-landed regardless of
defense skill. Two formulas exist (`OverpowerMethod` toggle);
Formula B (default in ACE) is:
```
overpowerChance = attacker.Overpower ?? 0 (percent, 0-100)
resistChance = defender.OverpowerResist ?? 0
final = rng() < overpowerChance*0.01
AND rng() >= resistChance*0.01
```
If overpowered, `AttackConditions.Overpower` flag is set and the
attacker notification reads "Overpower!".
---
## 8. Body-part targeting + per-part AL
### Player body parts (9 parts)
From `BodyPart.cs`:
| CombatBodyPart | BodyPart flag | DamageLocation (wire) | Height pool |
|----------------|---------------|-----------------------|-------------|
| Head | 0x001 | 0x0 | High |
| Chest | 0x002 | 0x1 | High, Mid |
| Abdomen | 0x004 | 0x2 | Mid |
| UpperArm | 0x008 | 0x3 | High, Mid |
| LowerArm | 0x010 | 0x4 | Mid |
| Hand | 0x020 | 0x5 | Mid |
| UpperLeg | 0x040 | 0x6 | Mid |
| LowerLeg | 0x080 | 0x7 | Low |
| Foot | 0x100 | 0x8 | Low |
The **High pool is 3 parts**, **Mid is 6 parts**, **Low is 2 parts**.
This is a meaningful targeting choice: High has 1/3 chance of head
(typically best helmet AL), Low has 50% chance of foot (commonly one
of the weakest pieces on an ungeared player).
### Monster body parts (27 parts, per-weenie table)
`CombatBodyPart` enum values 0..26 include generic humanoid parts
(Head/Chest/Abdomen/UpperArm/LowerArm/Hand/UpperLeg/LowerLeg/Foot) plus
monster-specific (Horn, FrontLeg, FrontFoot, RearLeg, RearFoot, Torso,
Tail, Arm, Leg, Claw, Wings, Breath, Tentacle, UpperTentacle,
LowerTentacle, Cloak). Not every creature has every part — the weenie's
`PropertiesBodyPart` dictionary holds only the parts it has, each with:
- `BaseArmor` (int) — AL for that part
- `HLF, MLF, LLF, HRF, MRF, LRF, HLB, MLB, LLB, HRB, MRB, LRB` (float) —
per-quadrant hit probability (most are 0.0, a few non-zero per part)
- `ArmorVsSlash, ArmorVsPierce, ..., ArmorVsNether` (int) — the
per-damage-type resist scaling (multiplied into BaseArmor)
A drudge skulker's torso dominates the MLF/MRF/MLB/MRB cells; its head
appears only in HLF/HRF/HLB/HRB. Creature armor is per-part, not global.
### Per-part AL + resistance scaling
`Creature_BodyPart.GetEffectiveArmorVsType`:
```csharp
var armorVsType = biota.BaseArmor * (float)Creature.GetArmorVsType(damageType);
var enchantMod = ignoreMagicResist ? 0 : EnchantmentManager.GetBodyArmorMod();
var effectiveAL = armorVsType + enchantMod;
foreach (var armorLayer in armorLayers)
effectiveAL += GetArmorMod(armorLayer, damageType, ignoreMagicArmor);
if (effectiveAL > 0)
effectiveAL *= armorRendingMod; // 1.0 unless ArmorRending imbue
return effectiveAL;
```
For players, the `armorLayers` are the actual equipped armor/clothing
pieces whose `ClothingPriority` covers the hit body part — so a chest
piece + a surcoat + an undershirt all stack. `Clothing.GetArmorMod()`
per layer:
```
effectiveAL_piece = piece.BaseArmorLevel + impenAdditive
effectiveRL_piece = piece.ResistanceVsType + baneAdditive
effectiveRL_piece = clamp(effectiveRL_piece, -2.0, +2.0)
layerAL = effectiveAL_piece * effectiveRL_piece
```
Sum across layers → final effective AL → `SkillFormula.CalcArmorMod`.
---
## 9. Damage types + resistances
### 7 damage types + 4 special
`DamageType.cs`:
| Flag | Value | Notes |
|------------|------------|-------------------------------------------|
| Slash | 0x001 | Physical |
| Pierce | 0x002 | Physical |
| Bludgeon | 0x004 | Physical |
| Cold | 0x008 | Elemental |
| Fire | 0x010 | Elemental |
| Acid | 0x020 | Elemental |
| Electric | 0x040 | Elemental |
| Health | 0x080 | Drain (harm) |
| Stamina | 0x100 | Drain |
| Mana | 0x200 | Drain |
| Nether | 0x400 | Void magic |
| Base | 0x10000000 | Prismatic arrow sentinel |
Helpers: `Physical = Slash|Pierce|Bludgeon`, `Elemental =
Cold|Fire|Acid|Electric`.
**Multi-damage weapons.** Many weapons have `Slash|Pierce` or similar
bitfields. The selection process in `Player.GetDamageType`:
1. If the weapon has a single damage type → use it.
2. If `DamageType.Slash|Pierce`:
- Unarmed: low power → Pierce, high power → Slash
- Thrust attack → Pierce
- Otherwise → Slash
3. Other multi-bit → `SelectDamageType(powerLevel)`:
- If `powerLevel < 0.33`, bias to Physical; else bias to Elemental.
- Randomly pick from the resulting subset.
This is how **pyreal (ivory-blade) weapons** and **Aerfalle-style
elemental weapons** work — the power bar biases whether you do physical
or elemental on a given swing.
### Resistances
Each creature has per-damage-type `ResistXxxMod` (float, 1.0 default):
```
ResistSlash, ResistPierce, ResistBludgeon,
ResistFire, ResistCold, ResistAcid, ResistElectric,
ResistNether,
ResistHealthBoost, ResistStaminaDrain, ResistManaDrain
```
A value of `0.5` = 50% reduction (Prot 5), `2.0` = 100% bonus damage
(Vuln 6). The clamp is [-2, +2].
### Natural resistances (players only)
Retail **April 2002 Betrayal patch**: player Str+End combination grants
a passive resistance to the 7 damage types, capping at 50% reduction
equivalent to Life Prot V. From `Player_Combat.cs:GetNaturalResistance`:
```csharp
var strAndEnd = Strength.Base + Endurance.Base;
if (strAndEnd <= 200)
return 1.0f;
var natRes = 1.0f - (float)(strAndEnd - 200) / 300 * 0.5f;
return Math.Max(natRes, 0.5f);
```
Tiers (for the UI description):
- ≤200: None
- 201260: Poor (110% reduction)
- 261320: Mediocre
- 321380: Hardy
- 381440: Resilient
- 441+: Indomitable (cap, 50% reduction)
**Crucial detail:** natural resists do NOT stack with Life Protection
spells. A higher-rank Prot spell **overwrites** natural resists. However,
vulns subtract against the prot; if the net is worse than natural, you
still don't go below natural. This is why some low-end characters are
tougher than you'd expect.
**Nether exception:** All creatures "under Asheron's protection" take
50% damage from Nether damage by default (fandom: Anniversary-patch
announcement). `GetNaturalResistance(DamageType.Nether)` hardcodes 0.5.
### Final resistance math
`ResistanceMod = playerDefender.GetResistanceMod(damageType, attacker,
weapon, weaponResistanceMod)`. Composed of:
```
naturalRes = GetNaturalResistance(damageType) // 0.5..1.0
prot/vuln = EnchantmentManager.GetResistanceMod(damageType)
× weaponResistanceMod // weapon rend / anti-mod
final = min(naturalRes, prot) AND × vuln effects
```
The exact `min`/`max` logic is subtle; the simplified version is: if the
life-prot is stronger than natural, prot wins; else natural wins (this
is what the April 2002 announcement said). Vulns cancel prots in full
before falling back to natural.
---
## 10. Wire format
### Client → Server (C2S GameActions)
All these wrap in the `GameAction` (0xF7B1) envelope over the
ordered/reliable message stream. Payload layout:
**Targeted Melee Attack — GameAction 0x0008:**
```
uint32 ObjectId // target guid
uint32 Height // AttackHeight enum: 1=H, 2=M, 3=L
float32 Power // [0.0, 1.0], clamped server-side
```
Total payload: 12 bytes (after the GameAction header).
**Targeted Missile Attack — GameAction 0x000A:**
```
uint32 ObjectId
uint32 Height
float32 Accuracy // [0.0, 1.0]
```
**Change Combat Mode — GameAction 0x0053:**
```
uint32 Mode // CombatMode enum
```
**Cancel Attack — GameAction 0x01B7:**
```
(empty body)
```
### Server → Client (S2C GameEvents wrapped in 0xF7B0 Ordered GameEvent)
**AttackerNotification (0x01B3) — "You hit X for N damage!":**
```
string16L DefenderName // 2-byte length, UTF-16LE bytes, dword-aligned
uint32 Type // DamageType
float64 DamagePercent // fraction of defender MaxHealth, 0..1
uint32 Damage // actual damage applied
uint32 Critical // 0 or 1 (really a bool, written as uint32)
uint32 AttackConditions // AttackConditionsMask bits
(align to 4 bytes)
```
Max observed length ~76 bytes.
**DefenderNotification (0x01B5) — "The monster hits you!":**
```
string16L AttackerName
uint32 Type // DamageType
float64 DamagePercent
uint32 Damage
uint32 Location // DamageLocation (0..8, player-only)
uint32 Critical
uint32 AttackConditions
(align to 4)
```
Max ~80 bytes.
**EvasionAttackerNotification (0x01B8):**
```
string16L DefenderName // "X evaded your attack."
```
**EvasionDefenderNotification (0x01B6):**
```
string16L AttackerName // "You evaded X's attack."
```
**AttackDone (0x01B4):**
```
uint32 Number // appears unused by client; WeenieError in ACE
```
**CommenceAttack (0x01B7):**
```
(empty body, just hourglass start for repeat attacks)
```
**PlayerDeathEvent (S2C direct 0x019E, not a GameEvent):**
```
string16L Message // Death message ("X killed you in battle!")
uint32 KilledId
uint32 KillerId
```
**VictimNotificationSelf / Other (GameEvent):**
```
string16L Message // death message for victim (self) or bystander (other)
```
### AttackConditions bit layout
From the retail client decompilation (`chunk_00560000.c:8173-8201`)
confirming bit meanings:
```c
if (param_6 != 0) "Critical hit! " // the `critical` param, separate
if ((uVar2 & 8) != 0) "Overpower! "
if ((uVar2 & 4) != 0) "Sneak Attack! "
if ((uVar2 & 2) != 0) "Recklessness! "
if ((uVar2 & 1) != 0) " Your target's Critical Protection augmentation allows them to avoid your critical hit!"
```
So the bits are exactly:
| Bit | Meaning |
|------|----------------------------------------------|
| 0x01 | CriticalProtectionAugmentation (critical defended) |
| 0x02 | Recklessness active |
| 0x04 | Sneak Attack triggered |
| 0x08 | Overpower triggered |
(ACE's `AttackConditions.cs` and Chorizite's `AttackConditionsMask` both
match this, modulo Chorizite missing the Overpower bit in its generated
enum — an oversight to correct in acdream.)
---
## 11. PvP vs PvE differences
### PK status flags
`PlayerKillerStatus`:
- `NPK` — non-PK, cannot attack or be attacked by players
- `PK` — full player killer, can attack other PKs
- `PKLite` — PK-Lite, non-lethal PvP (no corpse drop on death to other PKL)
- `Free` — special (event/gladiator), can attack anyone
- `NPK_Protected` — post-revive grace period
### Attack gating
`Player.CheckPKStatusVsTarget(target, spell)`:
1. Either side `Free` → allowed.
2. `NPK` attacker with harmful spell on player → reject
(`WeenieError.YouFailToAffect_YouAreNotPK`).
3. `NPK` defender → reject
(`WeenieError._FailsToAffectYou_TheyAreNotPK`).
4. Different PK types (PK vs PKL, etc.) → reject with
`NotSamePKType` unless it's a beneficial spell on NPK.
5. Housing permission check: `CheckHouseRestrictions` — attacks across
restricted-house cell boundaries denied.
Mismatched PK type on a monster target (e.g. a PK quest-monster vs NPK
attacker) also rejects.
### PK damage adjustments
In PvP:
- `PkDamageMod = Creature.GetPositiveRatingMod(attacker.GetPKDamageRating())`
added to the damage rating stack.
- `PkDamageResistanceMod = Creature.GetNegativeRatingMod(defender.
GetPKDamageResistRating())` added to damage resistance stack.
- PvP elemental damage bonus is **halved**:
```csharp
if (modifier > 1.0f && target is Player)
modifier = 1.0f + (modifier - 1.0f) * 0.5f;
```
### PK timers
- `LastPkAttackTimestamp` — updated on successful PvP hit (both sides).
- `pk_timer` property (typically 30s) — while active, cannot logout
normally, spawns are gated.
- `PKLogoffTimer = 2 minutes` — logout freeze window. During this window
all physical hits auto-crit against you.
### Lifestone protection
5 min invulnerability window after lifestone tie; `UnderLifestoneProtection`.
Any PvP attack dispels it on the attacker (not the defender). Server sends
`"The Lifestone's magic protects X from the attack!"` (FUN_00570000:2431
in the decompile, verbatim). Damage = 0, `LifestoneProtection` bit set.
---
## 12. Port plan for acdream
acdream is the **client**, so our combat layer splits into:
1. **Client-authoritative UI/prediction** — power bar widget, attack
staging, send attack GameActions, play swing animation while waiting.
2. **Wire decoder** — parse inbound `AttackerNotification`,
`DefenderNotification`, `EvasionNotification`, `AttackDone`,
`PlayerDeathEvent` and surface them to the plugin API + chat/HUD.
3. **Shadow damage calculator** — our own implementation of the ACE
formulas, used to (a) feed the power-bar damage preview, (b) sanity
check server damage (for anti-cheat debug output, never used as
authority), (c) give plugins access to "if I attacked with X, how
much would I do?" queries.
### Suggested namespace layout
```
acdream.Combat/
CombatMode.cs // enum: Undef|NonCombat|Melee|Missile|Magic
AttackType.cs // enum Flags, same bits as ACE
AttackHeight.cs // enum: High|Medium|Low + ToQuadrant()
AttackConditions.cs // enum Flags, 0x01/0x02/0x04/0x08
DamageType.cs // enum Flags, same bits as ACE
CombatBodyPart.cs // enum, 0..26
DamageLocation.cs // enum, 0..8 (player wire)
Quadrant.cs // enum Flags, High|Mid|Low|Left|Right|Front|Back
AttackRequest.cs // POCO: TargetGuid, Height, Power, Kind (Melee|Missile)
AttackResult.cs // POCO: Hit|Evade|Lifestone; DamageType; Damage;
// Critical; BodyPart; AttackConditions
DamageEvent.cs // POCO mirroring ACE's DamageEvent for shadow calc
CombatMath.cs // static helpers: GetSkillChance, CalcArmorMod,
// GetAttributeMod, AdditiveCombine,
// GetPositiveRatingMod, GetNegativeRatingMod,
// ThrustThreshold const 0.33f,
// defaultPhysicalCritFrequency 0.1f,
// defaultMagicCritFrequency 0.05f,
// DefaultMod 0.011f, BowMod 0.008f,
// ArmorMod 200f/3f
DefenseBuild.cs // struct: attack/defense skills, mods,
// weapon, stance, burden — for shadow calc
ArmorLayer.cs // struct: BaseAL, ResVsType map, Impen, Bane
BodyPartTable.cs // 12-quadrant probability table loader (from weenies)
Wire/
CombatC2S.cs // TargetedMeleeAttack / MissileAttack /
// ChangeCombatMode / CancelAttack senders
CombatS2C.cs // AttackerNotification / DefenderNotification /
// Evasion* / AttackDone / PlayerDeath decoders
CombatClient.cs // high-level: HandleAttackKey(), charge tick,
// release, process incoming notification,
// emit events to plugin API
```
### Conformance tests
Port these from ACE's test suite plus capture direct pcaps from retail:
- `SkillCheckTests.cs` — verify the logistic curve at ±50/100/200 skill
delta, factor 0.03 and 0.07.
- `SkillFormulaTests.cs` — already exists in ACE
(`references/ACE/Source/ACE.Server.Tests/SkillFormulaTests.cs`),
port the cases verbatim. `GetAttributeMod` at 55/100/200/400 skill,
bow and non-bow factors. `CalcArmorMod` at AL 0, 50, 100, 200, 500,
and negative values.
- `DamageEventTests.cs` — table-driven worked examples A, B, C from §5
here, with fixed RNG seeds to ensure exact reproducibility.
- `BodyPartTableTests.cs` — given a weenie's PropertiesBodyPart dict,
verify all 12 quadrants sum probabilities correctly and that
`RollBodyPart` is deterministic with a fixed seed.
- `AttackWireTests.cs` — byte-level round-trip tests against canned
hex payloads. Start from `AttackerNotification` with a known
DamageType/Damage/Critical/AttackConditions combination, ensure our
encoder produces the exact bytes the retail client expects, and our
decoder extracts the same values.
### Phase ordering (proposed)
| Phase label | What ships |
|-------------|------------|
| R2.A Wire | Enums + C2S senders + S2C decoders, 1:1 with Chorizite types. Integration into WorldSession. HUD shows "You hit X for N" chat messages. No shadow math yet. |
| R2.B Power | Power bar widget, charging, release → `Combat_TargetedMeleeAttack`. `GetPowerRange` mapping, ThrustThreshold logic. Attack key binding. |
| R2.C ShadowMath | `CombatMath` statics ported from SkillCheck/SkillFormula. Shadow calculator that consumes `DefenseBuild` + weapon + target state → predicted `AttackResult`. Conformance tests. |
| R2.D BodyParts | `BodyPartTable` loader from weenies, Quadrant/AttackHeight wiring, damage-location surfacing in HUD. |
| R2.E Plugin | `IAttackEvents` on the plugin API: `OnAttackerNotification`, `OnDefenderNotification`, `OnEvasion`. Plugin can query shadow math for "preview" computations. |
### Non-goals for acdream (server territory)
These are explicitly **server-side** and we do NOT implement them:
- Authoritative damage calculation (we predict, server decides).
- Enchantment arbitration (prots vs vulns vs items).
- Stamina, health, mana updates (server sends `PrivateUpdateVital`).
- Evasion RNG (we just receive evade/hit from server).
- Proc triggering (Blood Drinker etc.).
- NPC AI — attack target selection, aggro lists, faction matrices.
Our job: predict correctly, send correctly, display correctly.
---
## Appendix: Key constants cheat-sheet
| Constant | Value | Source |
|---------------------------------|--------------------|-----------------------------------------|
| `ThrustThreshold` | 0.33f | DM Guide p.150, `WorldObject_Weapon.cs:1033` |
| `KickThreshold` | 0.75f | `Player_Melee.cs:432` |
| `SkillFormula.DefaultMod` | 0.011f | melee/finesse/thrown/atlatl attribute scale |
| `SkillFormula.BowMod` | 0.008f | bow/crossbow attribute scale |
| `SkillFormula.ArmorMod` | 200f/3f ≈ 66.667 | armor half-life constant |
| `SkillCheck` physical factor | 0.03f | logistic steepness for melee/missile |
| `SkillCheck` magic factor | 0.07f | magic resist steepness |
| `defaultPhysicalCritFrequency` | 0.10f | 10% base melee/missile crit |
| `defaultMagicCritFrequency` | 0.05f | 5% base magic crit |
| `defaultCritDamageMultiplier` | 1.0f | crit multiplier added to 1.0 = 2.0× max |
| `ElementalDamageBonusPvPReduction` | 0.5f | PvP elemental halved |
| `MeleeDistance` | 0.6f | direct-attack range |
| `StickyDistance` | 4.0f | sticky melee range |
| `RepeatDistance` | 16.0f | auto-repeat cutoff |
| `MinAttackSpeed` | 0.5 | anim speed clamp (lower bound) |
| `MaxAttackSpeed` | 2.0 | anim speed clamp (upper bound) |
| Natural Res cap | 0.5f (= 50%) | at strAndEnd 440+ |
| PKLogoffTimer | 2 minutes | logout freeze |
| Lifestone protection window | 5 minutes | after LS tie |
| Stamina mod cap | 0.5f (50% less) | at Endurance ~290 |
| NoStaminaUse evasion cap | 0.75f (75%) | at Endurance ~290 |
**Remember:** every one of these is tunable by ACE's `PropertyManager` but
the defaults above match what retail clients observed across live pcaps.
Use these as the acdream defaults and expose them via the plugin API for
debugging / emulator compatibility.