Merge branch 'beta' into rogue-ball-balance-fix

This commit is contained in:
Opaque02 2024-10-25 09:40:52 +10:00
commit 1c019d88cd
183 changed files with 11183 additions and 8269 deletions

View File

@ -1,5 +1,5 @@
import tseslint from '@typescript-eslint/eslint-plugin'; 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 parser from '@typescript-eslint/parser';
import importX from 'eslint-plugin-import-x'; import importX from 'eslint-plugin-import-x';
@ -16,15 +16,15 @@ export default [
'@typescript-eslint': tseslint '@typescript-eslint': tseslint
}, },
rules: { rules: {
"eqeqeq": ["error", "always"], // Enforces the use of === and !== instead of == and != "eqeqeq": ["error", "always"], // Enforces the use of `===` and `!==` instead of `==` and `!=`
"indent": ["error", 2], // Enforces a 2-space indentation "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 "quotes": ["error", "double"], // Enforces the use of double quotes for strings
"no-var": "error", // Disallows the use of var, enforcing let or const instead "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 "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) "no-undef": "off", // Disables the rule that disallows the use of undeclared variables (TypeScript handles this)
"@typescript-eslint/no-unused-vars": [ "error", { "@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. "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 "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 "@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 "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 "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 "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 "no-trailing-spaces": ["error", { // Disallows trailing whitespace at the end of lines
"skipBlankLines": false, // Enforces the rule even on blank lines "skipBlankLines": false, // Enforces the rule even on blank lines
"ignoreComments": false // Enforces the rule on lines containing comments "ignoreComments": false // Enforces the rule on lines containing comments
}], }],
"space-before-blocks": ["error", "always"], // Enforces a space before blocks "space-before-blocks": ["error", "always"], // Enforces a space before blocks
"keyword-spacing": ["error", { "before": true, "after": true }], // Enforces spacing before and after keywords "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 "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 "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 "object-curly-spacing": ["error", "always", { "arraysInObjects": false, "objectsInObjects": false }], // Enforces consistent spacing inside braces of object literals, destructuring assignments, and import/export specifiers

Binary file not shown.

File diff suppressed because it is too large Load Diff

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 B

View File

