From cce6acec6f278de6bab2ae046f7313f30bdef102 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 15 Jun 2025 09:45:28 -0400 Subject: [PATCH] IV safety part 0.5 --- src/@types/stat-types.ts | 3 ++- src/@types/utility-types/global-augments.ts | 9 +++++++ src/battle-scene.ts | 25 ++++++++----------- src/data/egg.ts | 7 +++--- .../encounters/weird-dream-encounter.ts | 2 +- .../utils/encounter-phase-utils.ts | 3 ++- src/field/pokemon.ts | 4 +-- src/phases/encounter-phase.ts | 3 ++- src/system/game-data.ts | 11 ++++++-- src/system/pokemon-data.ts | 5 ++-- src/utils/common.ts | 6 +++-- test/moves/fusion_flare_bolt.test.ts | 12 +++++++-- 12 files changed, 59 insertions(+), 31 deletions(-) create mode 100644 src/@types/utility-types/global-augments.ts diff --git a/src/@types/stat-types.ts b/src/@types/stat-types.ts index bcba054383e..9b9ec8f5fea 100644 --- a/src/@types/stat-types.ts +++ b/src/@types/stat-types.ts @@ -19,7 +19,7 @@ export type PermanentStatTuple = Head>; /** Tuple containing all {@linkcode BattleStat}s of a Pokemon. */ export type BattleStatTuple = Tail; -// Since typescript lacks integer range unions, we have to use THIS abomination to strongly type an IV +/** Integer literal union containing all numbers from 0-31 inclusive; used to strongly type Pokemon IVs. */ export type IVType = | 0 | 1 @@ -56,4 +56,5 @@ export type IVType = type toIvTuple = { [k in keyof T]: IVType }; +/** A 6-length tuple of integers in the range [0-31]; used to strongly type Pokemon IVs. */ export type IVTuple = toIvTuple; diff --git a/src/@types/utility-types/global-augments.ts b/src/@types/utility-types/global-augments.ts new file mode 100644 index 00000000000..c0a74b42d9b --- /dev/null +++ b/src/@types/utility-types/global-augments.ts @@ -0,0 +1,9 @@ +export {}; + +declare global { + // Array global augments to allow semi-easy working with tuples...??? + interface Array { + map(callbackfn: (value: T, index: number, array: this) => U, thisArg?: any): { [K in keyof this]: U }; + slice(start: 0): { [K in keyof this]: T }; + } +} diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 0adce63134f..a2ebd8b9fd4 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -160,7 +160,7 @@ import { timedEventManager } from "./global-event-manager"; import { starterColors } from "./global-vars/starter-colors"; import { startingWave } from "./starting-wave"; import { PhaseManager } from "./phase-manager"; -import type { IVTuple } from "#app/@types/stat-types"; +import type { IVTuple, IVType } from "#app/@types/stat-types"; const DEBUG_RNG = false; @@ -169,7 +169,7 @@ const OPP_IVS_OVERRIDE_VALIDATED: IVTuple | null = ? null : Array.isArray(Overrides.OPP_IVS_OVERRIDE) ? Overrides.OPP_IVS_OVERRIDE - : new Array(6).fill(Overrides.OPP_IVS_OVERRIDE); + : (new Array(6).fill(Overrides.OPP_IVS_OVERRIDE) as IVTuple); export interface PokeballCounts { [pb: string]: number; @@ -910,7 +910,7 @@ export default class BattleScene extends SceneBase { gender?: Gender, shiny?: boolean, variant?: Variant, - ivs?: number[], + ivs?: IVTuple, nature?: Nature, dataSource?: Pokemon | PokemonData, postProcess?: (playerPokemon: PlayerPokemon) => void, @@ -958,23 +958,20 @@ export default class BattleScene extends SceneBase { } if (boss && !dataSource) { + // Generate a 2nd higher roll and linearly interpolate between the two. const secondaryIvs = getIvsFromId(randSeedInt(4294967296)); - for (let s = 0; s < pokemon.ivs.length; s++) { - pokemon.ivs[s] = Math.round( - Phaser.Math.Linear( - Math.min(pokemon.ivs[s], secondaryIvs[s]), - Math.max(pokemon.ivs[s], secondaryIvs[s]), - 0.75, - ), - ); - } + pokemon.ivs = pokemon.ivs.map( + (iv, i) => + Math.round(Phaser.Math.Linear(Math.min(iv, secondaryIvs[i]), Math.max(iv, secondaryIvs[i]), 0.75)) as IVType, // ts doesn't know the number will be between 0-31 + ); } + if (postProcess) { postProcess(pokemon); } - if (OPP_IVS_OVERRIDE_VALIDATED) { + if (OPP_IVS_OVERRIDE_VALIDATED !== null) { pokemon.ivs = OPP_IVS_OVERRIDE_VALIDATED; } pokemon.init(); @@ -2084,7 +2081,7 @@ export default class BattleScene extends SceneBase { let scoreIncrease = enemy.getSpeciesForm().getBaseExp() * (enemy.level / this.getMaxExpLevel()) * - ((enemy.ivs.reduce((iv: number, total: number) => (total += iv), 0) / 93) * 0.2 + 0.8); + ((enemy.ivs.reduce((total, iv) => total + iv, 0) / 93) * 0.2 + 0.8); this.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === enemy.id, false).map( m => (scoreIncrease *= (m as PokemonHeldItemModifier).getScoreMultiplier()), ); diff --git a/src/data/egg.ts b/src/data/egg.ts index 67cdb7b1344..15679fc7d02 100644 --- a/src/data/egg.ts +++ b/src/data/egg.ts @@ -38,6 +38,7 @@ import { HATCH_WAVES_LEGENDARY_EGG, } from "#app/data/balance/rates"; import { speciesEggTiers } from "#app/data/balance/species-egg-tiers"; +import type { IVType } from "#app/@types/stat-types"; export const EGG_SEED = 1073741824; @@ -270,9 +271,9 @@ export class Egg { const secondaryIvs = getIvsFromId(randSeedInt(4294967295)); - for (let s = 0; s < ret.ivs.length; s++) { - ret.ivs[s] = Math.max(ret.ivs[s], secondaryIvs[s]); - } + ret.ivs = ret.ivs.map( + (iv, i) => Math.max(iv, secondaryIvs[i]) as IVType, // ts doesn't know the number will be between 0-31 + ); }; ret = ret!; // Tell TS compiler it's defined now diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index 83e876d1aa8..c930c86ebb4 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -511,7 +511,7 @@ async function postProcessTransformedPokemon( const hiddenAbilityChance = new NumberHolder(256); globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); - const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); + const hasHiddenAbility = randSeedInt(hiddenAbilityChance.value) === 0; if (hasHiddenAbility) { newPokemon.abilityIndex = hiddenIndex; diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index e2b92230985..52bd49a281a 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -54,6 +54,7 @@ import { PokemonType } from "#enums/pokemon-type"; import { getNatureName } from "#app/data/nature"; import { getPokemonNameWithAffix } from "#app/messages"; import { timedEventManager } from "#app/global-event-manager"; +import type { IVTuple } from "#app/@types/stat-types"; /** * Animates exclamation sprite over trainer's head at start of encounter @@ -96,7 +97,7 @@ export interface EnemyPokemonConfig { passive?: boolean; moveSet?: MoveId[]; nature?: Nature; - ivs?: [number, number, number, number, number, number]; + ivs?: IVTuple; shiny?: boolean; /** Is only checked if Pokemon is shiny */ variant?: Variant; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index ddd469281fc..22077d958cf 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -6089,12 +6089,12 @@ export class EnemyPokemon extends Pokemon { if (this.hasTrainer() && globalScene.currentBattle) { const { waveIndex } = globalScene.currentBattle; - const ivs: IVTuple = Array.from( + const ivs = Array.from( { length: 6, }, () => randSeedIntRange(Math.floor(waveIndex / 10), 31), - ); + ) as IVTuple; this.ivs = ivs; } } diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index f2c23384627..d56a06918af 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -121,7 +121,8 @@ export class EncounterPhase extends BattlePhase { !!globalScene.getEncounterBossSegments(battle.waveIndex, level, enemySpecies), ); if (globalScene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { - battle.enemyParty[e].ivs = new Array(6).fill(31); + // max IVs for eternatus + battle.enemyParty[e].ivs = [31, 31, 31, 31, 31, 31]; } globalScene .getPlayerParty() diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 00b46b9e5f4..eb47fa33c2d 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -68,6 +68,7 @@ import { DexAttr } from "#enums/dex-attr"; import { AbilityAttr } from "#enums/ability-attr"; import { defaultStarterSpecies, saveKey } from "#app/constants"; import { encrypt, decrypt } from "#app/utils/data"; +import type { IVTuple } from "#app/@types/stat-types"; function getDataTypeKey(dataType: GameDataType, slotId = 0): string { switch (dataType) { @@ -1945,7 +1946,13 @@ export class GameData { _unlockSpeciesNature(species.speciesId); } - updateSpeciesDexIvs(speciesId: SpeciesId, ivs: number[]): void { + /** + * When a pokemon is caught or added, maximize its lowest pre-evolution's {@linkcode DexData} + * with the IVs of the newly caught pokemon. + * @param speciesId - The {@linkcode SpeciesId} to update dex data for. + * @param ivs - The {@linkcode IVTuple | IVs} of the caught pokemon. + */ + updateSpeciesDexIvs(speciesId: SpeciesId, ivs: IVTuple): void { let dexEntry: DexEntry; do { dexEntry = globalScene.gameData.dexData[speciesId]; @@ -1955,7 +1962,7 @@ export class GameData { dexIvs[i] = ivs[i]; } } - if (dexIvs.filter(iv => iv === 31).length === 6) { + if (dexIvs.every(iv => iv === 31)) { globalScene.validateAchv(achvs.PERFECT_IVS); } } while (pokemonPrevolutions.hasOwnProperty(speciesId) && (speciesId = pokemonPrevolutions[speciesId])); diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 7571f0cc82f..5b9a1a8c0f6 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -14,6 +14,7 @@ import type { MoveId } from "#enums/move-id"; import type { SpeciesId } from "#enums/species-id"; import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import type { PokemonType } from "#enums/pokemon-type"; +import type { IVTuple, PermanentStatTuple } from "#app/@types/stat-types"; export default class PokemonData { public id: number; @@ -31,8 +32,8 @@ export default class PokemonData { public levelExp: number; public gender: Gender; public hp: number; - public stats: number[]; - public ivs: number[]; + public stats: PermanentStatTuple; + public ivs: IVTuple; public nature: Nature; public moveset: PokemonMove[]; public status: Status | null; diff --git a/src/utils/common.ts b/src/utils/common.ts index f6036ed981d..b3ca840a88c 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -3,7 +3,7 @@ import { MoveId } from "#enums/move-id"; import i18next from "i18next"; import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; import type { Variant } from "#app/sprites/variant"; -import type { IVTuple } from "#app/@types/stat-types"; +import type { IVTuple, IVType } from "#app/@types/stat-types"; export type nil = null | undefined; @@ -88,6 +88,8 @@ export function randInt(range: number, min = 0): number { return Math.floor(Math.random() * range) + min; } +export function randSeedInt(range: 31, min?: IVType): IVType; +export function randSeedInt(range: number, min?: number): number; /** * Generate a random integer using the global seed, or the current battle's seed if called via `Battle.randSeedInt` * @param range - How large of a range of random numbers to choose from. If {@linkcode range} <= 1, returns {@linkcode min} @@ -191,7 +193,7 @@ export function getIvsFromId(id: number): IVTuple { (id & 0x00007c00) >>> 10, (id & 0x000003e0) >>> 5, id & 0x0000001f, - ]; + ] as IVTuple; // TS doesn't know the numbers are between 0-31 } export function formatLargeNumber(count: number, threshold: number): string { diff --git a/test/moves/fusion_flare_bolt.test.ts b/test/moves/fusion_flare_bolt.test.ts index f10ede8717c..f25741ae76f 100644 --- a/test/moves/fusion_flare_bolt.test.ts +++ b/test/moves/fusion_flare_bolt.test.ts @@ -11,6 +11,7 @@ import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import type { PermanentStatTuple } from "#app/@types/stat-types"; describe("Moves - Fusion Flare and Fusion Bolt", () => { let phaserGame: Phaser.Game; @@ -156,6 +157,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); }, 20000); + // TODO: Clean these tests up it("FUSION_FLARE and FUSION_BOLT alternating throughout turn should double power of subsequent moves", async () => { game.override.enemyMoveset(fusionFlare.id); await game.classicMode.startBattle([SpeciesId.ZEKROM, SpeciesId.ZEKROM]); @@ -168,7 +170,10 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { game.scene.clearEnemyModifiers(); // Mock stats by replacing entries in copy with desired values for specific stats - const stats = { + const stats: { + enemy: [PermanentStatTuple, PermanentStatTuple]; + player: [PermanentStatTuple, PermanentStatTuple]; + } = { enemy: [[...enemyParty[0].stats], [...enemyParty[1].stats]], player: [[...party[0].stats], [...party[1].stats]], }; @@ -222,7 +227,10 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { game.scene.clearEnemyModifiers(); // Mock stats by replacing entries in copy with desired values for specific stats - const stats = { + const stats: { + enemy: [PermanentStatTuple, PermanentStatTuple]; + player: [PermanentStatTuple, PermanentStatTuple]; + } = { enemy: [[...enemyParty[0].stats], [...enemyParty[1].stats]], player: [[...party[0].stats], [...party[1].stats]], };