Files
Crystal/src/game/scripts/treemons.ts
T
Sébastien QUEROLandClaude Opus 5.5 f259163da9 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>
2026-10-02 12:20:13 +02:00

98 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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) };
}