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

+113
View File
@@ -0,0 +1,113 @@
import type { HallOfFameTeam } from "../party/hall-of-fame";
import { PokemonData } from "../rom/pokemon";
import { JOHTO } from "../rom/profile";
import { fontTiles, FRAME_TILE, SPACE_TILE, textTiles } from "../rom/font";
import type { ScriptEffect } from "./interpreter";
import { blankScreen, SCREEN_BUTTONS, type TileScreen } from "./screen";
import { defaultTextContext, readString } from "./text";
import type { GameSound } from "../audio/sound";
/** _HallOfFamePC.DisplayMonAndStrings: DisplayHOFMon's page for one
* Pokémon of a team, over _LoadFontsBattleExtra's tiles, in
* SCGB_PLAYER_OR_MON_FRONTPIC_PALS's single palette. The picture is shown
* at rest: HOF_AnimateFrontpic's ANIM_MON_HOF animation is not ported. */
export function hallOfFameScreen(
data: PokemonData,
team: HallOfFameTeam,
slot: number,
frame: number,
): TileScreen {
const mon = team.mons[slot],
r = data.reader;
const tiles = fontTiles(r, frame);
tiles.set(r.slice(JOHTO.FontBattleExtra, 25 * 16), 0x60 * 16);
tiles.set(data.frontPic(mon.species, PokemonData.unownLetter(mon.dvs)));
const title = readString(
r,
JOHTO["_HallOfFamePC.TimeFamer"],
defaultTextContext(""),
);
const screen = blankScreen(
tiles,
`hof-${mon.species}-${mon.dvs}-${frame}`,
[data.palette(mon.species, PokemonData.shiny(mon.dvs))],
`${team.win}${title.trimStart()}: ${mon.nickname}, ${data.speciesName(mon.species)}, level ${mon.level}, ID ${String(mon.otId).padStart(5, "0")}`,
);
screen.map.fill(SPACE_TILE);
const put = (x: number, y: number, values: readonly number[]) =>
screen.map.splice(y * 20 + x, values.length, ...values);
const text = (x: number, y: number, value: string) =>
put(
x,
y,
[...value].flatMap((character) =>
character === '"' ? [0xe1] : textTiles(character),
),
);
// Textbox with 18 interior columns and three/four interior rows.
const box = (y: number, height: number) => {
put(0, y, [FRAME_TILE, ...Array(18).fill(FRAME_TILE + 1), FRAME_TILE + 2]);
for (let row = y + 1; row <= y + height; row++) {
put(0, row, [FRAME_TILE + 3]);
put(19, row, [FRAME_TILE + 3]);
}
put(0, y + height + 1, [
FRAME_TILE + 4,
...Array(18).fill(FRAME_TILE + 1),
FRAME_TILE + 5,
]);
};
box(0, 3);
box(12, 4);
// "HOF Master!" needs a count of HOF_MASTER_COUNT + 1, which the capped
// count never reaches: the original's bug (docs/bugs_and_glitches.md)
// leaves every team "-Time Famer".
text(1, 2, title);
text(2, 2, String(team.win).padStart(3, " "));
for (let x = 0; x < 7; x++)
for (let y = 0; y < 7; y++) put(x + 6, y + 5, [x * 7 + y]);
put(1, 13, [0x74, 0xf2]); // № and <DOT>.
text(3, 13, String(mon.species).padStart(3, "0"));
// GetBasePokemonName drops the Nidorans' gender sign.
text(7, 13, data.speciesName(mon.species).split(/[♂♀]/)[0]);
const gender = data.gender(mon.species, mon.dvs);
text(18, 13, gender === "male" ? "♂" : gender === "female" ? "♀" : " ");
text(8, 14, `/${mon.nickname}`);
// PrintLevel: <LV> and two digits, or three digits over the <LV>.
if (mon.level < 100) {
put(1, 16, [0x6e]);
text(2, 16, String(mon.level));
} else text(1, 16, String(mon.level));
put(7, 16, [0x73, 0x74, 0xf3]); // <ID>, № and /.
text(10, 16, String(mon.otId).padStart(5, "0"));
return screen;
}
/** _HallOfFamePC: LoadHOFTeam takes the teams newest first until an absent
* one (or NUM_HOF_TEAMS); A shows the team's next Pokémon, and after the
* last one the next team, Start skips to the next team and B leaves. Each
* page cries as ANIM_MON_HOF's CryNoWait does. */
export function* hallOfFamePC(
data: PokemonData,
teams: readonly HallOfFameTeam[],
frame: number,
sound?: GameSound,
): Generator<ScriptEffect, void, number | string | undefined> {
for (const team of teams) {
for (let slot = 0; slot < team.mons.length; slot++) {
sound?.playMonCry2(team.mons[slot].species);
const screen = hallOfFameScreen(data, team, slot, frame);
for (;;) {
const button = yield { kind: "screen", screen };
if (typeof button !== "string" || !SCREEN_BUTTONS.includes(button))
throw new Error("Invalid Hall of Fame button.");
if (button === "b") return;
if (button === "start") {
slot = team.mons.length;
break;
}
if (button === "a") break;
}
}
}
}