POKéGEAR in the start menu runs the clock, Town Map and phone cards with the original joypad rules. Calls to Elm and Mom run their callee scripts, with the out-of-area and out-of-hours cases and HangUp's corrected timing. Mom saves a quarter of trainer prizes once asked, talking to her at home opens BankOfMom (GET, SAVE, CHANGE and the DST checks) with its digit amount box, and MomTriesToBuySomething buys items and dolls with her savings. Saves gain Mom's money, her next item and its trigger balance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3456aa4e11
commit
209778cabe
29 files changed
+2726
-47
No files matched your search
+102
-5
@@ -15,6 +15,12 @@ import {
|
||||
SYMBOLS,
|
||||
} from "../rom/profile";
|
||||
import { OW } from "./constants";
|
||||
import {
|
||||
POKEGEAR_BUTTONS,
|
||||
pokegearClock,
|
||||
type PokegearView,
|
||||
} from "../scripts/pokegear";
|
||||
import { MOM_BANK_BUTTONS, type MomBankView } from "../scripts/mom";
|
||||
import {
|
||||
PLAYER,
|
||||
ScriptRun,
|
||||
@@ -99,6 +105,11 @@ export interface NativeSave {
|
||||
pcItems: ItemSlot[];
|
||||
pokedex: { seen: number[]; caught: number[] };
|
||||
money: number;
|
||||
/** wMomsMoney: Mom's savings. */
|
||||
momsMoney: number;
|
||||
/** wWhichMomItem and wMomItemTriggerBalance: Mom's purchases. */
|
||||
momItem: number;
|
||||
momItemTriggerBalance: number;
|
||||
fruitTrees: number[];
|
||||
dailyReset: number | null;
|
||||
/** wLastSpawnMapGroup/Number: the outdoor map last entered through a
|
||||
@@ -229,6 +240,10 @@ export type Choice =
|
||||
pocket: number;
|
||||
selected: number;
|
||||
}
|
||||
/** A Pokégear card, answered with the button pressed. */
|
||||
| { kind: "pokegear"; view: PokegearView; selected: 0 }
|
||||
/** BankOfMom's amount box, answered with the button pressed. */
|
||||
| { kind: "mombank"; view: MomBankView; selected: 0 }
|
||||
/** The mart's 1-99 quantity stepper; `selected` is the current amount. */
|
||||
| {
|
||||
kind: "martquantity";
|
||||
@@ -240,6 +255,10 @@ export type Choice =
|
||||
prompt: string;
|
||||
selected: number;
|
||||
};
|
||||
/** Choices that read the buttons themselves and are answered with the
|
||||
* button's name. */
|
||||
const buttonChoice = (choice: Choice) =>
|
||||
choice.kind === "pokegear" || choice.kind === "mombank";
|
||||
export interface Dialogue {
|
||||
pages: string[];
|
||||
index: number;
|
||||
@@ -1745,6 +1764,7 @@ export class GameWorld {
|
||||
playerCollision: () =>
|
||||
this.assets.collision(this.map, this.player.x, this.player.y),
|
||||
environment: () => this.assets.map(mapId).environment,
|
||||
phoneService: () => this.assets.map(mapId).phoneService,
|
||||
};
|
||||
}
|
||||
private weekdayName(day: number) {
|
||||
@@ -1839,11 +1859,19 @@ export class GameWorld {
|
||||
}
|
||||
/** Select in PlayerEvents (SelectMenuScript): the registered item. */
|
||||
useSelect(): boolean {
|
||||
if (this.dialogue?.choice?.kind === "pokegear") {
|
||||
this.choose("select");
|
||||
return true;
|
||||
}
|
||||
return this.openStartMenu(true);
|
||||
}
|
||||
/** Start in PlayerEvents (StartMenuScript): the start menu opens while
|
||||
* the player stands still. Its changes commit when it closes. */
|
||||
openStartMenu(select = false): boolean {
|
||||
if (!select && this.dialogue?.choice?.kind === "pokegear") {
|
||||
this.choose("start");
|
||||
return true;
|
||||
}
|
||||
if (this.busy || this.introPending) return false;
|
||||
this.clearNotice();
|
||||
this.script = {
|
||||
@@ -1858,6 +1886,8 @@ export class GameWorld {
|
||||
landmark: () => this.assets.map(this.mapId).landmark,
|
||||
fieldMove: (state, mon, move) =>
|
||||
this.menuFieldMove(state, mon, move),
|
||||
scriptHost: () => this.host(PLAYER),
|
||||
landmarkOf: (mapId) => this.assets.map(mapId).landmark,
|
||||
},
|
||||
this.menuCursors,
|
||||
select,
|
||||
@@ -2119,6 +2149,12 @@ export class GameWorld {
|
||||
tutorial: effect.tutorial,
|
||||
landmark: this.assets.map(this.mapId).landmark,
|
||||
partyMenuCursor: this.menuCursors.party,
|
||||
// Only MOM_SAVING_SOME_MONEY_F has an engine flag; nothing in the
|
||||
// ROM sets the half and all bits.
|
||||
mom: {
|
||||
saving: Number(draft.flags.has(OW.ENGINE_MOM_SAVING_MONEY)),
|
||||
money: draft.momsMoney,
|
||||
},
|
||||
});
|
||||
this.battle = { engine, selected: 1 };
|
||||
this.battleRun = engine.run();
|
||||
@@ -2154,6 +2190,22 @@ export class GameWorld {
|
||||
this.statsScreen = effect.mon;
|
||||
this.dialogue = { pages: ["Stats"], index: 0 };
|
||||
return true;
|
||||
case "pokegear":
|
||||
this.pokegear = effect.view;
|
||||
if (!effect.view) return false;
|
||||
this.dialogue = {
|
||||
pages: effect.pages.length ? effect.pages : [""],
|
||||
index: 0,
|
||||
choice: { kind: "pokegear", view: effect.view, selected: 0 },
|
||||
};
|
||||
return true;
|
||||
case "mombank":
|
||||
this.dialogue = {
|
||||
pages: effect.pages.length ? effect.pages : [""],
|
||||
index: 0,
|
||||
choice: { kind: "mombank", view: effect.view, selected: 0 },
|
||||
};
|
||||
return true;
|
||||
case "menu":
|
||||
this.dialogue = {
|
||||
pages: effect.pages.length ? effect.pages : [""],
|
||||
@@ -2572,6 +2624,7 @@ export class GameWorld {
|
||||
this.memScript ??= script.memScript;
|
||||
this.townMap = false;
|
||||
this.statsScreen = undefined;
|
||||
this.pokegear = undefined;
|
||||
this.dialogue = undefined;
|
||||
this.emote = undefined;
|
||||
this.picture = undefined;
|
||||
@@ -2611,6 +2664,7 @@ export class GameWorld {
|
||||
this.script = undefined;
|
||||
this.townMap = false;
|
||||
this.statsScreen = undefined;
|
||||
this.pokegear = undefined;
|
||||
this.dialogue = undefined;
|
||||
this.emote = undefined;
|
||||
this.picture = undefined;
|
||||
@@ -2639,11 +2693,12 @@ export class GameWorld {
|
||||
input = undefined;
|
||||
view.prompt = undefined;
|
||||
if (next.done) {
|
||||
if (this.script && next.value.prizeMoney)
|
||||
this.script.run.state.money = Math.min(
|
||||
C.MAX_MONEY,
|
||||
this.script.run.state.money + next.value.prizeMoney,
|
||||
);
|
||||
if (this.script) {
|
||||
const draft = this.script.run.state,
|
||||
{ prizeMoney = 0, momMoney = 0 } = next.value;
|
||||
draft.money = Math.min(C.MAX_MONEY, draft.money + prizeMoney);
|
||||
draft.momsMoney = Math.min(C.MAX_MONEY, draft.momsMoney + momMoney);
|
||||
}
|
||||
this.battle = undefined;
|
||||
this.battleRun = undefined;
|
||||
// ExitBattle: EvolveAfterBattle after a win, then CleanUpBattleRAM.
|
||||
@@ -3049,6 +3104,9 @@ export class GameWorld {
|
||||
townMap = false;
|
||||
/** The Pokémon a script's stats screen shows (Bill's PC). */
|
||||
statsScreen: PartyMon | undefined;
|
||||
/** The Pokégear card on screen while the Pokégear is open, including
|
||||
* during the phone calls it makes. */
|
||||
pokegear: PokegearView | undefined;
|
||||
ticks = 0;
|
||||
|
||||
tick(held?: Direction) {
|
||||
@@ -3069,6 +3127,17 @@ export class GameWorld {
|
||||
}
|
||||
const script = this.script;
|
||||
if (script) {
|
||||
// Pokegear_UpdateClock runs every frame while the clock card shows.
|
||||
const gear = this.pokegear;
|
||||
if (gear?.clock) {
|
||||
const { hours, minutes } = this.clock();
|
||||
gear.clock = pokegearClock(
|
||||
this.assets.reader,
|
||||
this.host(PLAYER).text(),
|
||||
hours,
|
||||
minutes,
|
||||
);
|
||||
}
|
||||
const evolution = this.evolution;
|
||||
if (evolution) {
|
||||
const frame = evolution.schedule[++evolution.step];
|
||||
@@ -3187,6 +3256,7 @@ export class GameWorld {
|
||||
if (choice && this.dialogue.index === this.dialogue.pages.length - 1) {
|
||||
// The naming screen is answered with text through choose().
|
||||
if (choice.kind === "pack") this.choosePackEntry();
|
||||
else if (buttonChoice(choice)) this.choose("a");
|
||||
else if (choice.kind !== "name") this.choose(choice.selected);
|
||||
} else this.advanceDialogue();
|
||||
return;
|
||||
@@ -3516,6 +3586,7 @@ export class GameWorld {
|
||||
const choice = dialogue.choice;
|
||||
if (choice.kind === "yesno") this.choose(0);
|
||||
else if (choice.kind === "martmenu") this.choose(2);
|
||||
else if (buttonChoice(choice)) this.choose("b");
|
||||
else if (
|
||||
choice.kind === "martlist" ||
|
||||
choice.kind === "martquantity" ||
|
||||
@@ -3545,6 +3616,7 @@ export class GameWorld {
|
||||
if (!choice || this.dialogue!.index !== this.dialogue!.pages.length - 1)
|
||||
return;
|
||||
if (choice.kind === "name") return;
|
||||
if (buttonChoice(choice)) return this.choose(direction);
|
||||
if (choice.kind === "yesno") {
|
||||
if (direction === "up") choice.selected = 1;
|
||||
if (direction === "down") choice.selected = 0;
|
||||
@@ -3623,6 +3695,13 @@ export class GameWorld {
|
||||
(answer !== "" && !NICKNAME_PATTERN.test(answer)))
|
||||
)
|
||||
return;
|
||||
if (
|
||||
(dialogue.choice.kind === "pokegear" &&
|
||||
!POKEGEAR_BUTTONS.includes(answer as never)) ||
|
||||
(dialogue.choice.kind === "mombank" &&
|
||||
!MOM_BANK_BUTTONS.includes(answer as never))
|
||||
)
|
||||
return;
|
||||
this.townMap = false;
|
||||
this.statsScreen = undefined;
|
||||
this.dialogue = undefined;
|
||||
@@ -3675,6 +3754,9 @@ export class GameWorld {
|
||||
caught: [...state.pokedex.caught].sort((a, b) => a - b),
|
||||
},
|
||||
money: state.money,
|
||||
momsMoney: state.momsMoney,
|
||||
momItem: state.momItem,
|
||||
momItemTriggerBalance: state.momItemTriggerBalance,
|
||||
decorations: { ...state.decorations },
|
||||
fruitTrees: [...state.fruitTrees].sort((a, b) => a - b),
|
||||
dailyReset: state.dailyReset,
|
||||
@@ -3759,6 +3841,9 @@ export class GameWorld {
|
||||
!species(save.pokedex.caught) ||
|
||||
!save.pokedex.caught.every((id) => save.pokedex.seen.includes(id)) ||
|
||||
!integer(save.money, 0, C.MAX_MONEY) ||
|
||||
!integer(save.momsMoney, 0, C.MAX_MONEY) ||
|
||||
!integer(save.momItem, 0, 10) ||
|
||||
!integer(save.momItemTriggerBalance, 0, 0xffffff) ||
|
||||
!Array.isArray(save.fruitTrees) ||
|
||||
!save.fruitTrees.every((id) => integer(id, 1, NUM_FRUIT_TREES)) ||
|
||||
new Set(save.fruitTrees).size !== save.fruitTrees.length ||
|
||||
@@ -3837,6 +3922,9 @@ export class GameWorld {
|
||||
caught: new Set(save.pokedex.caught),
|
||||
},
|
||||
money: save.money,
|
||||
momsMoney: save.momsMoney,
|
||||
momItem: save.momItem,
|
||||
momItemTriggerBalance: save.momItemTriggerBalance,
|
||||
fruitTrees: new Set(save.fruitTrees),
|
||||
dailyReset: save.dailyReset,
|
||||
lastSpawnMap: save.lastSpawnMap,
|
||||
@@ -3892,6 +3980,7 @@ export class GameWorld {
|
||||
this.playerId = save.playerId;
|
||||
this.townMap = false;
|
||||
this.statsScreen = undefined;
|
||||
this.pokegear = undefined;
|
||||
this.dialogue = undefined;
|
||||
this.emote = undefined;
|
||||
this.picture = undefined;
|
||||
@@ -3929,6 +4018,11 @@ export function migrateSave(
|
||||
decorations: initialDecorations(),
|
||||
}),
|
||||
...(!Object.hasOwn(save, "money") && { money: C.START_MONEY }),
|
||||
...(!Object.hasOwn(save, "momsMoney") && { momsMoney: 0 }),
|
||||
...(!Object.hasOwn(save, "momItem") && {
|
||||
momItem: 0,
|
||||
momItemTriggerBalance: OW.MOM_MONEY,
|
||||
}),
|
||||
...(!Object.hasOwn(save, "storage") && { storage: emptyStorage() }),
|
||||
...(!Object.hasOwn(save, "pcItems") && { pcItems: [] }),
|
||||
...(!Object.hasOwn(save, "fruitTrees") && { fruitTrees: [] }),
|
||||
@@ -3980,6 +4074,9 @@ export function migrateSave(
|
||||
specialPhoneCall: 0,
|
||||
rivalName: "???",
|
||||
money: C.START_MONEY,
|
||||
momsMoney: 0,
|
||||
momItem: 0,
|
||||
momItemTriggerBalance: OW.MOM_MONEY,
|
||||
fruitTrees: [],
|
||||
dailyReset: null,
|
||||
playerState: 0,
|
||||
|
||||
Reference in new issue
Block a user