Damage
What this site works out between a swing and the number over a monster's head — every step marked as the game's, ours, or missing.
This is the arithmetic this site runs when you fight a monster here. Almost all of it was read out of the real server's own code, step by step — but not quite all, and the point of this page is that you can see which is which.
Three marks are used here and on every page in this section:
- Game — read out of
SR_GameServer.exeand implemented as read. - Ours — our own decision, because the game's answer is not in anything we can read.
- Missing — not modelled at all, and why.
What happens to one swing
In order, from the moment a blow lands:
1. The swing itself · Game. Your attack power and the skill's percentage make a base number. A skill that says 350 % is a 3.5× multiplier on what your weapon and stats already give.
2. The roll · Game. The spread is not an even one. The server rolls three times and keeps the lowest, then adds or subtracts it with even odds. That is why damage here feels steady most of the time and then occasionally is not — the same feeling the real game gives, for the same reason.
3. Buffs · Game. Flat ones are added, percentage ones multiply.
4. The cap that surprises everyone · Game. Before any defence is touched, the base is clamped to 100. Every big number you ever see is made by the multipliers after this point, never by the attack power that went in.
5. Defence · Game. The defender's armour divides, and their flat defence is subtracted. Physical and magical attacks read a crossed pair of defence stats — physical damage is divided by magical defence and vice versa. That is not a slip in our copy; it is what the server does.
6. The floor · Game, and disputed. A blow the defence all but swallowed does not become zero: it is lifted back onto a small floor of about 5 % of the swing. See "Where we might be wrong" below — this is the one step where our two readings of the binary disagree with each other.
7. Skill power, elements, mastery, attack rating, reinforce · Game. In that order. Being ahead of the target in the weapon's mastery is worth up to 30 %.
8. What comes off the top · Game. Critical hits and blocks are rolled through the server's own dice, which are not a coin: it keeps a running account per skill, so a bad streak is paid back later. Over many swings the rate is exactly the number on the tooltip; over a handful, it is kinder than a fresh roll each time.
Where our number can differ from the shard's
Named rather than smoothed over, because a formula that is right in the middle and wrong at the edges is only useful if the edges are known.
| Step | What is unsettled |
|---|---|
| The 5 % floor | Our two readings disagree. The appendix below reads the branch as a log line that changes nothing; the fight's own code reads it as a real floor and applies one. The fight is the one that runs. Until somebody reconciles them at the addresses, this is the loudest open question on the page |
| The base swing | The server calls its attack-power routine twice with the same arguments and multiplies the two results. Whether the second call is meant to return something else — a second hand, a weapon term — is not settled |
| Reinforce | Used as a plain multiplier, which does not fit reading it as a percentage the way its siblings are read |
| Defence rating | The server works it out and then throws the result away. Whether that is a bug in this build or dead code is open |
What our fight leaves out, and why
| Missing | Why |
|---|---|
| The elemental term | No elemental skill is in play in a duel with a monster here, so the term is one. Present in the code, never anything but identity |
| Parry between elements | The real routine only reduces when attacker and defender both carry an element flag. Against an ordinary monster it returns nothing, so our fight computes without it |
| A player-only factor on the swing's ends | Real, and identity for a character in the ordinary state. The fight computes without it and says so here rather than pretending the function is not there |
For anyone opening the binary
Everything below is the research this page stands on: the constants, the stat indices, the disassembly and the exact order of operations. It is written for somebody checking our arithmetic against the real thing, not for somebody playing.
Constants
Read from .rdata; the address is where you will find each one if you open the
binary yourself.
| RVA | Value | Type | Used for |
|---|---|---|---|
0xB45AC0 |
1.0 | double | the + 1 in def / 100 + 1 |
0xB45F30 |
2.0 | double | magical balance bonus |
0xB45F60 |
0.05 | double | the 5 % minimum-damage check |
0xB45AF0 |
100.0 | double | percentage divisor (350 % / 100 = 3.5×) |
0xB45AF8 |
32767.0 | double | INT16_MAX, the random divisor |
0xB45E10 |
100.0 | float | base damage ceiling |
0xB45B44 |
10.0 | float | magical balance divisor |
0xB45B00 |
5.0 | double | attack rating coefficient |
0xB45D98 |
40.0 | double | attack rating constant |
0xB45BF0 |
0.8 | double | attack rating balance (80 %) |
0xB45DC8 |
0.03 | double | 3 % per mastery level of difference |
0xB45B8C |
0.3 | float | mastery bonus ceiling (30 %) |
0xB45BA4 |
4294967296.0 | float | 2³², used to correct negative integers |
Stat indices
GetStat(n) is vtable[0x104]. The indices matter more than they look:
physical and magical attacks read different defence stats, and they are
crossed over.
On the attacker
| Index | Meaning |
|---|---|
| 2 | feeds GetAttackRating — level, on the evidence of how it is used |
128 (0x80) |
fire attack, magical |
129 (0x81) |
ice attack, magical |
130 (0x82) |
fire attack, physical |
131 (0x83) |
ice attack, physical |
179 (0xB3) |
reinforce bonus |
189 (0xBD) |
magical balance |
259 (0x103) |
immunity pierce |
On the defender
| Index | Read by a physical attack | Read by a magical attack |
|---|---|---|
| 5 | — | magical defence |
| 6 | physical defence | — |
| 7 | — | physical defence |
| 8 | magical defence | — |
The crossover is the single most surprising thing in the routine: a physical hit divides by the defender's magical defence and then subtracts the physical one; a magical hit does the reverse.
1. Base damage
Both formulas start in the same routine, which takes the attacker, the skill, the defender and one extra parameter off the stack.
attackPower1 = GetAttackPower(attacker, skill) ; call 0x40DC10
skillMul = skillParam / 100 + 1 ; skillParam is the 5th arg
attackPower2 = GetAttackPower(attacker, skill) ; call 0x40DC10 again
baseDamage = attackPower1 * skillMul * attackPower2
The x87 sequence, for anyone checking against the disassembly:
call 0x40dc10 ; f(attacker, skill) -> float
fstps [esp+0x14] ; atk1
movzbl 0x2c(%esp),%eax ; skill parameter, 5th argument
fildl [esp+0x30] ; int -> float
fdivl [100.0]
faddl [1.0]
fstl [esp+0x1c] ; skillMul
fmuls [esp+0x18]
fstps [esp+0x18]
call 0x40dc10 ; atk2
fmull [esp+0x18]
fstps [esp+0x18] ; baseDamage
Randomness: three rolls, lowest wins
float offset = 100.0f;
for (int i = 0; i < 3; i++) { // rand() at 0x9DD338
float roll = (float)rand() / 32767.0f * 100.0f;
if (roll < offset) offset = roll;
}
int sign = (abs(rand()) & 0x80000001) + 1; // 1 or 2
damage = (sign == 1) ? damage - offset : damage + offset;
Taking the minimum of three rolls and then adding or subtracting it with even odds is not a symmetric spread: the offset itself is biased low, so most hits sit near the middle and the tails are thin. This is the part players describe as "damage feels consistent until it isn't".
Buffs
Ten skill offsets carry buff ids. Two are flat, eight are percentages.
| Offset | Kind |
|---|---|
+0x520 |
flat |
+0x4E0 |
flat |
+0x4EC, +0x4F0, +0x4F4, +0x4F8, +0x528, +0x530, +0x540, +0x560 |
percentage |
// flat
if (skill->buff != null && (v = FindBuffValue(attacker, buffId)) != null)
damage += (float)v;
// percentage
if (skill->buffPercent != null && (v = FindBuffValue(attacker, buffId)) != null)
damage *= (float)v / 100.0f + 1.0f;
The clamp that changes everything
if (damage < 0.0) damage = 0.0;
if (damage > 100.0) damage = 100.0; // ← base damage never exceeds 100
Base damage is capped at 100 before defence is applied. Every large number a player ever sees is produced by the multipliers after this point, not by the attack power that went in.
2. Physical damage — 0x40E830
phyDef = defender.GetStat(6);
magDef = defender.GetStat(8);
damage = baseDamage / (magDef / 100 + 1) - phyDef;
damage = max(0, damage);
if (skill.hasMagicParam) // e.g. 350 means 350 %
damage = damage * skill.magicParam / 100;
elemAtk = skill.isFire ? attacker.GetStat(130)
: skill.isIce ? attacker.GetStat(131) : 0;
if (defender.isImmune && defender.immunityType == 3)
immunityDamage = damage * attacker.GetStat(259) / 100;
parry = ParryCalc(attackerFlags, defenderFlags, defender);
if (parry > 0) damage *= parry;
damage *= elemAtk / 100 + 1;
damage += immunityDamage;
if (skill.isMagicalType)
damage *= attacker.GetStat(189) + 2.0;
if (attacker.mastery > defender.mastery)
damage *= 1.0 + min(0.3, (attacker.mastery - defender.mastery) * 0.03);
damage *= GetAttackRating(attacker);
if (damage < attackPower * 0.05)
Log("ResultDamage is under PhyAttackPoint's 5%%"); // warning only
if (attacker.GetStat(179) > 0)
damage *= attacker.GetStat(179);
GetAttackRating
float GetAttackRating(entity) {
if (!entity->vtable[0x1C]()) return 1.0f;
byte v = entity->vtable[0x128]();
float stat = entity->GetStat(2);
return max(0.0f, stat / (((v - 1) * 5 + 40) * 0.8f));
}
// which simplifies to stat / (v * 4 + 28)
GetDefenseRating(defender) is called on the line after and its result is
discarded. That is what the binary does; whether it is a bug in this build or
dead code left from an earlier one is open.
Parry
ParryCalc only returns a reduction when the attacker's element flag meets the
defender's element flag — fire against fire, fire against ice, ice against fire,
ice against ice. Anything else returns 0.0, which means no reduction at all.
The 5 % check does nothing
if (damage < attackPoint * 0.05)
Log("ResultDamage is under PhyAttackPoint's 5%");
The string is real and the branch is real, but damage is not modified. There is no 5 % floor in this build, only a log line — which is worth knowing, because the assumption that one exists is common.
3. Magical damage — 0x40E450
Identical except in five places.
- Defence stats are the other pair.
magDef = GetStat(5),phyDef = GetStat(7), and the division uses physical defence:damage = baseDamage / (phyDef / 100 + 1) - magDef. - Element stats are 128 / 129, not 130 / 131.
- One extra buff is added before defence. If the skill carries a buff at
offset
0x524and flag bit0x08is set, its value is added to base damage before the reduction — so it is multiplied by everything downstream. - Magical balance is always applied. In the physical path it is conditional;
here
damage *= GetStat(189) + 2.0runs unconditionally. - The skill flag checked is
0x04, not0x08.
4. Order of operations
Physical:
baseDamage = atk1 × (skillParam / 100 + 1) × atk2± min(rand₁, rand₂, rand₃) / 32767 × 100+ flat buffs,× percentage buffsclamp(0, 100)÷ (defender.MD / 100 + 1) − defender.PDmax(0, …)× skill power / 100× (element attack / 100 + 1)+ immunity pierce damage× parry result, when it fired× (magical balance + 2.0), magical-type skills only× (1 + mastery bonus), attacker ahead only× attack rating× reinforce bonus, when above zero
Magical is the same list with steps 5 and 8 reading the other stats, and step 11 unconditional.
What is still open
GetAttackPower(0x40DC10) is called twice with the same arguments and its two results are multiplied. Whether the second call is meant to return something different — a weapon term, a second hand — is not settled.GetStat(179)is used as a plain multiplier, which does not fit a percentage reading. Either the stat is stored differently from its siblings, or this build changed it.GetDefenseRatingis called and discarded.
These three are exactly why the page says partial at the top. A formula that reproduces the server for common cases and is wrong at the edges is still worth having — but only if the edges are named.