Files
Crystal/tests/foundation-fixture.ts
T
Sébastien QUEROLandClaude Opus 5.5 f259163da9 Port Headbutt and Rock Smash encounters
Facing a Headbutt tree asks to HEADBUTT it, and HEADBUTT from the party
menu skips the question. ShakeHeadbuttTree shakes the tree as a sprite over
grass, then TreeMonEncounter scores the facing tile against the trainer ID
and draws from the map's common or rare tree table. A Pokémon that falls
fights as BATTLETYPE_TREE: "fell out of the tree!", and the time of day's
AsleepTreeMons species start asleep.

Smashable rocks run SmashRockScript, and ROCK SMASH from the party menu
acts on the facing rock; after the rock breaks, RockMonEncounter gives the
map's rock set a 40% chance of a wild battle.

Closes #26

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:20:13 +02:00

147 lines
4.8 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. */
export function foundationFixture(
assets: CrystalAssets,
room:
| boolean
| "poster"
| "center"
| "candy"
| "pokegear"
| "mom"
| "fly"
| "headbutt",
) {
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") {
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 === "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 === "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 === "center" || room === "candy"
? ["26:5", 9, 2]
: room === "poster"
? ["24:7", 6, 1]
: room === "mom"
? ["24:6", 7, 3]
: room && room !== "pokegear" && room !== "fly"
? ["24:7", 2, 2]
: ["24:4", 9, 6];
world.restore({
...save,
map,
x,
y,
direction: room === "mom" ? "down" : "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.");
}