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

+14 -5
View File
@@ -537,7 +537,7 @@ describe.skipIf(!existsSync(romPath))("field moves with the local ROM", () => {
closeMenus(world);
});
it("recognises a smashable rock for Rock Smash, which is not ported further", () => {
it("recognises a smashable rock for Rock Smash and smashes it", () => {
// A rock (SPRITEMOVEDATA_SMASHABLE_ROCK) in Dark Cave at 27, 6.
const spot = { map: "3:78", x: 27, y: 6 };
const { assets, world } = fixture([spot.map]);
@@ -552,10 +552,19 @@ describe.skipIf(!existsSync(romPath))("field moves with the local ROM", () => {
expect(useFromMenu(world, "ROCK SMASH")).toEqual(["Can't use that\nhere."]);
closeMenus(world);
world.restore({ ...world.save(), direction: "up" });
useFromMenu(world, "ROCK SMASH");
expect(world.notice).toBe("Smashing rocks is not ported yet.");
closeMenus(world);
expect(world.diagnostic).toBe("");
const shown = useFromMenu(world, "ROCK SMASH");
for (let i = 0; i < 10 && world.dialogue?.choice; i++) world.advanceDialogue();
// Random always returns 0 here, so the rock hides a Krabby.
for (let tick = 0; tick < 500 && !world.battle; tick++) {
if (world.dialogue) {
shown.push(world.dialogue.pages[world.dialogue.index]);
world.advanceDialogue();
} else world.tick();
}
expect(world.notice, world.diagnostic).toBe("");
expect(shown).toContain("SWIMMY used\nROCK SMASH!");
expect(world.objects().some((o) => o.x === spot.x && o.y === spot.y)).toBe(false);
expect(world.battle?.engine.enemy.mon.species).toBe(98); // KRABBY
});
it("draws a wild Pokémon with Sweet Scent in grass, and nothing elsewhere", () => {