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:
kawaandClaude Opus 5.5 committed 2026-10-17 08:52:30 +02:00
1 parent 7d03f5c5ca
commit e49bc6de9a
49 files changed
+6028 -88

No files matched your search

+105 -1
View File
@@ -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);
});
},
);