-
Notifications
You must be signed in to change notification settings - Fork 0
Encounter Tables
Navigation: Home - Combat - Encounter Tables
Trace of how MM2 chooses monsters for combat. Addresses are code-hunk load
addresses (base 0). A4 = data_hunk_base + 0x7FFE; embedded tables map to
data hunk file offsets via off = 0x7FFE - a4_disp in
EXTRACTED/ghidra/mm2_data_00.bin (8604 bytes). Do not use code-hunk
offset 0x4874 — that address is 68000 code, not the table.
Codec: tools/decode_encounters.py, EXTRACTED/decomp/mm2_encounter.{h,c}.
| Path | Trigger | Monster IDs | Key ASM |
|---|---|---|---|
| Random step | Walking; roll @ 0x10A2
|
Built in 0x11F0A loop 0x1213E
|
0x12C6E combat enter |
| Arena / ticket | Items 0xD0–0xD3 after arena win |
A4-$718A[screen] + RNG @ 0x9E96
|
0x9F1E class-quest chain |
| Fixed script | Tile event | 10 bytes from OP_12/13 |
0x16300 → A4-$11DE[]
|
monsters.dat supplies stats only (unpacker 0x4C8E) after type ids land in
A4-$11DE[0..10]. FAQ coordinate “Encounters:” lists are event.dat OP_12
fights, not random pools.
Copied into the current-screen buffer A4-$561A on enter. Field reads use
negative offsets from that buffer (e.g. byte 0x09 → A4-$5611).
| File off | Attrib buffer | ASM | Role |
|---|---|---|---|
0x09 |
-$5611 |
0x10A2 |
Step rate: RNG(1, byte) == 1 triggers encounter check |
0x0A |
-$5610 |
0x12124 |
Group-size gate vs live monster count A4-$77BE
|
0x0B |
-$560F |
0x11FA2 |
Max monsters in random picker |
0x0C |
-$560E |
0x11FB2 |
Min monsters floor |
0x0D |
-$560D |
0x116CA, 0x13148
|
Retreat/run difficulty (not spawn rate) |
Typical values: rate 0x64 (1-in-100 step roll), retreat 30..200 (multiples
of 10). Dump all 60 screens:
python tools/decode_encounters.py
python tools/decode_encounters.py --json EXTRACTED/encounter_tables.json| A4 field | Data-hunk off | Size | ASM | Role |
|---|---|---|---|---|
-$718A |
0xE74 |
60 B | 0x9E96 |
Per-area byte added to arena monster base (ticket*3 + table[screen]) |
-$6FC0 |
0x103E |
4 B | 0x11F14 |
Disposition modifier indexed by A4-$79AE
|
-$6FCA |
0x1034 |
4 B | 0x12110 |
Running party XP budget vs proposed group |
Partial: the master monster pool blob (16-byte rows indexed by
base*16 + RNG in 0x11F0A) is not fully mapped. The picker at 0x11F0A uses
A4-$6FC0, attrib min/max, and RNG tiers — not A4-$718A (arena-only).
step on map (3D view, not town mode)
-> 0x10A2: gates (view -$79E2, env -$7660, tile type, rate byte 0x09)
-> on success: combat setup via -$7EDE -> 0x051C2 (combat_encounter_entry,
see mm2.capstone.annotated.asm; prints "Encounter!", preps HUD/turn
order — see docs/17-combat-system.md for the round loop it hands off to)
-> 0x12CE0: recompute live count (-$77BE) from filled slots + overflow
-> if -$796B lacks 0x80 (not pre-filled fight):
-> 0x1213E: loop 0x12072 (budget/gate check) + 0x11F0A (fill -$11DE)
-> 0x11C82 / 0x4C8E: unpack monsters.dat stats
-> 0x12A22: round loop
Other paths: rest ambush (0x19D64, mode -$796B = 3), encounter_mod
A4-$79A5 (post-combat tiering @ 0x114D6).
Resolved: there is no separate "monster pool blob". The picker selects
monster type ids directly from monsters.dat, which is organized in
16-entry tiers: type_id >> 4 = tier (difficulty band), type_id & 0xF =
index within the tier. This is confirmed by 0x12072 summing
(type_id >> 4) + 1 per filled slot as the "XP" cost, and by 0x11F0A
constructing new type ids as tier * 16 + rng(1,16) - 1.
loop:
0x12072() ; recompute done flag (-$6FC1)
if -$6FC1: break
0x11F0A() ; monster_adds_friends: add one "friend group"
0x12072() ; recompute done flag again
loop while !-$6FC1
Sums (A4-$11DE[i] >> 4) + 1 for i in [0, min(live_count,10)), plus (if
A4-$11D4 overflow type is set and live_count > 10) the overflow type's
tier cost × overflow count. Sets A4-$6FC1 (done) when either:
- running total ≥
A4-$6FCA(party_xp_budget), or -
A4-$77BE(live count) ≥A4-$5610(attrib byte0x0A, group-size gate)
CORRECTED (a first pass at this trace had the attrib clamp and the friend-
count roll swapped — the min/max clamp applies to the tier pick, not the
final copy count; fixed after a full re-read of 0x11F9E-0x11FBE):
-
Roll a tier-pick ceiling: `base = disposition_mod[A4-$79AE] + A4-$6FC2
- 1
(disposition_mod= data-hunk bytes atA4-$6FC0, confirmed{0,1,2,5}); rollrng(1,100)and add a bonus via thresholds<0x3D→+0,<0x51→+1,<0x60→+2, else →+3; if the sum exceeds13(0xD), it is **replaced** with exactly14(0xE`, not clamped down to 13).
- 1
-
Pick the tier:
tier_pick = rng(1, base), then clamped to[attrib min (-$560E = byte 0x0C), attrib max (-$560F = byte 0x0B)](ceiling clamp against max first, then floor clamp against min).tier_index = tier_pick - 1. -
Pick a type:
type_id = tier_index * 16 + rng(1,16) - 1. Temporarily writes this intoA4-$11DE[0]and calls the unpack/lookup thunk-$7EF6(a4)with arg 0, which fillsA4-$11B7(this monster's "friend count" field, sourced frommonsters.datOabil low nibble) as a side effect, then restores the originalA4-$11DE[0]. -
Pick a copy count:
rng(1, A4-$11B7)— not re-clamped by the attrib bytes (those only bound the tier pick in step 2). -
Fill slots: starting at the current live count (
A4-$77BE), writetype_idinto consecutiveA4-$11DE[]slots (incrementing live count) until either the count is exhausted or 11 slots are filled; any remainder beyond 11 slots is folded into the live-count overflow accounting instead ofA4-$11D4(only script-seeded overflow uses that field explicitly).
The per-monster "friend count" field written to A4-$11B7 by -$7EF6 is not
yet named in mm2_gamestate.h — flag for symbol harvesting.
Called once at combat setup (0x12CF4, right before the mode check that
decides whether to run the picker) to (re)compute both budget-related
globals from the current party, confirming the last two "unnamed field"
questions above:
party_xp_budget = 0
max_half_level = 0
for i in 0..party_size-1:
char = get_party_member_ptr(i) ; -$7F20(a4)
party_xp_budget += char.hp_current ; record +0x74 (u16)
half_level = char.level >> 1 ; record +0x71
if half_level > max_half_level: max_half_level = half_level
party_xp_budget /= 8 ; -$7B4E(a4) = long divide (0x24D9A)
if disposition == 0: party_xp_budget /= 4 ; total /32 (cautious -> weakest fights)
if disposition == 1: party_xp_budget /= 2 ; total /16
if disposition == 3: party_xp_budget *= 2 ; -$7B54(a4) = long multiply (0x24C74); total /4 (reckless -> hardest)
; disposition == 2: unchanged, total /8 (default)
A4-$6FC2 = max_half_level
So the encounter "budget" (A4-$6FCA) is total party current HP / 8,
scaled by the disposition setting (A4-$79AE, the same 0..3 "AI mood" cycled
by GameStateView::cycleDisposition()): disposition 0 (cautious) allows only
1/32 of total HP in monster-tier cost, disposition 1 is 1/16, the default (2)
is 1/8, and disposition 3 (reckless) is 1/4 — i.e. disposition scales
encounter difficulty, not just AI aggression. A4-$6FC2 (the tier-roll
modifier consumed by monster_adds_friends) is simply half the level of
the party's highest-level member (rounded down).
| A4 | Use |
|---|---|
-$79F2 |
Current screen 0..59 |
-$796C |
Move counter (rest ambush) |
-$796B |
Encounter mode (0x80 = event/pre-filled) |
-$11DE[] |
Monster type ids for this fight |
-$77BE |
Live monster count |
-$6FC1 |
Picker "done" flag (budget/gate exhausted) |
-$6FC2 |
Tier-roll modifier byte (unnamed, read @ 0x11F20) |
-$11B7 |
Picked monster's "friend count" field (set by -$7EF6) |
-
Trace— Resolved, see above: no blob, direct0x11F0Apool base pointermonsters.dattier indexing. - Correlate
A4-$718Abytes with arena difficulty tiers (raw dump shows zeros/low values on towns,5on late castles — likely pool tier, not monster id). - Grep all
event.datOP_12 blocks for fixed fight catalog. - Name
A4-$6FC2and confirm what-$7EF6(a4)unpacks besidesA4-$11B7.
- Docs Wiki Hub ← full doc index
- Getting Started ← read this first
- Overview
- Workspace Notes
- Open Questions
- Full Analysis
- Game Remake
- Startup and Init
- Runtime Memory Map
- Main Loop and Map
- Exploration Input and Options
- Party and Session
- Game State Struct
- Data Hunk mm2 data 00
- Time Era Calendar
- GFX Loading
- ANM TV Format
- PC DOS graphics
- SNES graphics
- 3D View and Game Screen
- Amiga 3D Render Process
- Scripted Scene Graphics
- Event Graphics Opcodes
- Title Screen Assets
- Title Screen Animation
- Format inventory
- items.dat
- monsters.dat
- roster.dat
- attrib.dat
- map.dat
- spells & item use
- event.dat
- Events by location ← 71 maps
- Events hub (numbered)
- Event Script Opcodes
-
Event Script DSL ←
.mm2evtauthoring - Event Text Rendering
- Event Graphics Opcodes
- Combat Overview
- Combat System
- Encounter Tables
- Spell Cast ASM
- monsters.dat abilities
- Spells and item use
- Amiga audio in exe ← Paula / DATA / remake
- Controls: Sounds / Walk Beep
- MM2 music format (superseded)
- Title/death audio paths (superseded)
- Known songs catalog (superseded)
- Town Services
- Spell Sources
- Character Mechanics
- Skills and Hirelings
- Commerce Formulas
- Commerce World Services
- Mount Farview Class Quest
- Class Quest HP Bug
- Event Runtime
- Event to String Path
- Embedded Exe Strings
- Copy Protection
- Time Era Calendar
- Game State Struct
- MM1 Overview ← hub + decode status
- MM1 MAZEDATA format
- MM1 to MM2 outdoor
- MM1 WALLPIX by sector
- MM1 art & graphics
- MM1 items / monsters (status)
- MM1 map walker ↗
- MM1 2D maps gallery
- MM1 WALLPIX gallery