Merge branch 'beta' into powder

This commit is contained in:
innerthunder 2024-10-20 14:50:57 -07:00
commit 030a96d32b
144 changed files with 12955 additions and 8545 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

4
package-lock.json generated
View File

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

View File

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

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 441 B

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -4,7 +4,7 @@ import Pokemon, { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
import PokemonSpecies, { allSpecies, getPokemonSpecies, PokemonSpeciesFilter } from "#app/data/pokemon-species"; import PokemonSpecies, { allSpecies, getPokemonSpecies, PokemonSpeciesFilter } from "#app/data/pokemon-species";
import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils"; import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
import { ConsumableModifier, ConsumablePokemonModifier, DoubleBattleChanceBoosterModifier, ExpBalanceModifier, ExpShareModifier, FusePokemonModifier, HealingBoosterModifier, Modifier, ModifierBar, ModifierPredicate, MultipleParticipantExpBonusModifier, overrideHeldItems, overrideModifiers, PersistentModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, TerastallizeModifier, TurnHeldItemTransferModifier } from "./modifier/modifier"; import { ConsumableModifier, ConsumablePokemonModifier, DoubleBattleChanceBoosterModifier, ExpBalanceModifier, ExpShareModifier, FusePokemonModifier, HealingBoosterModifier, Modifier, ModifierBar, ModifierPredicate, MultipleParticipantExpBonusModifier, overrideHeldItems, overrideModifiers, PersistentModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, RememberMoveModifier, TerastallizeModifier, TurnHeldItemTransferModifier } from "./modifier/modifier";
import { PokeballType } from "#app/data/pokeball"; import { PokeballType } from "#app/data/pokeball";
import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "#app/data/battle-anims"; import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "#app/data/battle-anims";
import { Phase } from "#app/phase"; import { Phase } from "#app/phase";
@ -95,6 +95,7 @@ import { ExpPhase } from "#app/phases/exp-phase";
import { ShowPartyExpBarPhase } from "#app/phases/show-party-exp-bar-phase"; import { ShowPartyExpBarPhase } from "#app/phases/show-party-exp-bar-phase";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { ExpGainsSpeed } from "#enums/exp-gains-speed"; import { ExpGainsSpeed } from "#enums/exp-gains-speed";
import { FRIENDSHIP_GAIN_FROM_BATTLE } from "#app/data/balance/starters";
export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1"; export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1";
@ -789,7 +790,7 @@ export default class BattleScene extends SceneBase {
} }
getEnemyParty(): EnemyPokemon[] { getEnemyParty(): EnemyPokemon[] {
return this.currentBattle?.enemyParty || []; return this.currentBattle?.enemyParty ?? [];
} }
getEnemyPokemon(): EnemyPokemon | undefined { getEnemyPokemon(): EnemyPokemon | undefined {
@ -1389,7 +1390,7 @@ export default class BattleScene extends SceneBase {
case Species.GRENINJA: case Species.GRENINJA:
return Utils.randSeedInt(2); return Utils.randSeedInt(2);
case Species.ZYGARDE: case Species.ZYGARDE:
return Utils.randSeedInt(3); return Utils.randSeedInt(4);
case Species.MINIOR: case Species.MINIOR:
return Utils.randSeedInt(6); return Utils.randSeedInt(6);
case Species.ALCREMIE: case Species.ALCREMIE:
@ -2425,7 +2426,7 @@ export default class BattleScene extends SceneBase {
return Math.floor(moneyValue / 10) * 10; return Math.floor(moneyValue / 10) * 10;
} }
addModifier(modifier: Modifier | null, ignoreUpdate?: boolean, playSound?: boolean, virtual?: boolean, instant?: boolean): Promise<boolean> { addModifier(modifier: Modifier | null, ignoreUpdate?: boolean, playSound?: boolean, virtual?: boolean, instant?: boolean, cost?: number): Promise<boolean> {
if (!modifier) { if (!modifier) {
return Promise.resolve(false); return Promise.resolve(false);
} }
@ -2482,6 +2483,8 @@ export default class BattleScene extends SceneBase {
} }
} else if (modifier instanceof FusePokemonModifier) { } else if (modifier instanceof FusePokemonModifier) {
args.push(this.getPokemonById(modifier.fusePokemonId) as PlayerPokemon); args.push(this.getPokemonById(modifier.fusePokemonId) as PlayerPokemon);
} else if (modifier instanceof RememberMoveModifier && !Utils.isNullOrUndefined(cost)) {
args.push(cost);
} }
if (modifier.shouldApply(pokemon, ...args)) { if (modifier.shouldApply(pokemon, ...args)) {
@ -3052,7 +3055,7 @@ export default class BattleScene extends SceneBase {
const pId = partyMember.id; const pId = partyMember.id;
const participated = participantIds.has(pId); const participated = participantIds.has(pId);
if (participated && pokemonDefeated) { if (participated && pokemonDefeated) {
partyMember.addFriendship(2); partyMember.addFriendship(FRIENDSHIP_GAIN_FROM_BATTLE);
const machoBraceModifier = partyMember.getHeldItems().find(m => m instanceof PokemonIncrementingStatModifier); const machoBraceModifier = partyMember.getHeldItems().find(m => m instanceof PokemonIncrementingStatModifier);
if (machoBraceModifier && machoBraceModifier.stackCount < machoBraceModifier.getMaxStackCount(this)) { if (machoBraceModifier && machoBraceModifier.stackCount < machoBraceModifier.getMaxStackCount(this)) {
machoBraceModifier.stackCount++; machoBraceModifier.stackCount++;
@ -3174,6 +3177,9 @@ export default class BattleScene extends SceneBase {
let encounter: MysteryEncounter | null; let encounter: MysteryEncounter | null;
if (!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_OVERRIDE) && allMysteryEncounters.hasOwnProperty(Overrides.MYSTERY_ENCOUNTER_OVERRIDE)) { if (!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_OVERRIDE) && allMysteryEncounters.hasOwnProperty(Overrides.MYSTERY_ENCOUNTER_OVERRIDE)) {
encounter = allMysteryEncounters[Overrides.MYSTERY_ENCOUNTER_OVERRIDE]; encounter = allMysteryEncounters[Overrides.MYSTERY_ENCOUNTER_OVERRIDE];
if (canBypass) {
return encounter;
}
} else if (canBypass) { } else if (canBypass) {
encounter = allMysteryEncounters[encounterType ?? -1]; encounter = allMysteryEncounters[encounterType ?? -1];
return encounter; return encounter;

View File

@ -4913,8 +4913,7 @@ export function initAbilities() {
.attr(TypeImmunityAddBattlerTagAbAttr, Type.FIRE, BattlerTagType.FIRE_BOOST, 1) .attr(TypeImmunityAddBattlerTagAbAttr, Type.FIRE, BattlerTagType.FIRE_BOOST, 1)
.ignorable(), .ignorable(),
new Ability(Abilities.SHIELD_DUST, 3) new Ability(Abilities.SHIELD_DUST, 3)
.attr(IgnoreMoveEffectsAbAttr) .attr(IgnoreMoveEffectsAbAttr),
.edgeCase(), // Does not work with secret power (unimplemented)
new Ability(Abilities.OWN_TEMPO, 3) new Ability(Abilities.OWN_TEMPO, 3)
.attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED) .attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED)
.attr(IntimidateImmunityAbAttr) .attr(IntimidateImmunityAbAttr)
@ -4958,8 +4957,7 @@ export function initAbilities() {
.attr(TypeImmunityStatStageChangeAbAttr, Type.ELECTRIC, Stat.SPATK, 1) .attr(TypeImmunityStatStageChangeAbAttr, Type.ELECTRIC, Stat.SPATK, 1)
.ignorable(), .ignorable(),
new Ability(Abilities.SERENE_GRACE, 3) new Ability(Abilities.SERENE_GRACE, 3)
.attr(MoveEffectChanceMultiplierAbAttr, 2) .attr(MoveEffectChanceMultiplierAbAttr, 2),
.edgeCase(), // does not work with secret power (unimplemented)
new Ability(Abilities.SWIFT_SWIM, 3) new Ability(Abilities.SWIFT_SWIM, 3)
.attr(StatMultiplierAbAttr, Stat.SPD, 2) .attr(StatMultiplierAbAttr, Stat.SPD, 2)
.condition(getWeatherCondition(WeatherType.RAIN, WeatherType.HEAVY_RAIN)), .condition(getWeatherCondition(WeatherType.RAIN, WeatherType.HEAVY_RAIN)),
@ -5535,16 +5533,18 @@ export function initAbilities() {
.attr(UnsuppressableAbilityAbAttr) .attr(UnsuppressableAbilityAbAttr)
.attr(NoFusionAbilityAbAttr) .attr(NoFusionAbilityAbAttr)
.bypassFaint(), .bypassFaint(),
new Ability(Abilities.POWER_CONSTRUCT, 7) // TODO: 10% Power Construct Zygarde isn't accounted for yet. If changed, update Zygarde's getSpeciesFormIndex entry accordingly new Ability(Abilities.POWER_CONSTRUCT, 7)
.attr(PostBattleInitFormChangeAbAttr, () => 2) .conditionalAttr(pokemon => pokemon.formIndex === 2 || pokemon.formIndex === 4, PostBattleInitFormChangeAbAttr, () => 2)
.attr(PostSummonFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2) .conditionalAttr(pokemon => pokemon.formIndex === 3 || pokemon.formIndex === 5, PostBattleInitFormChangeAbAttr, () => 3)
.attr(PostTurnFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2) .conditionalAttr(pokemon => pokemon.formIndex === 2 || pokemon.formIndex === 4, PostSummonFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2)
.conditionalAttr(pokemon => pokemon.formIndex === 2 || pokemon.formIndex === 4, PostTurnFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2)
.conditionalAttr(pokemon => pokemon.formIndex === 3 || pokemon.formIndex === 5, PostSummonFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "10-complete" ? 5 : 3)
.conditionalAttr(pokemon => pokemon.formIndex === 3 || pokemon.formIndex === 5, PostTurnFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "10-complete" ? 5 : 3)
.attr(UncopiableAbilityAbAttr) .attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr) .attr(UnswappableAbilityAbAttr)
.attr(UnsuppressableAbilityAbAttr) .attr(UnsuppressableAbilityAbAttr)
.attr(NoFusionAbilityAbAttr) .attr(NoFusionAbilityAbAttr)
.bypassFaint() .bypassFaint(),
.partial(),
new Ability(Abilities.CORROSION, 7) new Ability(Abilities.CORROSION, 7)
.attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ Type.STEEL, Type.POISON ]) .attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ Type.STEEL, Type.POISON ])
.edgeCase(), // Should interact correctly with magic coat/bounce (not yet implemented), fling with toxic orb (not implemented yet), and synchronize (not fully implemented yet) .edgeCase(), // Should interact correctly with magic coat/bounce (not yet implemented), fling with toxic orb (not implemented yet), and synchronize (not fully implemented yet)

View File

@ -1,10 +1,10 @@
import { Arena } from "#app/field/arena"; import { Arena } from "#app/field/arena";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import * as Utils from "#app/utils"; import { BooleanHolder, NumberHolder, toDmgValue } from "#app/utils";
import { MoveCategory, allMoves, MoveTarget, IncrementMovePriorityAttr, applyMoveAttrs } from "#app/data/move"; import { MoveCategory, allMoves, MoveTarget, IncrementMovePriorityAttr, applyMoveAttrs } from "#app/data/move";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import Pokemon, { HitResult, PlayerPokemon, PokemonMove, EnemyPokemon } 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, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability";
@ -36,7 +36,7 @@ export abstract class ArenaTag {
public side: ArenaTagSide = ArenaTagSide.BOTH public side: ArenaTagSide = ArenaTagSide.BOTH
) {} ) {}
apply(arena: Arena, args: any[]): boolean { apply(arena: Arena, simulated: boolean, ...args: unknown[]): boolean {
return true; return true;
} }
@ -71,6 +71,32 @@ export abstract class ArenaTag {
this.sourceId = source.sourceId; this.sourceId = source.sourceId;
this.side = source.side; this.side = source.side;
} }
/**
* Helper function that retrieves the source Pokemon
* @param scene medium to retrieve the source Pokemon
* @returns The source {@linkcode Pokemon} or `null` if none is found
*/
public getSourcePokemon(scene: BattleScene): Pokemon | null {
return this.sourceId ? scene.getPokemonById(this.sourceId) : null;
}
/**
* Helper function that retrieves the Pokemon affected
* @param scene - medium to retrieve the involved Pokemon
* @returns list of PlayerPokemon or EnemyPokemon on the field
*/
public getAffectedPokemon(scene: BattleScene): Pokemon[] {
switch (this.side) {
case ArenaTagSide.PLAYER:
return scene.getPlayerField() ?? [];
case ArenaTagSide.ENEMY:
return scene.getEnemyField() ?? [];
case ArenaTagSide.BOTH:
default:
return scene.getField(true) ?? [];
}
}
} }
/** /**
@ -96,10 +122,20 @@ export class MistTag extends ArenaTag {
} }
} }
apply(arena: Arena, args: any[]): boolean { /**
(args[0] as Utils.BooleanHolder).value = true; * Cancels the lowering of stats
* @param arena the {@linkcode Arena} containing this effect
* @param simulated `true` if the effect should be applied quietly
* @param cancelled a {@linkcode BooleanHolder} whose value is set to `true`
* to flag the stat reduction as cancelled
* @returns `true` if a stat reduction was cancelled; `false` otherwise
*/
override apply(arena: Arena, simulated: boolean, cancelled: BooleanHolder): boolean {
cancelled.value = true;
if (!simulated) {
arena.scene.queueMessage(i18next.t("arenaTag:mistApply")); arena.scene.queueMessage(i18next.t("arenaTag:mistApply"));
}
return true; return true;
} }
@ -131,17 +167,15 @@ export class WeakenMoveScreenTag extends ArenaTag {
/** /**
* Applies the weakening effect to the move. * Applies the weakening effect to the move.
* *
* @param arena - The arena where the move is applied. * @param arena the {@linkcode Arena} where the move is applied.
* @param args - The arguments for the move application. * @param simulated n/a
* @param args[0] - The category of the move. * @param moveCategory the attacking move's {@linkcode MoveCategory}.
* @param args[1] - A boolean indicating whether it is a double battle. * @param damageMultiplier A {@linkcode NumberHolder} containing the damage multiplier
* @param args[2] - An object of type `Utils.NumberHolder` that holds the damage multiplier * @returns `true` if the attacking move was weakened; `false` otherwise.
*
* @returns True if the move was weakened, otherwise false.
*/ */
apply(arena: Arena, args: any[]): boolean { override apply(arena: Arena, simulated: boolean, moveCategory: MoveCategory, damageMultiplier: NumberHolder): boolean {
if (this.weakenedCategories.includes((args[0] as MoveCategory))) { if (this.weakenedCategories.includes(moveCategory)) {
(args[2] as Utils.NumberHolder).value = (args[1] as boolean) ? 2732 / 4096 : 0.5; damageMultiplier.value = arena.scene.currentBattle.double ? 2732 / 4096 : 0.5;
return true; return true;
} }
return false; return false;
@ -223,39 +257,35 @@ export class ConditionalProtectTag extends ArenaTag {
onRemove(arena: Arena): void { } onRemove(arena: Arena): void { }
/** /**
* apply(): Checks incoming moves against the condition function * Checks incoming moves against the condition function
* and protects the target if conditions are met * and protects the target if conditions are met
* @param arena The arena containing this tag * @param arena the {@linkcode Arena} containing this tag
* @param args\[0\] (Utils.BooleanHolder) Signals if the move is cancelled * @param simulated `true` if the tag is applied quietly; `false` otherwise.
* @param args\[1\] (Pokemon) The Pokemon using the move * @param isProtected a {@linkcode BooleanHolder} used to flag if the move is protected against
* @param args\[2\] (Pokemon) The intended target of the move * @param attacker the attacking {@linkcode Pokemon}
* @param args\[3\] (Moves) The parameters to the condition function * @param defender the defending {@linkcode Pokemon}
* @param args\[4\] (Utils.BooleanHolder) Signals if the applied protection supercedes protection-ignoring effects * @param moveId the {@linkcode Moves | identifier} for the move being used
* @returns * @param ignoresProtectBypass a {@linkcode BooleanHolder} used to flag if a protection effect supercedes effects that ignore protection
* @returns `true` if this tag protected against the attack; `false` otherwise
*/ */
apply(arena: Arena, args: any[]): boolean { override apply(arena: Arena, simulated: boolean, isProtected: BooleanHolder, attacker: Pokemon, defender: Pokemon,
const [ cancelled, user, target, moveId, ignoresBypass ] = args; moveId: Moves, ignoresProtectBypass: BooleanHolder): boolean {
if (cancelled instanceof Utils.BooleanHolder if ((this.side === ArenaTagSide.PLAYER) === defender.isPlayer()
&& user instanceof Pokemon
&& target instanceof Pokemon
&& typeof moveId === "number"
&& ignoresBypass instanceof Utils.BooleanHolder) {
if ((this.side === ArenaTagSide.PLAYER) === target.isPlayer()
&& this.protectConditionFunc(arena, moveId)) { && this.protectConditionFunc(arena, moveId)) {
if (!cancelled.value) { if (!isProtected.value) {
cancelled.value = true; isProtected.value = true;
user.stopMultiHit(target); if (!simulated) {
attacker.stopMultiHit(defender);
new CommonBattleAnim(CommonAnim.PROTECT, target).play(arena.scene); new CommonBattleAnim(CommonAnim.PROTECT, defender).play(arena.scene);
arena.scene.queueMessage(i18next.t("arenaTag:conditionalProtectApply", { moveName: super.getMoveName(), pokemonNameWithAffix: getPokemonNameWithAffix(target) })); arena.scene.queueMessage(i18next.t("arenaTag:conditionalProtectApply", { moveName: super.getMoveName(), pokemonNameWithAffix: getPokemonNameWithAffix(defender) }));
}
} }
ignoresBypass.value = ignoresBypass.value || this.ignoresBypass; ignoresProtectBypass.value = ignoresProtectBypass.value || this.ignoresBypass;
return true; return true;
} }
}
return false; return false;
} }
} }
@ -270,7 +300,7 @@ export class ConditionalProtectTag extends ArenaTag {
*/ */
const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => { const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => {
const move = allMoves[moveId]; const move = allMoves[moveId];
const priority = new Utils.NumberHolder(move.priority); const priority = new NumberHolder(move.priority);
const effectPhase = arena.scene.getCurrentPhase(); const effectPhase = arena.scene.getCurrentPhase();
if (effectPhase instanceof MoveEffectPhase) { if (effectPhase instanceof MoveEffectPhase) {
@ -434,7 +464,7 @@ class WishTag extends ArenaTag {
if (user) { if (user) {
this.battlerIndex = user.getBattlerIndex(); this.battlerIndex = user.getBattlerIndex();
this.triggerMessage = i18next.t("arenaTag:wishTagOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(user) }); this.triggerMessage = i18next.t("arenaTag:wishTagOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(user) });
this.healHp = Utils.toDmgValue(user.getMaxHp() / 2); this.healHp = toDmgValue(user.getMaxHp() / 2);
} else { } else {
console.warn("Failed to get source for WishTag onAdd"); console.warn("Failed to get source for WishTag onAdd");
} }
@ -471,12 +501,19 @@ export class WeakenMoveTypeTag extends ArenaTag {
this.weakenedType = type; this.weakenedType = type;
} }
apply(arena: Arena, args: any[]): boolean { /**
if ((args[0] as Type) === this.weakenedType) { * Reduces an attack's power by 0.33x if it matches this tag's weakened type.
(args[1] as Utils.NumberHolder).value *= 0.33; * @param arena n/a
* @param simulated n/a
* @param type the attack's {@linkcode Type}
* @param power a {@linkcode NumberHolder} containing the attack's power
* @returns `true` if the attack's power was reduced; `false` otherwise.
*/
override apply(arena: Arena, simulated: boolean, type: Type, power: NumberHolder): boolean {
if (type === this.weakenedType) {
power.value *= 0.33;
return true; return true;
} }
return false; return false;
} }
} }
@ -537,13 +574,12 @@ export class IonDelugeTag extends ArenaTag {
/** /**
* Converts Normal-type moves to Electric type * Converts Normal-type moves to Electric type
* @param arena n/a * @param arena n/a
* @param args * @param simulated n/a
* - `[0]` {@linkcode Utils.NumberHolder} A container with a move's {@linkcode Type} * @param moveType a {@linkcode NumberHolder} containing a move's {@linkcode Type}
* @returns `true` if the given move type changed; `false` otherwise. * @returns `true` if the given move type changed; `false` otherwise.
*/ */
apply(arena: Arena, args: any[]): boolean { override apply(arena: Arena, simulated: boolean, moveType: NumberHolder): boolean {
const moveType = args[0]; if (moveType.value === Type.NORMAL) {
if (moveType instanceof Utils.NumberHolder && moveType.value === Type.NORMAL) {
moveType.value = Type.ELECTRIC; moveType.value = Type.ELECTRIC;
return true; return true;
} }
@ -582,16 +618,22 @@ export class ArenaTrapTag extends ArenaTag {
} }
} }
apply(arena: Arena, args: any[]): boolean { /**
const pokemon = args[0] as Pokemon; * Activates the hazard effect onto a Pokemon when it enters the field
* @param arena the {@linkcode Arena} containing this tag
* @param simulated if `true`, only checks if the hazard would activate.
* @param pokemon the {@linkcode Pokemon} triggering this hazard
* @returns `true` if this hazard affects the given Pokemon; `false` otherwise.
*/
override apply(arena: Arena, simulated: boolean, pokemon: Pokemon): boolean {
if (this.sourceId === pokemon.id || (this.side === ArenaTagSide.PLAYER) !== pokemon.isPlayer()) { if (this.sourceId === pokemon.id || (this.side === ArenaTagSide.PLAYER) !== pokemon.isPlayer()) {
return false; return false;
} }
return this.activateTrap(pokemon); return this.activateTrap(pokemon, simulated);
} }
activateTrap(pokemon: Pokemon): boolean { activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
return false; return false;
} }
@ -625,14 +667,18 @@ class SpikesTag extends ArenaTrapTag {
} }
} }
activateTrap(pokemon: Pokemon): boolean { override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
if (pokemon.isGrounded()) { if (pokemon.isGrounded()) {
const cancelled = new Utils.BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (simulated) {
return !cancelled.value;
}
if (!cancelled.value) { if (!cancelled.value) {
const damageHpRatio = 1 / (10 - 2 * this.layers); const damageHpRatio = 1 / (10 - 2 * this.layers);
const damage = Utils.toDmgValue(pokemon.getMaxHp() * damageHpRatio); const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio);
pokemon.scene.queueMessage(i18next.t("arenaTag:spikesActivateTrap", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); pokemon.scene.queueMessage(i18next.t("arenaTag:spikesActivateTrap", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
pokemon.damageAndUpdate(damage, HitResult.OTHER); pokemon.damageAndUpdate(damage, HitResult.OTHER);
@ -676,8 +722,11 @@ class ToxicSpikesTag extends ArenaTrapTag {
} }
} }
activateTrap(pokemon: Pokemon): boolean { override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
if (pokemon.isGrounded()) { if (pokemon.isGrounded()) {
if (simulated) {
return true;
}
if (pokemon.isOfType(Type.POISON)) { if (pokemon.isOfType(Type.POISON)) {
this.neutralized = true; this.neutralized = true;
if (pokemon.scene.arena.removeTag(this.tagType)) { if (pokemon.scene.arena.removeTag(this.tagType)) {
@ -781,8 +830,8 @@ class StealthRockTag extends ArenaTrapTag {
return damageHpRatio; return damageHpRatio;
} }
activateTrap(pokemon: Pokemon): boolean { override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
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) {
@ -792,12 +841,16 @@ class StealthRockTag extends ArenaTrapTag {
const damageHpRatio = this.getDamageHpRatio(pokemon); const damageHpRatio = this.getDamageHpRatio(pokemon);
if (damageHpRatio) { if (damageHpRatio) {
const damage = Utils.toDmgValue(pokemon.getMaxHp() * damageHpRatio); if (simulated) {
return true;
}
const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio);
pokemon.scene.queueMessage(i18next.t("arenaTag:stealthRockActivateTrap", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); pokemon.scene.queueMessage(i18next.t("arenaTag:stealthRockActivateTrap", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
pokemon.damageAndUpdate(damage, HitResult.OTHER); pokemon.damageAndUpdate(damage, HitResult.OTHER);
if (pokemon.turnData) { if (pokemon.turnData) {
pokemon.turnData.damageTaken += damage; pokemon.turnData.damageTaken += damage;
} }
return true;
} }
return false; return false;
@ -827,14 +880,20 @@ class StickyWebTag extends ArenaTrapTag {
} }
} }
activateTrap(pokemon: Pokemon): boolean { override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
if (pokemon.isGrounded()) { if (pokemon.isGrounded()) {
const cancelled = new Utils.BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled); applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled);
if (simulated) {
return !cancelled.value;
}
if (!cancelled.value) { if (!cancelled.value) {
pokemon.scene.queueMessage(i18next.t("arenaTag:stickyWebActivateTrap", { pokemonName: pokemon.getNameToRender() })); pokemon.scene.queueMessage(i18next.t("arenaTag:stickyWebActivateTrap", { pokemonName: pokemon.getNameToRender() }));
const stages = new Utils.NumberHolder(-1); const stages = new NumberHolder(-1);
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [ Stat.SPD ], stages.value)); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [ Stat.SPD ], stages.value));
return true;
} }
} }
@ -853,8 +912,15 @@ export class TrickRoomTag extends ArenaTag {
super(ArenaTagType.TRICK_ROOM, turnCount, Moves.TRICK_ROOM, sourceId); super(ArenaTagType.TRICK_ROOM, turnCount, Moves.TRICK_ROOM, sourceId);
} }
apply(arena: Arena, args: any[]): boolean { /**
const speedReversed = args[0] as Utils.BooleanHolder; * Reverses Speed-based turn order for all Pokemon on the field
* @param arena n/a
* @param simulated n/a
* @param speedReversed a {@linkcode BooleanHolder} used to flag if Speed-based
* turn order should be reversed.
* @returns `true` if turn order is successfully reversed; `false` otherwise
*/
override apply(arena: Arena, simulated: boolean, speedReversed: BooleanHolder): boolean {
speedReversed.value = !speedReversed.value; speedReversed.value = !speedReversed.value;
return true; return true;
} }
@ -978,36 +1044,24 @@ class NoneTag extends ArenaTag {
* Imprison will apply to any opposing Pokemon that switch onto the field as well. * Imprison will apply to any opposing Pokemon that switch onto the field as well.
*/ */
class ImprisonTag extends ArenaTrapTag { class ImprisonTag extends ArenaTrapTag {
private source: Pokemon;
constructor(sourceId: number, side: ArenaTagSide) { constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.IMPRISON, Moves.IMPRISON, sourceId, side, 1); super(ArenaTagType.IMPRISON, Moves.IMPRISON, sourceId, side, 1);
} }
/**
* Helper function that retrieves the Pokemon affected
* @param {BattleScene} scene medium to retrieve the involved Pokemon
* @returns list of PlayerPokemon or EnemyPokemon on the field
*/
private retrieveField(scene: BattleScene): PlayerPokemon[] | EnemyPokemon[] {
if (!this.source.isPlayer()) {
return scene.getPlayerField() ?? [];
}
return scene.getEnemyField() ?? [];
}
/** /**
* This function applies the effects of Imprison to the opposing Pokemon already present on the field. * This function applies the effects of Imprison to the opposing Pokemon already present on the field.
* @param arena * @param arena
*/ */
override onAdd({ scene }: Arena) { override onAdd({ scene }: Arena) {
this.source = scene.getPokemonById(this.sourceId!)!; const source = this.getSourcePokemon(scene);
if (this.source) { if (source) {
const party = this.retrieveField(scene); const party = this.getAffectedPokemon(scene);
party?.forEach((p: PlayerPokemon | EnemyPokemon ) => { party?.forEach((p: Pokemon ) => {
if (p.isAllowedInBattle()) {
p.addTag(BattlerTagType.IMPRISON, 1, Moves.IMPRISON, this.sourceId); p.addTag(BattlerTagType.IMPRISON, 1, Moves.IMPRISON, this.sourceId);
}
}); });
scene.queueMessage(i18next.t("battlerTags:imprisonOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(this.source) })); scene.queueMessage(i18next.t("battlerTags:imprisonOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(source) }));
} }
} }
@ -1016,8 +1070,9 @@ class ImprisonTag extends ArenaTrapTag {
* @param _arena * @param _arena
* @returns `true` if the source of the tag is still active on the field | `false` if not * @returns `true` if the source of the tag is still active on the field | `false` if not
*/ */
override lapse(_arena: Arena): boolean { override lapse({ scene }: Arena): boolean {
return this.source.isActive(true); const source = this.getSourcePokemon(scene);
return source ? source.isActive(true) : false;
} }
/** /**
@ -1026,7 +1081,8 @@ class ImprisonTag extends ArenaTrapTag {
* @returns `true` * @returns `true`
*/ */
override activateTrap(pokemon: Pokemon): boolean { override activateTrap(pokemon: Pokemon): boolean {
if (this.source.isActive(true)) { const source = this.getSourcePokemon(pokemon.scene);
if (source && source.isActive(true) && pokemon.isAllowedInBattle()) {
pokemon.addTag(BattlerTagType.IMPRISON, 1, Moves.IMPRISON, this.sourceId); pokemon.addTag(BattlerTagType.IMPRISON, 1, Moves.IMPRISON, this.sourceId);
} }
return true; return true;
@ -1037,8 +1093,8 @@ class ImprisonTag extends ArenaTrapTag {
* @param arena * @param arena
*/ */
override onRemove({ scene }: Arena): void { override onRemove({ scene }: Arena): void {
const party = this.retrieveField(scene); const party = this.getAffectedPokemon(scene);
party?.forEach((p: PlayerPokemon | EnemyPokemon) => { party?.forEach((p: Pokemon) => {
p.removeTag(BattlerTagType.IMPRISON); p.removeTag(BattlerTagType.IMPRISON);
}); });
} }
@ -1071,7 +1127,7 @@ class FireGrassPledgeTag extends ArenaTag {
pokemon.scene.queueMessage(i18next.t("arenaTag:fireGrassPledgeLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); pokemon.scene.queueMessage(i18next.t("arenaTag:fireGrassPledgeLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
// TODO: Replace this with a proper animation // TODO: Replace this with a proper animation
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.MAGMA_STORM)); pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.MAGMA_STORM));
pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / 8)); pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8));
}); });
return super.lapse(arena); return super.lapse(arena);
@ -1095,8 +1151,15 @@ class WaterFirePledgeTag extends ArenaTag {
arena.scene.queueMessage(i18next.t(`arenaTag:waterFirePledgeOnAdd${this.side === ArenaTagSide.PLAYER ? "Player" : this.side === ArenaTagSide.ENEMY ? "Enemy" : ""}`)); arena.scene.queueMessage(i18next.t(`arenaTag:waterFirePledgeOnAdd${this.side === ArenaTagSide.PLAYER ? "Player" : this.side === ArenaTagSide.ENEMY ? "Enemy" : ""}`));
} }
override apply(arena: Arena, args: any[]): boolean { /**
const moveChance = args[0] as Utils.NumberHolder; * Doubles the chance for the given move's secondary effect(s) to trigger
* @param arena the {@linkcode Arena} containing this tag
* @param simulated n/a
* @param moveChance a {@linkcode NumberHolder} containing
* the move's current effect chance
* @returns `true` if the move's effect chance was doubled (currently always `true`)
*/
override apply(arena: Arena, simulated: boolean, moveChance: NumberHolder): boolean {
moveChance.value *= 2; moveChance.value *= 2;
return true; return true;
} }