@ -0,0 +1,41 @@
{
"textures": [
{
"image": "future_self_f.png",
"format": "RGBA8888",
"size": {
"w": 29,
"h": 69
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 69,
"h": 69
},
"spriteSourceSize": {
"x": 21,
"y": 0,
"w": 29,
"h": 69
},
"frame": {
"x": 0,
"y": 0,
"w": 29,
"h": 69
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:4eb16332c2e77886e4e621b62269f05e:26f1bc53c853efdbe228d67604b95b54:d25525a5db42bd57d2afe4b6e3081ee1$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

View File

@ -0,0 +1,41 @@
{
"textures": [
{
"image": "future_self_m.png",
"format": "RGBA8888",
"size": {
"w": 36,
"h": 73
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 73,
"h": 73
},
"spriteSourceSize": {
"x": 18,
"y": 0,
"w": 36,
"h": 73
},
"frame": {
"x": 0,
"y": 0,
"w": 36,
"h": 73
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:0d8d68d06ae75bc93d72b54183a9df02:d3d509801da9ff5c0bd4793a05ece391:83c4b8c2ed25ea7d9795bec5c40c8602$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

@ -1 +1 @@
Subproject commit fc4a1effd5170def3c8314208a52cd0d8e6913ef Subproject commit 87615556d8a2bd7eef7abac818f84423a8a13b03

View File

@ -86,7 +86,7 @@ import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-ph
import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase";
import { ShopCursorTarget } from "#app/enums/shop-cursor-target"; import { ShopCursorTarget } from "#app/enums/shop-cursor-target";
import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; 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 { MysteryEncounterSaveData } from "#app/data/mystery-encounters/mystery-encounter-save-data";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
@ -1191,10 +1191,7 @@ export default class BattleScene extends SceneBase {
if (trainerConfigs[trainerType].doubleOnly) { if (trainerConfigs[trainerType].doubleOnly) {
doubleTrainer = true; doubleTrainer = true;
} else if (trainerConfigs[trainerType].hasDouble) { } else if (trainerConfigs[trainerType].hasDouble) {
const doubleChance = new Utils.IntegerHolder(newWaveIndex % 10 === 0 ? 32 : 8); doubleTrainer = !Utils.randSeedInt(this.getDoubleBattleChance(newWaveIndex, playerField));
this.applyModifiers(DoubleBattleChanceBoosterModifier, true, doubleChance);
playerField.forEach(p => applyAbAttrs(DoubleBattleChanceAbAttr, p, null, false, doubleChance));
doubleTrainer = !Utils.randSeedInt(doubleChance.value);
// Add a check that special trainers can't be double except for tate and liza - they should use the normal double chance // 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)) { if (trainerConfigs[trainerType].trainerTypeDouble && ![ TrainerType.TATE, TrainerType.LIZA ].includes(trainerType)) {
doubleTrainer = false; doubleTrainer = false;
@ -1207,12 +1204,10 @@ export default class BattleScene extends SceneBase {
// Check for mystery encounter // Check for mystery encounter
// Can only occur in place of a standard (non-boss) wild battle, waves 10-180 // 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; newBattleType = BattleType.MYSTERY_ENCOUNTER;
// Reset base spawn weight // Reset to base spawn weight
this.mysteryEncounterSaveData.encounterSpawnChance = BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT; this.mysteryEncounterSaveData.encounterSpawnChance = BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT;
} else if (newBattleType === BattleType.WILD) {
this.mysteryEncounterSaveData.encounterSpawnChance += WEIGHT_INCREMENT_ON_SPAWN_MISS;
} }
} }
@ -1392,7 +1387,7 @@ export default class BattleScene extends SceneBase {
case Species.ZYGARDE: case Species.ZYGARDE:
return Utils.randSeedInt(4); return Utils.randSeedInt(4);
case Species.MINIOR: case Species.MINIOR:
return Utils.randSeedInt(6); return Utils.randSeedInt(7);
case Species.ALCREMIE: case Species.ALCREMIE:
return Utils.randSeedInt(9); return Utils.randSeedInt(9);
case Species.MEOWSTIC: case Species.MEOWSTIC:
@ -2364,6 +2359,19 @@ export default class BattleScene extends SceneBase {
return false; 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() * 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 * @param phase {@linkcode Phase} the phase to be added
@ -3125,18 +3133,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}. * Determines whether a wave should randomly generate a {@linkcode MysteryEncounter}.
* Currently, the only modes that MEs are allowed in are Classic and Challenge. * 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 * Additionally, MEs cannot spawn outside of waves 10-180 in those modes
*
* @param newBattleType * @param newBattleType
* @param waveIndex * @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(); 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 // 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 sessionEncounterRate = this.mysteryEncounterSaveData.encounterSpawnChance;
const encounteredEvents = this.mysteryEncounterSaveData.encounteredEvents; const encounteredEvents = this.mysteryEncounterSaveData.encounteredEvents;

View File

@ -7,7 +7,7 @@ import { Weather, WeatherType } from "./weather";
import { BattlerTag, GroundedTag } from "./battler-tags"; import { BattlerTag, GroundedTag } from "./battler-tags";
import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect"; import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
import { Gender } from "./gender"; import { Gender } from "./gender";
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move";
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier"; import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier";
import { TerrainType } from "./terrain"; import { TerrainType } from "./terrain";
@ -1139,7 +1139,9 @@ export class MoveEffectChanceMultiplierAbAttr extends AbAttr {
apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
// Disable showAbility during getTargetBenefitScore // Disable showAbility during getTargetBenefitScore
this.showAbility = args[4]; this.showAbility = args[4];
if ((args[0] as Utils.NumberHolder).value <= 0 || (args[1] as Move).id === Moves.ORDER_UP) {
const exceptMoves = [ Moves.ORDER_UP, Moves.ELECTRO_SHOT ];
if ((args[0] as Utils.NumberHolder).value <= 0 || exceptMoves.includes((args[1] as Move).id)) {
return false; return false;
} }
@ -1329,7 +1331,6 @@ export class AddSecondStrikeAbAttr extends PreAttackAbAttr {
*/ */
const exceptAttrs: Constructor<MoveAttr>[] = [ const exceptAttrs: Constructor<MoveAttr>[] = [
MultiHitAttr, MultiHitAttr,
ChargeAttr,
SacrificialAttr, SacrificialAttr,
SacrificialAttrOnHit SacrificialAttrOnHit
]; ];
@ -1345,6 +1346,7 @@ export class AddSecondStrikeAbAttr extends PreAttackAbAttr {
/** Also check if this move is an Attack move and if it's only targeting one Pokemon */ /** Also check if this move is an Attack move and if it's only targeting one Pokemon */
return numTargets === 1 return numTargets === 1
&& !move.isChargingMove()
&& !exceptAttrs.some(attr => move.hasAttr(attr)) && !exceptAttrs.some(attr => move.hasAttr(attr))
&& !exceptMoves.some(id => move.id === id) && !exceptMoves.some(id => move.id === id)
&& move.category !== MoveCategory.STATUS; && move.category !== MoveCategory.STATUS;
@ -2420,11 +2422,12 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
super(true); super(true);
} }
applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { async applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): Promise<boolean> {
const targets = pokemon.getOpponents(); const targets = pokemon.getOpponents();
if (simulated || !targets.length) { if (simulated || !targets.length) {
return simulated; return simulated;
} }
const promises: Promise<void>[] = [];
let target: Pokemon; let target: Pokemon;
if (targets.length > 1) { if (targets.length > 1) {
@ -2433,7 +2436,7 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
target = targets[0]; target = targets[0];
} }
target = target!; // compiler doesn't know its guranteed to be defined target = target!;
pokemon.summonData.speciesForm = target.getSpeciesForm(); pokemon.summonData.speciesForm = target.getSpeciesForm();
pokemon.summonData.fusionSpeciesForm = target.getFusionSpeciesForm(); pokemon.summonData.fusionSpeciesForm = target.getFusionSpeciesForm();
pokemon.summonData.ability = target.getAbility().id; pokemon.summonData.ability = target.getAbility().id;
@ -2450,18 +2453,23 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
pokemon.setStatStage(s, target.getStatStage(s)); pokemon.setStatStage(s, target.getStatStage(s));
} }
pokemon.summonData.moveset = target.getMoveset().map(m => new PokemonMove(m?.moveId ?? Moves.NONE, m?.ppUsed, m?.ppUp)); pokemon.summonData.moveset = target.getMoveset().map(m => {
const pp = m?.getMove().pp ?? 0;
// if PP value is less than 5, do nothing. If greater, we need to reduce the value to 5 using a negative ppUp value.
const ppUp = pp <= 5 ? 0 : (5 - pp) / Math.max(Math.floor(pp / 5), 1);
return new PokemonMove(m?.moveId ?? Moves.NONE, 0, ppUp);
});
pokemon.summonData.types = target.getTypes(); pokemon.summonData.types = target.getTypes();
promises.push(pokemon.updateInfo());
pokemon.scene.queueMessage(i18next.t("abilityTriggers:postSummonTransform", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), targetName: target!.name, }));
pokemon.scene.playSound("battle_anims/PRSFX- Transform"); pokemon.scene.playSound("battle_anims/PRSFX- Transform");
promises.push(pokemon.loadAssets(false).then(() => {
pokemon.loadAssets(false).then(() => {
pokemon.playAnim(); pokemon.playAnim();
pokemon.updateInfo(); pokemon.updateInfo();
}); }));
pokemon.scene.queueMessage(i18next.t("abilityTriggers:postSummonTransform", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), targetName: target.name, })); await Promise.all(promises);
return true; return true;
} }
@ -4200,6 +4208,11 @@ export class RedirectTypeMoveAbAttr extends RedirectMoveAbAttr {
export class BlockRedirectAbAttr extends AbAttr { } export class BlockRedirectAbAttr extends AbAttr { }
/**
* Used by Early Bird, makes the pokemon wake up faster
* @param statusEffect - The {@linkcode StatusEffect} to check for
* @see {@linkcode apply}
*/
export class ReduceStatusEffectDurationAbAttr extends AbAttr { export class ReduceStatusEffectDurationAbAttr extends AbAttr {
private statusEffect: StatusEffect; private statusEffect: StatusEffect;
@ -4209,9 +4222,19 @@ export class ReduceStatusEffectDurationAbAttr extends AbAttr {
this.statusEffect = statusEffect; this.statusEffect = statusEffect;
} }
apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { /**
* Reduces the number of sleep turns remaining by an extra 1 when applied
* @param args - The args passed to the `AbAttr`:
* - `[0]` - The {@linkcode StatusEffect} of the Pokemon
* - `[1]` - The number of turns remaining until the status is healed
* @returns `true` if the ability was applied
*/
apply(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _cancelled: Utils.BooleanHolder, args: any[]): boolean {
if (!(args[1] instanceof Utils.NumberHolder)) {
return false;
}
if (args[0] === this.statusEffect) { if (args[0] === this.statusEffect) {
(args[1] as Utils.IntegerHolder).value = Utils.toDmgValue((args[1] as Utils.IntegerHolder).value / 2); args[1].value -= 1;
return true; return true;
} }
@ -4342,6 +4365,30 @@ export class AlwaysHitAbAttr extends AbAttr { }
/** Attribute for abilities that allow moves that make contact to ignore protection (i.e. Unseen Fist) */ /** Attribute for abilities that allow moves that make contact to ignore protection (i.e. Unseen Fist) */
export class IgnoreProtectOnContactAbAttr extends AbAttr { } 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 { export class UncopiableAbilityAbAttr extends AbAttr {
constructor() { constructor() {
super(false); super(false);
@ -5321,7 +5368,8 @@ export function initAbilities() {
.attr(PostSummonTransformAbAttr) .attr(PostSummonTransformAbAttr)
.attr(UncopiableAbilityAbAttr), .attr(UncopiableAbilityAbAttr),
new Ability(Abilities.INFILTRATOR, 5) new Ability(Abilities.INFILTRATOR, 5)
.unimplemented(), .attr(InfiltratorAbAttr)
.partial(), // does not bypass Mist
new Ability(Abilities.MUMMY, 5) new Ability(Abilities.MUMMY, 5)
.attr(PostDefendAbilityGiveAbAttr, Abilities.MUMMY) .attr(PostDefendAbilityGiveAbAttr, Abilities.MUMMY)
.bypassFaint(), .bypassFaint(),

View File

@ -7,7 +7,7 @@ import { getPokemonNameWithAffix } from "#app/messages";
import Pokemon, { HitResult, PokemonMove } from "#app/field/pokemon"; import Pokemon, { HitResult, PokemonMove } from "#app/field/pokemon";
import { StatusEffect } from "#app/data/status-effect"; import { StatusEffect } from "#app/data/status-effect";
import { BattlerIndex } from "#app/battle"; 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 { Stat } from "#enums/stat";
import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims"; import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims";
import i18next from "i18next"; import i18next from "i18next";
@ -130,7 +130,18 @@ export class MistTag extends ArenaTag {
* to flag the stat reduction as cancelled * to flag the stat reduction as cancelled
* @returns `true` if a stat reduction was cancelled; `false` otherwise * @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; cancelled.value = true;
if (!simulated) { if (!simulated) {
@ -169,12 +180,18 @@ export class WeakenMoveScreenTag extends ArenaTag {
* *
* @param arena the {@linkcode Arena} where the move is applied. * @param arena the {@linkcode Arena} where the move is applied.
* @param simulated n/a * @param simulated n/a
* @param attacker the attacking {@linkcode Pokemon}
* @param moveCategory the attacking move's {@linkcode MoveCategory}. * @param moveCategory the attacking move's {@linkcode MoveCategory}.
* @param damageMultiplier A {@linkcode NumberHolder} containing the damage multiplier * @param damageMultiplier A {@linkcode NumberHolder} containing the damage multiplier
* @returns `true` if the attacking move was weakened; `false` otherwise. * @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)) { 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; damageMultiplier.value = arena.scene.currentBattle.double ? 2732 / 4096 : 0.5;
return true; return true;
} }
@ -626,7 +643,7 @@ export class ArenaTrapTag extends ArenaTag {
* @returns `true` if this hazard affects the given Pokemon; `false` otherwise. * @returns `true` if this hazard affects the given Pokemon; `false` otherwise.
*/ */
override apply(arena: Arena, simulated: boolean, pokemon: Pokemon): boolean { 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; return false;
} }
@ -953,6 +970,9 @@ export class GravityTag extends ArenaTag {
if (pokemon !== null) { if (pokemon !== null) {
pokemon.removeTag(BattlerTagType.FLOATING); pokemon.removeTag(BattlerTagType.FLOATING);
pokemon.removeTag(BattlerTagType.TELEKINESIS); pokemon.removeTag(BattlerTagType.TELEKINESIS);
if (pokemon.getTag(BattlerTagType.FLYING)) {
pokemon.addTag(BattlerTagType.INTERRUPTED);
}
} }
}); });
} }

View File

@ -1,7 +1,6 @@
import { Gender } from "#app/data/gender"; import { Gender } from "#app/data/gender";
import { PokeballType } from "#app/data/pokeball"; import { PokeballType } from "#app/data/pokeball";
import Pokemon from "#app/field/pokemon"; import Pokemon from "#app/field/pokemon";
import { Stat } from "#enums/stat";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
import { WeatherType } from "#app/data/weather"; import { WeatherType } from "#app/data/weather";
@ -10,7 +9,7 @@ import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { TimeOfDay } from "#enums/time-of-day"; import { TimeOfDay } from "#enums/time-of-day";
import { DamageMoneyRewardModifier, ExtraModifierModifier, MoneyMultiplierModifier } from "#app/modifier/modifier"; import { DamageMoneyRewardModifier, ExtraModifierModifier, MoneyMultiplierModifier, TempExtraModifierModifier } from "#app/modifier/modifier";
import { SpeciesFormKey } from "#enums/species-form-key"; import { SpeciesFormKey } from "#enums/species-form-key";
@ -271,9 +270,21 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesEvolution(Species.MAROWAK, 28, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) new SpeciesEvolution(Species.MAROWAK, 28, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY))
], ],
[Species.TYROGUE]: [ [Species.TYROGUE]: [
new SpeciesEvolution(Species.HITMONLEE, 20, null, new SpeciesEvolutionCondition(p => p.stats[Stat.ATK] > p.stats[Stat.DEF])), /**
new SpeciesEvolution(Species.HITMONCHAN, 20, null, new SpeciesEvolutionCondition(p => p.stats[Stat.ATK] < p.stats[Stat.DEF])), * Custom: Evolves into Hitmonlee, Hitmonchan or Hitmontop at level 20
new SpeciesEvolution(Species.HITMONTOP, 20, null, new SpeciesEvolutionCondition(p => p.stats[Stat.ATK] === p.stats[Stat.DEF])) * if it knows Low Sweep, Mach Punch, or Rapid Spin, respectively.
* If Tyrogue knows multiple of these moves, its evolution is based on
* the first qualifying move in its moveset.
*/
new SpeciesEvolution(Species.HITMONLEE, 20, null, new SpeciesEvolutionCondition(p =>
p.getMoveset(true).find(move => move && [ Moves.LOW_SWEEP, Moves.MACH_PUNCH, Moves.RAPID_SPIN ].includes(move?.moveId))?.moveId === Moves.LOW_SWEEP
)),
new SpeciesEvolution(Species.HITMONCHAN, 20, null, new SpeciesEvolutionCondition(p =>
p.getMoveset(true).find(move => move && [ Moves.LOW_SWEEP, Moves.MACH_PUNCH, Moves.RAPID_SPIN ].includes(move?.moveId))?.moveId === Moves.MACH_PUNCH
)),
new SpeciesEvolution(Species.HITMONTOP, 20, null, new SpeciesEvolutionCondition(p =>
p.getMoveset(true).find(move => move && [ Moves.LOW_SWEEP, Moves.MACH_PUNCH, Moves.RAPID_SPIN ].includes(move?.moveId))?.moveId === Moves.RAPID_SPIN
)),
], ],
[Species.KOFFING]: [ [Species.KOFFING]: [
new SpeciesEvolution(Species.GALAR_WEEZING, 35, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), new SpeciesEvolution(Species.GALAR_WEEZING, 35, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)),
@ -1652,11 +1663,11 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesFormEvolution(Species.GHOLDENGO, "chest", "", 1, null, new SpeciesEvolutionCondition(p => p.evoCounter new SpeciesFormEvolution(Species.GHOLDENGO, "chest", "", 1, null, new SpeciesEvolutionCondition(p => p.evoCounter
+ p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length + p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length
+ p.scene.findModifiers(m => m instanceof MoneyMultiplierModifier + p.scene.findModifiers(m => m instanceof MoneyMultiplierModifier
|| m instanceof ExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG), || m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG),
new SpeciesFormEvolution(Species.GHOLDENGO, "roaming", "", 1, null, new SpeciesEvolutionCondition(p => p.evoCounter new SpeciesFormEvolution(Species.GHOLDENGO, "roaming", "", 1, null, new SpeciesEvolutionCondition(p => p.evoCounter
+ p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length + p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length
+ p.scene.findModifiers(m => m instanceof MoneyMultiplierModifier + p.scene.findModifiers(m => m instanceof MoneyMultiplierModifier
|| m instanceof ExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG) || m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG)
] ]
}; };

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
//import { battleAnimRawData } from "./battle-anim-raw-data"; //import { battleAnimRawData } from "./battle-anim-raw-data";
import BattleScene from "../battle-scene"; import BattleScene from "../battle-scene";
import { AttackMove, BeakBlastHeaderAttr, ChargeAttr, DelayedAttackAttr, MoveFlags, SelfStatusMove, allMoves } from "./move"; import { AttackMove, BeakBlastHeaderAttr, DelayedAttackAttr, MoveFlags, SelfStatusMove, allMoves } from "./move";
import Pokemon from "../field/pokemon"; import Pokemon from "../field/pokemon";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { BattlerIndex } from "../battle"; import { BattlerIndex } from "../battle";
@ -476,8 +476,11 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise<void> {
} else { } else {
const loadedCheckTimer = setInterval(() => { const loadedCheckTimer = setInterval(() => {
if (moveAnims.get(move) !== null) { if (moveAnims.get(move) !== null) {
const chargeAttr = allMoves[move].getAttrs(ChargeAttr)[0] || allMoves[move].getAttrs(DelayedAttackAttr)[0]; const chargeAnimSource = (allMoves[move].isChargingMove())
if (chargeAttr && chargeAnims.get(chargeAttr.chargeAnim) === null) { ? allMoves[move]
: (allMoves[move].getAttrs(DelayedAttackAttr)[0]
?? allMoves[move].getAttrs(BeakBlastHeaderAttr)[0]);
if (chargeAnimSource && chargeAnims.get(chargeAnimSource.chargeAnim) === null) {
return; return;
} }
clearInterval(loadedCheckTimer); clearInterval(loadedCheckTimer);
@ -507,11 +510,12 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise<void> {
} else { } else {
populateMoveAnim(move, ba); populateMoveAnim(move, ba);
} }
const chargeAttr = allMoves[move].getAttrs(ChargeAttr)[0] const chargeAnimSource = (allMoves[move].isChargingMove())
|| allMoves[move].getAttrs(DelayedAttackAttr)[0] ? allMoves[move]
|| allMoves[move].getAttrs(BeakBlastHeaderAttr)[0]; : (allMoves[move].getAttrs(DelayedAttackAttr)[0]
if (chargeAttr) { ?? allMoves[move].getAttrs(BeakBlastHeaderAttr)[0]);
initMoveChargeAnim(scene, chargeAttr.chargeAnim).then(() => resolve()); if (chargeAnimSource) {
initMoveChargeAnim(scene, chargeAnimSource.chargeAnim).then(() => resolve());
} else { } else {
resolve(); resolve();
} }
@ -638,11 +642,12 @@ export function loadMoveAnimAssets(scene: BattleScene, moveIds: Moves[], startLo
return new Promise(resolve => { return new Promise(resolve => {
const moveAnimations = moveIds.map(m => moveAnims.get(m) as AnimConfig).flat(); const moveAnimations = moveIds.map(m => moveAnims.get(m) as AnimConfig).flat();
for (const moveId of moveIds) { for (const moveId of moveIds) {
const chargeAttr = allMoves[moveId].getAttrs(ChargeAttr)[0] const chargeAnimSource = (allMoves[moveId].isChargingMove())
|| allMoves[moveId].getAttrs(DelayedAttackAttr)[0] ? allMoves[moveId]
|| allMoves[moveId].getAttrs(BeakBlastHeaderAttr)[0]; : (allMoves[moveId].getAttrs(DelayedAttackAttr)[0]
if (chargeAttr) { ?? allMoves[moveId].getAttrs(BeakBlastHeaderAttr)[0]);
const moveChargeAnims = chargeAnims.get(chargeAttr.chargeAnim); if (chargeAnimSource) {
const moveChargeAnims = chargeAnims.get(chargeAnimSource.chargeAnim);
moveAnimations.push(moveChargeAnims instanceof AnimConfig ? moveChargeAnims : moveChargeAnims![0]); // TODO: is the bang correct? moveAnimations.push(moveChargeAnims instanceof AnimConfig ? moveChargeAnims : moveChargeAnims![0]); // TODO: is the bang correct?
if (Array.isArray(moveChargeAnims)) { if (Array.isArray(moveChargeAnims)) {
moveAnimations.push(moveChargeAnims[1]); moveAnimations.push(moveChargeAnims[1]);

View File

@ -1,29 +1,43 @@
import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "./battle-anims"; import BattleScene from "#app/battle-scene";
import { getPokemonNameWithAffix } from "../messages"; import {
import Pokemon, { MoveResult, HitResult } from "../field/pokemon"; allAbilities,
import { StatusEffect } from "./status-effect"; applyAbAttrs,
import * as Utils from "../utils"; BlockNonDirectDamageAbAttr,
import { ChargeAttr, MoveFlags, allMoves, MoveCategory, applyMoveAttrs, StatusCategoryOnAllyAttr, HealOnAllyAttr, ConsecutiveUseDoublePowerAttr } from "./move"; FlinchEffectAbAttr,
import { Type } from "./type"; ProtectStatAbAttr,
import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs, ProtectStatAbAttr } from "./ability"; ReverseDrainAbAttr
import { TerrainType } from "./terrain"; } from "#app/data/ability";
import { WeatherType } from "./weather"; import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "#app/data/battle-anims";
import { allAbilities } from "./ability"; import Move, {
import { SpeciesFormChangeManualTrigger } from "./pokemon-forms"; allMoves,
import { Abilities } from "#enums/abilities"; applyMoveAttrs,
import { BattlerTagType } from "#enums/battler-tag-type"; ConsecutiveUseDoublePowerAttr,
import { Moves } from "#enums/moves"; HealOnAllyAttr,
import { Species } from "#enums/species"; MoveCategory,
import i18next from "#app/plugins/i18n"; MoveFlags,
import { Stat, type BattleStat, type EffectiveStat, EFFECTIVE_STATS, getStatKey } from "#app/enums/stat"; StatusCategoryOnAllyAttr
} from "#app/data/move";
import { SpeciesFormChangeManualTrigger } from "#app/data/pokemon-forms";
import { StatusEffect } from "#app/data/status-effect";
import { TerrainType } from "#app/data/terrain";
import { Type } from "#app/data/type";
import { WeatherType } from "#app/data/weather";
import Pokemon, { HitResult, MoveResult } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { CommonAnimPhase } from "#app/phases/common-anim-phase"; import { CommonAnimPhase } from "#app/phases/common-anim-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { MovePhase } from "#app/phases/move-phase"; import { MovePhase } from "#app/phases/move-phase";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { StatStageChangePhase, StatStageChangeCallback } from "#app/phases/stat-stage-change-phase"; import { StatStageChangeCallback, StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { PokemonAnimType } from "#app/enums/pokemon-anim-type"; import i18next from "#app/plugins/i18n";
import BattleScene from "#app/battle-scene"; import { BooleanHolder, getFrameMs, NumberHolder, toDmgValue } from "#app/utils";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import { PokemonAnimType } from "#enums/pokemon-anim-type";
import { Species } from "#enums/species";
import { EFFECTIVE_STATS, getStatKey, Stat, type BattleStat, type EffectiveStat } from "#enums/stat";
export enum BattlerTagLapseType { export enum BattlerTagLapseType {
FAINT, FAINT,
@ -33,6 +47,7 @@ export enum BattlerTagLapseType {
MOVE_EFFECT, MOVE_EFFECT,
TURN_END, TURN_END,
HIT, HIT,
AFTER_HIT,
CUSTOM CUSTOM
} }
@ -405,7 +420,7 @@ export class RechargingTag extends BattlerTag {
*/ */
export class BeakBlastChargingTag extends BattlerTag { export class BeakBlastChargingTag extends BattlerTag {
constructor() { constructor() {
super(BattlerTagType.BEAK_BLAST_CHARGING, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END ], 1, Moves.BEAK_BLAST); super(BattlerTagType.BEAK_BLAST_CHARGING, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END, BattlerTagLapseType.AFTER_HIT ], 1, Moves.BEAK_BLAST);
} }
onAdd(pokemon: Pokemon): void { onAdd(pokemon: Pokemon): void {
@ -421,16 +436,13 @@ export class BeakBlastChargingTag extends BattlerTag {
* to be removed after the source makes a move (or the turn ends, whichever comes first) * to be removed after the source makes a move (or the turn ends, whichever comes first)
* @param pokemon {@linkcode Pokemon} the owner of this tag * @param pokemon {@linkcode Pokemon} the owner of this tag
* @param lapseType {@linkcode BattlerTagLapseType} the type of functionality invoked in battle * @param lapseType {@linkcode BattlerTagLapseType} the type of functionality invoked in battle
* @returns `true` if invoked with the CUSTOM lapse type; `false` otherwise * @returns `true` if invoked with the `AFTER_HIT` lapse type
*/ */
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
if (lapseType === BattlerTagLapseType.CUSTOM) { if (lapseType === BattlerTagLapseType.AFTER_HIT) {
const effectPhase = pokemon.scene.getCurrentPhase(); const phaseData = getMoveEffectPhaseData(pokemon);
if (effectPhase instanceof MoveEffectPhase) { if (phaseData?.move.hasFlag(MoveFlags.MAKES_CONTACT)) {
const attacker = effectPhase.getPokemon(); phaseData.attacker.trySetStatus(StatusEffect.BURN, true, pokemon);
if (effectPhase.move.getMove().checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) {
attacker.trySetStatus(StatusEffect.BURN, true, pokemon);
}
} }
return true; return true;
} }
@ -444,11 +456,10 @@ export class BeakBlastChargingTag extends BattlerTag {
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Shell_Trap_(move) | Shell Trap} * @see {@link https://bulbapedia.bulbagarden.net/wiki/Shell_Trap_(move) | Shell Trap}
*/ */
export class ShellTrapTag extends BattlerTag { export class ShellTrapTag extends BattlerTag {
public activated: boolean; public activated: boolean = false;
constructor() { constructor() {
super(BattlerTagType.SHELL_TRAP, BattlerTagLapseType.TURN_END, 1); super(BattlerTagType.SHELL_TRAP, [ BattlerTagLapseType.TURN_END, BattlerTagLapseType.AFTER_HIT ], 1);
this.activated = false;
} }
onAdd(pokemon: Pokemon): void { onAdd(pokemon: Pokemon): void {
@ -459,10 +470,14 @@ export class ShellTrapTag extends BattlerTag {
* "Activates" the shell trap, causing the tag owner to move next. * "Activates" the shell trap, causing the tag owner to move next.
* @param pokemon {@linkcode Pokemon} the owner of this tag * @param pokemon {@linkcode Pokemon} the owner of this tag
* @param lapseType {@linkcode BattlerTagLapseType} the type of functionality invoked in battle * @param lapseType {@linkcode BattlerTagLapseType} the type of functionality invoked in battle
* @returns `true` if invoked with the `CUSTOM` lapse type; `false` otherwise * @returns `true` if invoked with the `AFTER_HIT` lapse type
*/ */
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
if (lapseType === BattlerTagLapseType.CUSTOM) { if (lapseType === BattlerTagLapseType.AFTER_HIT) {
const phaseData = getMoveEffectPhaseData(pokemon);
// Trap should only be triggered by opponent's Physical moves
if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) {
const shellTrapPhaseIndex = pokemon.scene.phaseQueue.findIndex( const shellTrapPhaseIndex = pokemon.scene.phaseQueue.findIndex(
phase => phase instanceof MovePhase && phase.pokemon === pokemon phase => phase instanceof MovePhase && phase.pokemon === pokemon
); );
@ -470,14 +485,18 @@ export class ShellTrapTag extends BattlerTag {
phase => phase instanceof MovePhase phase => phase instanceof MovePhase
); );
// Only shift MovePhase timing if it's not already next up
if (shellTrapPhaseIndex !== -1 && shellTrapPhaseIndex !== firstMovePhaseIndex) { if (shellTrapPhaseIndex !== -1 && shellTrapPhaseIndex !== firstMovePhaseIndex) {
const shellTrapMovePhase = pokemon.scene.phaseQueue.splice(shellTrapPhaseIndex, 1)[0]; const shellTrapMovePhase = pokemon.scene.phaseQueue.splice(shellTrapPhaseIndex, 1)[0];
pokemon.scene.prependToPhase(shellTrapMovePhase, MovePhase); pokemon.scene.prependToPhase(shellTrapMovePhase, MovePhase);
} }
this.activated = true; this.activated = true;
}
return true; return true;
} }
return super.lapse(pokemon, lapseType); return super.lapse(pokemon, lapseType);
} }
} }
@ -641,7 +660,7 @@ export class ConfusedTag extends BattlerTag {
if (pokemon.randSeedInt(3) === 0) { if (pokemon.randSeedInt(3) === 0) {
const atk = pokemon.getEffectiveStat(Stat.ATK); const atk = pokemon.getEffectiveStat(Stat.ATK);
const def = pokemon.getEffectiveStat(Stat.DEF); const def = pokemon.getEffectiveStat(Stat.DEF);
const damage = Utils.toDmgValue(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * (pokemon.randSeedIntRange(85, 100) / 100)); const damage = toDmgValue(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * (pokemon.randSeedIntRange(85, 100) / 100));
pokemon.scene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself")); pokemon.scene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself"));
pokemon.damageAndUpdate(damage); pokemon.damageAndUpdate(damage);
pokemon.battleData.hitCount++; pokemon.battleData.hitCount++;
@ -812,13 +831,13 @@ export class SeedTag extends BattlerTag {
if (ret) { if (ret) {
const source = pokemon.getOpponents().find(o => o.getBattlerIndex() === this.sourceIndex); const source = pokemon.getOpponents().find(o => o.getBattlerIndex() === this.sourceIndex);
if (source) { if (source) {
const cancelled = new Utils.BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) { if (!cancelled.value) {
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, source.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.LEECH_SEED)); pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, source.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.LEECH_SEED));
const damage = pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / 8)); const damage = pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8));
const reverseDrain = pokemon.hasAbilityWithAttr(ReverseDrainAbAttr, false); const reverseDrain = pokemon.hasAbilityWithAttr(ReverseDrainAbAttr, false);
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, source.getBattlerIndex(), pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, source.getBattlerIndex(),
!reverseDrain ? damage : damage * -1, !reverseDrain ? damage : damage * -1,
@ -838,7 +857,7 @@ export class SeedTag extends BattlerTag {
export class NightmareTag extends BattlerTag { export class NightmareTag extends BattlerTag {
constructor() { constructor() {
super(BattlerTagType.NIGHTMARE, BattlerTagLapseType.AFTER_MOVE, 1, Moves.NIGHTMARE); super(BattlerTagType.NIGHTMARE, BattlerTagLapseType.TURN_END, 1, Moves.NIGHTMARE);
} }
onAdd(pokemon: Pokemon): void { onAdd(pokemon: Pokemon): void {
@ -860,11 +879,11 @@ export class NightmareTag extends BattlerTag {
pokemon.scene.queueMessage(i18next.t("battlerTags:nightmareLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); pokemon.scene.queueMessage(i18next.t("battlerTags:nightmareLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), undefined, CommonAnim.CURSE)); // TODO: Update animation type pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), undefined, CommonAnim.CURSE)); // TODO: Update animation type
const cancelled = new Utils.BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) { if (!cancelled.value) {
pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / 4)); pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4));
} }
} }
@ -929,10 +948,6 @@ export class EncoreTag extends BattlerTag {
return false; return false;
} }
if (allMoves[repeatableMove.move].hasAttr(ChargeAttr) && repeatableMove.result === MoveResult.OTHER) {
return false;
}
this.moveId = repeatableMove.move; this.moveId = repeatableMove.move;
return true; return true;
@ -1004,7 +1019,7 @@ export class IngrainTag extends TrappedTag {
new PokemonHealPhase( new PokemonHealPhase(
pokemon.scene, pokemon.scene,
pokemon.getBattlerIndex(), pokemon.getBattlerIndex(),
Utils.toDmgValue(pokemon.getMaxHp() / 16), toDmgValue(pokemon.getMaxHp() / 16),
i18next.t("battlerTags:ingrainLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), i18next.t("battlerTags:ingrainLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }),
true true
) )
@ -1067,7 +1082,7 @@ export class AquaRingTag extends BattlerTag {
new PokemonHealPhase( new PokemonHealPhase(
pokemon.scene, pokemon.scene,
pokemon.getBattlerIndex(), pokemon.getBattlerIndex(),
Utils.toDmgValue(pokemon.getMaxHp() / 16), toDmgValue(pokemon.getMaxHp() / 16),
i18next.t("battlerTags:aquaRingLapse", { i18next.t("battlerTags:aquaRingLapse", {
moveName: this.getMoveName(), moveName: this.getMoveName(),
pokemonName: getPokemonNameWithAffix(pokemon) pokemonName: getPokemonNameWithAffix(pokemon)
@ -1161,11 +1176,11 @@ export abstract class DamagingTrapTag extends TrappedTag {
); );
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), undefined, this.commonAnim)); pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), undefined, this.commonAnim));
const cancelled = new Utils.BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) { if (!cancelled.value) {
pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / 8)); pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8));
} }
} }
@ -1356,7 +1371,7 @@ export class ContactDamageProtectedTag extends ProtectedTag {
if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) { if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) {
const attacker = effectPhase.getPokemon(); const attacker = effectPhase.getPokemon();
if (!attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) { if (!attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) {
attacker.damageAndUpdate(Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER); attacker.damageAndUpdate(toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER);
} }
} }
} }
@ -1709,7 +1724,7 @@ export class SemiInvulnerableTag extends BattlerTag {
onRemove(pokemon: Pokemon): void { onRemove(pokemon: Pokemon): void {
// Wait 2 frames before setting visible for battle animations that don't immediately show the sprite invisible // Wait 2 frames before setting visible for battle animations that don't immediately show the sprite invisible
pokemon.scene.tweens.addCounter({ pokemon.scene.tweens.addCounter({
duration: Utils.getFrameMs(2), duration: getFrameMs(2),
onComplete: () => pokemon.setVisible(true) onComplete: () => pokemon.setVisible(true)
}); });
} }
@ -1860,12 +1875,12 @@ export class SaltCuredTag extends BattlerTag {
if (ret) { if (ret) {
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.SALT_CURE)); pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.SALT_CURE));
const cancelled = new Utils.BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) { if (!cancelled.value) {
const pokemonSteelOrWater = pokemon.isOfType(Type.STEEL) || pokemon.isOfType(Type.WATER); const pokemonSteelOrWater = pokemon.isOfType(Type.STEEL) || pokemon.isOfType(Type.WATER);
pokemon.damageAndUpdate(Utils.toDmgValue(pokemonSteelOrWater ? pokemon.getMaxHp() / 4 : pokemon.getMaxHp() / 8)); pokemon.damageAndUpdate(toDmgValue(pokemonSteelOrWater ? pokemon.getMaxHp() / 4 : pokemon.getMaxHp() / 8));
pokemon.scene.queueMessage( pokemon.scene.queueMessage(
i18next.t("battlerTags:saltCuredLapse", { i18next.t("battlerTags:saltCuredLapse", {
@ -1907,11 +1922,11 @@ export class CursedTag extends BattlerTag {
if (ret) { if (ret) {
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.SALT_CURE)); pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.SALT_CURE));
const cancelled = new Utils.BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) { if (!cancelled.value) {
pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / 4)); pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4));
pokemon.scene.queueMessage(i18next.t("battlerTags:cursedLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); pokemon.scene.queueMessage(i18next.t("battlerTags:cursedLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
} }
} }
@ -2173,7 +2188,7 @@ export class GulpMissileTag extends BattlerTag {
return true; return true;
} }
const cancelled = new Utils.BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, attacker, cancelled); applyAbAttrs(BlockNonDirectDamageAbAttr, attacker, cancelled);
if (!cancelled.value) { if (!cancelled.value) {
@ -2289,7 +2304,7 @@ export class HealBlockTag extends MoveRestrictionBattlerTag {
* @returns `true` if the move cannot be used because the target is an ally * @returns `true` if the move cannot be used because the target is an ally
*/ */
override isMoveTargetRestricted(move: Moves, user: Pokemon, target: Pokemon) { override isMoveTargetRestricted(move: Moves, user: Pokemon, target: Pokemon) {
const moveCategory = new Utils.IntegerHolder(allMoves[move].category); const moveCategory = new NumberHolder(allMoves[move].category);
applyMoveAttrs(StatusCategoryOnAllyAttr, user, target, allMoves[move], moveCategory); applyMoveAttrs(StatusCategoryOnAllyAttr, user, target, allMoves[move], moveCategory);
if (allMoves[move].hasAttr(HealOnAllyAttr) && moveCategory.value === MoveCategory.STATUS ) { if (allMoves[move].hasAttr(HealOnAllyAttr) && moveCategory.value === MoveCategory.STATUS ) {
return true; return true;
@ -2506,7 +2521,7 @@ export class MysteryEncounterPostSummonTag extends BattlerTag {
const ret = super.lapse(pokemon, lapseType); const ret = super.lapse(pokemon, lapseType);
if (lapseType === BattlerTagLapseType.CUSTOM) { if (lapseType === BattlerTagLapseType.CUSTOM) {
const cancelled = new Utils.BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled); applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled);
if (!cancelled.value) { if (!cancelled.value) {
if (pokemon.mysteryEncounterBattleEffects) { if (pokemon.mysteryEncounterBattleEffects) {
@ -2571,7 +2586,7 @@ export class TormentTag extends MoveRestrictionBattlerTag {
// This checks for locking / momentum moves like Rollout and Hydro Cannon + if the user is under the influence of BattlerTagType.FRENZY // This checks for locking / momentum moves like Rollout and Hydro Cannon + if the user is under the influence of BattlerTagType.FRENZY
// Because Uproar's unique behavior is not implemented, it does not check for Uproar. Torment has been marked as partial in moves.ts // Because Uproar's unique behavior is not implemented, it does not check for Uproar. Torment has been marked as partial in moves.ts
const moveObj = allMoves[lastMove.move]; const moveObj = allMoves[lastMove.move];
const isUnaffected = moveObj.hasAttr(ConsecutiveUseDoublePowerAttr) || user.getTag(BattlerTagType.FRENZY) || moveObj.hasAttr(ChargeAttr); const isUnaffected = moveObj.hasAttr(ConsecutiveUseDoublePowerAttr) || user.getTag(BattlerTagType.FRENZY);
const validLastMoveResult = (lastMove.result === MoveResult.SUCCESS) || (lastMove.result === MoveResult.MISS); const validLastMoveResult = (lastMove.result === MoveResult.SUCCESS) || (lastMove.result === MoveResult.MISS);
if (lastMove.move === move && validLastMoveResult && lastMove.move !== Moves.STRUGGLE && !isUnaffected) { if (lastMove.move === move && validLastMoveResult && lastMove.move !== Moves.STRUGGLE && !isUnaffected) {
return true; return true;
@ -2955,3 +2970,22 @@ export function loadBattlerTag(source: BattlerTag | any): BattlerTag {
tag.loadTag(source); tag.loadTag(source);
return tag; return tag;
} }
/**
* Helper function to verify that the current phase is a MoveEffectPhase and provide quick access to commonly used fields
*
* @param pokemon {@linkcode Pokemon} The Pokémon used to access the current phase
* @returns null if current phase is not MoveEffectPhase, otherwise Object containing the {@linkcode MoveEffectPhase}, and its
* corresponding {@linkcode Move} and user {@linkcode Pokemon}
*/
function getMoveEffectPhaseData(pokemon: Pokemon): {phase: MoveEffectPhase, attacker: Pokemon, move: Move} | null {
const phase = pokemon.scene.getCurrentPhase();
if (phase instanceof MoveEffectPhase) {
return {
phase : phase,
attacker : phase.getPokemon(),
move : phase.move.getMove()
};
}
return null;
}

View File

@ -1,18 +1,20 @@
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import { isNullOrUndefined } from "#app/utils"; import { isNullOrUndefined } from "#app/utils";
import { Nature } from "#enums/nature";
/** /**
* Data that can customize a Pokemon in non-standard ways from its Species * 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 spriteScale: number;
public ability: Abilities | -1; public ability: Abilities | -1;
public passive: Abilities | -1; public passive: Abilities | -1;
public nature: Nature | -1;
public types: Type[]; public types: Type[];
constructor(data?: MysteryEncounterPokemonData | Partial<MysteryEncounterPokemonData>) { constructor(data?: CustomPokemonData | Partial<CustomPokemonData>) {
if (!isNullOrUndefined(data)) { if (!isNullOrUndefined(data)) {
Object.assign(this, data); Object.assign(this, data);
} }
@ -20,6 +22,7 @@ export class MysteryEncounterPokemonData {
this.spriteScale = this.spriteScale ?? -1; this.spriteScale = this.spriteScale ?? -1;
this.ability = this.ability ?? -1; this.ability = this.ability ?? -1;
this.passive = this.passive ?? -1; this.passive = this.passive ?? -1;
this.nature = this.nature ?? -1;
this.types = this.types ?? []; this.types = this.types ?? [];
} }
} }

View File

@ -8,7 +8,7 @@ import { Constructor, NumberHolder } from "#app/utils";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { WeatherType } from "./weather"; import { WeatherType } from "./weather";
import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; 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 { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier";
import { BattlerIndex, BattleType } from "../battle"; import { BattlerIndex, BattleType } from "../battle";
import { TerrainType } from "./terrain"; import { TerrainType } from "./terrain";
@ -289,10 +289,9 @@ export default class Move implements Localizable {
} }
/** /**
* Getter function that returns if the move targets itself or an ally * Getter function that returns if the move targets the user or its ally
* @returns boolean * @returns boolean
*/ */
isAllyTarget(): boolean { isAllyTarget(): boolean {
switch (this.moveTarget) { switch (this.moveTarget) {
case MoveTarget.USER: case MoveTarget.USER:
@ -306,6 +305,10 @@ export default class Move implements Localizable {
return false; return false;
} }
isChargingMove(): this is ChargingMove {
return false;
}
/** /**
* Checks if the move is immune to certain types. * Checks if the move is immune to certain types.
* Currently looks at cases of Grass types with powder moves and Dark types with moves affected by Prankster. * Currently looks at cases of Grass types with powder moves and Dark types with moves affected by Prankster.
@ -346,7 +349,11 @@ export default class Move implements Localizable {
return false; 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.SOUND_BASED)
&& !this.hasFlag(MoveFlags.IGNORE_SUBSTITUTE); && !this.hasFlag(MoveFlags.IGNORE_SUBSTITUTE);
} }
@ -889,6 +896,85 @@ export class SelfStatusMove extends Move {
} }
} }
type SubMove = new (...args: any[]) => Move;
function ChargeMove<TBase extends SubMove>(Base: TBase) {
return class extends Base {
/** The animation to play during the move's charging phase */
public readonly chargeAnim: ChargeAnim = ChargeAnim[`${Moves[this.id]}_CHARGING`];
/** The message to show during the move's charging phase */
private _chargeText: string;
/** Move attributes that apply during the move's charging phase */
public chargeAttrs: MoveAttr[] = [];
override isChargingMove(): this is ChargingMove {
return true;
}
/**
* Sets the text to be displayed during this move's charging phase.
* References to the user Pokemon should be written as "{USER}", and
* references to the target Pokemon should be written as "{TARGET}".
* @param chargeText the text to set
* @returns this {@linkcode Move} (for chaining API purposes)
*/
chargeText(chargeText: string): this {
this._chargeText = chargeText;
return this;
}
/**
* Queues the charge text to display to the player
* @param user the {@linkcode Pokemon} using this move
* @param target the {@linkcode Pokemon} targeted by this move (optional)
*/
showChargeText(user: Pokemon, target?: Pokemon): void {
user.scene.queueMessage(this._chargeText
.replace("{USER}", getPokemonNameWithAffix(user))
.replace("{TARGET}", getPokemonNameWithAffix(target))
);
}
/**
* Gets all charge attributes of the given attribute type.
* @param attrType any attribute that extends {@linkcode MoveAttr}
* @returns Array of attributes that match `attrType`, or an empty array if
* no matches are found.
*/
getChargeAttrs<T extends MoveAttr>(attrType: Constructor<T>): T[] {
return this.chargeAttrs.filter((attr): attr is T => attr instanceof attrType);
}
/**
* Checks if this move has an attribute of the given type.
* @param attrType any attribute that extends {@linkcode MoveAttr}
* @returns `true` if a matching attribute is found; `false` otherwise
*/
hasChargeAttr<T extends MoveAttr>(attrType: Constructor<T>): boolean {
return this.chargeAttrs.some((attr) => attr instanceof attrType);
}
/**
* Adds an attribute to this move to be applied during the move's charging phase
* @param ChargeAttrType the type of {@linkcode MoveAttr} being added
* @param args the parameters to construct the given {@linkcode MoveAttr} with
* @returns this {@linkcode Move} (for chaining API purposes)
*/
chargeAttr<T extends Constructor<MoveAttr>>(ChargeAttrType: T, ...args: ConstructorParameters<T>): this {
const chargeAttr = new ChargeAttrType(...args);
this.chargeAttrs.push(chargeAttr);
return this;
}
};
}
export class ChargingAttackMove extends ChargeMove(AttackMove) {}
export class ChargingSelfStatusMove extends ChargeMove(SelfStatusMove) {}
export type ChargingMove = ChargingAttackMove | ChargingSelfStatusMove;
/** /**
* Base class defining all {@linkcode Move} Attributes * Base class defining all {@linkcode Move} Attributes
* @abstract * @abstract
@ -2046,15 +2132,15 @@ export class WaterShurikenMultiHitTypeAttr extends ChangeMultiHitTypeAttr {
export class StatusEffectAttr extends MoveEffectAttr { export class StatusEffectAttr extends MoveEffectAttr {
public effect: StatusEffect; public effect: StatusEffect;
public cureTurn: integer | null; public turnsRemaining?: number;
public overrideStatus: boolean; public overrideStatus: boolean = false;
constructor(effect: StatusEffect, selfTarget?: boolean, cureTurn?: integer, overrideStatus?: boolean) { constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) {
super(selfTarget, MoveEffectTrigger.HIT); super(selfTarget, MoveEffectTrigger.HIT);
this.effect = effect; this.effect = effect;
this.cureTurn = cureTurn!; // TODO: is this bang correct? this.turnsRemaining = turnsRemaining;
this.overrideStatus = !!overrideStatus; this.overrideStatus = overrideStatus;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
@ -2074,14 +2160,14 @@ 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) { if (move.category === MoveCategory.STATUS) {
user.scene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target) })); user.scene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target) }));
} }
return false; return false;
} }
if ((!pokemon.status || (pokemon.status.effect === this.effect && moveChance < 0)) if ((!pokemon.status || (pokemon.status.effect === this.effect && moveChance < 0))
&& pokemon.trySetStatus(this.effect, true, user, this.cureTurn)) { && pokemon.trySetStatus(this.effect, true, user, this.turnsRemaining)) {
applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect); applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect);
return true; return true;
} }
@ -2098,8 +2184,8 @@ export class StatusEffectAttr extends MoveEffectAttr {
export class MultiStatusEffectAttr extends StatusEffectAttr { export class MultiStatusEffectAttr extends StatusEffectAttr {
public effects: StatusEffect[]; public effects: StatusEffect[];
constructor(effects: StatusEffect[], selfTarget?: boolean, cureTurn?: integer, overrideStatus?: boolean) { constructor(effects: StatusEffect[], selfTarget?: boolean, turnsRemaining?: number, overrideStatus?: boolean) {
super(effects[0], selfTarget, cureTurn, overrideStatus); super(effects[0], selfTarget, turnsRemaining, overrideStatus);
this.effects = effects; this.effects = effects;
} }
@ -2570,6 +2656,63 @@ export class OneHitKOAttr extends MoveAttr {
} }
} }
/**
* Attribute that allows charge moves to resolve in 1 turn under a given condition.
* Should only be used for {@linkcode ChargingMove | ChargingMoves} as a `chargeAttr`.
* @extends MoveAttr
*/
export class InstantChargeAttr extends MoveAttr {
/** The condition in which the move with this attribute instantly charges */
protected readonly condition: UserMoveConditionFunc;
constructor(condition: UserMoveConditionFunc) {
super(true);
this.condition = condition;
}
/**
* Flags the move with this attribute as instantly charged if this attribute's condition is met.
* @param user the {@linkcode Pokemon} using the move
* @param target n/a
* @param move the {@linkcode Move} associated with this attribute
* @param args
* - `[0]` a {@linkcode Utils.BooleanHolder | BooleanHolder} for the "instant charge" flag
* @returns `true` if the instant charge condition is met; `false` otherwise.
*/
override apply(user: Pokemon, target: Pokemon | null, move: Move, args: any[]): boolean {
const instantCharge = args[0];
if (!(instantCharge instanceof Utils.BooleanHolder)) {
return false;
}
if (this.condition(user, move)) {
instantCharge.value = true;
return true;
}
return false;
}
}
/**
* Attribute that allows charge moves to resolve in 1 turn while specific {@linkcode WeatherType | Weather}
* is active. Should only be used for {@linkcode ChargingMove | ChargingMoves} as a `chargeAttr`.
* @extends InstantChargeAttr
*/
export class WeatherInstantChargeAttr extends InstantChargeAttr {
constructor(weatherTypes: WeatherType[]) {
super((user, move) => {
const currentWeather = user.scene.arena.weather;
if (Utils.isNullOrUndefined(currentWeather?.weatherType)) {
return false;
} else {
return !currentWeather?.isEffectSuppressed(user.scene)
&& weatherTypes.includes(currentWeather?.weatherType);
}
});
}
}
export class OverrideMoveEffectAttr extends MoveAttr { export class OverrideMoveEffectAttr extends MoveAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise<boolean> {
//const overridden = args[0] as Utils.BooleanHolder; //const overridden = args[0] as Utils.BooleanHolder;
@ -2578,111 +2721,6 @@ export class OverrideMoveEffectAttr extends MoveAttr {
} }
} }
export class ChargeAttr extends OverrideMoveEffectAttr {
public chargeAnim: ChargeAnim;
private chargeText: string;
private tagType: BattlerTagType | null;
private chargeEffect: boolean;
public followUpPriority: integer | null;
constructor(chargeAnim: ChargeAnim, chargeText: string, tagType?: BattlerTagType | null, chargeEffect: boolean = false) {
super();
this.chargeAnim = chargeAnim;
this.chargeText = chargeText;
this.tagType = tagType!; // TODO: is this bang correct?
this.chargeEffect = chargeEffect;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise(resolve => {
const lastMove = user.getLastXMoves().find(() => true);
if (!lastMove || lastMove.move !== move.id || (lastMove.result !== MoveResult.OTHER && lastMove.turn !== user.scene.currentBattle.turn)) {
(args[0] as Utils.BooleanHolder).value = true;
new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, false, () => {
user.scene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
if (this.tagType) {
user.addTag(this.tagType, 1, move.id, user.id);
}
if (this.chargeEffect) {
applyMoveAttrs(MoveEffectAttr, user, target, move);
}
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER });
user.getMoveQueue().push({ move: move.id, targets: [ target.getBattlerIndex() ], ignorePP: true });
user.addTag(BattlerTagType.CHARGING, 1, move.id, user.id);
resolve(true);
});
} else {
user.lapseTag(BattlerTagType.CHARGING);
resolve(false);
}
});
}
usedChargeEffect(user: Pokemon, target: Pokemon | null, move: Move): boolean {
if (!this.chargeEffect) {
return false;
}
// Account for move history being populated when this function is called
const lastMoves = user.getLastXMoves(2);
return lastMoves.length === 2 && lastMoves[1].move === move.id && lastMoves[1].result === MoveResult.OTHER;
}
}
export class SunlightChargeAttr extends ChargeAttr {
constructor(chargeAnim: ChargeAnim, chargeText: string) {
super(chargeAnim, chargeText);
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise(resolve => {
const weatherType = user.scene.arena.weather?.weatherType;
if (!user.scene.arena.weather?.isEffectSuppressed(user.scene) && (weatherType === WeatherType.SUNNY || weatherType === WeatherType.HARSH_SUN)) {
resolve(false);
} else {
super.apply(user, target, move, args).then(result => resolve(result));
}
});
}
}
export class ElectroShotChargeAttr extends ChargeAttr {
private statIncreaseApplied: boolean;
constructor() {
super(ChargeAnim.ELECTRO_SHOT_CHARGING, i18next.t("moveTriggers:absorbedElectricity", { pokemonName: "{USER}" }), null, true);
// Add a flag because ChargeAttr skills use themselves twice instead of once over one-to-two turns
this.statIncreaseApplied = false;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise(resolve => {
const weatherType = user.scene.arena.weather?.weatherType;
if (!user.scene.arena.weather?.isEffectSuppressed(user.scene) && (weatherType === WeatherType.RAIN || weatherType === WeatherType.HEAVY_RAIN)) {
// Apply the SPATK increase every call when used in the rain
const statChangeAttr = new StatStageChangeAttr([ Stat.SPATK ], 1, true);
statChangeAttr.apply(user, target, move, args);
// After the SPATK is raised, execute the move resolution e.g. deal damage
resolve(false);
} else {
if (!this.statIncreaseApplied) {
// Apply the SPATK increase only if it hasn't been applied before e.g. on the first turn charge up animation
const statChangeAttr = new StatStageChangeAttr([ Stat.SPATK ], 1, true);
statChangeAttr.apply(user, target, move, args);
// Set the flag to true so that on the following turn it doesn't raise SPATK a second time
this.statIncreaseApplied = true;
}
super.apply(user, target, move, args).then(result => {
if (!result) {
// On the second turn, reset the statIncreaseApplied flag without applying the SPATK increase
this.statIncreaseApplied = false;
}
resolve(result);
});
}
});
}
}
export class DelayedAttackAttr extends OverrideMoveEffectAttr { export class DelayedAttackAttr extends OverrideMoveEffectAttr {
public tagType: ArenaTagType; public tagType: ArenaTagType;
public chargeAnim: ChargeAnim; public chargeAnim: ChargeAnim;
@ -4874,6 +4912,37 @@ export const frenzyMissFunc: UserMoveConditionFunc = (user: Pokemon, move: Move)
return true; return true;
}; };
/**
* Attribute that grants {@link https://bulbapedia.bulbagarden.net/wiki/Semi-invulnerable_turn | semi-invulnerability} to the user during
* the associated move's charging phase. Should only be used for {@linkcode ChargingMove | ChargingMoves} as a `chargeAttr`.
* @extends MoveEffectAttr
*/
export class SemiInvulnerableAttr extends MoveEffectAttr {
/** The type of {@linkcode SemiInvulnerableTag} to grant to the user */
public tagType: BattlerTagType;
constructor(tagType: BattlerTagType) {
super(true);
this.tagType = tagType;
}
/**
* Grants a {@linkcode SemiInvulnerableTag} to the associated move's user.
* @param user the {@linkcode Pokemon} using the move
* @param target n/a
* @param move the {@linkcode Move} being used
* @param args n/a
* @returns `true` if semi-invulnerability was successfully granted; `false` otherwise.
*/
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
if (!super.apply(user, target, move, args)) {
return false;
}
return user.addTag(this.tagType, 1, move.id, user.id);
}
}
export class AddBattlerTagAttr extends MoveEffectAttr { export class AddBattlerTagAttr extends MoveEffectAttr {
public tagType: BattlerTagType; public tagType: BattlerTagType;
public turnCountMin: integer; public turnCountMin: integer;
@ -5161,7 +5230,7 @@ export class ConfuseAttr extends AddBattlerTagAttr {
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { 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) { if (move.category === MoveCategory.STATUS) {
user.scene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target) })); user.scene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target) }));
} }
@ -6134,7 +6203,7 @@ const lastMoveCopiableCondition: MoveConditionFunc = (user, target, move) => {
return false; return false;
} }
if (allMoves[copiableMove].hasAttr(ChargeAttr)) { if (allMoves[copiableMove].isChargingMove()) {
return false; return false;
} }
@ -6282,7 +6351,7 @@ const targetMoveCopiableCondition: MoveConditionFunc = (user, target, move) => {
return false; return false;
} }
if (allMoves[copiableMove.move].hasAttr(ChargeAttr) && copiableMove.result === MoveResult.OTHER) { if (allMoves[copiableMove.move].isChargingMove() && copiableMove.result === MoveResult.OTHER) {
return false; return false;
} }
@ -6574,12 +6643,12 @@ export class SuppressAbilitiesIfActedAttr extends MoveEffectAttr {
} }
export class TransformAttr extends MoveEffectAttr { export class TransformAttr extends MoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { async apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise(resolve => {
if (!super.apply(user, target, move, args)) { if (!super.apply(user, target, move, args)) {
return resolve(false); return false;
} }
const promises: Promise<void>[] = [];
user.summonData.speciesForm = target.getSpeciesForm(); user.summonData.speciesForm = target.getSpeciesForm();
user.summonData.fusionSpeciesForm = target.getFusionSpeciesForm(); user.summonData.fusionSpeciesForm = target.getFusionSpeciesForm();
user.summonData.ability = target.getAbility().id; user.summonData.ability = target.getAbility().id;
@ -6599,17 +6668,24 @@ export class TransformAttr extends MoveEffectAttr {
user.setStatStage(s, target.getStatStage(s)); user.setStatStage(s, target.getStatStage(s));
} }
user.summonData.moveset = target.getMoveset().map(m => new PokemonMove(m?.moveId!, m?.ppUsed, m?.ppUp)); // TODO: is this bang correct? user.summonData.moveset = target.getMoveset().map(m => {
const pp = m?.getMove().pp ?? 0;
// if PP value is less than 5, do nothing. If greater, we need to reduce the value to 5 using a negative ppUp value.
const ppUp = pp <= 5 ? 0 : (5 - pp) / Math.max(Math.floor(pp / 5), 1);
return new PokemonMove(m?.moveId!, 0, ppUp);
});
user.summonData.types = target.getTypes(); user.summonData.types = target.getTypes();
promises.push(user.updateInfo());
user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) })); user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) }));
user.loadAssets(false).then(() => { promises.push(user.loadAssets(false).then(() => {
user.playAnim(); user.playAnim();
user.updateInfo(); user.updateInfo();
resolve(true); }));
});
}); await Promise.all(promises);
return true;
} }
} }
@ -6981,6 +7057,20 @@ function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null
}); });
} }
function applyMoveChargeAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: ChargingMove, args: any[]): Promise<void> {
return new Promise(resolve => {
const chargeAttrPromises: Promise<boolean>[] = [];
const chargeMoveAttrs = move.chargeAttrs.filter(a => attrFilter(a));
for (const attr of chargeMoveAttrs) {
const result = attr.apply(user, target, move, args);
if (result instanceof Promise) {
chargeAttrPromises.push(result);
}
}
Promise.allSettled(chargeAttrPromises).then(() => resolve());
});
}
export function applyMoveAttrs(attrType: Constructor<MoveAttr>, user: Pokemon | null, target: Pokemon | null, move: Move, ...args: any[]): Promise<void> { export function applyMoveAttrs(attrType: Constructor<MoveAttr>, user: Pokemon | null, target: Pokemon | null, move: Move, ...args: any[]): Promise<void> {
return applyMoveAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args); return applyMoveAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args);
} }
@ -6989,6 +7079,10 @@ export function applyFilteredMoveAttrs(attrFilter: MoveAttrFilter, user: Pokemon
return applyMoveAttrsInternal(attrFilter, user, target, move, args); return applyMoveAttrsInternal(attrFilter, user, target, move, args);
} }
export function applyMoveChargeAttrs(attrType: Constructor<MoveAttr>, user: Pokemon | null, target: Pokemon | null, move: ChargingMove, ...args: any[]): Promise<void> {
return applyMoveChargeAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args);
}
export class MoveCondition { export class MoveCondition {
protected func: MoveConditionFunc; protected func: MoveConditionFunc;
@ -7233,8 +7327,8 @@ export function initMoves() {
new AttackMove(Moves.GUILLOTINE, Type.NORMAL, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1) new AttackMove(Moves.GUILLOTINE, Type.NORMAL, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1)
.attr(OneHitKOAttr) .attr(OneHitKOAttr)
.attr(OneHitKOAccuracyAttr), .attr(OneHitKOAccuracyAttr),
new AttackMove(Moves.RAZOR_WIND, Type.NORMAL, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 1) new ChargingAttackMove(Moves.RAZOR_WIND, Type.NORMAL, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 1)
.attr(ChargeAttr, ChargeAnim.RAZOR_WIND_CHARGING, i18next.t("moveTriggers:whippedUpAWhirlwind", { pokemonName: "{USER}" })) .chargeText(i18next.t("moveTriggers:whippedUpAWhirlwind", { pokemonName: "{USER}" }))
.attr(HighCritAttr) .attr(HighCritAttr)
.windMove() .windMove()
.ignoresVirtual() .ignoresVirtual()
@ -7254,8 +7348,9 @@ export function initMoves() {
.hidesTarget() .hidesTarget()
.windMove() .windMove()
.partial(), // Should force random switches .partial(), // Should force random switches
new AttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1) new ChargingAttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1)
.attr(ChargeAttr, ChargeAnim.FLY_CHARGING, i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" }), BattlerTagType.FLYING) .chargeText(i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
.condition(failOnGravityCondition) .condition(failOnGravityCondition)
.ignoresVirtual(), .ignoresVirtual(),
new AttackMove(Moves.BIND, Type.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1) new AttackMove(Moves.BIND, Type.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1)
@ -7404,8 +7499,9 @@ export function initMoves() {
.makesContact(false) .makesContact(false)
.slicingMove() .slicingMove()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.SOLAR_BEAM, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1) new ChargingAttackMove(Moves.SOLAR_BEAM, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1)
.attr(SunlightChargeAttr, ChargeAnim.SOLAR_BEAM_CHARGING, i18next.t("moveTriggers:tookInSunlight", { pokemonName: "{USER}" })) .chargeText(i18next.t("moveTriggers:tookInSunlight", { pokemonName: "{USER}" }))
.chargeAttr(WeatherInstantChargeAttr, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ])
.attr(AntiSunlightPowerDecreaseAttr) .attr(AntiSunlightPowerDecreaseAttr)
.ignoresVirtual(), .ignoresVirtual(),
new StatusMove(Moves.POISON_POWDER, Type.POISON, 75, 35, -1, 0, 1) new StatusMove(Moves.POISON_POWDER, Type.POISON, 75, 35, -1, 0, 1)
@ -7454,8 +7550,9 @@ export function initMoves() {
.attr(OneHitKOAccuracyAttr) .attr(OneHitKOAccuracyAttr)
.attr(HitsTagAttr, BattlerTagType.UNDERGROUND) .attr(HitsTagAttr, BattlerTagType.UNDERGROUND)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1) new ChargingAttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1)
.attr(ChargeAttr, ChargeAnim.DIG_CHARGING, i18next.t("moveTriggers:dugAHole", { pokemonName: "{USER}" }), BattlerTagType.UNDERGROUND) .chargeText(i18next.t("moveTriggers:dugAHole", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERGROUND)
.ignoresVirtual(), .ignoresVirtual(),
new StatusMove(Moves.TOXIC, Type.POISON, 90, 10, -1, 0, 1) new StatusMove(Moves.TOXIC, Type.POISON, 90, 10, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.TOXIC) .attr(StatusEffectAttr, StatusEffect.TOXIC)
@ -7551,9 +7648,9 @@ export function initMoves() {
.attr(TrapAttr, BattlerTagType.CLAMP), .attr(TrapAttr, BattlerTagType.CLAMP),
new AttackMove(Moves.SWIFT, Type.NORMAL, MoveCategory.SPECIAL, 60, -1, 20, -1, 0, 1) new AttackMove(Moves.SWIFT, Type.NORMAL, MoveCategory.SPECIAL, 60, -1, 20, -1, 0, 1)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.SKULL_BASH, Type.NORMAL, MoveCategory.PHYSICAL, 130, 100, 10, -1, 0, 1) new ChargingAttackMove(Moves.SKULL_BASH, Type.NORMAL, MoveCategory.PHYSICAL, 130, 100, 10, -1, 0, 1)
.attr(ChargeAttr, ChargeAnim.SKULL_BASH_CHARGING, i18next.t("moveTriggers:loweredItsHead", { pokemonName: "{USER}" }), null, true) .chargeText(i18next.t("moveTriggers:loweredItsHead", { pokemonName: "{USER}" }))
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, true) .chargeAttr(StatStageChangeAttr, [ Stat.DEF ], 1, true)
.ignoresVirtual(), .ignoresVirtual(),
new AttackMove(Moves.SPIKE_CANNON, Type.NORMAL, MoveCategory.PHYSICAL, 20, 100, 15, -1, 0, 1) new AttackMove(Moves.SPIKE_CANNON, Type.NORMAL, MoveCategory.PHYSICAL, 20, 100, 15, -1, 0, 1)
.attr(MultiHitAttr) .attr(MultiHitAttr)
@ -7590,14 +7687,15 @@ export function initMoves() {
.triageMove(), .triageMove(),
new StatusMove(Moves.LOVELY_KISS, Type.NORMAL, 75, 10, -1, 0, 1) new StatusMove(Moves.LOVELY_KISS, Type.NORMAL, 75, 10, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.SLEEP), .attr(StatusEffectAttr, StatusEffect.SLEEP),
new AttackMove(Moves.SKY_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 1) new ChargingAttackMove(Moves.SKY_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 1)
.attr(ChargeAttr, ChargeAnim.SKY_ATTACK_CHARGING, i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" })) .chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" }))
.attr(HighCritAttr) .attr(HighCritAttr)
.attr(FlinchAttr) .attr(FlinchAttr)
.makesContact(false) .makesContact(false)
.ignoresVirtual(), .ignoresVirtual(),
new StatusMove(Moves.TRANSFORM, Type.NORMAL, -1, 10, -1, 0, 1) new StatusMove(Moves.TRANSFORM, Type.NORMAL, -1, 10, -1, 0, 1)
.attr(TransformAttr) .attr(TransformAttr)
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE))
.ignoresProtect(), .ignoresProtect(),
new AttackMove(Moves.BUBBLE, Type.WATER, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1) new AttackMove(Moves.BUBBLE, Type.WATER, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
.attr(StatStageChangeAttr, [ Stat.SPD ], -1) .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
@ -8028,7 +8126,7 @@ export function initMoves() {
.attr(RemoveScreensAttr), .attr(RemoveScreensAttr),
new StatusMove(Moves.YAWN, Type.NORMAL, -1, 10, -1, 0, 3) new StatusMove(Moves.YAWN, Type.NORMAL, -1, 10, -1, 0, 3)
.attr(AddBattlerTagAttr, BattlerTagType.DROWSY, false, true) .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) 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(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1)
.attr(RemoveHeldItemAttr, false), .attr(RemoveHeldItemAttr, false),
@ -8055,9 +8153,10 @@ export function initMoves() {
new AttackMove(Moves.SECRET_POWER, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, 30, 0, 3) new AttackMove(Moves.SECRET_POWER, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, 30, 0, 3)
.makesContact(false) .makesContact(false)
.attr(SecretPowerAttr), .attr(SecretPowerAttr),
new AttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3) new ChargingAttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3)
.attr(ChargeAttr, ChargeAnim.DIVE_CHARGING, i18next.t("moveTriggers:hidUnderwater", { pokemonName: "{USER}" }), BattlerTagType.UNDERWATER, true) .chargeText(i18next.t("moveTriggers:hidUnderwater", { pokemonName: "{USER}" }))
.attr(GulpMissileTagAttr) .chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERWATER)
.chargeAttr(GulpMissileTagAttr)
.ignoresVirtual(), .ignoresVirtual(),
new AttackMove(Moves.ARM_THRUST, Type.FIGHTING, MoveCategory.PHYSICAL, 15, 100, 20, -1, 0, 3) new AttackMove(Moves.ARM_THRUST, Type.FIGHTING, MoveCategory.PHYSICAL, 15, 100, 20, -1, 0, 3)
.attr(MultiHitAttr), .attr(MultiHitAttr),
@ -8190,8 +8289,9 @@ export function initMoves() {
.attr(RechargeAttr), .attr(RechargeAttr),
new SelfStatusMove(Moves.BULK_UP, Type.FIGHTING, -1, 20, -1, 0, 3) new SelfStatusMove(Moves.BULK_UP, Type.FIGHTING, -1, 20, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1, true), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1, true),
new AttackMove(Moves.BOUNCE, Type.FLYING, MoveCategory.PHYSICAL, 85, 85, 5, 30, 0, 3) new ChargingAttackMove(Moves.BOUNCE, Type.FLYING, MoveCategory.PHYSICAL, 85, 85, 5, 30, 0, 3)
.attr(ChargeAttr, ChargeAnim.BOUNCE_CHARGING, i18next.t("moveTriggers:sprangUp", { pokemonName: "{USER}" }), BattlerTagType.FLYING) .chargeText(i18next.t("moveTriggers:sprangUp", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.condition(failOnGravityCondition) .condition(failOnGravityCondition)
.ignoresVirtual(), .ignoresVirtual(),
@ -8546,8 +8646,9 @@ export function initMoves() {
new AttackMove(Moves.OMINOUS_WIND, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 4) new AttackMove(Moves.OMINOUS_WIND, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 4)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
.windMove(), .windMove(),
new AttackMove(Moves.SHADOW_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4) new ChargingAttackMove(Moves.SHADOW_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4)
.attr(ChargeAttr, ChargeAnim.SHADOW_FORCE_CHARGING, i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" }), BattlerTagType.HIDDEN) .chargeText(i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN)
.ignoresProtect() .ignoresProtect()
.ignoresVirtual(), .ignoresVirtual(),
new SelfStatusMove(Moves.HONE_CLAWS, Type.DARK, -1, 15, -1, 0, 5) new SelfStatusMove(Moves.HONE_CLAWS, Type.DARK, -1, 15, -1, 0, 5)
@ -8670,12 +8771,13 @@ export function initMoves() {
.attr( .attr(
MovePowerMultiplierAttr, MovePowerMultiplierAttr,
(user, target, move) => target.status || target.hasAbility(Abilities.COMATOSE) ? 2 : 1), (user, target, move) => target.status || target.hasAbility(Abilities.COMATOSE) ? 2 : 1),
new AttackMove(Moves.SKY_DROP, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5) new ChargingAttackMove(Moves.SKY_DROP, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
.partial() // Should immobilize the target, Flying types should take no damage. cf https://bulbapedia.bulbagarden.net/wiki/Sky_Drop_(move) and https://www.smogon.com/dex/sv/moves/sky-drop/ .chargeText(i18next.t("moveTriggers:tookTargetIntoSky", { pokemonName: "{USER}", targetName: "{TARGET}" }))
.attr(ChargeAttr, ChargeAnim.SKY_DROP_CHARGING, i18next.t("moveTriggers:tookTargetIntoSky", { pokemonName: "{USER}", targetName: "{TARGET}" }), BattlerTagType.FLYING) // TODO: Add 2nd turn message .chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
.condition(failOnGravityCondition) .condition(failOnGravityCondition)
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE)) .condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE))
.ignoresVirtual(), .ignoresVirtual()
.partial(), // Should immobilize the target, Flying types should take no damage. cf https://bulbapedia.bulbagarden.net/wiki/Sky_Drop_(move) and https://www.smogon.com/dex/sv/moves/sky-drop/
new SelfStatusMove(Moves.SHIFT_GEAR, Type.STEEL, -1, 10, -1, 0, 5) new SelfStatusMove(Moves.SHIFT_GEAR, Type.STEEL, -1, 10, -1, 0, 5)
.attr(StatStageChangeAttr, [ Stat.ATK ], 1, true) .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true)
.attr(StatStageChangeAttr, [ Stat.SPD ], 2, true), .attr(StatStageChangeAttr, [ Stat.SPD ], 2, true),
@ -8825,12 +8927,12 @@ export function initMoves() {
new AttackMove(Moves.FIERY_DANCE, Type.FIRE, MoveCategory.SPECIAL, 80, 100, 10, 50, 0, 5) new AttackMove(Moves.FIERY_DANCE, Type.FIRE, MoveCategory.SPECIAL, 80, 100, 10, 50, 0, 5)
.attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true) .attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true)
.danceMove(), .danceMove(),
new AttackMove(Moves.FREEZE_SHOCK, Type.ICE, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 5) new ChargingAttackMove(Moves.FREEZE_SHOCK, Type.ICE, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 5)
.attr(ChargeAttr, ChargeAnim.FREEZE_SHOCK_CHARGING, i18next.t("moveTriggers:becameCloakedInFreezingLight", { pokemonName: "{USER}" })) .chargeText(i18next.t("moveTriggers:becameCloakedInFreezingLight", { pokemonName: "{USER}" }))
.attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.ICE_BURN, Type.ICE, MoveCategory.SPECIAL, 140, 90, 5, 30, 0, 5) new ChargingAttackMove(Moves.ICE_BURN, Type.ICE, MoveCategory.SPECIAL, 140, 90, 5, 30, 0, 5)
.attr(ChargeAttr, ChargeAnim.ICE_BURN_CHARGING, i18next.t("moveTriggers:becameCloakedInFreezingAir", { pokemonName: "{USER}" })) .chargeText(i18next.t("moveTriggers:becameCloakedInFreezingAir", { pokemonName: "{USER}" }))
.attr(StatusEffectAttr, StatusEffect.BURN) .attr(StatusEffectAttr, StatusEffect.BURN)
.ignoresVirtual(), .ignoresVirtual(),
new AttackMove(Moves.SNARL, Type.DARK, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5) new AttackMove(Moves.SNARL, Type.DARK, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5)
@ -8872,8 +8974,9 @@ export function initMoves() {
.target(MoveTarget.ENEMY_SIDE), .target(MoveTarget.ENEMY_SIDE),
new AttackMove(Moves.FELL_STINGER, Type.BUG, MoveCategory.PHYSICAL, 50, 100, 25, -1, 0, 6) new AttackMove(Moves.FELL_STINGER, Type.BUG, MoveCategory.PHYSICAL, 50, 100, 25, -1, 0, 6)
.attr(PostVictoryStatStageChangeAttr, [ Stat.ATK ], 3, true ), .attr(PostVictoryStatStageChangeAttr, [ Stat.ATK ], 3, true ),
new AttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) new ChargingAttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
.attr(ChargeAttr, ChargeAnim.PHANTOM_FORCE_CHARGING, i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" }), BattlerTagType.HIDDEN) .chargeText(i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN)
.ignoresProtect() .ignoresProtect()
.ignoresVirtual(), .ignoresVirtual(),
new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6) new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6)
@ -8983,8 +9086,8 @@ export function initMoves() {
.ignoresSubstitute() .ignoresSubstitute()
.powderMove() .powderMove()
.unimplemented(), .unimplemented(),
new SelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6) new ChargingSelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6)
.attr(ChargeAttr, ChargeAnim.GEOMANCY_CHARGING, i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" })) .chargeText(i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" }))
.attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true) .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true)
.ignoresVirtual(), .ignoresVirtual(),
new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6) new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6)
@ -9193,8 +9296,9 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.ATK ], -1) .attr(StatStageChangeAttr, [ Stat.ATK ], -1)
.condition((user, target, move) => target.getStatStage(Stat.ATK) > -6) .condition((user, target, move) => target.getStatStage(Stat.ATK) > -6)
.triageMove(), .triageMove(),
new AttackMove(Moves.SOLAR_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, 0, 7) new ChargingAttackMove(Moves.SOLAR_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, 0, 7)
.attr(SunlightChargeAttr, ChargeAnim.SOLAR_BLADE_CHARGING, i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" })) .chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" }))
.chargeAttr(WeatherInstantChargeAttr, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ])
.attr(AntiSunlightPowerDecreaseAttr) .attr(AntiSunlightPowerDecreaseAttr)
.slicingMove(), .slicingMove(),
new AttackMove(Moves.LEAFAGE, Type.GRASS, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 7) new AttackMove(Moves.LEAFAGE, Type.GRASS, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 7)
@ -9620,9 +9724,9 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, false, true) .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, false, true)
.attr(MultiHitAttr) .attr(MultiHitAttr)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, 100, 0, 8) new ChargingAttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 8)
.attr(ChargeAttr, ChargeAnim.METEOR_BEAM_CHARGING, i18next.t("moveTriggers:isOverflowingWithSpacePower", { pokemonName: "{USER}" }), null, true) .chargeText(i18next.t("moveTriggers:isOverflowingWithSpacePower", { pokemonName: "{USER}" }))
.attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true) .chargeAttr(StatStageChangeAttr, [ Stat.SPATK ], 1, true)
.ignoresVirtual(), .ignoresVirtual(),
new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8) new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8)
.attr(ShellSideArmCategoryAttr) .attr(ShellSideArmCategoryAttr)
@ -10074,8 +10178,10 @@ export function initMoves() {
.attr(IvyCudgelTypeAttr) .attr(IvyCudgelTypeAttr)
.attr(HighCritAttr) .attr(HighCritAttr)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.ELECTRO_SHOT, Type.ELECTRIC, MoveCategory.SPECIAL, 130, 100, 10, 100, 0, 9) new ChargingAttackMove(Moves.ELECTRO_SHOT, Type.ELECTRIC, MoveCategory.SPECIAL, 130, 100, 10, 100, 0, 9)
.attr(ElectroShotChargeAttr) .chargeText(i18next.t("moveTriggers:absorbedElectricity", { pokemonName: "{USER}" }))
.chargeAttr(StatStageChangeAttr, [ Stat.SPATK ], 1, true)
.chargeAttr(WeatherInstantChargeAttr, [ WeatherType.RAIN, WeatherType.HEAVY_RAIN ])
.ignoresVirtual(), .ignoresVirtual(),
new AttackMove(Moves.TERA_STARSTORM, Type.NORMAL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9) new AttackMove(Moves.TERA_STARSTORM, Type.NORMAL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9)
.attr(TeraMoveCategoryAttr) .attr(TeraMoveCategoryAttr)

