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
@@ -177,8 +177,13 @@ export interface BattleOptions {
|
||||
tutorial?: boolean;
|
||||
/** BATTLETYPE_TREE: a wild Pokémon that fell from a Headbutt tree. */
|
||||
tree?: boolean;
|
||||
/** BATTLETYPE_FISH: a wild Pokémon hooked on a fishing rod. */
|
||||
fish?: boolean;
|
||||
/** GetWorldMapLocation for the current map (LevelUpHappinessMod). */
|
||||
landmark?: number;
|
||||
/** wMapGroup:wMapNumber, which LoadEnemyMon compares with the Lake of
|
||||
* Rage for wild Magikarp. */
|
||||
mapId?: string;
|
||||
/** wPartyMenuCursor as the last party menu outside battle left it. */
|
||||
partyMenuCursor?: number;
|
||||
/** wMomSavingMoney's MOM_SAVING_MONEY_MASK bits and wMomsMoney: how much
|
||||
@@ -219,6 +224,40 @@ export interface TypeMatchup {
|
||||
/** BattleCommand_DamageCalc's arithmetic. `boost` is a type-boosting held
|
||||
* item's percentage; `current` is the damage already in wCurDamage.
|
||||
* Returns undefined when the original leaves wCurDamage unchanged. */
|
||||
/** MAP_LAKE_OF_RAGE (constants/map_constants.asm). */
|
||||
const LAKE_OF_RAGE = { group: 9, number: 6 };
|
||||
|
||||
/** CalcMagikarpLength (engine/events/magikarp.asm): a length in feet and
|
||||
* inches from the DVs and the trainer ID. The index is rrc(rrc(DV byte))
|
||||
* xor rrc(ID byte), per byte. Its .BCLessThanDE compares only the high
|
||||
* bytes, so MagikarpLengths' thresholds act on the high byte alone and
|
||||
* the difference below them wraps around 16 bits, as in the original. */
|
||||
export function magikarpLength(
|
||||
r: RomReader,
|
||||
dvs: number,
|
||||
playerId: number,
|
||||
): { feet: number; inches: number } {
|
||||
const rrc = (value: number) => ((value >> 1) | (value << 7)) & 0xff;
|
||||
const b = rrc(rrc(dvs >> 8)) ^ rrc((playerId >> 8) & 0xff),
|
||||
c = rrc(rrc(dvs & 0xff)) ^ rrc(playerId & 0xff),
|
||||
bc = (b << 8) | c;
|
||||
let mm: number | undefined;
|
||||
let p = BATTLE.MagikarpLengths;
|
||||
if (bc < 10) mm = bc + 190;
|
||||
else
|
||||
for (let z = 2; z < 16; z++, p += 3)
|
||||
if (b < r.byte(p + 1)) {
|
||||
const quotient = Math.floor(((bc - r.word(p)) & 0xffff) / r.byte(p + 2));
|
||||
mm = ((quotient & 0xff) + 100 * z) & 0xffff;
|
||||
break;
|
||||
}
|
||||
// Past the table: bc minus its last threshold, plus 1600.
|
||||
mm ??= (((bc - r.word(p - 3)) & 0xffff) + 1600) & 0xffff;
|
||||
// Millimetres × 10 / 254 in a byte, then feet and inches.
|
||||
const total = Math.floor(((mm * 10) & 0xffff) / 254) & 0xff;
|
||||
return { feet: Math.floor(total / 12), inches: total % 12 };
|
||||
}
|
||||
|
||||
export function calculateDamage(input: {
|
||||
power: number;
|
||||
level: number;
|
||||
@@ -479,6 +518,7 @@ export class Battle {
|
||||
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;
|
||||
else if (options.fish) this.battleType = C.BATTLETYPE_FISH;
|
||||
}
|
||||
private get reader(): RomReader {
|
||||
return this.options.data.reader;
|
||||
@@ -637,14 +677,16 @@ export class Battle {
|
||||
}
|
||||
if (!this.options.wild) throw new UnsupportedBattle("Missing encounter.");
|
||||
const { species, level } = this.options.wild;
|
||||
if (species === C.UNOWN || species === C.MAGIKARP)
|
||||
throw new UnsupportedBattle("Unown and Magikarp encounters are not ported yet.");
|
||||
if (species === C.UNOWN)
|
||||
throw new UnsupportedBattle("Unown encounters are not ported yet.");
|
||||
const data = this.options.data,
|
||||
base = data.baseData(species);
|
||||
// 25% chance of an item: 23% the first, 2% the second.
|
||||
let item = 0;
|
||||
if (this.random() >= percent(75) + 1) item = this.random() < percent(8) ? base.items[1] : base.items[0];
|
||||
const dvs = (this.random() << 8) | this.random();
|
||||
let dvs: number;
|
||||
do dvs = (this.random() << 8) | this.random();
|
||||
while (species === C.MAGIKARP && this.rerollMagikarp(dvs));
|
||||
const stats = calcStats(data, species, dvs, level);
|
||||
const moves: number[] = [];
|
||||
for (const learned of data.learnset(species)) {
|
||||
@@ -680,6 +722,29 @@ export class Battle {
|
||||
this.wildMoves = [...moves];
|
||||
this.wildPP = [...this.enemy.mon.pp];
|
||||
}
|
||||
/** LoadEnemyMon's .Magikarp filters, true when the DVs are drawn again.
|
||||
* Their limits are in millimetres but CalcMagikarpLength gives feet and
|
||||
* inches: the two size checks need 6 feet, which no Magikarp reaches
|
||||
* (they only draw from Random), and the floor of 1024 mm compares
|
||||
* feet with 4. The Lake of Rage test matches its map group or its map
|
||||
* number on their own, and skips the floor there. */
|
||||
private rerollMagikarp(dvs: number): boolean {
|
||||
const { feet, inches } = magikarpLength(
|
||||
this.reader,
|
||||
dvs,
|
||||
this.options.playerId,
|
||||
);
|
||||
if (feet === 1536 >> 8 && this.random() >= percent(5)) {
|
||||
if (inches >= (1616 & 0xff)) return true;
|
||||
if (this.random() >= percent(20) - 1 && inches >= (1600 & 0xff))
|
||||
return true;
|
||||
}
|
||||
const [group, number] = (this.options.mapId ?? "0:0").split(":").map(Number);
|
||||
if (group === LAKE_OF_RAGE.group || number === LAKE_OF_RAGE.number)
|
||||
return false;
|
||||
if (this.random() < percent(39) + 1) return false;
|
||||
return feet < 1024 >> 8;
|
||||
}
|
||||
/** CheckSleepingTreeMon: in a tree battle, the time of day's
|
||||
* AsleepTreeMons list names the species that sleep. */
|
||||
private sleepingTreeMon(species: number) {
|
||||
@@ -794,9 +859,11 @@ export class Battle {
|
||||
yield* this.say(BATTLE.BattleText_EnemySentOut);
|
||||
} else
|
||||
yield* this.say(
|
||||
this.battleType === C.BATTLETYPE_TREE
|
||||
? BATTLE.PokemonFellFromTreeText
|
||||
: BATTLE.WildPokemonAppearedText,
|
||||
this.battleType === C.BATTLETYPE_FISH
|
||||
? BATTLE.HookedPokemonAttackedText
|
||||
: this.battleType === C.BATTLETYPE_TREE
|
||||
? BATTLE.PokemonFellFromTreeText
|
||||
: BATTLE.WildPokemonAppearedText,
|
||||
);
|
||||
this.resetBattleParticipants();
|
||||
this.initBattleMon();
|
||||
|
||||
Reference in new issue
Block a user