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
+51
-12
@@ -551,7 +551,12 @@ const isTimed = (effect: ScriptEffect | undefined) =>
|
||||
effect?.kind === "move" ||
|
||||
effect?.kind === "emote" ||
|
||||
effect?.kind === "pause" ||
|
||||
effect?.kind === "fly";
|
||||
effect?.kind === "fly" ||
|
||||
effect?.kind === "headbutt";
|
||||
/** ShakeHeadbuttTree after its graphics load: HideHeadbuttTree's WaitBGMap
|
||||
* (4 frames), 32 frames of shaking, then the WaitBGMap that restores the
|
||||
* tree, 40 frames in all. */
|
||||
const HEADBUTT_TICKS = 20;
|
||||
|
||||
/** Deterministic native world rules. Rendering, browser APIs and emulator
|
||||
* registers are deliberately absent; callers provide time and input. */
|
||||
@@ -639,6 +644,9 @@ export class GameWorld {
|
||||
private warpSettle?: { script?: number };
|
||||
/** FlyFromAnim or FlyToAnim while it plays, with the frames shown. */
|
||||
private flight?: { landing: boolean; species: number; frames: number };
|
||||
/** ShakeHeadbuttTree while it plays: the facing square and the frames
|
||||
* shown. */
|
||||
private shakingTree?: { x: number; y: number; frames: number };
|
||||
|
||||
constructor(
|
||||
readonly assets: CrystalAssets,
|
||||
@@ -828,6 +836,12 @@ export class GameWorld {
|
||||
};
|
||||
}
|
||||
|
||||
/** The tree ShakeHeadbuttTree shakes: its map square and the video
|
||||
* frames since the shaking sprite appeared. */
|
||||
get headbuttTree(): { x: number; y: number; frames: number } | undefined {
|
||||
return this.shakingTree && { ...this.shakingTree };
|
||||
}
|
||||
|
||||
/** Pixel positions for drawing, including partial steps. */
|
||||
poses(): Pose[] {
|
||||
// HideSprites and the Fly animations' loops leave only their icon.
|
||||
@@ -1697,12 +1711,19 @@ export class GameWorld {
|
||||
case OW.HEADBUTT:
|
||||
return tile === OW.COLL_HEADBUTT_TREE ||
|
||||
tile === OW.COLL_HEADBUTT_TREE_1D
|
||||
? { notice: "Headbutt trees are not ported yet." }
|
||||
? { script: OVERWORLD.HeadbuttFromMenuScript, fieldMove: { mon } }
|
||||
: failed;
|
||||
case OW.ROCK_SMASH:
|
||||
return facingObject()?.movement === OW.SPRITEMOVEDATA_SMASHABLE_ROCK
|
||||
? { notice: "Smashing rocks is not ported yet." }
|
||||
case OW.ROCK_SMASH: {
|
||||
// GetFacingObject leaves the rock in hLastTalked.
|
||||
const rock = facingObject();
|
||||
return rock?.movement === OW.SPRITEMOVEDATA_SMASHABLE_ROCK
|
||||
? {
|
||||
script: OVERWORLD.RockSmashFromMenuScript,
|
||||
fieldMove: { mon },
|
||||
lastTalked: rock.id,
|
||||
}
|
||||
: failed;
|
||||
}
|
||||
case OW.SWEET_SCENT: {
|
||||
// SweetScentEncounter, drawn now: no rate roll, and no battle
|
||||
// without a rate or a valid Pokémon.
|
||||
@@ -2307,6 +2328,7 @@ export class GameWorld {
|
||||
wild: effect.wild,
|
||||
trainer,
|
||||
canLose: effect.canLose,
|
||||
tree: effect.tree,
|
||||
text: () => this.host(PLAYER).text(),
|
||||
bag: draft.bag,
|
||||
storage: draft.storage,
|
||||
@@ -2350,6 +2372,16 @@ export class GameWorld {
|
||||
this.flight = { ...effect, frames: 0 };
|
||||
script.waitTicks = effect.landing ? 33 : 65;
|
||||
return true;
|
||||
case "headbutt": {
|
||||
const [dx, dy] = VECTORS[this.player.direction];
|
||||
this.shakingTree = {
|
||||
x: this.player.x + dx,
|
||||
y: this.player.y + dy,
|
||||
frames: 0,
|
||||
};
|
||||
script.waitTicks = HEADBUTT_TICKS;
|
||||
return true;
|
||||
}
|
||||
case "warpcheck":
|
||||
script.warpCheck = true;
|
||||
return false;
|
||||
@@ -2858,7 +2890,9 @@ export class GameWorld {
|
||||
if (script.run.deferred !== undefined)
|
||||
this.startScript(
|
||||
script.run.deferred,
|
||||
PLAYER,
|
||||
(script.run instanceof StartMenuRun &&
|
||||
script.run.deferredMove?.lastTalked) ||
|
||||
PLAYER,
|
||||
script.run instanceof StartMenuRun
|
||||
? {
|
||||
usedItem: script.run.deferredItem,
|
||||
@@ -2890,6 +2924,7 @@ export class GameWorld {
|
||||
this.portrait = undefined;
|
||||
this.evolution = undefined;
|
||||
this.flight = undefined;
|
||||
this.shakingTree = undefined;
|
||||
if (script.sighted !== undefined)
|
||||
this.refusedSighting = {
|
||||
map: this.mapId,
|
||||
@@ -3402,13 +3437,15 @@ export class GameWorld {
|
||||
else this.advanceMovement(1);
|
||||
} else if (isTimed(script.effect)) {
|
||||
// Pauses and emotes keep the map loop running; applymovement and
|
||||
// open text freeze other objects, and the Fly animations run their
|
||||
// own loops.
|
||||
// open text freeze other objects, and the Fly animations and the
|
||||
// Headbutt shake run their own loops.
|
||||
if (this.flight) this.flight.frames += 2;
|
||||
else if (this.shakingTree) this.shakingTree.frames += 2;
|
||||
else this.stepObjects();
|
||||
if (--script.waitTicks <= 0) {
|
||||
if (script.effect?.kind === "emote") this.emote = undefined;
|
||||
this.flight = undefined;
|
||||
this.shakingTree = undefined;
|
||||
this.pump();
|
||||
}
|
||||
}
|
||||
@@ -3649,10 +3686,12 @@ export class GameWorld {
|
||||
return true;
|
||||
}
|
||||
if (tile === OW.COLL_HEADBUTT_TREE || tile === OW.COLL_HEADBUTT_TREE_1D) {
|
||||
// TryHeadbuttOW: without Headbutt nothing happens; tree encounters
|
||||
// (TreeMonEncounter) are not ported.
|
||||
if (this.partyMove(OW.HEADBUTT) < 0) return false;
|
||||
this.notice = "Headbutt trees are not ported yet.";
|
||||
// TryHeadbuttOW: without Headbutt nothing happens.
|
||||
const mon = this.partyMove(OW.HEADBUTT);
|
||||
if (mon < 0) return false;
|
||||
this.startScript(OVERWORLD.AskHeadbuttScript, PLAYER, {
|
||||
fieldMove: { mon },
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return this.trySurfOW(tile);
|
||||
|
||||
Reference in new issue
Block a user