Port Repels and the other non-fishing field items
Repel, Super Repel and Max Repel count down 100, 200 or 250 steps where CountStep does, keep weaker wild Pokémon away from the first party member that has not fainted, and print "REPEL's effect wore off." on the last step. The Coin Case and the Blue Card show their balances, Sacred Ash revives the party once one Pokémon has fainted, and the Normal and Gorgeous Boxes send their trophy home. The Itemfinder looks for unfound hidden items on the original's screen around the player. The Card Key, the Basement Key and the Squirtbottle run the original's checks for their doors and Sudowoodo, whose maps are not open yet. Saves gain the Repel's steps, the coins and the Blue Card's points. Part of #27 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
10e1560212
commit
4cb4676c7d
13 files changed
+821
-21
No files matched your search
+71
-7
@@ -150,6 +150,10 @@ export interface NativeSave {
|
||||
noWildEncounters: boolean;
|
||||
/** wWhichRegisteredItem and wRegisteredItem, for Select. */
|
||||
registeredItem: { which: number; item: number };
|
||||
/** wRepelEffect, wCoins and wBlueCardBalance. */
|
||||
repel: number;
|
||||
coins: number;
|
||||
blueCardBalance: number;
|
||||
/** The option bytes, saved with the game as sOptions is. */
|
||||
options: GameOptions;
|
||||
}
|
||||
@@ -472,6 +476,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;
|
||||
/** 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. */
|
||||
const spawnActor = (object: MapObject): Actor => ({
|
||||
x: object.x,
|
||||
@@ -1461,10 +1467,24 @@ export class GameWorld {
|
||||
);
|
||||
if (event)
|
||||
this.startScript(event.script, PLAYER, { committedPosition: previous });
|
||||
else if (!this.checkSpecialPhoneCall(previous))
|
||||
else if (!this.checkSpecialPhoneCall(previous) && !this.repelStep(previous))
|
||||
this.randomEncounter(previous);
|
||||
}
|
||||
|
||||
/** CountStep's DoRepelStep: a Repel loses a step, and the step that ends
|
||||
* it runs RepelWoreOffScript instead of the encounter check. A save
|
||||
* during that text replays the step with the Repel's last step. */
|
||||
private repelStep(previous: { x: number; y: number }): boolean {
|
||||
const state = this.state;
|
||||
if (!state.repel) return false;
|
||||
if (--state.repel) return false;
|
||||
this.startScript(OVERWORLD.RepelWoreOffScript, PLAYER, {
|
||||
committedPosition: previous,
|
||||
});
|
||||
if (this.script) this.script.committed.repel = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** CountStep / CheckSpecialPhoneCall: a queued call waits for a step
|
||||
* after trainer, warp and coordinate events, before encounters. Its
|
||||
* SpecialPhoneCallList entry supplies the condition and caller script,
|
||||
@@ -1511,11 +1531,17 @@ export class GameWorld {
|
||||
if (!(this.random() < table.rate)) return;
|
||||
const chosen = this.chooseWildEncounter(table);
|
||||
if (!chosen) return;
|
||||
if (!state.party.some((mon) => mon.hp > 0)) return;
|
||||
// CheckRepelEffect: under a Repel, only Pokémon at least as high in
|
||||
// level as the first one that has not fainted.
|
||||
const lead = state.party.find((mon) => mon.hp > 0);
|
||||
if (!lead) return;
|
||||
if (state.repel && chosen.level < lead.level) return;
|
||||
this.startScript(SYMBOLS.wildBattleScript, PLAYER, {
|
||||
committedPosition: previous,
|
||||
wild: chosen,
|
||||
});
|
||||
// The step replays after a reload, so it keeps its Repel step.
|
||||
if (state.repel && this.script) this.script.committed.repel++;
|
||||
}
|
||||
/** GetMapEncounterRate: CheckOnWater picks the water table and rate,
|
||||
* which a Cleanse Tag halves. */
|
||||
@@ -1599,8 +1625,6 @@ export class GameWorld {
|
||||
state.flags.has(flag)
|
||||
? undefined
|
||||
: { texts: [OVERWORLD["CheckBadge.BadgeRequiredText"]] };
|
||||
const facingObject = () =>
|
||||
this.objects().find((object) => object.x === x && object.y === y);
|
||||
switch (move) {
|
||||
case OW.CUT: {
|
||||
const block = this.fieldMoveBlock(x, y, tile, "cut");
|
||||
@@ -1628,7 +1652,7 @@ export class GameWorld {
|
||||
if (
|
||||
this.assets.permission(tile) !== OW.WATER_TILE ||
|
||||
this.sideBlocked(current, tile, this.player.direction) ||
|
||||
facingObject()
|
||||
this.facingObject()
|
||||
)
|
||||
return cant;
|
||||
return {
|
||||
@@ -1715,7 +1739,7 @@ export class GameWorld {
|
||||
: failed;
|
||||
case OW.ROCK_SMASH: {
|
||||
// GetFacingObject leaves the rock in hLastTalked.
|
||||
const rock = facingObject();
|
||||
const rock = this.facingObject();
|
||||
return rock?.movement === OW.SPRITEMOVEDATA_SMASHABLE_ROCK
|
||||
? {
|
||||
script: OVERWORLD.RockSmashFromMenuScript,
|
||||
@@ -1950,8 +1974,30 @@ export class GameWorld {
|
||||
this.assets.collision(this.map, this.player.x, this.player.y),
|
||||
environment: () => this.assets.map(mapId).environment,
|
||||
phoneService: () => this.assets.map(mapId).phoneService,
|
||||
facingObject: () => this.facingObject(),
|
||||
};
|
||||
}
|
||||
/** GetFacingObject: the visible object on the tile the player faces. */
|
||||
private facingObject() {
|
||||
const [dx, dy] = VECTORS[this.player.direction];
|
||||
const x = this.player.x + dx,
|
||||
y = this.player.y + dy;
|
||||
return this.objects().find((object) => object.x === x && object.y === y);
|
||||
}
|
||||
/** CheckForHiddenItems: a BGEVENT_ITEM not found yet on the screen
|
||||
* around the player, from four squares left of and above the player to
|
||||
* five right of and four below it. */
|
||||
private hiddenItemOnScreen(state: ScriptState) {
|
||||
const r = this.assets.reader,
|
||||
{ x, y } = this.player;
|
||||
return this.map.backgroundEvents.some(
|
||||
(event) =>
|
||||
event.kind === BGEVENT_ITEM &&
|
||||
((y + 4 - event.y) & 0xff) < 9 &&
|
||||
((x + 5 - event.x) & 0xff) < 10 &&
|
||||
!state.events.has(r.word(event.script)),
|
||||
);
|
||||
}
|
||||
private weekdayName(day: number) {
|
||||
const r = this.assets.reader;
|
||||
return readString(
|
||||
@@ -2079,6 +2125,7 @@ export class GameWorld {
|
||||
scriptHost: () => this.host(PLAYER),
|
||||
landmarkOf: (mapId) => this.assets.map(mapId).landmark,
|
||||
gameTime: () => this.gameTime,
|
||||
hiddenItemOnScreen: (state) => this.hiddenItemOnScreen(state),
|
||||
},
|
||||
this.menuCursors,
|
||||
select,
|
||||
@@ -3597,7 +3644,7 @@ export class GameWorld {
|
||||
if (set !== (event.kind === 5)) return;
|
||||
script = r.near(event.script + 2, r.bank(event.script));
|
||||
} else if (event.kind === 0) script = event.script;
|
||||
else if (event.kind === 7) {
|
||||
else if (event.kind === BGEVENT_ITEM) {
|
||||
// BGEVENT_ITEM: hiddenitem data (event flag, item) for HiddenItemScript.
|
||||
const hidden = {
|
||||
event: r.word(event.script),
|
||||
@@ -4054,6 +4101,9 @@ export class GameWorld {
|
||||
flash: state.flash,
|
||||
noWildEncounters: state.noWildEncounters,
|
||||
registeredItem: { ...state.registeredItem },
|
||||
repel: state.repel,
|
||||
coins: state.coins,
|
||||
blueCardBalance: state.blueCardBalance,
|
||||
options: { ...state.options },
|
||||
};
|
||||
}
|
||||
@@ -4161,6 +4211,9 @@ export class GameWorld {
|
||||
!save.registeredItem ||
|
||||
!integer(save.registeredItem.which, 0, 255) ||
|
||||
!integer(save.registeredItem.item, 0, 255) ||
|
||||
!integer(save.repel, 0, 255) ||
|
||||
!integer(save.coins, 0, C.MAX_COINS) ||
|
||||
!integer(save.blueCardBalance, 0, 255) ||
|
||||
!validOptions(save.options)
|
||||
) {
|
||||
throw new Error(
|
||||
@@ -4228,6 +4281,9 @@ export class GameWorld {
|
||||
flash: save.flash,
|
||||
noWildEncounters: save.noWildEncounters,
|
||||
registeredItem: { ...save.registeredItem },
|
||||
repel: save.repel,
|
||||
coins: save.coins,
|
||||
blueCardBalance: save.blueCardBalance,
|
||||
options: { ...save.options },
|
||||
specialPhoneCall: save.specialPhoneCall,
|
||||
rivalName: save.rivalName,
|
||||
@@ -4338,6 +4394,11 @@ export function migrateSave(
|
||||
...(!Object.hasOwn(save, "registeredItem") && {
|
||||
registeredItem: { which: 0, item: 0 },
|
||||
}),
|
||||
...(!Object.hasOwn(save, "repel") && {
|
||||
repel: 0,
|
||||
coins: 0,
|
||||
blueCardBalance: 0,
|
||||
}),
|
||||
...(!Object.hasOwn(save, "intro") && { intro: false }),
|
||||
...(!Object.hasOwn(save, "gameTime") && { gameTime: emptyGameTime() }),
|
||||
...(!Object.hasOwn(save, "options") && { options: defaultOptions() }),
|
||||
@@ -4386,6 +4447,9 @@ export function migrateSave(
|
||||
flash: false,
|
||||
noWildEncounters: false,
|
||||
registeredItem: { which: 0, item: 0 },
|
||||
repel: 0,
|
||||
coins: 0,
|
||||
blueCardBalance: 0,
|
||||
intro: false,
|
||||
gameTime: emptyGameTime(),
|
||||
options: defaultOptions(),
|
||||
|
||||
Reference in new issue
Block a user