View File

@ -2,6 +2,12 @@ import { Species } from "#enums/species";
export const POKERUS_STARTER_COUNT = 5; export const POKERUS_STARTER_COUNT = 5;
// #region Friendship constants
export const CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER = 2;
export const FRIENDSHIP_GAIN_FROM_BATTLE = 2;
export const FRIENDSHIP_GAIN_FROM_RARE_CANDY = 5;
export const FRIENDSHIP_LOSS_FROM_FAINT = 10;
/** /**
* Function to get the cumulative friendship threshold at which a candy is earned * Function to get the cumulative friendship threshold at which a candy is earned
* @param starterCost The cost of the starter, found in {@linkcode speciesStarterCosts} * @param starterCost The cost of the starter, found in {@linkcode speciesStarterCosts}

View File

@ -23,6 +23,7 @@ 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 { StatStageChangePhase, StatStageChangeCallback } from "#app/phases/stat-stage-change-phase";
import { PokemonAnimType } from "#app/enums/pokemon-anim-type"; import { PokemonAnimType } from "#app/enums/pokemon-anim-type";
import BattleScene from "#app/battle-scene";
export enum BattlerTagLapseType { export enum BattlerTagLapseType {
FAINT, FAINT,
@ -90,6 +91,15 @@ export class BattlerTag {
this.sourceMove = source.sourceMove; this.sourceMove = source.sourceMove;
this.sourceId = source.sourceId; this.sourceId = source.sourceId;
} }
/**
* Helper function that retrieves the source Pokemon object
* @param scene medium to retrieve the source Pokemon
* @returns The source {@linkcode Pokemon} or `null` if none is found
*/
public getSourcePokemon(scene: BattleScene): Pokemon | null {
return this.sourceId ? scene.getPokemonById(this.sourceId) : null;
}
} }
export interface WeatherBattlerTag { export interface WeatherBattlerTag {
@ -120,7 +130,7 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag {
const phase = pokemon.scene.getCurrentPhase() as MovePhase; const phase = pokemon.scene.getCurrentPhase() as MovePhase;
const move = phase.move; const move = phase.move;
if (this.isMoveRestricted(move.moveId)) { if (this.isMoveRestricted(move.moveId, pokemon)) {
if (this.interruptedText(pokemon, move.moveId)) { if (this.interruptedText(pokemon, move.moveId)) {
pokemon.scene.queueMessage(this.interruptedText(pokemon, move.moveId)); pokemon.scene.queueMessage(this.interruptedText(pokemon, move.moveId));
} }
@ -136,10 +146,11 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag {
/** /**
* Gets whether this tag is restricting a move. * Gets whether this tag is restricting a move.
* *
* @param {Moves} move {@linkcode Moves} ID to check restriction for. * @param move - {@linkcode Moves} ID to check restriction for.
* @returns {boolean} `true` if the move is restricted by this tag, otherwise `false`. * @param user - The {@linkcode Pokemon} involved
* @returns `true` if the move is restricted by this tag, otherwise `false`.
*/ */
abstract isMoveRestricted(move: Moves): boolean; public abstract isMoveRestricted(move: Moves, user?: Pokemon): boolean;
/** /**
* Checks if this tag is restricting a move based on a user's decisions during the target selection phase * Checks if this tag is restricting a move based on a user's decisions during the target selection phase
@ -327,6 +338,16 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
pokemon.setStat(Stat.ATK, pokemon.getStat(Stat.ATK, false) * 1.5, false); pokemon.setStat(Stat.ATK, pokemon.getStat(Stat.ATK, false) * 1.5, false);
} }
/**
* Loads the Gorilla Tactics Battler Tag along with its unique class variable moveId
* @override
* @param source Gorilla Tactics' {@linkcode BattlerTag} information
*/
public override loadTag(source: BattlerTag | any): void {
super.loadTag(source);
this.moveId = source.moveId;
}
/** /**
* *
* @override * @override
@ -2561,8 +2582,6 @@ export class MysteryEncounterPostSummonTag extends BattlerTag {
* Torment does not interrupt the move if the move is performed consecutively in the same turn and right after Torment is applied * Torment does not interrupt the move if the move is performed consecutively in the same turn and right after Torment is applied
*/ */
export class TormentTag extends MoveRestrictionBattlerTag { export class TormentTag extends MoveRestrictionBattlerTag {
private target: Pokemon;
constructor(sourceId: number) { constructor(sourceId: number) {
super(BattlerTagType.TORMENT, BattlerTagLapseType.AFTER_MOVE, 1, Moves.TORMENT, sourceId); super(BattlerTagType.TORMENT, BattlerTagLapseType.AFTER_MOVE, 1, Moves.TORMENT, sourceId);
} }
@ -2574,7 +2593,6 @@ export class TormentTag extends MoveRestrictionBattlerTag {
*/ */
override onAdd(pokemon: Pokemon) { override onAdd(pokemon: Pokemon) {
super.onAdd(pokemon); super.onAdd(pokemon);
this.target = pokemon;
pokemon.scene.queueMessage(i18next.t("battlerTags:tormentOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500); pokemon.scene.queueMessage(i18next.t("battlerTags:tormentOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500);
} }
@ -2593,15 +2611,18 @@ export class TormentTag extends MoveRestrictionBattlerTag {
* @param {Moves} move the move under investigation * @param {Moves} move the move under investigation
* @returns `true` if there is valid consecutive usage | `false` if the moves are different from each other * @returns `true` if there is valid consecutive usage | `false` if the moves are different from each other
*/ */
override isMoveRestricted(move: Moves): boolean { public override isMoveRestricted(move: Moves, user: Pokemon): boolean {
const lastMove = this.target.getLastXMoves(1)[0]; if (!user) {
return false;
}
const lastMove = user.getLastXMoves(1)[0];
if ( !lastMove ) { if ( !lastMove ) {
return false; return false;
} }
// 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) || this.target.getTag(BattlerTagType.FRENZY) || moveObj.hasAttr(ChargeAttr); const isUnaffected = moveObj.hasAttr(ConsecutiveUseDoublePowerAttr) || user.getTag(BattlerTagType.FRENZY) || moveObj.hasAttr(ChargeAttr);
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;
@ -2653,37 +2674,39 @@ export class TauntTag extends MoveRestrictionBattlerTag {
* The tag is only removed when the source-user is removed from the field. * The tag is only removed when the source-user is removed from the field.
*/ */
export class ImprisonTag extends MoveRestrictionBattlerTag { export class ImprisonTag extends MoveRestrictionBattlerTag {
private source: Pokemon | null;
constructor(sourceId: number) { constructor(sourceId: number) {
super(BattlerTagType.IMPRISON, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.AFTER_MOVE ], 1, Moves.IMPRISON, sourceId); super(BattlerTagType.IMPRISON, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.AFTER_MOVE ], 1, Moves.IMPRISON, sourceId);
} }
override onAdd(pokemon: Pokemon) {
if (this.sourceId) {
this.source = pokemon.scene.getPokemonById(this.sourceId);
}
}
/** /**
* Checks if the source of Imprison is still active * Checks if the source of Imprison is still active
* @param _pokemon * @override
* @param _lapseType * @param pokemon The pokemon this tag is attached to
* @returns `true` if the source is still active * @returns `true` if the source is still active
*/ */
override lapse(_pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean { public override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
return this.source?.isActive(true) ?? false; const source = this.getSourcePokemon(pokemon.scene);
if (source) {
if (lapseType === BattlerTagLapseType.PRE_MOVE) {
return super.lapse(pokemon, lapseType) && source.isActive(true);
} else {
return source.isActive(true);
}
}
return false;
} }
/** /**
* Checks if the source of the tag has the parameter move in its moveset and that the source is still active * Checks if the source of the tag has the parameter move in its moveset and that the source is still active
* @override
* @param {Moves} move the move under investigation * @param {Moves} move the move under investigation
* @returns `false` if either condition is not met * @returns `false` if either condition is not met
*/ */
override isMoveRestricted(move: Moves): boolean { public override isMoveRestricted(move: Moves, user: Pokemon): boolean {
if (this.source) { const source = this.getSourcePokemon(user.scene);
const sourceMoveset = this.source.getMoveset().map(m => m!.moveId); if (source) {
return sourceMoveset?.includes(move) && this.source.isActive(true); const sourceMoveset = source.getMoveset().map(m => m!.moveId);
return sourceMoveset?.includes(move) && source.isActive(true);
} }
return false; return false;
} }
@ -2752,6 +2775,44 @@ export class TelekinesisTag extends BattlerTag {
} }
} }
/**
* Tag that swaps the user's base ATK stat with its base DEF stat.
* @extends BattlerTag
*/
export class PowerTrickTag extends BattlerTag {
constructor(sourceMove: Moves, sourceId: number) {
super(BattlerTagType.POWER_TRICK, BattlerTagLapseType.CUSTOM, 0, sourceMove, sourceId, true);
}
onAdd(pokemon: Pokemon): void {
this.swapStat(pokemon);
pokemon.scene.queueMessage(i18next.t("battlerTags:powerTrickActive", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
}
onRemove(pokemon: Pokemon): void {
this.swapStat(pokemon);
pokemon.scene.queueMessage(i18next.t("battlerTags:powerTrickActive", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
}
/**
* Removes the Power Trick tag and reverts any stat changes if the tag is already applied.
* @param {Pokemon} pokemon The {@linkcode Pokemon} that already has the Power Trick tag.
*/
onOverlap(pokemon: Pokemon): void {
pokemon.removeTag(this.tagType);
}
/**
* Swaps the user's base ATK stat with its base DEF stat.
* @param {Pokemon} pokemon The {@linkcode Pokemon} whose stats will be swapped.
*/
swapStat(pokemon: Pokemon): void {
const temp = pokemon.getStat(Stat.ATK, false);
pokemon.setStat(Stat.ATK, pokemon.getStat(Stat.DEF, false), false);
pokemon.setStat(Stat.DEF, temp, false);
}
}
/** /**
* Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID. * Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID.
* @param sourceId - The ID of the pokemon adding the tag * @param sourceId - The ID of the pokemon adding the tag
@ -2929,6 +2990,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new SyrupBombTag(sourceId); return new SyrupBombTag(sourceId);
case BattlerTagType.TELEKINESIS: case BattlerTagType.TELEKINESIS:
return new TelekinesisTag(sourceMove); return new TelekinesisTag(sourceMove);
case BattlerTagType.POWER_TRICK:
return new PowerTrickTag(sourceMove, sourceId);
case BattlerTagType.NONE: case BattlerTagType.NONE:
default: default:
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);

View File

@ -808,7 +808,7 @@ export default class Move implements Localizable {
source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, new Utils.IntegerHolder(0), power); source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, new Utils.IntegerHolder(0), power);
if (!this.hasAttr(TypelessAttr)) { if (!this.hasAttr(TypelessAttr)) {
source.scene.arena.applyTags(WeakenMoveTypeTag, this.type, power); source.scene.arena.applyTags(WeakenMoveTypeTag, simulated, this.type, power);
source.scene.applyModifiers(AttackTypeBoosterModifier, source.isPlayer(), source, this.type, power); source.scene.applyModifiers(AttackTypeBoosterModifier, source.isPlayer(), source, this.type, power);
} }
@ -1024,9 +1024,9 @@ export class MoveEffectAttr extends MoveAttr {
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, moveChance, move, target, selfEffect, showAbility); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, moveChance, move, target, selfEffect, showAbility);
if (!move.hasAttr(FlinchAttr) || moveChance.value <= move.chance) { if ((!move.hasAttr(FlinchAttr) || moveChance.value <= move.chance) && !move.hasAttr(SecretPowerAttr)) {
const userSide = user.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; const userSide = user.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
user.scene.arena.applyTagsForSide(ArenaTagType.WATER_FIRE_PLEDGE, userSide, moveChance); user.scene.arena.applyTagsForSide(ArenaTagType.WATER_FIRE_PLEDGE, userSide, false, moveChance);
} }
if (!selfEffect) { if (!selfEffect) {
@ -2875,6 +2875,162 @@ export class StatStageChangeAttr extends MoveEffectAttr {
} }
} }
/**
* Attribute used to determine the Biome/Terrain-based secondary effect of Secret Power
*/
export class SecretPowerAttr extends MoveEffectAttr {
constructor() {
super(false);
}
/**
* Used to determine if the move should apply a secondary effect based on Secret Power's 30% chance
* @returns `true` if the move's secondary effect should apply
*/
override canApply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
this.effectChanceOverride = move.chance;
const moveChance = this.getMoveChance(user, target, move, this.selfTarget);
if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) {
return true;
} else {
return false;
}
}
/**
* Used to apply the secondary effect to the target Pokemon
* @returns `true` if a secondary effect is successfully applied
*/
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean | Promise<boolean> {
if (!super.apply(user, target, move, args)) {
return false;
}
let secondaryEffect: MoveEffectAttr;
const terrain = user.scene.arena.getTerrainType();
if (terrain !== TerrainType.NONE) {
secondaryEffect = this.determineTerrainEffect(terrain);
} else {
const biome = user.scene.arena.biomeType;
secondaryEffect = this.determineBiomeEffect(biome);
}
// effectChanceOverride used in the application of the actual secondary effect
secondaryEffect.effectChanceOverride = 100;
return secondaryEffect.apply(user, target, move, []);
}
/**
* Determines the secondary effect based on terrain.
* Takes precedence over biome-based effects.
* ```
* Electric Terrain | Paralysis
* Misty Terrain | SpAtk -1
* Grassy Terrain | Sleep
* Psychic Terrain | Speed -1
* ```
* @param terrain - {@linkcode TerrainType} The current terrain
* @returns the chosen secondary effect {@linkcode MoveEffectAttr}
*/
private determineTerrainEffect(terrain: TerrainType): MoveEffectAttr {
let secondaryEffect: MoveEffectAttr;
switch (terrain) {
case TerrainType.ELECTRIC:
default:
secondaryEffect = new StatusEffectAttr(StatusEffect.PARALYSIS, false);
break;
case TerrainType.MISTY:
secondaryEffect = new StatStageChangeAttr([ Stat.SPATK ], -1, false);
break;
case TerrainType.GRASSY:
secondaryEffect = new StatusEffectAttr(StatusEffect.SLEEP, false);
break;
case TerrainType.PSYCHIC:
secondaryEffect = new StatStageChangeAttr([ Stat.SPD ], -1, false);
break;
}
return secondaryEffect;
}
/**
* Determines the secondary effect based on biome
* ```
* Town, Metropolis, Slum, Dojo, Laboratory, Power Plant + Default | Paralysis
* Plains, Grass, Tall Grass, Forest, Jungle, Meadow | Sleep
* Swamp, Mountain, Temple, Ruins | Speed -1
* Ice Cave, Snowy Forest | Freeze
* Volcano | Burn
* Fairy Cave | SpAtk -1
* Desert, Construction Site, Beach, Island, Badlands | Accuracy -1
* Sea, Lake, Seabed | Atk -1
* Cave, Wasteland, Graveyard, Abyss, Space | Flinch
* End | Def -1
* ```
* @param biome - The current {@linkcode Biome} the battle is set in
* @returns the chosen secondary effect {@linkcode MoveEffectAttr}
*/
private determineBiomeEffect(biome: Biome): MoveEffectAttr {
let secondaryEffect: MoveEffectAttr;
switch (biome) {
case Biome.PLAINS:
case Biome.GRASS:
case Biome.TALL_GRASS:
case Biome.FOREST:
case Biome.JUNGLE:
case Biome.MEADOW:
secondaryEffect = new StatusEffectAttr(StatusEffect.SLEEP, false);
break;
case Biome.SWAMP:
case Biome.MOUNTAIN:
case Biome.TEMPLE:
case Biome.RUINS:
secondaryEffect = new StatStageChangeAttr([ Stat.SPD ], -1, false);
break;
case Biome.ICE_CAVE:
case Biome.SNOWY_FOREST:
secondaryEffect = new StatusEffectAttr(StatusEffect.FREEZE, false);
break;
case Biome.VOLCANO:
secondaryEffect = new StatusEffectAttr(StatusEffect.BURN, false);
break;
case Biome.FAIRY_CAVE:
secondaryEffect = new StatStageChangeAttr([ Stat.SPATK ], -1, false);
break;
case Biome.DESERT:
case Biome.CONSTRUCTION_SITE:
case Biome.BEACH:
case Biome.ISLAND:
case Biome.BADLANDS:
secondaryEffect = new StatStageChangeAttr([ Stat.ACC ], -1, false);
break;
case Biome.SEA:
case Biome.LAKE:
case Biome.SEABED:
secondaryEffect = new StatStageChangeAttr([ Stat.ATK ], -1, false);
break;
case Biome.CAVE:
case Biome.WASTELAND:
case Biome.GRAVEYARD:
case Biome.ABYSS:
case Biome.SPACE:
secondaryEffect = new AddBattlerTagAttr(BattlerTagType.FLINCHED, false, true);
break;
case Biome.END:
secondaryEffect = new StatStageChangeAttr([ Stat.DEF ], -1, false);
break;
case Biome.TOWN:
case Biome.METROPOLIS:
case Biome.SLUM:
case Biome.DOJO:
case Biome.FACTORY:
case Biome.LABORATORY:
case Biome.POWER_PLANT:
default:
secondaryEffect = new StatusEffectAttr(StatusEffect.PARALYSIS, false);
break;
}
return secondaryEffect;
}
}
export class PostVictoryStatStageChangeAttr extends MoveAttr { export class PostVictoryStatStageChangeAttr extends MoveAttr {
private stats: BattleStat[]; private stats: BattleStat[];
private stages: number; private stages: number;
@ -3834,8 +3990,8 @@ export class LastMoveDoublePowerAttr extends VariablePowerAttr {
for (const p of pokemonActed) { for (const p of pokemonActed) {
const [ lastMove ] = p.getLastXMoves(1); const [ lastMove ] = p.getLastXMoves(1);
if (lastMove.result !== MoveResult.FAIL) { if (lastMove?.result !== MoveResult.FAIL) {
if ((lastMove.result === MoveResult.SUCCESS) && (lastMove.move === this.move)) { if ((lastMove?.result === MoveResult.SUCCESS) && (lastMove?.move === this.move)) {
power.value *= 2; power.value *= 2;
return true; return true;
} else { } else {
@ -4736,7 +4892,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
} }
canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!super.canApply(user, target, move, args) || (this.cancelOnFail === true && user.getLastXMoves(1)[0].result === MoveResult.FAIL)) { if (!super.canApply(user, target, move, args) || (this.cancelOnFail === true && user.getLastXMoves(1)[0]?.result === MoveResult.FAIL)) {
return false; return false;
} else { } else {
return true; return true;
@ -5174,7 +5330,7 @@ export class AddArenaTagAttr extends MoveEffectAttr {
return false; return false;
} }
if ((move.chance < 0 || move.chance === 100 || user.randSeedInt(100) < move.chance) && user.getLastXMoves(1)[0].result === MoveResult.SUCCESS) { if ((move.chance < 0 || move.chance === 100 || user.randSeedInt(100) < move.chance) && user.getLastXMoves(1)[0]?.result === MoveResult.SUCCESS) {
user.scene.arena.addTag(this.tagType, this.turnCount, move.id, user.id, (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY); user.scene.arena.addTag(this.tagType, this.turnCount, move.id, user.id, (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY);
return true; return true;
} }
@ -5249,7 +5405,7 @@ export class AddArenaTrapTagHitAttr extends AddArenaTagAttr {
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true); const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
const side = (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; const side = (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
const tag = user.scene.arena.getTagOnSide(this.tagType, side) as ArenaTrapTag; const tag = user.scene.arena.getTagOnSide(this.tagType, side) as ArenaTrapTag;
if ((moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) && user.getLastXMoves(1)[0].result === MoveResult.SUCCESS) { if ((moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) && user.getLastXMoves(1)[0]?.result === MoveResult.SUCCESS) {
user.scene.arena.addTag(this.tagType, 0, move.id, user.id, side); user.scene.arena.addTag(this.tagType, 0, move.id, user.id, side);
if (!tag) { if (!tag) {
return true; return true;
@ -5386,7 +5542,7 @@ export class AddPledgeEffectAttr extends AddArenaTagAttr {
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
// TODO: add support for `HIT` effect triggering in AddArenaTagAttr to remove the need for this check // TODO: add support for `HIT` effect triggering in AddArenaTagAttr to remove the need for this check
if (user.getLastXMoves(1)[0].result !== MoveResult.SUCCESS) { if (user.getLastXMoves(1)[0]?.result !== MoveResult.SUCCESS) {
return false; return false;
} }
@ -5484,37 +5640,38 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
*/ */
const switchOutTarget = this.selfSwitch ? user : target; const switchOutTarget = this.selfSwitch ? user : target;
if (switchOutTarget instanceof PlayerPokemon) { if (switchOutTarget instanceof PlayerPokemon) {
// Switch out logic for the player's Pokemon
if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
return false; return false;
} }
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
if (switchOutTarget.hp > 0) { if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
user.scene.prependToPhase(new SwitchPhase(user.scene, this.switchType, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase); user.scene.prependToPhase(new SwitchPhase(user.scene, this.switchType, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase);
return true; return true;
} }
return false; return false;
} else if (user.scene.currentBattle.battleType !== BattleType.WILD) { } else if (user.scene.currentBattle.battleType !== BattleType.WILD) {
// Switch out logic for trainer battles
if (switchOutTarget.scene.getEnemyParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { if (switchOutTarget.scene.getEnemyParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
return false; return false;
} }
// Switch out logic for trainer battles
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
if (switchOutTarget.hp > 0) { if (switchOutTarget.hp > 0) {
// for opponent switching out // for opponent switching out
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
user.scene.prependToPhase(new SwitchSummonPhase(user.scene, this.switchType, switchOutTarget.getFieldIndex(), user.scene.prependToPhase(new SwitchSummonPhase(user.scene, this.switchType, switchOutTarget.getFieldIndex(),
(user.scene.currentBattle.trainer ? user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0), (user.scene.currentBattle.trainer ? user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0),
false, false), MoveEndPhase); false, false), MoveEndPhase);
} }
} else { } else {
// Switch out logic for everything else (eg: WILD battles)
if (user.scene.currentBattle.waveIndex % 10 === 0) { if (user.scene.currentBattle.waveIndex % 10 === 0) {
return false; return false;
} }
// Switch out logic for everything else (eg: WILD battles)
switchOutTarget.leaveField(false);
if (switchOutTarget.hp) { if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(false);
user.scene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500); user.scene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500);
// in double battles redirect potential moves off fled pokemon // in double battles redirect potential moves off fled pokemon
@ -6429,6 +6586,9 @@ export class TransformAttr extends MoveEffectAttr {
user.summonData.gender = target.getGender(); user.summonData.gender = target.getGender();
user.summonData.fusionGender = target.getFusionGender(); user.summonData.fusionGender = target.getFusionGender();
// Power Trick's effect will not preserved after using Transform
user.removeTag(BattlerTagType.POWER_TRICK);
// Copy all stats (except HP) // Copy all stats (except HP)
for (const s of EFFECTIVE_STATS) { for (const s of EFFECTIVE_STATS) {
user.setStat(s, target.getStat(s, false), false); user.setStat(s, target.getStat(s, false), false);
@ -7883,7 +8043,8 @@ export function initMoves() {
.attr(SwitchAbilitiesAttr), .attr(SwitchAbilitiesAttr),
new StatusMove(Moves.IMPRISON, Type.PSYCHIC, 100, 10, -1, 0, 3) new StatusMove(Moves.IMPRISON, Type.PSYCHIC, 100, 10, -1, 0, 3)
.ignoresSubstitute() .ignoresSubstitute()
.attr(AddArenaTagAttr, ArenaTagType.IMPRISON, 1, true, false), .attr(AddArenaTagAttr, ArenaTagType.IMPRISON, 1, true, false)
.target(MoveTarget.ENEMY_SIDE),
new SelfStatusMove(Moves.REFRESH, Type.NORMAL, -1, 20, -1, 0, 3) new SelfStatusMove(Moves.REFRESH, Type.NORMAL, -1, 20, -1, 0, 3)
.attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN) .attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN)
.condition((user, target, move) => !!user.status && (user.status.effect === StatusEffect.PARALYSIS || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.BURN)), .condition((user, target, move) => !!user.status && (user.status.effect === StatusEffect.PARALYSIS || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.BURN)),
@ -7893,7 +8054,7 @@ export function initMoves() {
.unimplemented(), .unimplemented(),
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)
.partial(), // No effect implemented .attr(SecretPowerAttr),
new AttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3) new AttackMove(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) .attr(ChargeAttr, ChargeAnim.DIVE_CHARGING, i18next.t("moveTriggers:hidUnderwater", { pokemonName: "{USER}" }), BattlerTagType.UNDERWATER, true)
.attr(GulpMissileTagAttr) .attr(GulpMissileTagAttr)
@ -8151,7 +8312,7 @@ export function initMoves() {
.attr(OpponentHighHpPowerAttr, 120) .attr(OpponentHighHpPowerAttr, 120)
.makesContact(), .makesContact(),
new SelfStatusMove(Moves.POWER_TRICK, Type.PSYCHIC, -1, 10, -1, 0, 4) new SelfStatusMove(Moves.POWER_TRICK, Type.PSYCHIC, -1, 10, -1, 0, 4)
.unimplemented(), .attr(AddBattlerTagAttr, BattlerTagType.POWER_TRICK, true),
new StatusMove(Moves.GASTRO_ACID, Type.POISON, 100, 10, -1, 0, 4) new StatusMove(Moves.GASTRO_ACID, Type.POISON, 100, 10, -1, 0, 4)
.attr(SuppressAbilitiesAttr), .attr(SuppressAbilitiesAttr),
new StatusMove(Moves.LUCKY_CHANT, Type.NORMAL, -1, 30, -1, 0, 4) new StatusMove(Moves.LUCKY_CHANT, Type.NORMAL, -1, 30, -1, 0, 4)

View File

@ -154,7 +154,7 @@ export const ATrainersTestEncounter: MysteryEncounter =
}; };
encounter.setDialogueToken("eggType", i18next.t(`${namespace}:eggTypes.epic`)); encounter.setDialogueToken("eggType", i18next.t(`${namespace}:eggTypes.epic`));
setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.SACRED_ASH ], guaranteedModifierTiers: [ ModifierTier.ROGUE, ModifierTier.ULTRA ], fillRemaining: true }, [ eggOptions ]); setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.SACRED_ASH ], guaranteedModifierTiers: [ ModifierTier.ROGUE, ModifierTier.ULTRA ], fillRemaining: true }, [ eggOptions ]);
return initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
} }
) )
.withSimpleOption( .withSimpleOption(

View File

@ -286,7 +286,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
ignorePp: true ignorePp: true
}); });
transitionMysteryEncounterIntroVisuals(scene, true, true, 500); await transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]);
}) })
.build() .build()
@ -328,7 +328,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
}); });
await scene.updateModifiers(true); await scene.updateModifiers(true);
transitionMysteryEncounterIntroVisuals(scene, true, true, 500); await transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
leaveEncounterWithoutBattle(scene, true); leaveEncounterWithoutBattle(scene, true);
}) })
.build() .build()
@ -359,7 +359,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
greedent.moveset = [ new PokemonMove(Moves.THRASH), new PokemonMove(Moves.BODY_PRESS), new PokemonMove(Moves.STUFF_CHEEKS), new PokemonMove(Moves.SLACK_OFF) ]; greedent.moveset = [ new PokemonMove(Moves.THRASH), new PokemonMove(Moves.BODY_PRESS), new PokemonMove(Moves.STUFF_CHEEKS), new PokemonMove(Moves.SLACK_OFF) ];
greedent.passive = true; greedent.passive = true;
transitionMysteryEncounterIntroVisuals(scene, true, true, 500); await transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
await catchPokemon(scene, greedent, null, PokeballType.POKEBALL, false); await catchPokemon(scene, greedent, null, PokeballType.POKEBALL, false);
leaveEncounterWithoutBattle(scene, true); leaveEncounterWithoutBattle(scene, true);
}) })

