Implement native opening scenes, battle foundation and overworld movement
Checks / web (push) Canceled after 0s

This commit is contained in:
Sébastien QUEROL committed 2026-09-23 16:09:22 +02:00
1 parent 8b80d05ca2
commit c08d250cf6
39 files changed
+10727 -1524

No files matched your search

File diff suppressed because it is too large. Load diff
+294
View File
@@ -0,0 +1,294 @@
/** Battle constants under their PRET names (constants/*.asm and
* macros/scripts/battle_commands.asm). `npm run check:reference` compares
* every entry with the pinned source, so a wrong value cannot ship. */
export const C = {
// Move effects (constants/move_effect_constants.asm)
EFFECT_NORMAL_HIT: 0,
EFFECT_LEECH_HIT: 3,
EFFECT_SELFDESTRUCT: 7,
EFFECT_DREAM_EATER: 8,
EFFECT_ALWAYS_HIT: 17,
EFFECT_ATTACK_DOWN: 18,
EFFECT_EVASION_DOWN: 24,
EFFECT_BIDE: 26,
EFFECT_RAMPAGE: 27,
EFFECT_MULTI_HIT: 29,
EFFECT_CONVERSION: 30,
EFFECT_TOXIC: 33,
EFFECT_RAZOR_WIND: 39,
EFFECT_DOUBLE_HIT: 44,
EFFECT_JUMP_KICK: 45,
EFFECT_ATTACK_DOWN_2: 58,
EFFECT_EVASION_DOWN_2: 64,
EFFECT_ATTACK_DOWN_HIT: 68,
EFFECT_ACCURACY_DOWN_HIT: 73,
EFFECT_EVASION_DOWN_HIT: 74,
EFFECT_SKY_ATTACK: 75,
EFFECT_CONFUSE_HIT: 76,
EFFECT_POISON_MULTI_HIT: 77,
EFFECT_RAGE: 81,
EFFECT_SPLASH: 85,
EFFECT_SNORE: 92,
EFFECT_PRIORITY_HIT: 103,
EFFECT_TRIPLE_KICK: 104,
EFFECT_PROTECT: 111,
EFFECT_ENDURE: 116,
EFFECT_ROLLOUT: 117,
EFFECT_SWAGGER: 118,
EFFECT_FURY_CUTTER: 119,
EFFECT_SKULL_BASH: 145,
EFFECT_FUTURE_SIGHT: 148,
EFFECT_SOLARBEAM: 151,
EFFECT_THUNDER: 152,
EFFECT_BEAT_UP: 154,
EFFECT_FLY: 155,
// Moves (constants/move_constants.asm)
GUST: 16,
WHIRLWIND: 18,
FLY: 19,
THUNDER: 87,
EARTHQUAKE: 89,
FISSURE: 90,
DIG: 91,
MIMIC: 102,
MINIMIZE: 107,
SUBSTITUTE: 164,
STRUGGLE: 165,
FLAME_WHEEL: 172,
SNORE: 173,
SLEEP_TALK: 214,
SACRED_FIRE: 221,
MAGNITUDE: 222,
VITAL_THROW: 233,
TWISTER: 239,
// Types (constants/type_constants.asm)
NORMAL: 0,
FLYING: 2,
POISON: 3,
FIRE: 20,
SPECIAL: 20,
ICE: 25,
// Species and items
PIKACHU: 25,
FARFETCH_D: 83,
CUBONE: 104,
MAROWAK: 105,
CHANSEY: 113,
MAGIKARP: 129,
DITTO: 132,
UNOWN: 201,
NO_ITEM: 0,
LUCKY_PUNCH: 30,
METAL_POWDER: 35,
STICK: 105,
THICK_CLUB: 118,
EXP_SHARE: 57,
CLEANSE_TAG: 94,
LUCKY_EGG: 126,
LIGHT_BALL: 163,
// Held item effects (constants/item_data_constants.asm)
HELD_BERRY: 1,
HELD_LEFTOVERS: 3,
HELD_RESTORE_PP: 6,
HELD_HEAL_POISON: 10,
HELD_HEAL_PARALYZE: 14,
HELD_HEAL_STATUS: 15,
HELD_HEAL_CONFUSION: 16,
HELD_PREVENT_POISON: 20,
HELD_PREVENT_BURN: 21,
HELD_PREVENT_FREEZE: 22,
HELD_PREVENT_SLEEP: 23,
HELD_PREVENT_PARALYZE: 24,
HELD_PREVENT_CONFUSE: 25,
HELD_ESCAPE: 72,
HELD_CRITICAL_UP: 73,
HELD_QUICK_CLAW: 74,
HELD_FLINCH: 75,
HELD_ATTACK_UP: 31,
HELD_EVASION_UP: 37,
HELD_BRIGHTPOWDER: 77,
HELD_FOCUS_BAND: 79,
// Stats and levels
ATTACK: 0,
DEFENSE: 1,
SPEED: 2,
SP_ATTACK: 3,
SP_DEFENSE: 4,
ACCURACY: 5,
EVASION: 6,
BASE_STAT_LEVEL: 7,
MAX_STAT_LEVEL: 13,
MAX_LEVEL: 100,
PP_MASK: 63,
NUM_MOVES: 4,
BASE_HAPPINESS: 70,
START_MONEY: 3000,
MAX_MONEY: 999999,
// Status bits and masks
SLP_MASK: 7,
PSN: 3,
BRN: 4,
FRZ: 5,
PAR: 6,
// Substatus bits, by byte: 1 (Nightmare..In Love), 2 (Curled),
// 3 (Bide..Confused), 4 (X Accuracy..Leech Seed), 5 (Toxic..Can't Run)
SUBSTATUS_NIGHTMARE: 0,
SUBSTATUS_CURSE: 1,
SUBSTATUS_PROTECT: 2,
SUBSTATUS_IDENTIFIED: 3,
SUBSTATUS_PERISH: 4,
SUBSTATUS_ENDURE: 5,
SUBSTATUS_ROLLOUT: 6,
SUBSTATUS_IN_LOVE: 7,
SUBSTATUS_CURLED: 0,
SUBSTATUS_BIDE: 0,
SUBSTATUS_RAMPAGE: 1,
SUBSTATUS_IN_LOOP: 2,
SUBSTATUS_FLINCHED: 3,
SUBSTATUS_CHARGED: 4,
SUBSTATUS_UNDERGROUND: 5,
SUBSTATUS_FLYING: 6,
SUBSTATUS_CONFUSED: 7,
SUBSTATUS_X_ACCURACY: 0,
SUBSTATUS_MIST: 1,
SUBSTATUS_FOCUS_ENERGY: 2,
SUBSTATUS_SUBSTITUTE: 4,
SUBSTATUS_RECHARGE: 5,
SUBSTATUS_RAGE: 6,
SUBSTATUS_LEECH_SEED: 7,
SUBSTATUS_TOXIC: 0,
SUBSTATUS_TRANSFORMED: 3,
SUBSTATUS_ENCORED: 4,
SUBSTATUS_LOCK_ON: 5,
SUBSTATUS_DESTINY_BOND: 6,
SUBSTATUS_CANT_RUN: 7,
// Screens, weather and effectiveness
SCREENS_SPIKES: 0,
SCREENS_SAFEGUARD: 2,
SCREENS_LIGHT_SCREEN: 3,
SCREENS_REFLECT: 4,
WEATHER_NONE: 0,
WEATHER_RAIN: 1,
WEATHER_SUN: 2,
NO_EFFECT: 0,
NOT_VERY_EFFECTIVE: 5,
EFFECTIVE: 10,
SUPER_EFFECTIVE: 20,
STAB_DAMAGE: 128,
EFFECTIVENESS_MASK: 127,
BASE_PRIORITY: 1,
// Battle modes, types and results
WILD_BATTLE: 1,
TRAINER_BATTLE: 2,
BATTLETYPE_NORMAL: 0,
BATTLETYPE_DEBUG: 2,
BATTLETYPE_TUTORIAL: 3,
BATTLETYPE_ROAMING: 5,
BATTLETYPE_CONTEST: 6,
BATTLETYPE_FORCESHINY: 7,
BATTLETYPE_TRAP: 9,
BATTLETYPE_FORCEITEM: 10,
BATTLETYPE_CELEBI: 11,
BATTLETYPE_SUICUNE: 12,
WIN: 0,
LOSE: 1,
DRAW: 2,
MORN_F: 0,
DAY_F: 1,
NITE_F: 2,
} as const;
/** Battle command IDs (macros/scripts/battle_commands.asm). */
export const COMMAND = {
checkturn: 1,
checkobedience: 2,
usedmovetext: 3,
doturn: 4,
critical: 5,
damagestats: 6,
stab: 7,
damagevariation: 8,
checkhit: 9,
lowersub: 10,
moveanimnosub: 11,
raisesub: 12,
failuretext: 13,
applydamage: 14,
criticaltext: 15,
supereffectivetext: 16,
checkfaint: 17,
buildopponentrage: 18,
poisontarget: 19,
sleeptarget: 20,
draintarget: 21,
burntarget: 23,
freezetarget: 24,
paralyzetarget: 25,
flinchtarget: 37,
confuse: 42,
confusetarget: 43,
poison: 47,
paralyze: 48,
splash: 54,
kingsrock: 77,
foresight: 87,
damagecalc: 98,
checksafeguard: 101,
healmorn: 106,
healday: 107,
healnite: 108,
attackup: 112,
defenseup: 113,
speedup: 114,
specialattackup: 115,
specialdefenseup: 116,
accuracyup: 117,
evasionup: 118,
attackup2: 119,
defenseup2: 120,
speedup2: 121,
specialattackup2: 122,
specialdefenseup2: 123,
accuracyup2: 124,
evasionup2: 125,
attackdown: 126,
defensedown: 127,
speeddown: 128,
specialattackdown: 129,
specialdefensedown: 130,
accuracydown: 131,
evasiondown: 132,
attackdown2: 133,
defensedown2: 134,
speeddown2: 135,
specialattackdown2: 136,
specialdefensedown2: 137,
accuracydown2: 138,
evasiondown2: 139,
statupmessage: 140,
statdownmessage: 141,
statupfailtext: 142,
statdownfailtext: 143,
effectchance: 144,
statdownanim: 145,
statupanim: 146,
switchturn: 147,
rage: 151,
doubleflyingdamage: 152,
ragedamage: 162,
clearmissdamage: 169,
movedelay: 170,
moveanim: 171,
curl: 175,
endmove: 0xff,
endturn: 0xfe,
} as const;
+116
View File
@@ -0,0 +1,116 @@
import { RomReader } from "../rom/reader";
import { BATTLE } from "../rom/profile";
/** Readers for the battle engine's ROM tables. */
export class BattleData {
constructor(readonly reader: RomReader) {}
/** The effect script bytes for a move effect, up to endmove. */
effectScript(effect: number): number[] {
const r = this.reader;
let p = r.near(
BATTLE.MoveEffectsPointers + effect * 2,
r.bank(BATTLE.MoveEffectsPointers),
);
const script: number[] = [];
for (let i = 0; i < 64; i++) {
const command = r.byte(p++);
script.push(command);
if (command === 0xff) return script;
}
throw new Error("Move effect script is unterminated.");
}
/** TypeMatchups rows: attacking type, defending type, multiplier. The
* row with attacker 0xfe marks where Foresight stops the lookup. */
typeMatchups(): { attacker: number; defender: number; multiplier: number }[] {
const r = this.reader,
rows = [];
for (let p = BATTLE.TypeMatchups; rows.length < 256; ) {
const attacker = r.byte(p++);
if (attacker === 0xff) return rows;
if (attacker === 0xfe) {
rows.push({ attacker, defender: 0, multiplier: 0 });
continue;
}
rows.push({ attacker, defender: r.byte(p), multiplier: r.byte(p + 1) });
p += 2;
}
throw new Error("Type matchups are unterminated.");
}
/** Numerator and denominator pairs for stat levels 1-13. */
statMultiplier(level: number): [number, number] {
const p = BATTLE.StatLevelMultipliers + (level - 1) * 2;
return [this.reader.byte(p), this.reader.byte(p + 1)];
}
accuracyMultiplier(level: number): [number, number] {
const p = BATTLE.AccuracyLevelMultipliers + (level - 1) * 2;
return [this.reader.byte(p), this.reader.byte(p + 1)];
}
criticalChance(stage: number): number {
return this.reader.byte(BATTLE.CriticalHitChances + Math.min(stage, 6));
}
/** A -1 terminated byte list, such as CriticalHitMoves. */
byteList(address: number): number[] {
const values: number[] = [];
for (let p = address; values.length < 256; p++) {
const value = this.reader.byte(p);
if (value === 0xff) return values;
values.push(value);
}
throw new Error("List is unterminated.");
}
/** MoveEffectPriorities: effect, priority pairs. */
priority(effect: number, basePriority: number): number {
const r = this.reader;
for (let p = BATTLE.MoveEffectPriorities; ; p += 2) {
const entry = r.byte(p);
if (entry === effect) return r.byte(p + 1);
if (entry === 0xff) return basePriority;
}
}
/** TypeBoostItems: held effect, type pairs. */
typeBoost(heldEffect: number): number | undefined {
const r = this.reader;
for (let p = BATTLE.TypeBoostItems; ; p += 2) {
const entry = r.byte(p);
if (entry === 0xff) return undefined;
if (entry === heldEffect) return r.byte(p + 1);
}
}
/** GetItemHeldEffect: the held effect and its parameter. */
heldEffect(item: number): { effect: number; parameter: number } {
if (!item) return { effect: 0, parameter: 0 };
const p = BATTLE.ItemAttributes + (item - 1) * 7 + 2;
return { effect: this.reader.byte(p), parameter: this.reader.byte(p + 1) };
}
/** HeldStatusHealingEffects: the statuses a held item cures. */
heldStatusHealing(effect: number): number | undefined {
const r = this.reader;
for (let p = BATTLE.HeldStatusHealingEffects; ; p += 2) {
const entry = r.byte(p);
if (entry === 0xff) return undefined;
if (entry === effect) return r.byte(p + 1);
}
}
/** GetMoveGrammar: the grammar set a move's "used" text follows. */
moveGrammar(move: number): number {
const r = this.reader;
// Moves missing from every set use the set after the last one.
let set = 0;
for (let p = BATTLE.MoveGrammar; set < 8; ) {
const entry = r.byte(p++);
if (entry === 0xff || entry === move) return set;
if (entry === 0) set++;
}
throw new Error("Move grammar table is unterminated.");
}
/** GetStatName: the n-th "@"-terminated name in StatNames. */
statNameAddress(stat: number): number {
const r = this.reader;
let p = BATTLE.StatNames;
for (let skipped = 0; skipped < stat; skipped++) while (r.byte(p++) !== 0x50);
return p;
}
}