pokerogue/src/phases.ts

5460 lines
203 KiB
TypeScript
Raw Normal View History

import BattleScene, { bypassLogin } from "./battle-scene";
2024-03-01 01:08:50 +00:00
import { default as Pokemon, PlayerPokemon, EnemyPokemon, PokemonMove, MoveResult, DamageResult, FieldPosition, HitResult, TurnMove } from "./field/pokemon";
import * as Utils from "./utils";
import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, VariableAccuracyAttr, MoveTarget, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, PreMoveMessageAttr, HealStatusEffectAttr, IgnoreOpponentStatChangesAttr, NoEffectAttr, BypassRedirectAttr, FixedDamageAttr, PostVictoryStatChangeAttr, OneHitKOAccuracyAttr, ForceSwitchOutAttr, VariableTargetAttr, IncrementMovePriorityAttr } from "./data/move";
import { Mode } from "./ui/ui";
2023-04-10 19:12:01 +01:00
import { Command } from "./ui/command-ui-handler";
2023-04-20 20:46:05 +01:00
import { Stat } from "./data/pokemon-stat";
import { BerryModifier, ContactHeldItemTransferChanceModifier, EnemyAttackStatusEffectChanceModifier, EnemyPersistentModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, HealingBoosterModifier, HitHealModifier, LapsingPersistentModifier, MapModifier, Modifier, MultipleParticipantExpBonusModifier, PersistentModifier, PokemonExpBoosterModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, SwitchEffectTransferModifier, TempBattleStatBoosterModifier, TurnHealModifier, TurnHeldItemTransferModifier, MoneyMultiplierModifier, MoneyInterestModifier, IvScannerModifier, LapsingPokemonHeldItemModifier, PokemonMultiHitModifier, PokemonMoveAccuracyBoosterModifier, overrideModifiers, overrideHeldItems, BypassSpeedChanceModifier, TurnStatusEffectModifier } from "./modifier/modifier";
2023-04-11 16:04:39 +01:00
import PartyUiHandler, { PartyOption, PartyUiMode } from "./ui/party-ui-handler";
2023-07-04 22:50:51 +01:00
import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, PokeballType } from "./data/pokeball";
2023-04-20 20:46:05 +01:00
import { CommonAnim, CommonBattleAnim, MoveAnim, initMoveAnim, loadMoveAnimAssets } from "./data/battle-anims";
2023-05-19 21:13:11 +01:00
import { StatusEffect, getStatusEffectActivationText, getStatusEffectCatchRateMultiplier, getStatusEffectHealText, getStatusEffectObtainText, getStatusEffectOverlapText } from "./data/status-effect";
2023-04-10 19:12:01 +01:00
import { SummaryUiMode } from "./ui/summary-ui-handler";
import EvolutionSceneHandler from "./ui/evolution-scene-handler";
import { EvolutionPhase } from "./evolution-phase";
import { Phase } from "./phase";
2023-04-20 20:46:05 +01:00
import { BattleStat, getBattleStatLevelChangeDescription, getBattleStatName } from "./data/battle-stat";
import { biomeLinks, getBiomeName } from "./data/biomes";
import { ModifierTier } from "./modifier/modifier-tier";
import { FusePokemonModifierType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeOption, PokemonModifierType, PokemonMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, RememberMoveModifierType, TmModifierType, getDailyRunStarterModifiers, getEnemyBuffModifierForWave, getModifierType, getPlayerModifierTypeOptions, getPlayerShopModifierTypeOptionsForWave, modifierTypes, regenerateModifierPoolThresholds } from "./modifier/modifier-type";
2023-04-13 00:09:15 +01:00
import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
2024-03-13 21:09:23 +00:00
import { BattlerTagLapseType, EncoreTag, HideSpriteTag as HiddenTag, ProtectedTag, TrappedTag } from "./data/battler-tags";
import { getPokemonMessage, getPokemonNameWithAffix } from "./messages";
import { Starter } from "./ui/starter-select-ui-handler";
2023-04-20 20:46:05 +01:00
import { Gender } from "./data/gender";
import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather";
2023-04-20 20:46:05 +01:00
import { TempBattleStat } from "./data/temp-battle-stat";
import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag";
import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, WonderSkinAbAttr, applyPreDefendAbAttrs } from "./data/ability";
2023-04-29 06:40:24 +01:00
import { Unlockables, getUnlockableName } from "./system/unlockables";
2024-03-01 01:08:50 +00:00
import { getBiomeKey } from "./field/arena";
2023-10-07 21:08:33 +01:00
import { BattleType, BattlerIndex, TurnCommand } from "./battle";
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
import { ChallengeAchv, HealAchv, LevelAchv, achvs } from "./system/achv";
import { TrainerSlot, trainerConfigs } from "./data/trainer-config";
import { EggHatchPhase } from "./egg-hatch-phase";
import { Egg } from "./data/egg";
import { vouchers } from "./system/voucher";
2024-06-16 03:06:40 +01:00
import { clientSessionId, loggedInUser, updateUserInfo } from "./account";
import { SessionSaveData } from "./system/game-data";
2024-03-01 01:08:50 +00:00
import { addPokeballCaptureStars, addPokeballOpenParticles } from "./field/anims";
import { SpeciesFormChangeActiveTrigger, SpeciesFormChangeManualTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangePostMoveTrigger, SpeciesFormChangePreMoveTrigger } from "./data/pokemon-forms";
2024-05-17 02:41:38 +01:00
import { battleSpecDialogue, getCharVariantFromDialogue, miscDialogue } from "./data/dialogue";
import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "./ui/modifier-select-ui-handler";
2024-06-04 00:57:47 +01:00
import { SettingKeys } from "./system/settings/settings";
2024-02-13 23:42:11 +00:00
import { Tutorial, handleTutorial } from "./tutorial";
import { TerrainType } from "./data/terrain";
import { OptionSelectConfig, OptionSelectItem } from "./ui/abstact-option-select-ui-handler";
import { SaveSlotUiMode } from "./ui/save-slot-select-ui-handler";
2024-03-17 02:06:56 +00:00
import { fetchDailyRunSeed, getDailyRunStarters } from "./data/daily-run";
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
import { GameMode, GameModes, getGameMode } from "./game-mode";
import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "./data/pokemon-species";
import i18next from "./plugins/i18n";
import * as Overrides from "./overrides";
2024-05-17 02:41:38 +01:00
import { TextStyle, addTextObject } from "./ui/text";
import { Type } from "./data/type";
import { BerryUsedEvent, EncounterPhaseEvent, MoveUsedEvent, TurnEndEvent, TurnInitEvent } from "./events/battle-scene";
import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattleSpec } from "#enums/battle-spec";
import { BattleStyle } from "#enums/battle-style";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Biome } from "#enums/biome";
import { ExpNotification } from "#enums/exp-notification";
import { Moves } from "#enums/moves";
import { PlayerGender } from "#enums/player-gender";
import { Species } from "#enums/species";
import { TrainerType } from "#enums/trainer-type";
2023-12-30 23:41:25 +00:00
export class LoginPhase extends Phase {
2023-12-30 23:41:25 +00:00
private showText: boolean;
constructor(scene: BattleScene, showText?: boolean) {
super(scene);
this.showText = showText === undefined || !!showText;
}
start(): void {
super.start();
2024-04-10 06:32:49 +01:00
const hasSession = !!Utils.getCookie(Utils.sessionIdKey);
2023-12-30 23:41:25 +00:00
this.scene.ui.setMode(Mode.LOADING, { buttonActions: [] });
2024-04-10 06:32:49 +01:00
Utils.executeIf(bypassLogin || hasSession, updateUserInfo).then(response => {
const success = response ? response[0] : false;
const statusCode = response ? response[1] : null;
2023-12-30 23:41:25 +00:00
if (!success) {
2024-04-10 06:32:49 +01:00
if (!statusCode || statusCode === 400) {
if (this.showText) {
this.scene.ui.showText(i18next.t("menu:logInOrCreateAccount"));
}
2024-05-24 00:45:04 +01:00
this.scene.playSound("menu_open");
2024-04-10 06:32:49 +01:00
const loadData = () => {
updateUserInfo().then(success => {
if (!success[0]) {
Utils.setCookie(Utils.sessionIdKey, "");
this.scene.reset(true, true);
return;
}
this.scene.gameData.loadSystem().then(() => this.end());
});
2024-04-10 06:32:49 +01:00
};
2024-05-24 00:45:04 +01:00
2024-04-10 06:32:49 +01:00
this.scene.ui.setMode(Mode.LOGIN_FORM, {
buttonActions: [
() => {
this.scene.ui.playSelect();
loadData();
}, () => {
this.scene.playSound("menu_open");
2024-04-10 06:32:49 +01:00
this.scene.ui.setMode(Mode.REGISTRATION_FORM, {
buttonActions: [
() => {
this.scene.ui.playSelect();
updateUserInfo().then(success => {
if (!success[0]) {
Utils.setCookie(Utils.sessionIdKey, "");
this.scene.reset(true, true);
return;
}
this.end();
} );
2024-04-10 06:32:49 +01:00
}, () => {
this.scene.unshiftPhase(new LoginPhase(this.scene, false));
this.end();
}
]
});
}
]
});
} else if (statusCode === 401) {
Utils.setCookie(Utils.sessionIdKey, "");
this.scene.reset(true, true);
2024-04-10 06:32:49 +01:00
} else {
this.scene.unshiftPhase(new UnavailablePhase(this.scene));
super.end();
}
2023-12-30 23:41:25 +00:00
return null;
2024-02-20 01:36:10 +00:00
} else {
this.scene.gameData.loadSystem().then(success => {
if (success || bypassLogin) {
2024-02-20 01:36:10 +00:00
this.end();
} else {
2024-02-20 01:36:10 +00:00
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.showText(i18next.t("menu:failedToLoadSaveData"));
2024-02-20 01:36:10 +00:00
}
});
}
2023-12-30 23:41:25 +00:00
});
}
end(): void {
this.scene.ui.setMode(Mode.MESSAGE);
if (!this.scene.gameData.gender) {
2024-02-20 01:36:10 +00:00
this.scene.unshiftPhase(new SelectGenderPhase(this.scene));
}
2024-05-24 00:45:04 +01:00
2024-02-13 23:42:11 +00:00
handleTutorial(this.scene, Tutorial.Intro).then(() => super.end());
}
2023-12-30 23:41:25 +00:00
}
2023-04-10 19:12:01 +01:00
export class TitlePhase extends Phase {
2023-04-28 20:03:42 +01:00
private loaded: boolean;
2024-03-21 17:12:05 +00:00
private lastSessionData: SessionSaveData;
[Testing] Flexible Testing Wrapper for Phaser-Based Battle-Scenes (#1908) * refactor executed code while importing and initializing all of these in loading-scene * reset to main * fix server url * added rule no-trailing-spaces * made progress * test somme data from a session save is working * trying to launch a battle * added fetch wrapper to load data locally * trying to mockAllSettled * pushPhase & shiftPhase * check integrity of exported session * set toke + loggedInUser in tests * progress on starting new battle * tring to test phase but it's async * mocking fetch * working mock fetch * need to handle pile of data * attempt to use real phaser classes * reorder overrides * refactored to use some real classes from phaser * removed useless things * started to work on some container mock * finished the mockContainer time to add some logic * some more mock containers * removed addMethods since there is the mock classes now * commented issues * attempt to create mockTextureManager * fix tests * mockSprite & mockText * yes but not really * yes but not really * fix tutorial callback * reached mode title * added achievement tests * fix test achievements with current state of mock * correct sequence loading for BattleScene with mockLoader ! * deep dive into next step * working wait until starter selection screen * added newGame method into wrapper * expect to save_slot * trying to manage pokemon sprite for getAll without success yet * added test for egg output * fixed egg test for June * fix tests + locate next issue to fix * we are in battle baby * added new game in one-line * export is working but export only what's in the fetch * fix start game as guest * refactored how we start a battle + cleanup * overrided mewtwo but issue with currentBattle * refactor: rename InitAchievements to initAchievements * added missing mock method * override level and pokemon forms working as intended * bringToTop Obj * remove launch battle in achivement test * fix getIndex when same pokemon * can run all tests * first attack, faint, and shop modifiers, MockClock * on method for container * added doAttack one-liner * one-line export data * removed throw error * feat: Make `scenes` property of `GameWrapper` class public The `scenes` property of the `GameWrapper` class was changed from private to public. This change allows external access to the `scenes` map, which is used to store Phaser scenes. This modification was made to enable easier manipulation and interaction with the scenes in the game. * correction * removed CanvasRenderer * added a param to remove console.log and added a param to preven scene create call * fix encounter wave 30 when it's a trainer * test double-battle * test fight without KO * test double fight no ko * fix crashing texture + added Text wrapper to log fight * fix tests on boss - trainer - rival * chore: Refactor BattleScene initialization and add new phases Refactor the BattleScene initialization code to remove unnecessary delay and improve performance. Also, add new phases for the title and unavailable states to enhance the game experience. * rework of Game tests * skipFn is working * added onNextPrompt and restore Og Start * better newGame * added skipFN in remove * not yet working test but updated interceptors * do attack work but not on PostSummonPhase phase when there is mention of silcoon and wurmple * error located, it's just a double fight, i was not there yet * single OHKO & double no OHKO * added expirationFn into next prompt * all tests are passing * working test on non damaging move from opponent * cleaned a bit * removed phaser initialisation on every tests * renamed test file * added load system data * added some ability support * added onKill & onSummon abilities test * removed useless test + cleanup * removed useless test + cleanup * fixed tests after merge main * added itemHeld endTurn trigger test (toxic orb) * added runFrom..To * added mustRun to assert currentPhase * added no-miss move to test things * cleaner restore mock * fix test * fix moxie test + game speed * improve test speed * added onOurself and onOpponent mvoe test * added onDamage test for tackle * removed timeout in intervals to run tests faster * cleanup * added never crit override + separate file per test + remove randomness in randBattleSeedInt * move folders * better org * renamed itemHeld folder to items * fix deploy.yml * cleanup * simplified the gameManager start battle and allow single pokemon in party * remove the need of mode development * added input handler to test inputs + remove time from phaser into inputController * added keyboard support * added fakeMobile support * added details * removed a console.log + added logUp * move test to folder * fixed canvas issue * added starter select tests * added some more test on starter-select * added battle-order tests * added battle-order tests * fixing Phaser RNG * ordering stats for better reading * fix tests for main * adapt battle-order test to be more readable * fix merge * fix some errors and silent all errors from gameWrapper since it's not possible to avoid them * fix mocks to manage childs & stuffs * added some docs * fix achievement test * removed an unused file * separate misc tests to clean battle.test file * added a basic french lokalization test * added i18n where it needs to be used only * revers extracted method * removed unused method * removed handler fetch since we do not test anything server related * fix test with handlers removed * added intrepid sword test * fix enum exp party --------- Co-authored-by: Frederico Santos <frederico.f.santos@tecnico.ulisboa.pt>
2024-06-07 23:33:45 +01:00
public gameMode: GameModes;
2023-04-28 20:03:42 +01:00
constructor(scene: BattleScene) {
super(scene);
this.loaded = false;
}
start(): void {
2023-12-30 23:41:25 +00:00
super.start();
2024-03-17 02:06:56 +00:00
this.scene.ui.clearText();
this.scene.ui.fadeIn(250);
this.scene.playBgm("title", true);
2024-03-21 17:12:05 +00:00
this.scene.gameData.getSession(loggedInUser.lastSessionSlot).then(sessionData => {
if (sessionData) {
this.lastSessionData = sessionData;
const biomeKey = getBiomeKey(sessionData.arena.biome);
const bgTexture = `${biomeKey}_bg`;
this.scene.arenaBg.setTexture(bgTexture);
}
2024-03-21 17:12:05 +00:00
this.showOptions();
}).catch(err => {
console.error(err);
this.showOptions();
2024-03-21 17:12:05 +00:00
});
}
showOptions(): void {
const options: OptionSelectItem[] = [];
if (loggedInUser.lastSessionSlot > -1) {
options.push({
label: i18next.t("menu:continue"),
handler: () => {
this.loadSaveSlot(this.lastSessionData ? -1 : loggedInUser.lastSessionSlot);
return true;
}
});
}
options.push({
label: i18next.t("menu:newGame"),
handler: () => {
2024-04-07 04:50:26 +01:00
const setModeAndEnd = (gameMode: GameModes) => {
this.gameMode = gameMode;
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.clearText();
this.end();
};
if (this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) {
const options: OptionSelectItem[] = [
2024-04-07 04:50:26 +01:00
{
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
label: GameMode.getModeName(GameModes.CLASSIC),
handler: () => {
setModeAndEnd(GameModes.CLASSIC);
return true;
}
2024-04-07 04:50:26 +01:00
},
{
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
label: GameMode.getModeName(GameModes.CHALLENGE),
handler: () => {
setModeAndEnd(GameModes.CHALLENGE);
return true;
}
},
{
label: GameMode.getModeName(GameModes.ENDLESS),
handler: () => {
setModeAndEnd(GameModes.ENDLESS);
return true;
}
2024-04-07 04:50:26 +01:00
}
];
if (this.scene.gameData.unlocks[Unlockables.SPLICED_ENDLESS_MODE]) {
options.push({
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
label: GameMode.getModeName(GameModes.SPLICED_ENDLESS),
handler: () => {
setModeAndEnd(GameModes.SPLICED_ENDLESS);
return true;
}
2024-04-07 04:50:26 +01:00
});
}
options.push({
label: i18next.t("menu:cancel"),
2024-04-07 04:50:26 +01:00
handler: () => {
this.scene.clearPhaseQueue();
this.scene.pushPhase(new TitlePhase(this.scene));
super.end();
return true;
2024-04-07 04:50:26 +01:00
}
});
this.scene.ui.showText(i18next.t("menu:selectGameMode"), null, () => this.scene.ui.setOverlayMode(Mode.OPTION_SELECT, { options: options }));
} else {
this.gameMode = GameModes.CLASSIC;
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.clearText();
2024-04-07 04:50:26 +01:00
this.end();
}
return true;
}
},
{
label: i18next.t("menu:loadGame"),
handler: () => {
this.scene.ui.setOverlayMode(Mode.SAVE_SLOT, SaveSlotUiMode.LOAD,
(slotId: integer) => {
if (slotId === -1) {
return this.showOptions();
}
this.loadSaveSlot(slotId);
});
return true;
}
2024-03-17 02:06:56 +00:00
},
{
label: i18next.t("menu:dailyRun"),
handler: () => {
this.initDailyRun();
return true;
},
keepOpen: true
},
{
label: i18next.t("menu:settings"),
handler: () => {
this.scene.ui.setOverlayMode(Mode.SETTINGS);
return true;
},
keepOpen: true
2024-03-17 02:06:56 +00:00
});
const config: OptionSelectConfig = {
options: options,
2024-03-21 17:12:05 +00:00
noCancel: true,
yOffset: 47
};
2024-03-21 17:12:05 +00:00
this.scene.ui.setMode(Mode.TITLE, config);
}
loadSaveSlot(slotId: integer): void {
2024-03-21 17:12:05 +00:00
this.scene.sessionSlotId = slotId > -1 ? slotId : loggedInUser.lastSessionSlot;
this.scene.ui.setMode(Mode.MESSAGE);
2024-03-21 17:12:05 +00:00
this.scene.gameData.loadSession(this.scene, slotId, slotId === -1 ? this.lastSessionData : null).then((success: boolean) => {
if (success) {
this.loaded = true;
this.scene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end());
} else {
this.end();
}
}).catch(err => {
console.error(err);
this.scene.ui.showText(i18next.t("menu:failedToLoadSession"), null);
2023-04-28 20:03:42 +01:00
});
}
initDailyRun(): void {
this.scene.ui.setMode(Mode.SAVE_SLOT, SaveSlotUiMode.SAVE, (slotId: integer) => {
this.scene.clearPhaseQueue();
if (slotId === -1) {
this.scene.pushPhase(new TitlePhase(this.scene));
return super.end();
}
this.scene.sessionSlotId = slotId;
2024-03-17 02:06:56 +00:00
const generateDaily = (seed: string) => {
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
this.scene.gameMode = getGameMode(GameModes.DAILY);
2024-03-17 02:06:56 +00:00
this.scene.setSeed(seed);
this.scene.resetSeed(1);
this.scene.money = this.scene.gameMode.getStartingMoney();
const starters = getDailyRunStarters(this.scene, seed);
const startingLevel = this.scene.gameMode.getStartingLevel();
const party = this.scene.getParty();
const loadPokemonAssets: Promise<void>[] = [];
for (const starter of starters) {
2024-03-17 02:06:56 +00:00
const starterProps = this.scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr);
const starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0));
const starterGender = starter.species.malePercent !== null
? !starterProps.female ? Gender.MALE : Gender.FEMALE
: Gender.GENDERLESS;
2024-04-19 03:52:26 +01:00
const starterPokemon = this.scene.addPlayerPokemon(starter.species, startingLevel, starter.abilityIndex, starterFormIndex, starterGender, starterProps.shiny, starterProps.variant, undefined, starter.nature);
2024-03-17 02:06:56 +00:00
starterPokemon.setVisible(false);
party.push(starterPokemon);
loadPokemonAssets.push(starterPokemon.loadAssets());
}
2024-05-24 00:45:04 +01:00
2024-03-17 02:06:56 +00:00
regenerateModifierPoolThresholds(party, ModifierPoolType.DAILY_STARTER);
const modifiers: Modifier[] = Array(3).fill(null).map(() => modifierTypes.EXP_SHARE().withIdFromFunc(modifierTypes.EXP_SHARE).newModifier())
.concat(Array(3).fill(null).map(() => modifierTypes.GOLDEN_EXP_CHARM().withIdFromFunc(modifierTypes.GOLDEN_EXP_CHARM).newModifier()))
.concat(getDailyRunStarterModifiers(party));
for (const m of modifiers) {
2024-03-17 02:06:56 +00:00
this.scene.addModifier(m, true, false, false, true);
}
2024-03-17 02:06:56 +00:00
this.scene.updateModifiers(true, true);
Promise.all(loadPokemonAssets).then(() => {
this.scene.time.delayedCall(500, () => this.scene.playBgm());
this.scene.gameData.gameStats.dailyRunSessionsPlayed++;
this.scene.newArena(this.scene.gameMode.getStartingBiome(this.scene));
2024-03-17 02:06:56 +00:00
this.scene.newBattle();
this.scene.arena.init();
2024-03-17 02:06:56 +00:00
this.scene.sessionPlayTime = 0;
this.scene.lastSavePlayTime = 0;
2024-03-17 02:06:56 +00:00
this.end();
});
};
2024-05-24 00:45:04 +01:00
// If Online, calls seed fetch from db to generate daily run. If Offline, generates a daily run based on current date.
if (!Utils.isLocal) {
fetchDailyRunSeed().then(seed => {
generateDaily(seed);
}).catch(err => {
console.error("Failed to load daily run:\n", err);
});
} else {
generateDaily(btoa(new Date().toISOString().substring(0, 10)));
}
});
}
2023-04-28 20:03:42 +01:00
end(): void {
if (!this.loaded && !this.scene.gameMode.isDaily) {
2023-04-28 20:03:42 +01:00
this.scene.arena.preloadBgm();
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
this.scene.gameMode = getGameMode(this.gameMode);
if (this.gameMode === GameModes.CHALLENGE) {
this.scene.pushPhase(new SelectChallengePhase(this.scene));
} else {
this.scene.pushPhase(new SelectStarterPhase(this.scene));
}
this.scene.newArena(this.scene.gameMode.getStartingBiome(this.scene));
} else {
this.scene.playBgm();
}
2023-04-28 20:03:42 +01:00
this.scene.pushPhase(new EncounterPhase(this.scene, this.loaded));
2023-12-02 15:30:23 +00:00
if (this.loaded) {
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()).length;
2023-12-02 15:30:23 +00:00
2024-04-11 01:57:22 +01:00
this.scene.pushPhase(new SummonPhase(this.scene, 0, true, true));
if (this.scene.currentBattle.double && availablePartyMembers > 1) {
2024-04-11 01:57:22 +01:00
this.scene.pushPhase(new SummonPhase(this.scene, 1, true, true));
}
if (this.scene.currentBattle.battleType !== BattleType.TRAINER && (this.scene.currentBattle.waveIndex > 1 || !this.scene.gameMode.isDaily)) {
const minPartySize = this.scene.currentBattle.double ? 2 : 1;
if (availablePartyMembers > minPartySize) {
this.scene.pushPhase(new CheckSwitchPhase(this.scene, 0, this.scene.currentBattle.double));
if (this.scene.currentBattle.double) {
this.scene.pushPhase(new CheckSwitchPhase(this.scene, 1, this.scene.currentBattle.double));
}
}
2023-12-02 15:30:23 +00:00
}
}
2023-04-28 20:03:42 +01:00
for (const achv of Object.keys(this.scene.gameData.achvUnlocks)) {
if (vouchers.hasOwnProperty(achv)) {
this.scene.validateVoucher(vouchers[achv]);
}
}
2023-04-28 20:03:42 +01:00
super.end();
}
}
2024-04-10 06:32:49 +01:00
export class UnavailablePhase extends Phase {
constructor(scene: BattleScene) {
super(scene);
}
start(): void {
this.scene.ui.setMode(Mode.UNAVAILABLE, () => {
this.scene.unshiftPhase(new LoginPhase(this.scene, true));
this.end();
});
}
}
export class ReloadSessionPhase extends Phase {
private systemDataStr: string;
constructor(scene: BattleScene, systemDataStr?: string) {
super(scene);
this.systemDataStr = systemDataStr;
}
start(): void {
this.scene.ui.setMode(Mode.SESSION_RELOAD);
let delayElapsed = false;
let loaded = false;
this.scene.time.delayedCall(Utils.fixedInt(1500), () => {
if (loaded) {
this.end();
} else {
delayElapsed = true;
}
});
this.scene.gameData.clearLocalData();
(this.systemDataStr ? this.scene.gameData.initSystem(this.systemDataStr) : this.scene.gameData.loadSystem()).then(() => {
if (delayElapsed) {
this.end();
} else {
loaded = true;
}
});
}
}
2024-04-15 15:09:51 +01:00
export class OutdatedPhase extends Phase {
constructor(scene: BattleScene) {
super(scene);
}
start(): void {
this.scene.ui.setMode(Mode.OUTDATED);
}
}
export class SelectGenderPhase extends Phase {
2024-02-06 21:15:35 +00:00
constructor(scene: BattleScene) {
super(scene);
}
2024-05-24 00:45:04 +01:00
2024-02-06 21:15:35 +00:00
start(): void {
super.start();
this.scene.ui.showText(i18next.t("menu:boyOrGirl"), null, () => {
2024-02-06 21:15:35 +00:00
this.scene.ui.setMode(Mode.OPTION_SELECT, {
options: [
{
label: i18next.t("menu:boy"),
2024-02-06 21:15:35 +00:00
handler: () => {
this.scene.gameData.gender = PlayerGender.MALE;
2024-06-04 00:57:47 +01:00
this.scene.gameData.saveSetting(SettingKeys.Player_Gender, 0);
2024-02-06 21:15:35 +00:00
this.scene.gameData.saveSystem().then(() => this.end());
return true;
2024-02-06 21:15:35 +00:00
}
},
{
label: i18next.t("menu:girl"),
2024-02-06 21:15:35 +00:00
handler: () => {
this.scene.gameData.gender = PlayerGender.FEMALE;
2024-06-04 00:57:47 +01:00
this.scene.gameData.saveSetting(SettingKeys.Player_Gender, 1);
2024-02-06 21:15:35 +00:00
this.scene.gameData.saveSystem().then(() => this.end());
return true;
2024-02-06 21:15:35 +00:00
}
}
]
});
});
}
2024-03-17 14:11:24 +00:00
end(): void {
this.scene.ui.setMode(Mode.MESSAGE);
super.end();
}
2024-02-06 21:15:35 +00:00
}
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
export class SelectChallengePhase extends Phase {
constructor(scene: BattleScene) {
super(scene);
}
start() {
super.start();
this.scene.playBgm("menu");
this.scene.ui.setMode(Mode.CHALLENGE_SELECT);
}
}
export class SelectStarterPhase extends Phase {
2024-04-07 04:50:26 +01:00
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
constructor(scene: BattleScene) {
2023-04-10 19:12:01 +01:00
super(scene);
}
start() {
super.start();
this.scene.playBgm("menu");
2023-04-13 00:09:15 +01:00
this.scene.ui.setMode(Mode.STARTER_SELECT, (starters: Starter[]) => {
this.scene.ui.clearText();
this.scene.ui.setMode(Mode.SAVE_SLOT, SaveSlotUiMode.SAVE, (slotId: integer) => {
if (slotId === -1) {
this.scene.clearPhaseQueue();
this.scene.pushPhase(new TitlePhase(this.scene));
return this.end();
}
this.scene.sessionSlotId = slotId;
[Testing] Flexible Testing Wrapper for Phaser-Based Battle-Scenes (#1908) * refactor executed code while importing and initializing all of these in loading-scene * reset to main * fix server url * added rule no-trailing-spaces * made progress * test somme data from a session save is working * trying to launch a battle * added fetch wrapper to load data locally * trying to mockAllSettled * pushPhase & shiftPhase * check integrity of exported session * set toke + loggedInUser in tests * progress on starting new battle * tring to test phase but it's async * mocking fetch * working mock fetch * need to handle pile of data * attempt to use real phaser classes * reorder overrides * refactored to use some real classes from phaser * removed useless things * started to work on some container mock * finished the mockContainer time to add some logic * some more mock containers * removed addMethods since there is the mock classes now * commented issues * attempt to create mockTextureManager * fix tests * mockSprite & mockText * yes but not really * yes but not really * fix tutorial callback * reached mode title * added achievement tests * fix test achievements with current state of mock * correct sequence loading for BattleScene with mockLoader ! * deep dive into next step * working wait until starter selection screen * added newGame method into wrapper * expect to save_slot * trying to manage pokemon sprite for getAll without success yet * added test for egg output * fixed egg test for June * fix tests + locate next issue to fix * we are in battle baby * added new game in one-line * export is working but export only what's in the fetch * fix start game as guest * refactored how we start a battle + cleanup * overrided mewtwo but issue with currentBattle * refactor: rename InitAchievements to initAchievements * added missing mock method * override level and pokemon forms working as intended * bringToTop Obj * remove launch battle in achivement test * fix getIndex when same pokemon * can run all tests * first attack, faint, and shop modifiers, MockClock * on method for container * added doAttack one-liner * one-line export data * removed throw error * feat: Make `scenes` property of `GameWrapper` class public The `scenes` property of the `GameWrapper` class was changed from private to public. This change allows external access to the `scenes` map, which is used to store Phaser scenes. This modification was made to enable easier manipulation and interaction with the scenes in the game. * correction * removed CanvasRenderer * added a param to remove console.log and added a param to preven scene create call * fix encounter wave 30 when it's a trainer * test double-battle * test fight without KO * test double fight no ko * fix crashing texture + added Text wrapper to log fight * fix tests on boss - trainer - rival * chore: Refactor BattleScene initialization and add new phases Refactor the BattleScene initialization code to remove unnecessary delay and improve performance. Also, add new phases for the title and unavailable states to enhance the game experience. * rework of Game tests * skipFn is working * added onNextPrompt and restore Og Start * better newGame * added skipFN in remove * not yet working test but updated interceptors * do attack work but not on PostSummonPhase phase when there is mention of silcoon and wurmple * error located, it's just a double fight, i was not there yet * single OHKO & double no OHKO * added expirationFn into next prompt * all tests are passing * working test on non damaging move from opponent * cleaned a bit * removed phaser initialisation on every tests * renamed test file * added load system data * added some ability support * added onKill & onSummon abilities test * removed useless test + cleanup * removed useless test + cleanup * fixed tests after merge main * added itemHeld endTurn trigger test (toxic orb) * added runFrom..To * added mustRun to assert currentPhase * added no-miss move to test things * cleaner restore mock * fix test * fix moxie test + game speed * improve test speed * added onOurself and onOpponent mvoe test * added onDamage test for tackle * removed timeout in intervals to run tests faster * cleanup * added never crit override + separate file per test + remove randomness in randBattleSeedInt * move folders * better org * renamed itemHeld folder to items * fix deploy.yml * cleanup * simplified the gameManager start battle and allow single pokemon in party * remove the need of mode development * added input handler to test inputs + remove time from phaser into inputController * added keyboard support * added fakeMobile support * added details * removed a console.log + added logUp * move test to folder * fixed canvas issue * added starter select tests * added some more test on starter-select * added battle-order tests * added battle-order tests * fixing Phaser RNG * ordering stats for better reading * fix tests for main * adapt battle-order test to be more readable * fix merge * fix some errors and silent all errors from gameWrapper since it's not possible to avoid them * fix mocks to manage childs & stuffs * added some docs * fix achievement test * removed an unused file * separate misc tests to clean battle.test file * added a basic french lokalization test * added i18n where it needs to be used only * revers extracted method * removed unused method * removed handler fetch since we do not test anything server related * fix test with handlers removed * added intrepid sword test * fix enum exp party --------- Co-authored-by: Frederico Santos <frederico.f.santos@tecnico.ulisboa.pt>
2024-06-07 23:33:45 +01:00
this.initBattle(starters);
2023-04-13 00:09:15 +01:00
});
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
});
2023-04-10 19:12:01 +01:00
}
[Testing] Flexible Testing Wrapper for Phaser-Based Battle-Scenes (#1908) * refactor executed code while importing and initializing all of these in loading-scene * reset to main * fix server url * added rule no-trailing-spaces * made progress * test somme data from a session save is working * trying to launch a battle * added fetch wrapper to load data locally * trying to mockAllSettled * pushPhase & shiftPhase * check integrity of exported session * set toke + loggedInUser in tests * progress on starting new battle * tring to test phase but it's async * mocking fetch * working mock fetch * need to handle pile of data * attempt to use real phaser classes * reorder overrides * refactored to use some real classes from phaser * removed useless things * started to work on some container mock * finished the mockContainer time to add some logic * some more mock containers * removed addMethods since there is the mock classes now * commented issues * attempt to create mockTextureManager * fix tests * mockSprite & mockText * yes but not really * yes but not really * fix tutorial callback * reached mode title * added achievement tests * fix test achievements with current state of mock * correct sequence loading for BattleScene with mockLoader ! * deep dive into next step * working wait until starter selection screen * added newGame method into wrapper * expect to save_slot * trying to manage pokemon sprite for getAll without success yet * added test for egg output * fixed egg test for June * fix tests + locate next issue to fix * we are in battle baby * added new game in one-line * export is working but export only what's in the fetch * fix start game as guest * refactored how we start a battle + cleanup * overrided mewtwo but issue with currentBattle * refactor: rename InitAchievements to initAchievements * added missing mock method * override level and pokemon forms working as intended * bringToTop Obj * remove launch battle in achivement test * fix getIndex when same pokemon * can run all tests * first attack, faint, and shop modifiers, MockClock * on method for container * added doAttack one-liner * one-line export data * removed throw error * feat: Make `scenes` property of `GameWrapper` class public The `scenes` property of the `GameWrapper` class was changed from private to public. This change allows external access to the `scenes` map, which is used to store Phaser scenes. This modification was made to enable easier manipulation and interaction with the scenes in the game. * correction * removed CanvasRenderer * added a param to remove console.log and added a param to preven scene create call * fix encounter wave 30 when it's a trainer * test double-battle * test fight without KO * test double fight no ko * fix crashing texture + added Text wrapper to log fight * fix tests on boss - trainer - rival * chore: Refactor BattleScene initialization and add new phases Refactor the BattleScene initialization code to remove unnecessary delay and improve performance. Also, add new phases for the title and unavailable states to enhance the game experience. * rework of Game tests * skipFn is working * added onNextPrompt and restore Og Start * better newGame * added skipFN in remove * not yet working test but updated interceptors * do attack work but not on PostSummonPhase phase when there is mention of silcoon and wurmple * error located, it's just a double fight, i was not there yet * single OHKO & double no OHKO * added expirationFn into next prompt * all tests are passing * working test on non damaging move from opponent * cleaned a bit * removed phaser initialisation on every tests * renamed test file * added load system data * added some ability support * added onKill & onSummon abilities test * removed useless test + cleanup * removed useless test + cleanup * fixed tests after merge main * added itemHeld endTurn trigger test (toxic orb) * added runFrom..To * added mustRun to assert currentPhase * added no-miss move to test things * cleaner restore mock * fix test * fix moxie test + game speed * improve test speed * added onOurself and onOpponent mvoe test * added onDamage test for tackle * removed timeout in intervals to run tests faster * cleanup * added never crit override + separate file per test + remove randomness in randBattleSeedInt * move folders * better org * renamed itemHeld folder to items * fix deploy.yml * cleanup * simplified the gameManager start battle and allow single pokemon in party * remove the need of mode development * added input handler to test inputs + remove time from phaser into inputController * added keyboard support * added fakeMobile support * added details * removed a console.log + added logUp * move test to folder * fixed canvas issue * added starter select tests * added some more test on starter-select * added battle-order tests * added battle-order tests * fixing Phaser RNG * ordering stats for better reading * fix tests for main * adapt battle-order test to be more readable * fix merge * fix some errors and silent all errors from gameWrapper since it's not possible to avoid them * fix mocks to manage childs & stuffs * added some docs * fix achievement test * removed an unused file * separate misc tests to clean battle.test file * added a basic french lokalization test * added i18n where it needs to be used only * revers extracted method * removed unused method * removed handler fetch since we do not test anything server related * fix test with handlers removed * added intrepid sword test * fix enum exp party --------- Co-authored-by: Frederico Santos <frederico.f.santos@tecnico.ulisboa.pt>
2024-06-07 23:33:45 +01:00
/**
* Initialize starters before starting the first battle
* @param starters {@linkcode Pokemon} with which to start the first battle
*/
[Testing] Flexible Testing Wrapper for Phaser-Based Battle-Scenes (#1908) * refactor executed code while importing and initializing all of these in loading-scene * reset to main * fix server url * added rule no-trailing-spaces * made progress * test somme data from a session save is working * trying to launch a battle * added fetch wrapper to load data locally * trying to mockAllSettled * pushPhase & shiftPhase * check integrity of exported session * set toke + loggedInUser in tests * progress on starting new battle * tring to test phase but it's async * mocking fetch * working mock fetch * need to handle pile of data * attempt to use real phaser classes * reorder overrides * refactored to use some real classes from phaser * removed useless things * started to work on some container mock * finished the mockContainer time to add some logic * some more mock containers * removed addMethods since there is the mock classes now * commented issues * attempt to create mockTextureManager * fix tests * mockSprite & mockText * yes but not really * yes but not really * fix tutorial callback * reached mode title * added achievement tests * fix test achievements with current state of mock * correct sequence loading for BattleScene with mockLoader ! * deep dive into next step * working wait until starter selection screen * added newGame method into wrapper * expect to save_slot * trying to manage pokemon sprite for getAll without success yet * added test for egg output * fixed egg test for June * fix tests + locate next issue to fix * we are in battle baby * added new game in one-line * export is working but export only what's in the fetch * fix start game as guest * refactored how we start a battle + cleanup * overrided mewtwo but issue with currentBattle * refactor: rename InitAchievements to initAchievements * added missing mock method * override level and pokemon forms working as intended * bringToTop Obj * remove launch battle in achivement test * fix getIndex when same pokemon * can run all tests * first attack, faint, and shop modifiers, MockClock * on method for container * added doAttack one-liner * one-line export data * removed throw error * feat: Make `scenes` property of `GameWrapper` class public The `scenes` property of the `GameWrapper` class was changed from private to public. This change allows external access to the `scenes` map, which is used to store Phaser scenes. This modification was made to enable easier manipulation and interaction with the scenes in the game. * correction * removed CanvasRenderer * added a param to remove console.log and added a param to preven scene create call * fix encounter wave 30 when it's a trainer * test double-battle * test fight without KO * test double fight no ko * fix crashing texture + added Text wrapper to log fight * fix tests on boss - trainer - rival * chore: Refactor BattleScene initialization and add new phases Refactor the BattleScene initialization code to remove unnecessary delay and improve performance. Also, add new phases for the title and unavailable states to enhance the game experience. * rework of Game tests * skipFn is working * added onNextPrompt and restore Og Start * better newGame * added skipFN in remove * not yet working test but updated interceptors * do attack work but not on PostSummonPhase phase when there is mention of silcoon and wurmple * error located, it's just a double fight, i was not there yet * single OHKO & double no OHKO * added expirationFn into next prompt * all tests are passing * working test on non damaging move from opponent * cleaned a bit * removed phaser initialisation on every tests * renamed test file * added load system data * added some ability support * added onKill & onSummon abilities test * removed useless test + cleanup * removed useless test + cleanup * fixed tests after merge main * added itemHeld endTurn trigger test (toxic orb) * added runFrom..To * added mustRun to assert currentPhase * added no-miss move to test things * cleaner restore mock * fix test * fix moxie test + game speed * improve test speed * added onOurself and onOpponent mvoe test * added onDamage test for tackle * removed timeout in intervals to run tests faster * cleanup * added never crit override + separate file per test + remove randomness in randBattleSeedInt * move folders * better org * renamed itemHeld folder to items * fix deploy.yml * cleanup * simplified the gameManager start battle and allow single pokemon in party * remove the need of mode development * added input handler to test inputs + remove time from phaser into inputController * added keyboard support * added fakeMobile support * added details * removed a console.log + added logUp * move test to folder * fixed canvas issue * added starter select tests * added some more test on starter-select * added battle-order tests * added battle-order tests * fixing Phaser RNG * ordering stats for better reading * fix tests for main * adapt battle-order test to be more readable * fix merge * fix some errors and silent all errors from gameWrapper since it's not possible to avoid them * fix mocks to manage childs & stuffs * added some docs * fix achievement test * removed an unused file * separate misc tests to clean battle.test file * added a basic french lokalization test * added i18n where it needs to be used only * revers extracted method * removed unused method * removed handler fetch since we do not test anything server related * fix test with handlers removed * added intrepid sword test * fix enum exp party --------- Co-authored-by: Frederico Santos <frederico.f.santos@tecnico.ulisboa.pt>
2024-06-07 23:33:45 +01:00
initBattle(starters: Starter[]) {
const party = this.scene.getParty();
const loadPokemonAssets: Promise<void>[] = [];
starters.forEach((starter: Starter, i: integer) => {
if (!i && Overrides.STARTER_SPECIES_OVERRIDE) {
starter.species = getPokemonSpecies(Overrides.STARTER_SPECIES_OVERRIDE as Species);
}
const starterProps = this.scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr);
let starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0));
if (
starter.species.speciesId in Overrides.STARTER_FORM_OVERRIDES &&
starter.species.forms[Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]]
) {
starterFormIndex = Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId];
[Testing] Flexible Testing Wrapper for Phaser-Based Battle-Scenes (#1908) * refactor executed code while importing and initializing all of these in loading-scene * reset to main * fix server url * added rule no-trailing-spaces * made progress * test somme data from a session save is working * trying to launch a battle * added fetch wrapper to load data locally * trying to mockAllSettled * pushPhase & shiftPhase * check integrity of exported session * set toke + loggedInUser in tests * progress on starting new battle * tring to test phase but it's async * mocking fetch * working mock fetch * need to handle pile of data * attempt to use real phaser classes * reorder overrides * refactored to use some real classes from phaser * removed useless things * started to work on some container mock * finished the mockContainer time to add some logic * some more mock containers * removed addMethods since there is the mock classes now * commented issues * attempt to create mockTextureManager * fix tests * mockSprite & mockText * yes but not really * yes but not really * fix tutorial callback * reached mode title * added achievement tests * fix test achievements with current state of mock * correct sequence loading for BattleScene with mockLoader ! * deep dive into next step * working wait until starter selection screen * added newGame method into wrapper * expect to save_slot * trying to manage pokemon sprite for getAll without success yet * added test for egg output * fixed egg test for June * fix tests + locate next issue to fix * we are in battle baby * added new game in one-line * export is working but export only what's in the fetch * fix start game as guest * refactored how we start a battle + cleanup * overrided mewtwo but issue with currentBattle * refactor: rename InitAchievements to initAchievements * added missing mock method * override level and pokemon forms working as intended * bringToTop Obj * remove launch battle in achivement test * fix getIndex when same pokemon * can run all tests * first attack, faint, and shop modifiers, MockClock * on method for container * added doAttack one-liner * one-line export data * removed throw error * feat: Make `scenes` property of `GameWrapper` class public The `scenes` property of the `GameWrapper` class was changed from private to public. This change allows external access to the `scenes` map, which is used to store Phaser scenes. This modification was made to enable easier manipulation and interaction with the scenes in the game. * correction * removed CanvasRenderer * added a param to remove console.log and added a param to preven scene create call * fix encounter wave 30 when it's a trainer * test double-battle * test fight without KO * test double fight no ko * fix crashing texture + added Text wrapper to log fight * fix tests on boss - trainer - rival * chore: Refactor BattleScene initialization and add new phases Refactor the BattleScene initialization code to remove unnecessary delay and improve performance. Also, add new phases for the title and unavailable states to enhance the game experience. * rework of Game tests * skipFn is working * added onNextPrompt and restore Og Start * better newGame * added skipFN in remove * not yet working test but updated interceptors * do attack work but not on PostSummonPhase phase when there is mention of silcoon and wurmple * error located, it's just a double fight, i was not there yet * single OHKO & double no OHKO * added expirationFn into next prompt * all tests are passing * working test on non damaging move from opponent * cleaned a bit * removed phaser initialisation on every tests * renamed test file * added load system data * added some ability support * added onKill & onSummon abilities test * removed useless test + cleanup * removed useless test + cleanup * fixed tests after merge main * added itemHeld endTurn trigger test (toxic orb) * added runFrom..To * added mustRun to assert currentPhase * added no-miss move to test things * cleaner restore mock * fix test * fix moxie test + game speed * improve test speed * added onOurself and onOpponent mvoe test * added onDamage test for tackle * removed timeout in intervals to run tests faster * cleanup * added never crit override + separate file per test + remove randomness in randBattleSeedInt * move folders * better org * renamed itemHeld folder to items * fix deploy.yml * cleanup * simplified the gameManager start battle and allow single pokemon in party * remove the need of mode development * added input handler to test inputs + remove time from phaser into inputController * added keyboard support * added fakeMobile support * added details * removed a console.log + added logUp * move test to folder * fixed canvas issue * added starter select tests * added some more test on starter-select * added battle-order tests * added battle-order tests * fixing Phaser RNG * ordering stats for better reading * fix tests for main * adapt battle-order test to be more readable * fix merge * fix some errors and silent all errors from gameWrapper since it's not possible to avoid them * fix mocks to manage childs & stuffs * added some docs * fix achievement test * removed an unused file * separate misc tests to clean battle.test file * added a basic french lokalization test * added i18n where it needs to be used only * revers extracted method * removed unused method * removed handler fetch since we do not test anything server related * fix test with handlers removed * added intrepid sword test * fix enum exp party --------- Co-authored-by: Frederico Santos <frederico.f.santos@tecnico.ulisboa.pt>
2024-06-07 23:33:45 +01:00
}
[Testing] Flexible Testing Wrapper for Phaser-Based Battle-Scenes (#1908) * refactor executed code while importing and initializing all of these in loading-scene * reset to main * fix server url * added rule no-trailing-spaces * made progress * test somme data from a session save is working * trying to launch a battle * added fetch wrapper to load data locally * trying to mockAllSettled * pushPhase & shiftPhase * check integrity of exported session * set toke + loggedInUser in tests * progress on starting new battle * tring to test phase but it's async * mocking fetch * working mock fetch * need to handle pile of data * attempt to use real phaser classes * reorder overrides * refactored to use some real classes from phaser * removed useless things * started to work on some container mock * finished the mockContainer time to add some logic * some more mock containers * removed addMethods since there is the mock classes now * commented issues * attempt to create mockTextureManager * fix tests * mockSprite & mockText * yes but not really * yes but not really * fix tutorial callback * reached mode title * added achievement tests * fix test achievements with current state of mock * correct sequence loading for BattleScene with mockLoader ! * deep dive into next step * working wait until starter selection screen * added newGame method into wrapper * expect to save_slot * trying to manage pokemon sprite for getAll without success yet * added test for egg output * fixed egg test for June * fix tests + locate next issue to fix * we are in battle baby * added new game in one-line * export is working but export only what's in the fetch * fix start game as guest * refactored how we start a battle + cleanup * overrided mewtwo but issue with currentBattle * refactor: rename InitAchievements to initAchievements * added missing mock method * override level and pokemon forms working as intended * bringToTop Obj * remove launch battle in achivement test * fix getIndex when same pokemon * can run all tests * first attack, faint, and shop modifiers, MockClock * on method for container * added doAttack one-liner * one-line export data * removed throw error * feat: Make `scenes` property of `GameWrapper` class public The `scenes` property of the `GameWrapper` class was changed from private to public. This change allows external access to the `scenes` map, which is used to store Phaser scenes. This modification was made to enable easier manipulation and interaction with the scenes in the game. * correction * removed CanvasRenderer * added a param to remove console.log and added a param to preven scene create call * fix encounter wave 30 when it's a trainer * test double-battle * test fight without KO * test double fight no ko * fix crashing texture + added Text wrapper to log fight * fix tests on boss - trainer - rival * chore: Refactor BattleScene initialization and add new phases Refactor the BattleScene initialization code to remove unnecessary delay and improve performance. Also, add new phases for the title and unavailable states to enhance the game experience. * rework of Game tests * skipFn is working * added onNextPrompt and restore Og Start * better newGame * added skipFN in remove * not yet working test but updated interceptors * do attack work but not on PostSummonPhase phase when there is mention of silcoon and wurmple * error located, it's just a double fight, i was not there yet * single OHKO & double no OHKO * added expirationFn into next prompt * all tests are passing * working test on non damaging move from opponent * cleaned a bit * removed phaser initialisation on every tests * renamed test file * added load system data * added some ability support * added onKill & onSummon abilities test * removed useless test + cleanup * removed useless test + cleanup * fixed tests after merge main * added itemHeld endTurn trigger test (toxic orb) * added runFrom..To * added mustRun to assert currentPhase * added no-miss move to test things * cleaner restore mock * fix test * fix moxie test + game speed * improve test speed * added onOurself and onOpponent mvoe test * added onDamage test for tackle * removed timeout in intervals to run tests faster * cleanup * added never crit override + separate file per test + remove randomness in randBattleSeedInt * move folders * better org * renamed itemHeld folder to items * fix deploy.yml * cleanup * simplified the gameManager start battle and allow single pokemon in party * remove the need of mode development * added input handler to test inputs + remove time from phaser into inputController * added keyboard support * added fakeMobile support * added details * removed a console.log + added logUp * move test to folder * fixed canvas issue * added starter select tests * added some more test on starter-select * added battle-order tests * added battle-order tests * fixing Phaser RNG * ordering stats for better reading * fix tests for main * adapt battle-order test to be more readable * fix merge * fix some errors and silent all errors from gameWrapper since it's not possible to avoid them * fix mocks to manage childs & stuffs * added some docs * fix achievement test * removed an unused file * separate misc tests to clean battle.test file * added a basic french lokalization test * added i18n where it needs to be used only * revers extracted method * removed unused method * removed handler fetch since we do not test anything server related * fix test with handlers removed * added intrepid sword test * fix enum exp party --------- Co-authored-by: Frederico Santos <frederico.f.santos@tecnico.ulisboa.pt>
2024-06-07 23:33:45 +01:00
let starterGender = starter.species.malePercent !== null
? !starterProps.female ? Gender.MALE : Gender.FEMALE
: Gender.GENDERLESS;
if (Overrides.GENDER_OVERRIDE !== null) {
starterGender = Overrides.GENDER_OVERRIDE;
}
const starterIvs = this.scene.gameData.dexData[starter.species.speciesId].ivs.slice(0);
const starterPokemon = this.scene.addPlayerPokemon(starter.species, this.scene.gameMode.getStartingLevel(), starter.abilityIndex, starterFormIndex, starterGender, starterProps.shiny, starterProps.variant, starterIvs, starter.nature);
starterPokemon.tryPopulateMoveset(starter.moveset);
if (starter.passive) {
starterPokemon.passive = true;
}
starterPokemon.luck = this.scene.gameData.getDexAttrLuck(this.scene.gameData.dexData[starter.species.speciesId].caughtAttr);
if (starter.pokerus) {
starterPokemon.pokerus = true;
}
if (this.scene.gameMode.isSplicedOnly) {
starterPokemon.generateFusionSpecies(true);
}
starterPokemon.setVisible(false);
party.push(starterPokemon);
loadPokemonAssets.push(starterPokemon.loadAssets());
});
overrideModifiers(this.scene);
overrideHeldItems(this.scene, party[0]);
Promise.all(loadPokemonAssets).then(() => {
SoundFade.fadeOut(this.scene, this.scene.sound.get("menu"), 500, true);
this.scene.time.delayedCall(500, () => this.scene.playBgm());
if (this.scene.gameMode.isClassic) {
this.scene.gameData.gameStats.classicSessionsPlayed++;
} else {
this.scene.gameData.gameStats.endlessSessionsPlayed++;
}
this.scene.newBattle();
this.scene.arena.init();
this.scene.sessionPlayTime = 0;
this.scene.lastSavePlayTime = 0;
this.end();
});
}
2023-04-10 19:12:01 +01:00
}
export class BattlePhase extends Phase {
constructor(scene: BattleScene) {
super(scene);
}
showEnemyTrainer(trainerSlot: TrainerSlot = TrainerSlot.NONE): void {
const sprites = this.scene.currentBattle.trainer.getSprites();
const tintSprites = this.scene.currentBattle.trainer.getTintSprites();
for (let i = 0; i < sprites.length; i++) {
const visible = !trainerSlot || !i === (trainerSlot === TrainerSlot.TRAINER) || sprites.length < 2;
[ sprites[i], tintSprites[i] ].map(sprite => {
if (visible) {
sprite.x = trainerSlot || sprites.length < 2 ? 0 : i ? 16 : -16;
}
sprite.setVisible(visible);
sprite.clearTint();
});
sprites[i].setVisible(visible);
tintSprites[i].setVisible(visible);
sprites[i].clearTint();
tintSprites[i].clearTint();
}
this.scene.tweens.add({
targets: this.scene.currentBattle.trainer,
x: "-=16",
y: "+=16",
alpha: 1,
ease: "Sine.easeInOut",
duration: 750
});
}
hideEnemyTrainer(): void {
this.scene.tweens.add({
targets: this.scene.currentBattle.trainer,
x: "+=16",
y: "-=16",
alpha: 0,
ease: "Sine.easeInOut",
duration: 750
});
}
}
type PokemonFunc = (pokemon: Pokemon) => void;
export abstract class FieldPhase extends BattlePhase {
getOrder(): BattlerIndex[] {
const playerField = this.scene.getPlayerField().filter(p => p.isActive()) as Pokemon[];
const enemyField = this.scene.getEnemyField().filter(p => p.isActive()) as Pokemon[];
// We shuffle the list before sorting so speed ties produce random results
let orderedTargets: Pokemon[] = playerField.concat(enemyField);
// We seed it with the current turn to prevent an inconsistency where it
// was varying based on how long since you last reloaded
this.scene.executeWithSeedOffset(() => {
orderedTargets = Utils.randSeedShuffle(orderedTargets);
}, this.scene.currentBattle.turn, this.scene.waveSeed);
orderedTargets.sort((a: Pokemon, b: Pokemon) => {
const aSpeed = a?.getBattleStat(Stat.SPD) || 0;
const bSpeed = b?.getBattleStat(Stat.SPD) || 0;
return bSpeed - aSpeed;
});
const speedReversed = new Utils.BooleanHolder(false);
this.scene.arena.applyTags(TrickRoomTag, speedReversed);
if (speedReversed.value) {
orderedTargets = orderedTargets.reverse();
}
return orderedTargets.map(t => t.getFieldIndex() + (!t.isPlayer() ? BattlerIndex.ENEMY : 0));
}
executeForAll(func: PokemonFunc): void {
const field = this.scene.getField(true).filter(p => p.summonData);
field.forEach(pokemon => func(pokemon));
}
}
export abstract class PokemonPhase extends FieldPhase {
2023-12-04 05:09:38 +00:00
protected battlerIndex: BattlerIndex | integer;
public player: boolean;
public fieldIndex: integer;
2023-12-04 05:09:38 +00:00
constructor(scene: BattleScene, battlerIndex: BattlerIndex | integer) {
super(scene);
if (battlerIndex === undefined) {
battlerIndex = scene.getField().find(p => p?.isActive()).getBattlerIndex();
}
this.battlerIndex = battlerIndex;
this.player = battlerIndex < 2;
this.fieldIndex = battlerIndex % 2;
}
getPokemon() {
if (this.battlerIndex > BattlerIndex.ENEMY_2) {
2023-12-04 05:09:38 +00:00
return this.scene.getPokemonById(this.battlerIndex);
}
return this.scene.getField()[this.battlerIndex];
}
}
export abstract class PartyMemberPokemonPhase extends FieldPhase {
protected partyMemberIndex: integer;
protected fieldIndex: integer;
2023-10-07 21:08:33 +01:00
protected player: boolean;
2023-10-07 21:08:33 +01:00
constructor(scene: BattleScene, partyMemberIndex: integer, player: boolean) {
super(scene);
this.partyMemberIndex = partyMemberIndex;
this.fieldIndex = partyMemberIndex < this.scene.currentBattle.getBattlerCount()
? partyMemberIndex
: -1;
2023-10-07 21:08:33 +01:00
this.player = player;
}
2023-10-07 21:08:33 +01:00
getParty(): Pokemon[] {
return this.player ? this.scene.getParty() : this.scene.getEnemyParty();
}
getPokemon(): Pokemon {
return this.getParty()[this.partyMemberIndex];
}
}
export abstract class PlayerPartyMemberPokemonPhase extends PartyMemberPokemonPhase {
constructor(scene: BattleScene, partyMemberIndex: integer) {
super(scene, partyMemberIndex, true);
}
getPlayerPokemon(): PlayerPokemon {
return super.getPokemon() as PlayerPokemon;
}
}
export abstract class EnemyPartyMemberPokemonPhase extends PartyMemberPokemonPhase {
constructor(scene: BattleScene, partyMemberIndex: integer) {
super(scene, partyMemberIndex, false);
}
getEnemyPokemon(): EnemyPokemon {
return super.getPokemon() as EnemyPokemon;
}
}
2023-04-10 19:12:01 +01:00
export class EncounterPhase extends BattlePhase {
2023-04-28 20:03:42 +01:00
private loaded: boolean;
constructor(scene: BattleScene, loaded?: boolean) {
2023-04-10 19:12:01 +01:00
super(scene);
2023-04-28 20:03:42 +01:00
this.loaded = !!loaded;
2023-04-10 19:12:01 +01:00
}
start() {
super.start();
2024-05-10 11:42:28 +01:00
this.scene.updateGameInfo();
2024-01-12 01:27:50 +00:00
this.scene.initSession();
this.scene.eventTarget.dispatchEvent(new EncounterPhaseEvent());
// Failsafe if players somehow skip floor 200 in classic mode
if (this.scene.gameMode.isClassic && this.scene.currentBattle.waveIndex > 200) {
this.scene.unshiftPhase(new GameOverPhase(this.scene));
}
const loadEnemyAssets = [];
2023-04-10 19:12:01 +01:00
const battle = this.scene.currentBattle;
let totalBst = 0;
battle.enemyLevels.forEach((level, e) => {
if (!this.loaded) {
if (battle.battleType === BattleType.TRAINER) {
battle.enemyParty[e] = battle.trainer.genPartyMember(e);
} else {
2023-11-08 23:36:30 +00:00
const enemySpecies = this.scene.randomSpecies(battle.waveIndex, level, true);
battle.enemyParty[e] = this.scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, !!this.scene.getEncounterBossSegments(battle.waveIndex, level, enemySpecies));
if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) {
2024-02-20 03:58:25 +00:00
battle.enemyParty[e].ivs = new Array(6).fill(31);
}
2024-01-06 03:24:05 +00:00
this.scene.getParty().slice(0, !battle.double ? 1 : 2).reverse().forEach(playerPokemon => {
applyAbAttrs(SyncEncounterNatureAbAttr, playerPokemon, null, battle.enemyParty[e]);
});
}
}
2023-10-07 21:08:33 +01:00
const enemyPokemon = this.scene.getEnemyParty()[e];
if (e < (battle.double ? 2 : 1)) {
enemyPokemon.setX(-66 + enemyPokemon.getFieldPositionOffset()[0]);
enemyPokemon.resetSummonData();
}
2023-04-18 06:32:26 +01:00
if (!this.loaded) {
this.scene.gameData.setPokemonSeen(enemyPokemon, true, battle.battleType === BattleType.TRAINER);
}
if (enemyPokemon.species.speciesId === Species.ETERNATUS) {
2024-03-14 20:26:57 +00:00
if (this.scene.gameMode.isClassic && (battle.battleSpec === BattleSpec.FINAL_BOSS || this.scene.gameMode.isWaveFinal(battle.waveIndex))) {
2024-02-28 03:50:27 +00:00
if (battle.battleSpec !== BattleSpec.FINAL_BOSS) {
enemyPokemon.formIndex = 1;
2024-02-28 03:50:27 +00:00
enemyPokemon.updateScale();
}
enemyPokemon.setBoss();
2024-02-28 03:50:27 +00:00
} else if (!(battle.waveIndex % 1000)) {
enemyPokemon.formIndex = 1;
2024-02-28 03:50:27 +00:00
enemyPokemon.updateScale();
}
2024-01-08 04:17:24 +00:00
}
2024-05-24 00:45:04 +01:00
totalBst += enemyPokemon.getSpeciesForm().baseTotal;
2023-10-24 04:20:05 +01:00
loadEnemyAssets.push(enemyPokemon.loadAssets());
2024-05-24 00:45:04 +01:00
console.log(enemyPokemon.name, enemyPokemon.species.speciesId, enemyPokemon.stats);
});
2023-04-10 19:12:01 +01:00
if (this.scene.getParty().filter(p => p.isShiny()).length === 6) {
this.scene.validateAchv(achvs.SHINY_PARTY);
}
if (battle.battleType === BattleType.TRAINER) {
loadEnemyAssets.push(battle.trainer.loadAssets().then(() => battle.trainer.initSprite()));
} else {
if (battle.enemyParty.filter(p => p.isBoss()).length > 1) {
for (const enemyPokemon of battle.enemyParty) {
2024-03-29 14:29:19 +00:00
if (enemyPokemon.isBoss()) {
enemyPokemon.setBoss(true, Math.ceil(enemyPokemon.bossSegments * (enemyPokemon.getSpeciesForm().baseTotal / totalBst)));
2024-03-29 14:29:19 +00:00
enemyPokemon.initBattleInfo();
}
}
}
}
2023-10-07 21:08:33 +01:00
Promise.all(loadEnemyAssets).then(() => {
2023-10-07 21:08:33 +01:00
battle.enemyParty.forEach((enemyPokemon, e) => {
if (e < (battle.double ? 2 : 1)) {
if (battle.battleType === BattleType.WILD) {
this.scene.field.add(enemyPokemon);
battle.seenEnemyPartyMemberIds.add(enemyPokemon.id);
2023-10-07 21:08:33 +01:00
const playerPokemon = this.scene.getPlayerPokemon();
if (playerPokemon?.visible) {
2023-10-07 21:08:33 +01:00
this.scene.field.moveBelow(enemyPokemon as Pokemon, playerPokemon);
}
2023-10-07 21:08:33 +01:00
enemyPokemon.tint(0, 0.5);
} else if (battle.battleType === BattleType.TRAINER) {
enemyPokemon.setVisible(false);
this.scene.currentBattle.trainer.tint(0, 0.5);
}
if (battle.double) {
2023-10-07 21:08:33 +01:00
enemyPokemon.setFieldPosition(e ? FieldPosition.RIGHT : FieldPosition.LEFT);
}
2023-10-07 21:08:33 +01:00
}
});
2023-04-10 19:12:01 +01:00
2023-04-28 20:03:42 +01:00
if (!this.loaded) {
regenerateModifierPoolThresholds(this.scene.getEnemyField(), battle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD);
2023-04-28 20:03:42 +01:00
this.scene.generateEnemyModifiers();
}
2023-04-21 00:44:56 +01:00
2023-04-28 20:03:42 +01:00
this.scene.ui.setMode(Mode.MESSAGE).then(() => {
2023-11-13 04:47:04 +00:00
if (!this.loaded) {
this.scene.gameData.saveAll(this.scene, true, battle.waveIndex % 10 === 1 || this.scene.lastSavePlayTime >= 300).then(success => {
this.scene.disableMenu = false;
if (!success) {
2023-12-30 23:41:25 +00:00
return this.scene.reset(true);
}
2024-05-13 04:01:05 +01:00
this.doEncounter();
2023-12-30 23:41:25 +00:00
});
} else {
2023-12-30 23:41:25 +00:00
this.doEncounter();
}
2023-04-28 20:03:42 +01:00
});
2023-04-10 19:12:01 +01:00
});
}
doEncounter() {
this.scene.playBgm(undefined, true);
2023-12-16 15:15:25 +00:00
this.scene.updateModifiers(false);
this.scene.setFieldScale(1);
2023-12-16 15:15:25 +00:00
2023-11-29 02:35:52 +00:00
/*if (startingWave > 10) {
2023-05-30 14:46:42 +01:00
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.updateModifiers(true);
2023-11-29 02:35:52 +00:00
}*/
for (const pokemon of this.scene.getParty()) {
if (pokemon) {
2023-11-29 02:35:52 +00:00
pokemon.resetBattleData();
}
}
if (!this.loaded) {
this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false);
}
const enemyField = this.scene.getEnemyField();
2023-04-10 19:12:01 +01:00
this.scene.tweens.add({
2023-10-07 21:08:33 +01:00
targets: [ this.scene.arenaEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.arenaPlayer, this.scene.trainer ].flat(),
x: (_target, _key, value, fieldIndex: integer) => fieldIndex < 2 + (enemyField.length) ? value + 300 : value - 300,
2023-04-10 19:12:01 +01:00
duration: 2000,
onComplete: () => {
if (!this.tryOverrideForBattleSpec()) {
this.doEncounterCommon();
}
}
2023-04-10 19:12:01 +01:00
});
}
getEncounterMessage(): string {
const enemyField = this.scene.getEnemyField();
if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) {
return i18next.t("battle:bossAppeared", {bossName: enemyField[0].name});
}
if (this.scene.currentBattle.battleType === BattleType.TRAINER) {
if (this.scene.currentBattle.double) {
return i18next.t("battle:trainerAppearedDouble", {trainerName: this.scene.currentBattle.trainer.getName(TrainerSlot.NONE, true)});
} else {
return i18next.t("battle:trainerAppeared", {trainerName: this.scene.currentBattle.trainer.getName(TrainerSlot.NONE, true)});
}
}
return enemyField.length === 1
? i18next.t("battle:singleWildAppeared", {pokemonName: enemyField[0].name})
: i18next.t("battle:multiWildAppeared", {pokemonName1: enemyField[0].name, pokemonName2: enemyField[1].name});
}
doEncounterCommon(showEncounterMessage: boolean = true) {
2023-10-07 21:08:33 +01:00
const enemyField = this.scene.getEnemyField();
if (this.scene.currentBattle.battleType === BattleType.WILD) {
enemyField.forEach(enemyPokemon => {
enemyPokemon.untint(100, "Sine.easeOut");
2023-10-07 21:08:33 +01:00
enemyPokemon.cry();
enemyPokemon.showInfo();
if (enemyPokemon.isShiny()) {
this.scene.validateAchv(achvs.SEE_SHINY);
}
2023-10-07 21:08:33 +01:00
});
2024-02-15 04:25:12 +00:00
this.scene.updateFieldScale();
if (showEncounterMessage) {
this.scene.ui.showText(this.getEncounterMessage(), null, () => this.end(), 1500);
} else {
this.end();
}
2023-10-07 21:08:33 +01:00
} else if (this.scene.currentBattle.battleType === BattleType.TRAINER) {
const trainer = this.scene.currentBattle.trainer;
trainer.untint(100, "Sine.easeOut");
trainer.playAnim();
2024-05-24 00:45:04 +01:00
const doSummon = () => {
this.scene.currentBattle.started = true;
this.scene.playBgm(undefined);
this.scene.pbTray.showPbTray(this.scene.getParty());
this.scene.pbTrayEnemy.showPbTray(this.scene.getEnemyParty());
const doTrainerSummon = () => {
this.hideEnemyTrainer();
2023-12-02 15:30:23 +00:00
const availablePartyMembers = this.scene.getEnemyParty().filter(p => !p.isFainted()).length;
this.scene.unshiftPhase(new SummonPhase(this.scene, 0, false));
if (this.scene.currentBattle.double && availablePartyMembers > 1) {
this.scene.unshiftPhase(new SummonPhase(this.scene, 1, false));
}
this.end();
};
if (showEncounterMessage) {
this.scene.ui.showText(this.getEncounterMessage(), null, doTrainerSummon, 1500, true);
} else {
doTrainerSummon();
}
};
2024-05-24 00:45:04 +01:00
2024-02-14 19:41:39 +00:00
const encounterMessages = this.scene.currentBattle.trainer.getEncounterMessages();
if (!encounterMessages?.length) {
doSummon();
} else {
let message: string;
this.scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.waveIndex);
const showDialogueAndSummon = () => {
this.scene.ui.showDialogue(message, trainer.getName(TrainerSlot.NONE,true), null, () => {
this.scene.charSprite.hide().then(() => this.scene.hideFieldOverlay(250).then(() => doSummon()));
});
};
if (this.scene.currentBattle.trainer.config.hasCharSprite && !this.scene.ui.shouldSkipDialogue(message)) {
2024-02-26 00:09:24 +00:00
this.scene.showFieldOverlay(500).then(() => this.scene.charSprite.showCharacter(trainer.getKey(), getCharVariantFromDialogue(encounterMessages[0])).then(() => showDialogueAndSummon()));
} else {
showDialogueAndSummon();
}
}
2023-10-07 21:08:33 +01:00
}
}
2023-04-10 19:12:01 +01:00
end() {
const enemyField = this.scene.getEnemyField();
enemyField.forEach((enemyPokemon, e) => {
if (enemyPokemon.isShiny()) {
this.scene.unshiftPhase(new ShinySparklePhase(this.scene, BattlerIndex.ENEMY + e));
}
});
2023-11-12 17:49:06 +00:00
if (this.scene.currentBattle.battleType !== BattleType.TRAINER) {
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) {
return false;
}
// how many player pokemon are on the field ?
const pokemonsOnFieldCount = this.scene.getParty().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);
// if it's a double, there should be 2, otherwise 1
if (this.scene.currentBattle.double) {
return pokemonsOnFieldCount === requiredPokemonsOnField;
}
return pokemonsOnFieldCount === 1;
}));
2023-11-12 17:49:06 +00:00
const ivScannerModifier = this.scene.findModifier(m => m instanceof IvScannerModifier);
if (ivScannerModifier) {
2024-04-06 15:37:54 +01:00
enemyField.map(p => this.scene.pushPhase(new ScanIvsPhase(this.scene, p.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6))));
}
2023-11-12 17:49:06 +00:00
}
2023-12-02 15:30:23 +00:00
if (!this.loaded) {
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle());
2023-12-02 15:30:23 +00:00
if (!availablePartyMembers[0].isOnField()) {
2023-12-02 15:30:23 +00:00
this.scene.pushPhase(new SummonPhase(this.scene, 0));
}
2023-12-02 15:30:23 +00:00
if (this.scene.currentBattle.double) {
if (availablePartyMembers.length > 1) {
this.scene.pushPhase(new ToggleDoublePositionPhase(this.scene, true));
if (!availablePartyMembers[1].isOnField()) {
2023-12-02 15:30:23 +00:00
this.scene.pushPhase(new SummonPhase(this.scene, 1));
}
2023-12-02 15:30:23 +00:00
}
} else {
if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) {
2023-12-02 15:30:23 +00:00
this.scene.pushPhase(new ReturnPhase(this.scene, 1));
}
2023-12-02 15:30:23 +00:00
this.scene.pushPhase(new ToggleDoublePositionPhase(this.scene, false));
}
2024-05-24 00:45:04 +01:00
if (this.scene.currentBattle.battleType !== BattleType.TRAINER && (this.scene.currentBattle.waveIndex > 1 || !this.scene.gameMode.isDaily)) {
const minPartySize = this.scene.currentBattle.double ? 2 : 1;
if (availablePartyMembers.length > minPartySize) {
this.scene.pushPhase(new CheckSwitchPhase(this.scene, 0, this.scene.currentBattle.double));
if (this.scene.currentBattle.double) {
this.scene.pushPhase(new CheckSwitchPhase(this.scene, 1, this.scene.currentBattle.double));
}
}
2023-12-02 15:30:23 +00:00
}
}
2024-02-14 15:44:55 +00:00
handleTutorial(this.scene, Tutorial.Access_Menu).then(() => super.end());
2023-04-10 19:12:01 +01:00
}
tryOverrideForBattleSpec(): boolean {
switch (this.scene.currentBattle.battleSpec) {
case BattleSpec.FINAL_BOSS:
const enemy = this.scene.getEnemyPokemon();
this.scene.ui.showText(this.getEncounterMessage(), null, () => {
this.scene.ui.showDialogue(battleSpecDialogue[BattleSpec.FINAL_BOSS].encounter, enemy.species.name, null, () => {
this.doEncounterCommon(false);
});
}, 1500, true);
return true;
}
return false;
}
2023-04-10 19:12:01 +01:00
}
export class NextEncounterPhase extends EncounterPhase {
constructor(scene: BattleScene) {
super(scene);
}
start() {
super.start();
}
2023-04-10 19:12:01 +01:00
doEncounter(): void {
this.scene.playBgm(undefined, true);
for (const pokemon of this.scene.getParty()) {
if (pokemon) {
2024-02-26 20:14:47 +00:00
pokemon.resetBattleData();
}
2024-02-26 20:14:47 +00:00
}
this.scene.arenaNextEnemy.setBiome(this.scene.arena.biomeType);
this.scene.arenaNextEnemy.setVisible(true);
const enemyField = this.scene.getEnemyField();
2023-04-10 19:12:01 +01:00
this.scene.tweens.add({
targets: [ this.scene.arenaEnemy, this.scene.arenaNextEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.lastEnemyTrainer ].flat(),
x: "+=300",
2023-04-10 19:12:01 +01:00
duration: 2000,
onComplete: () => {
this.scene.arenaEnemy.setBiome(this.scene.arena.biomeType);
2023-04-10 19:12:01 +01:00
this.scene.arenaEnemy.setX(this.scene.arenaNextEnemy.x);
2023-05-07 22:05:19 +01:00
this.scene.arenaEnemy.setAlpha(1);
2023-04-10 19:12:01 +01:00
this.scene.arenaNextEnemy.setX(this.scene.arenaNextEnemy.x - 300);
this.scene.arenaNextEnemy.setVisible(false);
if (this.scene.lastEnemyTrainer) {
2023-10-23 03:45:48 +01:00
this.scene.lastEnemyTrainer.destroy();
}
2024-05-24 00:45:04 +01:00
if (!this.tryOverrideForBattleSpec()) {
this.doEncounterCommon();
}
2023-04-10 19:12:01 +01:00
}
});
}
}
export class NewBiomeEncounterPhase extends NextEncounterPhase {
constructor(scene: BattleScene) {
super(scene);
}
doEncounter(): void {
2024-03-17 02:06:56 +00:00
this.scene.playBgm(undefined, true);
for (const pokemon of this.scene.getParty()) {
if (pokemon) {
2023-11-29 02:35:52 +00:00
pokemon.resetBattleData();
}
2023-11-29 02:35:52 +00:00
}
2023-12-30 02:04:40 +00:00
this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false);
for (const pokemon of this.scene.getParty().filter(p => p.isOnField())) {
applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null);
}
const enemyField = this.scene.getEnemyField();
2023-04-10 19:12:01 +01:00
this.scene.tweens.add({
targets: [ this.scene.arenaEnemy, enemyField ].flat(),
x: "+=300",
2023-04-10 19:12:01 +01:00
duration: 2000,
onComplete: () => {
if (!this.tryOverrideForBattleSpec()) {
this.doEncounterCommon();
}
}
2023-04-10 19:12:01 +01:00
});
}
}
2023-07-04 22:50:51 +01:00
export class PostSummonPhase extends PokemonPhase {
constructor(scene: BattleScene, battlerIndex: BattlerIndex) {
super(scene, battlerIndex);
}
start() {
super.start();
const pokemon = this.getPokemon();
if (pokemon.status?.effect === StatusEffect.TOXIC) {
pokemon.status.turnCount = 0;
}
2023-07-04 22:50:51 +01:00
this.scene.arena.applyTags(ArenaTrapTag, pokemon);
2023-12-23 03:46:05 +00:00
applyPostSummonAbAttrs(PostSummonAbAttr, pokemon).then(() => this.end());
2023-07-04 22:50:51 +01:00
}
}
2023-04-12 05:37:56 +01:00
export class SelectBiomePhase extends BattlePhase {
2023-04-10 19:12:01 +01:00
constructor(scene: BattleScene) {
super(scene);
}
start() {
super.start();
2023-04-12 05:37:56 +01:00
const currentBiome = this.scene.arena.biomeType;
const setNextBiome = (nextBiome: Biome) => {
if (this.scene.currentBattle.waveIndex % 10 === 1) {
this.scene.applyModifiers(MoneyInterestModifier, true, this.scene);
this.scene.unshiftPhase(new PartyHealPhase(this.scene, false));
}
2023-04-12 05:37:56 +01:00
this.scene.unshiftPhase(new SwitchBiomePhase(this.scene, nextBiome));
this.end();
};
2024-03-17 02:06:56 +00:00
if ((this.scene.gameMode.isClassic && this.scene.gameMode.isWaveFinal(this.scene.currentBattle.waveIndex + 9))
2024-04-23 01:30:46 +01:00
|| (this.scene.gameMode.isDaily && this.scene.gameMode.isWaveFinal(this.scene.currentBattle.waveIndex))
|| (this.scene.gameMode.hasShortBiomes && !(this.scene.currentBattle.waveIndex % 50))) {
setNextBiome(Biome.END);
} else if (this.scene.gameMode.hasRandomBiomes) {
2023-12-07 18:41:47 +00:00
setNextBiome(this.generateNextBiome());
2024-06-14 18:16:32 +01:00
} else if (Array.isArray(biomeLinks[currentBiome])) {
let biomes: Biome[];
this.scene.executeWithSeedOffset(() => {
biomes = (biomeLinks[currentBiome] as (Biome | [Biome, integer])[])
.filter(b => !Array.isArray(b) || !Utils.randSeedInt(b[1]))
.map(b => !Array.isArray(b) ? b : b[0]);
}, this.scene.currentBattle.waveIndex);
if (biomes.length > 1 && this.scene.findModifier(m => m instanceof MapModifier)) {
let biomeChoices: Biome[];
this.scene.executeWithSeedOffset(() => {
2024-06-14 18:16:32 +01:00
biomeChoices = (!Array.isArray(biomeLinks[currentBiome])
? [ biomeLinks[currentBiome] as Biome ]
: biomeLinks[currentBiome] as (Biome | [Biome, integer])[])
.filter((b, i) => !Array.isArray(b) || !Utils.randSeedInt(b[1]))
.map(b => Array.isArray(b) ? b[0] : b);
}, this.scene.currentBattle.waveIndex);
const biomeSelectItems = biomeChoices.map(b => {
const ret: OptionSelectItem = {
label: getBiomeName(b),
handler: () => {
this.scene.ui.setMode(Mode.MESSAGE);
setNextBiome(b);
return true;
}
};
return ret;
});
this.scene.ui.setMode(Mode.OPTION_SELECT, {
options: biomeSelectItems,
delay: 1000
2023-04-27 00:19:39 +01:00
});
} else {
setNextBiome(biomes[Utils.randSeedInt(biomes.length)]);
}
} else if (biomeLinks.hasOwnProperty(currentBiome)) {
2024-06-14 18:16:32 +01:00
setNextBiome(biomeLinks[currentBiome] as Biome);
} else {
2024-04-23 01:30:46 +01:00
setNextBiome(this.generateNextBiome());
}
2023-04-12 05:37:56 +01:00
}
2023-12-07 18:41:47 +00:00
generateNextBiome(): Biome {
if (!(this.scene.currentBattle.waveIndex % 50)) {
2023-12-07 18:41:47 +00:00
return Biome.END;
}
return this.scene.generateRandomBiome(this.scene.currentBattle.waveIndex);
2023-12-07 18:41:47 +00:00
}
2023-04-12 05:37:56 +01:00
}
export class SwitchBiomePhase extends BattlePhase {
private nextBiome: Biome;
constructor(scene: BattleScene, nextBiome: Biome) {
super(scene);
this.nextBiome = nextBiome;
}
start() {
super.start();
if (this.nextBiome === undefined) {
return this.end();
}
2023-04-10 19:12:01 +01:00
this.scene.tweens.add({
targets: [ this.scene.arenaEnemy, this.scene.lastEnemyTrainer ],
x: "+=300",
2023-04-10 19:12:01 +01:00
duration: 2000,
onComplete: () => {
this.scene.arenaEnemy.setX(this.scene.arenaEnemy.x - 600);
this.scene.newArena(this.nextBiome);
2023-04-10 19:12:01 +01:00
const biomeKey = getBiomeKey(this.nextBiome);
2023-04-10 19:12:01 +01:00
const bgTexture = `${biomeKey}_bg`;
this.scene.arenaBgTransition.setTexture(bgTexture);
2023-04-10 19:12:01 +01:00
this.scene.arenaBgTransition.setAlpha(0);
this.scene.arenaBgTransition.setVisible(true);
this.scene.arenaPlayerTransition.setBiome(this.nextBiome);
2023-04-10 19:12:01 +01:00
this.scene.arenaPlayerTransition.setAlpha(0);
this.scene.arenaPlayerTransition.setVisible(true);
this.scene.tweens.add({
targets: [ this.scene.arenaPlayer, this.scene.arenaBgTransition, this.scene.arenaPlayerTransition ],
2023-04-10 19:12:01 +01:00
duration: 1000,
delay: 1000,
ease: "Sine.easeInOut",
2023-04-30 16:38:46 +01:00
alpha: (target: any) => target === this.scene.arenaPlayer ? 0 : 1,
2023-04-10 19:12:01 +01:00
onComplete: () => {
this.scene.arenaBg.setTexture(bgTexture);
this.scene.arenaPlayer.setBiome(this.nextBiome);
2023-04-30 16:38:46 +01:00
this.scene.arenaPlayer.setAlpha(1);
this.scene.arenaEnemy.setBiome(this.nextBiome);
2023-05-07 22:05:19 +01:00
this.scene.arenaEnemy.setAlpha(1);
this.scene.arenaNextEnemy.setBiome(this.nextBiome);
2023-04-10 19:12:01 +01:00
this.scene.arenaBgTransition.setVisible(false);
this.scene.arenaPlayerTransition.setVisible(false);
if (this.scene.lastEnemyTrainer) {
2023-10-23 03:45:48 +01:00
this.scene.lastEnemyTrainer.destroy();
}
2023-04-10 19:12:01 +01:00
this.end();
}
2023-04-30 16:38:46 +01:00
});
2023-04-10 19:12:01 +01:00
}
});
}
}
export class SummonPhase extends PartyMemberPokemonPhase {
2024-04-11 01:57:22 +01:00
private loaded: boolean;
constructor(scene: BattleScene, fieldIndex: integer, player: boolean = true, loaded: boolean = false) {
super(scene, fieldIndex, player);
this.loaded = loaded;
2023-04-10 19:12:01 +01:00
}
start() {
super.start();
this.preSummon();
}
/**
* Sends out a Pokemon before the battle begins and shows the appropriate messages
*/
2023-04-10 19:12:01 +01:00
preSummon(): void {
const partyMember = this.getPokemon();
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
// If the Pokemon about to be sent out is fainted or illegal under a challenge, switch to the first non-fainted legal Pokemon
if (!partyMember.isAllowedInBattle()) {
console.warn("The Pokemon about to be sent out is fainted or illegal under a challenge. Attempting to resolve...");
// First check if they're somehow still in play, if so remove them.
if (partyMember.isOnField()) {
partyMember.hideInfo();
partyMember.setVisible(false);
this.scene.field.remove(partyMember);
this.scene.triggerPokemonFormChange(partyMember, SpeciesFormChangeActiveTrigger, true);
}
2023-10-07 21:08:33 +01:00
const party = this.getParty();
2024-05-24 00:45:04 +01:00
// Find the first non-fainted Pokemon index above the current one
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
const legalIndex = party.findIndex((p, i) => i > this.partyMemberIndex && p.isAllowedInBattle());
if (legalIndex === -1) {
console.error("Party Details:\n", party);
console.error("All available Pokemon were fainted or illegal!");
this.scene.clearPhaseQueue();
this.scene.unshiftPhase(new GameOverPhase(this.scene));
this.end();
return;
}
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
// Swaps the fainted Pokemon and the first non-fainted legal Pokemon in the party
[party[this.partyMemberIndex], party[legalIndex]] = [party[legalIndex], party[this.partyMemberIndex]];
console.warn("Swapped %s %O with %s %O", partyMember?.name, partyMember, party[0]?.name, party[0]);
}
2023-10-07 21:08:33 +01:00
if (this.player) {
this.scene.ui.showText(i18next.t("battle:playerGo", { pokemonName: this.getPokemon().name }));
if (this.player) {
this.scene.pbTray.hide();
}
this.scene.trainer.setTexture(`trainer_${this.scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`);
this.scene.time.delayedCall(562, () => {
this.scene.trainer.setFrame("2");
this.scene.time.delayedCall(64, () => {
this.scene.trainer.setFrame("3");
});
});
2023-10-07 21:08:33 +01:00
this.scene.tweens.add({
targets: this.scene.trainer,
x: -36,
duration: 1000,
onComplete: () => this.scene.trainer.setVisible(false)
2023-10-07 21:08:33 +01:00
});
this.scene.time.delayedCall(750, () => this.summon());
} else {
const trainerName = this.scene.currentBattle.trainer.getName(!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER);
const pokemonName = this.getPokemon().name;
const message = i18next.t("battle:trainerSendOut", { trainerName, pokemonName });
this.scene.pbTrayEnemy.hide();
this.scene.ui.showText(message, null, () => this.summon());
}
2023-04-10 19:12:01 +01:00
}
summon(): void {
const pokemon = this.getPokemon();
const pokeball = this.scene.addFieldSprite(this.player ? 36 : 248, this.player ? 80 : 44, "pb", getPokeballAtlasKey(pokemon.pokeball));
2023-04-10 19:12:01 +01:00
pokeball.setVisible(false);
pokeball.setOrigin(0.5, 0.625);
this.scene.field.add(pokeball);
if (this.fieldIndex === 1) {
2023-10-07 21:08:33 +01:00
pokemon.setFieldPosition(FieldPosition.RIGHT, 0);
} else {
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
const availablePartyMembers = this.getParty().filter(p => p.isAllowedInBattle()).length;
2023-10-07 21:08:33 +01:00
pokemon.setFieldPosition(!this.scene.currentBattle.double || availablePartyMembers === 1 ? FieldPosition.CENTER : FieldPosition.LEFT);
}
2023-10-07 21:08:33 +01:00
const fpOffset = pokemon.getFieldPositionOffset();
2023-04-10 19:12:01 +01:00
pokeball.setVisible(true);
this.scene.tweens.add({
targets: pokeball,
duration: 650,
2023-10-07 21:08:33 +01:00
x: (this.player ? 100 : 236) + fpOffset[0]
});
2023-04-10 19:12:01 +01:00
this.scene.tweens.add({
targets: pokeball,
duration: 150,
ease: "Cubic.easeOut",
2023-10-07 21:08:33 +01:00
y: (this.player ? 70 : 34) + fpOffset[1],
2023-04-10 19:12:01 +01:00
onComplete: () => {
this.scene.tweens.add({
targets: pokeball,
duration: 500,
ease: "Cubic.easeIn",
angle: 1440,
2023-10-07 21:08:33 +01:00
y: (this.player ? 132 : 86) + fpOffset[1],
2023-04-10 19:12:01 +01:00
onComplete: () => {
this.scene.playSound("pb_rel");
2023-04-10 19:12:01 +01:00
pokeball.destroy();
2023-10-07 21:08:33 +01:00
this.scene.add.existing(pokemon);
this.scene.field.add(pokemon);
if (!this.player) {
const playerPokemon = this.scene.getPlayerPokemon() as Pokemon;
if (playerPokemon?.visible) {
2023-10-07 21:08:33 +01:00
this.scene.field.moveBelow(pokemon, playerPokemon);
}
this.scene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id);
2023-10-07 21:08:33 +01:00
}
addPokeballOpenParticles(this.scene, pokemon.x, pokemon.y - 16, pokemon.pokeball);
this.scene.updateModifiers(this.player);
2024-02-15 04:25:12 +00:00
this.scene.updateFieldScale();
2023-10-07 21:08:33 +01:00
pokemon.showInfo();
pokemon.playAnim();
pokemon.setVisible(true);
2023-10-31 18:09:33 +00:00
pokemon.getSprite().setVisible(true);
2023-10-07 21:08:33 +01:00
pokemon.setScale(0.5);
pokemon.tint(getPokeballTintColor(pokemon.pokeball));
pokemon.untint(250, "Sine.easeIn");
2024-02-15 04:25:12 +00:00
this.scene.updateFieldScale();
2023-04-10 19:12:01 +01:00
this.scene.tweens.add({
2023-10-07 21:08:33 +01:00
targets: pokemon,
2023-04-10 19:12:01 +01:00
duration: 250,
ease: "Sine.easeIn",
2024-02-15 04:25:12 +00:00
scale: pokemon.getSpriteScale(),
2023-04-10 19:12:01 +01:00
onComplete: () => {
2024-03-29 18:42:27 +00:00
pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 });
2023-10-07 21:08:33 +01:00
pokemon.getSprite().clearTint();
pokemon.resetSummonData();
2023-04-10 19:12:01 +01:00
this.scene.time.delayedCall(1000, () => this.end());
}
});
}
});
}
});
}
2023-07-05 21:13:00 +01:00
onEnd(): void {
const pokemon = this.getPokemon();
if (pokemon.isShiny()) {
this.scene.unshiftPhase(new ShinySparklePhase(this.scene, pokemon.getBattlerIndex()));
}
2023-04-10 19:12:01 +01:00
pokemon.resetTurnData();
if (!this.loaded || this.scene.currentBattle.battleType === BattleType.TRAINER || (this.scene.currentBattle.waveIndex % 10) === 1) {
2024-04-11 01:57:22 +01:00
this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
this.queuePostSummon();
}
2023-07-05 21:13:00 +01:00
}
queuePostSummon(): void {
this.scene.pushPhase(new PostSummonPhase(this.scene, this.getPokemon().getBattlerIndex()));
}
end() {
this.onEnd();
2023-05-04 15:25:11 +01:00
2023-04-10 19:12:01 +01:00
super.end();
}
}
export class SwitchSummonPhase extends SummonPhase {
private slotIndex: integer;
private doReturn: boolean;
2023-04-29 00:26:41 +01:00
private batonPass: boolean;
2023-04-10 19:12:01 +01:00
2023-10-07 21:08:33 +01:00
private lastPokemon: Pokemon;
2023-04-29 00:26:41 +01:00
2023-10-07 21:08:33 +01:00
constructor(scene: BattleScene, fieldIndex: integer, slotIndex: integer, doReturn: boolean, batonPass: boolean, player?: boolean) {
super(scene, fieldIndex, player !== undefined ? player : true);
2023-04-10 19:12:01 +01:00
this.slotIndex = slotIndex;
this.doReturn = doReturn;
2023-04-29 00:26:41 +01:00
this.batonPass = batonPass;
2023-04-10 19:12:01 +01:00
}
start(): void {
super.start();
}
2023-04-10 19:12:01 +01:00
preSummon(): void {
if (!this.player) {
if (this.slotIndex === -1) {
this.slotIndex = this.scene.currentBattle.trainer.getNextSummonIndex(!this.fieldIndex ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER);
}
if (this.slotIndex > -1) {
this.showEnemyTrainer(!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER);
this.scene.pbTrayEnemy.showPbTray(this.scene.getEnemyParty());
}
}
if (!this.doReturn || (this.slotIndex !== -1 && !(this.player ? this.scene.getParty() : this.scene.getEnemyParty())[this.slotIndex])) {
if (this.player) {
return this.switchAndSummon();
} else {
this.scene.time.delayedCall(750, () => this.switchAndSummon());
return;
}
2023-04-10 19:12:01 +01:00
}
2023-10-07 21:08:33 +01:00
const pokemon = this.getPokemon();
2023-04-10 19:12:01 +01:00
if (!this.batonPass) {
(this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id));
}
2023-04-23 03:14:53 +01:00
this.scene.ui.showText(this.player ?
i18next.t("battle:playerComeBack", { pokemonName: pokemon.name }) :
i18next.t("battle:trainerComeBack", {
trainerName: this.scene.currentBattle.trainer.getName(!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER),
pokemonName: pokemon.name
})
);
this.scene.playSound("pb_rel");
2023-10-07 21:08:33 +01:00
pokemon.hideInfo();
pokemon.tint(getPokeballTintColor(pokemon.pokeball), 1, 250, "Sine.easeIn");
2023-04-10 19:12:01 +01:00
this.scene.tweens.add({
2023-10-07 21:08:33 +01:00
targets: pokemon,
2023-04-10 19:12:01 +01:00
duration: 250,
ease: "Sine.easeIn",
2023-04-10 19:12:01 +01:00
scale: 0.5,
onComplete: () => {
2023-10-07 21:08:33 +01:00
pokemon.setVisible(false);
this.scene.field.remove(pokemon);
2024-01-10 04:34:43 +00:00
this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
2023-04-10 19:12:01 +01:00
this.scene.time.delayedCall(750, () => this.switchAndSummon());
}
});
}
switchAndSummon() {
const party = this.player ? this.getParty() : this.scene.getEnemyParty();
2023-04-10 19:12:01 +01:00
const switchedPokemon = party[this.slotIndex];
this.lastPokemon = this.getPokemon();
applyPreSwitchOutAbAttrs(PreSwitchOutAbAttr, this.lastPokemon);
if (this.batonPass && switchedPokemon) {
(this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.transferTagsBySourceId(this.lastPokemon.id, switchedPokemon.id));
2023-04-29 00:26:41 +01:00
if (!this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === switchedPokemon.id)) {
const batonPassModifier = this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier
&& (m as SwitchEffectTransferModifier).pokemonId === this.lastPokemon.id) as SwitchEffectTransferModifier;
if (batonPassModifier && !this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === switchedPokemon.id)) {
this.scene.tryTransferHeldItemModifier(batonPassModifier, switchedPokemon, false);
}
2023-04-29 00:26:41 +01:00
}
}
if (switchedPokemon) {
party[this.slotIndex] = this.lastPokemon;
party[this.fieldIndex] = switchedPokemon;
const showTextAndSummon = () => {
this.scene.ui.showText(this.player ?
i18next.t("battle:playerGo", { pokemonName: switchedPokemon.name }) :
i18next.t("battle:trainerGo", {
trainerName: this.scene.currentBattle.trainer.getName(!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER),
pokemonName: this.getPokemon().name
})
);
this.summon();
};
if (this.player) {
showTextAndSummon();
} else {
this.scene.time.delayedCall(1500, () => {
this.hideEnemyTrainer();
this.scene.pbTrayEnemy.hide();
showTextAndSummon();
});
}
} else {
this.end();
}
2023-04-10 19:12:01 +01:00
}
2023-04-29 00:26:41 +01:00
2023-07-05 21:13:00 +01:00
onEnd(): void {
super.onEnd();
2023-07-04 22:50:51 +01:00
const pokemon = this.getPokemon();
const moveId = this.lastPokemon?.scene.currentBattle.lastMove;
const lastUsedMove = moveId ? allMoves[moveId] : undefined;
const currentCommand = pokemon.scene.currentBattle.turnCommands[this.fieldIndex]?.command;
const lastPokemonIsForceSwitchedAndNotFainted = lastUsedMove?.hasAttr(ForceSwitchOutAttr) && !this.lastPokemon.isFainted();
// Compensate for turn spent summoning
// Or compensate for force switch move if switched out pokemon is not fainted
if (currentCommand === Command.POKEMON || lastPokemonIsForceSwitchedAndNotFainted) {
2024-03-14 22:03:38 +00:00
pokemon.battleSummonData.turnCount--;
}
if (this.batonPass && pokemon) {
pokemon.transferSummon(this.lastPokemon);
}
2023-04-29 00:26:41 +01:00
this.lastPokemon?.resetSummonData();
2024-01-10 04:34:43 +00:00
this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
2023-07-05 21:13:00 +01:00
}
2023-04-29 00:26:41 +01:00
2023-07-05 21:13:00 +01:00
queuePostSummon(): void {
this.scene.unshiftPhase(new PostSummonPhase(this.scene, this.getPokemon().getBattlerIndex()));
2023-04-29 00:26:41 +01:00
}
2023-04-10 19:12:01 +01:00
}
export class ReturnPhase extends SwitchSummonPhase {
constructor(scene: BattleScene, fieldIndex: integer) {
super(scene, fieldIndex, -1, true, false);
}
switchAndSummon(): void {
this.end();
}
summon(): void { }
2023-07-05 21:13:00 +01:00
onEnd(): void {
const pokemon = this.getPokemon();
pokemon.resetTurnData();
pokemon.resetSummonData();
2024-01-10 04:34:43 +00:00
2024-02-15 04:25:12 +00:00
this.scene.updateFieldScale();
2024-01-10 04:34:43 +00:00
this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger);
2023-07-05 21:13:00 +01:00
}
}
export class ShowTrainerPhase extends BattlePhase {
constructor(scene: BattleScene) {
super(scene);
}
start() {
super.start();
this.scene.trainer.setVisible(true);
this.scene.trainer.setTexture(`trainer_${this.scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`);
this.scene.tweens.add({
targets: this.scene.trainer,
x: 106,
duration: 1000,
onComplete: () => this.end()
});
}
}
export class ToggleDoublePositionPhase extends BattlePhase {
private double: boolean;
constructor(scene: BattleScene, double: boolean) {
super(scene);
this.double = double;
}
start() {
super.start();
const playerPokemon = this.scene.getPlayerField().find(p => p.isActive(true));
2023-11-25 20:53:38 +00:00
if (playerPokemon) {
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
playerPokemon.setFieldPosition(this.double && this.scene.getParty().filter(p => p.isAllowedInBattle()).length > 1 ? FieldPosition.LEFT : FieldPosition.CENTER, 500).then(() => {
2023-11-25 20:53:38 +00:00
if (playerPokemon.getFieldIndex() === 1) {
const party = this.scene.getParty();
party[1] = party[0];
party[0] = playerPokemon;
}
this.end();
});
} else {
this.end();
}
}
}
2023-04-10 19:12:01 +01:00
export class CheckSwitchPhase extends BattlePhase {
protected fieldIndex: integer;
protected useName: boolean;
constructor(scene: BattleScene, fieldIndex: integer, useName: boolean) {
super(scene);
this.fieldIndex = fieldIndex;
this.useName = useName;
2023-04-10 19:12:01 +01:00
}
start() {
super.start();
const pokemon = this.scene.getPlayerField()[this.fieldIndex];
if (this.scene.battleStyle === BattleStyle.SET) {
super.end();
return;
}
if (this.scene.field.getAll().indexOf(pokemon) === -1) {
this.scene.unshiftPhase(new SummonMissingPhase(this.scene, this.fieldIndex));
super.end();
return;
}
if (!this.scene.getParty().slice(1).filter(p => p.isActive()).length) {
super.end();
return;
}
if (pokemon.getTag(BattlerTagType.FRENZY)) {
2023-04-15 06:32:16 +01:00
super.end();
return;
}
this.scene.ui.showText(i18next.t("battle:switchQuestion", { pokemonName: this.useName ? pokemon.name : i18next.t("battle:pokemon") }), null, () => {
2023-04-10 19:12:01 +01:00
this.scene.ui.setMode(Mode.CONFIRM, () => {
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.tryRemovePhase(p => p instanceof PostSummonPhase && p.player && p.fieldIndex === this.fieldIndex);
this.scene.unshiftPhase(new SwitchPhase(this.scene, this.fieldIndex, false, true));
2023-04-10 19:12:01 +01:00
this.end();
}, () => {
this.scene.ui.setMode(Mode.MESSAGE);
this.end();
});
2023-04-10 19:12:01 +01:00
});
}
}
export class SummonMissingPhase extends SummonPhase {
constructor(scene: BattleScene, fieldIndex: integer) {
super(scene, fieldIndex);
}
preSummon(): void {
this.scene.ui.showText(i18next.t("battle:sendOutPokemon", { pokemonName: this.getPokemon().name}));
this.scene.time.delayedCall(250, () => this.summon());
}
}
export class LevelCapPhase extends FieldPhase {
constructor(scene: BattleScene) {
super(scene);
}
start(): void {
super.start();
this.scene.ui.setMode(Mode.MESSAGE).then(() => {
this.scene.playSound("level_up_fanfare");
this.scene.ui.showText(i18next.t("battle:levelCapUp", { levelCap: this.scene.getMaxExpLevel() }), null, () => this.end(), null, true);
this.executeForAll(pokemon => pokemon.updateInfo(true));
});
}
}
export class TurnInitPhase extends FieldPhase {
constructor(scene: BattleScene) {
super(scene);
}
start() {
super.start();
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
this.scene.getPlayerField().forEach(p => {
// If this pokemon is in play and evolved into something illegal under the current challenge, force a switch
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());
if (!allowedPokemon.length) {
// If there are no longer any legal pokemon in the party, game over.
this.scene.clearPhaseQueue();
this.scene.unshiftPhase(new GameOverPhase(this.scene));
} else if (allowedPokemon.length >= this.scene.currentBattle.getBattlerCount() || (this.scene.currentBattle.double && !allowedPokemon[0].isActive(true))) {
// If there is at least one pokemon in the back that is legal to switch in, force a switch.
p.switchOut(false, true);
} else {
// If there are no pokemon in the back but we're not game overing, just hide the pokemon.
// This should only happen in double battles.
p.hideInfo();
p.setVisible(false);
this.scene.field.remove(p);
this.scene.triggerPokemonFormChange(p, SpeciesFormChangeActiveTrigger, true);
}
if (allowedPokemon.length === 1 && this.scene.currentBattle.double) {
this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true));
}
}
});
2023-12-22 01:58:00 +00:00
//this.scene.pushPhase(new MoveAnimTestPhase(this.scene));
this.scene.eventTarget.dispatchEvent(new TurnInitEvent());
2023-12-22 01:58:00 +00:00
2023-06-01 18:54:52 +01:00
this.scene.getField().forEach((pokemon, i) => {
if (pokemon?.isActive()) {
if (pokemon.isPlayer()) {
2023-06-01 18:54:52 +01:00
this.scene.currentBattle.addParticipant(pokemon as PlayerPokemon);
}
2023-06-01 18:54:52 +01:00
pokemon.resetTurnData();
2023-06-01 18:54:52 +01:00
this.scene.pushPhase(pokemon.isPlayer() ? new CommandPhase(this.scene, i) : new EnemyCommandPhase(this.scene, i - BattlerIndex.ENEMY));
}
});
this.scene.pushPhase(new TurnStartPhase(this.scene));
this.end();
}
}
export class CommandPhase extends FieldPhase {
protected fieldIndex: integer;
constructor(scene: BattleScene, fieldIndex: integer) {
super(scene);
this.fieldIndex = fieldIndex;
2023-04-10 19:12:01 +01:00
}
start() {
super.start();
if (this.fieldIndex) {
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
// If we somehow are attempting to check the right pokemon but there's only one pokemon out
// Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching
if (this.scene.getPlayerField().filter(p => p.isActive()).length === 1) {
this.fieldIndex = FieldPosition.CENTER;
} else {
const allyCommand = this.scene.currentBattle.turnCommands[this.fieldIndex - 1];
if (allyCommand.command === Command.BALL || allyCommand.command === Command.RUN) {
this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: allyCommand.command, skip: true };
}
}
}
if (this.scene.currentBattle.turnCommands[this.fieldIndex]?.skip) {
return this.end();
}
const playerPokemon = this.scene.getPlayerField()[this.fieldIndex];
2023-04-13 17:16:36 +01:00
2023-04-21 02:32:48 +01:00
const moveQueue = playerPokemon.getMoveQueue();
while (moveQueue.length && moveQueue[0]
&& moveQueue[0].move && (!playerPokemon.getMoveset().find(m => m.moveId === moveQueue[0].move)
|| !playerPokemon.getMoveset()[playerPokemon.getMoveset().findIndex(m => m.moveId === moveQueue[0].move)].isUsable(playerPokemon, moveQueue[0].ignorePP))) {
moveQueue.shift();
}
2023-04-21 02:32:48 +01:00
if (moveQueue.length) {
const queuedMove = moveQueue[0];
if (!queuedMove.move) {
this.handleCommand(Command.FIGHT, -1, false);
} else {
const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move);
2023-10-25 14:41:37 +01:00
if (moveIndex > -1 && playerPokemon.getMoveset()[moveIndex].isUsable(playerPokemon, queuedMove.ignorePP)) {
this.handleCommand(Command.FIGHT, moveIndex, queuedMove.ignorePP, { targets: queuedMove.targets, multiple: queuedMove.targets.length > 1 });
} else {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}
2023-04-21 02:32:48 +01:00
}
} else {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}
2023-04-10 19:12:01 +01:00
}
2023-04-29 00:26:41 +01:00
handleCommand(command: Command, cursor: integer, ...args: any[]): boolean {
const playerPokemon = this.scene.getPlayerField()[this.fieldIndex];
const enemyField = this.scene.getEnemyField();
2023-04-10 19:12:01 +01:00
let success: boolean;
2023-04-12 00:08:03 +01:00
2023-04-10 19:12:01 +01:00
switch (command) {
case Command.FIGHT:
let useStruggle = false;
2024-05-24 00:45:04 +01:00
if (cursor === -1 ||
playerPokemon.trySelectMove(cursor, args[0] as boolean) ||
(useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m.isUsable(playerPokemon)).length)) {
const moveId = !useStruggle ? cursor > -1 ? playerPokemon.getMoveset()[cursor].moveId : Moves.NONE : Moves.STRUGGLE;
const turnCommand: TurnCommand = { command: Command.FIGHT, cursor: cursor, move: { move: moveId, targets: [], ignorePP: args[0] }, args: args };
const moveTargets: MoveTargetSet = args.length < 3 ? getMoveTargets(playerPokemon, moveId) : args[2];
if (!moveId) {
turnCommand.targets = [ this.fieldIndex ];
}
console.log(moveTargets, playerPokemon.name);
if (moveTargets.targets.length <= 1 || moveTargets.multiple) {
turnCommand.move.targets = moveTargets.targets;
} else if (playerPokemon.getTag(BattlerTagType.CHARGING) && playerPokemon.getMoveQueue().length >= 1) {
turnCommand.move.targets = playerPokemon.getMoveQueue()[0].targets;
} else {
this.scene.unshiftPhase(new SelectTargetPhase(this.scene, this.fieldIndex));
}
this.scene.currentBattle.turnCommands[this.fieldIndex] = turnCommand;
success = true;
} else if (cursor < playerPokemon.getMoveset().length) {
const move = playerPokemon.getMoveset()[cursor];
this.scene.ui.setMode(Mode.MESSAGE);
// Decides between a Disabled, Not Implemented, or No PP translation message
2024-05-24 00:45:04 +01:00
const errorMessage =
playerPokemon.summonData.disabledMove === move.moveId ? "battle:moveDisabled" :
move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" : "battle:moveNoPP";
const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator
this.scene.ui.showText(i18next.t(errorMessage, { moveName: moveName }), null, () => {
this.scene.ui.clearText();
this.scene.ui.setMode(Mode.FIGHT, this.fieldIndex);
}, null, true);
}
break;
case Command.BALL:
if (this.scene.arena.biomeType === Biome.END && (!this.scene.gameMode.isClassic || (this.scene.getEnemyField().filter(p => p.isActive(true)).some(p => !p.scene.gameData.dexData[p.species.speciesId].caughtAttr) && this.scene.gameData.getStarterCount(d => !!d.caughtAttr) < Object.keys(speciesStarters).length - 1))) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.showText(i18next.t("battle:noPokeballForce"), null, () => {
this.scene.ui.showText(null, 0);
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}, null, true);
} else if (this.scene.currentBattle.battleType === BattleType.TRAINER) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.showText(i18next.t("battle:noPokeballTrainer"), null, () => {
this.scene.ui.showText(null, 0);
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}, null, true);
} else {
const targets = this.scene.getEnemyField().filter(p => p.isActive(true)).map(p => p.getBattlerIndex());
if (targets.length > 1) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.showText(i18next.t("battle:noPokeballMulti"), null, () => {
2023-04-30 05:51:33 +01:00
this.scene.ui.showText(null, 0);
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}, null, true);
} else if (cursor < 5) {
const targetPokemon = this.scene.getEnemyField().find(p => p.isActive(true));
if (targetPokemon.isBoss() && targetPokemon.bossSegmentIndex >= 1 && !targetPokemon.hasAbility(Abilities.WONDER_GUARD, false, true) && cursor < PokeballType.MASTER_BALL) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.showText(i18next.t("battle:noPokeballStrong"), null, () => {
this.scene.ui.showText(null, 0);
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}, null, true);
} else {
this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: Command.BALL, cursor: cursor };
this.scene.currentBattle.turnCommands[this.fieldIndex].targets = targets;
if (this.fieldIndex) {
this.scene.currentBattle.turnCommands[this.fieldIndex - 1].skip = true;
}
success = true;
}
2023-04-10 19:12:01 +01:00
}
}
break;
case Command.POKEMON:
case Command.RUN:
const isSwitch = command === Command.POKEMON;
if (!isSwitch && this.scene.arena.biomeType === Biome.END) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.showText(i18next.t("battle:noEscapeForce"), null, () => {
this.scene.ui.showText(null, 0);
2023-10-28 00:56:15 +01:00
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}, null, true);
} else if (!isSwitch && this.scene.currentBattle.battleType === BattleType.TRAINER) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.showText(i18next.t("battle:noEscapeTrainer"), null, () => {
this.scene.ui.showText(null, 0);
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}, null, true);
} else {
const trapTag = playerPokemon.findTag(t => t instanceof TrappedTag) as TrappedTag;
const trapped = new Utils.BooleanHolder(false);
const batonPass = isSwitch && args[0] as boolean;
if (!batonPass) {
enemyField.forEach(enemyPokemon => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, enemyPokemon, trapped, playerPokemon));
}
if (batonPass || (!trapTag && !trapped.value)) {
this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch
? { command: Command.POKEMON, cursor: cursor, args: args }
: { command: Command.RUN };
success = true;
if (!isSwitch && this.fieldIndex) {
this.scene.currentBattle.turnCommands[this.fieldIndex - 1].skip = true;
}
} else if (trapTag) {
if (trapTag.sourceMove === Moves.INGRAIN && this.scene.getPokemonById(trapTag.sourceId).isOfType(Type.GHOST)) {
success = true;
2024-05-24 00:45:04 +01:00
this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch
? { command: Command.POKEMON, cursor: cursor, args: args }
: { command: Command.RUN };
break;
}
2024-05-24 00:45:04 +01:00
if (!isSwitch) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
this.scene.ui.setMode(Mode.MESSAGE);
}
this.scene.ui.showText(
i18next.t("battle:noEscapePokemon", {
pokemonName: this.scene.getPokemonById(trapTag.sourceId).name,
moveName: trapTag.getMoveName(),
escapeVerb: isSwitch ? i18next.t("battle:escapeVerbSwitch") : i18next.t("battle:escapeVerbFlee")
}),
null,
() => {
this.scene.ui.showText(null, 0);
if (!isSwitch) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}
}, null, true);
}
}
break;
2023-04-10 19:12:01 +01:00
}
2023-04-12 05:37:56 +01:00
if (success) {
2023-04-10 19:12:01 +01:00
this.end();
}
2023-04-10 19:12:01 +01:00
return success;
}
cancel() {
if (this.fieldIndex) {
this.scene.unshiftPhase(new CommandPhase(this.scene, 0));
this.scene.unshiftPhase(new CommandPhase(this.scene, 1));
this.end();
}
}
2023-11-16 05:58:57 +00:00
checkFightOverride(): boolean {
const pokemon = this.getPokemon();
const encoreTag = pokemon.getTag(EncoreTag) as EncoreTag;
if (!encoreTag) {
2023-11-16 05:58:57 +00:00
return false;
}
2023-11-16 05:58:57 +00:00
const moveIndex = pokemon.getMoveset().findIndex(m => m.moveId === encoreTag.moveId);
if (moveIndex === -1 || !pokemon.getMoveset()[moveIndex].isUsable(pokemon)) {
2023-11-16 05:58:57 +00:00
return false;
}
2023-11-16 05:58:57 +00:00
this.handleCommand(Command.FIGHT, moveIndex, false);
return true;
}
getFieldIndex(): integer {
return this.fieldIndex;
}
getPokemon(): PlayerPokemon {
return this.scene.getPlayerField()[this.fieldIndex];
}
2023-04-10 19:12:01 +01:00
end() {
this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end());
}
}
export class EnemyCommandPhase extends FieldPhase {
protected fieldIndex: integer;
constructor(scene: BattleScene, fieldIndex: integer) {
super(scene);
this.fieldIndex = fieldIndex;
}
start() {
super.start();
const enemyPokemon = this.scene.getEnemyField()[this.fieldIndex];
const battle = this.scene.currentBattle;
const trainer = battle.trainer;
if (trainer && !enemyPokemon.getMoveQueue().length) {
const opponents = enemyPokemon.getOpponents();
const trapTag = enemyPokemon.findTag(t => t instanceof TrappedTag) as TrappedTag;
const trapped = new Utils.BooleanHolder(false);
opponents.forEach(playerPokemon => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, playerPokemon, trapped, enemyPokemon));
if (!trapTag && !trapped.value) {
const partyMemberScores = trainer.getPartyMemberMatchupScores(enemyPokemon.trainerSlot, true);
if (partyMemberScores.length) {
const matchupScores = opponents.map(opp => enemyPokemon.getMatchupScore(opp));
const matchupScore = matchupScores.reduce((total, score) => total += score, 0) / matchupScores.length;
2024-05-24 00:45:04 +01:00
const sortedPartyMemberScores = trainer.getSortedPartyMemberMatchupScores(partyMemberScores);
const switchMultiplier = 1 - (battle.enemySwitchCounter ? Math.pow(0.1, (1 / battle.enemySwitchCounter)) : 0);
if (sortedPartyMemberScores[0][1] * switchMultiplier >= matchupScore * (trainer.config.isBoss ? 2 : 3)) {
2024-03-25 02:24:26 +00:00
const index = trainer.getNextSummonIndex(enemyPokemon.trainerSlot, partyMemberScores);
battle.turnCommands[this.fieldIndex + BattlerIndex.ENEMY] =
{ command: Command.POKEMON, cursor: index, args: [ false ] };
2024-05-24 00:45:04 +01:00
battle.enemySwitchCounter++;
return this.end();
}
}
}
}
const nextMove = enemyPokemon.getNextMove();
this.scene.currentBattle.turnCommands[this.fieldIndex + BattlerIndex.ENEMY] =
{ command: Command.FIGHT, move: nextMove };
this.scene.currentBattle.enemySwitchCounter = Math.max(this.scene.currentBattle.enemySwitchCounter - 1, 0);
this.end();
}
}
export class SelectTargetPhase extends PokemonPhase {
constructor(scene: BattleScene, fieldIndex: integer) {
super(scene, fieldIndex);
}
start() {
super.start();
const turnCommand = this.scene.currentBattle.turnCommands[this.fieldIndex];
const move = turnCommand.move?.move;
this.scene.ui.setMode(Mode.TARGET_SELECT, this.fieldIndex, move, (cursor: integer) => {
this.scene.ui.setMode(Mode.MESSAGE);
if (cursor === -1) {
this.scene.currentBattle.turnCommands[this.fieldIndex] = null;
this.scene.unshiftPhase(new CommandPhase(this.scene, this.fieldIndex));
} else {
turnCommand.targets = [ cursor ];
}
if (turnCommand.command === Command.BALL && this.fieldIndex) {
this.scene.currentBattle.turnCommands[this.fieldIndex - 1].skip = true;
}
this.end();
});
}
}
export class TurnStartPhase extends FieldPhase {
constructor(scene: BattleScene) {
super(scene);
}
start() {
super.start();
const field = this.scene.getField();
const order = this.getOrder();
2024-05-15 14:42:45 +01:00
const battlerBypassSpeed = {};
this.scene.getField(true).filter(p => p.summonData).map(p => {
const bypassSpeed = new Utils.BooleanHolder(false);
this.scene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed);
battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed;
});
const moveOrder = order.slice(0);
moveOrder.sort((a, b) => {
const aCommand = this.scene.currentBattle.turnCommands[a];
const bCommand = this.scene.currentBattle.turnCommands[b];
if (aCommand.command !== bCommand.command) {
if (aCommand.command === Command.FIGHT) {
return 1;
} else if (bCommand.command === Command.FIGHT) {
return -1;
}
} else if (aCommand.command === Command.FIGHT) {
2024-03-28 20:24:11 +00:00
const aMove = allMoves[aCommand.move.move];
const bMove = allMoves[bCommand.move.move];
const aPriority = new Utils.IntegerHolder(aMove.priority);
const bPriority = new Utils.IntegerHolder(bMove.priority);
2024-05-19 23:52:51 +01:00
applyMoveAttrs(IncrementMovePriorityAttr,this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a),null,aMove,aPriority);
applyMoveAttrs(IncrementMovePriorityAttr,this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b),null,bMove,bPriority);
2024-03-28 20:24:11 +00:00
applyAbAttrs(IncrementMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a), null, aMove, aPriority);
applyAbAttrs(IncrementMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b), null, bMove, bPriority);
2024-05-24 00:45:04 +01:00
if (aPriority.value !== bPriority.value) {
2024-03-28 20:24:11 +00:00
return aPriority.value < bPriority.value ? 1 : -1;
}
}
2024-05-15 14:42:45 +01:00
if (battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) {
2024-05-15 14:42:45 +01:00
return battlerBypassSpeed[a].value ? -1 : 1;
}
2024-05-24 00:45:04 +01:00
const aIndex = order.indexOf(a);
const bIndex = order.indexOf(b);
return aIndex < bIndex ? -1 : aIndex > bIndex ? 1 : 0;
});
for (const o of moveOrder) {
const pokemon = field[o];
const turnCommand = this.scene.currentBattle.turnCommands[o];
if (turnCommand.skip) {
continue;
}
switch (turnCommand.command) {
case Command.FIGHT:
const queuedMove = turnCommand.move;
if (!queuedMove) {
continue;
}
const move = pokemon.getMoveset().find(m => m.moveId === queuedMove.move) || new PokemonMove(queuedMove.move);
if (pokemon.isPlayer()) {
if (turnCommand.cursor === -1) {
this.scene.pushPhase(new MovePhase(this.scene, pokemon, turnCommand.targets || turnCommand.move.targets, move));
} else {
const playerPhase = new MovePhase(this.scene, pokemon, turnCommand.targets || turnCommand.move.targets, move, false, queuedMove.ignorePP);
this.scene.pushPhase(playerPhase);
}
} else {
this.scene.pushPhase(new MovePhase(this.scene, pokemon, turnCommand.targets || turnCommand.move.targets, move, false, queuedMove.ignorePP));
}
break;
case Command.BALL:
this.scene.unshiftPhase(new AttemptCapturePhase(this.scene, turnCommand.targets[0] % 2, turnCommand.cursor));
break;
case Command.POKEMON:
this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, pokemon.getFieldIndex(), turnCommand.cursor, true, turnCommand.args[0] as boolean, pokemon.isPlayer()));
break;
case Command.RUN:
let runningPokemon = pokemon;
if (this.scene.currentBattle.double) {
const playerActivePokemon = field.filter(pokemon => {
if (!!pokemon) {
return pokemon.isPlayer() && pokemon.isActive();
} else {
return;
}
});
// if only one pokemon is alive, use that one
if (playerActivePokemon.length > 1) {
// find which active pokemon has faster speed
const fasterPokemon = playerActivePokemon[0].getStat(Stat.SPD) > playerActivePokemon[1].getStat(Stat.SPD) ? playerActivePokemon[0] : playerActivePokemon[1];
// check if either active pokemon has the ability "Run Away"
const hasRunAway = playerActivePokemon.find(p => p.hasAbility(Abilities.RUN_AWAY));
runningPokemon = hasRunAway !== undefined ? hasRunAway : fasterPokemon;
}
}
this.scene.unshiftPhase(new AttemptRunPhase(this.scene, runningPokemon.getFieldIndex()));
break;
}
}
this.scene.pushPhase(new WeatherEffectPhase(this.scene));
for (const o of order) {
if (field[o].status && field[o].status.isPostTurn()) {
this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, o));
}
}
this.scene.pushPhase(new BerryPhase(this.scene));
this.scene.pushPhase(new TurnEndPhase(this.scene));
this.end();
}
}
/** The phase after attacks where the pokemon eat berries */
export class BerryPhase extends FieldPhase {
start() {
super.start();
this.executeForAll((pokemon) => {
const hasUsableBerry = !!this.scene.findModifier((m) => {
return m instanceof BerryModifier && m.shouldApply([pokemon]);
}, pokemon.isPlayer());
if (hasUsableBerry) {
const cancelled = new Utils.BooleanHolder(false);
pokemon.getOpponents().map((opp) => applyAbAttrs(PreventBerryUseAbAttr, opp, cancelled));
if (cancelled.value) {
pokemon.scene.queueMessage(getPokemonMessage(pokemon, " is too\nnervous to eat berries!"));
} else {
this.scene.unshiftPhase(
new CommonAnimPhase(this.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.USE_ITEM)
);
for (const berryModifier of this.scene.applyModifiers(BerryModifier, pokemon.isPlayer(), pokemon) as BerryModifier[]) {
if (berryModifier.consumed) {
if (!--berryModifier.stackCount) {
this.scene.removeModifier(berryModifier);
} else {
berryModifier.consumed = false;
}
}
this.scene.eventTarget.dispatchEvent(new BerryUsedEvent(berryModifier)); // Announce a berry was used
}
this.scene.updateModifiers(pokemon.isPlayer());
applyAbAttrs(HealFromBerryUseAbAttr, pokemon, new Utils.BooleanHolder(false));
}
}
});
this.end();
}
}
export class TurnEndPhase extends FieldPhase {
2023-04-13 17:16:36 +01:00
constructor(scene: BattleScene) {
super(scene);
}
start() {
super.start();
this.scene.currentBattle.incrementTurn(this.scene);
this.scene.eventTarget.dispatchEvent(new TurnEndEvent(this.scene.currentBattle.turn));
2024-05-24 00:45:04 +01:00
2023-04-19 23:19:55 +01:00
const handlePokemon = (pokemon: Pokemon) => {
2023-04-22 00:30:04 +01:00
pokemon.lapseTags(BattlerTagLapseType.TURN_END);
2024-05-24 00:45:04 +01:00
2023-10-25 14:41:37 +01:00
if (pokemon.summonData.disabledMove && !--pokemon.summonData.disabledTurns) {
this.scene.pushPhase(new MessagePhase(this.scene, i18next.t("battle:notDisabled", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: allMoves[pokemon.summonData.disabledMove].name })));
2024-02-25 17:45:41 +00:00
pokemon.summonData.disabledMove = Moves.NONE;
2023-04-19 23:19:55 +01:00
}
2023-04-21 01:15:16 +01:00
this.scene.applyModifiers(TurnHealModifier, pokemon.isPlayer(), pokemon);
if (this.scene.arena.terrain?.terrainType === TerrainType.GRASSY && pokemon.isGrounded()) {
this.scene.unshiftPhase(new PokemonHealPhase(this.scene, pokemon.getBattlerIndex(),
Math.max(pokemon.getMaxHp() >> 4, 1), getPokemonMessage(pokemon, "'s HP was restored."), true));
}
if (!pokemon.isPlayer()) {
this.scene.applyModifiers(EnemyTurnHealModifier, false, pokemon);
this.scene.applyModifier(EnemyStatusEffectHealChanceModifier, false, pokemon);
}
applyPostTurnAbAttrs(PostTurnAbAttr, pokemon);
2023-05-02 20:56:41 +01:00
this.scene.applyModifiers(TurnStatusEffectModifier, pokemon.isPlayer(), pokemon);
this.scene.applyModifiers(TurnHeldItemTransferModifier, pokemon.isPlayer(), pokemon);
2023-04-23 15:24:22 +01:00
2023-04-19 23:19:55 +01:00
pokemon.battleSummonData.turnCount++;
};
this.executeForAll(handlePokemon);
2024-05-24 00:45:04 +01:00
2023-04-22 00:30:04 +01:00
this.scene.arena.lapseTags();
2023-04-13 17:16:36 +01:00
if (this.scene.arena.weather && !this.scene.arena.weather.lapse()) {
this.scene.arena.trySetWeather(WeatherType.NONE, false);
}
if (this.scene.arena.terrain && !this.scene.arena.terrain.lapse()) {
2024-03-26 14:08:17 +00:00
this.scene.arena.trySetTerrain(TerrainType.NONE, false);
}
2024-03-26 14:08:17 +00:00
2023-04-13 17:16:36 +01:00
this.end();
}
}
export class BattleEndPhase extends BattlePhase {
start() {
super.start();
2024-03-17 15:36:19 +00:00
this.scene.currentBattle.addBattleScore(this.scene);
this.scene.gameData.gameStats.battles++;
if (this.scene.currentBattle.trainer) {
this.scene.gameData.gameStats.trainersDefeated++;
}
if (this.scene.gameMode.isEndless && this.scene.currentBattle.waveIndex + 1 > this.scene.gameData.gameStats.highestEndlessWave) {
this.scene.gameData.gameStats.highestEndlessWave = this.scene.currentBattle.waveIndex + 1;
}
2024-05-28 02:01:13 +01:00
// Endless graceful end
if (this.scene.gameMode.isEndless && this.scene.currentBattle.waveIndex >= 5850) {
this.scene.clearPhaseQueue();
this.scene.unshiftPhase(new GameOverPhase(this.scene, true));
}
for (const pokemon of this.scene.getField()) {
if (pokemon) {
pokemon.resetBattleSummonData();
}
}
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
for (const pokemon of this.scene.getParty().filter(p => p.isAllowedInBattle())) {
2024-03-07 02:05:23 +00:00
applyPostBattleAbAttrs(PostBattleAbAttr, pokemon);
}
2024-03-07 02:05:23 +00:00
if (this.scene.currentBattle.moneyScattered) {
this.scene.currentBattle.pickUpScatteredMoney(this.scene);
}
this.scene.clearEnemyHeldItemModifiers();
2023-04-21 00:44:56 +01:00
const lapsingModifiers = this.scene.findModifiers(m => m instanceof LapsingPersistentModifier || m instanceof LapsingPokemonHeldItemModifier) as (LapsingPersistentModifier | LapsingPokemonHeldItemModifier)[];
for (const m of lapsingModifiers) {
const args: any[] = [];
if (m instanceof LapsingPokemonHeldItemModifier) {
args.push(this.scene.getPokemonById(m.pokemonId));
}
if (!m.lapse(args)) {
this.scene.removeModifier(m);
}
}
this.scene.updateModifiers().then(() => this.end());
}
}
export class NewBattlePhase extends BattlePhase {
start() {
super.start();
2023-04-10 19:12:01 +01:00
this.scene.newBattle();
2023-04-10 19:12:01 +01:00
this.end();
2023-04-10 19:12:01 +01:00
}
}
2023-04-14 06:08:44 +01:00
export class CommonAnimPhase extends PokemonPhase {
private anim: CommonAnim;
private targetIndex: integer;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, targetIndex: BattlerIndex, anim: CommonAnim) {
super(scene, battlerIndex);
2023-04-14 06:08:44 +01:00
this.anim = anim;
this.targetIndex = targetIndex;
2023-04-14 06:08:44 +01:00
}
setAnimation(anim: CommonAnim) {
this.anim = anim;
}
2023-04-14 06:08:44 +01:00
start() {
2023-05-19 18:02:58 +01:00
new CommonBattleAnim(this.anim, this.getPokemon(), this.targetIndex !== undefined ? (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField())[this.targetIndex] : this.getPokemon()).play(this.scene, () => {
2023-04-14 06:08:44 +01:00
this.end();
});
}
}
export class MovePhase extends BattlePhase {
public pokemon: Pokemon;
public move: PokemonMove;
2024-03-28 04:24:24 +00:00
public targets: BattlerIndex[];
2023-04-16 23:40:32 +01:00
protected followUp: boolean;
protected ignorePp: boolean;
protected failed: boolean;
2023-04-12 00:08:03 +01:00
protected cancelled: boolean;
2023-04-10 19:12:01 +01:00
constructor(scene: BattleScene, pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp?: boolean, ignorePp?: boolean) {
2023-04-10 19:12:01 +01:00
super(scene);
this.pokemon = pokemon;
this.targets = targets;
2023-04-10 19:12:01 +01:00
this.move = move;
2023-04-16 23:40:32 +01:00
this.followUp = !!followUp;
this.ignorePp = !!ignorePp;
this.failed = false;
2023-04-12 00:08:03 +01:00
this.cancelled = false;
2023-04-10 19:12:01 +01:00
}
canMove(): boolean {
2023-10-25 14:41:37 +01:00
return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon, this.ignorePp) && !!this.targets.length;
2023-04-10 19:12:01 +01:00
}
/**Signifies the current move should fail but still use PP */
fail(): void {
this.failed = true;
}
/**Signifies the current move should cancel and retain PP */
2023-04-15 06:32:16 +01:00
cancel(): void {
this.cancelled = true;
}
2023-04-10 19:12:01 +01:00
start() {
super.start();
2023-04-19 23:19:55 +01:00
console.log(Moves[this.move.moveId]);
2023-05-04 21:38:56 +01:00
if (!this.canMove()) {
if (this.move.moveId && this.pokemon.summonData?.disabledMove === this.move.moveId) {
2023-05-04 21:38:56 +01:00
this.scene.queueMessage(`${this.move.getName()} is disabled!`);
}
if (this.move.ppUsed >= this.move.getMovePp()) { // if the move PP was reduced from Spite or otherwise, the move fails
this.fail();
this.showMoveText();
this.showFailedText();
}
2023-12-07 22:43:56 +00:00
return this.end();
2023-05-04 21:38:56 +01:00
}
if (!this.followUp) {
if (this.move.getMove().checkFlag(MoveFlags.IGNORE_ABILITIES, this.pokemon, null)) {
this.scene.arena.setIgnoreAbilities();
}
2024-04-03 16:42:09 +01:00
} else {
this.pokemon.turnData.hitsLeft = undefined;
this.pokemon.turnData.hitCount = undefined;
}
// Move redirection abilities (ie. Storm Drain) only support single target moves
const moveTarget = this.targets.length === 1
? new Utils.IntegerHolder(this.targets[0])
: null;
if (moveTarget) {
const oldTarget = moveTarget.value;
this.scene.getField(true).filter(p => p !== this.pokemon).forEach(p => applyAbAttrs(RedirectMoveAbAttr, p, null, this.move.moveId, moveTarget));
//Check if this move is immune to being redirected, and restore its target to the intended target if it is.
if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) || this.move.getMove().hasAttr(BypassRedirectAttr))) {
//If an ability prevented this move from being redirected, display its ability pop up.
if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) && !this.move.getMove().hasAttr(BypassRedirectAttr)) && oldTarget !== moveTarget.value) {
this.scene.unshiftPhase(new ShowAbilityPhase(this.scene, this.pokemon.getBattlerIndex(), this.pokemon.getPassiveAbility().hasAttr(BlockRedirectAbAttr)));
}
moveTarget.value = oldTarget;
}
this.targets[0] = moveTarget.value;
}
if (this.targets.length === 1 && this.targets[0] === BattlerIndex.ATTACKER) {
if (this.pokemon.turnData.attacksReceived.length) {
const attacker = this.pokemon.turnData.attacksReceived.length ? this.pokemon.scene.getPokemonById(this.pokemon.turnData.attacksReceived[0].sourceId) : null;
if (attacker?.isActive(true)) {
this.targets[0] = attacker.getBattlerIndex();
}
}
if (this.targets[0] === BattlerIndex.ATTACKER) {
this.fail(); // Marks the move as failed for later in doMove
this.showMoveText();
this.showFailedText();
}
}
const targets = this.scene.getField(true).filter(p => {
if (this.targets.indexOf(p.getBattlerIndex()) > -1) {
return true;
}
return false;
});
2023-04-16 23:40:32 +01:00
2023-04-12 00:08:03 +01:00
const doMove = () => {
this.pokemon.turnData.acted = true; // Record that the move was attempted, even if it fails
2024-05-24 00:45:04 +01:00
this.pokemon.lapseTags(BattlerTagLapseType.PRE_MOVE);
2024-05-24 00:45:04 +01:00
let ppUsed = 1;
// Filter all opponents to include only those this move is targeting
const targetedOpponents = this.pokemon.getOpponents().filter(o => this.targets.includes(o.getBattlerIndex()));
for (const opponent of targetedOpponents) {
if (this.move.ppUsed + ppUsed >= this.move.getMovePp()) { // If we're already at max PP usage, stop checking
break;
}
if (opponent.hasAbilityWithAttr(IncreasePpAbAttr)) { // Accounting for abilities like Pressure
ppUsed++;
}
}
2024-05-24 00:45:04 +01:00
if (!this.followUp && this.canMove() && !this.cancelled) {
this.pokemon.lapseTags(BattlerTagLapseType.MOVE);
}
const moveQueue = this.pokemon.getMoveQueue();
if (this.cancelled || this.failed) {
if (this.failed) {
2024-05-24 23:01:47 +01:00
this.move.usePp(ppUsed); // Only use PP if the move failed
this.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed));
}
// Record a failed move so Abilities like Truant don't trigger next turn and soft-lock
this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL });
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); // Remove any tags from moves like Fly/Dive/etc.
moveQueue.shift(); // Remove the second turn of charge moves
return this.end();
}
this.scene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
if (this.move.moveId) {
2023-11-27 18:47:32 +00:00
this.showMoveText();
}
// This should only happen when there are no valid targets left on the field
2024-05-24 00:45:04 +01:00
if ((moveQueue.length && moveQueue[0].move === Moves.NONE) || !targets.length) {
this.showFailedText();
2023-04-21 02:32:48 +01:00
this.cancel();
2024-05-24 00:45:04 +01:00
// Record a failed move so Abilities like Truant don't trigger next turn and soft-lock
this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL });
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); // Remove any tags from moves like Fly/Dive/etc.
moveQueue.shift();
2023-12-07 22:43:56 +00:00
return this.end();
2023-04-12 00:08:03 +01:00
}
2023-04-16 23:40:32 +01:00
2024-05-24 23:01:47 +01:00
if (!moveQueue.length || !moveQueue.shift().ignorePP) { // using .shift here clears out two turn moves once they've been used
this.move.usePp(ppUsed);
2024-05-24 23:01:47 +01:00
this.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed));
}
2023-04-16 23:40:32 +01:00
if (!allMoves[this.move.moveId].hasAttr(CopyMoveAttr)) {
this.scene.currentBattle.lastMove = this.move.moveId;
}
// Assume conditions affecting targets only apply to moves with a single target
let success = this.move.getMove().applyConditions(this.pokemon, targets[0], this.move.getMove());
const cancelled = new Utils.BooleanHolder(false);
let failedText = this.move.getMove().getFailedText(this.pokemon, targets[0], this.move.getMove(), cancelled);
if (success && this.scene.arena.isMoveWeatherCancelled(this.move.getMove())) {
success = false;
} else if (success && this.scene.arena.isMoveTerrainCancelled(this.pokemon, this.targets, this.move.getMove())) {
success = false;
if (failedText === null) {
failedText = getTerrainBlockMessage(targets[0], this.scene.arena.terrain.terrainType);
}
}
/**
* Trigger pokemon type change before playing the move animation
* Will still change the user's type when using Roar, Whirlwind, Trick-or-Treat, and Forest's Curse,
* regardless of whether the move successfully executes or not.
*/
if (success || [Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE].includes(this.move.moveId)) {
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove());
}
if (success) {
this.scene.unshiftPhase(this.getEffectPhase());
} else {
this.pokemon.pushMoveHistory({ move: this.move.moveId, targets: this.targets, result: MoveResult.FAIL, virtual: this.move.virtual });
if (!cancelled.value) {
this.showFailedText(failedText);
}
}
// Checks if Dancer ability is triggered
if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !this.followUp) {
// Pokemon with Dancer can be on either side of the battle so we check in both cases
this.scene.getPlayerField().forEach(pokemon => {
applyPostMoveUsedAbAttrs(PostMoveUsedAbAttr, pokemon, this.move, this.pokemon, this.targets);
});
this.scene.getEnemyField().forEach(pokemon => {
applyPostMoveUsedAbAttrs(PostMoveUsedAbAttr, pokemon, this.move, this.pokemon, this.targets);
});
}
2023-04-12 00:08:03 +01:00
this.end();
};
2023-04-16 23:40:32 +01:00
if (!this.followUp && this.pokemon.status && !this.pokemon.status.isPostTurn()) {
2023-04-12 00:08:03 +01:00
this.pokemon.status.incrementTurn();
let activated = false;
let healed = false;
2024-05-24 00:45:04 +01:00
2023-04-12 00:08:03 +01:00
switch (this.pokemon.status.effect) {
case StatusEffect.PARALYSIS:
if (!this.pokemon.randSeedInt(4)) {
activated = true;
this.cancelled = true;
}
break;
case StatusEffect.SLEEP:
applyMoveAttrs(BypassSleepAttr, this.pokemon, null, this.move.getMove());
healed = this.pokemon.status.turnCount === this.pokemon.status.cureTurn;
activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP);
this.cancelled = activated;
break;
case StatusEffect.FREEZE:
healed = !!this.move.getMove().findAttr(attr => attr instanceof HealStatusEffectAttr && attr.selfTarget && attr.isOfEffect(StatusEffect.FREEZE)) || !this.pokemon.randSeedInt(5);
activated = !healed;
this.cancelled = activated;
break;
2023-04-12 00:08:03 +01:00
}
2024-05-24 00:45:04 +01:00
2023-04-12 00:08:03 +01:00
if (activated) {
2023-04-22 00:30:04 +01:00
this.scene.queueMessage(getPokemonMessage(this.pokemon, getStatusEffectActivationText(this.pokemon.status.effect)));
this.scene.unshiftPhase(new CommonAnimPhase(this.scene, this.pokemon.getBattlerIndex(), undefined, CommonAnim.POISON + (this.pokemon.status.effect - 1)));
2023-04-15 06:32:16 +01:00
doMove();
2023-04-12 00:08:03 +01:00
} else {
if (healed) {
2023-04-22 00:30:04 +01:00
this.scene.queueMessage(getPokemonMessage(this.pokemon, getStatusEffectHealText(this.pokemon.status.effect)));
2023-04-12 00:08:03 +01:00
this.pokemon.resetStatus();
2023-04-20 20:46:05 +01:00
this.pokemon.updateInfo();
2023-04-12 00:08:03 +01:00
}
doMove();
}
} else {
2023-04-12 00:08:03 +01:00
doMove();
}
2023-04-10 19:12:01 +01:00
}
2023-04-16 23:40:32 +01:00
2023-04-10 19:12:01 +01:00
getEffectPhase(): MoveEffectPhase {
return new MoveEffectPhase(this.scene, this.pokemon.getBattlerIndex(), this.targets, this.move);
2023-04-10 19:12:01 +01:00
}
showMoveText(): void {
if (this.move.getMove().hasAttr(ChargeAttr)) {
2023-12-12 02:46:49 +00:00
const lastMove = this.pokemon.getLastXMoves() as TurnMove[];
if (!lastMove.length || lastMove[0].move !== this.move.getMove().id || lastMove[0].result !== MoveResult.OTHER) {
this.scene.queueMessage(i18next.t("battle:useMove", {
pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
moveName: this.move.getName()
}), 500);
2023-12-12 02:46:49 +00:00
return;
}
2023-12-12 02:46:49 +00:00
}
2024-01-08 04:17:24 +00:00
if (this.pokemon.getTag(BattlerTagType.RECHARGING || BattlerTagType.INTERRUPTED)) {
2024-01-08 04:17:24 +00:00
return;
}
2024-05-24 00:45:04 +01:00
this.scene.queueMessage(i18next.t("battle:useMove", {
pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
moveName: this.move.getName()
}), 500);
applyMoveAttrs(PreMoveMessageAttr, this.pokemon, this.pokemon.getOpponents().find(() => true), this.move.getMove());
}
showFailedText(failedText: string = null): void {
this.scene.queueMessage(failedText || i18next.t("battle:attackFailed"));
}
end() {
if (!this.followUp && this.canMove()) {
this.scene.unshiftPhase(new MoveEndPhase(this.scene, this.pokemon.getBattlerIndex()));
}
2023-04-10 19:12:01 +01:00
super.end();
2023-04-10 19:12:01 +01:00
}
}
2023-12-04 05:09:38 +00:00
export class MoveEffectPhase extends PokemonPhase {
2024-03-13 21:09:23 +00:00
public move: PokemonMove;
protected targets: BattlerIndex[];
2024-05-24 00:45:04 +01:00
constructor(scene: BattleScene, battlerIndex: BattlerIndex, targets: BattlerIndex[], move: PokemonMove) {
super(scene, battlerIndex);
2023-04-10 19:12:01 +01:00
this.move = move;
// In double battles, if the right Pokemon selects a spread move and the left Pokemon dies
// with no party members available to switch in, then the right Pokemon takes the index
// of the left Pokemon and gets hit unless this is checked.
if (targets.includes(battlerIndex) && this.move.getMove().moveTarget === MoveTarget.ALL_NEAR_OTHERS) {
const i = targets.indexOf(battlerIndex);
targets.splice(i,i+1);
}
this.targets = targets;
2023-04-10 19:12:01 +01:00
}
start() {
super.start();
const user = this.getUserPokemon();
const targets = this.getTargets();
2023-04-10 19:12:01 +01:00
if (!user?.isOnField()) {
return super.end();
}
2023-12-07 22:43:56 +00:00
2023-04-13 17:16:36 +01:00
const overridden = new Utils.BooleanHolder(false);
const move = this.move.getMove();
2023-04-10 21:17:25 +01:00
// Assume single target for override
applyMoveAttrs(OverrideMoveEffectAttr, user, this.getTarget(), move, overridden, this.move.virtual).then(() => {
2023-04-10 21:17:25 +01:00
if (overridden.value) {
2023-12-07 22:43:56 +00:00
return this.end();
}
2024-05-24 00:45:04 +01:00
2023-04-22 00:30:04 +01:00
user.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
2023-04-13 17:16:36 +01:00
if (user.turnData.hitsLeft === undefined) {
const hitCount = new Utils.IntegerHolder(1);
// Assume single target for multi hit
applyMoveAttrs(MultiHitAttr, user, this.getTarget(), move, hitCount);
if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) {
2024-04-03 16:42:09 +01:00
this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new Utils.IntegerHolder(0));
}
2023-05-03 05:00:21 +01:00
user.turnData.hitsLeft = user.turnData.hitCount = hitCount.value;
2023-04-13 17:16:36 +01:00
}
const moveHistoryEntry = { move: this.move.moveId, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual };
user.pushMoveHistory(moveHistoryEntry);
const targetHitChecks = Object.fromEntries(targets.map(p => [ p.getBattlerIndex(), this.hitCheck(p) ]));
const activeTargets = targets.map(t => t.isActive(true));
if (!activeTargets.length || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]])) {
2023-10-30 16:33:20 +00:00
user.turnData.hitCount = 1;
user.turnData.hitsLeft = 1;
if (activeTargets.length) {
this.scene.queueMessage(getPokemonMessage(user, "'s\nattack missed!"));
moveHistoryEntry.result = MoveResult.MISS;
applyMoveAttrs(MissEffectAttr, user, null, move);
} else {
this.scene.queueMessage(i18next.t("battle:attackFailed"));
moveHistoryEntry.result = MoveResult.FAIL;
}
2023-12-07 22:43:56 +00:00
return this.end();
2023-04-10 19:12:01 +01:00
}
2023-04-18 17:30:47 +01:00
2023-10-31 18:09:33 +00:00
const applyAttrs: Promise<void>[] = [];
// Move animation only needs one target
new MoveAnim(move.id as Moves, user, this.getTarget()?.getBattlerIndex()).play(this.scene, () => {
for (const target of targets) {
if (!targetHitChecks[target.getBattlerIndex()]) {
2023-10-30 16:33:20 +00:00
user.turnData.hitCount = 1;
user.turnData.hitsLeft = 1;
this.scene.queueMessage(getPokemonMessage(user, "'s\nattack missed!"));
if (moveHistoryEntry.result === MoveResult.PENDING) {
moveHistoryEntry.result = MoveResult.MISS;
}
applyMoveAttrs(MissEffectAttr, user, null, move);
continue;
2023-04-25 06:32:48 +01:00
}
const isProtected = !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target) && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType));
const firstHit = moveHistoryEntry.result !== MoveResult.SUCCESS;
moveHistoryEntry.result = MoveResult.SUCCESS;
2024-05-24 00:45:04 +01:00
const hitResult = !isProtected ? target.apply(user, move) : HitResult.NO_EFFECT;
2023-07-10 15:54:22 +01:00
this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
2024-01-10 04:34:43 +00:00
2023-10-31 18:09:33 +00:00
applyAttrs.push(new Promise(resolve => {
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit),
user, target, move).then(() => {
2023-10-31 18:09:33 +00:00
if (hitResult !== HitResult.FAIL) {
const chargeEffect = !!move.getAttrs(ChargeAttr).find(ca => ca.usedChargeEffect(user, this.getTarget(), move));
2023-10-31 18:09:33 +00:00
// Charge attribute with charge effect takes all effect attributes and applies them to charge stage, so ignore them if this is present
Utils.executeIf(!chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY
&& attr.selfTarget && (!attr.firstHitOnly || firstHit), user, target, move)).then(() => {
2023-10-31 18:09:33 +00:00
if (hitResult !== HitResult.NO_EFFECT) {
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY
&& !attr.selfTarget && (!attr.firstHitOnly || firstHit), user, target, move).then(() => {
2023-10-31 18:09:33 +00:00
if (hitResult < HitResult.NO_EFFECT) {
const flinched = new Utils.BooleanHolder(false);
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
if (flinched.value) {
2023-10-31 18:09:33 +00:00
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
}
2023-10-31 18:09:33 +00:00
}
Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.HIT && (!attr.firstHitOnly || firstHit),
user, target, move).then(() => {
return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, move, hitResult).then(() => {
if (!user.isPlayer() && move instanceof AttackMove) {
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
}
})).then(() => {
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, move, hitResult).then(() => {
if (move instanceof AttackMove) {
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target.getFieldIndex());
}
resolve();
});
});
})
2023-10-31 18:09:33 +00:00
).then(() => resolve());
});
} else {
applyMoveAttrs(NoEffectAttr, user, null, move).then(() => resolve());
}
2023-10-31 18:09:33 +00:00
});
} else {
resolve();
}
2023-10-31 18:09:33 +00:00
});
}));
2023-04-23 15:24:22 +01:00
}
// Trigger effect which should only apply one time after all targeted effects have already applied
const postTarget = applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET,
user, null, move);
2024-05-24 00:45:04 +01:00
if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after
applyAttrs[applyAttrs.length - 1]?.then(() => postTarget);
} else { // Otherwise, push a new asynchronous move effect
applyAttrs.push(postTarget);
}
2023-10-31 18:09:33 +00:00
Promise.allSettled(applyAttrs).then(() => this.end());
2023-04-13 17:16:36 +01:00
});
2023-04-10 19:12:01 +01:00
});
}
2023-04-10 21:17:25 +01:00
end() {
const move = this.move.getMove();
move.type = move.defaultType;
2023-04-10 21:17:25 +01:00
const user = this.getUserPokemon();
if (user) {
if (--user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) {
this.scene.unshiftPhase(this.getNewHitPhase());
} else {
2023-12-22 22:08:37 +00:00
const hitsTotal = user.turnData.hitCount - Math.max(user.turnData.hitsLeft, 0);
if (hitsTotal > 1) {
this.scene.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal }));
}
this.scene.applyModifiers(HitHealModifier, this.player, user);
}
2023-04-13 17:16:36 +01:00
}
2024-05-24 00:45:04 +01:00
2023-04-10 21:17:25 +01:00
super.end();
}
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)) {
2023-04-18 17:30:47 +01:00
return true;
}
2023-04-13 17:16:36 +01:00
const user = this.getUserPokemon();
// 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
// multi-hit move and proceed with all hits
if (user.turnData.hitsLeft < user.turnData.hitCount) {
if (!this.move.getMove().hasFlag(MoveFlags.CHECK_ALL_HITS) || user.hasAbilityWithAttr(MaxMultiHitAbAttr)) {
return true;
}
}
2023-10-30 16:33:20 +00:00
if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) {
2024-04-21 06:26:30 +01:00
return true;
}
2024-04-21 06:26:30 +01:00
// If the user should ignore accuracy on a target, check who the user targeted last turn and see if they match
if (user.getTag(BattlerTagType.IGNORE_ACCURACY) && (user.getLastXMoves().slice(1).find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1) {
return true;
}
2024-05-24 00:45:04 +01:00
const hiddenTag = target.getTag(HiddenTag);
if (hiddenTag && !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === hiddenTag.tagType)) {
return false;
}
const moveAccuracy = new Utils.NumberHolder(this.move.getMove().accuracy);
applyMoveAttrs(VariableAccuracyAttr, user, target, this.move.getMove(), moveAccuracy);
applyPreDefendAbAttrs(WonderSkinAbAttr, target, user, this.move.getMove(), { value: false }, moveAccuracy);
if (moveAccuracy.value === -1) {
return true;
}
2023-05-08 23:48:35 +01:00
const isOhko = this.move.getMove().hasAttr(OneHitKOAccuracyAttr);
if (!isOhko) {
user.scene.applyModifiers(PokemonMoveAccuracyBoosterModifier, user.isPlayer(), user, moveAccuracy);
}
2024-04-20 12:52:47 +01:00
if (this.scene.arena.weather?.weatherType === WeatherType.FOG) {
2024-04-07 22:12:53 +01:00
moveAccuracy.value = Math.floor(moveAccuracy.value * 0.9);
}
if (!isOhko && this.scene.arena.getTag(ArenaTagType.GRAVITY)) {
2023-05-08 23:48:35 +01:00
moveAccuracy.value = Math.floor(moveAccuracy.value * 1.67);
}
2024-05-24 00:45:04 +01:00
const userAccuracyLevel = new Utils.IntegerHolder(user.summonData.battleStats[BattleStat.ACC]);
const targetEvasionLevel = new Utils.IntegerHolder(target.summonData.battleStats[BattleStat.EVA]);
2023-11-06 04:27:40 +00:00
applyAbAttrs(IgnoreOpponentStatChangesAbAttr, target, null, userAccuracyLevel);
applyAbAttrs(IgnoreOpponentStatChangesAbAttr, user, null, targetEvasionLevel);
applyAbAttrs(IgnoreOpponentEvasionAbAttr, user, null, targetEvasionLevel);
applyMoveAttrs(IgnoreOpponentStatChangesAttr, user, target, this.move.getMove(), targetEvasionLevel);
this.scene.applyModifiers(TempBattleStatBoosterModifier, this.player, TempBattleStat.ACC, userAccuracyLevel);
const rand = user.randSeedInt(100, 1);
const accuracyMultiplier = new Utils.NumberHolder(1);
if (userAccuracyLevel.value !== targetEvasionLevel.value) {
accuracyMultiplier.value = userAccuracyLevel.value > targetEvasionLevel.value
? (3 + Math.min(userAccuracyLevel.value - targetEvasionLevel.value, 6)) / 3
: 3 / (3 + Math.min(targetEvasionLevel.value - userAccuracyLevel.value, 6));
2023-04-10 21:17:25 +01:00
}
applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, user, BattleStat.ACC, accuracyMultiplier, this.move.getMove());
const evasionMultiplier = new Utils.NumberHolder(1);
applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, this.getTarget(), BattleStat.EVA, evasionMultiplier);
accuracyMultiplier.value /= evasionMultiplier.value;
return rand <= moveAccuracy.value * accuracyMultiplier.value;
2023-04-10 21:17:25 +01:00
}
2023-04-10 19:12:01 +01:00
getUserPokemon(): Pokemon {
if (this.battlerIndex > BattlerIndex.ENEMY_2) {
2023-12-04 05:09:38 +00:00
return this.scene.getPokemonById(this.battlerIndex);
}
return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex];
2023-04-10 19:12:01 +01:00
}
getTargets(): Pokemon[] {
return this.scene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1);
2023-04-10 19:12:01 +01:00
}
2023-04-10 21:17:25 +01:00
getTarget(): Pokemon {
return this.getTargets().find(() => true);
2023-04-10 19:12:01 +01:00
}
2023-04-10 21:17:25 +01:00
getNewHitPhase() {
return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move);
2023-04-10 21:17:25 +01:00
}
2023-04-10 19:12:01 +01:00
}
2023-04-16 23:40:32 +01:00
export class MoveEndPhase extends PokemonPhase {
constructor(scene: BattleScene, battlerIndex: BattlerIndex) {
super(scene, battlerIndex);
2023-04-16 23:40:32 +01:00
}
start() {
super.start();
const pokemon = this.getPokemon();
if (pokemon.isActive(true)) {
pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
}
2023-04-16 23:40:32 +01:00
this.scene.arena.setIgnoreAbilities(false);
2023-04-16 23:40:32 +01:00
this.end();
}
}
2023-04-14 04:04:51 +01:00
export class MoveAnimTestPhase extends BattlePhase {
private moveQueue: Moves[];
constructor(scene: BattleScene, moveQueue?: Moves[]) {
super(scene);
2023-04-21 02:32:48 +01:00
this.moveQueue = moveQueue || Utils.getEnumValues(Moves).slice(1);
2023-04-14 04:04:51 +01:00
}
start() {
const moveQueue = this.moveQueue.slice(0);
this.playMoveAnim(moveQueue, true);
}
playMoveAnim(moveQueue: Moves[], player: boolean) {
const moveId = player ? moveQueue[0] : moveQueue.shift();
if (moveId === undefined) {
this.playMoveAnim(this.moveQueue.slice(0), true);
return;
} else if (player) {
2023-04-25 03:32:12 +01:00
console.log(Moves[moveId]);
}
2023-04-14 04:04:51 +01:00
initMoveAnim(this.scene, moveId).then(() => {
2023-04-14 04:04:51 +01:00
loadMoveAnimAssets(this.scene, [ moveId ], true)
.then(() => {
2023-11-30 21:37:16 +00:00
new MoveAnim(moveId, player ? this.scene.getPlayerPokemon() : this.scene.getEnemyPokemon(), (player !== (allMoves[moveId] instanceof SelfStatusMove) ? this.scene.getEnemyPokemon() : this.scene.getPlayerPokemon()).getBattlerIndex()).play(this.scene, () => {
if (player) {
2023-04-14 04:04:51 +01:00
this.playMoveAnim(moveQueue, false);
} else {
2023-04-14 04:04:51 +01:00
this.playMoveAnim(moveQueue, true);
}
2023-04-14 04:04:51 +01:00
});
});
2023-04-14 04:04:51 +01:00
});
}
}
export class ShowAbilityPhase extends PokemonPhase {
private passive: boolean;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, passive: boolean = false) {
super(scene, battlerIndex);
this.passive = passive;
}
start() {
super.start();
const pokemon = this.getPokemon();
this.scene.abilityBar.showAbility(pokemon, this.passive);
2024-06-12 19:39:37 +01:00
if (pokemon.battleData) {
pokemon.battleData.abilityRevealed = true;
}
this.end();
}
}
2023-04-11 04:15:06 +01:00
export class StatChangePhase extends PokemonPhase {
private stats: BattleStat[];
2023-04-27 04:33:13 +01:00
private selfTarget: boolean;
2023-04-11 04:15:06 +01:00
private levels: integer;
private showMessage: boolean;
2024-04-11 17:11:55 +01:00
private ignoreAbilities: boolean;
private canBeCopied: boolean;
2023-04-11 04:15:06 +01:00
constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], levels: integer, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true) {
super(scene, battlerIndex);
2023-04-11 04:15:06 +01:00
this.selfTarget = selfTarget;
this.stats = stats;
2023-04-11 04:15:06 +01:00
this.levels = levels;
this.showMessage = showMessage;
2024-04-11 17:11:55 +01:00
this.ignoreAbilities = ignoreAbilities;
this.canBeCopied = canBeCopied;
2023-04-11 04:15:06 +01:00
}
start() {
const pokemon = this.getPokemon();
2023-04-27 04:33:13 +01:00
let random = false;
if (this.stats.length === 1 && this.stats[0] === BattleStat.RAND) {
this.stats[0] = this.getRandomStat();
random = true;
}
this.aggregateStatChanges(random);
if (!pokemon.isActive(true)) {
return this.end();
}
const filteredStats = this.stats.map(s => s !== BattleStat.RAND ? s : this.getRandomStat()).filter(stat => {
2023-04-27 04:33:13 +01:00
const cancelled = new Utils.BooleanHolder(false);
if (!this.selfTarget && this.levels < 0) {
this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled);
}
if (!cancelled.value && !this.selfTarget && this.levels < 0) {
2023-05-06 17:13:35 +01:00
applyPreStatChangeAbAttrs(ProtectStatAbAttr, this.getPokemon(), stat, cancelled);
}
2024-05-24 00:45:04 +01:00
2023-04-27 04:33:13 +01:00
return !cancelled.value;
});
2023-11-06 03:11:38 +00:00
const levels = new Utils.IntegerHolder(this.levels);
2024-04-11 17:11:55 +01:00
if (!this.ignoreAbilities) {
2024-04-11 17:11:55 +01:00
applyAbAttrs(StatChangeMultiplierAbAttr, pokemon, null, levels);
}
2023-11-06 03:11:38 +00:00
2023-04-11 04:15:06 +01:00
const battleStats = this.getPokemon().summonData.battleStats;
2023-11-06 03:11:38 +00:00
const relLevels = filteredStats.map(stat => (levels.value >= 1 ? Math.min(battleStats[stat] + levels.value, 6) : Math.max(battleStats[stat] + levels.value, -6)) - battleStats[stat]);
2023-04-11 04:15:06 +01:00
const end = () => {
if (this.showMessage) {
const messages = this.getStatChangeMessages(filteredStats, levels.value, relLevels);
for (const message of messages) {
this.scene.queueMessage(message);
}
}
2023-04-11 04:15:06 +01:00
for (const stat of filteredStats) {
2023-11-06 03:11:38 +00:00
pokemon.summonData.battleStats[stat] = Math.max(Math.min(pokemon.summonData.battleStats[stat] + levels.value, 6), -6);
}
2024-05-24 00:45:04 +01:00
if (levels.value > 0 && this.canBeCopied) {
for (const opponent of pokemon.getOpponents()) {
applyAbAttrs(StatChangeCopyAbAttr, opponent, null, this.stats, levels.value);
}
}
2024-05-24 00:45:04 +01:00
2024-05-01 04:02:16 +01:00
applyPostStatChangeAbAttrs(PostStatChangeAbAttr, pokemon, filteredStats, this.levels, this.selfTarget);
2024-05-24 00:45:04 +01:00
2024-05-01 04:02:16 +01:00
pokemon.updateInfo();
handleTutorial(this.scene, Tutorial.Stat_Change).then(() => super.end());
2023-04-11 04:15:06 +01:00
};
if (relLevels.filter(l => l).length && this.scene.moveAnimations) {
2023-04-11 04:15:06 +01:00
pokemon.enableMask();
const pokemonMaskSprite = pokemon.maskSprite;
const tileX = (this.player ? 106 : 236) * pokemon.getSpriteScale() * this.scene.field.scale;
const tileY = ((this.player ? 148 : 84) + (levels.value >= 1 ? 160 : 0)) * pokemon.getSpriteScale() * this.scene.field.scale;
const tileWidth = 156 * this.scene.field.scale * pokemon.getSpriteScale();
const tileHeight = 316 * this.scene.field.scale * pokemon.getSpriteScale();
// On increase, show the red sprite located at ATK
// On decrease, show the blue sprite located at SPD
const spriteColor = levels.value >= 1 ? BattleStat[BattleStat.ATK].toLowerCase() : BattleStat[BattleStat.SPD].toLowerCase();
const statSprite = this.scene.add.tileSprite(tileX, tileY, tileWidth, tileHeight, "battle_stats", spriteColor);
2023-12-30 02:04:40 +00:00
statSprite.setPipeline(this.scene.fieldSpritePipeline);
2023-04-11 04:15:06 +01:00
statSprite.setAlpha(0);
statSprite.setScale(6);
statSprite.setOrigin(0.5, 1);
this.scene.playSound(`stat_${levels.value >= 1 ? "up" : "down"}`);
2023-04-11 04:15:06 +01:00
statSprite.setMask(new Phaser.Display.Masks.BitmapMask(this.scene, pokemonMaskSprite));
this.scene.tweens.add({
targets: statSprite,
duration: 250,
alpha: 0.8375,
onComplete: () => {
this.scene.tweens.add({
targets: statSprite,
delay: 1000,
duration: 250,
alpha: 0
});
}
});
2023-04-11 14:41:11 +01:00
2023-04-11 04:15:06 +01:00
this.scene.tweens.add({
targets: statSprite,
duration: 1500,
y: `${levels.value >= 1 ? "-" : "+"}=${160 * 6}`
2023-04-11 04:15:06 +01:00
});
2024-05-24 00:45:04 +01:00
2023-04-11 04:15:06 +01:00
this.scene.time.delayedCall(1750, () => {
pokemon.disableMask();
end();
});
} else {
2023-04-11 04:15:06 +01:00
end();
}
2023-04-11 04:15:06 +01:00
}
getRandomStat(): BattleStat {
const allStats = Utils.getEnumValues(BattleStat);
return allStats[this.getPokemon().randSeedInt(BattleStat.SPD + 1)];
}
aggregateStatChanges(random: boolean = false): void {
const isAccEva = [ BattleStat.ACC, BattleStat.EVA ].some(s => this.stats.includes(s));
let existingPhase: StatChangePhase;
if (this.stats.length === 1) {
while ((existingPhase = (this.scene.findPhase(p => p instanceof StatChangePhase && p.battlerIndex === this.battlerIndex && p.stats.length === 1
&& (p.stats[0] === this.stats[0] || (random && p.stats[0] === BattleStat.RAND))
&& p.selfTarget === this.selfTarget && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatChangePhase))) {
if (existingPhase.stats[0] === BattleStat.RAND) {
existingPhase.stats[0] = this.getRandomStat();
if (existingPhase.stats[0] !== this.stats[0]) {
continue;
}
}
this.levels += existingPhase.levels;
if (!this.scene.tryRemovePhase(p => p === existingPhase)) {
break;
}
}
}
while ((existingPhase = (this.scene.findPhase(p => p instanceof StatChangePhase && p.battlerIndex === this.battlerIndex && p.selfTarget === this.selfTarget
&& ([ BattleStat.ACC, BattleStat.EVA ].some(s => p.stats.includes(s)) === isAccEva)
&& p.levels === this.levels && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatChangePhase))) {
this.stats.push(...existingPhase.stats);
if (!this.scene.tryRemovePhase(p => p === existingPhase)) {
break;
}
}
}
2023-11-06 03:11:38 +00:00
getStatChangeMessages(stats: BattleStat[], levels: integer, relLevels: integer[]): string[] {
2023-04-11 04:15:06 +01:00
const messages: string[] = [];
const relLevelStatIndexes = {};
for (let rl = 0; rl < relLevels.length; rl++) {
const relLevel = relLevels[rl];
if (!relLevelStatIndexes[relLevel]) {
relLevelStatIndexes[relLevel] = [];
}
relLevelStatIndexes[relLevel].push(rl);
}
Object.keys(relLevelStatIndexes).forEach(rl => {
const relLevelStats = stats.filter((_, i) => relLevelStatIndexes[rl].includes(i));
let statsFragment = "";
if (relLevelStats.length > 1) {
statsFragment = relLevelStats.length >= 5
? "stats"
: `${relLevelStats.slice(0, -1).map(s => getBattleStatName(s)).join(", ")}${relLevelStats.length > 2 ? "," : ""} and ${getBattleStatName(relLevelStats[relLevelStats.length - 1])}`;
} else {
statsFragment = getBattleStatName(relLevelStats[0]);
}
messages.push(getPokemonMessage(this.getPokemon(), `'s ${statsFragment} ${getBattleStatLevelChangeDescription(Math.abs(parseInt(rl)), levels >= 1)}!`));
});
2023-04-11 04:15:06 +01:00
return messages;
}
}
export class WeatherEffectPhase extends CommonAnimPhase {
public weather: Weather;
constructor(scene: BattleScene) {
super(scene, undefined, undefined, CommonAnim.SUNNY + ((scene?.arena?.weather?.weatherType || WeatherType.NONE) - 1));
this.weather = scene?.arena?.weather;
}
start() {
// Update weather state with any changes that occurred during the turn
this.weather = this.scene?.arena?.weather;
if (!this.weather) {
this.end();
return;
}
this.setAnimation(CommonAnim.SUNNY + (this.weather.weatherType - 1));
if (this.weather.isDamaging()) {
2024-05-24 00:45:04 +01:00
const cancelled = new Utils.BooleanHolder(false);
this.executeForAll((pokemon: Pokemon) => applyPreWeatherEffectAbAttrs(SuppressWeatherEffectAbAttr, pokemon, this.weather, cancelled));
if (!cancelled.value) {
const inflictDamage = (pokemon: Pokemon) => {
const cancelled = new Utils.BooleanHolder(false);
applyPreWeatherEffectAbAttrs(PreWeatherDamageAbAttr, pokemon, this.weather, cancelled);
2024-04-02 20:14:07 +01:00
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (cancelled.value) {
return;
}
const damage = Math.ceil(pokemon.getMaxHp() / 16);
this.scene.queueMessage(getWeatherDamageMessage(this.weather.weatherType, pokemon));
pokemon.damageAndUpdate(damage, HitResult.EFFECTIVE, false, false, true);
};
this.executeForAll((pokemon: Pokemon) => {
2024-03-28 14:28:05 +00:00
const immune = !pokemon || !!pokemon.getTypes(true, true).filter(t => this.weather.isTypeDamageImmune(t)).length;
if (!immune) {
inflictDamage(pokemon);
}
});
}
}
this.scene.ui.showText(getWeatherLapseMessage(this.weather.weatherType), null, () => {
this.executeForAll((pokemon: Pokemon) => applyPostWeatherLapseAbAttrs(PostWeatherLapseAbAttr, pokemon, this.weather));
super.start();
});
}
}
2023-04-10 19:12:01 +01:00
export class ObtainStatusEffectPhase extends PokemonPhase {
private statusEffect: StatusEffect;
2023-04-16 23:40:32 +01:00
private cureTurn: integer;
private sourceText: string;
private sourcePokemon: Pokemon;
2023-04-10 19:12:01 +01:00
constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect: StatusEffect, cureTurn?: integer, sourceText?: string, sourcePokemon?: Pokemon) {
super(scene, battlerIndex);
2023-04-12 00:08:03 +01:00
this.statusEffect = statusEffect;
2023-04-16 23:40:32 +01:00
this.cureTurn = cureTurn;
this.sourceText = sourceText;
this.sourcePokemon = sourcePokemon; // For tracking which Pokemon caused the status effect
2023-04-10 19:12:01 +01:00
}
start() {
const pokemon = this.getPokemon();
2023-04-12 00:08:03 +01:00
if (!pokemon.status) {
if (pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) {
if (this.cureTurn) {
2023-04-16 23:40:32 +01:00
pokemon.status.cureTurn = this.cureTurn;
}
2023-04-12 00:08:03 +01:00
pokemon.updateInfo(true);
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect - 1), pokemon).play(this.scene, () => {
this.scene.queueMessage(getPokemonMessage(pokemon, getStatusEffectObtainText(this.statusEffect, this.sourceText)));
if (pokemon.status.isPostTurn()) {
this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, this.battlerIndex));
}
2023-04-12 00:08:03 +01:00
this.end();
});
return;
}
} else if (pokemon.status.effect === this.statusEffect) {
2023-04-22 00:30:04 +01:00
this.scene.queueMessage(getPokemonMessage(pokemon, getStatusEffectOverlapText(this.statusEffect)));
}
2023-04-12 00:08:03 +01:00
this.end();
}
}
export class PostTurnStatusEffectPhase extends PokemonPhase {
constructor(scene: BattleScene, battlerIndex: BattlerIndex) {
super(scene, battlerIndex);
2023-04-12 00:08:03 +01:00
}
start() {
const pokemon = this.getPokemon();
if (pokemon?.isActive(true) && pokemon.status && pokemon.status.isPostTurn()) {
2023-04-12 00:08:03 +01:00
pokemon.status.incrementTurn();
2024-04-02 20:14:07 +01:00
const cancelled = new Utils.BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) {
this.scene.queueMessage(getPokemonMessage(pokemon, getStatusEffectActivationText(pokemon.status.effect)));
let damage: integer = 0;
switch (pokemon.status.effect) {
case StatusEffect.POISON:
damage = Math.max(pokemon.getMaxHp() >> 3, 1);
break;
case StatusEffect.TOXIC:
damage = Math.max(Math.floor((pokemon.getMaxHp() / 16) * pokemon.status.turnCount), 1);
break;
case StatusEffect.BURN:
damage = Math.max(pokemon.getMaxHp() >> 4, 1);
break;
2024-04-02 20:14:07 +01:00
}
if (damage) {
// Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ...
this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage, false, true));
2024-04-02 20:14:07 +01:00
pokemon.updateInfo();
}
new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, () => this.end());
} else {
2024-04-02 20:14:07 +01:00
this.end();
}
} else {
2023-04-10 19:12:01 +01:00
this.end();
}
2023-04-10 19:12:01 +01:00
}
}
export class MessagePhase extends Phase {
2023-04-10 19:12:01 +01:00
private text: string;
2023-04-12 00:08:03 +01:00
private callbackDelay: integer;
2023-04-10 19:12:01 +01:00
private prompt: boolean;
2023-05-07 22:05:19 +01:00
private promptDelay: integer;
2023-04-10 19:12:01 +01:00
2023-05-07 22:05:19 +01:00
constructor(scene: BattleScene, text: string, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer) {
2023-04-10 19:12:01 +01:00
super(scene);
this.text = text;
2023-04-12 00:08:03 +01:00
this.callbackDelay = callbackDelay;
2023-04-10 19:12:01 +01:00
this.prompt = prompt;
2023-05-07 22:05:19 +01:00
this.promptDelay = promptDelay;
2023-04-10 19:12:01 +01:00
}
start() {
super.start();
if (this.text.indexOf("$") > -1) {
const pageIndex = this.text.indexOf("$");
this.scene.unshiftPhase(new MessagePhase(this.scene, this.text.slice(pageIndex + 1), this.callbackDelay, this.prompt, this.promptDelay));
this.text = this.text.slice(0, pageIndex).trim();
}
2023-05-07 22:05:19 +01:00
this.scene.ui.showText(this.text, null, () => this.end(), this.callbackDelay || (this.prompt ? 0 : 1500), this.prompt, this.promptDelay);
2023-04-10 19:12:01 +01:00
}
2023-04-27 06:14:15 +01:00
end() {
if (this.scene.abilityBar.shown) {
2023-04-27 06:14:15 +01:00
this.scene.abilityBar.hide();
}
2023-04-27 06:14:15 +01:00
super.end();
}
2023-04-10 19:12:01 +01:00
}
2023-04-15 06:32:16 +01:00
export class DamagePhase extends PokemonPhase {
private amount: integer;
2023-04-15 06:32:16 +01:00
private damageResult: DamageResult;
private critical: boolean;
2023-04-15 06:32:16 +01:00
constructor(scene: BattleScene, battlerIndex: BattlerIndex, amount: integer, damageResult?: DamageResult, critical: boolean = false) {
super(scene, battlerIndex);
2023-04-15 06:32:16 +01:00
this.amount = amount;
this.damageResult = damageResult || HitResult.EFFECTIVE;
this.critical = critical;
2023-04-15 06:32:16 +01:00
}
start() {
super.start();
2023-11-08 03:23:42 +00:00
if (this.damageResult === HitResult.ONE_HIT_KO) {
this.scene.toggleInvert(true);
this.scene.time.delayedCall(Utils.fixedInt(1000), () => {
this.scene.toggleInvert(false);
this.applyDamage();
});
return;
}
this.applyDamage();
}
updateAmount(amount: integer): void {
this.amount = amount;
}
2023-11-08 03:23:42 +00:00
applyDamage() {
2023-04-15 06:32:16 +01:00
switch (this.damageResult) {
case HitResult.EFFECTIVE:
this.scene.playSound("hit");
break;
case HitResult.SUPER_EFFECTIVE:
case HitResult.ONE_HIT_KO:
this.scene.playSound("hit_strong");
break;
case HitResult.NOT_VERY_EFFECTIVE:
this.scene.playSound("hit_weak");
break;
2023-04-15 06:32:16 +01:00
}
if (this.amount) {
this.scene.damageNumberHandler.add(this.getPokemon(), this.amount, this.damageResult, this.critical);
}
if (this.damageResult !== HitResult.OTHER) {
2023-04-18 17:30:47 +01:00
const flashTimer = this.scene.time.addEvent({
delay: 100,
repeat: 5,
startAt: 200,
callback: () => {
this.getPokemon().getSprite().setVisible(flashTimer.repeatCount % 2 === 0);
if (!flashTimer.repeatCount) {
2023-04-18 17:30:47 +01:00
this.getPokemon().updateInfo().then(() => this.end());
}
2023-04-18 17:30:47 +01:00
}
});
} else {
2023-04-18 17:30:47 +01:00
this.getPokemon().updateInfo().then(() => this.end());
}
2023-04-15 06:32:16 +01:00
}
end() {
switch (this.scene.currentBattle.battleSpec) {
case BattleSpec.FINAL_BOSS:
const pokemon = this.getPokemon();
if (pokemon instanceof EnemyPokemon && pokemon.isBoss() && !pokemon.formIndex && pokemon.bossSegmentIndex < 1) {
this.scene.fadeOutBgm(Utils.fixedInt(2000), false);
this.scene.ui.showDialogue(battleSpecDialogue[BattleSpec.FINAL_BOSS].firstStageWin, pokemon.species.name, null, () => {
this.scene.addEnemyModifier(getModifierType(modifierTypes.MINI_BLACK_HOLE).newModifier(pokemon) as PersistentModifier, false, true);
pokemon.generateAndPopulateMoveset(1);
this.scene.setFieldScale(0.75);
this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger, false);
this.scene.currentBattle.double = true;
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle());
if (availablePartyMembers.length > 1) {
this.scene.pushPhase(new ToggleDoublePositionPhase(this.scene, true));
if (!availablePartyMembers[1].isOnField()) {
this.scene.pushPhase(new SummonPhase(this.scene, 1));
}
}
super.end();
});
return;
}
break;
}
super.end();
}
2023-04-15 06:32:16 +01:00
}
2023-04-10 19:12:01 +01:00
export class FaintPhase extends PokemonPhase {
private preventEndure: boolean;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, preventEndure?: boolean) {
super(scene, battlerIndex);
this.preventEndure = preventEndure;
2023-04-10 19:12:01 +01:00
}
start() {
super.start();
if (!this.preventEndure) {
const instantReviveModifier = this.scene.applyModifier(PokemonInstantReviveModifier, this.player, this.getPokemon()) as PokemonInstantReviveModifier;
2023-06-06 13:16:07 +01:00
if (instantReviveModifier) {
if (!--instantReviveModifier.stackCount) {
this.scene.removeModifier(instantReviveModifier);
}
this.scene.updateModifiers(this.player);
return this.end();
}
2023-06-06 13:16:07 +01:00
}
if (!this.tryOverrideForBattleSpec()) {
this.doFaint();
}
}
doFaint(): void {
const pokemon = this.getPokemon();
// Track total times pokemon have been KO'd for supreme overlord/last respects
if (pokemon.isPlayer()) {
this.scene.currentBattle.playerFaints += 1;
} else {
this.scene.currentBattle.enemyFaints += 1;
}
this.scene.queueMessage(i18next.t("battle:fainted", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), null, true);
2023-04-15 06:32:16 +01:00
if (pokemon.turnData?.attacksReceived?.length) {
const lastAttack = pokemon.turnData.attacksReceived[0];
applyPostFaintAbAttrs(PostFaintAbAttr, pokemon, this.scene.getPokemonById(lastAttack.sourceId), new PokemonMove(lastAttack.move).getMove(), lastAttack.result);
}
const alivePlayField = this.scene.getField(true);
alivePlayField.forEach(p => applyPostKnockOutAbAttrs(PostKnockOutAbAttr, p, pokemon));
if (pokemon.turnData?.attacksReceived?.length) {
const defeatSource = this.scene.getPokemonById(pokemon.turnData.attacksReceived[0].sourceId);
if (defeatSource?.isOnField()) {
applyPostVictoryAbAttrs(PostVictoryAbAttr, defeatSource);
const pvmove = allMoves[pokemon.turnData.attacksReceived[0].move];
const pvattrs = pvmove.getAttrs(PostVictoryStatChangeAttr);
if (pvattrs.length) {
for (const pvattr of pvattrs) {
pvattr.applyPostVictory(defeatSource, defeatSource, pvmove);
}
}
}
}
if (this.player) {
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
const nonFaintedLegalPartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle());
const nonFaintedPartyMemberCount = nonFaintedLegalPartyMembers.length;
if (!nonFaintedPartyMemberCount) {
this.scene.unshiftPhase(new GameOverPhase(this.scene));
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
} else if (nonFaintedPartyMemberCount >= this.scene.currentBattle.getBattlerCount() || (this.scene.currentBattle.double && !nonFaintedLegalPartyMembers[0].isActive(true))) {
this.scene.pushPhase(new SwitchPhase(this.scene, this.fieldIndex, true, false));
}
if (nonFaintedPartyMemberCount === 1 && this.scene.currentBattle.double) {
this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true));
}
2023-10-07 21:08:33 +01:00
} else {
2023-05-29 17:24:38 +01:00
this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex));
2023-10-07 21:08:33 +01:00
if (this.scene.currentBattle.battleType === BattleType.TRAINER) {
const hasReservePartyMember = !!this.scene.getEnemyParty().filter(p => p.isActive() && !p.isOnField() && p.trainerSlot === (pokemon as EnemyPokemon).trainerSlot).length;
if (hasReservePartyMember) {
this.scene.pushPhase(new SwitchSummonPhase(this.scene, this.fieldIndex, -1, false, false, false));
}
2023-10-07 21:08:33 +01:00
}
}
2023-04-14 04:04:51 +01:00
2024-03-28 04:24:24 +00:00
if (this.scene.currentBattle.double) {
const allyPokemon = pokemon.getAlly();
if (allyPokemon?.isActive(true)) {
let targetingMovePhase: MovePhase;
do {
targetingMovePhase = this.scene.findPhase(mp => mp instanceof MovePhase && mp.targets.length === 1 && mp.targets[0] === pokemon.getBattlerIndex() && mp.pokemon.isPlayer() !== allyPokemon.isPlayer()) as MovePhase;
if (targetingMovePhase && targetingMovePhase.targets[0] !== allyPokemon.getBattlerIndex()) {
2024-03-28 04:24:24 +00:00
targetingMovePhase.targets[0] = allyPokemon.getBattlerIndex();
}
2024-03-28 04:24:24 +00:00
} while (targetingMovePhase);
}
}
2023-04-22 00:30:04 +01:00
pokemon.lapseTags(BattlerTagLapseType.FAINT);
this.scene.getField(true).filter(p => p !== pokemon).forEach(p => p.removeTagsBySourceId(pokemon.id));
2023-04-13 17:16:36 +01:00
2023-04-10 19:12:01 +01:00
pokemon.faintCry(() => {
if (pokemon instanceof PlayerPokemon) {
pokemon.addFriendship(-10);
}
2023-04-10 19:12:01 +01:00
pokemon.hideInfo();
this.scene.playSound("faint");
2023-04-10 19:12:01 +01:00
this.scene.tweens.add({
targets: pokemon,
duration: 500,
y: pokemon.y + 150,
ease: "Sine.easeIn",
2023-04-10 19:12:01 +01:00
onComplete: () => {
pokemon.setVisible(false);
pokemon.y -= 150;
2023-04-16 23:40:32 +01:00
pokemon.trySetStatus(StatusEffect.FAINT);
if (pokemon.isPlayer()) {
2023-04-10 19:12:01 +01:00
this.scene.currentBattle.removeFaintedParticipant(pokemon as PlayerPokemon);
} else {
2024-03-17 15:36:19 +00:00
this.scene.addFaintedEnemyScore(pokemon as EnemyPokemon);
2024-03-07 02:05:23 +00:00
this.scene.currentBattle.addPostBattleLoot(pokemon as EnemyPokemon);
2024-03-17 15:36:19 +00:00
}
2023-04-10 19:12:01 +01:00
this.scene.field.remove(pokemon);
this.end();
}
});
});
}
tryOverrideForBattleSpec(): boolean {
switch (this.scene.currentBattle.battleSpec) {
case BattleSpec.FINAL_BOSS:
if (!this.player) {
const enemy = this.getPokemon();
if (enemy.formIndex) {
this.scene.ui.showDialogue(battleSpecDialogue[BattleSpec.FINAL_BOSS].secondStageWin, enemy.species.name, null, () => this.doFaint());
} else {
// Final boss' HP threshold has been bypassed; cancel faint and force check for 2nd phase
enemy.hp++;
this.scene.unshiftPhase(new DamagePhase(this.scene, enemy.getBattlerIndex(), 0, HitResult.OTHER));
this.end();
}
return true;
}
}
return false;
}
2023-04-10 19:12:01 +01:00
}
export class VictoryPhase extends PokemonPhase {
2023-05-29 17:24:38 +01:00
constructor(scene: BattleScene, battlerIndex: BattlerIndex) {
super(scene, battlerIndex);
2023-04-10 19:12:01 +01:00
}
start() {
super.start();
this.scene.gameData.gameStats.pokemonDefeated++;
2023-04-10 19:12:01 +01:00
const participantIds = this.scene.currentBattle.playerParticipantIds;
const party = this.scene.getParty();
const expShareModifier = this.scene.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier;
2023-04-20 03:51:46 +01:00
const expBalanceModifier = this.scene.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier;
const multipleParticipantExpBonusModifier = this.scene.findModifier(m => m instanceof MultipleParticipantExpBonusModifier) as MultipleParticipantExpBonusModifier;
const nonFaintedPartyMembers = party.filter(p => p.hp);
const expPartyMembers = nonFaintedPartyMembers.filter(p => p.level < this.scene.getMaxExpLevel());
2023-04-20 03:51:46 +01:00
const partyMemberExp = [];
2023-11-24 21:27:26 +00:00
if (participantIds.size) {
let expValue = this.getPokemon().getExpValue();
if (this.scene.currentBattle.battleType === BattleType.TRAINER) {
2023-11-24 21:27:26 +00:00
expValue = Math.floor(expValue * 1.5);
}
for (const partyMember of nonFaintedPartyMembers) {
2023-11-24 21:27:26 +00:00
const pId = partyMember.id;
const participated = participantIds.has(pId);
if (participated) {
partyMember.addFriendship(2);
}
if (!expPartyMembers.includes(partyMember)) {
continue;
}
if (!participated && !expShareModifier) {
2023-11-24 21:27:26 +00:00
partyMemberExp.push(0);
continue;
}
let expMultiplier = 0;
if (participated) {
expMultiplier += (1 / participantIds.size);
if (participantIds.size > 1 && multipleParticipantExpBonusModifier) {
2023-11-24 21:27:26 +00:00
expMultiplier += multipleParticipantExpBonusModifier.getStackCount() * 0.2;
}
} else if (expShareModifier) {
2023-11-24 21:27:26 +00:00
expMultiplier += (expShareModifier.getStackCount() * 0.2) / participantIds.size;
}
if (partyMember.pokerus) {
2023-11-24 21:27:26 +00:00
expMultiplier *= 1.5;
}
if (Overrides.XP_MULTIPLIER_OVERRIDE !== null) {
expMultiplier = Overrides.XP_MULTIPLIER_OVERRIDE;
}
2023-11-24 21:27:26 +00:00
const pokemonExp = new Utils.NumberHolder(expValue * expMultiplier);
this.scene.applyModifiers(PokemonExpBoosterModifier, true, partyMember, pokemonExp);
partyMemberExp.push(Math.floor(pokemonExp.value));
2023-04-20 03:51:46 +01:00
}
2023-11-24 21:27:26 +00:00
if (expBalanceModifier) {
let totalLevel = 0;
let totalExp = 0;
expPartyMembers.forEach((expPartyMember, epm) => {
totalExp += partyMemberExp[epm];
totalLevel += expPartyMember.level;
});
2023-04-20 03:51:46 +01:00
2023-11-24 21:27:26 +00:00
const medianLevel = Math.floor(totalLevel / expPartyMembers.length);
2023-04-20 03:51:46 +01:00
2023-11-24 21:27:26 +00:00
const recipientExpPartyMemberIndexes = [];
expPartyMembers.forEach((expPartyMember, epm) => {
if (expPartyMember.level <= medianLevel) {
2023-11-24 21:27:26 +00:00
recipientExpPartyMemberIndexes.push(epm);
}
2023-11-24 21:27:26 +00:00
});
2023-04-20 03:51:46 +01:00
2023-11-24 21:27:26 +00:00
const splitExp = Math.floor(totalExp / recipientExpPartyMemberIndexes.length);
2023-04-20 03:51:46 +01:00
2023-11-24 21:27:26 +00:00
expPartyMembers.forEach((_partyMember, pm) => {
partyMemberExp[pm] = Phaser.Math.Linear(partyMemberExp[pm], recipientExpPartyMemberIndexes.indexOf(pm) > -1 ? splitExp : 0, 0.2 * expBalanceModifier.getStackCount());
2023-11-24 21:27:26 +00:00
});
}
2023-04-20 03:51:46 +01:00
2023-11-24 21:27:26 +00:00
for (let pm = 0; pm < expPartyMembers.length; pm++) {
const exp = partyMemberExp[pm];
2023-04-20 03:51:46 +01:00
2023-11-24 21:27:26 +00:00
if (exp) {
const partyMemberIndex = party.indexOf(expPartyMembers[pm]);
this.scene.unshiftPhase(expPartyMembers[pm].isOnField() ? new ExpPhase(this.scene, partyMemberIndex, exp) : new ShowPartyExpBarPhase(this.scene, partyMemberIndex, exp));
}
2023-04-10 19:12:01 +01:00
}
}
2024-05-24 00:45:04 +01:00
if (!this.scene.getEnemyParty().find(p => this.scene.currentBattle.battleType ? !p?.isFainted(true) : p.isOnField())) {
this.scene.pushPhase(new BattleEndPhase(this.scene));
if (this.scene.currentBattle.battleType === BattleType.TRAINER) {
this.scene.pushPhase(new TrainerVictoryPhase(this.scene));
}
2024-03-14 20:26:57 +00:00
if (this.scene.gameMode.isEndless || !this.scene.gameMode.isWaveFinal(this.scene.currentBattle.waveIndex)) {
this.scene.pushPhase(new EggLapsePhase(this.scene));
if (this.scene.currentBattle.waveIndex % 10) {
this.scene.pushPhase(new SelectModifierPhase(this.scene));
} else if (this.scene.gameMode.isDaily) {
2024-03-17 02:06:56 +00:00
this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.EXP_CHARM));
if (this.scene.currentBattle.waveIndex > 10 && !this.scene.gameMode.isWaveFinal(this.scene.currentBattle.waveIndex)) {
2024-03-17 02:06:56 +00:00
this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.GOLDEN_POKEBALL));
}
2024-03-17 02:06:56 +00:00
} else {
const superExpWave = !this.scene.gameMode.isEndless ? (this.scene.offsetGym ? 0 : 20) : 10;
if (this.scene.gameMode.isEndless && this.scene.currentBattle.waveIndex === 10) {
2024-03-24 15:17:12 +00:00
this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.EXP_SHARE));
}
if (this.scene.currentBattle.waveIndex <= 750 && (this.scene.currentBattle.waveIndex <= 500 || (this.scene.currentBattle.waveIndex % 30) === superExpWave)) {
this.scene.pushPhase(new ModifierRewardPhase(this.scene, (this.scene.currentBattle.waveIndex % 30) !== superExpWave || this.scene.currentBattle.waveIndex > 250 ? modifierTypes.EXP_CHARM : modifierTypes.SUPER_EXP_CHARM));
}
if (this.scene.currentBattle.waveIndex <= 150 && !(this.scene.currentBattle.waveIndex % 50)) {
this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.GOLDEN_POKEBALL));
}
2024-03-14 20:26:57 +00:00
if (this.scene.gameMode.isEndless && !(this.scene.currentBattle.waveIndex % 50)) {
2024-01-01 04:19:06 +00:00
this.scene.pushPhase(new ModifierRewardPhase(this.scene, !(this.scene.currentBattle.waveIndex % 250) ? modifierTypes.VOUCHER_PREMIUM : modifierTypes.VOUCHER_PLUS));
this.scene.pushPhase(new AddEnemyBuffModifierPhase(this.scene));
}
2023-11-28 21:59:40 +00:00
}
this.scene.pushPhase(new NewBattlePhase(this.scene));
2024-03-18 00:31:09 +00:00
} else {
this.scene.currentBattle.battleType = BattleType.CLEAR;
2024-03-18 00:58:12 +00:00
this.scene.score += this.scene.gameMode.getClearScoreBonus();
2024-03-19 04:03:41 +00:00
this.scene.updateScoreText();
this.scene.pushPhase(new GameOverPhase(this.scene, true));
2024-03-18 00:31:09 +00:00
}
}
2023-04-10 19:12:01 +01:00
this.end();
}
}
export class TrainerVictoryPhase extends BattlePhase {
constructor(scene: BattleScene) {
super(scene);
}
start() {
2024-04-18 05:24:57 +01:00
this.scene.disableMenu = true;
this.scene.playBgm(this.scene.currentBattle.trainer.config.victoryBgm);
this.scene.unshiftPhase(new MoneyRewardPhase(this.scene, this.scene.currentBattle.trainer.config.moneyMultiplier));
const modifierRewardFuncs = this.scene.currentBattle.trainer.config.modifierRewardFuncs;
for (const modifierRewardFunc of modifierRewardFuncs) {
this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, modifierRewardFunc));
}
const trainerType = this.scene.currentBattle.trainer.config.trainerType;
if (vouchers.hasOwnProperty(TrainerType[trainerType])) {
if (!this.scene.validateVoucher(vouchers[TrainerType[trainerType]]) && this.scene.currentBattle.trainer.config.isBoss) {
2024-05-01 04:02:16 +01:00
this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, [ modifierTypes.VOUCHER, modifierTypes.VOUCHER, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM ][vouchers[TrainerType[trainerType]].voucherType]));
}
}
this.scene.ui.showText(i18next.t("battle:trainerDefeated", { trainerName: this.scene.currentBattle.trainer.getName(TrainerSlot.NONE, true) }), null, () => {
2024-02-14 19:41:39 +00:00
const victoryMessages = this.scene.currentBattle.trainer.getVictoryMessages();
let message: string;
this.scene.executeWithSeedOffset(() => message = Utils.randSeedItem(victoryMessages), this.scene.currentBattle.waveIndex);
2024-05-24 00:45:04 +01:00
const showMessage = () => {
const originalFunc = showMessageOrEnd;
showMessageOrEnd = () => this.scene.ui.showDialogue(message, this.scene.currentBattle.trainer.getName(), null, originalFunc);
showMessageOrEnd();
};
let showMessageOrEnd = () => this.end();
if (victoryMessages?.length) {
if (this.scene.currentBattle.trainer.config.hasCharSprite && !this.scene.ui.shouldSkipDialogue(message)) {
const originalFunc = showMessageOrEnd;
showMessageOrEnd = () => this.scene.charSprite.hide().then(() => this.scene.hideFieldOverlay(250).then(() => originalFunc()));
2024-02-26 00:09:24 +00:00
this.scene.showFieldOverlay(500).then(() => this.scene.charSprite.showCharacter(this.scene.currentBattle.trainer.getKey(), getCharVariantFromDialogue(victoryMessages[0])).then(() => showMessage()));
} else {
showMessage();
}
} else {
showMessageOrEnd();
}
}, null, true);
this.showEnemyTrainer();
}
}
export class MoneyRewardPhase extends BattlePhase {
private moneyMultiplier: number;
constructor(scene: BattleScene, moneyMultiplier: number) {
super(scene);
this.moneyMultiplier = moneyMultiplier;
}
start() {
const moneyAmount = new Utils.IntegerHolder(this.scene.getWaveMoneyAmount(this.moneyMultiplier));
2024-01-18 22:22:18 +00:00
this.scene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount);
2024-04-22 02:17:07 +01:00
this.scene.addMoney(moneyAmount.value);
const userLocale = navigator.language || "en-US";
const formattedMoneyAmount = moneyAmount.value.toLocaleString(userLocale);
const message = i18next.t("battle:moneyWon", { moneyAmount: formattedMoneyAmount });
this.scene.ui.showText(message, null, () => this.end(), null, true);
}
}
export class ModifierRewardPhase extends BattlePhase {
protected modifierType: ModifierType;
constructor(scene: BattleScene, modifierTypeFunc: ModifierTypeFunc) {
super(scene);
this.modifierType = getModifierType(modifierTypeFunc);
}
start() {
super.start();
this.doReward().then(() => this.end());
}
doReward(): Promise<void> {
return new Promise<void>(resolve => {
const newModifier = this.modifierType.newModifier();
this.scene.addModifier(newModifier).then(() => {
this.scene.playSound("item_fanfare");
this.scene.ui.showText(`You received\n${newModifier.type.name}!`, null, () => resolve(), null, true);
});
});
}
}
export class GameOverModifierRewardPhase extends ModifierRewardPhase {
constructor(scene: BattleScene, modifierTypeFunc: ModifierTypeFunc) {
super(scene, modifierTypeFunc);
}
doReward(): Promise<void> {
return new Promise<void>(resolve => {
const newModifier = this.modifierType.newModifier();
this.scene.addModifier(newModifier).then(() => {
this.scene.playSound("level_up_fanfare");
this.scene.ui.setMode(Mode.MESSAGE);
2024-05-17 11:51:48 +01:00
this.scene.ui.fadeIn(250).then(() => {
this.scene.ui.showText(`You received\n${newModifier.type.name}!`, null, () => {
this.scene.time.delayedCall(1500, () => this.scene.arenaBg.setVisible(true));
resolve();
}, null, true, 1500);
2024-05-17 11:51:48 +01:00
});
});
});
}
}
export class RibbonModifierRewardPhase extends ModifierRewardPhase {
private species: PokemonSpecies;
constructor(scene: BattleScene, modifierTypeFunc: ModifierTypeFunc, species: PokemonSpecies) {
super(scene, modifierTypeFunc);
this.species = species;
}
doReward(): Promise<void> {
return new Promise<void>(resolve => {
const newModifier = this.modifierType.newModifier();
this.scene.addModifier(newModifier).then(() => {
this.scene.playSound("level_up_fanfare");
this.scene.ui.setMode(Mode.MESSAGE);
2024-05-17 02:41:38 +01:00
this.scene.ui.showText(`${this.species.name} beat ${this.scene.gameMode.getName()} Mode for the first time!\nYou received ${newModifier.type.name}!`, null, () => {
resolve();
}, null, true, 1500);
});
});
}
}
export class GameOverPhase extends BattlePhase {
private victory: boolean;
private firstRibbons: PokemonSpecies[] = [];
constructor(scene: BattleScene, victory?: boolean) {
super(scene);
this.victory = !!victory;
}
start() {
super.start();
// Failsafe if players somehow skip floor 200 in classic mode
if (this.scene.gameMode.isClassic && this.scene.currentBattle.waveIndex > 200) {
this.victory = true;
}
2024-05-28 02:36:32 +01:00
if (this.victory && this.scene.gameMode.isEndless) {
2024-05-28 02:01:13 +01:00
this.scene.ui.showDialogue(i18next.t("PGMmiscDialogue:ending_endless"), i18next.t("PGMmiscDialogue:ending_name"), 0, () => this.handleGameOver());
} else if (this.victory || !this.scene.enableRetries) {
this.handleGameOver();
} else {
this.scene.ui.showText("Would you like to retry from the start of the battle?", null, () => {
2024-04-04 20:22:05 +01:00
this.scene.ui.setMode(Mode.CONFIRM, () => {
this.scene.ui.fadeOut(1250).then(() => {
this.scene.reset();
2024-04-04 20:22:05 +01:00
this.scene.clearPhaseQueue();
this.scene.gameData.loadSession(this.scene, this.scene.sessionSlotId).then(() => {
this.scene.pushPhase(new EncounterPhase(this.scene, true));
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()).length;
2024-05-24 00:45:04 +01:00
2024-04-04 20:22:05 +01:00
this.scene.pushPhase(new SummonPhase(this.scene, 0));
if (this.scene.currentBattle.double && availablePartyMembers > 1) {
2024-04-04 20:22:05 +01:00
this.scene.pushPhase(new SummonPhase(this.scene, 1));
}
2024-04-04 20:22:05 +01:00
if (this.scene.currentBattle.waveIndex > 1 && this.scene.currentBattle.battleType !== BattleType.TRAINER) {
this.scene.pushPhase(new CheckSwitchPhase(this.scene, 0, this.scene.currentBattle.double));
if (this.scene.currentBattle.double && availablePartyMembers > 1) {
2024-04-04 20:22:05 +01:00
this.scene.pushPhase(new CheckSwitchPhase(this.scene, 1, this.scene.currentBattle.double));
}
2024-04-04 20:22:05 +01:00
}
this.scene.ui.fadeIn(1250);
this.end();
});
});
}, () => this.handleGameOver(), false, 0, 0, 1000);
2024-04-04 20:22:05 +01:00
});
}
}
handleGameOver(): void {
const doGameOver = (newClear: boolean) => {
2024-05-17 02:41:38 +01:00
this.scene.disableMenu = true;
this.scene.time.delayedCall(1000, () => {
let firstClear = false;
if (this.victory && newClear) {
2024-03-17 02:06:56 +00:00
if (this.scene.gameMode.isClassic) {
firstClear = this.scene.validateAchv(achvs.CLASSIC_VICTORY);
this.scene.gameData.gameStats.sessionsWon++;
for (const pokemon of this.scene.getParty()) {
this.awardRibbon(pokemon);
if (pokemon.species.getRootSpeciesId() !== pokemon.species.getRootSpeciesId(true)) {
this.awardRibbon(pokemon, true);
}
}
} else if (this.scene.gameMode.isDaily && newClear) {
2024-03-17 02:06:56 +00:00
this.scene.gameData.gameStats.dailyRunSessionsWon++;
}
}
const fadeDuration = this.victory ? 10000 : 5000;
this.scene.fadeOutBgm(fadeDuration, true);
const activeBattlers = this.scene.getField().filter(p => p?.isActive(true));
activeBattlers.map(p => p.hideInfo());
this.scene.ui.fadeOut(fadeDuration).then(() => {
2024-05-17 02:41:38 +01:00
activeBattlers.map(a => a.setVisible(false));
this.scene.setFieldScale(1, true);
this.scene.clearPhaseQueue();
this.scene.ui.clearText();
2024-05-17 02:41:38 +01:00
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
if (this.victory && this.scene.gameMode.isChallenge) {
this.scene.gameMode.challenges.forEach(c => this.scene.validateAchvs(ChallengeAchv, c));
}
2024-05-17 02:41:38 +01:00
const clear = (endCardPhase?: EndCardPhase) => {
if (newClear) {
2024-05-17 02:41:38 +01:00
this.handleUnlocks();
}
2024-05-17 02:41:38 +01:00
if (this.victory && newClear) {
for (const species of this.firstRibbons) {
2024-05-17 02:41:38 +01:00
this.scene.unshiftPhase(new RibbonModifierRewardPhase(this.scene, modifierTypes.VOUCHER_PLUS, species));
}
if (!firstClear) {
2024-05-17 02:41:38 +01:00
this.scene.unshiftPhase(new GameOverModifierRewardPhase(this.scene, modifierTypes.VOUCHER_PREMIUM));
}
2024-05-17 02:41:38 +01:00
}
this.scene.pushPhase(new PostGameOverPhase(this.scene, endCardPhase));
this.end();
};
2024-05-17 02:41:38 +01:00
2024-05-17 05:20:18 +01:00
if (this.victory && this.scene.gameMode.isClassic) {
const message = miscDialogue.ending[this.scene.gameData.gender === PlayerGender.FEMALE ? 0 : 1];
if (!this.scene.ui.shouldSkipDialogue(message)) {
this.scene.ui.fadeIn(500).then(() => {
this.scene.charSprite.showCharacter(`rival_${this.scene.gameData.gender === PlayerGender.FEMALE ? "m" : "f"}`, getCharVariantFromDialogue(miscDialogue.ending[this.scene.gameData.gender === PlayerGender.FEMALE ? 0 : 1])).then(() => {
this.scene.ui.showDialogue(message, this.scene.gameData.gender === PlayerGender.FEMALE ? trainerConfigs[TrainerType.RIVAL].name : trainerConfigs[TrainerType.RIVAL].nameFemale, null, () => {
this.scene.ui.fadeOut(500).then(() => {
this.scene.charSprite.hide().then(() => {
const endCardPhase = new EndCardPhase(this.scene);
this.scene.unshiftPhase(endCardPhase);
clear(endCardPhase);
});
2024-05-17 02:41:38 +01:00
});
});
});
});
} else {
const endCardPhase = new EndCardPhase(this.scene);
this.scene.unshiftPhase(endCardPhase);
clear(endCardPhase);
}
} else {
2024-05-17 02:41:38 +01:00
clear();
}
});
});
};
/* Added a local check to see if the game is running offline on victory
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));
} else {
this.scene.gameData.offlineNewClear(this.scene).then(result => {
doGameOver(result);
});
}
} else {
doGameOver(false);
}
}
2023-04-29 06:40:24 +01:00
handleUnlocks(): void {
2024-03-26 01:09:32 +00:00
if (this.victory && this.scene.gameMode.isClassic) {
if (!this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) {
2023-06-05 16:39:49 +01:00
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]) {
this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.SPLICED_ENDLESS_MODE));
}
if (!this.scene.gameData.unlocks[Unlockables.MINI_BLACK_HOLE]) {
2023-06-05 16:39:49 +01:00
this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.MINI_BLACK_HOLE));
}
2023-06-05 16:39:49 +01:00
}
2023-04-29 06:40:24 +01:00
}
awardRibbon(pokemon: Pokemon, forStarter: boolean = false): void {
const speciesId = getPokemonSpecies(pokemon.species.speciesId);
const speciesRibbonCount = this.scene.gameData.incrementRibbonCount(speciesId, forStarter);
// first time classic win, award voucher
if (speciesRibbonCount === 1) {
this.firstRibbons.push(getPokemonSpecies(pokemon.species.getRootSpeciesId(forStarter)));
}
}
2023-04-29 06:40:24 +01:00
}
2024-05-17 02:41:38 +01:00
export class EndCardPhase extends Phase {
public endCard: Phaser.GameObjects.Image;
public text: Phaser.GameObjects.Text;
constructor(scene: BattleScene) {
super(scene);
}
start(): void {
super.start();
this.scene.ui.getMessageHandler().bg.setVisible(false);
this.scene.ui.getMessageHandler().nameBoxContainer.setVisible(false);
this.endCard = this.scene.add.image(0, 0, `end_${this.scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}`);
2024-05-17 02:41:38 +01:00
this.endCard.setOrigin(0);
this.endCard.setScale(0.5);
this.scene.field.add(this.endCard);
this.text = addTextObject(this.scene, this.scene.game.canvas.width / 12, (this.scene.game.canvas.height / 6) - 16, "Congratulations!", TextStyle.SUMMARY, { fontSize: "128px" });
2024-05-17 02:41:38 +01:00
this.text.setOrigin(0.5);
this.scene.field.add(this.text);
this.scene.ui.clearText();
2024-05-24 00:45:04 +01:00
2024-05-17 02:41:38 +01:00
this.scene.ui.fadeIn(1000).then(() => {
2024-05-24 00:45:04 +01:00
this.scene.ui.showText("", null, () => {
2024-05-17 02:41:38 +01:00
this.scene.ui.getMessageHandler().bg.setVisible(true);
this.end();
}, null, true);
});
}
}
export class UnlockPhase extends Phase {
2023-04-29 06:40:24 +01:00
private unlockable: Unlockables;
constructor(scene: BattleScene, unlockable: Unlockables) {
super(scene);
this.unlockable = unlockable;
}
start(): void {
this.scene.time.delayedCall(2000, () => {
this.scene.gameData.unlocks[this.unlockable] = true;
this.scene.playSound("level_up_fanfare");
this.scene.ui.setMode(Mode.MESSAGE);
2024-05-17 02:41:38 +01:00
this.scene.ui.showText(`${getUnlockableName(this.unlockable)}\nhas been unlocked.`, null, () => {
this.scene.time.delayedCall(1500, () => this.scene.arenaBg.setVisible(true));
this.end();
}, null, true, 1500);
});
}
}
export class PostGameOverPhase extends Phase {
2024-05-17 02:41:38 +01:00
private endCardPhase: EndCardPhase;
constructor(scene: BattleScene, endCardPhase: EndCardPhase) {
super(scene);
2024-05-17 02:41:38 +01:00
this.endCardPhase = endCardPhase;
}
start() {
super.start();
2024-05-17 02:41:38 +01:00
const saveAndReset = () => {
this.scene.gameData.saveAll(this.scene, true, true, true).then(success => {
if (!success) {
return this.scene.reset(true);
}
2024-05-17 02:41:38 +01:00
this.scene.gameData.tryClearSession(this.scene, this.scene.sessionSlotId).then((success: boolean | [boolean, boolean]) => {
if (!success[0]) {
2024-05-17 02:41:38 +01:00
return this.scene.reset(true);
}
2024-05-17 02:41:38 +01:00
this.scene.reset();
this.scene.unshiftPhase(new TitlePhase(this.scene));
this.end();
});
2023-04-29 06:40:24 +01:00
});
2024-05-17 02:41:38 +01:00
};
if (this.endCardPhase) {
this.scene.ui.fadeOut(500).then(() => {
this.scene.ui.getMessageHandler().bg.setVisible(true);
this.endCardPhase.endCard.destroy();
this.endCardPhase.text.destroy();
saveAndReset();
});
} else {
2024-05-17 02:41:38 +01:00
saveAndReset();
}
2023-04-29 06:40:24 +01:00
}
}
2023-04-10 19:12:01 +01:00
export class SwitchPhase extends BattlePhase {
protected fieldIndex: integer;
2023-04-10 19:12:01 +01:00
private isModal: boolean;
private doReturn: boolean;
constructor(scene: BattleScene, fieldIndex: integer, isModal: boolean, doReturn: boolean) {
2023-04-10 19:12:01 +01:00
super(scene);
this.fieldIndex = fieldIndex;
2023-04-10 19:12:01 +01:00
this.isModal = isModal;
this.doReturn = doReturn;
}
start() {
super.start();
// Skip modal switch if impossible
if (this.isModal && !this.scene.getParty().filter(p => p.isAllowedInBattle() && !p.isActive(true)).length) {
return super.end();
}
2024-05-20 00:02:17 +01:00
// Check if there is any space still in field
if (this.isModal && this.scene.getPlayerField().filter(p => p.isAllowedInBattle() && p.isActive(true)).length >= this.scene.currentBattle.getBattlerCount()) {
2024-05-20 00:02:17 +01:00
return super.end();
}
2024-05-20 00:02:17 +01:00
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
// 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;
2023-11-25 20:53:38 +00:00
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) {
2023-11-25 20:53:38 +00:00
this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, fieldIndex, slotIndex, this.doReturn, option === PartyOption.PASS_BATON));
}
2023-04-10 19:12:01 +01:00
this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end());
}, PartyUiHandler.FilterNonFainted);
}
}
2023-10-07 21:08:33 +01:00
export class ExpPhase extends PlayerPartyMemberPokemonPhase {
2023-04-10 19:12:01 +01:00
private expValue: number;
constructor(scene: BattleScene, partyMemberIndex: integer, expValue: number) {
super(scene, partyMemberIndex);
this.expValue = expValue;
}
start() {
super.start();
const pokemon = this.getPokemon();
const exp = new Utils.NumberHolder(this.expValue);
2023-04-21 00:44:56 +01:00
this.scene.applyModifiers(ExpBoosterModifier, true, exp);
2023-04-10 19:12:01 +01:00
exp.value = Math.floor(exp.value);
this.scene.ui.showText(i18next.t("battle:expGain", { pokemonName: pokemon.name, exp: exp.value }), null, () => {
2023-04-10 19:12:01 +01:00
const lastLevel = pokemon.level;
pokemon.addExp(exp.value);
const newLevel = pokemon.level;
if (newLevel > lastLevel) {
2023-04-10 19:12:01 +01:00
this.scene.unshiftPhase(new LevelUpPhase(this.scene, this.partyMemberIndex, lastLevel, newLevel));
}
2023-04-10 19:12:01 +01:00
pokemon.updateInfo().then(() => this.end());
}, null, true);
}
}
2023-10-07 21:08:33 +01:00
export class ShowPartyExpBarPhase extends PlayerPartyMemberPokemonPhase {
private expValue: number;
constructor(scene: BattleScene, partyMemberIndex: integer, expValue: number) {
super(scene, partyMemberIndex);
this.expValue = expValue;
}
start() {
super.start();
const pokemon = this.getPokemon();
const exp = new Utils.NumberHolder(this.expValue);
this.scene.applyModifiers(ExpBoosterModifier, true, exp);
exp.value = Math.floor(exp.value);
const lastLevel = pokemon.level;
pokemon.addExp(exp.value);
const newLevel = pokemon.level;
if (newLevel > lastLevel) {
this.scene.unshiftPhase(new LevelUpPhase(this.scene, this.partyMemberIndex, lastLevel, newLevel));
}
this.scene.unshiftPhase(new HidePartyExpBarPhase(this.scene));
pokemon.updateInfo();
if (this.scene.expParty === ExpNotification.SKIP) {
this.end();
} else if (this.scene.expParty === ExpNotification.ONLY_LEVEL_UP) {
if (newLevel > lastLevel) { // this means if we level up
// instead of displaying the exp gain in the small frame, we display the new level
// we use the same method for mode 0 & 1, by giving a parameter saying to display the exp or the level
this.scene.partyExpBar.showPokemonExp(pokemon, exp.value, this.scene.expParty === ExpNotification.ONLY_LEVEL_UP, newLevel).then(() => {
setTimeout(() => this.end(), 800 / Math.pow(2, this.scene.expGainsSpeed));
});
} else {
this.end();
}
} else if (this.scene.expGainsSpeed < 3) {
this.scene.partyExpBar.showPokemonExp(pokemon, exp.value, false, newLevel).then(() => {
setTimeout(() => this.end(), 500 / Math.pow(2, this.scene.expGainsSpeed));
});
} else {
this.end();
}
}
}
export class HidePartyExpBarPhase extends BattlePhase {
constructor(scene: BattleScene) {
super(scene);
}
2023-04-10 19:12:01 +01:00
start() {
super.start();
2023-04-10 19:12:01 +01:00
this.scene.partyExpBar.hide().then(() => this.end());
}
2023-04-10 19:12:01 +01:00
}
2023-10-07 21:08:33 +01:00
export class LevelUpPhase extends PlayerPartyMemberPokemonPhase {
2023-04-10 19:12:01 +01:00
private lastLevel: integer;
private level: integer;
constructor(scene: BattleScene, partyMemberIndex: integer, lastLevel: integer, level: integer) {
super(scene, partyMemberIndex);
this.lastLevel = lastLevel;
this.level = level;
this.scene = scene;
2023-04-10 19:12:01 +01:00
}
start() {
super.start();
if (this.level > this.scene.gameData.gameStats.highestLevel) {
this.scene.gameData.gameStats.highestLevel = this.level;
}
this.scene.validateAchvs(LevelAchv, new Utils.IntegerHolder(this.level));
2023-04-10 19:12:01 +01:00
const pokemon = this.getPokemon();
const prevStats = pokemon.stats.slice(0);
pokemon.calculateStats();
pokemon.updateInfo();
if (this.scene.expParty === ExpNotification.DEFAULT) {
this.scene.playSound("level_up_fanfare");
this.scene.ui.showText(i18next.t("battle:levelUp", { pokemonName: this.getPokemon().name, level: this.level }), null, () => this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false).then(() => this.end()), null, true);
} else if (this.scene.expParty === ExpNotification.SKIP) {
this.end();
} else {
// we still want to display the stats if activated
this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false).then(() => this.end());
}
if (this.level <= 100) {
const levelMoves = this.getPokemon().getLevelMoves(this.lastLevel + 1);
for (const lm of levelMoves) {
this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, lm[1]));
}
}
if (!pokemon.pauseEvolutions) {
const evolution = pokemon.getEvolution();
if (evolution) {
2024-01-10 04:34:43 +00:00
this.scene.unshiftPhase(new EvolutionPhase(this.scene, pokemon as PlayerPokemon, evolution, this.lastLevel));
}
}
2023-04-10 19:12:01 +01:00
}
}
2023-10-07 21:08:33 +01:00
export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
2023-04-10 19:12:01 +01:00
private moveId: Moves;
constructor(scene: BattleScene, partyMemberIndex: integer, moveId: Moves) {
super(scene, partyMemberIndex);
this.moveId = moveId;
}
start() {
super.start();
const pokemon = this.getPokemon();
2023-04-21 02:32:48 +01:00
const move = allMoves[this.moveId];
2023-04-10 19:12:01 +01:00
const existingMoveIndex = pokemon.getMoveset().findIndex(m => m?.moveId === move.id);
2023-04-10 19:12:01 +01:00
if (existingMoveIndex > -1) {
2023-12-07 22:43:56 +00:00
return this.end();
}
2023-04-10 19:12:01 +01:00
const emptyMoveIndex = pokemon.getMoveset().length < 4
? pokemon.getMoveset().length
: pokemon.getMoveset().findIndex(m => m === null);
2023-04-10 19:12:01 +01:00
const messageMode = this.scene.ui.getHandler() instanceof EvolutionSceneHandler
? Mode.EVOLUTION_SCENE
: Mode.MESSAGE;
if (emptyMoveIndex > -1) {
pokemon.setMove(emptyMoveIndex, this.moveId);
initMoveAnim(this.scene, this.moveId).then(() => {
2023-04-10 19:12:01 +01:00
loadMoveAnimAssets(this.scene, [ this.moveId ], true)
.then(() => {
this.scene.ui.setMode(messageMode).then(() => {
this.scene.playSound("level_up_fanfare");
this.scene.ui.showText(i18next.t("battle:learnMove", { pokemonName: pokemon.name, moveName: move.name }), null, () => {
2024-01-10 04:34:43 +00:00
this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeMoveLearnedTrigger, true);
this.end();
}, messageMode === Mode.EVOLUTION_SCENE ? 1000 : null, true);
2023-04-10 19:12:01 +01:00
});
});
});
2023-04-10 19:12:01 +01:00
} else {
this.scene.ui.setMode(messageMode).then(() => {
this.scene.ui.showText(i18next.t("battle:learnMovePrompt", { pokemonName: pokemon.name, moveName: move.name }), null, () => {
this.scene.ui.showText(i18next.t("battle:learnMoveLimitReached", { pokemonName: pokemon.name }), null, () => {
this.scene.ui.showText(i18next.t("battle:learnMoveReplaceQuestion", { moveName: move.name }), null, () => {
2023-04-10 19:12:01 +01:00
const noHandler = () => {
this.scene.ui.setMode(messageMode).then(() => {
this.scene.ui.showText(i18next.t("battle:learnMoveStopTeaching", { moveName: move.name }), null, () => {
2023-04-10 19:12:01 +01:00
this.scene.ui.setModeWithoutClear(Mode.CONFIRM, () => {
this.scene.ui.setMode(messageMode);
this.scene.ui.showText(i18next.t("battle:learnMoveNotLearned", { pokemonName: pokemon.name, moveName: move.name }), null, () => this.end(), null, true);
2023-04-10 19:12:01 +01:00
}, () => {
this.scene.ui.setMode(messageMode);
this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, this.moveId));
this.end();
});
});
});
};
this.scene.ui.setModeWithoutClear(Mode.CONFIRM, () => {
this.scene.ui.setMode(messageMode);
this.scene.ui.showText(i18next.t("battle:learnMoveForgetQuestion"), null, () => {
2023-04-10 19:12:01 +01:00
this.scene.ui.setModeWithoutClear(Mode.SUMMARY, this.getPokemon(), SummaryUiMode.LEARN_MOVE, move, (moveIndex: integer) => {
if (moveIndex === 4) {
noHandler();
return;
}
this.scene.ui.setMode(messageMode).then(() => {
this.scene.ui.showText(i18next.t("battle:countdownPoof"), null, () => {
this.scene.ui.showText(i18next.t("battle:learnMoveForgetSuccess", { pokemonName: pokemon.name, moveName: pokemon.moveset[moveIndex].getName() }), null, () => {
this.scene.ui.showText(i18next.t("battle:learnMoveAnd"), null, () => {
pokemon.setMove(moveIndex, Moves.NONE);
2023-04-10 19:12:01 +01:00
this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, this.moveId));
this.end();
}, null, true);
}, null, true);
}, null, true);
});
});
}, null, true);
}, noHandler);
});
}, null, true);
}, null, true);
});
}
}
}
2023-04-14 06:08:44 +01:00
export class PokemonHealPhase extends CommonAnimPhase {
private hpHealed: integer;
2023-04-16 05:29:55 +01:00
private message: string;
private showFullHpMessage: boolean;
private skipAnim: boolean;
2023-06-06 15:14:53 +01:00
private revive: boolean;
private healStatus: boolean;
private preventFullHeal: boolean;
2023-04-14 06:08:44 +01:00
constructor(scene: BattleScene, battlerIndex: BattlerIndex, hpHealed: integer, message: string, showFullHpMessage: boolean, skipAnim: boolean = false, revive: boolean = false, healStatus: boolean = false, preventFullHeal: boolean = false) {
super(scene, battlerIndex, undefined, CommonAnim.HEALTH_UP);
2023-04-14 06:08:44 +01:00
this.hpHealed = hpHealed;
2023-04-16 05:29:55 +01:00
this.message = message;
this.showFullHpMessage = showFullHpMessage;
this.skipAnim = skipAnim;
this.revive = revive;
this.healStatus = healStatus;
this.preventFullHeal = preventFullHeal;
2023-04-16 05:29:55 +01:00
}
start() {
if (!this.skipAnim && (this.revive || this.getPokemon().hp) && this.getPokemon().getHpRatio() < 1) {
2023-04-16 05:29:55 +01:00
super.start();
} else {
2023-04-16 05:29:55 +01:00
this.end();
}
2023-04-14 06:08:44 +01:00
}
end() {
const pokemon = this.getPokemon();
2024-05-24 00:45:04 +01:00
2023-06-06 15:14:53 +01:00
if (!pokemon.isOnField() || (!this.revive && !pokemon.isActive())) {
2023-04-23 06:03:09 +01:00
super.end();
return;
}
2023-04-14 06:08:44 +01:00
2023-04-16 05:29:55 +01:00
const fullHp = pokemon.getHpRatio() >= 1;
const hasMessage = !!this.message;
const healOrDamage = (!fullHp || this.hpHealed < 0);
let lastStatusEffect = StatusEffect.NONE;
if (healOrDamage) {
2023-04-20 20:46:05 +01:00
const hpRestoreMultiplier = new Utils.IntegerHolder(1);
if (!this.revive) {
2023-06-06 15:14:53 +01:00
this.scene.applyModifiers(HealingBoosterModifier, this.player, hpRestoreMultiplier);
}
2024-02-20 16:42:05 +00:00
const healAmount = new Utils.NumberHolder(Math.floor(this.hpHealed * hpRestoreMultiplier.value));
if (healAmount.value < 0) {
2024-05-25 03:57:28 +01:00
pokemon.damageAndUpdate(healAmount.value * -1, HitResult.HEAL as DamageResult);
healAmount.value = 0;
}
// Prevent healing to full if specified (in case of healing tokens so Sturdy doesn't cause a softlock)
if (this.preventFullHeal && pokemon.hp + healAmount.value >= pokemon.getMaxHp()) {
healAmount.value = (pokemon.getMaxHp() - pokemon.hp) - 1;
}
2024-01-08 04:17:24 +00:00
healAmount.value = pokemon.heal(healAmount.value);
if (healAmount.value) {
this.scene.damageNumberHandler.add(pokemon, healAmount.value, HitResult.HEAL);
}
if (pokemon.isPlayer()) {
this.scene.validateAchvs(HealAchv, healAmount);
if (healAmount.value > this.scene.gameData.gameStats.highestHeal) {
this.scene.gameData.gameStats.highestHeal = healAmount.value;
}
}
if (this.healStatus && !this.revive && pokemon.status) {
lastStatusEffect = pokemon.status.effect;
pokemon.resetStatus();
}
2023-04-16 05:29:55 +01:00
pokemon.updateInfo().then(() => super.end());
2024-03-11 22:13:07 +00:00
} else if (this.healStatus && !this.revive && pokemon.status) {
lastStatusEffect = pokemon.status.effect;
pokemon.resetStatus();
pokemon.updateInfo().then(() => super.end());
} else if (this.showFullHpMessage) {
this.message = getPokemonMessage(pokemon, "'s\nHP is full!");
}
2023-04-16 05:29:55 +01:00
if (this.message) {
2023-04-22 00:30:04 +01:00
this.scene.queueMessage(this.message);
}
2023-04-16 05:29:55 +01:00
if (this.healStatus && lastStatusEffect && !hasMessage) {
this.scene.queueMessage(getPokemonMessage(pokemon, getStatusEffectHealText(lastStatusEffect)));
}
if (!healOrDamage && !lastStatusEffect) {
2023-04-16 05:29:55 +01:00
super.end();
}
2023-04-14 06:08:44 +01:00
}
}
export class AttemptCapturePhase extends PokemonPhase {
2023-04-10 19:12:01 +01:00
private pokeballType: PokeballType;
private pokeball: Phaser.GameObjects.Sprite;
private originalY: number;
constructor(scene: BattleScene, targetIndex: integer, pokeballType: PokeballType) {
super(scene, BattlerIndex.ENEMY + targetIndex);
2023-04-10 19:12:01 +01:00
this.pokeballType = pokeballType;
}
start() {
super.start();
2024-01-08 04:17:24 +00:00
const pokemon = this.getPokemon() as EnemyPokemon;
if (!pokemon?.hp) {
2023-12-07 22:43:56 +00:00
return this.end();
}
2023-04-10 19:12:01 +01:00
this.scene.pokeballCounts[this.pokeballType]--;
this.originalY = pokemon.y;
const _3m = 3 * pokemon.getMaxHp();
2023-04-10 19:12:01 +01:00
const _2h = 2 * pokemon.hp;
const catchRate = pokemon.species.catchRate;
const pokeballMultiplier = getPokeballCatchMultiplier(this.pokeballType);
const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1;
2023-04-10 19:12:01 +01:00
const x = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier);
const y = Math.round(65536 / Math.sqrt(Math.sqrt(255 / x)));
const fpOffset = pokemon.getFieldPositionOffset();
2023-04-10 19:12:01 +01:00
const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType);
this.pokeball = this.scene.addFieldSprite(16, 80, "pb", pokeballAtlasKey);
2023-04-10 19:12:01 +01:00
this.pokeball.setOrigin(0.5, 0.625);
this.scene.field.add(this.pokeball);
this.scene.playSound("pb_throw");
2023-04-10 19:12:01 +01:00
this.scene.time.delayedCall(300, () => {
2023-06-06 15:14:53 +01:00
this.scene.field.moveBelow(this.pokeball as Phaser.GameObjects.GameObject, pokemon);
2023-04-10 19:12:01 +01:00
});
2023-04-10 19:12:01 +01:00
this.scene.tweens.add({
targets: this.pokeball,
x: { value: 236 + fpOffset[0], ease: "Linear" },
y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" },
2023-04-10 19:12:01 +01:00
duration: 500,
onComplete: () => {
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`));
this.scene.playSound("pb_rel");
2023-04-10 19:12:01 +01:00
pokemon.tint(getPokeballTintColor(this.pokeballType));
addPokeballOpenParticles(this.scene, this.pokeball.x, this.pokeball.y, this.pokeballType);
2023-04-10 19:12:01 +01:00
this.scene.tweens.add({
targets: pokemon,
duration: 500,
ease: "Sine.easeIn",
2023-04-10 19:12:01 +01:00
scale: 0.25,
y: 20,
onComplete: () => {
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
2023-04-10 19:12:01 +01:00
pokemon.setVisible(false);
this.scene.playSound("pb_catch");
this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}`));
2023-04-10 19:12:01 +01:00
const doShake = () => {
2023-04-10 19:12:01 +01:00
let shakeCount = 0;
const pbX = this.pokeball.x;
const shakeCounter = this.scene.tweens.addCounter({
from: 0,
to: 1,
repeat: 4,
yoyo: true,
ease: "Cubic.easeOut",
2023-04-10 19:12:01 +01:00
duration: 250,
repeatDelay: 500,
onUpdate: t => {
if (shakeCount && shakeCount < 4) {
const value = t.getValue();
const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1;
this.pokeball.setX(pbX + value * 4 * directionMultiplier);
this.pokeball.setAngle(value * 27.5 * directionMultiplier);
}
},
onRepeat: () => {
2023-04-26 21:07:29 +01:00
if (!pokemon.species.isObtainable()) {
shakeCounter.stop();
this.failCatch(shakeCount);
} else if (shakeCount++ < 3) {
if (pokeballMultiplier === -1 || pokemon.randSeedInt(65536) < y) {
this.scene.playSound("pb_move");
} else {
2023-04-10 19:12:01 +01:00
shakeCounter.stop();
2023-04-11 06:31:18 +01:00
this.failCatch(shakeCount);
2023-04-10 19:12:01 +01:00
}
} else {
this.scene.playSound("pb_lock");
addPokeballCaptureStars(this.scene, this.pokeball);
2024-05-24 00:45:04 +01:00
const pbTint = this.scene.add.sprite(this.pokeball.x, this.pokeball.y, "pb", "pb");
pbTint.setOrigin(this.pokeball.originX, this.pokeball.originY);
pbTint.setTintFill(0);
pbTint.setAlpha(0);
this.scene.field.add(pbTint);
this.scene.tweens.add({
targets: pbTint,
alpha: 0.375,
duration: 200,
easing: "Sine.easeOut",
onComplete: () => {
this.scene.tweens.add({
targets: pbTint,
alpha: 0,
duration: 200,
easing: "Sine.easeIn",
onComplete: () => pbTint.destroy()
});
}
});
}
2023-04-10 19:12:01 +01:00
},
2023-04-21 00:44:56 +01:00
onComplete: () => this.catch()
2023-04-10 19:12:01 +01:00
});
};
2023-04-10 19:12:01 +01:00
this.scene.time.delayedCall(250, () => doPokeballBounceAnim(this.scene, this.pokeball, 16, 72, 350, doShake));
}
});
}
});
}
2023-04-11 06:31:18 +01:00
failCatch(shakeCount: integer) {
const pokemon = this.getPokemon();
2023-04-10 19:12:01 +01:00
this.scene.playSound("pb_rel");
2023-04-10 19:12:01 +01:00
pokemon.setY(this.originalY);
if (pokemon.status?.effect !== StatusEffect.SLEEP) {
pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 });
}
2023-04-10 19:12:01 +01:00
pokemon.tint(getPokeballTintColor(this.pokeballType));
pokemon.setVisible(true);
pokemon.untint(250, "Sine.easeOut");
2023-04-10 19:12:01 +01:00
const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType);
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`));
2023-04-10 19:12:01 +01:00
this.scene.tweens.add({
targets: pokemon,
duration: 250,
ease: "Sine.easeOut",
2023-04-10 19:12:01 +01:00
scale: 1
});
2024-05-24 00:45:04 +01:00
this.scene.currentBattle.lastUsedPokeball = this.pokeballType;
2023-04-10 19:12:01 +01:00
this.removePb();
this.end();
}
catch() {
const pokemon = this.getPokemon() as EnemyPokemon;
2023-05-29 17:24:38 +01:00
this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex));
2023-12-06 04:31:34 +00:00
const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm();
if (speciesForm.abilityHidden && (pokemon.fusionSpecies ? pokemon.fusionAbilityIndex : pokemon.abilityIndex) === speciesForm.getAbilityCount() - 1) {
this.scene.validateAchv(achvs.HIDDEN_ABILITY);
}
if (pokemon.species.subLegendary) {
this.scene.validateAchv(achvs.CATCH_SUB_LEGENDARY);
}
if (pokemon.species.legendary) {
this.scene.validateAchv(achvs.CATCH_LEGENDARY);
}
if (pokemon.species.mythical) {
this.scene.validateAchv(achvs.CATCH_MYTHICAL);
}
2024-03-08 03:43:15 +00:00
this.scene.pokemonInfoContainer.show(pokemon, true);
this.scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs);
2024-05-24 00:45:04 +01:00
this.scene.ui.showText(i18next.t("battle:pokemonCaught", { pokemonName: pokemon.name }), null, () => {
2023-04-10 21:52:27 +01:00
const end = () => {
2024-03-08 03:43:15 +00:00
this.scene.pokemonInfoContainer.hide();
2023-04-10 19:12:01 +01:00
this.removePb();
this.end();
2023-04-10 21:52:27 +01:00
};
const removePokemon = () => {
2024-03-17 15:36:19 +00:00
this.scene.addFaintedEnemyScore(pokemon);
2024-02-18 15:53:32 +00:00
this.scene.getPlayerField().filter(p => p.isActive(true)).forEach(playerPokemon => playerPokemon.removeTagsBySourceId(pokemon.id));
pokemon.hp = 0;
pokemon.trySetStatus(StatusEffect.FAINT);
this.scene.clearEnemyHeldItemModifiers();
this.scene.field.remove(pokemon, true);
};
2023-04-11 06:31:18 +01:00
const addToParty = () => {
const newPokemon = pokemon.addToParty(this.pokeballType);
2023-04-21 00:44:56 +01:00
const modifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false);
if (this.scene.getParty().filter(p => p.isShiny()).length === 6) {
this.scene.validateAchv(achvs.SHINY_PARTY);
}
Promise.all(modifiers.map(m => this.scene.addModifier(m, true))).then(() => {
this.scene.updateModifiers(true);
removePokemon();
if (newPokemon) {
2023-04-21 00:44:56 +01:00
newPokemon.loadAssets().then(end);
} else {
2023-04-21 00:44:56 +01:00
end();
}
2023-04-21 00:44:56 +01:00
});
2023-04-11 06:31:18 +01:00
};
Promise.all([ pokemon.hideInfo(), this.scene.gameData.setPokemonCaught(pokemon) ]).then(() => {
2023-04-18 06:32:26 +01:00
if (this.scene.getParty().length === 6) {
const promptRelease = () => {
this.scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.name }), null, () => {
this.scene.pokemonInfoContainer.makeRoomForConfirmUi();
2023-04-18 06:32:26 +01:00
this.scene.ui.setMode(Mode.CONFIRM, () => {
this.scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, this.fieldIndex, (slotIndex: integer, _option: PartyOption) => {
2023-04-18 06:32:26 +01:00
this.scene.ui.setMode(Mode.MESSAGE).then(() => {
if (slotIndex < 6) {
2023-04-18 06:32:26 +01:00
addToParty();
} else {
2023-04-18 06:32:26 +01:00
promptRelease();
}
2023-04-18 06:32:26 +01:00
});
2023-04-11 06:31:18 +01:00
});
2023-04-18 06:32:26 +01:00
}, () => {
this.scene.ui.setMode(Mode.MESSAGE).then(() => {
removePokemon();
end();
});
2023-04-11 06:31:18 +01:00
});
});
2023-04-18 06:32:26 +01:00
};
promptRelease();
} else {
2023-04-18 06:32:26 +01:00
addToParty();
}
2023-04-18 06:32:26 +01:00
});
2023-04-10 19:12:01 +01:00
}, 0, true);
}
removePb() {
this.scene.tweens.add({
targets: this.pokeball,
duration: 250,
delay: 250,
ease: "Sine.easeIn",
2023-04-10 19:12:01 +01:00
alpha: 0,
onComplete: () => this.pokeball.destroy()
});
}
}
export class AttemptRunPhase extends PokemonPhase {
constructor(scene: BattleScene, fieldIndex: integer) {
super(scene, fieldIndex);
2023-05-07 22:05:19 +01:00
}
start() {
super.start();
const playerPokemon = this.getPokemon();
const enemyField = this.scene.getEnemyField();
const enemySpeed = enemyField.reduce((total: integer, enemyPokemon: Pokemon) => total + enemyPokemon.getStat(Stat.SPD), 0) / enemyField.length;
2023-05-07 22:05:19 +01:00
2023-12-22 22:08:37 +00:00
const escapeChance = new Utils.IntegerHolder((((playerPokemon.getStat(Stat.SPD) * 128) / enemySpeed) + (30 * this.scene.currentBattle.escapeAttempts++)) % 256);
applyAbAttrs(RunSuccessAbAttr, playerPokemon, null, escapeChance);
2023-05-07 22:05:19 +01:00
if (playerPokemon.randSeedInt(256) < escapeChance.value) {
this.scene.playSound("flee");
this.scene.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500);
2024-05-24 00:45:04 +01:00
2023-05-07 22:05:19 +01:00
this.scene.tweens.add({
targets: [ this.scene.arenaEnemy, enemyField ].flat(),
2023-05-07 22:05:19 +01:00
alpha: 0,
duration: 250,
ease: "Sine.easeIn",
2023-10-31 18:09:33 +00:00
onComplete: () => enemyField.forEach(enemyPokemon => enemyPokemon.destroy())
2023-05-07 22:05:19 +01:00
});
this.scene.clearEnemyHeldItemModifiers();
2023-06-01 18:54:52 +01:00
enemyField.forEach(enemyPokemon => {
2023-10-31 18:09:33 +00:00
enemyPokemon.hideInfo().then(() => enemyPokemon.destroy());
2023-06-01 18:54:52 +01:00
enemyPokemon.hp = 0;
2023-11-04 04:32:12 +00:00
enemyPokemon.trySetStatus(StatusEffect.FAINT);
2023-06-01 18:54:52 +01:00
});
2023-05-07 22:05:19 +01:00
this.scene.pushPhase(new BattleEndPhase(this.scene));
this.scene.pushPhase(new NewBattlePhase(this.scene));
} else {
this.scene.queueMessage(i18next.t("battle:runAwayCannotEscape"), null, true, 500);
}
2023-05-07 22:05:19 +01:00
this.end();
}
}
2023-04-10 19:12:01 +01:00
export class SelectModifierPhase extends BattlePhase {
private rerollCount: integer;
private modifierTiers: ModifierTier[];
constructor(scene: BattleScene, rerollCount: integer = 0, modifierTiers?: ModifierTier[]) {
2023-04-10 19:12:01 +01:00
super(scene);
this.rerollCount = rerollCount;
this.modifierTiers = modifierTiers;
2023-04-10 19:12:01 +01:00
}
start() {
super.start();
if (!this.rerollCount) {
this.updateSeed();
2024-05-30 18:45:30 +01:00
} else {
this.scene.reroll = false;
}
2023-04-10 19:12:01 +01:00
const party = this.scene.getParty();
regenerateModifierPoolThresholds(party, this.getPoolType(), this.rerollCount);
2023-04-10 19:12:01 +01:00
const modifierCount = new Utils.IntegerHolder(3);
if (this.isPlayer()) {
this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount);
}
const typeOptions: ModifierTypeOption[] = this.getModifierTypeOptions(modifierCount.value);
2023-04-10 19:12:01 +01:00
const modifierSelectCallback = (rowCursor: integer, cursor: integer) => {
if (rowCursor < 0 || cursor < 0) {
this.scene.ui.showText(i18next.t("battle:skipItemQuestion"), null, () => {
2024-03-24 01:03:00 +00:00
this.scene.ui.setOverlayMode(Mode.CONFIRM, () => {
this.scene.ui.revertMode();
this.scene.ui.setMode(Mode.MESSAGE);
super.end();
}, () => this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)));
2024-03-24 01:03:00 +00:00
});
return false;
2023-04-10 19:12:01 +01:00
}
let modifierType: ModifierType;
let cost: integer;
switch (rowCursor) {
case 0:
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
switch (cursor) {
case 0:
const rerollCost = this.getRerollCost(typeOptions, this.scene.lockModifierTiers);
if (this.scene.money < rerollCost) {
this.scene.ui.playError();
return false;
} else {
2024-05-30 18:45:30 +01:00
this.scene.reroll = true;
this.scene.unshiftPhase(new SelectModifierPhase(this.scene, this.rerollCount + 1, typeOptions.map(o => o.type.tier)));
this.scene.ui.clearText();
this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end());
this.scene.money -= rerollCost;
this.scene.updateMoneyText();
this.scene.animateMoneyChanged(false);
this.scene.playSound("buy");
}
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
break;
case 1:
this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER, -1, (fromSlotIndex: integer, itemIndex: integer, itemQuantity: integer, toSlotIndex: integer) => {
if (toSlotIndex !== undefined && fromSlotIndex < 6 && toSlotIndex < 6 && fromSlotIndex !== toSlotIndex && itemIndex > -1) {
const itemModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
&& (m as PokemonHeldItemModifier).getTransferrable(true) && (m as PokemonHeldItemModifier).pokemonId === party[fromSlotIndex].id) as PokemonHeldItemModifier[];
const itemModifier = itemModifiers[itemIndex];
this.scene.tryTransferHeldItemModifier(itemModifier, party[toSlotIndex], true, itemQuantity);
} else {
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers));
}
}, PartyUiHandler.FilterItemMaxStacks);
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 06:07:23 +01:00
break;
case 2:
this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.CHECK, -1, () => {
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers));
});
break;
case 3:
this.scene.lockModifierTiers = !this.scene.lockModifierTiers;
const uiHandler = this.scene.ui.getHandler() as ModifierSelectUiHandler;
uiHandler.setRerollCost(this.getRerollCost(typeOptions, this.scene.lockModifierTiers));
uiHandler.updateLockRaritiesText();
uiHandler.updateRerollCostText();
return false;
}
return true;
case 1:
modifierType = typeOptions[cursor].type;
break;
default:
const shopOptions = getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, this.scene.getWaveMoneyAmount(1));
const shopOption = shopOptions[rowCursor > 2 || shopOptions.length <= SHOP_OPTIONS_ROW_LIMIT ? cursor : cursor + SHOP_OPTIONS_ROW_LIMIT];
modifierType = shopOption.type;
cost = shopOption.cost;
break;
}
if (cost && this.scene.money < cost) {
this.scene.ui.playError();
return false;
}
const applyModifier = (modifier: Modifier, playSound: boolean = false) => {
const result = this.scene.addModifier(modifier, false, playSound);
2024-03-14 21:15:01 +00:00
if (cost) {
result.then(success => {
if (success) {
this.scene.money -= cost;
this.scene.updateMoneyText();
this.scene.animateMoneyChanged(false);
this.scene.playSound("buy");
(this.scene.ui.getHandler() as ModifierSelectUiHandler).updateCostText();
} else {
this.scene.ui.playError();
}
});
2024-03-14 21:15:01 +00:00
} else {
const doEnd = () => {
this.scene.ui.clearText();
this.scene.ui.setMode(Mode.MESSAGE);
super.end();
};
if (result instanceof Promise) {
result.then(() => doEnd());
} else {
doEnd();
}
2024-03-14 21:15:01 +00:00
}
};
2023-04-10 19:12:01 +01:00
if (modifierType instanceof PokemonModifierType) {
2023-11-04 04:32:12 +00:00
if (modifierType instanceof FusePokemonModifierType) {
this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.SPLICE, -1, (fromSlotIndex: integer, spliceSlotIndex: integer) => {
if (spliceSlotIndex !== undefined && fromSlotIndex < 6 && spliceSlotIndex < 6 && fromSlotIndex !== spliceSlotIndex) {
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer()).then(() => {
const modifier = modifierType.newModifier(party[fromSlotIndex], party[spliceSlotIndex]);
applyModifier(modifier, true);
2023-11-04 04:32:12 +00:00
});
} else {
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers));
}
2023-11-04 04:32:12 +00:00
}, modifierType.selectFilter);
} else {
const pokemonModifierType = modifierType as PokemonModifierType;
const isMoveModifier = modifierType instanceof PokemonMoveModifierType;
const isTmModifier = modifierType instanceof TmModifierType;
const isRememberMoveModifier = modifierType instanceof RememberMoveModifierType;
const isPpRestoreModifier = (modifierType instanceof PokemonPpRestoreModifierType || modifierType instanceof PokemonPpUpModifierType);
2023-11-04 04:32:12 +00:00
const partyUiMode = isMoveModifier ? PartyUiMode.MOVE_MODIFIER
: isTmModifier ? PartyUiMode.TM_MODIFIER
: isRememberMoveModifier ? PartyUiMode.REMEMBER_MOVE_MODIFIER
: PartyUiMode.MODIFIER;
2023-11-04 04:32:12 +00:00
const tmMoveId = isTmModifier
? (modifierType as TmModifierType).moveId
: undefined;
this.scene.ui.setModeWithoutClear(Mode.PARTY, partyUiMode, -1, (slotIndex: integer, option: PartyOption) => {
if (slotIndex < 6) {
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer()).then(() => {
const modifier = !isMoveModifier
? !isRememberMoveModifier
? modifierType.newModifier(party[slotIndex])
: modifierType.newModifier(party[slotIndex], option as integer)
2023-11-04 04:32:12 +00:00
: modifierType.newModifier(party[slotIndex], option - PartyOption.MOVE_1);
applyModifier(modifier, true);
2023-11-04 04:32:12 +00:00
});
} else {
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers));
}
}, pokemonModifierType.selectFilter, modifierType instanceof PokemonMoveModifierType ? (modifierType as PokemonMoveModifierType).moveSelectFilter : undefined, tmMoveId, isPpRestoreModifier);
2023-11-04 04:32:12 +00:00
}
} else {
applyModifier(modifierType.newModifier());
}
return !cost;
2023-04-10 19:12:01 +01:00
};
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers));
}
updateSeed(): void {
this.scene.resetSeed();
}
isPlayer(): boolean {
return true;
}
getRerollCost(typeOptions: ModifierTypeOption[], lockRarities: boolean): integer {
let baseValue = 0;
if (lockRarities) {
const tierValues = [ 50, 125, 300, 750, 2000 ];
for (const opt of typeOptions) {
baseValue += tierValues[opt.type.tier];
}
} else {
baseValue = 250;
}
2024-04-26 18:05:48 +01:00
return Math.min(Math.ceil(this.scene.currentBattle.waveIndex / 10) * baseValue * Math.pow(2, this.rerollCount), Number.MAX_SAFE_INTEGER);
}
2024-05-24 00:45:04 +01:00
getPoolType(): ModifierPoolType {
return ModifierPoolType.PLAYER;
}
getModifierTypeOptions(modifierCount: integer): ModifierTypeOption[] {
return getPlayerModifierTypeOptions(modifierCount, this.scene.getParty(), this.scene.lockModifierTiers ? this.modifierTiers : undefined);
}
2024-04-16 20:22:19 +01:00
addModifier(modifier: Modifier): Promise<boolean> {
return this.scene.addModifier(modifier, false, true);
}
}
export class EggLapsePhase extends Phase {
constructor(scene: BattleScene) {
super(scene);
}
start() {
super.start();
const eggsToHatch: Egg[] = this.scene.gameData.eggs.filter((egg: Egg) => {
return Overrides.IMMEDIATE_HATCH_EGGS_OVERRIDE ? true : --egg.hatchWaves < 1;
});
let eggCount: integer = eggsToHatch.length;
if (eggCount) {
this.scene.queueMessage(i18next.t("battle:eggHatching"));
2024-05-24 00:45:04 +01:00
for (const egg of eggsToHatch) {
this.scene.unshiftPhase(new EggHatchPhase(this.scene, egg, eggCount));
if (eggCount > 0) {
eggCount--;
}
}
2024-05-24 00:45:04 +01:00
}
this.end();
}
}
export class AddEnemyBuffModifierPhase extends Phase {
constructor(scene: BattleScene) {
super(scene);
}
start() {
super.start();
const waveIndex = this.scene.currentBattle.waveIndex;
const tier = !(waveIndex % 1000) ? ModifierTier.ULTRA : !(waveIndex % 250) ? ModifierTier.GREAT : ModifierTier.COMMON;
regenerateModifierPoolThresholds(this.scene.getEnemyParty(), ModifierPoolType.ENEMY_BUFF);
2024-05-24 00:45:04 +01:00
const count = Math.ceil(waveIndex / 250);
for (let i = 0; i < count; i++) {
this.scene.addEnemyModifier(getEnemyBuffModifierForWave(tier, this.scene.findModifiers(m => m instanceof EnemyPersistentModifier, false), this.scene), true, true);
}
this.scene.updateModifiers(false, true).then(() => this.end());
2023-04-10 19:12:01 +01:00
}
}
/**
* Cures the party of all non-volatile status conditions, shows a message
* @param {BattleScene} scene The current scene
* @param {Pokemon} user The user of the move that cures the party
* @param {string} message The message that should be displayed
* @param {Abilities} abilityCondition Pokemon with this ability will not be affected ie. Soundproof
*/
export class PartyStatusCurePhase extends BattlePhase {
private user: Pokemon;
private message: string;
private abilityCondition: Abilities;
constructor(scene: BattleScene, user: Pokemon, message: string, abilityCondition: Abilities) {
super(scene);
this.user = user;
this.message = message;
this.abilityCondition = abilityCondition;
}
start() {
super.start();
for (const pokemon of this.scene.getParty()) {
if (!pokemon.isOnField() || pokemon === this.user) {
pokemon.resetStatus(false);
pokemon.updateInfo(true);
} else {
if (!pokemon.hasAbility(this.abilityCondition)) {
pokemon.resetStatus();
pokemon.updateInfo(true);
} else {
// Manually show ability bar, since we're not hooked into the targeting system
pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.id, pokemon.getPassiveAbility()?.id === this.abilityCondition));
}
}
}
if (this.message) {
this.scene.queueMessage(this.message);
}
this.end();
}
}
export class PartyHealPhase extends BattlePhase {
private resumeBgm: boolean;
constructor(scene: BattleScene, resumeBgm: boolean) {
super(scene);
this.resumeBgm = resumeBgm;
}
start() {
super.start();
const bgmPlaying = this.scene.isBgmPlaying();
if (bgmPlaying) {
this.scene.fadeOutBgm(1000, false);
}
this.scene.ui.fadeOut(1000).then(() => {
for (const pokemon of this.scene.getParty()) {
pokemon.hp = pokemon.getMaxHp();
pokemon.resetStatus();
for (const move of pokemon.moveset) {
move.ppUsed = 0;
}
pokemon.updateInfo(true);
}
const healSong = this.scene.playSoundWithoutBgm("heal");
2024-04-04 22:43:37 +01:00
this.scene.time.delayedCall(Utils.fixedInt(healSong.totalDuration * 1000), () => {
healSong.destroy();
if (this.resumeBgm && bgmPlaying) {
this.scene.playBgm();
}
this.scene.ui.fadeIn(500).then(() => this.end());
});
});
}
}
2023-04-10 19:12:01 +01:00
export class ShinySparklePhase extends PokemonPhase {
constructor(scene: BattleScene, battlerIndex: BattlerIndex) {
super(scene, battlerIndex);
2023-04-10 19:12:01 +01:00
}
start() {
super.start();
this.getPokemon().sparkle();
this.scene.time.delayedCall(1000, () => this.end());
}
}
2023-11-12 17:49:06 +00:00
export class ScanIvsPhase extends PokemonPhase {
private shownIvs: integer;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, shownIvs: integer) {
super(scene, battlerIndex);
this.shownIvs = shownIvs;
}
start() {
super.start();
if (!this.shownIvs) {
2023-11-12 17:49:06 +00:00
return this.end();
}
2023-11-12 17:49:06 +00:00
const pokemon = this.getPokemon();
this.scene.ui.showText(i18next.t("battle:ivScannerUseQuestion", { pokemonName: pokemon.name }), null, () => {
2023-11-12 17:49:06 +00:00
this.scene.ui.setMode(Mode.CONFIRM, () => {
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.clearText();
new CommonBattleAnim(CommonAnim.LOCK_ON, pokemon, pokemon).play(this.scene, () => {
this.scene.ui.getMessageHandler().promptIvs(pokemon.id, pokemon.ivs, this.shownIvs).then(() => this.end());
});
}, () => {
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.clearText();
this.end();
});
});
}
}
2023-12-12 02:46:49 +00:00
export class TrainerMessageTestPhase extends BattlePhase {
2024-02-06 21:15:35 +00:00
private trainerTypes: TrainerType[];
constructor(scene: BattleScene, ...trainerTypes: TrainerType[]) {
2023-12-12 02:46:49 +00:00
super(scene);
2024-05-24 00:45:04 +01:00
2024-02-06 21:15:35 +00:00
this.trainerTypes = trainerTypes;
2023-12-12 02:46:49 +00:00
}
start() {
super.start();
const testMessages: string[] = [];
2024-05-24 00:45:04 +01:00
for (const t of Object.keys(trainerConfigs)) {
2024-02-06 21:15:35 +00:00
const type = parseInt(t);
if (this.trainerTypes.length && !this.trainerTypes.find(tt => tt === type as TrainerType)) {
2024-02-06 21:15:35 +00:00
continue;
}
2024-02-06 21:15:35 +00:00
const config = trainerConfigs[type];
2023-12-12 02:46:49 +00:00
[ config.encounterMessages, config.femaleEncounterMessages, config.victoryMessages, config.femaleVictoryMessages, config.defeatMessages, config.femaleDefeatMessages ]
.map(messages => {
if (messages?.length) {
2023-12-12 02:46:49 +00:00
testMessages.push(...messages);
}
2023-12-12 02:46:49 +00:00
});
}
for (const message of testMessages) {
2023-12-12 02:46:49 +00:00
this.scene.pushPhase(new TestMessagePhase(this.scene, message));
}
2023-12-12 02:46:49 +00:00
this.end();
}
}
export class TestMessagePhase extends MessagePhase {
constructor(scene: BattleScene, message: string) {
super(scene, message, null, true);
}
}