View File

@ -228,7 +228,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
}) })
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
// Learn its Dance // Learn its Dance
hideOricorioPokemon(scene); await hideOricorioPokemon(scene);
leaveEncounterWithoutBattle(scene, true); leaveEncounterWithoutBattle(scene, true);
}) })
.build() .build()
@ -303,7 +303,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
} }
} }
hideOricorioPokemon(scene); await hideOricorioPokemon(scene);
await catchPokemon(scene, oricorio, null, PokeballType.POKEBALL, false); await catchPokemon(scene, oricorio, null, PokeballType.POKEBALL, false);
leaveEncounterWithoutBattle(scene, true); leaveEncounterWithoutBattle(scene, true);
}) })

View File

@ -182,7 +182,7 @@ export const DarkDealEncounter: MysteryEncounter =
const config: EnemyPartyConfig = { const config: EnemyPartyConfig = {
pokemonConfigs: [ pokemonConfig ], pokemonConfigs: [ pokemonConfig ],
}; };
return initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.build() .build()
) )

View File

@ -222,12 +222,13 @@ export const FieryFalloutEncounter: MysteryEncounter =
], ],
}) })
.withPreOptionPhase(async (scene: BattleScene) => { .withPreOptionPhase(async (scene: BattleScene) => {
// Do NOT await this, to prevent player from repeatedly pressing options
transitionMysteryEncounterIntroVisuals(scene, false, false, 2000); transitionMysteryEncounterIntroVisuals(scene, false, false, 2000);
}) })
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
// Fire types help calm the Volcarona // Fire types help calm the Volcarona
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
transitionMysteryEncounterIntroVisuals(scene); await transitionMysteryEncounterIntroVisuals(scene);
setEncounterRewards(scene, setEncounterRewards(scene,
{ fillRemaining: true }, { fillRemaining: true },
undefined, undefined,

View File

@ -152,7 +152,7 @@ export const FunAndGamesEncounter: MysteryEncounter =
}, },
async (scene: BattleScene) => { async (scene: BattleScene) => {
// Leave encounter with no rewards or exp // Leave encounter with no rewards or exp
transitionMysteryEncounterIntroVisuals(scene, true, true); await transitionMysteryEncounterIntroVisuals(scene, true, true);
leaveEncounterWithoutBattle(scene, true); leaveEncounterWithoutBattle(scene, true);
return true; return true;
} }

View File

@ -399,7 +399,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
if (modifier.stackCount === 0) { if (modifier.stackCount === 0) {
scene.removeModifier(modifier); scene.removeModifier(modifier);
} }
scene.updateModifiers(true, true); await scene.updateModifiers(true, true);
// Generate a trainer name // Generate a trainer name
const traderName = generateRandomTraderName(); const traderName = generateRandomTraderName();

View File

@ -129,7 +129,7 @@ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.with
* *
* @param scene Battle scene * @param scene Battle scene
*/ */
async function handlePokemonGuidingYouPhase(scene: BattleScene) { function handlePokemonGuidingYouPhase(scene: BattleScene) {
const laprasSpecies = getPokemonSpecies(Species.LAPRAS); const laprasSpecies = getPokemonSpecies(Species.LAPRAS);
const { mysteryEncounter } = scene.currentBattle; const { mysteryEncounter } = scene.currentBattle;

View File

@ -147,11 +147,11 @@ export const MysteriousChallengersEncounter: MysteryEncounter =
setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.TM_COMMON, modifierTypes.TM_GREAT, modifierTypes.MEMORY_MUSHROOM ], fillRemaining: true }); setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.TM_COMMON, modifierTypes.TM_GREAT, modifierTypes.MEMORY_MUSHROOM ], fillRemaining: true });
// Seed offsets to remove possibility of different trainers having exact same teams // Seed offsets to remove possibility of different trainers having exact same teams
let ret; let initBattlePromise: Promise<void>;
scene.executeWithSeedOffset(() => { scene.executeWithSeedOffset(() => {
ret = initBattleWithEnemyConfig(scene, config); initBattlePromise = initBattleWithEnemyConfig(scene, config);
}, scene.currentBattle.waveIndex * 10); }, scene.currentBattle.waveIndex * 10);
return ret; await initBattlePromise!;
} }
) )
.withSimpleOption( .withSimpleOption(
@ -172,11 +172,11 @@ export const MysteriousChallengersEncounter: MysteryEncounter =
setEncounterRewards(scene, { guaranteedModifierTiers: [ ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT ], fillRemaining: true }); setEncounterRewards(scene, { guaranteedModifierTiers: [ ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT ], fillRemaining: true });
// Seed offsets to remove possibility of different trainers having exact same teams // Seed offsets to remove possibility of different trainers having exact same teams
let ret; let initBattlePromise: Promise<void>;
scene.executeWithSeedOffset(() => { scene.executeWithSeedOffset(() => {
ret = initBattleWithEnemyConfig(scene, config); initBattlePromise = initBattleWithEnemyConfig(scene, config);
}, scene.currentBattle.waveIndex * 100); }, scene.currentBattle.waveIndex * 100);
return ret; await initBattlePromise!;
} }
) )
.withSimpleOption( .withSimpleOption(
@ -200,11 +200,11 @@ export const MysteriousChallengersEncounter: MysteryEncounter =
setEncounterRewards(scene, { guaranteedModifierTiers: [ ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT ], fillRemaining: true }); setEncounterRewards(scene, { guaranteedModifierTiers: [ ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT ], fillRemaining: true });
// Seed offsets to remove possibility of different trainers having exact same teams // Seed offsets to remove possibility of different trainers having exact same teams
let ret; let initBattlePromise: Promise<void>;
scene.executeWithSeedOffset(() => { scene.executeWithSeedOffset(() => {
ret = initBattleWithEnemyConfig(scene, config); initBattlePromise = initBattleWithEnemyConfig(scene, config);
}, scene.currentBattle.waveIndex * 1000); }, scene.currentBattle.waveIndex * 1000);
return ret; await initBattlePromise!;
} }
) )
.withOutroDialogue([ .withOutroDialogue([

View File

@ -184,7 +184,7 @@ export const MysteriousChestEncounter: MysteryEncounter =
scene.unshiftPhase(new GameOverPhase(scene)); scene.unshiftPhase(new GameOverPhase(scene));
} else { } else {
// Show which Pokemon was KOed, then start battle against Gimmighoul // Show which Pokemon was KOed, then start battle against Gimmighoul
transitionMysteryEncounterIntroVisuals(scene, true, true, 500); await transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
setEncounterRewards(scene, { fillRemaining: true }); setEncounterRewards(scene, { fillRemaining: true });
await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]);
} }

View File

@ -303,13 +303,16 @@ async function summonSafariPokemon(scene: BattleScene) {
scene.unshiftPhase(new SummonPhase(scene, 0, false)); scene.unshiftPhase(new SummonPhase(scene, 0, false));
encounter.setDialogueToken("pokemonName", getPokemonNameWithAffix(pokemon)); encounter.setDialogueToken("pokemonName", getPokemonNameWithAffix(pokemon));
showEncounterText(scene, getEncounterText(scene, "battle:singleWildAppeared") ?? "", null, 1500, false)
.then(() => { // TODO: If we await showEncounterText here, then the text will display without
// the wild Pokemon on screen, but if we don't await it, then the text never
// shows up and the IV scanner breaks. For now, we place the IV scanner code
// separately so that at least the IV scanner works.
const ivScannerModifier = scene.findModifier(m => m instanceof IvScannerModifier); const ivScannerModifier = scene.findModifier(m => m instanceof IvScannerModifier);
if (ivScannerModifier) { if (ivScannerModifier) {
scene.pushPhase(new ScanIvsPhase(scene, pokemon.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6))); scene.pushPhase(new ScanIvsPhase(scene, pokemon.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6)));
} }
});
} }
function throwPokeball(scene: BattleScene, pokemon: EnemyPokemon): Promise<boolean> { function throwPokeball(scene: BattleScene, pokemon: EnemyPokemon): Promise<boolean> {

View File

@ -142,7 +142,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter =
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);
chosenPokemon.updateInfo(); await chosenPokemon.updateInfo();
}) })
.build() .build()
) )
@ -204,7 +204,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter =
queueEncounterMessage(scene, `${namespace}:no_bad_effects`); queueEncounterMessage(scene, `${namespace}:no_bad_effects`);
setEncounterExp(scene, [ chosenPokemon.id ], 100); setEncounterExp(scene, [ chosenPokemon.id ], 100);
chosenPokemon.updateInfo(); await chosenPokemon.updateInfo();
}) })
.build() .build()
) )

View File

@ -149,7 +149,7 @@ export const TeleportingHijinksEncounter: MysteryEncounter =
const magnet = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.STEEL ])!; const magnet = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.STEEL ])!;
const metalCoat = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.ELECTRIC ])!; const metalCoat = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.ELECTRIC ])!;
setEncounterRewards(scene, { guaranteedModifierTypeOptions: [ magnet, metalCoat ], fillRemaining: true }); setEncounterRewards(scene, { guaranteedModifierTypeOptions: [ magnet, metalCoat ], fillRemaining: true });
transitionMysteryEncounterIntroVisuals(scene, true, true); await transitionMysteryEncounterIntroVisuals(scene, true, true);
await initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
} }
) )

View File

