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) }; }