View File

@ -1,4 +1,4 @@
import { leaveEncounterWithoutBattle, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { generateModifierType, leaveEncounterWithoutBattle, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { modifierTypes } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/modifier/modifier-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
@ -14,6 +14,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import i18next from "i18next";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/anOfferYouCantRefuse"; const namespace = "mysteryEncounters/anOfferYouCantRefuse";
@ -98,6 +99,8 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter =
} }
} }
const shinyCharm = generateModifierType(scene, modifierTypes.SHINY_CHARM);
encounter.setDialogueToken("itemName", shinyCharm?.name ?? i18next.t("modifierType:ModifierType.SHINY_CHARM.name"));
encounter.setDialogueToken("liepardName", getPokemonSpecies(Species.LIEPARD).getName()); encounter.setDialogueToken("liepardName", getPokemonSpecies(Species.LIEPARD).getName());
return true; return true;
@ -123,7 +126,7 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter =
return true; return true;
}) })
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
// Give the player a Shiny charm // Give the player a Shiny Charm
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.SHINY_CHARM)); scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.SHINY_CHARM));
leaveEncounterWithoutBattle(scene, true); leaveEncounterWithoutBattle(scene, true);
}) })
@ -132,9 +135,11 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter =
.withOption( .withOption(
MysteryEncounterOptionBuilder MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new CombinationPokemonRequirement( .withPrimaryPokemonRequirement(
new MoveRequirement(EXTORTION_MOVES), CombinationPokemonRequirement.Some(
new AbilityRequirement(EXTORTION_ABILITIES)) new MoveRequirement(EXTORTION_MOVES, true),
new AbilityRequirement(EXTORTION_ABILITIES, true)
)
) )
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.2.label`, buttonLabel: `${namespace}:option.2.label`,

View File

@ -42,6 +42,8 @@ import {
AttackTypeBoosterModifier, AttackTypeBoosterModifier,
BypassSpeedChanceModifier, BypassSpeedChanceModifier,
ContactHeldItemTransferChanceModifier, ContactHeldItemTransferChanceModifier,
GigantamaxAccessModifier,
MegaEvolutionAccessModifier,
PokemonHeldItemModifier PokemonHeldItemModifier
} from "#app/modifier/modifier"; } from "#app/modifier/modifier";
import i18next from "i18next"; import i18next from "i18next";
@ -191,12 +193,14 @@ const WAVE_LEVEL_BREAKPOINTS = [ 30, 50, 70, 100, 120, 140, 160 ];
export const BugTypeSuperfanEncounter: MysteryEncounter = export const BugTypeSuperfanEncounter: MysteryEncounter =
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.BUG_TYPE_SUPERFAN) MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.BUG_TYPE_SUPERFAN)
.withEncounterTier(MysteryEncounterTier.GREAT) .withEncounterTier(MysteryEncounterTier.GREAT)
.withPrimaryPokemonRequirement(new CombinationPokemonRequirement( .withPrimaryPokemonRequirement(
CombinationPokemonRequirement.Some(
// Must have at least 1 Bug type on team, OR have a bug item somewhere on the team // Must have at least 1 Bug type on team, OR have a bug item somewhere on the team
new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1), new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1),
new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1), new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1),
new TypeRequirement(Type.BUG, false, 1) new TypeRequirement(Type.BUG, false, 1)
)) )
)
.withMaxAllowedEncounters(1) .withMaxAllowedEncounters(1)
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
.withIntroSpriteConfigs([]) // These are set in onInit() .withIntroSpriteConfigs([]) // These are set in onInit()
@ -356,10 +360,17 @@ export const BugTypeSuperfanEncounter: MysteryEncounter =
}, },
]; ];
} else { } 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 // If the player has any evolution/form change items that are valid for their party,
const modifierOptions: ModifierTypeOption[] = [ generateModifierTypeOption(scene, modifierTypes.MASTER_BALL)!, generateModifierTypeOption(scene, modifierTypes.MAX_LURE)! ]; // 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[] = []; 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); const nonRareEvolutionModifier = generateModifierTypeOption(scene, modifierTypes.EVOLUTION_ITEM);
if (nonRareEvolutionModifier) { if (nonRareEvolutionModifier) {
specialOptions.push(nonRareEvolutionModifier); specialOptions.push(nonRareEvolutionModifier);
@ -396,11 +407,13 @@ export const BugTypeSuperfanEncounter: MysteryEncounter =
.build()) .build())
.withOption(MysteryEncounterOptionBuilder .withOption(MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
.withPrimaryPokemonRequirement(new CombinationPokemonRequirement( .withPrimaryPokemonRequirement(
CombinationPokemonRequirement.Some(
// Meets one or both of the below reqs // Meets one or both of the below reqs
new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1), new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1),
new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1) new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1)
)) )
)
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.3.label`, buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`, buttonTooltip: `${namespace}:option.3.tooltip`,

View File

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

View File

@ -236,7 +236,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
.withOption( .withOption(
MysteryEncounterOptionBuilder MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) .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({ .withDialogue({
buttonLabel: `${namespace}:option.3.label`, buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`, buttonTooltip: `${namespace}:option.3.tooltip`,

View File

@ -14,6 +14,7 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode
import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase";
import { PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import { PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { Challenges } from "#enums/challenges";
/** i18n namespace for encounter */ /** i18n namespace for encounter */
const namespace = "mysteryEncounters/darkDeal"; const namespace = "mysteryEncounters/darkDeal";
@ -141,6 +142,7 @@ export const DarkDealEncounter: MysteryEncounter =
// Removes random pokemon (including fainted) from party and adds name to dialogue data tokens // Removes random pokemon (including fainted) from party and adds name to dialogue data tokens
// Will never return last battle able mon and instead pick fainted/unable to battle // Will never return last battle able mon and instead pick fainted/unable to battle
const removedPokemon = getRandomPlayerPokemon(scene, true, false, true); const removedPokemon = getRandomPlayerPokemon(scene, true, false, true);
// Get all the pokemon's held items // Get all the pokemon's held items
const modifiers = removedPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier)); const modifiers = removedPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier));
scene.removePokemonFromPlayerParty(removedPokemon); scene.removePokemonFromPlayerParty(removedPokemon);
@ -160,7 +162,13 @@ export const DarkDealEncounter: MysteryEncounter =
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ROGUE_BALL)); scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ROGUE_BALL));
// Start encounter with random legendary (7-10 starter strength) that has level additive // Start encounter with random legendary (7-10 starter strength) that has level additive
const bossTypes: Type[] = encounter.misc.removedTypes; // If this is a mono-type challenge, always ensure the required type is filtered for
let bossTypes: Type[] = encounter.misc.removedTypes;
const singleTypeChallenges = scene.gameMode.challenges.filter(c => c.value && c.id === Challenges.SINGLE_TYPE);
if (scene.gameMode.isChallenge && singleTypeChallenges.length > 0) {
bossTypes = singleTypeChallenges.map(c => (c.value - 1) as Type);
}
const bossModifiers: PokemonHeldItemModifier[] = encounter.misc.modifiers; const bossModifiers: PokemonHeldItemModifier[] = encounter.misc.modifiers;
// Starter egg tier, 35/50/10/5 %odds for tiers 6/7/8/9+ // Starter egg tier, 35/50/10/5 %odds for tiers 6/7/8/9+
const roll = randSeedInt(100); const roll = randSeedInt(100);
@ -172,7 +180,8 @@ export const DarkDealEncounter: MysteryEncounter =
isBoss: true, isBoss: true,
modifierConfigs: bossModifiers.map(m => { modifierConfigs: bossModifiers.map(m => {
return { return {
modifier: m modifier: m,
stackCount: m.getStackCount(),
}; };
}) })
}; };

View File

@ -45,10 +45,13 @@ export const DelibirdyEncounter: MysteryEncounter =
.withEncounterTier(MysteryEncounterTier.GREAT) .withEncounterTier(MysteryEncounterTier.GREAT)
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
.withSceneRequirement(new MoneyRequirement(0, DELIBIRDY_MONEY_PRICE_MULTIPLIER)) // Must have enough money for it to spawn at the very least .withSceneRequirement(new MoneyRequirement(0, DELIBIRDY_MONEY_PRICE_MULTIPLIER)) // Must have enough money for it to spawn at the very least
.withPrimaryPokemonRequirement(new CombinationPokemonRequirement( // Must also have either option 2 or 3 available to spawn .withPrimaryPokemonRequirement(
CombinationPokemonRequirement.Some(
// Must also have either option 2 or 3 available to spawn
new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS), new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS),
new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true) new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true)
)) )
)
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
spriteKey: "", spriteKey: "",
@ -196,7 +199,7 @@ export const DelibirdyEncounter: MysteryEncounter =
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
const modifier: BerryModifier | HealingBoosterModifier = encounter.misc.chosenModifier; const modifier: BerryModifier | HealingBoosterModifier = encounter.misc.chosenModifier;
// Give the player a Candy Jar if they gave a Berry, and a Healing Charm for Reviver Seed // Give the player a Candy Jar if they gave a Berry, and a Berry Pouch for Reviver Seed
if (modifier instanceof BerryModifier) { if (modifier instanceof BerryModifier) {
// Check if the player has max stacks of that Candy Jar already // Check if the player has max stacks of that Candy Jar already
const existing = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier) as LevelIncrementBoosterModifier; const existing = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier) as LevelIncrementBoosterModifier;
@ -211,8 +214,8 @@ export const DelibirdyEncounter: MysteryEncounter =
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.CANDY_JAR)); scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.CANDY_JAR));
} }
} else { } else {
// Check if the player has max stacks of that Healing Charm already // Check if the player has max stacks of that Berry Pouch already
const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier; const existing = scene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier;
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
// At max stacks, give the first party pokemon a Shell Bell instead // At max stacks, give the first party pokemon a Shell Bell instead
@ -221,7 +224,7 @@ export const DelibirdyEncounter: MysteryEncounter =
scene.playSound("item_fanfare"); scene.playSound("item_fanfare");
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true);
} else { } else {
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM)); scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH));
} }
} }
@ -290,8 +293,8 @@ export const DelibirdyEncounter: MysteryEncounter =
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
const modifier = encounter.misc.chosenModifier; const modifier = encounter.misc.chosenModifier;
// Check if the player has max stacks of Berry Pouch already // Check if the player has max stacks of Healing Charm already
const existing = scene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier; const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier;
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
// At max stacks, give the first party pokemon a Shell Bell instead // At max stacks, give the first party pokemon a Shell Bell instead
@ -300,7 +303,7 @@ export const DelibirdyEncounter: MysteryEncounter =
scene.playSound("item_fanfare"); scene.playSound("item_fanfare");
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true);
} else { } else {
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH)); scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM));
} }
// Remove the modifier if its stacks go to 0 // Remove the modifier if its stacks go to 0

View File

@ -4,24 +4,30 @@ import { AttackTypeBoosterModifierType, modifierTypes, } from "#app/modifier/mod
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { AbilityRequirement, CombinationPokemonRequirement, TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Gender } from "#app/data/gender"; import { Gender } from "#app/data/gender";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { PokemonMove } from "#app/field/pokemon"; import Pokemon, { PokemonMove } from "#app/field/pokemon";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { EncounterBattleAnim } from "#app/data/battle-anims"; import { EncounterBattleAnim } from "#app/data/battle-anims";
import { WeatherType } from "#app/data/weather"; import { WeatherType } from "#app/data/weather";
import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { isNullOrUndefined, randSeedInt } from "#app/utils";
import { StatusEffect } from "#app/data/status-effect"; import { StatusEffect } from "#app/data/status-effect";
import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { applyAbilityOverrideToPokemon, applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { EncounterAnim } from "#enums/encounter-anims"; import { EncounterAnim } from "#enums/encounter-anims";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { Stat } from "#enums/stat";
import { Ability } from "#app/data/ability";
import { FIRE_RESISTANT_ABILITIES } from "#app/data/mystery-encounters/requirements/requirement-groups";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/fieryFallout"; const namespace = "mysteryEncounters/fieryFallout";
@ -62,16 +68,24 @@ export const FieryFalloutEncounter: MysteryEncounter =
{ {
species: volcaronaSpecies, species: volcaronaSpecies,
isBoss: false, isBoss: false,
gender: Gender.MALE gender: Gender.MALE,
tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ],
mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.SPDEF, Stat.SPD ], 1));
}
}, },
{ {
species: volcaronaSpecies, species: volcaronaSpecies,
isBoss: false, isBoss: false,
gender: Gender.FEMALE gender: Gender.FEMALE,
tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ],
mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.SPDEF, Stat.SPD ], 1));
}
} }
], ],
doubleBattle: true, doubleBattle: true,
disableSwitch: true disableSwitch: true,
}; };
encounter.enemyPartyConfigs = [ config ]; encounter.enemyPartyConfigs = [ config ];
@ -139,7 +153,7 @@ export const FieryFalloutEncounter: MysteryEncounter =
async (scene: BattleScene) => { async (scene: BattleScene) => {
// Pick battle // Pick battle
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
setEncounterRewards(scene, { fillRemaining: true }, undefined, () => giveLeadPokemonCharcoal(scene)); setEncounterRewards(scene, { fillRemaining: true }, undefined, () => giveLeadPokemonAttackTypeBoostItem(scene));
encounter.startOfBattleEffects.push( encounter.startOfBattleEffects.push(
{ {
@ -153,18 +167,6 @@ export const FieryFalloutEncounter: MysteryEncounter =
targets: [ BattlerIndex.PLAYER_2 ], targets: [ BattlerIndex.PLAYER_2 ],
move: new PokemonMove(Moves.FIRE_SPIN), move: new PokemonMove(Moves.FIRE_SPIN),
ignorePp: true ignorePp: true
},
{
sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [ BattlerIndex.ENEMY ],
move: new PokemonMove(Moves.QUIVER_DANCE),
ignorePp: true
},
{
sourceBattlerIndex: BattlerIndex.ENEMY_2,
targets: [ BattlerIndex.ENEMY_2 ],
move: new PokemonMove(Moves.QUIVER_DANCE),
ignorePp: true
}); });
await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]); await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]);
} }
@ -180,7 +182,7 @@ export const FieryFalloutEncounter: MysteryEncounter =
], ],
}, },
async (scene: BattleScene) => { async (scene: BattleScene) => {
// Damage non-fire types and burn 1 random non-fire type member // Damage non-fire types and burn 1 random non-fire type member + give it Heatproof
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
const nonFireTypes = scene.getParty().filter((p) => p.isAllowedInBattle() && !p.getTypes().includes(Type.FIRE)); const nonFireTypes = scene.getParty().filter((p) => p.isAllowedInBattle() && !p.getTypes().includes(Type.FIRE));
@ -198,7 +200,11 @@ export const FieryFalloutEncounter: MysteryEncounter =
if (chosenPokemon.trySetStatus(StatusEffect.BURN)) { if (chosenPokemon.trySetStatus(StatusEffect.BURN)) {
// Burn applied // Burn applied
encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender()); encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender());
encounter.setDialogueToken("abilityName", new Ability(Abilities.HEATPROOF, 3).name);
queueEncounterMessage(scene, `${namespace}:option.2.target_burned`); queueEncounterMessage(scene, `${namespace}:option.2.target_burned`);
// Also permanently change the burned Pokemon's ability to Heatproof
applyAbilityOverrideToPokemon(chosenPokemon, Abilities.HEATPROOF);
} }
} }
@ -209,8 +215,12 @@ export const FieryFalloutEncounter: MysteryEncounter =
.withOption( .withOption(
MysteryEncounterOptionBuilder MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new TypeRequirement(Type.FIRE, true, 1)) // Will set option3PrimaryName dialogue token automatically .withPrimaryPokemonRequirement(
.withSecondaryPokemonRequirement(new TypeRequirement(Type.FIRE, true, 1)) // Will set option3SecondaryName dialogue token automatically CombinationPokemonRequirement.Some(
new TypeRequirement(Type.FIRE, true, 1),
new AbilityRequirement(FIRE_RESISTANT_ABILITIES, true)
)
) // Will set option3PrimaryName dialogue token automatically
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.3.label`, buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`, buttonTooltip: `${namespace}:option.3.tooltip`,
@ -233,26 +243,32 @@ export const FieryFalloutEncounter: MysteryEncounter =
{ fillRemaining: true }, { fillRemaining: true },
undefined, undefined,
() => { () => {
giveLeadPokemonCharcoal(scene); giveLeadPokemonAttackTypeBoostItem(scene);
}); });
const primary = encounter.options[2].primaryPokemon!; const primary = encounter.options[2].primaryPokemon!;
const secondary = encounter.options[2].secondaryPokemon![0];
setEncounterExp(scene, [ primary.id, secondary.id ], getPokemonSpecies(Species.VOLCARONA).baseExp * 2); setEncounterExp(scene, [ primary.id ], getPokemonSpecies(Species.VOLCARONA).baseExp * 2);
leaveEncounterWithoutBattle(scene); leaveEncounterWithoutBattle(scene);
}) })
.build() .build()
) )
.build(); .build();
function giveLeadPokemonCharcoal(scene: BattleScene) { function giveLeadPokemonAttackTypeBoostItem(scene: BattleScene) {
// Give first party pokemon Charcoal for free at end of battle // Give first party pokemon attack type boost item for free at end of battle
const leadPokemon = scene.getParty()?.[0]; const leadPokemon = scene.getParty()?.[0];
if (leadPokemon) { if (leadPokemon) {
const charcoal = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.FIRE ]) as AttackTypeBoosterModifierType; // Generate type booster held item, default to Charcoal if item fails to generate
applyModifierTypeToPlayerPokemon(scene, leadPokemon, charcoal); let boosterModifierType = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER) as AttackTypeBoosterModifierType;
scene.currentBattle.mysteryEncounter!.setDialogueToken("leadPokemon", leadPokemon.getNameToRender()); if (!boosterModifierType) {
queueEncounterMessage(scene, `${namespace}:found_charcoal`); boosterModifierType = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.FIRE ]) as AttackTypeBoosterModifierType;
}
applyModifierTypeToPlayerPokemon(scene, leadPokemon, boosterModifierType);
const encounter = scene.currentBattle.mysteryEncounter!;
encounter.setDialogueToken("itemName", boosterModifierType.name);
encounter.setDialogueToken("leadPokemon", leadPokemon.getNameToRender());
queueEncounterMessage(scene, `${namespace}:found_item`);
} }
} }

