# 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 = `; `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.5–3.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 - 201–260: Poor (1–10% reduction) - 261–320: Mediocre - 321–380: Hardy - 381–440: 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.