Port MAIL in the party menu and the PC's MAIL BOX
Giving MAIL opens the mail entry: two lines of up to sixteen characters, kept with the player's name and ID, the holder's species and the MAIL. The party submenu's MAIL reads the letter or takes it, either to the PC's MAILBOX (ten at most) or into the pack without its message. The player's PC lists the MAILBOX by author to read, put in the pack or attach, and Bill's PC keeps a Pokémon holding MAIL out of the boxes. Saves keep each holder's MAIL and the MAILBOX. The letter shows its message and author natively; the stationery's graphics are not drawn yet. Closes #27 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e2dc5ba520
commit
5c1ac9d726
17 files changed
+871
-54
No files matched your search
+56
-7
@@ -62,6 +62,13 @@ import {
|
||||
} from "../scripts/start-menu";
|
||||
import { PartyEvolution } from "../scripts/evolution";
|
||||
import { fish } from "../scripts/fish";
|
||||
import {
|
||||
copyMail,
|
||||
MAILBOX_CAPACITY,
|
||||
validMail,
|
||||
validMailMessage,
|
||||
type Mail,
|
||||
} from "../party/mail";
|
||||
import type { MovementPose, MovementStep } from "../scripts/movement";
|
||||
import { emptyBag, validBag, type Bag, type ItemSlot } from "../party/bag";
|
||||
import {
|
||||
@@ -151,6 +158,8 @@ export interface NativeSave {
|
||||
noWildEncounters: boolean;
|
||||
/** wWhichRegisteredItem and wRegisteredItem, for Select. */
|
||||
registeredItem: { which: number; item: number };
|
||||
/** sMailboxes: the PC's MAIL. */
|
||||
mailbox: Mail[];
|
||||
/** wRepelEffect, wCoins and wBlueCardBalance. */
|
||||
repel: number;
|
||||
coins: number;
|
||||
@@ -255,8 +264,15 @@ export type Choice =
|
||||
| { kind: "weekday"; names: string[]; selected: number }
|
||||
/** InitClock's hour or minute: Up and Down wrap through `labels`. */
|
||||
| { kind: "clock"; labels: string[]; selected: number }
|
||||
/** The naming screen: an empty answer keeps `current`. */
|
||||
| { kind: "name"; current: string; max: number; selected?: undefined }
|
||||
/** The naming screen: an empty answer keeps `current`. With `mail`, the
|
||||
* mail entry's two lines. */
|
||||
| {
|
||||
kind: "name";
|
||||
current: string;
|
||||
max: number;
|
||||
mail?: boolean;
|
||||
selected?: undefined;
|
||||
}
|
||||
/** A vertical menu; `selected` answers with that item's index. */
|
||||
| {
|
||||
kind: "menu";
|
||||
@@ -2520,6 +2536,10 @@ export class GameWorld {
|
||||
this.statsScreen = effect.mon;
|
||||
this.dialogue = { pages: ["Stats"], index: 0 };
|
||||
return true;
|
||||
case "mail":
|
||||
this.mailView = { ...effect.mail };
|
||||
this.dialogue = { pages: ["MAIL"], index: 0 };
|
||||
return true;
|
||||
case "pokegear":
|
||||
this.pokegear = effect.view;
|
||||
if (!effect.view) return false;
|
||||
@@ -2585,7 +2605,12 @@ export class GameWorld {
|
||||
this.dialogue = {
|
||||
pages: effect.pages,
|
||||
index: 0,
|
||||
choice: { kind: "name", current: effect.current, max: effect.max },
|
||||
choice: {
|
||||
kind: "name",
|
||||
current: effect.current,
|
||||
max: effect.max,
|
||||
...(effect.mail && { mail: true }),
|
||||
},
|
||||
};
|
||||
return true;
|
||||
case "martmenu":
|
||||
@@ -2989,6 +3014,7 @@ export class GameWorld {
|
||||
this.memScript ??= script.memScript;
|
||||
this.townMap = false;
|
||||
this.statsScreen = undefined;
|
||||
this.mailView = undefined;
|
||||
this.pokegear = undefined;
|
||||
this.trainerCard = undefined;
|
||||
this.dialogue = undefined;
|
||||
@@ -3041,6 +3067,7 @@ export class GameWorld {
|
||||
this.script = undefined;
|
||||
this.townMap = false;
|
||||
this.statsScreen = undefined;
|
||||
this.mailView = undefined;
|
||||
this.pokegear = undefined;
|
||||
this.trainerCard = undefined;
|
||||
this.dialogue = undefined;
|
||||
@@ -3484,6 +3511,8 @@ export class GameWorld {
|
||||
townMap = false;
|
||||
/** The Pokémon a script's stats screen shows (Bill's PC). */
|
||||
statsScreen: PartyMon | undefined;
|
||||
/** A letter ReadAnyMail shows, until a button closes it. */
|
||||
mailView: Mail | undefined;
|
||||
/** The Pokégear card on screen while the Pokégear is open, including
|
||||
* during the phone calls it makes. */
|
||||
pokegear: PokegearView | undefined;
|
||||
@@ -3547,6 +3576,7 @@ export class GameWorld {
|
||||
if (--dialogue.holdTicks <= 0) {
|
||||
this.townMap = false;
|
||||
this.statsScreen = undefined;
|
||||
this.mailView = undefined;
|
||||
this.dialogue = undefined;
|
||||
this.pump();
|
||||
}
|
||||
@@ -4002,6 +4032,7 @@ export class GameWorld {
|
||||
}
|
||||
this.townMap = false;
|
||||
this.statsScreen = undefined;
|
||||
this.mailView = undefined;
|
||||
this.dialogue = undefined;
|
||||
this.pump();
|
||||
}
|
||||
@@ -4095,9 +4126,11 @@ export class GameWorld {
|
||||
// The naming screen only offers its own characters; keep it open.
|
||||
if (
|
||||
dialogue.choice.kind === "name" &&
|
||||
(typeof answer !== "string" ||
|
||||
answer.length > dialogue.choice.max ||
|
||||
(answer !== "" && !NICKNAME_PATTERN.test(answer)))
|
||||
(dialogue.choice.mail
|
||||
? !validMailMessage(answer)
|
||||
: typeof answer !== "string" ||
|
||||
answer.length > dialogue.choice.max ||
|
||||
(answer !== "" && !NICKNAME_PATTERN.test(answer)))
|
||||
)
|
||||
return;
|
||||
if (
|
||||
@@ -4115,6 +4148,7 @@ export class GameWorld {
|
||||
return;
|
||||
this.townMap = false;
|
||||
this.statsScreen = undefined;
|
||||
this.mailView = undefined;
|
||||
this.dialogue = undefined;
|
||||
this.pump(answer);
|
||||
}
|
||||
@@ -4184,6 +4218,7 @@ export class GameWorld {
|
||||
flash: state.flash,
|
||||
noWildEncounters: state.noWildEncounters,
|
||||
registeredItem: { ...state.registeredItem },
|
||||
mailbox: state.mailbox.map(copyMail),
|
||||
repel: state.repel,
|
||||
coins: state.coins,
|
||||
blueCardBalance: state.blueCardBalance,
|
||||
@@ -4255,7 +4290,17 @@ export class GameWorld {
|
||||
!integer(save.playerId, 0, 0xffff) ||
|
||||
!Array.isArray(save.party) ||
|
||||
save.party.length > 6 ||
|
||||
!save.party.every((mon) => validPartyMon(this.assets.pokemon, mon)) ||
|
||||
!save.party.every(
|
||||
(mon) =>
|
||||
validPartyMon(this.assets.pokemon, mon) &&
|
||||
// sPartyMail: a message exactly while MAIL is held.
|
||||
(this.assets.pokemon.itemIsMail(mon.item)
|
||||
? validMail(this.assets.pokemon, mon.mail)
|
||||
: !Object.hasOwn(mon, "mail")),
|
||||
) ||
|
||||
!Array.isArray(save.mailbox) ||
|
||||
save.mailbox.length > MAILBOX_CAPACITY ||
|
||||
!save.mailbox.every((mail) => validMail(this.assets.pokemon, mail)) ||
|
||||
!validBag(this.assets.pokemon, save.bag) ||
|
||||
!validStorage(save.storage) ||
|
||||
!validPCItems(this.assets.pokemon, save.pcItems) ||
|
||||
@@ -4364,6 +4409,7 @@ export class GameWorld {
|
||||
flash: save.flash,
|
||||
noWildEncounters: save.noWildEncounters,
|
||||
registeredItem: { ...save.registeredItem },
|
||||
mailbox: save.mailbox.map(copyMail),
|
||||
repel: save.repel,
|
||||
coins: save.coins,
|
||||
blueCardBalance: save.blueCardBalance,
|
||||
@@ -4416,6 +4462,7 @@ export class GameWorld {
|
||||
this.playerId = save.playerId;
|
||||
this.townMap = false;
|
||||
this.statsScreen = undefined;
|
||||
this.mailView = undefined;
|
||||
this.pokegear = undefined;
|
||||
this.trainerCard = undefined;
|
||||
this.dialogue = undefined;
|
||||
@@ -4477,6 +4524,7 @@ export function migrateSave(
|
||||
...(!Object.hasOwn(save, "registeredItem") && {
|
||||
registeredItem: { which: 0, item: 0 },
|
||||
}),
|
||||
...(!Object.hasOwn(save, "mailbox") && { mailbox: [] }),
|
||||
...(!Object.hasOwn(save, "repel") && {
|
||||
repel: 0,
|
||||
coins: 0,
|
||||
@@ -4530,6 +4578,7 @@ export function migrateSave(
|
||||
flash: false,
|
||||
noWildEncounters: false,
|
||||
registeredItem: { which: 0, item: 0 },
|
||||
mailbox: [],
|
||||
repel: 0,
|
||||
coins: 0,
|
||||
blueCardBalance: 0,
|
||||
|
||||
Reference in new issue
Block a user