Pokémon that levelled up evolve after a won battle, following EvolveAfterBattle's checks over the ROM's evolution bytes, with the evolution screen that B can stop and move learning outside battle. The pack uses Rare Candy, stones (with ABLE/NOT ABLE labels) and vitamins, and its pocket tabs now sit in their own row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1af2beacdc
commit
45275ba83b
19 files changed
+1032
-39
No files matched your search
+217
-5
@@ -2,7 +2,7 @@ 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 { generatePartyMon, maxPP } from "../src/game/party/pokemon";
|
||||
import { calcStats, generatePartyMon, maxPP } from "../src/game/party/pokemon";
|
||||
import { ENGINE_FLAGS } from "../src/game/rom/profile";
|
||||
import { settle } from "./world-driver";
|
||||
|
||||
@@ -12,21 +12,30 @@ const romPath =
|
||||
const NEW_BARK_TOWN = "24:4",
|
||||
PLAYERS_HOUSE = "24:6",
|
||||
CYNDAQUIL = 155,
|
||||
PIDGEY = 16;
|
||||
QUILAVA = 156,
|
||||
PIDGEY = 16,
|
||||
PIKACHU = 25,
|
||||
RAICHU = 26;
|
||||
const BICYCLE = 7,
|
||||
ANTIDOTE = 9,
|
||||
FULL_RESTORE = 0x0e,
|
||||
POTION = 0x12,
|
||||
REPEL = 0x14,
|
||||
THUNDERSTONE = 0x17,
|
||||
HP_UP = 0x1a,
|
||||
RARE_CANDY = 0x20,
|
||||
REVIVE = 0x27,
|
||||
PP_UP = 0x3e,
|
||||
ETHER = 0x3f,
|
||||
ELIXER = 0x41,
|
||||
EVERSTONE = 0x70,
|
||||
ENERGYPOWDER = 0x79,
|
||||
LEFTOVERS = 0x92,
|
||||
FLOWER_MAIL = 0x9e,
|
||||
BERRY = 0xad;
|
||||
const CUT = 0x0f,
|
||||
GUST = 0x10,
|
||||
SAND_ATTACK = 0x1c,
|
||||
TACKLE = 0x21,
|
||||
LEER = 0x2b,
|
||||
SMOKESCREEN = 0x6c;
|
||||
@@ -520,7 +529,7 @@ describe.skipIf(!existsSync(romPath))("the start menu", () => {
|
||||
const world = fixture();
|
||||
world.state.bag.items.push(
|
||||
{ item: BERRY, quantity: 1 },
|
||||
{ item: RARE_CANDY, quantity: 1 },
|
||||
{ item: REPEL, quantity: 1 },
|
||||
);
|
||||
world.state.bag.balls = [{ item: 5, quantity: 3 }];
|
||||
open(world);
|
||||
@@ -529,11 +538,214 @@ describe.skipIf(!existsSync(romPath))("the start menu", () => {
|
||||
pickItem(world, 5);
|
||||
expect(menuItems(world)).toEqual(["GIVE", "TOSS", "QUIT"]);
|
||||
pick(world, "QUIT");
|
||||
pickItem(world, REPEL);
|
||||
pick(world, "USE");
|
||||
expect(world.notice).toBe("Using REPEL is not ported yet.");
|
||||
close(world);
|
||||
expect(world.state.bag.items.find((slot) => slot.item === REPEL)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("raises a level with a Rare Candy, forgetting a move to learn a new one", () => {
|
||||
const world = fixture();
|
||||
const pidgey = world.state.party[1];
|
||||
pidgey.moves = [TACKLE, CUT, LEER, GUST];
|
||||
pidgey.pp = [35, 30, 30, 35];
|
||||
pidgey.hp = 0;
|
||||
world.state.bag.items.push({ item: RARE_CANDY, quantity: 2 });
|
||||
const data = world.assets.pokemon;
|
||||
open(world);
|
||||
pick(world, "PACK");
|
||||
pickItem(world, RARE_CANDY);
|
||||
pick(world, "USE");
|
||||
expect(world.notice).toBe("Using RARE CANDY is not ported yet.");
|
||||
const pages = pick(world, "PIDGEY");
|
||||
expect(pages.slice(0, 2)).toEqual([
|
||||
"PIDGEY grew to level 5!",
|
||||
pages[1],
|
||||
]);
|
||||
expect(pages[1]).toMatch(/^ATTACK \d+ DEFENSE \d+ SPCL\.ATK \d+ SPCL\.DEF \d+ SPEED \d+$/);
|
||||
expect(pages.slice(2)).toEqual([
|
||||
"PIDGEY is trying to learn SAND-ATTACK.",
|
||||
"But PIDGEY can't learn more than four moves.",
|
||||
"Delete an older move to make room for SAND-ATTACK?",
|
||||
]);
|
||||
world.choose(1);
|
||||
expect(until(world)).toEqual(["Which move should be forgotten?"]);
|
||||
expect(menuItems(world)).toEqual(["TACKLE", "CUT", "LEER", "GUST"]);
|
||||
expect(pick(world, "CUT")).toEqual([
|
||||
"HM moves can't be forgotten now.",
|
||||
"Which move should be forgotten?",
|
||||
]);
|
||||
expect(pick(world, "LEER")).toEqual([
|
||||
"1, 2 and… Poof!",
|
||||
"PIDGEY forgot LEER.",
|
||||
"And…",
|
||||
"PIDGEY learned SAND-ATTACK!",
|
||||
]);
|
||||
close(world);
|
||||
expect(world.state.bag.items.find((slot) => slot.item === RARE_CANDY)).toBeTruthy();
|
||||
const mon = world.state.party[1];
|
||||
expect(mon.level).toBe(5);
|
||||
expect(mon.exp).toBe(data.expAtLevel(data.baseData(PIDGEY).growthRate, 5));
|
||||
expect(mon.moves).toEqual([TACKLE, CUT, SAND_ATTACK, GUST]);
|
||||
expect(mon.pp[2]).toBe(data.move(SAND_ATTACK).pp);
|
||||
// The Rare Candy adds the maximum HP's rise even to a fainted Pokémon.
|
||||
expect(mon.hp).toBe(mon.stats[0] - pidgey.stats[0]);
|
||||
expect(mon.hp).toBeGreaterThan(0);
|
||||
expect(world.state.bag.items.at(-1)).toEqual({ item: RARE_CANDY, quantity: 1 });
|
||||
});
|
||||
|
||||
it("can decline to learn the move", () => {
|
||||
const world = fixture();
|
||||
world.state.party[1].moves = [TACKLE, CUT, LEER, GUST];
|
||||
world.state.bag.items.push({ item: RARE_CANDY, quantity: 1 });
|
||||
open(world);
|
||||
pick(world, "PACK");
|
||||
pickItem(world, RARE_CANDY);
|
||||
pick(world, "USE");
|
||||
pick(world, "PIDGEY");
|
||||
world.choose(0);
|
||||
expect(until(world)).toEqual(["Stop learning SAND-ATTACK?"]);
|
||||
world.choose(1);
|
||||
expect(until(world)).toEqual(["PIDGEY did not learn SAND-ATTACK."]);
|
||||
close(world);
|
||||
expect(world.state.party[1].moves).toEqual([TACKLE, CUT, LEER, GUST]);
|
||||
expect(world.state.party[1].level).toBe(5);
|
||||
});
|
||||
|
||||
it("evolves after a Rare Candy, and B can stop it", () => {
|
||||
for (const stop of [false, true]) {
|
||||
const world = fixture();
|
||||
const data = world.assets.pokemon;
|
||||
world.state.party[0] = generatePartyMon(data, {
|
||||
species: CYNDAQUIL,
|
||||
level: 13,
|
||||
item: 0,
|
||||
otId: world.playerId,
|
||||
ot: "CHRIS",
|
||||
dvs: 0x9abc,
|
||||
caughtData: 0,
|
||||
});
|
||||
world.state.bag.items.push({ item: RARE_CANDY, quantity: 1 });
|
||||
open(world);
|
||||
pick(world, "PACK");
|
||||
pickItem(world, RARE_CANDY);
|
||||
pick(world, "USE");
|
||||
world.choose(0);
|
||||
// Acknowledge pages until the evolution animation starts.
|
||||
const pages: string[] = [];
|
||||
for (let i = 0; i < 200 && !world.evolution; i++) {
|
||||
const dialogue = world.dialogue;
|
||||
if (dialogue) {
|
||||
pages.push(dialogue.pages[dialogue.index].replace(/\n/g, " "));
|
||||
if (dialogue.holdTicks !== undefined) world.tick();
|
||||
else world.advanceDialogue();
|
||||
} else world.tick();
|
||||
}
|
||||
expect(pages[0]).toBe("CYNDAQUIL grew to level 14!");
|
||||
expect(pages.at(-1)).toBe("What? CYNDAQUIL is evolving!");
|
||||
expect(world.picture).toBe(CYNDAQUIL);
|
||||
// B does nothing before the flashes begin.
|
||||
world.advanceDialogue();
|
||||
expect(world.evolution).toBeDefined();
|
||||
for (let tick = 0; tick < 40; tick++) world.tick();
|
||||
if (stop) {
|
||||
world.advanceDialogue();
|
||||
expect(until(world)).toEqual(["Huh? CYNDAQUIL stopped evolving!"]);
|
||||
} else {
|
||||
expect(until(world)).toEqual([
|
||||
"Congratulations! Your CYNDAQUIL",
|
||||
"evolved into QUILAVA!",
|
||||
]);
|
||||
}
|
||||
close(world);
|
||||
const mon = world.state.party[0];
|
||||
expect(mon.level).toBe(14);
|
||||
expect([mon.species, mon.nickname]).toEqual(
|
||||
stop ? [CYNDAQUIL, "CYNDAQUIL"] : [QUILAVA, "QUILAVA"],
|
||||
);
|
||||
expect(mon.stats).toEqual(
|
||||
calcStats(data, mon.species, mon.dvs, 14, mon.statExp),
|
||||
);
|
||||
expect(world.state.pokedex.caught.has(QUILAVA)).toBe(!stop);
|
||||
expect(world.state.bag.items.some((slot) => slot.item === RARE_CANDY)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("evolves with a stone, which B cannot stop, unless held back by an Everstone", () => {
|
||||
const world = fixture();
|
||||
const data = world.assets.pokemon;
|
||||
world.state.party[1] = generatePartyMon(data, {
|
||||
species: PIKACHU,
|
||||
level: 10,
|
||||
item: EVERSTONE,
|
||||
otId: world.playerId,
|
||||
ot: "CHRIS",
|
||||
dvs: 0x9abc,
|
||||
caughtData: 0,
|
||||
});
|
||||
world.state.party[1].nickname = "SPARKY";
|
||||
world.state.bag.items.push({ item: THUNDERSTONE, quantity: 2 });
|
||||
open(world);
|
||||
pick(world, "PACK");
|
||||
pickItem(world, THUNDERSTONE);
|
||||
expect(pick(world, "USE")).toEqual(["Use on which POKéMON?"]);
|
||||
const party = world.dialogue?.choice;
|
||||
expect(party?.kind === "menu" && party.details).toEqual(["NOT ABLE", "ABLE"]);
|
||||
expect(pick(world, "CYNDAQUIL")).toEqual(["It won't have any effect."]);
|
||||
pickItem(world, THUNDERSTONE);
|
||||
pick(world, "USE");
|
||||
expect(pick(world, "SPARKY")).toEqual(["It won't have any effect."]);
|
||||
close(world);
|
||||
world.state.party[1].item = 0;
|
||||
open(world);
|
||||
pick(world, "PACK");
|
||||
pickItem(world, THUNDERSTONE);
|
||||
pick(world, "USE");
|
||||
world.choose(1);
|
||||
for (let i = 0; i < 20 && !world.evolution; i++) {
|
||||
if (world.dialogue?.holdTicks !== undefined) world.tick();
|
||||
else world.advanceDialogue();
|
||||
}
|
||||
for (let tick = 0; tick < 60; tick++) world.tick();
|
||||
world.advanceDialogue();
|
||||
expect(until(world)).toEqual([
|
||||
"Congratulations! Your SPARKY",
|
||||
"evolved into RAICHU!",
|
||||
]);
|
||||
close(world);
|
||||
const mon = world.state.party[1];
|
||||
// A nickname stays.
|
||||
expect([mon.species, mon.nickname]).toEqual([RAICHU, "SPARKY"]);
|
||||
expect(world.state.bag.items.at(-1)).toEqual({ item: THUNDERSTONE, quantity: 1 });
|
||||
});
|
||||
|
||||
it("raises stat experience with a vitamin, up to its limit", () => {
|
||||
const world = fixture();
|
||||
const data = world.assets.pokemon;
|
||||
const mon = world.state.party[0];
|
||||
const hp = mon.hp,
|
||||
happiness = mon.happiness;
|
||||
world.state.bag.items.push({ item: HP_UP, quantity: 2 });
|
||||
open(world);
|
||||
pick(world, "PACK");
|
||||
pickItem(world, HP_UP);
|
||||
pick(world, "USE");
|
||||
expect(pick(world, "CYNDAQUIL")).toEqual(["CYNDAQUIL's HEALTH rose."]);
|
||||
close(world);
|
||||
const raised = world.state.party[0];
|
||||
expect(raised.statExp[0]).toBe(2560);
|
||||
expect(raised.stats).toEqual(calcStats(data, CYNDAQUIL, raised.dvs, 8, raised.statExp));
|
||||
expect(raised.hp).toBe(hp);
|
||||
expect(raised.happiness).toBeGreaterThan(happiness);
|
||||
|
||||
raised.statExp[0] = 100 << 8;
|
||||
open(world);
|
||||
pick(world, "PACK");
|
||||
pickItem(world, HP_UP);
|
||||
pick(world, "USE");
|
||||
expect(pick(world, "CYNDAQUIL")).toEqual(["It won't have any effect."]);
|
||||
close(world);
|
||||
expect(world.state.party[0].statExp[0]).toBe(100 << 8);
|
||||
expect(world.state.bag.items.at(-1)).toEqual({ item: HP_UP, quantity: 1 });
|
||||
});
|
||||
|
||||
it("saves the state from before the menu while it is open", () => {
|
||||
|
||||
Reference in new issue
Block a user