Finish the opening milestone's switching, items and acceptance run
- Decode evolution lists from the ROM; all 251 match the pinned PRET source. - Compare CalcMonStats and CalcExpAtLevel against the original routines in the reference emulator. - Add a continuous opening playthrough from the intro to the catching tutorial, reloading a JSON save after every stage. - Port player switching in battle: the PKMN menu, forced choices after a faint, Shift mode's offer and double faints, with the original texts. - Port HP and status healing items in battle through the ROM's ItemEffects, HealingHPAmounts and StatusHealingActions tables. - Add the battle party menu and the Items pocket to the UI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
eb03a20d8f
commit
f8e0f9c65a
20 files changed
+1902
-96
No files matched your search
@@ -0,0 +1,103 @@
|
||||
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. */
|
||||
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"
|
||||
? 0
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
throw new Error("Battle did not end.");
|
||||
}
|
||||
Reference in new issue
Block a user