Open Goldenrod City, Whitney's gym and the Radio Tower quiz (#32)

Enable Goldenrod City and its buildings, the department store with its
elevator and rooftop, the Underground and its rooms, the Radio Tower
floors and the Route 35 gate.

Port what their scripts need: the herb shop, bargain shop, pharmacy and
rooftop sale marts with ROM stock, prices and text; the Name Rater; the
haircut brothers' happiness tables; the Lucky Number show over the party
and boxes with its saved number and Friday countdown (RTC day timers with
the 140-day wrap); coin and money balance boxes; Game Corner prize
registration through NewPokedexEntry; gift Pokémon to the box when the
party is full, with OT, nickname and MAIL (Kenya's 15-letter first line
now stores). Saves gain hallOfFame, timers and luckyNumber, with
defaults for older saves, and wUndergroundSwitchPositions joins the
saved script bytes.

The playthrough continues to the Bicycle, Whitney, the PLAINBADGE and the
radio card. SlotMachine and CardFlip are still refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kawaandClaude Opus 5.5 committed 2026-10-07 09:26:07 +02:00
1 parent f4ef244fc7
commit 0930b780fc
19 files changed
+1596 -126

No files matched your search

+53 -6
View File
@@ -55,10 +55,12 @@ import {
type ScriptHost,
type Portrait,
type ScriptState,
type DayTimerName,
} from "../scripts/interpreter";
import { IntroRun } from "../scripts/intro";
import { FLY_MAP_BUTTONS, type FlyMapView } from "../scripts/fly-map";
import { SCREEN_BUTTONS, type TileScreen } from "../scripts/screen";
import { RTC_DAYS, validDayTimer, type DayTimer } from "./day-timers";
import { POKEDEX_BUTTONS, type PokedexView } from "../scripts/pokedex";
import { regionAtlas, type AtlasRegion, type AtlasView } from "./atlas";
import {
@@ -109,6 +111,9 @@ import {
type Warp,
} from "./types";
/** The day countdowns a save may hold. */
const DAY_TIMERS: readonly DayTimerName[] = ["luckyNumber"];
export interface NativeSave {
decorations: Decorations;
format: "crystal-native";
@@ -163,6 +168,8 @@ export interface NativeSave {
/** STATUSFLAGS_FLASH_F and STATUSFLAGS_NO_WILD_ENCOUNTERS_F. */
flash: boolean;
noWildEncounters: boolean;
/** STATUSFLAGS_HALL_OF_FAME_F. */
hallOfFame: boolean;
/** wWhichRegisteredItem and wRegisteredItem, for Select. */
registeredItem: { which: number; item: number };
/** sMailboxes: the PC's MAIL. */
@@ -178,6 +185,9 @@ export interface NativeSave {
/** wUnownDex and wFirstUnownSeen. */
unownDex: number[];
firstUnownSeen: number;
/** RTC day countdowns by name, and the Lucky Number show's number. */
timers: Partial<Record<DayTimerName, DayTimer>>;
luckyNumber: { id: number; day: number };
/** The option bytes, saved with the game as sOptions is. */
options: GameOptions;
}
@@ -274,7 +284,7 @@ export interface Pose {
inGrass?: boolean;
}
export type Choice =
| { kind: "yesno"; selected: 0 | 1 }
| { kind: "yesno"; selected: 0 | 1; balance?: string }
| { kind: "weekday"; names: string[]; selected: number }
/** InitClock's hour or minute: Up and Down wrap through `labels`. */
| { kind: "clock"; labels: string[]; selected: number }
@@ -295,6 +305,8 @@ export type Choice =
selected: number;
/** Left and right answer "left" or "right". */
sides?: boolean;
/** A money or coin box shown with the menu. */
balance?: string;
}
/** The mart's BUY/SELL/QUIT box. */
| { kind: "martmenu"; prompt: string; selected: number }
@@ -2103,6 +2115,7 @@ export class GameWorld {
environment: () => this.assets.map(mapId).environment,
phoneService: () => this.assets.map(mapId).phoneService,
facingObject: () => this.facingObject(),
day: () => this.gameDay(),
tileset: () => ({
graphics: this.assets.tileset(this.map).graphics,
palettes: this.assets.palettes(this.map, this.timeOfDay),
@@ -2408,7 +2421,11 @@ export class GameWorld {
this.dialogue = {
pages: effect.pages.length ? effect.pages : [""],
index: 0,
choice: { kind: "yesno", selected: 1 },
choice: {
kind: "yesno",
selected: 1,
...(effect.balance && { balance: effect.balance }),
},
};
return true;
case "weekday":
@@ -2718,6 +2735,7 @@ export class GameWorld {
...(effect.details && { details: effect.details }),
selected: effect.selected,
...(effect.sides && { sides: true }),
...(effect.balance && { balance: effect.balance }),
},
};
return true;
@@ -3332,12 +3350,18 @@ export class GameWorld {
/** CheckDailyResetTimer, from CheckTimeEvents after scene scripts: the
* first idle check on a new game day clears the daily engine flags. */
private checkDailyReset() {
const now = this.gameNow(this.state.clock);
const day =
/** The game clock's day number, which moves with SetDayOfWeek's
* offset as the RTC's days do. */
private gameDay(clock = this.liveState.clock) {
const now = this.gameNow(clock);
return (
Math.floor(
Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()) / 86_400_000,
) + this.state.clock.weekdayOffset;
) + clock.weekdayOffset
);
}
private checkDailyReset() {
const day = this.gameDay(this.state.clock);
if (this.state.dailyReset === day) return;
this.dailyFlags ??= this.assets.dailyEngineFlags();
for (const flag of this.dailyFlags) this.state.flags.delete(flag);
@@ -4362,6 +4386,7 @@ export class GameWorld {
digWarp: state.digWarp && { ...state.digWarp },
flash: state.flash,
noWildEncounters: state.noWildEncounters,
hallOfFame: state.hallOfFame,
registeredItem: { ...state.registeredItem },
mailbox: state.mailbox.map(copyMail),
repel: state.repel,
@@ -4371,6 +4396,8 @@ export class GameWorld {
memory: { ...state.memory },
unownDex: [...state.unownDex],
firstUnownSeen: state.firstUnownSeen,
timers: structuredClone(state.timers),
luckyNumber: { ...state.luckyNumber },
options: { ...state.options },
};
}
@@ -4485,6 +4512,7 @@ export class GameWorld {
) ||
typeof save.flash !== "boolean" ||
typeof save.noWildEncounters !== "boolean" ||
typeof save.hallOfFame !== "boolean" ||
!save.registeredItem ||
!integer(save.registeredItem.which, 0, 255) ||
!integer(save.registeredItem.item, 0, 255) ||
@@ -4503,6 +4531,14 @@ export class GameWorld {
!save.unownDex.every((letter) => integer(letter, 1, 26)) ||
new Set(save.unownDex).size !== save.unownDex.length ||
!integer(save.firstUnownSeen, 0, 26) ||
!save.timers ||
typeof save.timers !== "object" ||
Object.entries(save.timers).some(
([name, timer]) => !DAY_TIMERS.includes(name as never) || !validDayTimer(timer),
) ||
!save.luckyNumber ||
!integer(save.luckyNumber.id, 0, 0xffff) ||
!integer(save.luckyNumber.day, 0, RTC_DAYS) ||
!validOptions(save.options)
) {
throw new Error(
@@ -4569,6 +4605,7 @@ export class GameWorld {
digWarp: save.digWarp && { ...save.digWarp },
flash: save.flash,
noWildEncounters: save.noWildEncounters,
hallOfFame: save.hallOfFame,
registeredItem: { ...save.registeredItem },
mailbox: save.mailbox.map(copyMail),
repel: save.repel,
@@ -4578,6 +4615,8 @@ export class GameWorld {
memory: { ...save.memory },
unownDex: [...save.unownDex],
firstUnownSeen: save.firstUnownSeen,
timers: structuredClone(save.timers),
luckyNumber: { ...save.luckyNumber },
options: { ...save.options },
specialPhoneCall: save.specialPhoneCall,
rivalName: save.rivalName,
@@ -4686,6 +4725,7 @@ export function migrateSave(
...(!Object.hasOwn(save, "noWildEncounters") && {
noWildEncounters: false,
}),
...(!Object.hasOwn(save, "hallOfFame") && { hallOfFame: false }),
...(!Object.hasOwn(save, "registeredItem") && {
registeredItem: { which: 0, item: 0 },
}),
@@ -4712,6 +4752,10 @@ export function migrateSave(
unownDex: [],
firstUnownSeen: 0,
}),
...(!Object.hasOwn(save, "timers") && {
timers: {},
luckyNumber: { id: 0, day: 0 },
}),
...(!Object.hasOwn(save, "intro") && { intro: false }),
...(!Object.hasOwn(save, "gameTime") && { gameTime: emptyGameTime() }),
...(!Object.hasOwn(save, "options") && { options: defaultOptions() }),
@@ -4759,6 +4803,7 @@ export function migrateSave(
digWarp: null,
flash: false,
noWildEncounters: false,
hallOfFame: false,
registeredItem: { which: 0, item: 0 },
mailbox: [],
repel: 0,
@@ -4768,6 +4813,8 @@ export function migrateSave(
memory: {},
unownDex: [],
firstUnownSeen: 0,
timers: {},
luckyNumber: { id: 0, day: 0 },
intro: false,
gameTime: emptyGameTime(),
options: defaultOptions(),