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:
Sébastien QUEROLandClaude Opus 5.5 committed 2026-10-03 01:10:08 +02:00
1 parent e2dc5ba520
commit 5c1ac9d726
17 files changed
+871 -54

No files matched your search

+68 -10
View File
@@ -25,6 +25,12 @@ import { OptionsMenu, menuAccount } from "./options";
import { flyMap } from "./fly-map";
import { OW } from "../world/constants";
import { DecorationData } from "../world/decorations";
import {
MAIL_LINE_LENGTH,
MAILBOX_CAPACITY,
validMailMessage,
type Mail,
} from "../party/mail";
import { readString, readText, type TextContext } from "./text";
type Input = number | string | undefined;
@@ -271,10 +277,7 @@ export class StartMenuRun {
this.strings[RAM.monOrItemNameBuffer] = this.state.party[slot].nickname;
}
private isMail(item: number) {
const r = this.reader;
for (let p = MENU.MailItems; r.byte(p) !== 0xff; p++)
if (r.byte(p) === item) return true;
return false;
return this.data.itemIsMail(item);
}
// --------------------------------------------------------- start menu
@@ -523,7 +526,7 @@ export class StartMenuRun {
case MONMENUITEM_CANCEL:
break;
case MONMENUITEM_MAIL:
yield* this.notice(`MAIL ${NOT_PORTED}`);
yield* this.monMailAction(slot);
break;
default:
return yield* this.fieldMove(slot, item);
@@ -649,16 +652,12 @@ export class StartMenuRun {
item: number,
): Gen {
const mon = this.state.party[slot];
// ComposeMailMessage follows a given MAIL; it is refused before
// anything changes.
if (this.isMail(item)) return yield* this.notice(`Writing MAIL ${NOT_PORTED}`);
this.nameItem(item);
const held = mon.item;
if (!held) {
this.giveItemToPokemon(pocket, index, item);
yield* this.print(MENU.PokemonHoldItemText);
mon.item = item;
return;
return yield* this.givePartyItem(slot, item);
}
if (this.isMail(held)) return yield* this.print(MENU.PokemonRemoveMailText);
this.strings[RAM.stringBuffer1] = this.data.itemName(held);
@@ -669,7 +668,66 @@ export class StartMenuRun {
return yield* this.print(MENU.ItemStorageFullText);
}
yield* this.print(MENU.PokemonSwapItemText);
yield* this.givePartyItem(slot, item);
}
/** GivePartyItem: the Pokémon holds the item, and MAIL gets its message
* (ComposeMailMessage). */
private *givePartyItem(slot: number, item: number): Gen {
const mon = this.state.party[slot];
mon.item = item;
if (!this.isMail(item)) return;
// _ComposeMailMessage's entry, then the author (wPlayerName), wPlayerID,
// wCurPartySpecies and the MAIL (wCurItem).
// The mail entry has no title above its two lines.
const message = yield {
kind: "name",
pages: [""],
current: "",
max: MAIL_LINE_LENGTH,
mail: true,
};
if (!validMailMessage(message)) throw new Error("Invalid MAIL message.");
mon.mail = {
message,
author: this.text().player,
authorId: this.host.scriptHost().player().id,
species: mon.species,
type: item,
};
}
/** MonMailAction: READ, TAKE or QUIT for a Pokémon holding MAIL. TAKE
* sends the MAIL to the PC (SendMailToPC), or puts the item in the pack
* once its message may be lost. */
private *monMailAction(slot: number): Gen {
const mon = this.state.party[slot];
const choice = yield* this.menu(
[],
this.stringList(MENU["MonMailAction.MenuData"] + 2, 3),
);
if (choice === 0) return yield* this.readMail(mon.mail);
if (choice !== 1) return;
if (yield* this.yesNo(MENU["MonMailAction.MailAskSendToPCText"])) {
if (!mon.mail || this.state.mailbox.length >= MAILBOX_CAPACITY)
return yield* this.print(MENU["MonMailAction.MailboxFullText"]);
this.state.mailbox.push(mon.mail);
delete mon.mail;
mon.item = 0;
return yield* this.print(MENU["MonMailAction.MailSentToPCText"]);
}
// .RemoveMailToBag
if (!(yield* this.yesNo(MENU["MonMailAction.MailLoseMessageText"]))) return;
if (!receiveItem(this.data, this.state.bag, mon.item, 1))
return yield* this.print(MENU["MonMailAction.MailNoSpaceText"]);
mon.item = 0;
delete mon.mail;
this.strings[RAM.stringBuffer1] = mon.nickname;
yield* this.print(MENU["MonMailAction.MailDetachedText"]);
}
/** ReadPartyMonMail: the letter, until a button. */
private *readMail(mail: Mail | undefined): Gen {
if (!mail) throw new Error("The Pokémon holds MAIL without a message.");
yield { kind: "mail", mail: { ...mail } };
}
private giveItemToPokemon(pocket: PocketIndex, index: number, item: number) {
const list = this.pocketSlots(pocket);