@ -61,7 +61,7 @@ const POOL_1_POKEMON: (Species | BreederSpeciesEvolution)[][] = [
const POOL_2_POKEMON: (Species | BreederSpeciesEvolution)[][] = [ const POOL_2_POKEMON: (Species | BreederSpeciesEvolution)[][] = [
[ Species.PICHU, new BreederSpeciesEvolution(Species.PIKACHU, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.RAICHU, FINAL_STAGE_EVOLUTION_WAVE) ], [ Species.PICHU, new BreederSpeciesEvolution(Species.PIKACHU, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.RAICHU, FINAL_STAGE_EVOLUTION_WAVE) ],
[ Species.PICHU, new BreederSpeciesEvolution(Species.PIKACHU, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.ALOLA_RAICHU, FINAL_STAGE_EVOLUTION_WAVE) ], [ Species.PICHU, new BreederSpeciesEvolution(Species.PIKACHU, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.ALOLA_RAICHU, FINAL_STAGE_EVOLUTION_WAVE) ],
[ Species.JYNX ], [ Species.SMOOCHUM, new BreederSpeciesEvolution(Species.JYNX, SECOND_STAGE_EVOLUTION_WAVE) ],
[ Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONLEE, SECOND_STAGE_EVOLUTION_WAVE) ], [ Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONLEE, SECOND_STAGE_EVOLUTION_WAVE) ],
[ Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONCHAN, SECOND_STAGE_EVOLUTION_WAVE) ], [ Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONCHAN, SECOND_STAGE_EVOLUTION_WAVE) ],
[ Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONTOP, SECOND_STAGE_EVOLUTION_WAVE) ], [ Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONTOP, SECOND_STAGE_EVOLUTION_WAVE) ],
@ -245,7 +245,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
} }
encounter.onGameOver = onGameOver; encounter.onGameOver = onGameOver;
initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.withPostOptionPhase(async (scene: BattleScene) => { .withPostOptionPhase(async (scene: BattleScene) => {
await doPostEncounterCleanup(scene); await doPostEncounterCleanup(scene);
@ -297,7 +297,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
} }
encounter.onGameOver = onGameOver; encounter.onGameOver = onGameOver;
initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.withPostOptionPhase(async (scene: BattleScene) => { .withPostOptionPhase(async (scene: BattleScene) => {
await doPostEncounterCleanup(scene); await doPostEncounterCleanup(scene);
@ -349,7 +349,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
} }
encounter.onGameOver = onGameOver; encounter.onGameOver = onGameOver;
initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.withPostOptionPhase(async (scene: BattleScene) => { .withPostOptionPhase(async (scene: BattleScene) => {
await doPostEncounterCleanup(scene); await doPostEncounterCleanup(scene);

View File

@ -201,7 +201,7 @@ export const TheStrongStuffEncounter: MysteryEncounter =
}); });
encounter.dialogue.outro = []; encounter.dialogue.outro = [];
transitionMysteryEncounterIntroVisuals(scene, true, true, 500); await transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]);
} }
) )

View File

@ -111,8 +111,8 @@ export const TheWinstrateChallengeEncounter: MysteryEncounter =
}, },
async (scene: BattleScene) => { async (scene: BattleScene) => {
// Spawn 5 trainer battles back to back with Macho Brace in rewards // Spawn 5 trainer battles back to back with Macho Brace in rewards
scene.currentBattle.mysteryEncounter!.doContinueEncounter = (scene: BattleScene) => { scene.currentBattle.mysteryEncounter!.doContinueEncounter = async (scene: BattleScene) => {
return endTrainerBattleAndShowDialogue(scene); await endTrainerBattleAndShowDialogue(scene);
}; };
await transitionMysteryEncounterIntroVisuals(scene, true, false); await transitionMysteryEncounterIntroVisuals(scene, true, false);
await spawnNextTrainerOrEndEncounter(scene); await spawnNextTrainerOrEndEncounter(scene);

View File

@ -162,7 +162,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase); setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase);
return initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.build() .build()
) )
@ -238,7 +238,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase); setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase);
return initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.build() .build()
) )
@ -351,7 +351,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase); setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase);
return initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.build() .build()
) )

View File

@ -105,7 +105,7 @@ export const TrashToTreasureEncounter: MysteryEncounter =
}) })
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
// Gain 2 Leftovers and 2 Shell Bell // Gain 2 Leftovers and 2 Shell Bell
transitionMysteryEncounterIntroVisuals(scene); await transitionMysteryEncounterIntroVisuals(scene);
await tryApplyDigRewardItems(scene); await tryApplyDigRewardItems(scene);
const blackSludge = generateModifierType(scene, modifierTypes.MYSTERY_ENCOUNTER_BLACK_SLUDGE, [ SHOP_ITEM_COST_MULTIPLIER ]); const blackSludge = generateModifierType(scene, modifierTypes.MYSTERY_ENCOUNTER_BLACK_SLUDGE, [ SHOP_ITEM_COST_MULTIPLIER ]);
@ -136,7 +136,7 @@ export const TrashToTreasureEncounter: MysteryEncounter =
// Investigate garbage, battle Gmax Garbodor // Investigate garbage, battle Gmax Garbodor
scene.setFieldScale(0.75); scene.setFieldScale(0.75);
await showEncounterText(scene, `${namespace}:option.2.selected_2`); await showEncounterText(scene, `${namespace}:option.2.selected_2`);
transitionMysteryEncounterIntroVisuals(scene); await transitionMysteryEncounterIntroVisuals(scene);
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
@ -222,7 +222,7 @@ async function tryApplyDigRewardItems(scene: BattleScene) {
await showEncounterText(scene, i18next.t("battle:rewardGainCount", { modifierName: shellBell.name, count: 2 }), null, undefined, true); await showEncounterText(scene, i18next.t("battle:rewardGainCount", { modifierName: shellBell.name, count: 2 }), null, undefined, true);
} }
async function doGarbageDig(scene: BattleScene) { function doGarbageDig(scene: BattleScene) {
scene.playSound("battle_anims/PRSFX- Dig2"); scene.playSound("battle_anims/PRSFX- Dig2");
scene.time.delayedCall(SOUND_EFFECT_WAIT_TIME, () => { scene.time.delayedCall(SOUND_EFFECT_WAIT_TIME, () => {
scene.playSound("battle_anims/PRSFX- Dig2"); scene.playSound("battle_anims/PRSFX- Dig2");

View File

@ -799,8 +799,8 @@ export const pokemonFormChanges: PokemonFormChanges = {
[Species.ZYGARDE]: [ [Species.ZYGARDE]: [
new SpeciesFormChange(Species.ZYGARDE, "50-pc", "complete", new SpeciesFormChangeManualTrigger(), true), new SpeciesFormChange(Species.ZYGARDE, "50-pc", "complete", new SpeciesFormChangeManualTrigger(), true),
new SpeciesFormChange(Species.ZYGARDE, "complete", "50-pc", new SpeciesFormChangeManualTrigger(), true), new SpeciesFormChange(Species.ZYGARDE, "complete", "50-pc", new SpeciesFormChangeManualTrigger(), true),
new SpeciesFormChange(Species.ZYGARDE, "10-pc", "complete", new SpeciesFormChangeManualTrigger(), true), new SpeciesFormChange(Species.ZYGARDE, "10-pc", "10-complete", new SpeciesFormChangeManualTrigger(), true),
new SpeciesFormChange(Species.ZYGARDE, "complete", "10-pc", new SpeciesFormChangeManualTrigger(), true) new SpeciesFormChange(Species.ZYGARDE, "10-complete", "10-pc", new SpeciesFormChangeManualTrigger(), true)
], ],
[Species.DIANCIE]: [ [Species.DIANCIE]: [
new SpeciesFormChange(Species.DIANCIE, "", SpeciesFormKey.MEGA, new SpeciesFormChangeItemTrigger(FormChangeItem.DIANCITE)) new SpeciesFormChange(Species.DIANCIE, "", SpeciesFormKey.MEGA, new SpeciesFormChangeItemTrigger(FormChangeItem.DIANCITE))

View File

@ -425,6 +425,7 @@ export abstract class PokemonSpeciesForm {
case "hero": case "hero":
case "roaming": case "roaming":
case "complete": case "complete":
case "10-complete":
case "10": case "10":
case "10-pc": case "10-pc":
case "super": case "super":
@ -2135,7 +2136,8 @@ export function initSpecies() {
new PokemonForm("10% Forme", "10", Type.DRAGON, Type.GROUND, 1.2, 33.5, Abilities.AURA_BREAK, Abilities.NONE, Abilities.NONE, 486, 54, 100, 71, 61, 85, 115, 3, 0, 300, false, null, true), new PokemonForm("10% Forme", "10", Type.DRAGON, Type.GROUND, 1.2, 33.5, Abilities.AURA_BREAK, Abilities.NONE, Abilities.NONE, 486, 54, 100, 71, 61, 85, 115, 3, 0, 300, false, null, true),
new PokemonForm("50% Forme Power Construct", "50-pc", Type.DRAGON, Type.GROUND, 5, 305, Abilities.POWER_CONSTRUCT, Abilities.NONE, Abilities.NONE, 600, 108, 100, 121, 81, 95, 95, 3, 0, 300, false, "", true), new PokemonForm("50% Forme Power Construct", "50-pc", Type.DRAGON, Type.GROUND, 5, 305, Abilities.POWER_CONSTRUCT, Abilities.NONE, Abilities.NONE, 600, 108, 100, 121, 81, 95, 95, 3, 0, 300, false, "", true),
new PokemonForm("10% Forme Power Construct", "10-pc", Type.DRAGON, Type.GROUND, 1.2, 33.5, Abilities.POWER_CONSTRUCT, Abilities.NONE, Abilities.NONE, 486, 54, 100, 71, 61, 85, 115, 3, 0, 300, false, "10", true), new PokemonForm("10% Forme Power Construct", "10-pc", Type.DRAGON, Type.GROUND, 1.2, 33.5, Abilities.POWER_CONSTRUCT, Abilities.NONE, Abilities.NONE, 486, 54, 100, 71, 61, 85, 115, 3, 0, 300, false, "10", true),
new PokemonForm("Complete Forme", "complete", Type.DRAGON, Type.GROUND, 4.5, 610, Abilities.POWER_CONSTRUCT, Abilities.NONE, Abilities.NONE, 708, 216, 100, 121, 91, 95, 85, 3, 0, 300), new PokemonForm("Complete Forme (50% PC)", "complete", Type.DRAGON, Type.GROUND, 4.5, 610, Abilities.POWER_CONSTRUCT, Abilities.NONE, Abilities.NONE, 708, 216, 100, 121, 91, 95, 85, 3, 0, 300),
new PokemonForm("Complete Forme (10% PC)", "10-complete", Type.DRAGON, Type.GROUND, 4.5, 610, Abilities.POWER_CONSTRUCT, Abilities.NONE, Abilities.NONE, 708, 216, 100, 121, 91, 95, 85, 3, 0, 300, false, "complete"),
), ),
new PokemonSpecies(Species.DIANCIE, 6, false, false, true, "Jewel Pokémon", Type.ROCK, Type.FAIRY, 0.7, 8.8, Abilities.CLEAR_BODY, Abilities.NONE, Abilities.NONE, 600, 50, 100, 150, 100, 150, 50, 3, 50, 300, GrowthRate.SLOW, null, false, true, new PokemonSpecies(Species.DIANCIE, 6, false, false, true, "Jewel Pokémon", Type.ROCK, Type.FAIRY, 0.7, 8.8, Abilities.CLEAR_BODY, Abilities.NONE, Abilities.NONE, 600, 50, 100, 150, 100, 150, 50, 3, 50, 300, GrowthRate.SLOW, null, false, true,
new PokemonForm("Normal", "", Type.ROCK, Type.FAIRY, 0.7, 8.8, Abilities.CLEAR_BODY, Abilities.NONE, Abilities.NONE, 600, 50, 100, 150, 100, 150, 50, 3, 50, 300, false, null, true), new PokemonForm("Normal", "", Type.ROCK, Type.FAIRY, 0.7, 8.8, Abilities.CLEAR_BODY, Abilities.NONE, Abilities.NONE, 600, 50, 100, 150, 100, 150, 50, 3, 50, 300, false, null, true),

View File

@ -574,13 +574,13 @@ export class TrainerConfig {
case "magma": { case "magma": {
return { return {
[TrainerPoolTier.COMMON]: [ Species.GROWLITHE, Species.SLUGMA, Species.SOLROCK, Species.HIPPOPOTAS, Species.BALTOY, Species.ROLYCOLY, Species.GLIGAR, Species.TORKOAL, Species.HOUNDOUR, Species.MAGBY ], [TrainerPoolTier.COMMON]: [ Species.GROWLITHE, Species.SLUGMA, Species.SOLROCK, Species.HIPPOPOTAS, Species.BALTOY, Species.ROLYCOLY, Species.GLIGAR, Species.TORKOAL, Species.HOUNDOUR, Species.MAGBY ],
[TrainerPoolTier.UNCOMMON]: [ Species.TRAPINCH, Species.SILICOBRA, Species.RHYHORN, Species.ANORITH, Species.LILEEP, Species.HISUI_GROWLITHE, Species.TURTONATOR, Species.ARON, Species.BARBOACH ], [TrainerPoolTier.UNCOMMON]: [ Species.TRAPINCH, Species.SILICOBRA, Species.RHYHORN, Species.ANORITH, Species.LILEEP, Species.HISUI_GROWLITHE, Species.TURTONATOR, Species.ARON, Species.TOEDSCOOL ],
[TrainerPoolTier.RARE]: [ Species.CAPSAKID, Species.CHARCADET ] [TrainerPoolTier.RARE]: [ Species.CAPSAKID, Species.CHARCADET ]
}; };
} }
case "aqua": { case "aqua": {
return { return {
[TrainerPoolTier.COMMON]: [ Species.CORPHISH, Species.SPHEAL, Species.CLAMPERL, Species.CHINCHOU, Species.WOOPER, Species.WINGULL, Species.TENTACOOL, Species.AZURILL, Species.LOTAD, Species.WAILMER, Species.REMORAID ], [TrainerPoolTier.COMMON]: [ Species.CORPHISH, Species.SPHEAL, Species.CLAMPERL, Species.CHINCHOU, Species.WOOPER, Species.WINGULL, Species.TENTACOOL, Species.AZURILL, Species.LOTAD, Species.WAILMER, Species.REMORAID, Species.BARBOACH ],
[TrainerPoolTier.UNCOMMON]: [ Species.MANTYKE, Species.HISUI_QWILFISH, Species.ARROKUDA, Species.DHELMISE, Species.CLOBBOPUS, Species.FEEBAS, Species.PALDEA_WOOPER, Species.HORSEA, Species.SKRELP ], [TrainerPoolTier.UNCOMMON]: [ Species.MANTYKE, Species.HISUI_QWILFISH, Species.ARROKUDA, Species.DHELMISE, Species.CLOBBOPUS, Species.FEEBAS, Species.PALDEA_WOOPER, Species.HORSEA, Species.SKRELP ],
[TrainerPoolTier.RARE]: [ Species.DONDOZO, Species.BASCULEGION ] [TrainerPoolTier.RARE]: [ Species.DONDOZO, Species.BASCULEGION ]
}; };
@ -601,9 +601,9 @@ export class TrainerConfig {
} }
case "flare": { case "flare": {
return { return {
[TrainerPoolTier.COMMON]: [ Species.FLETCHLING, Species.LITLEO, Species.INKAY, Species.HELIOPTILE, Species.ELECTRIKE, Species.SKORUPI, Species.PURRLOIN, Species.CLAWITZER, Species.PANCHAM, Species.ESPURR, Species.BUNNELBY ], [TrainerPoolTier.COMMON]: [ Species.FLETCHLING, Species.LITLEO, Species.INKAY, Species.FOONGUS, Species.HELIOPTILE, Species.ELECTRIKE, Species.SKORUPI, Species.PURRLOIN, Species.CLAWITZER, Species.PANCHAM, Species.ESPURR, Species.BUNNELBY ],
[TrainerPoolTier.UNCOMMON]: [ Species.LITWICK, Species.SNEASEL, Species.PUMPKABOO, Species.PHANTUMP, Species.HONEDGE, Species.BINACLE, Species.HOUNDOUR, Species.SKRELP, Species.SLIGGOO ], [TrainerPoolTier.UNCOMMON]: [ Species.LITWICK, Species.SNEASEL, Species.PUMPKABOO, Species.PHANTUMP, Species.HONEDGE, Species.BINACLE, Species.HOUNDOUR, Species.SKRELP, Species.SLIGGOO ],
[TrainerPoolTier.RARE]: [ Species.NOIVERN, Species.HISUI_AVALUGG, Species.HISUI_SLIGGOO ] [TrainerPoolTier.RARE]: [ Species.NOIBAT, Species.HISUI_AVALUGG, Species.HISUI_SLIGGOO ]
}; };
} }
case "aether": { case "aether": {
@ -1504,7 +1504,7 @@ export const trainerConfigs: TrainerConfigs = {
.setSpeciesPools({ .setSpeciesPools({
[TrainerPoolTier.COMMON]: [ Species.SLUGMA, Species.POOCHYENA, Species.NUMEL, Species.ZIGZAGOON, Species.DIGLETT, Species.MAGBY, Species.TORKOAL, Species.GROWLITHE, Species.BALTOY ], [TrainerPoolTier.COMMON]: [ Species.SLUGMA, Species.POOCHYENA, Species.NUMEL, Species.ZIGZAGOON, Species.DIGLETT, Species.MAGBY, Species.TORKOAL, Species.GROWLITHE, Species.BALTOY ],
[TrainerPoolTier.UNCOMMON]: [ Species.SOLROCK, Species.HIPPOPOTAS, Species.SANDACONDA, Species.PHANPY, Species.ROLYCOLY, Species.GLIGAR, Species.RHYHORN, Species.HEATMOR ], [TrainerPoolTier.UNCOMMON]: [ Species.SOLROCK, Species.HIPPOPOTAS, Species.SANDACONDA, Species.PHANPY, Species.ROLYCOLY, Species.GLIGAR, Species.RHYHORN, Species.HEATMOR ],
[TrainerPoolTier.RARE]: [ Species.TRAPINCH, Species.LILEEP, Species.ANORITH, Species.HISUI_GROWLITHE, Species.TURTONATOR, Species.ARON ], [TrainerPoolTier.RARE]: [ Species.TRAPINCH, Species.LILEEP, Species.ANORITH, Species.HISUI_GROWLITHE, Species.TURTONATOR, Species.ARON, Species.TOEDSCOOL ],
[TrainerPoolTier.SUPER_RARE]: [ Species.CAPSAKID, Species.CHARCADET ] [TrainerPoolTier.SUPER_RARE]: [ Species.CAPSAKID, Species.CHARCADET ]
}), }),
[TrainerType.TABITHA]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("magma_admin", "magma", [ Species.CAMERUPT ]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_aqua_magma_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)), [TrainerType.TABITHA]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("magma_admin", "magma", [ Species.CAMERUPT ]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_aqua_magma_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)),
@ -1540,9 +1540,9 @@ export const trainerConfigs: TrainerConfigs = {
[TrainerType.FLARE_GRUNT]: new TrainerConfig(++t).setHasGenders("Flare Grunt Female").setHasDouble("Flare Grunts").setMoneyMultiplier(1.0).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_flare_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)) [TrainerType.FLARE_GRUNT]: new TrainerConfig(++t).setHasGenders("Flare Grunt Female").setHasDouble("Flare Grunts").setMoneyMultiplier(1.0).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_flare_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene))
.setSpeciesPools({ .setSpeciesPools({
[TrainerPoolTier.COMMON]: [ Species.FLETCHLING, Species.LITLEO, Species.PONYTA, Species.INKAY, Species.HOUNDOUR, Species.SKORUPI, Species.SCRAFTY, Species.CROAGUNK, Species.SCATTERBUG, Species.ESPURR ], [TrainerPoolTier.COMMON]: [ Species.FLETCHLING, Species.LITLEO, Species.PONYTA, Species.INKAY, Species.HOUNDOUR, Species.SKORUPI, Species.SCRAFTY, Species.CROAGUNK, Species.SCATTERBUG, Species.ESPURR ],
[TrainerPoolTier.UNCOMMON]: [ Species.HELIOPTILE, Species.ELECTRIKE, Species.SKRELP, Species.PANCHAM, Species.PURRLOIN, Species.POOCHYENA, Species.BINACLE, Species.CLAUNCHER, Species.PUMPKABOO, Species.PHANTUMP ], [TrainerPoolTier.UNCOMMON]: [ Species.HELIOPTILE, Species.ELECTRIKE, Species.SKRELP, Species.PANCHAM, Species.PURRLOIN, Species.POOCHYENA, Species.BINACLE, Species.CLAUNCHER, Species.PUMPKABOO, Species.PHANTUMP, Species.FOONGUS ],
[TrainerPoolTier.RARE]: [ Species.LITWICK, Species.SNEASEL, Species.PAWNIARD, Species.SLIGGOO ], [TrainerPoolTier.RARE]: [ Species.LITWICK, Species.SNEASEL, Species.PAWNIARD, Species.SLIGGOO ],
[TrainerPoolTier.SUPER_RARE]: [ Species.NOIVERN, Species.HISUI_SLIGGOO, Species.HISUI_AVALUGG ] [TrainerPoolTier.SUPER_RARE]: [ Species.NOIBAT, Species.HISUI_SLIGGOO, Species.HISUI_AVALUGG ]
}), }),
[TrainerType.BRYONY]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("flare_admin_female", "flare", [ Species.LIEPARD ]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_flare_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)), [TrainerType.BRYONY]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("flare_admin_female", "flare", [ Species.LIEPARD ]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_flare_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)),
[TrainerType.XEROSIC]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("flare_admin", "flare", [ Species.MALAMAR ]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_flare_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)), [TrainerType.XEROSIC]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("flare_admin", "flare", [ Species.MALAMAR ]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_flare_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)),
@ -1893,7 +1893,10 @@ export const trainerConfigs: TrainerConfigs = {
}), }),
[TrainerType.ROCKET_BOSS_GIOVANNI_1]: new TrainerConfig(t = TrainerType.ROCKET_BOSS_GIOVANNI_1).setName("Giovanni").initForEvilTeamLeader("Rocket Boss", []).setMixedBattleBgm("battle_rocket_boss").setVictoryBgm("victory_team_plasma") [TrainerType.ROCKET_BOSS_GIOVANNI_1]: new TrainerConfig(t = TrainerType.ROCKET_BOSS_GIOVANNI_1).setName("Giovanni").initForEvilTeamLeader("Rocket Boss", []).setMixedBattleBgm("battle_rocket_boss").setVictoryBgm("victory_team_plasma")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.PERSIAN, Species.ALOLA_PERSIAN ])) .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.PERSIAN, Species.ALOLA_PERSIAN ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.gender = Gender.MALE;
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.DUGTRIO, Species.ALOLA_DUGTRIO ])) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.DUGTRIO, Species.ALOLA_DUGTRIO ]))
.setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.HONCHKROW ])) .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.HONCHKROW ]))
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.NIDOKING, Species.NIDOQUEEN ])) .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.NIDOKING, Species.NIDOQUEEN ]))
@ -1945,6 +1948,7 @@ export const trainerConfigs: TrainerConfigs = {
p.pokeball = PokeballType.ULTRA_BALL; p.pokeball = PokeballType.ULTRA_BALL;
p.formIndex = 1; // Mega Camerupt p.formIndex = 1; // Mega Camerupt
p.generateName(); p.generateName();
p.gender = Gender.MALE;
})), })),
[TrainerType.MAXIE_2]: new TrainerConfig(++t).setName("Maxie").initForEvilTeamLeader("Magma Boss", [], true).setMixedBattleBgm("battle_aqua_magma_boss").setVictoryBgm("victory_team_plasma") [TrainerType.MAXIE_2]: new TrainerConfig(++t).setName("Maxie").initForEvilTeamLeader("Magma Boss", [], true).setMixedBattleBgm("battle_aqua_magma_boss").setVictoryBgm("victory_team_plasma")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.SOLROCK, Species.TYPHLOSION ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.SOLROCK, Species.TYPHLOSION ], TrainerSlot.TRAINER, true, p => {
@ -1967,6 +1971,7 @@ export const trainerConfigs: TrainerConfigs = {
p.pokeball = PokeballType.ULTRA_BALL; p.pokeball = PokeballType.ULTRA_BALL;
p.formIndex = 1; // Mega Camerupt p.formIndex = 1; // Mega Camerupt
p.generateName(); p.generateName();
p.gender = Gender.MALE;
})) }))
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.GROUDON ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.GROUDON ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2); p.setBoss(true, 2);
@ -1985,6 +1990,7 @@ export const trainerConfigs: TrainerConfigs = {
p.pokeball = PokeballType.ULTRA_BALL; p.pokeball = PokeballType.ULTRA_BALL;
p.formIndex = 1; // Mega Sharpedo p.formIndex = 1; // Mega Sharpedo
p.generateName(); p.generateName();
p.gender = Gender.MALE;
})), })),
[TrainerType.ARCHIE_2]: new TrainerConfig(++t).setName("Archie").initForEvilTeamLeader("Aqua Boss", [], true).setMixedBattleBgm("battle_aqua_magma_boss").setVictoryBgm("victory_team_plasma") [TrainerType.ARCHIE_2]: new TrainerConfig(++t).setName("Archie").initForEvilTeamLeader("Aqua Boss", [], true).setMixedBattleBgm("battle_aqua_magma_boss").setVictoryBgm("victory_team_plasma")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.EMPOLEON, Species.LUDICOLO ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.EMPOLEON, Species.LUDICOLO ], TrainerSlot.TRAINER, true, p => {
@ -2010,6 +2016,7 @@ export const trainerConfigs: TrainerConfigs = {
p.pokeball = PokeballType.ULTRA_BALL; p.pokeball = PokeballType.ULTRA_BALL;
p.formIndex = 1; // Mega Sharpedo p.formIndex = 1; // Mega Sharpedo
p.generateName(); p.generateName();
p.gender = Gender.MALE;
})) }))
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.KYOGRE ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.KYOGRE ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2); p.setBoss(true, 2);
@ -2031,6 +2038,7 @@ export const trainerConfigs: TrainerConfigs = {
p.setBoss(true, 2); p.setBoss(true, 2);
p.generateAndPopulateMoveset(); p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL; p.pokeball = PokeballType.ULTRA_BALL;
p.gender = Gender.MALE;
})), })),
[TrainerType.CYRUS_2]: new TrainerConfig(++t).setName("Cyrus").initForEvilTeamLeader("Galactic Boss", [], true).setMixedBattleBgm("battle_galactic_boss").setVictoryBgm("victory_team_plasma") [TrainerType.CYRUS_2]: new TrainerConfig(++t).setName("Cyrus").initForEvilTeamLeader("Galactic Boss", [], true).setMixedBattleBgm("battle_galactic_boss").setVictoryBgm("victory_team_plasma")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.AZELF, Species.UXIE, Species.MESPRIT ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.AZELF, Species.UXIE, Species.MESPRIT ], TrainerSlot.TRAINER, true, p => {
@ -2049,6 +2057,7 @@ export const trainerConfigs: TrainerConfigs = {
p.setBoss(true, 2); p.setBoss(true, 2);
p.generateAndPopulateMoveset(); p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL; p.pokeball = PokeballType.ULTRA_BALL;
p.gender = Gender.MALE;
})) }))
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.DARKRAI ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.DARKRAI ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2); p.setBoss(true, 2);
@ -2065,6 +2074,7 @@ export const trainerConfigs: TrainerConfigs = {
p.setBoss(true, 2); p.setBoss(true, 2);
p.generateAndPopulateMoveset(); p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL; p.pokeball = PokeballType.ULTRA_BALL;
p.gender = Gender.MALE;
})), })),
[TrainerType.GHETSIS_2]: new TrainerConfig(++t).setName("Ghetsis").initForEvilTeamLeader("Plasma Boss", [], true).setMixedBattleBgm("battle_plasma_boss").setVictoryBgm("victory_team_plasma") [TrainerType.GHETSIS_2]: new TrainerConfig(++t).setName("Ghetsis").initForEvilTeamLeader("Plasma Boss", [], true).setMixedBattleBgm("battle_plasma_boss").setVictoryBgm("victory_team_plasma")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.GENESECT ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.GENESECT ], TrainerSlot.TRAINER, true, p => {
@ -2084,6 +2094,11 @@ export const trainerConfigs: TrainerConfigs = {
p.setBoss(true, 2); p.setBoss(true, 2);
p.generateAndPopulateMoveset(); p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL; p.pokeball = PokeballType.ULTRA_BALL;
if (p.species.speciesId === Species.HYDREIGON) {
p.gender = Gender.MALE;
} else if (p.species.speciesId === Species.IRON_JUGULIS) {
p.gender = Gender.GENDERLESS;
}
})) }))
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.KYUREM ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.KYUREM ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2); p.setBoss(true, 2);
@ -2105,6 +2120,7 @@ export const trainerConfigs: TrainerConfigs = {
p.pokeball = PokeballType.ULTRA_BALL; p.pokeball = PokeballType.ULTRA_BALL;
p.formIndex = 1; // Mega Gyarados p.formIndex = 1; // Mega Gyarados
p.generateName(); p.generateName();
p.gender = Gender.MALE;
})), })),
[TrainerType.LYSANDRE_2]: new TrainerConfig(++t).setName("Lysandre").initForEvilTeamLeader("Flare Boss", [], true).setMixedBattleBgm("battle_flare_boss").setVictoryBgm("victory_team_plasma") [TrainerType.LYSANDRE_2]: new TrainerConfig(++t).setName("Lysandre").initForEvilTeamLeader("Flare Boss", [], true).setMixedBattleBgm("battle_flare_boss").setVictoryBgm("victory_team_plasma")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.SCREAM_TAIL, Species.FLUTTER_MANE ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.SCREAM_TAIL, Species.FLUTTER_MANE ], TrainerSlot.TRAINER, true, p => {
@ -2124,6 +2140,7 @@ export const trainerConfigs: TrainerConfigs = {
p.pokeball = PokeballType.ULTRA_BALL; p.pokeball = PokeballType.ULTRA_BALL;
p.formIndex = 1; // Mega Gyardos p.formIndex = 1; // Mega Gyardos
p.generateName(); p.generateName();
p.gender = Gender.MALE;
})) }))
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.YVELTAL ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.YVELTAL ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2); p.setBoss(true, 2);
@ -2131,7 +2148,10 @@ export const trainerConfigs: TrainerConfigs = {
p.pokeball = PokeballType.MASTER_BALL; p.pokeball = PokeballType.MASTER_BALL;
})), })),
[TrainerType.LUSAMINE]: new TrainerConfig(++t).setName("Lusamine").initForEvilTeamLeader("Aether Boss", []).setMixedBattleBgm("battle_aether_boss").setVictoryBgm("victory_team_plasma") [TrainerType.LUSAMINE]: new TrainerConfig(++t).setName("Lusamine").initForEvilTeamLeader("Aether Boss", []).setMixedBattleBgm("battle_aether_boss").setVictoryBgm("victory_team_plasma")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.CLEFABLE ])) .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.CLEFABLE ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.gender = Gender.FEMALE;
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.LILLIGANT, Species.HISUI_LILLIGANT ])) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.LILLIGANT, Species.HISUI_LILLIGANT ]))
.setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.MILOTIC, Species.PRIMARINA ])) .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.MILOTIC, Species.PRIMARINA ]))
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.GALAR_SLOWBRO, Species.GALAR_SLOWKING ])) .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.GALAR_SLOWBRO, Species.GALAR_SLOWKING ]))
@ -2148,7 +2168,10 @@ export const trainerConfigs: TrainerConfigs = {
p.pokeball = PokeballType.ROGUE_BALL; p.pokeball = PokeballType.ROGUE_BALL;
})) }))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.MILOTIC, Species.PRIMARINA ])) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.MILOTIC, Species.PRIMARINA ]))
.setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.CLEFABLE ])) .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.CLEFABLE ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.gender = Gender.FEMALE;
}))
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.STAKATAKA, Species.CELESTEELA, Species.GUZZLORD ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.STAKATAKA, Species.CELESTEELA, Species.GUZZLORD ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset(); p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ROGUE_BALL; p.pokeball = PokeballType.ROGUE_BALL;
@ -2191,6 +2214,7 @@ export const trainerConfigs: TrainerConfigs = {
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.GOLISOPOD ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.GOLISOPOD ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2); p.setBoss(true, 2);
p.generateAndPopulateMoveset(); p.generateAndPopulateMoveset();
p.gender = Gender.MALE;
p.pokeball = PokeballType.ULTRA_BALL; p.pokeball = PokeballType.ULTRA_BALL;
})), })),
[TrainerType.GUZMA_2]: new TrainerConfig(++t).setName("Guzma").initForEvilTeamLeader("Skull Boss", [], true).setMixedBattleBgm("battle_skull_boss").setVictoryBgm("victory_team_plasma") [TrainerType.GUZMA_2]: new TrainerConfig(++t).setName("Guzma").initForEvilTeamLeader("Skull Boss", [], true).setMixedBattleBgm("battle_skull_boss").setVictoryBgm("victory_team_plasma")
@ -2198,6 +2222,7 @@ export const trainerConfigs: TrainerConfigs = {
p.setBoss(true, 2); p.setBoss(true, 2);
p.generateAndPopulateMoveset(); p.generateAndPopulateMoveset();
p.abilityIndex = 2; //Anticipation p.abilityIndex = 2; //Anticipation
p.gender = Gender.MALE;
p.pokeball = PokeballType.ULTRA_BALL; p.pokeball = PokeballType.ULTRA_BALL;
})) }))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.SCIZOR, Species.KLEAVOR ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.SCIZOR, Species.KLEAVOR ], TrainerSlot.TRAINER, true, p => {
@ -2239,6 +2264,7 @@ export const trainerConfigs: TrainerConfigs = {
p.formIndex = 1; // G-Max Copperajah p.formIndex = 1; // G-Max Copperajah
p.generateName(); p.generateName();
p.pokeball = PokeballType.ULTRA_BALL; p.pokeball = PokeballType.ULTRA_BALL;
p.gender = Gender.FEMALE;
})), })),
[TrainerType.ROSE_2]: new TrainerConfig(++t).setName("Rose").initForEvilTeamLeader("Macro Boss", [], true).setMixedBattleBgm("battle_macro_boss").setVictoryBgm("victory_team_plasma") [TrainerType.ROSE_2]: new TrainerConfig(++t).setName("Rose").initForEvilTeamLeader("Macro Boss", [], true).setMixedBattleBgm("battle_macro_boss").setVictoryBgm("victory_team_plasma")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.ARCHALUDON ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.ARCHALUDON ], TrainerSlot.TRAINER, true, p => {
@ -2262,6 +2288,7 @@ export const trainerConfigs: TrainerConfigs = {
p.formIndex = 1; // G-Max Copperajah p.formIndex = 1; // G-Max Copperajah
p.generateName(); p.generateName();
p.pokeball = PokeballType.ULTRA_BALL; p.pokeball = PokeballType.ULTRA_BALL;
p.gender = Gender.FEMALE;
})), })),
[TrainerType.PENNY]: new TrainerConfig(++t).setName("Cassiopeia").initForEvilTeamLeader("Star Boss", []).setMixedBattleBgm("battle_star_boss").setVictoryBgm("victory_team_plasma") [TrainerType.PENNY]: new TrainerConfig(++t).setName("Cassiopeia").initForEvilTeamLeader("Star Boss", []).setMixedBattleBgm("battle_star_boss").setVictoryBgm("victory_team_plasma")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VAPOREON, Species.JOLTEON, Species.FLAREON ])) .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VAPOREON, Species.JOLTEON, Species.FLAREON ]))
@ -2275,8 +2302,9 @@ export const trainerConfigs: TrainerConfigs = {
p.formIndex = Utils.randSeedInt(5, 1); // Heat, Wash, Frost, Fan, or Mow p.formIndex = Utils.randSeedInt(5, 1); // Heat, Wash, Frost, Fan, or Mow
})) }))
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.SYLVEON ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.SYLVEON ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.abilityIndex = 2; // Pixilate p.abilityIndex = 2; // Pixilate
p.generateAndPopulateMoveset();
p.gender = Gender.FEMALE;
})) }))
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.EEVEE ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.EEVEE ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2); p.setBoss(true, 2);
@ -2290,20 +2318,21 @@ export const trainerConfigs: TrainerConfigs = {
return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ]; //TODO: is the bang correct? return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ]; //TODO: is the bang correct?
}), }),
[TrainerType.PENNY_2]: new TrainerConfig(++t).setName("Cassiopeia").initForEvilTeamLeader("Star Boss", [], true).setMixedBattleBgm("battle_star_boss").setVictoryBgm("victory_team_plasma") [TrainerType.PENNY_2]: new TrainerConfig(++t).setName("Cassiopeia").initForEvilTeamLeader("Star Boss", [], true).setMixedBattleBgm("battle_star_boss").setVictoryBgm("victory_team_plasma")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.REVAVROOM ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.SYLVEON ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2); p.setBoss(true, 2);
p.formIndex = Utils.randSeedInt(5, 1); //Random Starmobile form p.abilityIndex = 2; // Pixilate
p.generateAndPopulateMoveset(); p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL; p.gender = Gender.FEMALE;
})) }))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.ENTEI, Species.RAIKOU, Species.SUICUNE ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.ENTEI, Species.RAIKOU, Species.SUICUNE ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset(); p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL; p.pokeball = PokeballType.ULTRA_BALL;
})) }))
.setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.WALKING_WAKE, Species.GOUGING_FIRE, Species.RAGING_BOLT ])) .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.WALKING_WAKE, Species.GOUGING_FIRE, Species.RAGING_BOLT ]))
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.SYLVEON ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.REVAVROOM ], TrainerSlot.TRAINER, true, p => {
p.formIndex = Utils.randSeedInt(5, 1); //Random Starmobile form
p.generateAndPopulateMoveset(); p.generateAndPopulateMoveset();
p.abilityIndex = 2; // Pixilate p.pokeball = PokeballType.ROGUE_BALL;
})) }))
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.EEVEE ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.EEVEE ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2); p.setBoss(true, 2);
@ -2318,7 +2347,7 @@ export const trainerConfigs: TrainerConfigs = {
p.pokeball = PokeballType.MASTER_BALL; p.pokeball = PokeballType.MASTER_BALL;
})) }))
.setGenModifiersFunc(party => { .setGenModifiersFunc(party => {
const teraPokemon = party[3]; const teraPokemon = party[0];
return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ]; //TODO: is the bang correct? return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ]; //TODO: is the bang correct?
}), }),
[TrainerType.BUCK]: new TrainerConfig(++t).setName("Buck").initForStatTrainer([], true) [TrainerType.BUCK]: new TrainerConfig(++t).setName("Buck").initForStatTrainer([], true)

