Open Mr. Pokémon's house, the first rival battle and the return to Elm
Checks / web (push) Canceled after 0s
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:
1 parent
7c1c0cc45b
commit
dc10343520
18 files changed
+812
-34
No files matched your search
@@ -62,9 +62,9 @@ export interface ScriptState {
|
||||
* through a Pokémon Center door (EnterMapWarp's SetSpawn), used by
|
||||
* GetWhiteoutSpawn. Null before any Center has been entered. */
|
||||
lastSpawnMap: string | null;
|
||||
/** wSpecialPhoneCallID's low byte (checkphonecall): nonzero while a call
|
||||
* is pending. No ported content ever sets it, so it is not saved. */
|
||||
/** wSpecialPhoneCallID: nonzero while a saved story call is pending. */
|
||||
specialPhoneCall: number;
|
||||
rivalName: string;
|
||||
}
|
||||
export const emptyScriptState = (): ScriptState => ({
|
||||
events: new Set(),
|
||||
@@ -81,6 +81,7 @@ export const emptyScriptState = (): ScriptState => ({
|
||||
dailyReset: null,
|
||||
lastSpawnMap: null,
|
||||
specialPhoneCall: 0,
|
||||
rivalName: "???",
|
||||
});
|
||||
export const copyScriptState = (state: ScriptState): ScriptState => ({
|
||||
events: new Set(state.events),
|
||||
@@ -100,6 +101,7 @@ export const copyScriptState = (state: ScriptState): ScriptState => ({
|
||||
dailyReset: state.dailyReset,
|
||||
lastSpawnMap: state.lastSpawnMap,
|
||||
specialPhoneCall: state.specialPhoneCall,
|
||||
rivalName: state.rivalName,
|
||||
});
|
||||
|
||||
/** Object operands keep the script's numbering: 0 is the player, 2+ are map
|
||||
@@ -204,6 +206,8 @@ export class UnsupportedScript extends Error {
|
||||
type Input = number | string | undefined;
|
||||
/** ITEM_FROM_MEM: take the item from wScriptVar. */
|
||||
const ITEM_FROM_MEM = 0xff;
|
||||
/** wCallerContact's script-2 pointer (bank, address) within the contact. */
|
||||
const PHONE_CONTACT_SCRIPT2_BANK = 9;
|
||||
|
||||
/** TrainerWalkToPlayer and ComputePathToWalkToPlayer (home/movement.asm):
|
||||
* a one-tick sleep, then normal steps to the player's tile, dropping the
|
||||
@@ -289,6 +293,9 @@ export class ScriptRun {
|
||||
trainer?: { header: number; distance: number };
|
||||
/** wHiddenItemData, copied from a BGEVENT_ITEM's hiddenitem data. */
|
||||
hiddenItem?: { event: number; item: number };
|
||||
/** wCallerContact's second script, copied by CheckSpecialPhoneCall
|
||||
* from the queued SpecialPhoneCallList entry. */
|
||||
callerScript?: number;
|
||||
} = {},
|
||||
) {
|
||||
this.state = copyScriptState(state);
|
||||
@@ -313,6 +320,7 @@ export class ScriptRun {
|
||||
private text(): TextContext {
|
||||
return {
|
||||
...this.host.text(),
|
||||
rival: this.state.rivalName,
|
||||
strings: { ...this.host.text().strings, ...this.strings },
|
||||
numbers: { ...this.host.text().numbers, ...this.numbers },
|
||||
};
|
||||
@@ -406,6 +414,16 @@ export class ScriptRun {
|
||||
p = address;
|
||||
break;
|
||||
}
|
||||
case 0x02: {
|
||||
// memcall: only Script_ReceivePhoneCall's caller script is known.
|
||||
const address = word();
|
||||
const script = this.options.callerScript;
|
||||
if (address !== RAM.callerContact + PHONE_CONTACT_SCRIPT2_BANK || script === undefined)
|
||||
throw new UnsupportedScript(offset, opcode);
|
||||
stack.push(p);
|
||||
p = script;
|
||||
break;
|
||||
}
|
||||
case 0x03:
|
||||
p = near();
|
||||
break;
|
||||
@@ -527,6 +545,18 @@ export class ScriptRun {
|
||||
value = Number(
|
||||
receiveItem(data, this.state.bag, ball.item, ball.quantity),
|
||||
);
|
||||
} else if (routine === NATIVE_ROUTINES.hangUp) {
|
||||
// HangUp_Beep prints "Click!", then seven 20-frame waits for the
|
||||
// hang-up tones pass before closetext; no button is needed.
|
||||
write(SYMBOLS.phoneClickText);
|
||||
yield { kind: "text", pages: take(), holdTicks: 140 };
|
||||
} else if (
|
||||
routine === NATIVE_ROUTINES.ringTwiceStartCall ||
|
||||
routine === NATIVE_ROUTINES.initCallReceiveDelay
|
||||
) {
|
||||
// The ringing tones and caller name box are not presented yet.
|
||||
// InitCallReceiveDelay restarts the random-call timers, and
|
||||
// random calls are not ported.
|
||||
} else if (!Object.values(EMPTY_ROUTINES).includes(routine as never))
|
||||
throw new UnsupportedScript(offset, opcode);
|
||||
break;
|
||||
@@ -649,6 +679,14 @@ export class ScriptRun {
|
||||
if ((battleResult & 0x3f) === 1) p = SYMBOLS.battleWhiteoutScript;
|
||||
canLose = false;
|
||||
break;
|
||||
case 0x7b:
|
||||
// MAPSETUP_RELOADMAP restores graphics/audio after battle without
|
||||
// re-running callbacks or rebuilding objects. Native presentation
|
||||
// already retains the map; clear wBattleScriptFlags equivalents.
|
||||
wild = undefined;
|
||||
trainer = undefined;
|
||||
canLose = false;
|
||||
break;
|
||||
case 0x8a: {
|
||||
byte(); // newloadmap MAPSETUP_WARP
|
||||
const entry = SYMBOLS.spawnPoints + spawn * 4;
|
||||
@@ -663,6 +701,10 @@ export class ScriptRun {
|
||||
case 0x1c:
|
||||
value = this.readVar(byte(), offset);
|
||||
break;
|
||||
case 0x3b:
|
||||
// blackoutmod writes wLastSpawnMapGroup/wLastSpawnMapNumber.
|
||||
this.state.lastSpawnMap = `${byte()}:${byte()}`;
|
||||
break;
|
||||
case 0x9c:
|
||||
this.state.specialPhoneCall = word();
|
||||
break;
|
||||
@@ -1227,6 +1269,8 @@ export class ScriptRun {
|
||||
return this.host.position().x;
|
||||
case 0x13:
|
||||
return this.host.position().y;
|
||||
case 0x14:
|
||||
return this.state.specialPhoneCall;
|
||||
default:
|
||||
throw new UnsupportedScript(offset, 0x1c, ` (variable ${id})`);
|
||||
}
|
||||
@@ -1244,7 +1288,23 @@ export class ScriptRun {
|
||||
): Generator<ScriptEffect, number | undefined, Input> {
|
||||
const r = this.reader;
|
||||
switch (routine) {
|
||||
case SPECIAL_ROUTINES.nameRival: {
|
||||
// The naming screen's own title; any pending text was already shown.
|
||||
const fallback = readString(r, SYMBOLS.rivalDefaultName, this.text());
|
||||
const name = yield {
|
||||
kind: "name",
|
||||
pages: [...take(), readString(r, SYMBOLS.rivalNameString, this.text())],
|
||||
current: fallback,
|
||||
max: 7,
|
||||
};
|
||||
if (typeof name !== "string" || name.length > 7 || (name && !NICKNAME_PATTERN.test(name)))
|
||||
throw new Error("Invalid rival name.");
|
||||
// InitName: a name of zero or more spaces takes the default.
|
||||
this.state.rivalName = /^ *$/.test(name) ? fallback : name;
|
||||
return value;
|
||||
}
|
||||
case SPECIAL_ROUTINES.restartMapMusic:
|
||||
case SPECIAL_ROUTINES.fadeOutMusic:
|
||||
case SPECIAL_ROUTINES.fadeOutToWhite:
|
||||
case SPECIAL_ROUTINES.warpToSpawnPoint:
|
||||
case SPECIAL_ROUTINES.stubbedTrainerRankingsHealings:
|
||||
@@ -1253,6 +1313,9 @@ export class ScriptRun {
|
||||
// Rankings feature (Pokémon News) was cut, leaving a dead stub.
|
||||
return value;
|
||||
case SPECIAL_ROUTINES.healMachineAnim:
|
||||
case SPECIAL_ROUTINES.fadeOutToBlack:
|
||||
case SPECIAL_ROUTINES.fadeInFromBlack:
|
||||
case SPECIAL_ROUTINES.reloadSpritesNoPalettes:
|
||||
// Graphics, palettes and sound only: wPartyCount/wScriptVar select
|
||||
// an animation sequence but nothing persists past it.
|
||||
return value;
|
||||
|
||||
Reference in new issue
Block a user