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>
98 lines
3.6 KiB
TypeScript
98 lines
3.6 KiB
TypeScript
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) };
|
||
}
|