View File

@ -80,6 +80,7 @@ export enum BattlerTagType {
DOUBLE_SHOCKED = "DOUBLE_SHOCKED", DOUBLE_SHOCKED = "DOUBLE_SHOCKED",
AUTOTOMIZED = "AUTOTOMIZED", AUTOTOMIZED = "AUTOTOMIZED",
MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON", MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON",
POWER_TRICK = "POWER_TRICK",
HEAL_BLOCK = "HEAL_BLOCK", HEAL_BLOCK = "HEAL_BLOCK",
TORMENT = "TORMENT", TORMENT = "TORMENT",
TAUNT = "TAUNT", TAUNT = "TAUNT",

View File

@ -579,26 +579,28 @@ export class Arena {
* Applies each `ArenaTag` in this Arena, based on which side (self, enemy, or both) is passed in as a parameter * Applies each `ArenaTag` in this Arena, based on which side (self, enemy, or both) is passed in as a parameter
* @param tagType Either an {@linkcode ArenaTagType} string, or an actual {@linkcode ArenaTag} class to filter which ones to apply * @param tagType Either an {@linkcode ArenaTagType} string, or an actual {@linkcode ArenaTag} class to filter which ones to apply
* @param side {@linkcode ArenaTagSide} which side's arena tags to apply * @param side {@linkcode ArenaTagSide} which side's arena tags to apply
* @param simulated if `true`, this applies arena tags without changing game state
* @param args array of parameters that the called upon tags may need * @param args array of parameters that the called upon tags may need
*/ */
applyTagsForSide(tagType: ArenaTagType | Constructor<ArenaTag>, side: ArenaTagSide, ...args: unknown[]): void { applyTagsForSide(tagType: ArenaTagType | Constructor<ArenaTag>, side: ArenaTagSide, simulated: boolean, ...args: unknown[]): void {
let tags = typeof tagType === "string" let tags = typeof tagType === "string"
? this.tags.filter(t => t.tagType === tagType) ? this.tags.filter(t => t.tagType === tagType)
: this.tags.filter(t => t instanceof tagType); : this.tags.filter(t => t instanceof tagType);
if (side !== ArenaTagSide.BOTH) { if (side !== ArenaTagSide.BOTH) {
tags = tags.filter(t => t.side === side); tags = tags.filter(t => t.side === side);
} }
tags.forEach(t => t.apply(this, args)); tags.forEach(t => t.apply(this, simulated, ...args));
} }
/** /**
* Applies the specified tag to both sides (ie: both user and trainer's tag that match the Tag specified) * Applies the specified tag to both sides (ie: both user and trainer's tag that match the Tag specified)
* by calling {@linkcode applyTagsForSide()} * by calling {@linkcode applyTagsForSide()}
* @param tagType Either an {@linkcode ArenaTagType} string, or an actual {@linkcode ArenaTag} class to filter which ones to apply * @param tagType Either an {@linkcode ArenaTagType} string, or an actual {@linkcode ArenaTag} class to filter which ones to apply
* @param simulated if `true`, this applies arena tags without changing game state
* @param args array of parameters that the called upon tags may need * @param args array of parameters that the called upon tags may need
*/ */
applyTags(tagType: ArenaTagType | Constructor<ArenaTag>, ...args: unknown[]): void { applyTags(tagType: ArenaTagType | Constructor<ArenaTag>, simulated: boolean, ...args: unknown[]): void {
this.applyTagsForSide(tagType, ArenaTagSide.BOTH, ...args); this.applyTagsForSide(tagType, ArenaTagSide.BOTH, simulated, ...args);
} }
/** /**

View File

@ -5,7 +5,7 @@ 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, ChargeAttr, 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 { 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";
import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils"; import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
@ -19,7 +19,7 @@ import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims";
import { Status, StatusEffect, getRandomStatus } from "#app/data/status-effect"; import { Status, StatusEffect, getRandomStatus } from "#app/data/status-effect";
import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions"; import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions";
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/tms"; import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/tms";
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag } 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, 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";
@ -1538,7 +1538,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
applyMoveAttrs(VariableMoveTypeAttr, this, null, move, moveTypeHolder); applyMoveAttrs(VariableMoveTypeAttr, this, null, move, moveTypeHolder);
applyPreAttackAbAttrs(MoveTypeChangeAbAttr, this, null, move, simulated, moveTypeHolder); applyPreAttackAbAttrs(MoveTypeChangeAbAttr, this, null, move, simulated, moveTypeHolder);
this.scene.arena.applyTags(ArenaTagType.ION_DELUGE, moveTypeHolder); this.scene.arena.applyTags(ArenaTagType.ION_DELUGE, simulated, moveTypeHolder);
if (this.getTag(BattlerTagType.ELECTRIFIED)) { if (this.getTag(BattlerTagType.ELECTRIFIED)) {
moveTypeHolder.value = Type.ELECTRIC; moveTypeHolder.value = Type.ELECTRIC;
} }
@ -2605,7 +2605,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, move.category, this.scene.currentBattle.double, screenMultiplier); this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, simulated, 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:
@ -2810,15 +2810,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (this.isFainted()) { if (this.isFainted()) {
// set splice index here, so future scene queues happen before FaintedPhase // set splice index here, so future scene queues happen before FaintedPhase
this.scene.setPhaseQueueSplice(); this.scene.setPhaseQueueSplice();
if (!isNullOrUndefined(destinyTag) && dmg) {
// Destiny Bond will activate during FaintPhase
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo, destinyTag, source));
} else {
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo)); this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo));
}
this.destroySubstitute(); this.destroySubstitute();
this.resetSummonData(); this.resetSummonData();
} }
if (dmg) {
destinyTag?.lapse(source, BattlerTagLapseType.CUSTOM);
}
return result; return result;
} }
} }
@ -3048,6 +3049,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
continue; continue;
} }
if (tag instanceof PowerTrickTag) {
tag.swapStat(this);
}
this.summonData.tags.push(tag); this.summonData.tags.push(tag);
} }
@ -3062,8 +3067,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* *
* @see {@linkcode MoveRestrictionBattlerTag} * @see {@linkcode MoveRestrictionBattlerTag}
*/ */
isMoveRestricted(moveId: Moves): boolean { public isMoveRestricted(moveId: Moves, pokemon?: Pokemon): boolean {
return this.getRestrictingTag(moveId) !== null; return this.getRestrictingTag(moveId, pokemon) !== null;
} }
/** /**
@ -3096,7 +3101,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
*/ */
getRestrictingTag(moveId: Moves, user?: Pokemon, target?: Pokemon): MoveRestrictionBattlerTag | null { getRestrictingTag(moveId: Moves, user?: Pokemon, target?: Pokemon): MoveRestrictionBattlerTag | null {
for (const tag of this.findTags(t => t instanceof MoveRestrictionBattlerTag)) { for (const tag of this.findTags(t => t instanceof MoveRestrictionBattlerTag)) {
if ((tag as MoveRestrictionBattlerTag).isMoveRestricted(moveId)) { if ((tag as MoveRestrictionBattlerTag).isMoveRestricted(moveId, user)) {
return tag as MoveRestrictionBattlerTag; return tag as MoveRestrictionBattlerTag;
} else if (user && target && (tag as MoveRestrictionBattlerTag).isMoveTargetRestricted(moveId, user, target)) { } else if (user && target && (tag as MoveRestrictionBattlerTag).isMoveTargetRestricted(moveId, user, target)) {
return tag as MoveRestrictionBattlerTag; return tag as MoveRestrictionBattlerTag;
@ -4077,7 +4082,7 @@ export class PlayerPokemon extends Pokemon {
fusionStarterSpeciesId ? this.scene.gameData.starterData[fusionStarterSpeciesId] : null fusionStarterSpeciesId ? this.scene.gameData.starterData[fusionStarterSpeciesId] : null
].filter(d => !!d); ].filter(d => !!d);
const amount = new Utils.IntegerHolder(friendship); const amount = new Utils.IntegerHolder(friendship);
const starterAmount = new Utils.IntegerHolder(Math.floor(friendship * (this.scene.gameMode.isClassic && friendship > 0 ? 2 : 1) / (fusionStarterSpeciesId ? 2 : 1))); const starterAmount = new Utils.IntegerHolder(Math.floor(friendship * (this.scene.gameMode.isClassic && friendship > 0 ? CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER : 1) / (fusionStarterSpeciesId ? 2 : 1)));
if (amount.value > 0) { if (amount.value > 0) {
this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount);
this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, starterAmount); this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, starterAmount);
@ -5139,7 +5144,7 @@ export class PokemonMove {
* @returns `true` if the move can be selected and used by the Pokemon, otherwise `false`. * @returns `true` if the move can be selected and used by the Pokemon, otherwise `false`.
*/ */
isUsable(pokemon: Pokemon, ignorePp: boolean = false, ignoreRestrictionTags: boolean = false): boolean { isUsable(pokemon: Pokemon, ignorePp: boolean = false, ignoreRestrictionTags: boolean = false): boolean {
if (this.moveId && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId)) { if (this.moveId && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId, pokemon)) {
return false; return false;
} }

View File

@ -383,7 +383,7 @@ export default class Trainer extends Phaser.GameObjects.Container {
const battle = this.scene.currentBattle; const battle = this.scene.currentBattle;
const template = this.getPartyTemplate(); const template = this.getPartyTemplate();
let species: PokemonSpecies; let baseSpecies: PokemonSpecies;
if (this.config.speciesPools) { if (this.config.speciesPools) {
const tierValue = Utils.randSeedInt(512); const tierValue = Utils.randSeedInt(512);
let tier = tierValue >= 156 ? TrainerPoolTier.COMMON : tierValue >= 32 ? TrainerPoolTier.UNCOMMON : tierValue >= 6 ? TrainerPoolTier.RARE : tierValue >= 1 ? TrainerPoolTier.SUPER_RARE : TrainerPoolTier.ULTRA_RARE; let tier = tierValue >= 156 ? TrainerPoolTier.COMMON : tierValue >= 32 ? TrainerPoolTier.UNCOMMON : tierValue >= 6 ? TrainerPoolTier.RARE : tierValue >= 1 ? TrainerPoolTier.SUPER_RARE : TrainerPoolTier.ULTRA_RARE;
@ -393,17 +393,17 @@ export default class Trainer extends Phaser.GameObjects.Container {
tier--; tier--;
} }
const tierPool = this.config.speciesPools[tier]; const tierPool = this.config.speciesPools[tier];
species = getPokemonSpecies(Utils.randSeedItem(tierPool)); baseSpecies = getPokemonSpecies(Utils.randSeedItem(tierPool));
} else { } else {
species = this.scene.randomSpecies(battle.waveIndex, level, false, this.config.speciesFilter); baseSpecies = this.scene.randomSpecies(battle.waveIndex, level, false, this.config.speciesFilter);
} }
let ret = getPokemonSpecies(species.getTrainerSpeciesForLevel(level, true, strength, this.scene.currentBattle.waveIndex)); let ret = getPokemonSpecies(baseSpecies.getTrainerSpeciesForLevel(level, true, strength, this.scene.currentBattle.waveIndex));
let retry = false; let retry = false;
console.log(ret.getName()); console.log(ret.getName());
if (pokemonPrevolutions.hasOwnProperty(species.speciesId) && ret.speciesId !== species.speciesId) { if (pokemonPrevolutions.hasOwnProperty(baseSpecies.speciesId) && ret.speciesId !== baseSpecies.speciesId) {
retry = true; retry = true;
} else if (template.isBalanced(battle.enemyParty.length)) { } else if (template.isBalanced(battle.enemyParty.length)) {
const partyMemberTypes = battle.enemyParty.map(p => p.getTypes(true)).flat(); const partyMemberTypes = battle.enemyParty.map(p => p.getTypes(true)).flat();
@ -417,7 +417,7 @@ export default class Trainer extends Phaser.GameObjects.Container {
console.log("Attempting reroll of species evolution to fit specialty type..."); console.log("Attempting reroll of species evolution to fit specialty type...");
let evoAttempt = 0; let evoAttempt = 0;
while (retry && evoAttempt++ < 10) { while (retry && evoAttempt++ < 10) {
ret = getPokemonSpecies(species.getTrainerSpeciesForLevel(level, true, strength, this.scene.currentBattle.waveIndex)); ret = getPokemonSpecies(baseSpecies.getTrainerSpeciesForLevel(level, true, strength, this.scene.currentBattle.waveIndex));
console.log(ret.name); console.log(ret.name);
if (this.config.specialtyTypes.find(t => ret.isOfType(t))) { if (this.config.specialtyTypes.find(t => ret.isOfType(t))) {
retry = false; retry = false;
@ -426,7 +426,7 @@ export default class Trainer extends Phaser.GameObjects.Container {
} }
// Prompts reroll of party member species if species already present in the enemy party // Prompts reroll of party member species if species already present in the enemy party
if (this.checkDuplicateSpecies(ret)) { if (this.checkDuplicateSpecies(ret, baseSpecies)) {
console.log("Duplicate species detected, prompting reroll..."); console.log("Duplicate species detected, prompting reroll...");
retry = true; retry = true;
} }
@ -442,13 +442,16 @@ export default class Trainer extends Phaser.GameObjects.Container {
/** /**
* Checks if the enemy trainer already has the Pokemon species in their party * Checks if the enemy trainer already has the Pokemon species in their party
* @param {PokemonSpecies} species {@linkcode PokemonSpecies} * @param {PokemonSpecies} species {@linkcode PokemonSpecies}
* @param {PokemonSpecies} baseSpecies {@linkcode PokemonSpecies} - baseSpecies of the Pokemon if species is forced to evolve
* @returns `true` if the species is already present in the party * @returns `true` if the species is already present in the party
*/ */
checkDuplicateSpecies(species: PokemonSpecies): boolean { checkDuplicateSpecies(species: PokemonSpecies, baseSpecies: PokemonSpecies): boolean {
const staticPartyPokemon = (signatureSpecies[TrainerType[this.config.trainerType]] ?? []).flat(1);
const currentPartySpecies = this.scene.getEnemyParty().map(p => { const currentPartySpecies = this.scene.getEnemyParty().map(p => {
return p.species.speciesId; return p.species.speciesId;
}); });
return currentPartySpecies.includes(species.speciesId); return currentPartySpecies.includes(species.speciesId) || staticPartyPokemon.includes(baseSpecies.speciesId);
} }
getPartyMemberMatchupScores(trainerSlot: TrainerSlot = TrainerSlot.NONE, forSwitch: boolean = false): [integer, integer][] { getPartyMemberMatchupScores(trainerSlot: TrainerSlot = TrainerSlot.NONE, forSwitch: boolean = false): [integer, integer][] {

View File

@ -2227,7 +2227,8 @@ export function getPlayerShopModifierTypeOptionsForWave(waveIndex: integer, base
], ],
[ [
new ModifierTypeOption(modifierTypes.HYPER_POTION(), 0, baseCost * 0.8), new ModifierTypeOption(modifierTypes.HYPER_POTION(), 0, baseCost * 0.8),
new ModifierTypeOption(modifierTypes.MAX_REVIVE(), 0, baseCost * 2.75) new ModifierTypeOption(modifierTypes.MAX_REVIVE(), 0, baseCost * 2.75),
new ModifierTypeOption(modifierTypes.MEMORY_MUSHROOM(), 0, baseCost * 4)
], ],
[ [
new ModifierTypeOption(modifierTypes.MAX_POTION(), 0, baseCost * 1.5), new ModifierTypeOption(modifierTypes.MAX_POTION(), 0, baseCost * 1.5),

View File

@ -11,7 +11,7 @@ import Pokemon, { type PlayerPokemon } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { EvolutionPhase } from "#app/phases/evolution-phase"; import { EvolutionPhase } from "#app/phases/evolution-phase";
import { LearnMovePhase } from "#app/phases/learn-move-phase"; import { LearnMovePhase, LearnMoveType } from "#app/phases/learn-move-phase";
import { LevelUpPhase } from "#app/phases/level-up-phase"; import { LevelUpPhase } from "#app/phases/level-up-phase";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { achvs } from "#app/system/achv"; import { achvs } from "#app/system/achv";
@ -30,6 +30,7 @@ import { StatusEffect } from "#enums/status-effect";
import i18next from "i18next"; import i18next from "i18next";
import { type DoubleBattleChanceBoosterModifierType, type EvolutionItemModifierType, type FormChangeItemModifierType, type ModifierOverride, type ModifierType, type PokemonBaseStatTotalModifierType, type PokemonExpBoosterModifierType, type PokemonFriendshipBoosterModifierType, type PokemonMoveAccuracyBoosterModifierType, type PokemonMultiHitModifierType, type TerastallizeModifierType, type TmModifierType, getModifierType, ModifierPoolType, ModifierTypeGenerator, modifierTypes, PokemonHeldItemModifierType } from "./modifier-type"; import { type DoubleBattleChanceBoosterModifierType, type EvolutionItemModifierType, type FormChangeItemModifierType, type ModifierOverride, type ModifierType, type PokemonBaseStatTotalModifierType, type PokemonExpBoosterModifierType, type PokemonFriendshipBoosterModifierType, type PokemonMoveAccuracyBoosterModifierType, type PokemonMultiHitModifierType, type TerastallizeModifierType, type TmModifierType, getModifierType, ModifierPoolType, ModifierTypeGenerator, modifierTypes, PokemonHeldItemModifierType } from "./modifier-type";
import { Color, ShadowColor } from "#enums/color"; import { Color, ShadowColor } from "#enums/color";
import { FRIENDSHIP_GAIN_FROM_RARE_CANDY } from "#app/data/balance/starters";
export type ModifierPredicate = (modifier: Modifier) => boolean; export type ModifierPredicate = (modifier: Modifier) => boolean;
@ -2213,7 +2214,7 @@ export class PokemonLevelIncrementModifier extends ConsumablePokemonModifier {
playerPokemon.levelExp = 0; playerPokemon.levelExp = 0;
} }
playerPokemon.addFriendship(5); playerPokemon.addFriendship(FRIENDSHIP_GAIN_FROM_RARE_CANDY);
playerPokemon.scene.unshiftPhase(new LevelUpPhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), playerPokemon.level - levelCount.value, playerPokemon.level)); playerPokemon.scene.unshiftPhase(new LevelUpPhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), playerPokemon.level - levelCount.value, playerPokemon.level));
@ -2235,7 +2236,7 @@ export class TmModifier extends ConsumablePokemonModifier {
*/ */
override apply(playerPokemon: PlayerPokemon): boolean { override apply(playerPokemon: PlayerPokemon): boolean {
playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), this.type.moveId, true)); playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), this.type.moveId, LearnMoveType.TM));
return true; return true;
} }
@ -2255,8 +2256,9 @@ export class RememberMoveModifier extends ConsumablePokemonModifier {
* @param playerPokemon The {@linkcode PlayerPokemon} that should remember the move * @param playerPokemon The {@linkcode PlayerPokemon} that should remember the move
* @returns always `true` * @returns always `true`
*/ */
override apply(playerPokemon: PlayerPokemon): boolean { override apply(playerPokemon: PlayerPokemon, cost?: number): boolean {
playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), playerPokemon.getLearnableLevelMoves()[this.levelMoveIndex]));
playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), playerPokemon.getLearnableLevelMoves()[this.levelMoveIndex], LearnMoveType.MEMORY, cost));
return true; return true;
} }

View File

@ -114,8 +114,8 @@ export class CommandPhase extends FieldPhase {
// Decides between a Disabled, Not Implemented, or No PP translation message // Decides between a Disabled, Not Implemented, or No PP translation message
const errorMessage = const errorMessage =
playerPokemon.isMoveRestricted(move.moveId) playerPokemon.isMoveRestricted(move.moveId, playerPokemon)
? playerPokemon.getRestrictingTag(move.moveId)!.selectionDeniedText(playerPokemon, move.moveId) ? playerPokemon.getRestrictingTag(move.moveId, playerPokemon)!.selectionDeniedText(playerPokemon, move.moveId)
: move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" : "battle:moveNoPP"; : move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" : "battle:moveNoPP";
const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator

View File

@ -33,7 +33,7 @@ export class EggLapsePhase extends Phase {
if (eggsToHatchCount > 0) { if (eggsToHatchCount > 0) {
if (eggsToHatchCount >= this.minEggsToSkip && this.scene.eggSkipPreference === 1) { if (eggsToHatchCount >= this.minEggsToSkip && this.scene.eggSkipPreference === 1) {
this.scene.ui.showText(i18next.t("battle:eggHatching"), 0, () => { this.scene.ui.showText(i18next.t("battle:eggHatching"), 0, () => {
// show prompt for skip // show prompt for skip, blocking inputs for 1 second
this.scene.ui.showText(i18next.t("battle:eggSkipPrompt"), 0); this.scene.ui.showText(i18next.t("battle:eggSkipPrompt"), 0);
this.scene.ui.setModeWithoutClear(Mode.CONFIRM, () => { this.scene.ui.setModeWithoutClear(Mode.CONFIRM, () => {
this.hatchEggsSkipped(eggsToHatch); this.hatchEggsSkipped(eggsToHatch);
@ -41,7 +41,8 @@ export class EggLapsePhase extends Phase {
}, () => { }, () => {
this.hatchEggsRegular(eggsToHatch); this.hatchEggsRegular(eggsToHatch);
this.end(); this.end();
} },
null, null, null, 1000, true
); );
}, 100, true); }, 100, true);
} else if (eggsToHatchCount >= this.minEggsToSkip && this.scene.eggSkipPreference === 2) { } else if (eggsToHatchCount >= this.minEggsToSkip && this.scene.eggSkipPreference === 2) {

View File

@ -1,7 +1,6 @@
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { Phase } from "#app/phase"; import { Phase } from "#app/phase";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
import EggHatchSceneHandler from "#app/ui/egg-hatch-scene-handler";
import { EggHatchData } from "#app/data/egg-hatch-data"; import { EggHatchData } from "#app/data/egg-hatch-data";
/** /**
@ -11,7 +10,6 @@ import { EggHatchData } from "#app/data/egg-hatch-data";
*/ */
export class EggSummaryPhase extends Phase { export class EggSummaryPhase extends Phase {
private eggHatchData: EggHatchData[]; private eggHatchData: EggHatchData[];
private eggHatchHandler: EggHatchSceneHandler;
constructor(scene: BattleScene, eggHatchData: EggHatchData[]) { constructor(scene: BattleScene, eggHatchData: EggHatchData[]) {
super(scene); super(scene);
@ -26,7 +24,6 @@ export class EggSummaryPhase extends Phase {
if (i >= this.eggHatchData.length) { if (i >= this.eggHatchData.length) {
this.scene.ui.setModeForceTransition(Mode.EGG_HATCH_SUMMARY, this.eggHatchData).then(() => { this.scene.ui.setModeForceTransition(Mode.EGG_HATCH_SUMMARY, this.eggHatchData).then(() => {
this.scene.fadeOutBgm(undefined, false); this.scene.fadeOutBgm(undefined, false);
this.eggHatchHandler = this.scene.ui.getHandler() as EggHatchSceneHandler;
}); });
} else { } else {

View File

@ -1,12 +1,12 @@
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { BattlerIndex, BattleType } from "#app/battle"; import { BattlerIndex, BattleType } from "#app/battle";
import { applyPostFaintAbAttrs, PostFaintAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr } from "#app/data/ability"; import { applyPostFaintAbAttrs, PostFaintAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr } from "#app/data/ability";
import { BattlerTagLapseType } from "#app/data/battler-tags"; import { BattlerTagLapseType, DestinyBondTag } from "#app/data/battler-tags";
import { battleSpecDialogue } from "#app/data/dialogue"; import { battleSpecDialogue } from "#app/data/dialogue";
import { allMoves, PostVictoryStatStageChangeAttr } from "#app/data/move"; import { allMoves, PostVictoryStatStageChangeAttr } from "#app/data/move";
import { BattleSpec } from "#app/enums/battle-spec"; import { BattleSpec } from "#app/enums/battle-spec";
import { StatusEffect } from "#app/enums/status-effect"; import { StatusEffect } from "#app/enums/status-effect";
import { PokemonMove, EnemyPokemon, PlayerPokemon, HitResult } from "#app/field/pokemon"; import Pokemon, { PokemonMove, EnemyPokemon, PlayerPokemon, HitResult } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonInstantReviveModifier } from "#app/modifier/modifier"; import { PokemonInstantReviveModifier } from "#app/modifier/modifier";
import i18next from "i18next"; import i18next from "i18next";
@ -19,19 +19,40 @@ import { SwitchPhase } from "./switch-phase";
import { VictoryPhase } from "./victory-phase"; import { VictoryPhase } from "./victory-phase";
import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms";
import { SwitchType } from "#enums/switch-type"; import { SwitchType } from "#enums/switch-type";
import { isNullOrUndefined } from "#app/utils";
import { FRIENDSHIP_LOSS_FROM_FAINT } from "#app/data/balance/starters";
export class FaintPhase extends PokemonPhase { export class FaintPhase extends PokemonPhase {
/**
* Whether or not enduring (for this phase's purposes, Reviver Seed) should be prevented
*/
private preventEndure: boolean; private preventEndure: boolean;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, preventEndure?: boolean) { /**
* Destiny Bond tag belonging to the currently fainting Pokemon, if applicable
*/
private destinyTag?: DestinyBondTag;
/**
* The source Pokemon that dealt fatal damage and should get KO'd by Destiny Bond, if applicable
*/
private source?: Pokemon;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, preventEndure: boolean = false, destinyTag?: DestinyBondTag, source?: Pokemon) {
super(scene, battlerIndex); super(scene, battlerIndex);
this.preventEndure = preventEndure!; // TODO: is this bang correct? this.preventEndure = preventEndure;
this.destinyTag = destinyTag;
this.source = source;
} }
start() { start() {
super.start(); super.start();
if (!isNullOrUndefined(this.destinyTag) && !isNullOrUndefined(this.source)) {
this.destinyTag.lapse(this.source, BattlerTagLapseType.CUSTOM);
}
if (!this.preventEndure) { if (!this.preventEndure) {
const instantReviveModifier = this.scene.applyModifier(PokemonInstantReviveModifier, this.player, this.getPokemon()) as PokemonInstantReviveModifier; const instantReviveModifier = this.scene.applyModifier(PokemonInstantReviveModifier, this.player, this.getPokemon()) as PokemonInstantReviveModifier;
@ -127,7 +148,7 @@ export class FaintPhase extends PokemonPhase {
pokemon.faintCry(() => { pokemon.faintCry(() => {
if (pokemon instanceof PlayerPokemon) { if (pokemon instanceof PlayerPokemon) {
pokemon.addFriendship(-10); pokemon.addFriendship(-FRIENDSHIP_LOSS_FROM_FAINT);
} }
pokemon.hideInfo(); pokemon.hideInfo();
this.scene.playSound("se/faint"); this.scene.playSound("se/faint");

View File

@ -2,24 +2,37 @@ import BattleScene from "#app/battle-scene";
import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims"; import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims";
import Move, { allMoves } from "#app/data/move"; import Move, { allMoves } from "#app/data/move";
import { SpeciesFormChangeMoveLearnedTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangeMoveLearnedTrigger } from "#app/data/pokemon-forms";
import { Moves } from "#app/enums/moves"; import { Moves } from "#enums/moves";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import Overrides from "#app/overrides";
import EvolutionSceneHandler from "#app/ui/evolution-scene-handler"; import EvolutionSceneHandler from "#app/ui/evolution-scene-handler";
import { SummaryUiMode } from "#app/ui/summary-ui-handler"; import { SummaryUiMode } from "#app/ui/summary-ui-handler";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
import i18next from "i18next"; import i18next from "i18next";
import { PlayerPartyMemberPokemonPhase } from "./player-party-member-pokemon-phase"; import { PlayerPartyMemberPokemonPhase } from "#app/phases/player-party-member-pokemon-phase";
import Pokemon from "#app/field/pokemon"; import Pokemon from "#app/field/pokemon";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
export enum LearnMoveType {
/** For learning a move via level-up, evolution, or other non-item-based event */
LEARN_MOVE,
/** For learning a move via Memory Mushroom */
MEMORY,
/** For learning a move via TM */
TM
}
export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
private moveId: Moves; private moveId: Moves;
private messageMode: Mode; private messageMode: Mode;
private fromTM: boolean; private learnMoveType;
private cost: number;
constructor(scene: BattleScene, partyMemberIndex: integer, moveId: Moves, fromTM?: boolean) { constructor(scene: BattleScene, partyMemberIndex: integer, moveId: Moves, learnMoveType: LearnMoveType = LearnMoveType.LEARN_MOVE, cost: number = -1) {
super(scene, partyMemberIndex); super(scene, partyMemberIndex);
this.moveId = moveId; this.moveId = moveId;
this.fromTM = fromTM ?? false; this.learnMoveType = learnMoveType;
this.cost = cost;
} }
start() { start() {
@ -136,11 +149,23 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
* @param Pokemon The Pokemon learning the move * @param Pokemon The Pokemon learning the move
*/ */
async learnMove(index: number, move: Move, pokemon: Pokemon, textMessage?: string) { async learnMove(index: number, move: Move, pokemon: Pokemon, textMessage?: string) {
if (this.fromTM) { if (this.learnMoveType === LearnMoveType.TM) {
if (!pokemon.usedTMs) { if (!pokemon.usedTMs) {
pokemon.usedTMs = []; pokemon.usedTMs = [];
} }
pokemon.usedTMs.push(this.moveId); pokemon.usedTMs.push(this.moveId);
this.scene.tryRemovePhase((phase) => phase instanceof SelectModifierPhase);
} else if (this.learnMoveType === LearnMoveType.MEMORY) {
if (this.cost !== -1) {
if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) {
this.scene.money -= this.cost;
this.scene.updateMoneyText();
this.scene.animateMoneyChanged(false);
}
this.scene.playSound("se/buy");
} else {
this.scene.tryRemovePhase((phase) => phase instanceof SelectModifierPhase);
}
} }
pokemon.setMove(index, this.moveId); pokemon.setMove(index, this.moveId);
initMoveAnim(this.scene, this.moveId).then(() => { initMoveAnim(this.scene, this.moveId).then(() => {

View File

@ -99,8 +99,9 @@ export class MoveEffectPhase extends PokemonPhase {
const targetHitChecks = Object.fromEntries(targets.map(p => [ p.getBattlerIndex(), this.hitCheck(p) ])); const targetHitChecks = Object.fromEntries(targets.map(p => [ p.getBattlerIndex(), this.hitCheck(p) ]));
const hasActiveTargets = targets.some(t => t.isActive(true)); const hasActiveTargets = targets.some(t => t.isActive(true));
/** Check if the target is immune via ability to the attacking move */ /** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */
const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)); const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
&& !targets[0].getTag(SemiInvulnerableTag);
/** /**
* If no targets are left for the move to hit (FAIL), or the invoked move is single-target * If no targets are left for the move to hit (FAIL), or the invoked move is single-target
@ -140,7 +141,7 @@ export class MoveEffectPhase extends PokemonPhase {
const bypassIgnoreProtect = new Utils.BooleanHolder(false); const bypassIgnoreProtect = new Utils.BooleanHolder(false);
/** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */ /** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */
if (!this.move.getMove().isAllyTarget()) { if (!this.move.getMove().isAllyTarget()) {
this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect); this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, false, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect);
} }
/** Is the target protected by Protect, etc. or a relevant conditional protection effect? */ /** Is the target protected by Protect, etc. or a relevant conditional protection effect? */
@ -148,8 +149,9 @@ export class MoveEffectPhase extends PokemonPhase {
&& (hasConditionalProtectApplied.value || (!target.findTags(t => t instanceof DamageProtectedTag).length && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType))) && (hasConditionalProtectApplied.value || (!target.findTags(t => t instanceof DamageProtectedTag).length && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)))
|| (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); || (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType))));
/** Is the pokemon immune due to an ablility? */ /** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */
const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)); const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
&& !target.getTag(SemiInvulnerableTag);
/** /**
* If the move missed a target, stop all future hits against that target * If the move missed a target, stop all future hits against that target

View File

@ -128,7 +128,9 @@ export class MovePhase extends BattlePhase {
this.lapsePreMoveAndMoveTags(); this.lapsePreMoveAndMoveTags();
if (!(this.failed || this.cancelled)) {
this.resolveFinalPreMoveCancellationChecks(); this.resolveFinalPreMoveCancellationChecks();
}
if (this.cancelled || this.failed) { if (this.cancelled || this.failed) {
this.handlePreMoveFailures(); this.handlePreMoveFailures();
@ -145,8 +147,9 @@ export class MovePhase extends BattlePhase {
const moveQueue = this.pokemon.getMoveQueue(); const moveQueue = this.pokemon.getMoveQueue();
if (targets.length === 0 || (moveQueue.length && moveQueue[0].move === Moves.NONE)) { if (targets.length === 0 || (moveQueue.length && moveQueue[0].move === Moves.NONE)) {
this.showMoveText();
this.showFailedText(); this.showFailedText();
this.cancelled = true; this.cancel();
} }
} }

View File

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

View File

@ -20,7 +20,7 @@ export class PostSummonPhase extends PokemonPhase {
if (pokemon.status?.effect === StatusEffect.TOXIC) { if (pokemon.status?.effect === StatusEffect.TOXIC) {
pokemon.status.turnCount = 0; pokemon.status.turnCount = 0;
} }
this.scene.arena.applyTags(ArenaTrapTag, pokemon); this.scene.arena.applyTags(ArenaTrapTag, false, pokemon);
// If this is mystery encounter and has post summon phase tag, apply post summon effects // If this is mystery encounter and has post summon phase tag, apply post summon effects
if (this.scene.currentBattle.isBattleMysteryEncounter() && pokemon.findTags(t => t instanceof MysteryEncounterPostSummonTag).length > 0) { if (this.scene.currentBattle.isBattleMysteryEncounter() && pokemon.findTags(t => t instanceof MysteryEncounterPostSummonTag).length > 0) {

View File

@ -16,26 +16,32 @@ export class SelectModifierPhase extends BattlePhase {
private rerollCount: integer; private rerollCount: integer;
private modifierTiers?: ModifierTier[]; private modifierTiers?: ModifierTier[];
private customModifierSettings?: CustomModifierSettings; private customModifierSettings?: CustomModifierSettings;
private isCopy: boolean;
constructor(scene: BattleScene, rerollCount: integer = 0, modifierTiers?: ModifierTier[], customModifierSettings?: CustomModifierSettings) { private typeOptions: ModifierTypeOption[];
constructor(scene: BattleScene, rerollCount: integer = 0, modifierTiers?: ModifierTier[], customModifierSettings?: CustomModifierSettings, isCopy: boolean = false) {
super(scene); super(scene);
this.rerollCount = rerollCount; this.rerollCount = rerollCount;
this.modifierTiers = modifierTiers; this.modifierTiers = modifierTiers;
this.customModifierSettings = customModifierSettings; this.customModifierSettings = customModifierSettings;
this.isCopy = isCopy;
} }
start() { start() {
super.start(); super.start();
if (!this.rerollCount) { if (!this.rerollCount && !this.isCopy) {
this.updateSeed(); this.updateSeed();
} else { } else if (this.rerollCount) {
this.scene.reroll = false; this.scene.reroll = false;
} }
const party = this.scene.getParty(); const party = this.scene.getParty();
if (!this.isCopy) {
regenerateModifierPoolThresholds(party, this.getPoolType(), this.rerollCount); regenerateModifierPoolThresholds(party, this.getPoolType(), this.rerollCount);
}
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);
@ -54,7 +60,7 @@ export class SelectModifierPhase extends BattlePhase {
} }
} }
const typeOptions: ModifierTypeOption[] = this.getModifierTypeOptions(modifierCount.value); this.typeOptions = this.getModifierTypeOptions(modifierCount.value);
const modifierSelectCallback = (rowCursor: integer, cursor: integer) => { const modifierSelectCallback = (rowCursor: integer, cursor: integer) => {
if (rowCursor < 0 || cursor < 0) { if (rowCursor < 0 || cursor < 0) {
@ -63,13 +69,13 @@ export class SelectModifierPhase extends BattlePhase {
this.scene.ui.revertMode(); this.scene.ui.revertMode();
this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.setMode(Mode.MESSAGE);
super.end(); super.end();
}, () => this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers))); }, () => this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers)));
}); });
return false; return false;
} }
let modifierType: ModifierType; let modifierType: ModifierType;
let cost: integer; let cost: integer;
const rerollCost = this.getRerollCost(typeOptions, this.scene.lockModifierTiers); const rerollCost = this.getRerollCost(this.scene.lockModifierTiers);
switch (rowCursor) { switch (rowCursor) {
case 0: case 0:
switch (cursor) { switch (cursor) {
@ -79,7 +85,7 @@ export class SelectModifierPhase extends BattlePhase {
return false; return false;
} else { } else {
this.scene.reroll = true; this.scene.reroll = true;
this.scene.unshiftPhase(new SelectModifierPhase(this.scene, this.rerollCount + 1, typeOptions.map(o => o.type?.tier).filter(t => t !== undefined) as ModifierTier[])); this.scene.unshiftPhase(new SelectModifierPhase(this.scene, this.rerollCount + 1, this.typeOptions.map(o => o.type?.tier).filter(t => t !== undefined) as ModifierTier[]));
this.scene.ui.clearText(); this.scene.ui.clearText();
this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end()); this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end());
if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) { if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) {
@ -98,13 +104,13 @@ export class SelectModifierPhase extends BattlePhase {
const itemModifier = itemModifiers[itemIndex]; const itemModifier = itemModifiers[itemIndex];
this.scene.tryTransferHeldItemModifier(itemModifier, party[toSlotIndex], true, itemQuantity); this.scene.tryTransferHeldItemModifier(itemModifier, party[toSlotIndex], true, itemQuantity);
} else { } else {
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers));
} }
}, PartyUiHandler.FilterItemMaxStacks); }, PartyUiHandler.FilterItemMaxStacks);
break; break;
case 2: case 2:
this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.CHECK, -1, () => { this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.CHECK, -1, () => {
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers));
}); });
break; break;
case 3: case 3:
@ -115,21 +121,21 @@ export class SelectModifierPhase extends BattlePhase {
} }
this.scene.lockModifierTiers = !this.scene.lockModifierTiers; this.scene.lockModifierTiers = !this.scene.lockModifierTiers;
const uiHandler = this.scene.ui.getHandler() as ModifierSelectUiHandler; const uiHandler = this.scene.ui.getHandler() as ModifierSelectUiHandler;
uiHandler.setRerollCost(this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); uiHandler.setRerollCost(this.getRerollCost(this.scene.lockModifierTiers));
uiHandler.updateLockRaritiesText(); uiHandler.updateLockRaritiesText();
uiHandler.updateRerollCostText(); uiHandler.updateRerollCostText();
return false; return false;
} }
return true; return true;
case 1: case 1:
if (typeOptions.length === 0) { if (this.typeOptions.length === 0) {
this.scene.ui.clearText(); this.scene.ui.clearText();
this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.setMode(Mode.MESSAGE);
super.end(); super.end();
return true; return true;
} }
if (typeOptions[cursor].type) { if (this.typeOptions[cursor].type) {
modifierType = typeOptions[cursor].type; modifierType = this.typeOptions[cursor].type;
} }
break; break;
default: default:
@ -151,8 +157,16 @@ export class SelectModifierPhase extends BattlePhase {
} }
const applyModifier = (modifier: Modifier, playSound: boolean = false) => { const applyModifier = (modifier: Modifier, playSound: boolean = false) => {
const result = this.scene.addModifier(modifier, false, playSound); const result = this.scene.addModifier(modifier, false, playSound, undefined, undefined, cost);
if (cost) { // Queue a copy of this phase when applying a TM or Memory Mushroom.
// If the player selects either of these, then escapes out of consuming them,
// they are returned to a shop in the same state.
if (modifier.type instanceof RememberMoveModifierType ||
modifier.type instanceof TmModifierType) {
this.scene.unshiftPhase(this.copy());
}
if (cost && !(modifier.type instanceof RememberMoveModifierType)) {
result.then(success => { result.then(success => {
if (success) { if (success) {
if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) { if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) {
@ -189,7 +203,7 @@ export class SelectModifierPhase extends BattlePhase {
applyModifier(modifier, true); applyModifier(modifier, true);
}); });
} else { } else {
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers));
} }
}, modifierType.selectFilter); }, modifierType.selectFilter);
} else { } else {
@ -216,7 +230,7 @@ export class SelectModifierPhase extends BattlePhase {
applyModifier(modifier!, true); // TODO: is the bang correct? applyModifier(modifier!, true); // TODO: is the bang correct?
}); });
} else { } else {
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers));
} }
}, pokemonModifierType.selectFilter, modifierType instanceof PokemonMoveModifierType ? (modifierType as PokemonMoveModifierType).moveSelectFilter : undefined, tmMoveId, isPpRestoreModifier); }, pokemonModifierType.selectFilter, modifierType instanceof PokemonMoveModifierType ? (modifierType as PokemonMoveModifierType).moveSelectFilter : undefined, tmMoveId, isPpRestoreModifier);
} }
@ -226,7 +240,7 @@ export class SelectModifierPhase extends BattlePhase {
return !cost!;// TODO: is the bang correct? return !cost!;// TODO: is the bang correct?
}; };
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers));
} }
updateSeed(): void { updateSeed(): void {
@ -237,13 +251,13 @@ export class SelectModifierPhase extends BattlePhase {
return true; return true;
} }
getRerollCost(typeOptions: ModifierTypeOption[], lockRarities: boolean): number { getRerollCost(lockRarities: boolean): number {
let baseValue = 0; let baseValue = 0;
if (Overrides.WAIVE_ROLL_FEE_OVERRIDE) { if (Overrides.WAIVE_ROLL_FEE_OVERRIDE) {
return baseValue; return baseValue;
} else if (lockRarities) { } else if (lockRarities) {
const tierValues = [ 50, 125, 300, 750, 2000 ]; const tierValues = [ 50, 125, 300, 750, 2000 ];
for (const opt of typeOptions) { for (const opt of this.typeOptions) {
baseValue += tierValues[opt.type.tier ?? 0]; baseValue += tierValues[opt.type.tier ?? 0];
} }
} else { } else {
@ -271,6 +285,16 @@ export class SelectModifierPhase extends BattlePhase {
return getPlayerModifierTypeOptions(modifierCount, this.scene.getParty(), this.scene.lockModifierTiers ? this.modifierTiers : undefined, this.customModifierSettings); return getPlayerModifierTypeOptions(modifierCount, this.scene.getParty(), this.scene.lockModifierTiers ? this.modifierTiers : undefined, this.customModifierSettings);
} }
copy(): SelectModifierPhase {
return new SelectModifierPhase(
this.scene,
this.rerollCount,
this.modifierTiers,
{ guaranteedModifierTypeOptions: this.typeOptions, rerollMultiplier: this.customModifierSettings?.rerollMultiplier, allowLuckUpgrades: false },
true
);
}
addModifier(modifier: Modifier): Promise<boolean> { addModifier(modifier: Modifier): Promise<boolean> {
return this.scene.addModifier(modifier, false, true); return this.scene.addModifier(modifier, false, true);
} }

View File

@ -64,8 +64,7 @@ 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) {
// TODO: Include simulate boolean when tag applications can be simulated this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, false, cancelled);
this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled);
} }
if (!cancelled.value && !this.selfTarget && stages.value < 0) { if (!cancelled.value && !this.selfTarget && stages.value < 0) {

View File

@ -45,7 +45,7 @@ export class TurnStartPhase extends FieldPhase {
// Next, a check for Trick Room is applied to determine sort order. // Next, a check for Trick Room is applied to determine sort order.
const speedReversed = new Utils.BooleanHolder(false); const speedReversed = new Utils.BooleanHolder(false);
this.scene.arena.applyTags(TrickRoomTag, speedReversed); this.scene.arena.applyTags(TrickRoomTag, false, speedReversed);
// Adjust the sort function based on whether Trick Room is active. // Adjust the sort function based on whether Trick Room is active.
orderedTargets.sort((a: Pokemon, b: Pokemon) => { orderedTargets.sort((a: Pokemon, b: Pokemon) => {

View File

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

View File

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

View File

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

View File

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

View File

@ -32,7 +32,7 @@ describe("Abilities - POWER CONSTRUCT", () => {
}); });
test( test(
"check if fainted pokemon switches to base form on arena reset", "check if fainted 50% Power Construct Pokemon switches to base form on arena reset",
async () => { async () => {
const baseForm = 2, const baseForm = 2,
completeForm = 4; completeForm = 4;
@ -41,7 +41,37 @@ describe("Abilities - POWER CONSTRUCT", () => {
[Species.ZYGARDE]: completeForm, [Species.ZYGARDE]: completeForm,
}); });
await game.startBattle([ Species.MAGIKARP, Species.ZYGARDE ]); await game.classicMode.startBattle([ Species.MAGIKARP, Species.ZYGARDE ]);
const zygarde = game.scene.getParty().find((p) => p.species.speciesId === Species.ZYGARDE);
expect(zygarde).not.toBe(undefined);
expect(zygarde!.formIndex).toBe(completeForm);
zygarde!.hp = 0;
zygarde!.status = new Status(StatusEffect.FAINT);
expect(zygarde!.isFainted()).toBe(true);
game.move.select(Moves.SPLASH);
await game.doKillOpponents();
await game.phaseInterceptor.to(TurnEndPhase);
game.doSelectModifier();
await game.phaseInterceptor.to(QuietFormChangePhase);
expect(zygarde!.formIndex).toBe(baseForm);
},
);
test(
"check if fainted 10% Power Construct Pokemon switches to base form on arena reset",
async () => {
const baseForm = 3,
completeForm = 5;
game.override.startingWave(4);
game.override.starterForms({
[Species.ZYGARDE]: completeForm,
});
await game.classicMode.startBattle([ Species.MAGIKARP, Species.ZYGARDE ]);
const zygarde = game.scene.getParty().find((p) => p.species.speciesId === Species.ZYGARDE); const zygarde = game.scene.getParty().find((p) => p.species.speciesId === Species.ZYGARDE);
expect(zygarde).not.toBe(undefined); expect(zygarde).not.toBe(undefined);

View File

@ -71,4 +71,23 @@ 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 () => {
game.override.moveset(Moves.THUNDERBOLT);
game.override.enemyMoveset(Moves.DIVE);
game.override.enemySpecies(Species.MAGIKARP);
game.override.enemyAbility(Abilities.VOLT_ABSORB);
await game.classicMode.startBattle();
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.THUNDERBOLT);
enemyPokemon.hp = enemyPokemon.hp - 1;
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("MoveEffectPhase");
await game.move.forceMiss();
await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
});
}); });

View File

@ -1,7 +1,8 @@
import { Abilities } from "#app/enums/abilities"; import { Abilities } from "#app/enums/abilities";
import { Moves } from "#app/enums/moves"; import { Moves } from "#app/enums/moves";
import { ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type"; import { ModifierTier } from "#app/modifier/modifier-tier";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { Mode } from "#app/ui/ui";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phase from "phaser"; import Phase from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
@ -32,15 +33,16 @@ describe("Items - Lock Capsule", () => {
}); });
it("doesn't set the cost of common tier items to 0", async () => { it("doesn't set the cost of common tier items to 0", async () => {
await game.startBattle(); await game.classicMode.startBattle();
game.scene.overridePhase(new SelectModifierPhase(game.scene, 0, undefined, { guaranteedModifierTiers: [ ModifierTier.COMMON, ModifierTier.COMMON, ModifierTier.COMMON ], fillRemaining: false }));
game.move.select(Moves.SURF);
await game.phaseInterceptor.to(SelectModifierPhase, false);
const rewards = game.scene.getCurrentPhase() as SelectModifierPhase;
const potion = new ModifierTypeOption(modifierTypes.POTION(), 0, 40); // Common tier item
const rerollCost = rewards.getRerollCost([ potion, potion, potion ], true);
game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => {
const selectModifierPhase = game.scene.getCurrentPhase() as SelectModifierPhase;
const rerollCost = selectModifierPhase.getRerollCost(true);
expect(rerollCost).toBe(150); expect(rerollCost).toBe(150);
});
game.doSelectModifier();
await game.phaseInterceptor.to("SelectModifierPhase");
}, 20000); }, 20000);
}); });

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, move.category, defender.scene.currentBattle.double, multiplierHolder); defender.scene.arena.applyTagsForSide(ArenaTagType.AURORA_VEIL, side, false, move.category, multiplierHolder);
} }
return move.power * multiplierHolder.value; return move.power * multiplierHolder.value;

View File

@ -0,0 +1,255 @@
import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag";
import { allMoves } from "#app/data/move";
import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type";
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, vi } from "vitest";
import { BattlerIndex } from "#app/battle";
import { StatusEffect } from "#enums/status-effect";
import { PokemonInstantReviveModifier } from "#app/modifier/modifier";
describe("Moves - Destiny Bond", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const defaultParty = [ Species.BULBASAUR, Species.SQUIRTLE ];
const enemyFirst = [ BattlerIndex.ENEMY, BattlerIndex.PLAYER ];
const playerFirst = [ BattlerIndex.PLAYER, BattlerIndex.ENEMY ];
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override.battleType("single")
.ability(Abilities.UNNERVE) // Pre-emptively prevent flakiness from opponent berries
.enemySpecies(Species.RATTATA)
.enemyAbility(Abilities.RUN_AWAY)
.startingLevel(100) // Make sure tested moves KO
.enemyLevel(5)
.enemyMoveset(Moves.DESTINY_BOND);
});
it("should KO the opponent on the same turn", async () => {
const moveToUse = Moves.TACKLE;
game.override.moveset(moveToUse);
await game.classicMode.startBattle(defaultParty);
const enemyPokemon = game.scene.getEnemyPokemon();
const playerPokemon = game.scene.getPlayerPokemon();
game.move.select(moveToUse);
await game.setTurnOrder(enemyFirst);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon?.isFainted()).toBe(true);
expect(playerPokemon?.isFainted()).toBe(true);
});
it("should KO the opponent on the next turn", async () => {
const moveToUse = Moves.TACKLE;
game.override.moveset([ Moves.SPLASH, moveToUse ]);
await game.classicMode.startBattle(defaultParty);
const enemyPokemon = game.scene.getEnemyPokemon();
const playerPokemon = game.scene.getPlayerPokemon();
// Turn 1: Enemy uses Destiny Bond and doesn't faint
game.move.select(Moves.SPLASH);
await game.setTurnOrder(playerFirst);
await game.toNextTurn();
expect(enemyPokemon?.isFainted()).toBe(false);
expect(playerPokemon?.isFainted()).toBe(false);
// Turn 2: Player KO's the enemy before the enemy's turn
game.move.select(moveToUse);
await game.setTurnOrder(playerFirst);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon?.isFainted()).toBe(true);
expect(playerPokemon?.isFainted()).toBe(true);
});
it("should fail if used twice in a row", async () => {
const moveToUse = Moves.TACKLE;
game.override.moveset([ Moves.SPLASH, moveToUse ]);
await game.classicMode.startBattle(defaultParty);
const enemyPokemon = game.scene.getEnemyPokemon();
const playerPokemon = game.scene.getPlayerPokemon();
// Turn 1: Enemy uses Destiny Bond and doesn't faint
game.move.select(Moves.SPLASH);
await game.setTurnOrder(enemyFirst);
await game.toNextTurn();
expect(enemyPokemon?.isFainted()).toBe(false);
expect(playerPokemon?.isFainted()).toBe(false);
// Turn 2: Enemy should fail Destiny Bond then get KO'd
game.move.select(moveToUse);
await game.setTurnOrder(enemyFirst);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon?.isFainted()).toBe(true);
expect(playerPokemon?.isFainted()).toBe(false);
});
it("should not KO the opponent if the user dies to weather", async () => {
// Opponent will be reduced to 1 HP by False Swipe, then faint to Sandstorm
const moveToUse = Moves.FALSE_SWIPE;
game.override.moveset(moveToUse)
.ability(Abilities.SAND_STREAM);
await game.classicMode.startBattle(defaultParty);
const enemyPokemon = game.scene.getEnemyPokemon();
const playerPokemon = game.scene.getPlayerPokemon();
game.move.select(moveToUse);
await game.setTurnOrder(enemyFirst);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon?.isFainted()).toBe(true);
expect(playerPokemon?.isFainted()).toBe(false);
});
it("should not KO the opponent if the user had another turn", async () => {
const moveToUse = Moves.TACKLE;
game.override.moveset([ Moves.SPORE, moveToUse ]);
await game.classicMode.startBattle(defaultParty);
const enemyPokemon = game.scene.getEnemyPokemon();
const playerPokemon = game.scene.getPlayerPokemon();
// Turn 1: Enemy uses Destiny Bond and doesn't faint
game.move.select(Moves.SPORE);
await game.setTurnOrder(enemyFirst);
await game.toNextTurn();
expect(enemyPokemon?.isFainted()).toBe(false);
expect(playerPokemon?.isFainted()).toBe(false);
expect(enemyPokemon?.status?.effect).toBe(StatusEffect.SLEEP);
// Turn 2: Enemy should skip a turn due to sleep, then get KO'd
game.move.select(moveToUse);
await game.setTurnOrder(enemyFirst);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon?.isFainted()).toBe(true);
expect(playerPokemon?.isFainted()).toBe(false);
});
it("should not KO an ally", async () => {
game.override.moveset([ Moves.DESTINY_BOND, Moves.CRUNCH ])
.battleType("double");
await game.classicMode.startBattle([ Species.SHEDINJA, Species.BULBASAUR, Species.SQUIRTLE ]);
const enemyPokemon0 = game.scene.getEnemyField()[0];
const enemyPokemon1 = game.scene.getEnemyField()[1];
const playerPokemon0 = game.scene.getPlayerField()[0];
const playerPokemon1 = game.scene.getPlayerField()[1];
// Shedinja uses Destiny Bond, then ally Bulbasaur KO's Shedinja with Crunch
game.move.select(Moves.DESTINY_BOND, 0);
game.move.select(Moves.CRUNCH, 1, BattlerIndex.PLAYER);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon0?.isFainted()).toBe(false);
expect(enemyPokemon1?.isFainted()).toBe(false);
expect(playerPokemon0?.isFainted()).toBe(true);
expect(playerPokemon1?.isFainted()).toBe(false);
});
it("should not cause a crash if the user is KO'd by Ceaseless Edge", async () => {
const moveToUse = Moves.CEASELESS_EDGE;
vi.spyOn(allMoves[moveToUse], "accuracy", "get").mockReturnValue(100);
game.override.moveset(moveToUse);
await game.classicMode.startBattle(defaultParty);
const enemyPokemon = game.scene.getEnemyPokemon();
const playerPokemon = game.scene.getPlayerPokemon();
game.move.select(moveToUse);
await game.setTurnOrder(enemyFirst);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon?.isFainted()).toBe(true);
expect(playerPokemon?.isFainted()).toBe(true);
// Ceaseless Edge spikes effect should still activate
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag;
expect(tagAfter.tagType).toBe(ArenaTagType.SPIKES);
expect(tagAfter.layers).toBe(1);
});
it("should not cause a crash if the user is KO'd by Pledge moves", async () => {
game.override.moveset([ Moves.GRASS_PLEDGE, Moves.WATER_PLEDGE ])
.battleType("double");
await game.classicMode.startBattle(defaultParty);
const enemyPokemon0 = game.scene.getEnemyField()[0];
const enemyPokemon1 = game.scene.getEnemyField()[1];
const playerPokemon0 = game.scene.getPlayerField()[0];
const playerPokemon1 = game.scene.getPlayerField()[1];
game.move.select(Moves.GRASS_PLEDGE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.WATER_PLEDGE, 1, BattlerIndex.ENEMY);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2 ]);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon0?.isFainted()).toBe(true);
expect(enemyPokemon1?.isFainted()).toBe(false);
expect(playerPokemon0?.isFainted()).toBe(false);
expect(playerPokemon1?.isFainted()).toBe(true);
// Pledge secondary effect should still activate
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.GRASS_WATER_PLEDGE, ArenaTagSide.ENEMY) as ArenaTrapTag;
expect(tagAfter.tagType).toBe(ArenaTagType.GRASS_WATER_PLEDGE);
});
/**
* In particular, this should prevent something like
* {@link https://github.com/pagefaultgames/pokerogue/issues/4219}
* from occurring with fainting by KO'ing a Destiny Bond user with U-Turn.
*/
it("should not allow the opponent to revive via Reviver Seed", async () => {
const moveToUse = Moves.TACKLE;
game.override.moveset(moveToUse)
.startingHeldItems([{ name: "REVIVER_SEED" }]);
await game.classicMode.startBattle(defaultParty);
const enemyPokemon = game.scene.getEnemyPokemon();
const playerPokemon = game.scene.getPlayerPokemon();
game.move.select(moveToUse);
await game.setTurnOrder(enemyFirst);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon?.isFainted()).toBe(true);
expect(playerPokemon?.isFainted()).toBe(true);
// Check that the Tackle user's Reviver Seed did not activate
const revSeeds = game.scene.getModifiers(PokemonInstantReviveModifier).filter(m => m.pokemonId === playerPokemon?.id);
expect(revSeeds.length).toBe(1);
});
});

View File

@ -139,4 +139,58 @@ describe("Moves - Dragon Tail", () => {
expect(enemy.isFullHp()).toBe(false); expect(enemy.isFullHp()).toBe(false);
}); });
it("should force a switch upon fainting an opponent normally", async () => {
game.override.startingWave(5)
.startingLevel(1000); // To make sure Dragon Tail KO's the opponent
await game.classicMode.startBattle([ Species.DRATINI ]);
game.move.select(Moves.DRAGON_TAIL);
await game.toNextTurn();
// Make sure the enemy switched to a healthy Pokemon
const enemy = game.scene.getEnemyPokemon()!;
expect(enemy).toBeDefined();
expect(enemy.isFullHp()).toBe(true);
// Make sure the enemy has a fainted Pokemon in their party and not on the field
const faintedEnemy = game.scene.getEnemyParty().find(p => !p.isAllowedInBattle());
expect(faintedEnemy).toBeDefined();
expect(game.scene.getEnemyField().length).toBe(1);
});
it("should not cause a softlock when activating an opponent trainer's reviver seed", async () => {
game.override.startingWave(5)
.enemyHeldItems([{ name: "REVIVER_SEED" }])
.startingLevel(1000); // To make sure Dragon Tail KO's the opponent
await game.classicMode.startBattle([ Species.DRATINI ]);
game.move.select(Moves.DRAGON_TAIL);
await game.toNextTurn();
// Make sure the enemy field is not empty and has a revived Pokemon
const enemy = game.scene.getEnemyPokemon()!;
expect(enemy).toBeDefined();
expect(enemy.hp).toBe(Math.floor(enemy.getMaxHp() / 2));
expect(game.scene.getEnemyField().length).toBe(1);
});
it("should not cause a softlock when activating a player's reviver seed", async () => {
game.override.startingHeldItems([{ name: "REVIVER_SEED" }])
.enemyMoveset(Moves.DRAGON_TAIL)
.enemyLevel(1000); // To make sure Dragon Tail KO's the player
await game.classicMode.startBattle([ Species.DRATINI, Species.BULBASAUR ]);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
// Make sure the player's field is not empty and has a revived Pokemon
const dratini = game.scene.getPlayerPokemon()!;
expect(dratini).toBeDefined();
expect(dratini.hp).toBe(Math.floor(dratini.getMaxHp() / 2));
expect(game.scene.getPlayerField().length).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, move.category, defender.scene.currentBattle.double, multiplierHolder); defender.scene.arena.applyTagsForSide(ArenaTagType.LIGHT_SCREEN, side, false, move.category, multiplierHolder);
} }
return move.power * multiplierHolder.value; return move.power * multiplierHolder.value;

View File

@ -0,0 +1,113 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import { Moves } from "#enums/moves";
import { Stat } from "#enums/stat";
import { Species } from "#enums/species";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
describe("Moves - Power Trick", () => {
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")
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH)
.enemySpecies(Species.MEW)
.enemyLevel(200)
.moveset([ Moves.POWER_TRICK ])
.ability(Abilities.BALL_FETCH);
});
it("swaps the user's ATK and DEF stats", async () => {
await game.classicMode.startBattle([ Species.SHUCKLE ]);
const player = game.scene.getPlayerPokemon()!;
const baseATK = player.getStat(Stat.ATK, false);
const baseDEF = player.getStat(Stat.DEF, false);
game.move.select(Moves.POWER_TRICK);
await game.phaseInterceptor.to(TurnEndPhase);
expect(player.getStat(Stat.ATK, false)).toBe(baseDEF);
expect(player.getStat(Stat.DEF, false)).toBe(baseATK);
expect(player.getTag(BattlerTagType.POWER_TRICK)).toBeDefined();
});
it("resets initial ATK and DEF stat swap when used consecutively", async () => {
await game.classicMode.startBattle([ Species.SHUCKLE ]);
const player = game.scene.getPlayerPokemon()!;
const baseATK = player.getStat(Stat.ATK, false);
const baseDEF = player.getStat(Stat.DEF, false);
game.move.select(Moves.POWER_TRICK);
await game.phaseInterceptor.to(TurnEndPhase);
game.move.select(Moves.POWER_TRICK);
await game.phaseInterceptor.to(TurnEndPhase);
expect(player.getStat(Stat.ATK, false)).toBe(baseATK);
expect(player.getStat(Stat.DEF, false)).toBe(baseDEF);
expect(player.getTag(BattlerTagType.POWER_TRICK)).toBeUndefined();
});
it("should pass effect when using BATON_PASS", async () => {
await game.classicMode.startBattle([ Species.SHUCKLE, Species.SHUCKLE ]);
await game.override.moveset([ Moves.POWER_TRICK, Moves.BATON_PASS ]);
const player = game.scene.getPlayerPokemon()!;
player.addTag(BattlerTagType.POWER_TRICK);
game.move.select(Moves.BATON_PASS);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to(TurnEndPhase);
const switchedPlayer = game.scene.getPlayerPokemon()!;
const baseATK = switchedPlayer.getStat(Stat.ATK);
const baseDEF = switchedPlayer.getStat(Stat.DEF);
expect(switchedPlayer.getStat(Stat.ATK, false)).toBe(baseDEF);
expect(switchedPlayer.getStat(Stat.DEF, false)).toBe(baseATK);
expect(switchedPlayer.getTag(BattlerTagType.POWER_TRICK)).toBeDefined();
});
it("should remove effect after using Transform", async () => {
await game.classicMode.startBattle([ Species.SHUCKLE, Species.SHUCKLE ]);
await game.override.moveset([ Moves.POWER_TRICK, Moves.TRANSFORM ]);
const player = game.scene.getPlayerPokemon()!;
player.addTag(BattlerTagType.POWER_TRICK);
game.move.select(Moves.TRANSFORM);
await game.phaseInterceptor.to(TurnEndPhase);
const enemy = game.scene.getEnemyPokemon()!;
const baseATK = enemy.getStat(Stat.ATK);
const baseDEF = enemy.getStat(Stat.DEF);
expect(player.getStat(Stat.ATK, false)).toBe(baseATK);
expect(player.getStat(Stat.DEF, false)).toBe(baseDEF);
expect(player.getTag(BattlerTagType.POWER_TRICK)).toBeUndefined();
});
});

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, move.category, defender.scene.currentBattle.double, multiplierHolder); defender.scene.arena.applyTagsForSide(ArenaTagType.REFLECT, side, false, move.category, multiplierHolder);
} }
return move.power * multiplierHolder.value; return move.power * multiplierHolder.value;

View File

@ -0,0 +1,89 @@
import { Abilities } from "#enums/abilities";
import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves";
import { Stat } from "#enums/stat";
import { allMoves, SecretPowerAttr } from "#app/data/move";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { StatusEffect } from "#enums/status-effect";
import { BattlerIndex } from "#app/battle";
import { ArenaTagType } from "#enums/arena-tag-type";
import { ArenaTagSide } from "#app/data/arena-tag";
describe("Moves - Secret Power", () => {
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.SECRET_POWER ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyLevel(60)
.enemyAbility(Abilities.BALL_FETCH);
});
it("Secret Power checks for an active terrain first then looks at the biome for its secondary effect", async () => {
game.override
.startingBiome(Biome.VOLCANO)
.enemyMoveset([ Moves.SPLASH, Moves.MISTY_TERRAIN ]);
vi.spyOn(allMoves[Moves.SECRET_POWER], "chance", "get").mockReturnValue(100);
await game.classicMode.startBattle([ Species.FEEBAS ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
// No Terrain + Biome.VOLCANO --> Burn
game.move.select(Moves.SECRET_POWER);
await game.forceEnemyMove(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemyPokemon.status?.effect).toBe(StatusEffect.BURN);
// Misty Terrain --> SpAtk -1
game.move.select(Moves.SECRET_POWER);
await game.forceEnemyMove(Moves.MISTY_TERRAIN);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-1);
});
it("the 'rainbow' effect of fire+water pledge does not double the chance of secret power's secondary effect",
async () => {
game.override
.moveset([ Moves.FIRE_PLEDGE, Moves.WATER_PLEDGE, Moves.SECRET_POWER, Moves.SPLASH ])
.enemyMoveset([ Moves.SPLASH ])
.battleType("double");
await game.classicMode.startBattle([ Species.BLASTOISE, Species.CHARIZARD ]);
const secretPowerAttr = allMoves[Moves.SECRET_POWER].getAttrs(SecretPowerAttr)[0];
vi.spyOn(secretPowerAttr, "getMoveChance");
game.move.select(Moves.WATER_PLEDGE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.FIRE_PLEDGE, 1, BattlerIndex.ENEMY_2);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.getTagOnSide(ArenaTagType.WATER_FIRE_PLEDGE, ArenaTagSide.PLAYER)).toBeDefined();
game.move.select(Moves.SECRET_POWER, 0, BattlerIndex.ENEMY);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to("BerryPhase", false);
expect(secretPowerAttr.getMoveChance).toHaveLastReturnedWith(30);
}
);
});

View File

@ -96,4 +96,23 @@ describe("Moves - U-turn", () => {
expect(game.scene.getEnemyPokemon()!.battleData.abilityRevealed).toBe(true); // proxy for asserting ability activated expect(game.scene.getEnemyPokemon()!.battleData.abilityRevealed).toBe(true); // proxy for asserting ability activated
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
}, 20000); }, 20000);
it("still forces a switch if u-turn KO's the opponent", async () => {
game.override.startingLevel(1000); // Ensure that U-Turn KO's the opponent
await game.classicMode.startBattle([
Species.RAICHU,
Species.SHUCKLE
]);
const enemy = game.scene.getEnemyPokemon()!;
// KO the opponent with U-Turn
game.move.select(Moves.U_TURN);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to(TurnEndPhase);
expect(enemy.isFainted()).toBe(true);
// Check that U-Turn forced a switch
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.SHUCKLE);
});
}); });

View File

@ -124,10 +124,31 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => {
}); });
}); });
it("should start battle against the trainer", async () => { it("should start battle against the trainer with correctly loaded assets", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
let successfullyLoaded = false;
vi.spyOn(scene, "getEnemyParty").mockImplementation(() => {
const ace = scene.currentBattle?.enemyParty[0];
if (ace) {
// Pretend that loading assets takes an extra 500ms
vi.spyOn(ace, "loadAssets").mockImplementation(() => new Promise(resolve => {
setTimeout(() => {
successfullyLoaded = true;
resolve();
}, 500);
}));
}
return scene.currentBattle?.enemyParty ?? [];
});
await runMysteryEncounterToEnd(game, 1, undefined, true); await runMysteryEncounterToEnd(game, 1, undefined, true);
// Check that assets are successfully loaded
expect(successfullyLoaded).toBe(true);
// Check usual battle stuff
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
expect(scene.currentBattle.trainer).toBeDefined(); expect(scene.currentBattle.trainer).toBeDefined();
expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE);
@ -182,10 +203,31 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => {
}); });
}); });
it("should start battle against the trainer", async () => { it("should start battle against the trainer with correctly loaded assets", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
let successfullyLoaded = false;
vi.spyOn(scene, "getEnemyParty").mockImplementation(() => {
const ace = scene.currentBattle?.enemyParty[0];
if (ace) {
// Pretend that loading assets takes an extra 500ms
vi.spyOn(ace, "loadAssets").mockImplementation(() => new Promise(resolve => {
setTimeout(() => {
successfullyLoaded = true;
resolve();
}, 500);
}));
}
return scene.currentBattle?.enemyParty ?? [];
});
await runMysteryEncounterToEnd(game, 2, undefined, true); await runMysteryEncounterToEnd(game, 2, undefined, true);
// Check that assets are successfully loaded
expect(successfullyLoaded).toBe(true);
// Check usual battle stuff
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
expect(scene.currentBattle.trainer).toBeDefined(); expect(scene.currentBattle.trainer).toBeDefined();
expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE);
@ -240,10 +282,31 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => {
}); });
}); });
it("should start battle against the trainer", async () => { it("should start battle against the trainer with correctly loaded assets", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
let successfullyLoaded = false;
vi.spyOn(scene, "getEnemyParty").mockImplementation(() => {
const ace = scene.currentBattle?.enemyParty[0];
if (ace) {
// Pretend that loading assets takes an extra 500ms
vi.spyOn(ace, "loadAssets").mockImplementation(() => new Promise(resolve => {
setTimeout(() => {
successfullyLoaded = true;
resolve();
}, 500);
}));
}
return scene.currentBattle?.enemyParty ?? [];
});
await runMysteryEncounterToEnd(game, 3, undefined, true); await runMysteryEncounterToEnd(game, 3, undefined, true);
// Check that assets are successfully loaded
expect(successfullyLoaded).toBe(true);
// Check usual battle stuff
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
expect(scene.currentBattle.trainer).toBeDefined(); expect(scene.currentBattle.trainer).toBeDefined();
expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE);

View File

@ -63,11 +63,11 @@ describe("SelectModifierPhase", () => {
new ModifierTypeOption(modifierTypes.REVIVE(), 0, 1000) new ModifierTypeOption(modifierTypes.REVIVE(), 0, 1000)
]; ];
const selectModifierPhase1 = new SelectModifierPhase(scene); const selectModifierPhase1 = new SelectModifierPhase(scene, 0, undefined, { guaranteedModifierTypeOptions: options });
const selectModifierPhase2 = new SelectModifierPhase(scene, 0, undefined, { rerollMultiplier: 2 }); const selectModifierPhase2 = new SelectModifierPhase(scene, 0, undefined, { guaranteedModifierTypeOptions: options, rerollMultiplier: 2 });
const cost1 = selectModifierPhase1.getRerollCost(options, false); const cost1 = selectModifierPhase1.getRerollCost(false);
const cost2 = selectModifierPhase2.getRerollCost(options, false); const cost2 = selectModifierPhase2.getRerollCost(false);
expect(cost2).toEqual(cost1 * 2); expect(cost2).toEqual(cost1 * 2);
}); });

