Merge branch 'beta' into eviolite_weight_bug

This commit is contained in:
Amani H. 2024-11-16 12:47:46 -05:00 committed by GitHub
commit 16673ef50a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
314 changed files with 9282 additions and 2948 deletions

2
global.d.ts vendored
View File

@ -10,5 +10,5 @@ declare global {
* *
* To set up your own server in a test see `game_data.test.ts` * To set up your own server in a test see `game_data.test.ts`
*/ */
var i18nServer: SetupServerApi; var server: SetupServerApi;
} }

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "pokemon-rogue-battle", "name": "pokemon-rogue-battle",
"version": "1.1.6", "version": "1.2.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pokemon-rogue-battle", "name": "pokemon-rogue-battle",
"version": "1.1.6", "version": "1.2.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.1.6", "version": "1.2.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "vite", "start": "vite",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 454 B

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

View File

@ -1647,6 +1647,27 @@
"h": 25 "h": 25
} }
}, },
{
"filename": "85-f",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 40,
"h": 30
},
"spriteSourceSize": {
"x": 5,
"y": 3,
"w": 29,
"h": 25
},
"frame": {
"x": 55,
"y": 270,
"w": 29,
"h": 25
}
},
{ {
"filename": "22s", "filename": "22s",
"rotated": false, "rotated": false,
@ -1731,6 +1752,27 @@
"h": 25 "h": 25
} }
}, },
{
"filename": "85s-f",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 40,
"h": 30
},
"spriteSourceSize": {
"x": 5,
"y": 3,
"w": 29,
"h": 25
},
"frame": {
"x": 56,
"y": 317,
"w": 29,
"h": 25
}
},
{ {
"filename": "9s", "filename": "9s",
"rotated": false, "rotated": false,
@ -6456,6 +6498,27 @@
"h": 18 "h": 18
} }
}, },
{
"filename": "84-f",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 40,
"h": 30
},
"spriteSourceSize": {
"x": 9,
"y": 10,
"w": 21,
"h": 18
},
"frame": {
"x": 98,
"y": 712,
"w": 21,
"h": 18
}
},
{ {
"filename": "107", "filename": "107",
"rotated": false, "rotated": false,
@ -6519,6 +6582,27 @@
"h": 18 "h": 18
} }
}, },
{
"filename": "84s-f",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 40,
"h": 30
},
"spriteSourceSize": {
"x": 9,
"y": 10,
"w": 21,
"h": 18
},
"frame": {
"x": 96,
"y": 770,
"w": 21,
"h": 18
}
},
{ {
"filename": "88", "filename": "88",
"rotated": false, "rotated": false,

View File

@ -786,6 +786,27 @@
"h": 27 "h": 27
} }
}, },
{
"filename": "154-f",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 40,
"h": 30
},
"spriteSourceSize": {
"x": 8,
"y": 1,
"w": 23,
"h": 27
},
"frame": {
"x": 29,
"y": 147,
"w": 23,
"h": 27
}
},
{ {
"filename": "154s", "filename": "154s",
"rotated": false, "rotated": false,
@ -807,6 +828,27 @@
"h": 27 "h": 27
} }
}, },
{
"filename": "154s-f",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 40,
"h": 30
},
"spriteSourceSize": {
"x": 8,
"y": 1,
"w": 23,
"h": 27
},
"frame": {
"x": 29,
"y": 174,
"w": 23,
"h": 27
}
},
{ {
"filename": "229-mega", "filename": "229-mega",
"rotated": false, "rotated": false,

View File

@ -198,6 +198,27 @@
"h": 27 "h": 27
} }
}, },
{
"filename": "257-f-mega",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 40,
"h": 30
},
"spriteSourceSize": {
"x": 4,
"y": 2,
"w": 32,
"h": 27
},
"frame": {
"x": 0,
"y": 79,
"w": 32,
"h": 27
}
},
{ {
"filename": "257s-mega", "filename": "257s-mega",
"rotated": false, "rotated": false,
@ -219,6 +240,27 @@
"h": 27 "h": 27
} }
}, },
{
"filename": "257s-f-mega",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 40,
"h": 30
},
"spriteSourceSize": {
"x": 4,
"y": 2,
"w": 32,
"h": 27
},
"frame": {
"x": 0,
"y": 106,
"w": 32,
"h": 27
}
},
{ {
"filename": "323-mega", "filename": "323-mega",
"rotated": false, "rotated": false,
@ -1248,6 +1290,27 @@
"h": 26 "h": 26
} }
}, },
{
"filename": "257-f",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 40,
"h": 30
},
"spriteSourceSize": {
"x": 7,
"y": 2,
"w": 25,
"h": 26
},
"frame": {
"x": 28,
"y": 556,
"w": 25,
"h": 26
}
},
{ {
"filename": "257s", "filename": "257s",
"rotated": false, "rotated": false,
@ -1269,6 +1332,27 @@
"h": 26 "h": 26
} }
}, },
{
"filename": "257s-f",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 40,
"h": 30
},
"spriteSourceSize": {
"x": 7,
"y": 2,
"w": 25,
"h": 26
},
"frame": {
"x": 28,
"y": 582,
"w": 25,
"h": 26
}
},
{ {
"filename": "359-mega", "filename": "359-mega",
"rotated": false, "rotated": false,
@ -1605,6 +1689,27 @@
"h": 25 "h": 25
} }
}, },
{
"filename": "256-f",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 40,
"h": 30
},
"spriteSourceSize": {
"x": 8,
"y": 3,
"w": 23,
"h": 25
},
"frame": {
"x": 98,
"y": 72,
"w": 23,
"h": 25
}
},
{ {
"filename": "282s-mega", "filename": "282s-mega",
"rotated": false, "rotated": false,
@ -5553,6 +5658,27 @@
"h": 19 "h": 19
} }
}, },
{
"filename": "255-f",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 40,
"h": 30
},
"spriteSourceSize": {
"x": 13,
"y": 9,
"w": 13,
"h": 19
},
"frame": {
"x": 204,
"y": 342,
"w": 13,
"h": 19
}
},
{ {
"filename": "307s", "filename": "307s",
"rotated": false, "rotated": false,

@ -1 +1 @@
Subproject commit 3cf6d553541d79ba165387bc73fb06544d00f1f9 Subproject commit ed1b1df4776ccd4330e8ac1d2f44de611d04c2bc

View File

@ -0,0 +1,17 @@
import type { UserInfo } from "#app/@types/UserInfo";
export interface AccountInfoResponse extends UserInfo {}
export interface AccountLoginRequest {
username: string;
password: string;
}
export interface AccountLoginResponse {
token: string;
}
export interface AccountRegisterRequest {
username: string;
password: string;
}

View File

@ -0,0 +1,31 @@
export interface LinkAccountToDiscordIdRequest {
username: string;
discordId: string;
}
export interface UnlinkAccountFromDiscordIdRequest {
username: string;
discordId: string;
}
export interface LinkAccountToGoogledIdRequest {
username: string;
googleId: string;
}
export interface UnlinkAccountFromGoogledIdRequest {
username: string;
googleId: string;
}
export interface SearchAccountRequest {
username: string;
}
export interface SearchAccountResponse {
username: string;
discordId: string;
googleId: string;
lastLoggedIn: string;
registered: string;
}

View File

@ -0,0 +1,4 @@
export interface TitleStatsResponse {
playerCount: number;
battleCount: number;
}

View File

@ -0,0 +1,10 @@
import type { ScoreboardCategory } from "#app/ui/daily-run-scoreboard";
export interface GetDailyRankingsRequest {
category: ScoreboardCategory;
page?: number;
}
export interface GetDailyRankingsPageCountRequest {
category: ScoreboardCategory;
}

View File

@ -0,0 +1,8 @@
import type { SessionSaveData, SystemSaveData } from "#app/system/game-data";
export interface UpdateAllSavedataRequest {
system: SystemSaveData;
session: SessionSaveData;
sessionSlotId: number;
clientSessionId: string;
}

View File

@ -0,0 +1,40 @@
export class UpdateSessionSavedataRequest {
slot: number;
trainerId: number;
secretId: number;
clientSessionId: string;
}
/** This is **NOT** similar to {@linkcode ClearSessionSavedataRequest} */
export interface NewClearSessionSavedataRequest {
slot: number;
isVictory: boolean;
clientSessionId: string;
}
export interface GetSessionSavedataRequest {
slot: number;
clientSessionId: string;
}
export interface DeleteSessionSavedataRequest {
slot: number;
clientSessionId: string;
}
/** This is **NOT** similar to {@linkcode NewClearSessionSavedataRequest} */
export interface ClearSessionSavedataRequest {
slot: number;
trainerId: number;
clientSessionId: string;
}
/**
* Pokerogue API response for path: `/savedata/session/clear`
*/
export interface ClearSessionSavedataResponse {
/** Contains the error message if any occured */
error?: string;
/** Is `true` if the request was successfully processed */
success?: boolean;
}

View File

@ -0,0 +1,20 @@
import type { SystemSaveData } from "#app/system/game-data";
export interface GetSystemSavedataRequest {
clientSessionId: string;
}
export class UpdateSystemSavedataRequest {
clientSessionId: string;
trainerId?: number;
secretId?: number;
}
export interface VerifySystemSavedataRequest {
clientSessionId: string;
}
export interface VerifySystemSavedataResponse {
valid: boolean;
systemData: SystemSaveData;
}

7
src/@types/UserInfo.ts Normal file
View File

@ -0,0 +1,7 @@
export interface UserInfo {
username: string;
lastSessionSlot: number;
discordId: string;
googleId: string;
hasAdminRole: boolean;
}

View File

@ -1,9 +0,0 @@
/**
* Pokerogue API response for path: `/savedata/session/clear`
*/
export interface PokerogueApiClearSessionData {
/** Contains the error message if any occured */
error?: string;
/** Is `true` if the request was successfully processed */
success?: boolean;
}

View File

@ -1,14 +1,8 @@
import { pokerogueApi } from "#app/plugins/api/pokerogue-api";
import type { UserInfo } from "#app/@types/UserInfo";
import { bypassLogin } from "./battle-scene"; import { bypassLogin } from "./battle-scene";
import * as Utils from "./utils"; import * as Utils from "./utils";
export interface UserInfo {
username: string;
lastSessionSlot: integer;
discordId: string;
googleId: string;
hasAdminRole: boolean;
}
export let loggedInUser: UserInfo | null = null; export let loggedInUser: UserInfo | null = null;
// This is a random string that is used to identify the client session - unique per session (tab or window) so that the game will only save on the one that the server is expecting // This is a random string that is used to identify the client session - unique per session (tab or window) so that the game will only save on the one that the server is expecting
export const clientSessionId = Utils.randomString(32); export const clientSessionId = Utils.randomString(32);
@ -43,18 +37,14 @@ export function updateUserInfo(): Promise<[boolean, integer]> {
}); });
return resolve([ true, 200 ]); return resolve([ true, 200 ]);
} }
Utils.apiFetch("account/info", true).then(response => { pokerogueApi.account.getInfo().then(([ accountInfo, status ]) => {
if (!response.ok) { if (!accountInfo) {
resolve([ false, response.status ]); resolve([ false, status ]);
return; return;
} } else {
return response.json(); loggedInUser = accountInfo;
}).then(jsonResponse => {
loggedInUser = jsonResponse;
resolve([ true, 200 ]); resolve([ true, 200 ]);
}).catch(err => { }
console.error(err);
resolve([ false, 500 ]);
}); });
}); });
} }

View File

@ -5,7 +5,7 @@ import PokemonSpecies, { allSpecies, getPokemonSpecies, PokemonSpeciesFilter } f
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, RememberMoveModifier, 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 "#enums/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";
import { initGameSpeed } from "#app/system/game-speed"; import { initGameSpeed } from "#app/system/game-speed";
@ -13,9 +13,10 @@ import { Arena, ArenaBase } from "#app/field/arena";
import { GameData } from "#app/system/game-data"; import { GameData } from "#app/system/game-data";
import { addTextObject, getTextColor, TextStyle } from "#app/ui/text"; import { addTextObject, getTextColor, TextStyle } from "#app/ui/text";
import { allMoves } from "#app/data/move"; import { allMoves } from "#app/data/move";
import { MusicPreference } from "#app/system/settings/settings";
import { getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getModifierType, getPartyLuckValue, ModifierPoolType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getModifierType, getPartyLuckValue, ModifierPoolType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import AbilityBar from "#app/ui/ability-bar"; import AbilityBar from "#app/ui/ability-bar";
import { allAbilities, applyAbAttrs, applyPostBattleInitAbAttrs, BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, PostBattleInitAbAttr } from "#app/data/ability"; import { allAbilities, applyAbAttrs, applyPostBattleInitAbAttrs, applyPostItemLostAbAttrs, BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, PostBattleInitAbAttr, PostItemLostAbAttr } from "#app/data/ability";
import Battle, { BattleType, FixedBattleConfig } from "#app/battle"; import Battle, { BattleType, FixedBattleConfig } from "#app/battle";
import { GameMode, GameModes, getGameMode } from "#app/game-mode"; import { GameMode, GameModes, getGameMode } from "#app/game-mode";
import FieldSpritePipeline from "#app/pipelines/field-sprite"; import FieldSpritePipeline from "#app/pipelines/field-sprite";
@ -34,10 +35,11 @@ import { Gender } from "#app/data/gender";
import UIPlugin from "phaser3-rex-plugins/templates/ui/ui-plugin"; import UIPlugin from "phaser3-rex-plugins/templates/ui/ui-plugin";
import { addUiThemeOverrides } from "#app/ui/ui-theme"; import { addUiThemeOverrides } from "#app/ui/ui-theme";
import PokemonData from "#app/system/pokemon-data"; import PokemonData from "#app/system/pokemon-data";
import { Nature } from "#app/data/nature"; import { Nature } from "#enums/nature";
import { FormChangeItem, pokemonFormChanges, SpeciesFormChange, SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger } from "#app/data/pokemon-forms"; import { FormChangeItem, pokemonFormChanges, SpeciesFormChange, SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger } from "#app/data/pokemon-forms";
import { FormChangePhase } from "#app/phases/form-change-phase"; import { FormChangePhase } from "#app/phases/form-change-phase";
import { getTypeRgb } from "#app/data/type"; import { getTypeRgb } from "#app/data/type";
import { Type } from "#enums/type";
import PokemonSpriteSparkleHandler from "#app/field/pokemon-sprite-sparkle-handler"; import PokemonSpriteSparkleHandler from "#app/field/pokemon-sprite-sparkle-handler";
import CharSprite from "#app/ui/char-sprite"; import CharSprite from "#app/ui/char-sprite";
import DamageNumberHandler from "#app/field/damage-number-handler"; import DamageNumberHandler from "#app/field/damage-number-handler";
@ -95,7 +97,9 @@ 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 { BattlerTagType } from "#enums/battler-tag-type";
import { FRIENDSHIP_GAIN_FROM_BATTLE } from "#app/data/balance/starters"; import { FRIENDSHIP_GAIN_FROM_BATTLE } from "#app/data/balance/starters";
import { StatusEffect } from "#enums/status-effect";
export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1"; export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1";
@ -169,7 +173,7 @@ export default class BattleScene extends SceneBase {
public uiTheme: UiTheme = UiTheme.DEFAULT; public uiTheme: UiTheme = UiTheme.DEFAULT;
public windowType: integer = 0; public windowType: integer = 0;
public experimentalSprites: boolean = false; public experimentalSprites: boolean = false;
public musicPreference: integer = 0; public musicPreference: number = MusicPreference.MIXED;
public moveAnimations: boolean = true; public moveAnimations: boolean = true;
public expGainsSpeed: ExpGainsSpeed = ExpGainsSpeed.DEFAULT; public expGainsSpeed: ExpGainsSpeed = ExpGainsSpeed.DEFAULT;
public skipSeenDialogues: boolean = false; public skipSeenDialogues: boolean = false;
@ -764,57 +768,65 @@ export default class BattleScene extends SceneBase {
return true; return true;
} }
getParty(): PlayerPokemon[] { public getPlayerParty(): PlayerPokemon[] {
return this.party; return this.party;
} }
getPlayerPokemon(): PlayerPokemon | undefined {
return this.getPlayerField().find(p => p.isActive());
}
/** /**
* Finds the first {@linkcode Pokemon.isActive() | active PlayerPokemon} that isn't also currently switching out * @returns An array of {@linkcode PlayerPokemon} filtered from the player's party
* @returns Either the first {@linkcode PlayerPokemon} satisfying, or undefined if no player pokemon on the field satisfy * that are {@linkcode Pokemon.isAllowedInBattle | allowed in battle}.
*/ */
getNonSwitchedPlayerPokemon(): PlayerPokemon | undefined { public getPokemonAllowedInBattle(): PlayerPokemon[] {
return this.getPlayerField().find(p => p.isActive() && p.switchOutStatus === false); return this.getPlayerParty().filter(p => p.isAllowedInBattle());
} }
/** /**
* Returns an array of PlayerPokemon of length 1 or 2 depending on if double battles or not * @returns The first {@linkcode PlayerPokemon} that is {@linkcode getPlayerField on the field}
* and {@linkcode PlayerPokemon.isActive is active}
* (aka {@linkcode PlayerPokemon.isAllowedInBattle is allowed in battle}),
* or `undefined` if there are no valid pokemon
* @param includeSwitching Whether a pokemon that is currently switching out is valid, default `true`
*/
public getPlayerPokemon(includeSwitching: boolean = true): PlayerPokemon | undefined {
return this.getPlayerField().find(p => p.isActive() && (includeSwitching || p.switchOutStatus === false));
}
/**
* Returns an array of PlayerPokemon of length 1 or 2 depending on if in a double battle or not.
* Does not actually check if the pokemon are on the field or not.
* @returns array of {@linkcode PlayerPokemon} * @returns array of {@linkcode PlayerPokemon}
*/ */
getPlayerField(): PlayerPokemon[] { public getPlayerField(): PlayerPokemon[] {
const party = this.getParty(); const party = this.getPlayerParty();
return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1)); return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1));
} }
getEnemyParty(): EnemyPokemon[] { public getEnemyParty(): EnemyPokemon[] {
return this.currentBattle?.enemyParty ?? []; return this.currentBattle?.enemyParty ?? [];
} }
getEnemyPokemon(): EnemyPokemon | undefined {
return this.getEnemyField().find(p => p.isActive());
}
/** /**
* Finds the first {@linkcode Pokemon.isActive() | active EnemyPokemon} pokemon from the enemy that isn't also currently switching out * @returns The first {@linkcode EnemyPokemon} that is {@linkcode getEnemyField on the field}
* @returns Either the first {@linkcode EnemyPokemon} satisfying, or undefined if no player pokemon on the field satisfy * and {@linkcode EnemyPokemon.isActive is active}
* (aka {@linkcode EnemyPokemon.isAllowedInBattle is allowed in battle}),
* or `undefined` if there are no valid pokemon
* @param includeSwitching Whether a pokemon that is currently switching out is valid, default `true`
*/ */
getNonSwitchedEnemyPokemon(): EnemyPokemon | undefined { public getEnemyPokemon(includeSwitching: boolean = true): EnemyPokemon | undefined {
return this.getEnemyField().find(p => p.isActive() && p.switchOutStatus === false); return this.getEnemyField().find(p => p.isActive() && (includeSwitching || p.switchOutStatus === false));
} }
/** /**
* Returns an array of EnemyPokemon of length 1 or 2 depending on if double battles or not * Returns an array of EnemyPokemon of length 1 or 2 depending on if in a double battle or not.
* Does not actually check if the pokemon are on the field or not.
* @returns array of {@linkcode EnemyPokemon} * @returns array of {@linkcode EnemyPokemon}
*/ */
getEnemyField(): EnemyPokemon[] { public getEnemyField(): EnemyPokemon[] {
const party = this.getEnemyParty(); const party = this.getEnemyParty();
return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1)); return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1));
} }
getField(activeOnly: boolean = false): Pokemon[] { public getField(activeOnly: boolean = false): Pokemon[] {
const ret = new Array(4).fill(null); const ret = new Array(4).fill(null);
const playerField = this.getPlayerField(); const playerField = this.getPlayerField();
const enemyField = this.getEnemyField(); const enemyField = this.getEnemyField();
@ -867,7 +879,7 @@ export default class BattleScene extends SceneBase {
getPokemonById(pokemonId: integer): Pokemon | null { getPokemonById(pokemonId: integer): Pokemon | null {
const findInParty = (party: Pokemon[]) => party.find(p => p.id === pokemonId); const findInParty = (party: Pokemon[]) => party.find(p => p.id === pokemonId);
return (findInParty(this.getParty()) || findInParty(this.getEnemyParty())) ?? null; return (findInParty(this.getPlayerParty()) || findInParty(this.getEnemyParty())) ?? null;
} }
addPlayerPokemon(species: PokemonSpecies, level: integer, abilityIndex?: integer, formIndex?: integer, gender?: Gender, shiny?: boolean, variant?: Variant, ivs?: integer[], nature?: Nature, dataSource?: Pokemon | PokemonData, postProcess?: (playerPokemon: PlayerPokemon) => void): PlayerPokemon { addPlayerPokemon(species: PokemonSpecies, level: integer, abilityIndex?: integer, formIndex?: integer, gender?: Gender, shiny?: boolean, variant?: Variant, ivs?: integer[], nature?: Nature, dataSource?: Pokemon | PokemonData, postProcess?: (playerPokemon: PlayerPokemon) => void): PlayerPokemon {
@ -1062,7 +1074,7 @@ export default class BattleScene extends SceneBase {
this.modifierBar.removeAll(true); this.modifierBar.removeAll(true);
this.enemyModifierBar.removeAll(true); this.enemyModifierBar.removeAll(true);
for (const p of this.getParty()) { for (const p of this.getPlayerParty()) {
p.destroy(); p.destroy();
} }
this.party = []; this.party = [];
@ -1221,33 +1233,64 @@ export default class BattleScene extends SceneBase {
newDouble = !!double; newDouble = !!double;
} }
if (Overrides.BATTLE_TYPE_OVERRIDE === "double") { // Disable double battles on Endless/Endless Spliced Wave 50x boss battles (Introduced 1.2.0)
newDouble = true; if (this.gameMode.isEndlessBoss(newWaveIndex)) {
}
/* Override battles into single only if not fighting with trainers */
if (newBattleType !== BattleType.TRAINER && Overrides.BATTLE_TYPE_OVERRIDE === "single") {
newDouble = false; newDouble = false;
} }
const lastBattle = this.currentBattle; if (!isNullOrUndefined(Overrides.BATTLE_TYPE_OVERRIDE)) {
let doubleOverrideForWave: "single" | "double" | null = null;
if (lastBattle?.double && !newDouble) { switch (Overrides.BATTLE_TYPE_OVERRIDE) {
this.tryRemovePhase(p => p instanceof SwitchPhase); case "double":
doubleOverrideForWave = "double";
break;
case "single":
doubleOverrideForWave = "single";
break;
case "even-doubles":
doubleOverrideForWave = (newWaveIndex % 2) ? "single" : "double";
break;
case "odd-doubles":
doubleOverrideForWave = (newWaveIndex % 2) ? "double" : "single";
break;
} }
if (doubleOverrideForWave === "double") {
newDouble = true;
}
/**
* Override battles into single only if not fighting with trainers.
* @see {@link https://github.com/pagefaultgames/pokerogue/issues/1948 | GitHub Issue #1948}
*/
if (newBattleType !== BattleType.TRAINER && doubleOverrideForWave === "single") {
newDouble = false;
}
}
const lastBattle = this.currentBattle;
const maxExpLevel = this.getMaxExpLevel(); const maxExpLevel = this.getMaxExpLevel();
this.lastEnemyTrainer = lastBattle?.trainer ?? null; this.lastEnemyTrainer = lastBattle?.trainer ?? null;
this.lastMysteryEncounter = lastBattle?.mysteryEncounter; this.lastMysteryEncounter = lastBattle?.mysteryEncounter;
if (newBattleType === BattleType.MYSTERY_ENCOUNTER) {
// Disable double battle on mystery encounters (it may be re-enabled as part of encounter)
newDouble = false;
}
if (lastBattle?.double && !newDouble) {
this.tryRemovePhase(p => p instanceof SwitchPhase);
this.getPlayerField().forEach(p => p.lapseTag(BattlerTagType.COMMANDED));
}
this.executeWithSeedOffset(() => { this.executeWithSeedOffset(() => {
this.currentBattle = new Battle(this.gameMode, newWaveIndex, newBattleType, newTrainer, newDouble); this.currentBattle = new Battle(this.gameMode, newWaveIndex, newBattleType, newTrainer, newDouble);
}, newWaveIndex << 3, this.waveSeed); }, newWaveIndex << 3, this.waveSeed);
this.currentBattle.incrementTurn(this); this.currentBattle.incrementTurn(this);
if (newBattleType === BattleType.MYSTERY_ENCOUNTER) { if (newBattleType === BattleType.MYSTERY_ENCOUNTER) {
// Disable double battle on mystery encounters (it may be re-enabled as part of encounter)
this.currentBattle.double = false;
// Will generate the actual Mystery Encounter during NextEncounterPhase, to ensure it uses proper biome // Will generate the actual Mystery Encounter during NextEncounterPhase, to ensure it uses proper biome
this.currentBattle.mysteryEncounterType = mysteryEncounterType; this.currentBattle.mysteryEncounterType = mysteryEncounterType;
} }
@ -1269,13 +1312,15 @@ export default class BattleScene extends SceneBase {
if (resetArenaState) { if (resetArenaState) {
this.arena.resetArenaEffects(); this.arena.resetArenaEffects();
playerField.forEach((pokemon) => pokemon.lapseTag(BattlerTagType.COMMANDED));
playerField.forEach((pokemon, p) => { playerField.forEach((pokemon, p) => {
if (pokemon.isOnField()) { if (pokemon.isOnField()) {
this.pushPhase(new ReturnPhase(this, p)); this.pushPhase(new ReturnPhase(this, p));
} }
}); });
for (const pokemon of this.getParty()) { for (const pokemon of this.getPlayerParty()) {
pokemon.resetBattleData(); pokemon.resetBattleData();
applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon); applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon);
} }
@ -1285,7 +1330,7 @@ export default class BattleScene extends SceneBase {
} }
} }
for (const pokemon of this.getParty()) { for (const pokemon of this.getPlayerParty()) {
this.triggerPokemonFormChange(pokemon, SpeciesFormChangeTimeOfDayTrigger); this.triggerPokemonFormChange(pokemon, SpeciesFormChangeTimeOfDayTrigger);
} }
@ -1379,10 +1424,19 @@ export default class BattleScene extends SceneBase {
case Species.PALDEA_TAUROS: case Species.PALDEA_TAUROS:
return Utils.randSeedInt(species.forms.length); return Utils.randSeedInt(species.forms.length);
case Species.PIKACHU: case Species.PIKACHU:
if (this.currentBattle?.battleType === BattleType.TRAINER && this.currentBattle?.waveIndex < 30) {
return 0; // Ban Cosplay and Partner Pika from Trainers before wave 30
}
return Utils.randSeedInt(8); return Utils.randSeedInt(8);
case Species.EEVEE: case Species.EEVEE:
if (this.currentBattle?.battleType === BattleType.TRAINER && this.currentBattle?.waveIndex < 30) {
return 0; // No Partner Eevee for Wave 12 Preschoolers
}
return Utils.randSeedInt(2); return Utils.randSeedInt(2);
case Species.GRENINJA: case Species.GRENINJA:
if (this.currentBattle?.battleType === BattleType.TRAINER) {
return 0; // Don't give trainers Battle Bond Greninja
}
return Utils.randSeedInt(2); return Utils.randSeedInt(2);
case Species.ZYGARDE: case Species.ZYGARDE:
return Utils.randSeedInt(4); return Utils.randSeedInt(4);
@ -1480,7 +1534,7 @@ export default class BattleScene extends SceneBase {
} }
trySpreadPokerus(): void { trySpreadPokerus(): void {
const party = this.getParty(); const party = this.getPlayerParty();
const infectedIndexes: integer[] = []; const infectedIndexes: integer[] = [];
const spread = (index: number, spreadTo: number) => { const spread = (index: number, spreadTo: number) => {
const partyMember = party[index + spreadTo]; const partyMember = party[index + spreadTo];
@ -1677,7 +1731,7 @@ export default class BattleScene extends SceneBase {
updateAndShowText(duration: number): void { updateAndShowText(duration: number): void {
const labels = [ this.luckLabelText, this.luckText ]; const labels = [ this.luckLabelText, this.luckText ];
labels.forEach(t => t.setAlpha(0)); labels.forEach(t => t.setAlpha(0));
const luckValue = getPartyLuckValue(this.getParty()); const luckValue = getPartyLuckValue(this.getPlayerParty());
this.luckText.setText(getLuckString(luckValue)); this.luckText.setText(getLuckString(luckValue));
if (luckValue < 14) { if (luckValue < 14) {
this.luckText.setTint(getLuckTextTint(luckValue)); this.luckText.setTint(getLuckTextTint(luckValue));
@ -2554,14 +2608,15 @@ export default class BattleScene extends SceneBase {
* The quantity to transfer is automatically capped at how much the recepient can take before reaching the maximum stack size for the item. * The quantity to transfer is automatically capped at how much the recepient can take before reaching the maximum stack size for the item.
* A transfer that moves a quantity smaller than what is specified in the transferQuantity parameter is still considered successful. * A transfer that moves a quantity smaller than what is specified in the transferQuantity parameter is still considered successful.
* @param itemModifier {@linkcode PokemonHeldItemModifier} item to transfer (represents the whole stack) * @param itemModifier {@linkcode PokemonHeldItemModifier} item to transfer (represents the whole stack)
* @param target {@linkcode Pokemon} pokemon recepient in this transfer * @param target {@linkcode Pokemon} recepient in this transfer
* @param playSound {boolean} * @param playSound `true` to play a sound when transferring the item
* @param transferQuantity {@linkcode integer} how many items of the stack to transfer. Optional, defaults to 1 * @param transferQuantity How many items of the stack to transfer. Optional, defaults to `1`
* @param instant {boolean} * @param instant ??? (Optional)
* @param ignoreUpdate {boolean} * @param ignoreUpdate ??? (Optional)
* @returns true if the transfer was successful * @param itemLost If `true`, treat the item's current holder as losing the item (for now, this simply enables Unburden). Default is `true`.
* @returns `true` if the transfer was successful
*/ */
tryTransferHeldItemModifier(itemModifier: PokemonHeldItemModifier, target: Pokemon, playSound: boolean, transferQuantity: integer = 1, instant?: boolean, ignoreUpdate?: boolean): Promise<boolean> { tryTransferHeldItemModifier(itemModifier: PokemonHeldItemModifier, target: Pokemon, playSound: boolean, transferQuantity: number = 1, instant?: boolean, ignoreUpdate?: boolean, itemLost: boolean = true): Promise<boolean> {
return new Promise(resolve => { return new Promise(resolve => {
const source = itemModifier.pokemonId ? itemModifier.getPokemon(target.scene) : null; const source = itemModifier.pokemonId ? itemModifier.getPokemon(target.scene) : null;
const cancelled = new Utils.BooleanHolder(false); const cancelled = new Utils.BooleanHolder(false);
@ -2593,9 +2648,19 @@ export default class BattleScene extends SceneBase {
const addModifier = () => { const addModifier = () => {
if (!matchingModifier || this.removeModifier(matchingModifier, !target.isPlayer())) { if (!matchingModifier || this.removeModifier(matchingModifier, !target.isPlayer())) {
if (target.isPlayer()) { if (target.isPlayer()) {
this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant).then(() => resolve(true)); this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant).then(() => {
if (source && itemLost) {
applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false);
}
resolve(true);
});
} else { } else {
this.addEnemyModifier(newItemModifier, ignoreUpdate, instant).then(() => resolve(true)); this.addEnemyModifier(newItemModifier, ignoreUpdate, instant).then(() => {
if (source && itemLost) {
applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false);
}
resolve(true);
});
} }
} else { } else {
resolve(false); resolve(false);
@ -2615,7 +2680,7 @@ export default class BattleScene extends SceneBase {
removePartyMemberModifiers(partyMemberIndex: integer): Promise<void> { removePartyMemberModifiers(partyMemberIndex: integer): Promise<void> {
return new Promise(resolve => { return new Promise(resolve => {
const pokemonId = this.getParty()[partyMemberIndex].id; const pokemonId = this.getPlayerParty()[partyMemberIndex].id;
const modifiersToRemove = this.modifiers.filter(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).pokemonId === pokemonId); const modifiersToRemove = this.modifiers.filter(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).pokemonId === pokemonId);
for (const m of modifiersToRemove) { for (const m of modifiersToRemove) {
this.modifiers.splice(this.modifiers.indexOf(m), 1); this.modifiers.splice(this.modifiers.indexOf(m), 1);
@ -2742,7 +2807,7 @@ export default class BattleScene extends SceneBase {
} }
} }
this.updatePartyForModifiers(player ? this.getParty() : this.getEnemyParty(), instant).then(() => { this.updatePartyForModifiers(player ? this.getPlayerParty() : this.getEnemyParty(), instant).then(() => {
(player ? this.modifierBar : this.enemyModifierBar).updateModifiers(modifiers); (player ? this.modifierBar : this.enemyModifierBar).updateModifiers(modifiers);
if (!player) { if (!player) {
this.updateUIPositions(); this.updateUIPositions();
@ -2763,7 +2828,15 @@ export default class BattleScene extends SceneBase {
}); });
} }
removeModifier(modifier: PersistentModifier, enemy?: boolean): boolean { /**
* Removes a currently owned item. If the item is stacked, the entire item stack
* gets removed. This function does NOT apply in-battle effects, such as Unburden.
* If in-battle effects are needed, use {@linkcode Pokemon.loseHeldItem} instead.
* @param modifier The item to be removed.
* @param enemy If `true`, remove an item owned by the enemy. If `false`, remove an item owned by the player. Default is `false`.
* @returns `true` if the item exists and was successfully removed, `false` otherwise.
*/
removeModifier(modifier: PersistentModifier, enemy: boolean = false): boolean {
const modifiers = !enemy ? this.modifiers : this.enemyModifiers; const modifiers = !enemy ? this.modifiers : this.enemyModifiers;
const modifierIndex = modifiers.indexOf(modifier); const modifierIndex = modifiers.indexOf(modifier);
if (modifierIndex > -1) { if (modifierIndex > -1) {
@ -2960,12 +3033,21 @@ export default class BattleScene extends SceneBase {
updateGameInfo(): void { updateGameInfo(): void {
const gameInfo = { const gameInfo = {
playTime: this.sessionPlayTime ? this.sessionPlayTime : 0, playTime: this.sessionPlayTime ?? 0,
gameMode: this.currentBattle ? this.gameMode.getName() : "Title", gameMode: this.currentBattle ? this.gameMode.getName() : "Title",
biome: this.currentBattle ? getBiomeName(this.arena.biomeType) : "", biome: this.currentBattle ? getBiomeName(this.arena.biomeType) : "",
wave: this.currentBattle?.waveIndex || 0, wave: this.currentBattle?.waveIndex ?? 0,
party: this.party ? this.party.map(p => { party: this.party ? this.party.map((p) => {
return { name: p.name, level: p.level }; return {
name: p.name,
form: p.getFormKey(),
types: p.getTypes().map((type) => Type[type]),
teraType: p.getTeraType() !== Type.UNKNOWN ? Type[p.getTeraType()] : "",
level: p.level,
currentHP: p.hp,
maxHP: p.getMaxHp(),
status: p.status?.effect ? StatusEffect[p.status.effect] : ""
};
}) : [], }) : [],
modeChain: this.ui?.getModeChain() ?? [], modeChain: this.ui?.getModeChain() ?? [],
}; };
@ -2980,22 +3062,16 @@ export default class BattleScene extends SceneBase {
*/ */
getActiveKeys(): string[] { getActiveKeys(): string[] {
const keys: string[] = []; const keys: string[] = [];
const playerParty = this.getParty(); let activePokemon: (PlayerPokemon | EnemyPokemon)[] = this.getPlayerParty();
playerParty.forEach(p => { activePokemon = activePokemon.concat(this.getEnemyParty());
activePokemon.forEach((p) => {
keys.push(p.getSpriteKey(true)); keys.push(p.getSpriteKey(true));
if (p instanceof PlayerPokemon) {
keys.push(p.getBattleSpriteKey(true, true)); keys.push(p.getBattleSpriteKey(true, true));
keys.push("cry/" + p.species.getCryKey(p.formIndex));
if (p.fusionSpecies) {
keys.push("cry/" + p.fusionSpecies.getCryKey(p.fusionFormIndex));
} }
}); keys.push(p.species.getCryKey(p.formIndex));
// enemyParty has to be operated on separately from playerParty because playerPokemon =/= enemyPokemon
const enemyParty = this.getEnemyParty();
enemyParty.forEach(p => {
keys.push(p.getSpriteKey(true));
keys.push("cry/" + p.species.getCryKey(p.formIndex));
if (p.fusionSpecies) { if (p.fusionSpecies) {
keys.push("cry/" + p.fusionSpecies.getCryKey(p.fusionFormIndex)); keys.push(p.fusionSpecies.getCryKey(p.fusionFormIndex));
} }
}); });
return keys; return keys;
@ -3016,7 +3092,7 @@ export default class BattleScene extends SceneBase {
this.setFieldScale(0.75); this.setFieldScale(0.75);
this.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger, false); this.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger, false);
this.currentBattle.double = true; this.currentBattle.double = true;
const availablePartyMembers = this.getParty().filter((p) => p.isAllowedInBattle()); const availablePartyMembers = this.getPlayerParty().filter((p) => p.isAllowedInBattle());
if (availablePartyMembers.length > 1) { if (availablePartyMembers.length > 1) {
this.pushPhase(new ToggleDoublePositionPhase(this, true)); this.pushPhase(new ToggleDoublePositionPhase(this, true));
if (!availablePartyMembers[1].isOnField()) { if (!availablePartyMembers[1].isOnField()) {
@ -3041,7 +3117,7 @@ export default class BattleScene extends SceneBase {
*/ */
applyPartyExp(expValue: number, pokemonDefeated: boolean, useWaveIndexMultiplier?: boolean, pokemonParticipantIds?: Set<number>): void { applyPartyExp(expValue: number, pokemonDefeated: boolean, useWaveIndexMultiplier?: boolean, pokemonParticipantIds?: Set<number>): void {
const participantIds = pokemonParticipantIds ?? this.currentBattle.playerParticipantIds; const participantIds = pokemonParticipantIds ?? this.currentBattle.playerParticipantIds;
const party = this.getParty(); const party = this.getPlayerParty();
const expShareModifier = this.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier; const expShareModifier = this.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier;
const expBalanceModifier = this.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier; const expBalanceModifier = this.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier;
const multipleParticipantExpBonusModifier = this.findModifier(m => m instanceof MultipleParticipantExpBonusModifier) as MultipleParticipantExpBonusModifier; const multipleParticipantExpBonusModifier = this.findModifier(m => m instanceof MultipleParticipantExpBonusModifier) as MultipleParticipantExpBonusModifier;

View File

@ -4,13 +4,15 @@ import * as Utils from "./utils";
import Trainer, { TrainerVariant } from "./field/trainer"; import Trainer, { TrainerVariant } from "./field/trainer";
import { GameMode } from "./game-mode"; import { GameMode } from "./game-mode";
import { MoneyMultiplierModifier, PokemonHeldItemModifier } from "./modifier/modifier"; import { MoneyMultiplierModifier, PokemonHeldItemModifier } from "./modifier/modifier";
import { PokeballType } from "./data/pokeball"; import { PokeballType } from "#enums/pokeball";
import { trainerConfigs } from "#app/data/trainer-config"; import { trainerConfigs } from "#app/data/trainer-config";
import { SpeciesFormKey } from "#enums/species-form-key";
import Pokemon, { EnemyPokemon, PlayerPokemon, QueuedMove } from "#app/field/pokemon"; import Pokemon, { EnemyPokemon, PlayerPokemon, QueuedMove } from "#app/field/pokemon";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { BattleSpec } from "#enums/battle-spec"; import { BattleSpec } from "#enums/battle-spec";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
import { MusicPreference } from "#app/system/settings/settings";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import i18next from "#app/plugins/i18n"; import i18next from "#app/plugins/i18n";
@ -212,7 +214,6 @@ export default class Battle {
} }
getBgmOverride(scene: BattleScene): string | null { getBgmOverride(scene: BattleScene): string | null {
const battlers = this.enemyParty.slice(0, this.getBattlerCount());
if (this.isBattleMysteryEncounter() && this.mysteryEncounter?.encounterMode === MysteryEncounterMode.DEFAULT) { if (this.isBattleMysteryEncounter() && this.mysteryEncounter?.encounterMode === MysteryEncounterMode.DEFAULT) {
// Music is overridden for MEs during ME onInit() // Music is overridden for MEs during ME onInit()
// Should not use any BGM overrides before swapping from DEFAULT mode // Should not use any BGM overrides before swapping from DEFAULT mode
@ -221,7 +222,7 @@ export default class Battle {
if (!this.started && this.trainer?.config.encounterBgm && this.trainer?.getEncounterMessages()?.length) { if (!this.started && this.trainer?.config.encounterBgm && this.trainer?.getEncounterMessages()?.length) {
return `encounter_${this.trainer?.getEncounterBgm()}`; return `encounter_${this.trainer?.getEncounterBgm()}`;
} }
if (scene.musicPreference === 0) { if (scene.musicPreference === MusicPreference.CONSISTENT) {
return this.trainer?.getBattleBgm() ?? null; return this.trainer?.getBattleBgm() ?? null;
} else { } else {
return this.trainer?.getMixedBattleBgm() ?? null; return this.trainer?.getMixedBattleBgm() ?? null;
@ -229,143 +230,163 @@ export default class Battle {
} else if (this.gameMode.isClassic && this.waveIndex > 195 && this.battleSpec !== BattleSpec.FINAL_BOSS) { } else if (this.gameMode.isClassic && this.waveIndex > 195 && this.battleSpec !== BattleSpec.FINAL_BOSS) {
return "end_summit"; return "end_summit";
} }
for (const pokemon of battlers) { const wildOpponents = scene.getEnemyParty();
for (const pokemon of wildOpponents) {
if (this.battleSpec === BattleSpec.FINAL_BOSS) { if (this.battleSpec === BattleSpec.FINAL_BOSS) {
if (pokemon.formIndex) { if (pokemon.species.getFormSpriteKey(pokemon.formIndex) === SpeciesFormKey.ETERNAMAX) {
return "battle_final"; return "battle_final";
} }
return "battle_final_encounter"; return "battle_final_encounter";
} }
if (pokemon.species.legendary || pokemon.species.subLegendary || pokemon.species.mythical) { if (pokemon.species.legendary || pokemon.species.subLegendary || pokemon.species.mythical) {
if (scene.musicPreference === 0) { if (scene.musicPreference === MusicPreference.CONSISTENT) {
if (pokemon.species.speciesId === Species.REGIROCK || pokemon.species.speciesId === Species.REGICE || pokemon.species.speciesId === Species.REGISTEEL || pokemon.species.speciesId === Species.REGIGIGAS || pokemon.species.speciesId === Species.REGIELEKI || pokemon.species.speciesId === Species.REGIDRAGO) { switch (pokemon.species.speciesId) {
case Species.REGIROCK:
case Species.REGICE:
case Species.REGISTEEL:
case Species.REGIGIGAS:
case Species.REGIDRAGO:
case Species.REGIELEKI:
return "battle_legendary_regis_g5"; return "battle_legendary_regis_g5";
} case Species.KYUREM:
if (pokemon.species.speciesId === Species.COBALION || pokemon.species.speciesId === Species.TERRAKION || pokemon.species.speciesId === Species.VIRIZION || pokemon.species.speciesId === Species.TORNADUS || pokemon.species.speciesId === Species.THUNDURUS || pokemon.species.speciesId === Species.LANDORUS || pokemon.species.speciesId === Species.KELDEO || pokemon.species.speciesId === Species.MELOETTA || pokemon.species.speciesId === Species.GENESECT) {
return "battle_legendary_unova";
}
if (pokemon.species.speciesId === Species.KYUREM) {
return "battle_legendary_kyurem"; return "battle_legendary_kyurem";
} default:
if (pokemon.species.legendary) { if (pokemon.species.legendary) {
return "battle_legendary_res_zek"; return "battle_legendary_res_zek";
} }
return "battle_legendary_unova"; return "battle_legendary_unova";
} else { }
if (pokemon.species.speciesId === Species.ARTICUNO || pokemon.species.speciesId === Species.ZAPDOS || pokemon.species.speciesId === Species.MOLTRES || pokemon.species.speciesId === Species.MEWTWO || pokemon.species.speciesId === Species.MEW) { } else if (scene.musicPreference === MusicPreference.MIXED) {
switch (pokemon.species.speciesId) {
case Species.ARTICUNO:
case Species.ZAPDOS:
case Species.MOLTRES:
case Species.MEWTWO:
case Species.MEW:
return "battle_legendary_kanto"; return "battle_legendary_kanto";
} case Species.RAIKOU:
if (pokemon.species.speciesId === Species.RAIKOU) {
return "battle_legendary_raikou"; return "battle_legendary_raikou";
} case Species.ENTEI:
if (pokemon.species.speciesId === Species.ENTEI) {
return "battle_legendary_entei"; return "battle_legendary_entei";
} case Species.SUICUNE:
if (pokemon.species.speciesId === Species.SUICUNE) {
return "battle_legendary_suicune"; return "battle_legendary_suicune";
} case Species.LUGIA:
if (pokemon.species.speciesId === Species.LUGIA) {
return "battle_legendary_lugia"; return "battle_legendary_lugia";
} case Species.HO_OH:
if (pokemon.species.speciesId === Species.HO_OH) {
return "battle_legendary_ho_oh"; return "battle_legendary_ho_oh";
} case Species.REGIROCK:
if (pokemon.species.speciesId === Species.REGIROCK || pokemon.species.speciesId === Species.REGICE || pokemon.species.speciesId === Species.REGISTEEL || pokemon.species.speciesId === Species.REGIGIGAS || pokemon.species.speciesId === Species.REGIELEKI || pokemon.species.speciesId === Species.REGIDRAGO) { case Species.REGICE:
case Species.REGISTEEL:
case Species.REGIGIGAS:
case Species.REGIDRAGO:
case Species.REGIELEKI:
return "battle_legendary_regis_g6"; return "battle_legendary_regis_g6";
} case Species.GROUDON:
if (pokemon.species.speciesId === Species.GROUDON || pokemon.species.speciesId === Species.KYOGRE) { case Species.KYOGRE:
return "battle_legendary_gro_kyo"; return "battle_legendary_gro_kyo";
} case Species.RAYQUAZA:
if (pokemon.species.speciesId === Species.RAYQUAZA) {
return "battle_legendary_rayquaza"; return "battle_legendary_rayquaza";
} case Species.DEOXYS:
if (pokemon.species.speciesId === Species.DEOXYS) {
return "battle_legendary_deoxys"; return "battle_legendary_deoxys";
} case Species.UXIE:
if (pokemon.species.speciesId === Species.UXIE || pokemon.species.speciesId === Species.MESPRIT || pokemon.species.speciesId === Species.AZELF) { case Species.MESPRIT:
case Species.AZELF:
return "battle_legendary_lake_trio"; return "battle_legendary_lake_trio";
} case Species.HEATRAN:
if (pokemon.species.speciesId === Species.HEATRAN || pokemon.species.speciesId === Species.CRESSELIA || pokemon.species.speciesId === Species.DARKRAI || pokemon.species.speciesId === Species.SHAYMIN) { case Species.CRESSELIA:
case Species.DARKRAI:
case Species.SHAYMIN:
return "battle_legendary_sinnoh"; return "battle_legendary_sinnoh";
} case Species.DIALGA:
if (pokemon.species.speciesId === Species.DIALGA || pokemon.species.speciesId === Species.PALKIA) { case Species.PALKIA:
if (pokemon.getFormKey() === "") { if (pokemon.species.getFormSpriteKey(pokemon.formIndex) === SpeciesFormKey.ORIGIN) {
return "battle_legendary_dia_pal";
}
if (pokemon.getFormKey() === "origin") {
return "battle_legendary_origin_forme"; return "battle_legendary_origin_forme";
} }
} return "battle_legendary_dia_pal";
if (pokemon.species.speciesId === Species.GIRATINA) { case Species.GIRATINA:
return "battle_legendary_giratina"; return "battle_legendary_giratina";
} case Species.ARCEUS:
if (pokemon.species.speciesId === Species.ARCEUS) {
return "battle_legendary_arceus"; return "battle_legendary_arceus";
} case Species.COBALION:
if (pokemon.species.speciesId === Species.COBALION || pokemon.species.speciesId === Species.TERRAKION || pokemon.species.speciesId === Species.VIRIZION || pokemon.species.speciesId === Species.TORNADUS || pokemon.species.speciesId === Species.THUNDURUS || pokemon.species.speciesId === Species.LANDORUS || pokemon.species.speciesId === Species.KELDEO || pokemon.species.speciesId === Species.MELOETTA || pokemon.species.speciesId === Species.GENESECT) { case Species.TERRAKION:
case Species.VIRIZION:
case Species.KELDEO:
case Species.TORNADUS:
case Species.LANDORUS:
case Species.THUNDURUS:
case Species.MELOETTA:
case Species.GENESECT:
return "battle_legendary_unova"; return "battle_legendary_unova";
} case Species.KYUREM:
if (pokemon.species.speciesId === Species.KYUREM) {
return "battle_legendary_kyurem"; return "battle_legendary_kyurem";
} case Species.XERNEAS:
if (pokemon.species.speciesId === Species.XERNEAS || pokemon.species.speciesId === Species.YVELTAL || pokemon.species.speciesId === Species.ZYGARDE) { case Species.YVELTAL:
case Species.ZYGARDE:
return "battle_legendary_xern_yvel"; return "battle_legendary_xern_yvel";
} case Species.TAPU_KOKO:
if (pokemon.species.speciesId === Species.TAPU_KOKO || pokemon.species.speciesId === Species.TAPU_LELE || pokemon.species.speciesId === Species.TAPU_BULU || pokemon.species.speciesId === Species.TAPU_FINI) { case Species.TAPU_LELE:
case Species.TAPU_BULU:
case Species.TAPU_FINI:
return "battle_legendary_tapu"; return "battle_legendary_tapu";
} case Species.SOLGALEO:
if ([ Species.COSMOG, Species.COSMOEM, Species.SOLGALEO, Species.LUNALA ].includes(pokemon.species.speciesId)) { case Species.LUNALA:
return "battle_legendary_sol_lun"; return "battle_legendary_sol_lun";
} case Species.NECROZMA:
if (pokemon.species.speciesId === Species.NECROZMA) { switch (pokemon.getFormKey()) {
if (pokemon.getFormKey() === "") { case "dusk-mane":
return "battle_legendary_sol_lun"; case "dawn-wings":
}
if (pokemon.getFormKey() === "dusk-mane" || pokemon.getFormKey() === "dawn-wings") {
return "battle_legendary_dusk_dawn"; return "battle_legendary_dusk_dawn";
} case "ultra":
if (pokemon.getFormKey() === "ultra") {
return "battle_legendary_ultra_nec"; return "battle_legendary_ultra_nec";
default:
return "battle_legendary_sol_lun";
} }
} case Species.NIHILEGO:
if ([ Species.NIHILEGO, Species.BUZZWOLE, Species.PHEROMOSA, Species.XURKITREE, Species.CELESTEELA, Species.KARTANA, Species.GUZZLORD, Species.POIPOLE, Species.NAGANADEL, Species.STAKATAKA, Species.BLACEPHALON ].includes(pokemon.species.speciesId)) { case Species.PHEROMOSA:
case Species.BUZZWOLE:
case Species.XURKITREE:
case Species.CELESTEELA:
case Species.KARTANA:
case Species.GUZZLORD:
case Species.POIPOLE:
case Species.NAGANADEL:
case Species.STAKATAKA:
case Species.BLACEPHALON:
return "battle_legendary_ub"; return "battle_legendary_ub";
} case Species.ZACIAN:
if (pokemon.species.speciesId === Species.ZACIAN || pokemon.species.speciesId === Species.ZAMAZENTA) { case Species.ZAMAZENTA:
return "battle_legendary_zac_zam"; return "battle_legendary_zac_zam";
} case Species.GLASTRIER:
if (pokemon.species.speciesId === Species.GLASTRIER || pokemon.species.speciesId === Species.SPECTRIER) { case Species.SPECTRIER:
return "battle_legendary_glas_spec"; return "battle_legendary_glas_spec";
} case Species.CALYREX:
if (pokemon.species.speciesId === Species.CALYREX) {
if (pokemon.getFormKey() === "") {
return "battle_legendary_calyrex";
}
if (pokemon.getFormKey() === "ice" || pokemon.getFormKey() === "shadow") { if (pokemon.getFormKey() === "ice" || pokemon.getFormKey() === "shadow") {
return "battle_legendary_riders"; return "battle_legendary_riders";
} }
} return "battle_legendary_calyrex";
if (pokemon.species.speciesId === Species.GALAR_ARTICUNO || pokemon.species.speciesId === Species.GALAR_ZAPDOS || pokemon.species.speciesId === Species.GALAR_MOLTRES) { case Species.GALAR_ARTICUNO:
case Species.GALAR_ZAPDOS:
case Species.GALAR_MOLTRES:
return "battle_legendary_birds_galar"; return "battle_legendary_birds_galar";
} case Species.WO_CHIEN:
if (pokemon.species.speciesId === Species.WO_CHIEN || pokemon.species.speciesId === Species.CHIEN_PAO || pokemon.species.speciesId === Species.TING_LU || pokemon.species.speciesId === Species.CHI_YU) { case Species.CHIEN_PAO:
case Species.TING_LU:
case Species.CHI_YU:
return "battle_legendary_ruinous"; return "battle_legendary_ruinous";
} case Species.KORAIDON:
if (pokemon.species.speciesId === Species.KORAIDON || pokemon.species.speciesId === Species.MIRAIDON) { case Species.MIRAIDON:
return "battle_legendary_kor_mir"; return "battle_legendary_kor_mir";
} case Species.OKIDOGI:
if (pokemon.species.speciesId === Species.OKIDOGI || pokemon.species.speciesId === Species.MUNKIDORI || pokemon.species.speciesId === Species.FEZANDIPITI) { case Species.MUNKIDORI:
case Species.FEZANDIPITI:
return "battle_legendary_loyal_three"; return "battle_legendary_loyal_three";
} case Species.OGERPON:
if (pokemon.species.speciesId === Species.OGERPON) {
return "battle_legendary_ogerpon"; return "battle_legendary_ogerpon";
} case Species.TERAPAGOS:
if (pokemon.species.speciesId === Species.TERAPAGOS) {
return "battle_legendary_terapagos"; return "battle_legendary_terapagos";
} case Species.PECHARUNT:
if (pokemon.species.speciesId === Species.PECHARUNT) {
return "battle_legendary_pecharunt"; return "battle_legendary_pecharunt";
} default:
if (pokemon.species.legendary) { if (pokemon.species.legendary) {
return "battle_legendary_res_zek"; return "battle_legendary_res_zek";
} }
@ -373,6 +394,7 @@ export default class Battle {
} }
} }
} }
}
if (scene.gameMode.isClassic && this.waveIndex <= 4) { if (scene.gameMode.isClassic && this.waveIndex <= 4) {
return "battle_wild"; return "battle_wild";

View File

@ -3,3 +3,9 @@ export const PLAYER_PARTY_MAX_SIZE: number = 6;
/** Whether to use seasonal splash messages in general */ /** Whether to use seasonal splash messages in general */
export const USE_SEASONAL_SPLASH_MESSAGES: boolean = false; export const USE_SEASONAL_SPLASH_MESSAGES: boolean = false;
/** Name of the session ID cookie */
export const SESSION_ID_COOKIE_NAME: string = "pokerogue_sessionId";
/** Max value for an integer attribute in {@linkcode SystemSaveData} */
export const MAX_INT_ATTR_VALUE = 0x80000000;

View File

@ -1,15 +1,15 @@
import Pokemon, { HitResult, PlayerPokemon, PokemonMove } from "../field/pokemon"; import Pokemon, { EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove } from "../field/pokemon";
import { Type } from "./type"; import { Type } from "#enums/type";
import { Constructor } from "#app/utils"; import { Constructor } from "#app/utils";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
import { Weather, WeatherType } from "./weather"; import { Weather } from "#app/data/weather";
import { BattlerTag, GroundedTag } from "./battler-tags"; import { BattlerTag, BattlerTagLapseType, GroundedTag } from "./battler-tags";
import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect"; import { getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "#app/data/status-effect";
import { Gender } from "./gender"; import { Gender } from "./gender";
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move";
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier"; import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "../modifier/modifier";
import { TerrainType } from "./terrain"; import { TerrainType } from "./terrain";
import { SpeciesFormChangeManualTrigger, SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "./pokemon-forms"; import { SpeciesFormChangeManualTrigger, SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "./pokemon-forms";
import i18next from "i18next"; import i18next from "i18next";
@ -17,7 +17,7 @@ import { Localizable } from "#app/interfaces/locales";
import { Command } from "../ui/command-ui-handler"; import { Command } from "../ui/command-ui-handler";
import { BerryModifierType } from "#app/modifier/modifier-type"; import { BerryModifierType } from "#app/modifier/modifier-type";
import { getPokeballName } from "./pokeball"; import { getPokeballName } from "./pokeball";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex, BattleType } from "#app/battle";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
@ -29,6 +29,15 @@ 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 } from "#app/phases/stat-stage-change-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { SwitchType } from "#app/enums/switch-type";
import { SwitchPhase } from "#app/phases/switch-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { NewBattlePhase } from "#app/phases/new-battle-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase";
import { PokemonAnimType } from "#enums/pokemon-anim-type";
import { StatusEffect } from "#enums/status-effect";
import { WeatherType } from "#enums/weather-type";
export class Ability implements Localizable { export class Ability implements Localizable {
public id: Abilities; public id: Abilities;
@ -505,7 +514,11 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr {
} }
applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean {
if (move instanceof AttackMove && pokemon.getAttackTypeEffectiveness(attacker.getMoveType(move), attacker) < 2) { const modifierValue = args.length > 0
? (args[0] as Utils.NumberHolder).value
: pokemon.getAttackTypeEffectiveness(attacker.getMoveType(move), attacker, undefined, undefined, move);
if (move instanceof AttackMove && modifierValue < 2) {
cancelled.value = true; // Suppresses "No Effect" message cancelled.value = true; // Suppresses "No Effect" message
(args[0] as Utils.NumberHolder).value = 0; (args[0] as Utils.NumberHolder).value = 0;
return true; return true;
@ -572,15 +585,11 @@ export class PostDefendAbAttr extends AbAttr {
export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr { export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr {
applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean {
const attackPriority = new Utils.IntegerHolder(move.priority);
applyMoveAttrs(IncrementMovePriorityAttr, attacker, null, move, attackPriority);
applyAbAttrs(ChangeMovePriorityAbAttr, attacker, null, simulated, move, attackPriority);
if (move.moveTarget === MoveTarget.USER || move.moveTarget === MoveTarget.NEAR_ALLY) { if (move.moveTarget === MoveTarget.USER || move.moveTarget === MoveTarget.NEAR_ALLY) {
return false; return false;
} }
if (attackPriority.value > 0 && !move.isMultiTarget()) { if (move.getPriority(attacker) > 0 && !move.isMultiTarget()) {
cancelled.value = true; cancelled.value = true;
return true; return true;
} }
@ -1342,65 +1351,30 @@ export class AddSecondStrikeAbAttr extends PreAttackAbAttr {
this.damageMultiplier = damageMultiplier; this.damageMultiplier = damageMultiplier;
} }
/**
* Determines whether this attribute can apply to a given move.
* @param {Move} move the move to which this attribute may apply
* @param numTargets the number of {@linkcode Pokemon} targeted by this move
* @returns true if the attribute can apply to the move, false otherwise
*/
canApplyPreAttack(move: Move, numTargets: integer): boolean {
/**
* Parental Bond cannot apply to multi-hit moves, charging moves, or
* moves that cause the user to faint.
*/
const exceptAttrs: Constructor<MoveAttr>[] = [
MultiHitAttr,
SacrificialAttr,
SacrificialAttrOnHit
];
/** Parental Bond cannot apply to these specific moves */
const exceptMoves: Moves[] = [
Moves.FLING,
Moves.UPROAR,
Moves.ROLLOUT,
Moves.ICE_BALL,
Moves.ENDEAVOR
];
/** Also check if this move is an Attack move and if it's only targeting one Pokemon */
return numTargets === 1
&& !move.isChargingMove()
&& !exceptAttrs.some(attr => move.hasAttr(attr))
&& !exceptMoves.some(id => move.id === id)
&& move.category !== MoveCategory.STATUS;
}
/** /**
* If conditions are met, this doubles the move's hit count (via args[1]) * If conditions are met, this doubles the move's hit count (via args[1])
* or multiplies the damage of secondary strikes (via args[2]) * or multiplies the damage of secondary strikes (via args[2])
* @param {Pokemon} pokemon the Pokemon using the move * @param pokemon the {@linkcode Pokemon} using the move
* @param passive n/a * @param passive n/a
* @param defender n/a * @param defender n/a
* @param {Move} move the move used by the ability source * @param move the {@linkcode Move} used by the ability source
* @param args\[0\] the number of Pokemon this move is targeting * @param args Additional arguments:
* @param {Utils.IntegerHolder} args\[1\] the number of strikes with this move * - `[0]` the number of strikes this move currently has ({@linkcode Utils.NumberHolder})
* @param {Utils.NumberHolder} args\[2\] the damage multiplier for the current strike * - `[1]` the damage multiplier for the current strike ({@linkcode Utils.NumberHolder})
* @returns * @returns
*/ */
applyPreAttack(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, args: any[]): boolean { applyPreAttack(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, args: any[]): boolean {
const numTargets = args[0] as integer; const hitCount = args[0] as Utils.NumberHolder;
const hitCount = args[1] as Utils.IntegerHolder; const multiplier = args[1] as Utils.NumberHolder;
const multiplier = args[2] as Utils.NumberHolder;
if (this.canApplyPreAttack(move, numTargets)) { if (move.canBeMultiStrikeEnhanced(pokemon, true)) {
this.showAbility = !!hitCount?.value; this.showAbility = !!hitCount?.value;
if (!!hitCount?.value) { if (hitCount?.value) {
hitCount.value *= 2; hitCount.value += 1;
} }
if (!!multiplier?.value && pokemon.turnData.hitsLeft % 2 === 1 && pokemon.turnData.hitsLeft !== pokemon.turnData.hitCount) { if (multiplier?.value && pokemon.turnData.hitsLeft === 1) {
multiplier.value *= this.damageMultiplier; multiplier.value = this.damageMultiplier;
} }
return true; return true;
} }
@ -1950,6 +1924,10 @@ export class CopyFaintedAllyAbilityAbAttr extends PostKnockOutAbAttr {
} }
} }
/**
* Ability attribute for ignoring the opponent's stat changes
* @param stats the stats that should be ignored
*/
export class IgnoreOpponentStatStagesAbAttr extends AbAttr { export class IgnoreOpponentStatStagesAbAttr extends AbAttr {
private stats: readonly BattleStat[]; private stats: readonly BattleStat[];
@ -1959,6 +1937,15 @@ export class IgnoreOpponentStatStagesAbAttr extends AbAttr {
this.stats = stats ?? BATTLE_STATS; this.stats = stats ?? BATTLE_STATS;
} }
/**
* Modifies a BooleanHolder and returns the result to see if a stat is ignored or not
* @param _pokemon n/a
* @param _passive n/a
* @param simulated n/a
* @param _cancelled n/a
* @param args A BooleanHolder that represents whether or not to ignore a stat's stat changes
* @returns true if the stat is ignored, false otherwise
*/
apply(_pokemon: Pokemon, _passive: boolean, simulated: boolean, _cancelled: Utils.BooleanHolder, args: any[]) { apply(_pokemon: Pokemon, _passive: boolean, simulated: boolean, _cancelled: Utils.BooleanHolder, args: any[]) {
if (this.stats.includes(args[0])) { if (this.stats.includes(args[0])) {
(args[1] as Utils.BooleanHolder).value = true; (args[1] as Utils.BooleanHolder).value = true;
@ -2441,12 +2428,15 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr {
} }
} }
/**
* Used by Imposter
*/
export class PostSummonTransformAbAttr extends PostSummonAbAttr { export class PostSummonTransformAbAttr extends PostSummonAbAttr {
constructor() { constructor() {
super(true); super(true);
} }
async applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): Promise<boolean> { async applyPostSummon(pokemon: Pokemon, _passive: boolean, simulated: boolean, _args: any[]): Promise<boolean> {
const targets = pokemon.getOpponents(); const targets = pokemon.getOpponents();
if (simulated || !targets.length) { if (simulated || !targets.length) {
return simulated; return simulated;
@ -2455,17 +2445,31 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
let target: Pokemon; let target: Pokemon;
if (targets.length > 1) { if (targets.length > 1) {
pokemon.scene.executeWithSeedOffset(() => target = Utils.randSeedItem(targets), pokemon.scene.currentBattle.waveIndex); pokemon.scene.executeWithSeedOffset(() => {
// in a double battle, if one of the opposing pokemon is fused the other one will be chosen
// if both are fused, then Imposter will fail below
if (targets[0].fusionSpecies) {
target = targets[1];
return;
} else if (targets[1].fusionSpecies) {
target = targets[0];
return;
}
target = Utils.randSeedItem(targets);
}, pokemon.scene.currentBattle.waveIndex);
} else { } else {
target = targets[0]; target = targets[0];
} }
target = target!; target = target!;
// transforming from or into fusion pokemon causes various problems (including crashes and save corruption)
if (target.fusionSpecies || pokemon.fusionSpecies) {
return false;
}
pokemon.summonData.speciesForm = target.getSpeciesForm(); pokemon.summonData.speciesForm = target.getSpeciesForm();
pokemon.summonData.fusionSpeciesForm = target.getFusionSpeciesForm();
pokemon.summonData.ability = target.getAbility().id; pokemon.summonData.ability = target.getAbility().id;
pokemon.summonData.gender = target.getGender(); pokemon.summonData.gender = target.getGender();
pokemon.summonData.fusionGender = target.getFusionGender();
// Copy all stats (except HP) // Copy all stats (except HP)
for (const s of EFFECTIVE_STATS) { for (const s of EFFECTIVE_STATS) {
@ -2572,6 +2576,42 @@ export class PostSummonFormChangeByWeatherAbAttr extends PostSummonAbAttr {
} }
} }
/**
* Attribute implementing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Commander_(Ability) | Commander}.
* When the source of an ability with this attribute detects a Dondozo as their active ally, the source "jumps
* into the Dondozo's mouth," sharply boosting the Dondozo's stats, cancelling the source's moves, and
* causing attacks that target the source to always miss.
*/
export class CommanderAbAttr extends AbAttr {
constructor() {
super(true);
}
override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: null, args: any[]): boolean {
// TODO: Should this work with X + Dondozo fusions?
if (pokemon.scene.currentBattle?.double && pokemon.getAlly()?.species.speciesId === Species.DONDOZO) {
// If the ally Dondozo is fainted or was previously "commanded" by
// another Pokemon, this effect cannot apply.
if (pokemon.getAlly().isFainted() || pokemon.getAlly().getTag(BattlerTagType.COMMANDED)) {
return false;
}
if (!simulated) {
// Lapse the source's semi-invulnerable tags (to avoid visual inconsistencies)
pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
// Play an animation of the source jumping into the ally Dondozo's mouth
pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.COMMANDER_APPLY);
// Apply boosts from this effect to the ally Dondozo
pokemon.getAlly().addTag(BattlerTagType.COMMANDED, 0, Moves.NONE, pokemon.id);
// Cancel the source Pokemon's next move (if a move is queued)
pokemon.scene.tryRemovePhase((phase) => phase instanceof MovePhase && phase.pokemon === pokemon);
}
return true;
}
return false;
}
}
export class PreSwitchOutAbAttr extends AbAttr { export class PreSwitchOutAbAttr extends AbAttr {
constructor() { constructor() {
super(true); super(true);
@ -3092,7 +3132,7 @@ export class SuppressWeatherEffectAbAttr extends PreWeatherEffectAbAttr {
/** /**
* Condition function to applied to abilities related to Sheer Force. * Condition function to applied to abilities related to Sheer Force.
* Checks if last move used against target was affected by a Sheer Force user and: * Checks if last move used against target was affected by a Sheer Force user and:
* Disables: Color Change, Pickpocket, Wimp Out, Emergency Exit, Berserk, Anger Shell * Disables: Color Change, Pickpocket, Berserk, Anger Shell
* @returns {AbAttrCondition} If false disables the ability which the condition is applied to. * @returns {AbAttrCondition} If false disables the ability which the condition is applied to.
*/ */
function getSheerForceHitDisableAbCondition(): AbAttrCondition { function getSheerForceHitDisableAbCondition(): AbAttrCondition {
@ -3140,7 +3180,7 @@ function getAnticipationCondition(): AbAttrCondition {
continue; continue;
} }
// the move's base type (not accounting for variable type changes) is super effective // the move's base type (not accounting for variable type changes) is super effective
if (move.getMove() instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.getMove().type, opponent, true) >= 2) { if (move.getMove() instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.getMove().type, opponent, true, undefined, move.getMove()) >= 2) {
return true; return true;
} }
// move is a OHKO // move is a OHKO
@ -3838,6 +3878,41 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr {
} }
} }
/**
* Triggers after the Pokemon loses or consumes an item
* @extends AbAttr
*/
export class PostItemLostAbAttr extends AbAttr {
applyPostItemLost(pokemon: Pokemon, simulated: boolean, args: any[]): boolean | Promise<boolean> {
return false;
}
}
/**
* Applies a Battler Tag to the Pokemon after it loses or consumes item
* @extends PostItemLostAbAttr
*/
export class PostItemLostApplyBattlerTagAbAttr extends PostItemLostAbAttr {
private tagType: BattlerTagType;
constructor(tagType: BattlerTagType) {
super(true);
this.tagType = tagType;
}
/**
* Adds the last used Pokeball back into the player's inventory
* @param pokemon {@linkcode Pokemon} with this ability
* @param args N/A
* @returns true if BattlerTag was applied
*/
applyPostItemLost(pokemon: Pokemon, simulated: boolean, args: any[]): boolean | Promise<boolean> {
if (!pokemon.getTag(this.tagType) && !simulated) {
pokemon.addTag(this.tagType);
return true;
}
return false;
}
}
export class StatStageChangeMultiplierAbAttr extends AbAttr { export class StatStageChangeMultiplierAbAttr extends AbAttr {
private multiplier: integer; private multiplier: integer;
@ -4042,7 +4117,7 @@ export class PostBattleLootAbAttr extends PostBattleAbAttr {
if (!simulated && postBattleLoot.length) { if (!simulated && postBattleLoot.length) {
const randItem = Utils.randSeedItem(postBattleLoot); const randItem = Utils.randSeedItem(postBattleLoot);
//@ts-ignore - TODO see below //@ts-ignore - TODO see below
if (pokemon.scene.tryTransferHeldItemModifier(randItem, pokemon, true, 1, true)) { // TODO: fix. This is a promise!? if (pokemon.scene.tryTransferHeldItemModifier(randItem, pokemon, true, 1, true, undefined, false)) { // TODO: fix. This is a promise!?
postBattleLoot.splice(postBattleLoot.indexOf(randItem), 1); postBattleLoot.splice(postBattleLoot.indexOf(randItem), 1);
pokemon.scene.queueMessage(i18next.t("abilityTriggers:postBattleLoot", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), itemName: randItem.type.name })); pokemon.scene.queueMessage(i18next.t("abilityTriggers:postBattleLoot", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), itemName: randItem.type.name }));
return true; return true;
@ -4833,6 +4908,239 @@ async function applyAbAttrsInternal<TAttr extends AbAttr>(
} }
} }
class ForceSwitchOutHelper {
constructor(private switchType: SwitchType) {}
/**
* Handles the logic for switching out a Pokémon based on battle conditions, HP, and the switch type.
*
* @param pokemon The {@linkcode Pokemon} attempting to switch out.
* @returns `true` if the switch is successful
*/
public switchOutLogic(pokemon: Pokemon): boolean {
const switchOutTarget = pokemon;
/**
* If the switch-out target is a player-controlled Pokémon, the function checks:
* - Whether there are available party members to switch in.
* - If the Pokémon is still alive (hp > 0), and if so, it leaves the field and a new SwitchPhase is initiated.
*/
if (switchOutTarget instanceof PlayerPokemon) {
if (switchOutTarget.scene.getPlayerParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
return false;
}
if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
pokemon.scene.prependToPhase(new SwitchPhase(pokemon.scene, this.switchType, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase);
return true;
}
/**
* For non-wild battles, it checks if the opposing party has any available Pokémon to switch in.
* If yes, the Pokémon leaves the field and a new SwitchSummonPhase is initiated.
*/
} else if (pokemon.scene.currentBattle.battleType !== BattleType.WILD) {
if (switchOutTarget.scene.getEnemyParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
return false;
}
if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
pokemon.scene.prependToPhase(new SwitchSummonPhase(pokemon.scene, this.switchType, switchOutTarget.getFieldIndex(),
(pokemon.scene.currentBattle.trainer ? pokemon.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0),
false, false), MoveEndPhase);
return true;
}
/**
* For wild Pokémon battles, the Pokémon will flee if the conditions are met (waveIndex and double battles).
*/
} else {
if (!pokemon.scene.currentBattle.waveIndex && pokemon.scene.currentBattle.waveIndex % 10 === 0) {
return false;
}
if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(false);
pokemon.scene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500);
if (switchOutTarget.scene.currentBattle.double) {
const allyPokemon = switchOutTarget.getAlly();
switchOutTarget.scene.redirectPokemonMoves(switchOutTarget, allyPokemon);
}
}
if (!switchOutTarget.getAlly()?.isActive(true)) {
pokemon.scene.clearEnemyHeldItemModifiers();
if (switchOutTarget.hp) {
pokemon.scene.pushPhase(new BattleEndPhase(pokemon.scene));
pokemon.scene.pushPhase(new NewBattlePhase(pokemon.scene));
}
}
}
return false;
}
/**
* Determines if a Pokémon can switch out based on its status, the opponent's status, and battle conditions.
*
* @param pokemon The Pokémon attempting to switch out.
* @param opponent The opponent Pokémon.
* @returns `true` if the switch-out condition is met
*/
public getSwitchOutCondition(pokemon: Pokemon, opponent: Pokemon): boolean {
const switchOutTarget = pokemon;
const player = switchOutTarget instanceof PlayerPokemon;
if (player) {
const blockedByAbility = new Utils.BooleanHolder(false);
applyAbAttrs(ForceSwitchOutImmunityAbAttr, opponent, blockedByAbility);
return !blockedByAbility.value;
}
if (!player && pokemon.scene.currentBattle.battleType === BattleType.WILD) {
if (!pokemon.scene.currentBattle.waveIndex && pokemon.scene.currentBattle.waveIndex % 10 === 0) {
return false;
}
}
if (!player && pokemon.scene.currentBattle.isBattleMysteryEncounter() && !pokemon.scene.currentBattle.mysteryEncounter?.fleeAllowed) {
return false;
}
const party = player ? pokemon.scene.getPlayerParty() : pokemon.scene.getEnemyParty();
return (!player && pokemon.scene.currentBattle.battleType === BattleType.WILD)
|| party.filter(p => p.isAllowedInBattle()
&& (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > pokemon.scene.currentBattle.getBattlerCount();
}
/**
* Returns a message if the switch-out attempt fails due to ability effects.
*
* @param target The target Pokémon.
* @returns The failure message, or `null` if no failure.
*/
public getFailedText(target: Pokemon): string | null {
const blockedByAbility = new Utils.BooleanHolder(false);
applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility);
return blockedByAbility.value ? i18next.t("moveTriggers:cannotBeSwitchedOut", { pokemonName: getPokemonNameWithAffix(target) }) : null;
}
}
/**
* Calculates the amount of recovery from the Shell Bell item.
*
* If the Pokémon is holding a Shell Bell, this function computes the amount of health
* recovered based on the damage dealt in the current turn. The recovery is multiplied by the
* Shell Bell's modifier (if any).
*
* @param pokemon - The Pokémon whose Shell Bell recovery is being calculated.
* @returns The amount of health recovered by Shell Bell.
*/
function calculateShellBellRecovery(pokemon: Pokemon): number {
const shellBellModifier = pokemon.getHeldItems().find(m => m instanceof HitHealModifier);
if (shellBellModifier) {
return Utils.toDmgValue(pokemon.turnData.totalDamageDealt / 8) * shellBellModifier.stackCount;
}
return 0;
}
/**
* Triggers after the Pokemon takes any damage
* @extends AbAttr
*/
export class PostDamageAbAttr extends AbAttr {
public applyPostDamage(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[], source?: Pokemon): boolean | Promise<boolean> {
return false;
}
}
/**
* Ability attribute for forcing a Pokémon to switch out after its health drops below half.
* This attribute checks various conditions related to the damage received, the moves used by the Pokémon
* and its opponents, and determines whether a forced switch-out should occur.
*
* Used by Wimp Out and Emergency Exit
*
* @extends PostDamageAbAttr
* @see {@linkcode applyPostDamage}
*/
export class PostDamageForceSwitchAbAttr extends PostDamageAbAttr {
private helper: ForceSwitchOutHelper = new ForceSwitchOutHelper(SwitchType.SWITCH);
private hpRatio: number;
constructor(hpRatio: number = 0.5) {
super();
this.hpRatio = hpRatio;
}
/**
* Applies the switch-out logic after the Pokémon takes damage.
* Checks various conditions based on the moves used by the Pokémon, the opponents' moves, and
* the Pokémon's health after damage to determine whether the switch-out should occur.
*
* @param pokemon The Pokémon that took damage.
* @param damage The amount of damage taken by the Pokémon.
* @param passive N/A
* @param simulated Whether the ability is being simulated.
* @param args N/A
* @param source The Pokemon that dealt damage
* @returns `true` if the switch-out logic was successfully applied
*/
public override applyPostDamage(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[], source?: Pokemon): boolean | Promise<boolean> {
const moveHistory = pokemon.getMoveHistory();
// Will not activate when the Pokémon's HP is lowered by cutting its own HP
const fordbiddenAttackingMoves = [ Moves.BELLY_DRUM, Moves.SUBSTITUTE, Moves.CURSE, Moves.PAIN_SPLIT ];
if (moveHistory.length > 0) {
const lastMoveUsed = moveHistory[moveHistory.length - 1];
if (fordbiddenAttackingMoves.includes(lastMoveUsed.move)) {
return false;
}
}
// Dragon Tail and Circle Throw switch out Pokémon before the Ability activates.
const fordbiddenDefendingMoves = [ Moves.DRAGON_TAIL, Moves.CIRCLE_THROW ];
if (source) {
const enemyMoveHistory = source.getMoveHistory();
if (enemyMoveHistory.length > 0) {
const enemyLastMoveUsed = enemyMoveHistory[enemyMoveHistory.length - 1];
// Will not activate if the Pokémon's HP falls below half while it is in the air during Sky Drop.
if (fordbiddenDefendingMoves.includes(enemyLastMoveUsed.move) || enemyLastMoveUsed.move === Moves.SKY_DROP && enemyLastMoveUsed.result === MoveResult.OTHER) {
return false;
// Will not activate if the Pokémon's HP falls below half by a move affected by Sheer Force.
} else if (allMoves[enemyLastMoveUsed.move].chance >= 0 && source.hasAbility(Abilities.SHEER_FORCE)) {
return false;
// Activate only after the last hit of multistrike moves
} else if (source.turnData.hitsLeft > 1) {
return false;
}
if (source.turnData.hitCount > 1) {
damage = pokemon.turnData.damageTaken;
}
}
}
if (pokemon.hp + damage >= pokemon.getMaxHp() * this.hpRatio) {
// Activates if it falls below half and recovers back above half from a Shell Bell
const shellBellHeal = calculateShellBellRecovery(pokemon);
if (pokemon.hp - shellBellHeal < pokemon.getMaxHp() * this.hpRatio) {
for (const opponent of pokemon.getOpponents()) {
if (!this.helper.getSwitchOutCondition(pokemon, opponent)) {
return false;
}
}
return this.helper.switchOutLogic(pokemon);
} else {
return false;
}
} else {
return false;
}
}
public getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null {
return this.helper.getFailedText(target);
}
}
export function applyAbAttrs(attrType: Constructor<AbAttr>, pokemon: Pokemon, cancelled: Utils.BooleanHolder | null, simulated: boolean = false, ...args: any[]): Promise<void> { export function applyAbAttrs(attrType: Constructor<AbAttr>, pokemon: Pokemon, cancelled: Utils.BooleanHolder | null, simulated: boolean = false, ...args: any[]): Promise<void> {
return applyAbAttrsInternal<AbAttr>(attrType, pokemon, (attr, passive) => attr.apply(pokemon, passive, simulated, cancelled, args), args, false, simulated); return applyAbAttrsInternal<AbAttr>(attrType, pokemon, (attr, passive) => attr.apply(pokemon, passive, simulated, cancelled, args), args, false, simulated);
} }
@ -4866,6 +5174,11 @@ export function applyPostSetStatusAbAttrs(attrType: Constructor<PostSetStatusAbA
return applyAbAttrsInternal<PostSetStatusAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), args, false, simulated); return applyAbAttrsInternal<PostSetStatusAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), args, false, simulated);
} }
export function applyPostDamageAbAttrs(attrType: Constructor<PostDamageAbAttr>,
pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean = false, args: any[], source?: Pokemon): Promise<void> {
return applyAbAttrsInternal<PostDamageAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostDamage(pokemon, damage, passive, simulated, args, source), args);
}
/** /**
* Applies a field Stat multiplier attribute * Applies a field Stat multiplier attribute
* @param attrType {@linkcode FieldMultiplyStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being * @param attrType {@linkcode FieldMultiplyStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being
@ -4971,6 +5284,11 @@ export function applyPostFaintAbAttrs(attrType: Constructor<PostFaintAbAttr>,
return applyAbAttrsInternal<PostFaintAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostFaint(pokemon, passive, simulated, attacker, move, hitResult, args), args, false, simulated); return applyAbAttrsInternal<PostFaintAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostFaint(pokemon, passive, simulated, attacker, move, hitResult, args), args, false, simulated);
} }
export function applyPostItemLostAbAttrs(attrType: Constructor<PostItemLostAbAttr>,
pokemon: Pokemon, simulated: boolean = false, ...args: any[]): Promise<void> {
return applyAbAttrsInternal<PostItemLostAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostItemLost(pokemon, simulated, args), args);
}
function queueShowAbility(pokemon: Pokemon, passive: boolean): void { function queueShowAbility(pokemon: Pokemon, passive: boolean): void {
pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.id, passive)); pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.id, passive));
pokemon.scene.clearPhaseQueueSplice(); pokemon.scene.clearPhaseQueueSplice();
@ -5062,7 +5380,8 @@ 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)
.ignorable(),
new Ability(Abilities.OWN_TEMPO, 3) new Ability(Abilities.OWN_TEMPO, 3)
.attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED) .attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED)
.attr(IntimidateImmunityAbAttr) .attr(IntimidateImmunityAbAttr)
@ -5094,8 +5413,7 @@ export function initAbilities() {
.attr(EffectSporeAbAttr), .attr(EffectSporeAbAttr),
new Ability(Abilities.SYNCHRONIZE, 3) new Ability(Abilities.SYNCHRONIZE, 3)
.attr(SyncEncounterNatureAbAttr) .attr(SyncEncounterNatureAbAttr)
.attr(SynchronizeStatusAbAttr) .attr(SynchronizeStatusAbAttr),
.partial(), // interaction with psycho shift needs work, keeping to old Gen interaction for now
new Ability(Abilities.CLEAR_BODY, 3) new Ability(Abilities.CLEAR_BODY, 3)
.attr(ProtectStatAbAttr) .attr(ProtectStatAbAttr)
.ignorable(), .ignorable(),
@ -5116,6 +5434,7 @@ export function initAbilities() {
new Ability(Abilities.ILLUMINATE, 3) new Ability(Abilities.ILLUMINATE, 3)
.attr(ProtectStatAbAttr, Stat.ACC) .attr(ProtectStatAbAttr, Stat.ACC)
.attr(DoubleBattleChanceAbAttr) .attr(DoubleBattleChanceAbAttr)
.attr(IgnoreOpponentStatStagesAbAttr, [ Stat.EVA ])
.ignorable(), .ignorable(),
new Ability(Abilities.TRACE, 3) new Ability(Abilities.TRACE, 3)
.attr(PostSummonCopyAbilityAbAttr) .attr(PostSummonCopyAbilityAbAttr)
@ -5180,11 +5499,9 @@ export function initAbilities() {
new Ability(Abilities.CUTE_CHARM, 3) new Ability(Abilities.CUTE_CHARM, 3)
.attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED), .attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED),
new Ability(Abilities.PLUS, 3) new Ability(Abilities.PLUS, 3)
.conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5) .conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
.ignorable(),
new Ability(Abilities.MINUS, 3) new Ability(Abilities.MINUS, 3)
.conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5) .conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
.ignorable(),
new Ability(Abilities.FORECAST, 3) new Ability(Abilities.FORECAST, 3)
.attr(UncopiableAbilityAbAttr) .attr(UncopiableAbilityAbAttr)
.attr(NoFusionAbilityAbAttr) .attr(NoFusionAbilityAbAttr)
@ -5264,7 +5581,9 @@ export function initAbilities() {
new Ability(Abilities.ANGER_POINT, 4) new Ability(Abilities.ANGER_POINT, 4)
.attr(PostDefendCritStatStageChangeAbAttr, Stat.ATK, 6), .attr(PostDefendCritStatStageChangeAbAttr, Stat.ATK, 6),
new Ability(Abilities.UNBURDEN, 4) new Ability(Abilities.UNBURDEN, 4)
.unimplemented(), .attr(PostItemLostApplyBattlerTagAbAttr, BattlerTagType.UNBURDEN)
.bypassFaint() // Allows reviver seed to activate Unburden
.edgeCase(), // Should not restore Unburden boost if Pokemon loses then regains Unburden ability
new Ability(Abilities.HEATPROOF, 4) new Ability(Abilities.HEATPROOF, 4)
.attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 0.5) .attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 0.5)
.attr(ReduceBurnDamageAbAttr, 0.5) .attr(ReduceBurnDamageAbAttr, 0.5)
@ -5337,7 +5656,7 @@ export function initAbilities() {
new Ability(Abilities.FOREWARN, 4) new Ability(Abilities.FOREWARN, 4)
.attr(ForewarnAbAttr), .attr(ForewarnAbAttr),
new Ability(Abilities.UNAWARE, 4) new Ability(Abilities.UNAWARE, 4)
.attr(IgnoreOpponentStatStagesAbAttr) .attr(IgnoreOpponentStatStagesAbAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.ACC, Stat.EVA ])
.ignorable(), .ignorable(),
new Ability(Abilities.TINTED_LENS, 4) new Ability(Abilities.TINTED_LENS, 4)
.attr(DamageBoostAbAttr, 2, (user, target, move) => (target?.getMoveEffectiveness(user!, move) ?? 1) <= 0.5), .attr(DamageBoostAbAttr, 2, (user, target, move) => (target?.getMoveEffectiveness(user!, move) ?? 1) <= 0.5),
@ -5522,7 +5841,8 @@ export function initAbilities() {
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
.attr(MoveAbilityBypassAbAttr), .attr(MoveAbilityBypassAbAttr),
new Ability(Abilities.AROMA_VEIL, 6) new Ability(Abilities.AROMA_VEIL, 6)
.attr(UserFieldBattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK ]), .attr(UserFieldBattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK ])
.ignorable(),
new Ability(Abilities.FLOWER_VEIL, 6) new Ability(Abilities.FLOWER_VEIL, 6)
.ignorable() .ignorable()
.unimplemented(), .unimplemented(),
@ -5607,11 +5927,11 @@ export function initAbilities() {
new Ability(Abilities.STAMINA, 7) new Ability(Abilities.STAMINA, 7)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1), .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1),
new Ability(Abilities.WIMP_OUT, 7) new Ability(Abilities.WIMP_OUT, 7)
.condition(getSheerForceHitDisableAbCondition()) .attr(PostDamageForceSwitchAbAttr)
.unimplemented(), .edgeCase(), // Should not trigger when hurting itself in confusion
new Ability(Abilities.EMERGENCY_EXIT, 7) new Ability(Abilities.EMERGENCY_EXIT, 7)
.condition(getSheerForceHitDisableAbCondition()) .attr(PostDamageForceSwitchAbAttr)
.unimplemented(), .edgeCase(), // Should not trigger when hurting itself in confusion
new Ability(Abilities.WATER_COMPACTION, 7) new Ability(Abilities.WATER_COMPACTION, 7)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2), .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2),
new Ability(Abilities.MERCILESS, 7) new Ability(Abilities.MERCILESS, 7)
@ -5697,7 +6017,7 @@ export function initAbilities() {
.bypassFaint(), .bypassFaint(),
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)
new Ability(Abilities.COMATOSE, 7) new Ability(Abilities.COMATOSE, 7)
.attr(UncopiableAbilityAbAttr) .attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr) .attr(UnswappableAbilityAbAttr)
@ -5801,7 +6121,8 @@ export function initAbilities() {
.attr(NoFusionAbilityAbAttr) .attr(NoFusionAbilityAbAttr)
.attr(UncopiableAbilityAbAttr) .attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr) .attr(UnswappableAbilityAbAttr)
.bypassFaint(), .bypassFaint()
.edgeCase(), // Soft-locks the game if a form-changed Cramorant and its attacker both faint at the same time (ex. using Self-Destruct)
new Ability(Abilities.STALWART, 8) new Ability(Abilities.STALWART, 8)
.attr(BlockRedirectAbAttr), .attr(BlockRedirectAbAttr),
new Ability(Abilities.STEAM_ENGINE, 8) new Ability(Abilities.STEAM_ENGINE, 8)
@ -5944,9 +6265,11 @@ export function initAbilities() {
.attr(PreSwitchOutFormChangeAbAttr, (pokemon) => !pokemon.isFainted() ? 1 : pokemon.formIndex) .attr(PreSwitchOutFormChangeAbAttr, (pokemon) => !pokemon.isFainted() ? 1 : pokemon.formIndex)
.bypassFaint(), .bypassFaint(),
new Ability(Abilities.COMMANDER, 9) new Ability(Abilities.COMMANDER, 9)
.attr(CommanderAbAttr)
.attr(DoubleBattleChanceAbAttr)
.attr(UncopiableAbilityAbAttr) .attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr) .attr(UnswappableAbilityAbAttr)
.unimplemented(), .edgeCase(), // Encore, Frenzy, and other non-`TURN_END` tags don't lapse correctly on the commanding Pokemon.
new Ability(Abilities.ELECTROMORPHOSIS, 9) new Ability(Abilities.ELECTROMORPHOSIS, 9)
.attr(PostDefendApplyBattlerTagAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, BattlerTagType.CHARGED), .attr(PostDefendApplyBattlerTagAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, BattlerTagType.CHARGED),
new Ability(Abilities.PROTOSYNTHESIS, 9) new Ability(Abilities.PROTOSYNTHESIS, 9)
@ -5973,16 +6296,14 @@ export function initAbilities() {
.ignorable(), .ignorable(),
new Ability(Abilities.SWORD_OF_RUIN, 9) new Ability(Abilities.SWORD_OF_RUIN, 9)
.attr(FieldMultiplyStatAbAttr, Stat.DEF, 0.75) .attr(FieldMultiplyStatAbAttr, Stat.DEF, 0.75)
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) })) .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) })),
.ignorable(),
new Ability(Abilities.TABLETS_OF_RUIN, 9) new Ability(Abilities.TABLETS_OF_RUIN, 9)
.attr(FieldMultiplyStatAbAttr, Stat.ATK, 0.75) .attr(FieldMultiplyStatAbAttr, Stat.ATK, 0.75)
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonTabletsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) })) .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonTabletsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) }))
.ignorable(), .ignorable(),
new Ability(Abilities.BEADS_OF_RUIN, 9) new Ability(Abilities.BEADS_OF_RUIN, 9)
.attr(FieldMultiplyStatAbAttr, Stat.SPDEF, 0.75) .attr(FieldMultiplyStatAbAttr, Stat.SPDEF, 0.75)
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) })) .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) })),
.ignorable(),
new Ability(Abilities.ORICHALCUM_PULSE, 9) new Ability(Abilities.ORICHALCUM_PULSE, 9)
.attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY) .attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY)
.attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SUNNY) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SUNNY)

View File

@ -1,13 +1,13 @@
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 "#enums/type";
import { BooleanHolder, NumberHolder, toDmgValue } from "#app/utils"; import { BooleanHolder, NumberHolder, toDmgValue } from "#app/utils";
import { MoveCategory, allMoves, MoveTarget, IncrementMovePriorityAttr, applyMoveAttrs } from "#app/data/move"; import { MoveCategory, allMoves, MoveTarget } from "#app/data/move";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import Pokemon, { HitResult, PokemonMove } from "#app/field/pokemon"; import Pokemon, { HitResult, PokemonMove } from "#app/field/pokemon";
import { StatusEffect } from "#app/data/status-effect"; import { StatusEffect } from "#enums/status-effect";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, InfiltratorAbAttr, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability"; import { BlockNonDirectDamageAbAttr, InfiltratorAbAttr, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims"; import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims";
import i18next from "i18next"; import i18next from "i18next";
@ -313,20 +313,20 @@ export class ConditionalProtectTag extends ArenaTag {
* protection effect. * protection effect.
* @param arena {@linkcode Arena} The arena containing the protection effect * @param arena {@linkcode Arena} The arena containing the protection effect
* @param moveId {@linkcode Moves} The move to check against this condition * @param moveId {@linkcode Moves} The move to check against this condition
* @returns `true` if the incoming move's priority is greater than 0. This includes * @returns `true` if the incoming move's priority is greater than 0.
* moves with modified priorities from abilities (e.g. Prankster) * This includes moves with modified priorities from abilities (e.g. Prankster)
*/ */
const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => { const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => {
const move = allMoves[moveId]; const move = allMoves[moveId];
const priority = new NumberHolder(move.priority);
const effectPhase = arena.scene.getCurrentPhase(); const effectPhase = arena.scene.getCurrentPhase();
if (effectPhase instanceof MoveEffectPhase) { if (effectPhase instanceof MoveEffectPhase) {
const attacker = effectPhase.getUserPokemon()!; const attacker = effectPhase.getUserPokemon();
applyMoveAttrs(IncrementMovePriorityAttr, attacker, null, move, priority); if (attacker) {
applyAbAttrs(ChangeMovePriorityAbAttr, attacker, null, false, move, priority); return move.getPriority(attacker) > 0;
} }
return priority.value > 0; }
return move.priority > 0;
}; };
/** /**
@ -778,13 +778,14 @@ class ToxicSpikesTag extends ArenaTrapTag {
* Delays the attack's effect by a set amount of turns, usually 3 (including the turn the move is used), * Delays the attack's effect by a set amount of turns, usually 3 (including the turn the move is used),
* and deals damage after the turn count is reached. * and deals damage after the turn count is reached.
*/ */
class DelayedAttackTag extends ArenaTag { export class DelayedAttackTag extends ArenaTag {
public targetIndex: BattlerIndex; public targetIndex: BattlerIndex;
constructor(tagType: ArenaTagType, sourceMove: Moves | undefined, sourceId: number, targetIndex: BattlerIndex) { constructor(tagType: ArenaTagType, sourceMove: Moves | undefined, sourceId: number, targetIndex: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH) {
super(tagType, 3, sourceMove, sourceId); super(tagType, 3, sourceMove, sourceId, side);
this.targetIndex = targetIndex; this.targetIndex = targetIndex;
this.side = side;
} }
lapse(arena: Arena): boolean { lapse(arena: Arena): boolean {
@ -1203,6 +1204,24 @@ class GrassWaterPledgeTag extends ArenaTag {
} }
} }
/**
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Fairy_Lock_(move) Fairy Lock}.
* Fairy Lock prevents all Pokémon (except Ghost types) on the field from switching out or
* fleeing during their next turn.
* If a Pokémon that's on the field when Fairy Lock is used goes on to faint later in the same turn,
* the Pokémon that replaces it will still be unable to switch out in the following turn.
*/
export class FairyLockTag extends ArenaTag {
constructor(turnCount: number, sourceId: number) {
super(ArenaTagType.FAIRY_LOCK, turnCount, Moves.FAIRY_LOCK, sourceId);
}
onAdd(arena: Arena): void {
arena.scene.queueMessage(i18next.t("arenaTag:fairyLockOnAdd"));
}
}
// TODO: swap `sourceMove` and `sourceId` and make `sourceMove` an optional parameter // TODO: swap `sourceMove` and `sourceId` and make `sourceMove` an optional parameter
export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove: Moves | undefined, sourceId: number, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null { export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove: Moves | undefined, sourceId: number, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null {
switch (tagType) { switch (tagType) {
@ -1230,7 +1249,7 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove
return new ToxicSpikesTag(sourceId, side); return new ToxicSpikesTag(sourceId, side);
case ArenaTagType.FUTURE_SIGHT: case ArenaTagType.FUTURE_SIGHT:
case ArenaTagType.DOOM_DESIRE: case ArenaTagType.DOOM_DESIRE:
return new DelayedAttackTag(tagType, sourceMove, sourceId, targetIndex!); // TODO:questionable bang return new DelayedAttackTag(tagType, sourceMove, sourceId, targetIndex!, side); // TODO:questionable bang
case ArenaTagType.WISH: case ArenaTagType.WISH:
return new WishTag(turnCount, sourceId, side); return new WishTag(turnCount, sourceId, side);
case ArenaTagType.STEALTH_ROCK: case ArenaTagType.STEALTH_ROCK:
@ -1261,6 +1280,8 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove
return new WaterFirePledgeTag(sourceId, side); return new WaterFirePledgeTag(sourceId, side);
case ArenaTagType.GRASS_WATER_PLEDGE: case ArenaTagType.GRASS_WATER_PLEDGE:
return new GrassWaterPledgeTag(sourceId, side); return new GrassWaterPledgeTag(sourceId, side);
case ArenaTagType.FAIRY_LOCK:
return new FairyLockTag(turnCount, sourceId);
default: default:
return null; return null;
} }

View File

@ -1,4 +1,4 @@
import { Type } from "#app/data/type"; import { Type } from "#enums/type";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
import { pokemonEvolutions, SpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions"; import { pokemonEvolutions, SpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions";
import i18next from "i18next"; import i18next from "i18next";
@ -7666,7 +7666,7 @@ export function initBiomes() {
if (biome === Biome.END) { if (biome === Biome.END) {
const biomeList = Object.keys(Biome).filter(key => !isNaN(Number(key))); const biomeList = Object.keys(Biome).filter(key => !isNaN(Number(key)));
biomeList.pop(); // Removes Biome.END from the list biomeList.pop(); // Removes Biome.END from the list
const randIndex = Utils.randInt(biomeList.length, 1); // Will never be Biome.TOWN const randIndex = Utils.randSeedInt(biomeList.length, 1); // Will never be Biome.TOWN
biome = Biome[biomeList[randIndex]]; biome = Biome[biomeList[randIndex]];
} }
const linkedBiomes: (Biome | [ Biome, integer ])[] = Array.isArray(biomeLinks[biome]) const linkedBiomes: (Biome | [ Biome, integer ])[] = Array.isArray(biomeLinks[biome])

View File

@ -1,10 +1,10 @@
import { Gender } from "#app/data/gender"; import { Gender } from "#app/data/gender";
import { PokeballType } from "#app/data/pokeball"; import { PokeballType } from "#enums/pokeball";
import Pokemon from "#app/field/pokemon"; import Pokemon from "#app/field/pokemon";
import { Type } from "#app/data/type"; import { Type } from "#enums/type";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
import { WeatherType } from "#app/data/weather"; import { WeatherType } from "#enums/weather-type";
import { Nature } from "#app/data/nature"; import { Nature } from "#enums/nature";
import { Biome } from "#enums/biome"; import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
@ -478,7 +478,7 @@ export const pokemonEvolutions: PokemonEvolutions = {
], ],
[Species.NINCADA]: [ [Species.NINCADA]: [
new SpeciesEvolution(Species.NINJASK, 20, null, null), new SpeciesEvolution(Species.NINJASK, 20, null, null),
new SpeciesEvolution(Species.SHEDINJA, 20, null, new SpeciesEvolutionCondition(p => p.scene.getParty().length < 6 && p.scene.pokeballCounts[PokeballType.POKEBALL] > 0)) new SpeciesEvolution(Species.SHEDINJA, 20, null, new SpeciesEvolutionCondition(p => p.scene.getPlayerParty().length < 6 && p.scene.pokeballCounts[PokeballType.POKEBALL] > 0))
], ],
[Species.WHISMUR]: [ [Species.WHISMUR]: [
new SpeciesEvolution(Species.LOUDRED, 20, null, null) new SpeciesEvolution(Species.LOUDRED, 20, null, null)
@ -890,7 +890,7 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesEvolution(Species.GOGOAT, 32, null, null) new SpeciesEvolution(Species.GOGOAT, 32, null, null)
], ],
[Species.PANCHAM]: [ [Species.PANCHAM]: [
new SpeciesEvolution(Species.PANGORO, 32, null, new SpeciesEvolutionCondition(p => !!p.scene.getParty().find(p => p.getTypes(false, false, true).indexOf(Type.DARK) > -1)), SpeciesWildEvolutionDelay.MEDIUM) new SpeciesEvolution(Species.PANGORO, 32, null, new SpeciesEvolutionCondition(p => !!p.scene.getPlayerParty().find(p => p.getTypes(false, false, true).indexOf(Type.DARK) > -1)), SpeciesWildEvolutionDelay.MEDIUM)
], ],
[Species.ESPURR]: [ [Species.ESPURR]: [
new SpeciesFormEvolution(Species.MEOWSTIC, "", "female", 25, null, new SpeciesEvolutionCondition(p => p.gender === Gender.FEMALE, p => p.gender = Gender.FEMALE)), new SpeciesFormEvolution(Species.MEOWSTIC, "", "female", 25, null, new SpeciesEvolutionCondition(p => p.gender === Gender.FEMALE, p => p.gender = Gender.FEMALE)),
@ -1005,8 +1005,8 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesEvolution(Species.COSMOEM, 23, null, null) new SpeciesEvolution(Species.COSMOEM, 23, null, null)
], ],
[Species.COSMOEM]: [ [Species.COSMOEM]: [
new SpeciesEvolution(Species.SOLGALEO, 53, EvolutionItem.SUN_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG), new SpeciesEvolution(Species.SOLGALEO, 1, EvolutionItem.SUN_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG),
new SpeciesEvolution(Species.LUNALA, 53, EvolutionItem.MOON_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG) new SpeciesEvolution(Species.LUNALA, 1, EvolutionItem.MOON_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG)
], ],
[Species.MELTAN]: [ [Species.MELTAN]: [
new SpeciesEvolution(Species.MELMETAL, 48, null, null) new SpeciesEvolution(Species.MELMETAL, 48, null, null)

View File

@ -3302,11 +3302,23 @@ export const tmSpecies: TmSpecies = {
Species.FERALIGATR, Species.FERALIGATR,
Species.SENTRET, Species.SENTRET,
Species.FURRET, Species.FURRET,
Species.HOOTHOOT,
Species.NOCTOWL, Species.NOCTOWL,
Species.LEDYBA,
Species.LEDIAN,
Species.SPINARAK,
Species.ARIADOS,
Species.CROBAT, Species.CROBAT,
Species.CHINCHOU, Species.CHINCHOU,
Species.LANTURN, Species.LANTURN,
Species.PICHU,
Species.CLEFFA,
Species.IGGLYBUFF, Species.IGGLYBUFF,
Species.TYROGUE,
Species.TOGEPI,
Species.TOGETIC,
Species.NATU,
Species.XATU,
Species.MAREEP, Species.MAREEP,
Species.FLAAFFY, Species.FLAAFFY,
Species.AMPHAROS, Species.AMPHAROS,
@ -3328,6 +3340,7 @@ export const tmSpecies: TmSpecies = {
Species.UMBREON, Species.UMBREON,
Species.MURKROW, Species.MURKROW,
Species.SLOWKING, Species.SLOWKING,
Species.MISDREAVUS,
Species.GIRAFARIG, Species.GIRAFARIG,
Species.PINECO, Species.PINECO,
Species.FORRETRESS, Species.FORRETRESS,
@ -3338,11 +3351,21 @@ export const tmSpecies: TmSpecies = {
Species.GRANBULL, Species.GRANBULL,
Species.QWILFISH, Species.QWILFISH,
Species.SCIZOR, Species.SCIZOR,
Species.SHUCKLE,
Species.HERACROSS, Species.HERACROSS,
Species.SNEASEL,
Species.TEDDIURSA, Species.TEDDIURSA,
Species.URSARING, Species.URSARING,
Species.SLUGMA,
Species.MAGCARGO,
Species.SWINUB, Species.SWINUB,
Species.PILOSWINE, Species.PILOSWINE,
Species.CORSOLA,
Species.REMORAID,
Species.OCTILLERY,
Species.DELIBIRD,
Species.MANTINE,
Species.SKARMORY,
Species.HOUNDOUR, Species.HOUNDOUR,
Species.HOUNDOOM, Species.HOUNDOOM,
Species.KINGDRA, Species.KINGDRA,
@ -3350,9 +3373,12 @@ export const tmSpecies: TmSpecies = {
Species.DONPHAN, Species.DONPHAN,
Species.PORYGON2, Species.PORYGON2,
Species.STANTLER, Species.STANTLER,
Species.TYROGUE,
Species.HITMONTOP, Species.HITMONTOP,
Species.SMOOCHUM,
Species.ELEKID, Species.ELEKID,
Species.MAGBY, Species.MAGBY,
Species.MILTANK,
Species.BLISSEY, Species.BLISSEY,
Species.RAIKOU, Species.RAIKOU,
Species.ENTEI, Species.ENTEI,
@ -3362,6 +3388,9 @@ export const tmSpecies: TmSpecies = {
Species.TYRANITAR, Species.TYRANITAR,
Species.LUGIA, Species.LUGIA,
Species.HO_OH, Species.HO_OH,
Species.CELEBI,
Species.TREECKO,
Species.GROVYLE,
Species.SCEPTILE, Species.SCEPTILE,
Species.TORCHIC, Species.TORCHIC,
Species.COMBUSKEN, Species.COMBUSKEN,
@ -3371,41 +3400,116 @@ export const tmSpecies: TmSpecies = {
Species.SWAMPERT, Species.SWAMPERT,
Species.POOCHYENA, Species.POOCHYENA,
Species.MIGHTYENA, Species.MIGHTYENA,
Species.ZIGZAGOON,
Species.LINOONE,
Species.BEAUTIFLY,
Species.DUSTOX,
Species.LOTAD, Species.LOTAD,
Species.LOMBRE, Species.LOMBRE,
Species.LUDICOLO, Species.LUDICOLO,
Species.SEEDOT, Species.SEEDOT,
Species.NUZLEAF, Species.NUZLEAF,
Species.SHIFTRY, Species.SHIFTRY,
Species.TAILLOW,
Species.SWELLOW,
Species.WINGULL,
Species.PELIPPER,
Species.RALTS,
Species.KIRLIA,
Species.GARDEVOIR,
Species.SURSKIT,
Species.MASQUERAIN,
Species.SHROOMISH,
Species.BRELOOM,
Species.VIGOROTH, Species.VIGOROTH,
Species.SLAKING, Species.SLAKING,
Species.NINCADA,
Species.NINJASK,
Species.SHEDINJA,
Species.WHISMUR,
Species.LOUDRED,
Species.EXPLOUD,
Species.MAKUHITA, Species.MAKUHITA,
Species.HARIYAMA, Species.HARIYAMA,
Species.AZURILL,
Species.NOSEPASS, Species.NOSEPASS,
Species.SKITTY,
Species.DELCATTY,
Species.SABLEYE,
Species.MAWILE,
Species.ARON,
Species.LAIRON,
Species.AGGRON,
Species.MEDITITE,
Species.MEDICHAM,
Species.ELECTRIKE,
Species.MANECTRIC,
Species.PLUSLE,
Species.MINUN,
Species.VOLBEAT, Species.VOLBEAT,
Species.ILLUMISE, Species.ILLUMISE,
Species.ROSELIA,
Species.GULPIN,
Species.SWALOT, Species.SWALOT,
Species.CARVANHA,
Species.SHARPEDO,
Species.WAILMER,
Species.WAILORD,
Species.NUMEL, Species.NUMEL,
Species.CAMERUPT, Species.CAMERUPT,
Species.TORKOAL, Species.TORKOAL,
Species.SPOINK,
Species.GRUMPIG,
Species.SPINDA,
Species.TRAPINCH,
Species.VIBRAVA,
Species.FLYGON, Species.FLYGON,
Species.CACNEA,
Species.CACTURNE,
Species.SWABLU,
Species.ALTARIA, Species.ALTARIA,
Species.ZANGOOSE, Species.ZANGOOSE,
Species.SEVIPER, Species.SEVIPER,
Species.LUNATONE,
Species.SOLROCK,
Species.BARBOACH, Species.BARBOACH,
Species.WHISCASH, Species.WHISCASH,
Species.CORPHISH, Species.CORPHISH,
Species.CRAWDAUNT, Species.CRAWDAUNT,
Species.BALTOY,
Species.CLAYDOL,
Species.LILEEP,
Species.CRADILY,
Species.ANORITH,
Species.ARMALDO,
Species.FEEBAS, Species.FEEBAS,
Species.MILOTIC, Species.MILOTIC,
Species.CASTFORM,
Species.KECLEON,
Species.SHUPPET,
Species.BANETTE,
Species.DUSKULL,
Species.DUSCLOPS,
Species.TROPIUS, Species.TROPIUS,
Species.CHIMECHO, Species.CHIMECHO,
Species.ABSOL,
Species.SNORUNT,
Species.GLALIE,
Species.SPHEAL,
Species.SEALEO,
Species.WALREIN,
Species.CLAMPERL,
Species.HUNTAIL,
Species.GOREBYSS,
Species.RELICANTH,
Species.LUVDISC,
Species.BAGON, Species.BAGON,
Species.SHELGON, Species.SHELGON,
Species.SALAMENCE, Species.SALAMENCE,
Species.METANG, Species.METANG,
Species.METAGROSS, Species.METAGROSS,
Species.REGIROCK, Species.REGIROCK,
Species.REGICE,
Species.REGISTEEL, Species.REGISTEEL,
Species.LATIAS, Species.LATIAS,
Species.LATIOS, Species.LATIOS,
@ -3413,6 +3517,7 @@ export const tmSpecies: TmSpecies = {
Species.GROUDON, Species.GROUDON,
Species.RAYQUAZA, Species.RAYQUAZA,
Species.JIRACHI, Species.JIRACHI,
Species.DEOXYS,
Species.TURTWIG, Species.TURTWIG,
Species.GROTLE, Species.GROTLE,
Species.TORTERRA, Species.TORTERRA,
@ -64246,12 +64351,16 @@ export const tmSpecies: TmSpecies = {
Species.BLOODMOON_URSALUNA, Species.BLOODMOON_URSALUNA,
], ],
[Moves.LIQUIDATION]: [ [Moves.LIQUIDATION]: [
Species.SQUIRTLE,
Species.WARTORTLE,
Species.BLASTOISE, Species.BLASTOISE,
Species.PSYDUCK, Species.PSYDUCK,
Species.GOLDUCK, Species.GOLDUCK,
Species.POLIWAG, Species.POLIWAG,
Species.POLIWHIRL, Species.POLIWHIRL,
Species.POLIWRATH, Species.POLIWRATH,
Species.TENTACOOL,
Species.TENTACRUEL,
Species.SLOWPOKE, Species.SLOWPOKE,
Species.SLOWBRO, Species.SLOWBRO,
Species.DEWGONG, Species.DEWGONG,
@ -64267,7 +64376,11 @@ export const tmSpecies: TmSpecies = {
Species.KABUTO, Species.KABUTO,
Species.KABUTOPS, Species.KABUTOPS,
Species.MEW, Species.MEW,
Species.TOTODILE,
Species.CROCONAW,
Species.FERALIGATR, Species.FERALIGATR,
Species.CHINCHOU,
Species.LANTURN,
Species.MARILL, Species.MARILL,
Species.AZUMARILL, Species.AZUMARILL,
Species.POLITOED, Species.POLITOED,
@ -64280,6 +64393,9 @@ export const tmSpecies: TmSpecies = {
Species.MANTINE, Species.MANTINE,
Species.KINGDRA, Species.KINGDRA,
Species.SUICUNE, Species.SUICUNE,
Species.LUGIA,
Species.MUDKIP,
Species.MARSHTOMP,
Species.SWAMPERT, Species.SWAMPERT,
Species.WINGULL, Species.WINGULL,
Species.PELIPPER, Species.PELIPPER,
@ -64296,6 +64412,8 @@ export const tmSpecies: TmSpecies = {
Species.WALREIN, Species.WALREIN,
Species.RELICANTH, Species.RELICANTH,
Species.LUVDISC, Species.LUVDISC,
Species.LATIAS,
Species.LATIOS,
Species.KYOGRE, Species.KYOGRE,
Species.PIPLUP, Species.PIPLUP,
Species.PRINPLUP, Species.PRINPLUP,
@ -64407,11 +64525,13 @@ export const tmSpecies: TmSpecies = {
Species.ONIX, Species.ONIX,
Species.HYPNO, Species.HYPNO,
Species.LICKITUNG, Species.LICKITUNG,
Species.RHYHORN,
Species.RHYDON, Species.RHYDON,
Species.LAPRAS, Species.LAPRAS,
Species.SNORLAX, Species.SNORLAX,
Species.DRAGONITE, Species.DRAGONITE,
Species.MEW, Species.MEW,
Species.MEGANIUM,
Species.SUDOWOODO, Species.SUDOWOODO,
Species.QUAGSIRE, Species.QUAGSIRE,
Species.FORRETRESS, Species.FORRETRESS,
@ -64445,6 +64565,8 @@ export const tmSpecies: TmSpecies = {
Species.REGISTEEL, Species.REGISTEEL,
Species.GROUDON, Species.GROUDON,
Species.TORTERRA, Species.TORTERRA,
Species.RAMPARDOS,
Species.BASTIODON,
Species.BRONZONG, Species.BRONZONG,
Species.HIPPOPOTAS, Species.HIPPOPOTAS,
Species.HIPPOWDON, Species.HIPPOWDON,
@ -64459,6 +64581,7 @@ export const tmSpecies: TmSpecies = {
Species.HEATRAN, Species.HEATRAN,
Species.REGIGIGAS, Species.REGIGIGAS,
Species.ARCEUS, Species.ARCEUS,
Species.EMBOAR,
Species.ROGGENROLA, Species.ROGGENROLA,
Species.BOLDORE, Species.BOLDORE,
Species.GIGALITH, Species.GIGALITH,
@ -64471,6 +64594,7 @@ export const tmSpecies: TmSpecies = {
Species.CUBCHOO, Species.CUBCHOO,
Species.BEARTIC, Species.BEARTIC,
Species.GOLURK, Species.GOLURK,
Species.COBALION,
Species.RESHIRAM, Species.RESHIRAM,
Species.ZEKROM, Species.ZEKROM,
Species.KYUREM, Species.KYUREM,

View File

@ -428,7 +428,7 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent {
moveAnim.bgSprite.setScale(1.25); moveAnim.bgSprite.setScale(1.25);
moveAnim.bgSprite.setAlpha(this.opacity / 255); moveAnim.bgSprite.setAlpha(this.opacity / 255);
scene.field.add(moveAnim.bgSprite); scene.field.add(moveAnim.bgSprite);
const fieldPokemon = scene.getNonSwitchedEnemyPokemon() || scene.getNonSwitchedPlayerPokemon(); const fieldPokemon = scene.getEnemyPokemon(false) ?? scene.getPlayerPokemon(false);
if (!isNullOrUndefined(priority)) { if (!isNullOrUndefined(priority)) {
scene.field.moveTo(moveAnim.bgSprite as Phaser.GameObjects.GameObject, priority); scene.field.moveTo(moveAnim.bgSprite as Phaser.GameObjects.GameObject, priority);
} else if (fieldPokemon?.isOnField()) { } else if (fieldPokemon?.isOnField()) {
@ -999,7 +999,7 @@ export abstract class BattleAnim {
const setSpritePriority = (priority: integer) => { const setSpritePriority = (priority: integer) => {
switch (priority) { switch (priority) {
case 0: case 0:
scene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, scene.getNonSwitchedEnemyPokemon() || scene.getNonSwitchedPlayerPokemon()!); // This bang assumes that if (the EnemyPokemon is undefined, then the PlayerPokemon function must return an object), correct assumption? scene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, scene.getEnemyPokemon(false) ?? scene.getPlayerPokemon(false)!); // TODO: is this bang correct?
break; break;
case 1: case 1:
scene.field.moveTo(moveSprite, scene.field.getAll().length - 1); scene.field.moveTo(moveSprite, scene.field.getAll().length - 1);

View File

@ -18,10 +18,9 @@ import Move, {
StatusCategoryOnAllyAttr StatusCategoryOnAllyAttr
} from "#app/data/move"; } from "#app/data/move";
import { SpeciesFormChangeManualTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangeManualTrigger } from "#app/data/pokemon-forms";
import { StatusEffect } from "#app/data/status-effect"; import { getStatusEffectHealText } from "#app/data/status-effect";
import { TerrainType } from "#app/data/terrain"; import { TerrainType } from "#app/data/terrain";
import { Type } from "#app/data/type"; import { Type } from "#enums/type";
import { WeatherType } from "#app/data/weather";
import Pokemon, { HitResult, MoveResult } from "#app/field/pokemon"; import Pokemon, { HitResult, MoveResult } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { CommonAnimPhase } from "#app/phases/common-anim-phase"; import { CommonAnimPhase } from "#app/phases/common-anim-phase";
@ -38,6 +37,8 @@ import { Moves } from "#enums/moves";
import { PokemonAnimType } from "#enums/pokemon-anim-type"; import { PokemonAnimType } from "#enums/pokemon-anim-type";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { EFFECTIVE_STATS, getStatKey, Stat, type BattleStat, type EffectiveStat } from "#enums/stat"; import { EFFECTIVE_STATS, getStatKey, Stat, type BattleStat, type EffectiveStat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
import { WeatherType } from "#enums/weather-type";
export enum BattlerTagLapseType { export enum BattlerTagLapseType {
FAINT, FAINT,
@ -909,11 +910,15 @@ export class FrenzyTag extends BattlerTag {
} }
} }
export class EncoreTag extends BattlerTag { /**
* Applies the effects of the move Encore onto the target Pokemon
* Encore forces the target Pokemon to use its most-recent move for 3 turns
*/
export class EncoreTag extends MoveRestrictionBattlerTag {
public moveId: Moves; public moveId: Moves;
constructor(sourceId: number) { constructor(sourceId: number) {
super(BattlerTagType.ENCORE, BattlerTagLapseType.AFTER_MOVE, 3, Moves.ENCORE, sourceId); super(BattlerTagType.ENCORE, [ BattlerTagLapseType.CUSTOM, BattlerTagLapseType.AFTER_MOVE ], 3, Moves.ENCORE, sourceId);
} }
/** /**
@ -969,6 +974,39 @@ export class EncoreTag extends BattlerTag {
} }
} }
/**
* If the encored move has run out of PP, Encore ends early. Otherwise, Encore lapses based on the AFTER_MOVE battler tag lapse type.
* @returns `true` to persist | `false` to end and be removed
*/
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
if (lapseType === BattlerTagLapseType.CUSTOM) {
const encoredMove = pokemon.getMoveset().find(m => m?.moveId === this.moveId);
if (encoredMove && encoredMove?.getPpRatio() > 0) {
return true;
}
return false;
} else {
return super.lapse(pokemon, lapseType);
}
}
/**
* Checks if the move matches the moveId stored within the tag and returns a boolean value
* @param move {@linkcode Moves} the move selected
* @param user N/A
* @returns `true` if the move does not match with the moveId stored and as a result, restricted
*/
override isMoveRestricted(move: Moves, _user?: Pokemon): boolean {
if (move !== this.moveId) {
return true;
}
return false;
}
override selectionDeniedText(_pokemon: Pokemon, move: Moves): string {
return i18next.t("battle:moveDisabled", { moveName: allMoves[move].name });
}
onRemove(pokemon: Pokemon): void { onRemove(pokemon: Pokemon): void {
super.onRemove(pokemon); super.onRemove(pokemon);
@ -1464,9 +1502,14 @@ export class ContactBurnProtectedTag extends DamageProtectedTag {
} }
} }
/**
* `BattlerTag` class for effects that cause the affected Pokemon to survive lethal attacks at 1 HP.
* Used for {@link https://bulbapedia.bulbagarden.net/wiki/Endure_(move) | Endure} and
* Endure Tokens.
*/
export class EnduringTag extends BattlerTag { export class EnduringTag extends BattlerTag {
constructor(sourceMove: Moves) { constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType, sourceMove: Moves) {
super(BattlerTagType.ENDURING, BattlerTagLapseType.TURN_END, 0, sourceMove); super(tagType, lapseType, 0, sourceMove);
} }
onAdd(pokemon: Pokemon): void { onAdd(pokemon: Pokemon): void {
@ -1573,6 +1616,22 @@ export class AbilityBattlerTag extends BattlerTag {
} }
} }
/**
* Tag used by Unburden to double speed
* @extends AbilityBattlerTag
*/
export class UnburdenTag extends AbilityBattlerTag {
constructor() {
super(BattlerTagType.UNBURDEN, Abilities.UNBURDEN, BattlerTagLapseType.CUSTOM, 1);
}
onAdd(pokemon: Pokemon): void {
super.onAdd(pokemon);
}
onRemove(pokemon: Pokemon): void {
super.onRemove(pokemon);
}
}
export class TruantTag extends AbilityBattlerTag { export class TruantTag extends AbilityBattlerTag {
constructor() { constructor() {
super(BattlerTagType.TRUANT, Abilities.TRUANT, BattlerTagLapseType.MOVE, 1); super(BattlerTagType.TRUANT, Abilities.TRUANT, BattlerTagLapseType.MOVE, 1);
@ -1755,8 +1814,8 @@ export class TypeImmuneTag extends BattlerTag {
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Telekinesis_(move) | Moves.TELEKINESIS} * @see {@link https://bulbapedia.bulbagarden.net/wiki/Telekinesis_(move) | Moves.TELEKINESIS}
*/ */
export class FloatingTag extends TypeImmuneTag { export class FloatingTag extends TypeImmuneTag {
constructor(tagType: BattlerTagType, sourceMove: Moves) { constructor(tagType: BattlerTagType, sourceMove: Moves, turnCount: number) {
super(tagType, sourceMove, Type.GROUND, 5); super(tagType, sourceMove, Type.GROUND, turnCount);
} }
onAdd(pokemon: Pokemon): void { onAdd(pokemon: Pokemon): void {
@ -2075,6 +2134,42 @@ export class IceFaceBlockDamageTag extends FormBlockDamageTag {
} }
} }
/**
* Battler tag indicating a Tatsugiri with {@link https://bulbapedia.bulbagarden.net/wiki/Commander_(Ability) | Commander}
* has entered the tagged Pokemon's mouth.
*/
export class CommandedTag extends BattlerTag {
private _tatsugiriFormKey: string;
constructor(sourceId: number) {
super(BattlerTagType.COMMANDED, BattlerTagLapseType.CUSTOM, 0, Moves.NONE, sourceId);
}
public get tatsugiriFormKey(): string {
return this._tatsugiriFormKey;
}
/** Caches the Tatsugiri's form key and sharply boosts the tagged Pokemon's stats */
override onAdd(pokemon: Pokemon): void {
this._tatsugiriFormKey = this.getSourcePokemon(pokemon.scene)?.getFormKey() ?? "curly";
pokemon.scene.unshiftPhase(new StatStageChangePhase(
pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2
));
}
/** Triggers an {@linkcode PokemonAnimType | animation} of the tagged Pokemon "spitting out" Tatsugiri */
override onRemove(pokemon: Pokemon): void {
if (this.getSourcePokemon(pokemon.scene)?.isActive(true)) {
pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.COMMANDER_REMOVE);
}
}
override loadTag(source: BattlerTag | any): void {
super.loadTag(source);
this._tatsugiriFormKey = source._tatsugiriFormKey;
}
}
/** /**
* Battler tag enabling the Stockpile mechanic. This tag handles: * Battler tag enabling the Stockpile mechanic. This tag handles:
* - Stack tracking, including max limit enforcement (which is replicated in Stockpile for redundancy). * - Stack tracking, including max limit enforcement (which is replicated in Stockpile for redundancy).
@ -2313,7 +2408,7 @@ export class HealBlockTag extends MoveRestrictionBattlerTag {
} }
/** /**
* Uses DisabledTag's selectionDeniedText() message * Uses its own unique selectionDeniedText() message
*/ */
override selectionDeniedText(pokemon: Pokemon, move: Moves): string { override selectionDeniedText(pokemon: Pokemon, move: Moves): string {
return i18next.t("battle:moveDisabledHealBlock", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[move].name, healBlockName: allMoves[Moves.HEAL_BLOCK].name }); return i18next.t("battle:moveDisabledHealBlock", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[move].name, healBlockName: allMoves[Moves.HEAL_BLOCK].name });
@ -2480,7 +2575,10 @@ export class SubstituteTag extends BattlerTag {
onHit(pokemon: Pokemon): void { onHit(pokemon: Pokemon): void {
const moveEffectPhase = pokemon.scene.getCurrentPhase(); const moveEffectPhase = pokemon.scene.getCurrentPhase();
if (moveEffectPhase instanceof MoveEffectPhase) { if (moveEffectPhase instanceof MoveEffectPhase) {
const attacker = moveEffectPhase.getUserPokemon()!; const attacker = moveEffectPhase.getUserPokemon();
if (!attacker) {
return;
}
const move = moveEffectPhase.move.getMove(); const move = moveEffectPhase.move.getMove();
const firstHit = (attacker.turnData.hitCount === attacker.turnData.hitsLeft); const firstHit = (attacker.turnData.hitCount === attacker.turnData.hitsLeft);
@ -2777,6 +2875,67 @@ export class PowerTrickTag extends BattlerTag {
} }
} }
/**
* Tag associated with the move Grudge.
* If this tag is active when the bearer faints from an opponent's move, the tag reduces that move's PP to 0.
* Otherwise, it lapses when the bearer makes another move.
*/
export class GrudgeTag extends BattlerTag {
constructor() {
super(BattlerTagType.GRUDGE, [ BattlerTagLapseType.CUSTOM, BattlerTagLapseType.PRE_MOVE ], 1, Moves.GRUDGE);
}
onAdd(pokemon: Pokemon) {
super.onAdd(pokemon);
pokemon.scene.queueMessage(i18next.t("battlerTags:grudgeOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
}
/**
* Activates Grudge's special effect on the attacking Pokemon and lapses the tag.
* @param pokemon
* @param lapseType
* @param sourcePokemon {@linkcode Pokemon} the source of the move that fainted the tag's bearer
* @returns `false` if Grudge activates its effect or lapses
*/
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType, sourcePokemon?: Pokemon): boolean {
if (lapseType === BattlerTagLapseType.CUSTOM && sourcePokemon) {
if (sourcePokemon.isActive() && pokemon.isOpponent(sourcePokemon)) {
const lastMove = pokemon.turnData.attacksReceived[0];
const lastMoveData = sourcePokemon.getMoveset().find(m => m?.moveId === lastMove.move);
if (lastMoveData && lastMove.move !== Moves.STRUGGLE) {
lastMoveData.ppUsed = lastMoveData.getMovePp();
pokemon.scene.queueMessage(i18next.t("battlerTags:grudgeLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: lastMoveData.getName() }));
}
}
return false;
} else {
return super.lapse(pokemon, lapseType);
}
}
}
/**
* Tag used to heal the user of Psycho Shift of its status effect if Psycho Shift succeeds in transferring its status effect to the target Pokemon
*/
export class PsychoShiftTag extends BattlerTag {
constructor() {
super(BattlerTagType.PSYCHO_SHIFT, BattlerTagLapseType.AFTER_MOVE, 1, Moves.PSYCHO_SHIFT);
}
/**
* Heals Psycho Shift's user of its status effect after it uses a move
* @returns `false` to expire the tag immediately
*/
override lapse(pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean {
if (pokemon.status && pokemon.isActive(true)) {
pokemon.scene.queueMessage(getStatusEffectHealText(pokemon.status.effect, getPokemonNameWithAffix(pokemon)));
pokemon.resetStatus();
pokemon.updateInfo();
}
return 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
@ -2855,7 +3014,9 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
case BattlerTagType.BURNING_BULWARK: case BattlerTagType.BURNING_BULWARK:
return new ContactBurnProtectedTag(sourceMove); return new ContactBurnProtectedTag(sourceMove);
case BattlerTagType.ENDURING: case BattlerTagType.ENDURING:
return new EnduringTag(sourceMove); return new EnduringTag(tagType, BattlerTagLapseType.TURN_END, sourceMove);
case BattlerTagType.ENDURE_TOKEN:
return new EnduringTag(tagType, BattlerTagLapseType.AFTER_HIT, sourceMove);
case BattlerTagType.STURDY: case BattlerTagType.STURDY:
return new SturdyTag(sourceMove); return new SturdyTag(sourceMove);
case BattlerTagType.PERISH_SONG: case BattlerTagType.PERISH_SONG:
@ -2904,7 +3065,7 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
case BattlerTagType.CHARGED: case BattlerTagType.CHARGED:
return new TypeBoostTag(tagType, sourceMove, Type.ELECTRIC, 2, true); return new TypeBoostTag(tagType, sourceMove, Type.ELECTRIC, 2, true);
case BattlerTagType.FLOATING: case BattlerTagType.FLOATING:
return new FloatingTag(tagType, sourceMove); return new FloatingTag(tagType, sourceMove, turnCount);
case BattlerTagType.MINIMIZED: case BattlerTagType.MINIMIZED:
return new MinimizeTag(); return new MinimizeTag();
case BattlerTagType.DESTINY_BOND: case BattlerTagType.DESTINY_BOND:
@ -2913,6 +3074,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new IceFaceBlockDamageTag(tagType); return new IceFaceBlockDamageTag(tagType);
case BattlerTagType.DISGUISE: case BattlerTagType.DISGUISE:
return new FormBlockDamageTag(tagType); return new FormBlockDamageTag(tagType);
case BattlerTagType.COMMANDED:
return new CommandedTag(sourceId);
case BattlerTagType.STOCKPILING: case BattlerTagType.STOCKPILING:
return new StockpilingTag(sourceMove); return new StockpilingTag(sourceMove);
case BattlerTagType.OCTOLOCK: case BattlerTagType.OCTOLOCK:
@ -2934,6 +3097,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new ThroatChoppedTag(); return new ThroatChoppedTag();
case BattlerTagType.GORILLA_TACTICS: case BattlerTagType.GORILLA_TACTICS:
return new GorillaTacticsTag(); return new GorillaTacticsTag();
case BattlerTagType.UNBURDEN:
return new UnburdenTag();
case BattlerTagType.SUBSTITUTE: case BattlerTagType.SUBSTITUTE:
return new SubstituteTag(sourceMove, sourceId); return new SubstituteTag(sourceMove, sourceId);
case BattlerTagType.AUTOTOMIZED: case BattlerTagType.AUTOTOMIZED:
@ -2954,6 +3119,10 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new TelekinesisTag(sourceMove); return new TelekinesisTag(sourceMove);
case BattlerTagType.POWER_TRICK: case BattlerTagType.POWER_TRICK:
return new PowerTrickTag(sourceMove, sourceId); return new PowerTrickTag(sourceMove, sourceId);
case BattlerTagType.GRUDGE:
return new GrudgeTag();
case BattlerTagType.PSYCHO_SHIFT:
return new PsychoShiftTag();
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

@ -2,7 +2,7 @@ import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { HitResult } from "../field/pokemon"; import Pokemon, { HitResult } from "../field/pokemon";
import { getStatusEffectHealText } from "./status-effect"; import { getStatusEffectHealText } from "./status-effect";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { DoubleBerryEffectAbAttr, ReduceBerryUseThresholdAbAttr, applyAbAttrs } from "./ability"; import { DoubleBerryEffectAbAttr, PostItemLostAbAttr, ReduceBerryUseThresholdAbAttr, applyAbAttrs, applyPostItemLostAbAttrs } from "./ability";
import i18next from "i18next"; import i18next from "i18next";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
@ -61,13 +61,13 @@ export function getBerryPredicate(berryType: BerryType): BerryPredicate {
} }
} }
export type BerryEffectFunc = (pokemon: Pokemon) => void; export type BerryEffectFunc = (pokemon: Pokemon, berryOwner?: Pokemon) => void;
export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
switch (berryType) { switch (berryType) {
case BerryType.SITRUS: case BerryType.SITRUS:
case BerryType.ENIGMA: case BerryType.ENIGMA:
return (pokemon: Pokemon) => { return (pokemon: Pokemon, berryOwner?: Pokemon) => {
if (pokemon.battleData) { if (pokemon.battleData) {
pokemon.battleData.berriesEaten.push(berryType); pokemon.battleData.berriesEaten.push(berryType);
} }
@ -75,9 +75,10 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, hpHealed); applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, hpHealed);
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(),
hpHealed.value, i18next.t("battle:hpHealBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), berryName: getBerryName(berryType) }), true)); hpHealed.value, i18next.t("battle:hpHealBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), berryName: getBerryName(berryType) }), true));
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
}; };
case BerryType.LUM: case BerryType.LUM:
return (pokemon: Pokemon) => { return (pokemon: Pokemon, berryOwner?: Pokemon) => {
if (pokemon.battleData) { if (pokemon.battleData) {
pokemon.battleData.berriesEaten.push(berryType); pokemon.battleData.berriesEaten.push(berryType);
} }
@ -86,13 +87,14 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
} }
pokemon.resetStatus(true, true); pokemon.resetStatus(true, true);
pokemon.updateInfo(); pokemon.updateInfo();
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
}; };
case BerryType.LIECHI: case BerryType.LIECHI:
case BerryType.GANLON: case BerryType.GANLON:
case BerryType.PETAYA: case BerryType.PETAYA:
case BerryType.APICOT: case BerryType.APICOT:
case BerryType.SALAC: case BerryType.SALAC:
return (pokemon: Pokemon) => { return (pokemon: Pokemon, berryOwner?: Pokemon) => {
if (pokemon.battleData) { if (pokemon.battleData) {
pokemon.battleData.berriesEaten.push(berryType); pokemon.battleData.berriesEaten.push(berryType);
} }
@ -101,16 +103,18 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
const statStages = new Utils.NumberHolder(1); const statStages = new Utils.NumberHolder(1);
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statStages); applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statStages);
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], statStages.value)); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], statStages.value));
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
}; };
case BerryType.LANSAT: case BerryType.LANSAT:
return (pokemon: Pokemon) => { return (pokemon: Pokemon, berryOwner?: Pokemon) => {
if (pokemon.battleData) { if (pokemon.battleData) {
pokemon.battleData.berriesEaten.push(berryType); pokemon.battleData.berriesEaten.push(berryType);
} }
pokemon.addTag(BattlerTagType.CRIT_BOOST); pokemon.addTag(BattlerTagType.CRIT_BOOST);
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
}; };
case BerryType.STARF: case BerryType.STARF:
return (pokemon: Pokemon) => { return (pokemon: Pokemon, berryOwner?: Pokemon) => {
if (pokemon.battleData) { if (pokemon.battleData) {
pokemon.battleData.berriesEaten.push(berryType); pokemon.battleData.berriesEaten.push(berryType);
} }
@ -118,9 +122,10 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
const stages = new Utils.NumberHolder(2); const stages = new Utils.NumberHolder(2);
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, stages); applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, stages);
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ randStat ], stages.value)); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ randStat ], stages.value));
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
}; };
case BerryType.LEPPA: case BerryType.LEPPA:
return (pokemon: Pokemon) => { return (pokemon: Pokemon, berryOwner?: Pokemon) => {
if (pokemon.battleData) { if (pokemon.battleData) {
pokemon.battleData.berriesEaten.push(berryType); pokemon.battleData.berriesEaten.push(berryType);
} }
@ -128,6 +133,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
if (ppRestoreMove !== undefined) { if (ppRestoreMove !== undefined) {
ppRestoreMove!.ppUsed = Math.max(ppRestoreMove!.ppUsed - 10, 0); ppRestoreMove!.ppUsed = Math.max(ppRestoreMove!.ppUsed - 10, 0);
pokemon.scene.queueMessage(i18next.t("battle:ppHealBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: ppRestoreMove!.getName(), berryName: getBerryName(berryType) })); pokemon.scene.queueMessage(i18next.t("battle:ppHealBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: ppRestoreMove!.getName(), berryName: getBerryName(berryType) }));
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
} }
}; };
} }

View File

@ -7,11 +7,11 @@ import Pokemon, { PokemonMove } from "#app/field/pokemon";
import { BattleType, FixedBattleConfig } from "#app/battle"; import { BattleType, FixedBattleConfig } from "#app/battle";
import Trainer, { TrainerVariant } from "#app/field/trainer"; import Trainer, { TrainerVariant } from "#app/field/trainer";
import { GameMode } from "#app/game-mode"; import { GameMode } from "#app/game-mode";
import { Type } from "#app/data/type"; import { Type } from "#enums/type";
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { Nature } from "#app/data/nature"; import { Nature } from "#enums/nature";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { TypeColor, TypeShadow } from "#enums/color"; import { TypeColor, TypeShadow } from "#enums/color";
import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions";
@ -653,7 +653,7 @@ export class FreshStartChallenge extends Challenge {
pokemon.shiny = false; // Not shiny pokemon.shiny = false; // Not shiny
pokemon.variant = 0; // Not shiny pokemon.variant = 0; // Not shiny
pokemon.formIndex = 0; // Froakie should be base form pokemon.formIndex = 0; // Froakie should be base form
pokemon.ivs = [ 10, 10, 10, 10, 10, 10 ]; // Default IVs of 10 for all stats pokemon.ivs = [ 15, 15, 15, 15, 15, 15 ]; // Default IVs of 15 for all stats (Updated to 15 from 10 in 1.2.0)
return true; return true;
} }

View File

@ -1,5 +1,5 @@
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Type } from "#app/data/type"; import { Type } from "#enums/type";
import { isNullOrUndefined } from "#app/utils"; import { isNullOrUndefined } from "#app/utils";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";

View File

@ -6,6 +6,7 @@ import { Starter } from "#app/ui/starter-select-ui-handler";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
import PokemonSpecies, { PokemonSpeciesForm, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import PokemonSpecies, { PokemonSpeciesForm, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species";
import { speciesStarterCosts } from "#app/data/balance/starters"; import { speciesStarterCosts } from "#app/data/balance/starters";
import { pokerogueApi } from "#app/plugins/api/pokerogue-api";
export interface DailyRunConfig { export interface DailyRunConfig {
seed: integer; seed: integer;
@ -14,14 +15,9 @@ export interface DailyRunConfig {
export function fetchDailyRunSeed(): Promise<string | null> { export function fetchDailyRunSeed(): Promise<string | null> {
return new Promise<string | null>((resolve, reject) => { return new Promise<string | null>((resolve, reject) => {
Utils.apiFetch("daily/seed").then(response => { pokerogueApi.daily.getSeed().then(dailySeed => {
if (!response.ok) { resolve(dailySeed);
resolve(null); });
return;
}
return response.text();
}).then(seed => resolve(seed ?? null))
.catch(err => reject(err));
}); });
} }

View File

@ -1,14 +1,15 @@
import { ChargeAnim, initMoveAnim, loadMoveAnimAssets, MoveChargeAnim } from "./battle-anims"; import { ChargeAnim, initMoveAnim, loadMoveAnimAssets, MoveChargeAnim } from "./battle-anims";
import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, SubstituteTag, TrappedTag, TypeBoostTag } from "./battler-tags"; import { CommandedTag, EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, SubstituteTag, TrappedTag, TypeBoostTag } from "./battler-tags";
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon"; import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon";
import { getNonVolatileStatusEffects, getStatusEffectHealText, isNonVolatileStatusEffect, StatusEffect } from "./status-effect"; import { getNonVolatileStatusEffects, getStatusEffectHealText, isNonVolatileStatusEffect } from "./status-effect";
import { getTypeDamageMultiplier, Type } from "./type"; import { getTypeDamageMultiplier } from "./type";
import { Type } from "#enums/type";
import { Constructor, NumberHolder } from "#app/utils"; import { Constructor, NumberHolder } from "#app/utils";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { WeatherType } from "./weather"; import { WeatherType } from "#enums/weather-type";
import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag";
import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPostItemLostAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ChangeMovePriorityAbAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, PostDamageForceSwitchAbAttr, PostItemLostAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability";
import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier"; import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier";
import { BattlerIndex, BattleType } from "../battle"; import { BattlerIndex, BattleType } from "../battle";
import { TerrainType } from "./terrain"; import { TerrainType } from "./terrain";
@ -38,6 +39,7 @@ import { SpeciesFormChangeRevertWeatherFormTrigger } from "./pokemon-forms";
import { GameMode } from "#app/game-mode"; import { GameMode } from "#app/game-mode";
import { applyChallenges, ChallengeType } from "./challenge"; import { applyChallenges, ChallengeType } from "./challenge";
import { SwitchType } from "#enums/switch-type"; import { SwitchType } from "#enums/switch-type";
import { StatusEffect } from "enums/status-effect";
export enum MoveCategory { export enum MoveCategory {
PHYSICAL, PHYSICAL,
@ -666,12 +668,12 @@ export default class Move implements Localizable {
} }
/** /**
* Sees if, given the target pokemon, a move fails on it (by looking at each {@linkcode MoveAttr} of this move * Sees if a move has a custom failure text (by looking at each {@linkcode MoveAttr} of this move)
* @param user {@linkcode Pokemon} using the move * @param user {@linkcode Pokemon} using the move
* @param target {@linkcode Pokemon} receiving the move * @param target {@linkcode Pokemon} receiving the move
* @param move {@linkcode Move} using the move * @param move {@linkcode Move} using the move
* @param cancelled {@linkcode Utils.BooleanHolder} to hold boolean value * @param cancelled {@linkcode Utils.BooleanHolder} to hold boolean value
* @returns string of the failed text, or null * @returns string of the custom failure text, or `null` if it uses the default text ("But it failed!")
*/ */
getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null {
for (const attr of this.attrs) { for (const attr of this.attrs) {
@ -714,6 +716,10 @@ export default class Move implements Localizable {
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
let score = 0; let score = 0;
if (target.getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon(target.scene) === target) {
return 20 * (target.isPlayer() === user.isPlayer() ? -1 : 1); // always -20 with how the AI handles this score
}
for (const attr of this.attrs) { for (const attr of this.attrs) {
// conditionals to check if the move is self targeting (if so then you are applying the move to yourself, not the target) // conditionals to check if the move is self targeting (if so then you are applying the move to yourself, not the target)
score += attr.getTargetBenefitScore(user, !attr.selfTarget ? target : user, move) * (target !== user && attr.selfTarget ? -1 : 1); score += attr.getTargetBenefitScore(user, !attr.selfTarget ? target : user, move) * (target !== user && attr.selfTarget ? -1 : 1);
@ -812,8 +818,6 @@ export default class Move implements Localizable {
applyMoveAttrs(VariablePowerAttr, source, target, this, power); applyMoveAttrs(VariablePowerAttr, source, target, this, 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, simulated, 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);
@ -825,6 +829,54 @@ export default class Move implements Localizable {
return power.value; return power.value;
} }
getPriority(user: Pokemon, simulated: boolean = true) {
const priority = new Utils.NumberHolder(this.priority);
applyMoveAttrs(IncrementMovePriorityAttr, user, null, this, priority);
applyAbAttrs(ChangeMovePriorityAbAttr, user, null, simulated, this, priority);
return priority.value;
}
/**
* Returns `true` if this move can be given additional strikes
* by enhancing effects.
* Currently used for {@link https://bulbapedia.bulbagarden.net/wiki/Parental_Bond_(Ability) | Parental Bond}
* and {@linkcode PokemonMultiHitModifier | Multi-Lens}.
* @param user The {@linkcode Pokemon} using the move
* @param restrictSpread `true` if the enhancing effect
* should not affect multi-target moves (default `false`)
*/
canBeMultiStrikeEnhanced(user: Pokemon, restrictSpread: boolean = false): boolean {
// Multi-strike enhancers...
// ...cannot enhance moves that hit multiple targets
const { targets, multiple } = getMoveTargets(user, this.id);
const isMultiTarget = multiple && targets.length > 1;
// ...cannot enhance multi-hit or sacrificial moves
const exceptAttrs: Constructor<MoveAttr>[] = [
MultiHitAttr,
SacrificialAttr,
SacrificialAttrOnHit
];
// ...and cannot enhance these specific moves.
const exceptMoves: Moves[] = [
Moves.FLING,
Moves.UPROAR,
Moves.ROLLOUT,
Moves.ICE_BALL,
Moves.ENDEAVOR
];
return (!restrictSpread || !isMultiTarget)
&& !this.isChargingMove()
&& !exceptAttrs.some(attr => this.hasAttr(attr))
&& !exceptMoves.some(id => this.id === id)
&& this.category !== MoveCategory.STATUS;
}
} }
export class AttackMove extends Move { export class AttackMove extends Move {
@ -845,7 +897,7 @@ export class AttackMove extends Move {
let attackScore = 0; let attackScore = 0;
const effectiveness = target.getAttackTypeEffectiveness(this.type, user); const effectiveness = target.getAttackTypeEffectiveness(this.type, user, undefined, undefined, this);
attackScore = Math.pow(effectiveness - 1, 2) * effectiveness < 1 ? -2 : 2; attackScore = Math.pow(effectiveness - 1, 2) * effectiveness < 1 ? -2 : 2;
if (attackScore) { if (attackScore) {
if (this.category === MoveCategory.PHYSICAL) { if (this.category === MoveCategory.PHYSICAL) {
@ -1474,8 +1526,8 @@ export class RecoilAttr extends MoveEffectAttr {
return false; return false;
} }
const damageValue = (!this.useHp ? user.turnData.damageDealt : user.getMaxHp()) * this.damageRatio; const damageValue = (!this.useHp ? user.turnData.totalDamageDealt : user.getMaxHp()) * this.damageRatio;
const minValue = user.turnData.damageDealt ? 1 : 0; const minValue = user.turnData.totalDamageDealt ? 1 : 0;
const recoilDamage = Utils.toDmgValue(damageValue, minValue); const recoilDamage = Utils.toDmgValue(damageValue, minValue);
if (!recoilDamage) { if (!recoilDamage) {
return false; return false;
@ -1743,7 +1795,7 @@ export class PartyStatusCureAttr extends MoveEffectAttr {
if (!this.canApply(user, target, move, args)) { if (!this.canApply(user, target, move, args)) {
return false; return false;
} }
const partyPokemon = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty(); const partyPokemon = user.isPlayer() ? user.scene.getPlayerParty() : user.scene.getEnemyParty();
partyPokemon.forEach(p => this.cureStatus(p, user.id)); partyPokemon.forEach(p => this.cureStatus(p, user.id));
if (this.message) { if (this.message) {
@ -1815,7 +1867,7 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr {
} }
// We don't know which party member will be chosen, so pick the highest max HP in the party // We don't know which party member will be chosen, so pick the highest max HP in the party
const maxPartyMemberHp = user.scene.getParty().map(p => p.getMaxHp()).reduce((maxHp: integer, hp: integer) => Math.max(hp, maxHp), 0); const maxPartyMemberHp = user.scene.getPlayerParty().map(p => p.getMaxHp()).reduce((maxHp: integer, hp: integer) => Math.max(hp, maxHp), 0);
user.scene.pushPhase(new PokemonHealPhase(user.scene, user.getBattlerIndex(), user.scene.pushPhase(new PokemonHealPhase(user.scene, user.getBattlerIndex(),
maxPartyMemberHp, i18next.t("moveTriggers:sacrificialFullRestore", { pokemonName: getPokemonNameWithAffix(user) }), true, false, false, true), true); maxPartyMemberHp, i18next.t("moveTriggers:sacrificialFullRestore", { pokemonName: getPokemonNameWithAffix(user) }), true, false, false, true), true);
@ -1828,7 +1880,7 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr {
} }
getCondition(): MoveConditionFunc { getCondition(): MoveConditionFunc {
return (user, target, move) => user.scene.getParty().filter(p => p.isActive()).length > user.scene.currentBattle.getBattlerCount(); return (user, _target, _move) => user.scene.getPlayerParty().filter(p => p.isActive()).length > user.scene.currentBattle.getBattlerCount();
} }
} }
@ -2006,7 +2058,7 @@ export class HitHealAttr extends MoveEffectAttr {
message = i18next.t("battle:drainMessage", { pokemonName: getPokemonNameWithAffix(target) }); message = i18next.t("battle:drainMessage", { pokemonName: getPokemonNameWithAffix(target) });
} else { } else {
// Default healing formula used by draining moves like Absorb, Draining Kiss, Bitter Blade, etc. // Default healing formula used by draining moves like Absorb, Draining Kiss, Bitter Blade, etc.
healAmount = Utils.toDmgValue(user.turnData.currDamageDealt * this.healRatio); healAmount = Utils.toDmgValue(user.turnData.singleHitDamageDealt * this.healRatio);
message = i18next.t("battle:regainHealth", { pokemonName: getPokemonNameWithAffix(user) }); message = i18next.t("battle:regainHealth", { pokemonName: getPokemonNameWithAffix(user) });
} }
if (reverseDrain) { if (reverseDrain) {
@ -2158,7 +2210,7 @@ export class MultiHitAttr extends MoveAttr {
case MultiHitType._10: case MultiHitType._10:
return 10; return 10;
case MultiHitType.BEAT_UP: case MultiHitType.BEAT_UP:
const party = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty(); const party = user.isPlayer() ? user.scene.getPlayerParty() : user.scene.getEnemyParty();
// No status means the ally pokemon can contribute to Beat Up // No status means the ally pokemon can contribute to Beat Up
return party.reduce((total, pokemon) => { return party.reduce((total, pokemon) => {
return total + (pokemon.id === user.id ? 1 : pokemon?.status && pokemon.status.effect !== StatusEffect.NONE ? 0 : 1); return total + (pokemon.id === user.id ? 1 : pokemon?.status && pokemon.status.effect !== StatusEffect.NONE ? 0 : 1);
@ -2266,24 +2318,26 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
super(false, { trigger: MoveEffectTrigger.HIT }); super(false, { trigger: MoveEffectTrigger.HIT });
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { /**
* Applies the effect of Psycho Shift to its target
* Psycho Shift takes the user's status effect and passes it onto the target. The user is then healed after the move has been successfully executed.
* @returns `true` if Psycho Shift's effect is able to be applied to the target
*/
apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(Abilities.COMATOSE) ? StatusEffect.SLEEP : undefined); const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(Abilities.COMATOSE) ? StatusEffect.SLEEP : undefined);
if (target.status) { if (target.status) {
return false; return false;
} else { } else {
const canSetStatus = target.canSetStatus(statusToApply, true, false, user); const canSetStatus = target.canSetStatus(statusToApply, true, false, user);
const trySetStatus = canSetStatus ? target.trySetStatus(statusToApply, true, user) : false;
if (canSetStatus) { if (trySetStatus && user.status) {
if (user.status) { // PsychoShiftTag is added to the user if move succeeds so that the user is healed of its status effect after its move
user.scene.queueMessage(getStatusEffectHealText(user.status.effect, getPokemonNameWithAffix(user))); user.addTag(BattlerTagType.PSYCHO_SHIFT);
}
user.resetStatus();
user.updateInfo();
target.trySetStatus(statusToApply, true, user);
} }
return canSetStatus; return trySetStatus;
} }
} }
@ -2400,9 +2454,10 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
const removedItem = heldItems[user.randSeedInt(heldItems.length)]; const removedItem = heldItems[user.randSeedInt(heldItems.length)];
// Decrease item amount and update icon // Decrease item amount and update icon
!--removedItem.stackCount; target.loseHeldItem(removedItem);
target.scene.updateModifiers(target.isPlayer()); target.scene.updateModifiers(target.isPlayer());
if (this.berriesOnly) { if (this.berriesOnly) {
user.scene.queueMessage(i18next.t("moveTriggers:incineratedItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name })); user.scene.queueMessage(i18next.t("moveTriggers:incineratedItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name }));
} else { } else {
@ -2470,16 +2525,14 @@ export class EatBerryAttr extends MoveEffectAttr {
} }
reduceBerryModifier(target: Pokemon) { reduceBerryModifier(target: Pokemon) {
if (this.chosenBerry?.stackCount === 1) { if (this.chosenBerry) {
target.scene.removeModifier(this.chosenBerry, !target.isPlayer()); target.loseHeldItem(this.chosenBerry);
} else if (this.chosenBerry !== undefined && this.chosenBerry.stackCount > 1) {
this.chosenBerry.stackCount--;
} }
target.scene.updateModifiers(target.isPlayer()); target.scene.updateModifiers(target.isPlayer());
} }
eatBerry(consumer: Pokemon) { eatBerry(consumer: Pokemon, berryOwner?: Pokemon) {
getBerryEffectFunc(this.chosenBerry!.berryType)(consumer); // consumer eats the berry getBerryEffectFunc(this.chosenBerry!.berryType)(consumer, berryOwner); // consumer eats the berry
applyAbAttrs(HealFromBerryUseAbAttr, consumer, new Utils.BooleanHolder(false)); applyAbAttrs(HealFromBerryUseAbAttr, consumer, new Utils.BooleanHolder(false));
} }
} }
@ -2516,10 +2569,11 @@ export class StealEatBerryAttr extends EatBerryAttr {
} }
// if the target has berries, pick a random berry and steal it // if the target has berries, pick a random berry and steal it
this.chosenBerry = heldBerries[user.randSeedInt(heldBerries.length)]; this.chosenBerry = heldBerries[user.randSeedInt(heldBerries.length)];
applyPostItemLostAbAttrs(PostItemLostAbAttr, target, false);
const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: this.chosenBerry.type.name }); const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: this.chosenBerry.type.name });
user.scene.queueMessage(message); user.scene.queueMessage(message);
this.reduceBerryModifier(target); this.reduceBerryModifier(target);
this.eatBerry(user); this.eatBerry(user, target);
return true; return true;
} }
} }
@ -2535,12 +2589,11 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
/** /**
* @param selfTarget - Whether this move targets the user * @param selfTarget - Whether this move targets the user
* @param ...effects - List of status effects to cure * @param effects - status effect or list of status effects to cure
*/ */
constructor(selfTarget: boolean, ...effects: StatusEffect[]) { constructor(selfTarget: boolean, effects: StatusEffect | StatusEffect[]) {
super(selfTarget, { lastHitOnly: true }); super(selfTarget, { lastHitOnly: true });
this.effects = [ effects ].flat(1);
this.effects = effects;
} }
/** /**
@ -2781,6 +2834,14 @@ export class OverrideMoveEffectAttr extends MoveAttr {
} }
} }
/**
* Attack Move that doesn't hit the turn it is played and doesn't allow for multiple
* uses on the same target. Examples are Future Sight or Doom Desire.
* @extends OverrideMoveEffectAttr
* @param tagType The {@linkcode ArenaTagType} that will be placed on the field when the move is used
* @param chargeAnim The {@linkcode ChargeAnim | Charging Animation} used for the move
* @param chargeText The text to display when the move is used
*/
export class DelayedAttackAttr extends OverrideMoveEffectAttr { export class DelayedAttackAttr extends OverrideMoveEffectAttr {
public tagType: ArenaTagType; public tagType: ArenaTagType;
public chargeAnim: ChargeAnim; public chargeAnim: ChargeAnim;
@ -2795,13 +2856,18 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
// Edge case for the move applied on a pokemon that has fainted
if (!target) {
return Promise.resolve(true);
}
const side = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
return new Promise(resolve => { return new Promise(resolve => {
if (args.length < 2 || !args[1]) { if (args.length < 2 || !args[1]) {
new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, false, () => { new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, false, () => {
(args[0] as Utils.BooleanHolder).value = true; (args[0] as Utils.BooleanHolder).value = true;
user.scene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user))); user.scene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER }); user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER });
user.scene.arena.addTag(this.tagType, 3, move.id, user.id, ArenaTagSide.BOTH, false, target.getBattlerIndex()); user.scene.arena.addTag(this.tagType, 3, move.id, user.id, side, false, target.getBattlerIndex());
resolve(true); resolve(true);
}); });
@ -3228,6 +3294,41 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr {
} }
} }
/**
* Attribute implementing the stat boosting effect of {@link https://bulbapedia.bulbagarden.net/wiki/Order_Up_(move) | Order Up}.
* If the user has a Pokemon with {@link https://bulbapedia.bulbagarden.net/wiki/Commander_(Ability) | Commander} in their mouth,
* one of the user's stats are increased by 1 stage, depending on the "commanding" Pokemon's form. This effect does not respect
* effect chance, but Order Up itself may be boosted by Sheer Force.
*/
export class OrderUpStatBoostAttr extends MoveEffectAttr {
constructor() {
super(true, { trigger: MoveEffectTrigger.HIT });
}
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
const commandedTag = user.getTag(CommandedTag);
if (!commandedTag) {
return false;
}
let increasedStat: EffectiveStat = Stat.ATK;
switch (commandedTag.tatsugiriFormKey) {
case "curly":
increasedStat = Stat.ATK;
break;
case "droopy":
increasedStat = Stat.DEF;
break;
case "stretchy":
increasedStat = Stat.SPD;
break;
}
user.scene.unshiftPhase(new StatStageChangePhase(user.scene, user.getBattlerIndex(), this.selfTarget, [ increasedStat ], 1));
return true;
}
}
export class CopyStatsAttr extends MoveEffectAttr { export class CopyStatsAttr extends MoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!super.apply(user, target, move, args)) { if (!super.apply(user, target, move, args)) {
@ -3467,7 +3568,7 @@ export class MovePowerMultiplierAttr extends VariablePowerAttr {
* @returns The base power of the Beat Up hit. * @returns The base power of the Beat Up hit.
*/ */
const beatUpFunc = (user: Pokemon, allyIndex: number): number => { const beatUpFunc = (user: Pokemon, allyIndex: number): number => {
const party = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty(); const party = user.isPlayer() ? user.scene.getPlayerParty() : user.scene.getEnemyParty();
for (let i = allyIndex; i < party.length; i++) { for (let i = allyIndex; i < party.length; i++) {
const pokemon = party[i]; const pokemon = party[i];
@ -3495,7 +3596,7 @@ export class BeatUpAttr extends VariablePowerAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const power = args[0] as Utils.NumberHolder; const power = args[0] as Utils.NumberHolder;
const party = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty(); const party = user.isPlayer() ? user.scene.getPlayerParty() : user.scene.getEnemyParty();
const allyCount = party.filter(pokemon => { const allyCount = party.filter(pokemon => {
return pokemon.id === user.id || !pokemon.status?.effect; return pokemon.id === user.id || !pokemon.status?.effect;
}).length; }).length;
@ -4157,6 +4258,60 @@ export class CombinedPledgeStabBoostAttr extends MoveAttr {
} }
} }
/**
* Variable Power attribute for {@link https://bulbapedia.bulbagarden.net/wiki/Round_(move) | Round}.
* Doubles power if another Pokemon has previously selected Round this turn.
* @extends VariablePowerAttr
*/
export class RoundPowerAttr extends VariablePowerAttr {
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const power = args[0];
if (!(power instanceof Utils.NumberHolder)) {
return false;
}
if (user.turnData?.joinedRound) {
power.value *= 2;
return true;
}
return false;
}
}
/**
* Attribute for the "combo" effect of {@link https://bulbapedia.bulbagarden.net/wiki/Round_(move) | Round}.
* Preempts the next move in the turn order with the first instance of any Pokemon
* using Round. Also marks the Pokemon using the cued Round to double the move's power.
* @extends MoveEffectAttr
* @see {@linkcode RoundPowerAttr}
*/
export class CueNextRoundAttr extends MoveEffectAttr {
constructor() {
super(true, { lastHitOnly: true });
}
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
const nextRoundPhase = user.scene.findPhase<MovePhase>(phase =>
phase instanceof MovePhase && phase.move.moveId === Moves.ROUND
);
if (!nextRoundPhase) {
return false;
}
// Update the phase queue so that the next Pokemon using Round moves next
const nextRoundIndex = user.scene.phaseQueue.indexOf(nextRoundPhase);
const nextMoveIndex = user.scene.phaseQueue.findIndex(phase => phase instanceof MovePhase);
if (nextRoundIndex !== nextMoveIndex) {
user.scene.prependToPhase(user.scene.phaseQueue.splice(nextRoundIndex, 1)[0], MovePhase);
}
// Mark the corresponding Pokemon as having "joined the Round" (for doubling power later)
nextRoundPhase.pokemon.turnData.joinedRound = true;
return true;
}
}
export class VariableAtkAttr extends MoveAttr { export class VariableAtkAttr extends MoveAttr {
constructor() { constructor() {
super(); super();
@ -4465,7 +4620,7 @@ export class FormChangeItemTypeAttr extends VariableMoveTypeAttr {
} }
if ([ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.ARCEUS) || [ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.SILVALLY)) { if ([ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.ARCEUS) || [ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.SILVALLY)) {
const form = user.species.speciesId === Species.ARCEUS || user.species.speciesId === Species.SILVALLY ? user.formIndex : user.fusionSpecies?.formIndex!; // TODO: is this bang correct? const form = user.species.speciesId === Species.ARCEUS || user.species.speciesId === Species.SILVALLY ? user.formIndex : user.fusionSpecies?.formIndex!;
moveType.value = Type[Type[form]]; moveType.value = Type[Type[form]];
return true; return true;
@ -4815,22 +4970,6 @@ export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTy
} }
} }
export class WaterSuperEffectTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const multiplier = args[0] as Utils.NumberHolder;
if (target.isOfType(Type.WATER)) {
const effectivenessAgainstWater = new Utils.NumberHolder(getTypeDamageMultiplier(move.type, Type.WATER));
applyChallenges(user.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, effectivenessAgainstWater);
if (effectivenessAgainstWater.value !== 0) {
multiplier.value *= 2 / effectivenessAgainstWater.value;
return true;
}
}
return false;
}
}
export class IceNoEffectTypeAttr extends VariableMoveTypeMultiplierAttr { export class IceNoEffectTypeAttr extends VariableMoveTypeMultiplierAttr {
/** /**
* Checks to see if the Target is Ice-Type or not. If so, the move will have no effect. * Checks to see if the Target is Ice-Type or not. If so, the move will have no effect.
@ -4858,6 +4997,41 @@ export class FlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr {
} }
} }
/**
* Attribute for moves which have a custom type chart interaction.
*/
export class VariableMoveTypeChartAttr extends MoveAttr {
/**
* @param user {@linkcode Pokemon} using the move
* @param target {@linkcode Pokemon} target of the move
* @param move {@linkcode Move} with this attribute
* @param args [0] {@linkcode NumberHolder} holding the type effectiveness
* @param args [1] A single defensive type of the target
*
* @returns true if application of the attribute succeeds
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return false;
}
}
/**
* This class forces Freeze-Dry to be super effective against Water Type.
*/
export class FreezeDryAttr extends VariableMoveTypeChartAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const multiplier = args[0] as Utils.NumberHolder;
const defType = args[1] as Type;
if (defType === Type.WATER) {
multiplier.value = 2;
return true;
} else {
return false;
}
}
}
export class OneHitKOAccuracyAttr extends VariableAccuracyAttr { export class OneHitKOAccuracyAttr extends VariableAccuracyAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const accuracy = args[0] as Utils.NumberHolder; const accuracy = args[0] as Utils.NumberHolder;
@ -5476,7 +5650,8 @@ export class AddArenaTagAttr extends MoveEffectAttr {
} }
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); const side = (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
user.scene.arena.addTag(this.tagType, this.turnCount, move.id, user.id, side);
return true; return true;
} }
@ -5720,7 +5895,7 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
return new Promise(resolve => { return new Promise(resolve => {
// If user is player, checks if the user has fainted pokemon // If user is player, checks if the user has fainted pokemon
if (user instanceof PlayerPokemon if (user instanceof PlayerPokemon
&& user.scene.getParty().findIndex(p => p.isFainted()) > -1) { && user.scene.getPlayerParty().findIndex(p => p.isFainted()) > -1) {
(user as PlayerPokemon).revivalBlessing().then(() => { (user as PlayerPokemon).revivalBlessing().then(() => {
resolve(true); resolve(true);
}); });
@ -5761,6 +5936,7 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
} }
} }
export class ForceSwitchOutAttr extends MoveEffectAttr { export class ForceSwitchOutAttr extends MoveEffectAttr {
constructor( constructor(
private selfSwitch: boolean = false, private selfSwitch: boolean = false,
@ -5779,14 +5955,27 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
return false; return false;
} }
/** /** The {@linkcode Pokemon} to be switched out with this effect */
* Move the switch out logic inside the conditional block
* This ensures that the switch out only happens when the conditions are met
*/
const switchOutTarget = this.selfSwitch ? user : target; const switchOutTarget = this.selfSwitch ? user : target;
// If the switch-out target is a Dondozo with a Tatsugiri in its mouth
// (e.g. when it uses Flip Turn), make it spit out the Tatsugiri before switching out.
switchOutTarget.lapseTag(BattlerTagType.COMMANDED);
if (switchOutTarget instanceof PlayerPokemon) { if (switchOutTarget instanceof PlayerPokemon) {
/**
* Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch
* If it did, the user of U-turn or Volt Switch will not be switched out.
*/
if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) &&
(move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH || move.id === Moves.FLIP_TURN)
) {
if (this.hpDroppedBelowHalf(target)) {
return false;
}
}
// Switch out logic for the player's Pokemon // Switch out logic for the player's Pokemon
if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { if (switchOutTarget.scene.getPlayerParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
return false; return false;
} }
@ -5810,6 +5999,17 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
false, false), MoveEndPhase); false, false), MoveEndPhase);
} }
} else { } else {
/**
* Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch
* If it did, the user of U-turn or Volt Switch will not be switched out.
*/
if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) &&
(move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH) || move.id === Moves.FLIP_TURN) {
if (this.hpDroppedBelowHalf(target)) {
return false;
}
}
// Switch out logic for everything else (eg: WILD battles) // 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;
@ -5864,6 +6064,12 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
return false; return false;
} }
// Dondozo with an allied Tatsugiri in its mouth cannot be forced out
const commandedTag = switchOutTarget.getTag(BattlerTagType.COMMANDED);
if (commandedTag?.getSourcePokemon(switchOutTarget.scene)?.isActive(true)) {
return false;
}
if (!player && user.scene.currentBattle.isBattleMysteryEncounter() && !user.scene.currentBattle.mysteryEncounter?.fleeAllowed) { if (!player && user.scene.currentBattle.isBattleMysteryEncounter() && !user.scene.currentBattle.mysteryEncounter?.fleeAllowed) {
// Don't allow wild opponents to be force switched during MEs with flee disabled // Don't allow wild opponents to be force switched during MEs with flee disabled
return false; return false;
@ -5884,7 +6090,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
} }
} }
const party = player ? user.scene.getParty() : user.scene.getEnemyParty(); const party = player ? user.scene.getPlayerParty() : user.scene.getEnemyParty();
return (!player && !user.scene.currentBattle.battleType) return (!player && !user.scene.currentBattle.battleType)
|| party.filter(p => p.isAllowedInBattle() || party.filter(p => p.isAllowedInBattle()
&& (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > user.scene.currentBattle.getBattlerCount(); && (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > user.scene.currentBattle.getBattlerCount();
@ -5902,6 +6108,21 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
} }
return ret; return ret;
} }
/**
* Helper function to check if the Pokémon's health is below half after taking damage.
* Used for an edge case interaction with Wimp Out/Emergency Exit.
* If the Ability activates due to being hit by U-turn or Volt Switch, the user of that move will not be switched out.
*/
hpDroppedBelowHalf(target: Pokemon): boolean {
const pokemonHealth = target.hp;
const maxPokemonHealth = target.getMaxHp();
const damageTaken = target.turnData.damageTaken;
const initialHealth = pokemonHealth + damageTaken;
// Check if the Pokémon's health has dropped below half after the damage
return initialHealth >= maxPokemonHealth / 2 && pokemonHealth < maxPokemonHealth / 2;
}
} }
export class ChillyReceptionAttr extends ForceSwitchOutAttr { export class ChillyReceptionAttr extends ForceSwitchOutAttr {
@ -6203,10 +6424,17 @@ export class RandomMovesetMoveAttr extends OverrideMoveEffectAttr {
} }
export class RandomMoveAttr extends OverrideMoveEffectAttr { export class RandomMoveAttr extends OverrideMoveEffectAttr {
/**
* This function exists solely to allow tests to override the randomly selected move by mocking this function.
*/
public getMoveOverride(): Moves | null {
return null;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise(resolve => { return new Promise(resolve => {
const moveIds = Utils.getEnumValues(Moves).filter(m => !allMoves[m].hasFlag(MoveFlags.IGNORE_VIRTUAL) && !allMoves[m].name.endsWith(" (N)")); const moveIds = Utils.getEnumValues(Moves).filter(m => !allMoves[m].hasFlag(MoveFlags.IGNORE_VIRTUAL) && !allMoves[m].name.endsWith(" (N)"));
const moveId = moveIds[user.randSeedInt(moveIds.length)]; const moveId = this.getMoveOverride() ?? moveIds[user.randSeedInt(moveIds.length)];
const moveTargets = getMoveTargets(user, moveId); const moveTargets = getMoveTargets(user, moveId);
if (!moveTargets.targets.length) { if (!moveTargets.targets.length) {
@ -6589,7 +6817,8 @@ export class SketchAttr extends MoveEffectAttr {
return false; return false;
} }
const targetMove = target.getMoveHistory().filter(m => !m.virtual).at(-1); const targetMove = target.getLastXMoves(-1)
.find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual);
if (!targetMove) { if (!targetMove) {
return false; return false;
} }
@ -6817,6 +7046,9 @@ export class SuppressAbilitiesIfActedAttr extends MoveEffectAttr {
} }
} }
/**
* Used by Transform
*/
export class TransformAttr extends MoveEffectAttr { export class TransformAttr extends MoveEffectAttr {
async apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { async apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
if (!super.apply(user, target, move, args)) { if (!super.apply(user, target, move, args)) {
@ -6825,10 +7057,8 @@ export class TransformAttr extends MoveEffectAttr {
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
user.summonData.speciesForm = target.getSpeciesForm(); user.summonData.speciesForm = target.getSpeciesForm();
user.summonData.fusionSpeciesForm = target.getFusionSpeciesForm();
user.summonData.ability = target.getAbility().id; user.summonData.ability = target.getAbility().id;
user.summonData.gender = target.getGender(); user.summonData.gender = target.getGender();
user.summonData.fusionGender = target.getFusionGender();
// Power Trick's effect will not preserved after using Transform // Power Trick's effect will not preserved after using Transform
user.removeTag(BattlerTagType.POWER_TRICK); user.removeTag(BattlerTagType.POWER_TRICK);
@ -7220,7 +7450,7 @@ const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target
const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.scene.phaseQueue.find(phase => phase instanceof MovePhase) !== undefined; const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.scene.phaseQueue.find(phase => phase instanceof MovePhase) !== undefined;
const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => { const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => {
const party: Pokemon[] = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty(); const party: Pokemon[] = user.isPlayer() ? user.scene.getPlayerParty() : user.scene.getEnemyParty();
return party.some(pokemon => pokemon.isActive() && !pokemon.isOnField()); return party.some(pokemon => pokemon.isActive() && !pokemon.isOnField());
}; };
@ -7292,6 +7522,27 @@ export class FirstMoveCondition extends MoveCondition {
} }
} }
/**
* Condition used by the move {@link https://bulbapedia.bulbagarden.net/wiki/Upper_Hand_(move) | Upper Hand}.
* Moves with this condition are only successful when the target has selected
* a high-priority attack (after factoring in priority-boosting effects) and
* hasn't moved yet this turn.
*/
export class UpperHandCondition extends MoveCondition {
constructor() {
super((user, target, move) => {
const targetCommand = user.scene.currentBattle.turnCommands[target.getBattlerIndex()];
return !!targetCommand
&& targetCommand.command === Command.FIGHT
&& !target.turnData.acted
&& !!targetCommand.move?.move
&& allMoves[targetCommand.move.move].category !== MoveCategory.STATUS
&& allMoves[targetCommand.move.move].getPriority(target) > 0;
});
}
}
export class hitsSameTypeAttr extends VariableMoveTypeMultiplierAttr { export class hitsSameTypeAttr extends VariableMoveTypeMultiplierAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const multiplier = args[0] as Utils.NumberHolder; const multiplier = args[0] as Utils.NumberHolder;
@ -7878,7 +8129,8 @@ export function initMoves() {
.ignoresVirtual(), .ignoresVirtual(),
new StatusMove(Moves.TRANSFORM, Type.NORMAL, -1, 10, -1, 0, 1) new StatusMove(Moves.TRANSFORM, Type.NORMAL, -1, 10, -1, 0, 1)
.attr(TransformAttr) .attr(TransformAttr)
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE)) // transforming from or into fusion pokemon causes various problems (such as crashes)
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE) && !user.fusionSpecies && !target.fusionSpecies)
.ignoresProtect(), .ignoresProtect(),
new AttackMove(Moves.BUBBLE, Type.WATER, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1) new AttackMove(Moves.BUBBLE, Type.WATER, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
.attr(StatStageChangeAttr, [ Stat.SPD ], -1) .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
@ -8205,7 +8457,8 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -1) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
.ballBombMove(), .ballBombMove(),
new AttackMove(Moves.FUTURE_SIGHT, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 2) new AttackMove(Moves.FUTURE_SIGHT, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 2)
.partial() // Complete buggy mess .partial() // cannot be used on multiple Pokemon on the same side in a double battle, hits immediately when called by Metronome/etc
.ignoresProtect()
.attr(DelayedAttackAttr, ArenaTagType.FUTURE_SIGHT, ChargeAnim.FUTURE_SIGHT_CHARGING, i18next.t("moveTriggers:foresawAnAttack", { pokemonName: "{USER}" })), .attr(DelayedAttackAttr, ArenaTagType.FUTURE_SIGHT, ChargeAnim.FUTURE_SIGHT_CHARGING, i18next.t("moveTriggers:foresawAnAttack", { pokemonName: "{USER}" })),
new AttackMove(Moves.ROCK_SMASH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 15, 50, 0, 2) new AttackMove(Moves.ROCK_SMASH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 15, 50, 0, 2)
.attr(StatStageChangeAttr, [ Stat.DEF ], -1), .attr(StatStageChangeAttr, [ Stat.DEF ], -1),
@ -8282,7 +8535,8 @@ export function initMoves() {
new StatusMove(Moves.HELPING_HAND, Type.NORMAL, -1, 20, -1, 5, 3) new StatusMove(Moves.HELPING_HAND, Type.NORMAL, -1, 20, -1, 5, 3)
.attr(AddBattlerTagAttr, BattlerTagType.HELPING_HAND) .attr(AddBattlerTagAttr, BattlerTagType.HELPING_HAND)
.ignoresSubstitute() .ignoresSubstitute()
.target(MoveTarget.NEAR_ALLY), .target(MoveTarget.NEAR_ALLY)
.condition(failIfSingleBattle),
new StatusMove(Moves.TRICK, Type.PSYCHIC, 100, 10, -1, 0, 3) new StatusMove(Moves.TRICK, Type.PSYCHIC, 100, 10, -1, 0, 3)
.unimplemented(), .unimplemented(),
new StatusMove(Moves.ROLE_PLAY, Type.PSYCHIC, -1, 10, -1, 0, 3) new StatusMove(Moves.ROLE_PLAY, Type.PSYCHIC, -1, 10, -1, 0, 3)
@ -8328,10 +8582,10 @@ export function initMoves() {
.attr(AddArenaTagAttr, ArenaTagType.IMPRISON, 1, true, false) .attr(AddArenaTagAttr, ArenaTagType.IMPRISON, 1, true, false)
.target(MoveTarget.ENEMY_SIDE), .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)),
new SelfStatusMove(Moves.GRUDGE, Type.GHOST, -1, 5, -1, 0, 3) new SelfStatusMove(Moves.GRUDGE, Type.GHOST, -1, 5, -1, 0, 3)
.unimplemented(), .attr(AddBattlerTagAttr, BattlerTagType.GRUDGE, true, undefined, 1),
new SelfStatusMove(Moves.SNATCH, Type.DARK, -1, 10, -1, 4, 3) new SelfStatusMove(Moves.SNATCH, Type.DARK, -1, 10, -1, 4, 3)
.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)
@ -8512,7 +8766,8 @@ export function initMoves() {
.attr(ConfuseAttr) .attr(ConfuseAttr)
.pulseMove(), .pulseMove(),
new AttackMove(Moves.DOOM_DESIRE, Type.STEEL, MoveCategory.SPECIAL, 140, 100, 5, -1, 0, 3) new AttackMove(Moves.DOOM_DESIRE, Type.STEEL, MoveCategory.SPECIAL, 140, 100, 5, -1, 0, 3)
.partial() // Complete buggy mess .partial() // cannot be used on multiple Pokemon on the same side in a double battle, hits immediately when called by Metronome/etc
.ignoresProtect()
.attr(DelayedAttackAttr, ArenaTagType.DOOM_DESIRE, ChargeAnim.DOOM_DESIRE_CHARGING, i18next.t("moveTriggers:choseDoomDesireAsDestiny", { pokemonName: "{USER}" })), .attr(DelayedAttackAttr, ArenaTagType.DOOM_DESIRE, ChargeAnim.DOOM_DESIRE_CHARGING, i18next.t("moveTriggers:choseDoomDesireAsDestiny", { pokemonName: "{USER}" })),
new AttackMove(Moves.PSYCHO_BOOST, Type.PSYCHIC, MoveCategory.SPECIAL, 140, 90, 5, -1, 0, 3) new AttackMove(Moves.PSYCHO_BOOST, Type.PSYCHIC, MoveCategory.SPECIAL, 140, 90, 5, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true), .attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true),
@ -8634,7 +8889,7 @@ export function initMoves() {
new SelfStatusMove(Moves.AQUA_RING, Type.WATER, -1, 20, -1, 0, 4) new SelfStatusMove(Moves.AQUA_RING, Type.WATER, -1, 20, -1, 0, 4)
.attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true), .attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true),
new SelfStatusMove(Moves.MAGNET_RISE, Type.ELECTRIC, -1, 10, -1, 0, 4) new SelfStatusMove(Moves.MAGNET_RISE, Type.ELECTRIC, -1, 10, -1, 0, 4)
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, true, true) .attr(AddBattlerTagAttr, BattlerTagType.FLOATING, true, true, 5)
.condition((user, target, move) => !user.scene.arena.getTag(ArenaTagType.GRAVITY) && [ BattlerTagType.FLOATING, BattlerTagType.IGNORE_FLYING, BattlerTagType.INGRAIN ].every((tag) => !user.getTag(tag))), .condition((user, target, move) => !user.scene.arena.getTag(ArenaTagType.GRAVITY) && [ BattlerTagType.FLOATING, BattlerTagType.IGNORE_FLYING, BattlerTagType.INGRAIN ].every((tag) => !user.getTag(tag))),
new AttackMove(Moves.FLARE_BLITZ, Type.FIRE, MoveCategory.PHYSICAL, 120, 100, 15, 10, 0, 4) new AttackMove(Moves.FLARE_BLITZ, Type.FIRE, MoveCategory.PHYSICAL, 120, 100, 15, 10, 0, 4)
.attr(RecoilAttr, false, 0.33) .attr(RecoilAttr, false, 0.33)
@ -8922,8 +9177,9 @@ export function initMoves() {
.condition((user, target, move) => !target.turnData.acted) .condition((user, target, move) => !target.turnData.acted)
.attr(AfterYouAttr), .attr(AfterYouAttr),
new AttackMove(Moves.ROUND, Type.NORMAL, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 5) new AttackMove(Moves.ROUND, Type.NORMAL, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 5)
.soundBased() .attr(CueNextRoundAttr)
.partial(), // No effect implemented .attr(RoundPowerAttr)
.soundBased(),
new AttackMove(Moves.ECHOED_VOICE, Type.NORMAL, MoveCategory.SPECIAL, 40, 100, 15, -1, 0, 5) new AttackMove(Moves.ECHOED_VOICE, Type.NORMAL, MoveCategory.SPECIAL, 40, 100, 15, -1, 0, 5)
.attr(ConsecutiveUseMultiBasePowerAttr, 5, false) .attr(ConsecutiveUseMultiBasePowerAttr, 5, false)
.soundBased(), .soundBased(),
@ -8972,6 +9228,7 @@ export function initMoves() {
.target(MoveTarget.ALL_NEAR_ENEMIES) .target(MoveTarget.ALL_NEAR_ENEMIES)
.attr(RemoveHeldItemAttr, true), .attr(RemoveHeldItemAttr, true),
new StatusMove(Moves.QUASH, Type.DARK, 100, 15, -1, 0, 5) new StatusMove(Moves.QUASH, Type.DARK, 100, 15, -1, 0, 5)
.condition(failIfSingleBattle)
.unimplemented(), .unimplemented(),
new AttackMove(Moves.ACROBATICS, Type.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5) new AttackMove(Moves.ACROBATICS, Type.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5)
.attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferable).reduce((v, m) => v + m.stackCount, 0))), .attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferable).reduce((v, m) => v + m.stackCount, 0))),
@ -9150,7 +9407,7 @@ export function initMoves() {
.target(MoveTarget.ALL) .target(MoveTarget.ALL)
.condition((user, target, move) => { .condition((user, target, move) => {
// If any fielded pokémon is grass-type and grounded. // If any fielded pokémon is grass-type and grounded.
return [ ...user.scene.getEnemyParty(), ...user.scene.getParty() ].some((poke) => poke.isOfType(Type.GRASS) && poke.isGrounded()); return [ ...user.scene.getEnemyParty(), ...user.scene.getPlayerParty() ].some((poke) => poke.isOfType(Type.GRASS) && poke.isGrounded());
}) })
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded() }), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded() }),
new StatusMove(Moves.STICKY_WEB, Type.BUG, -1, 20, -1, 0, 6) new StatusMove(Moves.STICKY_WEB, Type.BUG, -1, 20, -1, 0, 6)
@ -9183,8 +9440,7 @@ export function initMoves() {
.target(MoveTarget.ALL_NEAR_OTHERS), .target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.FREEZE_DRY, Type.ICE, MoveCategory.SPECIAL, 70, 100, 20, 10, 0, 6) new AttackMove(Moves.FREEZE_DRY, Type.ICE, MoveCategory.SPECIAL, 70, 100, 20, 10, 0, 6)
.attr(StatusEffectAttr, StatusEffect.FREEZE) .attr(StatusEffectAttr, StatusEffect.FREEZE)
.attr(WaterSuperEffectTypeMultiplierAttr) .attr(FreezeDryAttr),
.edgeCase(), // This currently just multiplies the move's power instead of changing its effectiveness. It also doesn't account for abilities that modify type effectiveness such as tera shell.
new AttackMove(Moves.DISARMING_VOICE, Type.FAIRY, MoveCategory.SPECIAL, 40, -1, 15, -1, 0, 6) new AttackMove(Moves.DISARMING_VOICE, Type.FAIRY, MoveCategory.SPECIAL, 40, -1, 15, -1, 0, 6)
.soundBased() .soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
@ -9224,8 +9480,9 @@ export function initMoves() {
.target(MoveTarget.ALL_NEAR_OTHERS), .target(MoveTarget.ALL_NEAR_OTHERS),
new StatusMove(Moves.FAIRY_LOCK, Type.FAIRY, -1, 10, -1, 0, 6) new StatusMove(Moves.FAIRY_LOCK, Type.FAIRY, -1, 10, -1, 0, 6)
.ignoresSubstitute() .ignoresSubstitute()
.ignoresProtect()
.target(MoveTarget.BOTH_SIDES) .target(MoveTarget.BOTH_SIDES)
.unimplemented(), .attr(AddArenaTagAttr, ArenaTagType.FAIRY_LOCK, 2, true),
new SelfStatusMove(Moves.KINGS_SHIELD, Type.STEEL, -1, 10, -1, 4, 6) new SelfStatusMove(Moves.KINGS_SHIELD, Type.STEEL, -1, 10, -1, 4, 6)
.attr(ProtectAttr, BattlerTagType.KINGS_SHIELD) .attr(ProtectAttr, BattlerTagType.KINGS_SHIELD)
.condition(failIfLastCondition), .condition(failIfLastCondition),
@ -9258,6 +9515,7 @@ export function initMoves() {
new StatusMove(Moves.AROMATIC_MIST, Type.FAIRY, -1, 20, -1, 0, 6) new StatusMove(Moves.AROMATIC_MIST, Type.FAIRY, -1, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], 1) .attr(StatStageChangeAttr, [ Stat.SPDEF ], 1)
.ignoresSubstitute() .ignoresSubstitute()
.condition(failIfSingleBattle)
.target(MoveTarget.NEAR_ALLY), .target(MoveTarget.NEAR_ALLY),
new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6) new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2), .attr(StatStageChangeAttr, [ Stat.SPATK ], -2),
@ -9486,7 +9744,8 @@ export function initMoves() {
new AttackMove(Moves.LEAFAGE, Type.GRASS, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 7) new AttackMove(Moves.LEAFAGE, Type.GRASS, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 7)
.makesContact(false), .makesContact(false),
new StatusMove(Moves.SPOTLIGHT, Type.NORMAL, -1, 15, -1, 3, 7) new StatusMove(Moves.SPOTLIGHT, Type.NORMAL, -1, 15, -1, 3, 7)
.attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, false), .attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, false)
.condition(failIfSingleBattle),
new StatusMove(Moves.TOXIC_THREAD, Type.POISON, 100, 20, -1, 0, 7) new StatusMove(Moves.TOXIC_THREAD, Type.POISON, 100, 20, -1, 0, 7)
.attr(StatusEffectAttr, StatusEffect.POISON) .attr(StatusEffectAttr, StatusEffect.POISON)
.attr(StatStageChangeAttr, [ Stat.SPD ], -1), .attr(StatStageChangeAttr, [ Stat.SPD ], -1),
@ -9532,7 +9791,7 @@ export function initMoves() {
.condition( .condition(
(user: Pokemon, target: Pokemon, move: Move) => isNonVolatileStatusEffect(target.status?.effect!)) // TODO: is this bang correct? (user: Pokemon, target: Pokemon, move: Move) => isNonVolatileStatusEffect(target.status?.effect!)) // TODO: is this bang correct?
.attr(HealAttr, 0.5) .attr(HealAttr, 0.5)
.attr(HealStatusEffectAttr, false, ...getNonVolatileStatusEffects()) .attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects())
.triageMove(), .triageMove(),
new AttackMove(Moves.REVELATION_DANCE, Type.NORMAL, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 7) new AttackMove(Moves.REVELATION_DANCE, Type.NORMAL, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 7)
.danceMove() .danceMove()
@ -9616,11 +9875,9 @@ export function initMoves() {
.ignoresSubstitute() .ignoresSubstitute()
.partial(), // Does not steal stats .partial(), // Does not steal stats
new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7) new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7)
.ignoresAbilities() .ignoresAbilities(),
.edgeCase(), // Should not ignore abilities when called virtually (metronome)
new AttackMove(Moves.MOONGEIST_BEAM, Type.GHOST, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7) new AttackMove(Moves.MOONGEIST_BEAM, Type.GHOST, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7)
.ignoresAbilities() .ignoresAbilities(),
.edgeCase(), // Should not ignore abilities when called virtually (metronome)
new StatusMove(Moves.TEARFUL_LOOK, Type.NORMAL, -1, 20, -1, 0, 7) new StatusMove(Moves.TEARFUL_LOOK, Type.NORMAL, -1, 20, -1, 0, 7)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1),
new AttackMove(Moves.ZING_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 80, 100, 10, 30, 0, 7) new AttackMove(Moves.ZING_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 80, 100, 10, 30, 0, 7)
@ -9643,8 +9900,7 @@ export function initMoves() {
.punchingMove(), .punchingMove(),
new AttackMove(Moves.PHOTON_GEYSER, Type.PSYCHIC, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7) new AttackMove(Moves.PHOTON_GEYSER, Type.PSYCHIC, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7)
.attr(PhotonGeyserCategoryAttr) .attr(PhotonGeyserCategoryAttr)
.ignoresAbilities() .ignoresAbilities(),
.edgeCase(), // Should not ignore abilities when called virtually (metronome)
/* Unused */ /* Unused */
new AttackMove(Moves.LIGHT_THAT_BURNS_THE_SKY, Type.PSYCHIC, MoveCategory.SPECIAL, 200, -1, 1, -1, 0, 7) new AttackMove(Moves.LIGHT_THAT_BURNS_THE_SKY, Type.PSYCHIC, MoveCategory.SPECIAL, 200, -1, 1, -1, 0, 7)
.attr(PhotonGeyserCategoryAttr) .attr(PhotonGeyserCategoryAttr)
@ -9943,7 +10199,8 @@ export function initMoves() {
.unimplemented(), .unimplemented(),
new StatusMove(Moves.COACHING, Type.FIGHTING, -1, 10, -1, 0, 8) new StatusMove(Moves.COACHING, Type.FIGHTING, -1, 10, -1, 0, 8)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1)
.target(MoveTarget.NEAR_ALLY), .target(MoveTarget.NEAR_ALLY)
.condition(failIfSingleBattle),
new AttackMove(Moves.FLIP_TURN, Type.WATER, MoveCategory.PHYSICAL, 60, 100, 20, -1, 0, 8) new AttackMove(Moves.FLIP_TURN, Type.WATER, MoveCategory.PHYSICAL, 60, 100, 20, -1, 0, 8)
.attr(ForceSwitchOutAttr, true), .attr(ForceSwitchOutAttr, true),
new AttackMove(Moves.TRIPLE_AXEL, Type.ICE, MoveCategory.PHYSICAL, 20, 90, 10, -1, 0, 8) new AttackMove(Moves.TRIPLE_AXEL, Type.ICE, MoveCategory.PHYSICAL, 20, 90, 10, -1, 0, 8)
@ -9958,7 +10215,7 @@ export function initMoves() {
.attr(StatusEffectAttr, StatusEffect.BURN), .attr(StatusEffectAttr, StatusEffect.BURN),
new StatusMove(Moves.JUNGLE_HEALING, Type.GRASS, -1, 10, -1, 0, 8) new StatusMove(Moves.JUNGLE_HEALING, Type.GRASS, -1, 10, -1, 0, 8)
.attr(HealAttr, 0.25, true, false) .attr(HealAttr, 0.25, true, false)
.attr(HealStatusEffectAttr, false, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP) .attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects())
.target(MoveTarget.USER_AND_ALLIES), .target(MoveTarget.USER_AND_ALLIES),
new AttackMove(Moves.WICKED_BLOW, Type.DARK, MoveCategory.PHYSICAL, 75, 100, 5, -1, 0, 8) new AttackMove(Moves.WICKED_BLOW, Type.DARK, MoveCategory.PHYSICAL, 75, 100, 5, -1, 0, 8)
.attr(CritOnlyAttr) .attr(CritOnlyAttr)
@ -10062,12 +10319,12 @@ export function initMoves() {
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new StatusMove(Moves.LUNAR_BLESSING, Type.PSYCHIC, -1, 5, -1, 0, 8) new StatusMove(Moves.LUNAR_BLESSING, Type.PSYCHIC, -1, 5, -1, 0, 8)
.attr(HealAttr, 0.25, true, false) .attr(HealAttr, 0.25, true, false)
.attr(HealStatusEffectAttr, false, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP) .attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects())
.target(MoveTarget.USER_AND_ALLIES) .target(MoveTarget.USER_AND_ALLIES)
.triageMove(), .triageMove(),
new SelfStatusMove(Moves.TAKE_HEART, Type.PSYCHIC, -1, 10, -1, 0, 8) new SelfStatusMove(Moves.TAKE_HEART, Type.PSYCHIC, -1, 10, -1, 0, 8)
.attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF ], 1, true) .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF ], 1, true)
.attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP), .attr(HealStatusEffectAttr, true, [ StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP ]),
/* Unused /* Unused
new AttackMove(Moves.G_MAX_WILDFIRE, Type.FIRE, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) new AttackMove(Moves.G_MAX_WILDFIRE, Type.FIRE, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8)
.target(MoveTarget.ALL_NEAR_ENEMIES) .target(MoveTarget.ALL_NEAR_ENEMIES)
@ -10190,8 +10447,8 @@ export function initMoves() {
new AttackMove(Moves.LUMINA_CRASH, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9) new AttackMove(Moves.LUMINA_CRASH, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -2), .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2),
new AttackMove(Moves.ORDER_UP, Type.DRAGON, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 9) new AttackMove(Moves.ORDER_UP, Type.DRAGON, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 9)
.makesContact(false) .attr(OrderUpStatBoostAttr)
.partial(), // No effect implemented (requires Commander) .makesContact(false),
new AttackMove(Moves.JET_PUNCH, Type.WATER, MoveCategory.PHYSICAL, 60, 100, 15, -1, 1, 9) new AttackMove(Moves.JET_PUNCH, Type.WATER, MoveCategory.PHYSICAL, 60, 100, 15, -1, 1, 9)
.punchingMove(), .punchingMove(),
new StatusMove(Moves.SPICY_EXTRACT, Type.GRASS, -1, 15, -1, 0, 9) new StatusMove(Moves.SPICY_EXTRACT, Type.GRASS, -1, 15, -1, 0, 9)
@ -10405,8 +10662,7 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.HEAL_BLOCK, false, false, 2), .attr(AddBattlerTagAttr, BattlerTagType.HEAL_BLOCK, false, false, 2),
new AttackMove(Moves.UPPER_HAND, Type.FIGHTING, MoveCategory.PHYSICAL, 65, 100, 15, 100, 3, 9) new AttackMove(Moves.UPPER_HAND, Type.FIGHTING, MoveCategory.PHYSICAL, 65, 100, 15, 100, 3, 9)
.attr(FlinchAttr) .attr(FlinchAttr)
.condition((user, target, move) => user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.command === Command.FIGHT && !target.turnData.acted && allMoves[user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].category !== MoveCategory.STATUS && allMoves[user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].priority > 0 ) // TODO: is this bang correct? .condition(new UpperHandCondition()),
.partial(), // Should also apply when target move priority increased by ability ex. gale wings
new AttackMove(Moves.MALIGNANT_CHAIN, Type.POISON, MoveCategory.SPECIAL, 100, 100, 5, 50, 0, 9) new AttackMove(Moves.MALIGNANT_CHAIN, Type.POISON, MoveCategory.SPECIAL, 100, 100, 5, 50, 0, 9)
.attr(StatusEffectAttr, StatusEffect.TOXIC) .attr(StatusEffectAttr, StatusEffect.TOXIC)
); );

View File

@ -18,7 +18,7 @@ import { randInt } from "#app/utils";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { applyModifierTypeToPlayerPokemon, catchPokemon, getHighestLevelPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { applyModifierTypeToPlayerPokemon, catchPokemon, getHighestLevelPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { TrainerSlot } from "#app/data/trainer-config"; import { TrainerSlot } from "#app/data/trainer-config";
import { PokeballType } from "#app/data/pokeball"; import { PokeballType } from "#enums/pokeball";
import HeldModifierConfig from "#app/interfaces/held-modifier-config"; import HeldModifierConfig from "#app/interfaces/held-modifier-config";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
@ -181,7 +181,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
// Sort berries by party member ID to more easily re-add later if necessary // Sort berries by party member ID to more easily re-add later if necessary
const berryItemsMap = new Map<number, BerryModifier[]>(); const berryItemsMap = new Map<number, BerryModifier[]>();
scene.getParty().forEach(pokemon => { scene.getPlayerParty().forEach(pokemon => {
const pokemonBerries = berryItems.filter(b => b.pokemonId === pokemon.id); const pokemonBerries = berryItems.filter(b => b.pokemonId === pokemon.id);
if (pokemonBerries?.length > 0) { if (pokemonBerries?.length > 0) {
berryItemsMap.set(pokemon.id, pokemonBerries); berryItemsMap.set(pokemon.id, pokemonBerries);
@ -267,7 +267,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED); const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED);
encounter.setDialogueToken("foodReward", revSeed?.name ?? i18next.t("modifierType:ModifierType.REVIVER_SEED.name")); encounter.setDialogueToken("foodReward", revSeed?.name ?? i18next.t("modifierType:ModifierType.REVIVER_SEED.name"));
const givePartyPokemonReviverSeeds = () => { const givePartyPokemonReviverSeeds = () => {
const party = scene.getParty(); const party = scene.getPlayerParty();
party.forEach(p => { party.forEach(p => {
const heldItems = p.getHeldItems(); const heldItems = p.getHeldItems();
if (revSeed && !heldItems.some(item => item instanceof PokemonInstantReviveModifier)) { if (revSeed && !heldItems.some(item => item instanceof PokemonInstantReviveModifier)) {
@ -308,7 +308,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
const berryMap = encounter.misc.berryItemsMap; const berryMap = encounter.misc.berryItemsMap;
// Returns 2/5 of the berries stolen to each Pokemon // Returns 2/5 of the berries stolen to each Pokemon
const party = scene.getParty(); const party = scene.getPlayerParty();
party.forEach(pokemon => { party.forEach(pokemon => {
const stolenBerries: BerryModifier[] = berryMap.get(pokemon.id); const stolenBerries: BerryModifier[] = berryMap.get(pokemon.id);
const berryTypesAsArray: BerryType[] = []; const berryTypesAsArray: BerryType[] = [];

View File

@ -58,7 +58,7 @@ export const BerriesAboundEncounter: MysteryEncounter =
// Calculate boss mon // Calculate boss mon
const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER);
const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true);
const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true);
encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon));
const config: EnemyPartyConfig = { const config: EnemyPartyConfig = {
@ -77,7 +77,7 @@ export const BerriesAboundEncounter: MysteryEncounter =
scene.currentBattle.waveIndex > 160 ? 7 scene.currentBattle.waveIndex > 160 ? 7
: scene.currentBattle.waveIndex > 120 ? 5 : scene.currentBattle.waveIndex > 120 ? 5
: scene.currentBattle.waveIndex > 40 ? 4 : 2; : scene.currentBattle.waveIndex > 40 ? 4 : 2;
regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0); regenerateModifierPoolThresholds(scene.getPlayerParty(), ModifierPoolType.PLAYER, 0);
encounter.misc = { numBerries }; encounter.misc = { numBerries };
const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(bossPokemon); const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(bossPokemon);
@ -253,7 +253,7 @@ function tryGiveBerry(scene: BattleScene, prioritizedPokemon?: PlayerPokemon) {
const berryType = randSeedInt(Object.keys(BerryType).filter(s => !isNaN(Number(s))).length) as BerryType; const berryType = randSeedInt(Object.keys(BerryType).filter(s => !isNaN(Number(s))).length) as BerryType;
const berry = generateModifierType(scene, modifierTypes.BERRY, [ berryType ]) as BerryModifierType; const berry = generateModifierType(scene, modifierTypes.BERRY, [ berryType ]) as BerryModifierType;
const party = scene.getParty(); const party = scene.getPlayerParty();
// Will try to apply to prioritized pokemon first, then do normal application method if it fails // Will try to apply to prioritized pokemon first, then do normal application method if it fails
if (prioritizedPokemon) { if (prioritizedPokemon) {

View File

@ -36,7 +36,7 @@ import {
HeldItemRequirement, HeldItemRequirement,
TypeRequirement TypeRequirement
} from "#app/data/mystery-encounters/mystery-encounter-requirements"; } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { Type } from "#app/data/type"; import { Type } from "#enums/type";
import { AttackTypeBoosterModifierType, ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type"; import { AttackTypeBoosterModifierType, ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type";
import { import {
AttackTypeBoosterModifier, AttackTypeBoosterModifier,
@ -331,7 +331,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter =
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
// Player gets different rewards depending on the number of bug types they have // Player gets different rewards depending on the number of bug types they have
const numBugTypes = scene.getParty().filter(p => p.isOfType(Type.BUG, true)).length; const numBugTypes = scene.getPlayerParty().filter(p => p.isOfType(Type.BUG, true)).length;
const numBugTypesText = i18next.t(`${namespace}:numBugTypes`, { count: numBugTypes }); const numBugTypesText = i18next.t(`${namespace}:numBugTypes`, { count: numBugTypes });
encounter.setDialogueToken("numBugTypes", numBugTypesText); encounter.setDialogueToken("numBugTypes", numBugTypesText);
@ -477,12 +477,9 @@ export const BugTypeSuperfanEncounter: MysteryEncounter =
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
const modifier = encounter.misc.chosenModifier; const modifier = encounter.misc.chosenModifier;
const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon;
// Remove the modifier if its stacks go to 0 chosenPokemon.loseHeldItem(modifier, false);
modifier.stackCount -= 1;
if (modifier.stackCount === 0) {
scene.removeModifier(modifier);
}
scene.updateModifiers(true, true); scene.updateModifiers(true, true);
const bugNet = generateModifierTypeOption(scene, modifierTypes.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET)!; const bugNet = generateModifierTypeOption(scene, modifierTypes.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET)!;

View File

@ -12,7 +12,7 @@ import { TrainerType } from "#enums/trainer-type";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { applyAbilityOverrideToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { applyAbilityOverrideToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { Type } from "#app/data/type"; import { Type } from "#enums/type";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { randSeedInt, randSeedShuffle } from "#app/utils"; import { randSeedInt, randSeedShuffle } from "#app/utils";
@ -245,7 +245,7 @@ export const ClowningAroundEncounter: MysteryEncounter =
// So Vitamins, form change items, etc. are not included // So Vitamins, form change items, etc. are not included
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
const party = scene.getParty(); const party = scene.getPlayerParty();
let mostHeldItemsPokemon = party[0]; let mostHeldItemsPokemon = party[0];
let count = mostHeldItemsPokemon.getHeldItems() let count = mostHeldItemsPokemon.getHeldItems()
.filter(m => m.isTransferable && !(m instanceof BerryModifier)) .filter(m => m.isTransferable && !(m instanceof BerryModifier))
@ -328,7 +328,7 @@ export const ClowningAroundEncounter: MysteryEncounter =
.withPreOptionPhase(async (scene: BattleScene) => { .withPreOptionPhase(async (scene: BattleScene) => {
// Randomize the second type of all player's pokemon // Randomize the second type of all player's pokemon
// If the pokemon does not normally have a second type, it will gain 1 // If the pokemon does not normally have a second type, it will gain 1
for (const pokemon of scene.getParty()) { for (const pokemon of scene.getPlayerParty()) {
const originalTypes = pokemon.getTypes(false, false, true); const originalTypes = pokemon.getTypes(false, false, true);
// If the Pokemon has non-status moves that don't match the Pokemon's type, prioritizes those as the new type // If the Pokemon has non-status moves that don't match the Pokemon's type, prioritizes those as the new type

View File

@ -1,32 +1,32 @@
import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { BattlerIndex } from "#app/battle";
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { EncounterBattleAnim } from "#app/data/battle-anims";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Moves } from "#enums/moves";
import { TrainerSlot } from "#app/data/trainer-config";
import PokemonData from "#app/system/pokemon-data";
import { Biome } from "#enums/biome";
import { EncounterBattleAnim } from "#app/data/battle-anims";
import { BattlerTagType } from "#enums/battler-tag-type";
import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { DANCING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; import { DANCING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups";
import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { BattlerIndex } from "#app/battle"; import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { catchPokemon, getEncounterPokemonLevelForWave, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { catchPokemon, getEncounterPokemonLevelForWave, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { PokeballType } from "#enums/pokeball"; import { getPokemonSpecies } from "#app/data/pokemon-species";
import { TrainerSlot } from "#app/data/trainer-config";
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { modifierTypes } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/modifier/modifier-type";
import { LearnMovePhase } from "#app/phases/learn-move-phase"; import { LearnMovePhase } from "#app/phases/learn-move-phase";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { Stat } from "#enums/stat"; import PokemonData from "#app/system/pokemon-data";
import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Biome } from "#enums/biome";
import { EncounterAnim } from "#enums/encounter-anims"; import { EncounterAnim } from "#enums/encounter-anims";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { Moves } from "#enums/moves";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PokeballType } from "#enums/pokeball";
import { Species } from "#enums/species";
import { Stat } from "#enums/stat";
import i18next from "i18next"; import i18next from "i18next";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
@ -92,7 +92,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
.withCatchAllowed(true) .withCatchAllowed(true)
.withFleeAllowed(false) .withFleeAllowed(false)
.withOnVisualsStart((scene: BattleScene) => { .withOnVisualsStart((scene: BattleScene) => {
const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getParty()[0]); const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getPlayerPokemon()!);
danceAnim.play(scene); danceAnim.play(scene);
return true; return true;
@ -217,7 +217,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
const onPokemonSelected = (pokemon: PlayerPokemon) => { const onPokemonSelected = (pokemon: PlayerPokemon) => {
encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender());
scene.unshiftPhase(new LearnMovePhase(scene, scene.getParty().indexOf(pokemon), Moves.REVELATION_DANCE)); scene.unshiftPhase(new LearnMovePhase(scene, scene.getPlayerParty().indexOf(pokemon), Moves.REVELATION_DANCE));
// Play animation again to "learn" the dance // Play animation again to "learn" the dance
const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getPlayerPokemon()); const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getPlayerPokemon());

View File

@ -1,4 +1,4 @@
import { Type } from "#app/data/type"; import { Type } from "#enums/type";
import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { isNullOrUndefined, randSeedInt } from "#app/utils";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species"; import { Species } from "#enums/species";

View File

@ -1,22 +1,22 @@
import { generateModifierType, leaveEncounterWithoutBattle, selectPokemonForOption, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import Pokemon, { PlayerPokemon } from "#app/field/pokemon";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { CombinationPokemonRequirement, HeldItemRequirement, MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { CombinationPokemonRequirement, HeldItemRequirement, MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { generateModifierType, leaveEncounterWithoutBattle, selectPokemonForOption, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { BerryModifier, HealingBoosterModifier, LevelIncrementBoosterModifier, MoneyMultiplierModifier, PokemonHeldItemModifier, PreserveBerryModifier } from "#app/modifier/modifier";
import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import i18next from "#app/plugins/i18n";
import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/data/pokemon-species";
import Pokemon, { PlayerPokemon } from "#app/field/pokemon";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { BerryModifier, HealingBoosterModifier, LevelIncrementBoosterModifier, MoneyMultiplierModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, PreserveBerryModifier } from "#app/modifier/modifier";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase";
import i18next from "#app/plugins/i18n";
import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/delibirdy"; const namespace = "mysteryEncounters/delibirdy";
@ -133,7 +133,7 @@ export const DelibirdyEncounter: MysteryEncounter =
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
// At max stacks, give the first party pokemon a Shell Bell instead // At max stacks, give the first party pokemon a Shell Bell instead
const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType;
await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); await applyModifierTypeToPlayerPokemon(scene, scene.getPlayerPokemon()!, shellBell);
scene.playSound("item_fanfare"); scene.playSound("item_fanfare");
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true);
} else { } else {
@ -197,7 +197,8 @@ export const DelibirdyEncounter: MysteryEncounter =
}) })
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
const modifier: BerryModifier | HealingBoosterModifier = encounter.misc.chosenModifier; const modifier: BerryModifier | PokemonInstantReviveModifier = encounter.misc.chosenModifier;
const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon;
// Give the player a Candy Jar if they gave a Berry, and a Berry Pouch for Reviver Seed // Give the player a Candy Jar if they gave a Berry, and a Berry Pouch for Reviver Seed
if (modifier instanceof BerryModifier) { if (modifier instanceof BerryModifier) {
@ -207,7 +208,7 @@ export const DelibirdyEncounter: MysteryEncounter =
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
// At max stacks, give the first party pokemon a Shell Bell instead // At max stacks, give the first party pokemon a Shell Bell instead
const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType;
await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); await applyModifierTypeToPlayerPokemon(scene, scene.getPlayerPokemon()!, shellBell);
scene.playSound("item_fanfare"); scene.playSound("item_fanfare");
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true);
} else { } else {
@ -220,7 +221,7 @@ export const DelibirdyEncounter: MysteryEncounter =
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
// At max stacks, give the first party pokemon a Shell Bell instead // At max stacks, give the first party pokemon a Shell Bell instead
const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType;
await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); await applyModifierTypeToPlayerPokemon(scene, scene.getPlayerPokemon()!, shellBell);
scene.playSound("item_fanfare"); scene.playSound("item_fanfare");
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true);
} else { } else {
@ -228,11 +229,7 @@ export const DelibirdyEncounter: MysteryEncounter =
} }
} }
// Remove the modifier if its stacks go to 0 chosenPokemon.loseHeldItem(modifier, false);
modifier.stackCount -= 1;
if (modifier.stackCount === 0) {
scene.removeModifier(modifier);
}
leaveEncounterWithoutBattle(scene, true); leaveEncounterWithoutBattle(scene, true);
}) })
@ -292,6 +289,7 @@ export const DelibirdyEncounter: MysteryEncounter =
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
const modifier = encounter.misc.chosenModifier; const modifier = encounter.misc.chosenModifier;
const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon;
// Check if the player has max stacks of Healing Charm already // Check if the player has max stacks of Healing Charm already
const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier; const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier;
@ -299,18 +297,14 @@ export const DelibirdyEncounter: MysteryEncounter =
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
// At max stacks, give the first party pokemon a Shell Bell instead // At max stacks, give the first party pokemon a Shell Bell instead
const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType;
await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); await applyModifierTypeToPlayerPokemon(scene, scene.getPlayerParty()[0], shellBell);
scene.playSound("item_fanfare"); scene.playSound("item_fanfare");
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true);
} else { } else {
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM)); scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM));
} }
// Remove the modifier if its stacks go to 0 chosenPokemon.loseHeldItem(modifier, false);
modifier.stackCount -= 1;
if (modifier.stackCount === 0) {
scene.removeModifier(modifier);
}
leaveEncounterWithoutBattle(scene, true); leaveEncounterWithoutBattle(scene, true);
}) })

View File

@ -214,7 +214,7 @@ function pokemonAndMoveChosen(scene: BattleScene, pokemon: PlayerPokemon, move:
text: `${namespace}:incorrect_exp`, text: `${namespace}:incorrect_exp`,
}, },
]; ];
setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); setEncounterExp(scene, scene.getPlayerParty().map((p) => p.id), 50);
} else { } else {
encounter.selectedOption!.dialogue!.selected = [ encounter.selectedOption!.dialogue!.selected = [
{ {

View File

@ -8,14 +8,14 @@ import { AbilityRequirement, CombinationPokemonRequirement, TypeRequirement } fr
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Gender } from "#app/data/gender"; import { Gender } from "#app/data/gender";
import { Type } from "#app/data/type"; import { Type } from "#enums/type";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import Pokemon, { PokemonMove } from "#app/field/pokemon"; import Pokemon, { PokemonMove } from "#app/field/pokemon";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { EncounterBattleAnim } from "#app/data/battle-anims"; import { EncounterBattleAnim } from "#app/data/battle-anims";
import { WeatherType } from "#app/data/weather"; import { WeatherType } from "#enums/weather-type";
import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { isNullOrUndefined, randSeedInt } from "#app/utils";
import { StatusEffect } from "#app/data/status-effect"; import { StatusEffect } from "#enums/status-effect";
import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { applyAbilityOverrideToPokemon, applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { applyAbilityOverrideToPokemon, applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
@ -184,7 +184,7 @@ export const FieryFalloutEncounter: MysteryEncounter =
async (scene: BattleScene) => { async (scene: BattleScene) => {
// Damage non-fire types and burn 1 random non-fire type member + give it Heatproof // Damage non-fire types and burn 1 random non-fire type member + give it Heatproof
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
const nonFireTypes = scene.getParty().filter((p) => p.isAllowedInBattle() && !p.getTypes().includes(Type.FIRE)); const nonFireTypes = scene.getPlayerParty().filter((p) => p.isAllowedInBattle() && !p.getTypes().includes(Type.FIRE));
for (const pkm of nonFireTypes) { for (const pkm of nonFireTypes) {
const percentage = DAMAGE_PERCENTAGE / 100; const percentage = DAMAGE_PERCENTAGE / 100;
@ -257,7 +257,7 @@ export const FieryFalloutEncounter: MysteryEncounter =
function giveLeadPokemonAttackTypeBoostItem(scene: BattleScene) { function giveLeadPokemonAttackTypeBoostItem(scene: BattleScene) {
// Give first party pokemon attack type boost item for free at end of battle // Give first party pokemon attack type boost item for free at end of battle
const leadPokemon = scene.getParty()?.[0]; const leadPokemon = scene.getPlayerParty()?.[0];
if (leadPokemon) { if (leadPokemon) {
// Generate type booster held item, default to Charcoal if item fails to generate // Generate type booster held item, default to Charcoal if item fails to generate
let boosterModifierType = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER) as AttackTypeBoosterModifierType; let boosterModifierType = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER) as AttackTypeBoosterModifierType;

View File

@ -56,7 +56,7 @@ export const FightOrFlightEncounter: MysteryEncounter =
// Calculate boss mon // Calculate boss mon
const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER);
const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true);
const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true);
encounter.setDialogueToken("enemyPokemon", bossPokemon.getNameToRender()); encounter.setDialogueToken("enemyPokemon", bossPokemon.getNameToRender());
const config: EnemyPartyConfig = { const config: EnemyPartyConfig = {
@ -86,11 +86,11 @@ export const FightOrFlightEncounter: MysteryEncounter =
: scene.currentBattle.waveIndex > 40 : scene.currentBattle.waveIndex > 40
? ModifierTier.ULTRA ? ModifierTier.ULTRA
: ModifierTier.GREAT; : ModifierTier.GREAT;
regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0); regenerateModifierPoolThresholds(scene.getPlayerParty(), ModifierPoolType.PLAYER, 0);
let item: ModifierTypeOption | null = null; let item: ModifierTypeOption | null = null;
// TMs and Candy Jar excluded from possible rewards as they're too swingy in value for a singular item reward // TMs and Candy Jar excluded from possible rewards as they're too swingy in value for a singular item reward
while (!item || item.type.id.includes("TM_") || item.type.id === "CANDY_JAR") { while (!item || item.type.id.includes("TM_") || item.type.id === "CANDY_JAR") {
item = getPlayerModifierTypeOptions(1, scene.getParty(), [], { guaranteedModifierTiers: [ tier ], allowLuckUpgrades: false })[0]; item = getPlayerModifierTypeOptions(1, scene.getPlayerParty(), [], { guaranteedModifierTiers: [ tier ], allowLuckUpgrades: false })[0];
} }
encounter.setDialogueToken("itemName", item.type.name); encounter.setDialogueToken("itemName", item.type.name);
encounter.misc = item; encounter.misc = item;

View File

@ -165,7 +165,7 @@ async function summonPlayerPokemon(scene: BattleScene) {
const playerPokemon = encounter.misc.playerPokemon; const playerPokemon = encounter.misc.playerPokemon;
// Swaps the chosen Pokemon and the first player's lead Pokemon in the party // Swaps the chosen Pokemon and the first player's lead Pokemon in the party
const party = scene.getParty(); const party = scene.getPlayerParty();
const chosenIndex = party.indexOf(playerPokemon); const chosenIndex = party.indexOf(playerPokemon);
if (chosenIndex !== 0) { if (chosenIndex !== 0) {
const leadPokemon = party[0]; const leadPokemon = party[0];
@ -305,7 +305,7 @@ async function showWobbuffetHealthBar(scene: BattleScene) {
scene.field.add(wobbuffet); scene.field.add(wobbuffet);
const playerPokemon = scene.getPlayerPokemon() as Pokemon; const playerPokemon = scene.getPlayerPokemon() as Pokemon;
if (playerPokemon?.visible) { if (playerPokemon?.isOnField()) {
scene.field.moveBelow(wobbuffet, playerPokemon); scene.field.moveBelow(wobbuffet, playerPokemon);
} }
// Show health bar and trigger cry // Show health bar and trigger cry

View File

@ -1,6 +1,7 @@
import { leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { TrainerSlot, } from "#app/data/trainer-config"; import { TrainerSlot, } from "#app/data/trainer-config";
import { ModifierTier } from "#app/modifier/modifier-tier"; import { ModifierTier } from "#app/modifier/modifier-tier";
import { MusicPreference } from "#app/system/settings/settings";
import { getPlayerModifierTypeOptions, ModifierPoolType, ModifierTypeOption, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { getPlayerModifierTypeOptions, ModifierPoolType, ModifierTypeOption, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
@ -20,11 +21,12 @@ import PokemonData from "#app/system/pokemon-data";
import i18next from "i18next"; import i18next from "i18next";
import { Gender, getGenderSymbol } from "#app/data/gender"; import { Gender, getGenderSymbol } from "#app/data/gender";
import { getNatureName } from "#app/data/nature"; import { getNatureName } from "#app/data/nature";
import { getPokeballAtlasKey, getPokeballTintColor, PokeballType } from "#app/data/pokeball"; import { getPokeballAtlasKey, getPokeballTintColor } from "#app/data/pokeball";
import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { trainerNamePools } from "#app/data/trainer-names"; import { trainerNamePools } from "#app/data/trainer-names";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { addPokemonDataToDexAndValidateAchievements } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { addPokemonDataToDexAndValidateAchievements } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import type { PokeballType } from "#enums/pokeball";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/globalTradeSystem"; const namespace = "mysteryEncounters/globalTradeSystem";
@ -105,7 +107,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
// Load bgm // Load bgm
let bgmKey: string; let bgmKey: string;
if (scene.musicPreference === 0) { if (scene.musicPreference === MusicPreference.CONSISTENT) {
bgmKey = "mystery_encounter_gen_5_gts"; bgmKey = "mystery_encounter_gen_5_gts";
scene.loadBgm(bgmKey, `${bgmKey}.mp3`); scene.loadBgm(bgmKey, `${bgmKey}.mp3`);
} else { } else {
@ -191,7 +193,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
receivedPokemonData.pokeball = randInt(4) as PokeballType; receivedPokemonData.pokeball = randInt(4) as PokeballType;
const dataSource = new PokemonData(receivedPokemonData); const dataSource = new PokemonData(receivedPokemonData);
const newPlayerPokemon = scene.addPlayerPokemon(receivedPokemonData.species, receivedPokemonData.level, dataSource.abilityIndex, dataSource.formIndex, dataSource.gender, dataSource.shiny, dataSource.variant, dataSource.ivs, dataSource.nature, dataSource); const newPlayerPokemon = scene.addPlayerPokemon(receivedPokemonData.species, receivedPokemonData.level, dataSource.abilityIndex, dataSource.formIndex, dataSource.gender, dataSource.shiny, dataSource.variant, dataSource.ivs, dataSource.nature, dataSource);
scene.getParty().push(newPlayerPokemon); scene.getPlayerParty().push(newPlayerPokemon);
await newPlayerPokemon.loadAssets(); await newPlayerPokemon.loadAssets();
for (const mod of modifiers) { for (const mod of modifiers) {
@ -224,7 +226,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
const onPokemonSelected = (pokemon: PlayerPokemon) => { const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Randomly generate a Wonder Trade pokemon // Randomly generate a Wonder Trade pokemon
const randomTradeOption = generateTradeOption(scene.getParty().map(p => p.species)); const randomTradeOption = generateTradeOption(scene.getPlayerParty().map(p => p.species));
const tradePokemon = new EnemyPokemon(scene, randomTradeOption, pokemon.level, TrainerSlot.NONE, false); const tradePokemon = new EnemyPokemon(scene, randomTradeOption, pokemon.level, TrainerSlot.NONE, false);
// Extra shiny roll at 1/128 odds (boosted by events and charms) // Extra shiny roll at 1/128 odds (boosted by events and charms)
if (!tradePokemon.shiny) { if (!tradePokemon.shiny) {
@ -299,7 +301,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
receivedPokemonData.pokeball = randInt(4) as PokeballType; receivedPokemonData.pokeball = randInt(4) as PokeballType;
const dataSource = new PokemonData(receivedPokemonData); const dataSource = new PokemonData(receivedPokemonData);
const newPlayerPokemon = scene.addPlayerPokemon(receivedPokemonData.species, receivedPokemonData.level, dataSource.abilityIndex, dataSource.formIndex, dataSource.gender, dataSource.shiny, dataSource.variant, dataSource.ivs, dataSource.nature, dataSource); const newPlayerPokemon = scene.addPlayerPokemon(receivedPokemonData.species, receivedPokemonData.level, dataSource.abilityIndex, dataSource.formIndex, dataSource.gender, dataSource.shiny, dataSource.variant, dataSource.ivs, dataSource.nature, dataSource);
scene.getParty().push(newPlayerPokemon); scene.getPlayerParty().push(newPlayerPokemon);
await newPlayerPokemon.loadAssets(); await newPlayerPokemon.loadAssets();
for (const mod of modifiers) { for (const mod of modifiers) {
@ -343,6 +345,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
// Pokemon and item selected // Pokemon and item selected
encounter.setDialogueToken("chosenItem", modifier.type.name); encounter.setDialogueToken("chosenItem", modifier.type.name);
encounter.misc.chosenModifier = modifier; encounter.misc.chosenModifier = modifier;
encounter.misc.chosenPokemon = pokemon;
return true; return true;
}, },
}; };
@ -366,10 +369,12 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
}) })
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
const modifier = encounter.misc.chosenModifier; const modifier = encounter.misc.chosenModifier as PokemonHeldItemModifier;
const party = scene.getPlayerParty();
const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon;
// Check tier of the traded item, the received item will be one tier up // Check tier of the traded item, the received item will be one tier up
const type = modifier.type.withTierFromPool(); const type = modifier.type.withTierFromPool(ModifierPoolType.PLAYER, party);
let tier = type.tier ?? ModifierTier.GREAT; let tier = type.tier ?? ModifierTier.GREAT;
// Eggs and White Herb are not in the pool // Eggs and White Herb are not in the pool
if (type.id === "WHITE_HERB") { if (type.id === "WHITE_HERB") {
@ -384,21 +389,17 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
tier++; tier++;
} }
regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0); regenerateModifierPoolThresholds(party, ModifierPoolType.PLAYER, 0);
let item: ModifierTypeOption | null = null; let item: ModifierTypeOption | null = null;
// TMs excluded from possible rewards // TMs excluded from possible rewards
while (!item || item.type.id.includes("TM_")) { while (!item || item.type.id.includes("TM_")) {
item = getPlayerModifierTypeOptions(1, scene.getParty(), [], { guaranteedModifierTiers: [ tier ], allowLuckUpgrades: false })[0]; item = getPlayerModifierTypeOptions(1, party, [], { guaranteedModifierTiers: [ tier ], allowLuckUpgrades: false })[0];
} }
encounter.setDialogueToken("itemName", item.type.name); encounter.setDialogueToken("itemName", item.type.name);
setEncounterRewards(scene, { guaranteedModifierTypeOptions: [ item ], fillRemaining: false }); setEncounterRewards(scene, { guaranteedModifierTypeOptions: [ item ], fillRemaining: false });
// Remove the chosen modifier if its stacks go to 0 chosenPokemon.loseHeldItem(modifier, false);
modifier.stackCount -= 1;
if (modifier.stackCount === 0) {
scene.removeModifier(modifier);
}
await scene.updateModifiers(true, true); await scene.updateModifiers(true, true);
// Generate a trainer name // Generate a trainer name
@ -430,9 +431,9 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
function getPokemonTradeOptions(scene: BattleScene): Map<number, EnemyPokemon[]> { function getPokemonTradeOptions(scene: BattleScene): Map<number, EnemyPokemon[]> {
const tradeOptionsMap: Map<number, EnemyPokemon[]> = new Map<number, EnemyPokemon[]>(); const tradeOptionsMap: Map<number, EnemyPokemon[]> = new Map<number, EnemyPokemon[]>();
// Starts by filtering out any current party members as valid resulting species // Starts by filtering out any current party members as valid resulting species
const alreadyUsedSpecies: PokemonSpecies[] = scene.getParty().map(p => p.species); const alreadyUsedSpecies: PokemonSpecies[] = scene.getPlayerParty().map(p => p.species);
scene.getParty().forEach(pokemon => { scene.getPlayerParty().forEach(pokemon => {
// If the party member is legendary/mythical, the only trade options available are always pulled from generation-specific legendary trade pools // If the party member is legendary/mythical, the only trade options available are always pulled from generation-specific legendary trade pools
if (pokemon.species.legendary || pokemon.species.subLegendary || pokemon.species.mythical) { if (pokemon.species.legendary || pokemon.species.subLegendary || pokemon.species.mythical) {
const generation = pokemon.species.generation; const generation = pokemon.species.generation;

View File

@ -104,7 +104,7 @@ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.with
], ],
}, },
async (scene: BattleScene) => { async (scene: BattleScene) => {
const allowedPokemon = scene.getParty().filter((p) => p.isAllowedInBattle()); const allowedPokemon = scene.getPlayerParty().filter((p) => p.isAllowedInBattle());
for (const pkm of allowedPokemon) { for (const pkm of allowedPokemon) {
const percentage = DAMAGE_PERCENTAGE / 100; const percentage = DAMAGE_PERCENTAGE / 100;

View File

@ -1,19 +1,19 @@
import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { getHighestLevelPlayerPokemon, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { randSeedInt } from "#app/utils";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { getHighestLevelPlayerPokemon, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Species } from "#enums/species";
import { Moves } from "#enums/moves";
import { GameOverPhase } from "#app/phases/game-over-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { GameOverPhase } from "#app/phases/game-over-phase";
import { randSeedInt } from "#app/utils";
import { Moves } from "#enums/moves";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species";
/** i18n namespace for encounter */ /** i18n namespace for encounter */
const namespace = "mysteryEncounters/mysteriousChest"; const namespace = "mysteryEncounters/mysteriousChest";
@ -177,7 +177,7 @@ export const MysteriousChestEncounter: MysteryEncounter =
await showEncounterText(scene, `${namespace}:option.1.bad`); await showEncounterText(scene, `${namespace}:option.1.bad`);
// Handle game over edge case // Handle game over edge case
const allowedPokemon = scene.getParty().filter(p => p.isAllowedInBattle()); const allowedPokemon = scene.getPokemonAllowedInBattle();
if (allowedPokemon.length === 0) { if (allowedPokemon.length === 0) {
// If there are no longer any legal pokemon in the party, game over. // If there are no longer any legal pokemon in the party, game over.
scene.clearPhaseQueue(); scene.clearPhaseQueue();

View File

@ -6,7 +6,7 @@ import MysteryEncounterOption, { MysteryEncounterOptionBuilder } from "#app/data
import { TrainerSlot } from "#app/data/trainer-config"; import { TrainerSlot } from "#app/data/trainer-config";
import { HiddenAbilityRateBoosterModifier, IvScannerModifier } from "#app/modifier/modifier"; import { HiddenAbilityRateBoosterModifier, IvScannerModifier } from "#app/modifier/modifier";
import { EnemyPokemon } from "#app/field/pokemon"; import { EnemyPokemon } from "#app/field/pokemon";
import { PokeballType } from "#app/data/pokeball"; import { PokeballType } from "#enums/pokeball";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
import { IntegerHolder, randSeedInt } from "#app/utils"; import { IntegerHolder, randSeedInt } from "#app/utils";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/data/pokemon-species";

View File

@ -100,7 +100,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter =
// Only Pokemon that can gain benefits are above half HP with no status // Only Pokemon that can gain benefits are above half HP with no status
const selectableFilter = (pokemon: Pokemon) => { const selectableFilter = (pokemon: Pokemon) => {
// If pokemon meets primary pokemon reqs, it can be selected // If pokemon meets primary pokemon reqs, it can be selected
if (!pokemon.isAllowed()) { if (!pokemon.isAllowedInChallenge()) {
return i18next.t("partyUiHandler:cantBeUsed", { pokemonName: pokemon.getNameToRender() }) ?? null; return i18next.t("partyUiHandler:cantBeUsed", { pokemonName: pokemon.getNameToRender() }) ?? null;
} }
if (!encounter.pokemonMeetsPrimaryRequirements(scene, pokemon)) { if (!encounter.pokemonMeetsPrimaryRequirements(scene, pokemon)) {

View File

@ -3,7 +3,7 @@ import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifi
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { StatusEffect } from "#app/data/status-effect"; import { StatusEffect } from "#enums/status-effect";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";

View File

@ -12,7 +12,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { Biome } from "#enums/biome"; import { Biome } from "#enums/biome";
import { getBiomeKey } from "#app/field/arena"; import { getBiomeKey } from "#app/field/arena";
import { Type } from "#app/data/type"; import { Type } from "#enums/type";
import { getPartyLuckValue, modifierTypes } from "#app/modifier/modifier-type"; import { getPartyLuckValue, modifierTypes } from "#app/modifier/modifier-type";
import { TrainerSlot } from "#app/data/trainer-config"; import { TrainerSlot } from "#app/data/trainer-config";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
@ -134,7 +134,7 @@ export const TeleportingHijinksEncounter: MysteryEncounter =
// Init enemy // Init enemy
const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER);
const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true);
const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true);
encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon));
const config: EnemyPartyConfig = { const config: EnemyPartyConfig = {
@ -170,7 +170,7 @@ async function doBiomeTransitionDialogueAndBattleInit(scene: BattleScene) {
// Init enemy // Init enemy
const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER);
const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true);
const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true);
encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon));

View File

@ -23,7 +23,7 @@ import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/myst
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { achvs } from "#app/system/achv"; import { achvs } from "#app/system/achv";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { Type } from "#app/data/type"; import { Type } from "#enums/type";
import { getPokeballTintColor } from "#app/data/pokeball"; import { getPokeballTintColor } from "#app/data/pokeball";
import { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { PokemonHeldItemModifier } from "#app/modifier/modifier";
@ -126,7 +126,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
]; ];
// Determine the 3 pokemon the player can battle with // Determine the 3 pokemon the player can battle with
let partyCopy = scene.getParty().slice(0); let partyCopy = scene.getPlayerParty().slice(0);
partyCopy = partyCopy partyCopy = partyCopy
.filter(p => p.isAllowedInBattle()) .filter(p => p.isAllowedInBattle())
.sort((a, b) => a.friendship - b.friendship); .sort((a, b) => a.friendship - b.friendship);
@ -508,11 +508,11 @@ function getEggOptions(scene: BattleScene, commonEggs: number, rareEggs: number)
} }
function removePokemonFromPartyAndStoreHeldItems(scene: BattleScene, encounter: MysteryEncounter, chosenPokemon: PlayerPokemon) { function removePokemonFromPartyAndStoreHeldItems(scene: BattleScene, encounter: MysteryEncounter, chosenPokemon: PlayerPokemon) {
const party = scene.getParty(); const party = scene.getPlayerParty();
const chosenIndex = party.indexOf(chosenPokemon); const chosenIndex = party.indexOf(chosenPokemon);
party[chosenIndex] = party[0]; party[chosenIndex] = party[0];
party[0] = chosenPokemon; party[0] = chosenPokemon;
encounter.misc.originalParty = scene.getParty().slice(1); encounter.misc.originalParty = scene.getPlayerParty().slice(1);
encounter.misc.originalPartyHeldItems = encounter.misc.originalParty encounter.misc.originalPartyHeldItems = encounter.misc.originalParty
.map(p => p.getHeldItems()); .map(p => p.getHeldItems());
scene["party"] = [ scene["party"] = [
@ -529,7 +529,7 @@ function checkAchievement(scene: BattleScene) {
function restorePartyAndHeldItems(scene: BattleScene) { function restorePartyAndHeldItems(scene: BattleScene) {
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
// Restore original party // Restore original party
scene.getParty().push(...encounter.misc.originalParty); scene.getPlayerParty().push(...encounter.misc.originalParty);
// Restore held items // Restore held items
const originalHeldItems = encounter.misc.originalPartyHeldItems; const originalHeldItems = encounter.misc.originalPartyHeldItems;

View File

@ -8,7 +8,7 @@ import { catchPokemon, getRandomSpeciesByStarterTier, getSpriteKeysFromPokemon }
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/data/pokemon-species";
import { speciesStarterCosts } from "#app/data/balance/starters"; import { speciesStarterCosts } from "#app/data/balance/starters";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { PokeballType } from "#app/data/pokeball"; import { PokeballType } from "#enums/pokeball";
import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { showEncounterDialogue } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { showEncounterDialogue } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";

View File

@ -5,7 +5,7 @@ import BattleScene from "#app/battle-scene";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { Nature } from "#app/data/nature"; import { Nature } from "#enums/nature";
import Pokemon, { PokemonMove } from "#app/field/pokemon"; import Pokemon, { PokemonMove } from "#app/field/pokemon";
import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { modifyPlayerPokemonBST } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { modifyPlayerPokemonBST } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
@ -140,7 +140,7 @@ export const TheStrongStuffEncounter: MysteryEncounter =
// -15 to all base stats of highest BST (halved for HP), +10 to all base stats of rest of party (halved for HP) // -15 to all base stats of highest BST (halved for HP), +10 to all base stats of rest of party (halved for HP)
// Sort party by bst // Sort party by bst
const sortedParty = scene.getParty().slice(0) const sortedParty = scene.getPlayerParty().slice(0)
.sort((pokemon1, pokemon2) => { .sort((pokemon1, pokemon2) => {
const pokemon1Bst = pokemon1.calculateBaseStats().reduce((a, b) => a + b, 0); const pokemon1Bst = pokemon1.calculateBaseStats().reduce((a, b) => a + b, 0);
const pokemon2Bst = pokemon2.calculateBaseStats().reduce((a, b) => a + b, 0); const pokemon2Bst = pokemon2.calculateBaseStats().reduce((a, b) => a + b, 0);

View File

@ -10,7 +10,7 @@ import { Abilities } from "#enums/abilities";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import { Type } from "#app/data/type"; import { Type } from "#enums/type";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { SpeciesFormChangeManualTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangeManualTrigger } from "#app/data/pokemon-forms";
@ -23,6 +23,7 @@ import { ReturnPhase } from "#app/phases/return-phase";
import i18next from "i18next"; import i18next from "i18next";
import { ModifierTier } from "#app/modifier/modifier-tier"; import { ModifierTier } from "#app/modifier/modifier-tier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { BattlerTagType } from "#enums/battler-tag-type";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/theWinstrateChallenge"; const namespace = "mysteryEncounters/theWinstrateChallenge";
@ -187,9 +188,10 @@ function endTrainerBattleAndShowDialogue(scene: BattleScene): Promise<void> {
} else { } else {
scene.arena.resetArenaEffects(); scene.arena.resetArenaEffects();
const playerField = scene.getPlayerField(); const playerField = scene.getPlayerField();
playerField.forEach((pokemon) => pokemon.lapseTag(BattlerTagType.COMMANDED));
playerField.forEach((_, p) => scene.unshiftPhase(new ReturnPhase(scene, p))); playerField.forEach((_, p) => scene.unshiftPhase(new ReturnPhase(scene, p)));
for (const pokemon of scene.getParty()) { for (const pokemon of scene.getPlayerParty()) {
// Only trigger form change when Eiscue is in Noice form // Only trigger form change when Eiscue is in Noice form
// Hardcoded Eiscue for now in case it is fused with another pokemon // Hardcoded Eiscue for now in case it is fused with another pokemon
if (pokemon.species.speciesId === Species.EISCUE && pokemon.hasAbility(Abilities.ICE_FACE) && pokemon.formIndex === 1) { if (pokemon.species.speciesId === Species.EISCUE && pokemon.hasAbility(Abilities.ICE_FACE) && pokemon.formIndex === 1) {

View File

@ -1,6 +1,6 @@
import { Ability, allAbilities } from "#app/data/ability"; import { Ability, allAbilities } from "#app/data/ability";
import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { getNatureName, Nature } from "#app/data/nature"; import { getNatureName } from "#app/data/nature";
import { speciesStarterCosts } from "#app/data/balance/starters"; import { speciesStarterCosts } from "#app/data/balance/starters";
import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import Pokemon, { PlayerPokemon } from "#app/field/pokemon";
import { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { PokemonHeldItemModifier } from "#app/modifier/modifier";
@ -21,6 +21,7 @@ import i18next from "i18next";
import { getStatKey } from "#enums/stat"; import { getStatKey } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import type { Nature } from "#enums/nature";
/** The i18n namespace for the encounter */ /** The i18n namespace for the encounter */
const namespace = "mysteryEncounters/trainingSession"; const namespace = "mysteryEncounters/trainingSession";
@ -152,7 +153,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
} }
// Add pokemon and mods back // Add pokemon and mods back
scene.getParty().push(playerPokemon); scene.getPlayerParty().push(playerPokemon);
for (const mod of modifiers.value) { for (const mod of modifiers.value) {
mod.pokemonId = playerPokemon.id; mod.pokemonId = playerPokemon.id;
scene.addModifier(mod, true, false, false, true); scene.addModifier(mod, true, false, false, true);
@ -229,7 +230,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
scene.gameData.setPokemonCaught(playerPokemon, false); scene.gameData.setPokemonCaught(playerPokemon, false);
// Add pokemon and modifiers back // Add pokemon and modifiers back
scene.getParty().push(playerPokemon); scene.getPlayerParty().push(playerPokemon);
for (const mod of modifiers.value) { for (const mod of modifiers.value) {
mod.pokemonId = playerPokemon.id; mod.pokemonId = playerPokemon.id;
scene.addModifier(mod, true, false, false, true); scene.addModifier(mod, true, false, false, true);
@ -342,7 +343,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
scene.gameData.setPokemonCaught(playerPokemon, false); scene.gameData.setPokemonCaught(playerPokemon, false);
// Add pokemon and mods back // Add pokemon and mods back
scene.getParty().push(playerPokemon); scene.getPlayerParty().push(playerPokemon);
for (const mod of modifiers.value) { for (const mod of modifiers.value) {
mod.pokemonId = playerPokemon.id; mod.pokemonId = playerPokemon.id;
scene.addModifier(mod, true, false, false, true); scene.addModifier(mod, true, false, false, true);

View File

@ -164,7 +164,7 @@ async function tryApplyDigRewardItems(scene: BattleScene) {
const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType;
const leftovers = generateModifierType(scene, modifierTypes.LEFTOVERS) as PokemonHeldItemModifierType; const leftovers = generateModifierType(scene, modifierTypes.LEFTOVERS) as PokemonHeldItemModifierType;
const party = scene.getParty(); const party = scene.getPlayerParty();
// Iterate over the party until an item was successfully given // Iterate over the party until an item was successfully given
// First leftovers // First leftovers

View File

@ -51,7 +51,7 @@ export const UncommonBreedEncounter: MysteryEncounter =
// Calculate boss mon // Calculate boss mon
// Level equal to 2 below highest party member // Level equal to 2 below highest party member
const level = getHighestLevelPlayerPokemon(scene, false, true).level - 2; const level = getHighestLevelPlayerPokemon(scene, false, true).level - 2;
const species = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); const species = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true);
const pokemon = new EnemyPokemon(scene, species, level, TrainerSlot.NONE, true); const pokemon = new EnemyPokemon(scene, species, level, TrainerSlot.NONE, true);
// Pokemon will always have one of its egg moves in its moveset // Pokemon will always have one of its egg moves in its moveset

View File

@ -1,4 +1,4 @@
import { Type } from "#app/data/type"; import { Type } from "#enums/type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
@ -176,7 +176,7 @@ export const WeirdDreamEncounter: MysteryEncounter =
for (const transformation of scene.currentBattle.mysteryEncounter!.misc.teamTransformations) { for (const transformation of scene.currentBattle.mysteryEncounter!.misc.teamTransformations) {
scene.removePokemonFromPlayerParty(transformation.previousPokemon, false); scene.removePokemonFromPlayerParty(transformation.previousPokemon, false);
scene.getParty().push(transformation.newPokemon); scene.getPlayerParty().push(transformation.newPokemon);
} }
}) })
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
@ -280,7 +280,7 @@ export const WeirdDreamEncounter: MysteryEncounter =
const onBeforeRewards = () => { const onBeforeRewards = () => {
// Before battle rewards, unlock the passive on a pokemon in the player's team for the rest of the run (not permanently) // Before battle rewards, unlock the passive on a pokemon in the player's team for the rest of the run (not permanently)
// One random pokemon will get its passive unlocked // One random pokemon will get its passive unlocked
const passiveDisabledPokemon = scene.getParty().filter(p => !p.passive); const passiveDisabledPokemon = scene.getPlayerParty().filter(p => !p.passive);
if (passiveDisabledPokemon?.length > 0) { if (passiveDisabledPokemon?.length > 0) {
const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)]; const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)];
enablePassiveMon.passive = true; enablePassiveMon.passive = true;
@ -306,12 +306,13 @@ export const WeirdDreamEncounter: MysteryEncounter =
}, },
async (scene: BattleScene) => { async (scene: BattleScene) => {
// Leave, reduce party levels by 10% // Leave, reduce party levels by 10%
for (const pokemon of scene.getParty()) { for (const pokemon of scene.getPlayerParty()) {
pokemon.level = Math.max(Math.ceil((100 - PERCENT_LEVEL_LOSS_ON_REFUSE) / 100 * pokemon.level), 1); pokemon.level = Math.max(Math.ceil((100 - PERCENT_LEVEL_LOSS_ON_REFUSE) / 100 * pokemon.level), 1);
pokemon.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate); pokemon.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate);
pokemon.levelExp = 0; pokemon.levelExp = 0;
pokemon.calculateStats(); pokemon.calculateStats();
pokemon.getBattleInfo().setLevel(pokemon.level);
await pokemon.updateInfo(); await pokemon.updateInfo();
} }
@ -329,7 +330,7 @@ interface PokemonTransformation {
} }
function getTeamTransformations(scene: BattleScene): PokemonTransformation[] { function getTeamTransformations(scene: BattleScene): PokemonTransformation[] {
const party = scene.getParty(); const party = scene.getPlayerParty();
// Removes all pokemon from the party // Removes all pokemon from the party
const alreadyUsedSpecies: PokemonSpecies[] = party.map(p => p.species); const alreadyUsedSpecies: PokemonSpecies[] = party.map(p => p.species);
const pokemonTransformations: PokemonTransformation[] = party.map(p => { const pokemonTransformations: PokemonTransformation[] = party.map(p => {
@ -404,7 +405,7 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
if (shouldGetOldGateau(newPokemon)) { if (shouldGetOldGateau(newPokemon)) {
const stats = getOldGateauBoostedStats(newPokemon); const stats = getOldGateauBoostedStats(newPokemon);
const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU() const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU()
.generateType(scene.getParty(), [ OLD_GATEAU_STATS_UP, stats ]) .generateType(scene.getPlayerParty(), [ OLD_GATEAU_STATS_UP, stats ])
?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU); ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU);
const modifier = modType?.newModifier(newPokemon); const modifier = modType?.newModifier(newPokemon);
if (modifier) { if (modifier) {
@ -417,7 +418,7 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
} }
// One random pokemon will get its passive unlocked // One random pokemon will get its passive unlocked
const passiveDisabledPokemon = scene.getParty().filter(p => !p.passive); const passiveDisabledPokemon = scene.getPlayerParty().filter(p => !p.passive);
if (passiveDisabledPokemon?.length > 0) { if (passiveDisabledPokemon?.length > 0) {
const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)]; const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)];
enablePassiveMon.passive = true; enablePassiveMon.passive = true;

View File

@ -2,7 +2,7 @@ import { OptionTextDisplay } from "#app/data/mystery-encounters/mystery-encounte
import { Moves } from "#app/enums/moves"; import { Moves } from "#app/enums/moves";
import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import Pokemon, { PlayerPokemon } from "#app/field/pokemon";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { Type } from "../type"; import { Type } from "#enums/type";
import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement, TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement, TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { CanLearnMoveRequirement, CanLearnMoveRequirementOptions } from "./requirements/can-learn-move-requirement"; import { CanLearnMoveRequirement, CanLearnMoveRequirementOptions } from "./requirements/can-learn-move-requirement";
import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { isNullOrUndefined, randSeedInt } from "#app/utils";
@ -88,7 +88,7 @@ export default class MysteryEncounterOption implements IMysteryEncounterOption {
* @param pokemon * @param pokemon
*/ */
pokemonMeetsPrimaryRequirements(scene: BattleScene, pokemon: Pokemon): boolean { pokemonMeetsPrimaryRequirements(scene: BattleScene, pokemon: Pokemon): boolean {
return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id)); return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getPlayerParty()).map(p => p.id).includes(pokemon.id));
} }
/** /**
@ -102,10 +102,10 @@ export default class MysteryEncounterOption implements IMysteryEncounterOption {
if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) { if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) {
return true; return true;
} }
let qualified: PlayerPokemon[] = scene.getParty(); let qualified: PlayerPokemon[] = scene.getPlayerParty();
for (const req of this.primaryPokemonRequirements) { for (const req of this.primaryPokemonRequirements) {
if (req.meetsRequirement(scene)) { if (req.meetsRequirement(scene)) {
const queryParty = req.queryParty(scene.getParty()); const queryParty = req.queryParty(scene.getPlayerParty());
qualified = qualified.filter(pkmn => queryParty.includes(pkmn)); qualified = qualified.filter(pkmn => queryParty.includes(pkmn));
} else { } else {
this.primaryPokemon = undefined; this.primaryPokemon = undefined;
@ -162,10 +162,10 @@ export default class MysteryEncounterOption implements IMysteryEncounterOption {
return true; return true;
} }
let qualified: PlayerPokemon[] = scene.getParty(); let qualified: PlayerPokemon[] = scene.getPlayerParty();
for (const req of this.secondaryPokemonRequirements) { for (const req of this.secondaryPokemonRequirements) {
if (req.meetsRequirement(scene)) { if (req.meetsRequirement(scene)) {
const queryParty = req.queryParty(scene.getParty()); const queryParty = req.queryParty(scene.getPlayerParty());
qualified = qualified.filter(pkmn => queryParty.includes(pkmn)); qualified = qualified.filter(pkmn => queryParty.includes(pkmn));
} else { } else {
this.secondaryPokemon = []; this.secondaryPokemon = [];

View File

@ -1,21 +1,21 @@
import { PlayerPokemon } from "#app/field/pokemon";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { allAbilities } from "#app/data/ability";
import { EvolutionItem, pokemonEvolutions } from "#app/data/balance/pokemon-evolutions";
import { Nature } from "#enums/nature";
import { FormChangeItem, pokemonFormChanges, SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms";
import { StatusEffect } from "#enums/status-effect";
import { Type } from "#enums/type";
import { WeatherType } from "#enums/weather-type";
import { PlayerPokemon } from "#app/field/pokemon";
import { AttackTypeBoosterModifier } from "#app/modifier/modifier";
import { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type";
import { isNullOrUndefined } from "#app/utils"; import { isNullOrUndefined } from "#app/utils";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { TimeOfDay } from "#enums/time-of-day";
import { Nature } from "#app/data/nature";
import { EvolutionItem, pokemonEvolutions } from "#app/data/balance/pokemon-evolutions";
import { FormChangeItem, pokemonFormChanges, SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms";
import { StatusEffect } from "#app/data/status-effect";
import { Type } from "#app/data/type";
import { WeatherType } from "#app/data/weather";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { AttackTypeBoosterModifier } from "#app/modifier/modifier"; import { Species } from "#enums/species";
import { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type";
import { SpeciesFormKey } from "#enums/species-form-key"; import { SpeciesFormKey } from "#enums/species-form-key";
import { allAbilities } from "#app/data/ability"; import { TimeOfDay } from "#enums/time-of-day";
export interface EncounterRequirement { export interface EncounterRequirement {
meetsRequirement(scene: BattleScene): boolean; // Boolean to see if a requirement is met meetsRequirement(scene: BattleScene): boolean; // Boolean to see if a requirement is met
@ -333,7 +333,7 @@ export class PartySizeRequirement extends EncounterSceneRequirement {
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
if (!isNullOrUndefined(this.partySizeRange) && this.partySizeRange[0] <= this.partySizeRange[1]) { if (!isNullOrUndefined(this.partySizeRange) && this.partySizeRange[0] <= this.partySizeRange[1]) {
const partySize = this.excludeDisallowedPokemon ? scene.getParty().filter(p => p.isAllowedInBattle()).length : scene.getParty().length; const partySize = this.excludeDisallowedPokemon ? scene.getPokemonAllowedInBattle().length : scene.getPlayerParty().length;
if (partySize >= 0 && (this.partySizeRange[0] >= 0 && this.partySizeRange[0] > partySize) || (this.partySizeRange[1] >= 0 && this.partySizeRange[1] < partySize)) { if (partySize >= 0 && (this.partySizeRange[0] >= 0 && this.partySizeRange[0] > partySize) || (this.partySizeRange[1] >= 0 && this.partySizeRange[1] < partySize)) {
return false; return false;
} }
@ -343,7 +343,7 @@ export class PartySizeRequirement extends EncounterSceneRequirement {
} }
override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
return [ "partySize", scene.getParty().length.toString() ]; return [ "partySize", scene.getPlayerParty().length.toString() ];
} }
} }
@ -358,7 +358,7 @@ export class PersistentModifierRequirement extends EncounterSceneRequirement {
} }
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getPlayerParty();
if (isNullOrUndefined(partyPokemon) || this.requiredHeldItemModifiers?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredHeldItemModifiers?.length < 0) {
return false; return false;
} }
@ -421,7 +421,7 @@ export class SpeciesRequirement extends EncounterPokemonRequirement {
} }
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getPlayerParty();
if (isNullOrUndefined(partyPokemon) || this.requiredSpecies?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredSpecies?.length < 0) {
return false; return false;
} }
@ -459,7 +459,7 @@ export class NatureRequirement extends EncounterPokemonRequirement {
} }
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getPlayerParty();
if (isNullOrUndefined(partyPokemon) || this.requiredNature?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredNature?.length < 0) {
return false; return false;
} }
@ -498,7 +498,7 @@ export class TypeRequirement extends EncounterPokemonRequirement {
} }
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
let partyPokemon = scene.getParty(); let partyPokemon = scene.getPlayerParty();
if (isNullOrUndefined(partyPokemon)) { if (isNullOrUndefined(partyPokemon)) {
return false; return false;
@ -545,7 +545,7 @@ export class MoveRequirement extends EncounterPokemonRequirement {
} }
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getPlayerParty();
if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) {
return false; return false;
} }
@ -594,7 +594,7 @@ export class CompatibleMoveRequirement extends EncounterPokemonRequirement {
} }
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getPlayerParty();
if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) {
return false; return false;
} }
@ -635,7 +635,7 @@ export class AbilityRequirement extends EncounterPokemonRequirement {
} }
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getPlayerParty();
if (isNullOrUndefined(partyPokemon) || this.requiredAbilities?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredAbilities?.length < 0) {
return false; return false;
} }
@ -677,7 +677,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement {
} }
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getPlayerParty();
if (isNullOrUndefined(partyPokemon) || this.requiredStatusEffect?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredStatusEffect?.length < 0) {
return false; return false;
} }
@ -746,7 +746,7 @@ export class CanFormChangeWithItemRequirement extends EncounterPokemonRequiremen
} }
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getPlayerParty();
if (isNullOrUndefined(partyPokemon) || this.requiredFormChangeItem?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredFormChangeItem?.length < 0) {
return false; return false;
} }
@ -798,7 +798,7 @@ export class CanEvolveWithItemRequirement extends EncounterPokemonRequirement {
} }
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getPlayerParty();
if (isNullOrUndefined(partyPokemon) || this.requiredEvolutionItem?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredEvolutionItem?.length < 0) {
return false; return false;
} }
@ -849,7 +849,7 @@ export class HeldItemRequirement extends EncounterPokemonRequirement {
} }
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getPlayerParty();
if (isNullOrUndefined(partyPokemon)) { if (isNullOrUndefined(partyPokemon)) {
return false; return false;
} }
@ -900,7 +900,7 @@ export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRe
} }
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getPlayerParty();
if (isNullOrUndefined(partyPokemon)) { if (isNullOrUndefined(partyPokemon)) {
return false; return false;
} }
@ -957,7 +957,7 @@ export class LevelRequirement extends EncounterPokemonRequirement {
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
// Party Pokemon inside required level range // Party Pokemon inside required level range
if (!isNullOrUndefined(this.requiredLevelRange) && this.requiredLevelRange[0] <= this.requiredLevelRange[1]) { if (!isNullOrUndefined(this.requiredLevelRange) && this.requiredLevelRange[0] <= this.requiredLevelRange[1]) {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getPlayerParty();
const pokemonInRange = this.queryParty(partyPokemon); const pokemonInRange = this.queryParty(partyPokemon);
if (pokemonInRange.length < this.minNumberOfPokemon) { if (pokemonInRange.length < this.minNumberOfPokemon) {
return false; return false;
@ -995,7 +995,7 @@ export class FriendshipRequirement extends EncounterPokemonRequirement {
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
// Party Pokemon inside required friendship range // Party Pokemon inside required friendship range
if (!isNullOrUndefined(this.requiredFriendshipRange) && this.requiredFriendshipRange[0] <= this.requiredFriendshipRange[1]) { if (!isNullOrUndefined(this.requiredFriendshipRange) && this.requiredFriendshipRange[0] <= this.requiredFriendshipRange[1]) {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getPlayerParty();
const pokemonInRange = this.queryParty(partyPokemon); const pokemonInRange = this.queryParty(partyPokemon);
if (pokemonInRange.length < this.minNumberOfPokemon) { if (pokemonInRange.length < this.minNumberOfPokemon) {
return false; return false;
@ -1038,7 +1038,7 @@ export class HealthRatioRequirement extends EncounterPokemonRequirement {
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
// Party Pokemon's health inside required health range // Party Pokemon's health inside required health range
if (!isNullOrUndefined(this.requiredHealthRange) && this.requiredHealthRange[0] <= this.requiredHealthRange[1]) { if (!isNullOrUndefined(this.requiredHealthRange) && this.requiredHealthRange[0] <= this.requiredHealthRange[1]) {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getPlayerParty();
const pokemonInRange = this.queryParty(partyPokemon); const pokemonInRange = this.queryParty(partyPokemon);
if (pokemonInRange.length < this.minNumberOfPokemon) { if (pokemonInRange.length < this.minNumberOfPokemon) {
return false; return false;
@ -1082,7 +1082,7 @@ export class WeightRequirement extends EncounterPokemonRequirement {
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
// Party Pokemon's weight inside required weight range // Party Pokemon's weight inside required weight range
if (!isNullOrUndefined(this.requiredWeightRange) && this.requiredWeightRange[0] <= this.requiredWeightRange[1]) { if (!isNullOrUndefined(this.requiredWeightRange) && this.requiredWeightRange[0] <= this.requiredWeightRange[1]) {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getPlayerParty();
const pokemonInRange = this.queryParty(partyPokemon); const pokemonInRange = this.queryParty(partyPokemon);
if (pokemonInRange.length < this.minNumberOfPokemon) { if (pokemonInRange.length < this.minNumberOfPokemon) {
return false; return false;

View File

@ -5,7 +5,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import MysteryEncounterIntroVisuals, { MysteryEncounterSpriteConfig } from "#app/field/mystery-encounter-intro"; import MysteryEncounterIntroVisuals, { MysteryEncounterSpriteConfig } from "#app/field/mystery-encounter-intro";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
import { StatusEffect } from "../status-effect"; import { StatusEffect } from "#enums/status-effect";
import MysteryEncounterDialogue, { OptionTextDisplay } from "./mystery-encounter-dialogue"; import MysteryEncounterDialogue, { OptionTextDisplay } from "./mystery-encounter-dialogue";
import MysteryEncounterOption, { MysteryEncounterOptionBuilder, OptionPhaseCallback } from "./mystery-encounter-option"; import MysteryEncounterOption, { MysteryEncounterOptionBuilder, OptionPhaseCallback } from "./mystery-encounter-option";
import { EncounterPokemonRequirement, EncounterSceneRequirement, HealthRatioRequirement, PartySizeRequirement, StatusEffectRequirement, WaveRangeRequirement } from "./mystery-encounter-requirements"; import { EncounterPokemonRequirement, EncounterSceneRequirement, HealthRatioRequirement, PartySizeRequirement, StatusEffectRequirement, WaveRangeRequirement } from "./mystery-encounter-requirements";
@ -314,7 +314,7 @@ export default class MysteryEncounter implements IMysteryEncounter {
* @param pokemon * @param pokemon
*/ */
pokemonMeetsPrimaryRequirements(scene: BattleScene, pokemon: Pokemon): boolean { pokemonMeetsPrimaryRequirements(scene: BattleScene, pokemon: Pokemon): boolean {
return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id)); return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getPlayerParty()).map(p => p.id).includes(pokemon.id));
} }
/** /**
@ -326,18 +326,18 @@ export default class MysteryEncounter implements IMysteryEncounter {
*/ */
private meetsPrimaryRequirementAndPrimaryPokemonSelected(scene: BattleScene): boolean { private meetsPrimaryRequirementAndPrimaryPokemonSelected(scene: BattleScene): boolean {
if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) { if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) {
const activeMon = scene.getParty().filter(p => p.isActive(true)); const activeMon = scene.getPlayerParty().filter(p => p.isActive(true));
if (activeMon.length > 0) { if (activeMon.length > 0) {
this.primaryPokemon = activeMon[0]; this.primaryPokemon = activeMon[0];
} else { } else {
this.primaryPokemon = scene.getParty().filter(p => p.isAllowedInBattle())[0]; this.primaryPokemon = scene.getPlayerParty().filter(p => p.isAllowedInBattle())[0];
} }
return true; return true;
} }
let qualified: PlayerPokemon[] = scene.getParty(); let qualified: PlayerPokemon[] = scene.getPlayerParty();
for (const req of this.primaryPokemonRequirements) { for (const req of this.primaryPokemonRequirements) {
if (req.meetsRequirement(scene)) { if (req.meetsRequirement(scene)) {
qualified = qualified.filter(pkmn => req.queryParty(scene.getParty()).includes(pkmn)); qualified = qualified.filter(pkmn => req.queryParty(scene.getPlayerParty()).includes(pkmn));
} else { } else {
this.primaryPokemon = undefined; this.primaryPokemon = undefined;
return false; return false;
@ -394,10 +394,10 @@ export default class MysteryEncounter implements IMysteryEncounter {
return true; return true;
} }
let qualified: PlayerPokemon[] = scene.getParty(); let qualified: PlayerPokemon[] = scene.getPlayerParty();
for (const req of this.secondaryPokemonRequirements) { for (const req of this.secondaryPokemonRequirements) {
if (req.meetsRequirement(scene)) { if (req.meetsRequirement(scene)) {
qualified = qualified.filter(pkmn => req.queryParty(scene.getParty()).includes(pkmn)); qualified = qualified.filter(pkmn => req.queryParty(scene.getPlayerParty()).includes(pkmn));
} else { } else {
this.secondaryPokemon = []; this.secondaryPokemon = [];
return false; return false;

View File

@ -39,7 +39,7 @@ export class CanLearnMoveRequirement extends EncounterPokemonRequirement {
} }
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty().filter((pkm) => (this.includeFainted ? pkm.isAllowed() : pkm.isAllowedInBattle())); const partyPokemon = scene.getPlayerParty().filter((pkm) => (this.includeFainted ? pkm.isAllowedInChallenge() : pkm.isAllowedInBattle()));
if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) {
return false; return false;

View File

@ -19,11 +19,11 @@ import i18next from "i18next";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import Trainer, { TrainerVariant } from "#app/field/trainer"; import Trainer, { TrainerVariant } from "#app/field/trainer";
import { Gender } from "#app/data/gender"; import { Gender } from "#app/data/gender";
import { Nature } from "#app/data/nature"; import { Nature } from "#enums/nature";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims"; import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { Status, StatusEffect } from "#app/data/status-effect"; import { Status } from "#app/data/status-effect";
import { TrainerConfig, trainerConfigs, TrainerSlot } from "#app/data/trainer-config"; import { TrainerConfig, trainerConfigs, TrainerSlot } from "#app/data/trainer-config";
import PokemonSpecies from "#app/data/pokemon-species"; import PokemonSpecies from "#app/data/pokemon-species";
import { Egg, IEggOptions } from "#app/data/egg"; import { Egg, IEggOptions } from "#app/data/egg";
@ -37,6 +37,7 @@ import { GameOverPhase } from "#app/phases/game-over-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { PartyExpPhase } from "#app/phases/party-exp-phase"; import { PartyExpPhase } from "#app/phases/party-exp-phase";
import { Variant } from "#app/data/variant"; import { Variant } from "#app/data/variant";
import { StatusEffect } from "#enums/status-effect";
/** /**
* Animates exclamation sprite over trainer's head at start of encounter * Animates exclamation sprite over trainer's head at start of encounter
@ -418,9 +419,9 @@ export function generateModifierType(scene: BattleScene, modifier: () => Modifie
// Populates item id and tier (order matters) // Populates item id and tier (order matters)
result = result result = result
.withIdFromFunc(modifierTypes[modifierId]) .withIdFromFunc(modifierTypes[modifierId])
.withTierFromPool(ModifierPoolType.PLAYER, scene.getParty()); .withTierFromPool(ModifierPoolType.PLAYER, scene.getPlayerParty());
return result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result; return result instanceof ModifierTypeGenerator ? result.generateType(scene.getPlayerParty(), pregenArgs) : result;
} }
/** /**
@ -451,9 +452,9 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p
// Open party screen to choose pokemon // Open party screen to choose pokemon
scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: number, option: PartyOption) => { scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: number, option: PartyOption) => {
if (slotIndex < scene.getParty().length) { if (slotIndex < scene.getPlayerParty().length) {
scene.ui.setMode(modeToSetOnExit).then(() => { scene.ui.setMode(modeToSetOnExit).then(() => {
const pokemon = scene.getParty()[slotIndex]; const pokemon = scene.getPlayerParty()[slotIndex];
const secondaryOptions = onPokemonSelected(pokemon); const secondaryOptions = onPokemonSelected(pokemon);
if (!secondaryOptions) { if (!secondaryOptions) {
scene.currentBattle.mysteryEncounter!.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); scene.currentBattle.mysteryEncounter!.setDialogueToken("selectedPokemon", pokemon.getNameToRender());
@ -563,7 +564,7 @@ export function selectOptionThenPokemon(scene: BattleScene, options: OptionSelec
const selectPokemonAfterOption = (selectedOptionIndex: number) => { const selectPokemonAfterOption = (selectedOptionIndex: number) => {
// Open party screen to choose a Pokemon // Open party screen to choose a Pokemon
scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: number, option: PartyOption) => { scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: number, option: PartyOption) => {
if (slotIndex < scene.getParty().length) { if (slotIndex < scene.getPlayerParty().length) {
// Pokemon and option selected // Pokemon and option selected
scene.ui.setMode(modeToSetOnExit).then(() => { scene.ui.setMode(modeToSetOnExit).then(() => {
const result: PokemonAndOptionSelected = { selectedPokemonIndex: slotIndex, selectedOptionIndex: selectedOptionIndex }; const result: PokemonAndOptionSelected = { selectedPokemonIndex: slotIndex, selectedOptionIndex: selectedOptionIndex };
@ -713,7 +714,7 @@ export function leaveEncounterWithoutBattle(scene: BattleScene, addHealPhase: bo
* @param doNotContinue - default `false`. If set to true, will not end the battle and continue to next wave * @param doNotContinue - default `false`. If set to true, will not end the battle and continue to next wave
*/ */
export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: boolean = false, doNotContinue: boolean = false) { export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: boolean = false, doNotContinue: boolean = false) {
const allowedPkm = scene.getParty().filter((pkm) => pkm.isAllowedInBattle()); const allowedPkm = scene.getPlayerParty().filter((pkm) => pkm.isAllowedInBattle());
if (allowedPkm.length === 0) { if (allowedPkm.length === 0) {
scene.clearPhaseQueue(); scene.clearPhaseQueue();
@ -750,7 +751,7 @@ export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase:
* @param addHealPhase * @param addHealPhase
*/ */
export function handleMysteryEncounterBattleFailed(scene: BattleScene, addHealPhase: boolean = false, doNotContinue: boolean = false) { export function handleMysteryEncounterBattleFailed(scene: BattleScene, addHealPhase: boolean = false, doNotContinue: boolean = false) {
const allowedPkm = scene.getParty().filter((pkm) => pkm.isAllowedInBattle()); const allowedPkm = scene.getPlayerParty().filter((pkm) => pkm.isAllowedInBattle());
if (allowedPkm.length === 0) { if (allowedPkm.length === 0) {
scene.clearPhaseQueue(); scene.clearPhaseQueue();

View File

@ -3,15 +3,15 @@ import i18next from "i18next";
import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { isNullOrUndefined, randSeedInt } from "#app/utils";
import { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { PokemonHeldItemModifier } from "#app/modifier/modifier";
import Pokemon, { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import Pokemon, { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, PokeballType } from "#app/data/pokeball"; import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor } from "#app/data/pokeball";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims"; import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims";
import { getStatusEffectCatchRateMultiplier, StatusEffect } from "#app/data/status-effect"; import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect";
import { achvs } from "#app/system/achv"; import { achvs } from "#app/system/achv";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { Type } from "#app/data/type"; import { Type } from "#enums/type";
import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species"; import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species";
import { speciesStarterCosts } from "#app/data/balance/starters"; import { speciesStarterCosts } from "#app/data/balance/starters";
import { getEncounterText, queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getEncounterText, queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
@ -23,6 +23,8 @@ import { VictoryPhase } from "#app/phases/victory-phase";
import { SummaryUiMode } from "#app/ui/summary-ui-handler"; import { SummaryUiMode } from "#app/ui/summary-ui-handler";
import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import type { PokeballType } from "#enums/pokeball";
import { StatusEffect } from "#enums/status-effect";
/** Will give +1 level every 10 waves */ /** Will give +1 level every 10 waves */
export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1; export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1;
@ -53,24 +55,24 @@ export function getSpriteKeysFromPokemon(pokemon: Pokemon): { spriteKey: string,
} }
/** /**
* Will never remove the player's last non-fainted Pokemon (if they only have 1) * Will never remove the player's last non-fainted Pokemon (if they only have 1).
* Otherwise, picks a Pokemon completely at random and removes from the party * Otherwise, picks a Pokemon completely at random and removes from the party
* @param scene * @param scene
* @param isAllowed Default false. If true, only picks from legal mons. If no legal mons are found (or there is 1, with `doNotReturnLastAllowedMon = true), will return a mon that is not allowed. * @param isAllowed Default `false`. If `true`, only picks from legal mons. If no legal mons are found (or there is 1, with `doNotReturnLastAllowedMon = true`), will return a mon that is not allowed.
* @param isFainted Default false. If true, includes fainted mons. * @param isFainted Default `false`. If `true`, includes fainted mons.
* @param doNotReturnLastAllowedMon Default false. If true, will never return the last unfainted pokemon in the party. Useful when this function is being used to determine what Pokemon to remove from the party (Don't want to remove last unfainted) * @param doNotReturnLastAllowedMon Default `false`. If `true`, will never return the last unfainted pokemon in the party. Useful when this function is being used to determine what Pokemon to remove from the party (Don't want to remove last unfainted)
* @returns * @returns
*/ */
export function getRandomPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false, doNotReturnLastAllowedMon: boolean = false): PlayerPokemon { export function getRandomPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false, doNotReturnLastAllowedMon: boolean = false): PlayerPokemon {
const party = scene.getParty(); const party = scene.getPlayerParty();
let chosenIndex: number; let chosenIndex: number;
let chosenPokemon: PlayerPokemon | null = null; let chosenPokemon: PlayerPokemon | null = null;
const fullyLegalMons = party.filter(p => (!isAllowed || p.isAllowed()) && (isFainted || !p.isFainted())); const fullyLegalMons = party.filter(p => (!isAllowed || p.isAllowedInChallenge()) && (isFainted || !p.isFainted()));
const allowedOnlyMons = party.filter(p => p.isAllowed()); const allowedOnlyMons = party.filter(p => p.isAllowedInChallenge());
if (doNotReturnLastAllowedMon && fullyLegalMons.length === 1) { if (doNotReturnLastAllowedMon && fullyLegalMons.length === 1) {
// If there is only 1 legal/unfainted mon left, select from fainted legal mons // If there is only 1 legal/unfainted mon left, select from fainted legal mons
const faintedLegalMons = party.filter(p => (!isAllowed || p.isAllowed()) && p.isFainted()); const faintedLegalMons = party.filter(p => (!isAllowed || p.isAllowedInChallenge()) && p.isFainted());
if (faintedLegalMons.length > 0) { if (faintedLegalMons.length > 0) {
chosenIndex = randSeedInt(faintedLegalMons.length); chosenIndex = randSeedInt(faintedLegalMons.length);
chosenPokemon = faintedLegalMons[chosenIndex]; chosenPokemon = faintedLegalMons[chosenIndex];
@ -101,11 +103,11 @@ export function getRandomPlayerPokemon(scene: BattleScene, isAllowed: boolean =
* @returns * @returns
*/ */
export function getHighestLevelPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon { export function getHighestLevelPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon {
const party = scene.getParty(); const party = scene.getPlayerParty();
let pokemon: PlayerPokemon | null = null; let pokemon: PlayerPokemon | null = null;
for (const p of party) { for (const p of party) {
if (isAllowed && !p.isAllowed()) { if (isAllowed && !p.isAllowedInChallenge()) {
continue; continue;
} }
if (!isFainted && p.isFainted()) { if (!isFainted && p.isFainted()) {
@ -127,11 +129,11 @@ export function getHighestLevelPlayerPokemon(scene: BattleScene, isAllowed: bool
* @returns * @returns
*/ */
export function getHighestStatPlayerPokemon(scene: BattleScene, stat: PermanentStat, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon { export function getHighestStatPlayerPokemon(scene: BattleScene, stat: PermanentStat, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon {
const party = scene.getParty(); const party = scene.getPlayerParty();
let pokemon: PlayerPokemon | null = null; let pokemon: PlayerPokemon | null = null;
for (const p of party) { for (const p of party) {
if (isAllowed && !p.isAllowed()) { if (isAllowed && !p.isAllowedInChallenge()) {
continue; continue;
} }
if (!isFainted && p.isFainted()) { if (!isFainted && p.isFainted()) {
@ -152,11 +154,11 @@ export function getHighestStatPlayerPokemon(scene: BattleScene, stat: PermanentS
* @returns * @returns
*/ */
export function getLowestLevelPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon { export function getLowestLevelPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon {
const party = scene.getParty(); const party = scene.getPlayerParty();
let pokemon: PlayerPokemon | null = null; let pokemon: PlayerPokemon | null = null;
for (const p of party) { for (const p of party) {
if (isAllowed && !p.isAllowed()) { if (isAllowed && !p.isAllowedInChallenge()) {
continue; continue;
} }
if (!isFainted && p.isFainted()) { if (!isFainted && p.isFainted()) {
@ -177,11 +179,11 @@ export function getLowestLevelPlayerPokemon(scene: BattleScene, isAllowed: boole
* @returns * @returns
*/ */
export function getHighestStatTotalPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon { export function getHighestStatTotalPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon {
const party = scene.getParty(); const party = scene.getPlayerParty();
let pokemon: PlayerPokemon | null = null; let pokemon: PlayerPokemon | null = null;
for (const p of party) { for (const p of party) {
if (isAllowed && !p.isAllowed()) { if (isAllowed && !p.isAllowedInChallenge()) {
continue; continue;
} }
if (!isFainted && p.isFainted()) { if (!isFainted && p.isFainted()) {
@ -315,7 +317,7 @@ export function applyHealToPokemon(scene: BattleScene, pokemon: PlayerPokemon, h
*/ */
export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: number) { export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: number) {
const modType = modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE() const modType = modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE()
.generateType(pokemon.scene.getParty(), [ value ]) .generateType(pokemon.scene.getPlayerParty(), [ value ])
?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE); ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE);
const modifier = modType?.newModifier(pokemon); const modifier = modType?.newModifier(pokemon);
if (modifier) { if (modifier) {
@ -591,7 +593,7 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po
const addToParty = (slotIndex?: number) => { const addToParty = (slotIndex?: number) => {
const newPokemon = pokemon.addToParty(pokeballType, slotIndex); const newPokemon = pokemon.addToParty(pokeballType, slotIndex);
const modifiers = scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false); const modifiers = scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false);
if (scene.getParty().filter(p => p.isShiny()).length === 6) { if (scene.getPlayerParty().filter(p => p.isShiny()).length === 6) {
scene.validateAchv(achvs.SHINY_PARTY); scene.validateAchv(achvs.SHINY_PARTY);
} }
Promise.all(modifiers.map(m => scene.addModifier(m, true))).then(() => { Promise.all(modifiers.map(m => scene.addModifier(m, true))).then(() => {
@ -605,7 +607,7 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po
}); });
}; };
Promise.all([ pokemon.hideInfo(), scene.gameData.setPokemonCaught(pokemon) ]).then(() => { Promise.all([ pokemon.hideInfo(), scene.gameData.setPokemonCaught(pokemon) ]).then(() => {
if (scene.getParty().length === 6) { if (scene.getPlayerParty().length === 6) {
const promptRelease = () => { const promptRelease = () => {
scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => { scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => {
scene.pokemonInfoContainer.makeRoomForConfirmUi(1, true); scene.pokemonInfoContainer.makeRoomForConfirmUi(1, true);
@ -826,7 +828,7 @@ export async function addPokemonDataToDexAndValidateAchievements(scene: BattleSc
* @param invalidSelectionKey * @param invalidSelectionKey
*/ */
export function isPokemonValidForEncounterOptionSelection(pokemon: Pokemon, scene: BattleScene, invalidSelectionKey: string): string | null { export function isPokemonValidForEncounterOptionSelection(pokemon: Pokemon, scene: BattleScene, invalidSelectionKey: string): string | null {
if (!pokemon.isAllowed()) { if (!pokemon.isAllowedInChallenge()) {
return i18next.t("partyUiHandler:cantBeUsed", { pokemonName: pokemon.getNameToRender() }) ?? null; return i18next.t("partyUiHandler:cantBeUsed", { pokemonName: pokemon.getNameToRender() }) ?? null;
} }
if (!pokemon.isAllowedInBattle()) { if (!pokemon.isAllowedInBattle()) {

View File

@ -3,9 +3,7 @@ import { TextStyle, getBBCodeFrag } from "../ui/text";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import { UiTheme } from "#enums/ui-theme"; import { UiTheme } from "#enums/ui-theme";
import i18next from "i18next"; import i18next from "i18next";
import { Stat, EFFECTIVE_STATS, getShortenedStatKey } from "#app/enums/stat"; import { Stat, EFFECTIVE_STATS, getShortenedStatKey } from "#enums/stat";
export { Nature };
export function getNatureName(nature: Nature, includeStatEffects: boolean = false, forStarterSelect: boolean = false, ignoreBBCode: boolean = false, uiTheme: UiTheme = UiTheme.DEFAULT): string { export function getNatureName(nature: Nature, includeStatEffects: boolean = false, forStarterSelect: boolean = false, ignoreBBCode: boolean = false, uiTheme: UiTheme = UiTheme.DEFAULT): string {
let ret = Utils.toReadableString(Nature[nature]); let ret = Utils.toReadableString(Nature[nature]);

View File

@ -1,9 +1,9 @@
import { CriticalCatchChanceBoosterModifier } from "#app/modifier/modifier";
import { NumberHolder } from "#app/utils";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
import BattleScene from "../battle-scene"; import BattleScene from "../battle-scene";
import i18next from "i18next"; import i18next from "i18next";
export { PokeballType };
export const MAX_PER_TYPE_POKEBALLS: integer = 99; export const MAX_PER_TYPE_POKEBALLS: integer = 99;
export function getPokeballAtlasKey(type: PokeballType): string { export function getPokeballAtlasKey(type: PokeballType): string {
@ -82,11 +82,38 @@ export function getPokeballTintColor(type: PokeballType): number {
} }
} }
export function doPokeballBounceAnim(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite, y1: number, y2: number, baseBounceDuration: integer, callback: Function) { /**
* Gets the critical capture chance based on number of mons registered in Dex and modified {@link https://bulbapedia.bulbagarden.net/wiki/Catch_rate Catch rate}
* Formula from {@link https://www.dragonflycave.com/mechanics/gen-vi-vii-capturing Dragonfly Cave Gen 6 Capture Mechanics page}
* @param scene {@linkcode BattleScene} current BattleScene
* @param modifiedCatchRate the modified catch rate as calculated in {@linkcode AttemptCapturePhase}
* @returns the chance of getting a critical capture, out of 256
*/
export function getCriticalCaptureChance(scene: BattleScene, modifiedCatchRate: number): number {
if (scene.gameMode.isFreshStartChallenge()) {
return 0;
}
const dexCount = scene.gameData.getSpeciesCount(d => !!d.caughtAttr);
const catchingCharmMultiplier = new NumberHolder(1);
scene.findModifier(m => m instanceof CriticalCatchChanceBoosterModifier)?.apply(catchingCharmMultiplier);
const dexMultiplier = scene.gameMode.isDaily || dexCount > 800 ? 2.5
: dexCount > 600 ? 2
: dexCount > 400 ? 1.5
: dexCount > 200 ? 1
: dexCount > 100 ? 0.5
: 0;
return Math.floor(catchingCharmMultiplier.value * dexMultiplier * Math.min(255, modifiedCatchRate) / 6);
}
export function doPokeballBounceAnim(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite, y1: number, y2: number, baseBounceDuration: number, callback: Function, isCritical: boolean = false) {
let bouncePower = 1; let bouncePower = 1;
let bounceYOffset = y1; let bounceYOffset = y1;
let bounceY = y2; let bounceY = y2;
const yd = y2 - y1; const yd = y2 - y1;
const x0 = pokeball.x;
const x1 = x0 + 3;
const x2 = x0 - 3;
let critShakes = 4;
const doBounce = () => { const doBounce = () => {
scene.tweens.add({ scene.tweens.add({
@ -117,5 +144,40 @@ export function doPokeballBounceAnim(scene: BattleScene, pokeball: Phaser.GameOb
}); });
}; };
const doCritShake = () => {
scene.tweens.add({
targets: pokeball,
x: x2,
duration: 125,
ease: "Linear",
onComplete: () => {
scene.tweens.add({
targets: pokeball,
x: x1,
duration: 125,
ease: "Linear",
onComplete: () => {
critShakes--;
if (critShakes > 0) {
doCritShake();
} else {
scene.tweens.add({
targets: pokeball,
x: x0,
duration: 60,
ease: "Linear",
onComplete: () => scene.time.delayedCall(500, doBounce)
});
}
}
});
}
});
};
if (isCritical) {
scene.time.delayedCall(500, doCritShake);
} else {
doBounce(); doBounce();
}
} }

View File

@ -1,8 +1,8 @@
import { PokemonFormChangeItemModifier, TerastallizeModifier } from "../modifier/modifier"; import { PokemonFormChangeItemModifier, TerastallizeModifier } from "../modifier/modifier";
import Pokemon from "../field/pokemon"; import Pokemon from "../field/pokemon";
import { StatusEffect } from "./status-effect"; import { StatusEffect } from "#enums/status-effect";
import { MoveCategory, allMoves } from "./move"; import { MoveCategory, allMoves } from "./move";
import { Type } from "./type"; import { Type } from "#enums/type";
import { Constructor, nil } from "#app/utils"; import { Constructor, nil } from "#app/utils";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
@ -10,7 +10,7 @@ import { Species } from "#enums/species";
import { TimeOfDay } from "#enums/time-of-day"; import { TimeOfDay } from "#enums/time-of-day";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import i18next from "i18next"; import i18next from "i18next";
import { WeatherType } from "./weather"; import { WeatherType } from "#enums/weather-type";
import { Challenges } from "#app/enums/challenges"; import { Challenges } from "#app/enums/challenges";
import { SpeciesFormKey } from "#enums/species-form-key"; import { SpeciesFormKey } from "#enums/species-form-key";

View File

@ -12,7 +12,7 @@ import { uncatchableSpecies } from "#app/data/balance/biomes";
import { speciesEggMoves } from "#app/data/balance/egg-moves"; import { speciesEggMoves } from "#app/data/balance/egg-moves";
import { GrowthRate } from "#app/data/exp"; import { GrowthRate } from "#app/data/exp";
import { EvolutionLevel, SpeciesWildEvolutionDelay, pokemonEvolutions, pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions"; import { EvolutionLevel, SpeciesWildEvolutionDelay, pokemonEvolutions, pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions";
import { Type } from "#app/data/type"; import { Type } from "#enums/type";
import { LevelMoves, pokemonFormLevelMoves, pokemonFormLevelMoves as pokemonSpeciesFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves"; import { LevelMoves, pokemonFormLevelMoves, pokemonFormLevelMoves as pokemonSpeciesFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { Variant, VariantSet, variantColorCache, variantData } from "#app/data/variant"; import { Variant, VariantSet, variantColorCache, variantData } from "#app/data/variant";
@ -47,7 +47,7 @@ export function getPokemonSpecies(species: Species | Species[] | undefined): Pok
return allSpecies[species - 1]; return allSpecies[species - 1];
} }
export function getPokemonSpeciesForm(species: Species, formIndex: integer): PokemonSpeciesForm { export function getPokemonSpeciesForm(species: Species, formIndex: number): PokemonSpeciesForm {
const retSpecies: PokemonSpecies = species >= 2000 const retSpecies: PokemonSpecies = species >= 2000
? allSpecies.find(s => s.speciesId === species)! // TODO: is the bang correct? ? allSpecies.find(s => s.speciesId === species)! // TODO: is the bang correct?
: allSpecies[species - 1]; : allSpecies[species - 1];
@ -129,26 +129,27 @@ export type PokemonSpeciesFilter = (species: PokemonSpecies) => boolean;
export abstract class PokemonSpeciesForm { export abstract class PokemonSpeciesForm {
public speciesId: Species; public speciesId: Species;
public formIndex: integer; protected _formIndex: number;
public generation: integer; protected _generation: number;
public type1: Type; readonly type1: Type;
public type2: Type | null; readonly type2: Type | null;
public height: number; readonly height: number;
public weight: number; readonly weight: number;
public ability1: Abilities; readonly ability1: Abilities;
public ability2: Abilities; readonly ability2: Abilities;
public abilityHidden: Abilities; readonly abilityHidden: Abilities;
public baseTotal: integer; readonly baseTotal: number;
public baseStats: integer[]; readonly baseStats: number[];
public catchRate: integer; readonly catchRate: number;
public baseFriendship: integer; readonly baseFriendship: number;
public baseExp: integer; readonly baseExp: number;
public genderDiffs: boolean; readonly genderDiffs: boolean;
public isStarterSelectable: boolean; readonly isStarterSelectable: boolean;
constructor(type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities, constructor(type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities,
baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer, baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number,
catchRate: integer, baseFriendship: integer, baseExp: integer, genderDiffs: boolean, isStarterSelectable: boolean) { catchRate: number, baseFriendship: number, baseExp: number, genderDiffs: boolean, isStarterSelectable: boolean
) {
this.type1 = type1; this.type1 = type1;
this.type2 = type2; this.type2 = type2;
this.height = height; this.height = height;
@ -180,7 +181,23 @@ export abstract class PokemonSpeciesForm {
return ret; return ret;
} }
isOfType(type: integer): boolean { get generation(): number {
return this._generation;
}
set generation(generation: number) {
this._generation = generation;
}
get formIndex(): number {
return this._formIndex;
}
set formIndex(formIndex: number) {
this._formIndex = formIndex;
}
isOfType(type: number): boolean {
return this.type1 === type || (this.type2 !== null && this.type2 === type); return this.type1 === type || (this.type2 !== null && this.type2 === type);
} }
@ -188,7 +205,7 @@ export abstract class PokemonSpeciesForm {
* Method to get the total number of abilities a Pokemon species has. * Method to get the total number of abilities a Pokemon species has.
* @returns Number of abilities * @returns Number of abilities
*/ */
getAbilityCount(): integer { getAbilityCount(): number {
return this.abilityHidden !== Abilities.NONE ? 3 : 2; return this.abilityHidden !== Abilities.NONE ? 3 : 2;
} }
@ -197,7 +214,7 @@ export abstract class PokemonSpeciesForm {
* @param abilityIndex Which ability to get (should only be 0-2) * @param abilityIndex Which ability to get (should only be 0-2)
* @returns The id of the Ability * @returns The id of the Ability
*/ */
getAbility(abilityIndex: integer): Abilities { getAbility(abilityIndex: number): Abilities {
let ret: Abilities; let ret: Abilities;
if (abilityIndex === 0) { if (abilityIndex === 0) {
ret = this.ability1; ret = this.ability1;
@ -277,12 +294,12 @@ export abstract class PokemonSpeciesForm {
return ret; return ret;
} }
getSpriteAtlasPath(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string { getSpriteAtlasPath(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string {
const spriteId = this.getSpriteId(female, formIndex, shiny, variant).replace(/\_{2}/g, "/"); const spriteId = this.getSpriteId(female, formIndex, shiny, variant).replace(/\_{2}/g, "/");
return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`; return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`;
} }
getSpriteId(female: boolean, formIndex?: integer, shiny?: boolean, variant: integer = 0, back?: boolean): string { getSpriteId(female: boolean, formIndex?: number, shiny?: boolean, variant: number = 0, back?: boolean): string {
if (formIndex === undefined || this instanceof PokemonForm) { if (formIndex === undefined || this instanceof PokemonForm) {
formIndex = this.formIndex; formIndex = this.formIndex;
} }
@ -299,11 +316,11 @@ export abstract class PokemonSpeciesForm {
return `${back ? "back__" : ""}${shiny && (!variantSet || (!variant && !variantSet[variant || 0])) ? "shiny__" : ""}${baseSpriteKey}${shiny && variantSet && variantSet[variant] === 2 ? `_${variant + 1}` : ""}`; return `${back ? "back__" : ""}${shiny && (!variantSet || (!variant && !variantSet[variant || 0])) ? "shiny__" : ""}${baseSpriteKey}${shiny && variantSet && variantSet[variant] === 2 ? `_${variant + 1}` : ""}`;
} }
getSpriteKey(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string { getSpriteKey(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string {
return `pkmn__${this.getSpriteId(female, formIndex, shiny, variant)}`; return `pkmn__${this.getSpriteId(female, formIndex, shiny, variant)}`;
} }
abstract getFormSpriteKey(formIndex?: integer): string; abstract getFormSpriteKey(formIndex?: number): string;
/** /**
@ -311,9 +328,9 @@ export abstract class PokemonSpeciesForm {
* @param formIndex optional form index for pokemon with different forms * @param formIndex optional form index for pokemon with different forms
* @returns species id if no additional forms, index with formkey if a pokemon with a form * @returns species id if no additional forms, index with formkey if a pokemon with a form
*/ */
getVariantDataIndex(formIndex?: integer) { getVariantDataIndex(formIndex?: number) {
let formkey: string | null = null; let formkey: string | null = null;
let variantDataIndex: integer | string = this.speciesId; let variantDataIndex: number | string = this.speciesId;
const species = getPokemonSpecies(this.speciesId); const species = getPokemonSpecies(this.speciesId);
if (species.forms.length > 0 && formIndex !== undefined) { if (species.forms.length > 0 && formIndex !== undefined) {
formkey = species.forms[formIndex]?.getFormSpriteKey(formIndex); formkey = species.forms[formIndex]?.getFormSpriteKey(formIndex);
@ -324,13 +341,13 @@ export abstract class PokemonSpeciesForm {
return variantDataIndex; return variantDataIndex;
} }
getIconAtlasKey(formIndex?: integer, shiny?: boolean, variant?: integer): string { getIconAtlasKey(formIndex?: number, shiny?: boolean, variant?: number): string {
const variantDataIndex = this.getVariantDataIndex(formIndex); const variantDataIndex = this.getVariantDataIndex(formIndex);
const isVariant = shiny && variantData[variantDataIndex] && (variant !== undefined && variantData[variantDataIndex][variant]); const isVariant = shiny && variantData[variantDataIndex] && (variant !== undefined && variantData[variantDataIndex][variant]);
return `pokemon_icons_${this.generation}${isVariant ? "v" : ""}`; return `pokemon_icons_${this.generation}${isVariant ? "v" : ""}`;
} }
getIconId(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string { getIconId(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string {
if (formIndex === undefined) { if (formIndex === undefined) {
formIndex = this.formIndex; formIndex = this.formIndex;
} }
@ -346,6 +363,12 @@ export abstract class PokemonSpeciesForm {
} }
switch (this.speciesId) { switch (this.speciesId) {
case Species.DODUO:
case Species.DODRIO:
case Species.MEGANIUM:
case Species.TORCHIC:
case Species.COMBUSKEN:
case Species.BLAZIKEN:
case Species.HIPPOPOTAS: case Species.HIPPOPOTAS:
case Species.HIPPOWDON: case Species.HIPPOWDON:
case Species.UNFEZANT: case Species.UNFEZANT:
@ -379,7 +402,7 @@ export abstract class PokemonSpeciesForm {
return ret; return ret;
} }
getCryKey(formIndex?: integer): string { getCryKey(formIndex?: number): string {
let speciesId = this.speciesId; let speciesId = this.speciesId;
if (this.speciesId > 2000) { if (this.speciesId > 2000) {
switch (this.speciesId) { switch (this.speciesId) {
@ -443,10 +466,10 @@ export abstract class PokemonSpeciesForm {
break; break;
} }
} }
return ret; return `cry/${ret}`;
} }
validateStarterMoveset(moveset: StarterMoveset, eggMoves: integer): boolean { validateStarterMoveset(moveset: StarterMoveset, eggMoves: number): boolean {
const rootSpeciesId = this.getRootSpeciesId(); const rootSpeciesId = this.getRootSpeciesId();
for (const moveId of moveset) { for (const moveId of moveset) {
if (speciesEggMoves.hasOwnProperty(rootSpeciesId)) { if (speciesEggMoves.hasOwnProperty(rootSpeciesId)) {
@ -467,11 +490,11 @@ export abstract class PokemonSpeciesForm {
return true; return true;
} }
loadAssets(scene: BattleScene, female: boolean, formIndex?: integer, shiny?: boolean, variant?: Variant, startLoad?: boolean): Promise<void> { loadAssets(scene: BattleScene, female: boolean, formIndex?: number, shiny?: boolean, variant?: Variant, startLoad?: boolean): Promise<void> {
return new Promise(resolve => { return new Promise(resolve => {
const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant); const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant);
scene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant)); scene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant));
scene.load.audio(`cry/${this.getCryKey(formIndex)}`, `audio/cry/${this.getCryKey(formIndex)}.m4a`); scene.load.audio(`${this.getCryKey(formIndex)}`, `audio/${this.getCryKey(formIndex)}.m4a`);
scene.load.once(Phaser.Loader.Events.COMPLETE, () => { scene.load.once(Phaser.Loader.Events.COMPLETE, () => {
const originalWarn = console.warn; const originalWarn = console.warn;
// Ignore warnings for missing frames, because there will be a lot // Ignore warnings for missing frames, because there will be a lot
@ -482,11 +505,11 @@ export abstract class PokemonSpeciesForm {
scene.anims.create({ scene.anims.create({
key: this.getSpriteKey(female, formIndex, shiny, variant), key: this.getSpriteKey(female, formIndex, shiny, variant),
frames: frameNames, frames: frameNames,
frameRate: 12, frameRate: 10,
repeat: -1 repeat: -1
}); });
} else { } else {
scene.anims.get(spriteKey).frameRate = 12; scene.anims.get(spriteKey).frameRate = 10;
} }
let spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant).replace("variant/", "").replace(/_[1-3]$/, ""); let spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant).replace("variant/", "").replace(/_[1-3]$/, "");
const useExpSprite = scene.experimentalSprites && scene.hasExpSprite(spriteKey); const useExpSprite = scene.experimentalSprites && scene.hasExpSprite(spriteKey);
@ -529,14 +552,14 @@ export abstract class PokemonSpeciesForm {
if (cry?.pendingRemove) { if (cry?.pendingRemove) {
cry = null; cry = null;
} }
cry = scene.playSound(`cry/${(cry ?? cryKey)}`, soundConfig); cry = scene.playSound(cry ?? cryKey, soundConfig);
if (ignorePlay) { if (ignorePlay) {
cry.stop(); cry.stop();
} }
return cry; return cry;
} }
generateCandyColors(scene: BattleScene): integer[][] { generateCandyColors(scene: BattleScene): number[][] {
const sourceTexture = scene.textures.get(this.getSpriteKey(false)); const sourceTexture = scene.textures.get(this.getSpriteKey(false));
const sourceFrame = sourceTexture.frames[sourceTexture.firstFrame]; const sourceFrame = sourceTexture.frames[sourceTexture.firstFrame];
@ -544,7 +567,7 @@ export abstract class PokemonSpeciesForm {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
const spriteColors: integer[][] = []; const spriteColors: number[][] = [];
const context = canvas.getContext("2d"); const context = canvas.getContext("2d");
const frame = sourceFrame; const frame = sourceFrame;
@ -567,7 +590,7 @@ export abstract class PokemonSpeciesForm {
} }
for (let i = 0; i < pixelData.length; i += 4) { for (let i = 0; i < pixelData.length; i += 4) {
const total = pixelData.slice(i, i + 3).reduce((total: integer, value: integer) => total + value, 0); const total = pixelData.slice(i, i + 3).reduce((total: number, value: number) => total + value, 0);
if (!total) { if (!total) {
continue; continue;
} }
@ -586,27 +609,28 @@ export abstract class PokemonSpeciesForm {
Math.random = originalRandom; Math.random = originalRandom;
return Array.from(paletteColors.keys()).map(c => Object.values(rgbaFromArgb(c)) as integer[]); return Array.from(paletteColors.keys()).map(c => Object.values(rgbaFromArgb(c)) as number[]);
} }
} }
export default class PokemonSpecies extends PokemonSpeciesForm implements Localizable { export default class PokemonSpecies extends PokemonSpeciesForm implements Localizable {
public name: string; public name: string;
public subLegendary: boolean; readonly subLegendary: boolean;
public legendary: boolean; readonly legendary: boolean;
public mythical: boolean; readonly mythical: boolean;
public species: string; readonly species: string;
public growthRate: GrowthRate; readonly growthRate: GrowthRate;
public malePercent: number | null; readonly malePercent: number | null;
public genderDiffs: boolean; readonly genderDiffs: boolean;
public canChangeForm: boolean; readonly canChangeForm: boolean;
public forms: PokemonForm[]; readonly forms: PokemonForm[];
constructor(id: Species, generation: integer, subLegendary: boolean, legendary: boolean, mythical: boolean, species: string, constructor(id: Species, generation: number, subLegendary: boolean, legendary: boolean, mythical: boolean, species: string,
type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities, type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities,
baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer, baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number,
catchRate: integer, baseFriendship: integer, baseExp: integer, growthRate: GrowthRate, malePercent: number | null, catchRate: number, baseFriendship: number, baseExp: number, growthRate: GrowthRate, malePercent: number | null,
genderDiffs: boolean, canChangeForm?: boolean, ...forms: PokemonForm[]) { genderDiffs: boolean, canChangeForm?: boolean, ...forms: PokemonForm[]
) {
super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd, super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd,
catchRate, baseFriendship, baseExp, genderDiffs, false); catchRate, baseFriendship, baseExp, genderDiffs, false);
this.speciesId = id; this.speciesId = id;
@ -631,7 +655,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
}); });
} }
getName(formIndex?: integer): string { getName(formIndex?: number): string {
if (formIndex !== undefined && this.forms.length) { if (formIndex !== undefined && this.forms.length) {
const form = this.forms[formIndex]; const form = this.forms[formIndex];
let key: string | null; let key: string | null;
@ -662,11 +686,11 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
this.name = i18next.t(`pokemon:${Species[this.speciesId].toLowerCase()}`); this.name = i18next.t(`pokemon:${Species[this.speciesId].toLowerCase()}`);
} }
getWildSpeciesForLevel(level: integer, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): Species { getWildSpeciesForLevel(level: number, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): Species {
return this.getSpeciesForLevel(level, allowEvolving, false, (isBoss ? PartyMemberStrength.WEAKER : PartyMemberStrength.AVERAGE) + (gameMode?.isEndless ? 1 : 0)); return this.getSpeciesForLevel(level, allowEvolving, false, (isBoss ? PartyMemberStrength.WEAKER : PartyMemberStrength.AVERAGE) + (gameMode?.isEndless ? 1 : 0));
} }
getTrainerSpeciesForLevel(level: integer, allowEvolving: boolean = false, strength: PartyMemberStrength, currentWave: number = 0): Species { getTrainerSpeciesForLevel(level: number, allowEvolving: boolean = false, strength: PartyMemberStrength, currentWave: number = 0): Species {
return this.getSpeciesForLevel(level, allowEvolving, true, strength, currentWave); return this.getSpeciesForLevel(level, allowEvolving, true, strength, currentWave);
} }
@ -688,7 +712,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
* @param strength {@linkcode PartyMemberStrength} The strength of the party member in question * @param strength {@linkcode PartyMemberStrength} The strength of the party member in question
* @returns {@linkcode integer} The level difference from expected evolution level tolerated for a mon to be unevolved. Lower value = higher evolution chance. * @returns {@linkcode integer} The level difference from expected evolution level tolerated for a mon to be unevolved. Lower value = higher evolution chance.
*/ */
private getStrengthLevelDiff(strength: PartyMemberStrength): integer { private getStrengthLevelDiff(strength: PartyMemberStrength): number {
switch (Math.min(strength, PartyMemberStrength.STRONGER)) { switch (Math.min(strength, PartyMemberStrength.STRONGER)) {
case PartyMemberStrength.WEAKEST: case PartyMemberStrength.WEAKEST:
return 60; return 60;
@ -705,7 +729,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
} }
} }
getSpeciesForLevel(level: integer, allowEvolving: boolean = false, forTrainer: boolean = false, strength: PartyMemberStrength = PartyMemberStrength.WEAKER, currentWave: number = 0): Species { getSpeciesForLevel(level: number, allowEvolving: boolean = false, forTrainer: boolean = false, strength: PartyMemberStrength = PartyMemberStrength.WEAKER, currentWave: number = 0): Species {
const prevolutionLevels = this.getPrevolutionLevels(); const prevolutionLevels = this.getPrevolutionLevels();
if (prevolutionLevels.length) { if (prevolutionLevels.length) {
@ -847,7 +871,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
} }
// This could definitely be written better and more accurate to the getSpeciesForLevel logic, but it is only for generating movesets for evolved Pokemon // This could definitely be written better and more accurate to the getSpeciesForLevel logic, but it is only for generating movesets for evolved Pokemon
getSimulatedEvolutionChain(currentLevel: integer, forTrainer: boolean = false, isBoss: boolean = false, player: boolean = false): EvolutionLevel[] { getSimulatedEvolutionChain(currentLevel: number, forTrainer: boolean = false, isBoss: boolean = false, player: boolean = false): EvolutionLevel[] {
const ret: EvolutionLevel[] = []; const ret: EvolutionLevel[] = [];
if (pokemonPrevolutions.hasOwnProperty(this.speciesId)) { if (pokemonPrevolutions.hasOwnProperty(this.speciesId)) {
const prevolutionLevels = this.getPrevolutionLevels().reverse(); const prevolutionLevels = this.getPrevolutionLevels().reverse();
@ -870,17 +894,24 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
getCompatibleFusionSpeciesFilter(): PokemonSpeciesFilter { getCompatibleFusionSpeciesFilter(): PokemonSpeciesFilter {
const hasEvolution = pokemonEvolutions.hasOwnProperty(this.speciesId); const hasEvolution = pokemonEvolutions.hasOwnProperty(this.speciesId);
const hasPrevolution = pokemonPrevolutions.hasOwnProperty(this.speciesId); const hasPrevolution = pokemonPrevolutions.hasOwnProperty(this.speciesId);
const pseudoLegendary = this.subLegendary; const subLegendary = this.subLegendary;
const legendary = this.legendary; const legendary = this.legendary;
const mythical = this.mythical; const mythical = this.mythical;
return species => { return species => {
return (pseudoLegendary || legendary || mythical || return (
(pokemonEvolutions.hasOwnProperty(species.speciesId) === hasEvolution subLegendary
&& pokemonPrevolutions.hasOwnProperty(species.speciesId) === hasPrevolution)) || legendary
&& species.subLegendary === pseudoLegendary || mythical
|| (
pokemonEvolutions.hasOwnProperty(species.speciesId) === hasEvolution
&& pokemonPrevolutions.hasOwnProperty(species.speciesId) === hasPrevolution
)
)
&& species.subLegendary === subLegendary
&& species.legendary === legendary && species.legendary === legendary
&& species.mythical === mythical && species.mythical === mythical
&& (this.isTrainerForbidden() || !species.isTrainerForbidden()); && (this.isTrainerForbidden() || !species.isTrainerForbidden())
&& species.speciesId !== Species.DITTO;
}; };
} }
@ -899,7 +930,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
return variantData.hasOwnProperty(variantDataIndex) || variantData.hasOwnProperty(this.speciesId); return variantData.hasOwnProperty(variantDataIndex) || variantData.hasOwnProperty(this.speciesId);
} }
getFormSpriteKey(formIndex?: integer) { getFormSpriteKey(formIndex?: number) {
if (this.forms.length && (formIndex !== undefined && formIndex >= this.forms.length)) { if (this.forms.length && (formIndex !== undefined && formIndex >= this.forms.length)) {
console.warn(`Attempted accessing form with index ${formIndex} of species ${this.getName()} with only ${this.forms.length || 0} forms`); console.warn(`Attempted accessing form with index ${formIndex} of species ${this.getName()} with only ${this.forms.length || 0} forms`);
formIndex = Math.min(formIndex, this.forms.length - 1); formIndex = Math.min(formIndex, this.forms.length - 1);
@ -919,16 +950,17 @@ export class PokemonForm extends PokemonSpeciesForm {
private starterSelectableKeys: string[] = [ "10", "50", "10-pc", "50-pc", "red", "orange", "yellow", "green", "blue", "indigo", "violet" ]; private starterSelectableKeys: string[] = [ "10", "50", "10-pc", "50-pc", "red", "orange", "yellow", "green", "blue", "indigo", "violet" ];
constructor(formName: string, formKey: string, type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities, constructor(formName: string, formKey: string, type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities,
baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer, baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number,
catchRate: integer, baseFriendship: integer, baseExp: integer, genderDiffs?: boolean, formSpriteKey?: string | null, isStarterSelectable?: boolean, ) { catchRate: number, baseFriendship: number, baseExp: number, genderDiffs: boolean = false, formSpriteKey: string | null = null, isStarterSelectable: boolean = false
) {
super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd, super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd,
catchRate, baseFriendship, baseExp, !!genderDiffs, (!!isStarterSelectable || !formKey)); catchRate, baseFriendship, baseExp, genderDiffs, (isStarterSelectable || !formKey));
this.formName = formName; this.formName = formName;
this.formKey = formKey; this.formKey = formKey;
this.formSpriteKey = formSpriteKey !== undefined ? formSpriteKey : null; this.formSpriteKey = formSpriteKey;
} }
getFormSpriteKey(_formIndex?: integer) { getFormSpriteKey(_formIndex?: number) {
return this.formSpriteKey !== null ? this.formSpriteKey : this.formKey; return this.formSpriteKey !== null ? this.formSpriteKey : this.formKey;
} }
} }

View File

@ -2,8 +2,6 @@ import { randIntRange } from "#app/utils";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import i18next, { ParseKeys } from "i18next"; import i18next, { ParseKeys } from "i18next";
export { StatusEffect };
export class Status { export class Status {
public effect: StatusEffect; public effect: StatusEffect;
/** Toxic damage is `1/16 max HP * toxicTurnCount` */ /** Toxic damage is `1/16 max HP * toxicTurnCount` */

View File

@ -1,8 +1,6 @@
import Pokemon from "../field/pokemon"; import Pokemon from "../field/pokemon";
import Move from "./move"; import Move from "./move";
import { Type } from "./type"; import { Type } from "#enums/type";
import * as Utils from "../utils";
import { ChangeMovePriorityAbAttr, applyAbAttrs } from "./ability";
import { ProtectAttr } from "./move"; import { ProtectAttr } from "./move";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import i18next from "i18next"; import i18next from "i18next";
@ -58,10 +56,8 @@ export class Terrain {
switch (this.terrainType) { switch (this.terrainType) {
case TerrainType.PSYCHIC: case TerrainType.PSYCHIC:
if (!move.hasAttr(ProtectAttr)) { if (!move.hasAttr(ProtectAttr)) {
const priority = new Utils.IntegerHolder(move.priority);
applyAbAttrs(ChangeMovePriorityAbAttr, user, null, false, move, priority);
// Cancels move if the move has positive priority and targets a Pokemon grounded on the Psychic Terrain // Cancels move if the move has positive priority and targets a Pokemon grounded on the Psychic Terrain
return priority.value > 0 && user.getOpponents().some(o => targets.includes(o.getBattlerIndex()) && o.isGrounded()); return move.getPriority(user) > 0 && user.getOpponents().some(o => targets.includes(o.getBattlerIndex()) && o.isGrounded());
} }
} }

View File

@ -2,11 +2,11 @@ import BattleScene, { startingWave } from "#app/battle-scene";
import { ModifierTypeFunc, modifierTypes } from "#app/modifier/modifier-type"; import { ModifierTypeFunc, modifierTypes } from "#app/modifier/modifier-type";
import { EnemyPokemon, PokemonMove } from "#app/field/pokemon"; import { EnemyPokemon, PokemonMove } from "#app/field/pokemon";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
import { PokeballType } from "#app/data/pokeball"; import { PokeballType } from "#enums/pokeball";
import { pokemonEvolutions, pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions"; import { pokemonEvolutions, pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions";
import PokemonSpecies, { getPokemonSpecies, PokemonSpeciesFilter } from "#app/data/pokemon-species"; import PokemonSpecies, { getPokemonSpecies, PokemonSpeciesFilter } from "#app/data/pokemon-species";
import { tmSpecies } from "#app/data/balance/tms"; import { tmSpecies } from "#app/data/balance/tms";
import { Type } from "#app/data/type"; import { Type } from "#enums/type";
import { doubleBattleDialogue } from "#app/data/dialogue"; import { doubleBattleDialogue } from "#app/data/dialogue";
import { PersistentModifier } from "#app/modifier/modifier"; import { PersistentModifier } from "#app/modifier/modifier";
import { TrainerVariant } from "#app/field/trainer"; import { TrainerVariant } from "#app/field/trainer";
@ -1841,21 +1841,25 @@ export const trainerConfigs: TrainerConfigs = {
[TrainerType.RIVAL]: new TrainerConfig((t = TrainerType.RIVAL)).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL) [TrainerType.RIVAL]: new TrainerConfig((t = TrainerType.RIVAL)).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL)
.setModifierRewardFuncs(() => modifierTypes.SUPER_EXP_CHARM, () => modifierTypes.EXP_SHARE) .setModifierRewardFuncs(() => modifierTypes.SUPER_EXP_CHARM, () => modifierTypes.EXP_SHARE)
.setEventModifierRewardFuncs(() => modifierTypes.SHINY_CHARM, () => modifierTypes.ABILITY_CHARM) .setEventModifierRewardFuncs(() => modifierTypes.SHINY_CHARM, () => modifierTypes.ABILITY_CHARM)
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, Species.CHIKORITA, Species.CYNDAQUIL, Species.TOTODILE, Species.TREECKO, Species.TORCHIC, Species.MUDKIP, Species.TURTWIG, Species.CHIMCHAR, Species.PIPLUP, Species.SNIVY, Species.TEPIG, Species.OSHAWOTT, Species.CHESPIN, Species.FENNEKIN, Species.FROAKIE, Species.ROWLET, Species.LITTEN, Species.POPPLIO, Species.GROOKEY, Species.SCORBUNNY, Species.SOBBLE, Species.SPRIGATITO, Species.FUECOCO, Species.QUAXLY ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, Species.CHIKORITA, Species.CYNDAQUIL, Species.TOTODILE, Species.TREECKO, Species.TORCHIC, Species.MUDKIP, Species.TURTWIG, Species.CHIMCHAR, Species.PIPLUP, Species.SNIVY, Species.TEPIG, Species.OSHAWOTT, Species.CHESPIN, Species.FENNEKIN, Species.FROAKIE, Species.ROWLET, Species.LITTEN, Species.POPPLIO, Species.GROOKEY, Species.SCORBUNNY, Species.SOBBLE, Species.SPRIGATITO, Species.FUECOCO, Species.QUAXLY ], TrainerSlot.TRAINER, true,
(p => p.abilityIndex = 0)))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEY, Species.HOOTHOOT, Species.TAILLOW, Species.STARLY, Species.PIDOVE, Species.FLETCHLING, Species.PIKIPEK, Species.ROOKIDEE, Species.WATTREL ], TrainerSlot.TRAINER, true)), .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEY, Species.HOOTHOOT, Species.TAILLOW, Species.STARLY, Species.PIDOVE, Species.FLETCHLING, Species.PIKIPEK, Species.ROOKIDEE, Species.WATTREL ], TrainerSlot.TRAINER, true)),
[TrainerType.RIVAL_2]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setMoneyMultiplier(1.25).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL_2) [TrainerType.RIVAL_2]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setMoneyMultiplier(1.25).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL_2)
.setModifierRewardFuncs(() => modifierTypes.EXP_SHARE) .setModifierRewardFuncs(() => modifierTypes.EXP_SHARE)
.setEventModifierRewardFuncs(() => modifierTypes.SHINY_CHARM) .setEventModifierRewardFuncs(() => modifierTypes.SHINY_CHARM)
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.IVYSAUR, Species.CHARMELEON, Species.WARTORTLE, Species.BAYLEEF, Species.QUILAVA, Species.CROCONAW, Species.GROVYLE, Species.COMBUSKEN, Species.MARSHTOMP, Species.GROTLE, Species.MONFERNO, Species.PRINPLUP, Species.SERVINE, Species.PIGNITE, Species.DEWOTT, Species.QUILLADIN, Species.BRAIXEN, Species.FROGADIER, Species.DARTRIX, Species.TORRACAT, Species.BRIONNE, Species.THWACKEY, Species.RABOOT, Species.DRIZZILE, Species.FLORAGATO, Species.CROCALOR, Species.QUAXWELL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.IVYSAUR, Species.CHARMELEON, Species.WARTORTLE, Species.BAYLEEF, Species.QUILAVA, Species.CROCONAW, Species.GROVYLE, Species.COMBUSKEN, Species.MARSHTOMP, Species.GROTLE, Species.MONFERNO, Species.PRINPLUP, Species.SERVINE, Species.PIGNITE, Species.DEWOTT, Species.QUILLADIN, Species.BRAIXEN, Species.FROGADIER, Species.DARTRIX, Species.TORRACAT, Species.BRIONNE, Species.THWACKEY, Species.RABOOT, Species.DRIZZILE, Species.FLORAGATO, Species.CROCALOR, Species.QUAXWELL ], TrainerSlot.TRAINER, true,
(p => p.abilityIndex = 0)))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOTTO, Species.HOOTHOOT, Species.TAILLOW, Species.STARAVIA, Species.TRANQUILL, Species.FLETCHINDER, Species.TRUMBEAK, Species.CORVISQUIRE, Species.WATTREL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOTTO, Species.HOOTHOOT, Species.TAILLOW, Species.STARAVIA, Species.TRANQUILL, Species.FLETCHINDER, Species.TRUMBEAK, Species.CORVISQUIRE, Species.WATTREL ], TrainerSlot.TRAINER, true))
.setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)), .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)),
[TrainerType.RIVAL_3]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setMoneyMultiplier(1.5).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL_3) [TrainerType.RIVAL_3]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setMoneyMultiplier(1.5).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL_3)
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true,
(p => p.abilityIndex = 0)))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true))
.setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)) .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450))
.setSpeciesFilter(species => species.baseTotal >= 540), .setSpeciesFilter(species => species.baseTotal >= 540),
[TrainerType.RIVAL_4]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setBoss().setStaticParty().setMoneyMultiplier(1.75).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival_2").setMixedBattleBgm("battle_rival_2").setPartyTemplates(trainerPartyTemplates.RIVAL_4) [TrainerType.RIVAL_4]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setBoss().setStaticParty().setMoneyMultiplier(1.75).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival_2").setMixedBattleBgm("battle_rival_2").setPartyTemplates(trainerPartyTemplates.RIVAL_4)
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true,
(p => p.abilityIndex = 0)))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true))
.setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)) .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450))
.setSpeciesFilter(species => species.baseTotal >= 540) .setSpeciesFilter(species => species.baseTotal >= 540)
@ -1865,7 +1869,10 @@ export const trainerConfigs: TrainerConfigs = {
}), }),
[TrainerType.RIVAL_5]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setBoss().setStaticParty().setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival_3").setMixedBattleBgm("battle_rival_3").setPartyTemplates(trainerPartyTemplates.RIVAL_5) [TrainerType.RIVAL_5]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setBoss().setStaticParty().setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival_3").setMixedBattleBgm("battle_rival_3").setPartyTemplates(trainerPartyTemplates.RIVAL_5)
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true, .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true,
p => p.setBoss(true, 2))) p => {
p.setBoss(true, 2);
p.abilityIndex = 0;
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true))
.setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)) .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450))
.setSpeciesFilter(species => species.baseTotal >= 540) .setSpeciesFilter(species => species.baseTotal >= 540)
@ -1883,6 +1890,7 @@ export const trainerConfigs: TrainerConfigs = {
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true, .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true,
p => { p => {
p.setBoss(true, 3); p.setBoss(true, 3);
p.abilityIndex = 0;
p.generateAndPopulateMoveset(); p.generateAndPopulateMoveset();
})) }))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true, .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true,

View File

@ -1,25 +1,4 @@
export enum Type { import { Type } from "#enums/type";
UNKNOWN = -1,
NORMAL = 0,
FIGHTING,
FLYING,
POISON,
GROUND,
ROCK,
BUG,
GHOST,
STEEL,
FIRE,
WATER,
GRASS,
ELECTRIC,
PSYCHIC,
ICE,
DRAGON,
DARK,
FAIRY,
STELLAR
}
export type TypeDamageMultiplier = 0 | 0.125 | 0.25 | 0.5 | 1 | 2 | 4 | 8; export type TypeDamageMultiplier = 0 | 0.125 | 0.25 | 0.5 | 1 | 2 | 4 | 8;

View File

@ -2,7 +2,7 @@ import { Biome } from "#enums/biome";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
import Pokemon from "../field/pokemon"; import Pokemon from "../field/pokemon";
import { Type } from "./type"; import { Type } from "#enums/type";
import Move, { AttackMove } from "./move"; import Move, { AttackMove } from "./move";
import * as Utils from "../utils"; import * as Utils from "../utils";
import BattleScene from "../battle-scene"; import BattleScene from "../battle-scene";
@ -10,7 +10,6 @@ import { SuppressWeatherEffectAbAttr } from "./ability";
import { TerrainType, getTerrainName } from "./terrain"; import { TerrainType, getTerrainName } from "./terrain";
import i18next from "i18next"; import i18next from "i18next";
export { WeatherType };
export class Weather { export class Weather {
public weatherType: WeatherType; public weatherType: WeatherType;
public turnsLeft: integer; public turnsLeft: integer;

View File

@ -28,4 +28,5 @@ export enum ArenaTagType {
FIRE_GRASS_PLEDGE = "FIRE_GRASS_PLEDGE", FIRE_GRASS_PLEDGE = "FIRE_GRASS_PLEDGE",
WATER_FIRE_PLEDGE = "WATER_FIRE_PLEDGE", WATER_FIRE_PLEDGE = "WATER_FIRE_PLEDGE",
GRASS_WATER_PLEDGE = "GRASS_WATER_PLEDGE", GRASS_WATER_PLEDGE = "GRASS_WATER_PLEDGE",
FAIRY_LOCK = "FAIRY_LOCK",
} }

View File

@ -74,6 +74,7 @@ export enum BattlerTagType {
DRAGON_CHEER = "DRAGON_CHEER", DRAGON_CHEER = "DRAGON_CHEER",
NO_RETREAT = "NO_RETREAT", NO_RETREAT = "NO_RETREAT",
GORILLA_TACTICS = "GORILLA_TACTICS", GORILLA_TACTICS = "GORILLA_TACTICS",
UNBURDEN = "UNBURDEN",
THROAT_CHOPPED = "THROAT_CHOPPED", THROAT_CHOPPED = "THROAT_CHOPPED",
TAR_SHOT = "TAR_SHOT", TAR_SHOT = "TAR_SHOT",
BURNED_UP = "BURNED_UP", BURNED_UP = "BURNED_UP",
@ -87,5 +88,9 @@ export enum BattlerTagType {
IMPRISON = "IMPRISON", IMPRISON = "IMPRISON",
SYRUP_BOMB = "SYRUP_BOMB", SYRUP_BOMB = "SYRUP_BOMB",
ELECTRIFIED = "ELECTRIFIED", ELECTRIFIED = "ELECTRIFIED",
TELEKINESIS = "TELEKINESIS" TELEKINESIS = "TELEKINESIS",
COMMANDED = "COMMANDED",
GRUDGE = "GRUDGE",
PSYCHO_SHIFT = "PSYCHO_SHIFT",
ENDURE_TOKEN = "ENDURE_TOKEN",
} }

View File

@ -12,5 +12,15 @@ export enum PokemonAnimType {
* Removes a Pokemon's Substitute doll from the field. * Removes a Pokemon's Substitute doll from the field.
* The Pokemon then moves back to its original position. * The Pokemon then moves back to its original position.
*/ */
SUBSTITUTE_REMOVE SUBSTITUTE_REMOVE,
/**
* Brings Tatsugiri and Dondozo to the center of the field, with
* Tatsugiri jumping into the Dondozo's mouth
*/
COMMANDER_APPLY,
/**
* Dondozo "spits out" Tatsugiri, moving Tatsugiri back to its original
* field position.
*/
COMMANDER_REMOVE
} }

22
src/enums/type.ts Normal file
View File

@ -0,0 +1,22 @@
export enum Type {
UNKNOWN = -1,
NORMAL = 0,
FIGHTING,
FLYING,
POISON,
GROUND,
ROCK,
BUG,
GHOST,
STEEL,
FIRE,
WATER,
GRASS,
ELECTRIC,
PSYCHIC,
ICE,
DRAGON,
DARK,
FAIRY,
STELLAR
}

View File

@ -1,7 +1,7 @@
import { ArenaTagSide } from "#app/data/arena-tag"; import { ArenaTagSide } from "#app/data/arena-tag";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { TerrainType } from "#app/data/terrain"; import { TerrainType } from "#app/data/terrain";
import { WeatherType } from "#app/data/weather"; import { WeatherType } from "#enums/weather-type";
/** Alias for all {@linkcode ArenaEvent} type strings */ /** Alias for all {@linkcode ArenaEvent} type strings */
export enum ArenaEventType { export enum ArenaEventType {

View File

@ -1,5 +1,5 @@
import BattleScene from "../battle-scene"; import BattleScene from "../battle-scene";
import { PokeballType } from "../data/pokeball"; import { PokeballType } from "#enums/pokeball";
import * as Utils from "../utils"; import * as Utils from "../utils";
export function addPokeballOpenParticles(scene: BattleScene, x: number, y: number, pokeballType: PokeballType): void { export function addPokeballOpenParticles(scene: BattleScene, x: number, y: number, pokeballType: PokeballType): void {

View File

@ -3,9 +3,9 @@ import { biomePokemonPools, BiomePoolTier, BiomeTierTrainerPools, biomeTrainerPo
import { Constructor } from "#app/utils"; import { Constructor } from "#app/utils";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species"; import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species";
import { getTerrainClearMessage, getTerrainStartMessage, getWeatherClearMessage, getWeatherStartMessage, Weather, WeatherType } from "#app/data/weather"; import { getTerrainClearMessage, getTerrainStartMessage, getWeatherClearMessage, getWeatherStartMessage, Weather } from "#app/data/weather";
import { CommonAnim } from "#app/data/battle-anims"; import { CommonAnim } from "#app/data/battle-anims";
import { Type } from "#app/data/type"; import { Type } from "#enums/type";
import Move from "#app/data/move"; import Move from "#app/data/move";
import { ArenaTag, ArenaTagSide, ArenaTrapTag, getArenaTag } from "#app/data/arena-tag"; import { ArenaTag, ArenaTagSide, ArenaTrapTag, getArenaTag } from "#app/data/arena-tag";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
@ -31,6 +31,7 @@ import { Abilities } from "#enums/abilities";
import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#app/data/pokemon-forms";
import { CommonAnimPhase } from "#app/phases/common-anim-phase"; import { CommonAnimPhase } from "#app/phases/common-anim-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { WeatherType } from "#enums/weather-type";
export class Arena { export class Arena {
public scene: BattleScene; public scene: BattleScene;

View File

@ -212,7 +212,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
this.scene.anims.create({ this.scene.anims.create({
key: config.spriteKey, key: config.spriteKey,
frames: frameNames, frames: frameNames,
frameRate: 12, frameRate: 10,
repeat: -1 repeat: -1
}); });
} }

File diff suppressed because it is too large Load Diff

View File

@ -236,7 +236,7 @@ export class GameMode implements GameModeConfig {
* @returns true if waveIndex is a multiple of 50 in Endless * @returns true if waveIndex is a multiple of 50 in Endless
*/ */
isEndlessBoss(waveIndex: integer): boolean { isEndlessBoss(waveIndex: integer): boolean {
return !!(waveIndex % 50) && return waveIndex % 50 === 0 &&
(this.modeId === GameModes.ENDLESS || this.modeId === GameModes.SPLICED_ENDLESS); (this.modeId === GameModes.ENDLESS || this.modeId === GameModes.SPLICED_ENDLESS);
} }

View File

@ -232,7 +232,7 @@ export class LoadingScene extends SceneBase {
// Get current lang and load the types atlas for it. English will only load types while all other languages will load types and types_<lang> // Get current lang and load the types atlas for it. English will only load types while all other languages will load types and types_<lang>
const lang = i18next.resolvedLanguage; const lang = i18next.resolvedLanguage;
if (lang !== "en") { if (lang !== "en") {
if (Utils.verifyLang(lang)) { if (Utils.hasAllLocalizedSprites(lang)) {
this.loadAtlas(`statuses_${lang}`, ""); this.loadAtlas(`statuses_${lang}`, "");
this.loadAtlas(`types_${lang}`, ""); this.loadAtlas(`types_${lang}`, "");
} else { } else {

View File

@ -3,30 +3,31 @@ import { EvolutionItem, pokemonEvolutions } from "#app/data/balance/pokemon-evol
import { tmPoolTiers, tmSpecies } from "#app/data/balance/tms"; import { tmPoolTiers, tmSpecies } from "#app/data/balance/tms";
import { getBerryEffectDescription, getBerryName } from "#app/data/berry"; import { getBerryEffectDescription, getBerryName } from "#app/data/berry";
import { allMoves, AttackMove, selfStatLowerMoves } from "#app/data/move"; import { allMoves, AttackMove, selfStatLowerMoves } from "#app/data/move";
import { getNatureName, getNatureStatMultiplier, Nature } from "#app/data/nature"; import { getNatureName, getNatureStatMultiplier } from "#app/data/nature";
import { getPokeballCatchMultiplier, getPokeballName, MAX_PER_TYPE_POKEBALLS, PokeballType } from "#app/data/pokeball"; import { getPokeballCatchMultiplier, getPokeballName, MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball";
import { FormChangeItem, pokemonFormChanges, SpeciesFormChangeCondition, SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms"; import { FormChangeItem, pokemonFormChanges, SpeciesFormChangeCondition, SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms";
import { getStatusEffectDescriptor, StatusEffect } from "#app/data/status-effect"; import { getStatusEffectDescriptor } from "#app/data/status-effect";
import { Type } from "#app/data/type"; import { Type } from "#enums/type";
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { import { AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier, TempExtraModifierModifier, CriticalCatchChanceBoosterModifier } from "#app/modifier/modifier";
AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier, TempExtraModifierModifier
} from "#app/modifier/modifier";
import { ModifierTier } from "#app/modifier/modifier-tier"; import { ModifierTier } from "#app/modifier/modifier-tier";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { Unlockables } from "#app/system/unlockables"; import { Unlockables } from "#app/system/unlockables";
import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system/voucher"; import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system/voucher";
import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler"; import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler";
import { getModifierTierTextTint } from "#app/ui/text"; import { getModifierTierTextTint } from "#app/ui/text";
import { formatMoney, getEnumKeys, getEnumValues, IntegerHolder, isNullOrUndefined, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils"; import { formatMoney, getEnumKeys, getEnumValues, isNullOrUndefined, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Nature } from "#enums/nature";
import { PokeballType } from "#enums/pokeball";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { SpeciesFormKey } from "#enums/species-form-key"; import { SpeciesFormKey } from "#enums/species-form-key";
import { getStatKey, PermanentStat, Stat, TEMP_BATTLE_STATS, TempBattleStat } from "#enums/stat"; import { getStatKey, PermanentStat, Stat, TEMP_BATTLE_STATS, TempBattleStat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
import i18next from "i18next"; import i18next from "i18next";
const outputModifierData = false; const outputModifierData = false;
@ -730,7 +731,7 @@ export class MoneyRewardModifierType extends ModifierType {
} }
getDescription(scene: BattleScene): string { getDescription(scene: BattleScene): string {
const moneyAmount = new IntegerHolder(scene.getWaveMoneyAmount(this.moneyMultiplier)); const moneyAmount = new NumberHolder(scene.getWaveMoneyAmount(this.moneyMultiplier));
scene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount); scene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount);
const formattedMoney = formatMoney(scene.moneyFormat, moneyAmount.value); const formattedMoney = formatMoney(scene.moneyFormat, moneyAmount.value);
@ -1553,6 +1554,7 @@ export const modifierTypes = {
SHINY_CHARM: () => new ModifierType("modifierType:ModifierType.SHINY_CHARM", "shiny_charm", (type, _args) => new ShinyRateBoosterModifier(type)), SHINY_CHARM: () => new ModifierType("modifierType:ModifierType.SHINY_CHARM", "shiny_charm", (type, _args) => new ShinyRateBoosterModifier(type)),
ABILITY_CHARM: () => new ModifierType("modifierType:ModifierType.ABILITY_CHARM", "ability_charm", (type, _args) => new HiddenAbilityRateBoosterModifier(type)), ABILITY_CHARM: () => new ModifierType("modifierType:ModifierType.ABILITY_CHARM", "ability_charm", (type, _args) => new HiddenAbilityRateBoosterModifier(type)),
CATCHING_CHARM: () => new ModifierType("modifierType:ModifierType.CATCHING_CHARM", "catching_charm", (type, _args) => new CriticalCatchChanceBoosterModifier(type)),
IV_SCANNER: () => new ModifierType("modifierType:ModifierType.IV_SCANNER", "scanner", (type, _args) => new IvScannerModifier(type)), IV_SCANNER: () => new ModifierType("modifierType:ModifierType.IV_SCANNER", "scanner", (type, _args) => new IvScannerModifier(type)),
@ -1619,19 +1621,21 @@ const modifierPool: ModifierPool = {
new WeightedModifierType(modifierTypes.POKEBALL, (party: Pokemon[]) => (hasMaximumBalls(party, PokeballType.POKEBALL)) ? 0 : 6, 6), new WeightedModifierType(modifierTypes.POKEBALL, (party: Pokemon[]) => (hasMaximumBalls(party, PokeballType.POKEBALL)) ? 0 : 6, 6),
new WeightedModifierType(modifierTypes.RARE_CANDY, 2), new WeightedModifierType(modifierTypes.RARE_CANDY, 2),
new WeightedModifierType(modifierTypes.POTION, (party: Pokemon[]) => { new WeightedModifierType(modifierTypes.POTION, (party: Pokemon[]) => {
const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 10 || p.getHpRatio() <= 0.875) && !p.isFainted()).length, 3); const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 10 && p.getHpRatio() <= 0.875) && !p.isFainted()).length, 3);
return thresholdPartyMemberCount * 3; return thresholdPartyMemberCount * 3;
}, 9), }, 9),
new WeightedModifierType(modifierTypes.SUPER_POTION, (party: Pokemon[]) => { new WeightedModifierType(modifierTypes.SUPER_POTION, (party: Pokemon[]) => {
const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 25 || p.getHpRatio() <= 0.75) && !p.isFainted()).length, 3); const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 25 && p.getHpRatio() <= 0.75) && !p.isFainted()).length, 3);
return thresholdPartyMemberCount; return thresholdPartyMemberCount;
}, 3), }, 3),
new WeightedModifierType(modifierTypes.ETHER, (party: Pokemon[]) => { new WeightedModifierType(modifierTypes.ETHER, (party: Pokemon[]) => {
const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed >= Math.floor(m.getMovePp() / 2)).length).length, 3); const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA)
&& p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)).length).length, 3);
return thresholdPartyMemberCount * 3; return thresholdPartyMemberCount * 3;
}, 9), }, 9),
new WeightedModifierType(modifierTypes.MAX_ETHER, (party: Pokemon[]) => { new WeightedModifierType(modifierTypes.MAX_ETHER, (party: Pokemon[]) => {
const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed >= Math.floor(m.getMovePp() / 2)).length).length, 3); const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA)
&& p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)).length).length, 3);
return thresholdPartyMemberCount; return thresholdPartyMemberCount;
}, 3), }, 3),
new WeightedModifierType(modifierTypes.LURE, lureWeightFunc(10, 2)), new WeightedModifierType(modifierTypes.LURE, lureWeightFunc(10, 2)),
@ -1665,11 +1669,11 @@ const modifierPool: ModifierPool = {
return party.filter(p => p.isFainted()).length >= Math.ceil(party.length / 2) ? 1 : 0; return party.filter(p => p.isFainted()).length >= Math.ceil(party.length / 2) ? 1 : 0;
}, 1), }, 1),
new WeightedModifierType(modifierTypes.HYPER_POTION, (party: Pokemon[]) => { new WeightedModifierType(modifierTypes.HYPER_POTION, (party: Pokemon[]) => {
const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 100 || p.getHpRatio() <= 0.625) && !p.isFainted()).length, 3); const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 100 && p.getHpRatio() <= 0.625) && !p.isFainted()).length, 3);
return thresholdPartyMemberCount * 3; return thresholdPartyMemberCount * 3;
}, 9), }, 9),
new WeightedModifierType(modifierTypes.MAX_POTION, (party: Pokemon[]) => { new WeightedModifierType(modifierTypes.MAX_POTION, (party: Pokemon[]) => {
const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 150 || p.getHpRatio() <= 0.5) && !p.isFainted()).length, 3); const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 100 && p.getHpRatio() <= 0.5) && !p.isFainted()).length, 3);
return thresholdPartyMemberCount; return thresholdPartyMemberCount;
}, 3), }, 3),
new WeightedModifierType(modifierTypes.FULL_RESTORE, (party: Pokemon[]) => { new WeightedModifierType(modifierTypes.FULL_RESTORE, (party: Pokemon[]) => {
@ -1679,15 +1683,17 @@ const modifierPool: ModifierPool = {
} }
return false; return false;
})).length, 3); })).length, 3);
const thresholdPartyMemberCount = Math.floor((Math.min(party.filter(p => (p.getInverseHp() >= 150 || p.getHpRatio() <= 0.5) && !p.isFainted()).length, 3) + statusEffectPartyMemberCount) / 2); const thresholdPartyMemberCount = Math.floor((Math.min(party.filter(p => (p.getInverseHp() >= 100 && p.getHpRatio() <= 0.5) && !p.isFainted()).length, 3) + statusEffectPartyMemberCount) / 2);
return thresholdPartyMemberCount; return thresholdPartyMemberCount;
}, 3), }, 3),
new WeightedModifierType(modifierTypes.ELIXIR, (party: Pokemon[]) => { new WeightedModifierType(modifierTypes.ELIXIR, (party: Pokemon[]) => {
const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed >= Math.floor(m.getMovePp() / 2)).length).length, 3); const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA)
&& p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)).length).length, 3);
return thresholdPartyMemberCount * 3; return thresholdPartyMemberCount * 3;
}, 9), }, 9),
new WeightedModifierType(modifierTypes.MAX_ELIXIR, (party: Pokemon[]) => { new WeightedModifierType(modifierTypes.MAX_ELIXIR, (party: Pokemon[]) => {
const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed >= Math.floor(m.getMovePp() / 2)).length).length, 3); const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA)
&& p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)).length).length, 3);
return thresholdPartyMemberCount; return thresholdPartyMemberCount;
}, 3), }, 3),
new WeightedModifierType(modifierTypes.DIRE_HIT, 4), new WeightedModifierType(modifierTypes.DIRE_HIT, 4),
@ -1696,10 +1702,7 @@ const modifierPool: ModifierPool = {
new WeightedModifierType(modifierTypes.EVOLUTION_ITEM, (party: Pokemon[]) => { new WeightedModifierType(modifierTypes.EVOLUTION_ITEM, (party: Pokemon[]) => {
return Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 15), 8); return Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 15), 8);
}, 8), }, 8),
new WeightedModifierType(modifierTypes.MAP, new WeightedModifierType(modifierTypes.MAP, (party: Pokemon[]) => party[0].scene.gameMode.isClassic && party[0].scene.currentBattle.waveIndex < 180 ? 1 : 0, 1),
(party: Pokemon[]) => party[0].scene.gameMode.isClassic && party[0].scene.currentBattle.waveIndex < 180 ? party[0].scene.eventManager.isEventActive() ? 2 : 1 : 0,
(party: Pokemon[]) => party[0].scene.eventManager.isEventActive() ? 2 : 1),
new WeightedModifierType(modifierTypes.SOOTHE_BELL, (party: Pokemon[]) => party[0].scene.eventManager.isEventActive() ? 3 : 0),
new WeightedModifierType(modifierTypes.TM_GREAT, 3), new WeightedModifierType(modifierTypes.TM_GREAT, 3),
new WeightedModifierType(modifierTypes.MEMORY_MUSHROOM, (party: Pokemon[]) => { new WeightedModifierType(modifierTypes.MEMORY_MUSHROOM, (party: Pokemon[]) => {
if (!party.find(p => p.getLearnableLevelMoves().length)) { if (!party.find(p => p.getLearnableLevelMoves().length)) {
@ -1773,7 +1776,7 @@ const modifierPool: ModifierPool = {
new WeightedModifierType(modifierTypes.CANDY_JAR, skipInLastClassicWaveOrDefault(5)), new WeightedModifierType(modifierTypes.CANDY_JAR, skipInLastClassicWaveOrDefault(5)),
new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 9), new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 9),
new WeightedModifierType(modifierTypes.TM_ULTRA, 11), new WeightedModifierType(modifierTypes.TM_ULTRA, 11),
new WeightedModifierType(modifierTypes.RARER_CANDY, (party: Pokemon[]) => party[0].scene.eventManager.isEventActive() ? 6 : 4), new WeightedModifierType(modifierTypes.RARER_CANDY, 4),
new WeightedModifierType(modifierTypes.GOLDEN_PUNCH, skipInLastClassicWaveOrDefault(2)), new WeightedModifierType(modifierTypes.GOLDEN_PUNCH, skipInLastClassicWaveOrDefault(2)),
new WeightedModifierType(modifierTypes.IV_SCANNER, skipInLastClassicWaveOrDefault(4)), new WeightedModifierType(modifierTypes.IV_SCANNER, skipInLastClassicWaveOrDefault(4)),
new WeightedModifierType(modifierTypes.EXP_CHARM, skipInLastClassicWaveOrDefault(8)), new WeightedModifierType(modifierTypes.EXP_CHARM, skipInLastClassicWaveOrDefault(8)),
@ -1796,7 +1799,8 @@ const modifierPool: ModifierPool = {
new WeightedModifierType(modifierTypes.BATON, 2), new WeightedModifierType(modifierTypes.BATON, 2),
new WeightedModifierType(modifierTypes.SOUL_DEW, 7), new WeightedModifierType(modifierTypes.SOUL_DEW, 7),
//new WeightedModifierType(modifierTypes.OVAL_CHARM, 6), //new WeightedModifierType(modifierTypes.OVAL_CHARM, 6),
new WeightedModifierType(modifierTypes.SOOTHE_BELL, (party: Pokemon[]) => party[0].scene.eventManager.isEventActive() ? 0 : 4), new WeightedModifierType(modifierTypes.CATCHING_CHARM, (party: Pokemon[]) => !party[0].scene.gameMode.isFreshStartChallenge() && party[0].scene.gameData.getSpeciesCount(d => !!d.caughtAttr) > 100 ? 4 : 0, 4),
new WeightedModifierType(modifierTypes.SOOTHE_BELL, 4),
new WeightedModifierType(modifierTypes.ABILITY_CHARM, skipInClassicAfterWave(189, 6)), new WeightedModifierType(modifierTypes.ABILITY_CHARM, skipInClassicAfterWave(189, 6)),
new WeightedModifierType(modifierTypes.FOCUS_BAND, 5), new WeightedModifierType(modifierTypes.FOCUS_BAND, 5),
new WeightedModifierType(modifierTypes.KINGS_ROCK, 3), new WeightedModifierType(modifierTypes.KINGS_ROCK, 3),

View File

@ -6,7 +6,6 @@ import { allMoves } from "#app/data/move";
import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball"; import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball";
import { type FormChangeItem, SpeciesFormChangeItemTrigger, SpeciesFormChangeLapseTeraTrigger, SpeciesFormChangeTeraTrigger } from "#app/data/pokemon-forms"; import { type FormChangeItem, SpeciesFormChangeItemTrigger, SpeciesFormChangeLapseTeraTrigger, SpeciesFormChangeTeraTrigger } from "#app/data/pokemon-forms";
import { getStatusEffectHealText } from "#app/data/status-effect"; import { getStatusEffectHealText } from "#app/data/status-effect";
import { Type } from "#app/data/type";
import Pokemon, { type PlayerPokemon } from "#app/field/pokemon"; 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";
@ -22,15 +21,18 @@ import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, toDmgValue }
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
import { Moves } from "#enums/moves";
import type { Nature } from "#enums/nature"; import type { Nature } from "#enums/nature";
import type { PokeballType } from "#enums/pokeball"; import type { PokeballType } from "#enums/pokeball";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { type PermanentStat, type TempBattleStat, BATTLE_STATS, Stat, TEMP_BATTLE_STATS } from "#enums/stat"; import { type PermanentStat, type TempBattleStat, BATTLE_STATS, Stat, TEMP_BATTLE_STATS } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import { Type } from "#enums/type";
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"; import { FRIENDSHIP_GAIN_FROM_RARE_CANDY } from "#app/data/balance/starters";
import { applyAbAttrs, CommanderAbAttr } from "#app/data/ability";
export type ModifierPredicate = (modifier: Modifier) => boolean; export type ModifierPredicate = (modifier: Modifier) => boolean;
@ -727,10 +729,10 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier {
//Applies to items with chance of activating secondary effects ie Kings Rock //Applies to items with chance of activating secondary effects ie Kings Rock
getSecondaryChanceMultiplier(pokemon: Pokemon): number { getSecondaryChanceMultiplier(pokemon: Pokemon): number {
// Temporary quickfix to stop game from freezing when the opponet uses u-turn while holding on to king's rock // Temporary quickfix to stop game from freezing when the opponet uses u-turn while holding on to king's rock
if (!pokemon.getLastXMoves(0)[0]) { if (!pokemon.getLastXMoves()[0]) {
return 1; return 1;
} }
const sheerForceAffected = allMoves[pokemon.getLastXMoves(0)[0].move].chance >= 0 && pokemon.hasAbility(Abilities.SHEER_FORCE); const sheerForceAffected = allMoves[pokemon.getLastXMoves()[0].move].chance >= 0 && pokemon.hasAbility(Abilities.SHEER_FORCE);
if (sheerForceAffected) { if (sheerForceAffected) {
return 0; return 0;
@ -746,7 +748,7 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier {
return 0; return 0;
} }
if (pokemon.isPlayer() && forThreshold) { if (pokemon.isPlayer() && forThreshold) {
return scene.getParty().map(p => this.getMaxHeldItemCount(p)).reduce((stackCount: number, maxStackCount: number) => Math.max(stackCount, maxStackCount), 0); return scene.getPlayerParty().map(p => this.getMaxHeldItemCount(p)).reduce((stackCount: number, maxStackCount: number) => Math.max(stackCount, maxStackCount), 0);
} }
return this.getMaxHeldItemCount(pokemon); return this.getMaxHeldItemCount(pokemon);
} }
@ -1767,10 +1769,10 @@ export class HitHealModifier extends PokemonHeldItemModifier {
* @returns `true` if the {@linkcode Pokemon} was healed * @returns `true` if the {@linkcode Pokemon} was healed
*/ */
override apply(pokemon: Pokemon): boolean { override apply(pokemon: Pokemon): boolean {
if (pokemon.turnData.damageDealt && !pokemon.isFullHp()) { if (pokemon.turnData.totalDamageDealt && !pokemon.isFullHp()) {
const scene = pokemon.scene; const scene = pokemon.scene;
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(), scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(),
toDmgValue(pokemon.turnData.damageDealt / 8) * this.stackCount, i18next.t("modifier:hitHealApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }), true)); toDmgValue(pokemon.turnData.totalDamageDealt / 8) * this.stackCount, i18next.t("modifier:hitHealApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }), true));
} }
return true; return true;
@ -1937,10 +1939,16 @@ export class PokemonInstantReviveModifier extends PokemonHeldItemModifier {
* @returns always `true` * @returns always `true`
*/ */
override apply(pokemon: Pokemon): boolean { override apply(pokemon: Pokemon): boolean {
// Restore the Pokemon to half HP
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(),
toDmgValue(pokemon.getMaxHp() / 2), i18next.t("modifier:pokemonInstantReviveApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }), false, false, true)); toDmgValue(pokemon.getMaxHp() / 2), i18next.t("modifier:pokemonInstantReviveApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }), false, false, true));
// Remove the Pokemon's FAINT status
pokemon.resetStatus(true, false, true); pokemon.resetStatus(true, false, true);
// Reapply Commander on the Pokemon's side of the field, if applicable
const field = pokemon.isPlayer() ? pokemon.scene.getPlayerField() : pokemon.scene.getEnemyField();
field.forEach((p) => applyAbAttrs(CommanderAbAttr, p, null, false));
return true; return true;
} }
@ -2022,7 +2030,7 @@ export abstract class ConsumablePokemonModifier extends ConsumableModifier {
abstract override apply(playerPokemon: PlayerPokemon, ...args: unknown[]): boolean | Promise<boolean>; abstract override apply(playerPokemon: PlayerPokemon, ...args: unknown[]): boolean | Promise<boolean>;
getPokemon(scene: BattleScene) { getPokemon(scene: BattleScene) {
return scene.getParty().find(p => p.id === this.pokemonId); return scene.getPlayerParty().find(p => p.id === this.pokemonId);
} }
} }
@ -2224,7 +2232,7 @@ export class PokemonLevelIncrementModifier extends ConsumablePokemonModifier {
playerPokemon.addFriendship(FRIENDSHIP_GAIN_FROM_RARE_CANDY); 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.getPlayerParty().indexOf(playerPokemon), playerPokemon.level - levelCount.value, playerPokemon.level));
return true; return true;
} }
@ -2244,7 +2252,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, LearnMoveType.TM)); playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getPlayerParty().indexOf(playerPokemon), this.type.moveId, LearnMoveType.TM));
return true; return true;
} }
@ -2266,7 +2274,7 @@ export class RememberMoveModifier extends ConsumablePokemonModifier {
*/ */
override apply(playerPokemon: PlayerPokemon, cost?: number): boolean { override apply(playerPokemon: PlayerPokemon, cost?: number): boolean {
playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), playerPokemon.getLearnableLevelMoves()[this.levelMoveIndex], LearnMoveType.MEMORY, cost)); playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getPlayerParty().indexOf(playerPokemon), playerPokemon.getLearnableLevelMoves()[this.levelMoveIndex], LearnMoveType.MEMORY, cost));
return true; return true;
} }
@ -2682,32 +2690,57 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier {
} }
/** /**
* Applies {@linkcode PokemonMultiHitModifier} * For each stack, converts 25 percent of attack damage into an additional strike.
* @param _pokemon The {@linkcode Pokemon} using the move * @param pokemon The {@linkcode Pokemon} using the move
* @param count {@linkcode NumberHolder} holding the number of items * @param moveId The {@linkcode Moves | identifier} for the move being used
* @param power {@linkcode NumberHolder} holding the power of the move * @param count {@linkcode NumberHolder} holding the move's hit count for this turn
* @param damageMultiplier {@linkcode NumberHolder} holding a damage multiplier applied to a strike of this move
* @returns always `true` * @returns always `true`
*/ */
override apply(_pokemon: Pokemon, count: NumberHolder, power: NumberHolder): boolean { override apply(pokemon: Pokemon, moveId: Moves, count: NumberHolder | null = null, damageMultiplier: NumberHolder | null = null): boolean {
count.value *= (this.getStackCount() + 1); const move = allMoves[moveId];
/**
switch (this.getStackCount()) { * The move must meet Parental Bond's restrictions for this item
case 1: * to apply. This means
power.value *= 0.4; * - Only attacks are boosted
break; * - Multi-strike moves, charge moves, and self-sacrificial moves are not boosted
case 2: * (though Multi-Lens can still affect moves boosted by Parental Bond)
power.value *= 0.25; * - Multi-target moves are not boosted *unless* they can only hit a single Pokemon
break; * - Fling, Uproar, Rollout, Ice Ball, and Endeavor are not boosted
case 3: */
power.value *= 0.175; if (!move.canBeMultiStrikeEnhanced(pokemon)) {
break; return false;
} }
if (!isNullOrUndefined(count)) {
return this.applyHitCountBoost(count);
} else if (!isNullOrUndefined(damageMultiplier)) {
return this.applyDamageModifier(pokemon, damageMultiplier);
}
return false;
}
/** Adds strikes to a move equal to the number of stacked Multi-Lenses */
private applyHitCountBoost(count: NumberHolder): boolean {
count.value += this.getStackCount();
return true;
}
/**
* If applied to the first hit of a move, sets the damage multiplier
* equal to (1 - the number of stacked Multi-Lenses).
* Additional strikes beyond that are given a 0.25x damage multiplier
*/
private applyDamageModifier(pokemon: Pokemon, damageMultiplier: NumberHolder): boolean {
damageMultiplier.value = (pokemon.turnData.hitsLeft === pokemon.turnData.hitCount)
? (1 - (0.25 * this.getStackCount()))
: 0.25;
return true; return true;
} }
getMaxHeldItemCount(pokemon: Pokemon): number { getMaxHeldItemCount(pokemon: Pokemon): number {
return 3; return 2;
} }
} }
@ -2783,9 +2816,9 @@ export class MoneyRewardModifier extends ConsumableModifier {
battleScene.addMoney(moneyAmount.value); battleScene.addMoney(moneyAmount.value);
battleScene.getParty().map(p => { battleScene.getPlayerParty().map(p => {
if (p.species?.speciesId === Species.GIMMIGHOUL || p.fusionSpecies?.speciesId === Species.GIMMIGHOUL) { if (p.species?.speciesId === Species.GIMMIGHOUL || p.fusionSpecies?.speciesId === Species.GIMMIGHOUL) {
p.evoCounter ? p.evoCounter++ : p.evoCounter = 1; p.evoCounter ? p.evoCounter += Math.min(Math.floor(this.moneyMultiplier), 3) : p.evoCounter = Math.min(Math.floor(this.moneyMultiplier), 3);
const modifier = getModifierType(modifierTypes.EVOLUTION_TRACKER_GIMMIGHOUL).newModifier(p) as EvoTrackerModifier; const modifier = getModifierType(modifierTypes.EVOLUTION_TRACKER_GIMMIGHOUL).newModifier(p) as EvoTrackerModifier;
battleScene.addModifier(modifier); battleScene.addModifier(modifier);
} }
@ -2950,6 +2983,38 @@ export class ShinyRateBoosterModifier extends PersistentModifier {
} }
} }
export class CriticalCatchChanceBoosterModifier extends PersistentModifier {
constructor(type: ModifierType, stackCount?: number) {
super(type, stackCount);
}
match(modifier: Modifier): boolean {
return modifier instanceof CriticalCatchChanceBoosterModifier;
}
clone(): CriticalCatchChanceBoosterModifier {
return new CriticalCatchChanceBoosterModifier(this.type, this.stackCount);
}
/**
* Applies {@linkcode CriticalCatchChanceBoosterModifier}
* @param boost {@linkcode NumberHolder} holding the boost value
* @returns always `true`
*/
override apply(boost: NumberHolder): boolean {
// 1 stack: 2x
// 2 stack: 2.5x
// 3 stack: 3x
boost.value *= 1.5 + this.getStackCount() / 2;
return true;
}
getMaxStackCount(scene: BattleScene): number {
return 3;
}
}
export class LockModifierTiersModifier extends PersistentModifier { export class LockModifierTiersModifier extends PersistentModifier {
constructor(type: ModifierType, stackCount?: number) { constructor(type: ModifierType, stackCount?: number) {
super(type, stackCount); super(type, stackCount);
@ -3566,7 +3631,7 @@ export class EnemyEndureChanceModifier extends EnemyPersistentModifier {
super(type, stackCount || 10); super(type, stackCount || 10);
//Hardcode temporarily //Hardcode temporarily
this.chance = .02; this.chance = 2;
} }
match(modifier: Modifier) { match(modifier: Modifier) {
@ -3574,24 +3639,24 @@ export class EnemyEndureChanceModifier extends EnemyPersistentModifier {
} }
clone() { clone() {
return new EnemyEndureChanceModifier(this.type, this.chance * 100, this.stackCount); return new EnemyEndureChanceModifier(this.type, this.chance, this.stackCount);
} }
getArgs(): any[] { getArgs(): any[] {
return [ this.chance * 100 ]; return [ this.chance ];
} }
/** /**
* Applies {@linkcode EnemyEndureChanceModifier} * Applies a chance of enduring a lethal hit of an attack
* @param target {@linkcode Pokemon} to apply the {@linkcode BattlerTagType.ENDURING} chance to * @param target the {@linkcode Pokemon} to apply the {@linkcode BattlerTagType.ENDURING} chance to
* @returns `true` if {@linkcode Pokemon} endured * @returns `true` if {@linkcode Pokemon} endured
*/ */
override apply(target: Pokemon): boolean { override apply(target: Pokemon): boolean {
if (target.battleData.endured || Phaser.Math.RND.realInRange(0, 1) >= (this.chance * this.getStackCount())) { if (target.battleData.endured || target.randSeedInt(100) >= (this.chance * this.getStackCount())) {
return false; return false;
} }
target.addTag(BattlerTagType.ENDURING, 1); target.addTag(BattlerTagType.ENDURE_TOKEN, 1);
target.battleData.endured = true; target.battleData.endured = true;

View File

@ -1,20 +1,20 @@
import { type PokeballCounts } from "#app/battle-scene";
import { Gender } from "#app/data/gender";
import { Variant } from "#app/data/variant";
import { type ModifierOverride } from "#app/modifier/modifier-type";
import { Unlockables } from "#app/system/unlockables";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Biome } from "#enums/biome"; import { Biome } from "#enums/biome";
import { EggTier } from "#enums/egg-type"; import { EggTier } from "#enums/egg-type";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import { TimeOfDay } from "#enums/time-of-day"; import { TimeOfDay } from "#enums/time-of-day";
import { VariantTier } from "#enums/variant-tier"; import { VariantTier } from "#enums/variant-tier";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import { type PokeballCounts } from "./battle-scene";
import { Gender } from "./data/gender";
import { Variant } from "./data/variant";
import { type ModifierOverride } from "./modifier/modifier-type";
import { Unlockables } from "./system/unlockables";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
/** /**
* Overrides that are using when testing different in game situations * Overrides that are using when testing different in game situations
@ -47,7 +47,18 @@ class DefaultOverrides {
/** a specific seed (default: a random string of 24 characters) */ /** a specific seed (default: a random string of 24 characters) */
readonly SEED_OVERRIDE: string = ""; readonly SEED_OVERRIDE: string = "";
readonly WEATHER_OVERRIDE: WeatherType = WeatherType.NONE; readonly WEATHER_OVERRIDE: WeatherType = WeatherType.NONE;
readonly BATTLE_TYPE_OVERRIDE: "double" | "single" | null = null; /**
* If `null`, ignore this override.
*
* If `"single"`, set every non-trainer battle to be a single battle.
*
* If `"double"`, set every battle (including trainer battles) to be a double battle.
*
* If `"even-doubles"`, follow the `"double"` rule on even wave numbers, and follow the `"single"` rule on odd wave numbers.
*
* If `"odd-doubles"`, follow the `"double"` rule on odd wave numbers, and follow the `"single"` rule on even wave numbers.
*/
readonly BATTLE_TYPE_OVERRIDE: BattleStyle | null = null;
readonly STARTING_WAVE_OVERRIDE: number = 0; readonly STARTING_WAVE_OVERRIDE: number = 0;
readonly STARTING_BIOME_OVERRIDE: Biome = Biome.TOWN; readonly STARTING_BIOME_OVERRIDE: Biome = Biome.TOWN;
readonly ARENA_TINT_OVERRIDE: TimeOfDay | null = null; readonly ARENA_TINT_OVERRIDE: TimeOfDay | null = null;
@ -229,3 +240,5 @@ export default {
...defaultOverrides, ...defaultOverrides,
...overrides ...overrides
} satisfies InstanceType<typeof DefaultOverrides>; } satisfies InstanceType<typeof DefaultOverrides>;
export type BattleStyle = "double" | "single" | "even-doubles" | "odd-doubles";

View File

@ -1,21 +1,22 @@
import BattleScene from "#app/battle-scene";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { getPokeballCatchMultiplier, getPokeballAtlasKey, getPokeballTintColor, doPokeballBounceAnim } from "#app/data/pokeball"; import BattleScene from "#app/battle-scene";
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
import { SubstituteTag } from "#app/data/battler-tags";
import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, getCriticalCaptureChance } from "#app/data/pokeball";
import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect"; import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect";
import { PokeballType } from "#app/enums/pokeball"; import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims";
import { StatusEffect } from "#app/enums/status-effect";
import { addPokeballOpenParticles, addPokeballCaptureStars } from "#app/field/anims";
import { EnemyPokemon } from "#app/field/pokemon"; import { EnemyPokemon } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { PokemonHeldItemModifier } from "#app/modifier/modifier";
import { PokemonPhase } from "#app/phases/pokemon-phase";
import { VictoryPhase } from "#app/phases/victory-phase";
import { achvs } from "#app/system/achv"; import { achvs } from "#app/system/achv";
import { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler"; import { PartyOption, PartyUiMode } from "#app/ui/party-ui-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 { PokeballType } from "#enums/pokeball";
import { StatusEffect } from "#enums/status-effect";
import i18next from "i18next"; import i18next from "i18next";
import { PokemonPhase } from "./pokemon-phase";
import { VictoryPhase } from "./victory-phase";
import { SubstituteTag } from "#app/data/battler-tags";
export class AttemptCapturePhase extends PokemonPhase { export class AttemptCapturePhase extends PokemonPhase {
private pokeballType: PokeballType; private pokeballType: PokeballType;
@ -51,8 +52,10 @@ export class AttemptCapturePhase extends PokemonPhase {
const catchRate = pokemon.species.catchRate; const catchRate = pokemon.species.catchRate;
const pokeballMultiplier = getPokeballCatchMultiplier(this.pokeballType); const pokeballMultiplier = getPokeballCatchMultiplier(this.pokeballType);
const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1; const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1;
const x = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier); const modifiedCatchRate = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier);
const y = Math.round(65536 / Math.sqrt(Math.sqrt(255 / x))); const shakeProbability = Math.round(65536 / Math.pow((255 / modifiedCatchRate), 0.1875)); // Formula taken from gen 6
const criticalCaptureChance = getCriticalCaptureChance(this.scene, modifiedCatchRate);
const isCritical = pokemon.randSeedInt(256) < criticalCaptureChance;
const fpOffset = pokemon.getFieldPositionOffset(); const fpOffset = pokemon.getFieldPositionOffset();
const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType); const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType);
@ -60,17 +63,19 @@ export class AttemptCapturePhase extends PokemonPhase {
this.pokeball.setOrigin(0.5, 0.625); this.pokeball.setOrigin(0.5, 0.625);
this.scene.field.add(this.pokeball); this.scene.field.add(this.pokeball);
this.scene.playSound("se/pb_throw"); this.scene.playSound("se/pb_throw", isCritical ? { rate: 0.2 } : undefined); // Crit catch throws are higher pitched
this.scene.time.delayedCall(300, () => { this.scene.time.delayedCall(300, () => {
this.scene.field.moveBelow(this.pokeball as Phaser.GameObjects.GameObject, pokemon); this.scene.field.moveBelow(this.pokeball as Phaser.GameObjects.GameObject, pokemon);
}); });
this.scene.tweens.add({ this.scene.tweens.add({
// Throw animation
targets: this.pokeball, targets: this.pokeball,
x: { value: 236 + fpOffset[0], ease: "Linear" }, x: { value: 236 + fpOffset[0], ease: "Linear" },
y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" }, y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" },
duration: 500, duration: 500,
onComplete: () => { onComplete: () => {
// Ball opens
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`)); this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`));
this.scene.playSound("se/pb_rel"); this.scene.playSound("se/pb_rel");
@ -79,30 +84,33 @@ export class AttemptCapturePhase extends PokemonPhase {
addPokeballOpenParticles(this.scene, this.pokeball.x, this.pokeball.y, this.pokeballType); addPokeballOpenParticles(this.scene, this.pokeball.x, this.pokeball.y, this.pokeballType);
this.scene.tweens.add({ this.scene.tweens.add({
// Mon enters ball
targets: pokemon, targets: pokemon,
duration: 500, duration: 500,
ease: "Sine.easeIn", ease: "Sine.easeIn",
scale: 0.25, scale: 0.25,
y: 20, y: 20,
onComplete: () => { onComplete: () => {
// Ball closes
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
pokemon.setVisible(false); pokemon.setVisible(false);
this.scene.playSound("se/pb_catch"); this.scene.playSound("se/pb_catch");
this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}`)); this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}`));
const doShake = () => { const doShake = () => {
// After the overall catch rate check, the game does 3 shake checks before confirming the catch.
let shakeCount = 0; let shakeCount = 0;
const pbX = this.pokeball.x; const pbX = this.pokeball.x;
const shakeCounter = this.scene.tweens.addCounter({ const shakeCounter = this.scene.tweens.addCounter({
from: 0, from: 0,
to: 1, to: 1,
repeat: 4, repeat: isCritical ? 2 : 4, // Critical captures only perform 1 shake check
yoyo: true, yoyo: true,
ease: "Cubic.easeOut", ease: "Cubic.easeOut",
duration: 250, duration: 250,
repeatDelay: 500, repeatDelay: 500,
onUpdate: t => { onUpdate: t => {
if (shakeCount && shakeCount < 4) { if (shakeCount && shakeCount < (isCritical ? 2 : 4)) {
const value = t.getValue(); const value = t.getValue();
const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1; const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1;
this.pokeball.setX(pbX + value * 4 * directionMultiplier); this.pokeball.setX(pbX + value * 4 * directionMultiplier);
@ -113,13 +121,18 @@ export class AttemptCapturePhase extends PokemonPhase {
if (!pokemon.species.isObtainable()) { if (!pokemon.species.isObtainable()) {
shakeCounter.stop(); shakeCounter.stop();
this.failCatch(shakeCount); this.failCatch(shakeCount);
} else if (shakeCount++ < 3) { } else if (shakeCount++ < (isCritical ? 1 : 3)) {
if (pokeballMultiplier === -1 || pokemon.randSeedInt(65536) < y) { // Shake check (skip check for critical or guaranteed captures, but still play the sound)
if (pokeballMultiplier === -1 || isCritical || modifiedCatchRate >= 255 || pokemon.randSeedInt(65536) < shakeProbability) {
this.scene.playSound("se/pb_move"); this.scene.playSound("se/pb_move");
} else { } else {
shakeCounter.stop(); shakeCounter.stop();
this.failCatch(shakeCount); this.failCatch(shakeCount);
} }
} else if (isCritical && pokemon.randSeedInt(65536) >= shakeProbability) {
// Above, perform the one shake check for critical captures after the ball shakes once
shakeCounter.stop();
this.failCatch(shakeCount);
} else { } else {
this.scene.playSound("se/pb_lock"); this.scene.playSound("se/pb_lock");
addPokeballCaptureStars(this.scene, this.pokeball); addPokeballCaptureStars(this.scene, this.pokeball);
@ -152,7 +165,8 @@ export class AttemptCapturePhase extends PokemonPhase {
}); });
}; };
this.scene.time.delayedCall(250, () => doPokeballBounceAnim(this.scene, this.pokeball, 16, 72, 350, doShake)); // Ball bounces (handled in pokemon.ts)
this.scene.time.delayedCall(250, () => doPokeballBounceAnim(this.scene, this.pokeball, 16, 72, 350, doShake, isCritical));
} }
}); });
} }
@ -235,7 +249,7 @@ export class AttemptCapturePhase extends PokemonPhase {
const addToParty = (slotIndex?: number) => { const addToParty = (slotIndex?: number) => {
const newPokemon = pokemon.addToParty(this.pokeballType, slotIndex); const newPokemon = pokemon.addToParty(this.pokeballType, slotIndex);
const modifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false); const modifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false);
if (this.scene.getParty().filter(p => p.isShiny()).length === 6) { if (this.scene.getPlayerParty().filter(p => p.isShiny()).length === PLAYER_PARTY_MAX_SIZE) {
this.scene.validateAchv(achvs.SHINY_PARTY); this.scene.validateAchv(achvs.SHINY_PARTY);
} }
Promise.all(modifiers.map(m => this.scene.addModifier(m, true))).then(() => { Promise.all(modifiers.map(m => this.scene.addModifier(m, true))).then(() => {
@ -249,7 +263,7 @@ export class AttemptCapturePhase extends PokemonPhase {
}); });
}; };
Promise.all([ pokemon.hideInfo(), this.scene.gameData.setPokemonCaught(pokemon) ]).then(() => { Promise.all([ pokemon.hideInfo(), this.scene.gameData.setPokemonCaught(pokemon) ]).then(() => {
if (this.scene.getParty().length === 6) { if (this.scene.getPlayerParty().length === PLAYER_PARTY_MAX_SIZE) {
const promptRelease = () => { const promptRelease = () => {
this.scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => { this.scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => {
this.scene.pokemonInfoContainer.makeRoomForConfirmUi(1, true); this.scene.pokemonInfoContainer.makeRoomForConfirmUi(1, true);

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