- Enable Route 29, its Route 46 gate, Cherrygrove City and its buildings. - Run item balls through FindItemInBallScript and fruit trees through FruitTreeScript, with a native daily reset of the daily engine flags. - Port the Pokémon Center nurse (HealParty with the PP Up clamp, CheckPokerus, phone-call checks) and record the whiteout spawn on entering a Pokémon Center, as EnterMapWarp does. - Port standard Poké Marts with keyboard, D-pad and pointer menus. - Add the recoil battle command (Struggle) and VAR_BOXSPACE. - Save fruit trees, the daily reset day and the last spawn map; older version-3 backups migrate by absence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
11d1153802
commit
796fe8f6d5
24 files changed
+1549
-59
No files matched your search
@@ -0,0 +1,137 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { CrystalAssets } from "../src/game/rom/assets";
|
||||
import { C } from "../src/game/battle/constants";
|
||||
import { martFixtureWorld, talkToClerk } from "./mart-fixture";
|
||||
import { settle } from "./world-driver";
|
||||
|
||||
const romPath = process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc";
|
||||
const fixture = () => martFixtureWorld(new CrystalAssets(new Uint8Array(readFileSync(romPath))));
|
||||
|
||||
// constants/item_constants.asm
|
||||
const POKE_BALL = 5,
|
||||
BICYCLE = 7,
|
||||
POTION = 18;
|
||||
|
||||
describe.skipIf(!existsSync(romPath))("Cherrygrove Mart with the local ROM", () => {
|
||||
it("buys Potions at the ROM price and adds them to the bag", () => {
|
||||
const world = fixture();
|
||||
const money = world.state.money;
|
||||
talkToClerk(world);
|
||||
// BUY -> Potion (list index 0) -> quantity 3 -> confirm YES -> back -> QUIT
|
||||
const shown = settle(world, [0, 0, 3, 1, -1, 2]);
|
||||
expect(world.notice, world.diagnostic).toBe("");
|
||||
expect(shown.join("\n")).toContain("Here you are");
|
||||
expect(world.state.money).toBe(money - 300 * 3);
|
||||
expect(world.state.bag.items).toEqual([{ item: POTION, quantity: 3 }]);
|
||||
});
|
||||
|
||||
it("refuses to buy without enough money, changing nothing", () => {
|
||||
const world = fixture();
|
||||
world.state.money = 100; // less than one Potion (300)
|
||||
talkToClerk(world);
|
||||
const committed = world.save();
|
||||
const shown = settle(world, [0, 0, 1, 1, -1, 2]);
|
||||
expect(world.notice, world.diagnostic).toBe("");
|
||||
expect(shown.join("\n")).toMatch(/enough money/i);
|
||||
expect(world.save()).toEqual(committed);
|
||||
});
|
||||
|
||||
it("refuses to buy into a full pocket, changing nothing", () => {
|
||||
const world = fixture();
|
||||
const data = world.assets.pokemon;
|
||||
const fillers: number[] = [];
|
||||
for (let id = 1; fillers.length < 20; id++) {
|
||||
if (id === POTION) continue;
|
||||
try {
|
||||
if (data.itemPocket(id) === "item") fillers.push(id);
|
||||
} catch {
|
||||
// Not every id names an item; skip gaps.
|
||||
}
|
||||
}
|
||||
world.state.bag.items = fillers.map((item) => ({ item, quantity: 1 }));
|
||||
talkToClerk(world);
|
||||
const committed = world.save();
|
||||
const shown = settle(world, [0, 0, 1, 1, -1, 2]);
|
||||
expect(world.notice, world.diagnostic).toBe("");
|
||||
expect(shown.join("\n")).toMatch(/can't carry/i);
|
||||
expect(world.save()).toEqual(committed);
|
||||
});
|
||||
|
||||
it("sells items at half the ROM price", () => {
|
||||
const world = fixture();
|
||||
world.state.bag.items = [{ item: POTION, quantity: 5 }];
|
||||
talkToClerk(world);
|
||||
const money = world.state.money;
|
||||
// SELL -> Potion (index 0) -> quantity 2 -> confirm YES -> back -> QUIT
|
||||
const shown = settle(world, [1, 0, 2, 1, -1, 2]);
|
||||
expect(world.notice, world.diagnostic).toBe("");
|
||||
expect(shown.join("\n")).toContain("Got ¥300");
|
||||
expect(world.state.money).toBe(money + 300); // 300/2 * 2
|
||||
expect(world.state.bag.items).toEqual([{ item: POTION, quantity: 3 }]);
|
||||
});
|
||||
|
||||
it("caps money at C.MAX_MONEY when selling", () => {
|
||||
const world = fixture();
|
||||
world.state.bag.items = [{ item: POTION, quantity: 1 }];
|
||||
world.state.money = C.MAX_MONEY - 10;
|
||||
talkToClerk(world);
|
||||
const shown = settle(world, [1, 0, 1, 1, -1, 2]);
|
||||
expect(world.notice, world.diagnostic).toBe("");
|
||||
expect(shown.join("\n")).not.toBe("");
|
||||
expect(world.state.money).toBe(C.MAX_MONEY);
|
||||
expect(world.state.bag.items).toEqual([]);
|
||||
});
|
||||
|
||||
it("excludes key items from the sell list", () => {
|
||||
const world = fixture();
|
||||
world.state.bag.items = [{ item: POTION, quantity: 1 }];
|
||||
world.state.bag.keyItems = [BICYCLE];
|
||||
talkToClerk(world);
|
||||
world.choose(1); // SELL
|
||||
const choice = world.dialogue?.choice;
|
||||
expect(choice?.kind).toBe("martlist");
|
||||
if (choice?.kind === "martlist") {
|
||||
expect(choice.entries.map((entry) => entry.item)).toEqual([POTION]);
|
||||
}
|
||||
});
|
||||
|
||||
it("sells a Poké Ball but refuses to sell key items or unaffordable buys together", () => {
|
||||
// MART_CHERRYGROVE_DEX stocks a Poké Ball once the mystery egg is given;
|
||||
// exercising a ball-pocket item confirms pockets beyond "item" work too.
|
||||
const world = fixture();
|
||||
world.state.events.add(31); // EVENT_GAVE_MYSTERY_EGG_TO_ELM
|
||||
talkToClerk(world);
|
||||
const money = world.state.money;
|
||||
// BUY -> Poké Ball (list index 0 in the dex mart) -> quantity 1 -> YES -> back -> QUIT
|
||||
settle(world, [0, 0, 1, 1, -1, 2]);
|
||||
expect(world.notice, world.diagnostic).toBe("");
|
||||
expect(world.state.money).toBe(money - 200);
|
||||
expect(world.state.bag.balls).toEqual([{ item: POKE_BALL, quantity: 1 }]);
|
||||
});
|
||||
|
||||
it("quits immediately without changing state", () => {
|
||||
const world = fixture();
|
||||
const before = world.save();
|
||||
talkToClerk(world);
|
||||
settle(world, [2]);
|
||||
expect(world.notice, world.diagnostic).toBe("");
|
||||
expect(world.save()).toEqual(before);
|
||||
});
|
||||
|
||||
it("a mid-interaction save restores the committed state and replays it", () => {
|
||||
const world = fixture();
|
||||
talkToClerk(world);
|
||||
const committed = world.save();
|
||||
world.choose(0); // BUY
|
||||
world.choose(0); // Potion -> quantity picker
|
||||
expect(world.dialogue?.choice?.kind).toBe("martquantity");
|
||||
expect(world.save()).toEqual(committed);
|
||||
world.restore(committed);
|
||||
expect(world.dialogue).toBeUndefined();
|
||||
expect(world.save()).toEqual(committed);
|
||||
// The world is usable again: a fresh visit still works.
|
||||
talkToClerk(world);
|
||||
expect(world.dialogue?.choice?.kind).toBe("martmenu");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user