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

+26 -2
View File
@@ -175,6 +175,8 @@ export interface BattleOptions {
caughtData?: (level: number) => number;
/** BATTLETYPE_TUTORIAL: the Dude's catching demonstration. */
tutorial?: boolean;
/** BATTLETYPE_TREE: a wild Pokémon that fell from a Headbutt tree. */
tree?: boolean;
/** GetWorldMapLocation for the current map (LevelUpHappinessMod). */
landmark?: number;
/** wPartyMenuCursor as the last party menu outside battle left it. */
@@ -476,6 +478,7 @@ export class Battle {
this.partyMenuCursor = options.partyMenuCursor ?? 0;
if (options.trainer) this.battleMode = C.TRAINER_BATTLE;
if (options.tutorial) this.battleType = C.BATTLETYPE_TUTORIAL;
else if (options.tree) this.battleType = C.BATTLETYPE_TREE;
}
private get reader(): RomReader {
return this.options.data.reader;
@@ -659,7 +662,8 @@ export class Battle {
pp: moves.map((move) => (move ? data.move(move).pp : 0)),
happiness: C.BASE_HAPPINESS,
level,
status: 0,
// .TreeMon: some Pokémon fall from trees asleep.
status: this.sleepingTreeMon(species) ? C.TREEMON_SLEEP_TURNS : 0,
hp: stats[0],
maxHp: stats[0],
stats: stats.slice(1),
@@ -676,6 +680,21 @@ export class Battle {
this.wildMoves = [...moves];
this.wildPP = [...this.enemy.mon.pp];
}
/** CheckSleepingTreeMon: in a tree battle, the time of day's
* AsleepTreeMons list names the species that sleep. */
private sleepingTreeMon(species: number) {
if (this.battleType !== C.BATTLETYPE_TREE) return false;
const time = this.options.timeOfDay;
let p =
time < C.DAY_F
? BATTLE.AsleepTreeMonsMorn
: time === C.DAY_F
? BATTLE.AsleepTreeMonsDay
: BATTLE.AsleepTreeMonsNite;
for (const r = this.reader; r.byte(p) !== 0xff; p++)
if (r.byte(p) === species) return true;
return false;
}
/** InitBattleMon from the party, then status penalties. */
private initBattleMon() {
const mon = this.partyMon,
@@ -773,7 +792,12 @@ export class Battle {
if (trainer) {
yield* this.say(BATTLE.WantsToBattleText);
yield* this.say(BATTLE.BattleText_EnemySentOut);
} else yield* this.say(BATTLE.WildPokemonAppearedText);
} else
yield* this.say(
this.battleType === C.BATTLETYPE_TREE
? BATTLE.PokemonFellFromTreeText
: BATTLE.WildPokemonAppearedText,
);
this.resetBattleParticipants();
this.initBattleMon();
const amuletCoin =