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:
1 parent
ee55d344af
commit
f259163da9
18 files changed
+789
-36
No files matched your search
@@ -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) };
|
||||
}
|
||||
Reference in new issue
Block a user