Merge branch 'beta' into commander

This commit is contained in:
innerthunder 2024-10-22 00:01:32 -07:00
commit 196d665215
141 changed files with 12269 additions and 8529 deletions

View File

@ -1,5 +1,5 @@
import tseslint from '@typescript-eslint/eslint-plugin';
import stylisticTs from '@stylistic/eslint-plugin-ts'
import stylisticTs from '@stylistic/eslint-plugin-ts';
import parser from '@typescript-eslint/parser';
import importX from 'eslint-plugin-import-x';
@ -16,15 +16,15 @@ export default [
'@typescript-eslint': tseslint
},
rules: {
"eqeqeq": ["error", "always"], // Enforces the use of === and !== instead of == and !=
"indent": ["error", 2], // Enforces a 2-space indentation
"eqeqeq": ["error", "always"], // Enforces the use of `===` and `!==` instead of `==` and `!=`
"indent": ["error", 2, { "SwitchCase": 1 }], // Enforces a 2-space indentation, enforces indentation of `case ...:` statements
"quotes": ["error", "double"], // Enforces the use of double quotes for strings
"no-var": "error", // Disallows the use of var, enforcing let or const instead
"prefer-const": "error", // Prefers the use of const for variables that are never reassigned
"no-var": "error", // Disallows the use of `var`, enforcing `let` or `const` instead
"prefer-const": "error", // Enforces the use of `const` for variables that are never reassigned
"no-undef": "off", // Disables the rule that disallows the use of undeclared variables (TypeScript handles this)
"@typescript-eslint/no-unused-vars": [ "error", {
"args": "none", // Allows unused function parameters. Useful for functions with specific signatures where not all parameters are always used.
"ignoreRestSiblings": true // Allows unused variables that are part of a rest property in object destructuring. Useful for excluding certain properties from an object while using the rest.
"ignoreRestSiblings": true // Allows unused variables that are part of a rest property in object destructuring. Useful for excluding certain properties from an object while using the others.
}],
"eol-last": ["error", "always"], // Enforces at least one newline at the end of files
"@stylistic/ts/semi": ["error", "always"], // Requires semicolons for TypeScript-specific syntax
@ -32,14 +32,14 @@ export default [
"no-extra-semi": ["error"], // Disallows unnecessary semicolons for TypeScript-specific syntax
"brace-style": "off", // Note: you must disable the base rule as it can report incorrect errors
"curly": ["error", "all"], // Enforces the use of curly braces for all control statements
"@stylistic/ts/brace-style": ["error", "1tbs"],
"@stylistic/ts/brace-style": ["error", "1tbs"], // Enforces the following brace style: https://eslint.style/rules/js/brace-style#_1tbs
"no-trailing-spaces": ["error", { // Disallows trailing whitespace at the end of lines
"skipBlankLines": false, // Enforces the rule even on blank lines
"ignoreComments": false // Enforces the rule on lines containing comments
}],
"space-before-blocks": ["error", "always"], // Enforces a space before blocks
"keyword-spacing": ["error", { "before": true, "after": true }], // Enforces spacing before and after keywords
"comma-spacing": ["error", { "before": false, "after": true }], // Enforces spacing after comma
"comma-spacing": ["error", { "before": false, "after": true }], // Enforces spacing after commas
"import-x/extensions": ["error", "never", { "json": "always" }], // Enforces no extension for imports unless json
"array-bracket-spacing": ["error", "always", { "objectsInArrays": false, "arraysInArrays": false }], // Enforces consistent spacing inside array brackets
"object-curly-spacing": ["error", "always", { "arraysInObjects": false, "objectsInObjects": false }], // Enforces consistent spacing inside braces of object literals, destructuring assignments, and import/export specifiers

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "pokemon-rogue-battle",
"version": "1.0.4",
"version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pokemon-rogue-battle",
"version": "1.0.4",
"version": "1.1.0",
"hasInstallScript": true,
"dependencies": {
"@material/material-color-utilities": "^0.2.7",

View File

@ -1,7 +1,7 @@
{
"name": "pokemon-rogue-battle",
"private": true,
"version": "1.0.4",
"version": "1.1.0",
"type": "module",
"scripts": {
"start": "vite",

Binary file not shown.

View File

@ -3416,12 +3416,12 @@
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 24,
"h": 24
"w": 32,
"h": 32
},
"spriteSourceSize": {
"x": 1,
"y": 2,
"x": 5,
"y": 7,
"w": 22,
"h": 19
},
@ -8415,6 +8415,6 @@
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:934ea4080bad980d4fea720cc771f133:ed564bc47b79b15a763de57045178e88:110e074689c9edd2c54833ce2e4d9270$"
"smartupdate": "$TexturePacker:SmartUpdate:9ef21166268f7487fc9ff8d0f9b996e4:82658ac7bdd4c2b417e1f59168179262:110e074689c9edd2c54833ce2e4d9270$"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 285 B

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -86,7 +86,7 @@ import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-ph
import { TurnInitPhase } from "#app/phases/turn-init-phase";
import { ShopCursorTarget } from "#app/enums/shop-cursor-target";
import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
import { allMysteryEncounters, ANTI_VARIANCE_WEIGHT_MODIFIER, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT, mysteryEncountersByBiome, WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters";
import { allMysteryEncounters, ANTI_VARIANCE_WEIGHT_MODIFIER, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT, mysteryEncountersByBiome } from "#app/data/mystery-encounters/mystery-encounters";
import { MysteryEncounterSaveData } from "#app/data/mystery-encounters/mystery-encounter-save-data";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
@ -1192,10 +1192,7 @@ export default class BattleScene extends SceneBase {
if (trainerConfigs[trainerType].doubleOnly) {
doubleTrainer = true;
} else if (trainerConfigs[trainerType].hasDouble) {
const doubleChance = new Utils.IntegerHolder(newWaveIndex % 10 === 0 ? 32 : 8);
this.applyModifiers(DoubleBattleChanceBoosterModifier, true, doubleChance);
playerField.forEach(p => applyAbAttrs(DoubleBattleChanceAbAttr, p, null, false, doubleChance));
doubleTrainer = !Utils.randSeedInt(doubleChance.value);
doubleTrainer = !Utils.randSeedInt(this.getDoubleBattleChance(newWaveIndex, playerField));
// Add a check that special trainers can't be double except for tate and liza - they should use the normal double chance
if (trainerConfigs[trainerType].trainerTypeDouble && ![ TrainerType.TATE, TrainerType.LIZA ].includes(trainerType)) {
doubleTrainer = false;
@ -1208,12 +1205,10 @@ export default class BattleScene extends SceneBase {
// Check for mystery encounter
// Can only occur in place of a standard (non-boss) wild battle, waves 10-180
if (this.isWaveMysteryEncounter(newBattleType, newWaveIndex, mysteryEncounterType) || newBattleType === BattleType.MYSTERY_ENCOUNTER) {
if (this.isWaveMysteryEncounter(newBattleType, newWaveIndex) || newBattleType === BattleType.MYSTERY_ENCOUNTER) {
newBattleType = BattleType.MYSTERY_ENCOUNTER;
// Reset base spawn weight
// Reset to base spawn weight
this.mysteryEncounterSaveData.encounterSpawnChance = BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT;
} else if (newBattleType === BattleType.WILD) {
this.mysteryEncounterSaveData.encounterSpawnChance += WEIGHT_INCREMENT_ON_SPAWN_MISS;
}
}
@ -2367,6 +2362,19 @@ export default class BattleScene extends SceneBase {
return false;
}
/**
* Will search for a specific phase in {@linkcode phaseQueuePrepend} via filter, and remove the first result if a match is found.
* @param phaseFilter filter function
*/
tryRemoveUnshiftedPhase(phaseFilter: (phase: Phase) => boolean): boolean {
const phaseIndex = this.phaseQueuePrepend.findIndex(phaseFilter);
if (phaseIndex > -1) {
this.phaseQueuePrepend.splice(phaseIndex, 1);
return true;
}
return false;
}
/**
* Tries to add the input phase to index before target phase in the phaseQueue, else simply calls unshiftPhase()
* @param phase {@linkcode Phase} the phase to be added
@ -3128,18 +3136,26 @@ export default class BattleScene extends SceneBase {
}
}
/**
* Returns if a wave COULD spawn a {@linkcode MysteryEncounter}.
* Even if returns `true`, does not guarantee that a wave will actually be a ME.
* That check is made in {@linkcode BattleScene.isWaveMysteryEncounter} instead.
*/
isMysteryEncounterValidForWave(battleType: BattleType, waveIndex: number): boolean {
const [ lowestMysteryEncounterWave, highestMysteryEncounterWave ] = this.gameMode.getMysteryEncounterLegalWaves();
return this.gameMode.hasMysteryEncounters && battleType === BattleType.WILD && !this.gameMode.isBoss(waveIndex) && waveIndex < highestMysteryEncounterWave && waveIndex > lowestMysteryEncounterWave;
}
/**
* Determines whether a wave should randomly generate a {@linkcode MysteryEncounter}.
* Currently, the only modes that MEs are allowed in are Classic and Challenge.
* Additionally, MEs cannot spawn outside of waves 10-180 in those modes
*
* @param newBattleType
* @param waveIndex
* @param sessionDataEncounterType
*/
private isWaveMysteryEncounter(newBattleType: BattleType, waveIndex: number, sessionDataEncounterType?: MysteryEncounterType): boolean {
private isWaveMysteryEncounter(newBattleType: BattleType, waveIndex: number): boolean {
const [ lowestMysteryEncounterWave, highestMysteryEncounterWave ] = this.gameMode.getMysteryEncounterLegalWaves();
if (this.gameMode.hasMysteryEncounters && newBattleType === BattleType.WILD && !this.gameMode.isBoss(waveIndex) && waveIndex < highestMysteryEncounterWave && waveIndex > lowestMysteryEncounterWave) {
if (this.isMysteryEncounterValidForWave(newBattleType, waveIndex)) {
// Base spawn weight is BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT/256, and increases by WEIGHT_INCREMENT_ON_SPAWN_MISS/256 for each missed attempt at spawning an encounter on a valid floor
const sessionEncounterRate = this.mysteryEncounterSaveData.encounterSpawnChance;
const encounteredEvents = this.mysteryEncounterSaveData.encounteredEvents;
@ -3180,6 +3196,9 @@ export default class BattleScene extends SceneBase {
let encounter: MysteryEncounter | null;
if (!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_OVERRIDE) && allMysteryEncounters.hasOwnProperty(Overrides.MYSTERY_ENCOUNTER_OVERRIDE)) {
encounter = allMysteryEncounters[Overrides.MYSTERY_ENCOUNTER_OVERRIDE];
if (canBypass) {
return encounter;
}
} else if (canBypass) {
encounter = allMysteryEncounters[encounterType ?? -1];
return encounter;

View File

@ -4377,6 +4377,30 @@ export class AlwaysHitAbAttr extends AbAttr { }
/** Attribute for abilities that allow moves that make contact to ignore protection (i.e. Unseen Fist) */
export class IgnoreProtectOnContactAbAttr extends AbAttr { }
/**
* Attribute implementing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Infiltrator_(Ability) | Infiltrator}.
* Allows the source's moves to bypass the effects of opposing Light Screen, Reflect, Aurora Veil, Safeguard, Mist, and Substitute.
*/
export class InfiltratorAbAttr extends AbAttr {
/**
* Sets a flag to bypass screens, Substitute, Safeguard, and Mist
* @param pokemon n/a
* @param passive n/a
* @param simulated n/a
* @param cancelled n/a
* @param args `[0]` a {@linkcode Utils.BooleanHolder | BooleanHolder} containing the flag
* @returns `true` if the bypass flag was successfully set; `false` otherwise.
*/
override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: null, args: any[]): boolean {
const bypassed = args[0];
if (args[0] instanceof Utils.BooleanHolder) {
bypassed.value = true;
return true;
}
return false;
}
}
export class UncopiableAbilityAbAttr extends AbAttr {
constructor() {
super(false);
@ -4948,8 +4972,7 @@ export function initAbilities() {
.attr(TypeImmunityAddBattlerTagAbAttr, Type.FIRE, BattlerTagType.FIRE_BOOST, 1)
.ignorable(),
new Ability(Abilities.SHIELD_DUST, 3)
.attr(IgnoreMoveEffectsAbAttr)
.edgeCase(), // Does not work with secret power (unimplemented)
.attr(IgnoreMoveEffectsAbAttr),
new Ability(Abilities.OWN_TEMPO, 3)
.attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED)
.attr(IntimidateImmunityAbAttr)
@ -4993,8 +5016,7 @@ export function initAbilities() {
.attr(TypeImmunityStatStageChangeAbAttr, Type.ELECTRIC, Stat.SPATK, 1)
.ignorable(),
new Ability(Abilities.SERENE_GRACE, 3)
.attr(MoveEffectChanceMultiplierAbAttr, 2)
.edgeCase(), // does not work with secret power (unimplemented)
.attr(MoveEffectChanceMultiplierAbAttr, 2),
new Ability(Abilities.SWIFT_SWIM, 3)
.attr(StatMultiplierAbAttr, Stat.SPD, 2)
.condition(getWeatherCondition(WeatherType.RAIN, WeatherType.HEAVY_RAIN)),
@ -5358,7 +5380,8 @@ export function initAbilities() {
.attr(PostSummonTransformAbAttr)
.attr(UncopiableAbilityAbAttr),
new Ability(Abilities.INFILTRATOR, 5)
.unimplemented(),
.attr(InfiltratorAbAttr)
.partial(), // does not bypass Mist
new Ability(Abilities.MUMMY, 5)
.attr(PostDefendAbilityGiveAbAttr, Abilities.MUMMY)
.bypassFaint(),

View File

@ -7,7 +7,7 @@ import { getPokemonNameWithAffix } from "#app/messages";
import Pokemon, { HitResult, PokemonMove } from "#app/field/pokemon";
import { StatusEffect } from "#app/data/status-effect";
import { BattlerIndex } from "#app/battle";
import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability";
import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, InfiltratorAbAttr, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability";
import { Stat } from "#enums/stat";
import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims";
import i18next from "i18next";
@ -130,7 +130,18 @@ export class MistTag extends ArenaTag {
* to flag the stat reduction as cancelled
* @returns `true` if a stat reduction was cancelled; `false` otherwise
*/
override apply(arena: Arena, simulated: boolean, cancelled: BooleanHolder): boolean {
override apply(arena: Arena, simulated: boolean, attacker: Pokemon, cancelled: BooleanHolder): boolean {
// `StatStageChangePhase` currently doesn't have a reference to the source of stat drops,
// so this code currently has no effect on gameplay.
if (attacker) {
const bypassed = new BooleanHolder(false);
// TODO: Allow this to be simulated
applyAbAttrs(InfiltratorAbAttr, attacker, null, false, bypassed);
if (bypassed.value) {
return false;
}
}
cancelled.value = true;
if (!simulated) {
@ -169,12 +180,18 @@ export class WeakenMoveScreenTag extends ArenaTag {
*
* @param arena the {@linkcode Arena} where the move is applied.
* @param simulated n/a
* @param attacker the attacking {@linkcode Pokemon}
* @param moveCategory the attacking move's {@linkcode MoveCategory}.
* @param damageMultiplier A {@linkcode NumberHolder} containing the damage multiplier
* @returns `true` if the attacking move was weakened; `false` otherwise.
*/
override apply(arena: Arena, simulated: boolean, moveCategory: MoveCategory, damageMultiplier: NumberHolder): boolean {
override apply(arena: Arena, simulated: boolean, attacker: Pokemon, moveCategory: MoveCategory, damageMultiplier: NumberHolder): boolean {
if (this.weakenedCategories.includes(moveCategory)) {
const bypassed = new BooleanHolder(false);
applyAbAttrs(InfiltratorAbAttr, attacker, null, false, bypassed);
if (bypassed.value) {
return false;
}
damageMultiplier.value = arena.scene.currentBattle.double ? 2732 / 4096 : 0.5;
return true;
}
@ -626,7 +643,7 @@ export class ArenaTrapTag extends ArenaTag {
* @returns `true` if this hazard affects the given Pokemon; `false` otherwise.
*/
override apply(arena: Arena, simulated: boolean, pokemon: Pokemon): boolean {
if (this.sourceId === pokemon.id || (this.side === ArenaTagSide.PLAYER) !== pokemon.isPlayer()) {
if ((this.side === ArenaTagSide.PLAYER) !== pokemon.isPlayer()) {
return false;
}

View File

@ -838,7 +838,7 @@ export class SeedTag extends BattlerTag {
export class NightmareTag extends BattlerTag {
constructor() {
super(BattlerTagType.NIGHTMARE, BattlerTagLapseType.AFTER_MOVE, 1, Moves.NIGHTMARE);
super(BattlerTagType.NIGHTMARE, BattlerTagLapseType.TURN_END, 1, Moves.NIGHTMARE);
}
onAdd(pokemon: Pokemon): void {

View File

@ -1,18 +1,20 @@
import { Abilities } from "#enums/abilities";
import { Type } from "#app/data/type";
import { isNullOrUndefined } from "#app/utils";
import { Nature } from "#enums/nature";
/**
* Data that can customize a Pokemon in non-standard ways from its Species
* Currently only used by Mystery Encounters, may need to be renamed if it becomes more widely used
* Currently only used by Mystery Encounters and Mints.
*/
export class MysteryEncounterPokemonData {
export class CustomPokemonData {
public spriteScale: number;
public ability: Abilities | -1;
public passive: Abilities | -1;
public nature: Nature | -1;
public types: Type[];
constructor(data?: MysteryEncounterPokemonData | Partial<MysteryEncounterPokemonData>) {
constructor(data?: CustomPokemonData | Partial<CustomPokemonData>) {
if (!isNullOrUndefined(data)) {
Object.assign(this, data);
}
@ -20,6 +22,7 @@ export class MysteryEncounterPokemonData {
this.spriteScale = this.spriteScale ?? -1;
this.ability = this.ability ?? -1;
this.passive = this.passive ?? -1;
this.nature = this.nature ?? -1;
this.types = this.types ?? [];
}
}

View File

@ -8,7 +8,7 @@ import { Constructor, NumberHolder } from "#app/utils";
import * as Utils from "../utils";
import { WeatherType } from "./weather";
import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag";
import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability";
import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability";
import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier";
import { BattlerIndex, BattleType } from "../battle";
import { TerrainType } from "./terrain";
@ -346,7 +346,11 @@ export default class Move implements Localizable {
return false;
}
return !user.hasAbility(Abilities.INFILTRATOR)
const bypassed = new Utils.BooleanHolder(false);
// TODO: Allow this to be simulated
applyAbAttrs(InfiltratorAbAttr, user, null, false, bypassed);
return !bypassed.value
&& !this.hasFlag(MoveFlags.SOUND_BASED)
&& !this.hasFlag(MoveFlags.IGNORE_SUBSTITUTE);
}
@ -2078,7 +2082,7 @@ export class StatusEffectAttr extends MoveEffectAttr {
}
}
if (user !== target && target.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY)) {
if (user !== target && target.isSafeguarded(user)) {
if (move.category === MoveCategory.STATUS) {
user.scene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target) }));
}
@ -2895,8 +2899,6 @@ export class SecretPowerAttr extends MoveEffectAttr {
this.effectChanceOverride = move.chance;
const moveChance = this.getMoveChance(user, target, move, this.selfTarget);
if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) {
// effectChanceOverride used in the application of the actual secondary effect
this.effectChanceOverride = 100;
return true;
} else {
return false;
@ -2919,6 +2921,8 @@ export class SecretPowerAttr extends MoveEffectAttr {
const biome = user.scene.arena.biomeType;
secondaryEffect = this.determineBiomeEffect(biome);
}
// effectChanceOverride used in the application of the actual secondary effect
secondaryEffect.effectChanceOverride = 100;
return secondaryEffect.apply(user, target, move, []);
}
@ -5200,7 +5204,7 @@ export class ConfuseAttr extends AddBattlerTagAttr {
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!this.selfTarget && target.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY)) {
if (!this.selfTarget && target.isSafeguarded(user)) {
if (move.category === MoveCategory.STATUS) {
user.scene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target) }));
}
@ -7645,6 +7649,7 @@ export function initMoves() {
.ignoresVirtual(),
new StatusMove(Moves.TRANSFORM, Type.NORMAL, -1, 10, -1, 0, 1)
.attr(TransformAttr)
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE))
.ignoresProtect(),
new AttackMove(Moves.BUBBLE, Type.WATER, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
.attr(StatStageChangeAttr, [ Stat.SPD ], -1)
@ -8075,7 +8080,7 @@ export function initMoves() {
.attr(RemoveScreensAttr),
new StatusMove(Moves.YAWN, Type.NORMAL, -1, 10, -1, 0, 3)
.attr(AddBattlerTagAttr, BattlerTagType.DROWSY, false, true)
.condition((user, target, move) => !target.status && !target.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY)),
.condition((user, target, move) => !target.status && !target.isSafeguarded(user)),
new AttackMove(Moves.KNOCK_OFF, Type.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3)
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1)
.attr(RemoveHeldItemAttr, false),

View File

@ -133,8 +133,8 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter =
MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new CombinationPokemonRequirement(
new MoveRequirement(EXTORTION_MOVES),
new AbilityRequirement(EXTORTION_ABILITIES))
new MoveRequirement(EXTORTION_MOVES, true),
new AbilityRequirement(EXTORTION_ABILITIES, true))
)
.withDialogue({
buttonLabel: `${namespace}:option.2.label`,

View File

@ -42,6 +42,8 @@ import {
AttackTypeBoosterModifier,
BypassSpeedChanceModifier,
ContactHeldItemTransferChanceModifier,
GigantamaxAccessModifier,
MegaEvolutionAccessModifier,
PokemonHeldItemModifier
} from "#app/modifier/modifier";
import i18next from "i18next";
@ -356,10 +358,17 @@ export const BugTypeSuperfanEncounter: MysteryEncounter =
},
];
} else {
// If player has any evolution/form change items that are valid for their party, will spawn one of those items in addition to a Master Ball
const modifierOptions: ModifierTypeOption[] = [ generateModifierTypeOption(scene, modifierTypes.MASTER_BALL)!, generateModifierTypeOption(scene, modifierTypes.MAX_LURE)! ];
// If the player has any evolution/form change items that are valid for their party,
// spawn one of those items in addition to Dynamax Band, Mega Band, and Master Ball
const modifierOptions: ModifierTypeOption[] = [ generateModifierTypeOption(scene, modifierTypes.MASTER_BALL)! ];
const specialOptions: ModifierTypeOption[] = [];
if (!scene.findModifier(m => m instanceof MegaEvolutionAccessModifier)) {
modifierOptions.push(generateModifierTypeOption(scene, modifierTypes.MEGA_BRACELET)!);
}
if (!scene.findModifier(m => m instanceof GigantamaxAccessModifier)) {
modifierOptions.push(generateModifierTypeOption(scene, modifierTypes.DYNAMAX_BAND)!);
}
const nonRareEvolutionModifier = generateModifierTypeOption(scene, modifierTypes.EVOLUTION_ITEM);
if (nonRareEvolutionModifier) {
specialOptions.push(nonRareEvolutionModifier);

View File

@ -28,7 +28,7 @@ import { BattlerIndex } from "#app/battle";
import { Moves } from "#enums/moves";
import { EncounterBattleAnim } from "#app/data/battle-anims";
import { MoveCategory } from "#app/data/move";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { EncounterAnim } from "#enums/encounter-anims";
import { Challenges } from "#enums/challenges";
@ -133,7 +133,7 @@ export const ClowningAroundEncounter: MysteryEncounter =
},
{ // Blacephalon has the random ability from pool, and 2 entirely random types to fit with the theme of the encounter
species: getPokemonSpecies(Species.BLACEPHALON),
mysteryEncounterPokemonData: new MysteryEncounterPokemonData({ ability: ability, types: [ randSeedInt(18), randSeedInt(18) ]}),
customPokemonData: new CustomPokemonData({ ability: ability, types: [ randSeedInt(18), randSeedInt(18) ]}),
isBoss: true,
moveSet: [ Moves.TRICK, Moves.HYPNOSIS, Moves.SHADOW_BALL, Moves.MIND_BLOWN ]
},
@ -353,15 +353,15 @@ export const ClowningAroundEncounter: MysteryEncounter =
newTypes.push(secondType);
// Apply the type changes (to both base and fusion, if pokemon is fused)
if (!pokemon.mysteryEncounterPokemonData) {
pokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData();
if (!pokemon.customPokemonData) {
pokemon.customPokemonData = new CustomPokemonData();
}
pokemon.mysteryEncounterPokemonData.types = newTypes;
pokemon.customPokemonData.types = newTypes;
if (pokemon.isFusion()) {
if (!pokemon.fusionMysteryEncounterPokemonData) {
pokemon.fusionMysteryEncounterPokemonData = new MysteryEncounterPokemonData();
if (!pokemon.fusionCustomPokemonData) {
pokemon.fusionCustomPokemonData = new CustomPokemonData();
}
pokemon.fusionMysteryEncounterPokemonData.types = newTypes;
pokemon.fusionCustomPokemonData.types = newTypes;
}
}
})
@ -426,15 +426,15 @@ function onYesAbilitySwap(scene: BattleScene, resolve) {
// Do ability swap
const encounter = scene.currentBattle.mysteryEncounter!;
if (pokemon.isFusion()) {
if (!pokemon.fusionMysteryEncounterPokemonData) {
pokemon.fusionMysteryEncounterPokemonData = new MysteryEncounterPokemonData();
if (!pokemon.fusionCustomPokemonData) {
pokemon.fusionCustomPokemonData = new CustomPokemonData();
}
pokemon.fusionMysteryEncounterPokemonData.ability = encounter.misc.ability;
pokemon.fusionCustomPokemonData.ability = encounter.misc.ability;
} else {
if (!pokemon.mysteryEncounterPokemonData) {
pokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData();
if (!pokemon.customPokemonData) {
pokemon.customPokemonData = new CustomPokemonData();
}
pokemon.mysteryEncounterPokemonData.ability = encounter.misc.ability;
pokemon.customPokemonData.ability = encounter.misc.ability;
}
encounter.setDialogueToken("chosenPokemon", pokemon.getNameToRender());
scene.ui.setMode(Mode.MESSAGE).then(() => resolve(true));

View File

@ -236,7 +236,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
.withOption(
MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new MoveRequirement(DANCING_MOVES)) // Will set option3PrimaryName and option3PrimaryMove dialogue tokens automatically
.withPrimaryPokemonRequirement(new MoveRequirement(DANCING_MOVES, true)) // Will set option3PrimaryName and option3PrimaryMove dialogue tokens automatically
.withDialogue({
buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`,

View File

@ -145,7 +145,7 @@ export const FightOrFlightEncounter: MysteryEncounter =
.withOption(
MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically
.withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES, true)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically
.withDialogue({
buttonLabel: `${namespace}:option.2.label`,
buttonTooltip: `${namespace}:option.2.tooltip`,

View File

@ -227,7 +227,7 @@ export const PartTimerEncounter: MysteryEncounter =
.withOption(
MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new MoveRequirement(CHARMING_MOVES)) // Will set option3PrimaryName and option3PrimaryMove dialogue tokens automatically
.withPrimaryPokemonRequirement(new MoveRequirement(CHARMING_MOVES, true)) // Will set option3PrimaryName and option3PrimaryMove dialogue tokens automatically
.withDialogue({
buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`,

View File

@ -304,6 +304,11 @@ async function summonSafariPokemon(scene: BattleScene) {
encounter.setDialogueToken("pokemonName", getPokemonNameWithAffix(pokemon));
// TODO: If we await showEncounterText here, then the text will display without
// the wild Pokemon on screen, but if we don't await it, then the text never
// shows up and the IV scanner breaks. For now, we place the IV scanner code
// separately so that at least the IV scanner works.
const ivScannerModifier = scene.findModifier(m => m instanceof IvScannerModifier);
if (ivScannerModifier) {
scene.pushPhase(new ScanIvsPhase(scene, pokemon.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6)));

View File

@ -138,7 +138,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter =
newNature = randSeedInt(25) as Nature;
}
chosenPokemon.nature = newNature;
chosenPokemon.customPokemonData.nature = newNature;
encounter.setDialogueToken("newNature", getNatureName(newNature));
queueEncounterMessage(scene, `${namespace}:cheap_side_effects`);
setEncounterExp(scene, [ chosenPokemon.id ], 100);

View File

@ -18,7 +18,7 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode
import { PartyHealPhase } from "#app/phases/party-heal-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { BerryType } from "#enums/berry-type";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
/** i18n namespace for the encounter */
const namespace = "mysteryEncounters/slumberingSnorlax";
@ -72,7 +72,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter =
stackCount: 2
},
],
mysteryEncounterPokemonData: new MysteryEncounterPokemonData({ spriteScale: 1.25 }),
customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
aiType: AiType.SMART // Required to ensure Snorlax uses Sleep Talk while it is asleep
};
const config: EnemyPartyConfig = {
@ -143,7 +143,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter =
.withOption(
MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES))
.withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES, true))
.withDialogue({
buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`,

View File

@ -25,6 +25,7 @@ import { achvs } from "#app/system/achv";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { Type } from "#app/data/type";
import { getPokeballTintColor } from "#app/data/pokeball";
import { PokemonHeldItemModifier } from "#app/modifier/modifier";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/theExpertPokemonBreeder";
@ -61,7 +62,7 @@ const POOL_1_POKEMON: (Species | BreederSpeciesEvolution)[][] = [
const POOL_2_POKEMON: (Species | BreederSpeciesEvolution)[][] = [
[ Species.PICHU, new BreederSpeciesEvolution(Species.PIKACHU, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.RAICHU, FINAL_STAGE_EVOLUTION_WAVE) ],
[ Species.PICHU, new BreederSpeciesEvolution(Species.PIKACHU, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.ALOLA_RAICHU, FINAL_STAGE_EVOLUTION_WAVE) ],
[ Species.JYNX ],
[ Species.SMOOCHUM, new BreederSpeciesEvolution(Species.JYNX, SECOND_STAGE_EVOLUTION_WAVE) ],
[ Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONLEE, SECOND_STAGE_EVOLUTION_WAVE) ],
[ Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONCHAN, SECOND_STAGE_EVOLUTION_WAVE) ],
[ Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONTOP, SECOND_STAGE_EVOLUTION_WAVE) ],
@ -163,7 +164,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
if (pokemon2CommonEggs > 0) {
const eggsText = i18next.t(`${namespace}:numEggs`, { count: pokemon2CommonEggs, rarity: i18next.t("egg:defaultTier") });
pokemon2Tooltip += i18next.t(`${namespace}:eggs_tooltip`, { eggs: eggsText });
encounter.setDialogueToken("pokemon1CommonEggs", eggsText);
encounter.setDialogueToken("pokemon2CommonEggs", eggsText);
}
encounter.options[1].dialogue!.buttonTooltip = pokemon2Tooltip;
@ -221,7 +222,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.misc.chosenPokemon = pokemon1;
encounter.setDialogueToken("chosenPokemon", pokemon1.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon1CommonEggs, pokemon1RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions, () => doPostEncounterCleanup(scene));
// Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon1);
@ -247,9 +248,6 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.onGameOver = onGameOver;
await initBattleWithEnemyConfig(scene, config);
})
.withPostOptionPhase(async (scene: BattleScene) => {
await doPostEncounterCleanup(scene);
})
.build()
)
.withOption(
@ -273,7 +271,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.misc.chosenPokemon = pokemon2;
encounter.setDialogueToken("chosenPokemon", pokemon2.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon2CommonEggs, pokemon2RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions, () => doPostEncounterCleanup(scene));
// Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon2);
@ -299,9 +297,6 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.onGameOver = onGameOver;
await initBattleWithEnemyConfig(scene, config);
})
.withPostOptionPhase(async (scene: BattleScene) => {
await doPostEncounterCleanup(scene);
})
.build()
)
.withOption(
@ -325,7 +320,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.misc.chosenPokemon = pokemon3;
encounter.setDialogueToken("chosenPokemon", pokemon3.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon3CommonEggs, pokemon3RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions, () => doPostEncounterCleanup(scene));
// Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon3);
@ -351,9 +346,6 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.onGameOver = onGameOver;
await initBattleWithEnemyConfig(scene, config);
})
.withPostOptionPhase(async (scene: BattleScene) => {
await doPostEncounterCleanup(scene);
})
.build()
)
.withOutroDialogue([
@ -521,19 +513,19 @@ function checkAchievement(scene: BattleScene) {
}
}
async function restorePartyAndHeldItems(scene: BattleScene) {
function restorePartyAndHeldItems(scene: BattleScene) {
const encounter = scene.currentBattle.mysteryEncounter!;
// Restore original party
scene.getParty().push(...encounter.misc.originalParty);
// Restore held items
const originalHeldItems = encounter.misc.originalPartyHeldItems;
originalHeldItems.forEach(pokemonHeldItemsList => {
originalHeldItems.forEach((pokemonHeldItemsList: PokemonHeldItemModifier[]) => {
pokemonHeldItemsList.forEach(heldItem => {
scene.addModifier(heldItem, true, false, false, true);
});
});
await scene.updateModifiers(true);
scene.updateModifiers(true);
}
function onGameOver(scene: BattleScene) {
@ -609,13 +601,13 @@ function onGameOver(scene: BattleScene) {
return false;
}
async function doPostEncounterCleanup(scene: BattleScene) {
function doPostEncounterCleanup(scene: BattleScene) {
const encounter = scene.currentBattle.mysteryEncounter!;
if (!encounter.misc.encounterFailed) {
// Give achievement if in Space biome
checkAchievement(scene);
// Give 20 friendship to the chosen pokemon
encounter.misc.chosenPokemon.addFriendship(FRIENDSHIP_ADDED);
await restorePartyAndHeldItems(scene);
restorePartyAndHeldItems(scene);
}
}

View File

@ -14,7 +14,7 @@ import { BattlerIndex } from "#app/battle";
import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { Stat } from "#enums/stat";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
@ -79,7 +79,7 @@ export const TheStrongStuffEncounter: MysteryEncounter =
species: getPokemonSpecies(Species.SHUCKLE),
isBoss: true,
bossSegments: 5,
mysteryEncounterPokemonData: new MysteryEncounterPokemonData({ spriteScale: 1.25 }),
customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
nature: Nature.BOLD,
moveSet: [ Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER ],
modifierConfigs: [

View File

@ -210,7 +210,7 @@ export const UncommonBreedEncounter: MysteryEncounter =
.withOption(
MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new MoveRequirement(CHARMING_MOVES)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically
.withPrimaryPokemonRequirement(new MoveRequirement(CHARMING_MOVES, true)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically
.withDialogue({
buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`,

View File

@ -12,7 +12,7 @@ import { IntegerHolder, isNullOrUndefined, randSeedInt, randSeedShuffle } from "
import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species";
import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
import { achvs } from "#app/system/achv";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { modifierTypes } from "#app/modifier/modifier-type";
import i18next from "#app/plugins/i18n";
@ -379,10 +379,10 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
newType = randSeedInt(18) as Type;
}
newTypes.push(newType);
if (!newPokemon.mysteryEncounterPokemonData) {
newPokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData();
if (!newPokemon.customPokemonData) {
newPokemon.customPokemonData = new CustomPokemonData();
}
newPokemon.mysteryEncounterPokemonData.types = newTypes;
newPokemon.customPokemonData.types = newTypes;
for (const item of transformation.heldItems) {
item.pokemonId = newPokemon.id;

View File

@ -15,6 +15,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { AttackTypeBoosterModifier } from "#app/modifier/modifier";
import { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type";
import { SpeciesFormKey } from "#enums/species-form-key";
import { allAbilities } from "#app/data/ability";
export interface EncounterRequirement {
meetsRequirement(scene: BattleScene): boolean; // Boolean to see if a requirement is met
@ -476,9 +477,11 @@ export class MoveRequirement extends EncounterPokemonRequirement {
requiredMoves: Moves[] = [];
minNumberOfPokemon: number;
invertQuery: boolean;
excludeDisallowedPokemon: boolean;
constructor(moves: Moves | Moves[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
constructor(moves: Moves | Moves[], excludeDisallowedPokemon: boolean, minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
super();
this.excludeDisallowedPokemon = excludeDisallowedPokemon;
this.minNumberOfPokemon = minNumberOfPokemon;
this.invertQuery = invertQuery;
this.requiredMoves = Array.isArray(moves) ? moves : [ moves ];
@ -494,10 +497,15 @@ export class MoveRequirement extends EncounterPokemonRequirement {
override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
if (!this.invertQuery) {
return partyPokemon.filter((pokemon) => this.requiredMoves.filter((reqMove) => pokemon.moveset.filter((move) => move?.moveId === reqMove).length > 0).length > 0);
// get the Pokemon with at least one move in the required moves list
return partyPokemon.filter((pokemon) =>
(!this.excludeDisallowedPokemon || pokemon.isAllowedInBattle())
&& pokemon.moveset.some((move) => move?.moveId && this.requiredMoves.includes(move.moveId)));
} else {
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed moves
return partyPokemon.filter((pokemon) => this.requiredMoves.filter((reqMove) => pokemon.moveset.filter((move) => move?.moveId === reqMove).length === 0).length === 0);
return partyPokemon.filter((pokemon) =>
(!this.excludeDisallowedPokemon || pokemon.isAllowedInBattle())
&& !pokemon.moveset.some((move) => move?.moveId && this.requiredMoves.includes(move.moveId)));
}
}
@ -559,9 +567,11 @@ export class AbilityRequirement extends EncounterPokemonRequirement {
requiredAbilities: Abilities[];
minNumberOfPokemon: number;
invertQuery: boolean;
excludeDisallowedPokemon: boolean;
constructor(abilities: Abilities | Abilities[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
constructor(abilities: Abilities | Abilities[], excludeDisallowedPokemon: boolean, minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
super();
this.excludeDisallowedPokemon = excludeDisallowedPokemon;
this.minNumberOfPokemon = minNumberOfPokemon;
this.invertQuery = invertQuery;
this.requiredAbilities = Array.isArray(abilities) ? abilities : [ abilities ];
@ -577,16 +587,21 @@ export class AbilityRequirement extends EncounterPokemonRequirement {
override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
if (!this.invertQuery) {
return partyPokemon.filter((pokemon) => this.requiredAbilities.some((ability) => pokemon.getAbility().id === ability));
return partyPokemon.filter((pokemon) =>
(!this.excludeDisallowedPokemon || pokemon.isAllowedInBattle())
&& this.requiredAbilities.some((ability) => pokemon.hasAbility(ability, false)));
} else {
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed abilitiess
return partyPokemon.filter((pokemon) => this.requiredAbilities.filter((ability) => pokemon.getAbility().id === ability).length === 0);
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed abilities
return partyPokemon.filter((pokemon) =>
(!this.excludeDisallowedPokemon || pokemon.isAllowedInBattle())
&& this.requiredAbilities.filter((ability) => pokemon.hasAbility(ability, false)).length === 0);
}
}
override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
if (pokemon?.getAbility().id && this.requiredAbilities.some(a => pokemon.getAbility().id === a)) {
return [ "ability", pokemon.getAbility().name ];
override getDialogueToken(_scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
const matchingAbility = this.requiredAbilities.find(a => pokemon?.hasAbility(a, false));
if (!isNullOrUndefined(matchingAbility)) {
return [ "ability", allAbilities[matchingAbility].name ];
}
return [ "ability", "" ];
}

View File

@ -325,7 +325,7 @@ export default class MysteryEncounter implements IMysteryEncounter {
if (activeMon.length > 0) {
this.primaryPokemon = activeMon[0];
} else {
this.primaryPokemon = scene.getParty().filter(p => !p.isFainted())[0];
this.primaryPokemon = scene.getParty().filter(p => p.isAllowedInBattle())[0];
}
return true;
}

View File

@ -27,7 +27,7 @@ import { Status, StatusEffect } from "#app/data/status-effect";
import { TrainerConfig, trainerConfigs, TrainerSlot } from "#app/data/trainer-config";
import PokemonSpecies from "#app/data/pokemon-species";
import { Egg, IEggOptions } from "#app/data/egg";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import HeldModifierConfig from "#app/interfaces/held-modifier-config";
import { MovePhase } from "#app/phases/move-phase";
import { EggLapsePhase } from "#app/phases/egg-lapse-phase";
@ -71,7 +71,7 @@ export interface EnemyPokemonConfig {
nickname?: string;
bossSegments?: number;
bossSegmentModifier?: number; // Additive to the determined segment number
mysteryEncounterPokemonData?: MysteryEncounterPokemonData;
customPokemonData?: CustomPokemonData;
formIndex?: number;
abilityIndex?: number;
level?: number;
@ -145,7 +145,7 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
newTrainer.setVisible(false);
scene.field.add(newTrainer);
scene.currentBattle.trainer = newTrainer;
loadEnemyAssets.push(newTrainer.loadAssets());
loadEnemyAssets.push(newTrainer.loadAssets().then(() => newTrainer.initSprite()));
battle.enemyLevels = scene.currentBattle.trainer.getPartyLevels(scene.currentBattle.waveIndex);
} else {
@ -250,8 +250,8 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
}
// Set custom mystery encounter data fields (such as sprite scale, custom abilities, types, etc.)
if (!isNullOrUndefined(config.mysteryEncounterPokemonData)) {
enemyPokemon.mysteryEncounterPokemonData = config.mysteryEncounterPokemonData;
if (!isNullOrUndefined(config.customPokemonData)) {
enemyPokemon.customPokemonData = config.customPokemonData;
}
// Set Boss

View File

@ -22,7 +22,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags";
import { WeatherType } from "#app/data/weather";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag";
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs } from "#app/data/ability";
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr } from "#app/data/ability";
import PokemonData from "#app/system/pokemon-data";
import { BattlerIndex } from "#app/battle";
import { Mode } from "#app/ui/ui";
@ -62,7 +62,7 @@ import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-ph
import { Challenges } from "#enums/challenges";
import { PokemonAnimType } from "#enums/pokemon-anim-type";
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { SwitchType } from "#enums/switch-type";
import { SpeciesFormKey } from "#enums/species-form-key";
import { BASE_HIDDEN_ABILITY_CHANCE, BASE_SHINY_CHANCE, SHINY_EPIC_CHANCE, SHINY_VARIANT_CHANCE } from "#app/data/balance/rates";
@ -114,7 +114,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public fusionVariant: Variant;
public fusionGender: Gender;
public fusionLuck: integer;
public fusionMysteryEncounterPokemonData: MysteryEncounterPokemonData | null;
public fusionCustomPokemonData: CustomPokemonData | null;
private summonDataPrimer: PokemonSummonData | null;
@ -122,7 +122,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public battleData: PokemonBattleData;
public battleSummonData: PokemonBattleSummonData;
public turnData: PokemonTurnData;
public mysteryEncounterPokemonData: MysteryEncounterPokemonData;
public customPokemonData: CustomPokemonData;
/** Used by Mystery Encounters to execute pokemon-specific logic (such as stat boosts) at start of battle */
public mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void;
@ -193,7 +193,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
this.nature = dataSource.nature || 0 as Nature;
this.nickname = dataSource.nickname;
this.natureOverride = dataSource.natureOverride !== undefined ? dataSource.natureOverride : -1;
this.moveset = dataSource.moveset;
this.status = dataSource.status!; // TODO: is this bang correct?
this.friendship = dataSource.friendship !== undefined ? dataSource.friendship : this.species.baseFriendship;
@ -212,9 +211,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.fusionVariant = dataSource.fusionVariant || 0;
this.fusionGender = dataSource.fusionGender;
this.fusionLuck = dataSource.fusionLuck;
this.fusionMysteryEncounterPokemonData = dataSource.fusionMysteryEncounterPokemonData;
this.fusionCustomPokemonData = dataSource.fusionCustomPokemonData;
this.usedTMs = dataSource.usedTMs ?? [];
this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(dataSource.mysteryEncounterPokemonData);
this.customPokemonData = new CustomPokemonData(dataSource.customPokemonData);
} else {
this.id = Utils.randSeedInt(4294967296);
this.ivs = ivs || Utils.getIvsFromId(this.id);
@ -235,7 +234,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.variant = this.shiny ? this.generateVariant() : 0;
}
this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData();
this.customPokemonData = new CustomPokemonData();
if (nature !== undefined) {
this.setNature(nature);
@ -243,8 +242,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.generateNature();
}
this.natureOverride = -1;
this.friendship = species.baseFriendship;
this.metLevel = level;
this.metBiome = scene.currentBattle ? scene.arena.biomeType : -1;
@ -593,8 +590,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const formKey = this.getFormKey();
if (this.isMax() === true || formKey === "segin-starmobile" || formKey === "schedar-starmobile" || formKey === "navi-starmobile" || formKey === "ruchbah-starmobile" || formKey === "caph-starmobile") {
return 1.5;
} else if (this.mysteryEncounterPokemonData.spriteScale > 0) {
return this.mysteryEncounterPokemonData.spriteScale;
} else if (this.customPokemonData.spriteScale > 0) {
return this.customPokemonData.spriteScale;
}
return 1;
}
@ -1023,7 +1020,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
getNature(): Nature {
return this.natureOverride !== -1 ? this.natureOverride : this.nature;
return this.customPokemonData.nature !== -1 ? this.customPokemonData.nature : this.nature;
}
setNature(nature: Nature): void {
@ -1198,15 +1195,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (!types.length || !includeTeraType) {
if (!ignoreOverride && this.summonData?.types && this.summonData.types.length > 0) {
this.summonData.types.forEach(t => types.push(t));
} else if (this.mysteryEncounterPokemonData.types && this.mysteryEncounterPokemonData.types.length > 0) {
} else if (this.customPokemonData.types && this.customPokemonData.types.length > 0) {
// "Permanent" override for a Pokemon's normal types, currently only used by Mystery Encounters
types.push(this.mysteryEncounterPokemonData.types[0]);
types.push(this.customPokemonData.types[0]);
// Fusing a Pokemon onto something with "permanently changed" types will still apply the fusion's types as normal
const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOverride);
if (fusionSpeciesForm) {
// Check if the fusion Pokemon also had "permanently changed" types
const fusionMETypes = this.fusionMysteryEncounterPokemonData?.types;
const fusionMETypes = this.fusionCustomPokemonData?.types;
if (fusionMETypes && fusionMETypes.length >= 2 && fusionMETypes[1] !== types[0]) {
types.push(fusionMETypes[1]);
} else if (fusionMETypes && fusionMETypes.length === 1 && fusionMETypes[0] !== types[0]) {
@ -1218,8 +1215,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
if (types.length === 1 && this.mysteryEncounterPokemonData.types.length >= 2) {
types.push(this.mysteryEncounterPokemonData.types[1]);
if (types.length === 1 && this.customPokemonData.types.length >= 2) {
types.push(this.customPokemonData.types[1]);
}
} else {
const speciesForm = this.getSpeciesForm(ignoreOverride);
@ -1230,7 +1227,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (fusionSpeciesForm) {
// Check if the fusion Pokemon also had "permanently changed" types
// Otherwise, use standard fusion type logic
const fusionMETypes = this.fusionMysteryEncounterPokemonData?.types;
const fusionMETypes = this.fusionCustomPokemonData?.types;
if (fusionMETypes && fusionMETypes.length >= 2 && fusionMETypes[1] !== types[0]) {
types.push(fusionMETypes[1]);
} else if (fusionMETypes && fusionMETypes.length === 1 && fusionMETypes[0] !== types[0]) {
@ -1262,6 +1259,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
// If both types are the same (can happen in weird custom typing scenarios), reduce to single type
if (types.length > 1 && types[0] === types[1]) {
types.splice(0, 1);
}
return types;
}
@ -1288,14 +1290,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return allAbilities[Overrides.OPP_ABILITY_OVERRIDE];
}
if (this.isFusion()) {
if (!isNullOrUndefined(this.fusionMysteryEncounterPokemonData?.ability) && this.fusionMysteryEncounterPokemonData.ability !== -1) {
return allAbilities[this.fusionMysteryEncounterPokemonData.ability];
if (!isNullOrUndefined(this.fusionCustomPokemonData?.ability) && this.fusionCustomPokemonData.ability !== -1) {
return allAbilities[this.fusionCustomPokemonData.ability];
} else {
return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)];
}
}
if (!isNullOrUndefined(this.mysteryEncounterPokemonData.ability) && this.mysteryEncounterPokemonData.ability !== -1) {
return allAbilities[this.mysteryEncounterPokemonData.ability];
if (!isNullOrUndefined(this.customPokemonData.ability) && this.customPokemonData.ability !== -1) {
return allAbilities[this.customPokemonData.ability];
}
let abilityId = this.getSpeciesForm(ignoreOverride).getAbility(this.abilityIndex);
if (abilityId === Abilities.NONE) {
@ -1318,8 +1320,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE && !this.isPlayer()) {
return allAbilities[Overrides.OPP_PASSIVE_ABILITY_OVERRIDE];
}
if (!isNullOrUndefined(this.mysteryEncounterPokemonData.passive) && this.mysteryEncounterPokemonData.passive !== -1) {
return allAbilities[this.mysteryEncounterPokemonData.passive];
if (!isNullOrUndefined(this.customPokemonData.passive) && this.customPokemonData.passive !== -1) {
return allAbilities[this.customPokemonData.passive];
}
let starterSpeciesId = this.species.speciesId;
@ -2023,7 +2025,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.fusionVariant = 0;
this.fusionGender = 0;
this.fusionLuck = 0;
this.fusionMysteryEncounterPokemonData = null;
this.fusionCustomPokemonData = null;
this.generateName();
this.calculateStats();
@ -2192,8 +2194,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.moveset.push(new PokemonMove(movePool[index][0], 0, 0));
}
// Trigger FormChange, except for enemy Pokemon during Mystery Encounters, to avoid crashes
if (this.isPlayer() || !this.scene.currentBattle?.isBattleMysteryEncounter() || !this.scene.currentBattle?.mysteryEncounter) {
this.scene.triggerPokemonFormChange(this, SpeciesFormChangeMoveLearnedTrigger);
}
}
trySelectMove(moveIndex: integer, ignorePp?: boolean): boolean {
const move = this.getMoveset().length > moveIndex
@ -2610,7 +2615,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/** Reduces damage if this Pokemon has a relevant screen (e.g. Light Screen for special attacks) */
const screenMultiplier = new Utils.NumberHolder(1);
this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, simulated, moveCategory, screenMultiplier);
this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, simulated, source, moveCategory, screenMultiplier);
/**
* For each {@linkcode HitsTagAttr} the move has, doubles the damage of the move if:
@ -3354,13 +3359,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
const types = this.getTypes(true, true);
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (sourcePokemon && sourcePokemon !== this && this.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, defendingSide)) {
if (sourcePokemon && sourcePokemon !== this && this.isSafeguarded(sourcePokemon)) {
return false;
}
const types = this.getTypes(true, true);
switch (effect) {
case StatusEffect.POISON:
case StatusEffect.TOXIC:
@ -3506,6 +3510,23 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
/**
* Checks if this Pokemon is protected by Safeguard
* @param attacker the {@linkcode Pokemon} inflicting status on this Pokemon
* @returns `true` if this Pokemon is protected by Safeguard; `false` otherwise.
*/
isSafeguarded(attacker: Pokemon): boolean {
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (this.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, defendingSide)) {
const bypassed = new Utils.BooleanHolder(false);
if (attacker) {
applyAbAttrs(InfiltratorAbAttr, attacker, null, false, bypassed);
}
return !bypassed.value;
}
return false;
}
primeSummonData(summonDataPrimer: PokemonSummonData): void {
this.summonDataPrimer = summonDataPrimer;
}
@ -3984,10 +4005,14 @@ export class PlayerPokemon extends Pokemon {
if (Overrides.SHINY_OVERRIDE) {
this.shiny = true;
this.initShinySparkle();
if (Overrides.VARIANT_OVERRIDE) {
} else if (Overrides.SHINY_OVERRIDE === false) {
this.shiny = false;
}
if (Overrides.VARIANT_OVERRIDE !== null && this.shiny) {
this.variant = Overrides.VARIANT_OVERRIDE;
}
}
if (!dataSource) {
if (this.scene.gameMode.isDaily) {
this.generateAndPopulateMoveset();
@ -4299,12 +4324,33 @@ export class PlayerPokemon extends Pokemon {
changeForm(formChange: SpeciesFormChange): Promise<void> {
return new Promise(resolve => {
const previousFormIndex = this.formIndex;
this.formIndex = Math.max(this.species.forms.findIndex(f => f.formKey === formChange.formKey), 0);
this.generateName();
const abilityCount = this.getSpeciesForm().getAbilityCount();
if (this.abilityIndex >= abilityCount) { // Shouldn't happen
this.abilityIndex = abilityCount - 1;
}
// In cases where a form change updates the type of a Pokemon from its previous form (Arceus, Silvally, Castform, etc.),
// persist that type change in customPokemonData if necessary
const baseForm = this.species.forms[previousFormIndex];
const baseFormTypes = [ baseForm.type1, baseForm.type2 ];
if (this.customPokemonData.types.length > 0) {
if (this.getSpeciesForm().type1 !== baseFormTypes[0]) {
this.customPokemonData.types[0] = this.getSpeciesForm().type1;
}
const type2 = this.getSpeciesForm().type2;
if (!isNullOrUndefined(type2) && type2 !== baseFormTypes[1]) {
if (this.customPokemonData.types.length > 1) {
this.customPokemonData.types[1] = type2;
} else {
this.customPokemonData.types.push(type2);
}
}
}
this.compatibleTms.splice(0, this.compatibleTms.length);
this.generateCompatibleTms();
const updateAndResolve = () => {
@ -4341,7 +4387,7 @@ export class PlayerPokemon extends Pokemon {
this.fusionVariant = pokemon.variant;
this.fusionGender = pokemon.gender;
this.fusionLuck = pokemon.luck;
this.fusionMysteryEncounterPokemonData = pokemon.mysteryEncounterPokemonData;
this.fusionCustomPokemonData = pokemon.customPokemonData;
if ((pokemon.pauseEvolutions) || (this.pauseEvolutions)) {
this.pauseEvolutions = true;
}
@ -4455,10 +4501,13 @@ export class EnemyPokemon extends Pokemon {
if (Overrides.OPP_SHINY_OVERRIDE) {
this.shiny = true;
this.initShinySparkle();
} else if (Overrides.OPP_SHINY_OVERRIDE === false) {
this.shiny = false;
}
if (this.shiny) {
this.variant = this.generateVariant();
if (Overrides.OPP_VARIANT_OVERRIDE) {
if (Overrides.OPP_VARIANT_OVERRIDE !== null) {
this.variant = Overrides.OPP_VARIANT_OVERRIDE;
}
}

View File

@ -1126,7 +1126,7 @@ class EvolutionItemModifierTypeGenerator extends ModifierTypeGenerator {
}
class FormChangeItemModifierTypeGenerator extends ModifierTypeGenerator {
constructor(rare: boolean) {
constructor(isRareFormChangeItem: boolean) {
super((party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs && (pregenArgs.length === 1) && (pregenArgs[0] in FormChangeItem)) {
return new FormChangeItemModifierType(pregenArgs[0] as FormChangeItem);
@ -1167,7 +1167,7 @@ class FormChangeItemModifierTypeGenerator extends ModifierTypeGenerator {
}
return formChangeItemTriggers;
}).flat())
].flat().flatMap(fc => fc.item).filter(i => (i && i < 100) === rare);
].flat().flatMap(fc => fc.item).filter(i => (i && i < 100) === isRareFormChangeItem);
// convert it into a set to remove duplicate values, which can appear when the same species with a potential form change is in the party.
if (!formChangeItemPool.length) {

View File

@ -2188,7 +2188,7 @@ export class PokemonNatureChangeModifier extends ConsumablePokemonModifier {
* @returns
*/
override apply(playerPokemon: PlayerPokemon): boolean {
playerPokemon.natureOverride = this.nature;
playerPokemon.customPokemonData.nature = this.nature;
let speciesId = playerPokemon.species.speciesId;
playerPokemon.scene.gameData.dexData[speciesId].natureAttr |= 1 << (this.nature + 1);

View File

@ -113,8 +113,8 @@ class DefaultOverrides {
readonly STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE;
readonly GENDER_OVERRIDE: Gender | null = null;
readonly MOVESET_OVERRIDE: Moves | Array<Moves> = [];
readonly SHINY_OVERRIDE: boolean = false;
readonly VARIANT_OVERRIDE: Variant = 0;
readonly SHINY_OVERRIDE: boolean | null = null;
readonly VARIANT_OVERRIDE: Variant | null = null;
// --------------------------
// OPPONENT / ENEMY OVERRIDES
@ -134,8 +134,8 @@ class DefaultOverrides {
readonly OPP_STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE;
readonly OPP_GENDER_OVERRIDE: Gender | null = null;
readonly OPP_MOVESET_OVERRIDE: Moves | Array<Moves> = [];
readonly OPP_SHINY_OVERRIDE: boolean = false;
readonly OPP_VARIANT_OVERRIDE: Variant = 0;
readonly OPP_SHINY_OVERRIDE: boolean | null = null;
readonly OPP_VARIANT_OVERRIDE: Variant | null = null;
readonly OPP_IVS_OVERRIDE: number | number[] = [];
readonly OPP_FORM_OVERRIDES: Partial<Record<Species, number>> = {};
/**

View File

@ -35,6 +35,7 @@ import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-d
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
import { getGoldenBugNetSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { Biome } from "#enums/biome";
import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters";
import { BattlerTagType } from "#enums/battler-tag-type";
export class EncounterPhase extends BattlePhase {
@ -69,7 +70,7 @@ export class EncounterPhase extends BattlePhase {
this.scene.executeWithSeedOffset(() => {
const currentSessionEncounterType = battle.mysteryEncounterType;
battle.mysteryEncounter = this.scene.getMysteryEncounter(currentSessionEncounterType);
}, battle.waveIndex << 4);
}, battle.waveIndex * 16);
}
const mysteryEncounter = battle.mysteryEncounter;
if (mysteryEncounter) {
@ -252,6 +253,13 @@ export class EncounterPhase extends BattlePhase {
this.scene.updateModifiers(true);
}*/
const { battleType, waveIndex } = this.scene.currentBattle;
if (this.scene.isMysteryEncounterValidForWave(battleType, waveIndex) && !this.scene.currentBattle.isBattleMysteryEncounter()) {
// Increment ME spawn chance if an ME could have spawned but did not
// Only do this AFTER session has been saved to avoid duplicating increments
this.scene.mysteryEncounterSaveData.encounterSpawnChance += WEIGHT_INCREMENT_ON_SPAWN_MISS;
}
for (const pokemon of this.scene.getParty()) {
if (pokemon) {
pokemon.resetBattleData();

View File

@ -170,13 +170,16 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
pokemon.setMove(index, this.moveId);
initMoveAnim(this.scene, this.moveId).then(() => {
loadMoveAnimAssets(this.scene, [ this.moveId ], true);
this.scene.playSound("level_up_fanfare"); // Sound loaded into game as is
});
this.scene.ui.setMode(this.messageMode);
const learnMoveText = i18next.t("battle:learnMove", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: move.name });
textMessage = textMessage ? textMessage + "$" + learnMoveText : learnMoveText;
await this.scene.ui.showTextPromise(textMessage, this.messageMode === Mode.EVOLUTION_SCENE ? 1000 : undefined, true);
if (textMessage) {
await this.scene.ui.showTextPromise(textMessage);
}
this.scene.playSound("level_up_fanfare"); // Sound loaded into game as is
this.scene.ui.showText(learnMoveText, null, () => {
this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeMoveLearnedTrigger, true);
this.end();
}, this.messageMode === Mode.EVOLUTION_SCENE ? 1000 : undefined, true);
}
}

View File

@ -402,7 +402,7 @@ export class MysteryEncounterBattlePhase extends Phase {
}
}
const availablePartyMembers = scene.getParty().filter(p => !p.isFainted());
const availablePartyMembers = scene.getParty().filter(p => p.isAllowedInBattle());
if (!availablePartyMembers[0].isOnField()) {
scene.pushPhase(new SummonPhase(scene, 0));

View File

@ -1,13 +0,0 @@
import BattleScene from "#app/battle-scene";
import { Phase } from "#app/phase";
import { Mode } from "#app/ui/ui";
export class OutdatedPhase extends Phase {
constructor(scene: BattleScene) {
super(scene);
}
start(): void {
this.scene.ui.setMode(Mode.OUTDATED);
}
}

View File

@ -64,7 +64,8 @@ export class StatStageChangePhase extends PokemonPhase {
const cancelled = new BooleanHolder(false);
if (!this.selfTarget && stages.value < 0) {
this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, false, cancelled);
// TODO: add a reference to the source of the stat change to fix Infiltrator interaction
this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, false, null, false, cancelled);
}
if (!cancelled.value && !this.selfTarget && stages.value < 0) {

View File

@ -43,10 +43,9 @@ import { Species } from "#enums/species";
import { applyChallenges, ChallengeType } from "#app/data/challenge";
import { WeatherType } from "#enums/weather-type";
import { TerrainType } from "#app/data/terrain";
import { OutdatedPhase } from "#app/phases/outdated-phase";
import { ReloadSessionPhase } from "#app/phases/reload-session-phase";
import { RUN_HISTORY_LIMIT } from "#app/ui/run-history-ui-handler";
import { applySessionDataPatches, applySettingsDataPatches, applySystemDataPatches } from "#app/system/version-converter";
import { applySessionVersionMigration, applySystemVersionMigration, applySettingsVersionMigration } from "./version_migration/version_converter";
import { MysteryEncounterSaveData } from "#app/data/mystery-encounters/mystery-encounter-save-data";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PokerogueApiClearSessionData } from "#app/@types/pokerogue-api";
@ -403,10 +402,7 @@ export class GameData {
.then(error => {
this.scene.ui.savingIcon.hide();
if (error) {
if (error.startsWith("client version out of date")) {
this.scene.clearPhaseQueue();
this.scene.unshiftPhase(new OutdatedPhase(this.scene));
} else if (error.startsWith("session out of date")) {
if (error.startsWith("session out of date")) {
this.scene.clearPhaseQueue();
this.scene.unshiftPhase(new ReloadSessionPhase(this.scene));
}
@ -482,7 +478,7 @@ export class GameData {
localStorage.setItem(lsItemKey, "");
}
applySystemDataPatches(systemData);
applySystemVersionMigration(systemData);
this.trainerId = systemData.trainerId;
this.secretId = systemData.secretId;
@ -857,7 +853,7 @@ export class GameData {
const settings = JSON.parse(localStorage.getItem("settings")!); // TODO: is this bang correct?
applySettingsDataPatches(settings);
applySettingsVersionMigration(settings);
for (const setting of Object.keys(settings)) {
setSetting(this.scene, setting, settings[setting]);
@ -1313,7 +1309,7 @@ export class GameData {
return v;
}) as SessionSaveData;
applySessionDataPatches(sessionData);
applySessionVersionMigration(sessionData);
return sessionData;
}
@ -1354,10 +1350,7 @@ export class GameData {
this.scene.ui.savingIcon.hide();
}
if (error) {
if (error.startsWith("client version out of date")) {
this.scene.clearPhaseQueue();
this.scene.unshiftPhase(new OutdatedPhase(this.scene));
} else if (error.startsWith("session out of date")) {
if (error.startsWith("session out of date")) {
this.scene.clearPhaseQueue();
this.scene.unshiftPhase(new ReloadSessionPhase(this.scene));
}

View File

@ -12,7 +12,7 @@ import { loadBattlerTag } from "../data/battler-tags";
import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
export default class PokemonData {
public id: integer;
@ -33,7 +33,6 @@ export default class PokemonData {
public stats: integer[];
public ivs: integer[];
public nature: Nature;
public natureOverride: Nature | -1;
public moveset: (PokemonMove | null)[];
public status: Status | null;
public friendship: integer;
@ -54,14 +53,20 @@ export default class PokemonData {
public fusionVariant: Variant;
public fusionGender: Gender;
public fusionLuck: integer;
public fusionMysteryEncounterPokemonData: MysteryEncounterPokemonData;
public boss: boolean;
public bossSegments?: integer;
public summonData: PokemonSummonData;
/** Data that can customize a Pokemon in non-standard ways from its Species */
public mysteryEncounterPokemonData: MysteryEncounterPokemonData;
public customPokemonData: CustomPokemonData;
public fusionCustomPokemonData: CustomPokemonData;
// Deprecated attributes, needed for now to allow SessionData migration (see PR#4619 comments)
public natureOverride: Nature | -1;
public mysteryEncounterPokemonData: CustomPokemonData | null;
public fusionMysteryEncounterPokemonData: CustomPokemonData | null;
constructor(source: Pokemon | any, forHistory: boolean = false) {
const sourcePokemon = source instanceof Pokemon ? source : null;
@ -107,9 +112,13 @@ export default class PokemonData {
this.fusionVariant = source.fusionVariant;
this.fusionGender = source.fusionGender;
this.fusionLuck = source.fusionLuck !== undefined ? source.fusionLuck : (source.fusionShiny ? source.fusionVariant + 1 : 0);
this.fusionCustomPokemonData = new CustomPokemonData(source.fusionCustomPokemonData);
this.usedTMs = source.usedTMs ?? [];
this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(source.mysteryEncounterPokemonData);
this.customPokemonData = new CustomPokemonData(source.customPokemonData);
this.mysteryEncounterPokemonData = new CustomPokemonData(source.mysteryEncounterPokemonData);
this.fusionMysteryEncounterPokemonData = new CustomPokemonData(source.fusionMysteryEncounterPokemonData);
if (!forHistory) {
this.boss = (source instanceof EnemyPokemon && !!source.bossSegments) || (!this.player && !!source.boss);

View File

@ -1,157 +0,0 @@
import { allSpecies } from "#app/data/pokemon-species";
import { AbilityAttr, defaultStarterSpecies, DexAttr, SessionSaveData, SystemSaveData } from "./game-data";
import { SettingKeys } from "./settings/settings";
const LATEST_VERSION = "1.0.5";
export function applySessionDataPatches(data: SessionSaveData) {
const curVersion = data.gameVersion;
// Always sanitize money as a safeguard
data.money = Math.floor(data.money);
if (curVersion !== LATEST_VERSION) {
switch (curVersion) {
case "1.0.0":
case "1.0.1":
case "1.0.2":
case "1.0.3":
case "1.0.4":
// --- PATCHES ---
// Fix Battle Items, Vitamins, and Lures
data.modifiers.forEach((m) => {
if (m.className === "PokemonBaseStatModifier") {
m.className = "BaseStatModifier";
} else if (m.className === "PokemonResetNegativeStatStageModifier") {
m.className = "ResetNegativeStatStageModifier";
} else if (m.className === "TempBattleStatBoosterModifier") {
// Dire Hit no longer a part of the TempBattleStatBoosterModifierTypeGenerator
if (m.typeId !== "DIRE_HIT") {
m.className = "TempStatStageBoosterModifier";
m.typeId = "TEMP_STAT_STAGE_BOOSTER";
// Migration from TempBattleStat to Stat
const newStat = m.typePregenArgs[0] + 1;
m.typePregenArgs[0] = newStat;
// From [ stat, battlesLeft ] to [ stat, maxBattles, battleCount ]
m.args = [ newStat, 5, m.args[1] ];
} else {
m.className = "TempCritBoosterModifier";
m.typePregenArgs = [];
// From [ stat, battlesLeft ] to [ maxBattles, battleCount ]
m.args = [ 5, m.args[1] ];
}
} else if (m.className === "DoubleBattleChanceBoosterModifier" && m.args.length === 1) {
let maxBattles: number;
switch (m.typeId) {
case "MAX_LURE":
maxBattles = 30;
break;
case "SUPER_LURE":
maxBattles = 15;
break;
default:
maxBattles = 10;
break;
}
// From [ battlesLeft ] to [ maxBattles, battleCount ]
m.args = [ maxBattles, m.args[0] ];
}
});
data.enemyModifiers.forEach((m) => {
if (m.className === "PokemonBaseStatModifier") {
m.className = "BaseStatModifier";
} else if (m.className === "PokemonResetNegativeStatStageModifier") {
m.className = "ResetNegativeStatStageModifier";
}
});
}
data.gameVersion = LATEST_VERSION;
}
}
export function applySystemDataPatches(data: SystemSaveData) {
const curVersion = data.gameVersion;
if (curVersion !== LATEST_VERSION) {
switch (curVersion) {
case "1.0.0":
case "1.0.1":
case "1.0.2":
case "1.0.3":
case "1.0.4":
// --- LEGACY PATCHES ---
if (data.starterData && data.dexData) {
// Migrate ability starter data if empty for caught species
Object.keys(data.starterData).forEach(sd => {
if (data.dexData[sd]?.caughtAttr && (data.starterData[sd] && !data.starterData[sd].abilityAttr)) {
data.starterData[sd].abilityAttr = 1;
}
});
}
// Fix Legendary Stats
if (data.gameStats && (data.gameStats.legendaryPokemonCaught !== undefined && data.gameStats.subLegendaryPokemonCaught === undefined)) {
data.gameStats.subLegendaryPokemonSeen = 0;
data.gameStats.subLegendaryPokemonCaught = 0;
data.gameStats.subLegendaryPokemonHatched = 0;
allSpecies.filter(s => s.subLegendary).forEach(s => {
const dexEntry = data.dexData[s.speciesId];
data.gameStats.subLegendaryPokemonSeen += dexEntry.seenCount;
data.gameStats.legendaryPokemonSeen = Math.max(data.gameStats.legendaryPokemonSeen - dexEntry.seenCount, 0);
data.gameStats.subLegendaryPokemonCaught += dexEntry.caughtCount;
data.gameStats.legendaryPokemonCaught = Math.max(data.gameStats.legendaryPokemonCaught - dexEntry.caughtCount, 0);
data.gameStats.subLegendaryPokemonHatched += dexEntry.hatchedCount;
data.gameStats.legendaryPokemonHatched = Math.max(data.gameStats.legendaryPokemonHatched - dexEntry.hatchedCount, 0);
});
data.gameStats.subLegendaryPokemonSeen = Math.max(data.gameStats.subLegendaryPokemonSeen, data.gameStats.subLegendaryPokemonCaught);
data.gameStats.legendaryPokemonSeen = Math.max(data.gameStats.legendaryPokemonSeen, data.gameStats.legendaryPokemonCaught);
data.gameStats.mythicalPokemonSeen = Math.max(data.gameStats.mythicalPokemonSeen, data.gameStats.mythicalPokemonCaught);
}
// --- PATCHES ---
// Fix Starter Data
if (data.starterData && data.dexData) {
for (const starterId of defaultStarterSpecies) {
if (data.starterData[starterId]?.abilityAttr) {
data.starterData[starterId].abilityAttr |= AbilityAttr.ABILITY_1;
}
if (data.dexData[starterId]?.caughtAttr) {
data.dexData[starterId].caughtAttr |= DexAttr.FEMALE;
}
}
}
}
data.gameVersion = LATEST_VERSION;
}
}
export function applySettingsDataPatches(settings: Object) {
const curVersion = settings.hasOwnProperty("gameVersion") ? settings["gameVersion"] : "1.0.0";
if (curVersion !== LATEST_VERSION) {
switch (curVersion) {
case "1.0.0":
case "1.0.1":
case "1.0.2":
case "1.0.3":
case "1.0.4":
// --- PATCHES ---
// Fix Reward Cursor Target
if (settings.hasOwnProperty("REROLL_TARGET") && !settings.hasOwnProperty(SettingKeys.Shop_Cursor_Target)) {
settings[SettingKeys.Shop_Cursor_Target] = settings["REROLL_TARGET"];
delete settings["REROLL_TARGET"];
localStorage.setItem("settings", JSON.stringify(settings));
}
}
// Note that the current game version will be written at `saveSettings`
}
}

View File

@ -0,0 +1,197 @@
import { SessionSaveData, SystemSaveData } from "../game-data";
import { version } from "../../../package.json";
// --- v1.0.4 (and below) PATCHES --- //
import * as v1_0_4 from "./versions/v1_0_4";
// --- v1.1.0 PATCHES --- //
import * as v1_1_0 from "./versions/v1_1_0";
const LATEST_VERSION = version.split(".").map(value => parseInt(value));
/**
* Converts incoming {@linkcode SystemSaveData} that has a version below the
* current version number listed in `package.json`.
*
* Note that no transforms act on the {@linkcode data} if its version matches
* the current version or if there are no migrations made between its version up
* to the current version.
* @param data {@linkcode SystemSaveData}
* @see {@link SystemVersionConverter}
*/
export function applySystemVersionMigration(data: SystemSaveData) {
const curVersion = data.gameVersion.split(".").map(value => parseInt(value));
if (!curVersion.every((value, index) => value === LATEST_VERSION[index])) {
const converter = new SystemVersionConverter();
converter.applyStaticPreprocessors(data);
converter.applyMigration(data, curVersion);
}
}
/**
* Converts incoming {@linkcode SessionSavaData} that has a version below the
* current version number listed in `package.json`.
*
* Note that no transforms act on the {@linkcode data} if its version matches
* the current version or if there are no migrations made between its version up
* to the current version.
* @param data {@linkcode SessionSaveData}
* @see {@link SessionVersionConverter}
*/
export function applySessionVersionMigration(data: SessionSaveData) {
const curVersion = data.gameVersion.split(".").map(value => parseInt(value));
if (!curVersion.every((value, index) => value === LATEST_VERSION[index])) {
const converter = new SessionVersionConverter();
converter.applyStaticPreprocessors(data);
converter.applyMigration(data, curVersion);
}
}
/**
* Converts incoming settings data that has a version below the
* current version number listed in `package.json`.
*
* Note that no transforms act on the {@linkcode data} if its version matches
* the current version or if there are no migrations made between its version up
* to the current version.
* @param data Settings data object
* @see {@link SettingsVersionConverter}
*/
export function applySettingsVersionMigration(data: Object) {
const gameVersion: string = data.hasOwnProperty("gameVersion") ? data["gameVersion"] : "1.0.0";
const curVersion = gameVersion.split(".").map(value => parseInt(value));
if (!curVersion.every((value, index) => value === LATEST_VERSION[index])) {
const converter = new SettingsVersionConverter();
converter.applyStaticPreprocessors(data);
converter.applyMigration(data, curVersion);
}
}
/**
* Abstract class encapsulating the logic for migrating data from a given version up to
* the current version listed in `package.json`.
*
* Note that, for any version converter, the corresponding `applyMigration`
* function would only need to be changed once when the first migration for a
* given version is introduced. Similarly, a version file (within the `versions`
* folder) would only need to be created for a version once with the appropriate
* array nomenclature.
*/
abstract class VersionConverter {
/**
* Iterates through an array of designated migration functions that are each
* called one by one to transform the data.
* @param data The data to be operated on
* @param migrationArr An array of functions that will transform the incoming data
*/
callMigrators(data: any, migrationArr: readonly any[]) {
for (const migrate of migrationArr) {
migrate(data);
}
}
/**
* Applies any version-agnostic data sanitation as defined within the function
* body.
* @param data The data to be operated on
*/
applyStaticPreprocessors(_data: any): void {
}
/**
* Uses the current version the incoming data to determine the starting point
* of the migration which will cascade up to the latest version, calling the
* necessary migration functions in the process.
* @param data The data to be operated on
* @param curVersion [0] Current major version
* [1] Current minor version
* [2] Current patch version
*/
abstract applyMigration(data: any, curVersion: number[]): void;
}
/**
* Class encapsulating the logic for migrating {@linkcode SessionSaveData} from
* a given version up to the current version listed in `package.json`.
* @extends VersionConverter
*/
class SessionVersionConverter extends VersionConverter {
override applyStaticPreprocessors(data: SessionSaveData): void {
// Always sanitize money as a safeguard
data.money = Math.floor(data.money);
}
override applyMigration(data: SessionSaveData, curVersion: number[]): void {
const [ curMajor, curMinor, curPatch ] = curVersion;
if (curMajor === 1) {
if (curMinor === 0) {
if (curPatch <= 4) {
console.log("Applying v1.0.4 session data migration!");
this.callMigrators(data, v1_0_4.sessionMigrators);
}
}
if (curMinor <= 1) {
console.log("Applying v1.1.0 session data migration!");
this.callMigrators(data, v1_1_0.sessionMigrators);
}
}
console.log(`Session data successfully migrated to v${version}!`);
}
}
/**
* Class encapsulating the logic for migrating {@linkcode SystemSaveData} from
* a given version up to the current version listed in `package.json`.
* @extends VersionConverter
*/
class SystemVersionConverter extends VersionConverter {
override applyMigration(data: SystemSaveData, curVersion: number[]): void {
const [ curMajor, curMinor, curPatch ] = curVersion;
if (curMajor === 1) {
if (curMinor === 0) {
if (curPatch <= 4) {
console.log("Applying v1.0.4 system data migraton!");
this.callMigrators(data, v1_0_4.systemMigrators);
}
}
if (curMinor <= 1) {
console.log("Applying v1.1.0 system data migraton!");
this.callMigrators(data, v1_1_0.systemMigrators);
}
}
console.log(`System data successfully migrated to v${version}!`);
}
}
/**
* Class encapsulating the logic for migrating settings data from
* a given version up to the current version listed in `package.json`.
* @extends VersionConverter
*/
class SettingsVersionConverter extends VersionConverter {
override applyMigration(data: Object, curVersion: number[]): void {
const [ curMajor, curMinor, curPatch ] = curVersion;
if (curMajor === 1) {
if (curMinor === 0) {
if (curPatch <= 4) {
console.log("Applying v1.0.4 settings data migraton!");
this.callMigrators(data, v1_0_4.settingsMigrators);
}
}
if (curMinor <= 1) {
console.log("Applying v1.1.0 settings data migraton!");
this.callMigrators(data, v1_1_0.settingsMigrators);
}
}
console.log(`System data successfully migrated to v${version}!`);
}
}

View File

@ -0,0 +1,135 @@
import { SettingKeys } from "../../settings/settings";
import { AbilityAttr, defaultStarterSpecies, DexAttr, SystemSaveData, SessionSaveData } from "../../game-data";
import { allSpecies } from "../../../data/pokemon-species";
export const systemMigrators = [
/**
* Migrate ability starter data if empty for caught species.
* @param data {@linkcode SystemSaveData}
*/
function migrateAbilityData(data: SystemSaveData) {
if (data.starterData && data.dexData) {
Object.keys(data.starterData).forEach(sd => {
if (data.dexData[sd]?.caughtAttr && (data.starterData[sd] && !data.starterData[sd].abilityAttr)) {
data.starterData[sd].abilityAttr = 1;
}
});
}
},
/**
* Populate legendary Pokémon statistics if they are missing.
* @param data {@linkcode SystemSaveData}
*/
function fixLegendaryStats(data: SystemSaveData) {
if (data.gameStats && (data.gameStats.legendaryPokemonCaught !== undefined && data.gameStats.subLegendaryPokemonCaught === undefined)) {
data.gameStats.subLegendaryPokemonSeen = 0;
data.gameStats.subLegendaryPokemonCaught = 0;
data.gameStats.subLegendaryPokemonHatched = 0;
allSpecies.filter(s => s.subLegendary).forEach(s => {
const dexEntry = data.dexData[s.speciesId];
data.gameStats.subLegendaryPokemonSeen += dexEntry.seenCount;
data.gameStats.legendaryPokemonSeen = Math.max(data.gameStats.legendaryPokemonSeen - dexEntry.seenCount, 0);
data.gameStats.subLegendaryPokemonCaught += dexEntry.caughtCount;
data.gameStats.legendaryPokemonCaught = Math.max(data.gameStats.legendaryPokemonCaught - dexEntry.caughtCount, 0);
data.gameStats.subLegendaryPokemonHatched += dexEntry.hatchedCount;
data.gameStats.legendaryPokemonHatched = Math.max(data.gameStats.legendaryPokemonHatched - dexEntry.hatchedCount, 0);
});
data.gameStats.subLegendaryPokemonSeen = Math.max(data.gameStats.subLegendaryPokemonSeen, data.gameStats.subLegendaryPokemonCaught);
data.gameStats.legendaryPokemonSeen = Math.max(data.gameStats.legendaryPokemonSeen, data.gameStats.legendaryPokemonCaught);
data.gameStats.mythicalPokemonSeen = Math.max(data.gameStats.mythicalPokemonSeen, data.gameStats.mythicalPokemonCaught);
}
},
/**
* Unlock all starters' first ability and female gender option.
* @param data {@linkcode SystemSaveData}
*/
function fixStarterData(data: SystemSaveData) {
for (const starterId of defaultStarterSpecies) {
if (data.starterData[starterId]?.abilityAttr) {
data.starterData[starterId].abilityAttr |= AbilityAttr.ABILITY_1;
}
if (data.dexData[starterId]?.caughtAttr) {
data.dexData[starterId].caughtAttr |= DexAttr.FEMALE;
}
}
}
] as const;
export const settingsMigrators = [
/**
* Migrate from "REROLL_TARGET" property to {@linkcode
* SettingKeys.Shop_Cursor_Target}.
* @param data the `settings` object
*/
function fixRerollTarget(data: Object) {
if (data.hasOwnProperty("REROLL_TARGET") && !data.hasOwnProperty(SettingKeys.Shop_Cursor_Target)) {
data[SettingKeys.Shop_Cursor_Target] = data["REROLL_TARGET"];
delete data["REROLL_TARGET"];
localStorage.setItem("settings", JSON.stringify(data));
}
}
] as const;
export const sessionMigrators = [
/**
* Converts old lapsing modifiers (battle items, lures, and Dire Hit) and
* other miscellaneous modifiers (vitamins, White Herb) to any new class
* names and/or change in reload arguments.
* @param data {@linkcode SessionSaveData}
*/
function migrateModifiers(data: SessionSaveData) {
data.modifiers.forEach((m) => {
if (m.className === "PokemonBaseStatModifier") {
m.className = "BaseStatModifier";
} else if (m.className === "PokemonResetNegativeStatStageModifier") {
m.className = "ResetNegativeStatStageModifier";
} else if (m.className === "TempBattleStatBoosterModifier") {
const maxBattles = 5;
// Dire Hit no longer a part of the TempBattleStatBoosterModifierTypeGenerator
if (m.typeId !== "DIRE_HIT") {
m.className = "TempStatStageBoosterModifier";
m.typeId = "TEMP_STAT_STAGE_BOOSTER";
// Migration from TempBattleStat to Stat
const newStat = m.typePregenArgs[0] + 1;
m.typePregenArgs[0] = newStat;
// From [ stat, battlesLeft ] to [ stat, maxBattles, battleCount ]
m.args = [ newStat, maxBattles, Math.min(m.args[1], maxBattles) ];
} else {
m.className = "TempCritBoosterModifier";
m.typePregenArgs = [];
// From [ stat, battlesLeft ] to [ maxBattles, battleCount ]
m.args = [ maxBattles, Math.min(m.args[1], maxBattles) ];
}
} else if (m.className === "DoubleBattleChanceBoosterModifier" && m.args.length === 1) {
let maxBattles: number;
switch (m.typeId) {
case "MAX_LURE":
maxBattles = 30;
break;
case "SUPER_LURE":
maxBattles = 15;
break;
default:
maxBattles = 10;
break;
}
// From [ battlesLeft ] to [ maxBattles, battleCount ]
m.args = [ maxBattles, Math.min(m.args[0], maxBattles) ];
}
});
data.enemyModifiers.forEach((m) => {
if (m.className === "PokemonBaseStatModifier") {
m.className = "BaseStatModifier";
} else if (m.className === "PokemonResetNegativeStatStageModifier") {
m.className = "ResetNegativeStatStageModifier";
}
});
}
] as const;

View File

@ -0,0 +1,32 @@
import { SessionSaveData } from "../../game-data";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
export const systemMigrators = [] as const;
export const settingsMigrators = [] as const;
export const sessionMigrators = [
/**
* Converts old Pokemon natureOverride and mysteryEncounterData
* to use the new conjoined {@linkcode Pokemon.customPokemonData} structure instead.
* @param data {@linkcode SessionSaveData}
*/
function migrateCustomPokemonDataAndNatureOverrides(data: SessionSaveData) {
// Fix Pokemon nature overrides and custom data migration
data.party.forEach(pokemon => {
if (pokemon["mysteryEncounterPokemonData"]) {
pokemon.customPokemonData = new CustomPokemonData(pokemon["mysteryEncounterPokemonData"]);
pokemon["mysteryEncounterPokemonData"] = null;
}
if (pokemon["fusionMysteryEncounterPokemonData"]) {
pokemon.fusionCustomPokemonData = new CustomPokemonData(pokemon["fusionMysteryEncounterPokemonData"]);
pokemon["fusionMysteryEncounterPokemonData"] = null;
}
pokemon.customPokemonData = pokemon.customPokemonData ?? new CustomPokemonData();
if (pokemon["natureOverride"] && pokemon["natureOverride"] >= 0) {
pokemon.customPokemonData.nature = pokemon["natureOverride"];
pokemon["natureOverride"] = -1;
}
});
}
] as const;

View File

@ -0,0 +1,107 @@
import { ArenaTagSide } from "#app/data/arena-tag";
import { allMoves } from "#app/data/move";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Stat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Abilities - Infiltrator", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.TACKLE, Moves.WATER_GUN, Moves.SPORE, Moves.BABY_DOLL_EYES ])
.ability(Abilities.INFILTRATOR)
.battleType("single")
.disableCrits()
.enemySpecies(Species.SNORLAX)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH)
.startingLevel(100)
.enemyLevel(100);
});
it.each([
{ effectName: "Light Screen", tagType: ArenaTagType.LIGHT_SCREEN, move: Moves.WATER_GUN },
{ effectName: "Reflect", tagType: ArenaTagType.REFLECT, move: Moves.TACKLE },
{ effectName: "Aurora Veil", tagType: ArenaTagType.AURORA_VEIL, move: Moves.TACKLE }
])("should bypass the target's $effectName", async ({ tagType, move }) => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
const preScreenDmg = enemy.getAttackDamage(player, allMoves[move]).damage;
game.scene.arena.addTag(tagType, 1, Moves.NONE, enemy.id, ArenaTagSide.ENEMY, true);
const postScreenDmg = enemy.getAttackDamage(player, allMoves[move]).damage;
expect(postScreenDmg).toBe(preScreenDmg);
expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR);
});
it("should bypass the target's Safeguard", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
game.scene.arena.addTag(ArenaTagType.SAFEGUARD, 1, Moves.NONE, enemy.id, ArenaTagSide.ENEMY, true);
game.move.select(Moves.SPORE);
await game.phaseInterceptor.to("BerryPhase", false);
expect(enemy.status?.effect).toBe(StatusEffect.SLEEP);
expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR);
});
// TODO: fix this interaction to pass this test
it.skip("should bypass the target's Mist", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
game.scene.arena.addTag(ArenaTagType.MIST, 1, Moves.NONE, enemy.id, ArenaTagSide.ENEMY, true);
game.move.select(Moves.BABY_DOLL_EYES);
await game.phaseInterceptor.to("MoveEndPhase");
expect(enemy.getStatStage(Stat.ATK)).toBe(-1);
expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR);
});
it("should bypass the target's Substitute", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
enemy.addTag(BattlerTagType.SUBSTITUTE, 1, Moves.NONE, enemy.id);
game.move.select(Moves.BABY_DOLL_EYES);
await game.phaseInterceptor.to("MoveEndPhase");
expect(enemy.getStatStage(Stat.ATK)).toBe(-1);
expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR);
});
});

View File

@ -111,7 +111,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) =
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (defender.scene.arena.getTagOnSide(ArenaTagType.AURORA_VEIL, side)) {
defender.scene.arena.applyTagsForSide(ArenaTagType.AURORA_VEIL, side, false, move.category, multiplierHolder);
defender.scene.arena.applyTagsForSide(ArenaTagType.AURORA_VEIL, side, false, attacker, move.category, multiplierHolder);
}
return move.power * multiplierHolder.value;

View File

@ -94,7 +94,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) =
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (defender.scene.arena.getTagOnSide(ArenaTagType.LIGHT_SCREEN, side)) {
defender.scene.arena.applyTagsForSide(ArenaTagType.LIGHT_SCREEN, side, false, move.category, multiplierHolder);
defender.scene.arena.applyTagsForSide(ArenaTagType.LIGHT_SCREEN, side, false, attacker, move.category, multiplierHolder);
}
return move.power * multiplierHolder.value;

View File

@ -0,0 +1,54 @@
import { CommandPhase } from "#app/phases/command-phase";
import { TurnInitPhase } from "#app/phases/turn-init-phase";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { StatusEffect } from "#app/data/status-effect";
describe("Moves - Nightmare", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override.battleType("single")
.enemySpecies(Species.RATTATA)
.enemyMoveset(Moves.SPLASH)
.enemyAbility(Abilities.BALL_FETCH)
.enemyStatusEffect(StatusEffect.SLEEP)
.startingLevel(5)
.moveset([ Moves.NIGHTMARE, Moves.SPLASH ]);
});
it("lowers enemy hp by 1/4 each turn while asleep", async () => {
await game.classicMode.startBattle([ Species.HYPNO ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
const enemyMaxHP = enemyPokemon.hp;
game.move.select(Moves.NIGHTMARE);
await game.phaseInterceptor.to(TurnInitPhase);
expect(enemyPokemon.hp).toBe(enemyMaxHP - Math.floor(enemyMaxHP / 4));
// take a second turn to make sure damage occurs again
await game.phaseInterceptor.to(CommandPhase);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to(TurnInitPhase);
expect(enemyPokemon.hp).toBe(enemyMaxHP - Math.floor(enemyMaxHP / 4) - Math.floor(enemyMaxHP / 4));
});
});

View File

@ -94,7 +94,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) =
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (defender.scene.arena.getTagOnSide(ArenaTagType.REFLECT, side)) {
defender.scene.arena.applyTagsForSide(ArenaTagType.REFLECT, side, false, move.category, multiplierHolder);
defender.scene.arena.applyTagsForSide(ArenaTagType.REFLECT, side, false, attacker, move.category, multiplierHolder);
}
return move.power * multiplierHolder.value;

View File

@ -9,8 +9,6 @@ import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
const TIMEOUT = 20 * 1000;
describe("Moves - Toxic Spikes", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
@ -34,7 +32,7 @@ describe("Moves - Toxic Spikes", () => {
.enemyAbility(Abilities.BALL_FETCH)
.ability(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH)
.moveset([ Moves.TOXIC_SPIKES, Moves.SPLASH, Moves.ROAR ]);
.moveset([ Moves.TOXIC_SPIKES, Moves.SPLASH, Moves.ROAR, Moves.COURT_CHANGE ]);
});
it("should not affect the opponent if they do not switch", async () => {
@ -51,7 +49,7 @@ describe("Moves - Toxic Spikes", () => {
expect(enemy.hp).toBe(enemy.getMaxHp());
expect(enemy.status?.effect).toBeUndefined();
}, TIMEOUT);
});
it("should poison the opponent if they switch into 1 layer", async () => {
await game.classicMode.runToSummon([ Species.MIGHTYENA ]);
@ -65,7 +63,7 @@ describe("Moves - Toxic Spikes", () => {
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
expect(enemy.status?.effect).toBe(StatusEffect.POISON);
}, TIMEOUT);
});
it("should badly poison the opponent if they switch into 2 layers", async () => {
await game.classicMode.runToSummon([ Species.MIGHTYENA ]);
@ -80,25 +78,30 @@ describe("Moves - Toxic Spikes", () => {
const enemy = game.scene.getEnemyField()[0];
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
expect(enemy.status?.effect).toBe(StatusEffect.TOXIC);
}, TIMEOUT);
});
it("should be removed if a grounded poison pokemon switches in", async () => {
game.override.enemySpecies(Species.GRIMER);
await game.classicMode.runToSummon([ Species.MIGHTYENA ]);
await game.classicMode.runToSummon([ Species.MUK, Species.PIDGEY ]);
const muk = game.scene.getPlayerPokemon()!;
game.move.select(Moves.TOXIC_SPIKES);
await game.phaseInterceptor.to("TurnEndPhase");
game.move.select(Moves.TOXIC_SPIKES);
await game.phaseInterceptor.to("TurnEndPhase");
game.move.select(Moves.ROAR);
await game.phaseInterceptor.to("TurnEndPhase");
const enemy = game.scene.getEnemyField()[0];
expect(enemy.hp).toBe(enemy.getMaxHp());
expect(enemy.status?.effect).toBeUndefined();
await game.toNextTurn();
// also make sure the toxic spikes are removed even if the pokemon
// that set them up is the one switching in (https://github.com/pagefaultgames/pokerogue/issues/935)
game.move.select(Moves.COURT_CHANGE);
await game.toNextTurn();
game.doSwitchPokemon(1);
await game.toNextTurn();
game.doSwitchPokemon(1);
await game.toNextTurn();
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(muk.isFullHp()).toBe(true);
expect(muk.status?.effect).toBeUndefined();
expect(game.scene.arena.tags.length).toBe(0);
}, TIMEOUT);
});
it("shouldn't create multiple layers per use in doubles", async () => {
await game.classicMode.runToSummon([ Species.MIGHTYENA, Species.POOCHYENA ]);
@ -109,7 +112,7 @@ describe("Moves - Toxic Spikes", () => {
const arenaTags = (game.scene.arena.getTagOnSide(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag);
expect(arenaTags.tagType).toBe(ArenaTagType.TOXIC_SPIKES);
expect(arenaTags.layers).toBe(1);
}, TIMEOUT);
});
it("should persist through reload", async () => {
game.override.startingWave(1);
@ -132,5 +135,5 @@ describe("Moves - Toxic Spikes", () => {
expect(sessionData.arena.tags).toEqual(recoveredData.arena.tags);
localStorage.removeItem("sessionTestData");
}, TIMEOUT);
});
});

View File

@ -476,10 +476,11 @@ describe("Bug-Type Superfan - Mystery Encounter", () => {
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
expect(modifierSelectHandler.options.length).toEqual(3);
expect(modifierSelectHandler.options.length).toEqual(4);
expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("MASTER_BALL");
expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("MAX_LURE");
expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toBe("FORM_CHANGE_ITEM");
expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("MEGA_BRACELET");
expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toBe("DYNAMAX_BAND");
expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toBe("FORM_CHANGE_ITEM");
});
it("should leave encounter without battle", async () => {

View File

@ -118,11 +118,11 @@ describe("Clowning Around - Mystery Encounter", () => {
});
expect(config.pokemonConfigs?.[1]).toEqual({
species: getPokemonSpecies(Species.BLACEPHALON),
mysteryEncounterPokemonData: expect.anything(),
customPokemonData: expect.anything(),
isBoss: true,
moveSet: [ Moves.TRICK, Moves.HYPNOSIS, Moves.SHADOW_BALL, Moves.MIND_BLOWN ]
});
expect(config.pokemonConfigs?.[1].mysteryEncounterPokemonData?.types.length).toBe(2);
expect(config.pokemonConfigs?.[1].customPokemonData?.types.length).toBe(2);
expect([
Abilities.STURDY,
Abilities.PICKUP,
@ -139,8 +139,8 @@ describe("Clowning Around - Mystery Encounter", () => {
Abilities.MAGICIAN,
Abilities.SHEER_FORCE,
Abilities.PRANKSTER
]).toContain(config.pokemonConfigs?.[1].mysteryEncounterPokemonData?.ability);
expect(ClowningAroundEncounter.misc.ability).toBe(config.pokemonConfigs?.[1].mysteryEncounterPokemonData?.ability);
]).toContain(config.pokemonConfigs?.[1].customPokemonData?.ability);
expect(ClowningAroundEncounter.misc.ability).toBe(config.pokemonConfigs?.[1].customPokemonData?.ability);
await vi.waitFor(() => expect(moveInitSpy).toHaveBeenCalled());
await vi.waitFor(() => expect(moveLoadSpy).toHaveBeenCalled());
expect(onInitResult).toBe(true);
@ -219,7 +219,7 @@ describe("Clowning Around - Mystery Encounter", () => {
await game.phaseInterceptor.to(NewBattlePhase, false);
const leadPokemon = scene.getParty()[0];
expect(leadPokemon.mysteryEncounterPokemonData?.ability).toBe(abilityToTrain);
expect(leadPokemon.customPokemonData?.ability).toBe(abilityToTrain);
});
});
@ -340,9 +340,9 @@ describe("Clowning Around - Mystery Encounter", () => {
scene.getParty()[2].moveset = [];
await runMysteryEncounterToEnd(game, 3);
const leadTypesAfter = scene.getParty()[0].mysteryEncounterPokemonData?.types;
const secondaryTypesAfter = scene.getParty()[1].mysteryEncounterPokemonData?.types;
const thirdTypesAfter = scene.getParty()[2].mysteryEncounterPokemonData?.types;
const leadTypesAfter = scene.getParty()[0].customPokemonData?.types;
const secondaryTypesAfter = scene.getParty()[1].customPokemonData?.types;
const thirdTypesAfter = scene.getParty()[2].customPokemonData?.types;
expect(leadTypesAfter.length).toBe(2);
expect(leadTypesAfter[0]).toBe(Type.WATER);

View File

@ -21,7 +21,7 @@ import { BerryModifier, PokemonBaseStatTotalModifier } from "#app/modifier/modif
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { CommandPhase } from "#app/phases/command-phase";
import { MovePhase } from "#app/phases/move-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
@ -109,7 +109,7 @@ describe("The Strong Stuff - Mystery Encounter", () => {
species: getPokemonSpecies(Species.SHUCKLE),
isBoss: true,
bossSegments: 5,
mysteryEncounterPokemonData: new MysteryEncounterPokemonData({ spriteScale: 1.25 }),
customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
nature: Nature.BOLD,
moveSet: [ Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER ],
modifierConfigs: expect.any(Array),

View File

@ -122,7 +122,7 @@ describe("Weird Dream - Mystery Encounter", () => {
for (let i = 0; i < pokemonAfter.length; i++) {
const newPokemon = pokemonAfter[i];
expect(newPokemon.getSpeciesForm().speciesId).not.toBe(pokemonPrior[i].getSpeciesForm().speciesId);
expect(newPokemon.mysteryEncounterPokemonData?.types.length).toBe(2);
expect(newPokemon.customPokemonData?.types.length).toBe(2);
}
const plus90To110 = bstDiff.filter(bst => bst > 80);

View File

@ -23,6 +23,7 @@ import KeyboardPlugin = Phaser.Input.Keyboard.KeyboardPlugin;
import GamepadPlugin = Phaser.Input.Gamepad.GamepadPlugin;
import EventEmitter = Phaser.Events.EventEmitter;
import UpdateList = Phaser.GameObjects.UpdateList;
import { version } from "../../../package.json";
Object.defineProperty(window, "localStorage", {
value: mockLocalStorage(),
@ -101,6 +102,7 @@ export default class GameWrapper {
injectMandatory() {
this.game.config = {
seed: ["test"],
gameVersion: version
};
this.scene.game = this.game;
this.game.renderer = {

View File

@ -38,6 +38,10 @@ export class ChallengeModeHelper extends GameManagerHelper {
async runToSummon(species?: Species[]) {
await this.game.runToTitle();
if (this.game.override.disableShinies) {
this.game.override.shiny(false).enemyShiny(false);
}
this.game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
this.game.scene.gameMode.challenges = this.challenges;
const starters = generateStarter(this.game.scene, species);
@ -47,7 +51,7 @@ export class ChallengeModeHelper extends GameManagerHelper {
});
await this.game.phaseInterceptor.run(EncounterPhase);
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0) {
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) {
this.game.removeEnemyHeldItems();
}
}

View File

@ -20,9 +20,13 @@ export class ClassicModeHelper extends GameManagerHelper {
* @param species - Optional array of species to summon.
* @returns A promise that resolves when the summon phase is reached.
*/
async runToSummon(species?: Species[]) {
async runToSummon(species?: Species[]): Promise<void> {
await this.game.runToTitle();
if (this.game.override.disableShinies) {
this.game.override.shiny(false).enemyShiny(false);
}
this.game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
this.game.scene.gameMode = getGameMode(GameModes.CLASSIC);
const starters = generateStarter(this.game.scene, species);
@ -32,7 +36,7 @@ export class ClassicModeHelper extends GameManagerHelper {
});
await this.game.phaseInterceptor.run(EncounterPhase);
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0) {
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) {
this.game.removeEnemyHeldItems();
}
}
@ -42,7 +46,7 @@ export class ClassicModeHelper extends GameManagerHelper {
* @param species - Optional array of species to start the battle with.
* @returns A promise that resolves when the battle is started.
*/
async startBattle(species?: Species[]) {
async startBattle(species?: Species[]): Promise<void> {
await this.runToSummon(species);
if (this.game.scene.battleStyle === BattleStyle.SWITCH) {

View File

@ -21,6 +21,10 @@ export class DailyModeHelper extends GameManagerHelper {
async runToSummon() {
await this.game.runToTitle();
if (this.game.override.disableShinies) {
this.game.override.shiny(false).enemyShiny(false);
}
this.game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
const titlePhase = new TitlePhase(this.game.scene);
titlePhase.initDailyRun();
@ -33,7 +37,7 @@ export class DailyModeHelper extends GameManagerHelper {
await this.game.phaseInterceptor.to(EncounterPhase);
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0) {
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) {
this.game.removeEnemyHeldItems();
}
}

View File

@ -19,6 +19,11 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
* Helper to handle overrides in tests
*/
export class OverridesHelper extends GameManagerHelper {
/** If `true`, removes the starting items from enemies at the start of each test; default `true` */
public removeEnemyStartingItems: boolean = true;
/** If `true`, sets the shiny overrides to disable shinies at the start of each test; default `true` */
public disableShinies: boolean = true;
/**
* Override the starting biome
* @warning Any event listeners that are attached to [NewArenaEvent](events\battle-scene.ts) may need to be handled down the line
@ -368,23 +373,50 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override player shininess
* @param shininess Whether the player's Pokemon should be shiny.
* @param shininess - `true` or `false` to force the player's pokemon to be shiny or not shiny,
* `null` to disable the override and re-enable RNG shinies.
*/
shinyLevel(shininess: boolean): this {
shiny(shininess: boolean | null): this {
vi.spyOn(Overrides, "SHINY_OVERRIDE", "get").mockReturnValue(shininess);
this.log(`Set player Pokemon as ${shininess ? "" : "not "}shiny!`);
if (shininess === null) {
this.log("Disabled player Pokemon shiny override!");
} else {
this.log(`Set player Pokemon to be ${shininess ? "" : "not "}shiny!`);
}
return this;
}
/**
* Override player shiny variant
* @param variant The player's shiny variant.
* @param variant - The player's shiny variant.
*/
variantLevel(variant: Variant): this {
shinyVariant(variant: Variant): this {
vi.spyOn(Overrides, "VARIANT_OVERRIDE", "get").mockReturnValue(variant);
this.log(`Set player Pokemon's shiny variant to ${variant}!`);
return this;
}
/**
* Override enemy shininess
* @param shininess - `true` or `false` to force the enemy's pokemon to be shiny or not shiny,
* `null` to disable the override and re-enable RNG shinies.
* @param variant - (Optional) The enemy's shiny {@linkcode Variant}.
*/
enemyShiny(shininess: boolean | null, variant?: Variant): this {
vi.spyOn(Overrides, "OPP_SHINY_OVERRIDE", "get").mockReturnValue(shininess);
if (shininess === null) {
this.log("Disabled enemy Pokemon shiny override!");
} else {
this.log(`Set enemy Pokemon to be ${shininess ? "" : "not "}shiny!`);
}
if (variant !== undefined) {
vi.spyOn(Overrides, "OPP_VARIANT_OVERRIDE", "get").mockReturnValue(variant);
this.log(`Set enemy shiny variant to be ${variant}!`);
}
return this;
}
/**
* Override the enemy (Pokemon) to have the given amount of health segments
* @param healthSegments the number of segments to give

View File

@ -17,6 +17,9 @@ import { IntegerHolder } from "./../utils";
import Phaser from "phaser";
export const SHOP_OPTIONS_ROW_LIMIT = 7;
const SINGLE_SHOP_ROW_YOFFSET = 12;
const DOUBLE_SHOP_ROW_YOFFSET = 24;
const OPTION_BUTTON_YPOSITION = -62;
export default class ModifierSelectUiHandler extends AwaitableUiHandler {
private modifierContainer: Phaser.GameObjects.Container;
@ -68,7 +71,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
this.checkButtonWidth = context.measureText(i18next.t("modifierSelectUiHandler:checkTeam")).width;
}
this.transferButtonContainer = this.scene.add.container((this.scene.game.canvas.width - this.checkButtonWidth) / 6 - 21, -64);
this.transferButtonContainer = this.scene.add.container((this.scene.game.canvas.width - this.checkButtonWidth) / 6 - 21, OPTION_BUTTON_YPOSITION);
this.transferButtonContainer.setName("transfer-btn");
this.transferButtonContainer.setVisible(false);
ui.add(this.transferButtonContainer);
@ -78,7 +81,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
transferButtonText.setOrigin(1, 0);
this.transferButtonContainer.add(transferButtonText);
this.checkButtonContainer = this.scene.add.container((this.scene.game.canvas.width) / 6 - 1, -64);
this.checkButtonContainer = this.scene.add.container((this.scene.game.canvas.width) / 6 - 1, OPTION_BUTTON_YPOSITION);
this.checkButtonContainer.setName("use-btn");
this.checkButtonContainer.setVisible(false);
ui.add(this.checkButtonContainer);
@ -88,7 +91,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
checkButtonText.setOrigin(1, 0);
this.checkButtonContainer.add(checkButtonText);
this.rerollButtonContainer = this.scene.add.container(16, -64);
this.rerollButtonContainer = this.scene.add.container(16, OPTION_BUTTON_YPOSITION);
this.rerollButtonContainer.setName("reroll-brn");
this.rerollButtonContainer.setVisible(false);
ui.add(this.rerollButtonContainer);
@ -104,7 +107,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
this.rerollCostText.setPositionRelative(rerollButtonText, rerollButtonText.displayWidth + 5, 1);
this.rerollButtonContainer.add(this.rerollCostText);
this.lockRarityButtonContainer = this.scene.add.container(16, -64);
this.lockRarityButtonContainer = this.scene.add.container(16, OPTION_BUTTON_YPOSITION);
this.lockRarityButtonContainer.setVisible(false);
ui.add(this.lockRarityButtonContainer);
@ -191,7 +194,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
const shopTypeOptions = !removeHealShop
? getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, baseShopCost.value)
: [];
const optionsYOffset = shopTypeOptions.length >= SHOP_OPTIONS_ROW_LIMIT ? -8 : -24;
const optionsYOffset = shopTypeOptions.length > SHOP_OPTIONS_ROW_LIMIT ? -SINGLE_SHOP_ROW_YOFFSET : -DOUBLE_SHOP_ROW_YOFFSET;
for (let m = 0; m < typeOptions.length; m++) {
const sliceWidth = (this.scene.game.canvas.width / 6) / (typeOptions.length + 2);
@ -212,7 +215,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
const col = m < SHOP_OPTIONS_ROW_LIMIT ? m : m - SHOP_OPTIONS_ROW_LIMIT;
const rowOptions = shopTypeOptions.slice(row ? SHOP_OPTIONS_ROW_LIMIT : 0, row ? undefined : SHOP_OPTIONS_ROW_LIMIT);
const sliceWidth = (this.scene.game.canvas.width / 6) / (rowOptions.length + 2);
const option = new ModifierOption(this.scene, sliceWidth * (col + 1) + (sliceWidth * 0.5), ((-this.scene.game.canvas.height / 12) - (this.scene.game.canvas.height / 32) - (40 - (28 * row - 1))), shopTypeOptions[m]);
const option = new ModifierOption(this.scene, sliceWidth * (col + 1) + (sliceWidth * 0.5), ((-this.scene.game.canvas.height / 12) - (this.scene.game.canvas.height / 32) - (42 - (28 * row - 1))), shopTypeOptions[m]);
option.setScale(0.375);
this.scene.add.existing(option);
this.modifierContainer.add(option);
@ -456,16 +459,18 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
if (this.rowCursor === 1 && options.length === 0) {
// Continue button when no shop items
this.cursorObj.setScale(1.25);
this.cursorObj.setPosition((this.scene.game.canvas.width / 18) + 23, (-this.scene.game.canvas.height / 12) - (this.shopOptionsRows.length > 1 ? 6 : 22));
this.cursorObj.setPosition((this.scene.game.canvas.width / 18) + 23, (-this.scene.game.canvas.height / 12) - (this.shopOptionsRows.length > 1 ? SINGLE_SHOP_ROW_YOFFSET - 2 : DOUBLE_SHOP_ROW_YOFFSET - 2));
ui.showText(i18next.t("modifierSelectUiHandler:continueNextWaveDescription"));
return ret;
}
const sliceWidth = (this.scene.game.canvas.width / 6) / (options.length + 2);
if (this.rowCursor < 2) {
this.cursorObj.setPosition(sliceWidth * (cursor + 1) + (sliceWidth * 0.5) - 20, (-this.scene.game.canvas.height / 12) - (this.shopOptionsRows.length > 1 ? 6 : 22));
// Cursor on free items
this.cursorObj.setPosition(sliceWidth * (cursor + 1) + (sliceWidth * 0.5) - 20, (-this.scene.game.canvas.height / 12) - (this.shopOptionsRows.length > 1 ? SINGLE_SHOP_ROW_YOFFSET - 2 : DOUBLE_SHOP_ROW_YOFFSET - 2));
} else {
this.cursorObj.setPosition(sliceWidth * (cursor + 1) + (sliceWidth * 0.5) - 16, (-this.scene.game.canvas.height / 12 - this.scene.game.canvas.height / 32) - (-16 + 28 * (this.rowCursor - (this.shopOptionsRows.length - 1))));
// Cursor on paying items
this.cursorObj.setPosition(sliceWidth * (cursor + 1) + (sliceWidth * 0.5) - 16, (-this.scene.game.canvas.height / 12 - this.scene.game.canvas.height / 32) - (-14 + 28 * (this.rowCursor - (this.shopOptionsRows.length - 1))));
}
const type = options[this.cursor].modifierTypeOption.type;
@ -475,16 +480,16 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
this.moveInfoOverlay.show(allMoves[type.moveId]);
}
} else if (cursor === 0) {
this.cursorObj.setPosition(6, this.lockRarityButtonContainer.visible ? -72 : -60);
this.cursorObj.setPosition(6, this.lockRarityButtonContainer.visible ? OPTION_BUTTON_YPOSITION - 8 : OPTION_BUTTON_YPOSITION + 4);
ui.showText(i18next.t("modifierSelectUiHandler:rerollDesc"));
} else if (cursor === 1) {
this.cursorObj.setPosition((this.scene.game.canvas.width - this.transferButtonWidth - this.checkButtonWidth) / 6 - 30, -60);
this.cursorObj.setPosition((this.scene.game.canvas.width - this.transferButtonWidth - this.checkButtonWidth) / 6 - 30, OPTION_BUTTON_YPOSITION + 4);
ui.showText(i18next.t("modifierSelectUiHandler:transferDesc"));
} else if (cursor === 2) {
this.cursorObj.setPosition((this.scene.game.canvas.width - this.checkButtonWidth) / 6 - 10, -60);
this.cursorObj.setPosition((this.scene.game.canvas.width - this.checkButtonWidth) / 6 - 10, OPTION_BUTTON_YPOSITION + 4);
ui.showText(i18next.t("modifierSelectUiHandler:checkTeamDesc"));
} else {
this.cursorObj.setPosition(6, -60);
this.cursorObj.setPosition(6, OPTION_BUTTON_YPOSITION + 4);
ui.showText(i18next.t("modifierSelectUiHandler:lockRaritiesDesc"));
}

View File

@ -1,47 +0,0 @@
import BattleScene from "../battle-scene";
import { ModalConfig, ModalUiHandler } from "./modal-ui-handler";
import { addTextObject, TextStyle } from "./text";
import { Mode } from "./ui";
export default class OutdatedModalUiHandler extends ModalUiHandler {
constructor(scene: BattleScene, mode: Mode | null = null) {
super(scene, mode);
}
getModalTitle(): string {
return "";
}
getWidth(): number {
return 160;
}
getHeight(): number {
return 64;
}
getMargin(): [number, number, number, number] {
return [ 0, 0, 48, 0 ];
}
getButtonLabels(): string[] {
return [ ];
}
setup(): void {
super.setup();
const label = addTextObject(this.scene, this.getWidth() / 2, this.getHeight() / 2, "Your client is currently outdated.\nPlease reload to update the game.\n\nIf this error persists, please clear your browser cache.", TextStyle.WINDOW, { fontSize: "48px", align: "center" });
label.setOrigin(0.5, 0.5);
this.modalContainer.add(label);
}
show(args: any[]): boolean {
const config: ModalConfig = {
buttonActions: []
};
return super.show([ config ]);
}
}

View File

@ -34,7 +34,6 @@ import SaveSlotSelectUiHandler from "./save-slot-select-ui-handler";
import TitleUiHandler from "./title-ui-handler";
import SavingIconHandler from "./saving-icon-handler";
import UnavailableModalUiHandler from "./unavailable-modal-ui-handler";
import OutdatedModalUiHandler from "./outdated-modal-ui-handler";
import SessionReloadModalUiHandler from "./session-reload-modal-ui-handler";
import { Button } from "#enums/buttons";
import i18next from "i18next";
@ -90,7 +89,6 @@ export enum Mode {
LOADING,
SESSION_RELOAD,
UNAVAILABLE,
OUTDATED,
CHALLENGE_SELECT,
RENAME_POKEMON,
RUN_HISTORY,
@ -134,7 +132,6 @@ const noTransitionModes = [
Mode.LOADING,
Mode.SESSION_RELOAD,
Mode.UNAVAILABLE,
Mode.OUTDATED,
Mode.RENAME_POKEMON,
Mode.TEST_DIALOGUE,
Mode.AUTO_COMPLETE,
@ -200,7 +197,6 @@ export default class UI extends Phaser.GameObjects.Container {
new LoadingModalUiHandler(scene),
new SessionReloadModalUiHandler(scene),
new UnavailableModalUiHandler(scene),
new OutdatedModalUiHandler(scene),
new GameChallengesUiHandler(scene),
new RenameFormUiHandler(scene),
new RunHistoryUiHandler(scene),