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

+40 -1
View File
@@ -10,6 +10,11 @@ import {
type RenderViewport,
} from "./viewport";
/** Video frames before LoadOverworldTilemapAndAttrmapPals puts the
* Headbutt tree back: HideHeadbuttTree's WaitBGMap and the 32 shaking
* frames. */
const HEADBUTT_HIDDEN_FRAMES = 36;
export class WorldRenderer {
width: number = BASE_VIEWPORT.width;
height: number = BASE_VIEWPORT.height;
@@ -91,6 +96,10 @@ export class WorldRenderer {
const cameraX = Math.round(player.x - (this.width / 2 - 8)),
cameraY = Math.round(player.y - (this.height / 2 - 8)) + world.screenShake;
this.image.data.fill(255);
// HideHeadbuttTree puts tile $05 (grass) in the tree's four tilemap
// places, keeping their attributes, until the shaking ends.
const tree = world.headbuttTree;
const hidden = tree && tree.frames < HEADBUTT_HIDDEN_FRAMES ? tree : undefined;
for (
let tileY = Math.floor(cameraY / 8);
tileY < Math.ceil((cameraY + this.height) / 8);
@@ -111,7 +120,12 @@ export class WorldRenderer {
const encoded = tileset.metatiles[block * 16 + localY * 4 + localX];
const packed = tileset.paletteMap[encoded >> 1],
attribute = encoded & 1 ? packed >> 4 : packed & 15;
const tile = (encoded & 0x7f) + (attribute & 8 ? 0x60 : 0);
const covered =
hidden &&
tileX >> 1 === hidden.x &&
tileY >> 1 === hidden.y;
const tile =
(covered ? 0x05 : encoded & 0x7f) + (attribute & 8 ? 0x60 : 0);
drawTile(
this.image.data,
this.width,
@@ -200,6 +214,31 @@ export class WorldRenderer {
);
}
}
if (tree) {
// SPRITE_ANIM_OBJ_HEADBUTT over the tree's square, in PAL_OW_TREE:
// the two frames alternate every three animation steps, the second
// mirrored on every other pass. The first step lasts through
// HideHeadbuttTree's WaitBGMap, the last until the sprite is cleared.
const graphics = this.assets.headbuttTree(),
step = Math.min(32, Math.max(0, tree.frames - 4)),
phase = Math.floor(step / 3) % 4,
flip = phase === 3,
palette = this.assets.objectPalette(6, time);
for (let tile = 0; tile < 4; tile++) {
const column = tile & 1;
drawTile(
this.image.data,
this.width,
this.height,
graphics,
(phase & 1) * 4 + tile,
tree.x * 16 - cameraX + (flip ? 8 - column * 8 : column * 8),
tree.y * 16 - cameraY + (tile >> 1) * 8,
palette,
{ transparent: true, flipX: flip },
);
}
}
this.context.putImageData(this.image, 0, 0);
}
private actor(