Port the Pokégear, phone calls and Mom's bank
Checks / web (push) Canceled after 0s

POKéGEAR in the start menu runs the clock, Town Map and phone cards with
the original joypad rules. Calls to Elm and Mom run their callee scripts,
with the out-of-area and out-of-hours cases and HangUp's corrected timing.
Mom saves a quarter of trainer prizes once asked, talking to her at home
opens BankOfMom (GET, SAVE, CHANGE and the DST checks) with its digit
amount box, and MomTriesToBuySomething buys items and dolls with her
savings. Saves gain Mom's money, her next item and its trigger balance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sébastien QUEROLandClaude Opus 5.5 committed 2026-10-01 11:38:22 +02:00
1 parent 3456aa4e11
commit 209778cabe
29 files changed
+2726 -47

No files matched your search

+138 -1
View File
@@ -2,8 +2,10 @@ import { describe, expect, it } from "vitest";
import { existsSync, readFileSync } from "node:fs";
import { CrystalAssets } from "../src/game/rom/assets";
import { C } from "../src/game/battle/constants";
import { OW } from "../src/game/world/constants";
import { trainerFixtureWorld } from "./trainer-fixture";
import type { GameWorld } from "../src/game/world/world";
import { migrateSave, type GameWorld } from "../src/game/world/world";
import { DecorationData } from "../src/game/world/decorations";
const romPath = process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc";
@@ -65,6 +67,141 @@ describe.skipIf(!existsSync(romPath))("scripted trainer world transactions", ()
expect(world.state.money).toBe(C.MAX_MONEY);
});
it("sends some of the reward to Mom", () => {
const world = fixture();
world.state.flags.add(OW.ENGINE_MOM_SAVING_MONEY);
world.state.momsMoney = 100;
const before = world.save();
world.interact();
const messages = finish(world);
expect(world.notice, world.diagnostic).toBe("");
expect(messages).toContain("Sent some to MOM!");
expect(world.state.money).toBe(before.money + 48);
expect(world.state.momsMoney).toBe(116);
expect(world.state.momItemTriggerBalance).toBe(OW.MOM_MONEY);
});
describe("Mom's purchases (MomTriesToBuySomething)", () => {
const SUPER_POTION = 0x11,
MOM_ITEMS_1 = [
[SUPER_POTION, 600],
[0x09, 90], // ANTIDOTE
[0x05, 180], // POKE_BALL
[0x13, 450], // ESCAPE_ROPE
[0x04, 500], // GREAT_BALL
];
/** Beat Joey while Mom saves a quarter (¥16) of his ¥64; returns the
* pages of the call that follows, if any. */
function win(world: GameWorld, momsMoney: number) {
world.state.flags.add(OW.ENGINE_MOM_SAVING_MONEY);
world.state.momsMoney = momsMoney - 16;
world.interact();
const pages: string[] = [];
// The queued call starts at the next PlayerEvents.
let idle = 0;
for (let tick = 0; tick < 10000; tick++) {
const view = world.battle;
if (view) {
if (view.prompt?.kind === "action") world.battleAction({ action: "fight", move: 0 });
else if (view.prompt?.kind === "stats" || view.message?.prompt) world.interact();
else world.tick();
continue;
}
const dialogue = world.dialogue;
if (dialogue) {
const page = dialogue.pages[dialogue.index].replace(/\n/g, " ");
if (pages.at(-1) !== page) pages.push(page);
if (dialogue.holdTicks !== undefined) world.tick();
else world.advanceDialogue();
} else if (world.busy || idle++ < 2) world.tick();
else {
expect(world.notice, world.diagnostic).toBe("");
return pages;
}
}
throw new Error(`Trainer battle did not settle: ${world.diagnostic}`);
}
it("buys MomItems_2's next item once her savings reach its trigger", () => {
const world = fixture();
const pcItems = world.state.pcItems.map((slot) => ({ ...slot }));
const pages = win(world, 900);
expect(pages).toEqual([
"Hi, CHRIS! How are you?",
"I found a useful item shopping, so",
"I bought it with your money. Sorry!",
"It's in your PC. You'll like it!",
"Click!",
"……",
]);
expect(world.state.momsMoney).toBe(300);
expect(world.state.momItem).toBe(1);
expect(world.state.pcItems).toEqual([...pcItems, { item: SUPER_POTION, quantity: 1 }]);
// MomItems_2's next trigger is ¥4000.
expect(win(world, 3999)).toEqual([]);
expect(world.state.momItem).toBe(1);
const saved = world.save();
expect([saved.momItem, saved.momItemTriggerBalance]).toEqual([1, 4600]);
});
it("buys a random MomItems_1 item when her savings land on a ¥2300 step", () => {
const world = fixture();
world.state.momItem = 1;
const pages = win(world, 2300);
expect(pages[1]).toBe("I found a useful item shopping, so");
const bought = world.state.pcItems.at(-1)!;
const entry = MOM_ITEMS_1.find(([item]) => item === bought.item);
expect(entry).toBeDefined();
expect(world.state.momsMoney).toBe(2300 - entry![1]);
expect(world.state.momItem).toBe(1);
expect(world.state.momItemTriggerBalance).toBe(4600);
});
it("raises the ¥2300 step past her savings without buying anything", () => {
const world = fixture();
world.state.momItem = 1;
expect(win(world, 2400)).toEqual([]);
expect(world.state.momsMoney).toBe(2400);
expect(world.state.momItemTriggerBalance).toBe(4600);
});
it("sends dolls to the player's room", () => {
const world = fixture();
world.state.momItem = 3; // ¥10000: the Charmander doll for ¥1800.
const pages = win(world, 10000);
expect(pages.slice(0, 4)).toEqual([
"Hi, CHRIS! How are you?",
"While shopping today, I saw this adorable doll, so",
"I bought it with your money. Sorry!",
"It's in your room. You'll love it!",
]);
expect(world.state.momsMoney).toBe(8200);
expect(world.state.momItem).toBe(4);
const doll = new DecorationData(world.assets.reader, world.assets.pokemon);
const charmander = Array.from({ length: 52 }, (_, i) => i + 1).find(
(id) => doll.name(id) === "CHARMANDER DOLL",
)!;
expect(world.state.events.has(doll.attribute(charmander).event)).toBe(true);
});
it("buys nothing while the PC is full", () => {
const world = fixture();
world.state.pcItems = Array.from({ length: 50 }, (_, i) => ({ item: 0x20 + i, quantity: 1 }));
expect(win(world, 900)).toEqual([]);
expect(world.state.momsMoney).toBe(900);
expect(world.state.momItem).toBe(0);
});
it("migrates saves made before her purchases", () => {
const world = fixture();
const { momItem: _, momItemTriggerBalance: __, ...old } = world.save();
const migrated = migrateSave(old, () => 0)!;
expect([migrated.momItem, migrated.momItemTriggerBalance]).toEqual([0, OW.MOM_MONEY]);
world.restore(migrated);
expect(() => world.restore({ ...migrated, momItem: 11 })).toThrow();
});
});
it("rolls back reward, experience and sightings if the following script is unsupported", () => {
const world = fixture([0xff]);
const before = world.save();