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

+97
View File
@@ -0,0 +1,97 @@
import type { RomReader } from "../rom/reader";
import { OVERWORLD } from "../rom/profile";
import { OW } from "../world/constants";
/** Headbutt trees and Rock Smash rocks (engine/events/treemons.asm). */
export interface TreeMon {
species: number;
level: number;
}
/** TREEMON_SCORE_BAD, _GOOD and _RARE. */
const SCORE_BAD = 0,
SCORE_GOOD = 1,
SCORE_RARE = 2;
/** TreeMonEncounter: the map's TreeMonMaps set, scored by the facing tile
* (in wPlayerMapX/Y coordinates, four more than the map's) against the
* trainer ID, then an encounter roll and SelectTreeMon. Undefined when
* nothing falls; a Pokémon found fights as BATTLETYPE_TREE. */
export function treeMonEncounter(
r: RomReader,
mapId: string,
facing: { x: number; y: number },
playerId: number,
randomRange: (range: number) => number,
): TreeMon | undefined {
let table = treeMons(r, treeMonSet(r, OVERWORLD.TreeMonMaps, mapId));
if (table === undefined) return undefined;
// GetTreeMon: 10% for a bad score, 50% for a good one, 80% for a rare
// one, which also skips past the common table to the rare one.
const score = treeScore(facing.x, facing.y, playerId);
const draw = randomRange(10);
if (score === SCORE_BAD && draw !== 0) return undefined;
if (score === SCORE_GOOD && draw >= 5) return undefined;
if (score === SCORE_RARE) {
if (draw >= 8) return undefined;
while (r.byte(table++) !== 0xff);
}
return selectTreeMon(r, table, randomRange);
}
/** RockMonEncounter: the map's RockMonMaps set, a 40% chance, then
* SelectTreeMon. Undefined when the rock hides nothing. */
export function rockMonEncounter(
r: RomReader,
mapId: string,
randomRange: (range: number) => number,
): TreeMon | undefined {
const table = treeMons(r, treeMonSet(r, OVERWORLD.RockMonMaps, mapId));
if (table === undefined) return undefined;
if (randomRange(10) >= 4) return undefined;
return selectTreeMon(r, table, randomRange);
}
/** GetTreeScore: .CoordScore is (x × y + x + y) / 5 modulo 10 and
* .OTIDScore the trainer ID modulo 10; equal scores are rare, and a
* coordinate score less than five above the ID's (wrapping by ten) good. */
export function treeScore(x: number, y: number, playerId: number): number {
const coord = Math.floor(((x * y + x + y) & 0xffff) / 5) % 10;
let difference = coord - (playerId % 10);
if (difference === 0) return SCORE_RARE;
if (difference < 0) difference += 10;
return difference < 5 ? SCORE_GOOD : SCORE_BAD;
}
/** GetTreeMonSet: the TREEMON_SET_* of map `mapId` in TreeMonMaps or
* RockMonMaps (group, number, set entries ending with -1). */
function treeMonSet(r: RomReader, table: number, mapId: string) {
const [group, number] = mapId.split(":").map(Number);
for (let p = table; r.byte(p) !== 0xff; p += 3)
if (r.byte(p) === group && r.byte(p + 1) === number) return r.byte(p + 2);
return undefined;
}
/** GetTreeMons: the set's common table in TreeMons; TREEMON_SET_NONE and
* sets past NUM_TREEMON_SETS have none. */
function treeMons(r: RomReader, set: number | undefined) {
if (!set || set >= OW.NUM_TREEMON_SETS) return undefined;
return r.near(OVERWORLD.TreeMons + set * 2, r.bank(OVERWORLD.TreeMons));
}
/** SelectTreeMon: 0-99 against the table's percentages (percent, species,
* level entries ending with -1). */
function selectTreeMon(
r: RomReader,
table: number,
randomRange: (range: number) => number,
): TreeMon | undefined {
let roll = randomRange(100),
p = table;
while (roll >= r.byte(p)) {
roll -= r.byte(p);
p += 3;
}
if (r.byte(p) === 0xff) return undefined;
return { species: r.byte(p + 1), level: r.byte(p + 2) };
}