Here you’ll find all formula as found in the game
Magic
Classic
\[\text{Damage} =\left (\text{AttackerMag} + \text{Power} \right) \times \frac{265 - \text{TargetSpr}}{4} \times \frac{\text{Power}}{256} \times \frac{[0..32] + 240}{256}\]
ComputeMagicAndGFDamagereads TargetSpr from the sameBATTLE_SLOT_DATA[]slot array for players and monsters: the SPR term is one shared code path. The only monster-related asymmetry found is on the attacker side: if the caster is a monster, magic damage dealt is halved after this formula.
Demi, Percent
\[\text{Damage} = \frac{\text{AttackPower} \times \text{TargetCurrentHP}}{16}\]Demi power = 4
Percent = 15
GF
Classic damage:
ComputeMagicAndGFDamage (GF-damage case). Power = the gfPower byte (offset 0x07); LevelMod/PowerMod = the levelMod/powerMod tail bytes (offsets 0x83/0x82).
Modifiers
Monsters
If monster: Damage = Damage / 2
Elem
If elem: Damage = Damage * (900 - ElemDef) / 100
Protective magic:
If Shell: Damage = Damage / 2
Diablos
\[\text{Damage} = \frac{\text{TargetMaxHP} \times \text{Level}}{\text{PowerMod} - \text{LevelMod} + 100}\]Diablos PowerMod = 0
Diablos LevelMod = 0
Cactuar
\[\text{Damage} = 1000 \times \left(\frac{\text{AttackPower} \times \text{GFLevel}}{1000} + 1\right)\]Cactuar AttackPower = 90
Moomba
\[\text{Damage} = \text{TargetCurrentHP} - 1\]Angelo recover
\[\text{DamageHeal} = \frac{\text{Power} \times \text{TargetMaxHP}}{16}\]Item
Curative item
\[\text{DamageHeal} = 50 \times \text{Powers}\]Magic
Curative magic
\[\text{DamageHeal} = \text{Power} \times \frac{\text{Power} + \text{AttackerMagic} }{2} \times \frac{[0..32] + 240}{256}\]Protective magic:
If Shell: DamageHeal = Damage / 2
Blue magic
White wind
\[\text{DamageHeal} = \text{QuistisMaxHP} - \text{QuistisCurrentHP}\]Physical
Classic
Base physical damage, straight from the engine (ComputeWithDamageSTRFormula and the hit+crit-rolling computeAttackPhysical — identical arithmetic):
where AttackPower = weapon attack power, STR = attacker STR (with the weapon’s STR bonus added), VIT = target VIT (0 under Vit0/Meltdown or the ignore-VIT sub-formula). All divisions truncate.
The STR term is
STR + STR²/16, not the widely-circulatedSTR²/(16+STR)— the two differ by an order of magnitude (for STR 128: 1152 vs 114). This page previously used the wrong term and a spurious(CritDamage+40)/20factor; neither is in the exe.
Then HpModifierComputationForPhysical applies, in order: Protect ÷2, Back Attack ×2, Critical ×2, Zombie target ÷2, and the elemental term dmg += dmg × elemAttack% × (800 − elemDef)/10000. (VIT is read from the same BATTLE_SLOT_DATA[] slot array for players and monsters — one shared code path.)
Compute crit
Rolled in Damage_RollCrit: crit if a random byte 0..255 ≤ CritBonus + LUCK (the 255×x/255 in the code is a no-op), so
CritBonus is the weapon’s crit bonus (or the enemy attack’s / Shot’s crit increase), staged in RELATED_TO_CRIT_BONUS. A crit doubles the physical damage above.
Hit %
Physical accuracy, from computeAttackPhysical:
The attack lands if 255 × hit% / 100 ≥ rand(0..255). HitRate = 255 always hits (the roll is skipped); Darkness quarters HitRate first; Sleep/Stop targets are always hit.
Status inflict chance
Every “Status attack accuracy” byte across the kernel (Magic, GF, Enemy attacks, Renzokuken, Battle items, Non-J GF, Command abilities, Blue Magic, Shot, Duel, Rinoa, …) is the SAME kernel field — but which formula actually reads it (if any) depends entirely on the ability’s own Attack Type byte, which selects a damage-compute function in Battle_DamageGettingRelated. Each function below was individually decompiled — nothing here is inferred from the byte’s name alone:
| Attack Types | Function | Behaviour |
|---|---|---|
| Physical Attack, % Physical Damage, Renzokuken Finisher, Squall Gunblade Attack, Kamikaze, Everyone’s Grudge, Physical Attack (Ignore Target VIT) | Battle_ApplyStatusWithResistRoll via the STR/VIT physical core | chance = accuracy + STR/4 − VIT/4 − resistance, rolled |
| Magic Attack, % Magic Damage, GF, GF (Ignore Target SPR), % GF Damage, Magic Attack (Ignore Target SPR), LV? Attack, Unknown 4 | same roll, via Damage_ComputeMagicAndGF’s MAG/SPR path | chance = accuracy + MAG/4 − SPR/4 − resistance, rolled |
| Curative Item, White Wind, Give Percentage HP (Angelo Recover) | Damage_ComputeCurativeItemSpecial | cures if accuracy > rand(1..100) — flat %, no stat terms. Cures the listed statuses, doesn’t inflict them |
| Curative Magic, Unknown 1 (Demi-type) | Damage_ComputeCurativeMagic | checkDoubleStatusApply runs unconditionally — HIT_ATTACK_ACCURACY is never read. Byte is dead |
| Revive, Revive At Full HP | GetReviveHP | Clears Death unconditionally if present and not sealed — accuracy never read. Byte is dead, except reviving a Zombie-status target instead deals unmissable magic damage via the Magic-dispatch path |
| LV Down, LV Up | computeLvlUpDown | succeeds if accuracy > rand(0..255) — gates the WHOLE level-change action (not a status roll at all); also fails outright if the target is level-change-immune |
| Fixed Damage, Target Current HP - 1, Fixed Magic Damage Based on GF Level, 1 HP Damage | Damage_ComputeFixedSpecial | No reference to HIT_ATTACK_ACCURACY anywhere in the function. Byte is dead |
| Card | Battle_RollCardCommand | Capture depends on the target’s HP ratio, not this byte: chance = (256 − 255×curHP/maxHP)/256 (~0.4% at full HP → 100% near 0 HP); a second rand < 16 (6.25%) gives the rare card. Byte is dead |
| Devour | Devour dispatcher case | Success needs attackerHP ≥ targetHP, then chance = (attackerHP − targetHP)/attackerHP; the Devour effect comes from the Devour kernel section. Byte is dead |
| Scan, Angelo Search, Moogle Dance | Scan / Angelo Search / Moogle Dance dispatcher cases | Utility actions (reveal info / find item / GF HP recovery) — no roll, accuracy never read. Byte is dead |
| None, Summon Item?, Unknown 2, Unknown 3 | dispatcher LABEL_46 / no-op default | No damage or status roll at all. Byte is dead |
Battle_ApplyStatusWithResistRoll roll (the two confirmed-rolled rows above):
chance = accuracy + atkStat/4 − tgtStat/4 − targetResistance
accuracy = 255 -> guaranteed (stat/resistance check skipped entirely)
chance ≤ 0 -> fails (0%)
accuracy 250..254 -> guaranteed, as long as chance > 0
otherwise -> roll floor(chance × 255 / 100) against rand(0..255)
(fails outright if the target already has the status, or per-status resistance ≥100)
All 37 defined Attack Types (0–36) are accounted for above — every one was read out of the dispatcher
Battle_DamageGettingRelated. Only two groups actually consume the status-accuracy byte (the STR/VIT and MAG/SPR rows); for the rest it is genuinely dead, or the Attack Type has its own success mechanic (Card HP-ratio, Devour HP-ratio, LV Up/Down action gate).
Stat
Character stat
Each character stat is defined by 4 coefficients (noted stat_0..stat_3, i.e. the four bytes stored per stat in the Characters kernel section). There are three distinct curve shapes — HP, the STR family, and the SPD/LCK pair — all read below from the engine (Stat_ComputeCharaMaxHP for HP, Stat_ComputeCharaStat for the rest; the final sum is clamped by CapTo255).
The doomtrain editor charts SPD and LCK with the STR-family formula, which is incorrect — those two use the simpler linear shape below (no quadratic term, no
/4). Its HP and STR/VIT/MAG/SPR charts are correct.
LCK & SPD
magicJunctionnedValue is the junction value defined in kernel.bin for the stat.
\[\text{charaStat} = \text{CapTo255}\left( \text{charaBasedStat} + \text{charaLvl} \times \text{stat}_0 + \frac{\text{charaLvl}}{\text{stat}_1} + \text{stat}_2 - \frac{\text{charaLvl}}{\text{stat}_3} + \frac{\text{magicJunctionnedValue} \times \text{magicAmount}}{100} \right)\]STR & VIT & MAG & SPR
strBonus is 0 if we don’t compute STR
\[\text{statResult} = \frac{\text{charaLvl}^2}{\text{stat}_3}\] \[\text{statResult} = \text{CapTo255}\left( \text{charaBasedStat} + \text{strBonus} + \frac{\text{stat}_2 + \frac{\text{charaLvl} \times \text{stat}_0}{10} + \frac{\text{charaLvl}}{\text{stat}_1} - \left( \frac{\text{getLow32bit}(\text{statResult}) - \text{getHigh32bit}(\text{statResult})}{2} \right)}{4} + \frac{\text{magicJunctionnedValue} \times \text{magicAmount}}{100} \right)\]HP
\[\text{charaHP} = \text{charaBaseMaxHP} + \text{charaLvl} \times \text{hp}_0 - \frac{10 \times \text{charaLvl}^2}{\text{hp}_1} + \text{hp}_2 + \text{magicAmount} \times \text{magicJunctionnedValue}\]Only 3 of HP’s 4 coefficients are used:
hp_3(the 4th byte) is never read. There is no cap in this function; the final battle max-HP isjunctionMultiplier% × charaHP, capped at 9999.
Monster stat
Read from the exe. Coefficient blocks live in the monster’s battle-.dat info section (ff8_battle_monster_info): 4 bytes per stat. HP: computeMonsterHP; the other stats: Stat_ComputeMonsterStatCurve (with SPR/SPD computed inline in updateStatChange). Curve→stat mapping: STR and MAG use the quadratic curve; VIT, SPR, SPD, EVA use the linear curve. All divisions truncate.
updateStatChangeapplies a final per-monster multiplier to every battle stat:finalStat = CapTo255(curveResult × statMult / 10).statMultdefaults to 10 (×1.0), so the curve formulas below give the base value; AI scripts can scale a stat via this byte.
HP
\[\text{MonsterHP} = \left\lfloor \frac{\text{HP}_0 \times \text{Lvl}^2}{20} \right\rfloor + \text{Lvl} \times \left(\text{HP}_0 + 100 \times \text{HP}_2\right) + 10 \times \left(\text{HP}_1 + 100 \times \text{HP}_3\right)\]No outer
/100(an earlier version of this page divided by 100, which is wrong — it made HP 100× too small). Written directly to max-HP; no clamp in this function.
STR & MAG
Same shape as the character STR family (a single outer /4, quadratic term subtracted), capped at 255:
VIT & SPR & SPD & EVA
Plain linear curve, capped at 255:
\[\text{Stat} = \text{CapTo255}\left( \text{Lvl} \times \text{V}_0 + \left\lfloor\frac{\text{Lvl}}{\text{V}_1}\right\rfloor + \text{V}_2 - \left\lfloor\frac{\text{Lvl}}{\text{V}_3}\right\rfloor \right)\]Experience
Character level-up curve
The cumulative EXP a character needs to reach level L is driven by two bytes in the kernel Characters section: expLow at offset 0x06 scales a linear term, expHigh at offset 0x07 a quadratic term. (These were historically read together as one little-endian “EXP modifier” WORD, but they are independent parameters.)
Stat_ComputeLevelFromExpaccumulates this threshold per level and returns the first level whose threshold exceeds the character’s EXP. Retail value 100 (expLow=100, expHigh=0) gives a flat 1000 EXP per level (99 000 to reach level 100). A non-zero high byte makes the curve accelerate.
Monster-given experience
Work in progress
Regular experience
\[\text{Exp} = \left\lfloor X \times \left(5 \times \frac{\text{M} - \text{P}}{\text{P}} + 4\right) \right\rfloor\]Minimum of 1, provided X > 0 and it’s not a boss.
Kill bonus
\[\text{KillBonusExp} = \left\lfloor Y \times \left(5 \times \frac{\text{M} - \text{C}}{\text{C}} + 4\right) \right\rfloor\]Minimum of 1, provided Y > 0 and it’s not a boss.
M = monster level
P = average party level (active members only, rounded down)
C = level of character/GF who gets kill shot and thus kill bonus
X and Y depend on the monster
Draw formula
MagicDrawResist is defined in the kernel associated to the magic
MagicQuantity is the number of magic in the draw magic defined in the c0m file (always 0 in vanilla)
Crisis level
\[\text{CrisisValue} = \frac{15300 \times \text{CrisisLevel}}{255}\]Stat formula per level
GF HP
getGFhpForLvl. The quadratic term is genuinely added (unlike character HP, which subtracts it). HPMod_1/2/3 = the gfHPModifier1/2/3 bytes at kernel offsets 0x14/0x15/0x16.
No clamp in this function; the caller
computeGFBattleStatsdoesgf_hp = percentHPBonus% × GFHP(default ×1.0, raised by HP-Up abilities), capped at 9999.
GF exp for next level needed
getGFExpNeededForNextLevel (÷256 confirmed). NextLvlMod_1/2 = the nextLevelModifier1/2 bytes at kernel offsets 0x18/0x19.
Function addresses
Reverse-engineering addresses for the functions referenced above, for readers who want to look them up in the exe.
| Function | Address |
|---|---|
ComputeMagicAndGFDamage | 0x491ad0 |
ComputeWithDamageSTRFormula | 0x492c40 |
computeAttackPhysical | 0x492e10 |
HpModifierComputationForPhysical | 0x48f600 |
Damage_RollCrit | 0x492b60 |
Battle_DamageGettingRelated | 0x4922b0 |
Battle_ApplyStatusWithResistRoll | 0x48f9f0 |
Damage_ComputeCurativeItemSpecial | 0x493450 |
Damage_ComputeCurativeMagic | 0x493280 |
GetReviveHP | 0x491940 |
computeLvlUpDown | 0x493650 |
Damage_ComputeFixedSpecial | 0x4931c0 |
Battle_RollCardCommand | 0x48fba0 |
| Devour dispatcher case | 0x4926cf |
| Scan dispatcher case | 0x4925a6 |
| Angelo Search dispatcher case | 0x49284b |
| Moogle Dance dispatcher case | 0x4928d3 |
Stat_ComputeCharaMaxHP | 0x496310 |
Stat_ComputeCharaStat | 0x496440 |
CapTo255 | 0x495930 |
computeMonsterHP | 0x48c500 |
Stat_ComputeMonsterStatCurve | 0x48c3f0 |
updateStatChange | 0x48c1c0 |
Stat_ComputeLevelFromExp | 0x4961d0 |
getGFhpForLvl | 0x496120 |
computeGFBattleStats | 0x495e6b |
getGFExpNeededForNextLevel | 0x496080 |