Pick a class and a base weapon, add enhanced damage, flat damage, crits and attack speed from gear and skills, and read what it deals per second — then pin it, change one thing and compare, or set it against a monster. Damage follows the game's integer pipeline with the weapon's own stat bonus; attack frames come from the class's animation in the game files and match the published IAS tables.
Every step is one frame shorter. Next step at 39 IAS.
| IAS | Frames | Attacks / s | DPS |
|---|---|---|---|
| 0–3 | 16 | 1.56 | 429 |
| 4–12 | 15 | 1.67 | 458 |
| 13–22 | 14 | 1.79 | 490 |
| 23–38 | 13 | 1.92 | 528 |
| 39–59 | 12 | 2.08 | 572 |
| 60–98 | 11 | 2.27 | 624 |
| 99+ | 10 | 2.50 | 686 |

Expected values for one target: no crushing blow, open wounds, life regeneration, monster block or resistance-lowering curses.approximation
Weapon enhanced damage multiplies the base roll and is floored; flat minimum and maximum damage add after it; the stat bonus and off-weapon enhanced damage multiply the result, floored again.
Each weapon carries its own Strength and Dexterity bonus: the bonus is (Strength × StrBonus + Dexterity × DexBonus) / 100 percent, floored. A sword is 100 / 0, a Matriarchal Javelin 80 / 50, a bow 0 / 100.
EIAS = 120 × IAS / (120 + IAS), floored, minus the weapon speed modifier (negative is faster), plus skill speed, held between −85 and 75. Checked against every IAS column of the Amazon Basin's class tables.
The check runs when data/tools/anim.json is built (pipeline/build-tools-lab.mjs): every published IAS column reproduced on the game's animation records of this build.
Frames = 256 × (frames per direction − starting frame) / (animation speed × (base + effective bonus) / 100), rounded up, minus one. Base 100 for attacks and casting, 50 for hit recovery and blocking. Reproduces all 2,955 rows of the Amazon Basin's attack tables for seven classes and the published cast, hit-recovery and block tables.
Asserted when data/tools/anim.json is built and again by tests/tools.test.mjs on every test run; the Warlock's tables come from the game file alone (no published table to check against yet).
An Amazon or a Sorceress attacking with a sword, axe, mace, spear, polearm, dagger, javelin or staff starts the swing on frame 2, bare-handed on frame 1, so those attacks are two (one) frames shorter than the animation. Bows, crossbows, throws and the other classes start on frame 0. The Warlock has no published table yet: frame 0 is assumed.
Fanaticism, Burst of Speed and Frenzy add attack speed straight to EIAS, past the diminishing returns. Their value at a level is the skill's min → max curve: min + (max − min) × (110 × level / (level + 6)) / 100, floored, at most max — Fanaticism 14 % at level 1, 35 % at 20.
Both double physical damage and never stack: Deadly Strike is checked first and Critical Strike only when it fails, so the chance of a double hit is 1 − (1 − DS)(1 − CS). The average hit is multiplied by 1 + that chance.
Only the plain attack animation is timed (Attack 1, as the published tables do). Sequence skills (Zeal, Fury, Strafe, Whirlwind), shape-shifted forms, dual wielding and kicks have their own rules and are not modelled yet.
A monster's level is its monstats.txt Level for the difficulty — except that on Nightmare and Hell a monster without the boss or NoRatio flag takes the level of the area it is met in (levels.txt MonLvlEx), so the same Bone Warrior is level 77 in the Tower Cellar and 85 in the Pit. Act and quest bosses keep their table level (Baal 99 on Hell); a super-unique is its type at its home area + 3; champions are +2, random uniques and their minions +3; a Terror Zone lifts the level first. Every tool resolves the level through the one rule, defaulting to the monster's first area in the game's order and offering the others where the level differs. A superunique's home area comes from a hand table (data/boss_behaviors.json, legacy), not from the map files.
The game's rule (D2Game monster init; D2MOO Monster.cpp). The areas a monster is met in are its nmon spawn lists, the preset areas and super-unique homes, and its leader's areas for a minion — asserted equal to /data/monsters when data/tools/xp.json is built (integrity audit 2026-10-08, root cause 2).
A monster's life and defence are its level's values in monlvl.txt (the expansion columns for the difficulty) times the monster's own percentages in monstats.txt, the same rule as its experience. Baal: 26,484 / 117,596 / 493,701 life (checked at build against the published figures).
Baal's three figures are asserted when data/tools/combat.json is built (pipeline/build-tools-lab.mjs).
Each player in the game beyond the first adds half the monster's life: × (players + 1) / 2.
2 × attack rating / (attack rating + defence) × your level / (your level + its level), held between 5 % and 95 %.
Hits = life / (the average hit with crits × (100 − physical resistance) / 100), rounded up; seconds add the misses (÷ chance to hit) at the attack rate. Expected values: 100 % physical resistance or more is immune; crushing blow, open wounds, life regeneration, monster block and resistance-lowering curses are not counted.
Sources: weapons.txt, skills.txt, monstats.txt, monlvl.txt and animdata.d2 of build 3.3.93847; the attack tables of the Amazon Basin wiki.