Open Route 32 to Route 34, the Ruins of Alph and Azalea Town (#32)
Enable Route 32 and its Pokémon Center, the Ruins of Alph with the four chambers, item and word rooms, inner chamber and research center, Union Cave, Route 33, Azalea Town with Kurt's house, the kiln and Bugsy's gym, the Slowpoke Well, Ilex Forest and Route 34. Port what their scripts need: saved WRAM bytes by label (readmem, writemem, loadmem: wFarfetchdPosition, wKurtApricornQuantity), VAR_UNOWNCOUNT and VAR_KURT_APRICORNS, SelectApricornForKurt and verbosegiveitemvar, Unown letters (GetUnownLetter, per-letter pictures, CheckUnownLetter's redraw, UpdateUnownDex, wFirstUnownSeen), the chamber puzzles (_UnownPuzzle), DisplayUnownWords, HoOhChamber and OmanyteChamber. Minigames and specials that draw a whole screen in tiles now share a tile screen effect with ROM graphics, palettes and sprites, answered with buttons; the standard font and textbox frame load for them. The playthrough continues through Kurt's Slowpoke Well, Bugsy, the rival and the Farfetch'd to the Route 34 gate; its helpers move to tests/playthrough-driver.ts. New tests solve the Kabuto puzzle through the world, catch Unown and make Kurt's balls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
15a31f95e4
commit
f4ef244fc7
31 files changed
+2334
-528
No files matched your search
+73
-6
@@ -12,7 +12,9 @@ import {
|
||||
NUM_FRUIT_TREES,
|
||||
OVERWORLD,
|
||||
PC,
|
||||
SAVED_WRAM,
|
||||
SYMBOLS,
|
||||
type SavedByte,
|
||||
} from "../rom/profile";
|
||||
import { OW } from "./constants";
|
||||
import {
|
||||
@@ -56,6 +58,7 @@ import {
|
||||
} 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 { POKEDEX_BUTTONS, type PokedexView } from "../scripts/pokedex";
|
||||
import { regionAtlas, type AtlasRegion, type AtlasView } from "./atlas";
|
||||
import {
|
||||
@@ -170,6 +173,11 @@ export interface NativeSave {
|
||||
blueCardBalance: number;
|
||||
/** wTradeFlags: the in-game trades done, one bit per NPCTRADE_*. */
|
||||
tradeFlags: number;
|
||||
/** Saved WRAM bytes scripts use directly, by PRET label (SAVED_WRAM). */
|
||||
memory: Partial<Record<SavedByte, number>>;
|
||||
/** wUnownDex and wFirstUnownSeen. */
|
||||
unownDex: number[];
|
||||
firstUnownSeen: number;
|
||||
/** The option bytes, saved with the game as sOptions is. */
|
||||
options: GameOptions;
|
||||
}
|
||||
@@ -319,6 +327,8 @@ export type Choice =
|
||||
| { kind: "options"; view: OptionsView; selected: 0 }
|
||||
/** The Fly map, answered with the button pressed. */
|
||||
| { kind: "flymap"; view: FlyMapView; selected: 0 }
|
||||
/** A tile screen (a minigame), answered with the button pressed. */
|
||||
| { kind: "screen"; screen: TileScreen; selected: 0 }
|
||||
/** The mart's 1-99 quantity stepper; `selected` is the current amount. */
|
||||
| {
|
||||
kind: "martquantity";
|
||||
@@ -338,7 +348,8 @@ const buttonChoice = (choice: Choice) =>
|
||||
choice.kind === "trainercard" ||
|
||||
choice.kind === "pokedex" ||
|
||||
choice.kind === "options" ||
|
||||
choice.kind === "flymap";
|
||||
choice.kind === "flymap" ||
|
||||
choice.kind === "screen";
|
||||
export interface Dialogue {
|
||||
pages: string[];
|
||||
index: number;
|
||||
@@ -1697,8 +1708,14 @@ export class GameWorld {
|
||||
for (const threshold of [35, 65, 85, 95])
|
||||
if (variety >= Math.floor((threshold * 255) / 100)) chosen.level++;
|
||||
}
|
||||
// ValidateTempWildMonSpecies; Unown needs unlocked Unown (not ported).
|
||||
if (!chosen.species || chosen.species > 251 || chosen.species === C.UNOWN)
|
||||
// ValidateTempWildMonSpecies; no Unown before a letter set is unlocked.
|
||||
if (!chosen.species || chosen.species > 251) return undefined;
|
||||
if (
|
||||
chosen.species === C.UNOWN &&
|
||||
!Array.from({ length: 8 }, (_, bit) => OW.ENGINE_UNLOCKED_UNOWNS_A_TO_K + bit).some(
|
||||
(flag) => this.state.flags.has(flag),
|
||||
)
|
||||
)
|
||||
return undefined;
|
||||
return chosen;
|
||||
}
|
||||
@@ -2086,6 +2103,10 @@ export class GameWorld {
|
||||
environment: () => this.assets.map(mapId).environment,
|
||||
phoneService: () => this.assets.map(mapId).phoneService,
|
||||
facingObject: () => this.facingObject(),
|
||||
tileset: () => ({
|
||||
graphics: this.assets.tileset(this.map).graphics,
|
||||
palettes: this.assets.palettes(this.map, this.timeOfDay),
|
||||
}),
|
||||
};
|
||||
}
|
||||
/** FishFunction with rod 0-2 (Old, Good, Super): never while surfing,
|
||||
@@ -2230,7 +2251,10 @@ export class GameWorld {
|
||||
}
|
||||
/** Select in PlayerEvents (SelectMenuScript): the registered item. */
|
||||
useSelect(): boolean {
|
||||
if (this.dialogue?.choice?.kind === "pokegear") {
|
||||
if (
|
||||
this.dialogue?.choice?.kind === "pokegear" ||
|
||||
this.dialogue?.choice?.kind === "screen"
|
||||
) {
|
||||
this.choose("select");
|
||||
return true;
|
||||
}
|
||||
@@ -2243,7 +2267,11 @@ export class GameWorld {
|
||||
/** 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") {
|
||||
if (
|
||||
!select &&
|
||||
(this.dialogue?.choice?.kind === "pokegear" ||
|
||||
this.dialogue?.choice?.kind === "screen")
|
||||
) {
|
||||
this.choose("start");
|
||||
return true;
|
||||
}
|
||||
@@ -2555,6 +2583,11 @@ export class GameWorld {
|
||||
},
|
||||
shift: shiftBattleStyle(draft.options),
|
||||
battleScene: battleSceneOn(draft.options),
|
||||
unown: draft,
|
||||
// wUnlockedUnowns: the ENGINE_UNLOCKED_UNOWNS_* flags' bits.
|
||||
unlockedUnowns: Array.from({ length: 8 }, (_, bit) =>
|
||||
draft.flags.has(OW.ENGINE_UNLOCKED_UNOWNS_A_TO_K + bit) ? 1 << bit : 0,
|
||||
).reduce((bits, bit) => bits | bit, 0),
|
||||
});
|
||||
this.battle = { engine, selected: 1 };
|
||||
this.battleRun = engine.run();
|
||||
@@ -2571,6 +2604,13 @@ export class GameWorld {
|
||||
choice: { kind: "flymap", view: effect.view, selected: 0 },
|
||||
};
|
||||
return true;
|
||||
case "screen":
|
||||
this.dialogue = {
|
||||
pages: [""],
|
||||
index: 0,
|
||||
choice: { kind: "screen", screen: effect.screen, selected: 0 },
|
||||
};
|
||||
return true;
|
||||
case "fly":
|
||||
// FlyFromAnim's loop runs 129 frames after a DelayFrame, FlyToAnim's
|
||||
// 65: 65 and 33 overworld ticks.
|
||||
@@ -4246,7 +4286,9 @@ export class GameWorld {
|
||||
(dialogue.choice.kind === "options" &&
|
||||
!OPTIONS_BUTTONS.includes(answer as never)) ||
|
||||
(dialogue.choice.kind === "flymap" &&
|
||||
!FLY_MAP_BUTTONS.includes(answer as never))
|
||||
!FLY_MAP_BUTTONS.includes(answer as never)) ||
|
||||
(dialogue.choice.kind === "screen" &&
|
||||
!SCREEN_BUTTONS.includes(answer as never))
|
||||
)
|
||||
return;
|
||||
this.townMap = false;
|
||||
@@ -4326,6 +4368,9 @@ export class GameWorld {
|
||||
coins: state.coins,
|
||||
tradeFlags: state.tradeFlags,
|
||||
blueCardBalance: state.blueCardBalance,
|
||||
memory: { ...state.memory },
|
||||
unownDex: [...state.unownDex],
|
||||
firstUnownSeen: state.firstUnownSeen,
|
||||
options: { ...state.options },
|
||||
};
|
||||
}
|
||||
@@ -4447,6 +4492,17 @@ export class GameWorld {
|
||||
!integer(save.coins, 0, C.MAX_COINS) ||
|
||||
!integer(save.tradeFlags, 0, 0x7f) ||
|
||||
!integer(save.blueCardBalance, 0, 255) ||
|
||||
!save.memory ||
|
||||
typeof save.memory !== "object" ||
|
||||
Object.entries(save.memory).some(
|
||||
([label, byte]) =>
|
||||
!Object.hasOwn(SAVED_WRAM, label) || !integer(byte, 0, 255),
|
||||
) ||
|
||||
!Array.isArray(save.unownDex) ||
|
||||
save.unownDex.length > 26 ||
|
||||
!save.unownDex.every((letter) => integer(letter, 1, 26)) ||
|
||||
new Set(save.unownDex).size !== save.unownDex.length ||
|
||||
!integer(save.firstUnownSeen, 0, 26) ||
|
||||
!validOptions(save.options)
|
||||
) {
|
||||
throw new Error(
|
||||
@@ -4519,6 +4575,9 @@ export class GameWorld {
|
||||
coins: save.coins,
|
||||
tradeFlags: save.tradeFlags,
|
||||
blueCardBalance: save.blueCardBalance,
|
||||
memory: { ...save.memory },
|
||||
unownDex: [...save.unownDex],
|
||||
firstUnownSeen: save.firstUnownSeen,
|
||||
options: { ...save.options },
|
||||
specialPhoneCall: save.specialPhoneCall,
|
||||
rivalName: save.rivalName,
|
||||
@@ -4648,6 +4707,11 @@ export function migrateSave(
|
||||
}
|
||||
: {}),
|
||||
...(!Object.hasOwn(save, "tradeFlags") && { tradeFlags: 0 }),
|
||||
...(!Object.hasOwn(save, "memory") && { memory: {} }),
|
||||
...(!Object.hasOwn(save, "unownDex") && {
|
||||
unownDex: [],
|
||||
firstUnownSeen: 0,
|
||||
}),
|
||||
...(!Object.hasOwn(save, "intro") && { intro: false }),
|
||||
...(!Object.hasOwn(save, "gameTime") && { gameTime: emptyGameTime() }),
|
||||
...(!Object.hasOwn(save, "options") && { options: defaultOptions() }),
|
||||
@@ -4701,6 +4765,9 @@ export function migrateSave(
|
||||
coins: 0,
|
||||
blueCardBalance: 0,
|
||||
tradeFlags: 0,
|
||||
memory: {},
|
||||
unownDex: [],
|
||||
firstUnownSeen: 0,
|
||||
intro: false,
|
||||
gameTime: emptyGameTime(),
|
||||
options: defaultOptions(),
|
||||
|
||||
Reference in new issue
Block a user