Battle Forge
Combat simulation suite
SimulatorDBG SimulatorInfoDiscordBlog
Back to Blog
Reverse Engineering
Cracking the Dragon Battleground Damage Formula
It started with a list of 46 damage numbers and a hunch that there was a system hiding underneath them. Three days later, after two accounts, a leveled-up Nightshade, and a small pile of throwaway scripts, I had an exact formula. This is the whole messy path, dead ends included.
Why Dragon Battleground Is Different
Testing combat in Dragons of Atlantis is normally miserable. Too much happens at once, and almost none of it is exposed cleanly. Dragon Battleground is the exception: its replay feature shows every turn, who got hit, and the exact damage of each hit. That visibility is the only reason any of this was possible. Without the replay log, there would be nothing to reverse engineer.
It started with 46 numbers

I had a column of damage values copied out of replays. Forty-six of them, all hits from my level-20 Great Dragon into an enemy Great Dragon, with a handful flagged as going to a Water Dragon instead, and one reflect hit. My base attack was 121,256. Somewhere on top of that sat an RNG modifier, and I wanted to know two things: what its bounds were, and whether it was actually random or quietly skewed.

I half-expected this to be unsolvable. The numbers looked noisy, the game explains nothing, and I assumed there were enough hidden modifiers to keep the whole thing opaque from the outside. So the first thing I did was write a small script to stare at the numbers harder than I could by eye.

The first crack: it's a ladder, not a cloud

The breakthrough came faster than I expected. The RNG isn't a smooth continuous roll, it's discrete. Every damage value turned out to be a whole-number percentage of a fixed base: a uniform integer roll from 101 to 120, meaning ×1.01 to ×1.20 in exact 1% steps, twenty possible outcomes. Both endpoints showed up multiple times, so those really are the bounds.

That single fact explained the thing that had been bugging me most: the duplicates. I'd seen the exact same damage number appear in different battles, and that felt wrong for something supposedly random. Once you know the game only has twenty roll values to choose from against a given target, repeats stop being suspicious and start being expected. The number 160,124 showing up four times isn't a glitch, it's the same 1-in-20 roll landing again.

101120roll p, a whole-number percentage from 101 to 120
Every clean hit lands on one of twenty evenly spaced rungs, because the roll picks a whole percentage from 101 to 120. Gold dots are observed hits (stacked where the same roll repeats). The two red ✕ marks are the hits that fit no rung at all: the anomalies that haunted me for hours before the replay logs explained them.

The statistics backed it up. The spread of rolls matched a fair, even distribution (the standard goodness-of-fit check came back comfortably in the consistent-with-random range). The average roll was 110.7 against an expected 110.5, there was no lean toward high or low values, and one hit never predicted the next. Within its bounds, it really is 100% random. I called the spacing the unit: 1% of effective damage. For my Great Dragon hits that unit was about 1404.6, so a roll of 114 gave 1404.6 × 114 ≈ 160,124.

One oddity survived: the Great Dragon hits actually fell on two slightly different ladders, about 0.12% apart. And two hits, 148,120 and 158,315, sat between the rungs entirely. I flagged both and moved on, though both turned out to matter later for unrelated reasons.

Splitting by battle (and one good red herring)

To chase the two-ladder thing, I re-logged the data grouped by battle and ran another script. Every battle turned out to be ladder-pure (no battle mixed the two), and the split correlated almost perfectly with whether a Water Dragon was on the field. It looked like a clean discovery: a Water Dragon's presence buffs your damage against the Great Dragon by a hair.

It wasn't. The correlation was real in the data but wrong about the cause, and I only found out later when the armor experiment forced me to relabel everything. I'm leaving it in the story on purpose, because it's the kind of pattern that feels like a breakthrough right up until a controlled test debunks it.

Where does 1404.6 come from?

With the roll understood, the mystery collapsed onto a single question: where does the unit come from? I had my dragon's full stat sheet (Life 436,525, Defense 48,502, Melee and Ranged both 121,256), so I wrote a script to brute force it.

The script found something genuinely useful and something frustrating. The useful part: every dragon is basically one number in disguise. Defense was exactly 0.4 × attack, life about 3.6 × attack. So any formula mixing attack, defense and life just collapses back to “attack × some constant.” The frustrating part: that constant, 1.158374, isn't clean. I couldn't produce it from any tidy fraction, any linear combination of the stats, or any mitigation curve I tried. Whatever feeds the unit, at least one input wasn't on the stat screen I was looking at.

