Open Mr. Pokémon's house, the first rival battle and the return to Elm
Checks / web (push) Canceled after 0s

Mr. Pokémon's ROM scene gives the Mystery Egg and Pokédex, heals the
party, sets the Cherrygrove spawn with blackoutmod and queues Elm's
call. Cherrygrove's rival scene runs both triggers and all three
RIVAL1_1 parties as a CANLOSE battle; reloadmap returns to the script.
Smart AI is accepted for effects with no AI_Smart_EffectHandlers entry
(Normal Hit, Attack Down, Defense Down) and still refuses others before
any random draw, including for reserve Pokémon.

Special calls read SpecialPhoneCallList: the entry's condition decides,
and the ROM's CheckSpecialPhoneCall.script and Script_ReceivePhoneCall
run its caller script through memcall, ending with HangUp's timed
"Click!". Ringing and the caller box are not presented yet.

In Elm's lab, NameRival shows the ROM's "RIVAL'S NAME?" title (the
prompt previously had no page and soft-locked) and applies InitName's
default to blank names. Handing over the Egg and the aide's Poké Balls
run from the ROM. Saves gain rivalName and specialPhoneCall.

The quest fixtures use a seeded RNG (a constant byte never ends the
damage-variation loop) and no longer carry the house's moved objects
into Cherrygrove.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sébastien QUEROLandClaude Opus 5.5 committed 2026-09-24 19:38:20 +02:00
1 parent 7c1c0cc45b
commit dc10343520
18 files changed
+812 -34

No files matched your search

+46 -3
View File
@@ -50,6 +50,9 @@ export interface NativeSave {
variables: Record<number, number>;
scenes: Record<string, number>;
phone: number[];
/** Queued special call, persisted until its script completes. */
specialPhoneCall: number;
rivalName: string;
clock: { weekdayOffset: number; dst: boolean };
/** Current-map objects moved or turned by scripts, as saved object structs. */
objects: { id: number; x: number; y: number; direction: Direction }[];
@@ -253,6 +256,10 @@ const WONT_DELETE = 1 << 1,
NOCLIP_OBJS = 1 << 6,
SWIMMING = 1 << 5;
const OBJECTTYPE_TRAINER = 2;
/** Map environments (map_data_constants.asm) and the special call count. */
const TOWN = 1,
ROUTE = 2;
const NUM_SPECIALCALLS = 8;
/** STEP_SLOW: random walkers take 16 ticks per tile. */
const NPC_STEP_TICKS = 16;
const isTimed = (effect: ScriptEffect | undefined) =>
@@ -594,7 +601,7 @@ export class GameWorld {
let sourceState = this.state;
if (
(target.tileset === 7 || target.tileset === 21) && // TILESET_POKECENTER, TILESET_POKECOM_CENTER
(this.map.environment === 1 || this.map.environment === 2) // TOWN, ROUTE
(this.map.environment === TOWN || this.map.environment === ROUTE)
)
sourceState = {
...copyScriptState(sourceState),
@@ -689,7 +696,28 @@ export class GameWorld {
event.y === player.y,
);
if (event) this.startScript(event.script, PLAYER, { committedPosition: previous });
else this.randomEncounter(previous);
else if (!this.checkSpecialPhoneCall(previous)) this.randomEncounter(previous);
}
/** CountStep / CheckSpecialPhoneCall: a queued call waits for a step
* after trainer, warp and coordinate events, before encounters. Its
* SpecialPhoneCallList entry supplies the condition and caller script,
* which runs inside the ROM's receive-call script; the caller script
* commits the cleared queue and any story flag together. */
private checkSpecialPhoneCall(previous: { x: number; y: number }): boolean {
const id = this.state.specialPhoneCall;
if (id < 1 || id > NUM_SPECIALCALLS) return false;
const r = this.assets.reader,
entry = SYMBOLS.specialPhoneCallList + (id - 1) * 6;
const condition = r.near(entry, r.bank(entry));
if (condition === SYMBOLS.specialCallOnlyWhenOutside) {
if (this.map.environment !== TOWN && this.map.environment !== ROUTE) return false;
} else if (condition !== SYMBOLS.specialCallWhereverYouAre) return false;
this.startScript(SYMBOLS.receiveSpecialPhoneCall, PLAYER, {
committedPosition: previous,
callerScript: r.far(entry + 3),
});
return true;
}
/** RandomEncounter: cooldown, grass, the map's rate, then a slot. */
@@ -822,6 +850,7 @@ export class GameWorld {
deviceWeekday: () => this.now().getDay(),
text: () => ({
...defaultTextContext(this.name),
rival: this.state.rivalName,
weekday: this.weekdayName(this.clock().weekday),
}),
data: this.assets.pokemon,
@@ -862,6 +891,7 @@ export class GameWorld {
trainer,
hiddenItem,
sighted,
callerScript,
}: {
committedPosition?: { x: number; y: number };
scene?: boolean;
@@ -870,6 +900,7 @@ export class GameWorld {
trainer?: { header: number; distance: number };
hiddenItem?: { event: number; item: number };
sighted?: number;
callerScript?: number;
} = {},
) {
const committed = this.save();
@@ -890,6 +921,7 @@ export class GameWorld {
itemBall,
trainer,
hiddenItem,
callerScript,
},
),
committed,
@@ -1828,6 +1860,8 @@ export class GameWorld {
variables: { ...state.variables },
scenes: { ...state.scenes },
phone: [...state.phone],
specialPhoneCall: state.specialPhoneCall,
rivalName: state.rivalName,
clock: { ...state.clock },
objects: [...this.actors]
.filter(([, actor]) => actor.moved)
@@ -1893,6 +1927,10 @@ export class GameWorld {
Object.keys(save.scenes).some(
(id) => this.assets.sceneOwner(id) !== id,
) ||
!integer(save.specialPhoneCall, 0, 0xffff) ||
typeof save.rivalName !== "string" ||
save.rivalName.length > 7 ||
!NICKNAME_PATTERN.test(save.rivalName) ||
!Array.isArray(save.phone) ||
save.phone.length > 10 ||
!save.phone.every((id) => integer(id, 0, 255)) ||
@@ -1968,7 +2006,8 @@ export class GameWorld {
fruitTrees: new Set(save.fruitTrees),
dailyReset: save.dailyReset,
lastSpawnMap: save.lastSpawnMap,
specialPhoneCall: 0,
specialPhoneCall: save.specialPhoneCall,
rivalName: save.rivalName,
};
let entry;
try {
@@ -2034,6 +2073,8 @@ export function migrateSave(
...(!Object.hasOwn(save, "fruitTrees") && { fruitTrees: [] }),
...(!Object.hasOwn(save, "dailyReset") && { dailyReset: null }),
...(!Object.hasOwn(save, "lastSpawnMap") && { lastSpawnMap: null }),
...(!Object.hasOwn(save, "specialPhoneCall") && { specialPhoneCall: 0 }),
...(!Object.hasOwn(save, "rivalName") && { rivalName: "???" }),
} as unknown as NativeSave;
}
if (save.version !== 1 && save.version !== 2) return undefined;
@@ -2055,6 +2096,8 @@ export function migrateSave(
bag: emptyBag(),
pokedex: { seen: [], caught: [] },
lastSpawnMap: null,
specialPhoneCall: 0,
rivalName: "???",
money: C.START_MONEY,
fruitTrees: [],
dailyReset: null,