View File

@ -145,7 +145,7 @@ export const FightOrFlightEncounter: MysteryEncounter =
.withOption( .withOption(
MysteryEncounterOptionBuilder MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) .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({ .withDialogue({
buttonLabel: `${namespace}:option.2.label`, buttonLabel: `${namespace}:option.2.label`,
buttonTooltip: `${namespace}:option.2.tooltip`, buttonTooltip: `${namespace}:option.2.tooltip`,

View File

@ -56,7 +56,13 @@ export const MysteriousChallengersEncounter: MysteryEncounter =
// Hard difficulty trainer is another random trainer, but with AVERAGE_BALANCED config // Hard difficulty trainer is another random trainer, but with AVERAGE_BALANCED config
// Number of mons is based off wave: 1-20 is 2, 20-40 is 3, etc. capping at 6 after wave 100 // Number of mons is based off wave: 1-20 is 2, 20-40 is 3, etc. capping at 6 after wave 100
const hardTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex); let retries = 0;
let hardTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex);
while (retries < 5 && hardTrainerType === normalTrainerType) {
// Will try to use a different trainer from the normal trainer type
hardTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex);
retries++;
}
const hardTemplate = new TrainerPartyCompoundTemplate( const hardTemplate = new TrainerPartyCompoundTemplate(
new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER, false, true), new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER, false, true),
new TrainerPartyTemplate( new TrainerPartyTemplate(

View File

@ -227,7 +227,7 @@ export const PartTimerEncounter: MysteryEncounter =
.withOption( .withOption(
MysteryEncounterOptionBuilder MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) .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({ .withDialogue({
buttonLabel: `${namespace}:option.3.label`, buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`, buttonTooltip: `${namespace}:option.3.tooltip`,

View File

@ -21,7 +21,7 @@ import i18next from "i18next";
const namespace = "mysteryEncounters/shadyVitaminDealer"; const namespace = "mysteryEncounters/shadyVitaminDealer";
const VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER = 1.5; const VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER = 1.5;
const VITAMIN_DEALER_EXPENSIVE_PRICE_MULTIPLIER = 3.5; const VITAMIN_DEALER_EXPENSIVE_PRICE_MULTIPLIER = 5;
/** /**
* Shady Vitamin Dealer encounter. * Shady Vitamin Dealer encounter.
@ -138,7 +138,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter =
newNature = randSeedInt(25) as Nature; newNature = randSeedInt(25) as Nature;
} }
chosenPokemon.nature = newNature; chosenPokemon.customPokemonData.nature = newNature;
encounter.setDialogueToken("newNature", getNatureName(newNature)); encounter.setDialogueToken("newNature", getNatureName(newNature));
queueEncounterMessage(scene, `${namespace}:cheap_side_effects`); queueEncounterMessage(scene, `${namespace}:cheap_side_effects`);
setEncounterExp(scene, [ chosenPokemon.id ], 100); 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 { PartyHealPhase } from "#app/phases/party-heal-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { BerryType } from "#enums/berry-type"; 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 */ /** i18n namespace for the encounter */
const namespace = "mysteryEncounters/slumberingSnorlax"; const namespace = "mysteryEncounters/slumberingSnorlax";
@ -72,7 +72,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter =
stackCount: 2 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 aiType: AiType.SMART // Required to ensure Snorlax uses Sleep Talk while it is asleep
}; };
const config: EnemyPartyConfig = { const config: EnemyPartyConfig = {
@ -143,7 +143,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter =
.withOption( .withOption(
MysteryEncounterOptionBuilder MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES)) .withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES, true))
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.3.label`, buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`, 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 { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import { getPokeballTintColor } from "#app/data/pokeball"; import { getPokeballTintColor } from "#app/data/pokeball";
import { PokemonHeldItemModifier } from "#app/modifier/modifier";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/theExpertPokemonBreeder"; const namespace = "mysteryEncounters/theExpertPokemonBreeder";
@ -163,7 +164,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
if (pokemon2CommonEggs > 0) { if (pokemon2CommonEggs > 0) {
const eggsText = i18next.t(`${namespace}:numEggs`, { count: pokemon2CommonEggs, rarity: i18next.t("egg:defaultTier") }); const eggsText = i18next.t(`${namespace}:numEggs`, { count: pokemon2CommonEggs, rarity: i18next.t("egg:defaultTier") });
pokemon2Tooltip += i18next.t(`${namespace}:eggs_tooltip`, { eggs: eggsText }); pokemon2Tooltip += i18next.t(`${namespace}:eggs_tooltip`, { eggs: eggsText });
encounter.setDialogueToken("pokemon1CommonEggs", eggsText); encounter.setDialogueToken("pokemon2CommonEggs", eggsText);
} }
encounter.options[1].dialogue!.buttonTooltip = pokemon2Tooltip; encounter.options[1].dialogue!.buttonTooltip = pokemon2Tooltip;
@ -221,7 +222,10 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.misc.chosenPokemon = pokemon1; encounter.misc.chosenPokemon = pokemon1;
encounter.setDialogueToken("chosenPokemon", pokemon1.getNameToRender()); encounter.setDialogueToken("chosenPokemon", pokemon1.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon1CommonEggs, pokemon1RareEggs); const eggOptions = getEggOptions(scene, pokemon1CommonEggs, pokemon1RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions); setEncounterRewards(scene,
{ guaranteedModifierTypeFuncs: [ modifierTypes.SOOTHE_BELL ], fillRemaining: true },
eggOptions,
() => doPostEncounterCleanup(scene));
// Remove all Pokemon from the party except the chosen Pokemon // Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon1); removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon1);
@ -247,9 +251,6 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.onGameOver = onGameOver; encounter.onGameOver = onGameOver;
await initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.withPostOptionPhase(async (scene: BattleScene) => {
await doPostEncounterCleanup(scene);
})
.build() .build()
) )
.withOption( .withOption(
@ -273,7 +274,10 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.misc.chosenPokemon = pokemon2; encounter.misc.chosenPokemon = pokemon2;
encounter.setDialogueToken("chosenPokemon", pokemon2.getNameToRender()); encounter.setDialogueToken("chosenPokemon", pokemon2.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon2CommonEggs, pokemon2RareEggs); const eggOptions = getEggOptions(scene, pokemon2CommonEggs, pokemon2RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions); setEncounterRewards(scene,
{ guaranteedModifierTypeFuncs: [ modifierTypes.SOOTHE_BELL ], fillRemaining: true },
eggOptions,
() => doPostEncounterCleanup(scene));
// Remove all Pokemon from the party except the chosen Pokemon // Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon2); removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon2);
@ -299,9 +303,6 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.onGameOver = onGameOver; encounter.onGameOver = onGameOver;
await initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.withPostOptionPhase(async (scene: BattleScene) => {
await doPostEncounterCleanup(scene);
})
.build() .build()
) )
.withOption( .withOption(
@ -325,7 +326,10 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.misc.chosenPokemon = pokemon3; encounter.misc.chosenPokemon = pokemon3;
encounter.setDialogueToken("chosenPokemon", pokemon3.getNameToRender()); encounter.setDialogueToken("chosenPokemon", pokemon3.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon3CommonEggs, pokemon3RareEggs); const eggOptions = getEggOptions(scene, pokemon3CommonEggs, pokemon3RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions); setEncounterRewards(scene,
{ guaranteedModifierTypeFuncs: [ modifierTypes.SOOTHE_BELL ], fillRemaining: true },
eggOptions,
() => doPostEncounterCleanup(scene));
// Remove all Pokemon from the party except the chosen Pokemon // Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon3); removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon3);
@ -351,9 +355,6 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.onGameOver = onGameOver; encounter.onGameOver = onGameOver;
await initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.withPostOptionPhase(async (scene: BattleScene) => {
await doPostEncounterCleanup(scene);
})
.build() .build()
) )
.withOutroDialogue([ .withOutroDialogue([
@ -462,12 +463,16 @@ function calculateEggRewardsForPokemon(pokemon: PlayerPokemon): [number, number]
} }
// Maximum of 30 points // Maximum of 30 points
const totalPoints = Math.min(pointsFromStarterTier + pointsFromBst, 30); let totalPoints = Math.min(pointsFromStarterTier + pointsFromBst, 30);
// 1 Rare egg for every 6 points // First 5 points go to Common eggs
const numRares = Math.floor(totalPoints / 6); let numCommons = Math.min(totalPoints, 5);
totalPoints -= numCommons;
// Then, 1 Rare egg for every 4 points
const numRares = Math.floor(totalPoints / 4);
// 1 Common egg for every point leftover // 1 Common egg for every point leftover
const numCommons = totalPoints % 6; numCommons += totalPoints % 4;
return [ numCommons, numRares ]; return [ numCommons, numRares ];
} }
@ -521,19 +526,19 @@ function checkAchievement(scene: BattleScene) {
} }
} }
async function restorePartyAndHeldItems(scene: BattleScene) { function restorePartyAndHeldItems(scene: BattleScene) {
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
// Restore original party // Restore original party
scene.getParty().push(...encounter.misc.originalParty); scene.getParty().push(...encounter.misc.originalParty);
// Restore held items // Restore held items
const originalHeldItems = encounter.misc.originalPartyHeldItems; const originalHeldItems = encounter.misc.originalPartyHeldItems;
originalHeldItems.forEach(pokemonHeldItemsList => { originalHeldItems.forEach((pokemonHeldItemsList: PokemonHeldItemModifier[]) => {
pokemonHeldItemsList.forEach(heldItem => { pokemonHeldItemsList.forEach(heldItem => {
scene.addModifier(heldItem, true, false, false, true); scene.addModifier(heldItem, true, false, false, true);
}); });
}); });
await scene.updateModifiers(true); scene.updateModifiers(true);
} }
function onGameOver(scene: BattleScene) { function onGameOver(scene: BattleScene) {
@ -609,13 +614,13 @@ function onGameOver(scene: BattleScene) {
return false; return false;
} }
async function doPostEncounterCleanup(scene: BattleScene) { function doPostEncounterCleanup(scene: BattleScene) {
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
if (!encounter.misc.encounterFailed) { if (!encounter.misc.encounterFailed) {
// Give achievement if in Space biome // Give achievement if in Space biome
checkAchievement(scene); checkAchievement(scene);
// Give 20 friendship to the chosen pokemon // Give 20 friendship to the chosen pokemon
encounter.misc.chosenPokemon.addFriendship(FRIENDSHIP_ADDED); 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 { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; 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 { Stat } from "#enums/stat";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
@ -79,7 +79,7 @@ export const TheStrongStuffEncounter: MysteryEncounter =
species: getPokemonSpecies(Species.SHUCKLE), species: getPokemonSpecies(Species.SHUCKLE),
isBoss: true, isBoss: true,
bossSegments: 5, bossSegments: 5,
mysteryEncounterPokemonData: new MysteryEncounterPokemonData({ spriteScale: 1.25 }), customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
nature: Nature.BOLD, nature: Nature.BOLD,
moveSet: [ Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER ], moveSet: [ Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER ],
modifierConfigs: [ modifierConfigs: [

View File

@ -37,6 +37,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
.withScenePartySizeRequirement(2, 6, true) // Must have at least 2 unfainted pokemon in party .withScenePartySizeRequirement(2, 6, true) // Must have at least 2 unfainted pokemon in party
.withFleeAllowed(false) .withFleeAllowed(false)
.withHideWildIntroMessage(true) .withHideWildIntroMessage(true)
.withPreventGameStatsUpdates(true) // Do not count the Pokemon as seen or defeated since it is ours
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
spriteKey: "training_session_gear", spriteKey: "training_session_gear",

View File

@ -71,7 +71,7 @@ export const TrashToTreasureEncounter: MysteryEncounter =
moveSet: [ Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH ] moveSet: [ Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH ]
}; };
const config: EnemyPartyConfig = { const config: EnemyPartyConfig = {
levelAdditiveModifier: 1, levelAdditiveModifier: 0.5,
pokemonConfigs: [ pokemonConfig ], pokemonConfigs: [ pokemonConfig ],
disableSwitch: true disableSwitch: true
}; };

View File

@ -210,7 +210,7 @@ export const UncommonBreedEncounter: MysteryEncounter =
.withOption( .withOption(
MysteryEncounterOptionBuilder MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) .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({ .withDialogue({
buttonLabel: `${namespace}:option.3.label`, buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`, buttonTooltip: `${namespace}:option.3.tooltip`,

View File

@ -4,23 +4,30 @@ import { Species } from "#enums/species";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { leaveEncounterWithoutBattle, setEncounterRewards, } from "../utils/encounter-phase-utils"; import { EnemyPartyConfig, EnemyPokemonConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, } from "../utils/encounter-phase-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import Pokemon, { PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { IntegerHolder, isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils"; import { IntegerHolder, isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils";
import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species"; import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species";
import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
import { achvs } from "#app/system/achv"; 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 { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { modifierTypes } from "#app/modifier/modifier-type"; import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import i18next from "#app/plugins/i18n"; import i18next from "#app/plugins/i18n";
import { doPokemonTransformationSequence, TransformationScreenPosition } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; import { doPokemonTransformationSequence, TransformationScreenPosition } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence";
import { getLevelTotalExp } from "#app/data/exp"; import { getLevelTotalExp } from "#app/data/exp";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { PlayerGender } from "#enums/player-gender";
import { TrainerType } from "#enums/trainer-type";
import PokemonData from "#app/system/pokemon-data";
import { Nature } from "#enums/nature";
import HeldModifierConfig from "#app/interfaces/held-modifier-config";
import { trainerConfigs, TrainerPartyTemplate } from "#app/data/trainer-config";
import { PartyMemberStrength } from "#enums/party-member-strength";
/** i18n namespace for encounter */ /** i18n namespace for encounter */
const namespace = "mysteryEncounters/weirdDream"; const namespace = "mysteryEncounters/weirdDream";
@ -80,10 +87,11 @@ const EXCLUDED_TRANSFORMATION_SPECIES = [
const SUPER_LEGENDARY_BST_THRESHOLD = 600; const SUPER_LEGENDARY_BST_THRESHOLD = 600;
const NON_LEGENDARY_BST_THRESHOLD = 570; const NON_LEGENDARY_BST_THRESHOLD = 570;
const GAIN_OLD_GATEAU_ITEM_BST_THRESHOLD = 450;
const OLD_GATEAU_STATS_UP = 20;
/** 0-100 */ /** 0-100 */
const PERCENT_LEVEL_LOSS_ON_REFUSE = 12.5; const PERCENT_LEVEL_LOSS_ON_REFUSE = 10;
/** /**
* Value ranges of the resulting species BST transformations after adding values to original species * Value ranges of the resulting species BST transformations after adding values to original species
@ -105,7 +113,8 @@ export const WeirdDreamEncounter: MysteryEncounter =
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.WEIRD_DREAM) MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.WEIRD_DREAM)
.withEncounterTier(MysteryEncounterTier.ROGUE) .withEncounterTier(MysteryEncounterTier.ROGUE)
.withDisallowedChallenges(Challenges.SINGLE_TYPE, Challenges.SINGLE_GENERATION) .withDisallowedChallenges(Challenges.SINGLE_TYPE, Challenges.SINGLE_GENERATION)
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) // TODO: should reset minimum wave to 10 when there are more Rogue tiers in pool. Matching Dark Deal minimum for now.
.withSceneWaveRangeRequirement(30, 140)
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
spriteKey: "weird_dream_woman", spriteKey: "weird_dream_woman",
@ -131,6 +140,15 @@ export const WeirdDreamEncounter: MysteryEncounter =
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)
.withOnInit((scene: BattleScene) => { .withOnInit((scene: BattleScene) => {
scene.loadBgm("mystery_encounter_weird_dream", "mystery_encounter_weird_dream.mp3"); scene.loadBgm("mystery_encounter_weird_dream", "mystery_encounter_weird_dream.mp3");
// Calculate all the newly transformed Pokemon and begin asset load
const teamTransformations = getTeamTransformations(scene);
const loadAssets = teamTransformations.map(t => (t.newPokemon as PlayerPokemon).loadAssets());
scene.currentBattle.mysteryEncounter!.misc = {
teamTransformations,
loadAssets
};
return true; return true;
}) })
.withOnVisualsStart((scene: BattleScene) => { .withOnVisualsStart((scene: BattleScene) => {
@ -156,13 +174,10 @@ export const WeirdDreamEncounter: MysteryEncounter =
doShowDreamBackground(scene); doShowDreamBackground(scene);
}); });
// Calculate all the newly transformed Pokemon and begin asset load for (const transformation of scene.currentBattle.mysteryEncounter!.misc.teamTransformations) {
const teamTransformations = getTeamTransformations(scene); scene.removePokemonFromPlayerParty(transformation.previousPokemon, false);
const loadAssets = teamTransformations.map(t => (t.newPokemon as PlayerPokemon).loadAssets()); scene.getParty().push(transformation.newPokemon);
scene.currentBattle.mysteryEncounter!.misc = { }
teamTransformations,
loadAssets
};
}) })
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
// Starts cutscene dialogue, but does not await so that cutscene plays as player goes through dialogue // Starts cutscene dialogue, but does not await so that cutscene plays as player goes through dialogue
@ -193,7 +208,7 @@ export const WeirdDreamEncounter: MysteryEncounter =
await showEncounterText(scene, `${namespace}:option.1.dream_complete`); await showEncounterText(scene, `${namespace}:option.1.dream_complete`);
await doNewTeamPostProcess(scene, transformations); await doNewTeamPostProcess(scene, transformations);
setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.MEMORY_MUSHROOM, modifierTypes.ROGUE_BALL, modifierTypes.MINT, modifierTypes.MINT ]}); setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.MEMORY_MUSHROOM, modifierTypes.ROGUE_BALL, modifierTypes.MINT, modifierTypes.MINT, modifierTypes.MINT ], fillRemaining: false });
leaveEncounterWithoutBattle(scene, true); leaveEncounterWithoutBattle(scene, true);
}) })
.build() .build()
@ -209,7 +224,88 @@ export const WeirdDreamEncounter: MysteryEncounter =
], ],
}, },
async (scene: BattleScene) => { async (scene: BattleScene) => {
// Reduce party levels by 20% // Battle your "future" team for some item rewards
const transformations: PokemonTransformation[] = scene.currentBattle.mysteryEncounter!.misc.teamTransformations;
// Uses the pokemon that player's party would have transformed into
const enemyPokemonConfigs: EnemyPokemonConfig[] = [];
for (const transformation of transformations) {
const newPokemon = transformation.newPokemon;
const previousPokemon = transformation.previousPokemon;
await postProcessTransformedPokemon(scene, previousPokemon, newPokemon, newPokemon.species.getRootSpeciesId(), true);
const dataSource = new PokemonData(newPokemon);
dataSource.player = false;
// Copy held items to new pokemon
const newPokemonHeldItemConfigs: HeldModifierConfig[] = [];
for (const item of transformation.heldItems) {
newPokemonHeldItemConfigs.push({
modifier: item.clone() as PokemonHeldItemModifier,
stackCount: item.getStackCount(),
isTransferable: false
});
}
// Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats
if (shouldGetOldGateau(newPokemon)) {
const stats = getOldGateauBoostedStats(newPokemon);
newPokemonHeldItemConfigs.push({
modifier: generateModifierType(scene, modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU, [ OLD_GATEAU_STATS_UP, stats ]) as PokemonHeldItemModifierType,
stackCount: 1,
isTransferable: false
});
}
const enemyConfig: EnemyPokemonConfig = {
species: transformation.newSpecies,
isBoss: newPokemon.getSpeciesForm().getBaseStatTotal() > NON_LEGENDARY_BST_THRESHOLD,
level: previousPokemon.level,
dataSource: dataSource,
modifierConfigs: newPokemonHeldItemConfigs
};
enemyPokemonConfigs.push(enemyConfig);
}
const genderIndex = scene.gameData.gender ?? PlayerGender.UNSET;
const trainerConfig = trainerConfigs[genderIndex === PlayerGender.FEMALE ? TrainerType.FUTURE_SELF_F : TrainerType.FUTURE_SELF_M].clone();
trainerConfig.setPartyTemplates(new TrainerPartyTemplate(transformations.length, PartyMemberStrength.STRONG));
const enemyPartyConfig: EnemyPartyConfig = {
trainerConfig: trainerConfig,
pokemonConfigs: enemyPokemonConfigs,
female: genderIndex === PlayerGender.FEMALE
};
const onBeforeRewards = () => {
// Before battle rewards, unlock the passive on a pokemon in the player's team for the rest of the run (not permanently)
// One random pokemon will get its passive unlocked
const passiveDisabledPokemon = scene.getParty().filter(p => !p.passive);
if (passiveDisabledPokemon?.length > 0) {
const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)];
enablePassiveMon.passive = true;
enablePassiveMon.updateInfo(true);
}
};
setEncounterRewards(scene, { guaranteedModifierTiers: [ ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT ], fillRemaining: false }, undefined, onBeforeRewards);
await showEncounterText(scene, `${namespace}:option.2.selected_2`, null, undefined, true);
await initBattleWithEnemyConfig(scene, enemyPartyConfig);
}
)
.withSimpleOption(
{
buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`,
selected: [
{
text: `${namespace}:option.3.selected`,
},
],
},
async (scene: BattleScene) => {
// Leave, reduce party levels by 10%
for (const pokemon of scene.getParty()) { for (const pokemon of scene.getParty()) {
pokemon.level = Math.max(Math.ceil((100 - PERCENT_LEVEL_LOSS_ON_REFUSE) / 100 * pokemon.level), 1); pokemon.level = Math.max(Math.ceil((100 - PERCENT_LEVEL_LOSS_ON_REFUSE) / 100 * pokemon.level), 1);
pokemon.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate); pokemon.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate);
@ -235,7 +331,7 @@ interface PokemonTransformation {
function getTeamTransformations(scene: BattleScene): PokemonTransformation[] { function getTeamTransformations(scene: BattleScene): PokemonTransformation[] {
const party = scene.getParty(); const party = scene.getParty();
// Removes all pokemon from the party // Removes all pokemon from the party
const alreadyUsedSpecies: PokemonSpecies[] = []; const alreadyUsedSpecies: PokemonSpecies[] = party.map(p => p.species);
const pokemonTransformations: PokemonTransformation[] = party.map(p => { const pokemonTransformations: PokemonTransformation[] = party.map(p => {
return { return {
previousPokemon: p previousPokemon: p
@ -250,11 +346,11 @@ function getTeamTransformations(scene: BattleScene): PokemonTransformation[] {
// First, roll 2 of the party members to new Pokemon at a +90 to +110 BST difference // First, roll 2 of the party members to new Pokemon at a +90 to +110 BST difference
// Then, roll the remainder of the party members at a +40 to +50 BST difference // Then, roll the remainder of the party members at a +40 to +50 BST difference
const numPokemon = party.length; const numPokemon = party.length;
const removedPokemon = randSeedShuffle(party.slice(0));
for (let i = 0; i < numPokemon; i++) { for (let i = 0; i < numPokemon; i++) {
const removed = party[randSeedInt(party.length)]; const removed = removedPokemon[i];
const index = pokemonTransformations.findIndex(p => p.previousPokemon.id === removed.id); const index = pokemonTransformations.findIndex(p => p.previousPokemon.id === removed.id);
pokemonTransformations[index].heldItems = removed.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier)); pokemonTransformations[index].heldItems = removed.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier));
scene.removePokemonFromPlayerParty(removed, false);
const bst = removed.calculateBaseStats().reduce((a, b) => a + b, 0); const bst = removed.calculateBaseStats().reduce((a, b) => a + b, 0);
let newBstRange: [number, number]; let newBstRange: [number, number];
@ -276,14 +372,13 @@ function getTeamTransformations(scene: BattleScene): PokemonTransformation[] {
pokemonTransformations[index].newSpecies = newSpecies; pokemonTransformations[index].newSpecies = newSpecies;
console.log("New species: " + JSON.stringify(newSpecies));
alreadyUsedSpecies.push(newSpecies); alreadyUsedSpecies.push(newSpecies);
} }
for (const transformation of pokemonTransformations) { for (const transformation of pokemonTransformations) {
const newAbilityIndex = randSeedInt(transformation.newSpecies.getAbilityCount()); const newAbilityIndex = randSeedInt(transformation.newSpecies.getAbilityCount());
const newPlayerPokemon = scene.addPlayerPokemon(transformation.newSpecies, transformation.previousPokemon.level, newAbilityIndex, undefined); transformation.newPokemon = scene.addPlayerPokemon(transformation.newSpecies, transformation.previousPokemon.level, newAbilityIndex, undefined);
transformation.newPokemon = newPlayerPokemon;
scene.getParty().push(newPlayerPokemon);
} }
return pokemonTransformations; return pokemonTransformations;
@ -296,6 +391,56 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
const newPokemon = transformation.newPokemon; const newPokemon = transformation.newPokemon;
const speciesRootForm = newPokemon.species.getRootSpeciesId(); const speciesRootForm = newPokemon.species.getRootSpeciesId();
if (await postProcessTransformedPokemon(scene, previousPokemon, newPokemon, speciesRootForm)) {
atLeastOneNewStarter = true;
}
// Copy old items to new pokemon
for (const item of transformation.heldItems) {
item.pokemonId = newPokemon.id;
await scene.addModifier(item, false, false, false, true);
}
// Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats
if (shouldGetOldGateau(newPokemon)) {
const stats = getOldGateauBoostedStats(newPokemon);
const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU()
.generateType(scene.getParty(), [ OLD_GATEAU_STATS_UP, stats ])
?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU);
const modifier = modType?.newModifier(newPokemon);
if (modifier) {
await scene.addModifier(modifier, false, false, false, true);
}
}
newPokemon.calculateStats();
await newPokemon.updateInfo();
}
// One random pokemon will get its passive unlocked
const passiveDisabledPokemon = scene.getParty().filter(p => !p.passive);
if (passiveDisabledPokemon?.length > 0) {
const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)];
enablePassiveMon.passive = true;
await enablePassiveMon.updateInfo(true);
}
// If at least one new starter was unlocked, play 1 fanfare
if (atLeastOneNewStarter) {
scene.playSound("level_up_fanfare");
}
}
/**
* Applies special changes to the newly transformed pokemon, such as passing previous moves, gaining egg moves, etc.
* Returns whether the transformed pokemon unlocks a new starter for the player.
* @param scene
* @param previousPokemon
* @param newPokemon
* @param speciesRootForm
* @param forBattle Default `false`. If false, will perform achievements and dex unlocks for the player.
*/
async function postProcessTransformedPokemon(scene: BattleScene, previousPokemon: PlayerPokemon, newPokemon: PlayerPokemon, speciesRootForm: Species, forBattle: boolean = false): Promise<boolean> {
let isNewStarter = false;
// Roll HA a second time // Roll HA a second time
if (newPokemon.species.abilityHidden) { if (newPokemon.species.abilityHidden) {
const hiddenIndex = newPokemon.species.ability2 ? 2 : 1; const hiddenIndex = newPokemon.species.ability2 ? 2 : 1;
@ -317,8 +462,11 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
return newValue > iv ? newValue : iv; return newValue > iv ? newValue : iv;
}); });
// Roll a neutral nature
newPokemon.nature = [ Nature.HARDY, Nature.DOCILE, Nature.BASHFUL, Nature.QUIRKY, Nature.SERIOUS ][randSeedInt(5)];
// For pokemon at/below 570 BST or any shiny pokemon, unlock it permanently as if you had caught it // For pokemon at/below 570 BST or any shiny pokemon, unlock it permanently as if you had caught it
if (newPokemon.getSpeciesForm().getBaseStatTotal() <= NON_LEGENDARY_BST_THRESHOLD || newPokemon.isShiny()) { if (!forBattle && (newPokemon.getSpeciesForm().getBaseStatTotal() <= NON_LEGENDARY_BST_THRESHOLD || newPokemon.isShiny())) {
if (newPokemon.getSpeciesForm().abilityHidden && newPokemon.abilityIndex === newPokemon.getSpeciesForm().getAbilityCount() - 1) { if (newPokemon.getSpeciesForm().abilityHidden && newPokemon.abilityIndex === newPokemon.getSpeciesForm().getAbilityCount() - 1) {
scene.validateAchv(achvs.HIDDEN_ABILITY); scene.validateAchv(achvs.HIDDEN_ABILITY);
} }
@ -338,7 +486,7 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
scene.gameData.updateSpeciesDexIvs(newPokemon.species.getRootSpeciesId(true), newPokemon.ivs); scene.gameData.updateSpeciesDexIvs(newPokemon.species.getRootSpeciesId(true), newPokemon.ivs);
const newStarterUnlocked = await scene.gameData.setPokemonCaught(newPokemon, true, false, false); const newStarterUnlocked = await scene.gameData.setPokemonCaught(newPokemon, true, false, false);
if (newStarterUnlocked) { if (newStarterUnlocked) {
atLeastOneNewStarter = true; isNewStarter = true;
await showEncounterText(scene, i18next.t("battle:addedAsAStarter", { pokemonName: getPokemonSpecies(speciesRootForm).getName() })); await showEncounterText(scene, i18next.t("battle:addedAsAStarter", { pokemonName: getPokemonSpecies(speciesRootForm).getName() }));
} }
} }
@ -355,7 +503,7 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
}); });
// For pokemon that the player owns (including ones just caught), gain a candy // For pokemon that the player owns (including ones just caught), gain a candy
if (!!scene.gameData.dexData[speciesRootForm].caughtAttr) { if (!forBattle && !!scene.gameData.dexData[speciesRootForm].caughtAttr) {
scene.gameData.addStarterCandy(getPokemonSpecies(speciesRootForm), 1); scene.gameData.addStarterCandy(getPokemonSpecies(speciesRootForm), 1);
} }
@ -364,9 +512,9 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
// Store a copy of a "standard" generated moveset for the new pokemon, will be used later for finding a favored move // Store a copy of a "standard" generated moveset for the new pokemon, will be used later for finding a favored move
const newPokemonGeneratedMoveset = newPokemon.moveset; const newPokemonGeneratedMoveset = newPokemon.moveset;
newPokemon.moveset = previousPokemon.moveset; newPokemon.moveset = previousPokemon.moveset.slice(0);
const newEggMoveIndex = await addEggMoveToNewPokemonMoveset(scene, newPokemon, speciesRootForm); const newEggMoveIndex = await addEggMoveToNewPokemonMoveset(scene, newPokemon, speciesRootForm, forBattle);
// Try to add a favored STAB move (might fail if Pokemon already knows a bunch of moves from newPokemonGeneratedMoveset) // Try to add a favored STAB move (might fail if Pokemon already knows a bunch of moves from newPokemonGeneratedMoveset)
addFavoredMoveToNewPokemonMoveset(newPokemon, newPokemonGeneratedMoveset, newEggMoveIndex); addFavoredMoveToNewPokemonMoveset(newPokemon, newPokemonGeneratedMoveset, newEggMoveIndex);
@ -379,54 +527,41 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
newType = randSeedInt(18) as Type; newType = randSeedInt(18) as Type;
} }
newTypes.push(newType); newTypes.push(newType);
if (!newPokemon.mysteryEncounterPokemonData) { if (!newPokemon.customPokemonData) {
newPokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(); newPokemon.customPokemonData = new CustomPokemonData();
}
newPokemon.mysteryEncounterPokemonData.types = newTypes;
for (const item of transformation.heldItems) {
item.pokemonId = newPokemon.id;
await scene.addModifier(item, false, false, false, true);
}
// Any pokemon that is at or below 450 BST gets +20 permanent BST to 3 stats: HP (halved, +10), lowest of Atk/SpAtk, and lowest of Def/SpDef
if (newPokemon.getSpeciesForm().getBaseStatTotal() <= GAIN_OLD_GATEAU_ITEM_BST_THRESHOLD) {
const stats: Stat[] = [ Stat.HP ];
const baseStats = newPokemon.getSpeciesForm().baseStats.slice(0);
// Attack or SpAtk
stats.push(baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK);
// Def or SpDef
stats.push(baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF);
const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU()
.generateType(scene.getParty(), [ 20, stats ])
?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU);
const modifier = modType?.newModifier(newPokemon);
if (modifier) {
await scene.addModifier(modifier, false, false, false, true);
}
} }
newPokemon.customPokemonData.types = newTypes;
// Enable passive if previous had it // Enable passive if previous had it
newPokemon.passive = previousPokemon.passive; newPokemon.passive = previousPokemon.passive;
newPokemon.calculateStats(); return isNewStarter;
await newPokemon.updateInfo();
}
// One random pokemon will get its passive unlocked
const passiveDisabledPokemon = scene.getParty().filter(p => !p.passive);
if (passiveDisabledPokemon?.length > 0) {
const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)];
enablePassiveMon.passive = true;
await enablePassiveMon.updateInfo(true);
}
// If at least one new starter was unlocked, play 1 fanfare
if (atLeastOneNewStarter) {
scene.playSound("level_up_fanfare");
}
} }
/**
* @returns `true` if a given Pokemon has valid BST to be given an Old Gateau
*/
function shouldGetOldGateau(pokemon: Pokemon): boolean {
return pokemon.getSpeciesForm().getBaseStatTotal() < NON_LEGENDARY_BST_THRESHOLD;
}
/**
* Get the lowest of HP/Spd, lowest of Atk/SpAtk, and lowest of Def/SpDef
* @returns Array of 3 {@linkcode Stat}s to boost
*/
function getOldGateauBoostedStats(pokemon: Pokemon): Stat[] {
const stats: Stat[] = [];
const baseStats = pokemon.getSpeciesForm().baseStats.slice(0);
// HP or Speed
stats.push(baseStats[Stat.HP] < baseStats[Stat.SPD] ? Stat.HP : Stat.SPD);
// Attack or SpAtk
stats.push(baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK);
// Def or SpDef
stats.push(baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF);
return stats;
}
function getTransformedSpecies(originalBst: number, bstSearchRange: [number, number], hasPokemonBstHigherThan600: boolean, hasPokemonBstBetween570And600: boolean, alreadyUsedSpecies: PokemonSpecies[]): PokemonSpecies { function getTransformedSpecies(originalBst: number, bstSearchRange: [number, number], hasPokemonBstHigherThan600: boolean, hasPokemonBstBetween570And600: boolean, alreadyUsedSpecies: PokemonSpecies[]): PokemonSpecies {
let newSpecies: PokemonSpecies | undefined; let newSpecies: PokemonSpecies | undefined;
while (isNullOrUndefined(newSpecies)) { while (isNullOrUndefined(newSpecies)) {
@ -550,7 +685,7 @@ function doSideBySideTransformations(scene: BattleScene, transformations: Pokemo
* @param newPokemon * @param newPokemon
* @param speciesRootForm * @param speciesRootForm
*/ */
async function addEggMoveToNewPokemonMoveset(scene: BattleScene, newPokemon: PlayerPokemon, speciesRootForm: Species): Promise<number | null> { async function addEggMoveToNewPokemonMoveset(scene: BattleScene, newPokemon: PlayerPokemon, speciesRootForm: Species, forBattle: boolean = false): Promise<number | null> {
let eggMoveIndex: null | number = null; let eggMoveIndex: null | number = null;
const eggMoves = newPokemon.getEggMoves()?.slice(0); const eggMoves = newPokemon.getEggMoves()?.slice(0);
if (eggMoves) { if (eggMoves) {
@ -576,7 +711,7 @@ async function addEggMoveToNewPokemonMoveset(scene: BattleScene, newPokemon: Pla
} }
// For pokemon that the player owns (including ones just caught), unlock the egg move // For pokemon that the player owns (including ones just caught), unlock the egg move
if (!isNullOrUndefined(randomEggMoveIndex) && !!scene.gameData.dexData[speciesRootForm].caughtAttr) { if (!forBattle && !isNullOrUndefined(randomEggMoveIndex) && !!scene.gameData.dexData[speciesRootForm].caughtAttr) {
await scene.gameData.setEggMoveUnlocked(getPokemonSpecies(speciesRootForm), randomEggMoveIndex, true); await scene.gameData.setEggMoveUnlocked(getPokemonSpecies(speciesRootForm), randomEggMoveIndex, true);
} }
} }

View File

@ -15,6 +15,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { AttackTypeBoosterModifier } from "#app/modifier/modifier"; import { AttackTypeBoosterModifier } from "#app/modifier/modifier";
import { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type"; import { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type";
import { SpeciesFormKey } from "#enums/species-form-key"; import { SpeciesFormKey } from "#enums/species-form-key";
import { allAbilities } from "#app/data/ability";
export interface EncounterRequirement { export interface EncounterRequirement {
meetsRequirement(scene: BattleScene): boolean; // Boolean to see if a requirement is met meetsRequirement(scene: BattleScene): boolean; // Boolean to see if a requirement is met
@ -36,31 +37,58 @@ export abstract class EncounterSceneRequirement implements EncounterRequirement
abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string]; abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string];
} }
/**
* Combination of multiple {@linkcode EncounterSceneRequirement | EncounterSceneRequirements} (OR/AND possible. See {@linkcode isAnd})
*/
export class CombinationSceneRequirement extends EncounterSceneRequirement { export class CombinationSceneRequirement extends EncounterSceneRequirement {
orRequirements: EncounterSceneRequirement[]; /** If `true`, all requirements must be met (AND). If `false`, any requirement must be met (OR) */
private isAnd: boolean;
requirements: EncounterSceneRequirement[];
constructor(... orRequirements: EncounterSceneRequirement[]) { public static Some(...requirements: EncounterSceneRequirement[]): CombinationSceneRequirement {
return new CombinationSceneRequirement(false, ...requirements);
}
public static Every(...requirements: EncounterSceneRequirement[]): CombinationSceneRequirement {
return new CombinationSceneRequirement(true, ...requirements);
}
private constructor(isAnd: boolean, ...requirements: EncounterSceneRequirement[]) {
super(); super();
this.orRequirements = orRequirements; this.isAnd = isAnd;
this.requirements = requirements;
} }
/**
* Checks if all/any requirements are met (depends on {@linkcode isAnd})
* @param scene The {@linkcode BattleScene} to check against
* @returns true if all/any requirements are met (depends on {@linkcode isAnd})
*/
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
for (const req of this.orRequirements) { return this.isAnd
if (req.meetsRequirement(scene)) { ? this.requirements.every(req => req.meetsRequirement(scene))
return true; : this.requirements.some(req => req.meetsRequirement(scene));
}
}
return false;
} }
/**
* Retrieves a dialogue token key/value pair for the given {@linkcode EncounterSceneRequirement | requirements}.
* @param scene The {@linkcode BattleScene} to check against
* @param pokemon The {@linkcode PlayerPokemon} to check against
* @returns A dialogue token key/value pair
* @throws An {@linkcode Error} if {@linkcode isAnd} is `true` (not supported)
*/
override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
for (const req of this.orRequirements) { if (this.isAnd) {
throw new Error("Not implemented (Sorry)");
} else {
for (const req of this.requirements) {
if (req.meetsRequirement(scene)) { if (req.meetsRequirement(scene)) {
return req.getDialogueToken(scene, pokemon); return req.getDialogueToken(scene, pokemon);
} }
} }
return this.orRequirements[0].getDialogueToken(scene, pokemon); return this.requirements[0].getDialogueToken(scene, pokemon);
}
} }
} }
@ -89,44 +117,74 @@ export abstract class EncounterPokemonRequirement implements EncounterRequiremen
abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string]; abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string];
} }
/**
* Combination of multiple {@linkcode EncounterPokemonRequirement | EncounterPokemonRequirements} (OR/AND possible. See {@linkcode isAnd})
*/
export class CombinationPokemonRequirement extends EncounterPokemonRequirement { export class CombinationPokemonRequirement extends EncounterPokemonRequirement {
orRequirements: EncounterPokemonRequirement[]; /** If `true`, all requirements must be met (AND). If `false`, any requirement must be met (OR) */
private isAnd: boolean;
private requirements: EncounterPokemonRequirement[];
constructor(...orRequirements: EncounterPokemonRequirement[]) { public static Some(...requirements: EncounterPokemonRequirement[]): CombinationPokemonRequirement {
return new CombinationPokemonRequirement(false, ...requirements);
}
public static Every(...requirements: EncounterPokemonRequirement[]): CombinationPokemonRequirement {
return new CombinationPokemonRequirement(true, ...requirements);
}
private constructor(isAnd: boolean, ...requirements: EncounterPokemonRequirement[]) {
super(); super();
this.isAnd = isAnd;
this.invertQuery = false; this.invertQuery = false;
this.minNumberOfPokemon = 1; this.minNumberOfPokemon = 1;
this.orRequirements = orRequirements; this.requirements = requirements;
} }
/**
* Checks if all/any requirements are met (depends on {@linkcode isAnd})
* @param scene The {@linkcode BattleScene} to check against
* @returns true if all/any requirements are met (depends on {@linkcode isAnd})
*/
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
for (const req of this.orRequirements) { return this.isAnd
if (req.meetsRequirement(scene)) { ? this.requirements.every(req => req.meetsRequirement(scene))
return true; : this.requirements.some(req => req.meetsRequirement(scene));
}
}
return false;
} }
/**
* Queries the players party for all party members that are compatible with all/any requirements (depends on {@linkcode isAnd})
* @param partyPokemon The party of {@linkcode PlayerPokemon}
* @returns All party members that are compatible with all/any requirements (depends on {@linkcode isAnd})
*/
override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
for (const req of this.orRequirements) { if (this.isAnd) {
const result = req.queryParty(partyPokemon); return this.requirements.reduce((relevantPokemon, req) => req.queryParty(relevantPokemon), partyPokemon);
if (result?.length > 0) { } else {
return result; const matchingRequirement = this.requirements.find(req => req.queryParty(partyPokemon).length > 0);
return matchingRequirement ? matchingRequirement.queryParty(partyPokemon) : [];
} }
} }
return []; /**
} * Retrieves a dialogue token key/value pair for the given {@linkcode EncounterPokemonRequirement | requirements}.
* @param scene The {@linkcode BattleScene} to check against
* @param pokemon The {@linkcode PlayerPokemon} to check against
* @returns A dialogue token key/value pair
* @throws An {@linkcode Error} if {@linkcode isAnd} is `true` (not supported)
*/
override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
for (const req of this.orRequirements) { if (this.isAnd) {
throw new Error("Not implemented (Sorry)");
} else {
for (const req of this.requirements) {
if (req.meetsRequirement(scene)) { if (req.meetsRequirement(scene)) {
return req.getDialogueToken(scene, pokemon); return req.getDialogueToken(scene, pokemon);
} }
} }
return this.orRequirements[0].getDialogueToken(scene, pokemon); return this.requirements[0].getDialogueToken(scene, pokemon);
}
} }
} }
@ -476,9 +534,11 @@ export class MoveRequirement extends EncounterPokemonRequirement {
requiredMoves: Moves[] = []; requiredMoves: Moves[] = [];
minNumberOfPokemon: number; minNumberOfPokemon: number;
invertQuery: boolean; 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(); super();
this.excludeDisallowedPokemon = excludeDisallowedPokemon;
this.minNumberOfPokemon = minNumberOfPokemon; this.minNumberOfPokemon = minNumberOfPokemon;
this.invertQuery = invertQuery; this.invertQuery = invertQuery;
this.requiredMoves = Array.isArray(moves) ? moves : [ moves ]; this.requiredMoves = Array.isArray(moves) ? moves : [ moves ];
@ -494,10 +554,15 @@ export class MoveRequirement extends EncounterPokemonRequirement {
override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
if (!this.invertQuery) { 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 { } else {
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed moves // 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 +624,11 @@ export class AbilityRequirement extends EncounterPokemonRequirement {
requiredAbilities: Abilities[]; requiredAbilities: Abilities[];
minNumberOfPokemon: number; minNumberOfPokemon: number;
invertQuery: boolean; 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(); super();
this.excludeDisallowedPokemon = excludeDisallowedPokemon;
this.minNumberOfPokemon = minNumberOfPokemon; this.minNumberOfPokemon = minNumberOfPokemon;
this.invertQuery = invertQuery; this.invertQuery = invertQuery;
this.requiredAbilities = Array.isArray(abilities) ? abilities : [ abilities ]; this.requiredAbilities = Array.isArray(abilities) ? abilities : [ abilities ];
@ -577,16 +644,21 @@ export class AbilityRequirement extends EncounterPokemonRequirement {
override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
if (!this.invertQuery) { 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 { } else {
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed abilitiess // 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.requiredAbilities.filter((ability) => pokemon.getAbility().id === ability).length === 0); 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] { override getDialogueToken(_scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
if (pokemon?.getAbility().id && this.requiredAbilities.some(a => pokemon.getAbility().id === a)) { const matchingAbility = this.requiredAbilities.find(a => pokemon?.hasAbility(a, false));
return [ "ability", pokemon.getAbility().name ]; if (!isNullOrUndefined(matchingAbility)) {
return [ "ability", allAbilities[matchingAbility].name ];
} }
return [ "ability", "" ]; return [ "ability", "" ];
} }

View File

@ -53,6 +53,7 @@ export interface IMysteryEncounter {
hasBattleAnimationsWithoutTargets: boolean; hasBattleAnimationsWithoutTargets: boolean;
skipEnemyBattleTurns: boolean; skipEnemyBattleTurns: boolean;
skipToFightInput: boolean; skipToFightInput: boolean;
preventGameStatsUpdates: boolean;
onInit?: (scene: BattleScene) => boolean; onInit?: (scene: BattleScene) => boolean;
onVisualsStart?: (scene: BattleScene) => boolean; onVisualsStart?: (scene: BattleScene) => boolean;
@ -150,6 +151,10 @@ export default class MysteryEncounter implements IMysteryEncounter {
* If true, will skip COMMAND input and go straight to FIGHT (move select) input menu * If true, will skip COMMAND input and go straight to FIGHT (move select) input menu
*/ */
skipToFightInput: boolean; skipToFightInput: boolean;
/**
* If true, will prevent updating {@linkcode GameStats} for encountering and/or defeating Pokemon
*/
preventGameStatsUpdates: boolean;
// #region Event callback functions // #region Event callback functions
@ -325,7 +330,7 @@ export default class MysteryEncounter implements IMysteryEncounter {
if (activeMon.length > 0) { if (activeMon.length > 0) {
this.primaryPokemon = activeMon[0]; this.primaryPokemon = activeMon[0];
} else { } else {
this.primaryPokemon = scene.getParty().filter(p => !p.isFainted())[0]; this.primaryPokemon = scene.getParty().filter(p => p.isAllowedInBattle())[0];
} }
return true; return true;
} }
@ -548,6 +553,7 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
hasBattleAnimationsWithoutTargets: boolean = false; hasBattleAnimationsWithoutTargets: boolean = false;
skipEnemyBattleTurns: boolean = false; skipEnemyBattleTurns: boolean = false;
skipToFightInput: boolean = false; skipToFightInput: boolean = false;
preventGameStatsUpdates: boolean = false;
maxAllowedEncounters: number = 3; maxAllowedEncounters: number = 3;
expMultiplier: number = 1; expMultiplier: number = 1;
@ -735,6 +741,14 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
return Object.assign(this, { skipToFightInput }); return Object.assign(this, { skipToFightInput });
} }
/**
* If true, will prevent updating {@linkcode GameStats} for encountering and/or defeating Pokemon
* Default `false`
*/
withPreventGameStatsUpdates(preventGameStatsUpdates: boolean): this & Required<Pick<IMysteryEncounter, "preventGameStatsUpdates">> {
return Object.assign(this, { preventGameStatsUpdates });
}
/** /**
* Sets the maximum number of times that an encounter can spawn in a given Classic run * Sets the maximum number of times that an encounter can spawn in a given Classic run
* @param maxAllowedEncounters * @param maxAllowedEncounters

View File

@ -118,3 +118,20 @@ export const EXTORTION_ABILITIES = [
Abilities.SUCTION_CUPS, Abilities.SUCTION_CUPS,
Abilities.STICKY_HOLD Abilities.STICKY_HOLD
]; ];
/**
* Abilities that signify resistance to fire
*/
export const FIRE_RESISTANT_ABILITIES = [
Abilities.FLAME_BODY,
Abilities.FLASH_FIRE,
Abilities.WELL_BAKED_BODY,
Abilities.HEATPROOF,
Abilities.THERMAL_EXCHANGE,
Abilities.THICK_FAT,
Abilities.WATER_BUBBLE,
Abilities.MAGMA_ARMOR,
Abilities.WATER_VEIL,
Abilities.STEAM_ENGINE,
Abilities.PRIMORDIAL_SEA
];

View File

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

View File

@ -21,6 +21,8 @@ import { Gender } from "#app/data/gender";
import { PermanentStat } from "#enums/stat"; import { PermanentStat } from "#enums/stat";
import { VictoryPhase } from "#app/phases/victory-phase"; import { VictoryPhase } from "#app/phases/victory-phase";
import { SummaryUiMode } from "#app/ui/summary-ui-handler"; import { SummaryUiMode } from "#app/ui/summary-ui-handler";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { Abilities } from "#enums/abilities";
/** Will give +1 level every 10 waves */ /** Will give +1 level every 10 waves */
export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1; export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1;
@ -833,3 +835,21 @@ export function isPokemonValidForEncounterOptionSelection(pokemon: Pokemon, scen
return null; return null;
} }
/**
* Permanently overrides the ability (not passive) of a pokemon.
* If the pokemon is a fusion, instead overrides the fused pokemon's ability.
*/
export function applyAbilityOverrideToPokemon(pokemon: Pokemon, ability: Abilities) {
if (pokemon.isFusion()) {
if (!pokemon.fusionCustomPokemonData) {
pokemon.fusionCustomPokemonData = new CustomPokemonData();
}
pokemon.fusionCustomPokemonData.ability = ability;
} else {
if (!pokemon.customPokemonData) {
pokemon.customPokemonData = new CustomPokemonData();
}
pokemon.customPokemonData.ability = ability;
}
}

View File

@ -2580,11 +2580,11 @@ export function initSpecies() {
new PokemonSpecies(Species.VAROOM, 9, false, false, false, "Single-Cyl Pokémon", Type.STEEL, Type.POISON, 1, 35, Abilities.OVERCOAT, Abilities.NONE, Abilities.SLOW_START, 300, 45, 70, 63, 30, 45, 47, 190, 50, 60, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.VAROOM, 9, false, false, false, "Single-Cyl Pokémon", Type.STEEL, Type.POISON, 1, 35, Abilities.OVERCOAT, Abilities.NONE, Abilities.SLOW_START, 300, 45, 70, 63, 30, 45, 47, 190, 50, 60, GrowthRate.MEDIUM_FAST, 50, false),
new PokemonSpecies(Species.REVAVROOM, 9, false, false, false, "Multi-Cyl Pokémon", Type.STEEL, Type.POISON, 1.8, 120, Abilities.OVERCOAT, Abilities.NONE, Abilities.FILTER, 500, 80, 119, 90, 54, 67, 90, 75, 50, 175, GrowthRate.MEDIUM_FAST, 50, false, false, new PokemonSpecies(Species.REVAVROOM, 9, false, false, false, "Multi-Cyl Pokémon", Type.STEEL, Type.POISON, 1.8, 120, Abilities.OVERCOAT, Abilities.NONE, Abilities.FILTER, 500, 80, 119, 90, 54, 67, 90, 75, 50, 175, GrowthRate.MEDIUM_FAST, 50, false, false,
new PokemonForm("Normal", "", Type.STEEL, Type.POISON, 1.8, 120, Abilities.OVERCOAT, Abilities.NONE, Abilities.FILTER, 500, 80, 119, 90, 54, 67, 90, 75, 50, 175, false, null, true), new PokemonForm("Normal", "", Type.STEEL, Type.POISON, 1.8, 120, Abilities.OVERCOAT, Abilities.NONE, Abilities.FILTER, 500, 80, 119, 90, 54, 67, 90, 75, 50, 175, false, null, true),
new PokemonForm("Segin Starmobile", "segin-starmobile", Type.STEEL, Type.DARK, 1.8, 240, Abilities.INTIMIDATE, Abilities.NONE, Abilities.INTIMIDATE, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), new PokemonForm("Segin Starmobile", "segin-starmobile", Type.STEEL, Type.DARK, 1.8, 240, Abilities.INTIMIDATE, Abilities.NONE, Abilities.INTIMIDATE, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175),
new PokemonForm("Schedar Starmobile", "schedar-starmobile", Type.STEEL, Type.FIRE, 1.8, 240, Abilities.SPEED_BOOST, Abilities.NONE, Abilities.SPEED_BOOST, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), new PokemonForm("Schedar Starmobile", "schedar-starmobile", Type.STEEL, Type.FIRE, 1.8, 240, Abilities.SPEED_BOOST, Abilities.NONE, Abilities.SPEED_BOOST, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175),
new PokemonForm("Navi Starmobile", "navi-starmobile", Type.STEEL, Type.POISON, 1.8, 240, Abilities.TOXIC_DEBRIS, Abilities.NONE, Abilities.TOXIC_DEBRIS, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), new PokemonForm("Navi Starmobile", "navi-starmobile", Type.STEEL, Type.POISON, 1.8, 240, Abilities.TOXIC_DEBRIS, Abilities.NONE, Abilities.TOXIC_DEBRIS, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175),
new PokemonForm("Ruchbah Starmobile", "ruchbah-starmobile", Type.STEEL, Type.FAIRY, 1.8, 240, Abilities.MISTY_SURGE, Abilities.NONE, Abilities.MISTY_SURGE, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), new PokemonForm("Ruchbah Starmobile", "ruchbah-starmobile", Type.STEEL, Type.FAIRY, 1.8, 240, Abilities.MISTY_SURGE, Abilities.NONE, Abilities.MISTY_SURGE, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175),
new PokemonForm("Caph Starmobile", "caph-starmobile", Type.STEEL, Type.FIGHTING, 1.8, 240, Abilities.STAMINA, Abilities.NONE, Abilities.STAMINA, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), new PokemonForm("Caph Starmobile", "caph-starmobile", Type.STEEL, Type.FIGHTING, 1.8, 240, Abilities.STAMINA, Abilities.NONE, Abilities.STAMINA, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175),
), ),
new PokemonSpecies(Species.CYCLIZAR, 9, false, false, false, "Mount Pokémon", Type.DRAGON, Type.NORMAL, 1.6, 63, Abilities.SHED_SKIN, Abilities.NONE, Abilities.REGENERATOR, 501, 70, 95, 65, 85, 65, 121, 190, 50, 175, GrowthRate.MEDIUM_SLOW, 50, false), new PokemonSpecies(Species.CYCLIZAR, 9, false, false, false, "Mount Pokémon", Type.DRAGON, Type.NORMAL, 1.6, 63, Abilities.SHED_SKIN, Abilities.NONE, Abilities.REGENERATOR, 501, 70, 95, 65, 85, 65, 121, 190, 50, 175, GrowthRate.MEDIUM_SLOW, 50, false),
new PokemonSpecies(Species.ORTHWORM, 9, false, false, false, "Earthworm Pokémon", Type.STEEL, null, 2.5, 310, Abilities.EARTH_EATER, Abilities.NONE, Abilities.SAND_VEIL, 480, 70, 85, 145, 60, 55, 65, 25, 50, 240, GrowthRate.SLOW, 50, false), new PokemonSpecies(Species.ORTHWORM, 9, false, false, false, "Earthworm Pokémon", Type.STEEL, null, 2.5, 310, Abilities.EARTH_EATER, Abilities.NONE, Abilities.SAND_VEIL, 480, 70, 85, 145, 60, 55, 65, 25, 50, 240, GrowthRate.SLOW, 50, false),

View File

@ -1,4 +1,4 @@
import * as Utils from "../utils"; import { randIntRange } from "#app/utils";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import i18next, { ParseKeys } from "i18next"; import i18next, { ParseKeys } from "i18next";
@ -6,17 +6,21 @@ export { StatusEffect };
export class Status { export class Status {
public effect: StatusEffect; public effect: StatusEffect;
public turnCount: integer; /** Toxic damage is `1/16 max HP * toxicTurnCount` */
public cureTurn: integer | null; public toxicTurnCount: number = 0;
public sleepTurnsRemaining?: number;
constructor(effect: StatusEffect, turnCount: integer = 0, cureTurn?: integer) { constructor(effect: StatusEffect, toxicTurnCount: number = 0, sleepTurnsRemaining?: number) {
this.effect = effect; this.effect = effect;
this.turnCount = turnCount === undefined ? 0 : turnCount; this.toxicTurnCount = toxicTurnCount;
this.cureTurn = cureTurn!; // TODO: is this bang correct? this.sleepTurnsRemaining = sleepTurnsRemaining;
} }
incrementTurn(): void { incrementTurn(): void {
this.turnCount++; this.toxicTurnCount++;
if (this.sleepTurnsRemaining) {
this.sleepTurnsRemaining--;
}
} }
isPostTurn(): boolean { isPostTurn(): boolean {
@ -107,7 +111,7 @@ export function getStatusEffectCatchRateMultiplier(statusEffect: StatusEffect):
* Returns a random non-volatile StatusEffect * Returns a random non-volatile StatusEffect
*/ */
export function generateRandomStatusEffect(): StatusEffect { export function generateRandomStatusEffect(): StatusEffect {
return Utils.randIntRange(1, 6); return randIntRange(1, 6);
} }
/** /**
@ -123,7 +127,7 @@ export function getRandomStatusEffect(statusEffectA: StatusEffect, statusEffectB
return statusEffectA; return statusEffectA;
} }
return Utils.randIntRange(0, 2) ? statusEffectA : statusEffectB; return randIntRange(0, 2) ? statusEffectA : statusEffectB;
} }
/** /**
@ -140,7 +144,7 @@ export function getRandomStatus(statusA: Status | null, statusB: Status | null):
} }
return Utils.randIntRange(0, 2) ? statusA : statusB; return randIntRange(0, 2) ? statusA : statusB;
} }
/** /**

View File

@ -2500,6 +2500,22 @@ export const trainerConfigs: TrainerConfigs = {
[TrainerType.BUG_TYPE_SUPERFAN]: new TrainerConfig(++t).setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.ACE_TRAINER) [TrainerType.BUG_TYPE_SUPERFAN]: new TrainerConfig(++t).setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.ACE_TRAINER)
.setPartyTemplates(new TrainerPartyTemplate(2, PartyMemberStrength.AVERAGE)), .setPartyTemplates(new TrainerPartyTemplate(2, PartyMemberStrength.AVERAGE)),
[TrainerType.EXPERT_POKEMON_BREEDER]: new TrainerConfig(++t).setMoneyMultiplier(3).setEncounterBgm(TrainerType.ACE_TRAINER).setLocalizedName("Expert Pokemon Breeder") [TrainerType.EXPERT_POKEMON_BREEDER]: new TrainerConfig(++t).setMoneyMultiplier(3).setEncounterBgm(TrainerType.ACE_TRAINER).setLocalizedName("Expert Pokemon Breeder")
.setPartyTemplates(new TrainerPartyTemplate(3, PartyMemberStrength.STRONG)) .setPartyTemplates(new TrainerPartyTemplate(3, PartyMemberStrength.AVERAGE)),
[TrainerType.FUTURE_SELF_M]: new TrainerConfig(++t)
.setMoneyMultiplier(0)
.setEncounterBgm("mystery_encounter_weird_dream")
.setBattleBgm("mystery_encounter_weird_dream")
.setMixedBattleBgm("mystery_encounter_weird_dream")
.setVictoryBgm("mystery_encounter_weird_dream")
.setLocalizedName("Future Self M")
.setPartyTemplates(new TrainerPartyTemplate(6, PartyMemberStrength.STRONG)),
[TrainerType.FUTURE_SELF_F]: new TrainerConfig(++t)
.setMoneyMultiplier(0)
.setEncounterBgm("mystery_encounter_weird_dream")
.setBattleBgm("mystery_encounter_weird_dream")
.setMixedBattleBgm("mystery_encounter_weird_dream")
.setVictoryBgm("mystery_encounter_weird_dream")
.setLocalizedName("Future Self F")
.setPartyTemplates(new TrainerPartyTemplate(6, PartyMemberStrength.STRONG))
}; };

View File

@ -116,6 +116,8 @@ export enum TrainerType {
VITO, VITO,
BUG_TYPE_SUPERFAN, BUG_TYPE_SUPERFAN,
EXPERT_POKEMON_BREEDER, EXPERT_POKEMON_BREEDER,
FUTURE_SELF_M,
FUTURE_SELF_F,
BROCK = 200, BROCK = 200,
MISTY, MISTY,

View File

@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "#app/battle-scene";
import { Variant, VariantSet, variantColorCache } from "#app/data/variant"; import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
import { variantData } from "#app/data/variant"; import { variantData } from "#app/data/variant";
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info";
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr } from "#app/data/move"; import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr } from "#app/data/move";
import { default as PokemonSpecies, PokemonSpeciesForm, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { default as PokemonSpecies, PokemonSpeciesForm, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species";
import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER, getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER, getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters";
import { starterPassiveAbilities } from "#app/data/balance/passives"; import { starterPassiveAbilities } from "#app/data/balance/passives";
@ -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 { 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 { WeatherType } from "#app/data/weather";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; 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, 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 PokemonData from "#app/system/pokemon-data";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { Mode } from "#app/ui/ui"; 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 { Challenges } from "#enums/challenges";
import { PokemonAnimType } from "#enums/pokemon-anim-type"; import { PokemonAnimType } from "#enums/pokemon-anim-type";
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; 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 { SwitchType } from "#enums/switch-type";
import { SpeciesFormKey } from "#enums/species-form-key"; 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"; import { BASE_HIDDEN_ABILITY_CHANCE, BASE_SHINY_CHANCE, SHINY_EPIC_CHANCE, SHINY_VARIANT_CHANCE } from "#app/data/balance/rates";
@ -93,7 +93,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public stats: integer[]; public stats: integer[];
public ivs: integer[]; public ivs: integer[];
public nature: Nature; public nature: Nature;
public natureOverride: Nature | -1;
public moveset: (PokemonMove | null)[]; public moveset: (PokemonMove | null)[];
public status: Status | null; public status: Status | null;
public friendship: integer; public friendship: integer;
@ -114,7 +113,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public fusionVariant: Variant; public fusionVariant: Variant;
public fusionGender: Gender; public fusionGender: Gender;
public fusionLuck: integer; public fusionLuck: integer;
public fusionMysteryEncounterPokemonData: MysteryEncounterPokemonData | null; public fusionCustomPokemonData: CustomPokemonData | null;
private summonDataPrimer: PokemonSummonData | null; private summonDataPrimer: PokemonSummonData | null;
@ -122,7 +121,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public battleData: PokemonBattleData; public battleData: PokemonBattleData;
public battleSummonData: PokemonBattleSummonData; public battleSummonData: PokemonBattleSummonData;
public turnData: PokemonTurnData; 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 */ /** Used by Mystery Encounters to execute pokemon-specific logic (such as stat boosts) at start of battle */
public mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; public mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void;
@ -193,7 +192,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
this.nature = dataSource.nature || 0 as Nature; this.nature = dataSource.nature || 0 as Nature;
this.nickname = dataSource.nickname; this.nickname = dataSource.nickname;
this.natureOverride = dataSource.natureOverride !== undefined ? dataSource.natureOverride : -1;
this.moveset = dataSource.moveset; this.moveset = dataSource.moveset;
this.status = dataSource.status!; // TODO: is this bang correct? this.status = dataSource.status!; // TODO: is this bang correct?
this.friendship = dataSource.friendship !== undefined ? dataSource.friendship : this.species.baseFriendship; this.friendship = dataSource.friendship !== undefined ? dataSource.friendship : this.species.baseFriendship;
@ -212,9 +210,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.fusionVariant = dataSource.fusionVariant || 0; this.fusionVariant = dataSource.fusionVariant || 0;
this.fusionGender = dataSource.fusionGender; this.fusionGender = dataSource.fusionGender;
this.fusionLuck = dataSource.fusionLuck; this.fusionLuck = dataSource.fusionLuck;
this.fusionMysteryEncounterPokemonData = dataSource.fusionMysteryEncounterPokemonData; this.fusionCustomPokemonData = dataSource.fusionCustomPokemonData;
this.usedTMs = dataSource.usedTMs ?? []; this.usedTMs = dataSource.usedTMs ?? [];
this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(dataSource.mysteryEncounterPokemonData); this.customPokemonData = new CustomPokemonData(dataSource.customPokemonData);
} else { } else {
this.id = Utils.randSeedInt(4294967296); this.id = Utils.randSeedInt(4294967296);
this.ivs = ivs || Utils.getIvsFromId(this.id); this.ivs = ivs || Utils.getIvsFromId(this.id);
@ -235,7 +233,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.variant = this.shiny ? this.generateVariant() : 0; this.variant = this.shiny ? this.generateVariant() : 0;
} }
this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(); this.customPokemonData = new CustomPokemonData();
if (nature !== undefined) { if (nature !== undefined) {
this.setNature(nature); this.setNature(nature);
@ -243,8 +241,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.generateNature(); this.generateNature();
} }
this.natureOverride = -1;
this.friendship = species.baseFriendship; this.friendship = species.baseFriendship;
this.metLevel = level; this.metLevel = level;
this.metBiome = scene.currentBattle ? scene.arena.biomeType : -1; this.metBiome = scene.currentBattle ? scene.arena.biomeType : -1;
@ -593,8 +589,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const formKey = this.getFormKey(); const formKey = this.getFormKey();
if (this.isMax() === true || formKey === "segin-starmobile" || formKey === "schedar-starmobile" || formKey === "navi-starmobile" || formKey === "ruchbah-starmobile" || formKey === "caph-starmobile") { if (this.isMax() === true || formKey === "segin-starmobile" || formKey === "schedar-starmobile" || formKey === "navi-starmobile" || formKey === "ruchbah-starmobile" || formKey === "caph-starmobile") {
return 1.5; return 1.5;
} else if (this.mysteryEncounterPokemonData.spriteScale > 0) { } else if (this.customPokemonData.spriteScale > 0) {
return this.mysteryEncounterPokemonData.spriteScale; return this.customPokemonData.spriteScale;
} }
return 1; return 1;
} }
@ -1023,7 +1019,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
getNature(): Nature { getNature(): Nature {
return this.natureOverride !== -1 ? this.natureOverride : this.nature; return this.customPokemonData.nature !== -1 ? this.customPokemonData.nature : this.nature;
} }
setNature(nature: Nature): void { setNature(nature: Nature): void {
@ -1198,15 +1194,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (!types.length || !includeTeraType) { if (!types.length || !includeTeraType) {
if (!ignoreOverride && this.summonData?.types && this.summonData.types.length > 0) { if (!ignoreOverride && this.summonData?.types && this.summonData.types.length > 0) {
this.summonData.types.forEach(t => types.push(t)); 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 // "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 // Fusing a Pokemon onto something with "permanently changed" types will still apply the fusion's types as normal
const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOverride); const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOverride);
if (fusionSpeciesForm) { if (fusionSpeciesForm) {
// Check if the fusion Pokemon also had "permanently changed" types // 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]) { if (fusionMETypes && fusionMETypes.length >= 2 && fusionMETypes[1] !== types[0]) {
types.push(fusionMETypes[1]); types.push(fusionMETypes[1]);
} else if (fusionMETypes && fusionMETypes.length === 1 && fusionMETypes[0] !== types[0]) { } else if (fusionMETypes && fusionMETypes.length === 1 && fusionMETypes[0] !== types[0]) {
@ -1218,8 +1214,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
} }
if (types.length === 1 && this.mysteryEncounterPokemonData.types.length >= 2) { if (types.length === 1 && this.customPokemonData.types.length >= 2) {
types.push(this.mysteryEncounterPokemonData.types[1]); types.push(this.customPokemonData.types[1]);
} }
} else { } else {
const speciesForm = this.getSpeciesForm(ignoreOverride); const speciesForm = this.getSpeciesForm(ignoreOverride);
@ -1230,7 +1226,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (fusionSpeciesForm) { if (fusionSpeciesForm) {
// Check if the fusion Pokemon also had "permanently changed" types // Check if the fusion Pokemon also had "permanently changed" types
// Otherwise, use standard fusion type logic // 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]) { if (fusionMETypes && fusionMETypes.length >= 2 && fusionMETypes[1] !== types[0]) {
types.push(fusionMETypes[1]); types.push(fusionMETypes[1]);
} else if (fusionMETypes && fusionMETypes.length === 1 && fusionMETypes[0] !== types[0]) { } else if (fusionMETypes && fusionMETypes.length === 1 && fusionMETypes[0] !== types[0]) {
@ -1262,6 +1258,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; return types;
} }
@ -1288,14 +1289,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return allAbilities[Overrides.OPP_ABILITY_OVERRIDE]; return allAbilities[Overrides.OPP_ABILITY_OVERRIDE];
} }
if (this.isFusion()) { if (this.isFusion()) {
if (!isNullOrUndefined(this.fusionMysteryEncounterPokemonData?.ability) && this.fusionMysteryEncounterPokemonData.ability !== -1) { if (!isNullOrUndefined(this.fusionCustomPokemonData?.ability) && this.fusionCustomPokemonData.ability !== -1) {
return allAbilities[this.fusionMysteryEncounterPokemonData.ability]; return allAbilities[this.fusionCustomPokemonData.ability];
} else { } else {
return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)]; return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)];
} }
} }
if (!isNullOrUndefined(this.mysteryEncounterPokemonData.ability) && this.mysteryEncounterPokemonData.ability !== -1) { if (!isNullOrUndefined(this.customPokemonData.ability) && this.customPokemonData.ability !== -1) {
return allAbilities[this.mysteryEncounterPokemonData.ability]; return allAbilities[this.customPokemonData.ability];
} }
let abilityId = this.getSpeciesForm(ignoreOverride).getAbility(this.abilityIndex); let abilityId = this.getSpeciesForm(ignoreOverride).getAbility(this.abilityIndex);
if (abilityId === Abilities.NONE) { if (abilityId === Abilities.NONE) {
@ -1318,8 +1319,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE && !this.isPlayer()) { if (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE && !this.isPlayer()) {
return allAbilities[Overrides.OPP_PASSIVE_ABILITY_OVERRIDE]; return allAbilities[Overrides.OPP_PASSIVE_ABILITY_OVERRIDE];
} }
if (!isNullOrUndefined(this.mysteryEncounterPokemonData.passive) && this.mysteryEncounterPokemonData.passive !== -1) { if (!isNullOrUndefined(this.customPokemonData.passive) && this.customPokemonData.passive !== -1) {
return allAbilities[this.mysteryEncounterPokemonData.passive]; return allAbilities[this.customPokemonData.passive];
} }
let starterSpeciesId = this.species.speciesId; let starterSpeciesId = this.species.speciesId;
@ -2018,7 +2019,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.fusionVariant = 0; this.fusionVariant = 0;
this.fusionGender = 0; this.fusionGender = 0;
this.fusionLuck = 0; this.fusionLuck = 0;
this.fusionMysteryEncounterPokemonData = null; this.fusionCustomPokemonData = null;
this.generateName(); this.generateName();
this.calculateStats(); this.calculateStats();
@ -2119,7 +2120,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
// Trainers get a weight bump to stat buffing moves // Trainers get a weight bump to stat buffing moves
movePool = movePool.map(m => [ m[0], m[1] * (allMoves[m[0]].getAttrs(StatStageChangeAttr).some(a => a.stages > 1 && a.selfTarget) ? 1.25 : 1) ]); movePool = movePool.map(m => [ m[0], m[1] * (allMoves[m[0]].getAttrs(StatStageChangeAttr).some(a => a.stages > 1 && a.selfTarget) ? 1.25 : 1) ]);
// Trainers get a weight decrease to multiturn moves // Trainers get a weight decrease to multiturn moves
movePool = movePool.map(m => [ m[0], m[1] * (!!allMoves[m[0]].hasAttr(ChargeAttr) || !!allMoves[m[0]].hasAttr(RechargeAttr) ? 0.7 : 1) ]); movePool = movePool.map(m => [ m[0], m[1] * (!!allMoves[m[0]].isChargingMove() || !!allMoves[m[0]].hasAttr(RechargeAttr) ? 0.7 : 1) ]);
} }
// Weight towards higher power moves, by reducing the power of moves below the highest power. // Weight towards higher power moves, by reducing the power of moves below the highest power.
@ -2187,8 +2188,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.moveset.push(new PokemonMove(movePool[index][0], 0, 0)); 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); this.scene.triggerPokemonFormChange(this, SpeciesFormChangeMoveLearnedTrigger);
} }
}
trySelectMove(moveIndex: integer, ignorePp?: boolean): boolean { trySelectMove(moveIndex: integer, ignorePp?: boolean): boolean {
const move = this.getMoveset().length > moveIndex const move = this.getMoveset().length > moveIndex
@ -2285,6 +2289,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.levelExp = this.exp - getLevelTotalExp(this.level, this.species.growthRate); this.levelExp = this.exp - getLevelTotalExp(this.level, this.species.growthRate);
} }
/**
* Compares if `this` and {@linkcode target} are on the same team.
* @param target the {@linkcode Pokemon} to compare against.
* @returns `true` if the two pokemon are allies, `false` otherwise
*/
public isOpponent(target: Pokemon): boolean {
return this.isPlayer() !== target.isPlayer();
}
getOpponent(targetIndex: integer): Pokemon | null { getOpponent(targetIndex: integer): Pokemon | null {
const ret = this.getOpponents()[targetIndex]; const ret = this.getOpponents()[targetIndex];
if (ret.summonData) { if (ret.summonData) {
@ -2605,7 +2618,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) */ /** Reduces damage if this Pokemon has a relevant screen (e.g. Light Screen for special attacks) */
const screenMultiplier = new Utils.NumberHolder(1); 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: * For each {@linkcode HitsTagAttr} the move has, doubles the damage of the move if:
@ -3347,13 +3360,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
} }
const types = this.getTypes(true, true); if (sourcePokemon && sourcePokemon !== this && this.isSafeguarded(sourcePokemon)) {
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (sourcePokemon && sourcePokemon !== this && this.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, defendingSide)) {
return false; return false;
} }
const types = this.getTypes(true, true);
switch (effect) { switch (effect) {
case StatusEffect.POISON: case StatusEffect.POISON:
case StatusEffect.TOXIC: case StatusEffect.TOXIC:
@ -3417,7 +3429,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return true; return true;
} }
trySetStatus(effect: StatusEffect | undefined, asPhase: boolean = false, sourcePokemon: Pokemon | null = null, cureTurn: integer | null = 0, sourceText: string | null = null): boolean { trySetStatus(effect?: StatusEffect, asPhase: boolean = false, sourcePokemon: Pokemon | null = null, turnsRemaining: number = 0, sourceText: string | null = null): boolean {
if (!this.canSetStatus(effect, asPhase, false, sourcePokemon)) { if (!this.canSetStatus(effect, asPhase, false, sourcePokemon)) {
return false; return false;
} }
@ -3431,15 +3443,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
if (asPhase) { if (asPhase) {
this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, cureTurn, sourceText, sourcePokemon)); this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, turnsRemaining, sourceText, sourcePokemon));
return true; return true;
} }
let statusCureTurn: Utils.IntegerHolder; let sleepTurnsRemaining: Utils.NumberHolder;
if (effect === StatusEffect.SLEEP) { if (effect === StatusEffect.SLEEP) {
statusCureTurn = new Utils.IntegerHolder(this.randSeedIntRange(2, 4)); sleepTurnsRemaining = new Utils.NumberHolder(this.randSeedIntRange(2, 4));
applyAbAttrs(ReduceStatusEffectDurationAbAttr, this, null, false, effect, statusCureTurn);
this.setFrameRate(4); this.setFrameRate(4);
@ -3459,9 +3470,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
} }
statusCureTurn = statusCureTurn!; // tell TS compiler it's defined sleepTurnsRemaining = sleepTurnsRemaining!; // tell TS compiler it's defined
effect = effect!; // If `effect` is undefined then `trySetStatus()` will have already returned early via the `canSetStatus()` call effect = effect!; // If `effect` is undefined then `trySetStatus()` will have already returned early via the `canSetStatus()` call
this.status = new Status(effect, 0, statusCureTurn?.value); this.status = new Status(effect, 0, sleepTurnsRemaining?.value);
if (effect !== StatusEffect.FAINT) { if (effect !== StatusEffect.FAINT) {
this.scene.triggerPokemonFormChange(this, SpeciesFormChangeStatusEffectTrigger, true); this.scene.triggerPokemonFormChange(this, SpeciesFormChangeStatusEffectTrigger, true);
@ -3499,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 { primeSummonData(summonDataPrimer: PokemonSummonData): void {
this.summonDataPrimer = summonDataPrimer; this.summonDataPrimer = summonDataPrimer;
} }
@ -3993,16 +4021,20 @@ export class PlayerPokemon extends Pokemon {
super(scene, 106, 148, species, level, abilityIndex, formIndex, gender, shiny, variant, ivs, nature, dataSource); super(scene, 106, 148, species, level, abilityIndex, formIndex, gender, shiny, variant, ivs, nature, dataSource);
if (Overrides.STATUS_OVERRIDE) { if (Overrides.STATUS_OVERRIDE) {
this.status = new Status(Overrides.STATUS_OVERRIDE); this.status = new Status(Overrides.STATUS_OVERRIDE, 0, 4);
} }
if (Overrides.SHINY_OVERRIDE) { if (Overrides.SHINY_OVERRIDE) {
this.shiny = true; this.shiny = true;
this.initShinySparkle(); 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; this.variant = Overrides.VARIANT_OVERRIDE;
} }
}
if (!dataSource) { if (!dataSource) {
if (this.scene.gameMode.isDaily) { if (this.scene.gameMode.isDaily) {
this.generateAndPopulateMoveset(); this.generateAndPopulateMoveset();
@ -4272,7 +4304,6 @@ export class PlayerPokemon extends Pokemon {
if (newEvolution.condition?.predicate(this)) { if (newEvolution.condition?.predicate(this)) {
const newPokemon = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, undefined, this.shiny, this.variant, this.ivs, this.nature); const newPokemon = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, undefined, this.shiny, this.variant, this.ivs, this.nature);
newPokemon.natureOverride = this.natureOverride;
newPokemon.passive = this.passive; newPokemon.passive = this.passive;
newPokemon.moveset = this.moveset.slice(); newPokemon.moveset = this.moveset.slice();
newPokemon.moveset = this.copyMoveset(); newPokemon.moveset = this.copyMoveset();
@ -4314,12 +4345,33 @@ export class PlayerPokemon extends Pokemon {
changeForm(formChange: SpeciesFormChange): Promise<void> { changeForm(formChange: SpeciesFormChange): Promise<void> {
return new Promise(resolve => { return new Promise(resolve => {
const previousFormIndex = this.formIndex;
this.formIndex = Math.max(this.species.forms.findIndex(f => f.formKey === formChange.formKey), 0); this.formIndex = Math.max(this.species.forms.findIndex(f => f.formKey === formChange.formKey), 0);
this.generateName(); this.generateName();
const abilityCount = this.getSpeciesForm().getAbilityCount(); const abilityCount = this.getSpeciesForm().getAbilityCount();
if (this.abilityIndex >= abilityCount) { // Shouldn't happen if (this.abilityIndex >= abilityCount) { // Shouldn't happen
this.abilityIndex = abilityCount - 1; 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.compatibleTms.splice(0, this.compatibleTms.length);
this.generateCompatibleTms(); this.generateCompatibleTms();
const updateAndResolve = () => { const updateAndResolve = () => {
@ -4356,7 +4408,7 @@ export class PlayerPokemon extends Pokemon {
this.fusionVariant = pokemon.variant; this.fusionVariant = pokemon.variant;
this.fusionGender = pokemon.gender; this.fusionGender = pokemon.gender;
this.fusionLuck = pokemon.luck; this.fusionLuck = pokemon.luck;
this.fusionMysteryEncounterPokemonData = pokemon.mysteryEncounterPokemonData; this.fusionCustomPokemonData = pokemon.customPokemonData;
if ((pokemon.pauseEvolutions) || (this.pauseEvolutions)) { if ((pokemon.pauseEvolutions) || (this.pauseEvolutions)) {
this.pauseEvolutions = true; this.pauseEvolutions = true;
} }
@ -4448,7 +4500,7 @@ export class EnemyPokemon extends Pokemon {
} }
if (Overrides.OPP_STATUS_OVERRIDE) { if (Overrides.OPP_STATUS_OVERRIDE) {
this.status = new Status(Overrides.OPP_STATUS_OVERRIDE); this.status = new Status(Overrides.OPP_STATUS_OVERRIDE, 0, 4);
} }
if (Overrides.OPP_GENDER_OVERRIDE) { if (Overrides.OPP_GENDER_OVERRIDE) {
@ -4457,9 +4509,11 @@ export class EnemyPokemon extends Pokemon {
const speciesId = this.species.speciesId; const speciesId = this.species.speciesId;
if (speciesId in Overrides.OPP_FORM_OVERRIDES if (
speciesId in Overrides.OPP_FORM_OVERRIDES
&& Overrides.OPP_FORM_OVERRIDES[speciesId] && Overrides.OPP_FORM_OVERRIDES[speciesId]
&& this.species.forms[Overrides.OPP_FORM_OVERRIDES[speciesId]]) { && this.species.forms[Overrides.OPP_FORM_OVERRIDES[speciesId]]
) {
this.formIndex = Overrides.OPP_FORM_OVERRIDES[speciesId] ?? 0; this.formIndex = Overrides.OPP_FORM_OVERRIDES[speciesId] ?? 0;
} }
@ -4470,10 +4524,13 @@ export class EnemyPokemon extends Pokemon {
if (Overrides.OPP_SHINY_OVERRIDE) { if (Overrides.OPP_SHINY_OVERRIDE) {
this.shiny = true; this.shiny = true;
this.initShinySparkle(); this.initShinySparkle();
} else if (Overrides.OPP_SHINY_OVERRIDE === false) {
this.shiny = false;
} }
if (this.shiny) { if (this.shiny) {
this.variant = this.generateVariant(); this.variant = this.generateVariant();
if (Overrides.OPP_VARIANT_OVERRIDE) { if (Overrides.OPP_VARIANT_OVERRIDE !== null) {
this.variant = Overrides.OPP_VARIANT_OVERRIDE; this.variant = Overrides.OPP_VARIANT_OVERRIDE;
} }
} }

View File

@ -165,6 +165,8 @@ export class LoadingScene extends SceneBase {
this.loadImage("discord", "ui"); this.loadImage("discord", "ui");
this.loadImage("google", "ui"); this.loadImage("google", "ui");
this.loadImage("settings_icon", "ui"); this.loadImage("settings_icon", "ui");
this.loadImage("link_icon", "ui");
this.loadImage("unlink_icon", "ui");
this.loadImage("default_bg", "arenas"); this.loadImage("default_bg", "arenas");
// Load arena images // Load arena images

View File

@ -10,7 +10,9 @@ import { getStatusEffectDescriptor, StatusEffect } from "#app/data/status-effect
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier } from "#app/modifier/modifier"; import {
AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier, TempExtraModifierModifier
} from "#app/modifier/modifier";
import { ModifierTier } from "#app/modifier/modifier-tier"; import { ModifierTier } from "#app/modifier/modifier-tier";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { Unlockables } from "#app/system/unlockables"; import { Unlockables } from "#app/system/unlockables";
@ -1144,7 +1146,7 @@ class EvolutionItemModifierTypeGenerator extends ModifierTypeGenerator {
} }
class FormChangeItemModifierTypeGenerator extends ModifierTypeGenerator { class FormChangeItemModifierTypeGenerator extends ModifierTypeGenerator {
constructor(rare: boolean) { constructor(isRareFormChangeItem: boolean) {
super((party: Pokemon[], pregenArgs?: any[]) => { super((party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs && (pregenArgs.length === 1) && (pregenArgs[0] in FormChangeItem)) { if (pregenArgs && (pregenArgs.length === 1) && (pregenArgs[0] in FormChangeItem)) {
return new FormChangeItemModifierType(pregenArgs[0] as FormChangeItem); return new FormChangeItemModifierType(pregenArgs[0] as FormChangeItem);
@ -1185,7 +1187,7 @@ class FormChangeItemModifierTypeGenerator extends ModifierTypeGenerator {
} }
return formChangeItemTriggers; return formChangeItemTriggers;
}).flat()) }).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. // 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) { if (!formChangeItemPool.length) {
@ -1579,6 +1581,7 @@ export const modifierTypes = {
VOUCHER_PREMIUM: () => new AddVoucherModifierType(VoucherType.PREMIUM, 1), VOUCHER_PREMIUM: () => new AddVoucherModifierType(VoucherType.PREMIUM, 1),
GOLDEN_POKEBALL: () => new ModifierType("modifierType:ModifierType.GOLDEN_POKEBALL", "pb_gold", (type, _args) => new ExtraModifierModifier(type), undefined, "se/pb_bounce_1"), GOLDEN_POKEBALL: () => new ModifierType("modifierType:ModifierType.GOLDEN_POKEBALL", "pb_gold", (type, _args) => new ExtraModifierModifier(type), undefined, "se/pb_bounce_1"),
SILVER_POKEBALL: () => new ModifierType("modifierType:ModifierType.SILVER_POKEBALL", "pb_silver", (type, _args) => new TempExtraModifierModifier(type, 100), undefined, "se/pb_bounce_1"),
ENEMY_DAMAGE_BOOSTER: () => new ModifierType("modifierType:ModifierType.ENEMY_DAMAGE_BOOSTER", "wl_item_drop", (type, _args) => new EnemyDamageBoosterModifier(type, 5)), ENEMY_DAMAGE_BOOSTER: () => new ModifierType("modifierType:ModifierType.ENEMY_DAMAGE_BOOSTER", "wl_item_drop", (type, _args) => new EnemyDamageBoosterModifier(type, 5)),
ENEMY_DAMAGE_REDUCTION: () => new ModifierType("modifierType:ModifierType.ENEMY_DAMAGE_REDUCTION", "wl_guard_spec", (type, _args) => new EnemyDamageReducerModifier(type, 2.5)), ENEMY_DAMAGE_REDUCTION: () => new ModifierType("modifierType:ModifierType.ENEMY_DAMAGE_REDUCTION", "wl_guard_spec", (type, _args) => new EnemyDamageReducerModifier(type, 2.5)),
@ -1595,13 +1598,13 @@ export const modifierTypes = {
if (pregenArgs) { if (pregenArgs) {
return new PokemonBaseStatTotalModifierType(pregenArgs[0] as number); return new PokemonBaseStatTotalModifierType(pregenArgs[0] as number);
} }
return new PokemonBaseStatTotalModifierType(randSeedInt(20)); return new PokemonBaseStatTotalModifierType(randSeedInt(20, 1));
}), }),
MYSTERY_ENCOUNTER_OLD_GATEAU: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { MYSTERY_ENCOUNTER_OLD_GATEAU: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs) { if (pregenArgs) {
return new PokemonBaseStatFlatModifierType(pregenArgs[0] as number, pregenArgs[1] as Stat[]); return new PokemonBaseStatFlatModifierType(pregenArgs[0] as number, pregenArgs[1] as Stat[]);
} }
return new PokemonBaseStatFlatModifierType(randSeedInt(20), [ Stat.HP, Stat.ATK, Stat.DEF ]); return new PokemonBaseStatFlatModifierType(randSeedInt(20, 1), [ Stat.HP, Stat.ATK, Stat.DEF ]);
}), }),
MYSTERY_ENCOUNTER_BLACK_SLUDGE: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { MYSTERY_ENCOUNTER_BLACK_SLUDGE: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs) { if (pregenArgs) {

View File

@ -404,6 +404,14 @@ export abstract class LapsingPersistentModifier extends PersistentModifier {
this.battleCount = this.maxBattles; this.battleCount = this.maxBattles;
} }
/**
* Updates an existing modifier with a new `maxBattles` and `battleCount`.
*/
setNewBattleCount(count: number): void {
this.maxBattles = count;
this.battleCount = count;
}
getMaxBattles(): number { getMaxBattles(): number {
return this.maxBattles; return this.maxBattles;
} }
@ -960,7 +968,7 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier {
this.stackCount = pokemon this.stackCount = pokemon
? pokemon.evoCounter + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length ? pokemon.evoCounter + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length
+ pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier).length + pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length
: this.stackCount; : this.stackCount;
const text = scene.add.bitmapText(10, 15, "item-count", this.stackCount.toString(), 11); const text = scene.add.bitmapText(10, 15, "item-count", this.stackCount.toString(), 11);
@ -975,7 +983,7 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier {
getMaxHeldItemCount(pokemon: Pokemon): number { getMaxHeldItemCount(pokemon: Pokemon): number {
this.stackCount = pokemon.evoCounter + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length this.stackCount = pokemon.evoCounter + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length
+ pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier).length; + pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length;
return 999; return 999;
} }
} }
@ -2181,7 +2189,7 @@ export class PokemonNatureChangeModifier extends ConsumablePokemonModifier {
* @returns * @returns
*/ */
override apply(playerPokemon: PlayerPokemon): boolean { override apply(playerPokemon: PlayerPokemon): boolean {
playerPokemon.natureOverride = this.nature; playerPokemon.customPokemonData.nature = this.nature;
let speciesId = playerPokemon.species.speciesId; let speciesId = playerPokemon.species.speciesId;
playerPokemon.scene.gameData.dexData[speciesId].natureAttr |= 1 << (this.nature + 1); playerPokemon.scene.gameData.dexData[speciesId].natureAttr |= 1 << (this.nature + 1);
@ -3288,6 +3296,60 @@ export class ExtraModifierModifier extends PersistentModifier {
} }
} }
/**
* Modifier used for timed boosts to the player's shop item rewards.
* @extends LapsingPersistentModifier
* @see {@linkcode apply}
*/
export class TempExtraModifierModifier extends LapsingPersistentModifier {
constructor(type: ModifierType, maxBattles: number, battleCount?: number, stackCount?: number) {
super(type, maxBattles, battleCount, stackCount);
}
/**
* Goes through existing modifiers for any that match Silver Pokeball,
* which will then add the max count of the new item to the existing count of the current item.
* If no existing Silver Pokeballs are found, will add a new one.
* @param modifiers {@linkcode PersistentModifier} array of the player's modifiers
* @param _virtual N/A
* @param scene
* @returns true if the modifier was successfully added or applied, false otherwise
*/
add(modifiers: PersistentModifier[], _virtual: boolean, scene: BattleScene): boolean {
for (const modifier of modifiers) {
if (this.match(modifier)) {
const modifierInstance = modifier as TempExtraModifierModifier;
const newBattleCount = this.getMaxBattles() + modifierInstance.getBattleCount();
modifierInstance.setNewBattleCount(newBattleCount);
scene.playSound("se/restore");
return true;
}
}
modifiers.push(this);
return true;
}
clone() {
return new TempExtraModifierModifier(this.type, this.getMaxBattles(), this.getBattleCount(), this.stackCount);
}
match(modifier: Modifier): boolean {
return (modifier instanceof TempExtraModifierModifier);
}
/**
* Increases the current rewards in the battle by the `stackCount`.
* @returns `true` if the shop reward number modifier applies successfully
* @param count {@linkcode NumberHolder} that holds the resulting shop item reward count
*/
apply(count: NumberHolder): boolean {
count.value += this.getStackCount();
return true;
}
}
export abstract class EnemyPersistentModifier extends PersistentModifier { export abstract class EnemyPersistentModifier extends PersistentModifier {
constructor(type: ModifierType, stackCount?: number) { constructor(type: ModifierType, stackCount?: number) {
super(type, stackCount); super(type, stackCount);

View File

@ -75,6 +75,8 @@ class DefaultOverrides {
readonly ITEM_UNLOCK_OVERRIDE: Unlockables[] = []; readonly ITEM_UNLOCK_OVERRIDE: Unlockables[] = [];
/** Set to `true` to show all tutorials */ /** Set to `true` to show all tutorials */
readonly BYPASS_TUTORIAL_SKIP_OVERRIDE: boolean = false; readonly BYPASS_TUTORIAL_SKIP_OVERRIDE: boolean = false;
/** Set to `true` to force Paralysis and Freeze to always activate, or `false` to force them to not activate */
readonly STATUS_ACTIVATION_OVERRIDE: boolean | null = null;
// ---------------- // ----------------
// PLAYER OVERRIDES // PLAYER OVERRIDES
@ -113,8 +115,8 @@ class DefaultOverrides {
readonly STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE; readonly STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE;
readonly GENDER_OVERRIDE: Gender | null = null; readonly GENDER_OVERRIDE: Gender | null = null;
readonly MOVESET_OVERRIDE: Moves | Array<Moves> = []; readonly MOVESET_OVERRIDE: Moves | Array<Moves> = [];
readonly SHINY_OVERRIDE: boolean = false; readonly SHINY_OVERRIDE: boolean | null = null;
readonly VARIANT_OVERRIDE: Variant = 0; readonly VARIANT_OVERRIDE: Variant | null = null;
// -------------------------- // --------------------------
// OPPONENT / ENEMY OVERRIDES // OPPONENT / ENEMY OVERRIDES
@ -134,8 +136,8 @@ class DefaultOverrides {
readonly OPP_STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE; readonly OPP_STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE;
readonly OPP_GENDER_OVERRIDE: Gender | null = null; readonly OPP_GENDER_OVERRIDE: Gender | null = null;
readonly OPP_MOVESET_OVERRIDE: Moves | Array<Moves> = []; readonly OPP_MOVESET_OVERRIDE: Moves | Array<Moves> = [];
readonly OPP_SHINY_OVERRIDE: boolean = false; readonly OPP_SHINY_OVERRIDE: boolean | null = null;
readonly OPP_VARIANT_OVERRIDE: Variant = 0; readonly OPP_VARIANT_OVERRIDE: Variant | null = null;
readonly OPP_IVS_OVERRIDE: number | number[] = []; readonly OPP_IVS_OVERRIDE: number | number[] = [];
readonly OPP_FORM_OVERRIDES: Partial<Record<Species, number>> = {}; readonly OPP_FORM_OVERRIDES: Partial<Record<Species, number>> = {};
/** /**

View File

@ -30,6 +30,15 @@ export class CommandPhase extends FieldPhase {
start() { start() {
super.start(); super.start();
const commandUiHandler = this.scene.ui.handlers[Mode.COMMAND];
if (commandUiHandler) {
if (this.scene.currentBattle.turn === 1 || commandUiHandler.getCursor() === Command.POKEMON) {
commandUiHandler.setCursor(Command.FIGHT);
} else {
commandUiHandler.setCursor(commandUiHandler.getCursor());
}
}
if (this.fieldIndex) { if (this.fieldIndex) {
// If we somehow are attempting to check the right pokemon but there's only one pokemon out // If we somehow are attempting to check the right pokemon but there's only one pokemon out
// Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching // Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching

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 { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
import { getGoldenBugNetSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { getGoldenBugNetSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { Biome } from "#enums/biome"; import { Biome } from "#enums/biome";
import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters";
export class EncounterPhase extends BattlePhase { export class EncounterPhase extends BattlePhase {
private loaded: boolean; private loaded: boolean;
@ -68,7 +69,7 @@ export class EncounterPhase extends BattlePhase {
this.scene.executeWithSeedOffset(() => { this.scene.executeWithSeedOffset(() => {
const currentSessionEncounterType = battle.mysteryEncounterType; const currentSessionEncounterType = battle.mysteryEncounterType;
battle.mysteryEncounter = this.scene.getMysteryEncounter(currentSessionEncounterType); battle.mysteryEncounter = this.scene.getMysteryEncounter(currentSessionEncounterType);
}, battle.waveIndex << 4); }, battle.waveIndex * 16);
} }
const mysteryEncounter = battle.mysteryEncounter; const mysteryEncounter = battle.mysteryEncounter;
if (mysteryEncounter) { if (mysteryEncounter) {
@ -251,6 +252,13 @@ export class EncounterPhase extends BattlePhase {
this.scene.updateModifiers(true); 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()) { for (const pokemon of this.scene.getParty()) {
if (pokemon) { if (pokemon) {
pokemon.resetBattleData(); pokemon.resetBattleData();

View File

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

View File

@ -0,0 +1,84 @@
import BattleScene from "#app/battle-scene";
import { BattlerIndex } from "#app/battle";
import { MoveChargeAnim } from "#app/data/battle-anims";
import { applyMoveChargeAttrs, MoveEffectAttr, InstantChargeAttr } from "#app/data/move";
import Pokemon, { MoveResult, PokemonMove } from "#app/field/pokemon";
import { BooleanHolder } from "#app/utils";
import { MovePhase } from "#app/phases/move-phase";
import { PokemonPhase } from "#app/phases/pokemon-phase";
import { BattlerTagType } from "#enums/battler-tag-type";
import { MoveEndPhase } from "#app/phases/move-end-phase";
/**
* Phase for the "charging turn" of two-turn moves (e.g. Dig).
* @extends {@linkcode PokemonPhase}
*/
export class MoveChargePhase extends PokemonPhase {
/** The move instance that this phase applies */
public move: PokemonMove;
/** The field index targeted by the move (Charging moves assume single target) */
public targetIndex: BattlerIndex;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, targetIndex: BattlerIndex, move: PokemonMove) {
super(scene, battlerIndex);
this.move = move;
this.targetIndex = targetIndex;
}
public override start() {
super.start();
const user = this.getUserPokemon();
const target = this.getTargetPokemon();
const move = this.move.getMove();
// If the target is somehow not defined, or the move is somehow not a ChargingMove,
// immediately end this phase.
if (!target || !(move.isChargingMove())) {
console.warn("Invalid parameters for MoveChargePhase");
return super.end();
}
new MoveChargeAnim(move.chargeAnim, move.id, user).play(this.scene, false, () => {
move.showChargeText(user, target);
applyMoveChargeAttrs(MoveEffectAttr, user, target, move).then(() => {
user.addTag(BattlerTagType.CHARGING, 1, move.id, user.id);
this.end();
});
});
}
/** Checks the move's instant charge conditions, then ends this phase. */
public override end() {
const user = this.getUserPokemon();
const move = this.move.getMove();
if (move.isChargingMove()) {
const instantCharge = new BooleanHolder(false);
applyMoveChargeAttrs(InstantChargeAttr, user, null, move, instantCharge);
if (instantCharge.value) {
// this MoveEndPhase will be duplicated by the queued MovePhase if not removed
this.scene.tryRemovePhase((phase) => phase instanceof MoveEndPhase && phase.getPokemon() === user);
// queue a new MovePhase for this move's attack phase
this.scene.unshiftPhase(new MovePhase(this.scene, user, [ this.targetIndex ], this.move, false));
} else {
user.getMoveQueue().push({ move: move.id, targets: [ this.targetIndex ]});
}
// Add this move's charging phase to the user's move history
user.pushMoveHistory({ move: this.move.moveId, targets: [ this.targetIndex ], result: MoveResult.OTHER });
}
super.end();
}
public getUserPokemon(): Pokemon {
return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex];
}
public getTargetPokemon(): Pokemon | undefined {
return this.scene.getField(true).find((p) => this.targetIndex === p.getBattlerIndex());
}
}

View File

@ -4,7 +4,7 @@ import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr,
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
import { MoveAnim } from "#app/data/battle-anims"; import { MoveAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags"; import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags";
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, OneHitKOAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } from "#app/data/move"; import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, OneHitKOAttr, MoveEffectTrigger, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } from "#app/data/move";
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
import { BattlerTagType } from "#app/enums/battler-tag-type"; import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Moves } from "#app/enums/moves"; import { Moves } from "#app/enums/moves";
@ -108,7 +108,6 @@ export class MoveEffectPhase extends PokemonPhase {
* (and not random target) and failed the hit check against its target (MISS), log the move * (and not random target) and failed the hit check against its target (MISS), log the move
* as FAILed or MISSed (depending on the conditions above) and end this phase. * as FAILed or MISSed (depending on the conditions above) and end this phase.
*/ */
if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) { if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) {
this.stopMultiHit(); this.stopMultiHit();
if (hasActiveTargets) { if (hasActiveTargets) {
@ -241,15 +240,13 @@ export class MoveEffectPhase extends PokemonPhase {
user, target, move).then(() => { user, target, move).then(() => {
// All other effects require the move to not have failed or have been cancelled to trigger // All other effects require the move to not have failed or have been cancelled to trigger
if (hitResult !== HitResult.FAIL) { if (hitResult !== HitResult.FAIL) {
/** Are the move's effects tied to the first turn of a charge move? */
const chargeEffect = !!move.getAttrs(ChargeAttr).find(ca => ca.usedChargeEffect(user, this.getTarget() ?? null, move));
/** /**
* If the invoked move's effects are meant to trigger during the move's "charge turn," * If the invoked move's effects are meant to trigger during the move's "charge turn,"
* ignore all effects after this point. * ignore all effects after this point.
* Otherwise, apply all self-targeted POST_APPLY effects. * Otherwise, apply all self-targeted POST_APPLY effects.
*/ */
Utils.executeIf(!chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY
&& attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move)).then(() => { && attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move).then(() => {
// All effects past this point require the move to have hit the target // All effects past this point require the move to have hit the target
if (hitResult !== HitResult.NO_EFFECT) { if (hitResult !== HitResult.NO_EFFECT) {
// Apply all non-self-targeted POST_APPLY effects // Apply all non-self-targeted POST_APPLY effects
@ -267,7 +264,7 @@ export class MoveEffectPhase extends PokemonPhase {
} }
} }
// If the move was not protected against, apply all HIT effects // If the move was not protected against, apply all HIT effects
Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT Utils.executeIf(!isProtected, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT
&& (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => { && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => {
// Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them) // Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them)
return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => { return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => {
@ -280,10 +277,8 @@ export class MoveEffectPhase extends PokemonPhase {
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
} }
target.lapseTag(BattlerTagType.BEAK_BLAST_CHARGING); target.lapseTags(BattlerTagLapseType.AFTER_HIT);
if (move.category === MoveCategory.PHYSICAL && user.isPlayer() !== target.isPlayer()) {
target.lapseTag(BattlerTagType.SHELL_TRAP);
}
})).then(() => { })).then(() => {
// Apply the user's post-attack ability effects // Apply the user's post-attack ability effects
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => { applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => {

View File

@ -1,16 +1,17 @@
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs, BlockRedirectAbAttr, IncreasePpAbAttr, PokemonTypeChangeAbAttr, PostMoveUsedAbAttr, RedirectMoveAbAttr } from "#app/data/ability"; import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs, BlockRedirectAbAttr, IncreasePpAbAttr, PokemonTypeChangeAbAttr, PostMoveUsedAbAttr, RedirectMoveAbAttr, ReduceStatusEffectDurationAbAttr } from "#app/data/ability";
import { CommonAnim } from "#app/data/battle-anims"; import { CommonAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, CenterOfAttentionTag } from "#app/data/battler-tags"; import { BattlerTagLapseType, CenterOfAttentionTag } from "#app/data/battler-tags";
import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, ChargeAttr, CopyMoveAttr, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move"; import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, CopyMoveAttr, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move";
import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms";
import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect"; import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import { getTerrainBlockMessage } from "#app/data/weather"; import { getTerrainBlockMessage } from "#app/data/weather";
import { MoveUsedEvent } from "#app/events/battle-scene"; import { MoveUsedEvent } from "#app/events/battle-scene";
import Pokemon, { MoveResult, PokemonMove, TurnMove } from "#app/field/pokemon"; import Pokemon, { MoveResult, PokemonMove } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import Overrides from "#app/overrides";
import { BattlePhase } from "#app/phases/battle-phase"; import { BattlePhase } from "#app/phases/battle-phase";
import { CommonAnimPhase } from "#app/phases/common-anim-phase"; import { CommonAnimPhase } from "#app/phases/common-anim-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase";
@ -22,6 +23,7 @@ import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import i18next from "i18next"; import i18next from "i18next";
import { MoveChargePhase } from "#app/phases/move-charge-phase";
export class MovePhase extends BattlePhase { export class MovePhase extends BattlePhase {
protected _pokemon: Pokemon; protected _pokemon: Pokemon;
@ -134,6 +136,8 @@ export class MovePhase extends BattlePhase {
if (this.cancelled || this.failed) { if (this.cancelled || this.failed) {
this.handlePreMoveFailures(); this.handlePreMoveFailures();
} else if (this.move.getMove().isChargingMove() && !this.pokemon.getTag(BattlerTagType.CHARGING)) {
this.chargeMove();
} else { } else {
this.useMove(); this.useMove();
} }
@ -168,25 +172,31 @@ export class MovePhase extends BattlePhase {
switch (this.pokemon.status.effect) { switch (this.pokemon.status.effect) {
case StatusEffect.PARALYSIS: case StatusEffect.PARALYSIS:
if (!this.pokemon.randSeedInt(4)) { activated = (!this.pokemon.randSeedInt(4) || Overrides.STATUS_ACTIVATION_OVERRIDE === true) && Overrides.STATUS_ACTIVATION_OVERRIDE !== false;
activated = true;
this.cancelled = true;
}
break; break;
case StatusEffect.SLEEP: case StatusEffect.SLEEP:
applyMoveAttrs(BypassSleepAttr, this.pokemon, null, this.move.getMove()); applyMoveAttrs(BypassSleepAttr, this.pokemon, null, this.move.getMove());
healed = this.pokemon.status.turnCount === this.pokemon.status.cureTurn; const turnsRemaining = new NumberHolder(this.pokemon.status.sleepTurnsRemaining ?? 0);
applyAbAttrs(ReduceStatusEffectDurationAbAttr, this.pokemon, null, false, this.pokemon.status.effect, turnsRemaining);
this.pokemon.status.sleepTurnsRemaining = turnsRemaining.value;
healed = this.pokemon.status.sleepTurnsRemaining <= 0;
activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP); activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP);
this.cancelled = activated;
break; break;
case StatusEffect.FREEZE: case StatusEffect.FREEZE:
healed = !!this.move.getMove().findAttr(attr => attr instanceof HealStatusEffectAttr && attr.selfTarget && attr.isOfEffect(StatusEffect.FREEZE)) || !this.pokemon.randSeedInt(5); healed =
!!this.move.getMove().findAttr((attr) =>
attr instanceof HealStatusEffectAttr
&& attr.selfTarget
&& attr.isOfEffect(StatusEffect.FREEZE))
|| (!this.pokemon.randSeedInt(5) && Overrides.STATUS_ACTIVATION_OVERRIDE !== true)
|| Overrides.STATUS_ACTIVATION_OVERRIDE === false;
activated = !healed; activated = !healed;
this.cancelled = activated;
break; break;
} }
if (activated) { if (activated) {
this.cancel();
this.scene.queueMessage(getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon))); this.scene.queueMessage(getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)));
this.scene.unshiftPhase(new CommonAnimPhase(this.scene, this.pokemon.getBattlerIndex(), undefined, CommonAnim.POISON + (this.pokemon.status.effect - 1))); this.scene.unshiftPhase(new CommonAnimPhase(this.scene, this.pokemon.getBattlerIndex(), undefined, CommonAnim.POISON + (this.pokemon.status.effect - 1)));
} else if (healed) { } else if (healed) {
@ -219,12 +229,15 @@ export class MovePhase extends BattlePhase {
this.showMoveText(); this.showMoveText();
// TODO: Clean up implementation of two-turn moves.
if (moveQueue.length > 0) { if (moveQueue.length > 0) {
// Using .shift here clears out two turn moves once they've been used // Using .shift here clears out two turn moves once they've been used
this.ignorePp = moveQueue.shift()?.ignorePP ?? false; this.ignorePp = moveQueue.shift()?.ignorePP ?? false;
} }
if (this.pokemon.getTag(BattlerTagType.CHARGING)?.sourceMove === this.move.moveId) {
this.pokemon.lapseTag(BattlerTagType.CHARGING);
}
// "commit" to using the move, deducting PP. // "commit" to using the move, deducting PP.
if (!this.ignorePp) { if (!this.ignorePp) {
const ppUsed = 1 + this.getPpIncreaseFromPressure(targets); const ppUsed = 1 + this.getPpIncreaseFromPressure(targets);
@ -288,6 +301,9 @@ export class MovePhase extends BattlePhase {
} }
this.showFailedText(failedText); this.showFailedText(failedText);
// Remove the user from its semi-invulnerable state (if applicable)
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
} }
// Handle Dancer, which triggers immediately after a move is used (rather than waiting on `this.end()`). // Handle Dancer, which triggers immediately after a move is used (rather than waiting on `this.end()`).
@ -299,6 +315,35 @@ export class MovePhase extends BattlePhase {
} }
} }
/** Queues a {@linkcode MoveChargePhase} for this phase's invoked move. */
protected chargeMove() {
const move = this.move.getMove();
const targets = this.getActiveTargetPokemon();
if (move.applyConditions(this.pokemon, targets[0], move)) {
// Protean and Libero apply on the charging turn of charge moves
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove());
this.showMoveText();
this.scene.unshiftPhase(new MoveChargePhase(this.scene, this.pokemon.getBattlerIndex(), this.targets[0], this.move));
} else {
this.pokemon.pushMoveHistory({ move: this.move.moveId, targets: this.targets, result: MoveResult.FAIL, virtual: this.move.virtual });
let failedText: string | undefined;
const failureMessage = move.getFailedText(this.pokemon, targets[0], move, new BooleanHolder(false));
if (failureMessage) {
failedText = failureMessage;
}
this.showMoveText();
this.showFailedText(failedText);
// Remove the user from its semi-invulnerable state (if applicable)
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
}
}
/** /**
* Queues a {@linkcode MoveEndPhase} if the move wasn't a {@linkcode followUp} and {@linkcode canMove()} returns `true`, * Queues a {@linkcode MoveEndPhase} if the move wasn't a {@linkcode followUp} and {@linkcode canMove()} returns `true`,
* then ends the phase. * then ends the phase.
@ -412,8 +457,6 @@ export class MovePhase extends BattlePhase {
* - Lapses `AFTER_MOVE` tags: * - Lapses `AFTER_MOVE` tags:
* - This handles the effects of {@link Moves.SUBSTITUTE Substitute} * - This handles the effects of {@link Moves.SUBSTITUTE Substitute}
* - Removes the second turn of charge moves * - Removes the second turn of charge moves
*
* TODO: handle charge moves more gracefully
*/ */
protected handlePreMoveFailures(): void { protected handlePreMoveFailures(): void {
if (this.cancelled || this.failed) { if (this.cancelled || this.failed) {
@ -445,18 +488,7 @@ export class MovePhase extends BattlePhase {
return; return;
} }
if (this.move.getMove().hasAttr(ChargeAttr)) { if (this.pokemon.getTag(BattlerTagType.RECHARGING) || this.pokemon.getTag(BattlerTagType.INTERRUPTED)) {
const lastMove = this.pokemon.getLastXMoves() as TurnMove[];
if (!lastMove.length || lastMove[0].move !== this.move.getMove().id || lastMove[0].result !== MoveResult.OTHER) {
this.scene.queueMessage(i18next.t("battle:useMove", {
pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
moveName: this.move.getName()
}), 500);
return;
}
}
if (this.pokemon.getTag(BattlerTagType.RECHARGING || BattlerTagType.INTERRUPTED)) {
return; return;
} }

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()) { if (!availablePartyMembers[0].isOnField()) {
scene.pushPhase(new SummonPhase(scene, 0)); scene.pushPhase(new SummonPhase(scene, 0));

View File

@ -8,26 +8,26 @@ import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonPhase } from "./pokemon-phase"; import { PokemonPhase } from "./pokemon-phase";
export class ObtainStatusEffectPhase extends PokemonPhase { export class ObtainStatusEffectPhase extends PokemonPhase {
private statusEffect?: StatusEffect | undefined; private statusEffect?: StatusEffect;
private cureTurn?: integer | null; private turnsRemaining?: number;
private sourceText?: string | null; private sourceText?: string | null;
private sourcePokemon?: Pokemon | null; private sourcePokemon?: Pokemon | null;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect?: StatusEffect, cureTurn?: integer | null, sourceText?: string | null, sourcePokemon?: Pokemon | null) { constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect?: StatusEffect, turnsRemaining?: number, sourceText?: string | null, sourcePokemon?: Pokemon | null) {
super(scene, battlerIndex); super(scene, battlerIndex);
this.statusEffect = statusEffect; this.statusEffect = statusEffect;
this.cureTurn = cureTurn; this.turnsRemaining = turnsRemaining;
this.sourceText = sourceText; this.sourceText = sourceText;
this.sourcePokemon = sourcePokemon; // For tracking which Pokemon caused the status effect this.sourcePokemon = sourcePokemon;
} }
start() { start() {
const pokemon = this.getPokemon(); const pokemon = this.getPokemon();
if (pokemon && !pokemon.status) { if (pokemon && !pokemon.status) {
if (pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) { if (pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) {
if (this.cureTurn) { if (this.turnsRemaining) {
pokemon.status!.cureTurn = this.cureTurn; // TODO: is this bang correct? pokemon.status!.sleepTurnsRemaining = this.turnsRemaining;
} }
pokemon.updateInfo(true); pokemon.updateInfo(true);
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, false, () => { new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, false, () => {

View File

@ -18,7 +18,7 @@ export class PostSummonPhase extends PokemonPhase {
const pokemon = this.getPokemon(); const pokemon = this.getPokemon();
if (pokemon.status?.effect === StatusEffect.TOXIC) { if (pokemon.status?.effect === StatusEffect.TOXIC) {
pokemon.status.turnCount = 0; pokemon.status.toxicTurnCount = 0;
} }
this.scene.arena.applyTags(ArenaTrapTag, false, pokemon); this.scene.arena.applyTags(ArenaTrapTag, false, pokemon);

View File

@ -30,7 +30,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase {
damage.value = Math.max(pokemon.getMaxHp() >> 3, 1); damage.value = Math.max(pokemon.getMaxHp() >> 3, 1);
break; break;
case StatusEffect.TOXIC: case StatusEffect.TOXIC:
damage.value = Math.max(Math.floor((pokemon.getMaxHp() / 16) * pokemon.status.turnCount), 1); damage.value = Math.max(Math.floor((pokemon.getMaxHp() / 16) * pokemon.status.toxicTurnCount), 1);
break; break;
case StatusEffect.BURN: case StatusEffect.BURN:
damage.value = Math.max(pokemon.getMaxHp() >> 4, 1); damage.value = Math.max(pokemon.getMaxHp() >> 4, 1);

View File

@ -1,7 +1,7 @@
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { ModifierTier } from "#app/modifier/modifier-tier"; import { ModifierTier } from "#app/modifier/modifier-tier";
import { regenerateModifierPoolThresholds, ModifierTypeOption, ModifierType, getPlayerShopModifierTypeOptionsForWave, PokemonModifierType, FusePokemonModifierType, PokemonMoveModifierType, TmModifierType, RememberMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, ModifierPoolType, getPlayerModifierTypeOptions } from "#app/modifier/modifier-type"; import { regenerateModifierPoolThresholds, ModifierTypeOption, ModifierType, getPlayerShopModifierTypeOptionsForWave, PokemonModifierType, FusePokemonModifierType, PokemonMoveModifierType, TmModifierType, RememberMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, ModifierPoolType, getPlayerModifierTypeOptions } from "#app/modifier/modifier-type";
import { ExtraModifierModifier, HealShopCostModifier, Modifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import { ExtraModifierModifier, HealShopCostModifier, Modifier, PokemonHeldItemModifier, TempExtraModifierModifier } from "#app/modifier/modifier";
import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler"; import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler";
import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler"; import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
@ -45,6 +45,7 @@ export class SelectModifierPhase extends BattlePhase {
const modifierCount = new Utils.IntegerHolder(3); const modifierCount = new Utils.IntegerHolder(3);
if (this.isPlayer()) { if (this.isPlayer()) {
this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount); this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount);
this.scene.applyModifiers(TempExtraModifierModifier, true, modifierCount);
} }
// If custom modifiers are specified, overrides default item count // If custom modifiers are specified, overrides default item count
@ -274,7 +275,13 @@ export class SelectModifierPhase extends BattlePhase {
// Otherwise, continue with custom multiplier // Otherwise, continue with custom multiplier
multiplier = this.customModifierSettings.rerollMultiplier; multiplier = this.customModifierSettings.rerollMultiplier;
} }
return Math.min(Math.ceil(this.scene.currentBattle.waveIndex / 10) * baseValue * Math.pow(2, this.rerollCount) * multiplier, Number.MAX_SAFE_INTEGER);
const baseMultiplier = Math.min(Math.ceil(this.scene.currentBattle.waveIndex / 10) * baseValue * (2 ** this.rerollCount) * multiplier, Number.MAX_SAFE_INTEGER);
// Apply Black Sludge to reroll cost
const modifiedRerollCost = new NumberHolder(baseMultiplier);
this.scene.applyModifier(HealShopCostModifier, true, modifiedRerollCost);
return modifiedRerollCost.value;
} }
getPoolType(): ModifierPoolType { getPoolType(): ModifierPoolType {

View File

@ -64,7 +64,8 @@ export class StatStageChangePhase extends PokemonPhase {
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
if (!this.selfTarget && stages.value < 0) { 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) { if (!cancelled.value && !this.selfTarget && stages.value < 0) {

View File

@ -65,8 +65,9 @@ export class SwitchSummonPhase extends SummonPhase {
const pokemon = this.getPokemon(); const pokemon = this.getPokemon();
if (this.switchType === SwitchType.SWITCH) {
(this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id)); (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id));
if (this.switchType === SwitchType.SWITCH) {
const substitute = pokemon.getTag(SubstituteTag); const substitute = pokemon.getTag(SubstituteTag);
if (substitute) { if (substitute) {
this.scene.tweens.add({ this.scene.tweens.add({

View File

@ -25,12 +25,17 @@ export class VictoryPhase extends PokemonPhase {
start() { start() {
super.start(); super.start();
const isMysteryEncounter = this.scene.currentBattle.isBattleMysteryEncounter();
// update Pokemon defeated count except for MEs that disable it
if (!isMysteryEncounter || !this.scene.currentBattle.mysteryEncounter?.preventGameStatsUpdates) {
this.scene.gameData.gameStats.pokemonDefeated++; this.scene.gameData.gameStats.pokemonDefeated++;
}
const expValue = this.getPokemon().getExpValue(); const expValue = this.getPokemon().getExpValue();
this.scene.applyPartyExp(expValue, true); this.scene.applyPartyExp(expValue, true);
if (this.scene.currentBattle.isBattleMysteryEncounter()) { if (isMysteryEncounter) {
handleMysteryEncounterVictory(this.scene, false, this.isExpOnly); handleMysteryEncounterVictory(this.scene, false, this.isExpOnly);
return this.end(); return this.end();
} }

View File

@ -1569,6 +1569,10 @@ export class GameData {
} }
setPokemonSeen(pokemon: Pokemon, incrementCount: boolean = true, trainer: boolean = false): void { setPokemonSeen(pokemon: Pokemon, incrementCount: boolean = true, trainer: boolean = false): void {
// Some Mystery Encounters block updates to these stats
if (this.scene.currentBattle?.isBattleMysteryEncounter() && this.scene.currentBattle.mysteryEncounter?.preventGameStatsUpdates) {
return;
}
const dexEntry = this.dexData[pokemon.species.speciesId]; const dexEntry = this.dexData[pokemon.species.speciesId];
dexEntry.seenAttr |= pokemon.getDexAttr(); dexEntry.seenAttr |= pokemon.getDexAttr();
if (incrementCount) { if (incrementCount) {

View File

@ -12,7 +12,7 @@ import { loadBattlerTag } from "../data/battler-tags";
import { Biome } from "#enums/biome"; import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; 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 { export default class PokemonData {
public id: integer; public id: integer;
@ -33,7 +33,6 @@ export default class PokemonData {
public stats: integer[]; public stats: integer[];
public ivs: integer[]; public ivs: integer[];
public nature: Nature; public nature: Nature;
public natureOverride: Nature | -1;
public moveset: (PokemonMove | null)[]; public moveset: (PokemonMove | null)[];
public status: Status | null; public status: Status | null;
public friendship: integer; public friendship: integer;
@ -54,14 +53,20 @@ export default class PokemonData {
public fusionVariant: Variant; public fusionVariant: Variant;
public fusionGender: Gender; public fusionGender: Gender;
public fusionLuck: integer; public fusionLuck: integer;
public fusionMysteryEncounterPokemonData: MysteryEncounterPokemonData;
public boss: boolean; public boss: boolean;
public bossSegments?: integer; public bossSegments?: integer;
public summonData: PokemonSummonData; public summonData: PokemonSummonData;
/** Data that can customize a Pokemon in non-standard ways from its Species */ /** 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) { constructor(source: Pokemon | any, forHistory: boolean = false) {
const sourcePokemon = source instanceof Pokemon ? source : null; const sourcePokemon = source instanceof Pokemon ? source : null;
@ -87,7 +92,6 @@ export default class PokemonData {
this.stats = source.stats; this.stats = source.stats;
this.ivs = source.ivs; this.ivs = source.ivs;
this.nature = source.nature !== undefined ? source.nature : 0 as Nature; this.nature = source.nature !== undefined ? source.nature : 0 as Nature;
this.natureOverride = source.natureOverride !== undefined ? source.natureOverride : -1;
this.friendship = source.friendship !== undefined ? source.friendship : getPokemonSpecies(this.species).baseFriendship; this.friendship = source.friendship !== undefined ? source.friendship : getPokemonSpecies(this.species).baseFriendship;
this.metLevel = source.metLevel || 5; this.metLevel = source.metLevel || 5;
this.metBiome = source.metBiome !== undefined ? source.metBiome : -1; this.metBiome = source.metBiome !== undefined ? source.metBiome : -1;
@ -107,9 +111,15 @@ export default class PokemonData {
this.fusionVariant = source.fusionVariant; this.fusionVariant = source.fusionVariant;
this.fusionGender = source.fusionGender; this.fusionGender = source.fusionGender;
this.fusionLuck = source.fusionLuck !== undefined ? source.fusionLuck : (source.fusionShiny ? source.fusionVariant + 1 : 0); this.fusionLuck = source.fusionLuck !== undefined ? source.fusionLuck : (source.fusionShiny ? source.fusionVariant + 1 : 0);
this.fusionCustomPokemonData = new CustomPokemonData(source.fusionCustomPokemonData);
this.usedTMs = source.usedTMs ?? []; this.usedTMs = source.usedTMs ?? [];
this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(source.mysteryEncounterPokemonData); this.customPokemonData = new CustomPokemonData(source.customPokemonData);
// Deprecated, but needed for session data migration
this.natureOverride = source.natureOverride;
this.mysteryEncounterPokemonData = new CustomPokemonData(source.mysteryEncounterPokemonData);
this.fusionMysteryEncounterPokemonData = new CustomPokemonData(source.fusionMysteryEncounterPokemonData);
if (!forHistory) { if (!forHistory) {
this.boss = (source instanceof EnemyPokemon && !!source.bossSegments) || (!this.player && !!source.boss); this.boss = (source instanceof EnemyPokemon && !!source.bossSegments) || (!this.player && !!source.boss);
@ -128,7 +138,7 @@ export default class PokemonData {
this.moveset = (source.moveset || [ new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.GROWL) ]).filter(m => m).map((m: any) => new PokemonMove(m.moveId, m.ppUsed, m.ppUp)); this.moveset = (source.moveset || [ new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.GROWL) ]).filter(m => m).map((m: any) => new PokemonMove(m.moveId, m.ppUsed, m.ppUp));
if (!forHistory) { if (!forHistory) {
this.status = source.status this.status = source.status
? new Status(source.status.effect, source.status.turnCount, source.status.cureTurn) ? new Status(source.status.effect, source.status.toxicTurnCount, source.status.sleepTurnsRemaining)
: null; : null;
} }

View File

@ -4,6 +4,9 @@ import { version } from "../../../package.json";
// --- v1.0.4 (and below) PATCHES --- // // --- v1.0.4 (and below) PATCHES --- //
import * as v1_0_4 from "./versions/v1_0_4"; 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)); const LATEST_VERSION = version.split(".").map(value => parseInt(value));
/** /**
@ -131,6 +134,10 @@ class SessionVersionConverter extends VersionConverter {
this.callMigrators(data, v1_0_4.sessionMigrators); 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}!`); console.log(`Session data successfully migrated to v${version}!`);
@ -153,6 +160,10 @@ class SystemVersionConverter extends VersionConverter {
this.callMigrators(data, v1_0_4.systemMigrators); 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}!`); console.log(`System data successfully migrated to v${version}!`);
@ -175,6 +186,10 @@ class SettingsVersionConverter extends VersionConverter {
this.callMigrators(data, v1_0_4.settingsMigrators); 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}!`); console.log(`System data successfully migrated to v${version}!`);

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,93 @@
import { Status } from "#app/data/status-effect";
import { MoveResult } from "#app/field/pokemon";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { StatusEffect } from "#enums/status-effect";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Abilities - Early Bird", () => {
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.REST, Moves.BELLY_DRUM, Moves.SPLASH ])
.ability(Abilities.EARLY_BIRD)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("reduces Rest's sleep time to 1 turn", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
const player = game.scene.getPlayerPokemon()!;
game.move.select(Moves.BELLY_DRUM);
await game.toNextTurn();
game.move.select(Moves.REST);
await game.toNextTurn();
expect(player.status?.effect).toBe(StatusEffect.SLEEP);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(player.status?.effect).toBe(StatusEffect.SLEEP);
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(player.status?.effect).toBeUndefined();
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
});
it("reduces 3-turn sleep to 1 turn", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
const player = game.scene.getPlayerPokemon()!;
player.status = new Status(StatusEffect.SLEEP, 0, 4);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(player.status?.effect).toBe(StatusEffect.SLEEP);
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(player.status?.effect).toBeUndefined();
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
});
it("reduces 1-turn sleep to 0 turns", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
const player = game.scene.getPlayerPokemon()!;
player.status = new Status(StatusEffect.SLEEP, 0, 2);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(player.status?.effect).toBeUndefined();
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
});
});

View File

@ -36,9 +36,7 @@ describe("Abilities - Imposter", () => {
}); });
it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => { it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => {
await game.startBattle([ await game.classicMode.startBattle([ Species.DITTO ]);
Species.DITTO
]);
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
@ -78,9 +76,7 @@ describe("Abilities - Imposter", () => {
it("should copy in-battle overridden stats", async () => { it("should copy in-battle overridden stats", async () => {
game.override.enemyMoveset([ Moves.POWER_SPLIT ]); game.override.enemyMoveset([ Moves.POWER_SPLIT ]);
await game.startBattle([ await game.classicMode.startBattle([ Species.DITTO ]);
Species.DITTO
]);
const player = game.scene.getPlayerPokemon()!; const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;
@ -97,4 +93,18 @@ describe("Abilities - Imposter", () => {
expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk); expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk); expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
}); });
it("should set each move's pp to a maximum of 5", async () => {
game.override.enemyMoveset([ Moves.SWORDS_DANCE, Moves.GROWL, Moves.SKETCH, Moves.RECOVER ]);
await game.classicMode.startBattle([ Species.DITTO ]);
const player = game.scene.getPlayerPokemon()!;
game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to(TurnEndPhase);
player.getMoveset().forEach(move => {
expect(move!.getMovePp()).toBeLessThanOrEqual(5);
});
});
}); });

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

@ -150,7 +150,7 @@ describe("Abilities - Magic Guard", () => {
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
const toxicStartCounter = enemyPokemon.status!.turnCount; const toxicStartCounter = enemyPokemon.status!.toxicTurnCount;
//should be 0 //should be 0
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
@ -162,7 +162,7 @@ describe("Abilities - Magic Guard", () => {
* - The enemy Pokemon's hypothetical CatchRateMultiplier should be 1.5 * - The enemy Pokemon's hypothetical CatchRateMultiplier should be 1.5
*/ */
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(enemyPokemon.status!.turnCount).toBeGreaterThan(toxicStartCounter); expect(enemyPokemon.status!.toxicTurnCount).toBeGreaterThan(toxicStartCounter);
expect(getStatusEffectCatchRateMultiplier(enemyPokemon.status!.effect)).toBe(1.5); expect(getStatusEffectCatchRateMultiplier(enemyPokemon.status!.effect)).toBe(1.5);
} }
); );

View File

@ -52,6 +52,7 @@ describe("Abilities - Volt Absorb", () => {
expect(playerPokemon.getTag(BattlerTagType.CHARGED)).toBeDefined(); expect(playerPokemon.getTag(BattlerTagType.CHARGED)).toBeDefined();
expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase");
}); });
it("should activate regardless of accuracy checks", async () => { it("should activate regardless of accuracy checks", async () => {
game.override.moveset(Moves.THUNDERBOLT); game.override.moveset(Moves.THUNDERBOLT);
game.override.enemyMoveset(Moves.SPLASH); game.override.enemyMoveset(Moves.SPLASH);
@ -71,6 +72,7 @@ describe("Abilities - Volt Absorb", () => {
await game.phaseInterceptor.to("BerryPhase", false); await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
}); });
it("regardless of accuracy should not trigger on pokemon in semi invulnerable state", async () => { it("regardless of accuracy should not trigger on pokemon in semi invulnerable state", async () => {
game.override.moveset(Moves.THUNDERBOLT); game.override.moveset(Moves.THUNDERBOLT);
game.override.enemyMoveset(Moves.DIVE); game.override.enemyMoveset(Moves.DIVE);
@ -84,9 +86,7 @@ describe("Abilities - Volt Absorb", () => {
game.move.select(Moves.THUNDERBOLT); game.move.select(Moves.THUNDERBOLT);
enemyPokemon.hp = enemyPokemon.hp - 1; enemyPokemon.hp = enemyPokemon.hp - 1;
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("MoveEffectPhase");
await game.move.forceMiss();
await game.phaseInterceptor.to("BerryPhase", false); await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
}); });

View File

@ -1,8 +1,8 @@
import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move"; import { allMoves } from "#app/data/move";
import { Abilities } from "#app/enums/abilities"; import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#app/enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { BattlerTagType } from "#enums/battler-tag-type";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
@ -31,7 +31,8 @@ describe("Arena - Gravity", () => {
.ability(Abilities.UNNERVE) .ability(Abilities.UNNERVE)
.enemyAbility(Abilities.BALL_FETCH) .enemyAbility(Abilities.BALL_FETCH)
.enemySpecies(Species.SHUCKLE) .enemySpecies(Species.SHUCKLE)
.enemyMoveset(Moves.SPLASH); .enemyMoveset(Moves.SPLASH)
.enemyLevel(5);
}); });
// Reference: https://bulbapedia.bulbagarden.net/wiki/Gravity_(move) // Reference: https://bulbapedia.bulbagarden.net/wiki/Gravity_(move)
@ -42,102 +43,121 @@ describe("Arena - Gravity", () => {
vi.spyOn(moveToCheck, "calculateBattleAccuracy"); vi.spyOn(moveToCheck, "calculateBattleAccuracy");
// Setup Gravity on first turn // Setup Gravity on first turn
await game.startBattle([ Species.PIKACHU ]); await game.classicMode.startBattle([ Species.PIKACHU ]);
game.move.select(Moves.GRAVITY); game.move.select(Moves.GRAVITY);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
// Use non-OHKO move on second turn // Use non-OHKO move on second turn
await game.toNextTurn(); await game.toNextTurn();
game.move.select(Moves.TACKLE); game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to(MoveEffectPhase); await game.phaseInterceptor.to("MoveEffectPhase");
expect(moveToCheck.calculateBattleAccuracy).toHaveReturnedWith(100 * 1.67); expect(moveToCheck.calculateBattleAccuracy).toHaveLastReturnedWith(100 * 1.67);
}); });
it("OHKO move accuracy is not affected", async () => { it("OHKO move accuracy is not affected", async () => {
game.override.startingLevel(5);
game.override.enemyLevel(5);
/** See Fissure {@link https://bulbapedia.bulbagarden.net/wiki/Fissure_(move)} */ /** See Fissure {@link https://bulbapedia.bulbagarden.net/wiki/Fissure_(move)} */
const moveToCheck = allMoves[Moves.FISSURE]; const moveToCheck = allMoves[Moves.FISSURE];
vi.spyOn(moveToCheck, "calculateBattleAccuracy"); vi.spyOn(moveToCheck, "calculateBattleAccuracy");
// Setup Gravity on first turn // Setup Gravity on first turn
await game.startBattle([ Species.PIKACHU ]); await game.classicMode.startBattle([ Species.PIKACHU ]);
game.move.select(Moves.GRAVITY); game.move.select(Moves.GRAVITY);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
// Use OHKO move on second turn // Use OHKO move on second turn
await game.toNextTurn(); await game.toNextTurn();
game.move.select(Moves.FISSURE); game.move.select(Moves.FISSURE);
await game.phaseInterceptor.to(MoveEffectPhase); await game.phaseInterceptor.to("MoveEffectPhase");
expect(moveToCheck.calculateBattleAccuracy).toHaveReturnedWith(30); expect(moveToCheck.calculateBattleAccuracy).toHaveLastReturnedWith(30);
}); });
describe("Against flying types", () => { describe("Against flying types", () => {
it("can be hit by ground-type moves now", async () => { it("can be hit by ground-type moves now", async () => {
game.override game.override
.startingLevel(5)
.enemyLevel(5)
.enemySpecies(Species.PIDGEOT) .enemySpecies(Species.PIDGEOT)
.moveset([ Moves.GRAVITY, Moves.EARTHQUAKE ]); .moveset([ Moves.GRAVITY, Moves.EARTHQUAKE ]);
await game.startBattle([ Species.PIKACHU ]); await game.classicMode.startBattle([ Species.PIKACHU ]);
const pidgeot = game.scene.getEnemyPokemon()!; const pidgeot = game.scene.getEnemyPokemon()!;
vi.spyOn(pidgeot, "getAttackTypeEffectiveness"); vi.spyOn(pidgeot, "getAttackTypeEffectiveness");
// Try earthquake on 1st turn (fails!); // Try earthquake on 1st turn (fails!);
game.move.select(Moves.EARTHQUAKE); game.move.select(Moves.EARTHQUAKE);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(pidgeot.getAttackTypeEffectiveness).toHaveReturnedWith(0); expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(0);
// Setup Gravity on 2nd turn // Setup Gravity on 2nd turn
await game.toNextTurn(); await game.toNextTurn();
game.move.select(Moves.GRAVITY); game.move.select(Moves.GRAVITY);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
// Use ground move on 3rd turn // Use ground move on 3rd turn
await game.toNextTurn(); await game.toNextTurn();
game.move.select(Moves.EARTHQUAKE); game.move.select(Moves.EARTHQUAKE);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(pidgeot.getAttackTypeEffectiveness).toHaveReturnedWith(1); expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(1);
}); });
it("keeps super-effective moves super-effective after using gravity", async () => { it("keeps super-effective moves super-effective after using gravity", async () => {
game.override game.override
.startingLevel(5)
.enemyLevel(5)
.enemySpecies(Species.PIDGEOT) .enemySpecies(Species.PIDGEOT)
.moveset([ Moves.GRAVITY, Moves.THUNDERBOLT ]); .moveset([ Moves.GRAVITY, Moves.THUNDERBOLT ]);
await game.startBattle([ Species.PIKACHU ]); await game.classicMode.startBattle([ Species.PIKACHU ]);
const pidgeot = game.scene.getEnemyPokemon()!; const pidgeot = game.scene.getEnemyPokemon()!;
vi.spyOn(pidgeot, "getAttackTypeEffectiveness"); vi.spyOn(pidgeot, "getAttackTypeEffectiveness");
// Setup Gravity on 1st turn // Setup Gravity on 1st turn
game.move.select(Moves.GRAVITY); game.move.select(Moves.GRAVITY);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
// Use electric move on 2nd turn // Use electric move on 2nd turn
await game.toNextTurn(); await game.toNextTurn();
game.move.select(Moves.THUNDERBOLT); game.move.select(Moves.THUNDERBOLT);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(pidgeot.getAttackTypeEffectiveness).toHaveReturnedWith(2); expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(2);
}); });
}); });
it("cancels Fly if its user is semi-invulnerable", async () => {
game.override
.enemySpecies(Species.SNORLAX)
.enemyMoveset(Moves.FLY)
.moveset([ Moves.GRAVITY, Moves.SPLASH ]);
await game.classicMode.startBattle([ Species.CHARIZARD ]);
const charizard = game.scene.getPlayerPokemon()!;
const snorlax = game.scene.getEnemyPokemon()!;
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(snorlax.getTag(BattlerTagType.FLYING)).toBeDefined();
game.move.select(Moves.GRAVITY);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(snorlax.getTag(BattlerTagType.INTERRUPTED)).toBeDefined();
await game.phaseInterceptor.to("TurnEndPhase");
expect(charizard.hp).toBe(charizard.getMaxHp());
});
}); });

View File

@ -1,4 +1,5 @@
import { import {
Status,
StatusEffect, StatusEffect,
getStatusEffectActivationText, getStatusEffectActivationText,
getStatusEffectDescriptor, getStatusEffectDescriptor,
@ -6,14 +7,19 @@ import {
getStatusEffectObtainText, getStatusEffectObtainText,
getStatusEffectOverlapText, getStatusEffectOverlapText,
} from "#app/data/status-effect"; } from "#app/data/status-effect";
import { MoveResult } from "#app/field/pokemon";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import { mockI18next } from "#test/utils/testUtils"; import { mockI18next } from "#test/utils/testUtils";
import i18next from "i18next"; import i18next from "i18next";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const pokemonName = "PKM"; const pokemonName = "PKM";
const sourceText = "SOURCE"; const sourceText = "SOURCE";
describe("status-effect", () => { describe("Status Effect Messages", () => {
beforeAll(() => { beforeAll(() => {
i18next.init(); i18next.init();
}); });
@ -299,3 +305,99 @@ describe("status-effect", () => {
vi.resetAllMocks(); vi.resetAllMocks();
}); });
}); });
describe("Status Effects", () => {
describe("Paralysis", () => {
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
.enemySpecies(Species.MAGIKARP)
.enemyMoveset(Moves.SPLASH)
.enemyAbility(Abilities.BALL_FETCH)
.moveset([ Moves.QUICK_ATTACK ])
.ability(Abilities.BALL_FETCH)
.statusEffect(StatusEffect.PARALYSIS);
});
it("causes the pokemon's move to fail when activated", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.QUICK_ATTACK);
await game.move.forceStatusActivation(true);
await game.toNextTurn();
expect(game.scene.getEnemyPokemon()!.isFullHp()).toBe(true);
expect(game.scene.getPlayerPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
});
});
describe("Sleep", () => {
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.SPLASH ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should last the appropriate number of turns", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
const player = game.scene.getPlayerPokemon()!;
player.status = new Status(StatusEffect.SLEEP, 0, 4);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(player.status.effect).toBe(StatusEffect.SLEEP);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(player.status.effect).toBe(StatusEffect.SLEEP);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(player.status.effect).toBe(StatusEffect.SLEEP);
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(player.status?.effect).toBeUndefined();
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
});
});
});

View File

@ -7,8 +7,6 @@ import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const TIMEOUT = 20 * 1000;
describe("Items - Toxic orb", () => { describe("Items - Toxic orb", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
let game: GameManager; let game: GameManager;
@ -27,10 +25,10 @@ describe("Items - Toxic orb", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override game.override
.battleType("single") .battleType("single")
.enemySpecies(Species.RATTATA) .enemySpecies(Species.MAGIKARP)
.ability(Abilities.BALL_FETCH) .ability(Abilities.BALL_FETCH)
.enemyAbility(Abilities.BALL_FETCH) .enemyAbility(Abilities.BALL_FETCH)
.moveset([ Moves.SPLASH ]) .moveset(Moves.SPLASH)
.enemyMoveset(Moves.SPLASH) .enemyMoveset(Moves.SPLASH)
.startingHeldItems([{ .startingHeldItems([{
name: "TOXIC_ORB", name: "TOXIC_ORB",
@ -39,22 +37,19 @@ describe("Items - Toxic orb", () => {
vi.spyOn(i18next, "t"); vi.spyOn(i18next, "t");
}); });
it("badly poisons the holder", async () => { it("should badly poison the holder", async () => {
await game.classicMode.startBattle([ Species.MIGHTYENA ]); await game.classicMode.startBattle([ Species.FEEBAS ]);
const player = game.scene.getPlayerField()[0]; const player = game.scene.getPlayerPokemon()!;
expect(player.getHeldItems()[0].type.id).toBe("TOXIC_ORB");
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase"); await game.phaseInterceptor.to("TurnEndPhase");
// Toxic orb should trigger here await game.phaseInterceptor.to("MessagePhase");
await game.phaseInterceptor.run("MessagePhase");
expect(i18next.t).toHaveBeenCalledWith("statusEffect:toxic.obtainSource", expect.anything()); expect(i18next.t).toHaveBeenCalledWith("statusEffect:toxic.obtainSource", expect.anything());
await game.toNextTurn();
expect(player.status?.effect).toBe(StatusEffect.TOXIC); expect(player.status?.effect).toBe(StatusEffect.TOXIC);
// Damage should not have ticked yet. expect(player.status?.toxicTurnCount).toBe(0);
expect(player.status?.turnCount).toBe(0); });
}, TIMEOUT);
}); });

View File

@ -111,7 +111,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) =
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (defender.scene.arena.getTagOnSide(ArenaTagType.AURORA_VEIL, side)) { 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; return move.power * multiplierHolder.value;

View File

@ -34,7 +34,7 @@ describe("Moves - Baton Pass", () => {
.disableCrits(); .disableCrits();
}); });
it("transfers all stat stages when player uses it", async() => { it("transfers all stat stages when player uses it", async () => {
// arrange // arrange
await game.classicMode.startBattle([ Species.RAICHU, Species.SHUCKLE ]); await game.classicMode.startBattle([ Species.RAICHU, Species.SHUCKLE ]);
@ -91,7 +91,7 @@ describe("Moves - Baton Pass", () => {
]); ]);
}, 20000); }, 20000);
it("doesn't transfer effects that aren't transferrable", async() => { it("doesn't transfer effects that aren't transferrable", async () => {
game.override.enemyMoveset([ Moves.SALT_CURE ]); game.override.enemyMoveset([ Moves.SALT_CURE ]);
await game.classicMode.startBattle([ Species.PIKACHU, Species.FEEBAS ]); await game.classicMode.startBattle([ Species.PIKACHU, Species.FEEBAS ]);
@ -106,4 +106,28 @@ describe("Moves - Baton Pass", () => {
expect(player2.findTag((t) => t.tagType === BattlerTagType.SALT_CURED)).toBeUndefined(); expect(player2.findTag((t) => t.tagType === BattlerTagType.SALT_CURED)).toBeUndefined();
}, 20000); }, 20000);
it("doesn't allow binding effects from the user to persist", async () => {
game.override.moveset([ Moves.FIRE_SPIN, Moves.BATON_PASS ]);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]);
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.FIRE_SPIN);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.move.forceHit();
await game.toNextTurn();
expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeDefined();
game.move.select(Moves.BATON_PASS);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeUndefined();
});
}); });

114
src/test/moves/dig.test.ts Normal file
View File

@ -0,0 +1,114 @@
import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { StatusEffect } from "#enums/status-effect";
import { MoveResult } from "#app/field/pokemon";
import { describe, beforeAll, afterEach, beforeEach, it, expect } from "vitest";
import GameManager from "#test/utils/gameManager";
describe("Moves - Dig", () => {
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.DIG)
.battleType("single")
.startingLevel(100)
.enemySpecies(Species.SNORLAX)
.enemyLevel(100)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.TACKLE);
});
it("should make the user semi-invulnerable, then attack over 2 turns", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.DIG);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.UNDERGROUND)).toBeDefined();
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveQueue()[0].move).toBe(Moves.DIG);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.UNDERGROUND)).toBeUndefined();
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
const playerDig = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIG);
expect(playerDig?.ppUsed).toBe(1);
});
it("should not allow the user to evade attacks from Pokemon with No Guard", async () => {
game.override.enemyAbility(Abilities.NO_GUARD);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.DIG);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
});
it("should not expend PP when the attack phase is cancelled", async () => {
game.override
.enemyAbility(Abilities.NO_GUARD)
.enemyMoveset(Moves.SPORE);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.DIG);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.UNDERGROUND)).toBeUndefined();
expect(playerPokemon.status?.effect).toBe(StatusEffect.SLEEP);
const playerDig = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIG);
expect(playerDig?.ppUsed).toBe(0);
});
it("should cause the user to take double damage from Earthquake", async () => {
await game.classicMode.startBattle([ Species.DONDOZO ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
const preDigEarthquakeDmg = playerPokemon.getAttackDamage(enemyPokemon, allMoves[Moves.EARTHQUAKE]).damage;
game.move.select(Moves.DIG);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("MoveEffectPhase");
const postDigEarthquakeDmg = playerPokemon.getAttackDamage(enemyPokemon, allMoves[Moves.EARTHQUAKE]).damage;
// these hopefully get avoid rounding errors :shrug:
expect(postDigEarthquakeDmg).toBeGreaterThanOrEqual(2 * preDigEarthquakeDmg);
expect(postDigEarthquakeDmg).toBeLessThan(2 * (preDigEarthquakeDmg + 1));
});
});

137
src/test/moves/dive.test.ts Normal file
View File

@ -0,0 +1,137 @@
import { BattlerTagType } from "#enums/battler-tag-type";
import { StatusEffect } from "#enums/status-effect";
import { MoveResult } from "#app/field/pokemon";
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, it, expect } from "vitest";
import { WeatherType } from "#enums/weather-type";
describe("Moves - Dive", () => {
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.DIVE)
.battleType("single")
.startingLevel(100)
.enemySpecies(Species.SNORLAX)
.enemyLevel(100)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.TACKLE);
});
it("should make the user semi-invulnerable, then attack over 2 turns", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.DIVE);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeDefined();
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveQueue()[0].move).toBe(Moves.DIVE);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeUndefined();
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
const playerDive = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIVE);
expect(playerDive?.ppUsed).toBe(1);
});
it("should not allow the user to evade attacks from Pokemon with No Guard", async () => {
game.override.enemyAbility(Abilities.NO_GUARD);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.DIVE);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
});
it("should not expend PP when the attack phase is cancelled", async () => {
game.override
.enemyAbility(Abilities.NO_GUARD)
.enemyMoveset(Moves.SPORE);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.DIVE);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeUndefined();
expect(playerPokemon.status?.effect).toBe(StatusEffect.SLEEP);
const playerDive = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIVE);
expect(playerDive?.ppUsed).toBe(0);
});
it("should trigger on-contact post-defend ability effects", async () => {
game.override
.enemyAbility(Abilities.ROUGH_SKIN)
.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.DIVE);
await game.phaseInterceptor.to("TurnEndPhase");
await game.phaseInterceptor.to("MoveEndPhase");
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
expect(enemyPokemon.battleData.abilitiesApplied[0]).toBe(Abilities.ROUGH_SKIN);
});
it("should cancel attack after Harsh Sunlight is set", async () => {
game.override.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.DIVE);
await game.phaseInterceptor.to("TurnEndPhase");
await game.phaseInterceptor.to("TurnStartPhase", false);
game.scene.arena.trySetWeather(WeatherType.HARSH_SUN, false);
await game.phaseInterceptor.to("MoveEndPhase");
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeUndefined();
const playerDive = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIVE);
expect(playerDive?.ppUsed).toBe(1);
});
});

View File

@ -0,0 +1,104 @@
import { BattlerTagType } from "#enums/battler-tag-type";
import { Stat } from "#enums/stat";
import { WeatherType } from "#enums/weather-type";
import { MoveResult } from "#app/field/pokemon";
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, it, expect } from "vitest";
describe("Moves - Electro Shot", () => {
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.ELECTRO_SHOT)
.battleType("single")
.startingLevel(100)
.enemySpecies(Species.SNORLAX)
.enemyLevel(100)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should increase the user's Sp. Atk on the first turn, then attack on the second turn", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.ELECTRO_SHOT);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeDefined();
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.OTHER);
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined();
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
const playerElectroShot = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.ELECTRO_SHOT);
expect(playerElectroShot?.ppUsed).toBe(1);
});
it.each([
{ weatherType: WeatherType.RAIN, name: "Rain" },
{ weatherType: WeatherType.HEAVY_RAIN, name: "Heavy Rain" }
])("should fully resolve in one turn if $name is active", async ({ weatherType }) => {
game.override.weather(weatherType);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.ELECTRO_SHOT);
await game.phaseInterceptor.to("MoveEffectPhase", false);
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
await game.phaseInterceptor.to("MoveEndPhase");
expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined();
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
const playerElectroShot = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.ELECTRO_SHOT);
expect(playerElectroShot?.ppUsed).toBe(1);
});
it("should only increase Sp. Atk once with Multi-Lens", async () => {
game.override
.weather(WeatherType.RAIN)
.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.ELECTRO_SHOT);
await game.phaseInterceptor.to("MoveEndPhase");
expect(playerPokemon.turnData.hitCount).toBe(2);
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
});
});

