Port Kurt's balls and the Park Ball's multiplier (#28)
Add the Heavy, Level, Lure, Fast, Friend, Moon and Love Balls with their documented bugs, the Park Ball's multiplier outside the contest, and Friend Ball happiness in the party and the PC. The Heavy Ball is refused for Kadabra and Sunflora, whose original weight read runs into VRAM. The reference emulator compares 6,153 more catch states per browser against the unmodified PokeBallEffect. Validated with 582 unit tests, 153 browser tests, native/reference builds and reference/vendor/release checks. The Bug-Catching Contest and other special battle types remain on #28. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
77704fe848
commit
b547ca29ff
8 files changed
+264
-22
No files matched your search
@@ -19,6 +19,15 @@ type Input = {
|
||||
initialRate: number;
|
||||
caught: boolean;
|
||||
rolls: number[];
|
||||
/** wEnemyMonSpecies; Fast, Moon and Love Balls use wTempEnemyMonSpecies. */
|
||||
species: number;
|
||||
tempSpecies: number;
|
||||
enemyLevel: number;
|
||||
enemyDvs: number;
|
||||
playerSpecies: number;
|
||||
playerLevel: number;
|
||||
playerDvs: number;
|
||||
fish: boolean;
|
||||
};
|
||||
const make = (overrides: Partial<Input> = {}): Input => ({
|
||||
rate: 45,
|
||||
@@ -31,6 +40,14 @@ const make = (overrides: Partial<Input> = {}): Input => ({
|
||||
initialRate: 173,
|
||||
caught: false,
|
||||
rolls: [255, 0, 128, 255],
|
||||
species: 19,
|
||||
tempSpecies: 19,
|
||||
enemyLevel: 5,
|
||||
enemyDvs: 0x8888,
|
||||
playerSpecies: 19,
|
||||
playerLevel: 5,
|
||||
playerDvs: 0x8888,
|
||||
fish: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
@@ -59,6 +76,13 @@ async function compare(page: Page, mode: "capture" | "wobble", cases: Input[]) {
|
||||
status: at("wEnemyMonStatus"),
|
||||
species: at("wEnemyMonSpecies"),
|
||||
heldItem: at("wBattleMonItem"),
|
||||
tempSpecies: at("wTempEnemyMonSpecies"),
|
||||
tempBattleSpecies: at("wTempBattleMonSpecies"),
|
||||
enemyLevel: at("wEnemyMonLevel"),
|
||||
enemyDvs: at("wEnemyMonDVs"),
|
||||
playerLevel: at("wBattleMonLevel"),
|
||||
partyDvs: at("wPartyMon1DVs"),
|
||||
curBattleMon: at("wCurBattleMon"),
|
||||
};
|
||||
await openReference(page);
|
||||
const results = await page.evaluate(
|
||||
@@ -164,7 +188,7 @@ async function compare(page: Page, mode: "capture" | "wobble", cases: Input[]) {
|
||||
let draws = 0;
|
||||
const battle = new Battle({
|
||||
data: assets.pokemon,
|
||||
party: [],
|
||||
party: [{ species: input.playerSpecies, dvs: input.playerDvs }] as never,
|
||||
pokedex: { seen: new Set(), caught: new Set() },
|
||||
playerId: 1,
|
||||
playerName: "CHRIS",
|
||||
@@ -178,6 +202,8 @@ async function compare(page: Page, mode: "capture" | "wobble", cases: Input[]) {
|
||||
});
|
||||
const internal = battle as unknown as {
|
||||
finalCatchRate: number;
|
||||
enemySpecies: number;
|
||||
battleType: number;
|
||||
rollCapture(
|
||||
item: number,
|
||||
tutorial: boolean,
|
||||
@@ -189,8 +215,13 @@ async function compare(page: Page, mode: "capture" | "wobble", cases: Input[]) {
|
||||
hp: input.hp,
|
||||
maxHp: input.maxHp,
|
||||
status: input.status,
|
||||
species: input.species,
|
||||
level: input.enemyLevel,
|
||||
dvs: input.enemyDvs,
|
||||
} as never;
|
||||
battle.player.mon = { item: input.heldItem } as never;
|
||||
battle.player.mon = { item: input.heldItem, level: input.playerLevel } as never;
|
||||
internal.enemySpecies = input.tempSpecies;
|
||||
if (input.fish) internal.battleType = 4;
|
||||
internal.finalCatchRate = input.initialRate;
|
||||
const native =
|
||||
mode === "capture"
|
||||
@@ -205,8 +236,15 @@ async function compare(page: Page, mode: "capture" | "wobble", cases: Input[]) {
|
||||
write(a.wild, mode === "wobble" && input.caught ? 19 : 0);
|
||||
write(a.wobbles, 0);
|
||||
write(a.item, input.ball);
|
||||
write(a.battleType, input.tutorial ? 3 : 0);
|
||||
write(a.species, 19);
|
||||
write(a.battleType, input.tutorial ? 3 : input.fish ? 4 : 0);
|
||||
write(a.species, input.species);
|
||||
write(a.tempSpecies, input.tempSpecies);
|
||||
write(a.tempBattleSpecies, input.playerSpecies);
|
||||
write(a.enemyLevel, input.enemyLevel);
|
||||
write(a.playerLevel, input.playerLevel);
|
||||
word(a.enemyDvs, input.enemyDvs);
|
||||
word(a.partyDvs, input.playerDvs);
|
||||
write(a.curBattleMon, 0);
|
||||
write(a.status, input.status);
|
||||
write(a.heldItem, input.heldItem);
|
||||
word(a.hp, input.hp);
|
||||
@@ -310,3 +348,41 @@ test("ball shakes match original table boundaries, outcomes and draw counts", as
|
||||
cases.push(make({ initialRate: rate, rolls: [roll, 255] }));
|
||||
await compare(page, "wobble", cases);
|
||||
});
|
||||
|
||||
test("Kurt's and Park Balls match the original multipliers and their bugs", async ({
|
||||
page,
|
||||
}) => {
|
||||
const cases: Input[] = [];
|
||||
const all = Array.from({ length: 251 }, (_, i) => i + 1);
|
||||
// Heavy Ball: every species' weight (Transform changes wEnemyMonSpecies,
|
||||
// not the temp species), around the light-mon floor and the 255 cap.
|
||||
// Kadabra's and Sunflora's reads reach VRAM and are refused natively.
|
||||
for (const species of all.filter((species) => species !== 64 && species !== 192))
|
||||
for (const rate of [1, 20, 21, 45, 240])
|
||||
cases.push(make({ ball: C.HEAVY_BALL, species, tempSpecies: 19, rate, hp: 1, maxHp: 100 }));
|
||||
// Level Ball: each halving boundary and saturation; it skips HP and status.
|
||||
const levels = [1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 65, 100];
|
||||
for (const playerLevel of levels)
|
||||
for (const enemyLevel of levels)
|
||||
for (const rate of [1, 31, 32, 64, 255])
|
||||
cases.push(make({ ball: C.LEVEL_BALL, playerLevel, enemyLevel, rate, status: 1 }));
|
||||
// Lure Ball only on a fishing rod battle; Park Ball everywhere.
|
||||
for (let rate = 0; rate < 256; rate++) {
|
||||
cases.push(make({ ball: C.LURE_BALL, rate, fish: true, hp: 1, maxHp: 100 }));
|
||||
cases.push(make({ ball: C.LURE_BALL, rate, hp: 1, maxHp: 100 }));
|
||||
cases.push(make({ ball: C.PARK_BALL, rate, hp: 1, maxHp: 100 }));
|
||||
cases.push(make({ ball: C.FRIEND_BALL, rate, hp: 1, maxHp: 100 }));
|
||||
}
|
||||
// Fast and Moon Balls over every species (temp species, not wEnemyMonSpecies).
|
||||
for (const tempSpecies of all)
|
||||
for (const ball of [C.FAST_BALL, C.MOON_BALL])
|
||||
for (const rate of [3, 64])
|
||||
cases.push(make({ ball, tempSpecies, species: 19, rate, hp: 1, maxHp: 100 }));
|
||||
// Love Ball: same and different species, each gender pairing, genderless.
|
||||
for (const tempSpecies of all)
|
||||
for (const [playerDvs, enemyDvs] of [[0x0000, 0x0000], [0xffff, 0xffff], [0x0000, 0xffff], [0xffff, 0x0000], [0x7777, 0x8888]])
|
||||
cases.push(make({ ball: C.LOVE_BALL, tempSpecies, playerSpecies: tempSpecies, playerDvs, enemyDvs, rate: 30, hp: 1, maxHp: 100 }));
|
||||
for (const playerSpecies of [1, 29, 32, 81, 251])
|
||||
cases.push(make({ ball: C.LOVE_BALL, tempSpecies: 19, playerSpecies, rate: 30, hp: 1, maxHp: 100 }));
|
||||
await compare(page, "capture", cases);
|
||||
});
|
||||
Reference in new issue
Block a user