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.
1090 lines
45 KiB
Markdown
1090 lines
45 KiB
Markdown
# 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.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.
|