pokemon: data/stats Stage 3-4 — CalcStats core + verified gate

Stage 3 (core formula):
- src/home/pokemon.asm: GetMonHeader — BaseStats lookup via IndexToPokedex,
  rep movsb into wMonHeader (data tables are flat program-image labels, not
  EBP-relative GB memory, so we read them directly).
- src/home/move_mon.asm: CalcStat / CalcStats — faithful translation of the
  Gen-1 stat formula (base+IV, ceil(sqrt(statExp))/4, *level/100, +Level+10 for
  HP / +5 otherwise, cap 999). Big-endian result in H_MULTIPLICAND+1/+2.
- Wired pokemon.asm, move_mon.asm, math.asm, multiply_divide.asm into the build.

Math audit (the existing math was flagged as possibly inaccurate swarm output):
- multiply_divide.asm _Divide was BROKEN: used SM83 `sbc` (invalid x86 — the
  file never assembled) plus an unverified byte-level emulation with "let's just
  guess" comments. Rewrote with a single hardware div, same HRAM contract
  (quotient -> H_QUOTIENT big-endian, remainder -> H_REMAINDER).
- src/home/math.asm Multiply wrapper didn't preserve edx; our _Multiply clobbers
  it via `mul ecx` while GB _Multiply preserves de (CalcStat keeps the base stat
  in e across the stat-exp loop). Added push/pop edx.

Stage 4 (verification gate):
- DEBUG_CALCSTATS harness: entry.asm hook -> RunCalcStatsTest (debug_dump.asm)
  computes Bulbasaur L5/L100 stats, dumps DUMP.BIN. Makefile flag added.
- Running the EXE needs a DPMI host the sandbox lacks, so the assembled x86 was
  ALSO executed natively via an ELF32 harness. Output EXACTLY matches canonical
  Gen-1 values: L5 21/11/11/11/13, L100 230/133/133/125/165, and the stat-exp
  sqrt path L100-EVmax 293/196/196/188/228 (faithful to the b=255 cap quirk).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GcHpAsg8pwsVATD6LWPsoR
This commit is contained in:
Claude
2026-06-25 19:09:24 +00:00
parent 074bbaeeae
commit 63f4187ac8
8 changed files with 426 additions and 167 deletions

View File