122
src/test/moves/fly.test.ts Normal file
View File

@ -0,0 +1,122 @@
import { BattlerTagType } from "#enums/battler-tag-type";
import { StatusEffect } from "#enums/status-effect";
import { MoveResult } from "#app/field/pokemon";
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, it, expect, vi } from "vitest";
import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move";
describe("Moves - Fly", () => {
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.FLY)
.battleType("single")
.startingLevel(100)
.enemySpecies(Species.SNORLAX)
.enemyLevel(100)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.TACKLE);
vi.spyOn(allMoves[Moves.FLY], "accuracy", "get").mockReturnValue(100);
});
it("should make the user semi-invulnerable, then attack over 2 turns", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.FLY);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeDefined();
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveQueue()[0].move).toBe(Moves.FLY);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeUndefined();
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.FLY);
expect(playerFly?.ppUsed).toBe(1);
});
it("should not allow the user to evade attacks from Pokemon with No Guard", async () => {
game.override.enemyAbility(Abilities.NO_GUARD);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.FLY);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
});
it("should not expend PP when the attack phase is cancelled", async () => {
game.override
.enemyAbility(Abilities.NO_GUARD)
.enemyMoveset(Moves.SPORE);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.FLY);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeUndefined();
expect(playerPokemon.status?.effect).toBe(StatusEffect.SLEEP);
const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.FLY);
expect(playerFly?.ppUsed).toBe(0);
});
it("should be cancelled when another Pokemon uses Gravity", async () => {
game.override.enemyMoveset([ Moves.SPLASH, Moves.GRAVITY ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.FLY);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
await game.forceEnemyMove(Moves.GRAVITY);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.FLY);
expect(playerFly?.ppUsed).toBe(0);
});
});

