From 1fb6656c2204053cb262fb36a6b7c48f2bb75c69 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Tue, 20 Aug 2024 04:58:36 -0700 Subject: [PATCH] Fix off-by-one error in some random number calls --- src/battle-scene.ts | 14 +++++---- src/battle.ts | 2 +- src/data/ability.ts | 2 +- src/data/battler-tags.ts | 2 +- src/data/move.ts | 4 +-- src/field/pokemon.ts | 4 +-- src/phases/move-effect-phase.ts | 6 ++-- src/rng.md | 56 +++++++++++++++++++++++++++++++++ src/utils.ts | 1 + 9 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 src/rng.md diff --git a/src/battle-scene.ts b/src/battle-scene.ts index b72e79c866d..c786a183e13 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -49,8 +49,8 @@ import CandyBar from "./ui/candy-bar"; import { Variant, variantData } from "./data/variant"; import { Localizable } from "#app/interfaces/locales"; import Overrides from "#app/overrides"; -import {InputsController} from "./inputs-controller"; -import {UiInputs} from "./ui-inputs"; +import { InputsController } from "./inputs-controller"; +import { UiInputs } from "./ui-inputs"; import { NewArenaEvent } from "./events/battle-scene"; import { ArenaFlyout } from "./ui/arena-flyout"; import { EaseType } from "#enums/ease-type"; @@ -65,7 +65,7 @@ import { Species } from "#enums/species"; import { UiTheme } from "#enums/ui-theme"; import { TimedEventManager } from "#app/timed-event-manager.js"; import i18next from "i18next"; -import {TrainerType} from "#enums/trainer-type"; +import { TrainerType } from "#enums/trainer-type"; import { battleSpecDialogue } from "./data/dialogue"; import { LoadingScene } from "./loading-scene"; import { LevelCapPhase } from "./phases/level-cap-phase"; @@ -851,7 +851,7 @@ export default class BattleScene extends SceneBase { overrideModifiers(this, false); overrideHeldItems(this, pokemon, false); if (boss && !dataSource) { - const secondaryIvs = Utils.getIvsFromId(Utils.randSeedInt(4294967295)); + const secondaryIvs = Utils.getIvsFromId(Utils.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)); @@ -957,6 +957,7 @@ export default class BattleScene extends SceneBase { this.offsetGym = this.gameMode.isClassic && this.getGeneratedOffsetGym(); } + // What is the purpose of this function? Why not just use Battle.randSeedInt() directly? randBattleSeedInt(range: integer, min: integer = 0): integer { return this.currentBattle?.randSeedInt(this, range, min); } @@ -1107,7 +1108,8 @@ export default class BattleScene extends SceneBase { doubleTrainer = false; } } - newTrainer = trainerData !== undefined ? trainerData.toTrainer(this) : new Trainer(this, trainerType, doubleTrainer ? TrainerVariant.DOUBLE : Utils.randSeedInt(2) ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT); + const variant = doubleTrainer ? TrainerVariant.DOUBLE : (Utils.randSeedInt(2) ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT); + newTrainer = trainerData !== undefined ? trainerData.toTrainer(this) : new Trainer(this, trainerType, variant); this.field.add(newTrainer); } } @@ -2550,7 +2552,7 @@ export default class BattleScene extends SceneBase { if (mods.length < 1) { return mods; } - const rand = Math.floor(Utils.randSeedInt(mods.length)); + const rand = Math.floor(Utils.randSeedInt(mods.length)); // Why is there a floor() call around a number that can only be an integer? return [mods[rand], ...shuffleModifiers(mods.filter((_, i) => i !== rand))]; }; modifiers = shuffleModifiers(modifiers); diff --git a/src/battle.ts b/src/battle.ts index a82f1a3db9b..9fd80ce9544 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -6,7 +6,7 @@ import Trainer, { TrainerVariant } from "./field/trainer"; import { GameMode } from "./game-mode"; import { MoneyMultiplierModifier, PokemonHeldItemModifier } from "./modifier/modifier"; import { PokeballType } from "./data/pokeball"; -import {trainerConfigs} from "#app/data/trainer-config"; +import { trainerConfigs } from "#app/data/trainer-config"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattleSpec } from "#enums/battle-spec"; import { Moves } from "#enums/moves"; diff --git a/src/data/ability.ts b/src/data/ability.ts index 8e020849a17..4fa3b42ed9c 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2452,7 +2452,7 @@ export class ConfusionOnStatusEffectAbAttr extends PostAttackAbAttr { */ applyPostAttackAfterMoveTypeCheck(pokemon: Pokemon, passive: boolean, defender: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { if (this.effects.indexOf(args[0]) > -1 && !defender.isFainted()) { - return defender.addTag(BattlerTagType.CONFUSED, pokemon.randSeedInt(3,2), move.id, defender.id); + return defender.addTag(BattlerTagType.CONFUSED, pokemon.randSeedIntRange(2, 5), move.id, defender.id); } return false; } diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index ede8d029327..c7e2bb9b03e 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -347,7 +347,7 @@ export class ConfusedTag extends BattlerTag { if (pokemon.randSeedInt(3) === 0) { const atk = pokemon.getBattleStat(Stat.ATK); const def = pokemon.getBattleStat(Stat.DEF); - const damage = Math.ceil(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * (pokemon.randSeedInt(15, 85) / 100)); + const damage = Math.ceil(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * (pokemon.randSeedIntRange(85, 100) / 100)); pokemon.scene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself")); pokemon.damageAndUpdate(damage); pokemon.battleData.hitCount++; diff --git a/src/data/move.ts b/src/data/move.ts index acb61042e70..ef2de32c562 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -3,7 +3,7 @@ import { BattleStat, getBattleStatName } from "./battle-stat"; import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, TrappedTag, TypeBoostTag } from "./battler-tags"; import { getPokemonNameWithAffix } from "../messages"; import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon"; -import { StatusEffect, getStatusEffectHealText, isNonVolatileStatusEffect, getNonVolatileStatusEffects} from "./status-effect"; +import { StatusEffect, getStatusEffectHealText, isNonVolatileStatusEffect, getNonVolatileStatusEffects } from "./status-effect"; import { getTypeResistances, Type } from "./type"; import { Constructor } from "#app/utils"; import * as Utils from "../utils"; @@ -4334,7 +4334,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr { const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true); if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) { - return (this.selfTarget ? user : target).addTag(this.tagType, user.randSeedInt(this.turnCountMax - this.turnCountMin, this.turnCountMin), move.id, user.id); + return (this.selfTarget ? user : target).addTag(this.tagType, user.randSeedIntRange(this.turnCountMin, this.turnCountMax), move.id, user.id); } return false; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 6a445a83b4e..ea10c33ca46 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1573,7 +1573,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { }; this.fusionSpecies = this.scene.randomSpecies(this.scene.currentBattle?.waveIndex || 0, this.level, false, filter, true); - this.fusionAbilityIndex = (this.fusionSpecies.abilityHidden && hasHiddenAbility ? this.fusionSpecies.ability2 ? 2 : 1 : this.fusionSpecies.ability2 ? randAbilityIndex : 0); + this.fusionAbilityIndex = (this.fusionSpecies.abilityHidden && hasHiddenAbility ? 2 : this.fusionSpecies.ability2 !== this.fusionSpecies.ability1 ? randAbilityIndex : 0); this.fusionShiny = this.shiny; this.fusionVariant = this.variant; @@ -2081,7 +2081,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (!isTypeImmune) { const levelMultiplier = (2 * source.level / 5 + 2); - const randomMultiplier = ((this.scene.randBattleSeedInt(16) + 85) / 100); + const randomMultiplier = (this.randSeedIntRange(85, 100) / 100); damage.value = Math.ceil((((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2) * stabMultiplier.value * typeMultiplier.value diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index a5ac913cc5d..a98670f5002 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -379,16 +379,16 @@ export class MoveEffectPhase extends PokemonPhase { return false; } - const moveAccuracy = this.move.getMove().calculateBattleAccuracy(user!, target); // TODO: is the bang correct here? + const moveAccuracy = this.move.getMove().calculateBattleAccuracy(user, target); if (moveAccuracy === -1) { return true; } const accuracyMultiplier = user.getAccuracyMultiplier(target, this.move.getMove()); - const rand = user.randSeedInt(100, 1); + const rand = user.randSeedInt(100); - return rand <= moveAccuracy * (accuracyMultiplier!); // TODO: is this bang correct? + return rand < (moveAccuracy * accuracyMultiplier); } /** Returns the {@linkcode Pokemon} using this phase's invoked move */ diff --git a/src/rng.md b/src/rng.md new file mode 100644 index 00000000000..9121de18278 --- /dev/null +++ b/src/rng.md @@ -0,0 +1,56 @@ +`src/field/pokemon.ts -> Pokemon` +```ts + // This calls either `BattleScene:randBattleSeedInt()` in `src/battle-scene.ts` which calls `Battle:randSeedInt()` in `src/battle.ts` which calls `randSeedInt()` in `src/utils.ts` + // or it directly calls `randSeedInt()` in `src/utils.ts` + randSeedInt(range: integer, min: integer = 0): integer { + return this.scene.currentBattle + ? this.scene.randBattleSeedInt(range, min) + : Utils.randSeedInt(range, min); + } +``` + +`src/battle-scene.ts -> BattleScene` +```ts + // This calls `Battle:randSeedInt()` in `src/battle.ts` which calls `randSeedInt()` in `src/utils.ts` + randBattleSeedInt(range: integer, min: integer = 0): integer { + return this.currentBattle?.randSeedInt(this, range, min); + } +``` + +`src/battle.ts -> Battle` +```ts + // This calls `randSeedInt()` in `src/utils.ts` + randSeedInt(scene: BattleScene, range: integer, min: integer = 0): integer { + if (range <= 1) { + return min; + } + const tempRngCounter = scene.rngCounter; + const tempSeedOverride = scene.rngSeedOverride; + const state = Phaser.Math.RND.state(); + if (this.battleSeedState) { + Phaser.Math.RND.state(this.battleSeedState); + } else { + Phaser.Math.RND.sow([ Utils.shiftCharCodes(this.battleSeed, this.turn << 6) ]); + console.log("Battle Seed:", this.battleSeed); + } + scene.rngCounter = this.rngCounter++; + scene.rngSeedOverride = this.battleSeed; + const ret = Utils.randSeedInt(range, min); + this.battleSeedState = Phaser.Math.RND.state(); + Phaser.Math.RND.state(state); + scene.rngCounter = tempRngCounter; + scene.rngSeedOverride = tempSeedOverride; + return ret; + } +``` + +`src/utils.ts` +```ts +// This is the eventual endpoint of every other RSI function +export function randSeedInt(range: integer, min: integer = 0): integer { + if (range <= 1) { + return min; + } + return Phaser.Math.RND.integerInRange(min, (range - 1) + min); +} +``` diff --git a/src/utils.ts b/src/utils.ts index c51ac2b5b0b..6272fda9a20 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -82,6 +82,7 @@ export function randInt(range: integer, min: integer = 0): integer { return Math.floor(Math.random() * range) + min; } +// Is this only seeded if called via `Battle:randSeedInt()`? export function randSeedInt(range: integer, min: integer = 0): integer { if (range <= 1) { return min;