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>
175 lines
5.7 KiB
TypeScript
175 lines
5.7 KiB
TypeScript
import { CrystalAssets } from "../src/game/rom/assets";
|
|
import { GameWorld } from "../src/game/world/world";
|
|
import { OW } from "../src/game/world/constants";
|
|
import { generatePartyMon } from "../src/game/party/pokemon";
|
|
|
|
/** `room` places the player at the bedroom PC or poster, "center" at
|
|
* Cherrygrove's Pokémon Center PC with two Pokémon ("candy": the first at
|
|
* level 13, with a Rare Candy); otherwise outdoors ("pokegear": with the
|
|
* Pokégear, its map and phone cards, and Mom's and Elm's numbers). "mom"
|
|
* faces Mom in the living room after the Mystery Egg, with her bank open
|
|
* and ¥2300 saved. "fly" stands outdoors with a Pidgeot that knows Fly,
|
|
* the Storm Badge, and New Bark Town and Cherrygrove City visited.
|
|
* "headbutt" faces Route 29's first reachable Headbutt tree from below
|
|
* with a Pokémon that knows Headbutt; Random always returning 0 drops a
|
|
* sleeping Hoothoot. "fishing" faces the sea on New Bark Town's shore
|
|
* with the Old Rod, which then hooks a Magikarp. "mail" is "center" with a
|
|
* FLOWER MAIL in the pack. */
|
|
export function foundationFixture(
|
|
assets: CrystalAssets,
|
|
room:
|
|
| boolean
|
|
| "poster"
|
|
| "center"
|
|
| "candy"
|
|
| "pokegear"
|
|
| "mom"
|
|
| "fly"
|
|
| "headbutt"
|
|
| "fishing"
|
|
| "mail",
|
|
) {
|
|
const world = new GameWorld(
|
|
assets,
|
|
() => new Date("2026-09-22T12:00:00"),
|
|
() => 0,
|
|
);
|
|
world.state.scenes["24:4"] = 1;
|
|
world.state.bag.keyItems.push(OW.BICYCLE);
|
|
if (room === "center" || room === "candy" || room === "mail") {
|
|
world.state.party = [155, 16].map((species, i) =>
|
|
generatePartyMon(assets.pokemon, {
|
|
species,
|
|
level: room === "candy" && !i ? 13 : 8 - i * 4,
|
|
item: 0,
|
|
otId: world.playerId,
|
|
ot: "CHRIS",
|
|
dvs: 0x9abc,
|
|
caughtData: 0,
|
|
}),
|
|
);
|
|
world.state.bag.items = [{ item: 18, quantity: 5 }]; // POTION
|
|
if (room === "candy")
|
|
world.state.bag.items.push({ item: 0x20, quantity: 1 });
|
|
if (room === "mail")
|
|
world.state.bag.items.push({ item: 0x9e, quantity: 1 });
|
|
}
|
|
if (room === "pokegear" || room === "mom") {
|
|
world.state.party = [
|
|
generatePartyMon(assets.pokemon, {
|
|
species: 155,
|
|
level: 8,
|
|
item: 0,
|
|
otId: world.playerId,
|
|
ot: "CHRIS",
|
|
dvs: 0x9abc,
|
|
caughtData: 0,
|
|
}),
|
|
];
|
|
for (const flag of [OW.ENGINE_MAP_CARD, OW.ENGINE_PHONE_CARD, 4])
|
|
world.state.flags.add(flag); // 4: ENGINE_POKEGEAR
|
|
world.state.phone = [OW.PHONECONTACT_MOM, OW.PHONECONTACT_ELM];
|
|
world.state.events.add(26); // EVENT_GOT_A_POKEMON_FROM_ELM
|
|
// Walking outside clears the bedroom's temporary events.
|
|
for (let event = 0; event < 8; event++) world.state.events.delete(event);
|
|
}
|
|
if (room === "fly") {
|
|
const mon = generatePartyMon(assets.pokemon, {
|
|
species: 18, // PIDGEOT
|
|
level: 40,
|
|
item: 0,
|
|
otId: world.playerId,
|
|
ot: "CHRIS",
|
|
dvs: 0x9abc,
|
|
caughtData: 0,
|
|
});
|
|
mon.moves = [OW.FLY, 0, 0, 0];
|
|
mon.pp = [15, 0, 0, 0];
|
|
world.state.party = [mon];
|
|
world.state.flags.add(OW.ENGINE_STORMBADGE);
|
|
for (const spawn of [14, 15])
|
|
// SPAWN_NEW_BARK, SPAWN_CHERRYGROVE
|
|
world.state.flags.add(assets.spawnEngineFlag(spawn)!);
|
|
for (let event = 0; event < 8; event++) world.state.events.delete(event);
|
|
}
|
|
if (room === "fishing") {
|
|
world.state.party = [
|
|
generatePartyMon(assets.pokemon, {
|
|
species: 155,
|
|
level: 8,
|
|
item: 0,
|
|
otId: world.playerId,
|
|
ot: "CHRIS",
|
|
dvs: 0x9abc,
|
|
caughtData: 0,
|
|
}),
|
|
];
|
|
world.state.bag.keyItems.push(0x3a); // OLD_ROD
|
|
for (let event = 0; event < 8; event++) world.state.events.delete(event);
|
|
}
|
|
if (room === "headbutt") {
|
|
const mon = generatePartyMon(assets.pokemon, {
|
|
species: 158, // TOTODILE
|
|
level: 30,
|
|
item: 0,
|
|
otId: world.playerId,
|
|
ot: "CHRIS",
|
|
dvs: 0x9abc,
|
|
caughtData: 0,
|
|
});
|
|
mon.moves = [OW.HEADBUTT, 33, 0, 0]; // TACKLE
|
|
mon.pp = [15, 35, 0, 0];
|
|
world.state.party = [mon];
|
|
for (let event = 0; event < 8; event++) world.state.events.delete(event);
|
|
}
|
|
if (room === "mom") {
|
|
// EVENT_GOT_MYSTERY_EGG_FROM_MR_POKEMON, EVENT_GAVE_MYSTERY_EGG_TO_ELM
|
|
// and EVENT_TALKED_TO_MOM_AFTER_MYSTERY_EGG_QUEST.
|
|
for (const event of [30, 31, 64]) world.state.events.add(event);
|
|
world.state.flags.add(OW.ENGINE_MOM_ACTIVE);
|
|
world.state.scenes["24:6"] = 1;
|
|
world.state.momsMoney = 2300;
|
|
}
|
|
const save = world.save();
|
|
const [map, x, y] =
|
|
room === "headbutt"
|
|
? headbuttSpot(assets)
|
|
: room === "fishing"
|
|
? ["24:4", 17, 7]
|
|
: room === "center" || room === "candy" || room === "mail"
|
|
? ["26:5", 9, 2]
|
|
: room === "poster"
|
|
? ["24:7", 6, 1]
|
|
: room === "mom"
|
|
? ["24:6", 7, 3]
|
|
: room &&
|
|
room !== "pokegear" &&
|
|
room !== "fly" &&
|
|
room !== "fishing"
|
|
? ["24:7", 2, 2]
|
|
: ["24:4", 9, 6];
|
|
world.restore({
|
|
...save,
|
|
map,
|
|
x,
|
|
y,
|
|
direction: room === "mom" ? "down" : room === "fishing" ? "right" : "up",
|
|
objects: [],
|
|
});
|
|
return world;
|
|
}
|
|
|
|
/** Below Route 29's first Headbutt tree with walkable ground under it. */
|
|
function headbuttSpot(assets: CrystalAssets): [string, number, number] {
|
|
const map = assets.map("24:3");
|
|
for (let y = 1; y < map.height - 1; y++)
|
|
for (let x = 0; x < map.width; x++)
|
|
if (
|
|
assets.collision(map, x, y) === OW.COLL_HEADBUTT_TREE &&
|
|
assets.permission(assets.collision(map, x, y + 1)) === 0 &&
|
|
!map.objects.some((o) => o.x === x && o.y <= y + 1 && o.y >= y)
|
|
)
|
|
return ["24:3", x, y + 1];
|
|
throw new Error("No reachable Headbutt tree.");
|
|
}
|