View File

@ -0,0 +1,78 @@
import { EffectiveStat, Stat } from "#enums/stat";
import { MoveResult } from "#app/field/pokemon";
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, it, expect } from "vitest";
describe("Moves - Geomancy", () => {
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.GEOMANCY)
.battleType("single")
.startingLevel(100)
.enemySpecies(Species.SNORLAX)
.enemyLevel(100)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should boost the user's stats on the second turn of use", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const player = game.scene.getPlayerPokemon()!;
const affectedStats: EffectiveStat[] = [ Stat.SPATK, Stat.SPDEF, Stat.SPD ];
game.move.select(Moves.GEOMANCY);
await game.phaseInterceptor.to("TurnEndPhase");
affectedStats.forEach((stat) => expect(player.getStatStage(stat)).toBe(0));
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.OTHER);
await game.phaseInterceptor.to("TurnEndPhase");
affectedStats.forEach((stat) => expect(player.getStatStage(stat)).toBe(2));
expect(player.getMoveHistory()).toHaveLength(2);
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
const playerGeomancy = player.getMoveset().find((mv) => mv && mv.moveId === Moves.GEOMANCY);
expect(playerGeomancy?.ppUsed).toBe(1);
});
it("should execute over 2 turns between waves", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const player = game.scene.getPlayerPokemon()!;
const affectedStats: EffectiveStat[] = [ Stat.SPATK, Stat.SPDEF, Stat.SPD ];
game.move.select(Moves.GEOMANCY);
await game.phaseInterceptor.to("MoveEndPhase", false);
await game.doKillOpponents();
await game.toNextWave();
await game.phaseInterceptor.to("TurnEndPhase");
affectedStats.forEach((stat) => expect(player.getStatStage(stat)).toBe(2));
expect(player.getMoveHistory()).toHaveLength(2);
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
const playerGeomancy = player.getMoveset().find((mv) => mv && mv.moveId === Moves.GEOMANCY);
expect(playerGeomancy?.ppUsed).toBe(1);
});
});

