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 { emptyScriptState } from "../src/game/scripts/interpreter"; import { generatePartyMon } from "../src/game/party/pokemon"; import type { Direction } from "../src/game/world/types"; import { settle, walkTo } from "./world-driver"; const romPath = process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc"; const BURNED_TOWER_1F = "3:13", BURNED_TOWER_B1F = "3:14"; const EVENT_HOLE_IN_BURNED_TOWER = 818; /** A world on maps that normal play does not enable yet. */ function fixture(maps: string[]) { const assets = new CrystalAssets(new Uint8Array(readFileSync(romPath))); const ported = assets.isPorted.bind(assets); assets.isPorted = (id) => ported(id) || maps.includes(id); const world = new GameWorld(assets, () => new Date("2026-09-22T12:00:00"), () => 0); world.state = emptyScriptState(); return { assets, world }; } function hold(world: GameWorld, direction: Direction | undefined, ticks: number) { for (let tick = 0; tick < ticks; tick++) world.tick(direction); } describe.skipIf(!existsSync(romPath))("overworld foundation with the local ROM", () => { it("changeblock in Burned Tower's TILES callback closes the hole until the event is set", () => { const { assets, world } = fixture([BURNED_TOWER_1F, BURNED_TOWER_B1F]); world.state.scenes[BURNED_TOWER_1F] = 2; // SCENE_BURNEDTOWER1F_NOOP world.warpTo(BURNED_TOWER_1F, 10, 10); expect(assets.permission(assets.collision(world.map, 10, 9))).toBe(0); expect(assets.collision(world.map, 10, 9)).not.toBe(0x60); // The ROM's own block data still has the hole; only the working copy changed. expect(assets.collision(assets.map(BURNED_TOWER_1F), 10, 9)).toBe(0x60); world.state.events.add(EVENT_HOLE_IN_BURNED_TOWER); world.warpTo(BURNED_TOWER_1F, 10, 10); expect(assets.collision(world.map, 10, 9)).toBe(0x60); }); it("falls through a pit with FallIntoMapScript's skyfall and earthquake", () => { const { world } = fixture([BURNED_TOWER_1F, BURNED_TOWER_B1F]); world.state.scenes[BURNED_TOWER_1F] = 2; world.state.scenes[BURNED_TOWER_B1F] = 1; world.state.events.add(EVENT_HOLE_IN_BURNED_TOWER); world.warpTo(BURNED_TOWER_1F, 10, 10); // Turn, then walk up onto the hole. hold(world, "up", 4); hold(world, "up", 8); expect([world.mapId, world.x, world.y]).toEqual([BURNED_TOWER_1F, 10, 9]); // The arrival tick falls through; the next starts the rest of the script. hold(world, undefined, 2); expect(world.notice, world.diagnostic).toBe(""); expect([world.mapId, world.x, world.y]).toEqual([BURNED_TOWER_B1F, 10, 9]); expect(world.activity).toBe("scene"); // Invisible above the screen at first, then falling into view. expect(world.poses().some((pose) => pose.id === 0)).toBe(false); let shook = false; for (let tick = 0; tick < 200 && world.busy; tick++) { world.tick(); if (world.screenShake) shook = true; } expect(world.busy).toBe(false); expect(shook).toBe(true); expect(world.notice, world.diagnostic).toBe(""); expect(world.poses().find((pose) => pose.id === 0)).toMatchObject({ x: 160, y: 144 }); }); it("rides the Goldenrod Dept. Store elevator through its menu and -1 warps", () => { const [store1F, store3F, elevator] = ["11:11", "11:13", "11:17"]; const { world } = fixture([store1F, store3F, elevator]); world.warpTo(store1F, 2, 1); walkTo(world, 2, 0); expect([world.mapId, world.x, world.y]).toEqual([elevator, 1, 3]); // The arrival warp is numbered -1, so the entrance became the backup. expect(world.state.backupWarp).toEqual({ map: store1F, warp: 4 }); walkTo(world, 3, 1); world.move("up"); world.interact(); const menu = world.dialogue!; const shown = settle(world, [3]); expect(shown.join("\n")).toContain("Now on: 1F"); expect(menu.choice).toMatchObject({ kind: "menu" }); expect((menu.choice as { items: string[] }).items).toEqual([ "B1F", "1F", "2F", "3F", "4F", "5F", "6F", ]); expect(world.notice, world.diagnostic).toBe(""); expect(world.state.backupWarp).toEqual({ map: store3F, warp: 3 }); walkTo(world, 2, 3); world.move("down"); settle(world); // Arrival at warp 3 (2, 0), then the doorway's forced step down. expect([world.mapId, world.x, world.y]).toEqual([store3F, 2, 1]); expect(world.notice, world.diagnostic).toBe(""); }); it("keeps the floor when the elevator menu is cancelled or the same floor is chosen", () => { const [store1F, elevator] = ["11:11", "11:17"]; const { world } = fixture([store1F, elevator]); world.warpTo(store1F, 2, 1); walkTo(world, 2, 0); walkTo(world, 3, 1); world.move("up"); for (const answer of [-1, 1]) { world.interact(); settle(world, [answer]); expect(world.notice, world.diagnostic).toBe(""); expect(world.state.backupWarp).toEqual({ map: store1F, warp: 4 }); } }); describe("wild encounters (CanEncounterWildMon and ChooseWildEncounter)", () => { const UNION_CAVE_1F = "3:37"; const cave = () => { const { assets, world } = fixture([UNION_CAVE_1F]); world.state.party = [ generatePartyMon(assets.pokemon, { species: 155, level: 20, item: 0, otId: 1, ot: "CHRIS", dvs: 0, caughtData: 0, }), ]; return { assets, world }; }; /** Walk back and forth until a battle starts or the steps run out. */ const pace = (world: GameWorld, steps: number, directions: Direction[]) => { for (let step = 0; step < steps && !world.battle; step++) { world.move(directions[step % directions.length]); for (let tick = 0; tick < 4 && !world.battle; tick++) world.tick(); } return world.battle?.engine; }; it("starts on any cave floor after the five-step cooldown", () => { const { world } = cave(); world.warpTo(UNION_CAVE_1F, 9, 6); const battle = pace(world, 12, ["right", "left"]); expect(world.notice, world.diagnostic).toBe(""); expect(battle).toBeDefined(); expect(world.tilesWalked).toBe(5); }); it("stays off with wildoff", () => { const { world } = cave(); world.state.noWildEncounters = true; world.warpTo(UNION_CAVE_1F, 9, 6); expect(pace(world, 12, ["right", "left"])).toBeUndefined(); }); it("uses the water table's rate and slots while surfing", () => { const { assets, world } = cave(); const sea = assets.waterWildMons(UNION_CAVE_1F)!; expect(sea).toBeDefined(); world.state.playerState = 4; // PLAYER_SURF world.restore({ ...world.save(), map: UNION_CAVE_1F, x: 7, y: 4, direction: "right", objects: [] }); const battle = pace(world, 12, ["right", "left"]); expect(battle).toBeDefined(); // Random is always 0: the first slot, with no level variety. expect(battle!.enemy.mon).toMatchObject({ species: sea.slots[0].species, level: sea.slots[0].level, }); }); }); it("draws overworld Pokémon from their party menu icons (GetMonSprite)", () => { const { assets } = fixture([]); const unown = assets.sprite(0x80)!; // SPRITE_UNOWN expect(unown.walking).toBe(true); expect(unown.palette).toBe(0); expect(unown.graphics).toHaveLength(8 * 16); expect(assets.sprite(0x80 + 35)).toBeUndefined(); expect(assets.sprite(103)).toBeUndefined(); }); it("surfing onto a whirlpool spins the player back with Script_ForcedMovement", () => { const { assets, world } = fixture([]); // Find a whirlpool with water on the side a surfer comes from. let found: { map: string; x: number; y: number } | undefined; for (let group = 1; group <= 26 && !found; group++) for (let number = 1; number <= 40 && !found; number++) { let map; try { map = assets.map(`${group}:${number}`); } catch { continue; } for (let y = 1; y < map.height - 1 && !found; y++) for (let x = 0; x < map.width && !found; x++) if ( assets.collision(map, x, y) === 0x24 && assets.permission(assets.collision(map, x, y + 1)) === 1 && assets.permission(assets.collision(map, x, y + 2)) === 1 && !map.objects.some((o) => o.x === x && (o.y === y + 1 || o.y === y + 2)) ) found = { map: map.id, x, y }; } expect(found).toBeDefined(); const { map, x, y } = found!; const ported = assets.isPorted.bind(assets); assets.isPorted = (id) => ported(id) || id === map; world.state.playerState = 4; // PLAYER_SURF world.restore({ ...world.save(), map, x, y: y + 2, direction: "up", objects: [] }); hold(world, "up", 8); expect([world.x, world.y]).toEqual([x, y + 1]); hold(world, "up", 9); expect([world.x, world.y]).toEqual([x, y]); expect(world.activity).toBe("scene"); const directions = new Set(); for (let tick = 0; tick < 120 && world.busy; tick++) { world.tick(); directions.add(world.direction); } expect(world.notice, world.diagnostic).toBe(""); // .MovementData_up: spin, turn_in DOWN back off the whirlpool, spin, // then face down. expect([world.x, world.y, world.direction]).toEqual([x, y + 1, "down"]); expect(directions.size).toBe(4); }); }); describe.skipIf(!existsSync(romPath))("field moves with the local ROM", () => { const TOTODILE = 158; const partyWith = (assets: CrystalAssets, moves: number[], species = TOTODILE) => { const mon = generatePartyMon(assets.pokemon, { species, level: 30, item: 0, otId: 1, ot: "CHRIS", dvs: 0, caughtData: 0, }); mon.moves = [...moves, 0, 0, 0, 0].slice(0, 4) as typeof mon.moves; mon.pp = [10, 10, 10, 10]; mon.nickname = "SWIMMY"; return [mon]; }; /** Find a tile on some map, searching every group. */ const find = ( assets: CrystalAssets, test: (map: ReturnType, x: number, y: number) => boolean, ) => { for (let group = 1; group <= 26; group++) for (let number = 1; number <= 60; number++) { let map; try { map = assets.map(`${group}:${number}`); } catch { continue; } for (let y = 1; y < map.height - 1; y++) for (let x = 1; x < map.width - 1; x++) if (test(map, x, y)) return { map: map.id, x, y }; } throw new Error("No suitable tile."); }; it("surfs from New Bark Town's shore through AskSurfScript and lands again", () => { const { assets, world } = fixture([]); world.state.party = partyWith(assets, [0x39]); // SURF world.state.flags.add(30); // ENGINE_FOGBADGE world.state.scenes["24:4"] = 1; world.restore({ ...world.save(), map: "24:4", x: 17, y: 7, direction: "right", objects: [] }); world.interact(); const shown = settle(world).join("\n"); expect(world.notice, world.diagnostic).toBe(""); expect(shown).toContain("SWIMMY used\nSURF!"); expect(world.state.playerState).toBe(4); expect([world.x, world.y]).toEqual([18, 7]); // The surfer sprite (SPRITE_SURF) replaces Chris. expect(world.poses().find((pose) => pose.id === 0)?.sprite).toBe(0x53); // Water saves and reloads with the surfer on it. world.restore(world.save()); expect(world.state.playerState).toBe(4); world.move("left"); expect([world.x, world.y, world.state.playerState]).toEqual([17, 7, 0]); }); it("does not offer Surf without the Fog Badge or a Pokémon that knows it", () => { const { assets, world } = fixture([]); world.state.party = partyWith(assets, [0x39]); world.state.scenes["24:4"] = 1; world.restore({ ...world.save(), map: "24:4", x: 17, y: 7, direction: "right", objects: [] }); world.interact(); expect(world.dialogue).toBeUndefined(); world.state.flags.add(30); world.state.party = partyWith(assets, [0x0f]); world.interact(); expect(world.dialogue).toBeUndefined(); }); it("cuts a tree with AskCutScript until the map loads again", () => { const { assets, world } = fixture([]); const spot = find(assets, (map, x, y) => [1, 2, 3, 12, 25].includes(map.tileset) && assets.collision(map, x, y) === 0x12 && assets.permission(assets.collision(map, x, y + 1)) === 0 && !map.objects.some((o) => o.x === x && o.y === y + 1) && map.callbacks.length === 0, ); const ported = assets.isPorted.bind(assets); assets.isPorted = (id) => ported(id) || id === spot.map; world.state.party = partyWith(assets, [0x0f]); // CUT world.state.flags.add(28); // ENGINE_HIVEBADGE world.warpTo(spot.map, spot.x, spot.y + 1); world.restore({ ...world.save(), direction: "up" }); const before = world.map.blocks.slice(); world.interact(); const shown = settle(world).join("\n"); expect(world.notice, world.diagnostic).toBe(""); expect(shown).toContain("SWIMMY used\nCUT!"); expect(world.map.blocks).not.toEqual(before); expect(assets.permission(assets.collision(world.map, spot.x, spot.y))).toBe(0); world.move("up"); expect([world.x, world.y]).toEqual([spot.x, spot.y]); world.warpTo(spot.map, spot.x, spot.y + 1); expect(world.map.blocks).toEqual(before); }); it("explains a Cut tree without the move", () => { const { assets, world } = fixture([]); const spot = find(assets, (map, x, y) => assets.collision(map, x, y) === 0x12 && assets.permission(assets.collision(map, x, y + 1)) === 0 && !map.objects.some((o) => o.x === x && o.y === y + 1) && map.callbacks.length === 0, ); const ported = assets.isPorted.bind(assets); assets.isPorted = (id) => ported(id) || id === spot.map; world.state.party = partyWith(assets, [0x21]); world.warpTo(spot.map, spot.x, spot.y + 1); world.restore({ ...world.save(), direction: "up" }); world.interact(); const shown = settle(world).join("\n"); expect(world.notice, world.diagnostic).toBe(""); expect(shown.length).toBeGreaterThan(0); expect(assets.permission(assets.collision(world.map, spot.x, spot.y))).not.toBe(0); }); it("uses Strength through AskStrengthScript and pushes a boulder", () => { const { assets, world } = fixture([]); // A Strength boulder object with floor below it and two floor tiles above. const spot = find(assets, (map, x, y) => { const boulder = map.objects.find( (o) => o.x === x && o.y === y && o.movement === 25 && o.eventFlag === 0xffff, ); return Boolean( boulder && map.callbacks.length === 0 && [y + 1, y - 1, y - 2].every( (row) => assets.permission(assets.collision(map, x, row)) === 0 && !map.objects.some((o) => o !== boulder && o.x === x && o.y === row), ), ); }); const ported = assets.isPorted.bind(assets); assets.isPorted = (id) => ported(id) || id === spot.map; world.state.party = partyWith(assets, [0x46]); // STRENGTH world.state.flags.add(29); // ENGINE_PLAINBADGE world.warpTo(spot.map, spot.x, spot.y + 1); world.restore({ ...world.save(), direction: "up" }); world.interact(); const shown = settle(world).join("\n"); expect(world.notice, world.diagnostic).toBe(""); expect(shown).toContain("SWIMMY used\nSTRENGTH!"); expect(world.state.flags.has(24)).toBe(true); // ENGINE_STRENGTH_ACTIVE for (let tick = 0; tick < 20; tick++) world.tick("up"); const boulder = world.objects().find((o) => o.movement === 25 && o.x === spot.x); expect(boulder?.y).toBe(spot.y - 1); // Talking to it again: "Boulders may now be moved!" settle(world); }); it("rides the Bicycle on a route and gets off indoors", () => { const { assets, world } = fixture([]); world.state.party = partyWith(assets, [0x21]); world.state.bag.keyItems.push(7); // BICYCLE world.state.scenes["24:4"] = 1; world.restore({ ...world.save(), map: "24:4", x: 13, y: 7, direction: "down", objects: [] }); expect(world.useBicycle()).toBe(true); const shown = settle(world).join("\n"); expect(world.notice, world.diagnostic).toBe(""); expect(shown).toContain("got on the\nBICYCLE."); expect(world.state.playerState).toBe(1); expect(world.poses().find((pose) => pose.id === 0)?.sprite).toBe(2); // SPRITE_CHRIS_BIKE // Turn left (four ticks), then four ticks per tile on the bike. for (let tick = 0; tick < 4; tick++) world.tick("left"); world.tick("left"); expect(world.x).toBe(12); for (let tick = 0; tick < 4; tick++) world.tick("left"); expect(world.x).toBe(11); // Entering the house (indoors) gets the player off (CheckUpdatePlayerSprite). settle(world); walkTo(world, 13, 5); expect(world.mapId).toBe("24:6"); expect(world.state.playerState).toBe(0); expect(world.useBicycle()).toBe(false); }); it("offers Whirlpool and Waterfall only with the move and badge", () => { const { assets, world } = fixture([]); world.state.party = partyWith(assets, [0x39]); const whirlpool = find(assets, (map, x, y) => assets.collision(map, x, y) === 0x24 && assets.permission(assets.collision(map, x, y + 1)) === 1 && !map.objects.some((o) => o.x === x && o.y === y + 1), ); const ported = assets.isPorted.bind(assets); assets.isPorted = (id) => ported(id) || id === whirlpool.map; world.state.playerState = 4; world.restore({ ...world.save(), map: whirlpool.map, x: whirlpool.x, y: whirlpool.y + 1, direction: "up", objects: [] }); world.interact(); const mighty = settle(world).join("\n"); expect(world.notice, world.diagnostic).toBe(""); expect(mighty.length).toBeGreaterThan(0); world.state.party = partyWith(assets, [0x39, 250]); // SURF, WHIRLPOOL world.state.flags.add(33); // ENGINE_GLACIERBADGE const before = world.map.blocks.slice(); world.interact(); const used = settle(world).join("\n"); expect(world.notice, world.diagnostic).toBe(""); expect(used).toContain("used\nWHIRLPOOL!"); expect(world.map.blocks).not.toEqual(before); }); /** Start → POKéMON → the first Pokémon → `move`; returns the pages * shown until the menu closed or a choice (the party menu) returned. */ const useFromMenu = (world: GameWorld, move: string) => { expect(world.openStartMenu()).toBe(true); const choose = (label: string) => { const choice = world.dialogue?.choice; if (choice?.kind !== "menu") throw new Error(`No menu for ${label}`); const index = choice.items.findIndex((item) => item.startsWith(label)); expect(index, `${label} in ${choice.items}`).toBeGreaterThanOrEqual(0); world.choose(index); }; choose("POKéMON"); choose("SWIMMY"); choose(move); const pages: string[] = []; for (let i = 0; i < 50 && world.dialogue && !world.dialogue.choice; i++) { pages.push(world.dialogue.pages[world.dialogue.index]); world.advanceDialogue(); } return pages; }; /** B out of every menu, then let any queued script finish. */ const closeMenus = (world: GameWorld) => { for (let i = 0; i < 10 && world.dialogue?.choice; i++) world.advanceDialogue(); return settle(world).join("\n"); }; it("surfs from the party menu, after the badge and facing checks", () => { const { assets, world } = fixture([]); world.state.party = partyWith(assets, [0x0f, 0x39]); // CUT, SURF world.state.scenes["24:4"] = 1; world.restore({ ...world.save(), map: "24:4", x: 17, y: 7, direction: "right", objects: [] }); expect(world.dialogue).toBeUndefined(); // CUT and SURF come first in the submenu. expect(useFromMenu(world, "SURF")).toEqual(["Sorry! A new BADGE\nis required."]); closeMenus(world); world.state.flags.add(30); // ENGINE_FOGBADGE world.restore({ ...world.save(), direction: "up" }); expect(useFromMenu(world, "SURF")).toEqual(["You can't SURF\nhere."]); closeMenus(world); world.restore({ ...world.save(), direction: "right" }); const shown = [...useFromMenu(world, "SURF"), closeMenus(world)].join("\n"); expect(world.notice, world.diagnostic).toBe(""); // No "Want to SURF?" question from the menu. expect(shown).not.toContain("?"); expect(shown).toContain("SWIMMY used\nSURF!"); expect([world.x, world.y, world.state.playerState]).toEqual([18, 7, 4]); expect(useFromMenu(world, "SURF")).toEqual(["You're already\nSURFING."]); closeMenus(world); }); it("cuts from the party menu, or finds nothing to cut", () => { const { assets, world } = fixture([]); const spot = find(assets, (map, x, y) => [1, 2, 3, 12, 25].includes(map.tileset) && assets.collision(map, x, y) === 0x12 && assets.permission(assets.collision(map, x, y + 1)) === 0 && !map.objects.some((o) => o.x === x && o.y === y + 1) && map.callbacks.length === 0, ); const ported = assets.isPorted.bind(assets); assets.isPorted = (id) => ported(id) || id === spot.map; world.state.party = partyWith(assets, [0x0f]); world.state.flags.add(28); // ENGINE_HIVEBADGE world.warpTo(spot.map, spot.x, spot.y + 1); world.restore({ ...world.save(), direction: "down" }); expect(useFromMenu(world, "CUT")).toEqual(["There's nothing to\nCUT here."]); closeMenus(world); world.restore({ ...world.save(), direction: "up" }); const before = world.map.blocks.slice(); const shown = [...useFromMenu(world, "CUT"), closeMenus(world)].join("\n"); expect(world.notice, world.diagnostic).toBe(""); expect(shown).toContain("SWIMMY used\nCUT!"); expect(world.map.blocks).not.toEqual(before); }); it("uses Strength from the party menu", () => { const { assets, world } = fixture([]); world.state.party = partyWith(assets, [0x46]); world.state.flags.add(29); // ENGINE_PLAINBADGE world.state.scenes["24:4"] = 1; world.restore({ ...world.save(), map: "24:4", x: 13, y: 7, direction: "down", objects: [] }); const shown = [...useFromMenu(world, "STRENGTH"), closeMenus(world)].join("\n"); expect(world.notice, world.diagnostic).toBe(""); expect(shown).toContain("SWIMMY used\nSTRENGTH!"); expect(world.state.flags.has(24)).toBe(true); // ENGINE_STRENGTH_ACTIVE }); it("lights a dark cave with Flash, which fails elsewhere", () => { // Dark Cave's Violet entrance (PALETTE_DARK). const DARK_CAVE = "3:78"; const { assets, world } = fixture([DARK_CAVE]); const map = assets.map(DARK_CAVE); expect(map.paletteMode).toBe(4); let spot = { map: DARK_CAVE, x: 0, y: 0 }; search: for (let y = 1; y < map.height - 1; y++) for (let x = 1; x < map.width - 1; x++) if ( assets.permission(assets.collision(map, x, y)) === 0 && !map.objects.some((o) => o.x === x && o.y === y) ) { spot = { map: DARK_CAVE, x, y }; break search; } world.state.party = partyWith(assets, [0x94]); // FLASH world.state.flags.add(27); // ENGINE_ZEPHYRBADGE world.warpTo(spot.map, spot.x, spot.y); expect(world.timeOfDay).toBe(3); const shown = [...useFromMenu(world, "FLASH"), closeMenus(world)].join("\n"); expect(world.notice, world.diagnostic).toBe(""); expect(shown).toContain("A blinding FLASH\nlights the area!"); expect(world.state.flash).toBe(true); expect(world.timeOfDay).toBe(2); expect(useFromMenu(world, "FLASH")).toEqual(["Can't use that\nhere."]); closeMenus(world); }); it("recognises a smashable rock for Rock Smash and smashes it", () => { // A rock (SPRITEMOVEDATA_SMASHABLE_ROCK) in Dark Cave at 27, 6. const spot = { map: "3:78", x: 27, y: 6 }; const { assets, world } = fixture([spot.map]); expect( assets.map(spot.map).objects.some( (o) => o.x === spot.x && o.y === spot.y && o.movement === 0x18, ), ).toBe(true); world.state.party = partyWith(assets, [0xf9]); // ROCK_SMASH world.warpTo(spot.map, spot.x, spot.y + 1); world.restore({ ...world.save(), direction: "down" }); expect(useFromMenu(world, "ROCK SMASH")).toEqual(["Can't use that\nhere."]); closeMenus(world); world.restore({ ...world.save(), direction: "up" }); const shown = useFromMenu(world, "ROCK SMASH"); for (let i = 0; i < 10 && world.dialogue?.choice; i++) world.advanceDialogue(); // Random always returns 0 here, so the rock hides a Krabby. for (let tick = 0; tick < 500 && !world.battle; tick++) { if (world.dialogue) { shown.push(world.dialogue.pages[world.dialogue.index]); world.advanceDialogue(); } else world.tick(); } expect(world.notice, world.diagnostic).toBe(""); expect(shown).toContain("SWIMMY used\nROCK SMASH!"); expect(world.objects().some((o) => o.x === spot.x && o.y === spot.y)).toBe(false); expect(world.battle?.engine.enemy.mon.species).toBe(98); // KRABBY }); it("draws a wild Pokémon with Sweet Scent in grass, and nothing elsewhere", () => { const { assets, world } = fixture([]); world.state.party = partyWith(assets, [0xe6]); // SWEET_SCENT world.state.scenes["24:4"] = 1; world.restore({ ...world.save(), map: "24:4", x: 13, y: 7, direction: "down", objects: [] }); const nothing = [...useFromMenu(world, "SWEET SCENT"), closeMenus(world)].join("\n"); expect(world.notice, world.diagnostic).toBe(""); expect(nothing).toContain("SWIMMY used\nSWEET SCENT!"); expect(nothing).toContain("nothing"); const grass = find(assets, (map, x, y) => map.id === "24:3" && assets.grassCollisions().includes(assets.collision(map, x, y)) && !map.objects.some((o) => o.x === x && o.y === y), ); world.warpTo(grass.map, grass.x, grass.y); useFromMenu(world, "SWEET SCENT"); for (let i = 0; i < 10 && world.dialogue?.choice; i++) world.advanceDialogue(); for (let tick = 0; tick < 200 && !world.battle; tick++) { if (world.dialogue) world.advanceDialogue(); world.tick(); } expect(world.notice, world.diagnostic).toBe(""); expect(world.battle).toBeDefined(); }); }); describe.skipIf(!existsSync(romPath))("Strength boulders and stone tables with the local ROM", () => { it("drops a boulder pushed into Ice Path B1F's hole through its stone table script", () => { const ICE_PATH_B1F = "3:62"; const EVENT_BOULDER_IN_ICE_PATH_2A = 1806; const { assets, world } = fixture([ICE_PATH_B1F]); const mon = generatePartyMon(assets.pokemon, { species: 158, level: 30, item: 0, otId: 1, ot: "CHRIS", dvs: 0, caughtData: 0, }); world.state.party = [mon]; world.warpTo(ICE_PATH_B1F, 4, 5); world.state.flags.add(24); // ENGINE_STRENGTH_ACTIVE world.state.events.add(EVENT_BOULDER_IN_ICE_PATH_2A); world.restore({ ...world.save(), x: 4, y: 5, direction: "down", objects: [{ id: 3, x: 4, y: 6, direction: "down" }], }); world.state.flags.add(24); expect(world.objects().some((o) => o.id === 3)).toBe(true); world.tick("down"); const shown: string[] = []; for (let tick = 0; tick < 40 && world.activity !== "scene"; tick++) world.tick(); expect(world.activity).toBe("scene"); shown.push(...settle(world)); expect(world.notice, world.diagnostic).toBe(""); expect(shown.join("\n")).toMatch(/fell\s+through/i); expect(world.objects().some((o) => o.id === 3)).toBe(false); expect(world.state.events.has(EVENT_BOULDER_IN_ICE_PATH_2A)).toBe(false); }); });