Run the forced-shiny, trap, forced-item, Celebi, Suicune, roaming and Bug-Catching Contest battle types. Roaming battles keep their struct's DVs and HP byte, and every other battle makes CleanUpBattleRAM's roaming roll. The contest shows PARKBALL×, counts down Park Balls, ends when they run out, keeps its catch outside the party and asks before switching. Scripts load the special types with loadvar, and while a contest runs, encounters use BugCatchingContestBattleScript and ContestMons. Validated with 596 unit tests, 155 browser tests (including the contest menu and switch screen), native/reference builds and reference/vendor/release checks. National Park's contest event, the roaming release and the Ilex Forest Celebi event come with their maps. Closes #28 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b547ca29ff
commit
8fae1f0c0f
15 files changed
+787
-42
No files matched your search
+45
-7
@@ -11,6 +11,7 @@ import { LocalMapView } from "./game/rendering/minimap";
|
||||
import type { Choice, GameWorld } from "./game/world/world";
|
||||
import type { PokegearCard } from "./game/scripts/pokegear";
|
||||
import { drawTile } from "./game/rendering/pixels";
|
||||
import { C } from "./game/battle/constants";
|
||||
import { PokemonData } from "./game/rom/pokemon";
|
||||
import type { Direction, Palette } from "./game/world/types";
|
||||
import type { Button } from "./input/types";
|
||||
@@ -589,6 +590,9 @@ function handleFieldInput(button: Button): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Battle prompts answered with YES/NO. */
|
||||
const yesNoPrompt = (prompt?: { kind: string }) =>
|
||||
prompt?.kind === "yesno" || prompt?.kind === "contestswitch";
|
||||
function battleButtons() {
|
||||
return [
|
||||
...get("battle-menu").querySelectorAll<HTMLButtonElement>(
|
||||
@@ -636,7 +640,7 @@ function handleBattleInput(button: Button): boolean {
|
||||
if (["up", "down", "left", "right"].includes(button)) {
|
||||
const delta = button === "up" || button === "left" ? -1 : 1;
|
||||
battleCursor = (battleCursor + delta + buttons.length) % buttons.length;
|
||||
if (world.battle.prompt?.kind === "yesno")
|
||||
if (yesNoPrompt(world.battle.prompt))
|
||||
world.battle.selected = battleCursor === 0 ? 1 : 0;
|
||||
highlightBattleChoice();
|
||||
return true;
|
||||
@@ -699,7 +703,9 @@ function showBattle(world: GameWorld) {
|
||||
get("battle-text").textContent =
|
||||
prompt?.kind === "name"
|
||||
? prompt.title
|
||||
: prompt?.kind === "party" || prompt?.kind === "move"
|
||||
: prompt?.kind === "party" ||
|
||||
prompt?.kind === "move" ||
|
||||
prompt?.kind === "contestswitch"
|
||||
? prompt.text
|
||||
: (view.message?.text ?? "");
|
||||
if (prompt?.kind !== "action") showMoves = showPack = false;
|
||||
@@ -712,7 +718,7 @@ function showBattle(world: GameWorld) {
|
||||
battleCursor = 0;
|
||||
battleSelectionKey = selectionKey;
|
||||
}
|
||||
if (prompt?.kind === "yesno") battleCursor = view.selected === 1 ? 0 : 1;
|
||||
if (yesNoPrompt(prompt)) battleCursor = view.selected === 1 ? 0 : 1;
|
||||
const bag = world.liveState.bag;
|
||||
// The Pokémon out has its live battle HP; the others their party values.
|
||||
const party = world.liveState.party.map((mon, index) =>
|
||||
@@ -846,13 +852,45 @@ function showBattle(world: GameWorld) {
|
||||
showWorld(world);
|
||||
}),
|
||||
button("PKMN", () => runtime.battleAction({ action: "pkmn" })),
|
||||
button("PACK", () => {
|
||||
showPack = true;
|
||||
showWorld(world);
|
||||
}),
|
||||
// ContestBattleMenu: PARKBALL× with two digits throws a Park Ball.
|
||||
prompt.parkBalls !== undefined
|
||||
? button(
|
||||
`PARKBALL×${String(prompt.parkBalls).padStart(2, "0")}`,
|
||||
() =>
|
||||
runtime.battleAction({
|
||||
action: "item",
|
||||
item: C.PARK_BALL,
|
||||
slot: -1,
|
||||
}),
|
||||
)
|
||||
: button("PACK", () => {
|
||||
showPack = true;
|
||||
showWorld(world);
|
||||
}),
|
||||
button("RUN", () => runtime.battleAction({ action: "run" })),
|
||||
);
|
||||
}
|
||||
} else if (prompt?.kind === "contestswitch") {
|
||||
// DisplayCaughtContestMonStats: STOCK and THIS boxes, then Yes/No.
|
||||
for (const [label, mon] of [
|
||||
["STOCK", prompt.stock],
|
||||
["THIS", prompt.caught],
|
||||
] as const)
|
||||
buttons.push(
|
||||
button(`${label} ${mon.name} :L${mon.level}`, () => {}, {
|
||||
detail: `HEALTH ${mon.maxHp}`,
|
||||
disabled: " ",
|
||||
wide: true,
|
||||
}),
|
||||
);
|
||||
buttons.push(
|
||||
button("YES", () => runtime.battleChoose(1), {
|
||||
pressed: view.selected === 1,
|
||||
}),
|
||||
button("NO", () => runtime.battleChoose(0), {
|
||||
pressed: view.selected === 0,
|
||||
}),
|
||||
);
|
||||
} else if (prompt?.kind === "yesno") {
|
||||
buttons.push(
|
||||
button("YES", () => runtime.battleChoose(1), {
|
||||
|
||||
Reference in new issue
Block a user