View File

@ -94,7 +94,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) =
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (defender.scene.arena.getTagOnSide(ArenaTagType.LIGHT_SCREEN, side)) { 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; return move.power * multiplierHolder.value;

View File

@ -0,0 +1,52 @@
import { StatusEffect } from "#app/data/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("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.toNextTurn();
expect(enemyPokemon.hp).toBe(enemyMaxHP - Math.floor(enemyMaxHP / 4));
// take a second turn to make sure damage occurs again
game.move.select(Moves.SPLASH);
await game.toNextTurn();
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; const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (defender.scene.arena.getTagOnSide(ArenaTagType.REFLECT, side)) { 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; return move.power * multiplierHolder.value;

View File

@ -0,0 +1,102 @@
import { allMoves } from "#app/data/move";
import { BattlerTagType } from "#enums/battler-tag-type";
import { WeatherType } from "#enums/weather-type";
import { MoveResult } from "#app/field/pokemon";
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, it, expect, vi } from "vitest";
describe("Moves - Solar Beam", () => {
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.SOLAR_BEAM)
.battleType("single")
.startingLevel(100)
.enemySpecies(Species.SNORLAX)
.enemyLevel(100)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should deal damage in two turns if no weather is active", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.SOLAR_BEAM);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeDefined();
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.OTHER);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined();
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
const playerSolarBeam = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.SOLAR_BEAM);
expect(playerSolarBeam?.ppUsed).toBe(1);
});
it.each([
{ weatherType: WeatherType.SUNNY, name: "Sun" },
{ weatherType: WeatherType.HARSH_SUN, name: "Harsh Sun" }
])("should deal damage in one turn if $name is active", async ({ weatherType }) => {
game.override.weather(weatherType);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.SOLAR_BEAM);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined();
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
const playerSolarBeam = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.SOLAR_BEAM);
expect(playerSolarBeam?.ppUsed).toBe(1);
});
it.each([
{ weatherType: WeatherType.RAIN, name: "Rain" },
{ weatherType: WeatherType.HEAVY_RAIN, name: "Heavy Rain" }
])("should have its power halved in $name", async ({ weatherType }) => {
game.override.weather(weatherType);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const solarBeam = allMoves[Moves.SOLAR_BEAM];
vi.spyOn(solarBeam, "calculateBattlePower");
game.move.select(Moves.SOLAR_BEAM);
await game.phaseInterceptor.to("TurnEndPhase");
await game.phaseInterceptor.to("TurnEndPhase");
expect(solarBeam.calculateBattlePower).toHaveLastReturnedWith(60);
});
});

