Merge remote-tracking branch 'upstream/beta' into trick-room-challenge

This commit is contained in:
KimJeongSun 2024-11-08 23:48:37 +09:00
commit 3dd664770a
213 changed files with 7225 additions and 2300 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`
*/
var i18nServer: SetupServerApi;
var server: SetupServerApi;
}

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

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,39 @@
export class UpdateSessionSavedataRequest {
slot: number;
trainerId: number;
secretId: number;
clientSessionId: string;
}
/** This is **NOT** similar to {@linkcode ClearSessionSavedataRequest} */
export interface NewClearSessionSavedataRequest {
slot: number;
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 * as Utils from "./utils";
export interface UserInfo {
username: string;
lastSessionSlot: integer;
discordId: string;
googleId: string;
hasAdminRole: boolean;
}
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
export const clientSessionId = Utils.randomString(32);
@ -43,18 +37,14 @@ export function updateUserInfo(): Promise<[boolean, integer]> {
});
return resolve([ true, 200 ]);
}
Utils.apiFetch("account/info", true).then(response => {
if (!response.ok) {
resolve([ false, response.status ]);
pokerogueApi.account.getInfo().then(([ accountInfo, status ]) => {
if (!accountInfo) {
resolve([ false, status ]);
return;
}
return response.json();
}).then(jsonResponse => {
loggedInUser = jsonResponse;
} else {
loggedInUser = accountInfo;
resolve([ true, 200 ]);
}).catch(err => {
console.error(err);
resolve([ false, 500 ]);
}
});
});
}

View File

@ -13,9 +13,10 @@ import { Arena, ArenaBase } from "#app/field/arena";
import { GameData } from "#app/system/game-data";
import { addTextObject, getTextColor, TextStyle } from "#app/ui/text";
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 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 { GameMode, GameModes, getGameMode } from "#app/game-mode";
import FieldSpritePipeline from "#app/pipelines/field-sprite";
@ -37,7 +38,7 @@ import PokemonData from "#app/system/pokemon-data";
import { Nature } from "#app/data/nature";
import { FormChangeItem, pokemonFormChanges, SpeciesFormChange, SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger } from "#app/data/pokemon-forms";
import { FormChangePhase } from "#app/phases/form-change-phase";
import { getTypeRgb } from "#app/data/type";
import { getTypeRgb, Type } from "#app/data/type";
import PokemonSpriteSparkleHandler from "#app/field/pokemon-sprite-sparkle-handler";
import CharSprite from "#app/ui/char-sprite";
import DamageNumberHandler from "#app/field/damage-number-handler";
@ -95,7 +96,9 @@ import { ExpPhase } from "#app/phases/exp-phase";
import { ShowPartyExpBarPhase } from "#app/phases/show-party-exp-bar-phase";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
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 { StatusEffect } from "#enums/status-effect";
export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1";
@ -169,7 +172,7 @@ export default class BattleScene extends SceneBase {
public uiTheme: UiTheme = UiTheme.DEFAULT;
public windowType: integer = 0;
public experimentalSprites: boolean = false;
public musicPreference: integer = 0;
public musicPreference: number = MusicPreference.MIXED;
public moveAnimations: boolean = true;
public expGainsSpeed: ExpGainsSpeed = ExpGainsSpeed.DEFAULT;
public skipSeenDialogues: boolean = false;
@ -764,57 +767,65 @@ export default class BattleScene extends SceneBase {
return true;
}
getParty(): PlayerPokemon[] {
public getPlayerParty(): PlayerPokemon[] {
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 Either the first {@linkcode PlayerPokemon} satisfying, or undefined if no player pokemon on the field satisfy
* @returns An array of {@linkcode PlayerPokemon} filtered from the player's party
* that are {@linkcode PlayerPokemon.isAllowedInBattle | allowed in battle}.
*/
getNonSwitchedPlayerPokemon(): PlayerPokemon | undefined {
return this.getPlayerField().find(p => p.isActive() && p.switchOutStatus === false);
public getPokemonAllowedInBattle(): PlayerPokemon[] {
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}
*/
getPlayerField(): PlayerPokemon[] {
const party = this.getParty();
public getPlayerField(): PlayerPokemon[] {
const party = this.getPlayerParty();
return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1));
}
getEnemyParty(): EnemyPokemon[] {
public getEnemyParty(): EnemyPokemon[] {
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 Either the first {@linkcode EnemyPokemon} satisfying, or undefined if no player pokemon on the field satisfy
* @returns The first {@linkcode EnemyPokemon} that is {@linkcode getEnemyField on the field}
* 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 {
return this.getEnemyField().find(p => p.isActive() && p.switchOutStatus === false);
public getEnemyPokemon(includeSwitching: boolean = true): EnemyPokemon | undefined {
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}
*/
getEnemyField(): EnemyPokemon[] {
public getEnemyField(): EnemyPokemon[] {
const party = this.getEnemyParty();
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 playerField = this.getPlayerField();
const enemyField = this.getEnemyField();
@ -867,7 +878,7 @@ export default class BattleScene extends SceneBase {
getPokemonById(pokemonId: integer): Pokemon | null {
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 {
@ -1062,7 +1073,7 @@ export default class BattleScene extends SceneBase {
this.modifierBar.removeAll(true);
this.enemyModifierBar.removeAll(true);
for (const p of this.getParty()) {
for (const p of this.getPlayerParty()) {
p.destroy();
}
this.party = [];
@ -1269,13 +1280,15 @@ export default class BattleScene extends SceneBase {
if (resetArenaState) {
this.arena.resetArenaEffects();
playerField.forEach((pokemon) => pokemon.lapseTag(BattlerTagType.COMMANDED));
playerField.forEach((pokemon, p) => {
if (pokemon.isOnField()) {
this.pushPhase(new ReturnPhase(this, p));
}
});
for (const pokemon of this.getParty()) {
for (const pokemon of this.getPlayerParty()) {
pokemon.resetBattleData();
applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon);
}
@ -1285,7 +1298,7 @@ export default class BattleScene extends SceneBase {
}
}
for (const pokemon of this.getParty()) {
for (const pokemon of this.getPlayerParty()) {
this.triggerPokemonFormChange(pokemon, SpeciesFormChangeTimeOfDayTrigger);
}
@ -1480,7 +1493,7 @@ export default class BattleScene extends SceneBase {
}
trySpreadPokerus(): void {
const party = this.getParty();
const party = this.getPlayerParty();
const infectedIndexes: integer[] = [];
const spread = (index: number, spreadTo: number) => {
const partyMember = party[index + spreadTo];
@ -1677,7 +1690,7 @@ export default class BattleScene extends SceneBase {
updateAndShowText(duration: number): void {
const labels = [ this.luckLabelText, this.luckText ];
labels.forEach(t => t.setAlpha(0));
const luckValue = getPartyLuckValue(this.getParty());
const luckValue = getPartyLuckValue(this.getPlayerParty());
this.luckText.setText(getLuckString(luckValue));
if (luckValue < 14) {
this.luckText.setTint(getLuckTextTint(luckValue));
@ -2593,9 +2606,19 @@ export default class BattleScene extends SceneBase {
const addModifier = () => {
if (!matchingModifier || this.removeModifier(matchingModifier, !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) {
applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false);
}
resolve(true);
});
} else {
this.addEnemyModifier(newItemModifier, ignoreUpdate, instant).then(() => resolve(true));
this.addEnemyModifier(newItemModifier, ignoreUpdate, instant).then(() => {
if (source) {
applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false);
}
resolve(true);
});
}
} else {
resolve(false);
@ -2615,7 +2638,7 @@ export default class BattleScene extends SceneBase {
removePartyMemberModifiers(partyMemberIndex: integer): Promise<void> {
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);
for (const m of modifiersToRemove) {
this.modifiers.splice(this.modifiers.indexOf(m), 1);
@ -2742,7 +2765,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);
if (!player) {
this.updateUIPositions();
@ -2960,12 +2983,21 @@ export default class BattleScene extends SceneBase {
updateGameInfo(): void {
const gameInfo = {
playTime: this.sessionPlayTime ? this.sessionPlayTime : 0,
playTime: this.sessionPlayTime ?? 0,
gameMode: this.currentBattle ? this.gameMode.getName() : "Title",
biome: this.currentBattle ? getBiomeName(this.arena.biomeType) : "",
wave: this.currentBattle?.waveIndex || 0,
party: this.party ? this.party.map(p => {
return { name: p.name, level: p.level };
wave: this.currentBattle?.waveIndex ?? 0,
party: this.party ? this.party.map((p) => {
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() ?? [],
};
@ -2980,22 +3012,16 @@ export default class BattleScene extends SceneBase {
*/
getActiveKeys(): string[] {
const keys: string[] = [];
const playerParty = this.getParty();
playerParty.forEach(p => {
let activePokemon: (PlayerPokemon | EnemyPokemon)[] = this.getPlayerParty();
activePokemon = activePokemon.concat(this.getEnemyParty());
activePokemon.forEach((p) => {
keys.push(p.getSpriteKey(true));
if (p instanceof PlayerPokemon) {
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));
}
});
// 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));
keys.push(p.species.getCryKey(p.formIndex));
if (p.fusionSpecies) {
keys.push("cry/" + p.fusionSpecies.getCryKey(p.fusionFormIndex));
keys.push(p.fusionSpecies.getCryKey(p.fusionFormIndex));
}
});
return keys;
@ -3016,7 +3042,7 @@ export default class BattleScene extends SceneBase {
this.setFieldScale(0.75);
this.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger, false);
this.currentBattle.double = true;
const availablePartyMembers = this.getParty().filter((p) => p.isAllowedInBattle());
const availablePartyMembers = this.getPlayerParty().filter((p) => p.isAllowedInBattle());
if (availablePartyMembers.length > 1) {
this.pushPhase(new ToggleDoublePositionPhase(this, true));
if (!availablePartyMembers[1].isOnField()) {
@ -3041,7 +3067,7 @@ export default class BattleScene extends SceneBase {
*/
applyPartyExp(expValue: number, pokemonDefeated: boolean, useWaveIndexMultiplier?: boolean, pokemonParticipantIds?: Set<number>): void {
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 expBalanceModifier = this.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier;
const multipleParticipantExpBonusModifier = this.findModifier(m => m instanceof MultipleParticipantExpBonusModifier) as MultipleParticipantExpBonusModifier;

View File

@ -6,11 +6,13 @@ import { GameMode } from "./game-mode";
import { MoneyMultiplierModifier, PokemonHeldItemModifier } from "./modifier/modifier";
import { PokeballType } from "./data/pokeball";
import { trainerConfigs } from "#app/data/trainer-config";
import { SpeciesFormKey } from "#enums/species-form-key";
import Pokemon, { EnemyPokemon, PlayerPokemon, QueuedMove } from "#app/field/pokemon";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattleSpec } from "#enums/battle-spec";
import { Moves } from "#enums/moves";
import { PlayerGender } from "#enums/player-gender";
import { MusicPreference } from "#app/system/settings/settings";
import { Species } from "#enums/species";
import { TrainerType } from "#enums/trainer-type";
import i18next from "#app/plugins/i18n";
@ -212,7 +214,6 @@ export default class Battle {
}
getBgmOverride(scene: BattleScene): string | null {
const battlers = this.enemyParty.slice(0, this.getBattlerCount());
if (this.isBattleMysteryEncounter() && this.mysteryEncounter?.encounterMode === MysteryEncounterMode.DEFAULT) {
// Music is overridden for MEs during ME onInit()
// 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) {
return `encounter_${this.trainer?.getEncounterBgm()}`;
}
if (scene.musicPreference === 0) {
if (scene.musicPreference === MusicPreference.CONSISTENT) {
return this.trainer?.getBattleBgm() ?? null;
} else {
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) {
return "end_summit";
}
for (const pokemon of battlers) {
const wildOpponents = scene.getEnemyParty();
for (const pokemon of wildOpponents) {
if (this.battleSpec === BattleSpec.FINAL_BOSS) {
if (pokemon.formIndex) {
if (pokemon.species.getFormSpriteKey(pokemon.formIndex) === SpeciesFormKey.ETERNAMAX) {
return "battle_final";
}
return "battle_final_encounter";
}
if (pokemon.species.legendary || pokemon.species.subLegendary || pokemon.species.mythical) {
if (scene.musicPreference === 0) {
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) {
if (scene.musicPreference === MusicPreference.CONSISTENT) {
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";
}
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) {
case Species.KYUREM:
return "battle_legendary_kyurem";
}
default:
if (pokemon.species.legendary) {
return "battle_legendary_res_zek";
}
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";
}
if (pokemon.species.speciesId === Species.RAIKOU) {
case Species.RAIKOU:
return "battle_legendary_raikou";
}
if (pokemon.species.speciesId === Species.ENTEI) {
case Species.ENTEI:
return "battle_legendary_entei";
}
if (pokemon.species.speciesId === Species.SUICUNE) {
case Species.SUICUNE:
return "battle_legendary_suicune";
}
if (pokemon.species.speciesId === Species.LUGIA) {
case Species.LUGIA:
return "battle_legendary_lugia";
}
if (pokemon.species.speciesId === Species.HO_OH) {
case Species.HO_OH:
return "battle_legendary_ho_oh";
}
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.REGIROCK:
case Species.REGICE:
case Species.REGISTEEL:
case Species.REGIGIGAS:
case Species.REGIDRAGO:
case Species.REGIELEKI:
return "battle_legendary_regis_g6";
}
if (pokemon.species.speciesId === Species.GROUDON || pokemon.species.speciesId === Species.KYOGRE) {
case Species.GROUDON:
case Species.KYOGRE:
return "battle_legendary_gro_kyo";
}
if (pokemon.species.speciesId === Species.RAYQUAZA) {
case Species.RAYQUAZA:
return "battle_legendary_rayquaza";
}
if (pokemon.species.speciesId === Species.DEOXYS) {
case Species.DEOXYS:
return "battle_legendary_deoxys";
}
if (pokemon.species.speciesId === Species.UXIE || pokemon.species.speciesId === Species.MESPRIT || pokemon.species.speciesId === Species.AZELF) {
case Species.UXIE:
case Species.MESPRIT:
case Species.AZELF:
return "battle_legendary_lake_trio";
}
if (pokemon.species.speciesId === Species.HEATRAN || pokemon.species.speciesId === Species.CRESSELIA || pokemon.species.speciesId === Species.DARKRAI || pokemon.species.speciesId === Species.SHAYMIN) {
case Species.HEATRAN:
case Species.CRESSELIA:
case Species.DARKRAI:
case Species.SHAYMIN:
return "battle_legendary_sinnoh";
}
if (pokemon.species.speciesId === Species.DIALGA || pokemon.species.speciesId === Species.PALKIA) {
if (pokemon.getFormKey() === "") {
return "battle_legendary_dia_pal";
}
if (pokemon.getFormKey() === "origin") {
case Species.DIALGA:
case Species.PALKIA:
if (pokemon.species.getFormSpriteKey(pokemon.formIndex) === SpeciesFormKey.ORIGIN) {
return "battle_legendary_origin_forme";
}
}
if (pokemon.species.speciesId === Species.GIRATINA) {
return "battle_legendary_dia_pal";
case Species.GIRATINA:
return "battle_legendary_giratina";
}
if (pokemon.species.speciesId === Species.ARCEUS) {
case Species.ARCEUS:
return "battle_legendary_arceus";
}
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.COBALION:
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";
}
if (pokemon.species.speciesId === Species.KYUREM) {
case Species.KYUREM:
return "battle_legendary_kyurem";
}
if (pokemon.species.speciesId === Species.XERNEAS || pokemon.species.speciesId === Species.YVELTAL || pokemon.species.speciesId === Species.ZYGARDE) {
case Species.XERNEAS:
case Species.YVELTAL:
case Species.ZYGARDE:
return "battle_legendary_xern_yvel";
}
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_KOKO:
case Species.TAPU_LELE:
case Species.TAPU_BULU:
case Species.TAPU_FINI:
return "battle_legendary_tapu";
}
if ([ Species.COSMOG, Species.COSMOEM, Species.SOLGALEO, Species.LUNALA ].includes(pokemon.species.speciesId)) {
case Species.SOLGALEO:
case Species.LUNALA:
return "battle_legendary_sol_lun";
}
if (pokemon.species.speciesId === Species.NECROZMA) {
if (pokemon.getFormKey() === "") {
return "battle_legendary_sol_lun";
}
if (pokemon.getFormKey() === "dusk-mane" || pokemon.getFormKey() === "dawn-wings") {
case Species.NECROZMA:
switch (pokemon.getFormKey()) {
case "dusk-mane":
case "dawn-wings":
return "battle_legendary_dusk_dawn";
}
if (pokemon.getFormKey() === "ultra") {
case "ultra":
return "battle_legendary_ultra_nec";
default:
return "battle_legendary_sol_lun";
}
}
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.NIHILEGO:
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";
}
if (pokemon.species.speciesId === Species.ZACIAN || pokemon.species.speciesId === Species.ZAMAZENTA) {
case Species.ZACIAN:
case Species.ZAMAZENTA:
return "battle_legendary_zac_zam";
}
if (pokemon.species.speciesId === Species.GLASTRIER || pokemon.species.speciesId === Species.SPECTRIER) {
case Species.GLASTRIER:
case Species.SPECTRIER:
return "battle_legendary_glas_spec";
}
if (pokemon.species.speciesId === Species.CALYREX) {
if (pokemon.getFormKey() === "") {
return "battle_legendary_calyrex";
}
case Species.CALYREX:
if (pokemon.getFormKey() === "ice" || pokemon.getFormKey() === "shadow") {
return "battle_legendary_riders";
}
}
if (pokemon.species.speciesId === Species.GALAR_ARTICUNO || pokemon.species.speciesId === Species.GALAR_ZAPDOS || pokemon.species.speciesId === Species.GALAR_MOLTRES) {
return "battle_legendary_calyrex";
case Species.GALAR_ARTICUNO:
case Species.GALAR_ZAPDOS:
case Species.GALAR_MOLTRES:
return "battle_legendary_birds_galar";
}
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.WO_CHIEN:
case Species.CHIEN_PAO:
case Species.TING_LU:
case Species.CHI_YU:
return "battle_legendary_ruinous";
}
if (pokemon.species.speciesId === Species.KORAIDON || pokemon.species.speciesId === Species.MIRAIDON) {
case Species.KORAIDON:
case Species.MIRAIDON:
return "battle_legendary_kor_mir";
}
if (pokemon.species.speciesId === Species.OKIDOGI || pokemon.species.speciesId === Species.MUNKIDORI || pokemon.species.speciesId === Species.FEZANDIPITI) {
case Species.OKIDOGI:
case Species.MUNKIDORI:
case Species.FEZANDIPITI:
return "battle_legendary_loyal_three";
}
if (pokemon.species.speciesId === Species.OGERPON) {
case Species.OGERPON:
return "battle_legendary_ogerpon";
}
if (pokemon.species.speciesId === Species.TERAPAGOS) {
case Species.TERAPAGOS:
return "battle_legendary_terapagos";
}
if (pokemon.species.speciesId === Species.PECHARUNT) {
case Species.PECHARUNT:
return "battle_legendary_pecharunt";
}
default:
if (pokemon.species.legendary) {
return "battle_legendary_res_zek";
}
@ -373,6 +394,7 @@ export default class Battle {
}
}
}
}
if (scene.gameMode.isClassic && this.waveIndex <= 4) {
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 */
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 { Constructor } from "#app/utils";
import * as Utils from "../utils";
import { getPokemonNameWithAffix } from "../messages";
import { Weather, WeatherType } from "./weather";
import { BattlerTag, GroundedTag } from "./battler-tags";
import { BattlerTag, BattlerTagLapseType, GroundedTag } from "./battler-tags";
import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
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 { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier";
import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "../modifier/modifier";
import { TerrainType } from "./terrain";
import { SpeciesFormChangeManualTrigger, SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "./pokemon-forms";
import i18next from "i18next";
@ -17,7 +17,7 @@ import { Localizable } from "#app/interfaces/locales";
import { Command } from "../ui/command-ui-handler";
import { BerryModifierType } from "#app/modifier/modifier-type";
import { getPokeballName } from "./pokeball";
import { BattlerIndex } from "#app/battle";
import { BattlerIndex, BattleType } from "#app/battle";
import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type";
@ -29,6 +29,13 @@ import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
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";
export class Ability implements Localizable {
public id: Abilities;
@ -505,7 +512,11 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr {
}
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);
if (move instanceof AttackMove && modifierValue < 2) {
cancelled.value = true; // Suppresses "No Effect" message
(args[0] as Utils.NumberHolder).value = 0;
return true;
@ -1950,6 +1961,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 {
private stats: readonly BattleStat[];
@ -1959,6 +1974,15 @@ export class IgnoreOpponentStatStagesAbAttr extends AbAttr {
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[]) {
if (this.stats.includes(args[0])) {
(args[1] as Utils.BooleanHolder).value = true;
@ -2572,6 +2596,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 {
constructor() {
super(true);
@ -3092,7 +3152,7 @@ export class SuppressWeatherEffectAbAttr extends PreWeatherEffectAbAttr {
/**
* 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:
* 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.
*/
function getSheerForceHitDisableAbCondition(): AbAttrCondition {
@ -3838,6 +3898,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 {
private multiplier: integer;
@ -4833,6 +4928,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> {
return applyAbAttrsInternal<AbAttr>(attrType, pokemon, (attr, passive) => attr.apply(pokemon, passive, simulated, cancelled, args), args, false, simulated);
}
@ -4866,6 +5194,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);
}
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
* @param attrType {@linkcode FieldMultiplyStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being
@ -4971,6 +5304,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);
}
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 {
pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.id, passive));
pokemon.scene.clearPhaseQueueSplice();
@ -5062,7 +5400,8 @@ export function initAbilities() {
.attr(TypeImmunityAddBattlerTagAbAttr, Type.FIRE, BattlerTagType.FIRE_BOOST, 1)
.ignorable(),
new Ability(Abilities.SHIELD_DUST, 3)
.attr(IgnoreMoveEffectsAbAttr),
.attr(IgnoreMoveEffectsAbAttr)
.ignorable(),
new Ability(Abilities.OWN_TEMPO, 3)
.attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED)
.attr(IntimidateImmunityAbAttr)
@ -5094,8 +5433,7 @@ export function initAbilities() {
.attr(EffectSporeAbAttr),
new Ability(Abilities.SYNCHRONIZE, 3)
.attr(SyncEncounterNatureAbAttr)
.attr(SynchronizeStatusAbAttr)
.partial(), // interaction with psycho shift needs work, keeping to old Gen interaction for now
.attr(SynchronizeStatusAbAttr),
new Ability(Abilities.CLEAR_BODY, 3)
.attr(ProtectStatAbAttr)
.ignorable(),
@ -5116,6 +5454,7 @@ export function initAbilities() {
new Ability(Abilities.ILLUMINATE, 3)
.attr(ProtectStatAbAttr, Stat.ACC)
.attr(DoubleBattleChanceAbAttr)
.attr(IgnoreOpponentStatStagesAbAttr, [ Stat.EVA ])
.ignorable(),
new Ability(Abilities.TRACE, 3)
.attr(PostSummonCopyAbilityAbAttr)
@ -5180,11 +5519,9 @@ export function initAbilities() {
new Ability(Abilities.CUTE_CHARM, 3)
.attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED),
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)
.ignorable(),
.conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
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)
.ignorable(),
.conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
new Ability(Abilities.FORECAST, 3)
.attr(UncopiableAbilityAbAttr)
.attr(NoFusionAbilityAbAttr)
@ -5264,7 +5601,7 @@ export function initAbilities() {
new Ability(Abilities.ANGER_POINT, 4)
.attr(PostDefendCritStatStageChangeAbAttr, Stat.ATK, 6),
new Ability(Abilities.UNBURDEN, 4)
.unimplemented(),
.attr(PostItemLostApplyBattlerTagAbAttr, BattlerTagType.UNBURDEN),
new Ability(Abilities.HEATPROOF, 4)
.attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 0.5)
.attr(ReduceBurnDamageAbAttr, 0.5)
@ -5337,7 +5674,7 @@ export function initAbilities() {
new Ability(Abilities.FOREWARN, 4)
.attr(ForewarnAbAttr),
new Ability(Abilities.UNAWARE, 4)
.attr(IgnoreOpponentStatStagesAbAttr)
.attr(IgnoreOpponentStatStagesAbAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.ACC, Stat.EVA ])
.ignorable(),
new Ability(Abilities.TINTED_LENS, 4)
.attr(DamageBoostAbAttr, 2, (user, target, move) => (target?.getMoveEffectiveness(user!, move) ?? 1) <= 0.5),
@ -5522,7 +5859,8 @@ export function initAbilities() {
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
.attr(MoveAbilityBypassAbAttr),
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)
.ignorable()
.unimplemented(),
@ -5607,11 +5945,11 @@ export function initAbilities() {
new Ability(Abilities.STAMINA, 7)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1),
new Ability(Abilities.WIMP_OUT, 7)
.condition(getSheerForceHitDisableAbCondition())
.unimplemented(),
.attr(PostDamageForceSwitchAbAttr)
.edgeCase(), // Should not trigger when hurting itself in confusion
new Ability(Abilities.EMERGENCY_EXIT, 7)
.condition(getSheerForceHitDisableAbCondition())
.unimplemented(),
.attr(PostDamageForceSwitchAbAttr)
.edgeCase(), // Should not trigger when hurting itself in confusion
new Ability(Abilities.WATER_COMPACTION, 7)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2),
new Ability(Abilities.MERCILESS, 7)
@ -5697,7 +6035,7 @@ export function initAbilities() {
.bypassFaint(),
new Ability(Abilities.CORROSION, 7)
.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)
.attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr)
@ -5801,7 +6139,8 @@ export function initAbilities() {
.attr(NoFusionAbilityAbAttr)
.attr(UncopiableAbilityAbAttr)
.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)
.attr(BlockRedirectAbAttr),
new Ability(Abilities.STEAM_ENGINE, 8)
@ -5944,9 +6283,11 @@ export function initAbilities() {
.attr(PreSwitchOutFormChangeAbAttr, (pokemon) => !pokemon.isFainted() ? 1 : pokemon.formIndex)
.bypassFaint(),
new Ability(Abilities.COMMANDER, 9)
.attr(CommanderAbAttr)
.attr(DoubleBattleChanceAbAttr)
.attr(UncopiableAbilityAbAttr)
.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)
.attr(PostDefendApplyBattlerTagAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, BattlerTagType.CHARGED),
new Ability(Abilities.PROTOSYNTHESIS, 9)
@ -5973,16 +6314,14 @@ export function initAbilities() {
.ignorable(),
new Ability(Abilities.SWORD_OF_RUIN, 9)
.attr(FieldMultiplyStatAbAttr, Stat.DEF, 0.75)
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) }))
.ignorable(),
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) })),
new Ability(Abilities.TABLETS_OF_RUIN, 9)
.attr(FieldMultiplyStatAbAttr, Stat.ATK, 0.75)
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonTabletsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) }))
.ignorable(),
new Ability(Abilities.BEADS_OF_RUIN, 9)
.attr(FieldMultiplyStatAbAttr, Stat.SPDEF, 0.75)
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) }))
.ignorable(),
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) })),
new Ability(Abilities.ORICHALCUM_PULSE, 9)
.attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY)
.attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SUNNY)

View File

@ -313,8 +313,8 @@ export class ConditionalProtectTag extends ArenaTag {
* protection effect.
* @param arena {@linkcode Arena} The arena containing the protection effect
* @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
* moves with modified priorities from abilities (e.g. Prankster)
* @returns `true` if the incoming move's priority is greater than 0.
* This includes moves with modified priorities from abilities (e.g. Prankster)
*/
const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => {
const move = allMoves[moveId];
@ -322,10 +322,12 @@ const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => {
const effectPhase = arena.scene.getCurrentPhase();
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 priority.value > 0;
};
@ -778,13 +780,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),
* and deals damage after the turn count is reached.
*/
class DelayedAttackTag extends ArenaTag {
export class DelayedAttackTag extends ArenaTag {
public targetIndex: BattlerIndex;
constructor(tagType: ArenaTagType, sourceMove: Moves | undefined, sourceId: number, targetIndex: BattlerIndex) {
super(tagType, 3, sourceMove, sourceId);
constructor(tagType: ArenaTagType, sourceMove: Moves | undefined, sourceId: number, targetIndex: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH) {
super(tagType, 3, sourceMove, sourceId, side);
this.targetIndex = targetIndex;
this.side = side;
}
lapse(arena: Arena): boolean {
@ -1203,6 +1206,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
export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove: Moves | undefined, sourceId: number, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null {
switch (tagType) {
@ -1230,7 +1251,7 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove
return new ToxicSpikesTag(sourceId, side);
case ArenaTagType.FUTURE_SIGHT:
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:
return new WishTag(turnCount, sourceId, side);
case ArenaTagType.STEALTH_ROCK:
@ -1261,6 +1282,8 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove
return new WaterFirePledgeTag(sourceId, side);
case ArenaTagType.GRASS_WATER_PLEDGE:
return new GrassWaterPledgeTag(sourceId, side);
case ArenaTagType.FAIRY_LOCK:
return new FairyLockTag(turnCount, sourceId);
default:
return null;
}

View File

@ -7666,7 +7666,7 @@ export function initBiomes() {
if (biome === Biome.END) {
const biomeList = Object.keys(Biome).filter(key => !isNaN(Number(key)));
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]];
}
const linkedBiomes: (Biome | [ Biome, integer ])[] = Array.isArray(biomeLinks[biome])

View File

@ -478,7 +478,7 @@ export const pokemonEvolutions: PokemonEvolutions = {
],
[Species.NINCADA]: [
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]: [
new SpeciesEvolution(Species.LOUDRED, 20, null, null)
@ -890,7 +890,7 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesEvolution(Species.GOGOAT, 32, null, null)
],
[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]: [
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)
],
[Species.COSMOEM]: [
new SpeciesEvolution(Species.SOLGALEO, 53, EvolutionItem.SUN_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG),
new SpeciesEvolution(Species.LUNALA, 53, EvolutionItem.MOON_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG)
new SpeciesEvolution(Species.SOLGALEO, 1, EvolutionItem.SUN_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG),
new SpeciesEvolution(Species.LUNALA, 1, EvolutionItem.MOON_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG)
],
[Species.MELTAN]: [
new SpeciesEvolution(Species.MELMETAL, 48, null, null)

View File

@ -428,7 +428,7 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent {
moveAnim.bgSprite.setScale(1.25);
moveAnim.bgSprite.setAlpha(this.opacity / 255);
scene.field.add(moveAnim.bgSprite);
const fieldPokemon = scene.getNonSwitchedEnemyPokemon() || scene.getNonSwitchedPlayerPokemon();
const fieldPokemon = scene.getEnemyPokemon(false) ?? scene.getPlayerPokemon(false);
if (!isNullOrUndefined(priority)) {
scene.field.moveTo(moveAnim.bgSprite as Phaser.GameObjects.GameObject, priority);
} else if (fieldPokemon?.isOnField()) {
@ -999,7 +999,7 @@ export abstract class BattleAnim {
const setSpritePriority = (priority: integer) => {
switch (priority) {
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;
case 1:
scene.field.moveTo(moveSprite, scene.field.getAll().length - 1);

View File

@ -18,7 +18,7 @@ import Move, {
StatusCategoryOnAllyAttr
} from "#app/data/move";
import { SpeciesFormChangeManualTrigger } from "#app/data/pokemon-forms";
import { StatusEffect } from "#app/data/status-effect";
import { getStatusEffectHealText, StatusEffect } from "#app/data/status-effect";
import { TerrainType } from "#app/data/terrain";
import { Type } from "#app/data/type";
import { WeatherType } from "#app/data/weather";
@ -1573,6 +1573,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 {
constructor() {
super(BattlerTagType.TRUANT, Abilities.TRUANT, BattlerTagLapseType.MOVE, 1);
@ -2075,6 +2091,37 @@ 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);
}
}
}
/**
* Battler tag enabling the Stockpile mechanic. This tag handles:
* - Stack tracking, including max limit enforcement (which is replicated in Stockpile for redundancy).
@ -2480,7 +2527,10 @@ export class SubstituteTag extends BattlerTag {
onHit(pokemon: Pokemon): void {
const moveEffectPhase = pokemon.scene.getCurrentPhase();
if (moveEffectPhase instanceof MoveEffectPhase) {
const attacker = moveEffectPhase.getUserPokemon()!;
const attacker = moveEffectPhase.getUserPokemon();
if (!attacker) {
return;
}
const move = moveEffectPhase.move.getMove();
const firstHit = (attacker.turnData.hitCount === attacker.turnData.hitsLeft);
@ -2777,6 +2827,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.
* @param sourceId - The ID of the pokemon adding the tag
@ -2913,6 +3024,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new IceFaceBlockDamageTag(tagType);
case BattlerTagType.DISGUISE:
return new FormBlockDamageTag(tagType);
case BattlerTagType.COMMANDED:
return new CommandedTag(sourceId);
case BattlerTagType.STOCKPILING:
return new StockpilingTag(sourceMove);
case BattlerTagType.OCTOLOCK:
@ -2934,6 +3047,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new ThroatChoppedTag();
case BattlerTagType.GORILLA_TACTICS:
return new GorillaTacticsTag();
case BattlerTagType.UNBURDEN:
return new UnburdenTag();
case BattlerTagType.SUBSTITUTE:
return new SubstituteTag(sourceMove, sourceId);
case BattlerTagType.AUTOTOMIZED:
@ -2954,6 +3069,10 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new TelekinesisTag(sourceMove);
case BattlerTagType.POWER_TRICK:
return new PowerTrickTag(sourceMove, sourceId);
case BattlerTagType.GRUDGE:
return new GrudgeTag();
case BattlerTagType.PSYCHO_SHIFT:
return new PsychoShiftTag();
case BattlerTagType.NONE:
default:
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 { getStatusEffectHealText } from "./status-effect";
import * as Utils from "../utils";
import { DoubleBerryEffectAbAttr, ReduceBerryUseThresholdAbAttr, applyAbAttrs } from "./ability";
import { DoubleBerryEffectAbAttr, PostItemLostAbAttr, ReduceBerryUseThresholdAbAttr, applyAbAttrs, applyPostItemLostAbAttrs } from "./ability";
import i18next from "i18next";
import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type";
@ -75,6 +75,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, hpHealed);
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(),
hpHealed.value, i18next.t("battle:hpHealBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), berryName: getBerryName(berryType) }), true));
applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false);
};
case BerryType.LUM:
return (pokemon: Pokemon) => {
@ -86,6 +87,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
}
pokemon.resetStatus(true, true);
pokemon.updateInfo();
applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false);
};
case BerryType.LIECHI:
case BerryType.GANLON:
@ -101,6 +103,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
const statStages = new Utils.NumberHolder(1);
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statStages);
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], statStages.value));
applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false);
};
case BerryType.LANSAT:
return (pokemon: Pokemon) => {
@ -108,6 +111,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
pokemon.battleData.berriesEaten.push(berryType);
}
pokemon.addTag(BattlerTagType.CRIT_BOOST);
applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false);
};
case BerryType.STARF:
return (pokemon: Pokemon) => {
@ -118,6 +122,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
const stages = new Utils.NumberHolder(2);
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, stages);
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ randStat ], stages.value));
applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false);
};
case BerryType.LEPPA:
return (pokemon: Pokemon) => {
@ -128,6 +133,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
if (ppRestoreMove !== undefined) {
ppRestoreMove!.ppUsed = Math.max(ppRestoreMove!.ppUsed - 10, 0);
pokemon.scene.queueMessage(i18next.t("battle:ppHealBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: ppRestoreMove!.getName(), berryName: getBerryName(berryType) }));
applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false);
}
};
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -181,7 +181,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
// Sort berries by party member ID to more easily re-add later if necessary
const berryItemsMap = new Map<number, BerryModifier[]>();
scene.getParty().forEach(pokemon => {
scene.getPlayerParty().forEach(pokemon => {
const pokemonBerries = berryItems.filter(b => b.pokemonId === pokemon.id);
if (pokemonBerries?.length > 0) {
berryItemsMap.set(pokemon.id, pokemonBerries);
@ -267,7 +267,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED);
encounter.setDialogueToken("foodReward", revSeed?.name ?? i18next.t("modifierType:ModifierType.REVIVER_SEED.name"));
const givePartyPokemonReviverSeeds = () => {
const party = scene.getParty();
const party = scene.getPlayerParty();
party.forEach(p => {
const heldItems = p.getHeldItems();
if (revSeed && !heldItems.some(item => item instanceof PokemonInstantReviveModifier)) {
@ -308,7 +308,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
const berryMap = encounter.misc.berryItemsMap;
// Returns 2/5 of the berries stolen to each Pokemon
const party = scene.getParty();
const party = scene.getPlayerParty();
party.forEach(pokemon => {
const stolenBerries: BerryModifier[] = berryMap.get(pokemon.id);
const berryTypesAsArray: BerryType[] = [];

View File

@ -58,7 +58,7 @@ export const BerriesAboundEncounter: MysteryEncounter =
// Calculate boss mon
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);
encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon));
const config: EnemyPartyConfig = {
@ -77,7 +77,7 @@ export const BerriesAboundEncounter: MysteryEncounter =
scene.currentBattle.waveIndex > 160 ? 7
: scene.currentBattle.waveIndex > 120 ? 5
: scene.currentBattle.waveIndex > 40 ? 4 : 2;
regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0);
regenerateModifierPoolThresholds(scene.getPlayerParty(), ModifierPoolType.PLAYER, 0);
encounter.misc = { numBerries };
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 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
if (prioritizedPokemon) {

View File

@ -331,7 +331,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter =
const encounter = scene.currentBattle.mysteryEncounter!;
// 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 });
encounter.setDialogueToken("numBugTypes", numBugTypesText);

View File

@ -245,7 +245,7 @@ export const ClowningAroundEncounter: MysteryEncounter =
// So Vitamins, form change items, etc. are not included
const encounter = scene.currentBattle.mysteryEncounter!;
const party = scene.getParty();
const party = scene.getPlayerParty();
let mostHeldItemsPokemon = party[0];
let count = mostHeldItemsPokemon.getHeldItems()
.filter(m => m.isTransferable && !(m instanceof BerryModifier))
@ -328,7 +328,7 @@ export const ClowningAroundEncounter: MysteryEncounter =
.withPreOptionPhase(async (scene: BattleScene) => {
// Randomize the second type of all player's pokemon
// 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);
// 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 Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species";
import { BattlerIndex } from "#app/battle";
import BattleScene from "#app/battle-scene";
import { EncounterBattleAnim } from "#app/data/battle-anims";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
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 { DANCING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups";
import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
import { BattlerIndex } from "#app/battle";
import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
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 { 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 { LearnMovePhase } from "#app/phases/learn-move-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 { 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";
/** the i18n namespace for this encounter */
@ -92,7 +92,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
.withCatchAllowed(true)
.withFleeAllowed(false)
.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);
return true;
@ -217,7 +217,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
const onPokemonSelected = (pokemon: PlayerPokemon) => {
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
const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getPlayerPokemon());

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 MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
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 { generateModifierType, leaveEncounterWithoutBattle, selectPokemonForOption, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-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 Pokemon, { PlayerPokemon } from "#app/field/pokemon";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { BerryModifier, HealingBoosterModifier, LevelIncrementBoosterModifier, MoneyMultiplierModifier, PokemonHeldItemModifier, 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 */
const namespace = "mysteryEncounters/delibirdy";
@ -133,7 +133,7 @@ export const DelibirdyEncounter: MysteryEncounter =
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
// At max stacks, give the first party pokemon a Shell Bell instead
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");
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true);
} else {
@ -207,7 +207,7 @@ export const DelibirdyEncounter: MysteryEncounter =
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
// At max stacks, give the first party pokemon a Shell Bell instead
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");
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true);
} else {
@ -220,7 +220,7 @@ export const DelibirdyEncounter: MysteryEncounter =
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
// At max stacks, give the first party pokemon a Shell Bell instead
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");
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true);
} else {
@ -299,7 +299,7 @@ export const DelibirdyEncounter: MysteryEncounter =
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
// At max stacks, give the first party pokemon a Shell Bell instead
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");
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true);
} else {

View File

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

View File

@ -184,7 +184,7 @@ export const FieryFalloutEncounter: MysteryEncounter =
async (scene: BattleScene) => {
// Damage non-fire types and burn 1 random non-fire type member + give it Heatproof
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) {
const percentage = DAMAGE_PERCENTAGE / 100;
@ -257,7 +257,7 @@ export const FieryFalloutEncounter: MysteryEncounter =
function giveLeadPokemonAttackTypeBoostItem(scene: BattleScene) {
// 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) {
// Generate type booster held item, default to Charcoal if item fails to generate
let boosterModifierType = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER) as AttackTypeBoosterModifierType;

View File

@ -56,7 +56,7 @@ export const FightOrFlightEncounter: MysteryEncounter =
// Calculate boss mon
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);
encounter.setDialogueToken("enemyPokemon", bossPokemon.getNameToRender());
const config: EnemyPartyConfig = {
@ -86,11 +86,11 @@ export const FightOrFlightEncounter: MysteryEncounter =
: scene.currentBattle.waveIndex > 40
? ModifierTier.ULTRA
: ModifierTier.GREAT;
regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0);
regenerateModifierPoolThresholds(scene.getPlayerParty(), ModifierPoolType.PLAYER, 0);
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
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.misc = item;

View File

@ -165,7 +165,7 @@ async function summonPlayerPokemon(scene: BattleScene) {
const playerPokemon = encounter.misc.playerPokemon;
// 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);
if (chosenIndex !== 0) {
const leadPokemon = party[0];

View File

@ -1,6 +1,7 @@
import { leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { TrainerSlot, } from "#app/data/trainer-config";
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 { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene";
@ -105,7 +106,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
// Load bgm
let bgmKey: string;
if (scene.musicPreference === 0) {
if (scene.musicPreference === MusicPreference.CONSISTENT) {
bgmKey = "mystery_encounter_gen_5_gts";
scene.loadBgm(bgmKey, `${bgmKey}.mp3`);
} else {
@ -191,7 +192,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
receivedPokemonData.pokeball = randInt(4) as PokeballType;
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);
scene.getParty().push(newPlayerPokemon);
scene.getPlayerParty().push(newPlayerPokemon);
await newPlayerPokemon.loadAssets();
for (const mod of modifiers) {
@ -224,7 +225,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
const encounter = scene.currentBattle.mysteryEncounter!;
const onPokemonSelected = (pokemon: PlayerPokemon) => {
// 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);
// Extra shiny roll at 1/128 odds (boosted by events and charms)
if (!tradePokemon.shiny) {
@ -299,7 +300,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
receivedPokemonData.pokeball = randInt(4) as PokeballType;
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);
scene.getParty().push(newPlayerPokemon);
scene.getPlayerParty().push(newPlayerPokemon);
await newPlayerPokemon.loadAssets();
for (const mod of modifiers) {
@ -366,10 +367,11 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
})
.withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!;
const modifier = encounter.misc.chosenModifier;
const modifier = encounter.misc.chosenModifier as PokemonHeldItemModifier;
const party = scene.getPlayerParty();
// 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;
// Eggs and White Herb are not in the pool
if (type.id === "WHITE_HERB") {
@ -384,11 +386,11 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
tier++;
}
regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0);
regenerateModifierPoolThresholds(party, ModifierPoolType.PLAYER, 0);
let item: ModifierTypeOption | null = null;
// TMs excluded from possible rewards
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);
@ -430,9 +432,9 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
function getPokemonTradeOptions(scene: BattleScene): Map<number, EnemyPokemon[]> {
const tradeOptionsMap: Map<number, EnemyPokemon[]> = new Map<number, EnemyPokemon[]>();
// 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 (pokemon.species.legendary || pokemon.species.subLegendary || pokemon.species.mythical) {
const generation = pokemon.species.generation;

View File

@ -104,7 +104,7 @@ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.with
],
},
async (scene: BattleScene) => {
const allowedPokemon = scene.getParty().filter((p) => p.isAllowedInBattle());
const allowedPokemon = scene.getPlayerParty().filter((p) => p.isAllowedInBattle());
for (const pkm of allowedPokemon) {
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 MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
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 { 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 { 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 { 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 */
const namespace = "mysteryEncounters/mysteriousChest";
@ -177,7 +177,7 @@ export const MysteriousChestEncounter: MysteryEncounter =
await showEncounterText(scene, `${namespace}:option.1.bad`);
// Handle game over edge case
const allowedPokemon = scene.getParty().filter(p => p.isAllowedInBattle());
const allowedPokemon = scene.getPokemonAllowedInBattle();
if (allowedPokemon.length === 0) {
// If there are no longer any legal pokemon in the party, game over.
scene.clearPhaseQueue();

View File

@ -100,7 +100,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter =
// Only Pokemon that can gain benefits are above half HP with no status
const selectableFilter = (pokemon: Pokemon) => {
// 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;
}
if (!encounter.pokemonMeetsPrimaryRequirements(scene, pokemon)) {

View File

@ -134,7 +134,7 @@ export const TeleportingHijinksEncounter: MysteryEncounter =
// Init enemy
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);
encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon));
const config: EnemyPartyConfig = {
@ -170,7 +170,7 @@ async function doBiomeTransitionDialogueAndBattleInit(scene: BattleScene) {
// Init enemy
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);
encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon));

View File

@ -126,7 +126,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
];
// Determine the 3 pokemon the player can battle with
let partyCopy = scene.getParty().slice(0);
let partyCopy = scene.getPlayerParty().slice(0);
partyCopy = partyCopy
.filter(p => p.isAllowedInBattle())
.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) {
const party = scene.getParty();
const party = scene.getPlayerParty();
const chosenIndex = party.indexOf(chosenPokemon);
party[chosenIndex] = party[0];
party[0] = chosenPokemon;
encounter.misc.originalParty = scene.getParty().slice(1);
encounter.misc.originalParty = scene.getPlayerParty().slice(1);
encounter.misc.originalPartyHeldItems = encounter.misc.originalParty
.map(p => p.getHeldItems());
scene["party"] = [
@ -529,7 +529,7 @@ function checkAchievement(scene: BattleScene) {
function restorePartyAndHeldItems(scene: BattleScene) {
const encounter = scene.currentBattle.mysteryEncounter!;
// Restore original party
scene.getParty().push(...encounter.misc.originalParty);
scene.getPlayerParty().push(...encounter.misc.originalParty);
// Restore held items
const originalHeldItems = encounter.misc.originalPartyHeldItems;

View File

@ -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)
// Sort party by bst
const sortedParty = scene.getParty().slice(0)
const sortedParty = scene.getPlayerParty().slice(0)
.sort((pokemon1, pokemon2) => {
const pokemon1Bst = pokemon1.calculateBaseStats().reduce((a, b) => a + b, 0);
const pokemon2Bst = pokemon2.calculateBaseStats().reduce((a, b) => a + b, 0);

View File

@ -23,6 +23,7 @@ import { ReturnPhase } from "#app/phases/return-phase";
import i18next from "i18next";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { BattlerTagType } from "#enums/battler-tag-type";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/theWinstrateChallenge";
@ -187,9 +188,10 @@ function endTrainerBattleAndShowDialogue(scene: BattleScene): Promise<void> {
} else {
scene.arena.resetArenaEffects();
const playerField = scene.getPlayerField();
playerField.forEach((pokemon) => pokemon.lapseTag(BattlerTagType.COMMANDED));
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
// 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) {

View File

@ -152,7 +152,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
}
// Add pokemon and mods back
scene.getParty().push(playerPokemon);
scene.getPlayerParty().push(playerPokemon);
for (const mod of modifiers.value) {
mod.pokemonId = playerPokemon.id;
scene.addModifier(mod, true, false, false, true);
@ -229,7 +229,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
scene.gameData.setPokemonCaught(playerPokemon, false);
// Add pokemon and modifiers back
scene.getParty().push(playerPokemon);
scene.getPlayerParty().push(playerPokemon);
for (const mod of modifiers.value) {
mod.pokemonId = playerPokemon.id;
scene.addModifier(mod, true, false, false, true);
@ -342,7 +342,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
scene.gameData.setPokemonCaught(playerPokemon, false);
// Add pokemon and mods back
scene.getParty().push(playerPokemon);
scene.getPlayerParty().push(playerPokemon);
for (const mod of modifiers.value) {
mod.pokemonId = playerPokemon.id;
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 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
// First leftovers

View File

@ -51,7 +51,7 @@ export const UncommonBreedEncounter: MysteryEncounter =
// Calculate boss mon
// Level equal to 2 below highest party member
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);
// Pokemon will always have one of its egg moves in its moveset

View File

@ -176,7 +176,7 @@ export const WeirdDreamEncounter: MysteryEncounter =
for (const transformation of scene.currentBattle.mysteryEncounter!.misc.teamTransformations) {
scene.removePokemonFromPlayerParty(transformation.previousPokemon, false);
scene.getParty().push(transformation.newPokemon);
scene.getPlayerParty().push(transformation.newPokemon);
}
})
.withOptionPhase(async (scene: BattleScene) => {
@ -280,7 +280,7 @@ export const WeirdDreamEncounter: MysteryEncounter =
const onBeforeRewards = () => {
// Before battle rewards, unlock the passive on a pokemon in the player's team for the rest of the run (not permanently)
// One random pokemon will get its passive unlocked
const passiveDisabledPokemon = scene.getParty().filter(p => !p.passive);
const passiveDisabledPokemon = scene.getPlayerParty().filter(p => !p.passive);
if (passiveDisabledPokemon?.length > 0) {
const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)];
enablePassiveMon.passive = true;
@ -306,7 +306,7 @@ export const WeirdDreamEncounter: MysteryEncounter =
},
async (scene: BattleScene) => {
// 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.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate);
pokemon.levelExp = 0;
@ -329,7 +329,7 @@ interface PokemonTransformation {
}
function getTeamTransformations(scene: BattleScene): PokemonTransformation[] {
const party = scene.getParty();
const party = scene.getPlayerParty();
// Removes all pokemon from the party
const alreadyUsedSpecies: PokemonSpecies[] = party.map(p => p.species);
const pokemonTransformations: PokemonTransformation[] = party.map(p => {
@ -404,7 +404,7 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
if (shouldGetOldGateau(newPokemon)) {
const stats = getOldGateauBoostedStats(newPokemon);
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);
const modifier = modType?.newModifier(newPokemon);
if (modifier) {
@ -417,7 +417,7 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
}
// 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) {
const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)];
enablePassiveMon.passive = true;

View File

@ -88,7 +88,7 @@ export default class MysteryEncounterOption implements IMysteryEncounterOption {
* @param pokemon
*/
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) {
return true;
}
let qualified: PlayerPokemon[] = scene.getParty();
let qualified: PlayerPokemon[] = scene.getPlayerParty();
for (const req of this.primaryPokemonRequirements) {
if (req.meetsRequirement(scene)) {
const queryParty = req.queryParty(scene.getParty());
const queryParty = req.queryParty(scene.getPlayerParty());
qualified = qualified.filter(pkmn => queryParty.includes(pkmn));
} else {
this.primaryPokemon = undefined;
@ -162,10 +162,10 @@ export default class MysteryEncounterOption implements IMysteryEncounterOption {
return true;
}
let qualified: PlayerPokemon[] = scene.getParty();
let qualified: PlayerPokemon[] = scene.getPlayerParty();
for (const req of this.secondaryPokemonRequirements) {
if (req.meetsRequirement(scene)) {
const queryParty = req.queryParty(scene.getParty());
const queryParty = req.queryParty(scene.getPlayerParty());
qualified = qualified.filter(pkmn => queryParty.includes(pkmn));
} else {
this.secondaryPokemon = [];

View File

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

View File

@ -314,7 +314,7 @@ export default class MysteryEncounter implements IMysteryEncounter {
* @param pokemon
*/
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 {
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) {
this.primaryPokemon = activeMon[0];
} else {
this.primaryPokemon = scene.getParty().filter(p => p.isAllowedInBattle())[0];
this.primaryPokemon = scene.getPlayerParty().filter(p => p.isAllowedInBattle())[0];
}
return true;
}
let qualified: PlayerPokemon[] = scene.getParty();
let qualified: PlayerPokemon[] = scene.getPlayerParty();
for (const req of this.primaryPokemonRequirements) {
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 {
this.primaryPokemon = undefined;
return false;
@ -394,10 +394,10 @@ export default class MysteryEncounter implements IMysteryEncounter {
return true;
}
let qualified: PlayerPokemon[] = scene.getParty();
let qualified: PlayerPokemon[] = scene.getPlayerParty();
for (const req of this.secondaryPokemonRequirements) {
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 {
this.secondaryPokemon = [];
return false;

View File

@ -39,7 +39,7 @@ export class CanLearnMoveRequirement extends EncounterPokemonRequirement {
}
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) {
return false;

View File

@ -418,9 +418,9 @@ export function generateModifierType(scene: BattleScene, modifier: () => Modifie
// Populates item id and tier (order matters)
result = result
.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 +451,9 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p
// Open party screen to choose pokemon
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(() => {
const pokemon = scene.getParty()[slotIndex];
const pokemon = scene.getPlayerParty()[slotIndex];
const secondaryOptions = onPokemonSelected(pokemon);
if (!secondaryOptions) {
scene.currentBattle.mysteryEncounter!.setDialogueToken("selectedPokemon", pokemon.getNameToRender());
@ -563,7 +563,7 @@ export function selectOptionThenPokemon(scene: BattleScene, options: OptionSelec
const selectPokemonAfterOption = (selectedOptionIndex: number) => {
// Open party screen to choose a Pokemon
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
scene.ui.setMode(modeToSetOnExit).then(() => {
const result: PokemonAndOptionSelected = { selectedPokemonIndex: slotIndex, selectedOptionIndex: selectedOptionIndex };
@ -713,7 +713,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
*/
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) {
scene.clearPhaseQueue();
@ -750,7 +750,7 @@ export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase:
* @param addHealPhase
*/
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) {
scene.clearPhaseQueue();

View File

@ -53,24 +53,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
* @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 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 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 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
*/
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 chosenPokemon: PlayerPokemon | null = null;
const fullyLegalMons = party.filter(p => (!isAllowed || p.isAllowed()) && (isFainted || !p.isFainted()));
const allowedOnlyMons = party.filter(p => p.isAllowed());
const fullyLegalMons = party.filter(p => (!isAllowed || p.isAllowedInChallenge()) && (isFainted || !p.isFainted()));
const allowedOnlyMons = party.filter(p => p.isAllowedInChallenge());
if (doNotReturnLastAllowedMon && fullyLegalMons.length === 1) {
// 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) {
chosenIndex = randSeedInt(faintedLegalMons.length);
chosenPokemon = faintedLegalMons[chosenIndex];
@ -101,11 +101,11 @@ export function getRandomPlayerPokemon(scene: BattleScene, isAllowed: boolean =
* @returns
*/
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;
for (const p of party) {
if (isAllowed && !p.isAllowed()) {
if (isAllowed && !p.isAllowedInChallenge()) {
continue;
}
if (!isFainted && p.isFainted()) {
@ -127,11 +127,11 @@ export function getHighestLevelPlayerPokemon(scene: BattleScene, isAllowed: bool
* @returns
*/
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;
for (const p of party) {
if (isAllowed && !p.isAllowed()) {
if (isAllowed && !p.isAllowedInChallenge()) {
continue;
}
if (!isFainted && p.isFainted()) {
@ -152,11 +152,11 @@ export function getHighestStatPlayerPokemon(scene: BattleScene, stat: PermanentS
* @returns
*/
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;
for (const p of party) {
if (isAllowed && !p.isAllowed()) {
if (isAllowed && !p.isAllowedInChallenge()) {
continue;
}
if (!isFainted && p.isFainted()) {
@ -177,11 +177,11 @@ export function getLowestLevelPlayerPokemon(scene: BattleScene, isAllowed: boole
* @returns
*/
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;
for (const p of party) {
if (isAllowed && !p.isAllowed()) {
if (isAllowed && !p.isAllowedInChallenge()) {
continue;
}
if (!isFainted && p.isFainted()) {
@ -315,7 +315,7 @@ export function applyHealToPokemon(scene: BattleScene, pokemon: PlayerPokemon, h
*/
export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: number) {
const modType = modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE()
.generateType(pokemon.scene.getParty(), [ value ])
.generateType(pokemon.scene.getPlayerParty(), [ value ])
?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE);
const modifier = modType?.newModifier(pokemon);
if (modifier) {
@ -591,7 +591,7 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po
const addToParty = (slotIndex?: number) => {
const newPokemon = pokemon.addToParty(pokeballType, slotIndex);
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);
}
Promise.all(modifiers.map(m => scene.addModifier(m, true))).then(() => {
@ -605,7 +605,7 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po
});
};
Promise.all([ pokemon.hideInfo(), scene.gameData.setPokemonCaught(pokemon) ]).then(() => {
if (scene.getParty().length === 6) {
if (scene.getPlayerParty().length === 6) {
const promptRelease = () => {
scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => {
scene.pokemonInfoContainer.makeRoomForConfirmUi(1, true);
@ -826,7 +826,7 @@ export async function addPokemonDataToDexAndValidateAchievements(scene: BattleSc
* @param invalidSelectionKey
*/
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;
}
if (!pokemon.isAllowedInBattle()) {

View File

@ -1,3 +1,4 @@
import { NumberHolder } from "#app/utils";
import { PokeballType } from "#enums/pokeball";
import BattleScene from "../battle-scene";
import i18next from "i18next";
@ -82,11 +83,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 bounceYOffset = y1;
let bounceY = y2;
const yd = y2 - y1;
const x0 = pokeball.x;
const x1 = x0 + 3;
const x2 = x0 - 3;
let critShakes = 4;
const doBounce = () => {
scene.tweens.add({
@ -117,5 +145,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();
}
}

View File

@ -47,7 +47,7 @@ export function getPokemonSpecies(species: Species | Species[] | undefined): Pok
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
? allSpecies.find(s => s.speciesId === species)! // TODO: is the bang correct?
: allSpecies[species - 1];
@ -129,26 +129,27 @@ export type PokemonSpeciesFilter = (species: PokemonSpecies) => boolean;
export abstract class PokemonSpeciesForm {
public speciesId: Species;
public formIndex: integer;
public generation: integer;
public type1: Type;
public type2: Type | null;
public height: number;
public weight: number;
public ability1: Abilities;
public ability2: Abilities;
public abilityHidden: Abilities;
public baseTotal: integer;
public baseStats: integer[];
public catchRate: integer;
public baseFriendship: integer;
public baseExp: integer;
public genderDiffs: boolean;
public isStarterSelectable: boolean;
protected _formIndex: number;
protected _generation: number;
readonly type1: Type;
readonly type2: Type | null;
readonly height: number;
readonly weight: number;
readonly ability1: Abilities;
readonly ability2: Abilities;
readonly abilityHidden: Abilities;
readonly baseTotal: number;
readonly baseStats: number[];
readonly catchRate: number;
readonly baseFriendship: number;
readonly baseExp: number;
readonly genderDiffs: boolean;
readonly isStarterSelectable: boolean;
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,
catchRate: integer, baseFriendship: integer, baseExp: integer, genderDiffs: boolean, isStarterSelectable: boolean) {
baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number,
catchRate: number, baseFriendship: number, baseExp: number, genderDiffs: boolean, isStarterSelectable: boolean
) {
this.type1 = type1;
this.type2 = type2;
this.height = height;
@ -180,7 +181,23 @@ export abstract class PokemonSpeciesForm {
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);
}
@ -188,7 +205,7 @@ export abstract class PokemonSpeciesForm {
* Method to get the total number of abilities a Pokemon species has.
* @returns Number of abilities
*/
getAbilityCount(): integer {
getAbilityCount(): number {
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)
* @returns The id of the Ability
*/
getAbility(abilityIndex: integer): Abilities {
getAbility(abilityIndex: number): Abilities {
let ret: Abilities;
if (abilityIndex === 0) {
ret = this.ability1;
@ -277,12 +294,12 @@ export abstract class PokemonSpeciesForm {
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, "/");
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) {
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}` : ""}`;
}
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)}`;
}
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
* @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 variantDataIndex: integer | string = this.speciesId;
let variantDataIndex: number | string = this.speciesId;
const species = getPokemonSpecies(this.speciesId);
if (species.forms.length > 0 && formIndex !== undefined) {
formkey = species.forms[formIndex]?.getFormSpriteKey(formIndex);
@ -324,13 +341,13 @@ export abstract class PokemonSpeciesForm {
return variantDataIndex;
}
getIconAtlasKey(formIndex?: integer, shiny?: boolean, variant?: integer): string {
getIconAtlasKey(formIndex?: number, shiny?: boolean, variant?: number): string {
const variantDataIndex = this.getVariantDataIndex(formIndex);
const isVariant = shiny && variantData[variantDataIndex] && (variant !== undefined && variantData[variantDataIndex][variant]);
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) {
formIndex = this.formIndex;
}
@ -379,7 +396,7 @@ export abstract class PokemonSpeciesForm {
return ret;
}
getCryKey(formIndex?: integer): string {
getCryKey(formIndex?: number): string {
let speciesId = this.speciesId;
if (this.speciesId > 2000) {
switch (this.speciesId) {
@ -443,10 +460,10 @@ export abstract class PokemonSpeciesForm {
break;
}
}
return ret;
return `cry/${ret}`;
}
validateStarterMoveset(moveset: StarterMoveset, eggMoves: integer): boolean {
validateStarterMoveset(moveset: StarterMoveset, eggMoves: number): boolean {
const rootSpeciesId = this.getRootSpeciesId();
for (const moveId of moveset) {
if (speciesEggMoves.hasOwnProperty(rootSpeciesId)) {
@ -467,11 +484,11 @@ export abstract class PokemonSpeciesForm {
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 => {
const spriteKey = this.getSpriteKey(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, () => {
const originalWarn = console.warn;
// Ignore warnings for missing frames, because there will be a lot
@ -529,14 +546,14 @@ export abstract class PokemonSpeciesForm {
if (cry?.pendingRemove) {
cry = null;
}
cry = scene.playSound(`cry/${(cry ?? cryKey)}`, soundConfig);
cry = scene.playSound(cry ?? cryKey, soundConfig);
if (ignorePlay) {
cry.stop();
}
return cry;
}
generateCandyColors(scene: BattleScene): integer[][] {
generateCandyColors(scene: BattleScene): number[][] {
const sourceTexture = scene.textures.get(this.getSpriteKey(false));
const sourceFrame = sourceTexture.frames[sourceTexture.firstFrame];
@ -544,7 +561,7 @@ export abstract class PokemonSpeciesForm {
const canvas = document.createElement("canvas");
const spriteColors: integer[][] = [];
const spriteColors: number[][] = [];
const context = canvas.getContext("2d");
const frame = sourceFrame;
@ -567,7 +584,7 @@ export abstract class PokemonSpeciesForm {
}
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) {
continue;
}
@ -586,27 +603,28 @@ export abstract class PokemonSpeciesForm {
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 {
public name: string;
public subLegendary: boolean;
public legendary: boolean;
public mythical: boolean;
public species: string;
public growthRate: GrowthRate;
public malePercent: number | null;
public genderDiffs: boolean;
public canChangeForm: boolean;
public forms: PokemonForm[];
readonly subLegendary: boolean;
readonly legendary: boolean;
readonly mythical: boolean;
readonly species: string;
readonly growthRate: GrowthRate;
readonly malePercent: number | null;
readonly genderDiffs: boolean;
readonly canChangeForm: boolean;
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,
baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer,
catchRate: integer, baseFriendship: integer, baseExp: integer, growthRate: GrowthRate, malePercent: number | null,
genderDiffs: boolean, canChangeForm?: boolean, ...forms: PokemonForm[]) {
baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number,
catchRate: number, baseFriendship: number, baseExp: number, growthRate: GrowthRate, malePercent: number | null,
genderDiffs: boolean, canChangeForm?: boolean, ...forms: PokemonForm[]
) {
super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd,
catchRate, baseFriendship, baseExp, genderDiffs, false);
this.speciesId = id;
@ -631,7 +649,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
});
}
getName(formIndex?: integer): string {
getName(formIndex?: number): string {
if (formIndex !== undefined && this.forms.length) {
const form = this.forms[formIndex];
let key: string | null;
@ -662,11 +680,11 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
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));
}
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);
}
@ -688,7 +706,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
* @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.
*/
private getStrengthLevelDiff(strength: PartyMemberStrength): integer {
private getStrengthLevelDiff(strength: PartyMemberStrength): number {
switch (Math.min(strength, PartyMemberStrength.STRONGER)) {
case PartyMemberStrength.WEAKEST:
return 60;
@ -705,7 +723,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();
if (prevolutionLevels.length) {
@ -847,7 +865,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
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[] = [];
if (pokemonPrevolutions.hasOwnProperty(this.speciesId)) {
const prevolutionLevels = this.getPrevolutionLevels().reverse();
@ -899,7 +917,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
return variantData.hasOwnProperty(variantDataIndex) || variantData.hasOwnProperty(this.speciesId);
}
getFormSpriteKey(formIndex?: integer) {
getFormSpriteKey(formIndex?: number) {
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`);
formIndex = Math.min(formIndex, this.forms.length - 1);
@ -919,16 +937,17 @@ export class PokemonForm extends PokemonSpeciesForm {
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,
baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer,
catchRate: integer, baseFriendship: integer, baseExp: integer, genderDiffs?: boolean, formSpriteKey?: string | null, isStarterSelectable?: boolean, ) {
baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number,
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,
catchRate, baseFriendship, baseExp, !!genderDiffs, (!!isStarterSelectable || !formKey));
catchRate, baseFriendship, baseExp, genderDiffs, (isStarterSelectable || !formKey));
this.formName = formName;
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;
}
}

View File

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

View File

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

View File

@ -12,5 +12,15 @@ export enum PokemonAnimType {
* Removes a Pokemon's Substitute doll from the field.
* 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
}

View File

@ -224,66 +224,6 @@ export class Arena {
return 0;
}
getTypeForBiome() {
switch (this.biomeType) {
case Biome.TOWN:
case Biome.PLAINS:
case Biome.METROPOLIS:
return Type.NORMAL;
case Biome.GRASS:
case Biome.TALL_GRASS:
return Type.GRASS;
case Biome.FOREST:
case Biome.JUNGLE:
return Type.BUG;
case Biome.SLUM:
case Biome.SWAMP:
return Type.POISON;
case Biome.SEA:
case Biome.BEACH:
case Biome.LAKE:
case Biome.SEABED:
return Type.WATER;
case Biome.MOUNTAIN:
return Type.FLYING;
case Biome.BADLANDS:
return Type.GROUND;
case Biome.CAVE:
case Biome.DESERT:
return Type.ROCK;
case Biome.ICE_CAVE:
case Biome.SNOWY_FOREST:
return Type.ICE;
case Biome.MEADOW:
case Biome.FAIRY_CAVE:
case Biome.ISLAND:
return Type.FAIRY;
case Biome.POWER_PLANT:
return Type.ELECTRIC;
case Biome.VOLCANO:
return Type.FIRE;
case Biome.GRAVEYARD:
case Biome.TEMPLE:
return Type.GHOST;
case Biome.DOJO:
case Biome.CONSTRUCTION_SITE:
return Type.FIGHTING;
case Biome.FACTORY:
case Biome.LABORATORY:
return Type.STEEL;
case Biome.RUINS:
case Biome.SPACE:
return Type.PSYCHIC;
case Biome.WASTELAND:
case Biome.END:
return Type.DRAGON;
case Biome.ABYSS:
return Type.DARK;
default:
return Type.UNKNOWN;
}
}
getBgTerrainColorRatioForBiome(): number {
switch (this.biomeType) {
case Biome.SPACE:

View File

@ -7,12 +7,12 @@ import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, Varia
import { default as PokemonSpecies, PokemonSpeciesForm, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species";
import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER, getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters";
import { starterPassiveAbilities } from "#app/data/balance/passives";
import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils";
import { Constructor, isNullOrUndefined, randSeedInt, type nil } from "#app/utils";
import * as Utils from "#app/utils";
import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "#app/data/type";
import { getLevelTotalExp } from "#app/data/exp";
import { Stat, type PermanentStat, type BattleStat, type EffectiveStat, PERMANENT_STATS, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat";
import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier } from "#app/modifier/modifier";
import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier, PokemonMultiHitModifier } from "#app/modifier/modifier";
import { PokeballType } from "#app/data/pokeball";
import { Gender } from "#app/data/gender";
import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims";
@ -22,7 +22,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags";
import { WeatherType } from "#app/data/weather";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag";
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr } from "#app/data/ability";
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, PostDamageForceSwitchAbAttr } from "#app/data/ability";
import PokemonData from "#app/system/pokemon-data";
import { BattlerIndex } from "#app/battle";
import { Mode } from "#app/ui/ui";
@ -230,7 +230,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
if (this.variant === undefined) {
this.variant = this.shiny ? this.generateVariant() : 0;
this.variant = this.shiny ? this.generateShinyVariant() : 0;
}
this.customPokemonData = new CustomPokemonData();
@ -325,35 +325,45 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return this.scene.field.getIndex(this) > -1;
}
isFainted(checkStatus?: boolean): boolean {
return !this.hp && (!checkStatus || this.status?.effect === StatusEffect.FAINT);
/**
* Checks if a pokemon is fainted (ie: its `hp <= 0`).
* It's usually better to call {@linkcode isAllowedInBattle()}
* @param checkStatus `true` to also check that the pokemon's status is {@linkcode StatusEffect.FAINT}
* @returns `true` if the pokemon is fainted
*/
public isFainted(checkStatus: boolean = false): boolean {
return this.hp <= 0 && (!checkStatus || this.status?.effect === StatusEffect.FAINT);
}
/**
* Check if this pokemon is both not fainted (or a fled wild pokemon) and allowed to be in battle.
* This is frequently a better alternative to {@link isFainted}
* @returns {boolean} True if pokemon is allowed in battle
* Check if this pokemon is both not fainted and allowed to be in battle based on currently active challenges.
* @returns {boolean} `true` if pokemon is allowed in battle
*/
isAllowedInBattle(): boolean {
return !this.isFainted() && this.isAllowed();
public isAllowedInBattle(): boolean {
return !this.isFainted() && this.isAllowedInChallenge();
}
/**
* Check if this pokemon is allowed (no challenge exclusion)
* This is frequently a better alternative to {@link isFainted}
* @returns {boolean} True if pokemon is allowed in battle
* Check if this pokemon is allowed based on any active challenges.
* It's usually better to call {@linkcode isAllowedInBattle()}
* @returns {boolean} `true` if pokemon is allowed in battle
*/
isAllowed(): boolean {
public isAllowedInChallenge(): boolean {
const challengeAllowed = new Utils.BooleanHolder(true);
applyChallenges(this.scene.gameMode, ChallengeType.POKEMON_IN_BATTLE, this, challengeAllowed);
return challengeAllowed.value;
}
isActive(onField?: boolean): boolean {
/**
* Checks if the pokemon is allowed in battle (ie: not fainted, and allowed under any active challenges).
* @param onField `true` to also check if the pokemon is currently on the field, defaults to `false`
* @returns `true` if the pokemon is "active". Returns `false` if there is no active {@linkcode BattleScene}
*/
public isActive(onField: boolean = false): boolean {
if (!this.scene) {
return false;
}
return this.isAllowedInBattle() && !!this.scene && (!onField || this.isOnField());
return this.isAllowedInBattle() && (!onField || this.isOnField());
}
getDexAttr(): bigint {
@ -428,38 +438,26 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
resolve();
};
if (this.shiny) {
const populateVariantColors = (key: string, back: boolean = false): Promise<void> => {
const populateVariantColors = (isBackSprite: boolean = false): Promise<void> => {
return new Promise(resolve => {
const battleSpritePath = this.getBattleSpriteAtlasPath(back, ignoreOverride).replace("variant/", "").replace(/_[1-3]$/, "");
const battleSpritePath = this.getBattleSpriteAtlasPath(isBackSprite, ignoreOverride).replace("variant/", "").replace(/_[1-3]$/, "");
let config = variantData;
const useExpSprite = this.scene.experimentalSprites && this.scene.hasExpSprite(this.getBattleSpriteKey(back, ignoreOverride));
const useExpSprite = this.scene.experimentalSprites && this.scene.hasExpSprite(this.getBattleSpriteKey(isBackSprite, ignoreOverride));
battleSpritePath.split("/").map(p => config ? config = config[p] : null);
const variantSet: VariantSet = config as VariantSet;
if (variantSet && variantSet[this.variant] === 1) {
if (variantColorCache.hasOwnProperty(key)) {
return resolve();
const cacheKey = this.getBattleSpriteKey(isBackSprite);
if (!variantColorCache.hasOwnProperty(cacheKey)) {
this.populateVariantColorCache(cacheKey, useExpSprite, battleSpritePath);
}
this.scene.cachedFetch(`./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`).
then(res => {
// Prevent the JSON from processing if it failed to load
if (!res.ok) {
console.error(`Could not load ${res.url}!`);
return;
}
return res.json();
}).then(c => {
variantColorCache[key] = c;
resolve();
});
} else {
resolve();
}
});
};
if (this.isPlayer()) {
Promise.all([ populateVariantColors(this.getBattleSpriteKey(false)), populateVariantColors(this.getBattleSpriteKey(true), true) ]).then(() => updateFusionPaletteAndResolve());
Promise.all([ populateVariantColors(false), populateVariantColors(true) ]).then(() => updateFusionPaletteAndResolve());
} else {
populateVariantColors(this.getBattleSpriteKey(false)).then(() => updateFusionPaletteAndResolve());
populateVariantColors(false).then(() => updateFusionPaletteAndResolve());
}
} else {
updateFusionPaletteAndResolve();
@ -472,6 +470,45 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
});
}
/**
* Gracefully handle errors loading a variant sprite. Log if it fails and attempt to fall back on
* non-experimental sprites before giving up.
*
* @param cacheKey the cache key for the variant color sprite
* @param attemptedSpritePath the sprite path that failed to load
* @param useExpSprite was the attempted sprite experimental
* @param battleSpritePath the filename of the sprite
* @param optionalParams any additional params to log
*/
fallbackVariantColor(cacheKey: string, attemptedSpritePath: string, useExpSprite: boolean, battleSpritePath: string, ...optionalParams: any[]) {
console.warn(`Could not load ${attemptedSpritePath}!`, ...optionalParams);
if (useExpSprite) {
this.populateVariantColorCache(cacheKey, false, battleSpritePath);
}
}
/**
* Attempt to process variant sprite.
*
* @param cacheKey the cache key for the variant color sprite
* @param useExpSprite should the experimental sprite be used
* @param battleSpritePath the filename of the sprite
*/
populateVariantColorCache(cacheKey: string, useExpSprite: boolean, battleSpritePath: string) {
const spritePath = `./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`;
this.scene.cachedFetch(spritePath).then(res => {
// Prevent the JSON from processing if it failed to load
if (!res.ok) {
return this.fallbackVariantColor(cacheKey, res.url, useExpSprite, battleSpritePath, res.status, res.statusText);
}
return res.json();
}).catch(error => {
this.fallbackVariantColor(cacheKey, spritePath, useExpSprite, battleSpritePath, error);
}).then(c => {
variantColorCache[cacheKey] = c;
});
}
getFormKey(): string {
if (!this.species.forms.length || this.species.forms.length <= this.formIndex) {
return "";
@ -945,6 +982,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (this.status && this.status.effect === StatusEffect.PARALYSIS) {
ret >>= 1;
}
if (this.getTag(BattlerTagType.UNBURDEN) && !this.scene.getField(true).some(pokemon => pokemon !== this && pokemon.hasAbilityWithAttr(SuppressFieldAbilitiesAbAttr))) {
ret *= 2;
}
break;
}
@ -1159,7 +1199,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @returns an array of {@linkcode Moves}, the length of which is determined
* by how many learnable moves there are for the {@linkcode Pokemon}.
*/
getLearnableLevelMoves(): Moves[] {
public getLearnableLevelMoves(): Moves[] {
let levelMoves = this.getLevelMoves(1, true, false, true).map(lm => lm[1]);
if (this.metBiome === -1 && !this.scene.gameMode.isFreshStartChallenge() && !this.scene.gameMode.isDaily) {
levelMoves = this.getUnlockedEggMoves().concat(levelMoves);
@ -1173,13 +1213,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Gets the types of a pokemon
* @param includeTeraType boolean to include tera-formed type, default false
* @param forDefend boolean if the pokemon is defending from an attack
* @param ignoreOverride boolean if true, ignore ability changing effects
* @param includeTeraType - `true` to include tera-formed type; Default: `false`
* @param forDefend - `true` if the pokemon is defending from an attack; Default: `false`
* @param ignoreOverride - If `true`, ignore ability changing effects; Default: `false`
* @returns array of {@linkcode Type}
*/
getTypes(includeTeraType = false, forDefend: boolean = false, ignoreOverride?: boolean): Type[] {
const types : Type[] = [];
public getTypes(includeTeraType = false, forDefend: boolean = false, ignoreOverride: boolean = false): Type[] {
const types: Type[] = [];
if (includeTeraType) {
const teraType = this.getTeraType();
@ -1244,14 +1284,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
// this.scene potentially can be undefined for a fainted pokemon in doubles
// use optional chaining to avoid runtime errors
if (!types.length) { // become UNKNOWN if no types are present
// become UNKNOWN if no types are present
if (!types.length) {
types.push(Type.UNKNOWN);
}
if (types.length > 1 && types.includes(Type.UNKNOWN)) { // remove UNKNOWN if other types are present
// remove UNKNOWN if other types are present
if (types.length > 1 && types.includes(Type.UNKNOWN)) {
const index = types.indexOf(Type.UNKNOWN);
if (index !== -1) {
types.splice(index, 1);
@ -1271,19 +1310,27 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return types;
}
isOfType(type: Type, includeTeraType: boolean = true, forDefend: boolean = false, ignoreOverride?: boolean): boolean {
return !!this.getTypes(includeTeraType, forDefend, ignoreOverride).some(t => t === type);
/**
* Checks if the pokemon's typing includes the specified type
* @param type - {@linkcode Type} to check
* @param includeTeraType - `true` to include tera-formed type; Default: `true`
* @param forDefend - `true` if the pokemon is defending from an attack; Default: `false`
* @param ignoreOverride - If `true`, ignore ability changing effects; Default: `false`
* @returns `true` if the Pokemon's type matches
*/
public isOfType(type: Type, includeTeraType: boolean = true, forDefend: boolean = false, ignoreOverride: boolean = false): boolean {
return this.getTypes(includeTeraType, forDefend, ignoreOverride).some((t) => t === type);
}
/**
* Gets the non-passive ability of the pokemon. This accounts for fusions and ability changing effects.
* This should rarely be called, most of the time {@link hasAbility} or {@link hasAbilityWithAttr} are better used as
* This should rarely be called, most of the time {@linkcode hasAbility} or {@linkcode hasAbilityWithAttr} are better used as
* those check both the passive and non-passive abilities and account for ability suppression.
* @see {@link hasAbility} {@link hasAbilityWithAttr} Intended ways to check abilities in most cases
* @param {boolean} ignoreOverride If true, ignore ability changing effects
* @returns {Ability} The non-passive ability of the pokemon
* @see {@linkcode hasAbility} {@linkcode hasAbilityWithAttr} Intended ways to check abilities in most cases
* @param ignoreOverride - If `true`, ignore ability changing effects; Default: `false`
* @returns The non-passive {@linkcode Ability} of the pokemon
*/
getAbility(ignoreOverride?: boolean): Ability {
public getAbility(ignoreOverride: boolean = false): Ability {
if (!ignoreOverride && this.summonData?.ability) {
return allAbilities[this.summonData.ability];
}
@ -1312,12 +1359,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Gets the passive ability of the pokemon. This should rarely be called, most of the time
* {@link hasAbility} or {@link hasAbilityWithAttr} are better used as those check both the passive and
* {@linkcode hasAbility} or {@linkcode hasAbilityWithAttr} are better used as those check both the passive and
* non-passive abilities and account for ability suppression.
* @see {@link hasAbility} {@link hasAbilityWithAttr} Intended ways to check abilities in most cases
* @returns {Ability} The passive ability of the pokemon
* @see {@linkcode hasAbility} {@linkcode hasAbilityWithAttr} Intended ways to check abilities in most cases
* @returns The passive {@linkcode Ability} of the pokemon
*/
getPassiveAbility(): Ability {
public getPassiveAbility(): Ability {
if (Overrides.PASSIVE_ABILITY_OVERRIDE && this.isPlayer()) {
return allAbilities[Overrides.PASSIVE_ABILITY_OVERRIDE];
}
@ -1339,12 +1386,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* Gets a list of all instances of a given ability attribute among abilities this pokemon has.
* Accounts for all the various effects which can affect whether an ability will be present or
* in effect, and both passive and non-passive.
* @param attrType {@linkcode AbAttr} The ability attribute to check for.
* @param canApply {@linkcode Boolean} If false, it doesn't check whether the ability is currently active
* @param ignoreOverride {@linkcode Boolean} If true, it ignores ability changing effects
* @returns A list of all the ability attributes on this ability.
* @param attrType - {@linkcode AbAttr} The ability attribute to check for.
* @param canApply - If `false`, it doesn't check whether the ability is currently active; Default `true`
* @param ignoreOverride - If `true`, it ignores ability changing effects; Default `false`
* @returns An array of all the ability attributes on this ability.
*/
getAbilityAttrs<T extends AbAttr = AbAttr>(attrType: { new(...args: any[]): T }, canApply: boolean = true, ignoreOverride?: boolean): T[] {
public getAbilityAttrs<T extends AbAttr = AbAttr>(attrType: { new(...args: any[]): T }, canApply: boolean = true, ignoreOverride: boolean = false): T[] {
const abilityAttrs: T[] = [];
if (!canApply || this.canApplyAbility()) {
@ -1363,12 +1410,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* - bought with starter candy
* - set by override
* - is a boss pokemon
* @returns whether or not a pokemon should have a passive
* @returns `true` if the Pokemon has a passive
*/
hasPassive(): boolean {
public hasPassive(): boolean {
// returns override if valid for current case
if ((Overrides.PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE && this.isPlayer()) ||
(Overrides.OPP_PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE && !this.isPlayer())) {
if ((Overrides.PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE && this.isPlayer())
|| (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE && !this.isPlayer())) {
return true;
}
@ -1387,12 +1434,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Checks whether an ability of a pokemon can be currently applied. This should rarely be
* directly called, as {@link hasAbility} and {@link hasAbilityWithAttr} already call this.
* @see {@link hasAbility} {@link hasAbilityWithAttr} Intended ways to check abilities in most cases
* @param {boolean} passive If true, check if passive can be applied instead of non-passive
* @returns {Ability} The passive ability of the pokemon
* directly called, as {@linkcode hasAbility} and {@linkcode hasAbilityWithAttr} already call this.
* @see {@linkcode hasAbility} {@linkcode hasAbilityWithAttr} Intended ways to check abilities in most cases
* @param passive If true, check if passive can be applied instead of non-passive
* @returns `true` if the ability can be applied
*/
canApplyAbility(passive: boolean = false): boolean {
public canApplyAbility(passive: boolean = false): boolean {
if (passive && !this.hasPassive()) {
return false;
}
@ -1421,7 +1468,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return false;
}
}
return (!!this.hp || ability.isBypassFaint) && !ability.conditions.find(condition => !condition(this));
return (this.hp > 0 || ability.isBypassFaint) && !ability.conditions.find(condition => !condition(this));
}
/**
@ -1433,7 +1480,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param {boolean} ignoreOverride If true, it ignores ability changing effects
* @returns {boolean} Whether the ability is present and active
*/
hasAbility(ability: Abilities, canApply: boolean = true, ignoreOverride?: boolean): boolean {
public hasAbility(ability: Abilities, canApply: boolean = true, ignoreOverride?: boolean): boolean {
if (this.getAbility(ignoreOverride).id === ability && (!canApply || this.canApplyAbility())) {
return true;
}
@ -1453,7 +1500,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param {boolean} ignoreOverride If true, it ignores ability changing effects
* @returns {boolean} Whether an ability with that attribute is present and active
*/
hasAbilityWithAttr(attrType: Constructor<AbAttr>, canApply: boolean = true, ignoreOverride?: boolean): boolean {
public hasAbilityWithAttr(attrType: Constructor<AbAttr>, canApply: boolean = true, ignoreOverride?: boolean): boolean {
if ((!canApply || this.canApplyAbility()) && this.getAbility(ignoreOverride).hasAttr(attrType)) {
return true;
}
@ -1468,7 +1515,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* and then multiplicative modifiers happening after (Heavy Metal and Light Metal)
* @returns the kg of the Pokemon (minimum of 0.1)
*/
getWeight(): number {
public getWeight(): number {
const autotomizedTag = this.getTag(AutotomizedTag);
let weightRemoved = 0;
if (!Utils.isNullOrUndefined(autotomizedTag)) {
@ -1483,10 +1530,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* Gets the tera-formed type of the pokemon, or UNKNOWN if not present
* @returns the {@linkcode Type}
* @returns The tera-formed type of the pokemon, or {@linkcode Type.UNKNOWN} if not present
*/
getTeraType(): Type {
public getTeraType(): Type {
// this.scene can be undefined for a fainted mon in doubles
if (this.scene !== undefined) {
const teraModifier = this.scene.findModifier(m => m instanceof TerastallizeModifier
@ -1500,23 +1546,28 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return Type.UNKNOWN;
}
isTerastallized(): boolean {
public isTerastallized(): boolean {
return this.getTeraType() !== Type.UNKNOWN;
}
isGrounded(): boolean {
public isGrounded(): boolean {
return !!this.getTag(GroundedTag) || (!this.isOfType(Type.FLYING, true, true) && !this.hasAbility(Abilities.LEVITATE) && !this.getTag(BattlerTagType.FLOATING) && !this.getTag(SemiInvulnerableTag));
}
/**
* Determines whether this Pokemon is prevented from running or switching due
* to effects from moves and/or abilities.
* @param trappedAbMessages `string[]` If defined, ability trigger messages
* @param trappedAbMessages - If defined, ability trigger messages
* (e.g. from Shadow Tag) are forwarded through this array.
* @param simulated `boolean` if `true`, applies abilities via simulated calls.
* @returns
* @param simulated - If `true`, applies abilities via simulated calls.
* @returns `true` if the pokemon is trapped
*/
isTrapped(trappedAbMessages: string[] = [], simulated: boolean = true): boolean {
public isTrapped(trappedAbMessages: string[] = [], simulated: boolean = true): boolean {
const commandedTag = this.getTag(BattlerTagType.COMMANDED);
if (commandedTag?.getSourcePokemon(this.scene)?.isActive(true)) {
return true;
}
if (this.isOfType(Type.GHOST)) {
return false;
}
@ -1524,21 +1575,22 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const trappedByAbility = new Utils.BooleanHolder(false);
const opposingField = this.isPlayer() ? this.scene.getEnemyField() : this.scene.getPlayerField();
opposingField.forEach(opponent =>
opposingField.forEach((opponent) =>
applyCheckTrappedAbAttrs(CheckTrappedAbAttr, opponent, trappedByAbility, this, trappedAbMessages, simulated)
);
return (trappedByAbility.value || !!this.getTag(TrappedTag));
const side = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
return (trappedByAbility.value || !!this.getTag(TrappedTag) || !!this.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, side));
}
/**
* Calculates the type of a move when used by this Pokemon after
* type-changing move and ability attributes have applied.
* @param move {@linkcode Move} The move being used.
* @param simulated If `true`, prevents showing abilities applied in this calculation.
* @returns the {@linkcode Type} of the move after attributes are applied
* @param move - {@linkcode Move} The move being used.
* @param simulated - If `true`, prevents showing abilities applied in this calculation.
* @returns The {@linkcode Type} of the move after attributes are applied
*/
getMoveType(move: Move, simulated: boolean = true): Type {
public getMoveType(move: Move, simulated: boolean = true): Type {
const moveTypeHolder = new Utils.NumberHolder(move.type);
applyMoveAttrs(VariableMoveTypeAttr, this, null, move, moveTypeHolder);
@ -1903,13 +1955,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* Function that tries to set a Pokemon shiny based on seed.
* For manual use only, usually to roll a Pokemon's shiny chance a second time.
*
* The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / 65536
* @param thresholdOverride number that is divided by 2^16 (65536) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm)
* The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536`
* @param thresholdOverride number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm)
* @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride}
* @returns true if the Pokemon has been set as a shiny, false otherwise
* @returns `true` if the Pokemon has been set as a shiny, `false` otherwise
*/
trySetShinySeed(thresholdOverride?: integer, applyModifiersToOverride?: boolean): boolean {
const shinyThreshold = new Utils.IntegerHolder(BASE_SHINY_CHANCE);
public trySetShinySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean {
const shinyThreshold = new Utils.NumberHolder(BASE_SHINY_CHANCE);
if (thresholdOverride === undefined || applyModifiersToOverride) {
if (thresholdOverride !== undefined && applyModifiersToOverride) {
shinyThreshold.value = thresholdOverride;
@ -1934,13 +1986,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* Generates a variant
* Has a 10% of returning 2 (epic variant)
* And a 30% of returning 1 (rare variant)
* Returns 0 (basic shiny) if there is no variant or 60% of the time otherwise
* @returns the shiny variant
* Generates a shiny variant
* @returns `0-2`, with the following probabilities:
* - Has a 10% chance of returning `2` (epic variant)
* - Has a 30% chance of returning `1` (rare variant)
* - Has a 60% chance of returning `0` (basic shiny)
*/
generateVariant(): Variant {
protected generateShinyVariant(): Variant {
const formIndex: number = this.formIndex;
let variantDataIndex: string | number = this.species.speciesId;
if (this.species.forms.length > 0) {
@ -1966,8 +2018,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
generateFusionSpecies(forStarter?: boolean): void {
const hiddenAbilityChance = new Utils.IntegerHolder(BASE_HIDDEN_ABILITY_CHANCE);
public generateFusionSpecies(forStarter?: boolean): void {
const hiddenAbilityChance = new Utils.NumberHolder(BASE_HIDDEN_ABILITY_CHANCE);
if (!this.hasTrainer()) {
this.scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance);
}
@ -2016,7 +2068,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.generateName();
}
clearFusionSpecies(): void {
public clearFusionSpecies(): void {
this.fusionSpecies = null;
this.fusionFormIndex = 0;
this.fusionAbilityIndex = 0;
@ -2030,12 +2082,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.calculateStats();
}
generateAndPopulateMoveset(): void {
/** Generates a semi-random moveset for a Pokemon */
public generateAndPopulateMoveset(): void {
this.moveset = [];
let movePool: [Moves, number][] = [];
const allLevelMoves = this.getLevelMoves(1, true, true);
if (!allLevelMoves) {
console.log(this.species.speciesId, "ERROR");
console.warn("Error encountered trying to generate moveset for:", this.species.name);
return;
}
@ -2045,16 +2098,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
break;
}
let weight = levelMove[0];
if (weight === 0) { // Evo Moves
// Evolution Moves
if (weight === 0) {
weight = 50;
}
if (weight === 1 && allMoves[levelMove[1]].power >= 80) { // Assume level 1 moves with 80+ BP are "move reminder" moves and bump their weight
// Assume level 1 moves with 80+ BP are "move reminder" moves and bump their weight
if (weight === 1 && allMoves[levelMove[1]].power >= 80) {
weight = 40;
}
if (allMoves[levelMove[1]].name.endsWith(" (N)")) {
weight /= 100;
} // Unimplemented level up moves are possible to generate, but 1% of their normal chance.
if (!movePool.some(m => m[0] === levelMove[1])) {
if (!movePool.some(m => m[0] === levelMove[1]) && !allMoves[levelMove[1]].name.endsWith(" (N)")) {
movePool.push([ levelMove[1], weight ]);
}
}
@ -2086,7 +2138,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
if (this.level >= 60) { // No egg moves below level 60
// No egg moves below level 60
if (this.level >= 60) {
for (let i = 0; i < 3; i++) {
const moveId = speciesEggMoves[this.species.getRootSpeciesId()][i];
if (!movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) {
@ -2094,7 +2147,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
const moveId = speciesEggMoves[this.species.getRootSpeciesId()][3];
if (this.level >= 170 && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)") && !this.isBoss()) { // No rare egg moves before e4
// No rare egg moves before e4
if (this.level >= 170 && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)") && !this.isBoss()) {
movePool.push([ moveId, 30 ]);
}
if (this.fusionSpecies) {
@ -2105,14 +2159,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][3];
if (this.level >= 170 && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)") && !this.isBoss()) {// No rare egg moves before e4
// No rare egg moves before e4
if (this.level >= 170 && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)") && !this.isBoss()) {
movePool.push([ moveId, 30 ]);
}
}
}
}
if (this.isBoss()) { // Bosses never get self ko moves
// Bosses never get self ko moves
if (this.isBoss()) {
movePool = movePool.filter(m => !allMoves[m[0]].hasAttr(SacrificialAttr));
}
movePool = movePool.filter(m => !allMoves[m[0]].hasAttr(SacrificialAttrOnHit));
@ -2141,7 +2197,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const statRatio = worseCategory === MoveCategory.PHYSICAL ? atk / spAtk : spAtk / atk;
movePool = movePool.map(m => [ m[0], m[1] * (allMoves[m[0]].category === worseCategory ? statRatio : 1) ]);
let weightMultiplier = 0.9; // The higher this is the more the game weights towards higher level moves. At 0 all moves are equal weight.
/** The higher this is the more the game weights towards higher level moves. At `0` all moves are equal weight. */
let weightMultiplier = 0.9;
if (this.hasTrainer()) {
weightMultiplier += 0.7;
}
@ -2150,7 +2207,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
const baseWeights: [Moves, number][] = movePool.map(m => [ m[0], Math.ceil(Math.pow(m[1], weightMultiplier) * 100) ]);
if (this.hasTrainer() || this.isBoss()) { // Trainers and bosses always force a stab move
// Trainers and bosses always force a stab move
if (this.hasTrainer() || this.isBoss()) {
const stabMovePool = baseWeights.filter(m => allMoves[m[0]].category !== MoveCategory.STATUS && this.isOfType(allMoves[m[0]].type));
if (stabMovePool.length) {
@ -2180,8 +2238,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
// Sqrt the weight of any damaging moves with overlapping types. This is about a 0.05 - 0.1 multiplier.
// Other damaging moves 2x weight if 0-1 damaging moves, 0.5x if 2, 0.125x if 3. These weights double if STAB.
// Status moves remain unchanged on weight, this encourages 1-2
movePool = baseWeights.filter(m => !this.moveset.some(mo => m[0] === mo?.moveId)).map(m => [ m[0], this.moveset.some(mo => mo?.getMove().category !== MoveCategory.STATUS && mo?.getMove().type === allMoves[m[0]].type) ? Math.ceil(Math.sqrt(m[1])) : allMoves[m[0]].category !== MoveCategory.STATUS ? Math.ceil(m[1] / Math.max(Math.pow(4, this.moveset.filter(mo => (mo?.getMove().power!) > 1).length) / 8, 0.5) * (this.isOfType(allMoves[m[0]].type) ? 2 : 1)) : m[1] ]); // TODO: is this bang correct?
} else { // Non-trainer pokemon just use normal weights
movePool = baseWeights.filter(m => !this.moveset.some(mo => m[0] === mo?.moveId)).map((m) => {
let ret: number;
if (this.moveset.some(mo => mo?.getMove().category !== MoveCategory.STATUS && mo?.getMove().type === allMoves[m[0]].type)) {
ret = Math.ceil(Math.sqrt(m[1]));
} else if (allMoves[m[0]].category !== MoveCategory.STATUS) {
ret = Math.ceil(m[1] / Math.max(Math.pow(4, this.moveset.filter(mo => (mo?.getMove().power ?? 0) > 1).length) / 8, 0.5) * (this.isOfType(allMoves[m[0]].type) ? 2 : 1));
} else {
ret = m[1];
}
return [ m[0], ret ];
});
} else {
// Non-trainer pokemon just use normal weights
movePool = baseWeights.filter(m => !this.moveset.some(mo => m[0] === mo?.moveId));
}
const totalWeight = movePool.reduce((v, m) => v + m[1], 0);
@ -2199,11 +2268,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
trySelectMove(moveIndex: integer, ignorePp?: boolean): boolean {
public trySelectMove(moveIndex: integer, ignorePp?: boolean): boolean {
const move = this.getMoveset().length > moveIndex
? this.getMoveset()[moveIndex]
: null;
return move?.isUsable(this, ignorePp)!; // TODO: is this bang correct?
return move?.isUsable(this, ignorePp) ?? false;
}
showInfo(): void {
@ -2772,6 +2841,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
// In case of fatal damage, this tag would have gotten cleared before we could lapse it.
const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND);
const grudgeTag = this.getTag(BattlerTagType.GRUDGE);
const isOneHitKo = result === HitResult.ONE_HIT_KO;
@ -2791,7 +2861,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* We explicitly require to ignore the faint phase here, as we want to show the messages
* about the critical hit and the super effective/not very effective messages before the faint phase.
*/
const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true);
const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true, source);
if (damage > 0) {
if (source.isPlayer()) {
@ -2800,10 +2870,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.scene.gameData.gameStats.highestDamage = damage;
}
}
source.turnData.damageDealt += damage;
source.turnData.currDamageDealt = damage;
source.turnData.totalDamageDealt += damage;
source.turnData.singleHitDamageDealt = damage;
this.turnData.damageTaken += damage;
this.battleData.hitCount++;
// Multi-Lens and Parental Bond check for Wimp Out/Emergency Exit
if (this.hasAbilityWithAttr(PostDamageForceSwitchAbAttr)) {
const multiHitModifier = source.getHeldItems().find(m => m instanceof PokemonMultiHitModifier);
if (multiHitModifier || source.hasAbilityWithAttr(AddSecondStrikeAbAttr)) {
applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source);
}
}
const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() };
this.turnData.attacksReceived.unshift(attackResult);
if (source.isPlayer() && !this.isPlayer()) {
@ -2834,13 +2913,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (this.isFainted()) {
// set splice index here, so future scene queues happen before FaintedPhase
this.scene.setPhaseQueueSplice();
if (!isNullOrUndefined(destinyTag) && dmg) {
// Destiny Bond will activate during FaintPhase
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo, destinyTag, source));
} else {
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo));
}
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo, destinyTag, grudgeTag, source));
this.destroySubstitute();
this.lapseTag(BattlerTagType.COMMANDED);
this.resetSummonData();
}
@ -2889,9 +2965,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.scene.setPhaseQueueSplice();
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), preventEndure));
this.destroySubstitute();
this.lapseTag(BattlerTagType.COMMANDED);
this.resetSummonData();
}
return damage;
}
@ -2905,12 +2981,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage()
* @returns integer of damage done
*/
damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false): integer {
damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false, source?: Pokemon): integer {
const damagePhase = new DamagePhase(this.scene, this.getBattlerIndex(), damage, result as DamageResult, critical);
this.scene.unshiftPhase(damagePhase);
damage = this.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase);
// Damage amount may have changed, but needed to be queued before calling damage function
damagePhase.updateAmount(damage);
applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source);
return damage;
}
@ -2971,19 +3048,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
/** @overload */
getTag(tagType: BattlerTagType): BattlerTag | null;
getTag(tagType: BattlerTagType): BattlerTag | nil;
/** @overload */
getTag<T extends BattlerTag>(tagType: Constructor<T>): T | null;
getTag<T extends BattlerTag>(tagType: Constructor<T>): T | nil;
getTag(tagType: BattlerTagType | Constructor<BattlerTag>): BattlerTag | null {
getTag(tagType: BattlerTagType | Constructor<BattlerTag>): BattlerTag | nil {
if (!this.summonData) {
return null;
}
return (tagType instanceof Function
? this.summonData.tags.find(t => t instanceof tagType)
: this.summonData.tags.find(t => t.tagType === tagType)
)!; // TODO: is this bang correct?
);
}
findTag(tagFilter: ((tag: BattlerTag) => boolean)) {
@ -3193,7 +3270,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
try {
SoundFade.fadeOut(scene, cry, Utils.fixedInt(Math.ceil(duration * 0.2)));
fusionCry = this.getFusionSpeciesForm().cry(scene, Object.assign({ seek: Math.max(fusionCry.totalDuration * 0.4, 0) }, soundConfig));
SoundFade.fadeIn(scene, fusionCry, Utils.fixedInt(Math.ceil(duration * 0.2)), scene.masterVolume * scene.seVolume, 0);
SoundFade.fadeIn(scene, fusionCry, Utils.fixedInt(Math.ceil(duration * 0.2)), scene.masterVolume * scene.fieldVolume, 0);
} catch (err) {
console.error(err);
}
@ -3208,11 +3285,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return this.fusionFaintCry(callback);
}
const key = `cry/${this.species.getCryKey(this.formIndex)}`;
//eslint-disable-next-line @typescript-eslint/no-unused-vars
let i = 0;
const key = this.species.getCryKey(this.formIndex);
let rate = 0.85;
const cry = this.scene.playSound(key, { rate: rate }) as AnySound;
if (!cry || this.scene.fieldVolume === 0) {
return callback();
}
const sprite = this.getSprite();
const tintSprite = this.getTintSprite();
const delay = Math.max(this.scene.sound.get(key).totalDuration * 50, 25);
@ -3227,7 +3305,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
delay: Utils.fixedInt(delay),
repeat: -1,
callback: () => {
++i;
frameThreshold = sprite.anims.msPerFrame / rate;
frameProgress += delay;
while (frameProgress > frameThreshold) {
@ -3266,7 +3343,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
private fusionFaintCry(callback: Function): void {
const key = `cry/${this.species.getCryKey(this.formIndex)}`;
const key = this.species.getCryKey(this.formIndex);
let i = 0;
let rate = 0.85;
const cry = this.scene.playSound(key, { rate: rate }) as AnySound;
@ -3274,12 +3351,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const tintSprite = this.getTintSprite();
let duration = cry.totalDuration * 1000;
const fusionCryKey = `cry/${this.fusionSpecies?.getCryKey(this.fusionFormIndex)}`;
const fusionCryKey = this.fusionSpecies!.getCryKey(this.fusionFormIndex);
let fusionCry = this.scene.playSound(fusionCryKey, { rate: rate }) as AnySound;
if (!cry || !fusionCry || this.scene.fieldVolume === 0) {
return callback();
}
fusionCry.stop();
duration = Math.min(duration, fusionCry.totalDuration * 1000);
fusionCry.destroy();
const delay = Math.max(duration * 0.05, 25);
let transitionIndex = 0;
@ -3317,10 +3396,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
frameProgress -= frameThreshold;
}
if (i === transitionIndex) {
if (i === transitionIndex && fusionCryKey) {
SoundFade.fadeOut(this.scene, cry, Utils.fixedInt(Math.ceil((duration / rate) * 0.2)));
fusionCry = this.scene.playSound(fusionCryKey, Object.assign({ seek: Math.max(fusionCry.totalDuration * 0.4, 0), rate: rate }));
SoundFade.fadeIn(this.scene, fusionCry, Utils.fixedInt(Math.ceil((duration / rate) * 0.2)), this.scene.masterVolume * this.scene.seVolume, 0);
SoundFade.fadeIn(this.scene, fusionCry, Utils.fixedInt(Math.ceil((duration / rate) * 0.2)), this.scene.masterVolume * this.scene.fieldVolume, 0);
}
rate *= 0.99;
if (cry && !cry.pendingRemove) {
@ -4162,7 +4241,7 @@ export class PlayerPokemon extends Pokemon {
return new Promise(resolve => {
this.scene.ui.setMode(Mode.PARTY, PartyUiMode.REVIVAL_BLESSING, this.getFieldIndex(), (slotIndex:integer, option: PartyOption) => {
if (slotIndex >= 0 && slotIndex < 6) {
const pokemon = this.scene.getParty()[slotIndex];
const pokemon = this.scene.getPlayerParty()[slotIndex];
if (!pokemon || !pokemon.isFainted()) {
resolve();
}
@ -4172,7 +4251,7 @@ export class PlayerPokemon extends Pokemon {
pokemon.heal(Math.min(Utils.toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp()));
this.scene.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: pokemon.name }), 0, true);
if (this.scene.currentBattle.double && this.scene.getParty().length > 1) {
if (this.scene.currentBattle.double && this.scene.getPlayerParty().length > 1) {
const allyPokemon = this.getAlly();
if (slotIndex <= 1) {
// Revived ally pokemon
@ -4314,7 +4393,7 @@ export class PlayerPokemon extends Pokemon {
newPokemon.fusionLuck = this.fusionLuck;
newPokemon.usedTMs = this.usedTMs;
this.scene.getParty().push(newPokemon);
this.scene.getPlayerParty().push(newPokemon);
newPokemon.evolve((!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution)), evoSpecies);
const modifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier
&& m.pokemonId === this.id, true) as PokemonHeldItemModifier[];
@ -4430,8 +4509,8 @@ export class PlayerPokemon extends Pokemon {
this.generateCompatibleTms();
this.updateInfo(true);
const fusedPartyMemberIndex = this.scene.getParty().indexOf(pokemon);
let partyMemberIndex = this.scene.getParty().indexOf(this);
const fusedPartyMemberIndex = this.scene.getPlayerParty().indexOf(pokemon);
let partyMemberIndex = this.scene.getPlayerParty().indexOf(this);
if (partyMemberIndex > fusedPartyMemberIndex) {
partyMemberIndex--;
}
@ -4444,8 +4523,8 @@ export class PlayerPokemon extends Pokemon {
Promise.allSettled(transferModifiers).then(() => {
this.scene.updateModifiers(true, true).then(() => {
this.scene.removePartyMemberModifiers(fusedPartyMemberIndex);
this.scene.getParty().splice(fusedPartyMemberIndex, 1)[0];
const newPartyMemberIndex = this.scene.getParty().indexOf(this);
this.scene.getPlayerParty().splice(fusedPartyMemberIndex, 1)[0];
const newPartyMemberIndex = this.scene.getPlayerParty().indexOf(this);
pokemon.getMoveset(true).map((m: PokemonMove) => this.scene.unshiftPhase(new LearnMovePhase(this.scene, newPartyMemberIndex, m.getMove().id)));
pokemon.destroy();
this.updateFusionPalette();
@ -4526,7 +4605,7 @@ export class EnemyPokemon extends Pokemon {
}
if (this.shiny) {
this.variant = this.generateVariant();
this.variant = this.generateShinyVariant();
if (Overrides.OPP_VARIANT_OVERRIDE !== null) {
this.variant = Overrides.OPP_VARIANT_OVERRIDE;
}
@ -5000,26 +5079,6 @@ export class EnemyPokemon extends Pokemon {
}
}
heal(amount: integer): integer {
if (this.isBoss()) {
const amountRatio = amount / this.getMaxHp();
const segmentBypassCount = Math.floor(amountRatio / (1 / this.bossSegments));
const segmentSize = this.getMaxHp() / this.bossSegments;
for (let s = 1; s < this.bossSegments; s++) {
const hpThreshold = segmentSize * s;
if (this.hp <= Math.round(hpThreshold)) {
const healAmount = Math.min(amount, this.getMaxHp() - this.hp, Math.round(hpThreshold + (segmentSize * segmentBypassCount) - this.hp));
this.hp += healAmount;
return healAmount;
} else if (s >= this.bossSegmentIndex) {
return super.heal(amount);
}
}
}
return super.heal(amount);
}
getFieldIndex(): integer {
return this.scene.getEnemyField().indexOf(this);
}
@ -5036,7 +5095,7 @@ export class EnemyPokemon extends Pokemon {
* @returns the pokemon that was added or null if the pokemon could not be added
*/
addToParty(pokeballType: PokeballType, slotIndex: number = -1) {
const party = this.scene.getParty();
const party = this.scene.getPlayerParty();
let ret: PlayerPokemon | null = null;
if (party.length < PLAYER_PARTY_MAX_SIZE) {
@ -5094,7 +5153,6 @@ export class PokemonSummonData {
public tags: BattlerTag[] = [];
public abilitySuppressed: boolean = false;
public abilitiesApplied: Abilities[] = [];
public speciesForm: PokemonSpeciesForm | null;
public fusionSpeciesForm: PokemonSpeciesForm;
public ability: Abilities = Abilities.NONE;
@ -5134,8 +5192,8 @@ export class PokemonTurnData {
* - `0` = Move is finished
*/
public hitsLeft: number = -1;
public damageDealt: number = 0;
public currDamageDealt: number = 0;
public totalDamageDealt: number = 0;
public singleHitDamageDealt: number = 0;
public damageTaken: number = 0;
public attacksReceived: AttackMoveResult[] = [];
public order: number;
@ -5145,6 +5203,7 @@ export class PokemonTurnData {
public combiningPledge?: Moves;
public switchedInThisTurn: boolean = false;
public failedRunAway: boolean = false;
public joinedRound: boolean = false;
}
export enum AiType {

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>
const lang = i18next.resolvedLanguage;
if (lang !== "en") {
if (Utils.verifyLang(lang)) {
if (Utils.hasAllLocalizedSprites(lang)) {
this.loadAtlas(`statuses_${lang}`, "");
this.loadAtlas(`types_${lang}`, "");
} else {

View File

@ -31,6 +31,7 @@ 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 { Color, ShadowColor } from "#enums/color";
import { FRIENDSHIP_GAIN_FROM_RARE_CANDY } from "#app/data/balance/starters";
import { applyAbAttrs, CommanderAbAttr } from "#app/data/ability";
export type ModifierPredicate = (modifier: Modifier) => boolean;
@ -746,7 +747,7 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier {
return 0;
}
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);
}
@ -1767,10 +1768,10 @@ export class HitHealModifier extends PokemonHeldItemModifier {
* @returns `true` if the {@linkcode Pokemon} was healed
*/
override apply(pokemon: Pokemon): boolean {
if (pokemon.turnData.damageDealt && !pokemon.isFullHp()) {
if (pokemon.turnData.totalDamageDealt && !pokemon.isFullHp()) {
const scene = pokemon.scene;
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;
@ -1937,10 +1938,16 @@ export class PokemonInstantReviveModifier extends PokemonHeldItemModifier {
* @returns always `true`
*/
override apply(pokemon: Pokemon): boolean {
// Restore the Pokemon to half HP
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));
// Remove the Pokemon's FAINT status
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;
}
@ -2022,7 +2029,7 @@ export abstract class ConsumablePokemonModifier extends ConsumableModifier {
abstract override apply(playerPokemon: PlayerPokemon, ...args: unknown[]): boolean | Promise<boolean>;
getPokemon(scene: BattleScene) {
return scene.getParty().find(p => p.id === this.pokemonId);
return scene.getPlayerParty().find(p => p.id === this.pokemonId);
}
}
@ -2224,7 +2231,7 @@ export class PokemonLevelIncrementModifier extends ConsumablePokemonModifier {
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;
}
@ -2244,7 +2251,7 @@ export class TmModifier extends ConsumablePokemonModifier {
*/
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;
}
@ -2266,7 +2273,7 @@ export class RememberMoveModifier extends ConsumablePokemonModifier {
*/
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;
}
@ -2783,7 +2790,7 @@ export class MoneyRewardModifier extends ConsumableModifier {
battleScene.addMoney(moneyAmount.value);
battleScene.getParty().map(p => {
battleScene.getPlayerParty().map(p => {
if (p.species?.speciesId === Species.GIMMIGHOUL || p.fusionSpecies?.speciesId === Species.GIMMIGHOUL) {
p.evoCounter ? p.evoCounter++ : p.evoCounter = 1;
const modifier = getModifierType(modifierTypes.EVOLUTION_TRACKER_GIMMIGHOUL).newModifier(p) as EvoTrackerModifier;

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 { Biome } from "#enums/biome";
import { EggTier } from "#enums/egg-type";
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 { Species } from "#enums/species";
import { StatusEffect } from "#enums/status-effect";
import { TimeOfDay } from "#enums/time-of-day";
import { VariantTier } from "#enums/variant-tier";
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

View File

@ -1,21 +1,22 @@
import BattleScene from "#app/battle-scene";
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 { PokeballType } from "#app/enums/pokeball";
import { StatusEffect } from "#app/enums/status-effect";
import { addPokeballOpenParticles, addPokeballCaptureStars } from "#app/field/anims";
import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims";
import { EnemyPokemon } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
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 { 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 { Mode } from "#app/ui/ui";
import { PokeballType } from "#enums/pokeball";
import { StatusEffect } from "#enums/status-effect";
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 {
private pokeballType: PokeballType;
@ -51,8 +52,10 @@ export class AttemptCapturePhase extends PokemonPhase {
const catchRate = pokemon.species.catchRate;
const pokeballMultiplier = getPokeballCatchMultiplier(this.pokeballType);
const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1;
const x = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier);
const y = Math.round(65536 / Math.sqrt(Math.sqrt(255 / x)));
const modifiedCatchRate = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier);
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 pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType);
@ -60,17 +63,19 @@ export class AttemptCapturePhase extends PokemonPhase {
this.pokeball.setOrigin(0.5, 0.625);
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.field.moveBelow(this.pokeball as Phaser.GameObjects.GameObject, pokemon);
});
this.scene.tweens.add({
// Throw animation
targets: this.pokeball,
x: { value: 236 + fpOffset[0], ease: "Linear" },
y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" },
duration: 500,
onComplete: () => {
// Ball opens
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`));
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);
this.scene.tweens.add({
// Mon enters ball
targets: pokemon,
duration: 500,
ease: "Sine.easeIn",
scale: 0.25,
y: 20,
onComplete: () => {
// Ball closes
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
pokemon.setVisible(false);
this.scene.playSound("se/pb_catch");
this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}`));
const doShake = () => {
// After the overall catch rate check, the game does 3 shake checks before confirming the catch.
let shakeCount = 0;
const pbX = this.pokeball.x;
const shakeCounter = this.scene.tweens.addCounter({
from: 0,
to: 1,
repeat: 4,
repeat: isCritical ? 2 : 4, // Critical captures only perform 1 shake check
yoyo: true,
ease: "Cubic.easeOut",
duration: 250,
repeatDelay: 500,
onUpdate: t => {
if (shakeCount && shakeCount < 4) {
if (shakeCount && shakeCount < (isCritical ? 2 : 4)) {
const value = t.getValue();
const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1;
this.pokeball.setX(pbX + value * 4 * directionMultiplier);
@ -113,13 +121,18 @@ export class AttemptCapturePhase extends PokemonPhase {
if (!pokemon.species.isObtainable()) {
shakeCounter.stop();
this.failCatch(shakeCount);
} else if (shakeCount++ < 3) {
if (pokeballMultiplier === -1 || pokemon.randSeedInt(65536) < y) {
} else if (shakeCount++ < (isCritical ? 1 : 3)) {
// 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");
} else {
shakeCounter.stop();
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 {
this.scene.playSound("se/pb_lock");
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 newPokemon = pokemon.addToParty(this.pokeballType, slotIndex);
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);
}
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(() => {
if (this.scene.getParty().length === 6) {
if (this.scene.getPlayerParty().length === PLAYER_PARTY_MAX_SIZE) {
const promptRelease = () => {
this.scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => {
this.scene.pokemonInfoContainer.makeRoomForConfirmUi(1, true);

View File

@ -1,8 +1,8 @@
import BattleScene from "#app/battle-scene";
import { applyPostBattleAbAttrs, PostBattleAbAttr } from "#app/data/ability";
import { LapsingPersistentModifier, LapsingPokemonHeldItemModifier } from "#app/modifier/modifier";
import { BattlePhase } from "./battle-phase";
import { GameOverPhase } from "./game-over-phase";
import BattleScene from "#app/battle-scene";
export class BattleEndPhase extends BattlePhase {
/** If true, will increment battles won */
@ -41,7 +41,7 @@ export class BattleEndPhase extends BattlePhase {
}
}
for (const pokemon of this.scene.getParty().filter(p => p.isAllowedInBattle())) {
for (const pokemon of this.scene.getPokemonAllowedInBattle()) {
applyPostBattleAbAttrs(PostBattleAbAttr, pokemon);
}

View File

@ -37,7 +37,7 @@ export class CheckSwitchPhase extends BattlePhase {
return;
}
if (!this.scene.getParty().slice(1).filter(p => p.isActive()).length) {
if (!this.scene.getPlayerParty().slice(1).filter(p => p.isActive()).length) {
super.end();
return;
}

View File

@ -17,6 +17,8 @@ import { FieldPhase } from "./field-phase";
import { SelectTargetPhase } from "./select-target-phase";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { isNullOrUndefined } from "#app/utils";
import { ArenaTagSide } from "#app/data/arena-tag";
import { ArenaTagType } from "#app/enums/arena-tag-type";
export class CommandPhase extends FieldPhase {
protected fieldIndex: integer;
@ -30,6 +32,8 @@ export class CommandPhase extends FieldPhase {
start() {
super.start();
this.scene.updateGameInfo();
const commandUiHandler = this.scene.ui.handlers[Mode.COMMAND];
if (commandUiHandler) {
if (this.scene.currentBattle.turn === 1 || commandUiHandler.getCursor() === Command.POKEMON) {
@ -52,6 +56,11 @@ export class CommandPhase extends FieldPhase {
}
}
// If the Pokemon has applied Commander's effects to its ally, skip this command
if (this.scene.currentBattle?.double && this.getPokemon().getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon(this.scene) === this.getPokemon()) {
this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: Command.FIGHT, move: { move: Moves.NONE, targets: []}, skip: true };
}
if (this.scene.currentBattle.turnCommands[this.fieldIndex]?.skip) {
return this.end();
}
@ -90,7 +99,7 @@ export class CommandPhase extends FieldPhase {
handleCommand(command: Command, cursor: integer, ...args: any[]): boolean {
const playerPokemon = this.scene.getPlayerField()[this.fieldIndex];
let success: boolean;
let success: boolean = false;
switch (command) {
case Command.FIGHT:
@ -228,23 +237,21 @@ export class CommandPhase extends FieldPhase {
}, null, true);
} else {
const trapTag = playerPokemon.getTag(TrappedTag);
const fairyLockTag = playerPokemon.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER);
// trapTag should be defined at this point, but just in case...
if (!trapTag) {
currentBattle.turnCommands[this.fieldIndex] = isSwitch
? { command: Command.POKEMON, cursor: cursor, args: args }
: { command: Command.RUN };
if (!trapTag && !fairyLockTag) {
i18next.t(`battle:noEscape${isSwitch ? "Switch" : "Flee"}`);
break;
}
if (!isSwitch) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
this.scene.ui.setMode(Mode.MESSAGE);
}
const showNoEscapeText = (tag: any) => {
this.scene.ui.showText(
i18next.t("battle:noEscapePokemon", {
pokemonName: trapTag.sourceId && this.scene.getPokemonById(trapTag.sourceId) ? getPokemonNameWithAffix(this.scene.getPokemonById(trapTag.sourceId)!) : "",
moveName: trapTag.getMoveName(),
pokemonName: tag.sourceId && this.scene.getPokemonById(tag.sourceId) ? getPokemonNameWithAffix(this.scene.getPokemonById(tag.sourceId)!) : "",
moveName: tag.getMoveName(),
escapeVerb: isSwitch ? i18next.t("battle:escapeVerbSwitch") : i18next.t("battle:escapeVerbFlee")
}),
null,
@ -253,17 +260,27 @@ export class CommandPhase extends FieldPhase {
if (!isSwitch) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}
}, null, true);
},
null,
true
);
};
if (trapTag) {
showNoEscapeText(trapTag);
} else if (fairyLockTag) {
showNoEscapeText(fairyLockTag);
}
}
}
break;
}
if (success!) { // TODO: is the bang correct?
if (success) {
this.end();
}
return success!; // TODO: is the bang correct?
return success;
}
cancel() {

View File

@ -1,41 +1,42 @@
import BattleScene from "#app/battle-scene";
import { BattlerIndex, BattleType } from "#app/battle";
import BattleScene from "#app/battle-scene";
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
import { applyAbAttrs, SyncEncounterNatureAbAttr } from "#app/data/ability";
import { initEncounterAnims, loadEncounterAnimAssets } from "#app/data/battle-anims";
import { getCharVariantFromDialogue } from "#app/data/dialogue";
import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { doTrainerExclamation } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { getGoldenBugNetSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { TrainerSlot } from "#app/data/trainer-config";
import { getRandomWeatherType } from "#app/data/weather";
import { BattleSpec } from "#app/enums/battle-spec";
import { PlayerGender } from "#app/enums/player-gender";
import { Species } from "#app/enums/species";
import { EncounterPhaseEvent } from "#app/events/battle-scene";
import Pokemon, { FieldPosition } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { ModifierPoolType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type";
import { BoostBugSpawnModifier, IvScannerModifier, TurnHeldItemTransferModifier } from "#app/modifier/modifier";
import { ModifierPoolType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type";
import Overrides from "#app/overrides";
import { BattlePhase } from "#app/phases/battle-phase";
import { CheckSwitchPhase } from "#app/phases/check-switch-phase";
import { GameOverPhase } from "#app/phases/game-over-phase";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
import { PostSummonPhase } from "#app/phases/post-summon-phase";
import { ReturnPhase } from "#app/phases/return-phase";
import { ScanIvsPhase } from "#app/phases/scan-ivs-phase";
import { ShinySparklePhase } from "#app/phases/shiny-sparkle-phase";
import { SummonPhase } from "#app/phases/summon-phase";
import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase";
import { achvs } from "#app/system/achv";
import { handleTutorial, Tutorial } from "#app/tutorial";
import { Mode } from "#app/ui/ui";
import i18next from "i18next";
import { BattlePhase } from "./battle-phase";
import * as Utils from "#app/utils";
import { randSeedInt } from "#app/utils";
import { CheckSwitchPhase } from "./check-switch-phase";
import { GameOverPhase } from "./game-over-phase";
import { PostSummonPhase } from "./post-summon-phase";
import { ReturnPhase } from "./return-phase";
import { ScanIvsPhase } from "./scan-ivs-phase";
import { ShinySparklePhase } from "./shiny-sparkle-phase";
import { SummonPhase } from "./summon-phase";
import { ToggleDoublePositionPhase } from "./toggle-double-position-phase";
import Overrides from "#app/overrides";
import { initEncounterAnims, loadEncounterAnimAssets } from "#app/data/battle-anims";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { doTrainerExclamation } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
import { getGoldenBugNetSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { randSeedInt, randSeedItem } from "#app/utils";
import { BattleSpec } from "#enums/battle-spec";
import { Biome } from "#enums/biome";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { PlayerGender } from "#enums/player-gender";
import { Species } from "#enums/species";
import i18next from "i18next";
import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters";
import { BattlerTagType } from "#enums/battler-tag-type";
import { applyChallenges, ChallengeType } from "#app/data/challenge";
export class EncounterPhase extends BattlePhase {
@ -117,7 +118,7 @@ export class EncounterPhase extends BattlePhase {
if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) {
battle.enemyParty[e].ivs = new Array(6).fill(31);
}
this.scene.getParty().slice(0, !battle.double ? 1 : 2).reverse().forEach(playerPokemon => {
this.scene.getPlayerParty().slice(0, !battle.double ? 1 : 2).reverse().forEach(playerPokemon => {
applyAbAttrs(SyncEncounterNatureAbAttr, playerPokemon, null, false, battle.enemyParty[e]);
});
}
@ -157,7 +158,7 @@ export class EncounterPhase extends BattlePhase {
return true;
});
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);
}
@ -249,7 +250,7 @@ export class EncounterPhase extends BattlePhase {
/*if (startingWave > 10) {
for (let m = 0; m < Math.min(Math.floor(startingWave / 10), 99); m++)
this.scene.addModifier(getPlayerModifierTypeOptionsForWave((m + 1) * 10, 1, this.scene.getParty())[0].type.newModifier(), true);
this.scene.addModifier(getPlayerModifierTypeOptionsForWave((m + 1) * 10, 1, this.scene.getPlayerParty())[0].type.newModifier(), true);
this.scene.updateModifiers(true);
}*/
@ -260,7 +261,7 @@ export class EncounterPhase extends BattlePhase {
this.scene.mysteryEncounterSaveData.encounterSpawnChance += WEIGHT_INCREMENT_ON_SPAWN_MISS;
}
for (const pokemon of this.scene.getParty()) {
for (const pokemon of this.scene.getPlayerParty()) {
if (pokemon) {
pokemon.resetBattleData();
}
@ -339,7 +340,7 @@ export class EncounterPhase extends BattlePhase {
const doSummon = () => {
this.scene.currentBattle.started = true;
this.scene.playBgm(undefined);
this.scene.pbTray.showPbTray(this.scene.getParty());
this.scene.pbTray.showPbTray(this.scene.getPlayerParty());
this.scene.pbTrayEnemy.showPbTray(this.scene.getEnemyParty());
const doTrainerSummon = () => {
this.hideEnemyTrainer();
@ -363,7 +364,7 @@ export class EncounterPhase extends BattlePhase {
doSummon();
} else {
let message: string;
this.scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.waveIndex);
this.scene.executeWithSeedOffset(() => message = randSeedItem(encounterMessages), this.scene.currentBattle.waveIndex);
message = message!; // tell TS compiler it's defined now
const showDialogueAndSummon = () => {
this.scene.ui.showDialogue(message, trainer?.getName(TrainerSlot.NONE, true), null, () => {
@ -449,13 +450,13 @@ export class EncounterPhase extends BattlePhase {
if (![ BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER ].includes(this.scene.currentBattle.battleType)) {
enemyField.map(p => this.scene.pushConditionalPhase(new PostSummonPhase(this.scene, p.getBattlerIndex()), () => {
// if there is not a player party, we can't continue
if (!this.scene.getParty()?.length) {
if (!this.scene.getPlayerParty().length) {
return false;
}
// how many player pokemon are on the field ?
const pokemonsOnFieldCount = this.scene.getParty().filter(p => p.isOnField()).length;
const pokemonsOnFieldCount = this.scene.getPlayerParty().filter(p => p.isOnField()).length;
// if it's a 2vs1, there will never be a 2nd pokemon on our field even
const requiredPokemonsOnField = Math.min(this.scene.getParty().filter((p) => !p.isFainted()).length, 2);
const requiredPokemonsOnField = Math.min(this.scene.getPlayerParty().filter((p) => !p.isFainted()).length, 2);
// if it's a double, there should be 2, otherwise 1
if (this.scene.currentBattle.double) {
return pokemonsOnFieldCount === requiredPokemonsOnField;
@ -469,7 +470,7 @@ export class EncounterPhase extends BattlePhase {
}
if (!this.loaded) {
const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle());
const availablePartyMembers = this.scene.getPokemonAllowedInBattle();
if (!availablePartyMembers[0].isOnField()) {
this.scene.pushPhase(new SummonPhase(this.scene, 0));
@ -484,6 +485,7 @@ export class EncounterPhase extends BattlePhase {
}
} else {
if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) {
this.scene.getPlayerField().forEach((pokemon) => pokemon.lapseTag(BattlerTagType.COMMANDED));
this.scene.pushPhase(new ReturnPhase(this.scene, 1));
}
this.scene.pushPhase(new ToggleDoublePositionPhase(this.scene, false));

View File

@ -2,6 +2,8 @@ import BattleScene from "#app/battle-scene";
import { BattlerIndex } from "#app/battle";
import { Command } from "#app/ui/command-ui-handler";
import { FieldPhase } from "./field-phase";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
/**
* Phase for determining an enemy AI's action for the next turn.
@ -34,6 +36,11 @@ export class EnemyCommandPhase extends FieldPhase {
const trainer = battle.trainer;
if (battle.double && enemyPokemon.hasAbility(Abilities.COMMANDER)
&& enemyPokemon.getAlly().getTag(BattlerTagType.COMMANDED)) {
this.skipTurn = true;
}
/**
* If the enemy has a trainer, decide whether or not the enemy should switch
* to another member in its party.

View File

@ -1,12 +1,12 @@
import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
import { Phase } from "#app/phase";
import BattleScene from "#app/battle-scene";
import BattleScene, { AnySound } from "#app/battle-scene";
import { SpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions";
import EvolutionSceneHandler from "#app/ui/evolution-scene-handler";
import * as Utils from "#app/utils";
import { Mode } from "#app/ui/ui";
import { cos, sin } from "#app/field/anims";
import { PlayerPokemon } from "#app/field/pokemon";
import Pokemon, { PlayerPokemon } from "#app/field/pokemon";
import { getTypeRgb } from "#app/data/type";
import i18next from "i18next";
import { getPokemonNameWithAffix } from "#app/messages";
@ -17,7 +17,11 @@ export class EvolutionPhase extends Phase {
protected pokemon: PlayerPokemon;
protected lastLevel: integer;
private preEvolvedPokemonName: string;
private evolution: SpeciesFormEvolution | null;
private evolutionBgm: AnySound;
private evolutionHandler: EvolutionSceneHandler;
protected evolutionContainer: Phaser.GameObjects.Container;
protected evolutionBaseBg: Phaser.GameObjects.Image;
@ -56,9 +60,9 @@ export class EvolutionPhase extends Phase {
this.scene.fadeOutBgm(undefined, false);
const evolutionHandler = this.scene.ui.getHandler() as EvolutionSceneHandler;
this.evolutionHandler = this.scene.ui.getHandler() as EvolutionSceneHandler;
this.evolutionContainer = evolutionHandler.evolutionContainer;
this.evolutionContainer = this.evolutionHandler.evolutionContainer;
this.evolutionBaseBg = this.scene.add.image(0, 0, "default_bg");
this.evolutionBaseBg.setOrigin(0, 0);
@ -111,16 +115,13 @@ export class EvolutionPhase extends Phase {
sprite.pipelineData[k] = this.pokemon.getSprite().pipelineData[k];
});
});
this.preEvolvedPokemonName = getPokemonNameWithAffix(this.pokemon);
this.doEvolution();
});
}
doEvolution(): void {
const evolutionHandler = this.scene.ui.getHandler() as EvolutionSceneHandler;
const preName = getPokemonNameWithAffix(this.pokemon);
this.scene.ui.showText(i18next.t("menu:evolving", { pokemonName: preName }), null, () => {
this.scene.ui.showText(i18next.t("menu:evolving", { pokemonName: this.preEvolvedPokemonName }), null, () => {
this.pokemon.cry();
this.pokemon.getPossibleEvolution(this.evolution).then(evolvedPokemon => {
@ -140,7 +141,7 @@ export class EvolutionPhase extends Phase {
});
this.scene.time.delayedCall(1000, () => {
const evolutionBgm = this.scene.playSoundWithoutBgm("evolution");
this.evolutionBgm = this.scene.playSoundWithoutBgm("evolution");
this.scene.tweens.add({
targets: this.evolutionBgOverlay,
alpha: 1,
@ -174,10 +175,30 @@ export class EvolutionPhase extends Phase {
this.scene.time.delayedCall(1500, () => {
this.pokemonEvoTintSprite.setScale(0.25);
this.pokemonEvoTintSprite.setVisible(true);
evolutionHandler.canCancel = true;
this.evolutionHandler.canCancel = true;
this.doCycle(1).then(success => {
if (!success) {
if (success) {
this.handleSuccessEvolution(evolvedPokemon);
} else {
this.handleFailedEvolution(evolvedPokemon);
}
});
});
});
}
});
}
});
});
});
}, 1000);
}
/**
* Handles a failed/stopped evolution
* @param evolvedPokemon - The evolved Pokemon
*/
private handleFailedEvolution(evolvedPokemon: Pokemon): void {
this.pokemonSprite.setVisible(true);
this.pokemonTintSprite.setScale(1);
this.scene.tweens.add({
@ -189,12 +210,12 @@ export class EvolutionPhase extends Phase {
}
});
SoundFade.fadeOut(this.scene, evolutionBgm, 100);
SoundFade.fadeOut(this.scene, this.evolutionBgm, 100);
this.scene.unshiftPhase(new EndEvolutionPhase(this.scene));
this.scene.ui.showText(i18next.t("menu:stoppedEvolving", { pokemonName: preName }), null, () => {
this.scene.ui.showText(i18next.t("menu:pauseEvolutionsQuestion", { pokemonName: preName }), null, () => {
this.scene.ui.showText(i18next.t("menu:stoppedEvolving", { pokemonName: this.preEvolvedPokemonName }), null, () => {
this.scene.ui.showText(i18next.t("menu:pauseEvolutionsQuestion", { pokemonName: this.preEvolvedPokemonName }), null, () => {
const end = () => {
this.scene.ui.showText("", 0);
this.scene.playBgm();
@ -204,26 +225,45 @@ export class EvolutionPhase extends Phase {
this.scene.ui.setOverlayMode(Mode.CONFIRM, () => {
this.scene.ui.revertMode();
this.pokemon.pauseEvolutions = true;
this.scene.ui.showText(i18next.t("menu:evolutionsPaused", { pokemonName: preName }), null, end, 3000);
this.scene.ui.showText(i18next.t("menu:evolutionsPaused", { pokemonName: this.preEvolvedPokemonName }), null, end, 3000);
}, () => {
this.scene.ui.revertMode();
this.scene.time.delayedCall(3000, end);
});
});
}, null, true);
return;
}
/**
* Handles a successful evolution
* @param evolvedPokemon - The evolved Pokemon
*/
private handleSuccessEvolution(evolvedPokemon: Pokemon): void {
this.scene.playSound("se/sparkle");
this.pokemonEvoSprite.setVisible(true);
this.doCircleInward();
const onEvolutionComplete = () => {
SoundFade.fadeOut(this.scene, this.evolutionBgm, 100);
this.scene.time.delayedCall(250, () => {
this.pokemon.cry();
this.scene.time.delayedCall(1250, () => {
this.scene.playSoundWithoutBgm("evolution_fanfare");
evolvedPokemon.destroy();
this.scene.ui.showText(i18next.t("menu:evolutionDone", { pokemonName: this.preEvolvedPokemonName, evolvedPokemonName: this.pokemon.name }), null, () => this.end(), null, true, Utils.fixedInt(4000));
this.scene.time.delayedCall(Utils.fixedInt(4250), () => this.scene.playBgm());
});
});
};
this.scene.time.delayedCall(900, () => {
evolutionHandler.canCancel = false;
this.evolutionHandler.canCancel = false;
this.pokemon.evolve(this.evolution, this.pokemon.species).then(() => {
const levelMoves = this.pokemon.getLevelMoves(this.lastLevel + 1, true);
for (const lm of levelMoves) {
this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.scene.getParty().indexOf(this.pokemon), lm[1]));
this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.scene.getPlayerParty().indexOf(this.pokemon), lm[1]));
}
this.scene.unshiftPhase(new EndEvolutionPhase(this.scene));
@ -248,28 +288,7 @@ export class EvolutionPhase extends Phase {
targets: this.evolutionBgOverlay,
alpha: 0,
duration: 250,
onComplete: () => {
SoundFade.fadeOut(this.scene, evolutionBgm, 100);
this.scene.time.delayedCall(250, () => {
this.pokemon.cry();
this.scene.time.delayedCall(1250, () => {
this.scene.playSoundWithoutBgm("evolution_fanfare");
evolvedPokemon.destroy();
this.scene.ui.showText(i18next.t("menu:evolutionDone", { pokemonName: preName, evolvedPokemonName: this.pokemon.name }), null, () => this.end(), null, true, Utils.fixedInt(4000));
this.scene.time.delayedCall(Utils.fixedInt(4250), () => this.scene.playBgm());
});
});
}
});
}
});
}
});
});
});
});
});
onComplete: onEvolutionComplete
});
}
});
@ -277,7 +296,6 @@ export class EvolutionPhase extends Phase {
});
});
});
}, 1000);
}
doSpiralUpward() {
@ -320,7 +338,6 @@ export class EvolutionPhase extends Phase {
doCycle(l: number, lastCycle: integer = 15): Promise<boolean> {
return new Promise(resolve => {
const evolutionHandler = this.scene.ui.getHandler() as EvolutionSceneHandler;
const isLastCycle = l === lastCycle;
this.scene.tweens.add({
targets: this.pokemonTintSprite,
@ -336,7 +353,7 @@ export class EvolutionPhase extends Phase {
duration: 500 / l,
yoyo: !isLastCycle,
onComplete: () => {
if (evolutionHandler.cancelled) {
if (this.evolutionHandler.cancelled) {
return resolve(false);
}
if (l < lastCycle) {

View File

@ -1,24 +1,24 @@
import BattleScene from "#app/battle-scene";
import { BattlerIndex, BattleType } from "#app/battle";
import { applyPostFaintAbAttrs, PostFaintAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr } from "#app/data/ability";
import { BattlerTagLapseType, DestinyBondTag } from "#app/data/battler-tags";
import BattleScene from "#app/battle-scene";
import { applyPostFaintAbAttrs, applyPostKnockOutAbAttrs, applyPostVictoryAbAttrs, PostFaintAbAttr, PostKnockOutAbAttr, PostVictoryAbAttr } from "#app/data/ability";
import { BattlerTagLapseType, DestinyBondTag, GrudgeTag } from "#app/data/battler-tags";
import { battleSpecDialogue } from "#app/data/dialogue";
import { allMoves, PostVictoryStatStageChangeAttr } from "#app/data/move";
import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms";
import { BattleSpec } from "#app/enums/battle-spec";
import { StatusEffect } from "#app/enums/status-effect";
import Pokemon, { PokemonMove, EnemyPokemon, PlayerPokemon, HitResult } from "#app/field/pokemon";
import Pokemon, { EnemyPokemon, HitResult, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonInstantReviveModifier } from "#app/modifier/modifier";
import { SwitchType } from "#enums/switch-type";
import i18next from "i18next";
import { DamagePhase } from "./damage-phase";
import { GameOverPhase } from "./game-over-phase";
import { PokemonPhase } from "./pokemon-phase";
import { SwitchPhase } from "./switch-phase";
import { SwitchSummonPhase } from "./switch-summon-phase";
import { ToggleDoublePositionPhase } from "./toggle-double-position-phase";
import { GameOverPhase } from "./game-over-phase";
import { SwitchPhase } from "./switch-phase";
import { VictoryPhase } from "./victory-phase";
import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms";
import { SwitchType } from "#enums/switch-type";
import { isNullOrUndefined } from "#app/utils";
import { FRIENDSHIP_LOSS_FROM_FAINT } from "#app/data/balance/starters";
@ -31,18 +31,24 @@ export class FaintPhase extends PokemonPhase {
/**
* Destiny Bond tag belonging to the currently fainting Pokemon, if applicable
*/
private destinyTag?: DestinyBondTag;
private destinyTag?: DestinyBondTag | null;
/**
* The source Pokemon that dealt fatal damage and should get KO'd by Destiny Bond, if applicable
* Grudge tag belonging to the currently fainting Pokemon, if applicable
*/
private grudgeTag?: GrudgeTag | null;
/**
* The source Pokemon that dealt fatal damage
*/
private source?: Pokemon;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, preventEndure: boolean = false, destinyTag?: DestinyBondTag, source?: Pokemon) {
constructor(scene: BattleScene, battlerIndex: BattlerIndex, preventEndure: boolean = false, destinyTag?: DestinyBondTag | null, grudgeTag?: GrudgeTag | null, source?: Pokemon) {
super(scene, battlerIndex);
this.preventEndure = preventEndure;
this.destinyTag = destinyTag;
this.grudgeTag = grudgeTag;
this.source = source;
}
@ -53,6 +59,10 @@ export class FaintPhase extends PokemonPhase {
this.destinyTag.lapse(this.source, BattlerTagLapseType.CUSTOM);
}
if (!isNullOrUndefined(this.grudgeTag) && !isNullOrUndefined(this.source)) {
this.grudgeTag.lapse(this.getPokemon(), BattlerTagLapseType.CUSTOM, this.source);
}
if (!this.preventEndure) {
const instantReviveModifier = this.scene.applyModifier(PokemonInstantReviveModifier, this.player, this.getPokemon()) as PokemonInstantReviveModifier;
@ -120,7 +130,7 @@ export class FaintPhase extends PokemonPhase {
if (this.player) {
/** The total number of Pokemon in the player's party that can legally fight */
const legalPlayerPokemon = this.scene.getParty().filter(p => p.isAllowedInBattle());
const legalPlayerPokemon = this.scene.getPokemonAllowedInBattle();
/** The total number of legal player Pokemon that aren't currently on the field */
const legalPlayerPartyPokemon = legalPlayerPokemon.filter(p => !p.isActive(true));
if (!legalPlayerPokemon.length) {

View File

@ -1,8 +1,8 @@
import { clientSessionId } from "#app/account";
import { BattleType } from "#app/battle";
import BattleScene from "#app/battle-scene";
import { getCharVariantFromDialogue } from "#app/data/dialogue";
import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions";
import { getCharVariantFromDialogue } from "#app/data/dialogue";
import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species";
import { trainerConfigs } from "#app/data/trainer-config";
import Pokemon from "#app/field/pokemon";
@ -23,6 +23,7 @@ import * as Utils from "#app/utils";
import { PlayerGender } from "#enums/player-gender";
import { TrainerType } from "#enums/trainer-type";
import i18next from "i18next";
import { pokerogueApi } from "#app/plugins/api/pokerogue-api";
export class GameOverPhase extends BattlePhase {
private victory: boolean;
@ -65,7 +66,7 @@ export class GameOverPhase extends BattlePhase {
this.scene.gameData.loadSession(this.scene, this.scene.sessionSlotId).then(() => {
this.scene.pushPhase(new EncounterPhase(this.scene, true));
const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()).length;
const availablePartyMembers = this.scene.getPokemonAllowedInBattle().length;
this.scene.pushPhase(new SummonPhase(this.scene, 0));
if (this.scene.currentBattle.double && availablePartyMembers > 1) {
@ -97,7 +98,7 @@ export class GameOverPhase extends BattlePhase {
firstClear = this.scene.validateAchv(achvs.CLASSIC_VICTORY);
this.scene.validateAchv(achvs.UNEVOLVED_CLASSIC_VICTORY);
this.scene.gameData.gameStats.sessionsWon++;
for (const pokemon of this.scene.getParty()) {
for (const pokemon of this.scene.getPlayerParty()) {
this.awardRibbon(pokemon);
if (pokemon.species.getRootSpeciesId() !== pokemon.species.getRootSpeciesId(true)) {
@ -176,10 +177,9 @@ export class GameOverPhase extends BattlePhase {
If Online, execute apiFetch as intended
If Offline, execute offlineNewClear(), a localStorage implementation of newClear daily run checks */
if (this.victory) {
if (!Utils.isLocal) {
Utils.apiFetch(`savedata/session/newclear?slot=${this.scene.sessionSlotId}&clientSessionId=${clientSessionId}`, true)
.then(response => response.json())
.then(newClear => doGameOver(newClear));
if (!Utils.isLocal || Utils.isLocalServerConnected) {
pokerogueApi.savedata.session.newclear({ slot: this.scene.sessionSlotId, clientSessionId })
.then((success) => doGameOver(!!success));
} else {
this.scene.gameData.offlineNewClear(this.scene).then(result => {
doGameOver(result);
@ -195,13 +195,13 @@ export class GameOverPhase extends BattlePhase {
if (!this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) {
this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.ENDLESS_MODE));
}
if (this.scene.getParty().filter(p => p.fusionSpecies).length && !this.scene.gameData.unlocks[Unlockables.SPLICED_ENDLESS_MODE]) {
if (this.scene.getPlayerParty().filter(p => p.fusionSpecies).length && !this.scene.gameData.unlocks[Unlockables.SPLICED_ENDLESS_MODE]) {
this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.SPLICED_ENDLESS_MODE));
}
if (!this.scene.gameData.unlocks[Unlockables.MINI_BLACK_HOLE]) {
this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.MINI_BLACK_HOLE));
}
if (!this.scene.gameData.unlocks[Unlockables.EVIOLITE] && this.scene.getParty().some(p => p.getSpeciesForm(true).speciesId in pokemonEvolutions)) {
if (!this.scene.gameData.unlocks[Unlockables.EVIOLITE] && this.scene.getPlayerParty().some(p => p.getSpeciesForm(true).speciesId in pokemonEvolutions)) {
this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.EVIOLITE));
}
}

View File

@ -1,20 +1,63 @@
import BattleScene from "#app/battle-scene";
import { BattlerIndex } from "#app/battle";
import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, applyPostDefendAbAttrs, PostDefendAbAttr, applyPostAttackAbAttrs, PostAttackAbAttr, MaxMultiHitAbAttr, AlwaysHitAbAttr, TypeImmunityAbAttr } from "#app/data/ability";
import BattleScene from "#app/battle-scene";
import {
AddSecondStrikeAbAttr,
AlwaysHitAbAttr,
applyPostAttackAbAttrs,
applyPostDefendAbAttrs,
applyPreAttackAbAttrs,
IgnoreMoveEffectsAbAttr,
MaxMultiHitAbAttr,
PostAttackAbAttr,
PostDefendAbAttr,
TypeImmunityAbAttr,
} from "#app/data/ability";
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
import { MoveAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags";
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, OneHitKOAttr, MoveEffectTrigger, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } from "#app/data/move";
import {
BattlerTagLapseType,
DamageProtectedTag,
ProtectedTag,
SemiInvulnerableTag,
SubstituteTag,
} from "#app/data/battler-tags";
import {
applyFilteredMoveAttrs,
applyMoveAttrs,
AttackMove,
DelayedAttackAttr,
FixedDamageAttr,
HitsTagAttr,
MissEffectAttr,
MoveAttr,
MoveCategory,
MoveEffectAttr,
MoveEffectTrigger,
MoveFlags,
MoveTarget,
MultiHitAttr,
NoEffectAttr,
OneHitKOAttr,
OverrideMoveEffectAttr,
ToxicAccuracyAttr,
VariableTargetAttr,
} from "#app/data/move";
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Moves } from "#app/enums/moves";
import Pokemon, { PokemonMove, MoveResult, HitResult } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonMultiHitModifier, FlinchChanceModifier, EnemyAttackStatusEffectChanceModifier, ContactHeldItemTransferChanceModifier, HitHealModifier } from "#app/modifier/modifier";
import i18next from "i18next";
import * as Utils from "#app/utils";
import { PokemonPhase } from "./pokemon-phase";
import { Type } from "#app/data/type";
import Pokemon, { HitResult, MoveResult, PokemonMove } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import {
ContactHeldItemTransferChanceModifier,
EnemyAttackStatusEffectChanceModifier,
FlinchChanceModifier,
HitHealModifier,
PokemonMultiHitModifier,
} from "#app/modifier/modifier";
import { PokemonPhase } from "#app/phases/pokemon-phase";
import { BooleanHolder, executeIf, NumberHolder } from "#app/utils";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import i18next from "i18next";
export class MoveEffectPhase extends PokemonPhase {
public move: PokemonMove;
@ -35,7 +78,7 @@ export class MoveEffectPhase extends PokemonPhase {
this.targets = targets;
}
start() {
public override start(): void {
super.start();
/** The Pokemon using this phase's invoked move */
@ -43,8 +86,13 @@ export class MoveEffectPhase extends PokemonPhase {
/** All Pokemon targeted by this phase's invoked move */
const targets = this.getTargets();
/** If the user was somehow removed from the field, end this phase */
if (!user?.isOnField()) {
if (!user) {
return super.end();
}
const isDelayedAttack = this.move.getMove().hasAttr(DelayedAttackAttr);
/** If the user was somehow removed from the field and it's not a delayed attack, end this phase */
if (!user.isOnField() && !isDelayedAttack) {
return super.end();
}
@ -52,12 +100,12 @@ export class MoveEffectPhase extends PokemonPhase {
* Does an effect from this move override other effects on this turn?
* e.g. Charging moves (Fly, etc.) on their first turn of use.
*/
const overridden = new Utils.BooleanHolder(false);
const overridden = new BooleanHolder(false);
/** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */
const move = this.move.getMove();
// Assume single target for override
applyMoveAttrs(OverrideMoveEffectAttr, user, this.getTarget() ?? null, move, overridden, this.move.virtual).then(() => {
applyMoveAttrs(OverrideMoveEffectAttr, user, this.getFirstTarget() ?? null, move, overridden, this.move.virtual).then(() => {
// If other effects were overriden, stop this phase before they can be applied
if (overridden.value) {
return this.end();
@ -71,14 +119,14 @@ export class MoveEffectPhase extends PokemonPhase {
* effects of the move itself, Parental Bond, and Multi-Lens to do so.
*/
if (user.turnData.hitsLeft === -1) {
const hitCount = new Utils.IntegerHolder(1);
const hitCount = new NumberHolder(1);
// Assume single target for multi hit
applyMoveAttrs(MultiHitAttr, user, this.getTarget() ?? null, move, hitCount);
applyMoveAttrs(MultiHitAttr, user, this.getFirstTarget() ?? null, move, hitCount);
// If Parental Bond is applicable, double the hit count
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new Utils.IntegerHolder(0));
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new NumberHolder(0));
// If Multi-Lens is applicable, multiply the hit count by 1 + the number of Multi-Lenses held by the user
if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) {
this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new Utils.IntegerHolder(0));
this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new NumberHolder(0));
}
// Set the user's relevant turnData fields to reflect the final hit count
user.turnData.hitCount = hitCount.value;
@ -100,8 +148,9 @@ export class MoveEffectPhase extends PokemonPhase {
const hasActiveTargets = targets.some(t => t.isActive(true));
/** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */
const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
&& !targets[0].getTag(SemiInvulnerableTag);
const isImmune = targets[0]?.hasAbilityWithAttr(TypeImmunityAbAttr)
&& (targets[0]?.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
&& !targets[0]?.getTag(SemiInvulnerableTag);
/**
* If no targets are left for the move to hit (FAIL), or the invoked move is single-target
@ -111,9 +160,9 @@ export class MoveEffectPhase extends PokemonPhase {
if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) {
this.stopMultiHit();
if (hasActiveTargets) {
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getTarget() ? getPokemonNameWithAffix(this.getTarget()!) : "" }));
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" }));
moveHistoryEntry.result = MoveResult.MISS;
applyMoveAttrs(MissEffectAttr, user, null, move);
applyMoveAttrs(MissEffectAttr, user, null, this.move.getMove());
} else {
this.scene.queueMessage(i18next.t("battle:attackFailed"));
moveHistoryEntry.result = MoveResult.FAIL;
@ -127,36 +176,49 @@ export class MoveEffectPhase extends PokemonPhase {
const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false;
// Move animation only needs one target
new MoveAnim(move.id as Moves, user, this.getTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getTarget()!), () => {
new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getFirstTarget()!), () => {
/** Has the move successfully hit a target (for damage) yet? */
let hasHit: boolean = false;
for (const target of targets) {
// Prevent ENEMY_SIDE targeted moves from occurring twice in double battles
if (move.moveTarget === MoveTarget.ENEMY_SIDE && target !== targets[targets.length - 1]) {
continue;
}
/** The {@linkcode ArenaTagSide} to which the target belongs */
const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
/** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */
const hasConditionalProtectApplied = new Utils.BooleanHolder(false);
const hasConditionalProtectApplied = new BooleanHolder(false);
/** Does the applied conditional protection bypass Protect-ignoring effects? */
const bypassIgnoreProtect = new Utils.BooleanHolder(false);
const bypassIgnoreProtect = new BooleanHolder(false);
/** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */
if (!this.move.getMove().isAllyTarget()) {
this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, false, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect);
}
/** Is the target protected by Protect, etc. or a relevant conditional protection effect? */
const isProtected = (bypassIgnoreProtect.value || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target))
&& (hasConditionalProtectApplied.value || (!target.findTags(t => t instanceof DamageProtectedTag).length && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)))
|| (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType))));
const isProtected = (
bypassIgnoreProtect.value
|| !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target))
&& (hasConditionalProtectApplied.value
|| (!target.findTags(t => t instanceof DamageProtectedTag).length
&& target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)))
|| (this.move.getMove().category !== MoveCategory.STATUS
&& target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType))));
/** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */
const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr)
&& (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
&& !target.getTag(SemiInvulnerableTag);
/** Is the target hidden by the effects of its Commander ability? */
const isCommanding = this.scene.currentBattle.double && target.getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon(this.scene) === target;
/**
* If the move missed a target, stop all future hits against that target
* and move on to the next target (if there is one).
*/
if (!isImmune && !isProtected && !targetHitChecks[target.getBattlerIndex()]) {
if (isCommanding || (!isImmune && !isProtected && !targetHitChecks[target.getBattlerIndex()])) {
this.stopMultiHit(target);
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) }));
if (moveHistoryEntry.result === MoveResult.PENDING) {
@ -218,7 +280,7 @@ export class MoveEffectPhase extends PokemonPhase {
}
/** Does this phase represent the invoked move's last strike? */
const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive());
const lastHit = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive());
/**
* If the user can change forms by using the invoked move,
@ -234,85 +296,48 @@ export class MoveEffectPhase extends PokemonPhase {
* These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger
* type requires different conditions to be met with respect to the move's hit result.
*/
applyAttrs.push(new Promise(resolve => {
// Apply all effects with PRE_MOVE triggers (if the target isn't immune to the move)
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && hitResult !== HitResult.NO_EFFECT,
user, target, move).then(() => {
// All other effects require the move to not have failed or have been cancelled to trigger
if (hitResult !== HitResult.FAIL) {
/**
* If the invoked move's effects are meant to trigger during the move's "charge turn,"
* ignore all effects after this point.
* Otherwise, apply all self-targeted POST_APPLY effects.
*/
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY
&& attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move).then(() => {
// All effects past this point require the move to have hit the target
if (hitResult !== HitResult.NO_EFFECT) {
// Apply all non-self-targeted POST_APPLY effects
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY
&& !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()).then(() => {
/**
* If the move hit, and the target doesn't have Shield Dust,
* apply the chance to flinch the target gained from King's Rock
*/
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !move.hitsSubstitute(user, target)) {
const flinched = new Utils.BooleanHolder(false);
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
if (flinched.value) {
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
}
}
// If the move was not protected against, apply all HIT effects
Utils.executeIf(!isProtected, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT
&& (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => {
// Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them)
return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => {
// Only apply the following effects if the move was not deflected by a substitute
if (move.hitsSubstitute(user, target)) {
const k = new Promise<void>((resolve) => {
//Start promise chain and apply PRE_APPLY move attributes
let promiseChain: Promise<void | null> = applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.PRE_APPLY
&& (!attr.firstHitOnly || firstHit)
&& (!attr.lastHitOnly || lastHit)
&& hitResult !== HitResult.NO_EFFECT, user, target, move);
/** Don't complete if the move failed */
if (hitResult === HitResult.FAIL) {
return resolve();
}
// If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tokens
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
}
target.lapseTags(BattlerTagLapseType.AFTER_HIT);
/** Apply Move/Ability Effects in correct order */
promiseChain = promiseChain
.then(this.applySelfTargetEffects(user, target, firstHit, lastHit));
})).then(() => {
// Apply the user's post-attack ability effects
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => {
/**
* If the invoked move is an attack, apply the user's chance to
* steal an item from the target granted by Grip Claw
*/
if (this.move.getMove() instanceof AttackMove) {
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
}
resolve();
});
});
})
).then(() => resolve());
});
if (hitResult !== HitResult.NO_EFFECT) {
promiseChain
.then(this.applyPostApplyEffects(user, target, firstHit, lastHit))
.then(this.applyHeldItemFlinchCheck(user, target, dealsDamage))
.then(this.applySuccessfulAttackEffects(user, target, firstHit, lastHit, !!isProtected, hitResult, firstTarget))
.then(() => resolve());
} else {
applyMoveAttrs(NoEffectAttr, user, null, move).then(() => resolve());
promiseChain
.then(() => applyMoveAttrs(NoEffectAttr, user, null, move))
.then(resolve);
}
});
} else {
resolve();
}
});
}));
applyAttrs.push(k);
}
// Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved
const postTarget = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()) ?
const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ?
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) :
null;
if (!!postTarget) {
if (postTarget) {
if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after
applyAttrs[applyAttrs.length - 1]?.then(() => postTarget);
applyAttrs[applyAttrs.length - 1].then(() => postTarget);
} else { // Otherwise, push a new asynchronous move effect
applyAttrs.push(postTarget);
}
@ -327,7 +352,7 @@ export class MoveEffectPhase extends PokemonPhase {
*/
targets.forEach(target => {
const substitute = target.getTag(SubstituteTag);
if (!!substitute && substitute.hp <= 0) {
if (substitute && substitute.hp <= 0) {
target.lapseTag(BattlerTagType.SUBSTITUTE);
}
});
@ -337,7 +362,7 @@ export class MoveEffectPhase extends PokemonPhase {
});
}
end() {
public override end(): void {
const user = this.getUserPokemon();
/**
* If this phase isn't for the invoked move's last strike,
@ -347,7 +372,7 @@ export class MoveEffectPhase extends PokemonPhase {
* to the user.
*/
if (user) {
if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) {
if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getFirstTarget()?.isActive()) {
this.scene.unshiftPhase(this.getNewHitPhase());
} else {
// Queue message for number of hits made by multi-move
@ -367,17 +392,145 @@ export class MoveEffectPhase extends PokemonPhase {
}
/**
* Resolves whether this phase's invoked move hits or misses the given target
* @param target {@linkcode Pokemon} the Pokemon targeted by the invoked move
* @returns `true` if the move does not miss the target; `false` otherwise
* Apply self-targeted effects that trigger `POST_APPLY`
*
* @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param firstHit - `true` if this is the first hit in a multi-hit attack
* @param lastHit - `true` if this is the last hit in a multi-hit attack
* @returns a function intended to be passed into a `then()` call.
*/
hitCheck(target: Pokemon): boolean {
protected applySelfTargetEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise<void | null> {
return () => applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.POST_APPLY
&& attr.selfTarget
&& (!attr.firstHitOnly || firstHit)
&& (!attr.lastHitOnly || lastHit), user, target, this.move.getMove());
}
/**
* Applies non-self-targeted effects that trigger `POST_APPLY`
* (i.e. Smelling Salts curing Paralysis, and the forced switch from U-Turn, Dragon Tail, etc)
* @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param firstHit - `true` if this is the first hit in a multi-hit attack
* @param lastHit - `true` if this is the last hit in a multi-hit attack
* @returns a function intended to be passed into a `then()` call.
*/
protected applyPostApplyEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise<void | null> {
return () => applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.POST_APPLY
&& !attr.selfTarget
&& (!attr.firstHitOnly || firstHit)
&& (!attr.lastHitOnly || lastHit), user, target, this.move.getMove());
}
/**
* Applies effects that trigger on HIT
* (i.e. Final Gambit, Power-Up Punch, Drain Punch)
* @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param firstHit - `true` if this is the first hit in a multi-hit attack
* @param lastHit - `true` if this is the last hit in a multi-hit attack
* @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move}
* @returns a function intended to be passed into a `then()` call.
*/
protected applyOnHitEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, firstTarget: boolean): Promise<void> {
return applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.HIT
&& (!attr.firstHitOnly || firstHit)
&& (!attr.lastHitOnly || lastHit)
&& (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove());
}
/**
* Applies reactive effects that occur when a Pokémon is hit.
* (i.e. Effect Spore, Disguise, Liquid Ooze, Beak Blast)
* @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param hitResult - The {@linkcode HitResult} of the attempted move
* @returns a `Promise` intended to be passed into a `then()` call.
*/
protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): Promise<void | null> {
return executeIf(!target.isFainted() || target.canApplyAbility(), () =>
applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult)
.then(() => {
if (!this.move.getMove().hitsSubstitute(user, target)) {
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
}
target.lapseTags(BattlerTagLapseType.AFTER_HIT);
}
})
);
}
/**
* Applies all effects and attributes that require a move to connect with a target,
* namely reactive effects like Weak Armor, on-hit effects like that of Power-Up Punch, and item stealing effects
* @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param firstHit - `true` if this is the first hit in a multi-hit attack
* @param lastHit - `true` if this is the last hit in a multi-hit attack
* @param isProtected - `true` if the target is protected by effects such as Protect
* @param hitResult - The {@linkcode HitResult} of the attempted move
* @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move}
* @returns a function intended to be passed into a `then()` call.
*/
protected applySuccessfulAttackEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, isProtected : boolean, hitResult: HitResult, firstTarget: boolean) : () => Promise<void | null> {
return () => executeIf(!isProtected, () =>
this.applyOnHitEffects(user, target, firstHit, lastHit, firstTarget).then(() =>
this.applyOnGetHitAbEffects(user, target, hitResult)).then(() =>
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult)).then(() => { // Item Stealing Effects
if (this.move.getMove() instanceof AttackMove) {
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
}
})
);
}
/**
* Handles checking for and applying Flinches
* @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param dealsDamage - `true` if the attempted move successfully dealt damage
* @returns a function intended to be passed into a `then()` call.
*/
protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : () => void {
return () => {
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) {
const flinched = new BooleanHolder(false);
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
if (flinched.value) {
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
}
}
};
}
/**
* Resolves whether this phase's invoked move hits the given target
* @param target - The {@linkcode Pokemon} targeted by the invoked move
* @returns `true` if the move hits the target
*/
public hitCheck(target: Pokemon): boolean {
// Moves targeting the user and entry hazards can't miss
if ([ MoveTarget.USER, MoveTarget.ENEMY_SIDE ].includes(this.move.getMove().moveTarget)) {
return true;
}
const user = this.getUserPokemon()!; // TODO: is this bang correct?
const user = this.getUserPokemon();
if (!user) {
return false;
}
// Hit check only calculated on first hit for multi-hit moves unless flag is set to check all hits.
// However, if an ability with the MaxMultiHitAbAttr, namely Skill Link, is present, act as a normal
@ -425,29 +578,29 @@ export class MoveEffectPhase extends PokemonPhase {
return rand < (moveAccuracy * accuracyMultiplier);
}
/** Returns the {@linkcode Pokemon} using this phase's invoked move */
getUserPokemon(): Pokemon | undefined {
/** @returns The {@linkcode Pokemon} using this phase's invoked move */
public getUserPokemon(): Pokemon | null {
if (this.battlerIndex > BattlerIndex.ENEMY_2) {
return this.scene.getPokemonById(this.battlerIndex) ?? undefined;
return this.scene.getPokemonById(this.battlerIndex);
}
return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex];
}
/** Returns an array of all {@linkcode Pokemon} targeted by this phase's invoked move */
getTargets(): Pokemon[] {
/** @returns An array of all {@linkcode Pokemon} targeted by this phase's invoked move */
public getTargets(): Pokemon[] {
return this.scene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1);
}
/** Returns the first target of this phase's invoked move */
getTarget(): Pokemon | undefined {
/** @returns The first target of this phase's invoked move */
public getFirstTarget(): Pokemon | undefined {
return this.getTargets()[0];
}
/**
* Removes the given {@linkcode Pokemon} from this phase's target list
* @param target {@linkcode Pokemon} the Pokemon to be removed
* @param target - The {@linkcode Pokemon} to be removed
*/
removeTarget(target: Pokemon): void {
protected removeTarget(target: Pokemon): void {
const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex());
if (targetIndex !== -1) {
this.targets.splice(this.targets.findIndex(ind => ind === target.getBattlerIndex()), 1);
@ -456,26 +609,27 @@ export class MoveEffectPhase extends PokemonPhase {
/**
* Prevents subsequent strikes of this phase's invoked move from occurring
* @param target {@linkcode Pokemon} if defined, only stop subsequent
* strikes against this Pokemon
* @param target - If defined, only stop subsequent strikes against this {@linkcode Pokemon}
*/
stopMultiHit(target?: Pokemon): void {
/** If given a specific target, remove the target from subsequent strikes */
public stopMultiHit(target?: Pokemon): void {
// If given a specific target, remove the target from subsequent strikes
if (target) {
this.removeTarget(target);
}
/**
* If no target specified, or the specified target was the last of this move's
* targets, completely cancel all subsequent strikes.
*/
const user = this.getUserPokemon();
if (!user) {
return;
}
// If no target specified, or the specified target was the last of this move's
// targets, completely cancel all subsequent strikes.
if (!target || this.targets.length === 0 ) {
this.getUserPokemon()!.turnData.hitCount = 1; // TODO: is the bang correct here?
this.getUserPokemon()!.turnData.hitsLeft = 1; // TODO: is the bang correct here?
user.turnData.hitCount = 1;
user.turnData.hitsLeft = 1;
}
}
/** Returns a new MoveEffectPhase with the same properties as this phase */
getNewHitPhase() {
/** @returns A new `MoveEffectPhase` with the same properties as this phase */
protected getNewHitPhase(): MoveEffectPhase {
return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move);
}
}

View File

@ -1,9 +1,31 @@
import { BattlerIndex } from "#app/battle";
import BattleScene from "#app/battle-scene";
import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs, BlockRedirectAbAttr, IncreasePpAbAttr, PokemonTypeChangeAbAttr, PostMoveUsedAbAttr, RedirectMoveAbAttr, ReduceStatusEffectDurationAbAttr } from "#app/data/ability";
import {
applyAbAttrs,
applyPostMoveUsedAbAttrs,
applyPreAttackAbAttrs,
BlockRedirectAbAttr,
IncreasePpAbAttr,
PokemonTypeChangeAbAttr,
PostMoveUsedAbAttr,
RedirectMoveAbAttr,
ReduceStatusEffectDurationAbAttr
} from "#app/data/ability";
import { DelayedAttackTag } from "#app/data/arena-tag";
import { CommonAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, CenterOfAttentionTag } from "#app/data/battler-tags";
import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, CopyMoveAttr, frenzyMissFunc, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move";
import {
allMoves,
applyMoveAttrs,
BypassRedirectAttr,
BypassSleepAttr,
CopyMoveAttr,
DelayedAttackAttr,
frenzyMissFunc,
HealStatusEffectAttr,
MoveFlags,
PreMoveMessageAttr
} from "#app/data/move";
import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms";
import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect";
import { Type } from "#app/data/type";
@ -14,16 +36,17 @@ import { getPokemonNameWithAffix } from "#app/messages";
import Overrides from "#app/overrides";
import { BattlePhase } from "#app/phases/battle-phase";
import { CommonAnimPhase } from "#app/phases/common-anim-phase";
import { MoveChargePhase } from "#app/phases/move-charge-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { BooleanHolder, NumberHolder } from "#app/utils";
import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import { StatusEffect } from "#enums/status-effect";
import i18next from "i18next";
import { MoveChargePhase } from "#app/phases/move-charge-phase";
export class MovePhase extends BattlePhase {
protected _pokemon: Pokemon;
@ -227,6 +250,32 @@ export class MovePhase extends BattlePhase {
// form changes happen even before we know that the move wll execute.
this.scene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
const isDelayedAttack = this.move.getMove().hasAttr(DelayedAttackAttr);
if (isDelayedAttack) {
// Check the player side arena if future sight is active
const futureSightTags = this.scene.arena.findTags(t => t.tagType === ArenaTagType.FUTURE_SIGHT);
const doomDesireTags = this.scene.arena.findTags(t => t.tagType === ArenaTagType.DOOM_DESIRE);
let fail = false;
const currentTargetIndex = targets[0].getBattlerIndex();
for (const tag of futureSightTags) {
if ((tag as DelayedAttackTag).targetIndex === currentTargetIndex) {
fail = true;
break;
}
}
for (const tag of doomDesireTags) {
if ((tag as DelayedAttackTag).targetIndex === currentTargetIndex) {
fail = true;
break;
}
}
if (fail) {
this.showMoveText();
this.showFailedText();
return this.end();
}
}
this.showMoveText();
if (moveQueue.length > 0) {

View File

@ -1,31 +1,31 @@
import { BattlerTagLapseType } from "#app/data/battler-tags";
import MysteryEncounterOption, { OptionPhaseCallback } from "#app/data/mystery-encounters/mystery-encounter-option";
import { SeenEncounterData } from "#app/data/mystery-encounters/mystery-encounter-save-data";
import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { CheckSwitchPhase } from "#app/phases/check-switch-phase";
import { GameOverPhase } from "#app/phases/game-over-phase";
import { NewBattlePhase } from "#app/phases/new-battle-phase";
import { PostTurnStatusEffectPhase } from "#app/phases/post-turn-status-effect-phase";
import { ReturnPhase } from "#app/phases/return-phase";
import { ScanIvsPhase } from "#app/phases/scan-ivs-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { SummonPhase } from "#app/phases/summon-phase";
import { SwitchPhase } from "#app/phases/switch-phase";
import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase";
import { BattleSpec } from "#enums/battle-spec";
import { BattlerTagType } from "#enums/battler-tag-type";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { SwitchType } from "#enums/switch-type";
import i18next from "i18next";
import BattleScene from "../battle-scene";
import { getCharVariantFromDialogue } from "../data/dialogue";
import { OptionSelectSettings, transitionMysteryEncounterIntroVisuals } from "../data/mystery-encounters/utils/encounter-phase-utils";
import { TrainerSlot } from "../data/trainer-config";
import { IvScannerModifier } from "../modifier/modifier";
import { Phase } from "../phase";
import { Mode } from "../ui/ui";
import { transitionMysteryEncounterIntroVisuals, OptionSelectSettings } from "../data/mystery-encounters/utils/encounter-phase-utils";
import MysteryEncounterOption, { OptionPhaseCallback } from "#app/data/mystery-encounters/mystery-encounter-option";
import { getCharVariantFromDialogue } from "../data/dialogue";
import { TrainerSlot } from "../data/trainer-config";
import { BattleSpec } from "#enums/battle-spec";
import { IvScannerModifier } from "../modifier/modifier";
import * as Utils from "../utils";
import { isNullOrUndefined } from "../utils";
import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { BattlerTagLapseType } from "#app/data/battler-tags";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { PostTurnStatusEffectPhase } from "#app/phases/post-turn-status-effect-phase";
import { SummonPhase } from "#app/phases/summon-phase";
import { ScanIvsPhase } from "#app/phases/scan-ivs-phase";
import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase";
import { ReturnPhase } from "#app/phases/return-phase";
import { CheckSwitchPhase } from "#app/phases/check-switch-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { NewBattlePhase } from "#app/phases/new-battle-phase";
import { GameOverPhase } from "#app/phases/game-over-phase";
import { SwitchPhase } from "#app/phases/switch-phase";
import { SeenEncounterData } from "#app/data/mystery-encounters/mystery-encounter-save-data";
import { SwitchType } from "#enums/switch-type";
import { BattlerTagType } from "#enums/battler-tag-type";
/**
* Will handle (in order):
@ -238,7 +238,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase {
}
// The total number of Pokemon in the player's party that can legally fight
const legalPlayerPokemon = this.scene.getParty().filter(p => p.isAllowedInBattle());
const legalPlayerPokemon = this.scene.getPokemonAllowedInBattle();
// The total number of legal player Pokemon that aren't currently on the field
const legalPlayerPartyPokemon = legalPlayerPokemon.filter(p => !p.isActive(true));
if (!legalPlayerPokemon.length) {
@ -343,7 +343,7 @@ export class MysteryEncounterBattlePhase extends Phase {
const doSummon = () => {
scene.currentBattle.started = true;
scene.playBgm(undefined);
scene.pbTray.showPbTray(scene.getParty());
scene.pbTray.showPbTray(scene.getPlayerParty());
scene.pbTrayEnemy.showPbTray(scene.getEnemyParty());
const doTrainerSummon = () => {
this.hideEnemyTrainer();
@ -402,7 +402,7 @@ export class MysteryEncounterBattlePhase extends Phase {
}
}
const availablePartyMembers = scene.getParty().filter(p => p.isAllowedInBattle());
const availablePartyMembers = scene.getPlayerParty().filter(p => p.isAllowedInBattle());
if (!availablePartyMembers[0].isOnField()) {
scene.pushPhase(new SummonPhase(scene, 0));
@ -417,6 +417,7 @@ export class MysteryEncounterBattlePhase extends Phase {
}
} else {
if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) {
scene.getPlayerField().forEach((pokemon) => pokemon.lapseTag(BattlerTagType.COMMANDED));
scene.pushPhase(new ReturnPhase(scene, 1));
}
scene.pushPhase(new ToggleDoublePositionPhase(scene, false));

View File

@ -11,13 +11,13 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase {
doEncounter(): void {
this.scene.playBgm(undefined, true);
for (const pokemon of this.scene.getParty()) {
for (const pokemon of this.scene.getPlayerParty()) {
if (pokemon) {
pokemon.resetBattleData();
}
}
for (const pokemon of this.scene.getParty().filter(p => p.isOnField())) {
for (const pokemon of this.scene.getPlayerParty().filter(p => p.isOnField())) {
applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null);
}

View File

@ -13,7 +13,7 @@ export class NextEncounterPhase extends EncounterPhase {
doEncounter(): void {
this.scene.playBgm(undefined, true);
for (const pokemon of this.scene.getParty()) {
for (const pokemon of this.scene.getPlayerParty()) {
if (pokemon) {
pokemon.resetBattleData();
}

View File

@ -19,7 +19,7 @@ export class PartyHealPhase extends BattlePhase {
this.scene.fadeOutBgm(1000, false);
}
this.scene.ui.fadeOut(1000).then(() => {
for (const pokemon of this.scene.getParty()) {
for (const pokemon of this.scene.getPlayerParty()) {
pokemon.hp = pokemon.getMaxHp();
pokemon.resetStatus();
for (const move of pokemon.moveset) {

View File

@ -18,7 +18,7 @@ export abstract class PartyMemberPokemonPhase extends FieldPhase {
}
getParty(): Pokemon[] {
return this.player ? this.scene.getParty() : this.scene.getEnemyParty();
return this.player ? this.scene.getPlayerParty() : this.scene.getEnemyParty();
}
getPokemon(): Pokemon {

View File

@ -1,8 +1,10 @@
import BattleScene from "#app/battle-scene";
import { SubstituteTag } from "#app/data/battler-tags";
import { PokemonAnimType } from "#enums/pokemon-anim-type";
import Pokemon from "#app/field/pokemon";
import { BattlePhase } from "#app/phases/battle-phase";
import { isNullOrUndefined } from "#app/utils";
import { PokemonAnimType } from "#enums/pokemon-anim-type";
import { Species } from "#enums/species";
export class PokemonAnimPhase extends BattlePhase {
@ -37,14 +39,20 @@ export class PokemonAnimPhase extends BattlePhase {
case PokemonAnimType.SUBSTITUTE_REMOVE:
this.doSubstituteRemoveAnim();
break;
case PokemonAnimType.COMMANDER_APPLY:
this.doCommanderApplyAnim();
break;
case PokemonAnimType.COMMANDER_REMOVE:
this.doCommanderRemoveAnim();
break;
default:
this.end();
}
}
doSubstituteAddAnim(): void {
private doSubstituteAddAnim(): void {
const substitute = this.pokemon.getTag(SubstituteTag);
if (substitute === null) {
if (isNullOrUndefined(substitute)) {
return this.end();
}
@ -106,7 +114,7 @@ export class PokemonAnimPhase extends BattlePhase {
});
}
doSubstitutePreMoveAnim(): void {
private doSubstitutePreMoveAnim(): void {
if (this.fieldAssets.length !== 1) {
return this.end();
}
@ -135,7 +143,7 @@ export class PokemonAnimPhase extends BattlePhase {
});
}
doSubstitutePostMoveAnim(): void {
private doSubstitutePostMoveAnim(): void {
if (this.fieldAssets.length !== 1) {
return this.end();
}
@ -164,7 +172,7 @@ export class PokemonAnimPhase extends BattlePhase {
});
}
doSubstituteRemoveAnim(): void {
private doSubstituteRemoveAnim(): void {
if (this.fieldAssets.length !== 1) {
return this.end();
}
@ -233,4 +241,121 @@ export class PokemonAnimPhase extends BattlePhase {
}
});
}
private doCommanderApplyAnim(): void {
if (!this.scene.currentBattle?.double) {
return this.end();
}
const dondozo = this.pokemon.getAlly();
if (dondozo?.species?.speciesId !== Species.DONDOZO) {
return this.end();
}
const tatsugiriX = this.pokemon.x + this.pokemon.getSprite().x;
const tatsugiriY = this.pokemon.y + this.pokemon.getSprite().y;
const getSourceSprite = () => {
const sprite = this.scene.addPokemonSprite(this.pokemon, tatsugiriX, tatsugiriY, this.pokemon.getSprite().texture, this.pokemon.getSprite()!.frame.name, true);
[ "spriteColors", "fusionSpriteColors" ].map(k => sprite.pipelineData[k] = this.pokemon.getSprite().pipelineData[k]);
sprite.setPipelineData("spriteKey", this.pokemon.getBattleSpriteKey());
sprite.setPipelineData("shiny", this.pokemon.shiny);
sprite.setPipelineData("variant", this.pokemon.variant);
sprite.setPipelineData("ignoreFieldPos", true);
sprite.setOrigin(0.5, 1);
this.pokemon.getSprite().on("animationupdate", (_anim, frame) => sprite.setFrame(frame.textureFrame));
this.scene.field.add(sprite);
return sprite;
};
const sourceSprite = getSourceSprite();
this.pokemon.setVisible(false);
const sourceFpOffset = this.pokemon.getFieldPositionOffset();
const dondozoFpOffset = dondozo.getFieldPositionOffset();
this.scene.playSound("se/pb_throw");
this.scene.tweens.add({
targets: sourceSprite,
duration: 375,
scale: 0.5,
x: { value: tatsugiriX + (dondozoFpOffset[0] - sourceFpOffset[0]) / 2, ease: "Linear" },
y: { value: (this.pokemon.isPlayer() ? 100 : 65) + sourceFpOffset[1], ease: "Sine.easeOut" },
onComplete: () => {
this.scene.field.bringToTop(dondozo);
this.scene.tweens.add({
targets: sourceSprite,
duration: 375,
scale: 0.01,
x: { value: dondozo.x, ease: "Linear" },
y: { value: dondozo.y + dondozo.height / 2, ease: "Sine.easeIn" },
onComplete: () => {
sourceSprite.destroy();
this.scene.playSound("battle_anims/PRSFX- Liquidation1.wav");
this.scene.tweens.add({
targets: dondozo,
duration: 250,
ease: "Sine.easeInOut",
scale: 0.85,
yoyo: true,
onComplete: () => this.end()
});
}
});
}
});
}
private doCommanderRemoveAnim(): void {
// Note: unlike the other Commander animation, this is played through the
// Dondozo instead of the Tatsugiri.
const tatsugiri = this.pokemon.getAlly();
const tatsuSprite = this.scene.addPokemonSprite(
tatsugiri,
this.pokemon.x + this.pokemon.getSprite().x,
this.pokemon.y + this.pokemon.getSprite().y + this.pokemon.height / 2,
tatsugiri.getSprite().texture,
tatsugiri.getSprite()!.frame.name,
true
);
[ "spriteColors", "fusionSpriteColors" ].map(k => tatsuSprite.pipelineData[k] = tatsugiri.getSprite().pipelineData[k]);
tatsuSprite.setPipelineData("spriteKey", tatsugiri.getBattleSpriteKey());
tatsuSprite.setPipelineData("shiny", tatsugiri.shiny);
tatsuSprite.setPipelineData("variant", tatsugiri.variant);
tatsuSprite.setPipelineData("ignoreFieldPos", true);
this.pokemon.getSprite().on("animationupdate", (_anim, frame) => tatsuSprite.setFrame(frame.textureFrame));
tatsuSprite.setOrigin(0.5, 1);
tatsuSprite.setScale(0.01);
this.scene.field.add(tatsuSprite);
this.scene.field.bringToTop(this.pokemon);
tatsuSprite.setVisible(true);
this.scene.tweens.add({
targets: this.pokemon,
duration: 250,
ease: "Sine.easeInOut",
scale: 1.15,
yoyo: true,
onComplete: () => {
this.scene.playSound("battle_anims/PRSFX- Liquidation4.wav");
this.scene.tweens.add({
targets: tatsuSprite,
duration: 500,
scale: 1,
x: { value: tatsugiri.x + tatsugiri.getSprite().x, ease: "Linear" },
y: { value: tatsugiri.y + tatsugiri.getSprite().y, ease: "Sine.easeIn" },
onComplete: () => {
tatsugiri.setVisible(true);
tatsuSprite.destroy();
this.end();
}
});
}
});
}
}

View File

@ -1,6 +1,6 @@
import BattleScene from "#app/battle-scene";
import { BattlerIndex } from "#app/battle";
import { applyPostSummonAbAttrs, PostSummonAbAttr } from "#app/data/ability";
import { applyAbAttrs, applyPostSummonAbAttrs, CommanderAbAttr, PostSummonAbAttr } from "#app/data/ability";
import { ArenaTrapTag } from "#app/data/arena-tag";
import { StatusEffect } from "#app/enums/status-effect";
import { PokemonPhase } from "./pokemon-phase";
@ -28,5 +28,8 @@ export class PostSummonPhase extends PokemonPhase {
}
applyPostSummonAbAttrs(PostSummonAbAttr, pokemon).then(() => this.end());
const field = pokemon.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField();
field.forEach((p) => applyAbAttrs(CommanderAbAttr, p, null, false));
}
}

View File

@ -1,6 +1,6 @@
import BattleScene from "#app/battle-scene";
import { BattlerIndex } from "#app/battle";
import { applyAbAttrs, BlockNonDirectDamageAbAttr, BlockStatusDamageAbAttr, ReduceBurnDamageAbAttr } from "#app/data/ability";
import { applyAbAttrs, applyPostDamageAbAttrs, BlockNonDirectDamageAbAttr, BlockStatusDamageAbAttr, PostDamageAbAttr, ReduceBurnDamageAbAttr } from "#app/data/ability";
import { CommonBattleAnim, CommonAnim } from "#app/data/battle-anims";
import { getStatusEffectActivationText } from "#app/data/status-effect";
import { BattleSpec } from "#app/enums/battle-spec";
@ -41,6 +41,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase {
// Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ...
this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true));
pokemon.updateInfo();
applyPostDamageAbAttrs(PostDamageAbAttr, pokemon, damage.value, pokemon.hasPassive(), false, []);
}
new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, false, () => this.end());
} else {

View File

@ -38,7 +38,7 @@ export class SelectModifierPhase extends BattlePhase {
this.scene.reroll = false;
}
const party = this.scene.getParty();
const party = this.scene.getPlayerParty();
if (!this.isCopy) {
regenerateModifierPoolThresholds(party, this.getPoolType(), this.rerollCount);
}
@ -289,7 +289,7 @@ export class SelectModifierPhase extends BattlePhase {
}
getModifierTypeOptions(modifierCount: integer): ModifierTypeOption[] {
return getPlayerModifierTypeOptions(modifierCount, this.scene.getParty(), this.scene.lockModifierTiers ? this.modifierTiers : undefined, this.customModifierSettings);
return getPlayerModifierTypeOptions(modifierCount, this.scene.getPlayerParty(), this.scene.lockModifierTiers ? this.modifierTiers : undefined, this.customModifierSettings);
}
copy(): SelectModifierPhase {

View File

@ -3,16 +3,15 @@ import { applyChallenges, ChallengeType } from "#app/data/challenge";
import { Gender } from "#app/data/gender";
import { SpeciesFormChangeMoveLearnedTrigger } from "#app/data/pokemon-forms";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Species } from "#app/enums/species";
import { PlayerPokemon } from "#app/field/pokemon";
import { overrideModifiers, overrideHeldItems } from "#app/modifier/modifier";
import { overrideHeldItems, overrideModifiers } from "#app/modifier/modifier";
import Overrides from "#app/overrides";
import { Phase } from "#app/phase";
import { TitlePhase } from "#app/phases/title-phase";
import { SaveSlotUiMode } from "#app/ui/save-slot-select-ui-handler";
import { Starter } from "#app/ui/starter-select-ui-handler";
import { Mode } from "#app/ui/ui";
import { Species } from "#enums/species";
import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
import { TitlePhase } from "./title-phase";
import Overrides from "#app/overrides";
export class SelectStarterPhase extends Phase {
@ -44,7 +43,7 @@ export class SelectStarterPhase extends Phase {
* @param starters {@linkcode Pokemon} with which to start the first battle
*/
initBattle(starters: Starter[]) {
const party = this.scene.getParty();
const party = this.scene.getPlayerParty();
const loadPokemonAssets: Promise<void>[] = [];
starters.forEach((starter: Starter, i: integer) => {
if (!i && Overrides.STARTER_SPECIES_OVERRIDE) {
@ -103,7 +102,7 @@ export class SelectStarterPhase extends Phase {
this.scene.sessionPlayTime = 0;
this.scene.lastSavePlayTime = 0;
// Ensures Keldeo (or any future Pokemon that have this type of form change) starts in the correct form
this.scene.getParty().forEach((p: PlayerPokemon) => {
this.scene.getPlayerParty().forEach((p) => {
this.scene.triggerPokemonFormChange(p, SpeciesFormChangeMoveLearnedTrigger);
});
this.end();

View File

@ -1,7 +1,7 @@
import BattleScene from "#app/battle-scene";
import { SwitchType } from "#enums/switch-type";
import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler";
import PartyUiHandler, { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler";
import { Mode } from "#app/ui/ui";
import { SwitchType } from "#enums/switch-type";
import { BattlePhase } from "./battle-phase";
import { SwitchSummonPhase } from "./switch-summon-phase";
@ -38,7 +38,7 @@ export class SwitchPhase extends BattlePhase {
super.start();
// Skip modal switch if impossible (no remaining party members that aren't in battle)
if (this.isModal && !this.scene.getParty().filter(p => p.isAllowedInBattle() && !p.isActive(true)).length) {
if (this.isModal && !this.scene.getPlayerParty().filter(p => p.isAllowedInBattle() && !p.isActive(true)).length) {
return super.end();
}
@ -49,7 +49,7 @@ export class SwitchPhase extends BattlePhase {
* if the mon should have already been returned but is still alive and well
* on the field. see also; battle.test.ts
*/
if (this.isModal && !this.doReturn && !this.scene.getParty()[this.fieldIndex].isFainted()) {
if (this.isModal && !this.doReturn && !this.scene.getPlayerParty()[this.fieldIndex].isFainted()) {
return super.end();
}
@ -59,7 +59,7 @@ export class SwitchPhase extends BattlePhase {
}
// Override field index to 0 in case of double battle where 2/3 remaining legal party members fainted at once
const fieldIndex = this.scene.currentBattle.getBattlerCount() === 1 || this.scene.getParty().filter(p => p.isAllowedInBattle()).length > 1 ? this.fieldIndex : 0;
const fieldIndex = this.scene.currentBattle.getBattlerCount() === 1 || this.scene.getPokemonAllowedInBattle().length > 1 ? this.fieldIndex : 0;
this.scene.ui.setMode(Mode.PARTY, this.isModal ? PartyUiMode.FAINT_SWITCH : PartyUiMode.POST_BATTLE_SWITCH, fieldIndex, (slotIndex: integer, option: PartyOption) => {
if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) {

View File

@ -54,7 +54,7 @@ export class SwitchSummonPhase extends SummonPhase {
}
}
if (!this.doReturn || (this.slotIndex !== -1 && !(this.player ? this.scene.getParty() : this.scene.getEnemyParty())[this.slotIndex])) {
if (!this.doReturn || (this.slotIndex !== -1 && !(this.player ? this.scene.getPlayerParty() : this.scene.getEnemyParty())[this.slotIndex])) {
if (this.player) {
return this.switchAndSummon();
} else {

View File

@ -1,21 +1,21 @@
import { loggedInUser } from "#app/account";
import BattleScene from "#app/battle-scene";
import { BattleType } from "#app/battle";
import { getDailyRunStarters, fetchDailyRunSeed } from "#app/data/daily-run";
import BattleScene from "#app/battle-scene";
import { fetchDailyRunSeed, getDailyRunStarters } from "#app/data/daily-run";
import { Gender } from "#app/data/gender";
import { getBiomeKey } from "#app/field/arena";
import { GameModes, GameMode, getGameMode } from "#app/game-mode";
import { regenerateModifierPoolThresholds, ModifierPoolType, modifierTypes, getDailyRunStarterModifiers } from "#app/modifier/modifier-type";
import { GameMode, GameModes, getGameMode } from "#app/game-mode";
import { Modifier } from "#app/modifier/modifier";
import { getDailyRunStarterModifiers, ModifierPoolType, modifierTypes, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type";
import { Phase } from "#app/phase";
import { SessionSaveData } from "#app/system/game-data";
import { Unlockables } from "#app/system/unlockables";
import { vouchers } from "#app/system/voucher";
import { OptionSelectItem, OptionSelectConfig } from "#app/ui/abstact-option-select-ui-handler";
import { OptionSelectConfig, OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
import { SaveSlotUiMode } from "#app/ui/save-slot-select-ui-handler";
import { Mode } from "#app/ui/ui";
import i18next from "i18next";
import * as Utils from "#app/utils";
import { Modifier } from "#app/modifier/modifier";
import i18next from "i18next";
import { CheckSwitchPhase } from "./check-switch-phase";
import { EncounterPhase } from "./encounter-phase";
import { SelectChallengePhase } from "./select-challenge-phase";
@ -203,7 +203,7 @@ export class TitlePhase extends Phase {
const starters = getDailyRunStarters(this.scene, seed);
const startingLevel = this.scene.gameMode.getStartingLevel();
const party = this.scene.getParty();
const party = this.scene.getPlayerParty();
const loadPokemonAssets: Promise<void>[] = [];
for (const starter of starters) {
const starterProps = this.scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr);
@ -243,7 +243,7 @@ export class TitlePhase extends Phase {
};
// If Online, calls seed fetch from db to generate daily run. If Offline, generates a daily run based on current date.
if (!Utils.isLocal) {
if (!Utils.isLocal || Utils.isLocalServerConnected) {
fetchDailyRunSeed().then(seed => {
if (seed) {
generateDaily(seed);
@ -276,7 +276,7 @@ export class TitlePhase extends Phase {
this.scene.pushPhase(new EncounterPhase(this.scene, this.loaded));
if (this.loaded) {
const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()).length;
const availablePartyMembers = this.scene.getPokemonAllowedInBattle().length;
this.scene.pushPhase(new SummonPhase(this.scene, 0, true, true));
if (this.scene.currentBattle.double && availablePartyMembers > 1) {

View File

@ -16,9 +16,9 @@ export class ToggleDoublePositionPhase extends BattlePhase {
const playerPokemon = this.scene.getPlayerField().find(p => p.isActive(true));
if (playerPokemon) {
playerPokemon.setFieldPosition(this.double && this.scene.getParty().filter(p => p.isAllowedInBattle()).length > 1 ? FieldPosition.LEFT : FieldPosition.CENTER, 500).then(() => {
playerPokemon.setFieldPosition(this.double && this.scene.getPokemonAllowedInBattle().length > 1 ? FieldPosition.LEFT : FieldPosition.CENTER, 500).then(() => {
if (playerPokemon.getFieldIndex() === 1) {
const party = this.scene.getParty();
const party = this.scene.getPlayerParty();
party[1] = party[0];
party[0] = playerPokemon;
}

View File

@ -1,15 +1,15 @@
import BattleScene from "#app/battle-scene";
import { BattlerIndex } from "#app/battle";
import BattleScene from "#app/battle-scene";
import { handleMysteryEncounterBattleStartEffects, handleMysteryEncounterTurnStartEffects } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { TurnInitEvent } from "#app/events/battle-scene";
import { PlayerPokemon } from "#app/field/pokemon";
import i18next from "i18next";
import { FieldPhase } from "./field-phase";
import { ToggleDoublePositionPhase } from "./toggle-double-position-phase";
import { CommandPhase } from "./command-phase";
import { EnemyCommandPhase } from "./enemy-command-phase";
import { FieldPhase } from "./field-phase";
import { GameOverPhase } from "./game-over-phase";
import { ToggleDoublePositionPhase } from "./toggle-double-position-phase";
import { TurnStartPhase } from "./turn-start-phase";
import { handleMysteryEncounterBattleStartEffects, handleMysteryEncounterTurnStartEffects } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
export class TurnInitPhase extends FieldPhase {
constructor(scene: BattleScene) {
@ -24,7 +24,7 @@ export class TurnInitPhase extends FieldPhase {
if (p.isOnField() && !p.isAllowedInBattle()) {
this.scene.queueMessage(i18next.t("challenges:illegalEvolution", { "pokemon": p.name }), null, true);
const allowedPokemon = this.scene.getParty().filter(p => p.isAllowedInBattle());
const allowedPokemon = this.scene.getPokemonAllowedInBattle();
if (!allowedPokemon.length) {
// If there are no longer any legal pokemon in the party, game over.

View File

@ -0,0 +1,93 @@
import { SESSION_ID_COOKIE_NAME } from "#app/constants";
import { getCookie } from "#app/utils";
type DataType = "json" | "form-urlencoded";
export abstract class ApiBase {
//#region Fields
public readonly ERR_GENERIC: string = "There was an error";
protected readonly base: string;
//#region Public
constructor(base: string) {
this.base = base;
}
//#region Protected
/**
* Send a GET request.
* @param path The path to send the request to.
*/
protected async doGet(path: string) {
return this.doFetch(path, { method: "GET" });
}
/**
* Send a POST request.
* @param path THe path to send the request to.
* @param bodyData The body-data to send.
* @param dataType The data-type of the {@linkcode bodyData}.
*/
protected async doPost<D = undefined>(path: string, bodyData?: D, dataType: DataType = "json") {
let body: string | undefined = undefined;
const headers: HeadersInit = {};
if (bodyData) {
if (dataType === "json") {
body = typeof bodyData === "string" ? bodyData : JSON.stringify(bodyData);
headers["Content-Type"] = "application/json";
} else if (dataType === "form-urlencoded") {
if (bodyData instanceof Object) {
body = this.toUrlSearchParams(bodyData).toString();
} else {
console.warn("Could not add body data to form-urlencoded!", bodyData);
}
headers["Content-Type"] = "application/x-www-form-urlencoded";
} else {
console.warn(`Unsupported data type: ${dataType}`);
body = String(bodyData);
headers["Content-Type"] = "text/plain";
}
}
return await this.doFetch(path, { method: "POST", body, headers });
}
/**
* A generic request helper.
* @param path The path to send the request to.
* @param config The request {@linkcode RequestInit | Configuration}.
*/
protected async doFetch(path: string, config: RequestInit): Promise<Response> {
config.headers = {
...config.headers,
Authorization: getCookie(SESSION_ID_COOKIE_NAME),
"Content-Type": config.headers?.["Content-Type"] ?? "application/json",
};
if (import.meta.env.DEV) {
console.log(`Sending ${config.method ?? "GET"} request to: `, this.base + path, config);
}
return await fetch(this.base + path, config);
}
/**
* Helper to transform data to {@linkcode URLSearchParams}
* Any key with a value of `undefined` will be ignored.
* Any key with a value of `null` will be included.
* @param data the data to transform to {@linkcode URLSearchParams}
* @returns a {@linkcode URLSearchParams} representaton of {@linkcode data}
*/
protected toUrlSearchParams<D extends Record<string, any>>(data: D) {
const arr = Object.entries(data)
.map(([ key, value ]) => (value !== undefined ? [ key, String(value) ] : [ key, "" ]))
.filter(([ , value ]) => value !== "");
return new URLSearchParams(arr);
}
}

View File

@ -0,0 +1,101 @@
import type {
AccountInfoResponse,
AccountLoginRequest,
AccountLoginResponse,
AccountRegisterRequest,
} from "#app/@types/PokerogueAccountApi";
import { SESSION_ID_COOKIE_NAME } from "#app/constants";
import { ApiBase } from "#app/plugins/api/api-base";
import { removeCookie, setCookie } from "#app/utils";
/**
* A wrapper for PokéRogue account API requests.
*/
export class PokerogueAccountApi extends ApiBase {
//#region Public
/**
* Request the {@linkcode AccountInfoResponse | UserInfo} of the logged in user.
* The user is identified by the {@linkcode SESSION_ID_COOKIE_NAME | session cookie}.
*/
public async getInfo(): Promise<[data: AccountInfoResponse | null, status: number]> {
try {
const response = await this.doGet("/account/info");
if (response.ok) {
const resData = (await response.json()) as AccountInfoResponse;
return [ resData, response.status ];
} else {
console.warn("Could not get account info!", response.status, response.statusText);
return [ null, response.status ];
}
} catch (err) {
console.warn("Could not get account info!", err);
return [ null, 500 ];
}
}
/**
* Register a new account.
* @param registerData The {@linkcode AccountRegisterRequest} to send
* @returns An error message if something went wrong
*/
public async register(registerData: AccountRegisterRequest) {
try {
const response = await this.doPost("/account/register", registerData, "form-urlencoded");
if (response.ok) {
return null;
} else {
return response.text();
}
} catch (err) {
console.warn("Register failed!", err);
}
return "Unknown error!";
}
/**
* Send a login request.
* Sets the session cookie on success.
* @param loginData The {@linkcode AccountLoginRequest} to send
* @returns An error message if something went wrong
*/
public async login(loginData: AccountLoginRequest) {
try {
const response = await this.doPost("/account/login", loginData, "form-urlencoded");
if (response.ok) {
const loginResponse = (await response.json()) as AccountLoginResponse;
setCookie(SESSION_ID_COOKIE_NAME, loginResponse.token);
return null;
} else {
console.warn("Login failed!", response.status, response.statusText);
return response.text();
}
} catch (err) {
console.warn("Login failed!", err);
}
return "Unknown error!";
}
/**
* Send a logout request.
* **Always** (no matter if failed or not) removes the session cookie.
*/
public async logout() {
try {
const response = await this.doGet("/account/logout");
if (!response.ok) {
throw new Error(`${response.status}: ${response.statusText}`);
}
} catch (err) {
console.warn("Log out failed!", err);
}
removeCookie(SESSION_ID_COOKIE_NAME); // we are always clearing the cookie.
}
}

View File

@ -0,0 +1,140 @@
import type {
LinkAccountToDiscordIdRequest,
LinkAccountToGoogledIdRequest,
SearchAccountRequest,
SearchAccountResponse,
UnlinkAccountFromDiscordIdRequest,
UnlinkAccountFromGoogledIdRequest,
} from "#app/@types/PokerogueAdminApi";
import { ApiBase } from "#app/plugins/api/api-base";
export class PokerogueAdminApi extends ApiBase {
public readonly ERR_USERNAME_NOT_FOUND: string = "Username not found!";
/**
* Links an account to a discord id.
* @param params The {@linkcode LinkAccountToDiscordIdRequest} to send
* @returns `null` if successful, error message if not
*/
public async linkAccountToDiscord(params: LinkAccountToDiscordIdRequest) {
try {
const response = await this.doPost("/admin/account/discordLink", params, "form-urlencoded");
if (response.ok) {
return null;
} else {
console.warn("Could not link account with discord!", response.status, response.statusText);
if (response.status === 404) {
return this.ERR_USERNAME_NOT_FOUND;
}
}
} catch (err) {
console.warn("Could not link account with discord!", err);
}
return this.ERR_GENERIC;
}
/**
* Unlinks an account from a discord id.
* @param params The {@linkcode UnlinkAccountFromDiscordIdRequest} to send
* @returns `null` if successful, error message if not
*/
public async unlinkAccountFromDiscord(params: UnlinkAccountFromDiscordIdRequest) {
try {
const response = await this.doPost("/admin/account/discordUnlink", params, "form-urlencoded");
if (response.ok) {
return null;
} else {
console.warn("Could not unlink account from discord!", response.status, response.statusText);
if (response.status === 404) {
return this.ERR_USERNAME_NOT_FOUND;
}
}
} catch (err) {
console.warn("Could not unlink account from discord!", err);
}
return this.ERR_GENERIC;
}
/**
* Links an account to a google id.
* @param params The {@linkcode LinkAccountToGoogledIdRequest} to send
* @returns `null` if successful, error message if not
*/
public async linkAccountToGoogleId(params: LinkAccountToGoogledIdRequest) {
try {
const response = await this.doPost("/admin/account/googleLink", params, "form-urlencoded");
if (response.ok) {
return null;
} else {
console.warn("Could not link account with google!", response.status, response.statusText);
if (response.status === 404) {
return this.ERR_USERNAME_NOT_FOUND;
}
}
} catch (err) {
console.warn("Could not link account with google!", err);
}
return this.ERR_GENERIC;
}
/**
* Unlinks an account from a google id.
* @param params The {@linkcode UnlinkAccountFromGoogledIdRequest} to send
* @returns `null` if successful, error message if not
*/
public async unlinkAccountFromGoogleId(params: UnlinkAccountFromGoogledIdRequest) {
try {
const response = await this.doPost("/admin/account/googleUnlink", params, "form-urlencoded");
if (response.ok) {
return null;
} else {
console.warn("Could not unlink account from google!", response.status, response.statusText);
if (response.status === 404) {
return this.ERR_USERNAME_NOT_FOUND;
}
}
} catch (err) {
console.warn("Could not unlink account from google!", err);
}
return this.ERR_GENERIC;
}
/**
* Search an account.
* @param params The {@linkcode SearchAccountRequest} to send
* @returns an array of {@linkcode SearchAccountResponse} and error. Both can be `undefined`
*/
public async searchAccount(params: SearchAccountRequest): Promise<[data?: SearchAccountResponse, error?: string]> {
try {
const urlSearchParams = this.toUrlSearchParams(params);
const response = await this.doGet(`/admin/account/adminSearch?${urlSearchParams}`);
if (response.ok) {
const resData: SearchAccountResponse = await response.json();
return [ resData, undefined ];
} else {
console.warn("Could not find account!", response.status, response.statusText);
if (response.status === 404) {
return [ undefined, this.ERR_USERNAME_NOT_FOUND ];
}
}
} catch (err) {
console.warn("Could not find account!", err);
}
return [ undefined, this.ERR_GENERIC ];
}
}

View File

@ -0,0 +1,83 @@
import type { TitleStatsResponse } from "#app/@types/PokerogueApi";
import { ApiBase } from "#app/plugins/api/api-base";
import { PokerogueAccountApi } from "#app/plugins/api/pokerogue-account-api";
import { PokerogueAdminApi } from "#app/plugins/api/pokerogue-admin-api";
import { PokerogueDailyApi } from "#app/plugins/api/pokerogue-daily-api";
import { PokerogueSavedataApi } from "#app/plugins/api/pokerogue-savedata-api";
/**
* A wrapper for PokéRogue API requests.
*/
export class PokerogueApi extends ApiBase {
//#region Fields∏
public readonly account: PokerogueAccountApi;
public readonly daily: PokerogueDailyApi;
public readonly admin: PokerogueAdminApi;
public readonly savedata: PokerogueSavedataApi;
//#region Public
constructor(base: string) {
super(base);
this.account = new PokerogueAccountApi(base);
this.daily = new PokerogueDailyApi(base);
this.admin = new PokerogueAdminApi(base);
this.savedata = new PokerogueSavedataApi(base);
}
/**
* Request game title-stats.
*/
public async getGameTitleStats() {
try {
const response = await this.doGet("/game/titlestats");
return (await response.json()) as TitleStatsResponse;
} catch (err) {
console.warn("Could not get game title stats!", err);
return null;
}
}
/**
* Unlink the currently logged in user from Discord.
* @returns `true` if unlinking was successful, `false` if not
*/
public async unlinkDiscord() {
try {
const response = await this.doPost("/auth/discord/logout");
if (response.ok) {
return true;
} else {
console.warn(`Discord unlink failed (${response.status}: ${response.statusText})`);
}
} catch (err) {
console.warn("Could not unlink Discord!", err);
}
return false;
}
/**
* Unlink the currently logged in user from Google.
* @returns `true` if unlinking was successful, `false` if not
*/
public async unlinkGoogle() {
try {
const response = await this.doPost("/auth/google/logout");
if (response.ok) {
return true;
} else {
console.warn(`Google unlink failed (${response.status}: ${response.statusText})`);
}
} catch (err) {
console.warn("Could not unlink Google!", err);
}
return false;
}
//#endregion
}
export const pokerogueApi = new PokerogueApi(import.meta.env.VITE_SERVER_URL ?? "http://localhost:8001");

View File

@ -0,0 +1,57 @@
import type { GetDailyRankingsPageCountRequest, GetDailyRankingsRequest } from "#app/@types/PokerogueDailyApi";
import { ApiBase } from "#app/plugins/api/api-base";
import type { RankingEntry } from "#app/ui/daily-run-scoreboard";
/**
* A wrapper for daily-run PokéRogue API requests.
*/
export class PokerogueDailyApi extends ApiBase {
//#region Public
/**
* Request the daily-run seed.
* @returns The active daily-run seed as `string`.
*/
public async getSeed() {
try {
const response = await this.doGet("/daily/seed");
return response.text();
} catch (err) {
console.warn("Could not get daily-run seed!", err);
return null;
}
}
/**
* Get the daily rankings for a {@linkcode ScoreboardCategory}.
* @param params The {@linkcode GetDailyRankingsRequest} to send
*/
public async getRankings(params: GetDailyRankingsRequest) {
try {
const urlSearchParams = this.toUrlSearchParams(params);
const response = await this.doGet(`/daily/rankings?${urlSearchParams}`);
return (await response.json()) as RankingEntry[];
} catch (err) {
console.warn("Could not get daily rankings!", err);
return null;
}
}
/**
* Get the page count of the daily rankings for a {@linkcode ScoreboardCategory}.
* @param params The {@linkcode GetDailyRankingsPageCountRequest} to send.
*/
public async getRankingsPageCount(params: GetDailyRankingsPageCountRequest) {
try {
const urlSearchParams = this.toUrlSearchParams(params);
const response = await this.doGet(`/daily/rankingpagecount?${urlSearchParams}`);
const json = await response.json();
return Number(json);
} catch (err) {
console.warn("Could not get daily rankings page count!", err);
return 1;
}
}
}

View File

@ -0,0 +1,41 @@
import type { UpdateAllSavedataRequest } from "#app/@types/PokerogueSavedataApi";
import { MAX_INT_ATTR_VALUE } from "#app/constants";
import { ApiBase } from "#app/plugins/api/api-base";
import { PokerogueSessionSavedataApi } from "#app/plugins/api/pokerogue-session-savedata-api";
import { PokerogueSystemSavedataApi } from "#app/plugins/api/pokerogue-system-savedata-api";
/**
* A wrapper for PokéRogue savedata API requests.
*/
export class PokerogueSavedataApi extends ApiBase {
//#region Fields
public readonly system: PokerogueSystemSavedataApi;
public readonly session: PokerogueSessionSavedataApi;
//#region Public
constructor(base: string) {
super(base);
this.system = new PokerogueSystemSavedataApi(base);
this.session = new PokerogueSessionSavedataApi(base);
}
/**
* Update all savedata
* @param bodyData The {@linkcode UpdateAllSavedataRequest | request data} to send
* @returns An error message if something went wrong
*/
public async updateAll(bodyData: UpdateAllSavedataRequest) {
try {
const rawBodyData = JSON.stringify(bodyData, (_k: any, v: any) =>
typeof v === "bigint" ? (v <= MAX_INT_ATTR_VALUE ? Number(v) : v.toString()) : v
);
const response = await this.doPost("/savedata/updateall", rawBodyData);
return await response.text();
} catch (err) {
console.warn("Could not update all savedata!", err);
return "Unknown error";
}
}
}

View File

@ -0,0 +1,115 @@
import type {
ClearSessionSavedataRequest,
ClearSessionSavedataResponse,
DeleteSessionSavedataRequest,
GetSessionSavedataRequest,
NewClearSessionSavedataRequest,
UpdateSessionSavedataRequest,
} from "#app/@types/PokerogueSessionSavedataApi";
import { ApiBase } from "#app/plugins/api/api-base";
import type { SessionSaveData } from "#app/system/game-data";
/**
* A wrapper for PokéRogue session savedata API requests.
*/
export class PokerogueSessionSavedataApi extends ApiBase {
//#region Public
/**
* Mark a session as cleared aka "newclear".\
* *This is **NOT** the same as {@linkcode clear | clear()}.*
* @param params The {@linkcode NewClearSessionSavedataRequest} to send
* @returns The raw savedata as `string`.
*/
public async newclear(params: NewClearSessionSavedataRequest) {
try {
const urlSearchParams = this.toUrlSearchParams(params);
const response = await this.doGet(`/savedata/session/newclear?${urlSearchParams}`);
const json = await response.json();
return Boolean(json);
} catch (err) {
console.warn("Could not newclear session!", err);
return false;
}
}
/**
* Get a session savedata.
* @param params The {@linkcode GetSessionSavedataRequest} to send
* @returns The session as `string`
*/
public async get(params: GetSessionSavedataRequest) {
try {
const urlSearchParams = this.toUrlSearchParams(params);
const response = await this.doGet(`/savedata/session/get?${urlSearchParams}`);
return await response.text();
} catch (err) {
console.warn("Could not get session savedata!", err);
return null;
}
}
/**
* Update a session savedata.
* @param params The {@linkcode UpdateSessionSavedataRequest} to send
* @param rawSavedata The raw savedata (as `string`)
* @returns An error message if something went wrong
*/
public async update(params: UpdateSessionSavedataRequest, rawSavedata: string) {
try {
const urlSearchParams = this.toUrlSearchParams(params);
const response = await this.doPost(`/savedata/session/update?${urlSearchParams}`, rawSavedata);
return await response.text();
} catch (err) {
console.warn("Could not update session savedata!", err);
}
return "Unknown Error!";
}
/**
* Delete a session savedata slot.
* @param params The {@linkcode DeleteSessionSavedataRequest} to send
* @returns An error message if something went wrong
*/
public async delete(params: DeleteSessionSavedataRequest) {
try {
const urlSearchParams = this.toUrlSearchParams(params);
const response = await this.doGet(`/savedata/session/delete?${urlSearchParams}`);
if (response.ok) {
return null;
} else {
return await response.text();
}
} catch (err) {
console.warn("Could not delete session savedata!", err);
return "Unknown error";
}
}
/**
* Clears the session savedata of the given slot.\
* *This is **NOT** the same as {@linkcode newclear | newclear()}.*
* @param params The {@linkcode ClearSessionSavedataRequest} to send
* @param sessionData The {@linkcode SessionSaveData} object
*/
public async clear(params: ClearSessionSavedataRequest, sessionData: SessionSaveData) {
try {
const urlSearchParams = this.toUrlSearchParams(params);
const response = await this.doPost(`/savedata/session/clear?${urlSearchParams}`, sessionData);
return (await response.json()) as ClearSessionSavedataResponse;
} catch (err) {
console.warn("Could not clear session savedata!", err);
}
return {
error: "Unknown error",
success: false,
} as ClearSessionSavedataResponse;
}
}

View File

@ -0,0 +1,77 @@
import type {
GetSystemSavedataRequest,
UpdateSystemSavedataRequest,
VerifySystemSavedataRequest,
VerifySystemSavedataResponse,
} from "#app/@types/PokerogueSystemSavedataApi";
import { ApiBase } from "#app/plugins/api/api-base";
/**
* A wrapper for PokéRogue system savedata API requests.
*/
export class PokerogueSystemSavedataApi extends ApiBase {
//#region Public
/**
* Get a system savedata.
* @param params The {@linkcode GetSystemSavedataRequest} to send
* @returns The system savedata as `string` or `null` on error
*/
public async get(params: GetSystemSavedataRequest) {
try {
const urlSearchParams = this.toUrlSearchParams(params);
const response = await this.doGet(`/savedata/system/get?${urlSearchParams}`);
const rawSavedata = await response.text();
return rawSavedata;
} catch (err) {
console.warn("Could not get system savedata!", err);
return null;
}
}
/**
* Verify if the session is valid.
* If not the {@linkcode SystemSaveData} is returned.
* @param params The {@linkcode VerifySystemSavedataRequest} to send
* @returns A {@linkcode SystemSaveData} if **NOT** valid, otherwise `null`.
*
* TODO: add handling for errors
*/
public async verify(params: VerifySystemSavedataRequest) {
const urlSearchParams = this.toUrlSearchParams(params);
const response = await this.doGet(`/savedata/system/verify?${urlSearchParams}`);
if (response.ok) {
const verifySavedata = (await response.json()) as VerifySystemSavedataResponse;
if (!verifySavedata.valid) {
console.warn("Invalid system savedata!");
return verifySavedata.systemData;
}
} else {
console.warn("System savedata verification failed!", response.status, response.statusText);
}
return null;
}
/**
* Update a system savedata.
* @param params The {@linkcode UpdateSystemSavedataRequest} to send
* @param rawSystemData The raw {@linkcode SystemSaveData}
* @returns An error message if something went wrong
*/
public async update(params: UpdateSystemSavedataRequest, rawSystemData: string) {
try {
const urSearchParams = this.toUrlSearchParams(params);
const response = await this.doPost(`/savedata/system/update?${urSearchParams}`, rawSystemData);
return await response.text();
} catch (err) {
console.warn("Could not update system savedata!", err);
}
return "Unknown Error";
}
}

View File

@ -330,7 +330,7 @@ export const achvs = {
HIDDEN_ABILITY: new Achv("HIDDEN_ABILITY", "", "HIDDEN_ABILITY.description", "ability_charm", 75),
PERFECT_IVS: new Achv("PERFECT_IVS", "", "PERFECT_IVS.description", "blunder_policy", 100),
CLASSIC_VICTORY: new Achv("CLASSIC_VICTORY", "", "CLASSIC_VICTORY.description", "relic_crown", 150, c => c.gameData.gameStats.sessionsWon === 0),
UNEVOLVED_CLASSIC_VICTORY: new Achv("UNEVOLVED_CLASSIC_VICTORY", "", "UNEVOLVED_CLASSIC_VICTORY.description", "eviolite", 175, c => c.getParty().some(p => p.getSpeciesForm(true).speciesId in pokemonEvolutions)),
UNEVOLVED_CLASSIC_VICTORY: new Achv("UNEVOLVED_CLASSIC_VICTORY", "", "UNEVOLVED_CLASSIC_VICTORY.description", "eviolite", 175, c => c.getPlayerParty().some(p => p.getSpeciesForm(true).speciesId in pokemonEvolutions)),
MONO_GEN_ONE_VICTORY: new ChallengeAchv("MONO_GEN_ONE", "", "MONO_GEN_ONE.description", "ribbon_gen1", 100, (c, { gameMode }) => c instanceof SingleGenerationChallenge && c.value === 1 && !gameMode.hasChallenge(Challenges.INVERSE_BATTLE) && !gameMode.hasChallenge(Challenges.TRICK_ROOM)),
MONO_GEN_TWO_VICTORY: new ChallengeAchv("MONO_GEN_TWO", "", "MONO_GEN_TWO.description", "ribbon_gen2", 100, (c, { gameMode }) => c instanceof SingleGenerationChallenge && c.value === 2 && !gameMode.hasChallenge(Challenges.INVERSE_BATTLE) && !gameMode.hasChallenge(Challenges.TRICK_ROOM)),
MONO_GEN_THREE_VICTORY: new ChallengeAchv("MONO_GEN_THREE", "", "MONO_GEN_THREE.description", "ribbon_gen3", 100, (c, { gameMode }) => c instanceof SingleGenerationChallenge && c.value === 3 && !gameMode.hasChallenge(Challenges.INVERSE_BATTLE) && !gameMode.hasChallenge(Challenges.TRICK_ROOM)),

View File

@ -48,7 +48,7 @@ import { RUN_HISTORY_LIMIT } from "#app/ui/run-history-ui-handler";
import { applySessionVersionMigration, applySystemVersionMigration, applySettingsVersionMigration } from "./version_migration/version_converter";
import { MysteryEncounterSaveData } from "#app/data/mystery-encounters/mystery-encounter-save-data";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PokerogueApiClearSessionData } from "#app/@types/pokerogue-api";
import { pokerogueApi } from "#app/plugins/api/pokerogue-api";
import { ArenaTrapTag } from "#app/data/arena-tag";
export const defaultStarterSpecies: Species[] = [
@ -397,8 +397,7 @@ export class GameData {
localStorage.setItem(`data_${loggedInUser?.username}`, encrypt(systemData, bypassLogin));
if (!bypassLogin) {
Utils.apiPost(`savedata/system/update?clientSessionId=${clientSessionId}`, systemData, undefined, true)
.then(response => response.text())
pokerogueApi.savedata.system.update({ clientSessionId }, systemData)
.then(error => {
this.scene.ui.savingIcon.hide();
if (error) {
@ -428,23 +427,22 @@ export class GameData {
}
if (!bypassLogin) {
Utils.apiFetch(`savedata/system/get?clientSessionId=${clientSessionId}`, true)
.then(response => response.text())
.then(response => {
if (!response.length || response[0] !== "{") {
if (response.startsWith("sql: no rows in result set")) {
pokerogueApi.savedata.system.get({ clientSessionId })
.then(saveDataOrErr => {
if (!saveDataOrErr || saveDataOrErr.length === 0 || saveDataOrErr[0] !== "{") {
if (saveDataOrErr?.startsWith("sql: no rows in result set")) {
this.scene.queueMessage("Save data could not be found. If this is a new account, you can safely ignore this message.", null, true);
return resolve(true);
} else if (response.indexOf("Too many connections") > -1) {
} else if (saveDataOrErr?.includes("Too many connections")) {
this.scene.queueMessage("Too many people are trying to connect and the server is overloaded. Please try again later.", null, true);
return resolve(false);
}
console.error(response);
console.error(saveDataOrErr);
return resolve(false);
}
const cachedSystem = localStorage.getItem(`data_${loggedInUser?.username}`);
this.initSystem(response, cachedSystem ? AES.decrypt(cachedSystem, saveKey).toString(enc.Utf8) : undefined).then(resolve);
this.initSystem(saveDataOrErr, cachedSystem ? AES.decrypt(cachedSystem, saveKey).toString(enc.Utf8) : undefined).then(resolve);
});
} else {
this.initSystem(decrypt(localStorage.getItem(`data_${loggedInUser?.username}`)!, bypassLogin)).then(resolve); // TODO: is this bang correct?
@ -580,6 +578,7 @@ export class GameData {
if (!Utils.isLocal) {
/**
* Networking Code DO NOT DELETE!
* Note: Might have to be migrated to `pokerogue-api.ts`
*
const response = await Utils.apiFetch("savedata/runHistory", true);
const data = await response.json();
@ -660,6 +659,7 @@ export class GameData {
return false;
}
}
NOTE: should be adopted to `pokerogue-api.ts`
*/
return true;
}
@ -704,12 +704,11 @@ export class GameData {
return true;
}
const response = await Utils.apiFetch(`savedata/system/verify?clientSessionId=${clientSessionId}`, true)
.then(response => response.json());
const systemData = await pokerogueApi.savedata.system.verify({ clientSessionId });
if (!response.valid) {
if (systemData) {
this.scene.clearPhaseQueue();
this.scene.unshiftPhase(new ReloadSessionPhase(this.scene, JSON.stringify(response.systemData)));
this.scene.unshiftPhase(new ReloadSessionPhase(this.scene, JSON.stringify(systemData)));
this.clearLocalData();
return false;
}
@ -949,7 +948,7 @@ export class GameData {
seed: scene.seed,
playTime: scene.sessionPlayTime,
gameMode: scene.gameMode.modeId,
party: scene.getParty().map(p => new PokemonData(p)),
party: scene.getPlayerParty().map(p => new PokemonData(p)),
enemyParty: scene.getEnemyParty().map(p => new PokemonData(p)),
modifiers: scene.findModifiers(() => true).map(m => new PersistentModifierData(m, true)),
enemyModifiers: scene.findModifiers(() => true, false).map(m => new PersistentModifierData(m, false)),
@ -984,10 +983,9 @@ export class GameData {
};
if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`)) {
Utils.apiFetch(`savedata/session/get?slot=${slotId}&clientSessionId=${clientSessionId}`, true)
.then(response => response.text())
pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId })
.then(async response => {
if (!response.length || response[0] !== "{") {
if (!response || response?.length === 0 || response?.[0] !== "{") {
console.error(response);
return resolve(null);
}
@ -1028,7 +1026,7 @@ export class GameData {
const loadPokemonAssets: Promise<void>[] = [];
const party = scene.getParty();
const party = scene.getPlayerParty();
party.splice(0, party.length);
for (const p of sessionData.party) {
@ -1149,14 +1147,7 @@ export class GameData {
if (success !== null && !success) {
return resolve(false);
}
Utils.apiFetch(`savedata/session/delete?slot=${slotId}&clientSessionId=${clientSessionId}`, true).then(response => {
if (response.ok) {
loggedInUser!.lastSessionSlot = -1; // TODO: is the bang correct?
localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`);
resolve(true);
}
return response.text();
}).then(error => {
pokerogueApi.savedata.session.delete({ slot: slotId, clientSessionId }).then(error => {
if (error) {
if (error.startsWith("session out of date")) {
this.scene.clearPhaseQueue();
@ -1164,8 +1155,15 @@ export class GameData {
}
console.error(error);
resolve(false);
} else {
if (loggedInUser) {
loggedInUser.lastSessionSlot = -1;
}
localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`);
resolve(true);
}
});
});
});
@ -1215,17 +1213,15 @@ export class GameData {
result = [ true, true ];
} else {
const sessionData = this.getSessionSaveData(scene);
const response = await Utils.apiPost(`savedata/session/clear?slot=${slotId}&trainerId=${this.trainerId}&secretId=${this.secretId}&clientSessionId=${clientSessionId}`, JSON.stringify(sessionData), undefined, true);
const { trainerId } = this;
const jsonResponse = await pokerogueApi.savedata.session.clear({ slot: slotId, trainerId, clientSessionId }, sessionData);
if (response.ok) {
loggedInUser!.lastSessionSlot = -1; // TODO: is the bang correct?
localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`);
if (!jsonResponse?.error) {
result = [ true, jsonResponse?.success ?? false ];
if (loggedInUser) {
loggedInUser!.lastSessionSlot = -1;
}
const jsonResponse: PokerogueApiClearSessionData = await response.json();
if (!jsonResponse.error) {
result = [ true, jsonResponse.success ?? false ];
localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`);
} else {
if (jsonResponse && jsonResponse.error?.startsWith("session out of date")) {
this.scene.clearPhaseQueue();
@ -1342,8 +1338,7 @@ export class GameData {
console.debug("Session data saved");
if (!bypassLogin && sync) {
Utils.apiPost("savedata/updateall", JSON.stringify(request, (k: any, v: any) => typeof v === "bigint" ? v <= maxIntAttrValue ? Number(v) : v.toString() : v), undefined, true)
.then(response => response.text())
pokerogueApi.savedata.updateAll(request)
.then(error => {
if (sync) {
this.scene.lastSavePlayTime = 0;
@ -1387,10 +1382,16 @@ export class GameData {
link.remove();
};
if (!bypassLogin && dataType < GameDataType.SETTINGS) {
Utils.apiFetch(`savedata/${dataType === GameDataType.SYSTEM ? "system" : "session"}/get?clientSessionId=${clientSessionId}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ""}`, true)
.then(response => response.text())
.then(response => {
if (!response.length || response[0] !== "{") {
let promise: Promise<string | null> = Promise.resolve(null);
if (dataType === GameDataType.SYSTEM) {
promise = pokerogueApi.savedata.system.get({ clientSessionId });
} else if (dataType === GameDataType.SESSION) {
promise = pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId });
}
promise.then(response => {
if (!response?.length || response[0] !== "{") {
console.error(response);
resolve(false);
return;
@ -1477,14 +1478,14 @@ export class GameData {
if (!success[0]) {
return displayError(`Could not contact the server. Your ${dataName} data could not be imported.`);
}
let url: string;
const { trainerId, secretId } = this;
let updatePromise: Promise<string | null>;
if (dataType === GameDataType.SESSION) {
url = `savedata/session/update?slot=${slotId}&trainerId=${this.trainerId}&secretId=${this.secretId}&clientSessionId=${clientSessionId}`;
updatePromise = pokerogueApi.savedata.session.update({ slot: slotId, trainerId, secretId, clientSessionId }, dataStr);
} else {
url = `savedata/system/update?trainerId=${this.trainerId}&secretId=${this.secretId}&clientSessionId=${clientSessionId}`;
updatePromise = pokerogueApi.savedata.system.update({ trainerId, secretId, clientSessionId }, dataStr);
}
Utils.apiPost(url, dataStr, undefined, true)
.then(response => response.text())
updatePromise
.then(error => {
if (error) {
console.error(error);
@ -1829,17 +1830,40 @@ export class GameData {
return starterCount;
}
getSpeciesDefaultDexAttr(species: PokemonSpecies, forSeen: boolean = false, optimistic: boolean = false): bigint {
getSpeciesDefaultDexAttr(species: PokemonSpecies, _forSeen: boolean = false, optimistic: boolean = false): bigint {
let ret = 0n;
const dexEntry = this.dexData[species.speciesId];
const attr = dexEntry.caughtAttr;
ret |= optimistic
? attr & DexAttr.SHINY ? DexAttr.SHINY : DexAttr.NON_SHINY
: attr & DexAttr.NON_SHINY || !(attr & DexAttr.SHINY) ? DexAttr.NON_SHINY : DexAttr.SHINY;
if (optimistic) {
if (attr & DexAttr.SHINY) {
ret |= DexAttr.SHINY;
if (attr & DexAttr.VARIANT_3) {
ret |= DexAttr.VARIANT_3;
} else if (attr & DexAttr.VARIANT_2) {
ret |= DexAttr.VARIANT_2;
} else {
ret |= DexAttr.DEFAULT_VARIANT;
}
} else {
ret |= DexAttr.NON_SHINY;
ret |= DexAttr.DEFAULT_VARIANT;
}
} else {
// Default to non shiny. Fallback to shiny if it's the only thing that's unlocked
ret |= (attr & DexAttr.NON_SHINY || !(attr & DexAttr.SHINY)) ? DexAttr.NON_SHINY : DexAttr.SHINY;
if (attr & DexAttr.DEFAULT_VARIANT) {
ret |= DexAttr.DEFAULT_VARIANT;
} else if (attr & DexAttr.VARIANT_2) {
ret |= DexAttr.VARIANT_2;
} else if (attr & DexAttr.VARIANT_3) {
ret |= DexAttr.VARIANT_3;
} else {
ret |= DexAttr.DEFAULT_VARIANT;
}
}
ret |= attr & DexAttr.MALE || !(attr & DexAttr.FEMALE) ? DexAttr.MALE : DexAttr.FEMALE;
ret |= optimistic
? attr & DexAttr.SHINY ? attr & DexAttr.VARIANT_3 ? DexAttr.VARIANT_3 : attr & DexAttr.VARIANT_2 ? DexAttr.VARIANT_2 : DexAttr.DEFAULT_VARIANT : DexAttr.DEFAULT_VARIANT
: attr & DexAttr.DEFAULT_VARIANT ? DexAttr.DEFAULT_VARIANT : attr & DexAttr.VARIANT_2 ? DexAttr.VARIANT_2 : attr & DexAttr.VARIANT_3 ? DexAttr.VARIANT_3 : DexAttr.DEFAULT_VARIANT;
ret |= this.getFormAttr(this.getFormIndex(attr));
return ret;
}
@ -1847,7 +1871,14 @@ export class GameData {
getSpeciesDexAttrProps(species: PokemonSpecies, dexAttr: bigint): DexAttrProps {
const shiny = !(dexAttr & DexAttr.NON_SHINY);
const female = !(dexAttr & DexAttr.MALE);
const variant = dexAttr & DexAttr.DEFAULT_VARIANT ? 0 : dexAttr & DexAttr.VARIANT_2 ? 1 : dexAttr & DexAttr.VARIANT_3 ? 2 : 0;
let variant: Variant = 0;
if (dexAttr & DexAttr.DEFAULT_VARIANT) {
variant = 0;
} else if (dexAttr & DexAttr.VARIANT_2) {
variant = 1;
} else if (dexAttr & DexAttr.VARIANT_3) {
variant = 2;
}
const formIndex = this.getFormIndex(dexAttr);
return {

View File

@ -38,7 +38,7 @@ export default class ModifierData {
type.id = this.typeId;
if (type instanceof ModifierTypeGenerator) {
type = (type as ModifierTypeGenerator).generateType(this.player ? scene.getParty() : scene.getEnemyField(), this.typePregenArgs);
type = (type as ModifierTypeGenerator).generateType(this.player ? scene.getPlayerParty() : scene.getEnemyField(), this.typePregenArgs);
}
const ret = Reflect.construct(constructor, ([ type ] as any[]).concat(this.args).concat(this.stackCount)) as PersistentModifier;

View File

@ -163,6 +163,11 @@ export const SettingKeys = {
Shop_Overlay_Opacity: "SHOP_OVERLAY_OPACITY"
};
export enum MusicPreference {
CONSISTENT,
MIXED
}
/**
* All Settings not related to controls
*/
@ -634,7 +639,7 @@ export const Setting: Array<Setting> = [
label: i18next.t("settings:mixed")
}
],
default: 0,
default: MusicPreference.MIXED,
type: SettingType.AUDIO,
requireReload: true
},

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