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
+84
-25
@@ -2,10 +2,18 @@ import { RomReader } from "../rom/reader";
|
||||
import { BATTLE, RAM, SYMBOLS } from "../rom/profile";
|
||||
import type { PokemonData } from "../rom/pokemon";
|
||||
import type { Bag } from "../party/bag";
|
||||
import {
|
||||
MONS_PER_BOX,
|
||||
currentBox,
|
||||
restorePP,
|
||||
type BoxMon,
|
||||
type PokemonStorage,
|
||||
} from "../party/storage";
|
||||
import {
|
||||
NICKNAME_PATTERN,
|
||||
PARTY_LENGTH,
|
||||
calcStats,
|
||||
levelForExp,
|
||||
type PartyMon,
|
||||
} from "../party/pokemon";
|
||||
import { readString, readTextEnd, type TextContext } from "../scripts/text";
|
||||
@@ -140,6 +148,8 @@ export interface BattleOptions {
|
||||
text: () => TextContext;
|
||||
/** The player's pack, needed to throw Poké Balls. */
|
||||
bag?: Bag;
|
||||
/** Bill's PC: a catch with a full party goes to the current box. */
|
||||
storage?: PokemonStorage;
|
||||
/** ENGINE_POKEDEX: a first catch shows the new-data message. */
|
||||
hasPokedex?: boolean;
|
||||
/** SetCaughtData's word for a Pokémon caught at this level. */
|
||||
@@ -148,6 +158,8 @@ export interface BattleOptions {
|
||||
tutorial?: boolean;
|
||||
}
|
||||
|
||||
/** BATTLERESULT_BOX_FULL: a catch filled the current box. */
|
||||
const BATTLERESULT_BOX_FULL = 0x80;
|
||||
const SUB1 = 1,
|
||||
SUB2 = 2,
|
||||
SUB3 = 3,
|
||||
@@ -844,12 +856,22 @@ export class Battle {
|
||||
this.tossBall(item, slot);
|
||||
return true;
|
||||
}
|
||||
// With a full party the Pokémon would go to the current box; if that is
|
||||
// full too, the ball is not used (Ball_BoxIsFullMessage).
|
||||
const storage = this.options.storage;
|
||||
if (
|
||||
this.options.party.length >= PARTY_LENGTH &&
|
||||
(!storage || currentBox(storage).length >= MONS_PER_BOX)
|
||||
) {
|
||||
// The Dude's tutorial reaches this check too, and the original then
|
||||
// repeats the unusable ball forever (docs/bugs_and_glitches.md).
|
||||
if (tutorial) throw new UnsupportedBattle("The Dude's Poké Ball cannot be used with a full party and box.");
|
||||
if (!storage) throw new UnsupportedBattle("Missing PC boxes.");
|
||||
yield* this.say(BATTLE.BallBoxFullText);
|
||||
return false;
|
||||
}
|
||||
if (![C.MASTER_BALL, C.ULTRA_BALL, C.GREAT_BALL, C.POKE_BALL].includes(item as never))
|
||||
throw new UnsupportedBattle(`${data.itemName(item)} is not ported yet.`);
|
||||
// A full party sends the Pokémon into the current box (SendMonIntoBox).
|
||||
// The tutorial never keeps its Pokémon, so it does not reach the box.
|
||||
if (!tutorial && this.options.party.length >= PARTY_LENGTH)
|
||||
throw new UnsupportedBattle("PC boxes are not ported yet.");
|
||||
this.wildMon = 0;
|
||||
yield* this.say(BATTLE.ItemUsedText);
|
||||
|
||||
@@ -920,10 +942,15 @@ export class Battle {
|
||||
// NewPokedexEntry's Pokédex page is not presented yet.
|
||||
yield* this.say(BATTLE.NewDexDataText);
|
||||
}
|
||||
// TryAddMonToParty with wBattleMode set: GeneratePartyMonStats copies
|
||||
// the wild Pokémon, then SetCaughtData.
|
||||
if (!this.options.caughtData) throw new UnsupportedBattle("Missing caught data.");
|
||||
const name = data.speciesName(species);
|
||||
if (this.options.party.length >= PARTY_LENGTH) {
|
||||
yield* this.sendToPC(species, level, name);
|
||||
this.tossBall(item, slot);
|
||||
return true;
|
||||
}
|
||||
// TryAddMonToParty with wBattleMode set: GeneratePartyMonStats copies
|
||||
// the wild Pokémon, then SetCaughtData.
|
||||
const mon: PartyMon = {
|
||||
species,
|
||||
item: enemy.item,
|
||||
@@ -944,24 +971,59 @@ export class Battle {
|
||||
stats: calcStats(data, species, enemy.dvs, level),
|
||||
};
|
||||
this.options.party.push(mon);
|
||||
this.strings[RAM.stringBuffer1] = name;
|
||||
yield* this.say(BATTLE.AskGiveNicknameText);
|
||||
if (yield { kind: "yesno", text: "" }) {
|
||||
// NamingScreen (NAME_MON): "<SPECIES>'S" / "NICKNAME?".
|
||||
let second = SYMBOLS.nicknameStrings;
|
||||
while (this.reader.byte(second++) !== 0x50);
|
||||
const context = this.context(),
|
||||
suffix = readString(this.reader, SYMBOLS.nicknameStrings, context),
|
||||
title = readString(this.reader, second, context);
|
||||
const answer = yield { kind: "name", title: `${name}${suffix}\n${title}`, current: name, max: 10 };
|
||||
if (typeof answer !== "string" || answer.length > 10 || (answer && !NICKNAME_PATTERN.test(answer)))
|
||||
throw new UnsupportedBattle("Invalid nickname.");
|
||||
// InitName: a name of zero or more spaces takes the species name.
|
||||
if (!/^ *$/.test(answer)) mon.nickname = answer;
|
||||
}
|
||||
mon.nickname = yield* this.askNickname(name);
|
||||
this.tossBall(item, slot);
|
||||
return true;
|
||||
}
|
||||
/** AskGiveNicknameText, then NamingScreen (NAME_MON) if accepted: the
|
||||
* chosen name, or the species name for a declined or blank one. */
|
||||
private *askNickname(name: string): Gen<string> {
|
||||
this.strings[RAM.stringBuffer1] = name;
|
||||
yield* this.say(BATTLE.AskGiveNicknameText);
|
||||
if (!(yield { kind: "yesno", text: "" })) return name;
|
||||
// "<SPECIES>'S" / "NICKNAME?".
|
||||
let second = SYMBOLS.nicknameStrings;
|
||||
while (this.reader.byte(second++) !== 0x50);
|
||||
const context = this.context(),
|
||||
suffix = readString(this.reader, SYMBOLS.nicknameStrings, context),
|
||||
title = readString(this.reader, second, context);
|
||||
const answer = yield { kind: "name", title: `${name}${suffix}\n${title}`, current: name, max: 10 };
|
||||
if (typeof answer !== "string" || answer.length > 10 || (answer && !NICKNAME_PATTERN.test(answer)))
|
||||
throw new UnsupportedBattle("Invalid nickname.");
|
||||
// InitName: a name of zero or more spaces takes the species name.
|
||||
return /^ *$/.test(answer) ? name : answer;
|
||||
}
|
||||
/** PokeBallEffect's .SendToPC: SendMonIntoBox puts the caught Pokémon
|
||||
* first in the current box (from wEnemyMon, with restored PP), then
|
||||
* SetBoxMonCaughtData. A box filled by it sets BATTLERESULT_BOX_FULL for
|
||||
* Bill's call; the nickname and BallSentToPCText follow. */
|
||||
private *sendToPC(species: number, level: number, name: string): Gen {
|
||||
const data = this.options.data,
|
||||
storage = this.options.storage!,
|
||||
box = currentBox(storage);
|
||||
const mon: BoxMon = {
|
||||
species,
|
||||
item: this.enemy.mon.item,
|
||||
moves: [...this.wildMoves] as BoxMon["moves"],
|
||||
otId: this.options.playerId,
|
||||
ot: this.options.playerName,
|
||||
nickname: name,
|
||||
exp: data.expAtLevel(data.baseData(species).growthRate, level),
|
||||
statExp: [0, 0, 0, 0, 0],
|
||||
dvs: this.enemy.mon.dvs,
|
||||
pp: [...this.wildPP] as BoxMon["pp"],
|
||||
happiness: C.BASE_HAPPINESS,
|
||||
pokerus: 0,
|
||||
caughtData: this.options.caughtData!(level),
|
||||
level,
|
||||
};
|
||||
restorePP(data, mon);
|
||||
box.unshift(mon);
|
||||
if (box.length === MONS_PER_BOX) this.battleResult |= BATTLERESULT_BOX_FULL;
|
||||
mon.nickname = yield* this.askNickname(name);
|
||||
this.strings[RAM.monOrItemNameBuffer] = mon.nickname;
|
||||
yield* this.say(BATTLE.BallSentToPCText);
|
||||
}
|
||||
/** TossItem after a throw: RemoveItemFromPocket takes the ball from the
|
||||
* selected slot if it matches, otherwise from the first matching slot. */
|
||||
private tossBall(item: number, slot: number) {
|
||||
@@ -2798,10 +2860,7 @@ export class Battle {
|
||||
}
|
||||
/** CalcLevel: the highest level whose exp does not exceed the total. */
|
||||
private levelForExp(species: number, exp: number) {
|
||||
const growth = this.options.data.baseData(species).growthRate;
|
||||
let level = 1;
|
||||
while (level < C.MAX_LEVEL && this.options.data.expAtLevel(growth, level + 1) <= exp) level++;
|
||||
return level;
|
||||
return levelForExp(this.options.data, species, exp);
|
||||
}
|
||||
/** LearnLevelMoves: moves the species learns at exactly this level. */
|
||||
private *learnLevelMoves(mon: PartyMon, index: number, level: number): Gen {
|
||||
|
||||
Reference in new issue
Block a user