@@ -42,18 +42,25 @@ in UPPERCASE (`W_PARTY_COUNT`).
pokemon_data.asm` exposes the globals; wired into Makefile (`POKEMON_SRCS` +
`assets` target); full project builds + links. (TM/HM bitfield zeroed → Stage 6.)
- [ ] **Stage 3 — Core formula.** Write `GetMonHeader` (new
`dos_port/src/home/pokemon.asm`, from `home/pokemon.asm:403`) and `CalcStat` +
`CalcStats` (new `dos_port/src/home/move_mon.asm`, from `home/move_mon.asm:34`).
Depends only on already-ported `Multiply`/`Divide`/`AddNTimes` + HRAM scratch.
Wire `src/home/pokemon.asm`, `src/home/move_mon.asm`, `src/home/math.asm`,
`src/engine/math/multiply_divide.asm` + new asset includes into the Makefile
`ALL_SRCS`.
- [x] **Stage 3 — Core formula.** DONE. Wrote `GetMonHeader`
(`src/home/pokemon.asm`) and `CalcStat`/`CalcStats` (`src/home/move_mon.asm`).
Wired `src/home/pokemon.asm`, `src/home/move_mon.asm`, `src/home/math.asm`,
`src/engine/math/multiply_divide.asm` into the Makefile. Builds + links.
**Math audit (per the "swarm code may be inaccurate" warning):**
- `_Divide` (`multiply_divide.asm`) was broken — used the SM83 mnemonic `sbc`
(invalid x86; the file never assembled) and an unverified byte-level loop.
Rewrote it with a single hardware `div`, same HRAM memory contract.
- `Multiply` wrapper (`src/home/math.asm`) didn't preserve `edx`, but our
`_Multiply` clobbers it via `mul ecx` (GB `_Multiply` preserves `de`, which
`CalcStat` keeps the base stat in). Added `push/pop edx`.
- [ ] **Stage 4 — Verify CalcStats (milestone gate).** `DEBUG_CALCSTATS` harness
seeds a known case (Bulbasaur L5, DVs=15, 0 stat-exp; + a stat-exp case), runs
`CalcStats`, dumps the 5 stats to `DUMP.BIN`; compare against canonical Gen-1
values on the host. No renderer in the loop.
- [x] **Stage 4 — Verify CalcStats (milestone gate).** DONE. `DEBUG_CALCSTATS`
harness (entry.asm hook → `RunCalcStatsTest` in debug_dump.asm) dumps Bulbasaur
L5/L100 stats to `DUMP.BIN`. Because running the EXE needs a DPMI host the
sandbox lacks, also **validated the actual assembled x86 natively** via an
ELF32 harness (nasm -f elf32 + ld -m elf_i386, scratch). Results EXACT vs.
canonical Gen-1: L5 `21/11/11/11/13`, L100 `230/133/133/125/165`, and the
stat-exp/√ path L100-EVmax `293/196/196/188/228` (faithful to the `b`=255 cap).
- [ ] **Stage 5 — Creation / loading (unblocks Oak gift).** Write `_AddPartyMon`
(`src/engine/pokemon/add_mon.asm`, from `engine/pokemon/add_mon.asm:1`; stub

View File

@@ -107,7 +107,11 @@ GAME_SRCS := \
# Pokémon data/stats engine (current_plan_pokemon_engine.md). Grows per stage.
POKEMON_SRCS := \
src/data/pokemon_data.asm
src/data/pokemon_data.asm \
src/home/math.asm \
src/engine/math/multiply_divide.asm \
src/home/pokemon.asm \
src/home/move_mon.asm
# Debug-only sources (linked only when the corresponding flag is set).
# Any debug flag implies SKIP_TITLE — debug harnesses boot straight to the overworld.
@@ -136,6 +140,13 @@ NEED_DEBUG_DUMP := 1
NASMFLAGS += -D DEBUG_NPC_WALK
NASMFLAGS += -D SKIP_TITLE
endif
# CalcStats gate: compute known Bulbasaur stats at boot, dump DUMP.BIN, exit.
# make DEBUG_CALCSTATS=1
ifdef DEBUG_CALCSTATS
NEED_DEBUG_DUMP := 1
NASMFLAGS += -D DEBUG_CALCSTATS
NASMFLAGS += -D SKIP_TITLE
endif
ifdef NEED_DEBUG_DUMP
GAME_SRCS += src/debug/debug_dump.asm
endif

View File

@@ -30,6 +30,9 @@ extern pit_restore ; boot/timing.asm
extern joypad_init ; src/input/joypad.asm
extern joypad_restore ; src/input/joypad.asm
extern Init ; src/init/init.asm — power-on init
%ifdef DEBUG_CALCSTATS
extern RunCalcStatsTest ; src/debug/debug_dump.asm — Pokémon CalcStats gate
%endif
; ---------------------------------------------------------------------------
; Exported symbols
@@ -82,6 +85,10 @@ start:
call pit_init ; reprogram PIT to ~60 Hz, install tick ISR
call joypad_init ; hook IRQ 1 (keyboard) → GB joypad state
%ifdef DEBUG_CALCSTATS
call RunCalcStatsTest ; compute known stats, dump DUMP.BIN, exit (never returns)
%endif
call Init ; power-on init → title screen (runs game loop)
; Execution reaches here only if Init returns without exiting via pad_quit.
call cleanup

View File

@@ -22,6 +22,11 @@ bits 32
%include "gb_macros.inc"
extern ds_base
%ifdef DEBUG_CALCSTATS
extern GetMonHeader
extern CalcStats
global RunCalcStatsTest
%endif
global DebugDumpMemory
global DumpBackbuffer
@@ -67,6 +72,20 @@ fnlog: db "NPCLOG.BIN", 0
; 0x180 0xC3A0 wTileMap (final 20x18 view) — H1: tilemap
; 0x1C0 0xD358 map header vars (curmap/dims/dataptr) — header setup
; 0x200 0xD520 tileset pointers (bank/blocks/gfx) — pointer setup
%ifdef DEBUG_CALCSTATS
; CalcStats gate: one 64-byte window over the test scratch at $D1E0 covers the
; scratch mon (DVs at +$1B) and both stat results (L5 at +$20, L100 at +$30).
windows:
dd 0xD1E0
dd 0xD1E0
dd 0xD1E0
dd 0xD1E0
dd 0xD1E0
dd 0xD1E0
dd 0xD1E0
dd 0xD1E0
dd 0xD1E0
%else
windows:
dd 0x4600
dd 0x4B20
@@ -77,6 +96,7 @@ windows:
dd 0xC3A0
dd 0xD358
dd 0xD520
%endif
; ---------------------------------------------------------------------------
section .bss
@@ -97,6 +117,33 @@ dbg_destTile: resb 1 ; tile CL at CanWalkOntoTile entry (saved before
; ---------------------------------------------------------------------------
section .text
%ifdef DEBUG_CALCSTATS
; ---------------------------------------------------------------------------
; RunCalcStatsTest — compute Bulbasaur (internal $99) stats at L5 and L100 with
; DVs=15 / stat-exp=0 into the $D1E0 scratch, then dump to DUMP.BIN. Validates
; GetMonHeader + CalcStat + _Multiply/_Divide end-to-end against canonical values.
; Never returns. Expected (big-endian words, host hexdump):
; dump +$20 (L5): HP=0015 Atk=000B Def=000B Spd=000B Spc=000D (21/11/11/11/13)
; dump +$30 (L100): HP=00E6 Atk=0085 Def=0085 Spd=007D Spc=00A5 (230/133/133/125/165)
; In: EBP = GB memory base.
; ---------------------------------------------------------------------------
RunCalcStatsTest:
mov byte [ebp + wCurSpecies], 0x99 ; Bulbasaur internal index
call GetMonHeader
mov word [ebp + 0xD1FB], 0xFFFF ; scratch DVs (all 15) at monbase+MON_DVS
mov byte [ebp + wCurEnemyLevel], 5 ; --- L5 ---
xor bh, bh ; b=0: ignore stat exp
mov esi, 0xD1F0 ; stat-exp base ptr (= monbase + $10)
mov edx, 0xD200 ; result dest
call CalcStats
mov byte [ebp + wCurEnemyLevel], 100 ; --- L100 ---
xor bh, bh
mov esi, 0xD1F0
mov edx, 0xD210
call CalcStats
jmp DebugDumpMemory ; writes DUMP.BIN, exits
%endif
; ---------------------------------------------------------------------------
; DebugDumpMemory — gather windows, write DUMP.BIN, exit. Never returns.
; In: EBP = GB memory base.

View File

@@ -1,4 +1,14 @@
; dos_port/src/util/multiply_divide.asm
; dos_port/engine/math/multiply_divide.asm
;
; _Multiply / _Divide — emulate the Game Boy 24-bit×8-bit multiply and the
; multi-byte ÷ 8-bit divide, preserving the exact HRAM scratch side-effects so
; callers (CalcStat, CalcExperience, damage calc, …) see identical results.
;
; Source: engine/math/multiply_divide.asm (pret/pokeyellow).
;
; HRAM map (gb_memmap.inc): H_PRODUCT=FF95(4) ; H_MULTIPLICAND=FF96(3) ;
; H_MULTIPLIER=FF99 ; H_DIVIDEND=FF95(4) ; H_DIVISOR=FF99 ; H_QUOTIENT=FF95(4) ;
; H_REMAINDER=FF99. (Quotient overlaps dividend; remainder overlaps divisor.)
%include "gb_macros.inc"
%include "gb_memmap.inc"
@@ -9,28 +19,21 @@ global _Multiply
global _Divide
; -----------------------------------------------------------------------------
; _Multiply
;
; Emulates the Game Boy 24-bit by 8-bit multiplication, storing the 32-bit result
; at H_PRODUCT. Uses hardware multiplication for speed while maintaining exact
; memory side effects.
; _Multiply — 24-bit multiplicand (H_MULTIPLICAND, big-endian) × 8-bit multiplier
; (H_MULTIPLIER) -> 32-bit product (H_PRODUCT, big-endian). Zeros H_MULTIPLIER,
; matching the GB loop's end state. Caller (wrapper) preserves esi/edx/bx.
; -----------------------------------------------------------------------------
_Multiply:
; Load 8-bit multiplier
movzx ecx, byte [ebp + H_MULTIPLIER]
; Load 24-bit multiplicand (big endian)
movzx eax, byte [ebp + H_MULTIPLICAND + 0]
shl eax, 8
mov al, byte [ebp + H_MULTIPLICAND + 1]
shl eax, 8
mov al, byte [ebp + H_MULTIPLICAND + 2]
; Hardware multiply (EAX * ECX -> EDX:EAX)
; Since max is 24-bit * 8-bit, the result is exactly 32-bit and fits in EAX.
mul ecx
mul ecx ; EDX:EAX = eax * ecx (fits in EAX)
; Store the 32-bit product to H_PRODUCT (big endian)
mov byte [ebp + H_PRODUCT + 3], al
shr eax, 8
mov byte [ebp + H_PRODUCT + 2], al
@@ -39,160 +42,56 @@ _Multiply:
shr eax, 8
mov byte [ebp + H_PRODUCT + 0], al
; Match exact Game Boy loop side-effects
mov byte [ebp + H_MULTIPLIER], 0
; (Optional but faithful) - gb code left the product in hMultiplyBuffer too
; But we don't strictly have a constant for H_MULTIPLY_BUFFER yet. It's at H_DIVIDE_BUFFER.
; We'll just omit it unless needed, as nothing reads it.
ret
; -----------------------------------------------------------------------------
; _Divide
; _Divide — divide the BH-byte big-endian dividend at H_DIVIDEND by the 8-bit
; divisor at H_DIVISOR. Writes the 32-bit quotient big-endian to H_QUOTIENT and
; the remainder to H_REMAINDER. BH = dividend length in bytes (1..4).
;
; Emulates the Game Boy long division.
; Divides a multi-byte dividend at H_DIVIDEND by an 8-bit divisor at H_DIVISOR.
; BH (b) contains the length of the dividend in bytes.
; Uses 32-bit hardware division for maximum efficiency.
; Rewritten from the original (broken) draft, which used the SM83 mnemonic `sbc`
; (invalid x86; the file never assembled) and an unverified byte-level emulation.
; This uses a single hardware divide with the same memory contract.
; -----------------------------------------------------------------------------
_Divide:
; Read divisor
movzx ecx, byte [ebp + H_DIVISOR]
; Sanity check: prevent divide by zero
push ebx
push edx
push edi
movzx ecx, byte [ebp + H_DIVISOR] ; divisor
test ecx, ecx
jz .div_by_zero
jz .done ; guard divide-by-zero (GB would loop forever)
movzx ebx, bh ; ebx = dividend length in bytes
test ebx, ebx
jz .done
; Assemble the big-endian dividend (first BH bytes of H_DIVIDEND) into EAX
; before any quotient store, since H_QUOTIENT overlaps H_DIVIDEND.
xor eax, eax
xor edi, edi
.assemble:
shl eax, 8
mov al, byte [ebp + H_DIVIDEND + edi]
inc edi
cmp edi, ebx
jb .assemble
; Determine the size of the dividend from BH (b)
; GB loop shifts 'b' bytes.
; hDividend is a 4-byte buffer (FF95-FF98).
; If b=1, dividend is at H_DIVIDEND+3 (or H_DIVIDEND+0? GB code reads from H_DIVIDEND+1 and shifts left).
; Wait, the GB code assumes the dividend is in the LAST 'b' bytes of hDividend!
; Actually, let's just do a faithful implementation using registers to avoid subtle bugs
; with how the bytes are aligned in H_DIVIDEND.
; We'll use the faithful division by subtraction, but highly optimized in registers.
; EAX = H_DIVIDEND (32-bit big endian)
mov eax, dword [ebp + H_DIVIDEND]
bswap eax
; EDI = {H_DIVISOR, hDivideBuffer[0]}
; wait, hDivideBuffer[0] is initially 0.
movzx edi, byte [ebp + H_DIVISOR]
shl edi, 8
; EDX = quotient (hDivideBuffer+1..+4), initially 0
xor edx, edx
div ecx ; EAX = quotient, EDX = remainder
.loop_byte:
; process 8 bits
mov ch, 8
.loop_bit:
; subtract 16-bit divisor (EDI) from top 16 bits of EAX?
; In GB: hDivideBuffer[0] and hDividend[1] - hDivisor
; Effectively, EAX is shifted left into a 40-bit buffer.
; Let's just emulate the byte-level exact logic instead of guessing the 32-bit math equivalent.
; GB:
; ldh a, [hDivideBuffer]
; sub hDividend+1
; ...
; This is getting complicated to map to registers exactly. Let's just use the memory exactly like GB!
; Zero out hDivideBuffer (5 bytes)
; We'll assume H_DIVIDE_BUFFER is at FF9A (it is).
mov byte [ebp + H_DIVIDE_BUFFER + 0], 0
mov byte [ebp + H_DIVIDE_BUFFER + 1], 0
mov byte [ebp + H_DIVIDE_BUFFER + 2], 0
mov byte [ebp + H_DIVIDE_BUFFER + 3], 0
mov byte [ebp + H_DIVIDE_BUFFER + 4], 0
mov ch, 9 ; e = 9
.div_loop:
mov al, byte [ebp + H_DIVIDE_BUFFER + 0]
mov cl, al
mov al, byte [ebp + H_DIVIDEND + 1]
sub al, cl
mov dl, al
mov al, byte [ebp + H_DIVISOR]
mov cl, al
mov al, byte [ebp + H_DIVIDEND]
sbc al, cl
jc .div_next
mov byte [ebp + H_DIVIDEND], al
mov al, dl
mov byte [ebp + H_DIVIDEND + 1], al
mov al, byte [ebp + H_DIVIDE_BUFFER + 4]
inc al
mov byte [ebp + H_DIVIDE_BUFFER + 4], al
jmp .div_loop
.div_next:
cmp bh, 1
jz .div_done
mov al, byte [ebp + H_DIVIDE_BUFFER + 4]
shl al, 1
mov byte [ebp + H_DIVIDE_BUFFER + 4], al
mov al, byte [ebp + H_DIVIDE_BUFFER + 3]
rcl al, 1
mov byte [ebp + H_DIVIDE_BUFFER + 3], al
mov al, byte [ebp + H_DIVIDE_BUFFER + 2]
rcl al, 1
mov byte [ebp + H_DIVIDE_BUFFER + 2], al
mov al, byte [ebp + H_DIVIDE_BUFFER + 1]
rcl al, 1
mov byte [ebp + H_DIVIDE_BUFFER + 1], al
dec ch
jnz .div_next2
mov ch, 8
mov al, byte [ebp + H_DIVIDE_BUFFER + 0]
mov byte [ebp + H_DIVISOR], al
mov byte [ebp + H_DIVIDE_BUFFER + 0], 0
mov al, byte [ebp + H_DIVIDEND + 1]
mov byte [ebp + H_DIVIDEND], al
mov al, byte [ebp + H_DIVIDEND + 2]
mov byte [ebp + H_DIVIDEND + 1], al
mov al, byte [ebp + H_DIVIDEND + 3]
mov byte [ebp + H_DIVIDEND + 2], al
.div_next2:
cmp ch, 1
jnz .div_okay
dec bh
.div_okay:
mov al, byte [ebp + H_DIVISOR]
shr al, 1
mov byte [ebp + H_DIVISOR], al
mov al, byte [ebp + H_DIVIDE_BUFFER + 0]
rcr al, 1
mov byte [ebp + H_DIVIDE_BUFFER + 0], al
jmp .div_loop
.div_done:
mov al, byte [ebp + H_DIVIDEND + 1]
mov byte [ebp + H_REMAINDER], al
mov al, byte [ebp + H_DIVIDE_BUFFER + 4]
mov byte [ebp + H_QUOTIENT + 3], al
mov al, byte [ebp + H_DIVIDE_BUFFER + 3]
shr eax, 8
mov byte [ebp + H_QUOTIENT + 2], al
mov al, byte [ebp + H_DIVIDE_BUFFER + 2]
shr eax, 8
mov byte [ebp + H_QUOTIENT + 1], al
mov al, byte [ebp + H_DIVIDE_BUFFER + 1]
shr eax, 8
mov byte [ebp + H_QUOTIENT + 0], al
.div_by_zero:
mov byte [ebp + H_REMAINDER], dl
.done:
pop edi
pop edx
pop ebx
ret

View File

@@ -12,9 +12,11 @@ section .text
; FF95-FF98 = product
Multiply:
push esi
push bx
call _Multiply
push edx ; GB _Multiply preserves de; our _Multiply clobbers
push bx ; edx via `mul ecx`, so save it (CalcStat keeps the
call _Multiply ; base stat in e/dl across the stat-exp multiply loop).
pop bx
pop edx
pop esi
ret

View File

@@ -0,0 +1,235 @@
; move_mon.asm — CalcStats / CalcStat (Pokémon data/stats plan).
;
; Source: home/move_mon.asm:CalcStats, CalcStat (pret/pokeyellow).
;
; Stat formula (per stat): (((Base + IV) * 2 + ceil(sqrt(statExp))/4) * Level)/100
; then + Level + 10 for HP, or + 5 for the others; capped at MAX_STAT_VALUE (999).
;
; Register map: a=AL, b=BH, c=BL, d=DH, e=DL, hl=ESI, de=EDX, bc=EBX.
; GB memory at [EBP+addr]; HRAM math scratch via H_MULTIPLICAND/H_PRODUCT/etc.
; The big-endian 2-byte result is left in H_MULTIPLICAND+1 (high) / +2 (low),
; exactly as the original, so CalcStats can copy it straight to [de].
;
; Build: nasm -f coff -I include/ -I . -o move_mon.o move_mon.asm
bits 32
%include "gb_memmap.inc"
%include "gb_constants.inc"
extern Multiply
extern Divide
global CalcStats
global CalcStat
MAX_STAT_HIGH equ (MAX_STAT_VALUE >> 8) & 0xFF ; HIGH(999) = 0x03
MAX_STAT_LOW equ MAX_STAT_VALUE & 0xFF ; LOW(999) = 0xE7
section .text
; calculates all 5 stats of the current mon and writes them to [de] (2 bytes each)
; In: BH (b) = consider stat exp?; ESI (hl) = base ptr to stat exp values;
; EDX (de) = destination pointer (GB address).
CalcStats:
mov bl, 0 ; c = 0
.statsLoop:
inc bl ; inc c
call CalcStat
mov al, [ebp + H_MULTIPLICAND + 1] ; stat high byte
mov [ebp + edx], al
inc edx
mov al, [ebp + H_MULTIPLICAND + 2] ; stat low byte
mov [ebp + edx], al
inc edx
cmp bl, NUM_STATS
jne .statsLoop
ret
; calculates stat c of the current mon
; In: BL (c) = stat (HP=1,Atk=2,Def=3,Spd=4,Spc=5); BH (b) = consider stat exp?;
; ESI (hl) = base ptr to stat exp ([hl+2c-1] and [hl+2c]).
; Out: result in H_MULTIPLICAND+1/+2 (big-endian). ESI/EDX/EBX preserved.
CalcStat:
push esi
push edx
push ebx
mov al, bh ; ld a, b
mov dh, al ; ld d, a (consider-stat-exp flag)
push esi ; push hl
mov esi, wMonHeader ; ld hl, wMonHeader
mov bh, 0 ; ld b, 0
movzx ecx, bx ; add hl, bc
add esi, ecx
mov al, [ebp + esi] ; ld a, [hl] base stat value
mov dl, al ; ld e, a
pop esi ; pop hl (stat exp base ptr)
push esi ; push hl
shl bl, 1 ; sla c (c *= 2)
mov al, dh ; ld a, d
test al, al ; and a (consider stat exp?)
jz .statExpDone
movzx ecx, bx ; add hl, bc (skip to stat exp value)
add esi, ecx
.statExpLoop: ; ceil(sqrt(statExp)) in b
xor al, al
mov [ebp + H_MULTIPLICAND], al
mov [ebp + H_MULTIPLICAND + 1], al
inc bh ; inc b
mov al, bh
cmp al, 0xFF
je .statExpDone
mov [ebp + H_MULTIPLICAND + 2], al ; b
mov [ebp + H_MULTIPLIER], al ; b
call Multiply ; b*b -> product
mov al, [ebp + esi] ; ld a, [hld] (stat exp low byte)
dec esi
mov dh, al ; ld d, a
mov al, [ebp + H_PRODUCT + 3]
sub al, dh ; sub d (sets borrow)
mov al, [ebp + esi] ; ld a, [hli] (stat exp high byte; mov/inc keep CF)
inc esi
mov dh, al ; ld d, a
mov al, [ebp + H_PRODUCT + 2]
sbb al, dh ; sbc d (test b^2 < statExp)
jc .statExpLoop
.statExpDone:
shr bl, 1 ; srl c (back to stat number)
pop esi ; pop hl (stat exp base ptr)
push ebx ; push bc
mov ebx, MON_DVS - (MON_HP_EXP - 1) ; ld bc, $0B
movzx ecx, bx ; add hl, bc -> hl = MON_DVS
add esi, ecx
pop ebx ; pop bc (stat number back in c/BL)
mov al, bl ; ld a, c
cmp al, 2
je .getAttackIV
cmp al, 3
je .getDefenseIV
cmp al, 4
je .getSpeedIV
cmp al, 5
je .getSpecialIV
; HP IV = LSB of the other four IVs
push ebx
mov al, [ebp + esi] ; Atk IV byte (DV byte 0)
rol al, 4 ; swap a
and al, 1
shl al, 1
shl al, 1
shl al, 1
mov bh, al
mov al, [ebp + esi] ; Def IV (byte 0 low nibble); hl++
inc esi
and al, 1
shl al, 1
shl al, 1
add al, bh
mov bh, al
mov al, [ebp + esi] ; Spd IV (byte 1 high nibble)
rol al, 4 ; swap a
and al, 1
shl al, 1
add al, bh
mov bh, al
mov al, [ebp + esi] ; Spc IV (byte 1 low nibble)
and al, 1
add al, bh ; HP IV
pop ebx
jmp .calcStatFromIV
.getAttackIV:
mov al, [ebp + esi]
rol al, 4 ; swap a
and al, 0x0F
jmp .calcStatFromIV
.getDefenseIV:
mov al, [ebp + esi]
and al, 0x0F
jmp .calcStatFromIV
.getSpeedIV:
inc esi
mov al, [ebp + esi]
rol al, 4 ; swap a
and al, 0x0F
jmp .calcStatFromIV
.getSpecialIV:
inc esi
mov al, [ebp + esi]
and al, 0x0F
.calcStatFromIV:
mov dh, 0 ; ld d, 0
add al, dl ; add e (IV + Base)
mov dl, al ; ld e, a
jnc .noCarry
inc dh ; de = Base + IV
.noCarry:
shl dl, 1
rcl dh, 1 ; de = (Base + IV) * 2
shr bh, 1
shr bh, 1 ; b = ceil(sqrt(statExp)) / 4
mov al, bh
add al, dl ; add e
jnc .noCarry2
inc dh
.noCarry2:
mov [ebp + H_MULTIPLICAND + 2], al
mov al, dh
mov [ebp + H_MULTIPLICAND + 1], al
xor al, al
mov [ebp + H_MULTIPLICAND], al
mov al, [ebp + wCurEnemyLevel]
mov [ebp + H_MULTIPLIER], al
call Multiply ; * Level
mov al, [ebp + H_MULTIPLICAND]
mov [ebp + H_DIVIDEND], al
mov al, [ebp + H_MULTIPLICAND + 1]
mov [ebp + H_DIVIDEND + 1], al
mov al, [ebp + H_MULTIPLICAND + 2]
mov [ebp + H_DIVIDEND + 2], al
mov al, 0x64
mov [ebp + H_DIVISOR], al
mov bh, 3 ; b = 3-byte dividend
call Divide ; / 100
mov al, bl ; ld a, c
cmp al, 1
mov al, 5 ; +5 for non-HP
jne .notHPStat
mov al, [ebp + wCurEnemyLevel] ; HP: + Level first
mov bh, al
mov al, [ebp + H_MULTIPLICAND + 2]
add al, bh
mov [ebp + H_MULTIPLICAND + 2], al
jnc .noCarry3
mov al, [ebp + H_MULTIPLICAND + 1]
inc al
mov [ebp + H_MULTIPLICAND + 1], al
.noCarry3:
mov al, 10 ; +10 for HP
.notHPStat:
mov bh, al
mov al, [ebp + H_MULTIPLICAND + 2]
add al, bh
mov [ebp + H_MULTIPLICAND + 2], al
jnc .noCarry4
mov al, [ebp + H_MULTIPLICAND + 1]
inc al
mov [ebp + H_MULTIPLICAND + 1], al
.noCarry4:
mov al, [ebp + H_MULTIPLICAND + 1] ; overflow check (>999)
cmp al, MAX_STAT_HIGH + 1
jnc .overflow
cmp al, MAX_STAT_HIGH
jc .noOverflow
mov al, [ebp + H_MULTIPLICAND + 2]
cmp al, MAX_STAT_LOW + 1
jc .noOverflow
.overflow:
mov al, MAX_STAT_HIGH
mov [ebp + H_MULTIPLICAND + 1], al
mov al, MAX_STAT_LOW
mov [ebp + H_MULTIPLICAND + 2], al
.noOverflow:
pop ebx
pop edx
pop esi
ret

View File

@@ -0,0 +1,51 @@
; pokemon.asm — GetMonHeader (Pokémon data/stats plan).
;
; Source: home/pokemon.asm:GetMonHeader + engine/menus/pokedex.asm:IndexToPokedex.
;
; Copies the 28-byte base-stats record for the internal species index in
; [wCurSpecies] into wMonHeader, then overwrites byte 0 (the dex id) with the
; internal index — matching the original.
;
; DIVERGENCE FROM GB: the data tables (BaseStats, IndexToPokedex) live in the
; program image as flat labels, not in EBP-relative GB memory, so we index them
; directly and `rep movsb` into [ebp+wMonHeader] instead of going through the
; GB CopyData/AddNTimes (which assume EBP-relative source). The fossil/ghost
; special sprite IDs are not handled (no battle sprites in this port yet).
;
; Build: nasm -f coff -I include/ -I . -o pokemon.o pokemon.asm
bits 32
%include "gb_memmap.inc"
%include "gb_constants.inc"
extern BaseStats
extern IndexToPokedex
global GetMonHeader
section .text
GetMonHeader:
pushad
; dex = IndexToPokedex[wCurSpecies - 1] (internal index -> national dex)
movzx eax, byte [ebp + wCurSpecies]
dec eax
movzx eax, byte [IndexToPokedex + eax]
; src = BaseStats + (dex - 1) * BASE_DATA_SIZE
dec eax
imul eax, eax, BASE_DATA_SIZE
lea esi, [BaseStats + eax] ; flat (program-image) source
lea edi, [ebp + wMonHeader] ; flat dest in GB memory
mov ecx, BASE_DATA_SIZE
rep movsb
; wMonHIndex = wCurSpecies (write internal index back over the dex byte)
mov al, [ebp + wCurSpecies]
mov [ebp + wMonHIndex], al
popad
ret