pokemon: Stage 5b — _AddPartyMon (player gift path), validated

Wrote _AddPartyMon fresh (src/engine/pokemon/add_party_mon.asm) for the player,
non-battle gift path rather than reusing the broken swarm draft in add_mon.asm
(which references the not-yet-ported Moves table and the same flat-table-via-EBP
pattern). Faithful translation of engine/pokemon/add_mon.asm:_AddPartyMon:
appends the party-list entry + 0xFF terminator, copies the OT name, then writes
the 44-byte party_struct — species, random DVs, current HP = max HP (CalcStat),
box level/status, types + catch rate, level-1 moves, OT id, experience
(CalcExperience), EVs zeroed, level, and a fresh CalcStats stat block.

Validated natively (ELF32 harness): adding a level-5 Bulbasaur to an empty party
yields count=1, species list 0x99/0xFF, struct species 0x99, level 5, exp 135
(= CalcExperience(5), Medium Slow), curHP==maxHP=19, stats 19/9/9/9/11 (exact for
DV=0, cross-checked against the formula with the read-back DVs), types 22/3
(Grass/Poison), moves 33/45 (Tackle/Growl).

Documented stubs (TODO): move PP written as 0 (needs the Moves table, Stage 6);
OT id 0 (needs pokeyellow.sym for wPlayerID); WriteMonMoves level-up learnset
(keeps level-1 base moves — correct for low levels); Pokédex flags / AskName;
enemy-party and wild-caught (wIsInBattle) paths.

Also wired src/home/array.asm (AddNTimes/SkipFixedLengthTextEntries) into the
build and fixed its `extern NAME_LENGTH` to include gb_constants.inc instead.
Full project + DEBUG_CALCSTATS gate both build and link.

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 20:34:49 +00:00
parent 5e529e2da4
commit 5c233b729b
5 changed files with 214 additions and 7 deletions

View File

