Port the fishing rods and wild Magikarp
The Old, Good and Super Rods run FishFunction facing water: the map's fish group and Fish pick no fish, no bite, or a Pokémon from FishGroups and the time of day's TimeFishGroups. The player casts and jiggles with the fishing frames drawn from LoadFishingGFX's tiles, shows "!" on a bite, and the hooked Pokémon attacks as BATTLETYPE_FISH. Facing pieces now use signed offsets, which the rod above and left of the player needs. Wild Magikarp no longer stop a battle: LoadEnemyMon redraws their DVs through CalcMagikarpLength and its filters, as the original does. The length routine matches the original in the reference emulator. Part of #27 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4cb4676c7d
commit
397dcef44c
20 files changed
+681
-47
No files matched your search
+61
-2
@@ -61,6 +61,7 @@ import {
|
||||
type MenuFieldMove,
|
||||
} from "../scripts/start-menu";
|
||||
import { PartyEvolution } from "../scripts/evolution";
|
||||
import { fish } from "../scripts/fish";
|
||||
import type { MovementPose, MovementStep } from "../scripts/movement";
|
||||
import { emptyBag, validBag, type Bag, type ItemSlot } from "../party/bag";
|
||||
import {
|
||||
@@ -211,6 +212,9 @@ interface Actor {
|
||||
hidden: boolean;
|
||||
fixedFacing: boolean;
|
||||
moved: boolean;
|
||||
/** OBJECT_ACTION_FISHING, from fish_cast_rod or fish_got_bite until
|
||||
* PutTheRodAway: the fishing frames replace the standing ones. */
|
||||
fishing?: boolean;
|
||||
/** HandleNPCStep state for map objects: OBJECT_INIT_X/Y, the step type
|
||||
* and OBJECT_STEP_DURATION. "reset" runs the movement function next. */
|
||||
npc?: {
|
||||
@@ -476,6 +480,8 @@ const forcedDirection = (tile: number): Direction | undefined => {
|
||||
const TURN_WAIT_TICKS = 3;
|
||||
/** STEP_BIKE: 4 ticks at 4 pixels per tick (bikes and ice). */
|
||||
const BIKE_TICKS = 4;
|
||||
/** The emote bubble's first tile (Emotes' vTiles0 tile $f8). */
|
||||
const EMOTE_BUBBLE_TILE = 0xf8;
|
||||
/** BGEVENT_ITEM: a hidden item's bg_event (constants/script_constants.asm). */
|
||||
const BGEVENT_ITEM = 7;
|
||||
/** An object's state as LoadMapObjects creates it from the map data. */
|
||||
@@ -653,6 +659,9 @@ export class GameWorld {
|
||||
/** ShakeHeadbuttTree while it plays: the facing square and the frames
|
||||
* shown. */
|
||||
private shakingTree?: { x: number; y: number; frames: number };
|
||||
/** The EMOTE_* whose graphics are in the emote bubble's tiles, which
|
||||
* show_emote displays. */
|
||||
private bubbleEmote = 0;
|
||||
|
||||
constructor(
|
||||
readonly assets: CrystalAssets,
|
||||
@@ -877,7 +886,9 @@ export class GameWorld {
|
||||
: {};
|
||||
const posed = actor.pose && this.posePresentation(actor.pose);
|
||||
const poseFacing =
|
||||
actor.pose?.pose === "fish-bite" || actor.pose?.pose === "fish-cast"
|
||||
actor.pose?.pose === "fish-bite" ||
|
||||
actor.pose?.pose === "fish-cast" ||
|
||||
(!actor.pose && actor.fishing)
|
||||
? 0x10 + DIRECTION_ORDER.indexOf(actor.direction)
|
||||
: actor.pose?.pose === "tree"
|
||||
? FACING_WEIRD_TREE_0 + ((actor.stepFrame >> 2) & 3)
|
||||
@@ -1977,6 +1988,34 @@ export class GameWorld {
|
||||
facingObject: () => this.facingObject(),
|
||||
};
|
||||
}
|
||||
/** FishFunction with rod 0-2 (Old, Good, Super): never while surfing,
|
||||
* and only facing water (the facing tile may hold an object, as in the
|
||||
* original). The map's fish group decides the script: nothing at all,
|
||||
* no bite, or a bite with the hooked Pokémon. A failure (.FailFish)
|
||||
* prints nothing before Oak's warning. */
|
||||
private fishFunction(state: ScriptState, rod: number): MenuFieldMove {
|
||||
const surfing =
|
||||
state.playerState === OW.PLAYER_SURF ||
|
||||
state.playerState === OW.PLAYER_SURF_PIKA;
|
||||
const [dx, dy] = VECTORS[this.player.direction];
|
||||
const tile =
|
||||
this.assets.collision(this.map, this.player.x + dx, this.player.y + dy) ?? 0;
|
||||
if (surfing || this.assets.permission(tile) !== OW.WATER_TILE)
|
||||
return { texts: [] };
|
||||
const fieldMove = { mon: 0 },
|
||||
group = this.map.fishGroup;
|
||||
if (!group) return { script: OVERWORLD.Script_NotEvenANibble2, fieldMove };
|
||||
const hooked = fish(
|
||||
this.assets.reader,
|
||||
group,
|
||||
rod,
|
||||
this.clockTimeOfDay,
|
||||
() => this.random() & 0xff,
|
||||
);
|
||||
return hooked
|
||||
? { script: OVERWORLD.Script_GotABite, fieldMove, wild: hooked, fish: true }
|
||||
: { script: OVERWORLD.Script_NotEvenANibble, fieldMove };
|
||||
}
|
||||
/** GetFacingObject: the visible object on the tile the player faces. */
|
||||
private facingObject() {
|
||||
const [dx, dy] = VECTORS[this.player.direction];
|
||||
@@ -2021,8 +2060,11 @@ export class GameWorld {
|
||||
callerScript,
|
||||
fieldMove,
|
||||
usedItem,
|
||||
fish,
|
||||
}: {
|
||||
usedItem?: number;
|
||||
/** BATTLETYPE_FISH, set by FishFunction for a hooked Pokémon. */
|
||||
fish?: boolean;
|
||||
fieldMove?: FieldMoveContext;
|
||||
committedPosition?: { x: number; y: number };
|
||||
scene?: boolean;
|
||||
@@ -2055,6 +2097,7 @@ export class GameWorld {
|
||||
callerScript,
|
||||
fieldMove,
|
||||
usedItem,
|
||||
fish,
|
||||
},
|
||||
),
|
||||
committed,
|
||||
@@ -2126,6 +2169,7 @@ export class GameWorld {
|
||||
landmarkOf: (mapId) => this.assets.map(mapId).landmark,
|
||||
gameTime: () => this.gameTime,
|
||||
hiddenItemOnScreen: (state) => this.hiddenItemOnScreen(state),
|
||||
fish: (state, rod) => this.fishFunction(state, rod),
|
||||
},
|
||||
this.menuCursors,
|
||||
select,
|
||||
@@ -2321,6 +2365,7 @@ export class GameWorld {
|
||||
case "emote":
|
||||
// ShowEmoteScript: show (1 tick), pause N ticks, hide (1 tick).
|
||||
this.emote = { object: effect.object, emote: effect.emote };
|
||||
this.bubbleEmote = effect.emote;
|
||||
script.waitTicks = effect.ticks + 2;
|
||||
return true;
|
||||
case "pause":
|
||||
@@ -2376,6 +2421,8 @@ export class GameWorld {
|
||||
trainer,
|
||||
canLose: effect.canLose,
|
||||
tree: effect.tree,
|
||||
fish: effect.fish,
|
||||
mapId: this.mapId,
|
||||
text: () => this.host(PLAYER).text(),
|
||||
bag: draft.bag,
|
||||
storage: draft.storage,
|
||||
@@ -2419,6 +2466,15 @@ export class GameWorld {
|
||||
this.flight = { ...effect, frames: 0 };
|
||||
script.waitTicks = effect.landing ? 33 : 65;
|
||||
return true;
|
||||
case "loademote":
|
||||
// Emotes bound for the bubble's tiles ($f8) replace its graphics;
|
||||
// the others (the rod, the shadow, dust) go elsewhere.
|
||||
if (this.assets.emoteTile(effect.emote) === EMOTE_BUBBLE_TILE)
|
||||
this.bubbleEmote = effect.emote;
|
||||
return false;
|
||||
case "putrodaway":
|
||||
this.player.fishing = false;
|
||||
return false;
|
||||
case "headbutt": {
|
||||
const [dx, dy] = VECTORS[this.player.direction];
|
||||
this.shakingTree = {
|
||||
@@ -2663,6 +2719,8 @@ export class GameWorld {
|
||||
return true;
|
||||
case "pose":
|
||||
actor.pose = this.startPose(actor, step.pose);
|
||||
if (step.pose === "fish-cast" || step.pose === "fish-bite")
|
||||
actor.fishing = true;
|
||||
if (step.pose === "tree") actor.stepFrame = (actor.stepFrame + 1) & 15;
|
||||
movement.wait = step.ticks;
|
||||
return true;
|
||||
@@ -2680,7 +2738,7 @@ export class GameWorld {
|
||||
return false;
|
||||
case "emote":
|
||||
this.emote = step.shown
|
||||
? { object: id, emote: this.emote?.emote ?? 0 }
|
||||
? { object: id, emote: this.bubbleEmote }
|
||||
: undefined;
|
||||
return false;
|
||||
case "shake":
|
||||
@@ -2945,6 +3003,7 @@ export class GameWorld {
|
||||
usedItem: script.run.deferredItem,
|
||||
fieldMove: script.run.deferredMove?.fieldMove,
|
||||
wild: script.run.deferredMove?.wild,
|
||||
fish: script.run.deferredMove?.fish,
|
||||
}
|
||||
: {},
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user