- 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>
137 lines
3.8 KiB
TypeScript
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;
|
|
},
|
|
};
|
|
}
|