Then I noticed the Great Dragon's Range stat was 1400, suspiciously close to 1404.6. I really wanted that to be the answer. A quick check killed it with the help of a level-44 dragon: range doesn't scale with attack the way the unit does, and the level-44 numbers came out wildly wrong under that theory. But that same level-44 data dropped a real clue. Its implied matchup constant was 1.42-1.79, nowhere near 1.158. So the target's level or stats definitely enter the formula. Weaker, lower targets take proportionally more.

I pushed harder with a much bigger search: roughly 69,000 distinct arithmetic expressions over all eight attacker and target stats, across three formula shapes, with coefficients restricted to the kind of round fractions a developer actually types. Three real matchups, three equations. Result: zero clean fits. Loosening the constraints produced infinite garbage like 12952346/69·(Rt/Lt), which is how I knew the search itself worked. It was perfectly capable of forcing a fake answer, and it refused to. The honest conclusion was uncomfortable: the displayed stats probably aren't the formula's inputs.

The armor experiment

This was the turning point, and it came from changing my own dragon instead of staring at the targets. I bolted armor onto my level-20 Great Dragon, which shifts its visible stats, and fought the same kinds of opponents again. Then a script compared the old and new battles.

It cracked the attacker side completely. Every new unit was the old unit × 1.0164804, and exactly one candidate explained that: the attacker input is the average of Melee and Ranged attack, and nothing else. Melee alone, a weighted blend, anything involving life or defense or range, all of them missed by thousands. My +15% life, +4.7% defense and +9.6% range changed the damage by nothing.

The same script fixed my earlier mistake. My two damage ladders had been mislabeled: only one assignment kept the attacker ratio consistent across all three targets. The weaker dragon takes slightly more damage, which is the sane direction, and the “Water Dragon presence aura” from earlier fell apart. It had only ever looked real because the weaker Great Dragon happened to always appear alongside the Water Dragon. Two coincidences had stacked into a fake mechanic, and the controlled experiment cleared up both.

damage = round( (Melee + Ranged) / 2 × h(target) × p / 100 ),   p ∈ {101…120}

That left one unknown: h(target), the target-side multiplier.

Meeting h(target)

The way I got at h was to stop predicting and start dividing. Once the attacker input and the roll were pinned down, every clean hit became a probe: take the damage, divide out my attack and the roll, and whatever's left is h. Do that for hits against the same target and the leftovers collapse onto one value, which is how you know you're measuring something real instead of forcing a pattern. For my three early targets it came out to 1.158374 (Great Dragon 20), 1.159771 (Great Dragon 17), and 1.169818 (Water Dragon 11).

I also took a detour to check whether Dragon Battleground was just reusing the normal-combat damage multiplier: the attack-÷-defense ratio clamped between ×0.5 and ×2.5. It can't be. Every Great Dragon is built with attack = 2.5 × defense, so a mirror match sits exactly on the ×2.5 cap, which would force identical h for all three targets. They weren't identical. A tempting shortcut, ruled out cleanly, which is its own kind of progress.

Reading the replay logs properly

Up to here I'd been hand-copying numbers. Then I started exporting the full replay logs as JSON (turn lists, targets, skill invocations, attacker stats) and wrote a decoder for them. This batch solved three things at once, including one anomaly that had been nagging at me since the start.

The anomaly 148,120, one of the two hits that never fit a rung, was never a damage roll at all. It was a bubble. The enemy Great Dragon's shield is a flat one-charge pool that absorbs the next hit, and the replay had shown me the absorbed amount, not a damage value. That pool was exactly 148,121. Mystery one, solved by accident. The heal formula fell out too: heal = 0.48 × atk × p/100, same 101-120 roll, clean across 48 heals. And with attacker stats now in the logs, I could validate the damage model in both directions: enemy dragons hitting me fit the same formula, with my own h. Eighty hits, every one predicted to within a damage point.

A second account's logs, run through the same pipeline, then proved the model is universal: from a level-13 Earth Dragon's 74k pokes to a level-63 Great Dragon's 26-million hits, every pair fit a unit with rolls inside 101-120. All eight dragon types I had, levels 13 to 63. My old “maybe the roll cap scales with level” theory died here, because nothing anywhere ever needed a roll above 120. Heals even became a measuring tool: since heal is a fixed fraction of attack, an enemy's heal number reveals its attack stat.

