Author: HobbitDur
Informations & stats
| Offset | Length | Description |
|---|---|---|
| 0 | 24 bytes | Monster name in FF8 String |
| 24 | 4 bytes | HP values |
| 28 | 4 bytes | Str values |
| 32 | 4 bytes | Vit values |
| 36 | 4 bytes | Mag values |
| 40 | 4 bytes | Spr values |
| 44 | 4 bytes | Spd values |
| 48 | 4 bytes | Eva values |
| 52 | 16*4 bytes | Abilities, low level |
| 116 | 16*4 bytes | Abilities, med level |
| 180 | 16*4 bytes | Abilities, high level |
| 244 | 1 byte | Med level start |
| 245 | 1 byte | High level start |
| 246 | 1 byte | Camera category — a small integer selecting how camera sequences frame this monster. See details below |
| 247 | 1 byte | [LSB] Zombie / Fly / unused / LvUp-Down Immunity / HP Hidden / Auto-Reflect / Auto-Shell / Auto-Protect [MSB]. Bit 0x04 (third from LSB) is never read by the loader — it has no effect (setMonsterInfoFromDatInfoSection at 0x48BBD0 is the only reader of this byte and tests every other bit) |
| 248 | 3 bytes | Card (drop/mod/rare mod) |
| 251 | 3 bytes | Devour (low/med/high) |
| 254 | 1 byte | [LSB] IncreaseSurpriseRNG / DecreaseSurpriseRNG / SurpriseAttackImmunity / IncreaseChanceEscape / DecreaseChanceEscape / unused / Gravity Immunity / Always obtains card [MSB] |
| 255 | 1 byte | DevourCategory. Consumed by the Devour code, which returns it as the “damage” of the devour hit — technically real but invisible in practice, so likely vestigial. |
| 256 | 2 bytes | Extra EXP |
| 258 | 2 bytes | EXP |
| 260 | 8 bytes | Draw (low) |
| 268 | 8 bytes | Draw (med) |
| 276 | 8 bytes | Draw (high) |
| 284 | 8 bytes | Mug(low) |
| 292 | 8 bytes | Mug(med) |
| 300 | 8 bytes | Mug(high) |
| 308 | 8 bytes | Drop (low) |
| 316 | 8 bytes | Drop (med) |
| 324 | 8 bytes | Drop (high) |
| 332 | 1 byte | Mug rate |
| 333 | 1 byte | Drop rate |
| 334 | 1 byte | Padding (0x00) |
| 335 | 1 byte | APs |
| 336 | 16 bytes | Renzokuken data |
| 352 | 8 bytes | Elemental resistance (Fire, Ice, Thunder, Earth, Poison, Wind, Water, Holy) |
| 360 | 20 bytes | Status resistance (Death, Poison, Petrify, Darkness, Silence, Berserk, Zombie, Sleep, Haste, Slow, Stop, Regen, Reflect, Doom, Slow Petrify, Float, Confuse, Drain, Expulsion, VIT0 (unused by the game)) |
Stat values
Each stat (bytes 24–51) is four bytes, A B C D in file order, turned into the stat at the monster’s level L. Every division truncates toward zero.
| Stat | Value at level L |
|---|---|
| HP | A×L²/20 + A×L + 10×B + 100×C×L + 1000×D |
| Str, Mag | (C + L×A/10 + L/B − (L²/D)/2) / 4 |
| Vit, Spr, Spd, Eva | C + L×A + L/B − L/D |
The L² term of Str and Mag is subtracted: with a small D it overtakes the rest, and the stat falls as the level rises. Str to Eva are then capped at 255, but only from above: the result is stored in a byte, so a negative value wraps around (−3 becomes 253). A steep enough Str/Mag curve at a high level therefore turns into a very high stat. B and D are divisors with no zero check: a 0 there crashes the game.
In battle, Str, Vit, Mag, Spr, Spd and Eva are also multiplied by the monster’s AI stat variable divided by 10 (10 by default, so ×1), which its AI script can change.
Abilities
Each ability entry is four bytes. The low, medium, and high level-tier lists each contain 16 entries, starting at section-relative offsets 0x34, 0x74, and 0xB4. The normal AI USE instruction selects an entry from the active tier; its type determines how to interpret the ability ID (for example, type 2 indexes magic data and type 8 indexes enemy attacks). See Battle Cameras for the full ability-to-action-to-camera path.
Abilities are composed of:
| Length | Description |
|---|---|
| 1 byte | AbilityTypeID |
| 1 byte | AnimationSequenceID |
| 2 byte | AbilityID |
AnimationSequenceID
Refers to the actor’s sequence in Animation sequences, not a camera-animation index. The queued action calls this field animationSequenceId (formerly unk3). For monster Magic in the relevant opening-camera branch, the engine tests whether this ID is 11 and the actor’s saved base sequence is 1, then selects camera 0 or 1 from the monster’s section 6. Other command types can take different branches; see the selection rule and command scope.
Renzokuken data
The data is 8*2 bytes, each 2 bytes corresponding to an ID on the Special Action list
Draw Mug Drop
Each section is composed of 8 bytes corresponding to 4 id (magic for draw, Item for mug & drop) and 4 quantity. Quantity is not used for draw (always 0 but no impact on game when changing it)
| Length | Description |
|---|---|
| 1 byte | ID 1 |
| 1 byte | Quantity 1 |
| 1 byte | ID 2 |
| 1 byte | Quantity 2 |
| 1 byte | ID 3 |
| 1 byte | Quantity 3 |
| 1 byte | ID 4 |
| 1 byte | Quantity 4 |
Elemental resistance
The % def value follows the formula: value% = 900 - value_hex * 10
and to revert back: value_hex = floor((900 - value%) / 10))
value% is the damage the monster takes from that element:
- 100 is normal damage.
- 200 is double damage (a weakness).
- 0 is no damage (immune).
- Below 0 the monster absorbs the element: the damage heals it and shows as a green number.
When the monster loads, the game keeps elemDef = value_hex * 10. Magic and GF damage of an elemental attack is then multiplied by (900 - elemDef) / 100, which is value% / 100.
- An attack with several elements uses only the first one set, in the order Fire, Ice, Thunder, Earth, Poison, Wind, Water, Holy.
- Holy against a Zombie target always counts as 200%.
- Weakness has no cap.
- A physical attack uses the attack’s element percentage as well:
damage + damage * element_percent / 100 * (value% - 100) / 100.
Examples from vanilla: Snow Lion has Fire 250 and Ice -100 (weak to fire, absorbs ice), Bomb has Fire -100 and Ice 300.
Status resistance
Each byte is copied as-is into the monster’s battle status resistance (FF8BattleMentalStatus) when it loads. Byte n covers these statuses, in this order:
| Bytes | Statuses |
|---|---|
| 0-6 | Death, Poison, Petrify, Darkness, Silence, Berserk, Zombie |
| 7-11 | Sleep, Haste, Slow, Stop, Regen |
| 12-18 | Reflect, Doom, Slow Petrify, Float, Confuse, Drain, Expulsion |
| 19 | VIT0 - never read: the resistance to VIT0 stays at 100 |
Every status the list does not name (Protect, Shell, Aura, Curse, Invincible…) gets 100.
The % def value follows the formula: value% = value_hex - 100
and to revert back: value_hex = value% + 100
value_hex is the resistance itself:
- 100 (0%) is neutral.
- Each point above 100 lowers the chance of inflicting that status by 1%.
- 200 or more (100% or more) is immune. Only a status attack accuracy of 255 still lands, because it skips the resistance check.
- Below 100 is more vulnerable than neutral. Retail monsters go down to 80 (-20%).
The chance of a status attack landing is: chance = accuracy + attacker_stat/4 - target_stat/4 - value_hex
The stats are STR and VIT for a physical attack, and MAG and SPR for magic and GFs. Vit0 on the target makes its stat count as 0. The chance must be above 0. An accuracy of 250-254 then lands; any other accuracy lands when GetRandomInt() <= 255 * chance / 100.
Rate value
The mug rate (byte 332) and drop rate (byte 333) are raw bytes (0–255), not stored as a percentage. The engine compares them directly against a uniform battle random number in the range 0–255 (GetRandomInt), so the true probability is out of 256, with a +1 from the <= comparison.
Drop — ComputeProbabilityGetItemMug, rolled on kill inside applyDamageAndHandleDeath:
the drop succeeds when GetRandomInt() <= DropRate // GetRandomInt() ∈ [0, 255]
=> P(drop) = (DropRate + 1) / 256
There is no zero-guard on the drop, so DropRate = 0 still yields a 1/256 (≈0.39%) chance, and DropRate = 255 is a guaranteed drop.
Mug — getMugObjectIdAndQuantity, used by the steal/Mug path:
if MugRate == 0 => never steal
the mug succeeds when GetRandomInt() <= MugRate + attacker_SPD / 2
=> P(mug) = (MugRate + min(attacker_SPD/2, ...) + 1) / 256
Unlike the drop, the mug has a hard MugRate == 0 => never guard and adds half the attacker’s SPD to the threshold, so a faster stealer succeeds more often.
A convenient approximation for display is % ≈ (value_hex + 1) / 256 * 100 (e.g. 128 ≈ 50.4%, 255 = 100%). The older value_hex * 100 / 255 formula is close but slightly off (wrong denominator, and it ignores the +1).
The battle module also has generic probability helpers used by the auto-summon / escape / card rolls — isRandomProbaNumDen255 and isRandomNumeratorDenominator — which compute 255 * numerator / denominator and succeed when that value >= GetRandomInt(). This is the same 0–255 convention (a roll quoted as “32/255” means a threshold of 32 against the 0–255 random).
Byte 246: Camera category
This byte is a small per-monster integer that tells the battle camera how to frame the monster (roughly, a size/distance class — it is what makes the camera pull back further for large enemies when a spell is cast on them, etc.). It is not a bitfield in the usual sense; it is read once per entity at battle start and converted into a camera value on the entity, which the camera-sequence VM then reads back.
How the engine uses it:
- At battle start,
initAnimationSequenceAtStartBattlereads this byte throughgetMonsterCameraCategoryand converts it into the entity’scameraDataRelatedfield using the mapping below. Playable characters (com id < 0x10) always getcameraDataRelated = 0; only monsters use the byte. - During a fight, the camera-sequence VM can read a target’s
cameraDataRelatedback via C3 special variable0x15(21), letting a camera sequence branch on the target’s category to pick a framing.
Conversion from byte 246 to cameraDataRelated:
| Byte 246 value | cameraDataRelated | Notes |
|---|---|---|
bit 0x8 set | 5 | Not used by any vanilla monster |
bit 0x4 set (and not 0x8) | 4 | i.e. value 4 |
| 3 | 0 | 3 − value for the remaining low values |
| 2 | 1 | |
| 1 | 2 | |
| 0 | 3 | Default / closest framing |
Vanilla monsters only use values 0–4, so the effective camera value ranges 0–4 (the 0x8 → 5 branch exists but is never triggered by shipped data).
Byte 255: Devour category
It is an integer, a per-monster category consumed by the Devour system:
What the engine does with it (PC version):
- When a Devour command resolves,
computeDevourpicks the devour effect ID from bytes 251–253 (by level tier). If the ID is 255 (Immune) the devour fails immediately. Otherwise it stores the effect ID (DEVOUR_EFFECT_ID) and this byte (DEVOUR_CATEGORY_OR_SWAP_COM_ID) into a pending-devour block. Battle_DamageGettingRelated’sATTACK_TYPE_DEVOURcase then rolls the devour: it succeeds only if the devourer’s current HP ≥ the target’s current HP, with probability(attacker_hp − target_hp) / attacker_hp(so devouring gets more reliable the more the target is weakened — a previously undocumented formula). On success the target gets the Eject status (this is how Devour removes a monster without death rewards) and the devour message is printed; on failure the miss flag is set and the byte is overwritten with 8.- Either way, the function returns this byte as the “damage” value of the devour hit. So on PC its gameplay effect is real but essentially invisible: a successful devour deals 0–7 damage to a monster that is being ejected anyway, and on a failure the miss flag suppresses the 8. In practice you cannot see it in game — which is why it stayed unidentified so long. The categorized values strongly suggest it originally had a richer purpose (flavor text or per-category outcome).
- Side note: the effect ID meanwhile drives everything visible — statuses and heal/damage quantities from the kernel devour table, and the Str/Vit/Mag/Spr/Spd/HP permanent bonuses via
relatedToDevour.
The data itself is a deliberate classification. No name for the categories survives in the engine or the reference data — the names in the Name column below are proposed based on which monsters use each value, not something extracted from code. Monster lists come from scanning all vanilla c0m files (144–199 are garbage filler):
| Value | Name | Monsters | Reading |
|---|---|---|---|
| 0 | DEVOUR_CATEGORY_PRIME_BEAST | Thrustaevis, Mesmerize, Blue Dragon, Adamantoise, Chimera, Behemoth, T-Rexaur, Ruby Dragon, Cactuar, Tonberry, Torama | Notably includes every stat-bonus devour (Str/Vit/Mag/HP +1) |
| 1 | DEVOUR_CATEGORY_STANDARD | SAM08G, Snow Lion, Caterchipillar, Fastitocalon, Hexadragon, Armadodo, Turtapod, Wendigo, Gayla, Death Claw, Grand Mantis, Grendel, PuPu, Lefty, Righty, Vysage | Mostly Full HP Recovery devours |
| 2 | DEVOUR_CATEGORY_SMALL_CRITTER | Glacial Eye, Buel, Red Bat, Blitz, Fastitocalon-F, Jelleye, Imp, Anacondaur, Cockatrice, Tri-Face, Bomb, Abyss Worm | Small / flying / venomous critters |
| 3 | DEVOUR_CATEGORY_PLANT | Grat, Ochu, Malboro, Funguar | Plants |
| 4 | DEVOUR_CATEGORY_AMORPHOUS | Blobra, Gesper, Blood Soul | Amorphous / spirits |
| 5 | DEVOUR_CATEGORY_UNDEAD | Forbidden | Undead |
| 6 | DEVOUR_CATEGORY_PEST | Geezard, Bite Bug | Small pest group |
| 7 | DEVOUR_CATEGORY_MACHINE | GIM52A, GIM47N, Elastoid, Iron Giant, Belhelmel | Machines / inorganic |
| 8 | DEVOUR_CATEGORY_INEDIBLE | Every boss/story monster (c0m061+) and early bosses | Bosses / story monsters. Matches the engine forcing this value on a failed devour; these monsters are all Devour-immune anyway, so it’s never even read for them |
Note the consistency: DEVOUR_CATEGORY_INEDIBLE (8) is the failure code the engine itself writes, and it is exactly the value stored on every non-devourable monster. Modders can effectively treat this byte as free space (its only vanilla effect is 0–7 phantom damage on a successful devour), or repurpose the already-wired data path: monster byte 255 → DEVOUR_CATEGORY_OR_SWAP_COM_ID → returned as the devour hit’s damage.
Function/address reference
For readers following along in IDA / a disassembler. Names above are the ones set in the shared IDA database.
| Name | Address | Role |
|---|---|---|
Stat_ComputeMonsterStats | 0x48C1C0 | Monster battle stats: each curve’s value × the AI stat variable / 10 |
Stat_ComputeMonsterStatCurve | 0x48C3F0 | Str/Vit/Mag/Spr/Spd/Eva at a level from the four curve bytes (formulas above) |
Stat_ComputeMonsterMaxHP | 0x48C500 | Max HP at a level from the HP curve bytes |
CapTo255 | 0x495930 | Caps a stat at 255 from above only; the byte store then wraps a negative value |
initAnimationSequenceAtStartBattle | 0x5027D0 | Per-entity battle-start init; converts byte 246 into the entity’s cameraDataRelated |
getMonsterCameraCategory | 0x48B9F0 | Reads byte 246 (camera_category) from a monster’s info section |
CameraSeq_ReadSpecialVar_C3 (renamed from a3ParamAnimSeqForCamera in the community IDB) | 0x509640 | Camera-sequence VM special-variable reader; C3 var 0x15 returns a target’s cameraDataRelated |
ComputeProbabilityGetItemMug | 0x486650 | On-kill item drop: reads byte 333 (DropRate), drop succeeds when GetRandomInt() <= DropRate |
getMugObjectIdAndQuantity | 0x4867C0 | Mug/steal: reads byte 332 (MugRate); 0 = never, else succeeds when GetRandomInt() <= MugRate + attacker_SPD/2 |
GetRandomInt | 0x48F020 | Battle random byte generator, returns a value in [0, 255] |
setMonsterInfoFromDatInfoSection | 0x48BBD0 | Copies the 20 status resistance bytes into the battle slot’s FF8BattleMentalStatus (byte 19 is skipped); other statuses get 100 |
Battle_ApplyStatusWithResistRoll | 0x48F9F0 | Status attack roll: immune when the resistance is >= 200 (cmp 0C8h), else the chance formula above |
Damage_ComputeMagicAndGF | 0x491AD0 | Magic/GF damage: multiplied by (900 - elemDef) / 100 for the attack’s first element |
Damage_ApplyPhysicalModifiers | 0x48F600 | Physical damage: + damage * element_percent * (800 - elemDef) / 10000 |
computeDevour | 0x48FC60 | Picks the devour effect ID by level tier, stores it and byte 255 into the pending-devour block |
Battle_DamageGettingRelated | 0x4922B0 | Resolves attack-type-specific damage; ATTACK_TYPE_DEVOUR case at 0x4926BC rolls the devour and returns byte 255 as the hit damage |
relatedToDevour | 0x492220 | Applies the devour effect’s permanent Str/Vit/Mag/Spr/Spd/HP bonuses from the kernel devour table |
isRandomProbaNumDen255 | 0x48F0F0 | Generic probability helper: succeeds if 255*numerator/denominator >= GetRandomInt() (verified IDA function) |
isRandomNumeratorDenominator | 0x48F0C0 | Generic probability helper, same convention as above (verified IDA function) |
DEVOUR_EFFECT_ID | 0x1D28E27 | Pending devour effect ID (from bytes 251–253) |
DEVOUR_CATEGORY_OR_SWAP_COM_ID | 0x1D28E28 | Pending devour category (byte 255); reused as a scratch com-id by the unrelated party-swap flow |