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>
This commit is contained in:
Sébastien QUEROLandClaude Opus 5.5 committed 2026-10-02 12:20:13 +02:00
1 parent ee55d344af
commit f259163da9
18 files changed
+789 -36

No files matched your search

+53 -11
View File
@@ -9,10 +9,21 @@ import { generatePartyMon } from "../src/game/party/pokemon";
* 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. */
* 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",
room:
| boolean
| "poster"
| "center"
| "candy"
| "pokegear"
| "mom"
| "fly"
| "headbutt",
) {
const world = new GameWorld(
assets,
@@ -73,6 +84,21 @@ export function foundationFixture(
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.
@@ -83,15 +109,17 @@ export function foundationFixture(
}
const save = world.save();
const [map, x, y] =
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];
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,
@@ -102,3 +130,17 @@ export function foundationFixture(
});
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.");
}