Port evolution, Rare Candy, evolution stones and vitamins
Checks / web (push) Canceled after 0s

Pokémon that levelled up evolve after a won battle, following
EvolveAfterBattle's checks over the ROM's evolution bytes, with the
evolution screen that B can stop and move learning outside battle. The pack
uses Rare Candy, stones (with ABLE/NOT ABLE labels) and vitamins, and its
pocket tabs now sit in their own row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sébastien QUEROLandClaude Opus 5.5 committed 2026-09-30 16:21:59 +02:00
1 parent 1af2beacdc
commit 45275ba83b
19 files changed
+1032 -39

No files matched your search

+111 -4
View File
@@ -28,6 +28,7 @@ import {
} from "../scripts/interpreter";
import { IntroRun } from "../scripts/intro";
import { StartMenuRun, type MenuCursors } from "../scripts/start-menu";
import { PartyEvolution } from "../scripts/evolution";
import type { MovementPose, MovementStep } from "../scripts/movement";
import { emptyBag, validBag, type Bag, type ItemSlot } from "../party/bag";
import {
@@ -254,6 +255,26 @@ export interface BattleView {
/** Yes/No cursor, as on the original boxes. */
selected: 0 | 1;
}
/** EvolutionAnimation in overworld ticks (two frames each): 80 frames on
* the old species, then .AnimationSequence's seven rounds of a B-checking
* wait (14, 12 … 2 frames) followed by that many flashes of the new and
* old pictures (about four frames each). */
function evolutionSchedule(from: number, to: number) {
const frames: { species: number; cancel: boolean }[] = [];
const show = (species: number, ticks: number, cancel = false) => {
for (let i = 0; i < ticks; i++) frames.push({ species, cancel });
};
show(from, 40);
for (let round = 1; round <= 7; round++) {
show(from, 8 - round, true);
for (let flash = 0; flash < round; flash++) {
show(to, 2);
show(from, 2);
}
}
return frames;
}
/** Ticks a `done` battle message stays before the battle continues. */
const BATTLE_MESSAGE_TICKS = 30;
@@ -288,6 +309,12 @@ interface ActiveScript {
warp?: { map: string; x: number; y: number; facing?: Direction };
/** warpcheck: the player's tile events run on the tick after the script. */
warpCheck?: boolean;
/** Routines that run before the script resumes with `result`: the
* evolutions ExitBattle starts after a won battle. */
interlude?: {
run: Generator<ScriptEffect, unknown, number | string | undefined>;
result: number;
};
/** LoadMemScript: queued for the next PlayerEvents once this commits. */
memScript?: MemScript;
/** The trainer whose sight started this script, if any. */
@@ -479,6 +506,17 @@ export class GameWorld {
picture: number | undefined;
/** The introduction's trainer or player picture, if any. */
portrait: Portrait | undefined;
/** An evolution animation in progress: the frame shown on each tick, and
* whether B may stop it then. */
evolution:
| {
from: number;
to: number;
cancelable: boolean;
step: number;
schedule: { species: number; cancel: boolean }[];
}
| undefined;
/** True from a new game until the introduction finishes. */
private introPending = false;
/** The battle in progress, if any. */
@@ -1609,6 +1647,14 @@ export class GameWorld {
this.pump();
}
private evolutionHost() {
return {
reader: this.assets.reader,
data: this.assets.pokemon,
timeOfDay: () => this.clockTimeOfDay,
text: () => this.host(PLAYER).text(),
};
}
/** Start in PlayerEvents (StartMenuScript): the start menu opens while
* the player stands still. Its changes commit when it closes. */
openStartMenu(): boolean {
@@ -1622,6 +1668,8 @@ export class GameWorld {
data: this.assets.pokemon,
text: () => this.host(PLAYER).text(),
bicycleScript: (state) => this.bicycleScript(state),
timeOfDay: () => this.clockTimeOfDay,
landmark: () => this.assets.map(this.mapId).landmark,
},
this.menuCursors,
),
@@ -1679,8 +1727,21 @@ export class GameWorld {
if (!script) return;
try {
for (;;) {
const effect = script.run.resume(input);
input = undefined;
let effect: ScriptEffect | undefined;
const interlude = script.interlude;
if (interlude) {
const next = interlude.run.next(input);
input = undefined;
if (next.done) {
script.interlude = undefined;
input = interlude.result;
continue;
}
effect = next.value;
} else {
effect = script.run.resume(input);
input = undefined;
}
script.effect = effect;
if (!effect) {
this.finishScript();
@@ -1955,6 +2016,16 @@ export class GameWorld {
},
};
return true;
case "evolve":
this.picture = effect.from;
this.evolution = {
from: effect.from,
to: effect.to,
cancelable: effect.cancelable,
step: 0,
schedule: evolutionSchedule(effect.from, effect.to),
};
return true;
case "pack":
this.dialogue = {
pages: [""],
@@ -2350,6 +2421,7 @@ export class GameWorld {
this.emote = undefined;
this.picture = undefined;
this.portrait = undefined;
this.evolution = undefined;
if (script.sighted !== undefined)
this.refusedSighting = {
map: this.mapId,
@@ -2380,9 +2452,19 @@ export class GameWorld {
);
this.battle = undefined;
this.battleRun = undefined;
// CleanUpBattleRAM
// ExitBattle: EvolveAfterBattle after a win, then CleanUpBattleRAM.
const { result, evolvable } = next.value;
this.menuCursors = { start: 0, party: 0, pocket: 0 };
this.pump(next.value.result);
if (this.script && evolvable && !(result & 0x0f)) {
this.script.interlude = {
run: new PartyEvolution(
this.script.run.state,
this.evolutionHost(),
).evolveAfterBattle(evolvable),
result,
};
this.pump();
} else this.pump(result);
return;
}
const effect = next.value;
@@ -2793,6 +2875,13 @@ export class GameWorld {
}
const script = this.script;
if (script) {
const evolution = this.evolution;
if (evolution) {
const frame = evolution.schedule[++evolution.step];
if (frame) this.picture = frame.species;
else this.finishEvolution(false);
return;
}
const dialogue = this.dialogue;
if (dialogue) {
if (
@@ -3198,9 +3287,25 @@ export class GameWorld {
)
state.playerState = OW.PLAYER_NORMAL;
}
/** EvolutionAnimation ends: the new species, or the old one if B
* stopped it. */
private finishEvolution(stopped: boolean) {
const evolution = this.evolution!;
this.evolution = undefined;
this.picture = stopped ? evolution.from : evolution.to;
this.pump(stopped ? 1 : 0);
}
/** B, or A on a page without a choice. */
advanceDialogue() {
if (this.battle) return this.battleB();
const evolution = this.evolution;
if (evolution) {
// .WaitFrames_CheckPressedB: only between flashes, and never for a
// forced (evolution stone) evolution.
if (evolution.cancelable && evolution.schedule[evolution.step]?.cancel)
this.finishEvolution(true);
return;
}
const dialogue = this.dialogue;
if (!dialogue || !this.script) return;
const last = dialogue.index === dialogue.pages.length - 1;
@@ -3587,9 +3692,11 @@ export class GameWorld {
this.emote = undefined;
this.picture = undefined;
this.portrait = undefined;
this.evolution = undefined;
this.battle = undefined;
this.battleRun = undefined;
this.memScript = undefined;
this.menuCursors = { start: 0, party: 0, pocket: 0 };
this.clearNotice();
this.introPending = false;
// A save made during the introduction starts it again.