Verify random battle and capture routines against the original engine
Checks / web (push) Canceled after 0s

This commit is contained in:
Sébastien QUEROL committed 2026-09-30 15:48:00 +02:00
1 parent be7575a4d0
commit 00972bad10
9 files changed
+978 -74

No files matched your search

+38 -28
View File
@@ -296,7 +296,7 @@ export function applyTypeMatchups(
* native translation. Unsupported AI, switching, contest and item paths are refused. */
/** PokeBallEffect's wFinalCatchRate for Poké, Great and Ultra Balls, in
* the original 8- and 16-bit registers. Returns undefined when the
* formula would divide by zero (a maximum HP of 342). */
* formula would divide by zero (e.g. maximum HP of 342 or 683). */
export function catchRate(input: {
rate: number;
ball: number;
@@ -1310,33 +1310,7 @@ export class Battle {
this.wildMon = 0;
yield* this.say(BATTLE.ItemUsedText);
let caught = tutorial || item === C.MASTER_BALL;
if (!caught) {
const held = this.data.heldEffect(this.player.mon?.item ?? 0);
const chance = catchRate({
rate: this.enemyBase.catchRate,
ball: item,
hp: this.enemy.mon.hp,
maxHp: this.enemy.mon.maxHp,
status: this.enemy.mon.status,
held,
});
if (chance === undefined) throw new UnsupportedBattle("The catch rate divides by zero.");
this.finalCatchRate = chance;
caught = this.random() <= chance;
}
// BattleAnim_ThrowPokeBall's .Shake loop runs GetPokeBallWobble until
// the ball clicks or the Pokémon breaks free.
let wobbles = 0;
for (;;) {
wobbles++;
if (wobbles === 4) break;
if (caught) continue;
let entry = BATTLE.WobbleProbabilities;
while (this.reader.byte(entry) < this.finalCatchRate) entry += 2;
if (this.random() >= this.reader.byte(entry + 1)) break;
}
const { caught, wobbles } = this.rollCapture(item, tutorial);
if (!caught) {
yield* this.say(
@@ -1410,6 +1384,42 @@ export class Battle {
this.tossItem("balls", item, slot);
return true;
}
/** PokeBallEffect's decision, before animation and caught-mon loading. */
private rollCapture(item: number, tutorial: boolean) {
let caught = tutorial || item === C.MASTER_BALL;
if (!caught) {
const held = this.data.heldEffect(this.player.mon?.item ?? 0);
const chance = catchRate({
rate: this.enemyBase.catchRate,
ball: item,
hp: this.enemy.mon.hp,
maxHp: this.enemy.mon.maxHp,
status: this.enemy.mon.status,
held,
});
if (chance === undefined) throw new UnsupportedBattle("The catch rate divides by zero.");
this.finalCatchRate = chance;
caught = this.random() <= chance;
}
return { caught, wobbles: this.ballWobbles(caught) };
}
private ballWobbles(caught: boolean): number {
// BattleAnim_ThrowPokeBall's .Shake loop runs GetPokeBallWobble until
// the ball clicks or the Pokémon breaks free.
let wobbles = 0;
for (;;) {
wobbles++;
if (wobbles === 4) break;
if (caught) continue;
let entry = BATTLE.WobbleProbabilities;
while (this.reader.byte(entry) < this.finalCatchRate) entry += 2;
if (this.random() >= this.reader.byte(entry + 1)) break;
}
return wobbles;
}
/** AskGiveNicknameText, then NamingScreen (NAME_MON) if accepted: the
* chosen name, or the species name for a declined or blank one. */
private *askNickname(name: string): Gen<string> {