Read TileCollisionStdScripts from the ROM so the Pokémon Center PC runs PokemonCenterPC: Bill's PC (deposit, withdraw, release, change and name boxes), the player's item storage (withdraw, deposit, toss, mail box) and Oak's Pokédex rating. The house PC now opens the full player's PC menu. A catch with a full party goes to the current box through SendMonIntoBox; a full box keeps the ball, and filling it queues Bill's call as a memory script after the battle. Saves gain boxes, box names and PC items, migrated by absence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
451464f7c6
commit
3bdca0099e
21 files changed
+1999
-131
No files matched your search
+67
-15
@@ -10,6 +10,7 @@ import {
|
||||
INITIAL_POSITION,
|
||||
NUM_FRUIT_TREES,
|
||||
OVERWORLD,
|
||||
PC,
|
||||
SYMBOLS,
|
||||
} from "../rom/profile";
|
||||
import { OW } from "./constants";
|
||||
@@ -19,12 +20,20 @@ import {
|
||||
copyScriptState,
|
||||
emptyScriptState,
|
||||
type MartEntry,
|
||||
type MartMode,
|
||||
type ScriptEffect,
|
||||
type ScriptHost,
|
||||
type ScriptState,
|
||||
} from "../scripts/interpreter";
|
||||
import type { MovementPose, MovementStep } from "../scripts/movement";
|
||||
import { emptyBag, validBag, type Bag } from "../party/bag";
|
||||
import { emptyBag, validBag, type Bag, type ItemSlot } from "../party/bag";
|
||||
import {
|
||||
copyStorage,
|
||||
emptyStorage,
|
||||
validPCItems,
|
||||
validStorage,
|
||||
type PokemonStorage,
|
||||
} from "../party/storage";
|
||||
import {
|
||||
NICKNAME_PATTERN,
|
||||
caughtData,
|
||||
@@ -75,6 +84,8 @@ export interface NativeSave {
|
||||
playerId: number;
|
||||
party: PartyMon[];
|
||||
bag: Bag;
|
||||
storage: PokemonStorage;
|
||||
pcItems: ItemSlot[];
|
||||
pokedex: { seen: number[]; caught: number[] };
|
||||
money: number;
|
||||
fruitTrees: number[];
|
||||
@@ -186,20 +197,20 @@ export type Choice =
|
||||
/** The naming screen: an empty answer keeps `current`. */
|
||||
| { kind: "name"; current: string; max: number; selected?: undefined }
|
||||
/** A vertical menu; `selected` answers with that item's index. */
|
||||
| { kind: "menu"; items: string[]; selected: number }
|
||||
| { kind: "menu"; items: string[]; details?: string[]; selected: number }
|
||||
/** The mart's BUY/SELL/QUIT box. */
|
||||
| { kind: "martmenu"; prompt: string; selected: number }
|
||||
/** The mart's item list; `selected` answers with that entry's index. */
|
||||
| {
|
||||
kind: "martlist";
|
||||
mode: "buy" | "sell";
|
||||
mode: MartMode;
|
||||
entries: MartEntry[];
|
||||
selected: number;
|
||||
}
|
||||
/** The mart's 1-99 quantity stepper; `selected` is the current amount. */
|
||||
| {
|
||||
kind: "martquantity";
|
||||
mode: "buy" | "sell";
|
||||
mode: MartMode;
|
||||
item: number;
|
||||
name: string;
|
||||
price: number;
|
||||
@@ -263,10 +274,18 @@ interface ActiveScript {
|
||||
warp?: { map: string; x: number; y: number; facing?: Direction };
|
||||
/** warpcheck: the player's tile events run on the tick after the script. */
|
||||
warpCheck?: boolean;
|
||||
/** LoadMemScript: queued for the next PlayerEvents once this commits. */
|
||||
memScript?: MemScript;
|
||||
/** The trainer whose sight started this script, if any. */
|
||||
sighted?: number;
|
||||
}
|
||||
|
||||
/** wMapReentryScriptQueueFlag's script: work RAM, so it is not saved. */
|
||||
interface MemScript {
|
||||
script: number;
|
||||
callerScript?: number;
|
||||
}
|
||||
|
||||
const OPPOSITE: Record<Direction, Direction> = {
|
||||
down: "up",
|
||||
up: "down",
|
||||
@@ -455,6 +474,7 @@ export class GameWorld {
|
||||
notice = "";
|
||||
diagnostic = "";
|
||||
private actors = new Map<number, Actor>();
|
||||
private memScript?: MemScript;
|
||||
private currentMap?: GameMap;
|
||||
/** wCmdQueue's CMDQUEUE_STONETABLE entries for the current map. */
|
||||
private stoneTables: number[] = [];
|
||||
@@ -1731,6 +1751,7 @@ export class GameWorld {
|
||||
canLose: effect.canLose,
|
||||
text: () => this.host(PLAYER).text(),
|
||||
bag: draft.bag,
|
||||
storage: draft.storage,
|
||||
hasPokedex: draft.flags.has(ENGINE_FLAGS.pokedex),
|
||||
caughtData: (level) =>
|
||||
caughtData(
|
||||
@@ -1757,6 +1778,16 @@ export class GameWorld {
|
||||
case "warpcheck":
|
||||
script.warpCheck = true;
|
||||
return false;
|
||||
case "notice":
|
||||
this.notice = effect.text;
|
||||
return false;
|
||||
case "memscript":
|
||||
// LoadMemScript keeps a script that is already queued.
|
||||
script.memScript ??= {
|
||||
script: effect.script,
|
||||
callerScript: effect.callerScript,
|
||||
};
|
||||
return false;
|
||||
case "townmap":
|
||||
this.townMap = true;
|
||||
this.dialogue = { pages: ["Town Map"], index: 0 };
|
||||
@@ -1768,6 +1799,7 @@ export class GameWorld {
|
||||
choice: {
|
||||
kind: "menu",
|
||||
items: effect.items,
|
||||
...(effect.details && { details: effect.details }),
|
||||
selected: effect.selected,
|
||||
},
|
||||
};
|
||||
@@ -2153,6 +2185,7 @@ export class GameWorld {
|
||||
this.arrival = { x: this.x, y: this.y };
|
||||
}
|
||||
this.script = undefined;
|
||||
this.memScript ??= script.memScript;
|
||||
this.townMap = false;
|
||||
this.dialogue = undefined;
|
||||
this.emote = undefined;
|
||||
@@ -2686,6 +2719,15 @@ export class GameWorld {
|
||||
}
|
||||
if (this.busy || this.mapId !== map) return;
|
||||
} else if (this.checkTrainers()) return;
|
||||
// RunMemScript precedes RunSceneScript.
|
||||
const memScript = this.memScript;
|
||||
if (memScript) {
|
||||
this.memScript = undefined;
|
||||
this.startScript(memScript.script, PLAYER, {
|
||||
callerScript: memScript.callerScript,
|
||||
});
|
||||
if (this.busy) return;
|
||||
}
|
||||
this.checkScene();
|
||||
if (this.busy) return;
|
||||
this.checkDailyReset();
|
||||
@@ -2791,17 +2833,16 @@ export class GameWorld {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const standard: Record<number, number> = {
|
||||
0x91: 2,
|
||||
0x97: 9,
|
||||
0x9d: 8,
|
||||
0x96: 6,
|
||||
0x9f: 5,
|
||||
};
|
||||
if (standard[collision] !== undefined && this.player.direction === "up")
|
||||
script = this.assets.reader.far(
|
||||
SYMBOLS.standardScripts + standard[collision] * 3,
|
||||
);
|
||||
// CheckFacingTileForStdScript: TileCollisionStdScripts pairs a
|
||||
// collision type with a std script index, in any facing direction.
|
||||
const r = this.assets.reader;
|
||||
for (let entry = PC.TileCollisionStdScripts; ; entry += 3) {
|
||||
const type = r.byte(entry);
|
||||
if (type === 0xff) break;
|
||||
if (type !== collision) continue;
|
||||
script = r.far(SYMBOLS.standardScripts + r.word(entry + 1) * 3);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (script === undefined) {
|
||||
const [fx, fy] = VECTORS[this.player.direction];
|
||||
@@ -3136,6 +3177,8 @@ export class GameWorld {
|
||||
playerId: this.playerId,
|
||||
party: structuredClone(state.party),
|
||||
bag: structuredClone(state.bag),
|
||||
storage: copyStorage(state.storage),
|
||||
pcItems: structuredClone(state.pcItems),
|
||||
pokedex: {
|
||||
seen: [...state.pokedex.seen].sort((a, b) => a - b),
|
||||
caught: [...state.pokedex.caught].sort((a, b) => a - b),
|
||||
@@ -3215,6 +3258,8 @@ export class GameWorld {
|
||||
save.party.length > 6 ||
|
||||
!save.party.every((mon) => validPartyMon(this.assets.pokemon, mon)) ||
|
||||
!validBag(this.assets.pokemon, save.bag) ||
|
||||
!validStorage(save.storage) ||
|
||||
!validPCItems(this.assets.pokemon, save.pcItems) ||
|
||||
!save.pokedex ||
|
||||
!species(save.pokedex.seen) ||
|
||||
!species(save.pokedex.caught) ||
|
||||
@@ -3288,6 +3333,8 @@ export class GameWorld {
|
||||
clock: { ...save.clock },
|
||||
party: structuredClone(save.party),
|
||||
bag: structuredClone(save.bag),
|
||||
storage: copyStorage(save.storage),
|
||||
pcItems: structuredClone(save.pcItems),
|
||||
pokedex: {
|
||||
seen: new Set(save.pokedex.seen),
|
||||
caught: new Set(save.pokedex.caught),
|
||||
@@ -3351,6 +3398,7 @@ export class GameWorld {
|
||||
this.picture = undefined;
|
||||
this.battle = undefined;
|
||||
this.battleRun = undefined;
|
||||
this.memScript = undefined;
|
||||
this.clearNotice();
|
||||
}
|
||||
}
|
||||
@@ -3376,6 +3424,8 @@ export function migrateSave(
|
||||
decorations: initialDecorations(),
|
||||
}),
|
||||
...(!Object.hasOwn(save, "money") && { money: C.START_MONEY }),
|
||||
...(!Object.hasOwn(save, "storage") && { storage: emptyStorage() }),
|
||||
...(!Object.hasOwn(save, "pcItems") && { pcItems: [] }),
|
||||
...(!Object.hasOwn(save, "fruitTrees") && { fruitTrees: [] }),
|
||||
...(!Object.hasOwn(save, "dailyReset") && { dailyReset: null }),
|
||||
...(!Object.hasOwn(save, "lastSpawnMap") && { lastSpawnMap: null }),
|
||||
@@ -3407,6 +3457,8 @@ export function migrateSave(
|
||||
playerId: newPlayerId(),
|
||||
party: [],
|
||||
bag: emptyBag(),
|
||||
storage: emptyStorage(),
|
||||
pcItems: [],
|
||||
pokedex: { seen: [], caught: [] },
|
||||
lastSpawnMap: null,
|
||||
specialPhoneCall: 0,
|
||||
|
||||
Reference in new issue
Block a user