Checks / web (push) Canceled after 0s
Field moves chosen in the party menu run their original checks and the ROM's from-menu scripts (Cut, Surf, Strength, Flash, Whirlpool, Waterfall, Sweet Scent), and Softboiled and Milk Drink share HP. SEL registers a key item that Select then uses, replacing the Key Items shortcut, and Bill's PC moves Pokémon between the party and every box. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
661 lines
22 KiB
TypeScript
661 lines
22 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { CrystalAssets } from "../src/game/rom/assets";
|
|
import { GameWorld } from "../src/game/world/world";
|
|
import {
|
|
calcStats,
|
|
generatePartyMon,
|
|
validPartyMon,
|
|
} from "../src/game/party/pokemon";
|
|
import {
|
|
MAX_PC_ITEMS,
|
|
emptyStorage,
|
|
receivePCItem,
|
|
toBoxMon,
|
|
validStorage,
|
|
} from "../src/game/party/storage";
|
|
import { removeFromSlot } from "../src/game/party/bag";
|
|
import { C } from "../src/game/battle/constants";
|
|
import { ENGINE_FLAGS } from "../src/game/rom/profile";
|
|
import { settle } from "./world-driver";
|
|
|
|
const romPath =
|
|
process.env.CRYSTAL_ROM_PATH ??
|
|
"Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc";
|
|
const POKEMON_CENTER = "26:5",
|
|
CYNDAQUIL = 155,
|
|
PIDGEY = 16,
|
|
POTION = 18,
|
|
MYSTERY_EGG = 69;
|
|
|
|
describe.skipIf(!existsSync(romPath))("the Pokémon Center PC", () => {
|
|
const assets = () => new CrystalAssets(new Uint8Array(readFileSync(romPath)));
|
|
/** In front of Cherrygrove's PC (COLL_PC at 9,1). */
|
|
function fixture() {
|
|
let seed = 5;
|
|
const world = new GameWorld(
|
|
assets(),
|
|
() => new Date("2026-09-25T12:00:00"),
|
|
() => {
|
|
seed = (seed * 1103515245 + 12345) >>> 0;
|
|
return (seed >>> 16) & 255;
|
|
},
|
|
);
|
|
const data = world.assets.pokemon;
|
|
const mon = (species: number, level: number) =>
|
|
generatePartyMon(data, {
|
|
species,
|
|
level,
|
|
item: 0,
|
|
otId: world.playerId,
|
|
ot: "CHRIS",
|
|
dvs: 0x9abc,
|
|
caughtData: 0,
|
|
});
|
|
world.state.party = [mon(CYNDAQUIL, 8), mon(PIDGEY, 4)];
|
|
world.state.bag.items = [{ item: POTION, quantity: 5 }];
|
|
world.restore({
|
|
...world.save(),
|
|
map: POKEMON_CENTER,
|
|
x: 9,
|
|
y: 2,
|
|
direction: "up",
|
|
objects: [],
|
|
});
|
|
return world;
|
|
}
|
|
/** Acknowledge pages and timed messages up to the next choice; returns
|
|
* the pages seen. */
|
|
function until(world: GameWorld) {
|
|
const pages: string[] = [];
|
|
for (let tick = 0; tick < 500; tick++) {
|
|
const dialogue = world.dialogue;
|
|
if (!dialogue) {
|
|
if (!world.busy) return pages;
|
|
world.tick();
|
|
continue;
|
|
}
|
|
const page = dialogue.pages[dialogue.index].replace(/\n/g, " ");
|
|
if (pages.at(-1) !== page) pages.push(page);
|
|
const last = dialogue.index === dialogue.pages.length - 1;
|
|
if (last && dialogue.choice) return pages;
|
|
if (last && dialogue.holdTicks !== undefined) world.tick();
|
|
else world.advanceDialogue();
|
|
}
|
|
throw new Error("No choice appeared.");
|
|
}
|
|
/** Choose the menu item matching `label` (the whole label, or a regex). */
|
|
function pick(world: GameWorld, label: string | RegExp) {
|
|
const choice = world.dialogue?.choice;
|
|
if (choice?.kind !== "menu")
|
|
throw new Error(`Expected a menu, got ${choice?.kind}`);
|
|
const index = choice.items.findIndex((item) =>
|
|
typeof label === "string" ? item === label : label.test(item),
|
|
);
|
|
expect(
|
|
index,
|
|
`${label} in ${choice.items.join(", ")}`,
|
|
).toBeGreaterThanOrEqual(0);
|
|
world.choose(index);
|
|
return until(world);
|
|
}
|
|
function menuItems(world: GameWorld) {
|
|
const choice = world.dialogue?.choice;
|
|
return choice?.kind === "menu" ? choice.items : [];
|
|
}
|
|
function open(world: GameWorld) {
|
|
world.interact();
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
return until(world);
|
|
}
|
|
/** Leave every menu with B until the script ends. */
|
|
function close(world: GameWorld) {
|
|
for (let i = 0; i < 20 && world.busy; i++) {
|
|
if (world.dialogue?.choice) world.advanceDialogue();
|
|
until(world);
|
|
}
|
|
settle(world);
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
}
|
|
|
|
it("lists Bill's PC and the player's PC, and Oak's PC after the Pokédex", () => {
|
|
const world = fixture();
|
|
const pages = open(world);
|
|
expect(pages).toEqual([
|
|
`${world.name} turned on the PC.`,
|
|
"Access whose PC?",
|
|
]);
|
|
expect(menuItems(world)).toEqual([
|
|
"BILL's PC",
|
|
`${world.name}'s PC`,
|
|
"TURN OFF",
|
|
]);
|
|
expect(pick(world, "TURN OFF")).toContain("… Link closed…");
|
|
settle(world);
|
|
world.state.flags.add(ENGINE_FLAGS.pokedex);
|
|
open(world);
|
|
expect(menuItems(world)).toEqual([
|
|
"BILL's PC",
|
|
`${world.name}'s PC`,
|
|
"PROF.OAK's PC",
|
|
"TURN OFF",
|
|
]);
|
|
});
|
|
|
|
it("refuses to start without a Pokémon", () => {
|
|
const world = fixture();
|
|
world.state.party = [];
|
|
world.interact();
|
|
expect(settle(world).join(" ").replace(/\n/g, " ")).toContain(
|
|
"Bzzzzt! You must have a POKéMON to use this!",
|
|
);
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
});
|
|
|
|
it("deposits, keeps the last Pokémon and withdraws with recalculated stats", () => {
|
|
const world = fixture();
|
|
const data = world.assets.pokemon;
|
|
const pidgey = world.state.party[1];
|
|
pidgey.hp = 1;
|
|
pidgey.status = 0x08;
|
|
pidgey.pp = [0, 0, 0, 0];
|
|
pidgey.statExp = [0xffff, 0xffff, 0xffff, 0xffff, 0xffff];
|
|
open(world);
|
|
expect(pick(world, "BILL's PC")).toEqual([
|
|
"BILL's PC accessed.",
|
|
"POKéMON Storage System opened.",
|
|
"What?",
|
|
]);
|
|
expect(menuItems(world)).toEqual([
|
|
"WITHDRAW POKéMON",
|
|
"DEPOSIT POKéMON",
|
|
"CHANGE BOX",
|
|
"MOVE POKéMON W/O MAIL",
|
|
"SEE YA!",
|
|
]);
|
|
pick(world, /^DEPOSIT/);
|
|
expect(world.dialogue?.pages[0]).toBe("PARTY POKéMON\nChoose a POKéMON.");
|
|
expect(menuItems(world)).toEqual(["CYNDAQUIL", "PIDGEY", "CANCEL"]);
|
|
const choice = world.dialogue?.choice;
|
|
expect(choice?.kind === "menu" && choice.details).toEqual([
|
|
":L8",
|
|
":L4",
|
|
"",
|
|
]);
|
|
pick(world, "PIDGEY");
|
|
expect(menuItems(world)).toEqual(["DEPOSIT", "STATS", "RELEASE", "CANCEL"]);
|
|
expect(pick(world, "DEPOSIT")).toContain("Stored PIDGEY!");
|
|
// A mid-PC save holds the committed game.
|
|
expect(world.save().party).toHaveLength(2);
|
|
expect(world.liveState.party).toHaveLength(1);
|
|
const [stored] = world.liveState.storage.boxes[0];
|
|
expect(stored).not.toHaveProperty("hp");
|
|
// RestorePPOfDepositedPokemon restores PP; status and HP are dropped.
|
|
expect(stored.pp).toEqual(
|
|
stored.moves.map((move) => (move ? data.move(move).pp : 0)),
|
|
);
|
|
// BillsPC_CheckMail_PreventBlackout keeps the last Pokémon.
|
|
expect(menuItems(world)).toEqual(["CYNDAQUIL", "CANCEL"]);
|
|
pick(world, "CYNDAQUIL");
|
|
expect(pick(world, "DEPOSIT")).toContain("It's your last POKéMON!");
|
|
expect(pick(world, "CANCEL")).toContain("What?");
|
|
pick(world, /^WITHDRAW/);
|
|
expect(world.dialogue?.pages[0]).toBe("BOX1\nChoose a POKéMON.");
|
|
pick(world, "PIDGEY");
|
|
expect(menuItems(world)).toEqual([
|
|
"WITHDRAW",
|
|
"STATS",
|
|
"RELEASE",
|
|
"CANCEL",
|
|
]);
|
|
expect(pick(world, "WITHDRAW")).toContain("Got PIDGEY!");
|
|
pick(world, "CANCEL");
|
|
close(world);
|
|
const back = world.state.party[1];
|
|
expect(world.state.storage.boxes[0]).toEqual([]);
|
|
// SendGetMonIntoFromBox: CalcMonStats with stat experience, full HP.
|
|
expect(back.status).toBe(0);
|
|
expect(back.hp).toBe(back.stats[0]);
|
|
expect(back.stats).toEqual(
|
|
calcStats(data, PIDGEY, pidgey.dvs, 4, pidgey.statExp),
|
|
);
|
|
expect(back.stats[0]).toBeGreaterThan(pidgey.stats[0]);
|
|
expect(validPartyMon(data, back)).toBe(true);
|
|
const saved = world.save();
|
|
world.restore(saved);
|
|
expect(world.save()).toEqual(saved);
|
|
});
|
|
|
|
it("moves Pokémon between the party and boxes without mail", () => {
|
|
const world = fixture();
|
|
const data = world.assets.pokemon;
|
|
const [cyndaquil, pidgey] = world.state.party;
|
|
pidgey.hp = 1;
|
|
pidgey.pp = [1, 0, 0, 0];
|
|
const boxed = toBoxMon(
|
|
generatePartyMon(data, {
|
|
species: 19, // RATTATA
|
|
level: 6,
|
|
item: 0,
|
|
otId: world.playerId,
|
|
ot: "CHRIS",
|
|
dvs: 0x1234,
|
|
caughtData: 0,
|
|
}),
|
|
);
|
|
world.state.storage.boxes[0] = [boxed];
|
|
open(world);
|
|
pick(world, "BILL's PC");
|
|
expect(pick(world, "MOVE POKéMON W/O MAIL")).toEqual([
|
|
"Each time you move a POKéMON, data will be saved. OK?",
|
|
]);
|
|
world.choose(1);
|
|
until(world);
|
|
// The party, then the fourteen boxes, with the current box highlighted.
|
|
const boxes = world.dialogue?.choice;
|
|
expect(boxes?.kind === "menu" && [boxes.items.length, boxes.selected]).toEqual([15, 1]);
|
|
expect(menuItems(world)[0]).toBe("PARTY POKéMON");
|
|
// PIDGEY from the party to the end of BOX1: its PP are restored.
|
|
pick(world, "PARTY POKéMON");
|
|
pick(world, /^PIDGEY/);
|
|
expect(menuItems(world)).toEqual(["MOVE", "STATS", "CANCEL"]);
|
|
pick(world, "MOVE");
|
|
pick(world, "BOX1");
|
|
expect(menuItems(world)).toEqual(["RATTATA", "▶"]);
|
|
pick(world, "▶");
|
|
// RATTATA from BOX1 to the front of the party, with full HP.
|
|
pick(world, "BOX1");
|
|
pick(world, /^RATTATA/);
|
|
pick(world, "MOVE");
|
|
pick(world, "PARTY POKéMON");
|
|
pick(world, /^CYNDAQUIL/);
|
|
expect(world.liveState.party.map((mon) => mon.species)).toEqual([19, 155]);
|
|
// Within the party: RATTATA to the end.
|
|
pick(world, "PARTY POKéMON");
|
|
pick(world, /^RATTATA/);
|
|
pick(world, "MOVE");
|
|
pick(world, "PARTY POKéMON");
|
|
pick(world, "▶");
|
|
close(world);
|
|
expect(world.state.party.map((mon) => mon.species)).toEqual([155, 19]);
|
|
const rattata = world.state.party[1];
|
|
expect(rattata.hp).toBe(rattata.stats[0]);
|
|
expect(rattata.stats).toEqual(calcStats(data, 19, 0x1234, 6, rattata.statExp));
|
|
expect(world.state.storage.boxes[0].map((mon) => mon.species)).toEqual([16]);
|
|
expect(world.state.storage.boxes[0][0].pp[0]).toBe(data.move(pidgey.moves[0]).pp);
|
|
expect(cyndaquil.species).toBe(155);
|
|
});
|
|
|
|
it("keeps the last Pokémon and refuses a full destination when moving", () => {
|
|
const world = fixture();
|
|
const data = world.assets.pokemon;
|
|
world.state.party = [world.state.party[0]];
|
|
const full = Array.from({ length: 20 }, () =>
|
|
toBoxMon(
|
|
generatePartyMon(data, {
|
|
species: 16,
|
|
level: 3,
|
|
item: 0,
|
|
otId: world.playerId,
|
|
ot: "CHRIS",
|
|
dvs: 0,
|
|
caughtData: 0,
|
|
}),
|
|
),
|
|
);
|
|
world.state.storage.boxes[1] = full;
|
|
world.state.storage.boxes[2] = [structuredClone(full[0])];
|
|
world.state.storage.boxes[2][0].nickname = "SPARE";
|
|
open(world);
|
|
pick(world, "BILL's PC");
|
|
pick(world, "MOVE POKéMON W/O MAIL");
|
|
world.choose(1);
|
|
until(world);
|
|
pick(world, "PARTY POKéMON");
|
|
expect(pick(world, /^CYNDAQUIL/)).toEqual(["What's up?"]);
|
|
expect(pick(world, "MOVE")).toContain("It's your last POKéMON!");
|
|
// BOX3 into the full BOX2: no room, and the position is chosen again.
|
|
pick(world, "BOX3");
|
|
pick(world, /^SPARE/);
|
|
pick(world, "MOVE");
|
|
pick(world, "BOX2");
|
|
expect(pick(world, "▶")).toContain("There's no room!");
|
|
// B leaves the destination list for the list of boxes to move from.
|
|
world.advanceDialogue();
|
|
until(world);
|
|
// Within the full BOX2 a Pokémon can still move.
|
|
pick(world, "BOX2");
|
|
pick(world, /^PIDGEY/);
|
|
pick(world, "MOVE");
|
|
pick(world, "BOX2");
|
|
expect(pick(world, "▶")).toContain("Saving… Leave ON!");
|
|
close(world);
|
|
expect(world.state.storage.boxes[1]).toHaveLength(20);
|
|
expect(world.state.storage.boxes[2].map((mon) => mon.nickname)).toEqual(["SPARE"]);
|
|
expect(world.state.party).toHaveLength(1);
|
|
});
|
|
|
|
it("opens the stats screen from Bill's PC for party and boxed Pokémon", () => {
|
|
const world = fixture();
|
|
const data = world.assets.pokemon;
|
|
const pidgey = world.state.party[1];
|
|
pidgey.hp = 1;
|
|
pidgey.status = 0x08;
|
|
open(world);
|
|
pick(world, "BILL's PC");
|
|
pick(world, /^DEPOSIT/);
|
|
pick(world, "PIDGEY");
|
|
const stats = () => {
|
|
const choice = world.dialogue?.choice;
|
|
if (choice?.kind !== "menu") throw new Error("Expected a menu");
|
|
world.choose(choice.items.indexOf("STATS"));
|
|
for (let tick = 0; tick < 10 && !world.statsScreen; tick++) world.tick();
|
|
const shown = world.statsScreen;
|
|
world.advanceDialogue();
|
|
until(world);
|
|
return shown;
|
|
};
|
|
// A party Pokémon shows its own HP and status.
|
|
expect(stats()).toMatchObject({ species: PIDGEY, hp: 1, status: 0x08 });
|
|
expect(world.statsScreen).toBeUndefined();
|
|
expect(menuItems(world)).toEqual(["DEPOSIT", "STATS", "RELEASE", "CANCEL"]);
|
|
pick(world, "DEPOSIT");
|
|
pick(world, "CANCEL");
|
|
pick(world, /^WITHDRAW/);
|
|
pick(world, "PIDGEY");
|
|
// CalcTempmonStats: a boxed Pokémon shows computed stats, full HP and
|
|
// no status.
|
|
const boxed = stats()!;
|
|
expect(boxed.status).toBe(0);
|
|
expect(boxed.hp).toBe(boxed.stats[0]);
|
|
expect(boxed.stats).toEqual(
|
|
calcStats(data, PIDGEY, pidgey.dvs, 4, pidgey.statExp),
|
|
);
|
|
expect(menuItems(world)).toEqual(["WITHDRAW", "STATS", "RELEASE", "CANCEL"]);
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
});
|
|
|
|
it("refuses to withdraw into a full party or deposit into a full box", () => {
|
|
const world = fixture();
|
|
const party = world.state.party;
|
|
world.state.party = Array.from({ length: 6 }, (_, i) =>
|
|
structuredClone(party[i % 2]),
|
|
);
|
|
world.state.storage.boxes[0] = [toBoxMon(party[1])];
|
|
open(world);
|
|
pick(world, "BILL's PC");
|
|
pick(world, /^WITHDRAW/);
|
|
pick(world, "PIDGEY");
|
|
const full = pick(world, "WITHDRAW");
|
|
expect(full).toContain("The party's full!");
|
|
expect(full.at(-1)).toBe("What's up?");
|
|
pick(world, "CANCEL");
|
|
pick(world, "CANCEL");
|
|
world.liveState.storage.boxes[0] = Array.from({ length: 20 }, () =>
|
|
toBoxMon(party[1]),
|
|
);
|
|
pick(world, /^DEPOSIT/);
|
|
pick(world, "PIDGEY");
|
|
expect(pick(world, "DEPOSIT")).toContain("The BOX is full.");
|
|
close(world);
|
|
expect(world.state.party).toHaveLength(6);
|
|
});
|
|
|
|
it("releases after confirmation and says goodbye by species", () => {
|
|
const world = fixture();
|
|
const nicknamed = toBoxMon(world.state.party[1]);
|
|
nicknamed.nickname = "BIRDY";
|
|
world.state.storage.boxes[0] = [nicknamed];
|
|
open(world);
|
|
pick(world, "BILL's PC");
|
|
pick(world, /^WITHDRAW/);
|
|
pick(world, "BIRDY");
|
|
expect(pick(world, "RELEASE")).toEqual(["Release POKéMON?"]);
|
|
world.choose(0);
|
|
expect(until(world)).toContain("What's up?");
|
|
pick(world, "RELEASE");
|
|
world.choose(1);
|
|
expect(until(world)).toEqual([
|
|
"Released POKéMON.",
|
|
"Bye, PIDGEY!",
|
|
"BOX1 Choose a POKéMON.",
|
|
]);
|
|
close(world);
|
|
expect(world.state.storage.boxes[0]).toEqual([]);
|
|
});
|
|
|
|
it("switches and names boxes", () => {
|
|
const world = fixture();
|
|
world.state.storage.boxes[1] = [toBoxMon(world.state.party[1])];
|
|
open(world);
|
|
pick(world, "BILL's PC");
|
|
pick(world, "CHANGE BOX");
|
|
expect(world.dialogue?.pages[0]).toBe("CURRENT\nBOX1\nChoose a BOX.");
|
|
expect(menuItems(world)).toHaveLength(14);
|
|
const choice = world.dialogue?.choice;
|
|
expect(choice?.kind === "menu" && choice.details?.slice(0, 2)).toEqual([
|
|
"0/20",
|
|
"1/20",
|
|
]);
|
|
pick(world, "BOX2");
|
|
expect(menuItems(world)).toEqual(["SWITCH", "NAME", "PRINT", "QUIT"]);
|
|
expect(pick(world, "SWITCH")).toEqual([
|
|
"When you change a POKéMON BOX, data will be saved. OK?",
|
|
]);
|
|
world.choose(1);
|
|
until(world);
|
|
pick(world, "BOX3");
|
|
const naming = pick(world, "NAME");
|
|
expect(naming).toEqual(["BOX NAME?"]);
|
|
world.choose("FAVS");
|
|
until(world);
|
|
pick(world, "FAVS");
|
|
pick(world, "NAME");
|
|
// InitString: a blank name keeps the box's name.
|
|
world.choose(" ");
|
|
until(world);
|
|
pick(world, "FAVS");
|
|
expect(pick(world, "PRINT")).toContain("There's no POKéMON.");
|
|
close(world);
|
|
expect(world.state.storage.current).toBe(1);
|
|
expect(world.state.storage.names[2]).toBe("FAVS");
|
|
open(world);
|
|
pick(world, "BILL's PC");
|
|
pick(world, /^WITHDRAW/);
|
|
expect(world.dialogue?.pages[0]).toBe("BOX2\nChoose a POKéMON.");
|
|
expect(menuItems(world)).toEqual(["PIDGEY", "CANCEL"]);
|
|
close(world);
|
|
});
|
|
|
|
it("stores, withdraws and tosses items in the player's PC", () => {
|
|
const world = fixture();
|
|
world.state.bag.keyItems = [MYSTERY_EGG];
|
|
open(world);
|
|
expect(pick(world, `${world.name}'s PC`)).toEqual([
|
|
"Accessed own PC.",
|
|
"Item Storage System opened.",
|
|
"What do you want to do?",
|
|
]);
|
|
expect(menuItems(world)).toEqual([
|
|
"WITHDRAW ITEM",
|
|
"DEPOSIT ITEM",
|
|
"TOSS ITEM",
|
|
"MAIL BOX",
|
|
"LOG OFF",
|
|
]);
|
|
world.choose(1);
|
|
let list = world.dialogue?.choice;
|
|
expect(
|
|
list?.kind === "martlist" && list.entries.map((e) => e.name),
|
|
).toEqual(["POTION", "MYSTERY EGG"]);
|
|
world.choose(0);
|
|
const quantity = world.dialogue?.choice;
|
|
expect(quantity?.kind === "martquantity" && quantity.prompt).toBe(
|
|
"How many do you\nwant to deposit?",
|
|
);
|
|
world.choose(3);
|
|
expect(until(world)).toContain("Deposited 3 POTION(S).");
|
|
// Key items cannot be tossed, so they move one at a time.
|
|
world.choose(1);
|
|
expect(until(world)).toContain("Deposited 1 MYSTERY EGG(S).");
|
|
world.choose(-1);
|
|
until(world);
|
|
expect(world.liveState.pcItems).toEqual([
|
|
{ item: POTION, quantity: 3 },
|
|
{ item: MYSTERY_EGG, quantity: 1 },
|
|
]);
|
|
expect(world.liveState.bag).toMatchObject({
|
|
items: [{ item: POTION, quantity: 2 }],
|
|
keyItems: [],
|
|
});
|
|
pick(world, "TOSS ITEM");
|
|
world.choose(1);
|
|
expect(until(world)).toContain("That's too impor- tant to toss out!");
|
|
world.choose(0);
|
|
world.choose(2);
|
|
expect(until(world)).toEqual(["Throw away 2 POTION(S)?"]);
|
|
world.choose(1);
|
|
expect(until(world)).toContain("Discarded POTION(S).");
|
|
world.choose(-1);
|
|
until(world);
|
|
pick(world, "WITHDRAW ITEM");
|
|
world.choose(1);
|
|
expect(until(world)).toContain("Withdrew 1 MYSTERY EGG(S).");
|
|
world.choose(0);
|
|
world.choose(1);
|
|
expect(until(world)).toContain("Withdrew 1 POTION(S).");
|
|
world.choose(-1);
|
|
until(world);
|
|
expect(pick(world, "MAIL BOX")).toContain("There's no MAIL here.");
|
|
close(world);
|
|
expect(world.state.pcItems).toEqual([]);
|
|
expect(world.state.bag).toMatchObject({
|
|
items: [{ item: POTION, quantity: 3 }],
|
|
keyItems: [MYSTERY_EGG],
|
|
});
|
|
});
|
|
|
|
it("rates the Pokédex on Oak's PC", () => {
|
|
const world = fixture();
|
|
world.state.flags.add(ENGINE_FLAGS.pokedex);
|
|
for (const species of [1, 2, 3, 4, 5])
|
|
world.state.pokedex.seen.add(species);
|
|
world.state.pokedex.caught.add(1);
|
|
open(world);
|
|
expect(pick(world, "PROF.OAK's PC")).toEqual([
|
|
"PROF.OAK's PC accessed.",
|
|
"POKéDEX Rating System opened.",
|
|
"Want to get your POKéDEX rated?",
|
|
]);
|
|
world.choose(1);
|
|
const pages = until(world).join(" ");
|
|
expect(pages).toContain("Current POKéDEX completion level:");
|
|
expect(pages).toContain("5 POKéMON seen 1 POKéMON owned");
|
|
expect(pages).toContain("Look for POKéMON in grassy areas!");
|
|
expect(pages).toContain("The link to PROF. OAK's PC closed.");
|
|
close(world);
|
|
});
|
|
|
|
it("validates and migrates stored Pokémon and items", () => {
|
|
const world = fixture();
|
|
const before = world.save();
|
|
const box = toBoxMon(world.state.party[1]);
|
|
for (const [storage, pcItems] of [
|
|
[{ ...before.storage, current: 14 }, before.pcItems],
|
|
[
|
|
{
|
|
...before.storage,
|
|
boxes: before.storage.boxes.map((_, i) =>
|
|
i ? [] : Array.from({ length: 21 }, () => box),
|
|
),
|
|
},
|
|
before.pcItems,
|
|
],
|
|
[
|
|
{
|
|
...before.storage,
|
|
boxes: before.storage.boxes.map((_, i) =>
|
|
i ? [] : [{ ...box, hp: 10 }],
|
|
),
|
|
},
|
|
before.pcItems,
|
|
],
|
|
[
|
|
{ ...before.storage, names: before.storage.names.map(() => " ") },
|
|
before.pcItems,
|
|
],
|
|
[before.storage, [{ item: C.POKE_BALL, quantity: 100 }]],
|
|
[
|
|
before.storage,
|
|
Array.from({ length: 51 }, () => ({ item: POTION, quantity: 1 })),
|
|
],
|
|
] as const) {
|
|
expect(() => world.restore({ ...before, storage, pcItems })).toThrow(
|
|
/Invalid native save/,
|
|
);
|
|
expect(world.save()).toEqual(before);
|
|
}
|
|
const old = world.save() as Partial<ReturnType<GameWorld["save"]>>;
|
|
delete old.storage;
|
|
delete old.pcItems;
|
|
world.restore(old);
|
|
expect(world.state.storage.boxes.every((box) => !box.length)).toBe(true);
|
|
expect(world.state.storage.names.every((name) => name === null)).toBe(true);
|
|
expect(world.state.pcItems).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("PC storage rules", () => {
|
|
it("removes from the selected stack, or else the first match", () => {
|
|
const list = [
|
|
{ item: 1, quantity: 99 },
|
|
{ item: 2, quantity: 5 },
|
|
{ item: 1, quantity: 3 },
|
|
];
|
|
expect(removeFromSlot(list, 2, 1, 3)).toBe(true);
|
|
expect(list).toEqual([
|
|
{ item: 1, quantity: 99 },
|
|
{ item: 2, quantity: 5 },
|
|
]);
|
|
expect(removeFromSlot(list, 1, 1, 9)).toBe(true);
|
|
expect(list[0]).toEqual({ item: 1, quantity: 90 });
|
|
expect(removeFromSlot(list, 1, 2, 6)).toBe(false);
|
|
expect(removeFromSlot(list, 0, 3, 1)).toBe(false);
|
|
});
|
|
|
|
it("holds fifty stacks, filling existing stacks first", () => {
|
|
const items = Array.from({ length: MAX_PC_ITEMS }, (_, i) => ({
|
|
item: i + 1,
|
|
quantity: 98,
|
|
}));
|
|
expect(receivePCItem(items, 7, 1)).toBe(true);
|
|
expect(items[6].quantity).toBe(99);
|
|
expect(receivePCItem(items, 7, 1)).toBe(false);
|
|
expect(receivePCItem(items, 99, 1)).toBe(false);
|
|
});
|
|
|
|
it("accepts only complete storage", () => {
|
|
const storage = emptyStorage();
|
|
expect(validStorage(storage)).toBe(true);
|
|
expect(validStorage({ ...storage, boxes: storage.boxes.slice(1) })).toBe(
|
|
false,
|
|
);
|
|
expect(validStorage({ ...storage, names: [...storage.names, null] })).toBe(
|
|
false,
|
|
);
|
|
expect(
|
|
validStorage({
|
|
...storage,
|
|
names: ["LONGNAME9", ...storage.names.slice(1)],
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
validStorage({
|
|
...storage,
|
|
names: ["GOOD ONE", ...storage.names.slice(1)],
|
|
}),
|
|
).toBe(true);
|
|
expect(validStorage({ ...storage, current: -1 })).toBe(false);
|
|
});
|
|
});
|