The number that wouldn't sit still

Here's where I got properly stuck. With stat sheets in hand, I turned the whole second-account dataset into absolute h measurements, and the picture made no sense. On my weak account, a level-20 dragon had h ≈ 1.16. On my developed account, dragons at similar or lower levels were coming out at 1.58 to 1.61. Same kind of dragon, same stat neighborhood, wildly different h.

I threw everything at it. Within one tier h turned out to be a clean curve in defense, but no single global formula spanned the two accounts. Was it the target's might, the game's own power rating? No: a Nightshade 20 and a Great Dragon 20 have identical might (25,600) and h of 1.612 vs 1.158. Was it fielded Team Power? No: the same enemy showed up in three battles with team powers of 14.9M, 17.1M and 43.5M and gave h = 1.61238 every time, to five decimals. The variable wasn't in the battle at all. It was attached to the attacker's account.

The +4.1667% that explained everything

The fix was a clean controlled experiment. On my weak account I ran 12 battles, then completed the Aerial Combat research, then ran 12 more against the same kinds of targets, and compared the two sets.

Every hit, against every target, went up by exactly +4.1667% (×25/24). Damage taken didn't move at all: pure offense. The odd 25/24 ratio means the bonus stepped from +20% to +25%, so I'd had 20% of it already. A second test at level 2 confirmed it: each level adds a flat 1/24 slice, and they don't compound. With a cap at level 10, the maximum is ×1.41667.

×1.41667 at level 10 (max)0246810Aerial Combat research level1.00×1.42×
Aerial Combat is a flat, linear stack: +4.1667% damage per level, no compounding, capped at level 10 for a maximum of +41.67%. Three different target types all moved by the identical ratio while incoming damage held perfectly still. About as unambiguous as reverse engineering ever gets.

And there it was. The mysterious ~1.4× factor that made my developed account's dragons hit so much harder was Aerial Combat, maxed. Every endgame player has it at level 10, so everything they do is ×1.41667. The “1.61 cap” on h was never a cap, it was the ordinary 1.16 band multiplied by everyone's research. And my weak account had been at level 0 the whole time, so every early measurement was already the pure, unmodified system and needed no recalibration.

One curve, finally

Dividing every developed-account measurement by its research multiplier did something satisfying: the chaos resolved into a single, gently declining curve. Once research is removed, h is one smooth function that falls as the target gets tougher, from about 1.17 for tiny dragons down through 1.0 around level 45 and toward 0.84 by level 63, with only small per-type wobble.

0.81.01.21.41.6102030405060target dragon levelas measured÷ Aerial 1.417
This is the chart I kept coming back to. Gold squares are h as measured, a baffling split between a low band and a high band. Divide the developed-account points by the Aerial Combat multiplier (the dashed drops) and every one lands on the same declining curve as the green dots. The mystery was never in the targets, it was research hiding in the attacker.
Defense drives h, but not on its own

The sharpest clue yet came from staring at the table: a Great Dragon and a Toxic Dragon with almost identical defense (1,586,196 vs 1,586,000) had almost identical h (0.94807 vs 0.94809). That's defense driving h, full stop. Within a tier, for the Great/Toxic family, h tracked defense on a clean curve, and the fit was nearly as tight as the measurements themselves.

But “defense only” couldn't be the whole story, and two airtight facts proved it. The armor experiment: going from Great Dragon 17 to 20 cost 0.00140 h per 10,670 defense, but armoring the 20 cost 0.00188 h per only 2,278 defense, a slope six times steeper. And at low levels the order went the wrong way: an Earth Dragon 13 (less defense) took less damage than a Nightshade 20 (more defense), when more defense should mean less damage taken. No plain function of defense does both. Something else was in there.

The dragon type that wasn't there

For a long stretch I was convinced the target's dragon type was a core ingredient of h, not just its level. The first hint was small: a Water Dragon took about 1% more damage than a Great Dragon at a similar level. Once more types showed up in the data, the gaps looked enormous. At level 20 a Nightshade or a Fire Dragon took far more damage than a Great Dragon (Nightshade 20 landed near 1.61, a Great Dragon 20 at 1.158). And the ordering tracked exactly how tanky each type felt in the game: Earth Dragons tankiest with the lowest h, then Toxic, then Great, then Water, with Fire Dragons the squishiest at the top.

