Port weather, copying moves, held items and obedience (#28)
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:
Sébastien QUEROL committed 2026-10-03 15:03:19 +02:00
1 parent 8f5b464efc
commit 32d736ecae
13 files changed
+1509 -75

No files matched your search

+82
View File
@@ -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([]);
});