Files
DramaticShapeVoxelMod/model_extract/pipeline/battle.py
T
2026-08-05 18:30:41 -04:00

126 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""
Per-species battle data.
`func_84302658` in src/fragments/62 DMAs a 0xB90-byte table per species out of
the 0x70D3A0 segment, addressed through the D_80075BD0 pointer table. It is an
array of 0x10-byte entries: byte 0 is an index into that Pokemon's animation
list, byte 1 indexes the auxiliary (texture) animation list.
entries 0..164 one per move, so entry n drives move n + 1
entries 165+ fixed battle contexts
Every one of the 151 species' tables indexes only animations that species
actually has, which is what confirms the layout.
"""
import os
import struct
STRIDE = 0xB90
ENTRY = 0x10
N_MOVES = 165
# `evidence` records how far each label can be trusted:
# code - the battle code in src/fragments/62 names the slot outright
# data - inferred from what the referenced animation does, measured over all
# 151 species
CONTEXT_SLOTS = {
165: ('idle', 'code',
'The standby loop. func_8432B0A4 restores this slot whenever the Pokemon '
'returns to neutral, and it resolves to animation 0 for all 151 species.'),
166: ('attack_default', 'data',
'Resolves to animation 2 for 149/151 species, the same animation slots '
'178-181 use in the reaction paths. Called "hit" until the move table '
'was read against it: it is the animation most of a species\' MOVES '
'play, the default attack rather than a damage reaction (the name has '
'to match lib/StadiumPack.lua\'s CONTEXT and StadiumBuild\'s CONTEXTS).'),
167: ('faint', 'data',
'The referenced animation always ends far from the standing pose - the '
'model collapses to 0.03-0.84x its idle height, or leaves the frame '
'entirely for fliers.'),
168: ('entrance', 'code+data',
'The default slot in func_8430506C / func_8432AF70. The referenced '
'animation ends at exactly idle height, so it is a full cycle that '
'settles back into the standby pose.'),
169: ('reaction_169', 'data', 'Resolves to the idle animation for 139/151 species.'),
170: ('reaction_170', 'data', 'Split between the idle and hit animations.'),
171: ('reaction_171', 'data', 'Resolves to the idle animation for 138/151 species.'),
172: ('reaction_172', 'data', 'Resolves to the idle animation for 148/151 species.'),
173: ('reaction_173', 'data', 'Resolves to the hit animation for 97/151 species.'),
174: ('reaction_174', 'data', 'Resolves to the idle animation for 138/151 species.'),
175: ('struggle', 'code', 'Passed as slot 0xAF to func_84305A74.'),
176: ('idle_alt', 'code',
'Substituted for the idle slot when battle flag 0x200 is set.'),
177: ('faint_alt', 'data', 'Same animation as slot 167 for almost every species.'),
178: ('flinch', 'code',
'Used when the incoming move is one of the two listed in D_84384598.'),
179: ('reaction_179', 'data', 'Resolves to the hit animation for all 151 species.'),
180: ('reaction_180', 'data', 'Resolves to the hit animation for all 151 species.'),
181: ('reaction_181', 'data', 'Resolves to the hit animation for all 151 species.'),
182: ('reaction_182', 'data',
'Resolves to animation 0 for 136 species and animation 1 for the other 15.'),
183: ('entrance_alt', 'data', 'Same animation as slot 168 for every species.'),
184: ('idle_return', 'code', 'Passed as slot 0xB8 to func_84305A74.'),
}
MOVE_CONSTANTS = 'oldnotes/stadium1/constants/move_constants.s'
def load_move_names(repo_root='.'):
"""Move IDs come from the repo's own extracted constants when available."""
path = os.path.join(repo_root, MOVE_CONSTANTS)
names = {}
if os.path.exists(path):
for line in open(path, encoding='utf-8', errors='replace'):
# the file also carries an ABC_* section indexing moves alphabetically
# by their Japanese names; only the real move IDs are wanted
if ' EQU ' not in line or line.startswith('ABC_'):
continue
name, val = line.split(' EQU ')
val = val.split(';', 1)[0].strip()
if val:
names[int(val, 0)] = name.strip().replace('_', ' ').title()
return {i: names.get(i, f'Move {i}') for i in range(1, N_MOVES + 1)}
class BattleTables:
def __init__(self, rom):
from rom import BATTLE_DATA, PTR_TABLE_VRAM
self.rom = rom
self.base = BATTLE_DATA
self.ptr_table = rom.vram_to_rom(PTR_TABLE_VRAM)
def offset(self, species):
raw = self.rom.u32(self.ptr_table + (species - 1) * 4)
return self.base + (raw & 0xFFFFFF)
def rows(self, species):
"""Returns [(animIndex, auxIndex)] for every entry, aux 0xFF -> -1."""
o = self.offset(species)
out = []
for e in range(STRIDE // ENTRY):
anim = self.rom.data[o + e * ENTRY]
aux = self.rom.data[o + e * ENTRY + 1]
out.append((anim, -1 if aux == 0xFF else aux))
return out
SPECIES = {}
_NAMES = (
"Bulbasaur Ivysaur Venusaur Charmander Charmeleon Charizard Squirtle Wartortle Blastoise "
"Caterpie Metapod Butterfree Weedle Kakuna Beedrill Pidgey Pidgeotto Pidgeot Rattata Raticate "
"Spearow Fearow Ekans Arbok Pikachu Raichu Sandshrew Sandslash NidoranF Nidorina Nidoqueen "
"NidoranM Nidorino Nidoking Clefairy Clefable Vulpix Ninetales Jigglypuff Wigglytuff Zubat "
"Golbat Oddish Gloom Vileplume Paras Parasect Venonat Venomoth Diglett Dugtrio Meowth Persian "
"Psyduck Golduck Mankey Primeape Growlithe Arcanine Poliwag Poliwhirl Poliwrath Abra Kadabra "
"Alakazam Machop Machoke Machamp Bellsprout Weepinbell Victreebel Tentacool Tentacruel Geodude "
"Graveler Golem Ponyta Rapidash Slowpoke Slowbro Magnemite Magneton Farfetchd Doduo Dodrio "
"Seel Dewgong Grimer Muk Shellder Cloyster Gastly Haunter Gengar Onix Drowzee Hypno Krabby "
"Kingler Voltorb Electrode Exeggcute Exeggutor Cubone Marowak Hitmonlee Hitmonchan Lickitung "
"Koffing Weezing Rhyhorn Rhydon Chansey Tangela Kangaskhan Horsea Seadra Goldeen Seaking "
"Staryu Starmie MrMime Scyther Jynx Electabuzz Magmar Pinsir Tauros Magikarp Gyarados Lapras "
"Ditto Eevee Vaporeon Jolteon Flareon Porygon Omanyte Omastar Kabuto Kabutops Aerodactyl "
"Snorlax Articuno Zapdos Moltres Dratini Dragonair Dragonite Mewtwo Mew").split()
for _i, _n in enumerate(_NAMES):
SPECIES[_i + 1] = _n