Earth Dragons were the worst offender. They seemed to carry a stubborn “tank bonus,” taking noticeably less damage than their stats alone could explain, and nothing on the stat screen accounted for it. So the working theory hardened into something grim: every dragon type would need its own curve, maybe its own table. That is a lot of dragons to measure.

It was a mirage. Type does not actually enter the formula at all. Once I removed the hidden research multiplier and used each dragon's real per-level defense instead of stats I'd guessed, a Toxic Dragon landed exactly on the Great Dragon curve at equal defense, and Earth's tank bonus simply dissolved. The apparent type differences were two things in disguise: different types carry very different defense at a given level (a Fire Dragon has much lower defense than a Great Dragon at the same level, so of course it takes more), and the research inflation on developed accounts stretched the gaps further. Strip both away and h depends on nothing but the target's defense and level. That was the relief that made the rest of the math click: there was only ever one curve to find.

The closed form

The final attempt threw nonlinear saturation families at the whole dataset, and the answer had been hiding behind an assumption I'd rejected too early. Earlier I'd dismissed the classic “defense with diminishing returns” shape because, with a fixed constant, it genuinely doesn't fit. The missing idea was letting that constant grow with level.

h = 1.2 × K(level) / ( K(level) + defense )

What sold it wasn't the fit. It was inverting it. If the cap is exactly 1.2, then every measured h gives its own K via K = def·h / (1.2 − h). Computing that K for all the measurements, dragons at the same level but a different type produced the same K to within a few thousandths of a percent. Great Dragon 49 and Toxic 49: same K. That can't be coincidence. It means K is a pure per-level number and 1.2 is the hardcoded ceiling. The clincher: plugging the armored defense into the formula, with K taken from the unarmored dragon, predicted h = 1.15649. Measured value: 1.15649. A five-decimal hit with zero fitted parameters.

cap = 1.2 (no hit gets past it)GD201.20.6target defense → (each level has its own K; one illustrative level shown)
The shape of the law: h climbs toward a hard cap of 1.2 as defense shrinks, and falls away as defense grows. Dragon type never enters. Earth's old “tank bonus” and Toxic landing on the Great Dragon curve were the same paradox, just this one curve seen from a misleading angle. Each level carries its own K, so the curve shown is for a single illustrative level.
What K actually is

Calling h “solved” needs one honest footnote. The formula is solved and final. But K isn't calculated from anything. The developers just picked a number for each level, the same way they hand-set every level's defense and life in the game's own data tables. So we know exactly how the game uses K; the 90 K-values themselves are content, not math. We figured out the recipe; the amount of each ingredient is written on a card per level.

Every clean hit against a new level reads another card for free, so I kept feeding batches through the same extractor, plus a from-scratch re-derivation on a 90-battle corpus once the export format improved. The K table climbed from 15 of 90 levels to 23, every new batch validating the formula blind. At one point I got an 8-for-8 exact replay on a fresh account. The same logs threw off bonus finds: the Golden Dragon's skill is an AoE plus lifesteal of exactly 18% of its own max life per enemy hit, and Aerial Combat turns out to boost heals too.

What's still open

The damage core is done. Putting it all together, every normal hit in the game is:

damage = round( (Melee+Ranged)/2 × (1 + Aerial%) × 1.2·K/(K+def) × p/100 )

Attack comes from the stat tables, Aerial% is +4.1667% per research level, the middle term is the defense law, and p is the proven 101-120 roll. For any matchup in the measured bands that predicts every hit exactly; the only irreducible uncertainty is the roll itself.

What's left isn't really about damage anymore. The K table is 23 of 90 levels filled and self-fills from ordinary play. The elemental layer switches on only between two level-51+ dragons. It adds roughly +16% at the bottom of its range and grows from there, and pinning its exact math is blocked mostly on logging the attacker's true stats. Bubble pool sizes are mechanically understood but player-bound, so they need a measured table. And the battle-flow AI (turn order, targeting, skill triggers) is pure log-mining from data I already generate.

Why this was worth it

The thing I keep coming back to isn't the formula. It's that the system was knowable at all. I went in convinced the damage was a black box, and it turned out to be an ordinary fixed formula with a tidy 20-value dice roll on top, wrapped in two confounders (research and a per-level constant) that made it look like chaos. Every breakthrough came from a controlled experiment: swap armor, finish a research, level a dragon one step. Almost every wrong turn came from trusting a correlation I hadn't isolated yet.

Not bad for a problem that started as “here's a list of 46 damage numbers, find the RNG.”