Checks / web (push) Canceled after 0s
- Random walks and spins from SpriteMovementData, with random sleeps. - CanObjectMoveInDirection: side walls, land, objects' current and previous tiles, the radius (stored plus one) and the screen edge. - Objects freeze while text is open and during applymovement, and return home when both they and their home leave the object window. - Wandering positions are not saved; test helpers replan around NPCs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
128 lines
4.8 KiB
TypeScript
128 lines
4.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { CrystalAssets } from "../src/game/rom/assets";
|
|
import { GameWorld, migrateSave } from "../src/game/world/world";
|
|
import { ENGINE_FLAGS } from "../src/game/rom/profile";
|
|
import { settle, findPath, walkTo } from "./world-driver";
|
|
import { generatePartyMon } from "../src/game/party/pokemon";
|
|
|
|
const romPath =
|
|
process.env.CRYSTAL_ROM_PATH ??
|
|
"Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc";
|
|
const BERRY = 0xad;
|
|
const ROUTE_29 = "24:3";
|
|
|
|
describe.skipIf(!existsSync(romPath))("Route 29 with the local ROM", () => {
|
|
/** `now` is mutable per test. */
|
|
const fixture = (date = "2026-09-22T12:00:00") => {
|
|
const assets = new CrystalAssets(new Uint8Array(readFileSync(romPath)));
|
|
const clock = { now: new Date(date) };
|
|
const world = new GameWorld(assets, () => clock.now);
|
|
// Facing FRUITTREE_ROUTE_29 at (12, 2).
|
|
world.restore({ ...world.save(), map: ROUTE_29, x: 12, y: 3, direction: "up" });
|
|
world.tick();
|
|
return { world, clock };
|
|
};
|
|
const berries = (world: GameWorld) =>
|
|
world.state.bag.items.find((slot) => slot.item === BERRY)?.quantity ?? 0;
|
|
const pick = (world: GameWorld) => {
|
|
world.interact();
|
|
const pages = settle(world);
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
return pages.join("\n");
|
|
};
|
|
|
|
it("picks a Berry once per day and regrows after the daily reset", () => {
|
|
const { world, clock } = fixture();
|
|
expect(world.state.dailyReset).not.toBeNull();
|
|
expect(pick(world)).toMatch(/BERRY/);
|
|
expect(berries(world)).toBe(1);
|
|
expect(world.state.fruitTrees).toEqual(new Set([1]));
|
|
expect(world.state.flags.has(ENGINE_FLAGS.allFruitTrees)).toBe(true);
|
|
|
|
// Same day, even across a save and reload: nothing to pick.
|
|
const saved = world.save();
|
|
expect(saved).toMatchObject({ fruitTrees: [1] });
|
|
world.restore(saved);
|
|
expect(world.save()).toEqual(saved);
|
|
pick(world);
|
|
expect(berries(world)).toBe(1);
|
|
|
|
// The first idle tick of the next day clears the daily flag.
|
|
clock.now = new Date("2026-09-23T00:01:00");
|
|
world.tick();
|
|
expect(world.state.flags.has(ENGINE_FLAGS.allFruitTrees)).toBe(false);
|
|
expect(pick(world)).toMatch(/BERRY/);
|
|
expect(berries(world)).toBe(2);
|
|
});
|
|
|
|
it("leaves the tree unpicked when the item pocket is full", () => {
|
|
const { world } = fixture();
|
|
world.state.bag.items = Array.from({ length: 20 }, (_, index) => ({
|
|
item: index + 1,
|
|
quantity: 1,
|
|
}));
|
|
const before = world.state.bag.items.map((slot) => ({ ...slot }));
|
|
pick(world);
|
|
expect(world.state.bag.items).toEqual(before);
|
|
expect(world.state.fruitTrees.size).toBe(0);
|
|
});
|
|
|
|
it("migrates version-3 saves without daily state and rejects invalid trees", () => {
|
|
const { world } = fixture();
|
|
const { fruitTrees: _, dailyReset: __, ...legacy } = world.save();
|
|
expect(migrateSave(legacy, () => 0)).toMatchObject({
|
|
fruitTrees: [],
|
|
dailyReset: null,
|
|
});
|
|
expect(() => world.restore({ ...world.save(), fruitTrees: [0] })).toThrow(
|
|
"Invalid native save",
|
|
);
|
|
expect(() => world.restore({ ...world.save(), fruitTrees: [1, 1] })).toThrow(
|
|
"Invalid native save",
|
|
);
|
|
});
|
|
|
|
it("walks from New Bark Town across Route 29 into Cherrygrove and a house", () => {
|
|
const assets = new CrystalAssets(new Uint8Array(readFileSync(romPath)));
|
|
// Random byte 255 never passes an encounter-rate check.
|
|
const world = new GameWorld(assets, () => new Date("2026-09-22T12:00:00"), () => 255);
|
|
world.state.party.push(
|
|
generatePartyMon(assets.pokemon, {
|
|
species: 155, level: 5, item: 0, otId: world.playerId,
|
|
ot: "CHRIS", dvs: 0x823b, caughtData: 0,
|
|
}),
|
|
);
|
|
// After the starter, Elm sets SCENE_NEWBARKTOWN_NOOP: the teacher lets you go.
|
|
world.restore({ ...world.save(), map: "24:4", x: 5, y: 8, scenes: { "24:4": 1 } });
|
|
const crossWest = (next: string) => {
|
|
const edge = Array.from({ length: world.map.height }, (_, y) => y).find((y) => {
|
|
try {
|
|
findPath(world, 0, y);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
})!;
|
|
walkTo(world, 0, edge);
|
|
expect(world.move("left"), world.notice).toBe(true);
|
|
settle(world);
|
|
expect(world.mapId).toBe(next);
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
};
|
|
crossWest(ROUTE_29);
|
|
// Route29TuscanyCallback hides Tuscany without the Zephyr Badge, even on
|
|
// a Tuesday (2026-09-22).
|
|
expect(world.objects().some((object) => object.id === 8)).toBe(false);
|
|
crossWest("26:3");
|
|
walkTo(world, 17, 8);
|
|
expect(world.move("up")).toBe(true);
|
|
settle(world);
|
|
expect(world.mapId).toBe("26:6");
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
const saved = world.save();
|
|
world.restore(saved);
|
|
expect(world.save()).toEqual(saved);
|
|
});
|
|
});
|