View File

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

View File

@ -165,6 +165,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
if (this.config.delay) { if (this.config.delay) {
this.blockInput = true; this.blockInput = true;
this.optionSelectText.setAlpha(0.5); this.optionSelectText.setAlpha(0.5);
this.cursorObj?.setAlpha(0.8);
this.scene.time.delayedCall(Utils.fixedInt(this.config.delay), () => this.unblockInput()); this.scene.time.delayedCall(Utils.fixedInt(this.config.delay), () => this.unblockInput());
} }
@ -256,6 +257,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
this.blockInput = false; this.blockInput = false;
this.optionSelectText.setAlpha(1); this.optionSelectText.setAlpha(1);
this.cursorObj?.setAlpha(1);
} }
getOptionsWithScroll(): OptionSelectItem[] { getOptionsWithScroll(): OptionSelectItem[] {

View File

@ -76,7 +76,8 @@ export default class ConfirmUiHandler extends AbstractOptionSelectUiHandler {
} }
} }
], ],
delay: args.length >= 6 && args[5] !== null ? args[5] as integer : 0 delay: args.length >= 6 && args[5] !== null ? args[5] as number : 0,
noCancel: args.length >= 7 && args[6] !== null ? args[6] as boolean : false,
}; };
super.show([ config ]); super.show([ config ]);
@ -96,7 +97,7 @@ export default class ConfirmUiHandler extends AbstractOptionSelectUiHandler {
} }
processInput(button: Button): boolean { processInput(button: Button): boolean {
if (button === Button.CANCEL && this.blockInput) { if (button === Button.CANCEL && this.blockInput && !this.config?.noCancel) {
this.unblockInput(); this.unblockInput();
} }

View File

@ -42,6 +42,9 @@ export default class EggSummaryUiHandler extends MessageUiHandler {
private scrollGridHandler : ScrollableGridUiHandler; private scrollGridHandler : ScrollableGridUiHandler;
private cursorObj: Phaser.GameObjects.Image; private cursorObj: Phaser.GameObjects.Image;
/** used to add a delay before which it is not possible to exit the summary */
private blockExit: boolean;
/** /**
* Allows subscribers to listen for events * Allows subscribers to listen for events
* *
@ -168,6 +171,13 @@ export default class EggSummaryUiHandler extends MessageUiHandler {
this.setCursor(0); this.setCursor(0);
this.scene.playSoundWithoutBgm("evolution_fanfare"); this.scene.playSoundWithoutBgm("evolution_fanfare");
// Prevent exiting the egg summary for 2 seconds if the egg hatching
// was skipped automatically and for 1 second otherwise
const exitBlockingDuration = (this.scene.eggSkipPreference === 2) ? 2000 : 1000;
this.blockExit = true;
this.scene.time.delayedCall(exitBlockingDuration, () => this.blockExit = false);
return true; return true;
} }
@ -203,13 +213,17 @@ export default class EggSummaryUiHandler extends MessageUiHandler {
const ui = this.getUi(); const ui = this.getUi();
let success = false; let success = false;
const error = false; let error = false;
if (button === Button.CANCEL) { if (button === Button.CANCEL) {
if (!this.blockExit) {
const phase = this.scene.getCurrentPhase(); const phase = this.scene.getCurrentPhase();
if (phase instanceof EggSummaryPhase) { if (phase instanceof EggSummaryPhase) {
phase.end(); phase.end();
} }
success = true; success = true;
} else {
error = true;
}
} else { } else {
this.scrollGridHandler.processInput(button); this.scrollGridHandler.processInput(button);
} }

View File

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

View File

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

View File

@ -671,6 +671,9 @@ export default class PartyUiHandler extends MessageUiHandler {
} else if (this.cursor === 6) { } else if (this.cursor === 6) {
this.partyCancelButton.select(); this.partyCancelButton.select();
} }
if (this.lastCursor < 6 && this.lastCursor >= party.length) {
this.lastCursor = party.length - 1;
}
for (const p in party) { for (const p in party) {
const slotIndex = parseInt(p); const slotIndex = parseInt(p);

View File

@ -587,13 +587,13 @@ export default class RunInfoUiHandler extends UiHandler {
const typeText = typeTextColor + typeShadowColor + i18next.t(`pokemonInfo:Type.${typeRule}`)! + "[/color]" + "[/shadow]"; const typeText = typeTextColor + typeShadowColor + i18next.t(`pokemonInfo:Type.${typeRule}`)! + "[/color]" + "[/shadow]";
rules.push(typeText); rules.push(typeText);
break; break;
case Challenges.FRESH_START:
rules.push(i18next.t("challenges:freshStart.name"));
break;
case Challenges.INVERSE_BATTLE: case Challenges.INVERSE_BATTLE:
//
rules.push(i18next.t("challenges:inverseBattle.shortName")); rules.push(i18next.t("challenges:inverseBattle.shortName"));
break; break;
default:
const localisationKey = Challenges[this.runInfo.challenges[i].id].split("_").map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join("");
rules.push(i18next.t(`challenges:${localisationKey}.name`));
break;
} }
} }
} }

View File

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