Files
Crystal/tests/battle-driver.ts
T
Sébastien QUEROLandClaude Opus 5.5 b13e09ec20 Port the remaining battle items, the stats screen and hBattleTurn at the menu
- Run every non-ball item effect usable in battle from the ROM's
  ItemEffects: Full Restore, revives, bitter medicine, Bitter Berry, Ethers
  and Elixers, X items, X Accuracy, Guard Spec, Dire Hit and Poké Doll.
- Read happiness changes from HappinessChanges, including the larger
  level-up gain where a Pokémon was caught.
- Model hBattleTurn at the battle menu, including the trainer AI's writes,
  and drop the status-heal refusal.
- Port the stats screen for battles and Bill's PC.
- Compare the send-out and withdraw texts, RestoreHealth, RestorePP,
  ChangeHappiness and CalcPlayerStats with the original routines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:24:55 +02:00

137 lines
3.8 KiB
TypeScript

import type { CrystalAssets } from "../src/game/rom/assets";
import type { TrainerData } from "../src/game/rom/trainers";
import type {
Battle,
BattleEffect,
BattleInput,
BattleOptions,
} from "../src/game/battle/battle";
import type { Bag } from "../src/game/party/bag";
import { generatePartyMon, type PartyMon } from "../src/game/party/pokemon";
import { defaultTextContext } from "../src/game/scripts/text";
export const partyMon = (
assets: CrystalAssets,
species: number,
level: number,
): PartyMon =>
generatePartyMon(assets.pokemon, {
species,
level,
item: 0,
otId: 1234,
ot: "CHRIS",
dvs: 0x823b,
caughtData: 0,
});
/** Battle options for a wild Pokémon or a ROM trainer, with a seeded
* stand-in for the hardware Random routine. */
export function battleOptions(
assets: CrystalAssets,
trainers: TrainerData,
party: PartyMon[],
enemy:
| { wild: { species: number; level: number } }
| { trainer: [number, number] },
{ seed = 1, bag }: { seed?: number; bag?: Bag } = {},
): BattleOptions {
const base = {
data: assets.pokemon,
party,
bag,
playerId: 1234,
playerName: "CHRIS",
timeOfDay: 1,
pokedex: { seen: new Set<number>(), caught: new Set<number>() },
random: () => {
seed = (seed * 1103515245 + 12345) >>> 0;
return (seed >>> 16) & 255;
},
text: () => defaultTextContext("CHRIS"),
};
if ("wild" in enemy) return { ...base, wild: enemy.wild };
const trainer = trainers.trainer(...enemy.trainer);
return {
...base,
trainer: {
name: trainer.name,
className: trainer.className,
party: trainers.createParty(trainer),
aiFlags: trainer.attributes.aiMoveFlags,
switchFlags: trainer.attributes.aiItemSwitchFlags,
reward: trainer.reward,
},
};
}
/** Run a battle. Menus take queued answers per prompt kind, then the
* defaults: the first move, YES, cancelling a move to forget, and the
* first party slot or move slot. */
export function playBattle(
battle: Battle,
queued: Partial<Record<BattleEffect["kind"], BattleInput[]>> = {},
) {
const texts: string[] = [],
prompts: BattleEffect[] = [];
const run = battle.run();
let input: BattleInput;
for (let step = 0; step < 3000; step++) {
const next = run.next(input);
if (next.done) return { texts, prompts, result: next.value.result };
const effect = next.value;
input = undefined;
if (effect.kind === "text") texts.push(effect.text.replace(/\n/g, " "));
else if (effect.kind !== "hud") {
prompts.push(effect);
const answer = queued[effect.kind]?.shift();
input =
answer !== undefined
? answer
: effect.kind === "action"
? { action: "fight", move: 0 }
: effect.kind === "yesno"
? 1
: effect.kind === "forget"
? 4
: effect.kind === "party" || effect.kind === "move"
? 0
: undefined;
}
}
throw new Error("Battle did not end.");
}
/** Step a battle by hand: texts are collected up to each prompt, and
* `answer` feeds the current prompt's input. */
export function battleSteps(battle: Battle) {
const run = battle.run();
const texts: string[] = [];
let next = run.next();
const collect = () => {
while (
!next.done &&
(next.value.kind === "text" || next.value.kind === "hud")
) {
if (next.value.kind === "text")
texts.push(next.value.text.replace(/\n/g, " "));
next = run.next();
}
};
collect();
return {
texts,
get prompt(): BattleEffect | undefined {
return next.done ? undefined : next.value;
},
get done() {
return Boolean(next.done);
},
answer(input: BattleInput) {
next = run.next(input);
collect();
return this;
},
};
}