This commit is contained in:
1 parent
6a74f59e46
commit
3e92936cb9
20 files changed
+1417
-32
No files matched your search
@@ -0,0 +1,151 @@
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { CrystalAssets } from "../src/game/rom/assets";
|
||||
import { TrainerData } from "../src/game/rom/trainers";
|
||||
import {
|
||||
Battle,
|
||||
UnsupportedBattle,
|
||||
type BattleInput,
|
||||
type BattleOptions,
|
||||
} from "../src/game/battle/battle";
|
||||
import { generatePartyMon } from "../src/game/party/pokemon";
|
||||
import { defaultTextContext } from "../src/game/scripts/text";
|
||||
|
||||
const romPath =
|
||||
process.env.CRYSTAL_ROM_PATH ??
|
||||
"Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc";
|
||||
describe.skipIf(!existsSync(romPath))("single-Pokémon trainer battles", () => {
|
||||
let assets: CrystalAssets, trainers: TrainerData;
|
||||
beforeAll(() => {
|
||||
assets = new CrystalAssets(new Uint8Array(readFileSync(romPath)));
|
||||
trainers = new TrainerData(assets.reader);
|
||||
});
|
||||
const setup = (level = 15) => {
|
||||
const trainer = trainers.trainer(22, 1); // Youngster Joey, level 4 Rattata.
|
||||
const mon = generatePartyMon(assets.pokemon, {
|
||||
species: 155,
|
||||
level,
|
||||
item: 0,
|
||||
otId: 1234,
|
||||
ot: "CHRIS",
|
||||
dvs: 0x823b,
|
||||
caughtData: 0,
|
||||
});
|
||||
let seed = 1;
|
||||
const options: BattleOptions = {
|
||||
data: assets.pokemon,
|
||||
party: [mon],
|
||||
playerId: 1234,
|
||||
playerName: "CHRIS",
|
||||
timeOfDay: 1,
|
||||
pokedex: { seen: new Set(), caught: new Set() },
|
||||
random: () => {
|
||||
seed = (seed * 1103515245 + 12345) >>> 0;
|
||||
return (seed >>> 16) & 255;
|
||||
},
|
||||
text: () => defaultTextContext("CHRIS"),
|
||||
trainer: {
|
||||
name: trainer.name,
|
||||
className: trainer.className,
|
||||
party: trainers.createParty(trainer),
|
||||
aiFlags: trainer.attributes.aiMoveFlags,
|
||||
reward: trainer.reward,
|
||||
},
|
||||
};
|
||||
return { mon, options };
|
||||
};
|
||||
const finish = (battle: Battle, runFirst = false) => {
|
||||
const gen = battle.run();
|
||||
const texts: string[] = [];
|
||||
let input: BattleInput,
|
||||
triedRun = false;
|
||||
for (let guard = 0; guard < 2000; guard++) {
|
||||
const next = gen.next(input);
|
||||
if (next.done)
|
||||
return { ...next.value, texts: texts.join(" ").replace(/\n/g, " ") };
|
||||
const effect = next.value;
|
||||
input = undefined;
|
||||
if (effect.kind === "text") texts.push(effect.text);
|
||||
if (effect.kind === "action") {
|
||||
input =
|
||||
runFirst && !triedRun
|
||||
? { action: "run" }
|
||||
: { action: "fight", move: 0 };
|
||||
triedRun = true;
|
||||
}
|
||||
if (effect.kind === "yesno") input = 0;
|
||||
if (effect.kind === "forget") input = 4;
|
||||
}
|
||||
throw new Error("Battle did not finish.");
|
||||
};
|
||||
it("announces the trainer, refuses escape without spending a turn, and awards trainer experience and money", () => {
|
||||
const direct = setup(),
|
||||
escape = setup();
|
||||
const before = direct.mon.exp;
|
||||
const won = finish(new Battle(direct.options));
|
||||
const escaped = finish(new Battle(escape.options), true);
|
||||
expect(won.result).toBe(0);
|
||||
expect(won.prizeMoney).toBe(64);
|
||||
expect(won.texts).toContain("YOUNGSTER JOEY wants to battle!");
|
||||
expect(won.texts).toContain("sent out RATTATA!");
|
||||
expect(won.texts).toContain("was defeated!");
|
||||
expect(won.texts).toContain("CHRIS got ¥64");
|
||||
expect(escaped.texts).toContain("trainer battle");
|
||||
expect(escape.mon).toEqual(direct.mon);
|
||||
expect(direct.mon.exp - before).toBe(48); // floor(57 * 4 / 7), then 1.5x.
|
||||
expect(direct.options.pokedex.seen.has(19)).toBe(true);
|
||||
});
|
||||
it("doubles prize money when the active Pokémon holds an Amulet Coin", () => {
|
||||
const { mon, options } = setup();
|
||||
mon.item = 0x5b;
|
||||
const result = finish(new Battle(options));
|
||||
expect(result.prizeMoney).toBe(128);
|
||||
expect(result.texts).toContain("CHRIS got ¥128");
|
||||
});
|
||||
it("scores exhausted trainer moves before the menu, then uses Struggle", () => {
|
||||
const { options } = setup();
|
||||
options.trainer!.party[0].pp.fill(0);
|
||||
let draws = 0;
|
||||
options.random = () => { draws++; return 0; };
|
||||
const battle = new Battle(options), run = battle.run();
|
||||
for (let guard = 0; guard < 20; guard++) {
|
||||
const next = run.next();
|
||||
if (!next.done && next.value.kind === "action") break;
|
||||
}
|
||||
expect(draws).toBeGreaterThan(0);
|
||||
// The AI selected a normal move despite exhausted PP; ParseEnemyAction
|
||||
// replaces it before the first attack, as the cartridge does.
|
||||
expect(battle.enemy.curMove).toBe(33);
|
||||
run.next({ action: "fight", move: 0 });
|
||||
expect(battle.enemy.curMove).toBe(165);
|
||||
});
|
||||
it("loses without receiving prize money", () => {
|
||||
const { mon, options } = setup(2);
|
||||
mon.hp = 1;
|
||||
const lost = finish(new Battle(options));
|
||||
expect(lost.result).toBe(1);
|
||||
expect(lost.prizeMoney).toBeUndefined();
|
||||
expect(mon.hp).toBe(0);
|
||||
});
|
||||
it("heals the party after a CANLOSE victory", () => {
|
||||
const { mon, options } = setup();
|
||||
options.canLose = true;
|
||||
mon.hp--;
|
||||
finish(new Battle(options));
|
||||
expect(mon.hp).toBe(mon.stats[0]);
|
||||
expect(mon.status).toBe(0);
|
||||
expect(mon.pp[0]).toBe(assets.pokemon.move(mon.moves[0]).pp);
|
||||
});
|
||||
it("refuses unported party switching and AI before mutating the party or Pokédex", () => {
|
||||
for (const kind of ["party", "ai"] as const) {
|
||||
const { options } = setup();
|
||||
if (kind === "party")
|
||||
options.trainer!.party.push(structuredClone(options.trainer!.party[0]));
|
||||
else options.trainer!.aiFlags |= 1 << 4;
|
||||
const before = structuredClone(options.party);
|
||||
expect(() => new Battle(options).run().next()).toThrow(UnsupportedBattle);
|
||||
expect(options.party).toEqual(before);
|
||||
expect(options.pokedex.seen.size).toBe(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user