Breed at the Day-Care, hatch Eggs, draw MAIL stationery and browse the Hall of Fame (Closes #29)
- Day-Care and breeding: deposits, withdrawal pricing and growth, compatibility, egg species, DVs and moves, DayCareStep, egg collection and the Odd Egg, compared with the cartridge's routines. - CountStep: walking happiness, DoEggStep, DayCareStep and DoBikeStep in the original's order; HatchEggs with the ROM's hatch animation, compared frame by frame with the original. - MAIL: all ten stationery types and the Portrait MAIL picture, compared with the cartridge's tilemaps, graphics and palettes. - Printing: Bill's PC boxes, Pokedex PRNT, MAIL and the Photo Studio take the original's absent-printer path. - Hall of Fame records (saved apart from the League flag, ready for the League to call) and the Hall of Fame PC, compared with the cartridge's _HallOfFamePC on 255 pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7d03f5c5ca
commit
e49bc6de9a
49 files changed
+6028
-88
No files matched your search
@@ -3,9 +3,12 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { CrystalAssets } from "../src/game/rom/assets";
|
||||
import { GameWorld } from "../src/game/world/world";
|
||||
import { generatePartyMon } from "../src/game/party/pokemon";
|
||||
import { addHallOfFameEntry } from "../src/game/party/hall-of-fame";
|
||||
import { ENGINE_FLAGS } from "../src/game/rom/profile";
|
||||
import { CRYSTAL_ROM } from "../src/rom/validate";
|
||||
import { encodeSave } from "../src/storage/saves";
|
||||
import { foundationFixture } from "./foundation-fixture";
|
||||
import { settle } from "./world-driver";
|
||||
|
||||
// Backups for the manual checks on the project board's "A tester" column,
|
||||
// built from the local ROM and imported through Savegames → Import backup
|
||||
@@ -28,7 +31,9 @@ describe.skipIf(!existsSync(romPath) || !process.env.MAKE_TEST_SAVES)(
|
||||
function write(name: string, world: GameWorld) {
|
||||
const state = new TextEncoder().encode(JSON.stringify(world.save()));
|
||||
// The backup must load as the app would load it.
|
||||
new GameWorld(assets()).restore(JSON.parse(new TextDecoder().decode(state)));
|
||||
new GameWorld(assets()).restore(
|
||||
JSON.parse(new TextDecoder().decode(state)),
|
||||
);
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
writeFileSync(
|
||||
`${OUT}/${name}.crystal-save.json`,
|
||||
@@ -97,5 +102,104 @@ describe.skipIf(!existsSync(romPath) || !process.env.MAKE_TEST_SAVES)(
|
||||
expect(world.mapId).toBe("24:3");
|
||||
write("headbutt", world);
|
||||
});
|
||||
|
||||
/** A Pokémon of the player's, from (species, level, DVs). */
|
||||
const caught = (
|
||||
a: CrystalAssets,
|
||||
world: GameWorld,
|
||||
species: number,
|
||||
level: number,
|
||||
dvs = 0x9abc,
|
||||
) =>
|
||||
generatePartyMon(a.pokemon, {
|
||||
species,
|
||||
level,
|
||||
item: 0,
|
||||
otId: world.playerId,
|
||||
ot: "CHRIS",
|
||||
dvs,
|
||||
caughtData: 0,
|
||||
});
|
||||
|
||||
it("writes day-care: inside the Day-Care with a pair that breeds", () => {
|
||||
const a = assets();
|
||||
const world = new GameWorld(a, () => new Date("2026-09-22T12:00:00"));
|
||||
// A female Cyndaquil and a male Typhlosion (Attack and Speed DVs 0
|
||||
// and 15), and a Totodile to keep. The Odd Egg is still to come.
|
||||
world.state.party = [
|
||||
caught(a, world, 155, 20, 0x0201), // CYNDAQUIL
|
||||
caught(a, world, 157, 40, 0xf7f2), // TYPHLOSION
|
||||
caught(a, world, 158, 15), // TOTODILE
|
||||
];
|
||||
world.state.money = 50000;
|
||||
world.warpTo("11:24", 2, 4); // facing the man
|
||||
settle(world);
|
||||
world.restore({ ...world.save(), direction: "up" });
|
||||
expect(world.mapId).toBe("11:24");
|
||||
write("day-care", world);
|
||||
});
|
||||
|
||||
it("writes egg-hatching: Route 29 with two Eggs two steps from hatching", () => {
|
||||
const a = assets();
|
||||
const world = new GameWorld(a, () => new Date("2026-09-22T12:00:00"));
|
||||
const egg = (species: number, dvs: number, happiness: number) => ({
|
||||
...caught(a, world, species, 5, dvs),
|
||||
egg: true as const,
|
||||
nickname: "EGG",
|
||||
happiness,
|
||||
hp: 0,
|
||||
});
|
||||
// The Togepi Egg reaches zero on step 128 and stops DoEggStep; the
|
||||
// shiny Pichu's is already zero, so both hatch.
|
||||
world.state.party = [
|
||||
caught(a, world, 155, 20),
|
||||
egg(175, 0x8888, 1), // TOGEPI
|
||||
egg(172, 0x2aaa, 0), // PICHU
|
||||
];
|
||||
world.restore({
|
||||
...world.save(),
|
||||
map: "24:3",
|
||||
x: 12,
|
||||
y: 3,
|
||||
direction: "left",
|
||||
stepCount: 0x7e,
|
||||
});
|
||||
settle(world);
|
||||
write("egg-hatching", world);
|
||||
});
|
||||
|
||||
it("writes hall-of-fame: a Pokémon Center whose PC has three teams", () => {
|
||||
const a = assets();
|
||||
const world = new GameWorld(a, () => new Date("2026-09-22T12:00:00"));
|
||||
world.state.flags.add(ENGINE_FLAGS.pokedex);
|
||||
const record = (party: [number, number, number][]) =>
|
||||
addHallOfFameEntry(
|
||||
world.state,
|
||||
party.map(([species, level, dvs]) =>
|
||||
caught(a, world, species, level, dvs),
|
||||
),
|
||||
);
|
||||
record([[201, 50, 0xffff]]); // UNOWN
|
||||
record([
|
||||
[157, 55, 0x9abc], // TYPHLOSION
|
||||
[29, 30, 0x0000], // NIDORAN♀
|
||||
[32, 31, 0xffff], // NIDORAN♂
|
||||
[25, 100, 0x2aaa], // PIKACHU, shiny
|
||||
[175, 12, 0x9abc], // TOGEPI
|
||||
[249, 70, 0x9abc], // LUGIA
|
||||
]);
|
||||
record([[158, 42, 0x9abc]]); // TOTODILE
|
||||
world.state.party = [caught(a, world, 155, 20)];
|
||||
world.restore({
|
||||
...world.save(),
|
||||
map: "26:5",
|
||||
x: 9,
|
||||
y: 2,
|
||||
direction: "up",
|
||||
objects: [],
|
||||
});
|
||||
settle(world);
|
||||
write("hall-of-fame", world);
|
||||
});
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user