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(), caught: new Set() }, 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> = {}, ) { 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; }, }; }