import { describe, expect, it } from "vitest"; import { existsSync, readFileSync } from "node:fs"; import { CrystalAssets, MON_SPRITE_ID } from "../src/game/rom/assets"; import { GameWorld } from "../src/game/world/world"; import { calcStats, generatePartyMon, type PartyMon, } from "../src/game/party/pokemon"; import { toBoxMon, type BoxMon } from "../src/game/party/storage"; import { breedingCompatibility, dayCareStep, emptyDayCare, initBreeding, oddEgg, } from "../src/game/party/day-care"; import { C } from "../src/game/battle/constants"; import { settle } from "./world-driver"; const romPath = process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc"; describe.skipIf(!existsSync(romPath))( "The Day-Care, breeding and Eggs with the local ROM", () => { const assets = existsSync(romPath) ? new CrystalAssets(new Uint8Array(readFileSync(romPath))) : undefined; const data = () => assets!.pokemon; const DAY_CARE = "11:24", ROUTE_34 = "11:1"; const PICHU = 172, PIKACHU = 25, CYNDAQUIL = 155, QUILAVA = 156, TYPHLOSION = 157, TOTODILE = 158, NIDORAN_F = 29, NIDORAN_M = 32, NIDORINO = 33, DITTO = 132, MAGNEMITE = 81, MAGIKARP = 129, TYROGUE = 236, EGG_SPECIES = 0xfd; // Engine flags (constants/engine_flags.asm) and events (event_flags.asm). const HAS_EGG = 5, MAN_HAS_MON = 6, LADY_HAS_MON = 7; const EVENT_GOT_ODD_EGG = 0x33e, EVENT_DAY_CARE_MAN_IN_DAY_CARE = 0x6e5, EVENT_DAY_CARE_MAN_ON_ROUTE_34 = 0x6e6, EVENT_DAY_CARE_MON_1 = 0x6e7, EVENT_DAY_CARE_MON_2 = 0x6e8; const EGG_TICKET = 0x81; /** attack, defense, speed, special DVs as a 16-bit value. */ const dvs = (atk: number, def: number, spd: number, spc: number) => (atk << 12) | (def << 8) | (spd << 4) | spc; /** Attack and Speed DVs of 15 make a male of any species with a ratio * below 255; zeros make a female (one that is not male-only). */ const MALE = (def = 0, spc = 0) => dvs(15, def, 15, spc); const FEMALE = (def = 0, spc = 0) => dvs(0, def, 0, spc); const moveId = (name: string) => { for (let move = 1; move <= 251; move++) if (data().moveName(move) === name) return move; throw new Error(`No move ${name}`); }; /** A world whose clock `now` moves, and whose random bytes come from a * fixed generator. */ const world = (date = "2026-09-22T12:00:00") => { let seed = 13; const clock = { now: new Date(date) }; const game = new GameWorld( assets!, () => clock.now, () => { seed = (seed * 1103515245 + 12345) >>> 0; return (seed >>> 16) & 255; }, ); return { game, clock }; }; const mon = ( game: GameWorld, species: number, level: number, extra: Partial = {}, ): PartyMon => Object.assign( generatePartyMon(data(), { species, level, item: 0, otId: game.playerId, ot: "KAWA", dvs: extra.dvs ?? 0x8888, caughtData: 0, }), extra, ); /** Enter `map`, stand at (x, y) facing `face`, press A and answer. */ const talk = ( game: GameWorld, map: string, x: number, y: number, face: "up" | "down" | "left" | "right", answers: number[] = [], ) => { if (game.mapId !== map) { game.warpTo(map, x, y); settle(game); } game.restore({ ...game.save(), x, y, direction: face }); game.interact(); const shown = settle(game, answers).join(" "); expect(game.notice, game.diagnostic).toBe(""); return shown; }; /** The Day-Care man stands at (2, 3) and his wife at (5, 3). */ const man = (game: GameWorld, answers: number[] = []) => talk(game, DAY_CARE, 2, 4, "up", answers); const lady = (game: GameWorld, answers: number[] = []) => talk(game, DAY_CARE, 5, 4, "up", answers); /** A fresh world whose Odd Egg has been given, with `party` (by default a * Cyndaquil and a Totodile). */ const started = (party?: (game: GameWorld) => PartyMon[]) => { const created = world(); created.game.state.events.add(EVENT_GOT_ODD_EGG); created.game.state.party = party ? party(created.game) : [mon(created.game, CYNDAQUIL, 20), mon(created.game, TOTODILE, 15)]; return created; }; /** A pair that breeds, and a third Pokémon to keep in the party. */ const breedingParty = (game: GameWorld) => [ mon(game, CYNDAQUIL, 20, { dvs: FEMALE(2, 1), otId: 11 }), mon(game, TYPHLOSION, 40, { dvs: MALE(7, 2), otId: 22 }), mon(game, TOTODILE, 15), ]; /** A byte stream for the Random routine: an error when it runs dry. */ const bytes = (...values: number[]) => { let index = 0; const next = () => { if (index >= values.length) throw new Error("Random ran out of bytes."); return values[index++]; }; return Object.assign(next, { used: () => index }); }; const boxed = ( game: GameWorld, species: number, extra: Partial = {}, ): BoxMon => toBoxMon(mon(game, species, 20, extra)); describe("breeding compatibility (CheckBreedmonCompatibility)", () => { const { game } = world(); /** Two stored Pokémon of different trainers unless said otherwise. */ const pair = ( a: Partial & { species: number }, b: Partial & { species: number }, ) => breedingCompatibility( data(), boxed(game, a.species, { otId: 1, ...a }), boxed(game, b.species, { otId: 2, ...b }), ); it("rates the same species by whether the OT IDs match", () => { const male = { species: CYNDAQUIL, dvs: MALE(3, 1) }, female = { species: CYNDAQUIL, dvs: FEMALE(8, 2) }; expect(pair({ ...male, otId: 1 }, { ...female, otId: 2 })).toBe(254); expect(pair({ ...male, otId: 7 }, { ...female, otId: 7 })).toBe( 254 - 77, ); }); it("rates different species of a shared egg group lower", () => { // Pikachu and Cyndaquil share the Field group. const a = { species: PIKACHU, dvs: MALE(3, 1) }, b = { species: CYNDAQUIL, dvs: FEMALE(8, 2) }; expect(pair({ ...a, otId: 1 }, { ...b, otId: 2 })).toBe(128); expect(pair({ ...a, otId: 5 }, { ...b, otId: 5 })).toBe(128 - 77); }); it("does not breed Pokémon of different egg groups, or in the No Eggs group", () => { expect( pair( { species: PIKACHU, dvs: MALE() }, { species: MAGIKARP, dvs: FEMALE(4) }, ), ).toBe(0); // Pichu is a baby, and Nidorina is in Undiscovered too. expect( pair( { species: PICHU, dvs: MALE() }, { species: PIKACHU, dvs: FEMALE(4) }, ), ).toBe(0); expect( pair( { species: PIKACHU, dvs: FEMALE(4) }, { species: TYROGUE, dvs: MALE() }, ), ).toBe(0); // A Ditto suits every group but that one. expect( pair( { species: DITTO, dvs: 0x1234 }, { species: PICHU, dvs: MALE() }, ), ).toBe(0); }); it("needs two genders, or a Ditto", () => { expect( pair( { species: CYNDAQUIL, dvs: MALE(3) }, { species: CYNDAQUIL, dvs: MALE(9) }, ), ).toBe(0); expect( pair( { species: CYNDAQUIL, dvs: FEMALE(3) }, { species: CYNDAQUIL, dvs: FEMALE(9) }, ), ).toBe(0); // Gender-unknown Pokémon only breed with Ditto. expect( pair( { species: MAGNEMITE, dvs: 0x1111 }, { species: MAGNEMITE, dvs: 0x2222 }, ), ).toBe(0); expect( pair( { species: MAGNEMITE, dvs: 0x1111 }, { species: DITTO, dvs: 0x2222, otId: 99 }, ), ).toBe(128); expect( pair( { species: DITTO, dvs: 0x2222 }, { species: MAGNEMITE, dvs: 0x1111, otId: 99 }, ), ).toBe(128); // A Ditto pair never breeds. expect( pair( { species: DITTO, dvs: 0x1111 }, { species: DITTO, dvs: 0x2222 }, ), ).toBe(0); // A Ditto with a gendered Pokémon is rated like any pair. expect( pair( { species: DITTO, dvs: 0x1111, otId: 3 }, { species: PIKACHU, dvs: MALE(2), otId: 3 }, ), ).toBe(128 - 77); expect( pair( { species: DITTO, dvs: 0x1111 }, { species: PIKACHU, dvs: FEMALE(2) }, ), ).toBe(128); }); it("avoids breeding Pokémon with equal Defense DVs and low Special DV bits (255)", () => { const a = { species: CYNDAQUIL, dvs: MALE(5, 3) }; // Special DVs 3 and 11 agree in their low 3 bits: the pair is avoided. expect(pair(a, { species: CYNDAQUIL, dvs: FEMALE(5, 11) })).toBe(255); // A different Defense DV, or a different low bit, is not. expect(pair(a, { species: CYNDAQUIL, dvs: FEMALE(6, 11) })).toBe(254); expect(pair(a, { species: CYNDAQUIL, dvs: FEMALE(5, 4) })).toBe(254); }); }); describe("making the Egg (DayCare_InitBreeding)", () => { const { game } = world(); const player = { id: 0x1234, name: "CHRIS" }; /** A Day-Care holding two Pokémon, ready for InitBreeding. */ const holding = (mon1: BoxMon, mon2: BoxMon) => { const host = { flags: new Set([MAN_HAS_MON, LADY_HAS_MON]), dayCare: emptyDayCare(), }; host.dayCare.mon1 = mon1; host.dayCare.mon2 = mon2; return host; }; it("rolls the steps to the egg check from 150 up, and hatches the mother's baby form", () => { // Pikachu (the mother) and a male Cyndaquil: Pichu cannot breed, but // its egg is Pichu, the pre-evolution of the mother. const host = holding( boxed(game, PIKACHU, { dvs: FEMALE(2, 1), otId: 1 }), boxed(game, CYNDAQUIL, { dvs: MALE(6, 4), otId: 2 }), ); const random = bytes(10, 149, 150 + 7, 0x12, 0x34); initBreeding(data(), host, player, random); const { dayCare } = host; // 10 and 149 are rejected: the byte must be at least 150. expect(dayCare.stepsToEgg).toBe(157); expect(dayCare.compatible).toBe(true); expect(random.used()).toBe(5); expect(dayCare.motherOrNonDitto).toBe(0); const egg = dayCare.egg!; expect(egg).toMatchObject({ species: PICHU, item: 0, otId: player.id, ot: "CHRIS", nickname: "EGG", level: 5, happiness: 10, pokerus: 0, caughtData: 0, statExp: [0, 0, 0, 0, 0], exp: data().expAtLevel(data().baseData(PICHU).growthRate, 5), egg: true, }); }); it("takes the mother from the second slot when the first is male", () => { const host = holding( boxed(game, CYNDAQUIL, { dvs: MALE(6, 4), otId: 2 }), boxed(game, PIKACHU, { dvs: FEMALE(2, 1), otId: 1 }), ); initBreeding(data(), host, player, bytes(200, 0, 0)); expect(host.dayCare.motherOrNonDitto).toBe(1); expect(host.dayCare.egg!.species).toBe(PICHU); }); it("lets a Ditto's partner be the mother whichever slot it is in", () => { for (const dittoFirst of [true, false]) { const ditto = boxed(game, DITTO, { dvs: 0x4321, otId: 9 }), pikachu = boxed(game, PIKACHU, { dvs: MALE(2, 1), otId: 1 }); const host = holding( ...((dittoFirst ? [ditto, pikachu] : [pikachu, ditto]) as [ BoxMon, BoxMon, ]), ); initBreeding(data(), host, player, bytes(150, 0xff, 0xff)); expect(host.dayCare.motherOrNonDitto).toBe(dittoFirst ? 1 : 0); expect(host.dayCare.egg!.species).toBe(PICHU); // The Egg's DVs come from the Ditto: its Defense DV (3) and the low // bits of its Special DV (1), with the random high bits. expect(host.dayCare.egg!.dvs).toBe(0xf3f9); } }); it("gives Nidoran♀ either gender of Nidoran", () => { const female = boxed(game, NIDORAN_F, { dvs: FEMALE(1, 1), otId: 1 }), male = boxed(game, NIDORINO, { dvs: MALE(8, 4), otId: 2 }); for (const [draw, species] of [ [0, NIDORAN_F], [127, NIDORAN_F], [128, NIDORAN_M], [255, NIDORAN_M], ] as const) { const host = holding(female, male); const random = bytes(160, draw, 0x40, 0x40); initBreeding(data(), host, player, random); expect(host.dayCare.egg!.species, `draw ${draw}`).toBe(species); expect(random.used()).toBe(4); } // Nidoran♂ is not given the draw: its Egg is always a Nidoran♂. const host = holding( boxed(game, DITTO, { dvs: 0x1111 }), boxed(game, NIDORAN_M, { dvs: MALE(2, 2) }), ); const random = bytes(170, 0x40, 0x40); initBreeding(data(), host, player, random); expect(host.dayCare.egg!.species).toBe(NIDORAN_M); expect(random.used()).toBe(3); }); it("gives a female Egg the father's Defense DV and a male Egg the mother's", () => { // Cyndaquil is 12.5% female: the Egg is male when the Attack and Speed // DV bytes make a value above 31. const mother = boxed(game, CYNDAQUIL, { dvs: FEMALE(0xa, 5), otId: 1 }), father = boxed(game, CYNDAQUIL, { dvs: MALE(3, 0xb), otId: 2 }); // A male Egg (0xa5, 0xc3: value 0xac) takes the mother's Defense DV // and low Special bits; a female (0x05, 0x13: value 1) the father's. for (const motherFirst of [true, false]) { const [first, second] = motherFirst ? [mother, father] : [father, mother]; const maleHost = holding(first, second); initBreeding(data(), maleHost, player, bytes(150, 0xa5, 0xc3)); expect( maleHost.dayCare.egg!.dvs, `male, mother first ${motherFirst}`, ).toBe((((0xa5 & 0xf0) | 0xa) << 8) | ((0xc3 & 0xf8) | (5 & 7))); const femaleHost = holding(first, second); initBreeding(data(), femaleHost, player, bytes(150, 0x05, 0x13)); expect( femaleHost.dayCare.egg!.dvs, `female, mother first ${motherFirst}`, ).toBe((((0x05 & 0xf0) | 3) << 8) | ((0x13 & 0xf8) | (0xb & 7))); } }); it("keeps the random DVs of a gender-unknown Egg", () => { const host = holding( boxed(game, MAGNEMITE, { dvs: 0x1111 }), boxed(game, DITTO, { dvs: 0x2222 }), ); // Ditto is the second Pokémon, so it is the DV source for every // gendered egg; a Magnemite egg has no gender and skips inheritance // only when neither parent is a Ditto: here Ditto still applies. initBreeding(data(), host, player, bytes(150, 0xab, 0xcd)); expect(host.dayCare.egg!.species).toBe(MAGNEMITE); expect(host.dayCare.egg!.dvs).toBe( (((0xab & 0xf0) | 0x2) << 8) | ((0xcd & 0xf8) | 2), ); }); it("does not breed incompatible or avoided pairs, or when one is missing", () => { const avoided = holding( boxed(game, CYNDAQUIL, { dvs: MALE(5, 3) }), boxed(game, CYNDAQUIL, { dvs: FEMALE(5, 11) }), ); initBreeding(data(), avoided, player, bytes()); expect(avoided.dayCare.compatible).toBe(false); expect(avoided.dayCare.egg).toBeNull(); const alone = holding( boxed(game, CYNDAQUIL, { dvs: MALE(5, 3) }), boxed(game, CYNDAQUIL, { dvs: FEMALE(2, 1) }), ); alone.flags.delete(LADY_HAS_MON); initBreeding(data(), alone, player, bytes()); expect(alone.dayCare.compatible).toBe(false); }); describe("moves", () => { /** Cyndaquil's egg moves include Quick Attack and Reversal. */ const QUICK_ATTACK = moveId("QUICK ATTACK"), REVERSAL = moveId("REVERSAL"), SMOKESCREEN = moveId("SMOKESCREEN"), SUNNY_DAY = moveId("SUNNY DAY"), THUNDERBOLT = moveId("THUNDERBOLT"), SCRATCH = moveId("SCRATCH"), TACKLE = moveId("TACKLE"), LEER = moveId("LEER"); const eggFor = ( mother: Partial & { species: number }, father: Partial & { species: number }, order: "motherFirst" | "fatherFirst" = "motherFirst", ) => { const m = boxed(game, mother.species, { dvs: FEMALE(1, 1), otId: 1, ...mother, }); const f = boxed(game, father.species, { dvs: MALE(8, 4), otId: 2, ...father, }); const host = holding( ...((order === "motherFirst" ? [m, f] : [f, m]) as [ BoxMon, BoxMon, ]), ); initBreeding(data(), host, player, bytes(150, 0x40, 0x40)); return host.dayCare.egg!; }; it("starts with the baby's level-up moves at level 5", () => { const egg = eggFor( { species: CYNDAQUIL }, { species: TYPHLOSION, moves: [0, 0, 0, 0] }, ); expect(egg.moves).toEqual([33, 43, 0, 0]); expect(egg.pp).toEqual([35, 30, 0, 0]); }); it("inherits the father's egg moves", () => { const egg = eggFor( { species: CYNDAQUIL }, { species: PIKACHU, moves: [QUICK_ATTACK, REVERSAL, THUNDERBOLT, 0], }, ); expect(egg.moves).toEqual([TACKLE, LEER, QUICK_ATTACK, REVERSAL]); expect(egg.pp).toEqual([ 35, 30, data().move(QUICK_ATTACK).pp, data().move(REVERSAL).pp, ]); // Thunderbolt is neither an egg move nor a TM Cyndaquil learns. expect(data().canLearnTMHM(CYNDAQUIL, THUNDERBOLT)).toBe(false); expect(egg.moves).not.toContain(THUNDERBOLT); }); it("uses the mother's moves only for the level-up moves both know", () => { // Smokescreen is a level-up move of Cyndaquil (at 6, after the Egg's // moves) and no TM, so the egg gets it only when the mother knows // it too. expect(data().canLearnTMHM(CYNDAQUIL, SMOKESCREEN)).toBe(false); const father = { species: TYPHLOSION, moves: [SMOKESCREEN, 0, 0, 0] as PartyMon["moves"], }; expect( eggFor( { species: CYNDAQUIL, moves: [SCRATCH, SMOKESCREEN, 0, 0] }, father, ).moves, ).toContain(SMOKESCREEN); expect( eggFor({ species: CYNDAQUIL, moves: [SCRATCH, 0, 0, 0] }, father) .moves, ).not.toContain(SMOKESCREEN); // The same holds with the Pokémon in either slot. expect( eggFor( { species: CYNDAQUIL, moves: [SCRATCH, SMOKESCREEN, 0, 0] }, father, "fatherFirst", ).moves, ).toContain(SMOKESCREEN); }); it("inherits the father's TM and HM moves the baby can learn", () => { expect(data().canLearnTMHM(CYNDAQUIL, SUNNY_DAY)).toBe(true); const egg = eggFor( { species: CYNDAQUIL }, { species: TYPHLOSION, moves: [SUNNY_DAY, THUNDERBOLT, 0, 0] }, ); expect(egg.moves).toContain(SUNNY_DAY); expect(egg.moves).not.toContain(THUNDERBOLT); }); it("shifts the baby's first move out when all four slots are full", () => { // Cyndaquil starts with two moves; a father with four egg moves // fills the other two, then pushes out the oldest of them. const egg = eggFor( { species: CYNDAQUIL }, { species: PIKACHU, moves: [ QUICK_ATTACK, REVERSAL, moveId("THRASH"), moveId("FORESIGHT"), ], }, ); // Tackle and Leer were shifted out for the two newest moves. expect(egg.moves).toEqual([ QUICK_ATTACK, REVERSAL, moveId("THRASH"), moveId("FORESIGHT"), ]); }); it("takes a Ditto's moves only when the Ditto is the father", () => { // Ditto knows Transform: it is no egg move, level-up move or TM. const TRANSFORM = moveId("TRANSFORM"); const ditto = (moves: PartyMon["moves"]) => boxed(game, DITTO, { dvs: 0x4321, moves }); const female = boxed(game, CYNDAQUIL, { dvs: FEMALE(2, 1), moves: [SCRATCH, QUICK_ATTACK, 0, 0], }); const male = boxed(game, CYNDAQUIL, { dvs: MALE(2, 1), moves: [QUICK_ATTACK, REVERSAL, 0, 0], }); // Female partner: Ditto is the father, so its moves are offered. const withFemale = holding( ditto([TRANSFORM, QUICK_ATTACK, 0, 0]), female, ); initBreeding(data(), withFemale, player, bytes(150, 0x40, 0x40)); expect(withFemale.dayCare.egg!.moves).toContain(QUICK_ATTACK); expect(withFemale.dayCare.egg!.moves).not.toContain(TRANSFORM); // Male partner: the male is the father; the Ditto's moves are not. const withMale = holding( male, ditto([TRANSFORM, moveId("THRASH"), 0, 0]), ); initBreeding(data(), withMale, player, bytes(150, 0x40, 0x40)); expect(withMale.dayCare.egg!.moves).toContain(REVERSAL); expect(withMale.dayCare.egg!.moves).not.toContain(moveId("THRASH")); }); }); }); describe("the step counter and the egg chance (DayCareStep)", () => { const { game } = world(); const holding = (compatibleMons = true) => { const host = { flags: new Set([MAN_HAS_MON, LADY_HAS_MON]), dayCare: emptyDayCare(), }; host.dayCare.mon1 = boxed(game, CYNDAQUIL, { dvs: MALE(2, 1), otId: 1, }); host.dayCare.mon2 = boxed(game, CYNDAQUIL, { dvs: FEMALE(7, 2), otId: 2, }); host.dayCare.compatible = compatibleMons; host.dayCare.stepsToEgg = 200; return host; }; it("adds an experience point per step to each Pokémon in", () => { const host = holding(false); const exp1 = host.dayCare.mon1!.exp; for (let step = 0; step < 300; step++) dayCareStep(data(), host, bytes()); expect(host.dayCare.mon1!.exp).toBe(exp1 + 300); expect(host.dayCare.mon2!.exp).toBe(exp1 + 300); // A Pokémon withdrawn (flag clear) or stored at level 100 gains none. host.flags.delete(LADY_HAS_MON); host.dayCare.mon2!.level = 100; const before = host.dayCare.mon2!.exp; dayCareStep(data(), host, bytes()); expect(host.dayCare.mon2!.exp).toBe(before); expect(host.dayCare.mon1!.exp).toBe(exp1 + 301); }); it("carries the experience between its bytes", () => { const host = holding(false); host.dayCare.mon1!.exp = 0x0100ff; dayCareStep(data(), host, bytes()); expect(host.dayCare.mon1!.exp).toBe(0x010100); host.dayCare.mon1!.exp = 0x00ffff; dayCareStep(data(), host, bytes()); expect(host.dayCare.mon1!.exp).toBe(0x010000); }); it("counts wStepsToEgg down only while compatible and checks the egg at zero", () => { const idle = holding(false); dayCareStep(data(), idle, bytes()); expect(idle.dayCare.stepsToEgg).toBe(200); const host = holding(); host.dayCare.stepsToEgg = 3; dayCareStep(data(), host, bytes()); dayCareStep(data(), host, bytes()); expect(host.dayCare.stepsToEgg).toBe(1); // At zero: a new step count, then the chance (compatibility 128 + ... // here two Cyndaquil with different OTs rate 254: below 31 percent + 1 // = 80 gives an egg, else nothing). const draws = bytes(33, 79); dayCareStep(data(), host, draws); expect(draws.used()).toBe(2); expect(host.dayCare.stepsToEgg).toBe(33); expect(host.dayCare.compatible).toBe(false); expect(host.flags.has(HAS_EGG)).toBe(true); }); it("uses 31, 16, 12 and 4 percent for the four bands of compatibility", () => { // Chance bytes: 31 percent + 1 = 80, 16 percent = 40, 12 percent = 30, // 4 percent = 10; the egg comes when the draw is below it. const rate = ( a: Partial, b: Partial, draw: number, ) => { const host = holding(); host.dayCare.mon1 = boxed(game, CYNDAQUIL, a); host.dayCare.mon2 = boxed( game, a.species === TOTODILE ? TOTODILE : CYNDAQUIL, b, ); host.dayCare.stepsToEgg = 1; dayCareStep(data(), host, bytes(50, draw)); return host.flags.has(HAS_EGG); }; const m = (extra: Partial) => ({ dvs: MALE(2, 1), ...extra }); const f = (extra: Partial) => ({ dvs: FEMALE(7, 2), ...extra, }); // Same species, different OTs: 254. expect([ rate(m({ otId: 1 }), f({ otId: 2 }), 79), rate(m({ otId: 1 }), f({ otId: 2 }), 80), ]).toEqual([true, false]); // Same species, same OT: 177 (the 16 percent band). expect([ rate(m({ otId: 1 }), f({ otId: 1 }), 39), rate(m({ otId: 1 }), f({ otId: 1 }), 40), ]).toEqual([true, false]); // Different species, different OTs: 128 (the 12 percent band). const different = (draw: number) => { const host = holding(); host.dayCare.mon1 = boxed(game, PIKACHU, { dvs: MALE(2, 1), otId: 1, }); host.dayCare.mon2 = boxed(game, CYNDAQUIL, { dvs: FEMALE(7, 2), otId: 2, }); host.dayCare.stepsToEgg = 1; dayCareStep(data(), host, bytes(50, draw)); return host.flags.has(HAS_EGG); }; expect([different(29), different(30)]).toEqual([true, false]); // Different species, same OT: 51 (the 4 percent band). const sameOwner = (draw: number) => { const host = holding(); host.dayCare.mon1 = boxed(game, PIKACHU, { dvs: MALE(2, 1), otId: 3, }); host.dayCare.mon2 = boxed(game, CYNDAQUIL, { dvs: FEMALE(7, 2), otId: 3, }); host.dayCare.stepsToEgg = 1; dayCareStep(data(), host, bytes(50, draw)); return host.flags.has(HAS_EGG); }; expect([sameOwner(9), sameOwner(10)]).toEqual([true, false]); }); }); describe("depositing a Pokémon", () => { it("asks, takes the chosen Pokémon and prepares nothing with one alone", () => { const { game } = started(); const shown = man(game, [1, 1]); expect(shown).toContain( "I'm the DAY-CARE\nMAN. Want me to\nraise a POKéMON?", ); expect(shown).toContain("What should I\nraise for you?"); expect(shown).toContain("OK. I'll raise\nyour TOTODILE."); expect(shown).toContain("Come back for it\nlater."); expect(game.state.party.map((m) => m.species)).toEqual([CYNDAQUIL]); const { dayCare } = game.state; expect(dayCare.mon1).toMatchObject({ species: TOTODILE, nickname: "TOTODILE", ot: "KAWA", level: 15, }); expect(dayCare.mon1).not.toHaveProperty("hp"); expect(dayCare.manActive).toBe(true); expect(game.state.flags.has(MAN_HAS_MON)).toBe(true); expect(dayCare.compatible).toBe(false); expect(dayCare.egg).toBeNull(); }); it("uses the longer introduction for the lady once, and keeps a deposited Pokémon's nickname", () => { const { game } = started((g) => [ mon(g, CYNDAQUIL, 20, { nickname: "FLAME" }), mon(g, TOTODILE, 15, { nickname: "SPLASH" }), ]); const first = lady(game, [0]); expect(first).toContain("about EGGS?"); expect(game.state.dayCare.ladyActive).toBe(true); expect(first).toContain("Come again."); expect(game.state.party).toHaveLength(2); const second = lady(game, [1, 0]); expect(second).not.toContain("EGGS"); expect(second).toContain("Should I raise a\nPOKéMON for you?"); expect(game.state.dayCare.mon2).toMatchObject({ nickname: "FLAME", species: CYNDAQUIL, }); expect(game.state.flags.has(LADY_HAS_MON)).toBe(true); }); it("says no to a lone Pokémon, a cancelled menu, an Egg, the last healthy one and MAIL", () => { const { game } = started((g) => [mon(g, CYNDAQUIL, 20)]); expect(man(game, [1])).toContain("Oh? But you have\njust one POKéMON."); expect(game.state.dayCare.mon1).toBeNull(); const party = (...mons: PartyMon[]) => (game.state.party = mons); party(mon(game, CYNDAQUIL, 20), mon(game, TOTODILE, 15)); // B in the party menu. expect(man(game, [1, -1])).toContain("Oh, fine then."); expect(man(game, [1, -1])).toContain("Come again."); // An Egg. party( mon(game, CYNDAQUIL, 20), mon(game, PICHU, 5, { egg: true, hp: 0, nickname: "EGG" }), ); expect(man(game, [1, 1])).toContain( "Sorry, but I can't\naccept an EGG.", ); // Only the chosen Pokémon can fight. party(mon(game, CYNDAQUIL, 20, { hp: 0 }), mon(game, TOTODILE, 15)); expect(man(game, [1, 1])).toContain( "If you give me\nthat, what will\nyou battle with?", ); // Fainted others do not count, nor do Eggs. party( mon(game, CYNDAQUIL, 20, { hp: 0 }), mon(game, TOTODILE, 15), mon(game, PICHU, 5, { egg: true, hp: 0 }), ); expect(man(game, [1, 1])).toContain("what will\nyou battle with?"); // MAIL. const mail = Array.from({ length: 190 }, (_, i) => i + 1).find((item) => data().itemIsMail(item), )!; const letter = { message: "HELLO", author: "KAWA", authorId: game.playerId, species: TOTODILE, type: mail, }; party( mon(game, CYNDAQUIL, 20), mon(game, TOTODILE, 15, { item: mail, mail: letter }), ); expect(man(game, [1, 1])).toContain( "Remove MAIL before\nyou come see me.", ); expect(game.state.dayCare.mon1).toBeNull(); expect(game.state.party).toHaveLength(2); // And a plain no to the introduction's question. expect(man(game, [0])).toContain("Come again."); expect(game.state.party).toHaveLength(2); }); it("prepares an Egg when the pair breeds and keeps it hidden until it is found", () => { const { game } = started(breedingParty); man(game, [1, 0]); lady(game, [1, 0]); const { dayCare } = game.state; expect(game.state.party.map((m) => m.species)).toEqual([TOTODILE]); expect(dayCare.mon1!.species).toBe(CYNDAQUIL); expect(dayCare.mon2!.species).toBe(TYPHLOSION); expect(dayCare.compatible).toBe(true); expect(dayCare.stepsToEgg).toBeGreaterThanOrEqual(150); expect(dayCare.egg!.species).toBe(CYNDAQUIL); expect(game.state.flags.has(HAS_EGG)).toBe(false); }); }); describe("the walking Day-Care (DayCareStep from the overworld)", () => { it("gives both Pokémon experience for each step and counts down the egg", () => { const { game } = started(breedingParty); man(game, [1, 0]); lady(game, [1, 0]); const before = structuredClone(game.state.dayCare); game.warpTo(DAY_CARE, 3, 5); settle(game); let walked = 0; for (const direction of [ "right", "right", "left", "left", "right", "left", ] as const) { expect(game.move(direction)).toBe(true); settle(game); walked++; } const after = game.state.dayCare; expect(after.mon1!.exp).toBe(before.mon1!.exp + walked); expect(after.mon2!.exp).toBe(before.mon2!.exp + walked); expect(after.stepsToEgg).toBe(before.stepsToEgg - walked); }); it("skips the Day-Care on the step that starts a hatch, as CountStep does", () => { const { game } = started(breedingParty); man(game, [1, 0]); lady(game, [1, 0]); game.warpTo(DAY_CARE, 3, 5); settle(game); game.state.party.push({ ...structuredClone(game.state.party[0]), egg: true, nickname: "EGG", happiness: 1, hp: 0, }); // Step 127 counts for the Day-Care; step 128 runs DoEggStep, whose // ready Egg jumps to the hatch before DayCareStep. game.state.stepCount = 0x7e; const before = structuredClone(game.state.dayCare); expect(game.move("right")).toBe(true); settle(game); expect(game.state.dayCare.mon1!.exp).toBe(before.mon1!.exp + 1); expect(game.state.dayCare.stepsToEgg).toBe(before.stepsToEgg - 1); const counted = structuredClone(game.state.dayCare); expect(game.move("left")).toBe(true); for (let i = 0; i < 100 && !game.dialogue && game.busy; i++) game.tick(); expect(game.dialogue).toBeDefined(); expect(game.state.stepCount).toBe(0x80); expect(game.state.dayCare).toEqual(counted); }); }); describe("taking a Pokémon back", () => { /** A Totodile (level 15) left with the man, then given the experience * of a level 21 Pokémon and `extra`. */ const left = (extra = 0, party?: (game: GameWorld) => PartyMon[]) => { const { game, clock } = started(party); man(game, [1, 1]); const growth = data().baseData(TOTODILE).growthRate; game.state.dayCare.mon1!.exp = data().expAtLevel(growth, 21) + extra; return { game, clock }; }; it("charges ¥100 and ¥100 a level, grows it and teaches the moves it learned", () => { const { game } = left(37); const money = game.state.money; const shown = man(game, [1, 1]); expect(shown).toContain( "Are we geniuses or\nwhat? Want to see\nyour TOTODILE?", ); expect(shown).toContain("has grown a lot."); expect(shown).toContain("it's\ngrown by 6."); expect(shown).toContain("cost ¥700."); expect(shown).toContain("Perfect! Here's\nyour POKéMON."); expect(shown).toContain("CHRIS got back\nTOTODILE."); expect(shown).toContain("Come again."); expect(game.state.money).toBe(money - 700); const back = game.state.party[1]; expect(back).toMatchObject({ species: TOTODILE, level: 21, status: 0, nickname: "TOTODILE", ot: "KAWA", }); // Bite at level 20 pushes Scratch out; the stored experience beyond // level 21 is lost ("Pokémon deposited in the Day-Care might lose // experience"). expect(back.moves).toEqual([ moveId("LEER"), moveId("RAGE"), moveId("WATER GUN"), moveId("BITE"), ]); expect(back.exp).toBe( data().expAtLevel(data().baseData(TOTODILE).growthRate, 21), ); expect(back.hp).toBe(back.stats[0]); expect(back.stats).toEqual( calcStats(data(), TOTODILE, 0x8888, 21, [0, 0, 0, 0, 0]), ); expect(back.pp).toEqual(back.moves.map((move) => data().move(move).pp)); expect(game.state.dayCare.mon1).toBeNull(); expect(game.state.flags.has(MAN_HAS_MON)).toBe(false); expect(game.state.dayCare.compatible).toBe(false); }); it("shifts the PP Ups with the moves, and gives a learned move none", () => { const { game } = left(); // Scratch, Leer (1 PP Up), Rage and Water Gun (2 PP Ups). game.state.dayCare.mon1!.pp = [35, (1 << 6) | 30, 20, (2 << 6) | 25]; man(game, [1, 1]); const back = game.state.party[1]; // Bite pushed Scratch out: FillMoves moves the PP bytes along (the // byte it shares with wEvolutionOldSpecies is TRUE), then RestoreAllPP // refills each from its move and its own PP Ups. expect(back.moves).toEqual([ moveId("LEER"), moveId("RAGE"), moveId("WATER GUN"), moveId("BITE"), ]); expect(back.pp).toEqual([ (1 << 6) | (30 + 6), 20, (2 << 6) | (25 + 2 * 5), data().move(moveId("BITE")).pp, ]); }); it("says it is too soon for ¥100 when no level was gained", () => { const { game } = started(); man(game, [1, 1]); const money = game.state.money; const shown = man(game, [1]); expect(shown).toContain("Huh? Back already?"); expect(shown).toContain("cost ¥100."); expect(shown).not.toContain("geniuses"); expect(game.state.money).toBe(money - 100); expect(game.state.party.map((m) => m.species)).toEqual([ CYNDAQUIL, TOTODILE, ]); // The experience was not reset to the level's minimum by an exact // level-up of zero levels? It is: the original sets it either way. expect(game.state.party[1].level).toBe(15); }); it("refuses politely, for want of money, or for want of room", () => { const { game } = left(); const money = game.state.money; expect(man(game, [0])).toContain("Oh, fine then."); expect(man(game, [1, 0])).toContain("Oh, fine then."); expect(game.state.dayCare.mon1).not.toBeNull(); game.state.money = 699; expect(man(game, [1, 1])).toContain("You don't have\nenough money."); expect(game.state.dayCare.mon1).not.toBeNull(); game.state.money = money; game.state.party = Array.from({ length: 6 }, () => mon(game, CYNDAQUIL, 20), ); expect(man(game, [1, 1])).toContain("You have no room\nfor it."); expect(game.state.dayCare.mon1).not.toBeNull(); expect(game.state.money).toBe(money); }); it("returns the lady's Pokémon and clears the man's Egg check", () => { const { game } = started(breedingParty); man(game, [1, 0]); lady(game, [1, 0]); expect(game.state.dayCare.compatible).toBe(true); const shown = lady(game, [1]); expect(shown).toContain("Huh? Back already?"); expect(game.state.dayCare.compatible).toBe(false); expect(game.state.dayCare.mon2).toBeNull(); expect(game.state.flags.has(LADY_HAS_MON)).toBe(false); expect(game.state.party.map((m) => m.species)).toContain(TYPHLOSION); // The man's Pokémon stays. expect(game.state.dayCare.mon1!.species).toBe(CYNDAQUIL); }); it("grows a Pokémon left to a level cap at 100 and bills for the levels", () => { const { game } = started(); man(game, [1, 1]); game.state.dayCare.mon1!.exp = data().expAtLevel( data().baseData(TOTODILE).growthRate, 100, ); game.state.money = C.MAX_MONEY; const shown = man(game, [1, 1]); expect(shown).toContain("it's\ngrown by 85."); expect(shown).toContain("cost ¥8600."); expect(game.state.party[1].level).toBe(100); }); }); describe("Eggs: the man outside on Route 34", () => { /** A Day-Care with a pair and an Egg waiting; the world is on Route 34. */ const waiting = (party?: (game: GameWorld) => PartyMon[]) => { const { game, clock } = started(party ?? breedingParty); man(game, [1, 0]); lady(game, [1, 0]); game.state.flags.add(HAS_EGG); game.state.dayCare.compatible = false; return { game, clock }; }; it("puts the man outside, and the lady refers to him", () => { const { game } = waiting(); expect(lady(game)).toContain("Gramps was looking\nfor you."); game.warpTo(ROUTE_34, 15, 18); settle(game); expect(game.state.events.has(EVENT_DAY_CARE_MAN_IN_DAY_CARE)).toBe( true, ); expect(game.state.events.has(EVENT_DAY_CARE_MAN_ON_ROUTE_34)).toBe( false, ); game.warpTo(DAY_CARE, 3, 5); settle(game); expect(game.objects().map((o) => o.id)).toEqual([3]); }); it("hands over the Egg, clears the flag, and prepares the next", () => { const { game } = waiting(); const egg = game.state.dayCare.egg!; game.state.party = [mon(game, TOTODILE, 15)]; const shown = talk(game, ROUTE_34, 15, 17, "up"); expect(shown).toContain("Your POKéMON had\nan EGG!"); expect(shown).toContain("CHRIS received\nthe EGG!"); expect(shown).toContain("Take good care of\nit."); const given = game.state.party[1]; expect(given).toMatchObject({ species: egg.species, egg: true, nickname: "EGG", ot: "CHRIS", otId: game.playerId, level: 5, hp: 0, status: 0, happiness: data().baseData(egg.species).eggCycles, dvs: egg.dvs, moves: egg.moves, }); expect(given.stats).toEqual( data() .baseData(egg.species) .stats.map((_, i) => given.stats[i]), ); expect(game.state.flags.has(HAS_EGG)).toBe(false); // DayCare_InitBreeding runs again for the pair still there. expect(game.state.dayCare.compatible).toBe(true); expect(game.state.dayCare.stepsToEgg).toBeGreaterThanOrEqual(150); // The man walked in; the Pokédex is not touched. expect(game.state.pokedex.seen.has(egg.species)).toBe(false); }); it("keeps the Egg waiting when the party is full", () => { const { game } = waiting(); game.state.party = Array.from({ length: 6 }, () => mon(game, TOTODILE, 15), ); const shown = talk(game, ROUTE_34, 15, 17, "up"); expect(shown).toContain( "You have no room\nin your party.\nCome back later.", ); expect(game.state.party).toHaveLength(6); expect(game.state.flags.has(HAS_EGG)).toBe(true); expect(game.state.dayCare.egg).not.toBeNull(); }); it("loses the Egg when the player says no (the script clears the flag)", () => { const { game } = waiting(); game.state.party = [mon(game, TOTODILE, 15)]; const shown = talk(game, ROUTE_34, 15, 17, "up", [0]); expect(shown).toContain("Well then, I'll\nkeep it. Thanks!"); expect(game.state.party).toHaveLength(1); expect(game.state.flags.has(HAS_EGG)).toBe(false); // InitBreeding did not run again: the pair stays unprepared. expect(game.state.dayCare.compatible).toBe(false); }); }); describe("the Pokémon in Route 34's fence (DayCareMon1, DayCareMon2)", () => { const fence = (game: GameWorld) => game.map.objects.filter( (object) => object.sprite === 0xe0 || object.sprite === 0xe1, ); it("shows only the Pokémon that are in the Day-Care, with their own sprites", () => { const { game } = started(breedingParty); game.warpTo(ROUTE_34, 10, 14); settle(game); expect(game.state.events.has(EVENT_DAY_CARE_MON_1)).toBe(true); expect(game.state.events.has(EVENT_DAY_CARE_MON_2)).toBe(true); expect(fence(game)).toHaveLength(2); expect(game.poses().some((pose) => pose.sprite >= MON_SPRITE_ID)).toBe( false, ); man(game, [1, 0]); lady(game, [1, 0]); game.warpTo(ROUTE_34, 10, 14); settle(game); expect(game.state.events.has(EVENT_DAY_CARE_MON_1)).toBe(false); expect(game.state.events.has(EVENT_DAY_CARE_MON_2)).toBe(false); expect( game .poses() .map((pose) => pose.sprite) .filter((sprite) => sprite >= MON_SPRITE_ID) .sort(), ).toEqual( [MON_SPRITE_ID + CYNDAQUIL, MON_SPRITE_ID + TYPHLOSION].sort(), ); }); /** Talk to a Pokémon in the fence from whichever side is open. */ const fenceTalk = (game: GameWorld, which: 1 | 2) => { game.warpTo(ROUTE_34, 10, 14); settle(game); const object = fence(game)[which - 1]; const at = game .objects() .find((candidate) => candidate.id === object.id)!; for (const [dx, dy, face] of [ [0, 1, "up"], [0, -1, "down"], [-1, 0, "right"], [1, 0, "left"], ] as const) { try { game.restore({ ...game.save(), x: at.x + dx, y: at.y + dy, direction: face, }); } catch { continue; } game.interact(); return settle(game).join(" "); } throw new Error("The fence Pokémon cannot be reached."); }; /** Both Pokémon in through the scripts, then replaced by `mon1` and * `mon2` (named FLARE and EMBER) to rate. */ const pairOf = ( mon1: Partial & { species: number }, mon2: Partial & { species: number }, ) => { const { game } = started(breedingParty); man(game, [1, 0]); lady(game, [1, 0]); const { dayCare } = game.state; dayCare.mon1 = boxed(game, mon1.species, { nickname: "FLARE", ...mon1, }); dayCare.mon2 = boxed(game, mon2.species, { nickname: "EMBER", ...mon2, }); return game; }; it("names the Pokémon and, alone, only that", () => { const { game } = started(breedingParty); man(game, [1, 0]); const shown = fenceTalk(game, 1); expect(shown).toContain( "It's CYNDAQUIL\nthat was left with\nthe DAY-CARE MAN.", ); expect(shown).not.toContain("interest"); }); it("says how the two get on, by compatibility", () => { const male = { species: CYNDAQUIL, dvs: MALE(7, 2) }, female = { species: CYNDAQUIL, dvs: FEMALE(2, 1) }; // Same species, different OTs: 254, "appears to care". const game = pairOf({ ...female, otId: 1 }, { ...male, otId: 2 }); const shown = fenceTalk(game, 1); expect(shown).toContain( "It's FLARE\nthat was left with\nthe DAY-CARE MAN.", ); expect(shown).toContain("It appears to care\nfor EMBER."); const other = fenceTalk(game, 2); expect(other).toContain( "It's EMBER\nthat was left with\nthe DAY-CARE LADY.", ); expect(other).toContain("It appears to care\nfor FLARE."); const set = ( a: Partial & { species: number }, b: Partial & { species: number }, ) => { game.state.dayCare.mon1 = boxed(game, a.species, { nickname: "FLARE", ...a, }); game.state.dayCare.mon2 = boxed(game, b.species, { nickname: "EMBER", ...b, }); }; // Same species and OT: 177, "friendly". set({ ...female, otId: 1 }, { ...male, otId: 1 }); expect(fenceTalk(game, 1)).toContain("It's friendly with\nEMBER."); // Equal Defense DVs: 255, "brimming with energy". set( { ...female, otId: 1 }, { species: CYNDAQUIL, dvs: MALE(2, 1), otId: 2 }, ); expect(fenceTalk(game, 1)).toContain("It's brimming with\nenergy."); // Two males: 0, "no interest". set( { ...male, otId: 1 }, { species: CYNDAQUIL, dvs: MALE(9, 4), otId: 2 }, ); expect(fenceTalk(game, 1)).toContain("It has no interest\nin EMBER."); // Different species and OTs: 128, "friendly"; the same OT: 51, "shows // interest". set( { species: PIKACHU, dvs: FEMALE(2, 1), otId: 1 }, { ...male, otId: 2 }, ); expect(fenceTalk(game, 1)).toContain("It's friendly with\nEMBER."); set( { species: PIKACHU, dvs: FEMALE(2, 1), otId: 1 }, { ...male, otId: 1 }, ); expect(fenceTalk(game, 1)).toContain("It shows interest\nin EMBER."); }); }); describe("the Odd Egg (GiveOddEgg)", () => { it("is given once, with an Egg Ticket used up if there is one", () => { const { game } = world(); game.state.party = [mon(game, CYNDAQUIL, 20)]; game.state.bag.keyItems.push(EGG_TICKET); const shown = man(game); expect(shown).toContain("Well, wouldn't you\nlike this EGG?"); expect(shown).toContain("CHRIS received\nODD EGG!"); expect(shown).toContain("I found that when"); const egg = game.state.party[1]; expect(egg).toMatchObject({ egg: true, nickname: "EGG", ot: "ODD", level: 5, hp: 0, happiness: 20, }); expect([172, 173, 174, 238, 240, 239, 236]).toContain(egg.species); expect(game.state.bag.keyItems).not.toContain(EGG_TICKET); expect(game.state.events.has(EVENT_GOT_ODD_EGG)).toBe(true); // Afterwards the man is the keeper. expect(man(game, [0])).toContain("Want me to\nraise a POKéMON?"); expect(game.state.party).toHaveLength(2); }); it("is refused with a full party and tried again", () => { const { game } = world(); game.state.party = Array.from({ length: 6 }, () => mon(game, CYNDAQUIL, 20), ); expect(man(game)).toContain("You've no room for\nthis."); expect(game.state.events.has(EVENT_GOT_ODD_EGG)).toBe(false); expect(game.state.party).toHaveLength(6); }); it("picks from the ROM's table by two random bytes", () => { const rows = (sub: number, add: number) => oddEgg(data(), bytes(sub, add)); // Probabilities are percentages of $ffff: Pichu 8 + 1, then Cleffa // 16 + 3 ... in the table's order, normal and shiny variants. const species = (sub: number, add: number) => rows(sub, add).species; expect(species(0x00, 0x00)).toBe(PICHU); expect(species(0x14, 0x7a)).toBe(PICHU); // 8 percent of $ffff = $147a expect(species(0x14, 0x7b)).toBe(PICHU); // the 1 percent shiny Pichu follows const first = rows(0x00, 0x00), second = rows(0x15, 0x00); expect(first.dvs).toBe(0); expect(second.dvs).toBe(0x2aaa); expect(first.otId).toBe(8); expect(species(0xff, 0xfe)).toBe(TYROGUE); expect(rows(0xff, 0xff).species).toBe(TYROGUE); // The full table: every row is a valid party Pokémon of its own. for (let sub = 0; sub < 256; sub += 5) for (const add of [0, 0x80, 0xff]) { const egg = oddEgg(data(), bytes(sub, add)); expect(egg).toMatchObject({ egg: true, level: 5, hp: 0, nickname: "EGG", ot: "ODD", }); } }); }); describe("saving", () => { it("keeps the Pokémon, the Egg and the counters through a reload", () => { const { game } = started(breedingParty); man(game, [1, 0]); lady(game, [1, 0]); game.state.flags.add(HAS_EGG); game.state.dayCare.compatible = false; game.state.dayCare.stepsToEgg = 77; const saved = JSON.parse(JSON.stringify(game.save())); const other = world().game; other.restore(saved); expect(other.state.dayCare).toEqual(game.state.dayCare); expect(other.state.flags.has(HAS_EGG)).toBe(true); expect(JSON.parse(JSON.stringify(other.save())).dayCare).toEqual( saved.dayCare, ); // The Egg can still be collected after the reload. other.state.party = [mon(other, TOTODILE, 15)]; expect(talk(other, ROUTE_34, 15, 17, "up")).toContain( "CHRIS received\nthe EGG!", ); }); it("rejects breeding without a prepared Egg and simultaneous ready/breeding flags", () => { const { game } = started(breedingParty); man(game, [1, 0]); lady(game, [1, 0]); const saved = JSON.parse(JSON.stringify(game.save())); const eggless = structuredClone(saved); eggless.dayCare.egg = null; expect(() => world().game.restore(eggless)).toThrow( /Invalid native save/, ); const alreadyReady = structuredClone(saved); alreadyReady.flags.push(HAS_EGG); expect(() => world().game.restore(alreadyReady)).toThrow( /Invalid native save/, ); const explicitNull = structuredClone(saved); explicitNull.dayCare = null; expect(() => world().game.restore(explicitNull)).toThrow( /Invalid native save/, ); }); it("migrates a save without the Day-Care to an empty one, and rejects inconsistent ones", () => { const { game } = started(); const saved = JSON.parse(JSON.stringify(game.save())); delete saved.dayCare; const loaded = world().game; loaded.restore(saved); expect(loaded.state.dayCare).toEqual(emptyDayCare()); const bad = (change: (save: any) => void) => { const copy = JSON.parse(JSON.stringify(game.save())); change(copy); expect(() => world().game.restore(copy)).toThrow( /Invalid native save/, ); }; // A flag with no Pokémon, and a Pokémon with no flag. bad((save) => save.flags.push(MAN_HAS_MON)); man(game, [1, 1]); const withMon = JSON.parse(JSON.stringify(game.save())); const noFlag = structuredClone(withMon); noFlag.flags = noFlag.flags.filter( (flag: number) => flag !== MAN_HAS_MON, ); expect(() => world().game.restore(noFlag)).toThrow( /Invalid native save/, ); // An Egg waiting without an Egg, and a damaged step count. const eggless = structuredClone(withMon); eggless.flags.push(HAS_EGG); expect(() => world().game.restore(eggless)).toThrow( /Invalid native save/, ); const steps = structuredClone(withMon); steps.dayCare.stepsToEgg = 300; expect(() => world().game.restore(steps)).toThrow( /Invalid native save/, ); const species = structuredClone(withMon); species.dayCare.mon1.species = 252; expect(() => world().game.restore(species)).toThrow( /Invalid native save/, ); }); }); it("matches the species constants it uses", () => { expect(data().speciesName(NIDORAN_F)).toBe("NIDORAN♀"); expect(data().speciesName(NIDORAN_M)).toBe("NIDORAN♂"); expect(data().speciesName(DITTO)).toBe("DITTO"); expect(data().speciesName(EGG_SPECIES)).toBe("EGG"); }); }, );