@@ -68,10 +68,15 @@ in UPPERCASE (`W_PARTY_COUNT`).
1000000/1059860/800000/1250000). The draft had THREE bugs: systematic `hli`
inversion (read wrong GrowthRateTable bytes), flat-table double-`ebp`
(segfault), and reliance on the broken `_Divide`/`Multiply` (fixed Stage 3).
- [ ] `_AddPartyMon` (the core creation routine — next).
- [ ] wire/audit `load_mon_data`, `set_types`, `remove_mon`. `_AddPartyMon`
from `engine/pokemon/add_mon.asm:1` (stub `AskName`/Pokédex); verify by adding
a known mon and dumping its party_struct.
- [x] `_AddPartyMon` — written fresh (`src/engine/pokemon/add_party_mon.asm`,
player non-battle gift path) rather than reusing the broken draft. Validated
natively: adds L5 Bulbasaur → count 1, species list correct, struct species
0x99, level 5, exp 135 (`CalcExperience(5)`), curHP==maxHP, stats `19/9/9/9/11`
(exact for DV=0), types Grass/Poison, moves Tackle/Growl. Wired into Makefile
(also wired `array.asm`, fixed its `NAME_LENGTH` extern → `gb_constants.inc`).
Stubs (TODO): PP=0 (needs Moves table), OT id=0 (needs sym), WriteMonMoves/
Pokédex/AskName/enemy+wild paths.
- [ ] wire/audit `load_mon_data`, `set_types`, `remove_mon` (Stage 5 tail).
- [ ] **Stage 6 — Evolution / learnset / PC.** Generate `EvosMovesPointerTable` +
`MonsterNames`; wire `evos_moves.asm` (`WriteMonMoves`/`GetMonLearnset` needed by

View File

@@ -110,9 +110,11 @@ POKEMON_SRCS := \
src/data/pokemon_data.asm \
src/home/math.asm \
src/engine/math/multiply_divide.asm \
src/home/array.asm \
src/home/pokemon.asm \
src/home/move_mon.asm \
src/engine/pokemon/experience.asm
src/engine/pokemon/experience.asm \
src/engine/pokemon/add_party_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.

View File

@@ -142,6 +142,7 @@ wPartySpecies equ 0xD163 ; wPartyCount+1; 6 species + $FF sentinel
wPartyMons equ 0xD16A
wPartyMonOT equ 0xD272
wPartyMonNicks equ 0xD2B4
wPlayerName equ 0xD157 ; = W_PLAYER_NAME (OT name source)
; Sprite state data 16 slots × $10 bytes each, two parallel arrays.
; wSpriteStateData1 ($C100) and wSpriteStateData2 ($C200) sym-verified.

View File

@@ -0,0 +1,198 @@
; add_party_mon.asm — _AddPartyMon (Pokémon data/stats plan, Stage 5).
;
; Source: engine/pokemon/add_mon.asm:_AddPartyMon (player, non-battle path).
;
; Adds a new mon to the PLAYER's party (wMonDataLocation low nibble = 0), the
; non-battle gift/normal path. Caller sets wCurPartySpecies and wCurEnemyLevel.
; Writes the party-list entry + the 44-byte party_struct (species, random DVs,
; current HP = max HP, box level/status, types, level-1 moves, OT, experience,
; level, fresh stats). Returns CF set on success, CF clear if the party is full.
;
; DEFERRED (TODO):
; - Enemy party + wild-caught (wIsInBattle) paths — need wEnemyParty*/wIsInBattle
; addresses (pending pokeyellow.sym).
; - WriteMonMoves (level-up learnset): the level-1 base moves are kept (correct
; for low-level mons); needs the evos_moves audit (Stage 6).
; - Move PP: written as 0 (needs the Moves table, Stage 6); de still advances 4.
; - Pokédex owned/seen flags and AskName naming — skipped (no effect on stats).
; - OT id written as 0 (wPlayerID address pending the sym).
;
; Register map: a=AL, b=BH, c=BL, d=DH, hl=ESI, de=EDX, bc=EBX.
bits 32
%include "gb_memmap.inc"
%include "gb_constants.inc"
extern GetMonHeader
extern CalcStat
extern CalcStats
extern CalcExperience
extern Random_
extern SkipFixedLengthTextEntries
extern CopyData
extern AddNTimes
global _AddPartyMon
section .text
_AddPartyMon:
mov al, [ebp + wPartyCount]
inc al
cmp al, PARTY_LENGTH + 1
jc .notFull
ret ; party full (ret nc): CF clear
.notFull:
mov [ebp + wPartyCount], al ; new count (doubles as hNewPartyLength)
; append species: edx = wPartyCount + count -> &wPartySpecies[count-1]
movzx ecx, al
lea edx, [wPartyCount + ecx]
mov al, [ebp + wCurPartySpecies]
mov [ebp + edx], al
inc edx
mov byte [ebp + edx], 0xFF ; list terminator
; OT name slot: esi = wPartyMonOT + (count-1)*NAME_LENGTH
mov esi, wPartyMonOT
mov al, [ebp + wPartyCount]
dec al
call SkipFixedLengthTextEntries
mov edx, esi ; de = OT dest
mov esi, wPlayerName
mov bx, NAME_LENGTH
call CopyData
; esi = wPartyMons + (count-1)*PARTYMON_STRUCT_LENGTH
mov esi, wPartyMons
mov al, [ebp + wPartyCount]
dec al
mov bx, PARTYMON_STRUCT_LENGTH
call AddNTimes
mov edx, esi ; de = struct start (write cursor)
push esi ; [S1] struct ptr (for final CalcStats)
; species byte (internal index)
mov al, [ebp + wCurPartySpecies]
mov [ebp + wCurSpecies], al
call GetMonHeader
mov al, [ebp + wMonHeader]
mov [ebp + edx], al
inc edx ; de = struct+1
; random DVs (non-battle): bh = 1st byte, al = 2nd byte
call Random_
mov bh, al
call Random_
mov esi, [esp] ; struct ptr
add esi, MON_DVS
mov [ebp + esi], al ; DV byte 0
inc esi
mov [ebp + esi], bh ; DV byte 1
; current HP = max HP: CalcStat(c=1 HP, b=0)
mov esi, [esp]
add esi, MON_HP_EXP - 1
mov bl, 1
mov bh, 0
call CalcStat
mov al, [ebp + H_MULTIPLICAND + 1]
mov [ebp + edx], al
inc edx
mov al, [ebp + H_MULTIPLICAND + 2]
mov [ebp + edx], al
inc edx ; de = struct+3
xor al, al
mov [ebp + edx], al ; box level 0
inc edx
mov [ebp + edx], al ; status 0
inc edx ; de = struct+5
; types + catch rate from wMonHTypes
mov esi, wMonHTypes
mov al, [ebp + esi]
inc esi
mov [ebp + edx], al ; type1
inc edx
mov al, [ebp + esi]
inc esi
mov [ebp + edx], al ; type2
inc edx
mov al, [ebp + esi]
mov [ebp + edx], al ; catch rate (de not yet incremented)
; level-1 moves from wMonHMoves
mov esi, wMonHMoves
mov al, [ebp + esi]
inc esi
inc edx ; de = struct+8 (MON_MOVES)
push edx ; [S2] moves ptr (for PP)
mov [ebp + edx], al
mov al, [ebp + esi]
inc esi
inc edx
mov [ebp + edx], al
mov al, [ebp + esi]
inc esi
inc edx
mov [ebp + edx], al
mov al, [ebp + esi]
inc esi
inc edx
mov [ebp + edx], al ; de = struct+11
; OT id (stub 0)
inc edx
mov byte [ebp + edx], 0 ; OTID hi (struct+12)
inc edx
mov byte [ebp + edx], 0 ; OTID lo (struct+13)
; experience = CalcExperience(level)
push edx ; [S3]
mov al, [ebp + wCurEnemyLevel]
mov dh, al
call CalcExperience
pop edx ; [S3]
inc edx
mov al, [ebp + H_EXPERIENCE]
mov [ebp + edx], al ; exp hi (struct+14)
inc edx
mov al, [ebp + H_EXPERIENCE + 1]
mov [ebp + edx], al
inc edx
mov al, [ebp + H_EXPERIENCE + 2]
mov [ebp + edx], al ; de = struct+16
; zero EVs (NUM_STATS*2 bytes)
mov bh, NUM_STATS * 2
.evLoop:
inc edx
mov byte [ebp + edx], 0
dec bh
jnz .evLoop ; de = struct+0x1A
inc edx
inc edx ; de = struct+0x1C
pop esi ; [S2] moves ptr (PP stub doesn't use it; balances)
; PP stub: advance de by NUM_MOVES, writing 0 (TODO real PP — Moves table)
mov bh, NUM_MOVES
.ppStub:
inc edx
mov byte [ebp + edx], 0
dec bh
jnz .ppStub ; de = struct+0x20
; level
inc edx
mov al, [ebp + wCurEnemyLevel]
mov [ebp + edx], al ; struct+MON_LEVEL (0x21)
inc edx ; de = struct+MON_STATS (0x22)
; fresh stats
pop esi ; [S1] struct ptr
add esi, MON_HP_EXP - 1
mov bh, 0
call CalcStats
stc ; success
ret

View File

@@ -1,9 +1,10 @@
; dos_port/home/array.asm
bits 32
%include "gb_constants.inc" ; NAME_LENGTH
global SkipFixedLengthTextEntries
global AddNTimes
extern NAME_LENGTH
section .text
; skips AL (A) text entries, each of size NAME_LENGTH