Port weather, copying moves, held items and obedience (#28)
Checks / web (push) Canceled after 0s
Checks / web (push) Canceled after 0s
Complete the weather, Substitute, Transform, called-move and forced-switch effects. Add held-item behavior, Exp. Share awards and badge-based traded Pokemon obedience, and persist trainer berry consumption. Validated with 576 passing unit tests and 149 browser tests, native/reference builds, reference/vendor/release checks and 320 Metal Powder emulator comparisons per browser. Smart AI and special balls/battle types remain on #28.
This commit is contained in:
1 parent
8f5b464efc
commit
32d736ecae
13 files changed
+1509
-75
No files matched your search
@@ -0,0 +1,82 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { existsSync } from "node:fs";
|
||||
import { C } from "../src/game/battle/constants";
|
||||
import { romPath, symbolsPath, loadSymbols, openReference } from "./reference-fixture";
|
||||
|
||||
test.skip(!existsSync(romPath) || !existsSync(symbolsPath), "Needs the private ROM and pinned reference rebuild.");
|
||||
|
||||
test("Metal Powder matches the original species check and byte overflow", async ({ page }) => {
|
||||
const { at, routine } = loadSymbols();
|
||||
const addresses = {
|
||||
command: routine("DittoMetalPowder"),
|
||||
romBank: at("hROMBank"),
|
||||
turn: at("hBattleTurn"),
|
||||
curBattleMon: at("wCurBattleMon"),
|
||||
partySpecies: at("wPartyMon1Species"),
|
||||
enemySpecies: at("wTempEnemyMonSpecies"),
|
||||
playerItem: at("wBattleMonItem"),
|
||||
enemyItem: at("wEnemyMonItem"),
|
||||
};
|
||||
await openReference(page);
|
||||
const differences = await page.evaluate(async ({ addresses, ditto, powder }) => {
|
||||
const { BinjgbEmulator } = await import("/src/emulator/binjgb.ts");
|
||||
const { CrystalAssets } = await import("/src/game/rom/assets.ts");
|
||||
const { Battle } = await import("/src/game/battle/battle.ts");
|
||||
const file = (document.querySelector("#reference-fixture") as HTMLInputElement).files![0];
|
||||
const rom = new Uint8Array(await file.arrayBuffer());
|
||||
const assets = new CrystalAssets(rom);
|
||||
const reference = new BinjgbEmulator();
|
||||
await reference.load(rom, document.createElement("canvas"));
|
||||
const write = (address: number, value: number) => reference.writeMemory(address, value & 255);
|
||||
const read = (address: number) => reference.readMemory(address);
|
||||
const failures: unknown[] = [];
|
||||
let compared = 0;
|
||||
write(0xff70, 1);
|
||||
write(addresses.curBattleMon, 0);
|
||||
for (const turn of [0, 1]) for (const species of [ditto, 25]) for (const item of [powder, 0])
|
||||
for (const attack of [1, 2, 101, 255]) for (const defense of [1, 2, 100, 169, 170, 171, 172, 200, 254, 255]) {
|
||||
const battle = new Battle({
|
||||
data: assets.pokemon, party: [{ species }] as never,
|
||||
pokedex: { seen: new Set(), caught: new Set() },
|
||||
playerId: 1, playerName: "CHRIS", timeOfDay: 1,
|
||||
text: () => ({}) as never, random: () => 0,
|
||||
});
|
||||
const internal = battle as unknown as {
|
||||
enemySpecies: number;
|
||||
dittoMetalPowder(side: unknown, attack: number, defense: number): number[];
|
||||
};
|
||||
internal.enemySpecies = species;
|
||||
const target = turn ? battle.player : battle.enemy;
|
||||
target.mon = { species: 25, item } as never; // transformed battle species
|
||||
const native = internal.dittoMetalPowder(target, attack, defense);
|
||||
write(addresses.turn, turn);
|
||||
write(addresses.partySpecies, species);
|
||||
write(addresses.enemySpecies, species);
|
||||
write(addresses.playerItem, item);
|
||||
write(addresses.enemyItem, item);
|
||||
const { bank, address } = addresses.command;
|
||||
// DI; SP; ROM bank; BC; call the unmodified routine; save B/C;
|
||||
// sentinel in audio RAM (no interrupts); loop.
|
||||
const program = [
|
||||
0xf3, 0x31, 0xfe, 0xc7,
|
||||
0x3e, bank, 0xe0, addresses.romBank & 255, 0xea, 0x00, 0x20,
|
||||
0x06, attack, 0x0e, defense, 0xcd, address & 255, address >> 8,
|
||||
0x78, 0xea, 0xe0, 0xc0, 0x79, 0xea, 0xe1, 0xc0,
|
||||
0x3e, 0x42, 0xea, 0xf0, 0xc0, 0x18, 0xfe,
|
||||
];
|
||||
write(0xc0f0, 0);
|
||||
program.forEach((byte, i) => write(0xc000 + i, byte));
|
||||
reference.setProgramCounter(0xc000);
|
||||
for (let frame = 0; frame < 60 && read(0xc0f0) !== 0x42; frame++) reference.advanceFrames(1);
|
||||
if (read(0xc0f0) !== 0x42) throw new Error("DittoMetalPowder did not return");
|
||||
const original = [read(0xc0e0), read(0xc0e1)];
|
||||
if (original.some((value, i) => value !== native[i]))
|
||||
failures.push({ turn, species, item, attack, defense, original, native });
|
||||
compared++;
|
||||
}
|
||||
reference.dispose();
|
||||
return { compared, failures };
|
||||
}, { addresses, ditto: C.DITTO, powder: C.METAL_POWDER });
|
||||
expect(differences.compared).toBe(320);
|
||||
expect(differences.failures).toEqual([]);
|
||||
});
|
||||
Reference in new issue
Block a user