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
+64
-23
@@ -30,9 +30,12 @@ export const PARTY_LENGTH = 6;
|
||||
/** Characters a nickname can hold: the naming screen's letters and
|
||||
* symbols, plus the digits and apostrophes of default species names. */
|
||||
export const NICKNAME_PATTERN = /^[A-Za-z0-9 ()":;[\]?!♂♀/.,×'é-]{1,10}$/u;
|
||||
/** The same characters, for box names (up to eight). */
|
||||
export const BOX_NAME_PATTERN = /^[A-Za-z0-9 ()":;[\]?!♂♀/.,×'é-]{1,8}$/u;
|
||||
/** BASE_HAPPINESS */
|
||||
const BASE_HAPPINESS = 70;
|
||||
const MAX_STAT_VALUE = 999;
|
||||
const MAX_LEVEL = 100;
|
||||
|
||||
/** GetSquareRoot: the first of 1..255 whose square is not below `value`. */
|
||||
function squareRoot(value: number) {
|
||||
@@ -159,11 +162,30 @@ export function generatePartyMon(
|
||||
};
|
||||
}
|
||||
|
||||
/** Validate a stored Pokémon against the ROM's rules before restoring it. */
|
||||
export function validPartyMon(
|
||||
/** CalcLevel: the highest level whose experience the total reaches. */
|
||||
export function levelForExp(
|
||||
data: PokemonData,
|
||||
value: unknown,
|
||||
): value is PartyMon {
|
||||
species: number,
|
||||
exp: number,
|
||||
): number {
|
||||
const growth = data.baseData(species).growthRate;
|
||||
let level = 1;
|
||||
while (level < MAX_LEVEL && data.expAtLevel(growth, level + 1) <= exp)
|
||||
level++;
|
||||
return level;
|
||||
}
|
||||
|
||||
/** ComputeMaxPP for a move with `ups` PP Ups: each adds a fifth of the base
|
||||
* PP, clamped to 7, so a 40 PP move maxes at 61 instead of overflowing the
|
||||
* packed byte at 64. The PP Ups stay in the top two bits. */
|
||||
export function maxPP(data: PokemonData, move: number, ups: number): number {
|
||||
const base = data.move(move).pp;
|
||||
return (ups << 6) | (base + Math.min(Math.floor(base / 5), 7) * ups);
|
||||
}
|
||||
|
||||
/** The box_struct fields of a stored Pokémon: everything but status, HP
|
||||
* and stats. */
|
||||
export function validBoxFields(value: unknown): boolean {
|
||||
const mon = value as PartyMon;
|
||||
const integer = (value: unknown, min: number, max: number): value is number =>
|
||||
Number.isInteger(value) && Number(value) >= min && Number(value) <= max;
|
||||
@@ -171,28 +193,47 @@ export function validPartyMon(
|
||||
Array.isArray(value) &&
|
||||
value.length === length &&
|
||||
value.every((item) => integer(item, 0, max));
|
||||
try {
|
||||
return (
|
||||
Boolean(mon) &&
|
||||
typeof mon === "object" &&
|
||||
integer(mon.species, 1, 251) &&
|
||||
integer(mon.item, 0, 255) &&
|
||||
tuple(mon.moves, 4, 251) &&
|
||||
integer(mon.otId, 0, 0xffff) &&
|
||||
typeof mon.ot === "string" &&
|
||||
/^[A-Za-z0-9 -]{1,7}$/.test(mon.ot) &&
|
||||
typeof mon.nickname === "string" &&
|
||||
NICKNAME_PATTERN.test(mon.nickname) &&
|
||||
integer(mon.exp, 0, 0xffffff) &&
|
||||
tuple(mon.statExp, 5, 0xffff) &&
|
||||
integer(mon.dvs, 0, 0xffff) &&
|
||||
tuple(mon.pp, 4, 255) &&
|
||||
integer(mon.happiness, 0, 255) &&
|
||||
integer(mon.pokerus, 0, 255) &&
|
||||
integer(mon.caughtData, 0, 0xffff) &&
|
||||
integer(mon.level, 1, 100)
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Validate a stored Pokémon against the ROM's rules before restoring it. */
|
||||
export function validPartyMon(
|
||||
data: PokemonData,
|
||||
value: unknown,
|
||||
): value is PartyMon {
|
||||
const mon = value as PartyMon;
|
||||
const integer = (value: unknown, min: number, max: number): value is number =>
|
||||
Number.isInteger(value) && Number(value) >= min && Number(value) <= max;
|
||||
try {
|
||||
if (
|
||||
!mon ||
|
||||
typeof mon !== "object" ||
|
||||
!integer(mon.species, 1, 251) ||
|
||||
!integer(mon.item, 0, 255) ||
|
||||
!tuple(mon.moves, 4, 251) ||
|
||||
!integer(mon.otId, 0, 0xffff) ||
|
||||
typeof mon.ot !== "string" ||
|
||||
!/^[A-Za-z0-9 -]{1,7}$/.test(mon.ot) ||
|
||||
typeof mon.nickname !== "string" ||
|
||||
!NICKNAME_PATTERN.test(mon.nickname) ||
|
||||
!integer(mon.exp, 0, 0xffffff) ||
|
||||
!tuple(mon.statExp, 5, 0xffff) ||
|
||||
!integer(mon.dvs, 0, 0xffff) ||
|
||||
!tuple(mon.pp, 4, 255) ||
|
||||
!integer(mon.happiness, 0, 255) ||
|
||||
!integer(mon.pokerus, 0, 255) ||
|
||||
!integer(mon.caughtData, 0, 0xffff) ||
|
||||
!integer(mon.level, 1, 100) ||
|
||||
!validBoxFields(mon) ||
|
||||
!integer(mon.status, 0, 255) ||
|
||||
!tuple(mon.stats, 6, MAX_STAT_VALUE) ||
|
||||
!Array.isArray(mon.stats) ||
|
||||
mon.stats.length !== 6 ||
|
||||
!mon.stats.every((stat) => integer(stat, 0, MAX_STAT_VALUE)) ||
|
||||
!integer(mon.hp, 0, mon.stats[0])
|
||||
)
|
||||
return false;
|
||||
|
||||
Reference in new issue
Block a user