Implement native opening scenes, battle foundation and overworld movement
Checks / web (push) Canceled after 0s
Checks / web (push) Canceled after 0s
This commit is contained in:
1 parent
8b80d05ca2
commit
c08d250cf6
39 files changed
+10727
-1524
No files matched your search
@@ -0,0 +1,190 @@
|
||||
import type { PokemonData } from "../rom/pokemon";
|
||||
|
||||
/** A party Pokémon with the fields of the original party_struct. */
|
||||
export interface PartyMon {
|
||||
species: number;
|
||||
item: number;
|
||||
/** Move IDs; 0 marks an empty slot. */
|
||||
moves: [number, number, number, number];
|
||||
otId: number;
|
||||
ot: string;
|
||||
nickname: string;
|
||||
exp: number;
|
||||
/** HP, Attack, Defense, Speed, Special stat experience. */
|
||||
statExp: [number, number, number, number, number];
|
||||
/** Attack, Defense, Speed, Special DVs as four nibbles. */
|
||||
dvs: number;
|
||||
pp: [number, number, number, number];
|
||||
happiness: number;
|
||||
pokerus: number;
|
||||
/** Time of day and level, then gender and landmark (SetCaughtData). */
|
||||
caughtData: number;
|
||||
level: number;
|
||||
status: number;
|
||||
hp: number;
|
||||
/** Max HP, Attack, Defense, Speed, Sp. Atk, Sp. Def. */
|
||||
stats: [number, number, number, number, number, number];
|
||||
}
|
||||
|
||||
export const PARTY_LENGTH = 6;
|
||||
/** Characters a nickname can hold: the naming screen's letters and
|
||||
* symbols, plus the digits and apostrophes of default species names. */
|
||||
export const NICKNAME_PATTERN = /^[A-Za-z0-9 ()":;[\]?!♂♀/.,×'é-]{1,10}$/u;
|
||||
/** BASE_HAPPINESS */
|
||||
const BASE_HAPPINESS = 70;
|
||||
const MAX_STAT_VALUE = 999;
|
||||
|
||||
/** GetSquareRoot: the first of 1..255 whose square is not below `value`. */
|
||||
function squareRoot(value: number) {
|
||||
for (let root = 1; root < 255; root++) if (root * root >= value) return root;
|
||||
return 255;
|
||||
}
|
||||
|
||||
/** DVs per stat; the HP DV is built from the low bits of the others. */
|
||||
export function statDv(dvs: number, stat: number): number {
|
||||
const attack = (dvs >> 12) & 15,
|
||||
defense = (dvs >> 8) & 15,
|
||||
speed = (dvs >> 4) & 15,
|
||||
special = dvs & 15;
|
||||
if (stat === 0)
|
||||
return (
|
||||
((attack & 1) << 3) |
|
||||
((defense & 1) << 2) |
|
||||
((speed & 1) << 1) |
|
||||
(special & 1)
|
||||
);
|
||||
return [attack, defense, speed, special, special][stat - 1];
|
||||
}
|
||||
|
||||
/** CalcMonStatC for stat 0-5 (HP, Atk, Def, Spd, SpAtk, SpDef). */
|
||||
export function calcStat(
|
||||
base: number,
|
||||
dvs: number,
|
||||
stat: number,
|
||||
level: number,
|
||||
statExp: readonly number[] | undefined,
|
||||
): number {
|
||||
// Special attack and defense share the Special stat experience.
|
||||
const experience = statExp ? statExp[Math.min(stat, 4)] : 0;
|
||||
const bonus = statExp ? Math.floor(squareRoot(experience) / 4) : 0;
|
||||
const value = Math.floor(
|
||||
(((base + statDv(dvs, stat)) * 2 + bonus) * level) / 100,
|
||||
);
|
||||
return Math.min(MAX_STAT_VALUE, value + (stat === 0 ? level + 10 : 5));
|
||||
}
|
||||
|
||||
export function calcStats(
|
||||
data: PokemonData,
|
||||
species: number,
|
||||
dvs: number,
|
||||
level: number,
|
||||
statExp?: readonly number[],
|
||||
): PartyMon["stats"] {
|
||||
const base = data.baseData(species).stats;
|
||||
return base.map((value, stat) =>
|
||||
calcStat(value, dvs, stat, level, statExp),
|
||||
) as PartyMon["stats"];
|
||||
}
|
||||
|
||||
/** FillMoves for a new Pokémon: learn every move up to its level, skipping
|
||||
* repeats and shifting out the oldest move when all slots are full. */
|
||||
export function initialMoves(
|
||||
data: PokemonData,
|
||||
species: number,
|
||||
level: number,
|
||||
): PartyMon["moves"] {
|
||||
const moves: number[] = [];
|
||||
for (const learned of data.learnset(species)) {
|
||||
if (learned.level > level) break;
|
||||
if (moves.includes(learned.move)) continue;
|
||||
if (moves.length === 4) moves.shift();
|
||||
moves.push(learned.move);
|
||||
}
|
||||
while (moves.length < 4) moves.push(0);
|
||||
return moves as PartyMon["moves"];
|
||||
}
|
||||
|
||||
/** GeneratePartyMonStats for a Pokémon given by a script (not a wild or
|
||||
* trainer battle): random DVs, level-up moves, full PP and HP. */
|
||||
export function generatePartyMon(
|
||||
data: PokemonData,
|
||||
options: {
|
||||
species: number;
|
||||
level: number;
|
||||
item: number;
|
||||
otId: number;
|
||||
ot: string;
|
||||
dvs: number;
|
||||
caughtData: number;
|
||||
},
|
||||
): PartyMon {
|
||||
const { species, level } = options;
|
||||
if (!Number.isInteger(level) || level < 1 || level > 100)
|
||||
throw new Error("Invalid level.");
|
||||
const moves = initialMoves(data, species, level);
|
||||
const stats = calcStats(data, species, options.dvs, level);
|
||||
return {
|
||||
species,
|
||||
item: options.item,
|
||||
moves,
|
||||
otId: options.otId,
|
||||
ot: options.ot,
|
||||
nickname: data.speciesName(species),
|
||||
exp: data.expAtLevel(data.baseData(species).growthRate, level),
|
||||
statExp: [0, 0, 0, 0, 0],
|
||||
dvs: options.dvs,
|
||||
pp: moves.map((move) => (move ? data.move(move).pp : 0)) as PartyMon["pp"],
|
||||
happiness: BASE_HAPPINESS,
|
||||
pokerus: 0,
|
||||
caughtData: options.caughtData,
|
||||
level,
|
||||
status: 0,
|
||||
hp: stats[0],
|
||||
stats,
|
||||
};
|
||||
}
|
||||
|
||||
/** Validate a stored Pokémon against the ROM's rules before restoring it. */
|
||||
export function validPartyMon(
|
||||
data: PokemonData,
|
||||
value: unknown,
|
||||
): value is PartyMon {
|
||||
const mon = value as PartyMon;
|
||||
const integer = (value: unknown, min: number, max: number): value is number =>
|
||||
Number.isInteger(value) && Number(value) >= min && Number(value) <= max;
|
||||
const tuple = (value: unknown, length: number, max: number) =>
|
||||
Array.isArray(value) &&
|
||||
value.length === length &&
|
||||
value.every((item) => integer(item, 0, max));
|
||||
try {
|
||||
if (
|
||||
!mon ||
|
||||
typeof mon !== "object" ||
|
||||
!integer(mon.species, 1, 251) ||
|
||||
!integer(mon.item, 0, 255) ||
|
||||
!tuple(mon.moves, 4, 251) ||
|
||||
!integer(mon.otId, 0, 0xffff) ||
|
||||
typeof mon.ot !== "string" ||
|
||||
!/^[A-Za-z0-9 -]{1,7}$/.test(mon.ot) ||
|
||||
typeof mon.nickname !== "string" ||
|
||||
!NICKNAME_PATTERN.test(mon.nickname) ||
|
||||
!integer(mon.exp, 0, 0xffffff) ||
|
||||
!tuple(mon.statExp, 5, 0xffff) ||
|
||||
!integer(mon.dvs, 0, 0xffff) ||
|
||||
!tuple(mon.pp, 4, 255) ||
|
||||
!integer(mon.happiness, 0, 255) ||
|
||||
!integer(mon.pokerus, 0, 255) ||
|
||||
!integer(mon.caughtData, 0, 0xffff) ||
|
||||
!integer(mon.level, 1, 100) ||
|
||||
!integer(mon.status, 0, 255) ||
|
||||
!tuple(mon.stats, 6, MAX_STAT_VALUE) ||
|
||||
!integer(mon.hp, 0, mon.stats[0])
|
||||
)
|
||||
return false;
|
||||
// Stored stats must be the ones the game would calculate.
|
||||
const stats = calcStats(data, mon.species, mon.dvs, mon.level, mon.statExp);
|
||||
return stats.every((stat, index) => stat === mon.stats[index]);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user