View File

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

View File

@ -36,9 +36,7 @@ describe("Moves - Transform", () => {
}); });
it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => { it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => {
await game.startBattle([ await game.classicMode.startBattle([ Species.DITTO ]);
Species.DITTO
]);
game.move.select(Moves.TRANSFORM); game.move.select(Moves.TRANSFORM);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
@ -78,9 +76,7 @@ describe("Moves - Transform", () => {
it("should copy in-battle overridden stats", async () => { it("should copy in-battle overridden stats", async () => {
game.override.enemyMoveset([ Moves.POWER_SPLIT ]); game.override.enemyMoveset([ Moves.POWER_SPLIT ]);
await game.startBattle([ await game.classicMode.startBattle([ Species.DITTO ]);
Species.DITTO
]);
const player = game.scene.getPlayerPokemon()!; const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;
@ -97,4 +93,18 @@ describe("Moves - Transform", () => {
expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk); expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk); expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
}); });
it ("should set each move's pp to a maximum of 5", async () => {
game.override.enemyMoveset([ Moves.SWORDS_DANCE, Moves.GROWL, Moves.SKETCH, Moves.RECOVER ]);
await game.classicMode.startBattle([ Species.DITTO ]);
const player = game.scene.getPlayerPokemon()!;
game.move.select(Moves.TRANSFORM);
await game.phaseInterceptor.to(TurnEndPhase);
player.getMoveset().forEach(move => {
expect(move!.getMovePp()).toBeLessThanOrEqual(5);
});
});
}); });

View File

@ -41,7 +41,8 @@ describe("Moves - Whirlwind", () => {
const staraptor = game.scene.getPlayerPokemon()!; const staraptor = game.scene.getPlayerPokemon()!;
game.move.select(move); game.move.select(move);
await game.toNextTurn();
await game.phaseInterceptor.to("BerryPhase", false);
expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined(); expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined();
expect(game.scene.getEnemyPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.MISS); expect(game.scene.getEnemyPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);

View File

@ -0,0 +1,53 @@
import { BattlerIndex } from "#app/battle";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { StatusEffect } from "#enums/status-effect";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Will-O-Wisp", () => {
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.WILL_O_WISP, Moves.SPLASH ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should burn the opponent", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.WILL_O_WISP);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.move.forceHit();
await game.toNextTurn();
expect(enemy.status?.effect).toBe(StatusEffect.BURN);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(enemy.status?.effect).toBe(StatusEffect.BURN);
});
});

View File

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

View File

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

View File

@ -206,7 +206,7 @@ describe("Delibird-y - Mystery Encounter", () => {
expect(candyJarAfter?.stackCount).toBe(1); expect(candyJarAfter?.stackCount).toBe(1);
}); });
it("Should remove Reviver Seed and give the player a Healing Charm", async () => { it("Should remove Reviver Seed and give the player a Berry Pouch", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
// Set 1 Reviver Seed on party lead // Set 1 Reviver Seed on party lead
@ -220,11 +220,11 @@ describe("Delibird-y - Mystery Encounter", () => {
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 });
const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier); const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier);
const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier);
expect(reviverSeedAfter).toBeUndefined(); expect(reviverSeedAfter).toBeUndefined();
expect(healingCharmAfter).toBeDefined(); expect(berryPouchAfter).toBeDefined();
expect(healingCharmAfter?.stackCount).toBe(1); expect(berryPouchAfter?.stackCount).toBe(1);
}); });
it("Should give the player a Shell Bell if they have max stacks of Candy Jars", async () => { it("Should give the player a Shell Bell if they have max stacks of Candy Jars", async () => {
@ -256,13 +256,13 @@ describe("Delibird-y - Mystery Encounter", () => {
expect(shellBellAfter?.stackCount).toBe(1); expect(shellBellAfter?.stackCount).toBe(1);
}); });
it("Should give the player a Shell Bell if they have max stacks of Healing Charms", async () => { it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
// 5 Healing Charms // 3 Berry Pouches
scene.modifiers = []; scene.modifiers = [];
const healingCharm = generateModifierType(scene, modifierTypes.HEALING_CHARM)!.newModifier() as HealingBoosterModifier; const healingCharm = generateModifierType(scene, modifierTypes.BERRY_POUCH)!.newModifier() as PreserveBerryModifier;
healingCharm.stackCount = 5; healingCharm.stackCount = 3;
await scene.addModifier(healingCharm, true, false, false, true); await scene.addModifier(healingCharm, true, false, false, true);
// Set 1 Reviver Seed on party lead // Set 1 Reviver Seed on party lead
@ -275,12 +275,12 @@ describe("Delibird-y - Mystery Encounter", () => {
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 });
const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier); const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier);
const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); const healingCharmAfter = scene.findModifier(m => m instanceof PreserveBerryModifier);
const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier);
expect(reviverSeedAfter).toBeUndefined(); expect(reviverSeedAfter).toBeUndefined();
expect(healingCharmAfter).toBeDefined(); expect(healingCharmAfter).toBeDefined();
expect(healingCharmAfter?.stackCount).toBe(5); expect(healingCharmAfter?.stackCount).toBe(3);
expect(shellBellAfter).toBeDefined(); expect(shellBellAfter).toBeDefined();
expect(shellBellAfter?.stackCount).toBe(1); expect(shellBellAfter?.stackCount).toBe(1);
}); });
@ -347,7 +347,7 @@ describe("Delibird-y - Mystery Encounter", () => {
}); });
}); });
it("Should decrease held item stacks and give the player a Berry Pouch", async () => { it("Should decrease held item stacks and give the player a Healing Charm", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
// Set 2 Soul Dew on party lead // Set 2 Soul Dew on party lead
@ -361,14 +361,14 @@ describe("Delibird-y - Mystery Encounter", () => {
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier);
const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier);
expect(soulDewAfter?.stackCount).toBe(1); expect(soulDewAfter?.stackCount).toBe(1);
expect(berryPouchAfter).toBeDefined(); expect(healingCharmAfter).toBeDefined();
expect(berryPouchAfter?.stackCount).toBe(1); expect(healingCharmAfter?.stackCount).toBe(1);
}); });
it("Should remove held item and give the player a Berry Pouch", async () => { it("Should remove held item and give the player a Healing Charm", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
// Set 1 Soul Dew on party lead // Set 1 Soul Dew on party lead
@ -382,20 +382,20 @@ describe("Delibird-y - Mystery Encounter", () => {
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier);
const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier);
expect(soulDewAfter).toBeUndefined(); expect(soulDewAfter).toBeUndefined();
expect(berryPouchAfter).toBeDefined(); expect(healingCharmAfter).toBeDefined();
expect(berryPouchAfter?.stackCount).toBe(1); expect(healingCharmAfter?.stackCount).toBe(1);
}); });
it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => { it("Should give the player a Shell Bell if they have max stacks of Healing Charms", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
// 5 Healing Charms // 5 Healing Charms
scene.modifiers = []; scene.modifiers = [];
const healingCharm = generateModifierType(scene, modifierTypes.BERRY_POUCH)!.newModifier() as PreserveBerryModifier; const healingCharm = generateModifierType(scene, modifierTypes.HEALING_CHARM)!.newModifier() as HealingBoosterModifier;
healingCharm.stackCount = 3; healingCharm.stackCount = 5;
await scene.addModifier(healingCharm, true, false, false, true); await scene.addModifier(healingCharm, true, false, false, true);
// Set 1 Soul Dew on party lead // Set 1 Soul Dew on party lead
@ -408,12 +408,12 @@ describe("Delibird-y - Mystery Encounter", () => {
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier);
const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier);
const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier);
expect(soulDewAfter).toBeUndefined(); expect(soulDewAfter).toBeUndefined();
expect(berryPouchAfter).toBeDefined(); expect(healingCharmAfter).toBeDefined();
expect(berryPouchAfter?.stackCount).toBe(3); expect(healingCharmAfter?.stackCount).toBe(5);
expect(shellBellAfter).toBeDefined(); expect(shellBellAfter).toBeDefined();
expect(shellBellAfter?.stackCount).toBe(1); expect(shellBellAfter?.stackCount).toBe(1);
}); });

Some files were not shown because too many files have changed in this diff Show More