This commit is contained in:
AJ Fontaine 2025-02-21 12:01:48 -05:00
commit 009348d13d
65 changed files with 3650 additions and 1691 deletions

View File

@ -170,7 +170,7 @@ input:-internal-autofill-selected {
#touchControls:not(.config-mode):not([data-ui-mode='STARTER_SELECT']) #apadCycleNature, #touchControls:not(.config-mode):not([data-ui-mode='STARTER_SELECT']) #apadCycleNature,
#touchControls:not(.config-mode):not([data-ui-mode='STARTER_SELECT'], [data-ui-mode='POKEDEX_PAGE'], [data-ui-mode='RUN_INFO']) #apadCycleAbility, #touchControls:not(.config-mode):not([data-ui-mode='STARTER_SELECT'], [data-ui-mode='POKEDEX_PAGE'], [data-ui-mode='RUN_INFO']) #apadCycleAbility,
#touchControls:not(.config-mode):not([data-ui-mode='STARTER_SELECT'], [data-ui-mode='POKEDEX_PAGE']) #apadCycleGender, #touchControls:not(.config-mode):not([data-ui-mode='STARTER_SELECT'], [data-ui-mode='POKEDEX_PAGE']) #apadCycleGender,
#touchControls:not(.config-mode):not([data-ui-mode='STARTER_SELECT'], [data-ui-mode='POKEDEX']) #apadCycleVariant { #touchControls:not(.config-mode):not([data-ui-mode='STARTER_SELECT'], [data-ui-mode='POKEDEX']) #apadCycleTera {
display: none; display: none;
} }

View File

@ -129,7 +129,7 @@
<div id="apadCycleNature" class="apad-button apad-square apad-small" data-key="CYCLE_NATURE"> <div id="apadCycleNature" class="apad-button apad-square apad-small" data-key="CYCLE_NATURE">
<span class="apad-label">N</span> <span class="apad-label">N</span>
</div> </div>
<div id="apadCycleVariant" class="apad-button apad-square apad-small" data-key="V"> <div id="apadCycleTera" class="apad-button apad-square apad-small" data-key="CYCLE_TERA">
<span class="apad-label">V</span> <span class="apad-label">V</span>
</div> </div>
<!-- buttons to display battle-specific information --> <!-- buttons to display battle-specific information -->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

@ -1 +1 @@
Subproject commit 58dda14ee834204c4bd5ece47694a3c068df4b0e Subproject commit ef43efffe5fe454862c350f1b9393c3ad755bcc2

File diff suppressed because it is too large Load Diff

View File

@ -180,9 +180,9 @@ const cfg_keyboard_qwerty = {
[SettingKeyboard.Button_Cycle_Gender]: Button.CYCLE_GENDER, [SettingKeyboard.Button_Cycle_Gender]: Button.CYCLE_GENDER,
[SettingKeyboard.Button_Cycle_Ability]: Button.CYCLE_ABILITY, [SettingKeyboard.Button_Cycle_Ability]: Button.CYCLE_ABILITY,
[SettingKeyboard.Button_Cycle_Nature]: Button.CYCLE_NATURE, [SettingKeyboard.Button_Cycle_Nature]: Button.CYCLE_NATURE,
[SettingKeyboard.Button_Cycle_Variant]: Button.V,
[SettingKeyboard.Button_Speed_Up]: Button.SPEED_UP, [SettingKeyboard.Button_Speed_Up]: Button.SPEED_UP,
[SettingKeyboard.Button_Slow_Down]: Button.SLOW_DOWN, [SettingKeyboard.Button_Slow_Down]: Button.SLOW_DOWN,
[SettingKeyboard.Button_Cycle_Tera]: Button.CYCLE_TERA,
[SettingKeyboard.Alt_Button_Up]: Button.UP, [SettingKeyboard.Alt_Button_Up]: Button.UP,
[SettingKeyboard.Alt_Button_Down]: Button.DOWN, [SettingKeyboard.Alt_Button_Down]: Button.DOWN,
[SettingKeyboard.Alt_Button_Left]: Button.LEFT, [SettingKeyboard.Alt_Button_Left]: Button.LEFT,
@ -197,9 +197,9 @@ const cfg_keyboard_qwerty = {
[SettingKeyboard.Alt_Button_Cycle_Gender]: Button.CYCLE_GENDER, [SettingKeyboard.Alt_Button_Cycle_Gender]: Button.CYCLE_GENDER,
[SettingKeyboard.Alt_Button_Cycle_Ability]: Button.CYCLE_ABILITY, [SettingKeyboard.Alt_Button_Cycle_Ability]: Button.CYCLE_ABILITY,
[SettingKeyboard.Alt_Button_Cycle_Nature]: Button.CYCLE_NATURE, [SettingKeyboard.Alt_Button_Cycle_Nature]: Button.CYCLE_NATURE,
[SettingKeyboard.Alt_Button_Cycle_Variant]: Button.V,
[SettingKeyboard.Alt_Button_Speed_Up]: Button.SPEED_UP, [SettingKeyboard.Alt_Button_Speed_Up]: Button.SPEED_UP,
[SettingKeyboard.Alt_Button_Slow_Down]: Button.SLOW_DOWN, [SettingKeyboard.Alt_Button_Slow_Down]: Button.SLOW_DOWN,
[SettingKeyboard.Alt_Button_Cycle_Tera]: Button.CYCLE_TERA,
}, },
default: { default: {
KEY_ARROW_UP: SettingKeyboard.Button_Up, KEY_ARROW_UP: SettingKeyboard.Button_Up,
@ -216,7 +216,7 @@ const cfg_keyboard_qwerty = {
KEY_G: SettingKeyboard.Button_Cycle_Gender, KEY_G: SettingKeyboard.Button_Cycle_Gender,
KEY_E: SettingKeyboard.Button_Cycle_Ability, KEY_E: SettingKeyboard.Button_Cycle_Ability,
KEY_N: SettingKeyboard.Button_Cycle_Nature, KEY_N: SettingKeyboard.Button_Cycle_Nature,
KEY_V: SettingKeyboard.Button_Cycle_Variant, KEY_V: SettingKeyboard.Button_Cycle_Tera,
KEY_PLUS: -1, KEY_PLUS: -1,
KEY_MINUS: -1, KEY_MINUS: -1,
KEY_A: SettingKeyboard.Alt_Button_Left, KEY_A: SettingKeyboard.Alt_Button_Left,

View File

@ -53,7 +53,7 @@ const pad_dualshock = {
[SettingGamepad.Button_Action]: Button.ACTION, [SettingGamepad.Button_Action]: Button.ACTION,
[SettingGamepad.Button_Cancel]: Button.CANCEL, [SettingGamepad.Button_Cancel]: Button.CANCEL,
[SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE, [SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE,
[SettingGamepad.Button_Cycle_Variant]: Button.V, [SettingGamepad.Button_Cycle_Tera]: Button.CYCLE_TERA,
[SettingGamepad.Button_Menu]: Button.MENU, [SettingGamepad.Button_Menu]: Button.MENU,
[SettingGamepad.Button_Stats]: Button.STATS, [SettingGamepad.Button_Stats]: Button.STATS,
[SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM, [SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM,
@ -72,7 +72,7 @@ const pad_dualshock = {
RC_S: SettingGamepad.Button_Action, RC_S: SettingGamepad.Button_Action,
RC_E: SettingGamepad.Button_Cancel, RC_E: SettingGamepad.Button_Cancel,
RC_W: SettingGamepad.Button_Cycle_Nature, RC_W: SettingGamepad.Button_Cycle_Nature,
RC_N: SettingGamepad.Button_Cycle_Variant, RC_N: SettingGamepad.Button_Cycle_Tera,
START: SettingGamepad.Button_Menu, START: SettingGamepad.Button_Menu,
SELECT: SettingGamepad.Button_Stats, SELECT: SettingGamepad.Button_Stats,
LB: SettingGamepad.Button_Cycle_Form, LB: SettingGamepad.Button_Cycle_Form,

View File

@ -51,7 +51,7 @@ const pad_generic = {
[SettingGamepad.Button_Action]: Button.ACTION, [SettingGamepad.Button_Action]: Button.ACTION,
[SettingGamepad.Button_Cancel]: Button.CANCEL, [SettingGamepad.Button_Cancel]: Button.CANCEL,
[SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE, [SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE,
[SettingGamepad.Button_Cycle_Variant]: Button.V, [SettingGamepad.Button_Cycle_Tera]: Button.CYCLE_TERA,
[SettingGamepad.Button_Menu]: Button.MENU, [SettingGamepad.Button_Menu]: Button.MENU,
[SettingGamepad.Button_Stats]: Button.STATS, [SettingGamepad.Button_Stats]: Button.STATS,
[SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM, [SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM,
@ -69,7 +69,7 @@ const pad_generic = {
RC_S: SettingGamepad.Button_Action, RC_S: SettingGamepad.Button_Action,
RC_E: SettingGamepad.Button_Cancel, RC_E: SettingGamepad.Button_Cancel,
RC_W: SettingGamepad.Button_Cycle_Nature, RC_W: SettingGamepad.Button_Cycle_Nature,
RC_N: SettingGamepad.Button_Cycle_Variant, RC_N: SettingGamepad.Button_Cycle_Tera,
START: SettingGamepad.Button_Menu, START: SettingGamepad.Button_Menu,
SELECT: SettingGamepad.Button_Stats, SELECT: SettingGamepad.Button_Stats,
LB: SettingGamepad.Button_Cycle_Form, LB: SettingGamepad.Button_Cycle_Form,

View File

@ -52,7 +52,7 @@ const pad_procon = {
[SettingGamepad.Button_Action]: Button.ACTION, [SettingGamepad.Button_Action]: Button.ACTION,
[SettingGamepad.Button_Cancel]: Button.CANCEL, [SettingGamepad.Button_Cancel]: Button.CANCEL,
[SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE, [SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE,
[SettingGamepad.Button_Cycle_Variant]: Button.V, [SettingGamepad.Button_Cycle_Tera]: Button.CYCLE_TERA,
[SettingGamepad.Button_Menu]: Button.MENU, [SettingGamepad.Button_Menu]: Button.MENU,
[SettingGamepad.Button_Stats]: Button.STATS, [SettingGamepad.Button_Stats]: Button.STATS,
[SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM, [SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM,
@ -70,7 +70,7 @@ const pad_procon = {
RC_S: SettingGamepad.Button_Action, RC_S: SettingGamepad.Button_Action,
RC_E: SettingGamepad.Button_Cancel, RC_E: SettingGamepad.Button_Cancel,
RC_W: SettingGamepad.Button_Cycle_Nature, RC_W: SettingGamepad.Button_Cycle_Nature,
RC_N: SettingGamepad.Button_Cycle_Variant, RC_N: SettingGamepad.Button_Cycle_Tera,
START: SettingGamepad.Button_Menu, START: SettingGamepad.Button_Menu,
SELECT: SettingGamepad.Button_Stats, SELECT: SettingGamepad.Button_Stats,
LB: SettingGamepad.Button_Cycle_Form, LB: SettingGamepad.Button_Cycle_Form,

View File

@ -43,7 +43,7 @@ const pad_unlicensedSNES = {
[SettingGamepad.Button_Action]: Button.ACTION, [SettingGamepad.Button_Action]: Button.ACTION,
[SettingGamepad.Button_Cancel]: Button.CANCEL, [SettingGamepad.Button_Cancel]: Button.CANCEL,
[SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE, [SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE,
[SettingGamepad.Button_Cycle_Variant]: Button.V, [SettingGamepad.Button_Cycle_Tera]: Button.CYCLE_TERA,
[SettingGamepad.Button_Menu]: Button.MENU, [SettingGamepad.Button_Menu]: Button.MENU,
[SettingGamepad.Button_Stats]: Button.STATS, [SettingGamepad.Button_Stats]: Button.STATS,
[SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM, [SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM,
@ -61,7 +61,7 @@ const pad_unlicensedSNES = {
RC_S: SettingGamepad.Button_Action, RC_S: SettingGamepad.Button_Action,
RC_E: SettingGamepad.Button_Cancel, RC_E: SettingGamepad.Button_Cancel,
RC_W: SettingGamepad.Button_Cycle_Nature, RC_W: SettingGamepad.Button_Cycle_Nature,
RC_N: SettingGamepad.Button_Cycle_Variant, RC_N: SettingGamepad.Button_Cycle_Tera,
START: SettingGamepad.Button_Menu, START: SettingGamepad.Button_Menu,
SELECT: SettingGamepad.Button_Stats, SELECT: SettingGamepad.Button_Stats,
LB: SettingGamepad.Button_Cycle_Form, LB: SettingGamepad.Button_Cycle_Form,

View File

@ -51,7 +51,7 @@ const pad_xbox360 = {
[SettingGamepad.Button_Action]: Button.ACTION, [SettingGamepad.Button_Action]: Button.ACTION,
[SettingGamepad.Button_Cancel]: Button.CANCEL, [SettingGamepad.Button_Cancel]: Button.CANCEL,
[SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE, [SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE,
[SettingGamepad.Button_Cycle_Variant]: Button.V, [SettingGamepad.Button_Cycle_Tera]: Button.CYCLE_TERA,
[SettingGamepad.Button_Menu]: Button.MENU, [SettingGamepad.Button_Menu]: Button.MENU,
[SettingGamepad.Button_Stats]: Button.STATS, [SettingGamepad.Button_Stats]: Button.STATS,
[SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM, [SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM,
@ -69,7 +69,7 @@ const pad_xbox360 = {
RC_S: SettingGamepad.Button_Action, RC_S: SettingGamepad.Button_Action,
RC_E: SettingGamepad.Button_Cancel, RC_E: SettingGamepad.Button_Cancel,
RC_W: SettingGamepad.Button_Cycle_Nature, RC_W: SettingGamepad.Button_Cycle_Nature,
RC_N: SettingGamepad.Button_Cycle_Variant, RC_N: SettingGamepad.Button_Cycle_Tera,
START: SettingGamepad.Button_Menu, START: SettingGamepad.Button_Menu,
SELECT: SettingGamepad.Button_Stats, SELECT: SettingGamepad.Button_Stats,
LB: SettingGamepad.Button_Cycle_Form, LB: SettingGamepad.Button_Cycle_Form,

File diff suppressed because it is too large Load Diff

View File

@ -708,6 +708,7 @@ export class FreshStartChallenge extends Challenge {
pokemon.variant = 0; // Not shiny pokemon.variant = 0; // Not shiny
pokemon.formIndex = 0; // Froakie should be base form pokemon.formIndex = 0; // Froakie should be base form
pokemon.ivs = [ 15, 15, 15, 15, 15, 15 ]; // Default IVs of 15 for all stats (Updated to 15 from 10 in 1.2.0) pokemon.ivs = [ 15, 15, 15, 15, 15, 15 ]; // Default IVs of 15 for all stats (Updated to 15 from 10 in 1.2.0)
pokemon.teraType = pokemon.species.type1; // Always primary tera type
return true; return true;
} }

View File

@ -1,5 +1,16 @@
import { ChargeAnim, initMoveAnim, loadMoveAnimAssets, MoveChargeAnim } from "./battle-anims"; import { ChargeAnim, MoveChargeAnim } from "./battle-anims";
import { CommandedTag, EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, SubstituteTag, TrappedTag, TypeBoostTag } from "./battler-tags"; import {
CommandedTag,
EncoreTag,
GulpMissileTag,
HelpingHandTag,
SemiInvulnerableTag,
ShellTrapTag,
StockpilingTag,
SubstituteTag,
TrappedTag,
TypeBoostTag,
} from "./battler-tags";
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
import type { AttackMoveResult, TurnMove } from "../field/pokemon"; import type { AttackMoveResult, TurnMove } from "../field/pokemon";
import type Pokemon from "../field/pokemon"; import type Pokemon from "../field/pokemon";
@ -30,7 +41,7 @@ import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { MoveUsedEvent } from "#app/events/battle-scene"; import { MoveUsedEvent } from "#app/events/battle-scene";
import { BATTLE_STATS, type BattleStat, EFFECTIVE_STATS, type EffectiveStat, getStatKey, Stat } from "#app/enums/stat"; import { BATTLE_STATS, type BattleStat, type EffectiveStat, getStatKey, Stat } from "#app/enums/stat";
import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase";
import { MovePhase } from "#app/phases/move-phase"; import { MovePhase } from "#app/phases/move-phase";
@ -46,6 +57,10 @@ import { applyChallenges, ChallengeType } from "./challenge";
import { SwitchType } from "#enums/switch-type"; import { SwitchType } from "#enums/switch-type";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { RevivalBlessingPhase } from "#app/phases/revival-blessing-phase";
import { LoadMoveAnimPhase } from "#app/phases/load-move-anim-phase";
import { PokemonTransformPhase } from "#app/phases/pokemon-transform-phase";
import { MoveAnimPhase } from "#app/phases/move-anim-phase";
export enum MoveCategory { export enum MoveCategory {
PHYSICAL, PHYSICAL,
@ -1057,7 +1072,7 @@ export abstract class MoveAttr {
* @param args Set of unique arguments needed by this attribute * @param args Set of unique arguments needed by this attribute
* @returns true if application of the ability succeeds * @returns true if application of the ability succeeds
*/ */
apply(user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): boolean | Promise<boolean> { apply(user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): boolean {
return true; return true;
} }
@ -1200,7 +1215,7 @@ export class MoveEffectAttr extends MoveAttr {
} }
/** Applies move effects so long as they are able based on {@linkcode canApply} */ /** Applies move effects so long as they are able based on {@linkcode canApply} */
apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean | Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
return this.canApply(user, target, move, args); return this.canApply(user, target, move, args);
} }
@ -1866,7 +1881,7 @@ export class FlameBurstAttr extends MoveEffectAttr {
* @param args - n/a * @param args - n/a
* @returns A boolean indicating whether the effect was successfully applied. * @returns A boolean indicating whether the effect was successfully applied.
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const targetAlly = target.getAlly(); const targetAlly = target.getAlly();
const cancelled = new Utils.BooleanHolder(false); const cancelled = new Utils.BooleanHolder(false);
@ -2406,32 +2421,27 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
this.chance = chance; this.chance = chance;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return new Promise<boolean>(resolve => { if (move.hitsSubstitute(user, target)) {
if (move.hitsSubstitute(user, target)) { return false;
return resolve(false); }
}
const rand = Phaser.Math.RND.realInRange(0, 1);
if (rand >= this.chance) {
return resolve(false);
}
const heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferable);
if (heldItems.length) {
const poolType = target.isPlayer() ? ModifierPoolType.PLAYER : target.hasTrainer() ? ModifierPoolType.TRAINER : ModifierPoolType.WILD;
const highestItemTier = heldItems.map(m => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is the bang after tier correct?
const tierHeldItems = heldItems.filter(m => m.type.getOrInferTier(poolType) === highestItemTier);
const stolenItem = tierHeldItems[user.randSeedInt(tierHeldItems.length)];
globalScene.tryTransferHeldItemModifier(stolenItem, user, false).then(success => {
if (success) {
globalScene.queueMessage(i18next.t("moveTriggers:stoleItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: stolenItem.type.name }));
}
resolve(success);
});
return;
}
resolve(false); const rand = Phaser.Math.RND.realInRange(0, 1);
}); if (rand >= this.chance) {
return false;
}
const heldItems = this.getTargetHeldItems(target).filter((i) => i.isTransferable);
if (heldItems.length) {
const poolType = target.isPlayer() ? ModifierPoolType.PLAYER : target.hasTrainer() ? ModifierPoolType.TRAINER : ModifierPoolType.WILD;
const highestItemTier = heldItems.map((m) => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is the bang after tier correct?
const tierHeldItems = heldItems.filter((m) => m.type.getOrInferTier(poolType) === highestItemTier);
const stolenItem = tierHeldItems[user.randSeedInt(tierHeldItems.length)];
if (globalScene.tryTransferHeldItemModifier(stolenItem, user, false)) {
globalScene.queueMessage(i18next.t("moveTriggers:stoleItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: stolenItem.type.name }));
return true;
}
}
return false;
} }
getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] { getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] {
@ -2875,9 +2885,7 @@ export class WeatherInstantChargeAttr extends InstantChargeAttr {
} }
export class OverrideMoveEffectAttr extends MoveAttr { export class OverrideMoveEffectAttr extends MoveAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
//const overridden = args[0] as Utils.BooleanHolder;
//const virtual = arg[1] as boolean;
return true; return true;
} }
} }
@ -2903,26 +2911,27 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
this.chargeText = chargeText; this.chargeText = chargeText;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
// Edge case for the move applied on a pokemon that has fainted // Edge case for the move applied on a pokemon that has fainted
if (!target) { if (!target) {
return Promise.resolve(true); return true;
} }
const side = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
return new Promise(resolve => {
if (args.length < 2 || !args[1]) {
new MoveChargeAnim(this.chargeAnim, move.id, user).play(false, () => {
(args[0] as Utils.BooleanHolder).value = true;
globalScene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER });
globalScene.arena.addTag(this.tagType, 3, move.id, user.id, side, false, target.getBattlerIndex());
resolve(true); const overridden = args[0] as Utils.BooleanHolder;
}); const virtual = args[1] as boolean;
} else {
globalScene.ui.showText(i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(target.id) ?? undefined), moveName: move.name }), null, () => resolve(true)); if (!virtual) {
} overridden.value = true;
}); globalScene.unshiftPhase(new MoveAnimPhase(new MoveChargeAnim(this.chargeAnim, move.id, user)));
globalScene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER });
const side = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
globalScene.arena.addTag(this.tagType, 3, move.id, user.id, side, false, target.getBattlerIndex());
} else {
globalScene.queueMessage(i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(target.id) ?? undefined), moveName: move.name }));
}
return true;
} }
} }
@ -3053,7 +3062,7 @@ export class StatStageChangeAttr extends MoveEffectAttr {
* @param args unused * @param args unused
* @returns whether stat stages were changed * @returns whether stat stages were changed
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean | Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
if (!super.apply(user, target, move, args) || (this.condition && !this.condition(user, target, move))) { if (!super.apply(user, target, move, args) || (this.condition && !this.condition(user, target, move))) {
return false; return false;
} }
@ -3131,7 +3140,7 @@ export class SecretPowerAttr extends MoveEffectAttr {
* Used to apply the secondary effect to the target Pokemon * Used to apply the secondary effect to the target Pokemon
* @returns `true` if a secondary effect is successfully applied * @returns `true` if a secondary effect is successfully applied
*/ */
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean | Promise<boolean> { override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
if (!super.apply(user, target, move, args)) { if (!super.apply(user, target, move, args)) {
return false; return false;
} }
@ -3286,8 +3295,8 @@ export class AcupressureStatStageChangeAttr extends MoveEffectAttr {
super(); super();
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise<boolean> { override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const randStats = BATTLE_STATS.filter(s => target.getStatStage(s) < 6); const randStats = BATTLE_STATS.filter((s) => target.getStatStage(s) < 6);
if (randStats.length > 0) { if (randStats.length > 0) {
const boostStat = [ randStats[user.randSeedInt(randStats.length)] ]; const boostStat = [ randStats[user.randSeedInt(randStats.length)] ];
globalScene.unshiftPhase(new StatStageChangePhase(target.getBattlerIndex(), this.selfTarget, boostStat, 2)); globalScene.unshiftPhase(new StatStageChangePhase(target.getBattlerIndex(), this.selfTarget, boostStat, 2));
@ -3324,17 +3333,14 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr {
this.messageCallback = messageCallback; this.messageCallback = messageCallback;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return new Promise<boolean>(resolve => { user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / this.cutRatio), HitResult.OTHER, false, true);
user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / this.cutRatio), HitResult.OTHER, false, true); user.updateInfo();
user.updateInfo().then(() => { const ret = super.apply(user, target, move, args);
const ret = super.apply(user, target, move, args); if (this.messageCallback) {
if (this.messageCallback) { this.messageCallback(user);
this.messageCallback(user); }
} return ret;
resolve(ret);
});
});
} }
getCondition(): MoveConditionFunc { getCondition(): MoveConditionFunc {
@ -3426,28 +3432,27 @@ export class ResetStatsAttr extends MoveEffectAttr {
super(); super();
this.targetAllPokemon = targetAllPokemon; this.targetAllPokemon = targetAllPokemon;
} }
async apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
const promises: Promise<void>[] = []; override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (this.targetAllPokemon) { // Target all pokemon on the field when Freezy Frost or Haze are used if (this.targetAllPokemon) {
// Target all pokemon on the field when Freezy Frost or Haze are used
const activePokemon = globalScene.getField(true); const activePokemon = globalScene.getField(true);
activePokemon.forEach(p => promises.push(this.resetStats(p))); activePokemon.forEach((p) => this.resetStats(p));
globalScene.queueMessage(i18next.t("moveTriggers:statEliminated")); globalScene.queueMessage(i18next.t("moveTriggers:statEliminated"));
} else { // Affects only the single target when Clear Smog is used } else { // Affects only the single target when Clear Smog is used
if (!move.hitsSubstitute(user, target)) { if (!move.hitsSubstitute(user, target)) {
promises.push(this.resetStats(target)); this.resetStats(target);
globalScene.queueMessage(i18next.t("moveTriggers:resetStats", { pokemonName: getPokemonNameWithAffix(target) })); globalScene.queueMessage(i18next.t("moveTriggers:resetStats", { pokemonName: getPokemonNameWithAffix(target) }));
} }
} }
await Promise.all(promises);
return true; return true;
} }
async resetStats(pokemon: Pokemon): Promise<void> { private resetStats(pokemon: Pokemon): void {
for (const s of BATTLE_STATS) { for (const s of BATTLE_STATS) {
pokemon.setStatStage(s, 0); pokemon.setStatStage(s, 0);
} }
return pokemon.updateInfo(); pokemon.updateInfo();
} }
} }
@ -3503,43 +3508,28 @@ export class SwapStatStagesAttr extends MoveEffectAttr {
} }
export class HpSplitAttr extends MoveEffectAttr { export class HpSplitAttr extends MoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return new Promise(resolve => { if (!super.apply(user, target, move, args)) {
if (!super.apply(user, target, move, args)) { return false;
return resolve(false); }
}
const infoUpdates: Promise<void>[] = []; const hpValue = Math.floor((target.hp + user.hp) / 2);
[ user, target ].forEach((p) => {
const hpValue = Math.floor((target.hp + user.hp) / 2); if (p.hp < hpValue) {
if (user.hp < hpValue) { const healing = p.heal(hpValue - p.hp);
const healing = user.heal(hpValue - user.hp);
if (healing) { if (healing) {
globalScene.damageNumberHandler.add(user, healing, HitResult.HEAL); globalScene.damageNumberHandler.add(p, healing, HitResult.HEAL);
} }
} else if (user.hp > hpValue) { } else if (p.hp > hpValue) {
const damage = user.damage(user.hp - hpValue, true); const damage = p.damage(p.hp - hpValue, true);
if (damage) { if (damage) {
globalScene.damageNumberHandler.add(user, damage); globalScene.damageNumberHandler.add(p, damage);
} }
} }
infoUpdates.push(user.updateInfo()); p.updateInfo();
if (target.hp < hpValue) {
const healing = target.heal(hpValue - target.hp);
if (healing) {
globalScene.damageNumberHandler.add(user, healing, HitResult.HEAL);
}
} else if (target.hp > hpValue) {
const damage = target.damage(target.hp - hpValue, true);
if (damage) {
globalScene.damageNumberHandler.add(target, damage);
}
}
infoUpdates.push(target.updateInfo());
return Promise.all(infoUpdates).then(() => resolve(true));
}); });
return true;
} }
} }
@ -6024,44 +6014,44 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
* @param args N/A * @param args N/A
* @returns Promise, true if function succeeds. * @returns Promise, true if function succeeds.
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return new Promise(resolve => { // If user is player, checks if the user has fainted pokemon
// If user is player, checks if the user has fainted pokemon if (user instanceof PlayerPokemon) {
if (user instanceof PlayerPokemon globalScene.unshiftPhase(new RevivalBlessingPhase(user));
&& globalScene.getPlayerParty().findIndex(p => p.isFainted()) > -1) { return true;
(user as PlayerPokemon).revivalBlessing().then(() => { } else if (user instanceof EnemyPokemon && user.hasTrainer() && globalScene.getEnemyParty().findIndex((p) => p.isFainted() && !p.isBoss()) > -1) {
resolve(true); // If used by an enemy trainer with at least one fainted non-boss Pokemon, this
}); // revives one of said Pokemon selected at random.
// If user is enemy, checks that it is a trainer, and it has fainted non-boss pokemon in party const faintedPokemon = globalScene.getEnemyParty().filter((p) => p.isFainted() && !p.isBoss());
} else if (user instanceof EnemyPokemon const pokemon = faintedPokemon[user.randSeedInt(faintedPokemon.length)];
&& user.hasTrainer() const slotIndex = globalScene.getEnemyParty().findIndex((p) => pokemon.id === p.id);
&& globalScene.getEnemyParty().findIndex(p => p.isFainted() && !p.isBoss()) > -1) { pokemon.resetStatus();
// Selects a random fainted pokemon pokemon.heal(Math.min(Utils.toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp()));
const faintedPokemon = globalScene.getEnemyParty().filter(p => p.isFainted() && !p.isBoss()); globalScene.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: getPokemonNameWithAffix(pokemon) }), 0, true);
const pokemon = faintedPokemon[user.randSeedInt(faintedPokemon.length)];
const slotIndex = globalScene.getEnemyParty().findIndex(p => pokemon.id === p.id);
pokemon.resetStatus();
pokemon.heal(Math.min(Utils.toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp()));
globalScene.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: getPokemonNameWithAffix(pokemon) }), 0, true);
if (globalScene.currentBattle.double && globalScene.getEnemyParty().length > 1) { if (globalScene.currentBattle.double && globalScene.getEnemyParty().length > 1) {
const allyPokemon = user.getAlly(); const allyPokemon = user.getAlly();
if (slotIndex <= 1) { if (slotIndex <= 1) {
globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, pokemon.getFieldIndex(), slotIndex, false, false)); globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, pokemon.getFieldIndex(), slotIndex, false, false));
} else if (allyPokemon.isFainted()) { } else if (allyPokemon.isFainted()) {
globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, false)); globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, false));
}
} }
resolve(true);
} else {
globalScene.queueMessage(i18next.t("battle:attackFailed"));
resolve(false);
} }
}); return true;
}
return false;
} }
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { getCondition(): MoveConditionFunc {
if (user.hasTrainer() && globalScene.getEnemyParty().findIndex(p => p.isFainted() && !p.isBoss()) > -1) { return (user, target, move) =>
(user instanceof PlayerPokemon && globalScene.getPlayerParty().some((p) => p.isFainted())) ||
(user instanceof EnemyPokemon &&
user.hasTrainer() &&
globalScene.getEnemyParty().some((p) => p.isFainted() && !p.isBoss()));
}
override getUserBenefitScore(user: Pokemon, _target: Pokemon, _move: Move): number {
if (user.hasTrainer() && globalScene.getEnemyParty().some((p) => p.isFainted() && !p.isBoss())) {
return 20; return 20;
} }
@ -6579,7 +6569,7 @@ export class FirstMoveTypeAttr extends MoveEffectAttr {
class CallMoveAttr extends OverrideMoveEffectAttr { class CallMoveAttr extends OverrideMoveEffectAttr {
protected invalidMoves: Moves[]; protected invalidMoves: Moves[];
protected hasTarget: boolean; protected hasTarget: boolean;
async apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const replaceMoveTarget = move.moveTarget === MoveTarget.NEAR_OTHER ? MoveTarget.NEAR_ENEMY : undefined; const replaceMoveTarget = move.moveTarget === MoveTarget.NEAR_OTHER ? MoveTarget.NEAR_ENEMY : undefined;
const moveTargets = getMoveTargets(user, move.id, replaceMoveTarget); const moveTargets = getMoveTargets(user, move.id, replaceMoveTarget);
if (moveTargets.targets.length === 0) { if (moveTargets.targets.length === 0) {
@ -6589,11 +6579,8 @@ class CallMoveAttr extends OverrideMoveEffectAttr {
? moveTargets.targets ? moveTargets.targets
: [ this.hasTarget ? target.getBattlerIndex() : moveTargets.targets[user.randSeedInt(moveTargets.targets.length)] ]; // account for Mirror Move having a target already : [ this.hasTarget ? target.getBattlerIndex() : moveTargets.targets[user.randSeedInt(moveTargets.targets.length)] ]; // account for Mirror Move having a target already
user.getMoveQueue().push({ move: move.id, targets: targets, virtual: true, ignorePP: true }); user.getMoveQueue().push({ move: move.id, targets: targets, virtual: true, ignorePP: true });
globalScene.unshiftPhase(new LoadMoveAnimPhase(move.id));
globalScene.unshiftPhase(new MovePhase(user, targets, new PokemonMove(move.id, 0, 0, true), true, true)); globalScene.unshiftPhase(new MovePhase(user, targets, new PokemonMove(move.id, 0, 0, true), true, true));
await Promise.resolve(initMoveAnim(move.id).then(() => {
loadMoveAnimAssets([ move.id ], true);
}));
return true; return true;
} }
} }
@ -6626,7 +6613,7 @@ export class RandomMoveAttr extends CallMoveAttr {
* @param move Move being used * @param move Move being used
* @param args Unused * @param args Unused
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const moveIds = Utils.getEnumValues(Moves).map(m => !this.invalidMoves.includes(m) && !allMoves[m].name.endsWith(" (N)") ? m : Moves.NONE); const moveIds = Utils.getEnumValues(Moves).map(m => !this.invalidMoves.includes(m) && !allMoves[m].name.endsWith(" (N)") ? m : Moves.NONE);
let moveId: Moves = Moves.NONE; let moveId: Moves = Moves.NONE;
do { do {
@ -6663,7 +6650,7 @@ export class RandomMovesetMoveAttr extends CallMoveAttr {
* @param move Move being used * @param move Move being used
* @param args Unused * @param args Unused
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return super.apply(user, target, allMoves[this.moveId], args); return super.apply(user, target, allMoves[this.moveId], args);
} }
@ -6965,145 +6952,141 @@ const invalidCopycatMoves = [
]; ];
export class NaturePowerAttr extends OverrideMoveEffectAttr { export class NaturePowerAttr extends OverrideMoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return new Promise(resolve => { let moveId;
let moveId; switch (globalScene.arena.getTerrainType()) {
switch (globalScene.arena.getTerrainType()) { // this allows terrains to 'override' the biome move
// this allows terrains to 'override' the biome move case TerrainType.NONE:
case TerrainType.NONE: switch (globalScene.arena.biomeType) {
switch (globalScene.arena.biomeType) { case Biome.TOWN:
case Biome.TOWN: moveId = Moves.ROUND;
moveId = Moves.ROUND; break;
break; case Biome.METROPOLIS:
case Biome.METROPOLIS: moveId = Moves.TRI_ATTACK;
moveId = Moves.TRI_ATTACK; break;
break; case Biome.SLUM:
case Biome.SLUM: moveId = Moves.SLUDGE_BOMB;
moveId = Moves.SLUDGE_BOMB; break;
break; case Biome.PLAINS:
case Biome.PLAINS: moveId = Moves.SILVER_WIND;
moveId = Moves.SILVER_WIND; break;
break; case Biome.GRASS:
case Biome.GRASS: moveId = Moves.GRASS_KNOT;
moveId = Moves.GRASS_KNOT; break;
break; case Biome.TALL_GRASS:
case Biome.TALL_GRASS: moveId = Moves.POLLEN_PUFF;
moveId = Moves.POLLEN_PUFF; break;
break; case Biome.MEADOW:
case Biome.MEADOW: moveId = Moves.GIGA_DRAIN;
moveId = Moves.GIGA_DRAIN; break;
break; case Biome.FOREST:
case Biome.FOREST: moveId = Moves.BUG_BUZZ;
moveId = Moves.BUG_BUZZ; break;
break; case Biome.JUNGLE:
case Biome.JUNGLE: moveId = Moves.LEAF_STORM;
moveId = Moves.LEAF_STORM; break;
break; case Biome.SEA:
case Biome.SEA: moveId = Moves.HYDRO_PUMP;
moveId = Moves.HYDRO_PUMP; break;
break; case Biome.SWAMP:
case Biome.SWAMP: moveId = Moves.MUD_BOMB;
moveId = Moves.MUD_BOMB; break;
break; case Biome.BEACH:
case Biome.BEACH: moveId = Moves.SCALD;
moveId = Moves.SCALD; break;
break; case Biome.LAKE:
case Biome.LAKE: moveId = Moves.BUBBLE_BEAM;
moveId = Moves.BUBBLE_BEAM; break;
break; case Biome.SEABED:
case Biome.SEABED: moveId = Moves.BRINE;
moveId = Moves.BRINE; break;
break; case Biome.ISLAND:
case Biome.ISLAND: moveId = Moves.LEAF_TORNADO;
moveId = Moves.LEAF_TORNADO; break;
break; case Biome.MOUNTAIN:
case Biome.MOUNTAIN: moveId = Moves.AIR_SLASH;
moveId = Moves.AIR_SLASH; break;
break; case Biome.BADLANDS:
case Biome.BADLANDS: moveId = Moves.EARTH_POWER;
moveId = Moves.EARTH_POWER; break;
break; case Biome.DESERT:
case Biome.DESERT: moveId = Moves.SCORCHING_SANDS;
moveId = Moves.SCORCHING_SANDS; break;
break; case Biome.WASTELAND:
case Biome.WASTELAND: moveId = Moves.DRAGON_PULSE;
moveId = Moves.DRAGON_PULSE; break;
break; case Biome.CONSTRUCTION_SITE:
case Biome.CONSTRUCTION_SITE: moveId = Moves.STEEL_BEAM;
moveId = Moves.STEEL_BEAM; break;
break; case Biome.CAVE:
case Biome.CAVE: moveId = Moves.POWER_GEM;
moveId = Moves.POWER_GEM; break;
break; case Biome.ICE_CAVE:
case Biome.ICE_CAVE: moveId = Moves.ICE_BEAM;
moveId = Moves.ICE_BEAM; break;
break; case Biome.SNOWY_FOREST:
case Biome.SNOWY_FOREST: moveId = Moves.FROST_BREATH;
moveId = Moves.FROST_BREATH; break;
break; case Biome.VOLCANO:
case Biome.VOLCANO: moveId = Moves.LAVA_PLUME;
moveId = Moves.LAVA_PLUME; break;
break; case Biome.GRAVEYARD:
case Biome.GRAVEYARD: moveId = Moves.SHADOW_BALL;
moveId = Moves.SHADOW_BALL; break;
break; case Biome.RUINS:
case Biome.RUINS: moveId = Moves.ANCIENT_POWER;
moveId = Moves.ANCIENT_POWER; break;
break; case Biome.TEMPLE:
case Biome.TEMPLE: moveId = Moves.EXTRASENSORY;
moveId = Moves.EXTRASENSORY; break;
break; case Biome.DOJO:
case Biome.DOJO: moveId = Moves.FOCUS_BLAST;
moveId = Moves.FOCUS_BLAST; break;
break; case Biome.FAIRY_CAVE:
case Biome.FAIRY_CAVE: moveId = Moves.ALLURING_VOICE;
moveId = Moves.ALLURING_VOICE; break;
break; case Biome.ABYSS:
case Biome.ABYSS: moveId = Moves.OMINOUS_WIND;
moveId = Moves.OMINOUS_WIND; break;
break; case Biome.SPACE:
case Biome.SPACE: moveId = Moves.DRACO_METEOR;
moveId = Moves.DRACO_METEOR; break;
break; case Biome.FACTORY:
case Biome.FACTORY: moveId = Moves.FLASH_CANNON;
moveId = Moves.FLASH_CANNON; break;
break; case Biome.LABORATORY:
case Biome.LABORATORY: moveId = Moves.ZAP_CANNON;
moveId = Moves.ZAP_CANNON; break;
break; case Biome.POWER_PLANT:
case Biome.POWER_PLANT: moveId = Moves.CHARGE_BEAM;
moveId = Moves.CHARGE_BEAM; break;
break; case Biome.END:
case Biome.END: moveId = Moves.ETERNABEAM;
moveId = Moves.ETERNABEAM; break;
break; }
} break;
break; case TerrainType.MISTY:
case TerrainType.MISTY: moveId = Moves.MOONBLAST;
moveId = Moves.MOONBLAST; break;
break; case TerrainType.ELECTRIC:
case TerrainType.ELECTRIC: moveId = Moves.THUNDERBOLT;
moveId = Moves.THUNDERBOLT; break;
break; case TerrainType.GRASSY:
case TerrainType.GRASSY: moveId = Moves.ENERGY_BALL;
moveId = Moves.ENERGY_BALL; break;
break; case TerrainType.PSYCHIC:
case TerrainType.PSYCHIC: moveId = Moves.PSYCHIC;
moveId = Moves.PSYCHIC; break;
break; default:
default: // Just in case there's no match
// Just in case there's no match moveId = Moves.TRI_ATTACK;
moveId = Moves.TRI_ATTACK; break;
break; }
}
user.getMoveQueue().push({ move: moveId, targets: [ target.getBattlerIndex() ], ignorePP: true }); user.getMoveQueue().push({ move: moveId, targets: [ target.getBattlerIndex() ], ignorePP: true });
globalScene.unshiftPhase(new MovePhase(user, [ target.getBattlerIndex() ], new PokemonMove(moveId, 0, 0, true), true)); globalScene.unshiftPhase(new LoadMoveAnimPhase(moveId));
initMoveAnim(moveId).then(() => { globalScene.unshiftPhase(new MovePhase(user, [ target.getBattlerIndex() ], new PokemonMove(moveId, 0, 0, true), true));
loadMoveAnimAssets([ moveId ], true) return true;
.then(() => resolve(true));
});
});
} }
} }
@ -7121,7 +7104,7 @@ export class CopyMoveAttr extends CallMoveAttr {
this.invalidMoves = invalidMoves; this.invalidMoves = invalidMoves;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
this.hasTarget = this.mirrorMove; this.hasTarget = this.mirrorMove;
const lastMove = this.mirrorMove ? target.getLastXMoves()[0].move : globalScene.currentBattle.lastMove; const lastMove = this.mirrorMove ? target.getLastXMoves()[0].move : globalScene.currentBattle.lastMove;
return super.apply(user, target, allMoves[lastMove], args); return super.apply(user, target, allMoves[lastMove], args);
@ -7682,50 +7665,15 @@ export class SuppressAbilitiesIfActedAttr extends MoveEffectAttr {
* Used by Transform * Used by Transform
*/ */
export class TransformAttr extends MoveEffectAttr { export class TransformAttr extends MoveEffectAttr {
async apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!super.apply(user, target, move, args)) { if (!super.apply(user, target, move, args)) {
return false; return false;
} }
const promises: Promise<void>[] = []; globalScene.unshiftPhase(new PokemonTransformPhase(user.getBattlerIndex(), target.getBattlerIndex()));
user.summonData.speciesForm = target.getSpeciesForm();
user.summonData.gender = target.getGender();
// Power Trick's effect will not preserved after using Transform
user.removeTag(BattlerTagType.POWER_TRICK);
// Copy all stats (except HP)
for (const s of EFFECTIVE_STATS) {
user.setStat(s, target.getStat(s, false), false);
}
// Copy all stat stages
for (const s of BATTLE_STATS) {
user.setStatStage(s, target.getStatStage(s));
}
user.summonData.moveset = target.getMoveset().map((m) => {
if (m) {
// If PP value is less than 5, do nothing. If greater, we need to reduce the value to 5.
return new PokemonMove(m.moveId, 0, 0, false, Math.min(m.getMove().pp, 5));
} else {
console.warn(`Transform: somehow iterating over a ${m} value when copying moveset!`);
return new PokemonMove(Moves.NONE);
}
});
user.summonData.types = target.getTypes();
promises.push(user.updateInfo());
globalScene.queueMessage(i18next.t("moveTriggers:transformedIntoTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) })); globalScene.queueMessage(i18next.t("moveTriggers:transformedIntoTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) }));
promises.push(user.loadAssets(false).then(() => {
user.playAnim();
user.updateInfo();
// If the new ability activates immediately, it needs to happen after all the transform animations
user.setTempAbility(target.getAbility());
}));
await Promise.all(promises);
return true; return true;
} }
} }
@ -8035,6 +7983,56 @@ export class AfterYouAttr extends MoveEffectAttr {
} }
} }
/**
* Move effect to force the target to move last, ignoring priority.
* If applied to multiple targets, they move in speed order after all other moves.
* @extends MoveEffectAttr
*/
export class ForceLastAttr extends MoveEffectAttr {
/**
* Forces the target of this move to move last.
*
* @param user {@linkcode Pokemon} that is using the move.
* @param target {@linkcode Pokemon} that will be forced to move last.
* @param move {@linkcode Move} {@linkcode Moves.QUASH}
* @param _args N/A
* @returns true
*/
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
globalScene.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) }));
const targetMovePhase = globalScene.findPhase<MovePhase>((phase) => phase.pokemon === target);
if (targetMovePhase && !targetMovePhase.isForcedLast() && globalScene.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
// Finding the phase to insert the move in front of -
// Either the end of the turn or in front of another, slower move which has also been forced last
const prependPhase = globalScene.findPhase((phase) =>
[ MovePhase, MoveEndPhase ].every(cls => !(phase instanceof cls))
|| (phase instanceof MovePhase) && phaseForcedSlower(phase, target, !!globalScene.arena.getTag(ArenaTagType.TRICK_ROOM))
);
if (prependPhase) {
globalScene.phaseQueue.splice(
globalScene.phaseQueue.indexOf(prependPhase),
0,
new MovePhase(target, [ ...targetMovePhase.targets ], targetMovePhase.move, false, false, false, true)
);
}
}
return true;
}
}
/** Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target} */
const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean): boolean => {
let slower: boolean;
// quashed pokemon still have speed ties
if (phase.pokemon.getEffectiveStat(Stat.SPD) === target.getEffectiveStat(Stat.SPD)) {
slower = !!target.randSeedInt(2);
} else {
slower = !trickRoom ? phase.pokemon.getEffectiveStat(Stat.SPD) < target.getEffectiveStat(Stat.SPD) : phase.pokemon.getEffectiveStat(Stat.SPD) > target.getEffectiveStat(Stat.SPD);
}
return phase.isForcedLast() && slower;
};
const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY); const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY);
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune(); const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
@ -8078,44 +8076,54 @@ const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) =
export type MoveAttrFilter = (attr: MoveAttr) => boolean; export type MoveAttrFilter = (attr: MoveAttr) => boolean;
function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise<void> { function applyMoveAttrsInternal(
return new Promise(resolve => { attrFilter: MoveAttrFilter,
const attrPromises: Promise<boolean>[] = []; user: Pokemon | null,
const moveAttrs = move.attrs.filter(a => attrFilter(a)); target: Pokemon | null,
for (const attr of moveAttrs) { move: Move,
const result = attr.apply(user, target, move, args); args: any[],
if (result instanceof Promise) { ): void {
attrPromises.push(result); move.attrs.filter((attr) => attrFilter(attr)).forEach((attr) => attr.apply(user, target, move, args));
}
}
Promise.allSettled(attrPromises).then(() => resolve());
});
} }
function applyMoveChargeAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: ChargingMove, args: any[]): Promise<void> { function applyMoveChargeAttrsInternal(
return new Promise(resolve => { attrFilter: MoveAttrFilter,
const chargeAttrPromises: Promise<boolean>[] = []; user: Pokemon | null,
const chargeMoveAttrs = move.chargeAttrs.filter(a => attrFilter(a)); target: Pokemon | null,
for (const attr of chargeMoveAttrs) { move: ChargingMove,
const result = attr.apply(user, target, move, args); args: any[],
if (result instanceof Promise) { ): void {
chargeAttrPromises.push(result); move.chargeAttrs.filter((attr) => attrFilter(attr)).forEach((attr) => attr.apply(user, target, move, args));
}
}
Promise.allSettled(chargeAttrPromises).then(() => resolve());
});
} }
export function applyMoveAttrs(attrType: Constructor<MoveAttr>, user: Pokemon | null, target: Pokemon | null, move: Move, ...args: any[]): Promise<void> { export function applyMoveAttrs(
return applyMoveAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args); attrType: Constructor<MoveAttr>,
user: Pokemon | null,
target: Pokemon | null,
move: Move,
...args: any[]
): void {
applyMoveAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args);
} }
export function applyFilteredMoveAttrs(attrFilter: MoveAttrFilter, user: Pokemon, target: Pokemon | null, move: Move, ...args: any[]): Promise<void> { export function applyFilteredMoveAttrs(
return applyMoveAttrsInternal(attrFilter, user, target, move, args); attrFilter: MoveAttrFilter,
user: Pokemon,
target: Pokemon | null,
move: Move,
...args: any[]
): void {
applyMoveAttrsInternal(attrFilter, user, target, move, args);
} }
export function applyMoveChargeAttrs(attrType: Constructor<MoveAttr>, user: Pokemon | null, target: Pokemon | null, move: ChargingMove, ...args: any[]): Promise<void> { export function applyMoveChargeAttrs(
return applyMoveChargeAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args); attrType: Constructor<MoveAttr>,
user: Pokemon | null,
target: Pokemon | null,
move: ChargingMove,
...args: any[]
): void {
applyMoveChargeAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args);
} }
export class MoveCondition { export class MoveCondition {
@ -9914,7 +9922,8 @@ export function initMoves() {
.attr(RemoveHeldItemAttr, true), .attr(RemoveHeldItemAttr, true),
new StatusMove(Moves.QUASH, Type.DARK, 100, 15, -1, 0, 5) new StatusMove(Moves.QUASH, Type.DARK, 100, 15, -1, 0, 5)
.condition(failIfSingleBattle) .condition(failIfSingleBattle)
.unimplemented(), .condition((user, target, move) => !target.turnData.acted)
.attr(ForceLastAttr),
new AttackMove(Moves.ACROBATICS, Type.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5) new AttackMove(Moves.ACROBATICS, Type.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5)
.attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferable).reduce((v, m) => v + m.stackCount, 0))), .attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferable).reduce((v, m) => v + m.stackCount, 0))),
new StatusMove(Moves.REFLECT_TYPE, Type.NORMAL, -1, 15, -1, 0, 5) new StatusMove(Moves.REFLECT_TYPE, Type.NORMAL, -1, 15, -1, 0, 5)

View File

@ -151,7 +151,7 @@ async function spawnNextTrainerOrEndEncounter() {
// Give 10x Voucher // Give 10x Voucher
const newModifier = modifierTypes.VOUCHER_PREMIUM().newModifier(); const newModifier = modifierTypes.VOUCHER_PREMIUM().newModifier();
await globalScene.addModifier(newModifier); globalScene.addModifier(newModifier);
globalScene.playSound("item_fanfare"); globalScene.playSound("item_fanfare");
await showEncounterText(i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name })); await showEncounterText(i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name }));

View File

@ -406,7 +406,7 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) {
// Copy old items to new pokemon // Copy old items to new pokemon
for (const item of transformation.heldItems) { for (const item of transformation.heldItems) {
item.pokemonId = newPokemon.id; item.pokemonId = newPokemon.id;
await globalScene.addModifier(item, false, false, false, true); globalScene.addModifier(item, false, false, false, true);
} }
// Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats // Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats
if (shouldGetOldGateau(newPokemon)) { if (shouldGetOldGateau(newPokemon)) {
@ -416,7 +416,7 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) {
?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU); ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU);
const modifier = modType?.newModifier(newPokemon); const modifier = modType?.newModifier(newPokemon);
if (modifier) { if (modifier) {
await globalScene.addModifier(modifier, false, false, false, true); globalScene.addModifier(modifier, false, false, false, true);
} }
} }

View File

@ -326,7 +326,7 @@ export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: numb
?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE); ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE);
const modifier = modType?.newModifier(pokemon); const modifier = modType?.newModifier(pokemon);
if (modifier) { if (modifier) {
await globalScene.addModifier(modifier, false, false, false, true); globalScene.addModifier(modifier, false, false, false, true);
pokemon.calculateStats(); pokemon.calculateStats();
} }
} }
@ -359,7 +359,7 @@ export async function applyModifierTypeToPlayerPokemon(pokemon: PlayerPokemon, m
return applyModifierTypeToPlayerPokemon(pokemon, fallbackModifierType); return applyModifierTypeToPlayerPokemon(pokemon, fallbackModifierType);
} }
await globalScene.addModifier(modifier, false, false, false, true); globalScene.addModifier(modifier, false, false, false, true);
} }
/** /**

View File

@ -1850,7 +1850,7 @@ export function initSpecies() {
new PokemonSpecies(Species.REGIGIGAS, 4, true, false, false, "Colossal Pokémon", Type.NORMAL, null, 3.7, 420, Abilities.SLOW_START, Abilities.NONE, Abilities.NORMALIZE, 670, 110, 160, 110, 80, 110, 100, 3, 0, 335, GrowthRate.SLOW, null, false), new PokemonSpecies(Species.REGIGIGAS, 4, true, false, false, "Colossal Pokémon", Type.NORMAL, null, 3.7, 420, Abilities.SLOW_START, Abilities.NONE, Abilities.NORMALIZE, 670, 110, 160, 110, 80, 110, 100, 3, 0, 335, GrowthRate.SLOW, null, false),
new PokemonSpecies(Species.GIRATINA, 4, false, true, false, "Renegade Pokémon", Type.GHOST, Type.DRAGON, 4.5, 750, Abilities.PRESSURE, Abilities.NONE, Abilities.TELEPATHY, 680, 150, 100, 120, 100, 120, 90, 3, 0, 340, GrowthRate.SLOW, null, false, true, new PokemonSpecies(Species.GIRATINA, 4, false, true, false, "Renegade Pokémon", Type.GHOST, Type.DRAGON, 4.5, 750, Abilities.PRESSURE, Abilities.NONE, Abilities.TELEPATHY, 680, 150, 100, 120, 100, 120, 90, 3, 0, 340, GrowthRate.SLOW, null, false, true,
new PokemonForm("Altered Forme", "altered", Type.GHOST, Type.DRAGON, 4.5, 750, Abilities.PRESSURE, Abilities.NONE, Abilities.TELEPATHY, 680, 150, 100, 120, 100, 120, 90, 3, 0, 340, false, null, true), new PokemonForm("Altered Forme", "altered", Type.GHOST, Type.DRAGON, 4.5, 750, Abilities.PRESSURE, Abilities.NONE, Abilities.TELEPATHY, 680, 150, 100, 120, 100, 120, 90, 3, 0, 340, false, null, true),
new PokemonForm("Origin Forme", "origin", Type.GHOST, Type.DRAGON, 6.9, 650, Abilities.LEVITATE, Abilities.NONE, Abilities.NONE, 680, 150, 120, 100, 120, 100, 90, 3, 0, 340), new PokemonForm("Origin Forme", "origin", Type.GHOST, Type.DRAGON, 6.9, 650, Abilities.LEVITATE, Abilities.NONE, Abilities.LEVITATE, 680, 150, 120, 100, 120, 100, 90, 3, 0, 340),
), ),
new PokemonSpecies(Species.CRESSELIA, 4, true, false, false, "Lunar Pokémon", Type.PSYCHIC, null, 1.5, 85.6, Abilities.LEVITATE, Abilities.NONE, Abilities.NONE, 580, 120, 70, 110, 75, 120, 85, 3, 100, 300, GrowthRate.SLOW, 0, false), new PokemonSpecies(Species.CRESSELIA, 4, true, false, false, "Lunar Pokémon", Type.PSYCHIC, null, 1.5, 85.6, Abilities.LEVITATE, Abilities.NONE, Abilities.NONE, 580, 120, 70, 110, 75, 120, 85, 3, 100, 300, GrowthRate.SLOW, 0, false),
new PokemonSpecies(Species.PHIONE, 4, false, false, true, "Sea Drifter Pokémon", Type.WATER, null, 0.4, 3.1, Abilities.HYDRATION, Abilities.NONE, Abilities.NONE, 480, 80, 80, 80, 80, 80, 80, 30, 70, 240, GrowthRate.SLOW, null, false), new PokemonSpecies(Species.PHIONE, 4, false, false, true, "Sea Drifter Pokémon", Type.WATER, null, 0.4, 3.1, Abilities.HYDRATION, Abilities.NONE, Abilities.NONE, 480, 80, 80, 80, 80, 80, 80, 30, 70, 240, GrowthRate.SLOW, null, false),

View File

@ -13,7 +13,7 @@ export enum Button {
CYCLE_GENDER, CYCLE_GENDER,
CYCLE_ABILITY, CYCLE_ABILITY,
CYCLE_NATURE, CYCLE_NATURE,
V, CYCLE_TERA,
SPEED_UP, SPEED_UP,
SLOW_DOWN SLOW_DOWN,
} }

View File

@ -104,7 +104,6 @@ import { MoveEndPhase } from "#app/phases/move-end-phase";
import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase"; import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase";
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
import { PokemonAnimType } from "#enums/pokemon-anim-type"; import { PokemonAnimType } from "#enums/pokemon-anim-type";
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
@ -2348,12 +2347,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const maxPower = Math.min(movePool.reduce((v, m) => Math.max(allMoves[m[0]].power, v), 40), 90); const maxPower = Math.min(movePool.reduce((v, m) => Math.max(allMoves[m[0]].power, v), 40), 90);
movePool = movePool.map(m => [ m[0], m[1] * (allMoves[m[0]].category === MoveCategory.STATUS ? 1 : Math.max(Math.min(allMoves[m[0]].power / maxPower, 1), 0.5)) ]); movePool = movePool.map(m => [ m[0], m[1] * (allMoves[m[0]].category === MoveCategory.STATUS ? 1 : Math.max(Math.min(allMoves[m[0]].power / maxPower, 1), 0.5)) ]);
// Weight damaging moves against the lower stat // Weight damaging moves against the lower stat. This uses a non-linear relationship.
// If the higher stat is 1 - 1.09x higher, no change. At higher stat ~1.38x lower stat, off-stat moves have half weight.
// One third weight at ~1.58x higher, one quarter weight at ~1.73x higher, one fifth at ~1.87x, and one tenth at ~2.35x higher.
const atk = this.getStat(Stat.ATK); const atk = this.getStat(Stat.ATK);
const spAtk = this.getStat(Stat.SPATK); const spAtk = this.getStat(Stat.SPATK);
const worseCategory: MoveCategory = atk > spAtk ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL; const worseCategory: MoveCategory = atk > spAtk ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL;
const statRatio = worseCategory === MoveCategory.PHYSICAL ? atk / spAtk : spAtk / atk; const statRatio = worseCategory === MoveCategory.PHYSICAL ? atk / spAtk : spAtk / atk;
movePool = movePool.map(m => [ m[0], m[1] * (allMoves[m[0]].category === worseCategory ? statRatio : 1) ]); movePool = movePool.map(m => [ m[0], m[1] * (allMoves[m[0]].category === worseCategory ? Math.min(Math.pow(statRatio, 3) * 1.3, 1) : 1) ]);
/** The higher this is the more the game weights towards higher level moves. At `0` all moves are equal weight. */ /** The higher this is the more the game weights towards higher level moves. At `0` all moves are equal weight. */
let weightMultiplier = 0.9; let weightMultiplier = 0.9;
@ -4509,43 +4510,6 @@ export class PlayerPokemon extends Pokemon {
this.friendship = Math.max(this.friendship + friendship, 0); this.friendship = Math.max(this.friendship + friendship, 0);
} }
} }
/**
* Handles Revival Blessing when used by player.
* @returns Promise to revive a pokemon.
* @see {@linkcode RevivalBlessingAttr}
*/
revivalBlessing(): Promise<void> {
return new Promise(resolve => {
globalScene.ui.setMode(Mode.PARTY, PartyUiMode.REVIVAL_BLESSING, this.getFieldIndex(), (slotIndex:number, option: PartyOption) => {
if (slotIndex >= 0 && slotIndex < 6) {
const pokemon = globalScene.getPlayerParty()[slotIndex];
if (!pokemon || !pokemon.isFainted()) {
resolve();
}
pokemon.resetTurnData();
pokemon.resetStatus();
pokemon.heal(Math.min(Utils.toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp()));
globalScene.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: pokemon.name }), 0, true);
if (globalScene.currentBattle.double && globalScene.getPlayerParty().length > 1) {
const allyPokemon = this.getAlly();
if (slotIndex <= 1) {
// Revived ally pokemon
globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, pokemon.getFieldIndex(), slotIndex, false, true));
globalScene.unshiftPhase(new ToggleDoublePositionPhase(true));
} else if (allyPokemon.isFainted()) {
// Revived party pokemon, and ally pokemon is fainted
globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, true));
globalScene.unshiftPhase(new ToggleDoublePositionPhase(true));
}
}
}
globalScene.ui.setMode(Mode.MESSAGE).then(() => resolve());
}, PartyUiHandler.FilterFainted);
});
}
getPossibleEvolution(evolution: SpeciesFormEvolution | null): Promise<Pokemon> { getPossibleEvolution(evolution: SpeciesFormEvolution | null): Promise<Pokemon> {
if (!evolution) { if (!evolution) {
@ -4727,70 +4691,62 @@ export class PlayerPokemon extends Pokemon {
} }
/** /**
* Returns a Promise to fuse two PlayerPokemon together * Returns a Promise to fuse two PlayerPokemon together
* @param pokemon The PlayerPokemon to fuse to this one * @param pokemon The PlayerPokemon to fuse to this one
*/ */
fuse(pokemon: PlayerPokemon): Promise<void> { fuse(pokemon: PlayerPokemon): void {
return new Promise(resolve => { this.fusionSpecies = pokemon.species;
this.fusionSpecies = pokemon.species; this.fusionFormIndex = pokemon.formIndex;
this.fusionFormIndex = pokemon.formIndex; this.fusionAbilityIndex = pokemon.abilityIndex;
this.fusionAbilityIndex = pokemon.abilityIndex; this.fusionShiny = pokemon.shiny;
this.fusionShiny = pokemon.shiny; this.fusionVariant = pokemon.variant;
this.fusionVariant = pokemon.variant; this.fusionGender = pokemon.gender;
this.fusionGender = pokemon.gender; this.fusionLuck = pokemon.luck;
this.fusionLuck = pokemon.luck; this.fusionCustomPokemonData = pokemon.customPokemonData;
this.fusionCustomPokemonData = pokemon.customPokemonData; if (pokemon.pauseEvolutions || this.pauseEvolutions) {
if ((pokemon.pauseEvolutions) || (this.pauseEvolutions)) { this.pauseEvolutions = true;
this.pauseEvolutions = true; }
}
globalScene.validateAchv(achvs.SPLICE); globalScene.validateAchv(achvs.SPLICE);
globalScene.gameData.gameStats.pokemonFused++; globalScene.gameData.gameStats.pokemonFused++;
// Store the average HP% that each Pokemon has // Store the average HP% that each Pokemon has
const maxHp = this.getMaxHp(); const maxHp = this.getMaxHp();
const newHpPercent = ((pokemon.hp / pokemon.getMaxHp()) + (this.hp / maxHp)) / 2; const newHpPercent = (pokemon.hp / pokemon.getMaxHp() + this.hp / maxHp) / 2;
this.generateName(); this.generateName();
this.calculateStats(); this.calculateStats();
// Set this Pokemon's HP to the average % of both fusion components // Set this Pokemon's HP to the average % of both fusion components
this.hp = Math.round(maxHp * newHpPercent); this.hp = Math.round(maxHp * newHpPercent);
if (!this.isFainted()) { if (!this.isFainted()) {
// If this Pokemon hasn't fainted, make sure the HP wasn't set over the new maximum // If this Pokemon hasn't fainted, make sure the HP wasn't set over the new maximum
this.hp = Math.min(this.hp, maxHp); this.hp = Math.min(this.hp, maxHp);
this.status = getRandomStatus(this.status, pokemon.status); // Get a random valid status between the two this.status = getRandomStatus(this.status, pokemon.status); // Get a random valid status between the two
} else if (!pokemon.isFainted()) { } else if (!pokemon.isFainted()) {
// If this Pokemon fainted but the other hasn't, make sure the HP wasn't set to zero // If this Pokemon fainted but the other hasn't, make sure the HP wasn't set to zero
this.hp = Math.max(this.hp, 1); this.hp = Math.max(this.hp, 1);
this.status = pokemon.status; // Inherit the other Pokemon's status this.status = pokemon.status; // Inherit the other Pokemon's status
} }
this.generateCompatibleTms(); this.generateCompatibleTms();
this.updateInfo(true); this.updateInfo(true);
const fusedPartyMemberIndex = globalScene.getPlayerParty().indexOf(pokemon); const fusedPartyMemberIndex = globalScene.getPlayerParty().indexOf(pokemon);
let partyMemberIndex = globalScene.getPlayerParty().indexOf(this); let partyMemberIndex = globalScene.getPlayerParty().indexOf(this);
if (partyMemberIndex > fusedPartyMemberIndex) { if (partyMemberIndex > fusedPartyMemberIndex) {
partyMemberIndex--; partyMemberIndex--;
} }
const fusedPartyMemberHeldModifiers = globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier const fusedPartyMemberHeldModifiers = globalScene.findModifiers((m) => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id, true) as PokemonHeldItemModifier[];
&& m.pokemonId === pokemon.id, true) as PokemonHeldItemModifier[]; for (const modifier of fusedPartyMemberHeldModifiers) {
const transferModifiers: Promise<boolean>[] = []; globalScene.tryTransferHeldItemModifier(modifier, this, false, modifier.getStackCount(), true, true, false);
for (const modifier of fusedPartyMemberHeldModifiers) { }
transferModifiers.push(globalScene.tryTransferHeldItemModifier(modifier, this, false, modifier.getStackCount(), true, true, false)); globalScene.updateModifiers(true, true);
} globalScene.removePartyMemberModifiers(fusedPartyMemberIndex);
Promise.allSettled(transferModifiers).then(() => { globalScene.getPlayerParty().splice(fusedPartyMemberIndex, 1)[0];
globalScene.updateModifiers(true, true).then(() => { const newPartyMemberIndex = globalScene.getPlayerParty().indexOf(this);
globalScene.removePartyMemberModifiers(fusedPartyMemberIndex); pokemon.getMoveset(true).map((m: PokemonMove) => globalScene.unshiftPhase(new LearnMovePhase(newPartyMemberIndex, m.getMove().id)));
globalScene.getPlayerParty().splice(fusedPartyMemberIndex, 1)[0]; pokemon.destroy();
const newPartyMemberIndex = globalScene.getPlayerParty().indexOf(this); this.updateFusionPalette();
pokemon.getMoveset(true).map((m: PokemonMove) => globalScene.unshiftPhase(new LearnMovePhase(newPartyMemberIndex, m.getMove().id)));
pokemon.destroy();
this.updateFusionPalette();
resolve();
});
});
});
} }
unfuse(): Promise<void> { unfuse(): Promise<void> {

View File

@ -1822,7 +1822,7 @@ const modifierPool: ModifierPool = {
if (!isHoldingOrb) { if (!isHoldingOrb) {
const moveset = p.getMoveset(true).filter(m => !isNullOrUndefined(m)).map(m => m.moveId); const moveset = p.getMoveset(true).filter(m => !isNullOrUndefined(m)).map(m => m.moveId);
const canSetStatus = p.canSetStatus(StatusEffect.TOXIC, true, true, null, true); const canSetStatus = p.canSetStatus(StatusEffect.BURN, true, true, null, true);
// Moves that take advantage of obtaining the actual status effect // Moves that take advantage of obtaining the actual status effect
const hasStatusMoves = [ Moves.FACADE, Moves.PSYCHO_SHIFT ] const hasStatusMoves = [ Moves.FACADE, Moves.PSYCHO_SHIFT ]

View File

@ -158,7 +158,7 @@ export abstract class Modifier {
* Handles applying of {@linkcode Modifier} * Handles applying of {@linkcode Modifier}
* @param args collection of all passed parameters * @param args collection of all passed parameters
*/ */
abstract apply(...args: unknown[]): boolean | Promise<boolean>; abstract apply(...args: unknown[]): boolean;
} }
export abstract class PersistentModifier extends Modifier { export abstract class PersistentModifier extends Modifier {
@ -1949,7 +1949,7 @@ export abstract class ConsumablePokemonModifier extends ConsumableModifier {
* @param playerPokemon The {@linkcode PlayerPokemon} that consumes the item * @param playerPokemon The {@linkcode PlayerPokemon} that consumes the item
* @param args Additional arguments passed to {@linkcode ConsumablePokemonModifier.apply} * @param args Additional arguments passed to {@linkcode ConsumablePokemonModifier.apply}
*/ */
abstract override apply(playerPokemon: PlayerPokemon, ...args: unknown[]): boolean | Promise<boolean>; abstract override apply(playerPokemon: PlayerPokemon, ...args: unknown[]): boolean;
getPokemon() { getPokemon() {
return globalScene.getPlayerParty().find(p => p.id === this.pokemonId); return globalScene.getPlayerParty().find(p => p.id === this.pokemonId);
@ -2288,8 +2288,8 @@ export class FusePokemonModifier extends ConsumablePokemonModifier {
* @param playerPokemon2 {@linkcode PlayerPokemon} that should be fused with {@linkcode playerPokemon} * @param playerPokemon2 {@linkcode PlayerPokemon} that should be fused with {@linkcode playerPokemon}
* @returns always Promise<true> * @returns always Promise<true>
*/ */
override async apply(playerPokemon: PlayerPokemon, playerPokemon2: PlayerPokemon): Promise<boolean> { override apply(playerPokemon: PlayerPokemon, playerPokemon2: PlayerPokemon): boolean {
await playerPokemon.fuse(playerPokemon2); playerPokemon.fuse(playerPokemon2);
return true; return true;
} }
} }
@ -3136,8 +3136,6 @@ export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier {
let highestItemTier = itemModifiers.map(m => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is this bang correct? let highestItemTier = itemModifiers.map(m => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is this bang correct?
let tierItemModifiers = itemModifiers.filter(m => m.type.getOrInferTier(poolType) === highestItemTier); let tierItemModifiers = itemModifiers.filter(m => m.type.getOrInferTier(poolType) === highestItemTier);
const heldItemTransferPromises: Promise<void>[] = [];
for (let i = 0; i < transferredItemCount; i++) { for (let i = 0; i < transferredItemCount; i++) {
if (!tierItemModifiers.length) { if (!tierItemModifiers.length) {
while (highestItemTier-- && !tierItemModifiers.length) { while (highestItemTier-- && !tierItemModifiers.length) {
@ -3149,19 +3147,15 @@ export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier {
} }
const randItemIndex = pokemon.randSeedInt(itemModifiers.length); const randItemIndex = pokemon.randSeedInt(itemModifiers.length);
const randItem = itemModifiers[randItemIndex]; const randItem = itemModifiers[randItemIndex];
heldItemTransferPromises.push(globalScene.tryTransferHeldItemModifier(randItem, pokemon, false).then(success => { if (globalScene.tryTransferHeldItemModifier(randItem, pokemon, false)) {
if (success) { transferredModifierTypes.push(randItem.type);
transferredModifierTypes.push(randItem.type); itemModifiers.splice(randItemIndex, 1);
itemModifiers.splice(randItemIndex, 1); }
}
}));
} }
Promise.all(heldItemTransferPromises).then(() => { for (const mt of transferredModifierTypes) {
for (const mt of transferredModifierTypes) { globalScene.queueMessage(this.getTransferMessage(pokemon, targetPokemon, mt));
globalScene.queueMessage(this.getTransferMessage(pokemon, targetPokemon, mt)); }
}
});
return !!transferredModifierTypes.length; return !!transferredModifierTypes.length;
} }
@ -3390,7 +3384,7 @@ abstract class EnemyDamageMultiplierModifier extends EnemyPersistentModifier {
* @returns always `true` * @returns always `true`
*/ */
override apply(multiplier: NumberHolder): boolean { override apply(multiplier: NumberHolder): boolean {
multiplier.value = Math.floor(multiplier.value * Math.pow(this.damageMultiplier, this.getStackCount())); multiplier.value = toDmgValue(multiplier.value * Math.pow(this.damageMultiplier, this.getStackCount()));
return true; return true;
} }

View File

@ -21,6 +21,7 @@ export class AddEnemyBuffModifierPhase extends Phase {
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
globalScene.addEnemyModifier(getEnemyBuffModifierForWave(tier, globalScene.findModifiers(m => m instanceof EnemyPersistentModifier, false)), true, true); globalScene.addEnemyModifier(getEnemyBuffModifierForWave(tier, globalScene.findModifiers(m => m instanceof EnemyPersistentModifier, false)), true, true);
} }
globalScene.updateModifiers(false, true).then(() => this.end()); globalScene.updateModifiers(false, true);
this.end();
} }
} }

View File

@ -63,6 +63,7 @@ export class BattleEndPhase extends BattlePhase {
} }
} }
globalScene.updateModifiers().then(() => this.end()); globalScene.updateModifiers();
this.end();
} }
} }

View File

@ -38,6 +38,7 @@ import { Species } from "#enums/species";
import { overrideHeldItems, overrideModifiers } from "#app/modifier/modifier"; import { overrideHeldItems, overrideModifiers } from "#app/modifier/modifier";
import i18next from "i18next"; import i18next from "i18next";
import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters";
import { Nature } from "#enums/nature";
export class EncounterPhase extends BattlePhase { export class EncounterPhase extends BattlePhase {
private loaded: boolean; private loaded: boolean;
@ -156,7 +157,31 @@ export class EncounterPhase extends BattlePhase {
loadEnemyAssets.push(enemyPokemon.loadAssets()); loadEnemyAssets.push(enemyPokemon.loadAssets());
console.log(`Pokemon: ${getPokemonNameWithAffix(enemyPokemon)}`, `Species ID: ${enemyPokemon.species.speciesId}`, `Stats: ${enemyPokemon.stats}`, `Ability: ${enemyPokemon.getAbility().name}`, `Passive Ability: ${enemyPokemon.getPassiveAbility().name}`); const stats: string[] = [
`HP: ${enemyPokemon.stats[0]} (${enemyPokemon.ivs[0]})`,
` Atk: ${enemyPokemon.stats[1]} (${enemyPokemon.ivs[1]})`,
` Def: ${enemyPokemon.stats[2]} (${enemyPokemon.ivs[2]})`,
` Spatk: ${enemyPokemon.stats[3]} (${enemyPokemon.ivs[3]})`,
` Spdef: ${enemyPokemon.stats[4]} (${enemyPokemon.ivs[4]})`,
` Spd: ${enemyPokemon.stats[5]} (${enemyPokemon.ivs[5]})`,
];
const moveset: string[] = [];
enemyPokemon.getMoveset().forEach((move) => {
moveset.push(move!.getName());
});
console.log(
`Pokemon: ${getPokemonNameWithAffix(enemyPokemon)}`,
`| Species ID: ${enemyPokemon.species.speciesId}`,
`| Nature: ${Nature[enemyPokemon.getNature()]}`,
);
console.log(`Stats (IVs): ${stats}`);
console.log(
`Ability: ${enemyPokemon.getAbility().name}`,
`| Passive Ability${enemyPokemon.isBoss() ? "" : " (inactive)"}: ${enemyPokemon.getPassiveAbility().name}`,
`${enemyPokemon.isBoss() ? `| Boss Bars: ${enemyPokemon.bossSegments}` : ""}`
);
console.log("Moveset:", moveset);
return true; return true;
}); });

View File

@ -12,16 +12,22 @@ export class GameOverModifierRewardPhase extends ModifierRewardPhase {
doReward(): Promise<void> { doReward(): Promise<void> {
return new Promise<void>(resolve => { return new Promise<void>(resolve => {
const newModifier = this.modifierType.newModifier(); const newModifier = this.modifierType.newModifier();
globalScene.addModifier(newModifier).then(() => { globalScene.addModifier(newModifier);
// Sound loaded into game as is // Sound loaded into game as is
globalScene.playSound("level_up_fanfare"); globalScene.playSound("level_up_fanfare");
globalScene.ui.setMode(Mode.MESSAGE); globalScene.ui.setMode(Mode.MESSAGE);
globalScene.ui.fadeIn(250).then(() => { globalScene.ui.fadeIn(250).then(() => {
globalScene.ui.showText(i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name }), null, () => { globalScene.ui.showText(
i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name }),
null,
() => {
globalScene.time.delayedCall(1500, () => globalScene.arenaBg.setVisible(true)); globalScene.time.delayedCall(1500, () => globalScene.arenaBg.setVisible(true));
resolve(); resolve();
}, null, true, 1500); },
}); null,
true,
1500,
);
}); });
}); });
} }

View File

@ -0,0 +1,20 @@
import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims";
import type { Moves } from "#enums/moves";
import { Phase } from "#app/phase";
/**
* Phase for synchronous move animation loading.
* Should be used when a move invokes another move that
* isn't already loaded (e.g. for Metronome)
*/
export class LoadMoveAnimPhase extends Phase {
constructor(protected moveId: Moves) {
super();
}
public override start(): void {
initMoveAnim(this.moveId)
.then(() => loadMoveAnimAssets([ this.moveId ], true))
.then(() => this.end());
}
}

View File

@ -22,10 +22,9 @@ export class ModifierRewardPhase extends BattlePhase {
doReward(): Promise<void> { doReward(): Promise<void> {
return new Promise<void>(resolve => { return new Promise<void>(resolve => {
const newModifier = this.modifierType.newModifier(); const newModifier = this.modifierType.newModifier();
globalScene.addModifier(newModifier).then(() => { globalScene.addModifier(newModifier);
globalScene.playSound("item_fanfare"); globalScene.playSound("item_fanfare");
globalScene.ui.showText(i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name }), null, () => resolve(), null, true); globalScene.ui.showText(i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name }), null, () => resolve(), null, true);
});
}); });
} }
} }

View File

@ -0,0 +1,20 @@
import type { MoveAnim } from "#app/data/battle-anims";
import { Phase } from "#app/phase";
/**
* Plays the given {@linkcode MoveAnim} sequentially.
*/
export class MoveAnimPhase<Anim extends MoveAnim> extends Phase {
constructor(
protected anim: Anim,
protected onSubstitute: boolean = false,
) {
super();
}
public override start(): void {
super.start();
this.anim.play(this.onSubstitute, () => this.end());
}
}

View File

@ -44,10 +44,9 @@ export class MoveChargePhase extends PokemonPhase {
new MoveChargeAnim(move.chargeAnim, move.id, user).play(false, () => { new MoveChargeAnim(move.chargeAnim, move.id, user).play(false, () => {
move.showChargeText(user, target); move.showChargeText(user, target);
applyMoveChargeAttrs(MoveEffectAttr, user, target, move).then(() => { applyMoveChargeAttrs(MoveEffectAttr, user, target, move);
user.addTag(BattlerTagType.CHARGING, 1, move.id, user.id); user.addTag(BattlerTagType.CHARGING, 1, move.id, user.id);
this.end(); this.end();
});
}); });
} }

View File

@ -61,7 +61,7 @@ import {
PokemonMultiHitModifier, PokemonMultiHitModifier,
} from "#app/modifier/modifier"; } from "#app/modifier/modifier";
import { PokemonPhase } from "#app/phases/pokemon-phase"; import { PokemonPhase } from "#app/phases/pokemon-phase";
import { BooleanHolder, executeIf, isNullOrUndefined, NumberHolder } from "#app/utils"; import { BooleanHolder, isNullOrUndefined, NumberHolder } from "#app/utils";
import { type nil } from "#app/utils"; import { type nil } from "#app/utils";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import type { Moves } from "#enums/moves"; import type { Moves } from "#enums/moves";
@ -143,86 +143,86 @@ export class MoveEffectPhase extends PokemonPhase {
const move = this.move.getMove(); const move = this.move.getMove();
// Assume single target for override // Assume single target for override
applyMoveAttrs(OverrideMoveEffectAttr, user, this.getFirstTarget() ?? null, move, overridden, this.move.virtual).then(() => { applyMoveAttrs(OverrideMoveEffectAttr, user, this.getFirstTarget() ?? null, move, overridden, this.move.virtual);
// If other effects were overriden, stop this phase before they can be applied
if (overridden.value) { // If other effects were overriden, stop this phase before they can be applied
return this.end(); if (overridden.value) {
return this.end();
}
user.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
// If the user is acting again (such as due to Instruct), reset hitsLeft/hitCount so that
// the move executes correctly (ensures all hits of a multi-hit are properly calculated)
if (user.turnData.hitsLeft === 0 && user.turnData.hitCount > 0 && user.turnData.extraTurns > 0) {
user.turnData.hitsLeft = -1;
user.turnData.hitCount = 0;
user.turnData.extraTurns--;
}
/**
* If this phase is for the first hit of the invoked move,
* resolve the move's total hit count. This block combines the
* effects of the move itself, Parental Bond, and Multi-Lens to do so.
*/
if (user.turnData.hitsLeft === -1) {
const hitCount = new NumberHolder(1);
// Assume single target for multi hit
applyMoveAttrs(MultiHitAttr, user, this.getFirstTarget() ?? null, move, hitCount);
// If Parental Bond is applicable, add another hit
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, hitCount, null);
// If Multi-Lens is applicable, add hits equal to the number of held Multi-Lenses
globalScene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, move.id, hitCount);
// Set the user's relevant turnData fields to reflect the final hit count
user.turnData.hitCount = hitCount.value;
user.turnData.hitsLeft = hitCount.value;
}
/**
* Log to be entered into the user's move history once the move result is resolved.
* Note that `result` (a {@linkcode MoveResult}) logs whether the move was successfully
* used in the sense of "Does it have an effect on the user?".
*/
const moveHistoryEntry = { move: this.move.moveId, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual };
/**
* Stores results of hit checks of the invoked move against all targets, organized by battler index.
* @see {@linkcode hitCheck}
*/
const targetHitChecks = Object.fromEntries(targets.map(p => [ p.getBattlerIndex(), this.hitCheck(p) ]));
const hasActiveTargets = targets.some(t => t.isActive(true));
/** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */
const isImmune = targets[0]?.hasAbilityWithAttr(TypeImmunityAbAttr)
&& (targets[0]?.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
&& !targets[0]?.getTag(SemiInvulnerableTag);
const mayBounce = move.hasFlag(MoveFlags.REFLECTABLE) && !this.reflected && targets.some(t => t.hasAbilityWithAttr(ReflectStatusMoveAbAttr) || !!t.getTag(BattlerTagType.MAGIC_COAT));
/**
* If no targets are left for the move to hit (FAIL), or the invoked move is non-reflectable, single-target
* (and not random target) and failed the hit check against its target (MISS), log the move
* as FAILed or MISSed (depending on the conditions above) and end this phase.
*/
if (!hasActiveTargets || (!mayBounce && !move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) {
this.stopMultiHit();
if (hasActiveTargets) {
globalScene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" }));
moveHistoryEntry.result = MoveResult.MISS;
applyMoveAttrs(MissEffectAttr, user, null, this.move.getMove());
} else {
globalScene.queueMessage(i18next.t("battle:attackFailed"));
moveHistoryEntry.result = MoveResult.FAIL;
} }
user.pushMoveHistory(moveHistoryEntry);
return this.end();
}
user.lapseTags(BattlerTagLapseType.MOVE_EFFECT); const playOnEmptyField = globalScene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false;
// Move animation only needs one target
// If the user is acting again (such as due to Instruct), reset hitsLeft/hitCount so that new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex(), playOnEmptyField).play(
// the move executes correctly (ensures all hits of a multi-hit are properly calculated) move.hitsSubstitute(user, this.getFirstTarget()!),
if (user.turnData.hitsLeft === 0 && user.turnData.hitCount > 0 && user.turnData.extraTurns > 0) { () => {
user.turnData.hitsLeft = -1;
user.turnData.hitCount = 0;
user.turnData.extraTurns--;
}
/**
* If this phase is for the first hit of the invoked move,
* resolve the move's total hit count. This block combines the
* effects of the move itself, Parental Bond, and Multi-Lens to do so.
*/
if (user.turnData.hitsLeft === -1) {
const hitCount = new NumberHolder(1);
// Assume single target for multi hit
applyMoveAttrs(MultiHitAttr, user, this.getFirstTarget() ?? null, move, hitCount);
// If Parental Bond is applicable, add another hit
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, hitCount, null);
// If Multi-Lens is applicable, add hits equal to the number of held Multi-Lenses
globalScene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, move.id, hitCount);
// Set the user's relevant turnData fields to reflect the final hit count
user.turnData.hitCount = hitCount.value;
user.turnData.hitsLeft = hitCount.value;
}
/**
* Log to be entered into the user's move history once the move result is resolved.
* Note that `result` (a {@linkcode MoveResult}) logs whether the move was successfully
* used in the sense of "Does it have an effect on the user?".
*/
const moveHistoryEntry = { move: this.move.moveId, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual };
/**
* Stores results of hit checks of the invoked move against all targets, organized by battler index.
* @see {@linkcode hitCheck}
*/
const targetHitChecks = Object.fromEntries(targets.map(p => [ p.getBattlerIndex(), this.hitCheck(p) ]));
const hasActiveTargets = targets.some(t => t.isActive(true));
/** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */
const isImmune = targets[0]?.hasAbilityWithAttr(TypeImmunityAbAttr)
&& (targets[0]?.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
&& !targets[0]?.getTag(SemiInvulnerableTag);
const mayBounce = move.hasFlag(MoveFlags.REFLECTABLE) && !this.reflected && targets.some(t => t.hasAbilityWithAttr(ReflectStatusMoveAbAttr) || !!t.getTag(BattlerTagType.MAGIC_COAT));
/**
* If no targets are left for the move to hit (FAIL), or the invoked move is non-reflectable, single-target
* (and not random target) and failed the hit check against its target (MISS), log the move
* as FAILed or MISSed (depending on the conditions above) and end this phase.
*/
if (!hasActiveTargets || (!mayBounce && !move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) {
this.stopMultiHit();
if (hasActiveTargets) {
globalScene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" }));
moveHistoryEntry.result = MoveResult.MISS;
applyMoveAttrs(MissEffectAttr, user, null, this.move.getMove());
} else {
globalScene.queueMessage(i18next.t("battle:attackFailed"));
moveHistoryEntry.result = MoveResult.FAIL;
}
user.pushMoveHistory(moveHistoryEntry);
return this.end();
}
/** All move effect attributes are chained together in this array to be applied asynchronously. */
const applyAttrs: Promise<void>[] = [];
const playOnEmptyField = globalScene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false;
// Move animation only needs one target
new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex(), playOnEmptyField).play(move.hitsSubstitute(user, this.getFirstTarget()!), () => {
/** Has the move successfully hit a target (for damage) yet? */ /** Has the move successfully hit a target (for damage) yet? */
let hasHit: boolean = false; let hasHit: boolean = false;
@ -313,7 +313,7 @@ export class MoveEffectPhase extends PokemonPhase {
} }
/** Does this phase represent the invoked move's first strike? */ /** Does this phase represent the invoked move's first strike? */
const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount); const firstHit = user.turnData.hitsLeft === user.turnData.hitCount;
// Only log the move's result on the first strike // Only log the move's result on the first strike
if (firstHit) { if (firstHit) {
@ -363,7 +363,7 @@ export class MoveEffectPhase extends PokemonPhase {
} }
/** Does this phase represent the invoked move's last strike? */ /** Does this phase represent the invoked move's last strike? */
const lastHit = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()); const lastHit = user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive();
/** /**
* If the user can change forms by using the invoked move, * If the user can change forms by using the invoked move,
@ -381,43 +381,29 @@ export class MoveEffectPhase extends PokemonPhase {
} }
} }
/** applyFilteredMoveAttrs(
* Create a Promise that applies *all* effects from the invoked move's MoveEffectAttrs. (attr: MoveAttr) =>
* These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger attr instanceof MoveEffectAttr &&
* type requires different conditions to be met with respect to the move's hit result. attr.trigger === MoveEffectTrigger.PRE_APPLY &&
*/ (!attr.firstHitOnly || firstHit) &&
const k = new Promise<void>((resolve) => { (!attr.lastHitOnly || lastHit) &&
//Start promise chain and apply PRE_APPLY move attributes hitResult !== HitResult.NO_EFFECT,
let promiseChain: Promise<void | null> = applyFilteredMoveAttrs((attr: MoveAttr) => user,
attr instanceof MoveEffectAttr target,
&& attr.trigger === MoveEffectTrigger.PRE_APPLY move,
&& (!attr.firstHitOnly || firstHit) );
&& (!attr.lastHitOnly || lastHit)
&& hitResult !== HitResult.NO_EFFECT, user, target, move);
/** Don't complete if the move failed */ if (hitResult !== HitResult.FAIL) {
if (hitResult === HitResult.FAIL) { this.applySelfTargetEffects(user, target, firstHit, lastHit);
return resolve();
}
/** Apply Move/Ability Effects in correct order */
promiseChain = promiseChain
.then(this.applySelfTargetEffects(user, target, firstHit, lastHit));
if (hitResult !== HitResult.NO_EFFECT) { if (hitResult !== HitResult.NO_EFFECT) {
promiseChain this.applyPostApplyEffects(user, target, firstHit, lastHit);
.then(this.applyPostApplyEffects(user, target, firstHit, lastHit)) this.applyHeldItemFlinchCheck(user, target, dealsDamage);
.then(this.applyHeldItemFlinchCheck(user, target, dealsDamage)) this.applySuccessfulAttackEffects(user, target, firstHit, lastHit, !!isProtected, hitResult, firstTarget);
.then(this.applySuccessfulAttackEffects(user, target, firstHit, lastHit, !!isProtected, hitResult, firstTarget))
.then(() => resolve());
} else { } else {
promiseChain applyMoveAttrs(NoEffectAttr, user, null, move);
.then(() => applyMoveAttrs(NoEffectAttr, user, null, move))
.then(resolve);
} }
}); }
applyAttrs.push(k);
} }
// Apply queued phases // Apply queued phases
@ -425,41 +411,35 @@ export class MoveEffectPhase extends PokemonPhase {
globalScene.appendToPhase(queuedPhases, MoveEndPhase); globalScene.appendToPhase(queuedPhases, MoveEndPhase);
} }
// Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved // Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved
const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ? if (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) {
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) : applyFilteredMoveAttrs(
null; (attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET,
user,
if (postTarget) { null,
if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after move,
applyAttrs[applyAttrs.length - 1].then(() => postTarget); );
} else { // Otherwise, push a new asynchronous move effect
applyAttrs.push(postTarget);
}
} }
// Wait for all move effects to finish applying, then end this phase /**
Promise.allSettled(applyAttrs).then(() => { * Remove the target's substitute (if it exists and has expired)
/** * after all targeted effects have applied.
* Remove the target's substitute (if it exists and has expired) * This prevents blocked effects from applying until after this hit resolves.
* after all targeted effects have applied. */
* This prevents blocked effects from applying until after this hit resolves. targets.forEach((target) => {
*/ const substitute = target.getTag(SubstituteTag);
targets.forEach(target => { if (substitute && substitute.hp <= 0) {
const substitute = target.getTag(SubstituteTag); target.lapseTag(BattlerTagType.SUBSTITUTE);
if (substitute && substitute.hp <= 0) {
target.lapseTag(BattlerTagType.SUBSTITUTE);
}
});
const moveType = user.getMoveType(move, true);
if (move.category !== MoveCategory.STATUS && !user.stellarTypesBoosted.includes(moveType)) {
user.stellarTypesBoosted.push(moveType);
} }
this.end();
}); });
});
}); const moveType = user.getMoveType(move, true);
if (move.category !== MoveCategory.STATUS && !user.stellarTypesBoosted.includes(moveType)) {
user.stellarTypesBoosted.push(moveType);
}
this.end();
},
);
} }
public override end(): void { public override end(): void {
@ -500,8 +480,8 @@ export class MoveEffectPhase extends PokemonPhase {
* @param lastHit - `true` if this is the last hit in a multi-hit attack * @param lastHit - `true` if this is the last hit in a multi-hit attack
* @returns a function intended to be passed into a `then()` call. * @returns a function intended to be passed into a `then()` call.
*/ */
protected applySelfTargetEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise<void | null> { protected applySelfTargetEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): void {
return () => applyFilteredMoveAttrs((attr: MoveAttr) => applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.POST_APPLY && attr.trigger === MoveEffectTrigger.POST_APPLY
&& attr.selfTarget && attr.selfTarget
@ -518,8 +498,8 @@ export class MoveEffectPhase extends PokemonPhase {
* @param lastHit - `true` if this is the last hit in a multi-hit attack * @param lastHit - `true` if this is the last hit in a multi-hit attack
* @returns a function intended to be passed into a `then()` call. * @returns a function intended to be passed into a `then()` call.
*/ */
protected applyPostApplyEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise<void | null> { protected applyPostApplyEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): void {
return () => applyFilteredMoveAttrs((attr: MoveAttr) => applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.POST_APPLY && attr.trigger === MoveEffectTrigger.POST_APPLY
&& !attr.selfTarget && !attr.selfTarget
@ -537,8 +517,8 @@ export class MoveEffectPhase extends PokemonPhase {
* @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move} * @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move}
* @returns a function intended to be passed into a `then()` call. * @returns a function intended to be passed into a `then()` call.
*/ */
protected applyOnHitEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, firstTarget: boolean): Promise<void> { protected applyOnHitEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, firstTarget: boolean): void {
return applyFilteredMoveAttrs((attr: MoveAttr) => applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.HIT && attr.trigger === MoveEffectTrigger.HIT
&& (!attr.firstHitOnly || firstHit) && (!attr.firstHitOnly || firstHit)
@ -554,21 +534,18 @@ export class MoveEffectPhase extends PokemonPhase {
* @param hitResult - The {@linkcode HitResult} of the attempted move * @param hitResult - The {@linkcode HitResult} of the attempted move
* @returns a `Promise` intended to be passed into a `then()` call. * @returns a `Promise` intended to be passed into a `then()` call.
*/ */
protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): Promise<void | null> { protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): void {
return executeIf(!target.isFainted() || target.canApplyAbility(), () => if (!target.isFainted() || target.canApplyAbility()) {
applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult) applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult);
.then(() => {
if (!this.move.getMove().hitsSubstitute(user, target)) { if (!this.move.getMove().hitsSubstitute(user, target)) {
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
globalScene.applyShuffledModifiers(EnemyAttackStatusEffectChanceModifier, false, target); globalScene.applyShuffledModifiers(EnemyAttackStatusEffectChanceModifier, false, target);
} }
target.lapseTags(BattlerTagLapseType.AFTER_HIT); target.lapseTags(BattlerTagLapseType.AFTER_HIT);
} }
}
})
);
} }
/** /**
@ -583,17 +560,15 @@ export class MoveEffectPhase extends PokemonPhase {
* @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move} * @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move}
* @returns a function intended to be passed into a `then()` call. * @returns a function intended to be passed into a `then()` call.
*/ */
protected applySuccessfulAttackEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, isProtected : boolean, hitResult: HitResult, firstTarget: boolean) : () => Promise<void | null> { protected applySuccessfulAttackEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean, isProtected: boolean, hitResult: HitResult, firstTarget: boolean): void {
return () => executeIf(!isProtected, () => if (!isProtected) {
this.applyOnHitEffects(user, target, firstHit, lastHit, firstTarget).then(() => this.applyOnHitEffects(user, target, firstHit, lastHit, firstTarget);
this.applyOnGetHitAbEffects(user, target, hitResult)).then(() => this.applyOnGetHitAbEffects(user, target, hitResult);
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult)).then(() => { // Item Stealing Effects applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult);
if (this.move.getMove() instanceof AttackMove) {
if (this.move.getMove() instanceof AttackMove) { globalScene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
globalScene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); }
} }
})
);
} }
/** /**
@ -603,20 +578,18 @@ export class MoveEffectPhase extends PokemonPhase {
* @param dealsDamage - `true` if the attempted move successfully dealt damage * @param dealsDamage - `true` if the attempted move successfully dealt damage
* @returns a function intended to be passed into a `then()` call. * @returns a function intended to be passed into a `then()` call.
*/ */
protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : () => void { protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : void {
return () => { if (this.move.getMove().hasAttr(FlinchAttr)) {
if (this.move.getMove().hasAttr(FlinchAttr)) { return;
return; }
}
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) { if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) {
const flinched = new BooleanHolder(false); const flinched = new BooleanHolder(false);
globalScene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); globalScene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
if (flinched.value) { if (flinched.value) {
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id); target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
}
} }
}; }
} }
/** /**

View File

@ -22,9 +22,8 @@ export class MoveHeaderPhase extends BattlePhase {
super.start(); super.start();
if (this.canMove()) { if (this.canMove()) {
applyMoveAttrs(MoveHeaderAttr, this.pokemon, null, this.move.getMove()).then(() => this.end()); applyMoveAttrs(MoveHeaderAttr, this.pokemon, null, this.move.getMove());
} else {
this.end();
} }
this.end();
} }
} }

View File

@ -56,6 +56,7 @@ export class MovePhase extends BattlePhase {
protected _targets: BattlerIndex[]; protected _targets: BattlerIndex[];
protected followUp: boolean; protected followUp: boolean;
protected ignorePp: boolean; protected ignorePp: boolean;
protected forcedLast: boolean;
protected failed: boolean = false; protected failed: boolean = false;
protected cancelled: boolean = false; protected cancelled: boolean = false;
protected reflected: boolean = false; protected reflected: boolean = false;
@ -90,7 +91,8 @@ export class MovePhase extends BattlePhase {
* @param reflected Indicates that the move was reflected by Magic Coat or Magic Bounce. * @param reflected Indicates that the move was reflected by Magic Coat or Magic Bounce.
* Reflected moves cannot be reflected again and will not trigger Dancer. * Reflected moves cannot be reflected again and will not trigger Dancer.
*/ */
constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp: boolean = false, ignorePp: boolean = false, reflected: boolean = false) {
constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp: boolean = false, ignorePp: boolean = false, reflected: boolean = false, forcedLast: boolean = false) {
super(); super();
this.pokemon = pokemon; this.pokemon = pokemon;
@ -99,6 +101,7 @@ export class MovePhase extends BattlePhase {
this.followUp = followUp; this.followUp = followUp;
this.ignorePp = ignorePp; this.ignorePp = ignorePp;
this.reflected = reflected; this.reflected = reflected;
this.forcedLast = forcedLast;
} }
/** /**
@ -120,6 +123,15 @@ export class MovePhase extends BattlePhase {
this.cancelled = true; this.cancelled = true;
} }
/**
* Shows whether the current move has been forced to the end of the turn
* Needed for speed order, see {@linkcode Moves.QUASH}
* */
public isForcedLast(): boolean {
return this.forcedLast;
}
public start(): void { public start(): void {
super.start(); super.start();

View File

@ -0,0 +1,77 @@
import type { BattlerIndex } from "#app/battle";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import { EFFECTIVE_STATS, BATTLE_STATS } from "#enums/stat";
import { PokemonMove } from "#app/field/pokemon";
import { globalScene } from "#app/global-scene";
import { PokemonPhase } from "./pokemon-phase";
/**
* Transforms a Pokemon into another Pokemon on the field.
* Used for Transform (move) and Imposter (ability)
*/
export class PokemonTransformPhase extends PokemonPhase {
protected targetIndex: BattlerIndex;
private playSound: boolean;
constructor(userIndex: BattlerIndex, targetIndex: BattlerIndex, playSound: boolean = false) {
super(userIndex);
this.targetIndex = targetIndex;
this.playSound = playSound;
}
public override start(): void {
const user = this.getPokemon();
const target = globalScene.getField(true).find((p) => p.getBattlerIndex() === this.targetIndex);
if (!target) {
return this.end();
}
user.summonData.speciesForm = target.getSpeciesForm();
user.summonData.ability = target.getAbility().id;
user.summonData.gender = target.getGender();
// Power Trick's effect is removed after using Transform
user.removeTag(BattlerTagType.POWER_TRICK);
// Copy all stats (except HP)
for (const s of EFFECTIVE_STATS) {
user.setStat(s, target.getStat(s, false), false);
}
// Copy all stat stages
for (const s of BATTLE_STATS) {
user.setStatStage(s, target.getStatStage(s));
}
user.summonData.moveset = target.getMoveset().map((m) => {
if (m) {
// If PP value is less than 5, do nothing. If greater, we need to reduce the value to 5.
return new PokemonMove(m.moveId, 0, 0, false, Math.min(m.getMove().pp, 5));
} else {
console.warn(`Transform: somehow iterating over a ${m} value when copying moveset!`);
return new PokemonMove(Moves.NONE);
}
});
user.summonData.types = target.getTypes();
const promises = [ user.updateInfo() ];
if (this.playSound) {
globalScene.playSound("battle_anims/PRSFX- Transform");
}
promises.push(
user.loadAssets(false).then(() => {
user.playAnim();
user.updateInfo();
// If the new ability activates immediately, it needs to happen after all the transform animations
user.setTempAbility(target.getAbility());
}),
);
Promise.allSettled(promises).then(() => this.end());
}
}

View File

@ -27,12 +27,10 @@ export class PostSummonPhase extends PokemonPhase {
pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON); pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON);
} }
applyPostSummonAbAttrs(PostSummonAbAttr, pokemon) applyPostSummonAbAttrs(PostSummonAbAttr, pokemon);
.then(() => { const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); field.forEach((p) => applyAbAttrs(CommanderAbAttr, p, null, false));
field.forEach((p) => applyAbAttrs(CommanderAbAttr, p, null, false));
this.end(); this.end();
});
} }
} }

View File

@ -0,0 +1,61 @@
import { SwitchType } from "#enums/switch-type";
import { globalScene } from "#app/global-scene";
import type { PartyOption } from "#app/ui/party-ui-handler";
import PartyUiHandler, { PartyUiMode } from "#app/ui/party-ui-handler";
import { Mode } from "#app/ui/ui";
import i18next from "i18next";
import * as Utils from "#app/utils";
import { BattlePhase } from "#app/phases/battle-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase";
import type { PlayerPokemon } from "#app/field/pokemon";
/**
* Sets the Party UI and handles the effect of Revival Blessing
* when used by one of the player's Pokemon.
*/
export class RevivalBlessingPhase extends BattlePhase {
constructor(protected user: PlayerPokemon) {
super();
}
public override start(): void {
globalScene.ui.setMode(
Mode.PARTY,
PartyUiMode.REVIVAL_BLESSING,
this.user.getFieldIndex(),
(slotIndex: integer, option: PartyOption) => {
if (slotIndex >= 0 && slotIndex < 6) {
const pokemon = globalScene.getPlayerParty()[slotIndex];
if (!pokemon || !pokemon.isFainted()) {
return this.end();
}
pokemon.resetTurnData();
pokemon.resetStatus();
pokemon.heal(Math.min(Utils.toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp()));
globalScene.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: pokemon.name }), 0, true);
if (globalScene.currentBattle.double && globalScene.getPlayerParty().length > 1) {
const allyPokemon = this.user.getAlly();
if (slotIndex <= 1) {
// Revived ally pokemon
globalScene.unshiftPhase(
new SwitchSummonPhase(SwitchType.SWITCH, pokemon.getFieldIndex(), slotIndex, false, true),
);
globalScene.unshiftPhase(new ToggleDoublePositionPhase(true));
} else if (allyPokemon.isFainted()) {
// Revived party pokemon, and ally pokemon is fainted
globalScene.unshiftPhase(
new SwitchSummonPhase(SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, true),
);
globalScene.unshiftPhase(new ToggleDoublePositionPhase(true));
}
}
}
globalScene.ui.setMode(Mode.MESSAGE).then(() => this.end());
},
PartyUiHandler.FilterFainted,
);
}
}

View File

@ -17,17 +17,16 @@ export class RibbonModifierRewardPhase extends ModifierRewardPhase {
doReward(): Promise<void> { doReward(): Promise<void> {
return new Promise<void>(resolve => { return new Promise<void>(resolve => {
const newModifier = this.modifierType.newModifier(); const newModifier = this.modifierType.newModifier();
globalScene.addModifier(newModifier).then(() => { globalScene.addModifier(newModifier);
globalScene.playSound("level_up_fanfare"); globalScene.playSound("level_up_fanfare");
globalScene.ui.setMode(Mode.MESSAGE); globalScene.ui.setMode(Mode.MESSAGE);
globalScene.ui.showText(i18next.t("battle:beatModeFirstTime", { globalScene.ui.showText(i18next.t("battle:beatModeFirstTime", {
speciesName: this.species.name, speciesName: this.species.name,
gameMode: globalScene.gameMode.getName(), gameMode: globalScene.gameMode.getName(),
newModifier: newModifier?.type.name newModifier: newModifier?.type.name,
}), null, () => { }), null, () => {
resolve(); resolve();
}, null, true, 1500); }, null, true, 1500);
});
}); });
} }
} }

View File

@ -171,30 +171,21 @@ export class SelectModifierPhase extends BattlePhase {
} }
if (cost && !(modifier.type instanceof RememberMoveModifierType)) { if (cost && !(modifier.type instanceof RememberMoveModifierType)) {
result.then(success => { if (result) {
if (success) { if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) {
if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) { globalScene.money -= cost;
globalScene.money -= cost; globalScene.updateMoneyText();
globalScene.updateMoneyText(); globalScene.animateMoneyChanged(false);
globalScene.animateMoneyChanged(false);
}
globalScene.playSound("se/buy");
(globalScene.ui.getHandler() as ModifierSelectUiHandler).updateCostText();
} else {
globalScene.ui.playError();
} }
}); globalScene.playSound("se/buy");
} else { (globalScene.ui.getHandler() as ModifierSelectUiHandler).updateCostText();
const doEnd = () => {
globalScene.ui.clearText();
globalScene.ui.setMode(Mode.MESSAGE);
super.end();
};
if (result instanceof Promise) {
result.then(() => doEnd());
} else { } else {
doEnd(); globalScene.ui.playError();
} }
} else {
globalScene.ui.clearText();
globalScene.ui.setMode(Mode.MESSAGE);
super.end();
} }
}; };
@ -304,7 +295,7 @@ export class SelectModifierPhase extends BattlePhase {
); );
} }
addModifier(modifier: Modifier): Promise<boolean> { addModifier(modifier: Modifier): boolean {
return globalScene.addModifier(modifier, false, true); return globalScene.addModifier(modifier, false, true);
} }
} }

View File

@ -12,6 +12,7 @@ import type { Starter } from "#app/ui/starter-select-ui-handler";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
import type { Species } from "#enums/species"; import type { Species } from "#enums/species";
import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
import * as Utils from "../utils";
export class SelectStarterPhase extends Phase { export class SelectStarterPhase extends Phase {
@ -79,6 +80,12 @@ export class SelectStarterPhase extends Phase {
starterPokemon.nickname = starter.nickname; starterPokemon.nickname = starter.nickname;
} }
if (!Utils.isNullOrUndefined(starter.teraType)) {
starterPokemon.teraType = starter.teraType;
} else {
starterPokemon.teraType = starterPokemon.species.type1;
}
if (globalScene.gameMode.isSplicedOnly || Overrides.STARTER_FUSION_OVERRIDE) { if (globalScene.gameMode.isSplicedOnly || Overrides.STARTER_FUSION_OVERRIDE) {
starterPokemon.generateFusionSpecies(true); starterPokemon.generateFusionSpecies(true);
} }

View File

@ -55,6 +55,7 @@ import { MysteryEncounterSaveData } from "#app/data/mystery-encounters/mystery-e
import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; import { pokerogueApi } from "#app/plugins/api/pokerogue-api";
import { ArenaTrapTag } from "#app/data/arena-tag"; import { ArenaTrapTag } from "#app/data/arena-tag";
import type { Type } from "#enums/type";
export const defaultStarterSpecies: Species[] = [ export const defaultStarterSpecies: Species[] = [
Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE,
@ -229,6 +230,7 @@ export interface StarterAttributes {
shiny?: boolean; shiny?: boolean;
favorite?: boolean; favorite?: boolean;
nickname?: string; nickname?: string;
tera?: Type;
} }
export interface StarterPreferences { export interface StarterPreferences {

View File

@ -21,7 +21,7 @@ export enum SettingGamepad {
Button_Cycle_Gender = "BUTTON_CYCLE_GENDER", Button_Cycle_Gender = "BUTTON_CYCLE_GENDER",
Button_Cycle_Ability = "BUTTON_CYCLE_ABILITY", Button_Cycle_Ability = "BUTTON_CYCLE_ABILITY",
Button_Cycle_Nature = "BUTTON_CYCLE_NATURE", Button_Cycle_Nature = "BUTTON_CYCLE_NATURE",
Button_Cycle_Variant = "BUTTON_CYCLE_VARIANT", Button_Cycle_Tera = "BUTTON_CYCLE_TERA",
Button_Speed_Up = "BUTTON_SPEED_UP", Button_Speed_Up = "BUTTON_SPEED_UP",
Button_Slow_Down = "BUTTON_SLOW_DOWN", Button_Slow_Down = "BUTTON_SLOW_DOWN",
Button_Submit = "BUTTON_SUBMIT", Button_Submit = "BUTTON_SUBMIT",
@ -45,7 +45,7 @@ export const settingGamepadOptions = {
[SettingGamepad.Button_Cycle_Gender]: [ `KEY ${Button.CYCLE_GENDER.toString()}`, pressAction ], [SettingGamepad.Button_Cycle_Gender]: [ `KEY ${Button.CYCLE_GENDER.toString()}`, pressAction ],
[SettingGamepad.Button_Cycle_Ability]: [ `KEY ${Button.CYCLE_ABILITY.toString()}`, pressAction ], [SettingGamepad.Button_Cycle_Ability]: [ `KEY ${Button.CYCLE_ABILITY.toString()}`, pressAction ],
[SettingGamepad.Button_Cycle_Nature]: [ `KEY ${Button.CYCLE_NATURE.toString()}`, pressAction ], [SettingGamepad.Button_Cycle_Nature]: [ `KEY ${Button.CYCLE_NATURE.toString()}`, pressAction ],
[SettingGamepad.Button_Cycle_Variant]: [ `KEY ${Button.V.toString()}`, pressAction ], [SettingGamepad.Button_Cycle_Tera]: [ `KEY ${Button.CYCLE_TERA.toString()}`, pressAction ],
[SettingGamepad.Button_Speed_Up]: [ `KEY ${Button.SPEED_UP.toString()}`, pressAction ], [SettingGamepad.Button_Speed_Up]: [ `KEY ${Button.SPEED_UP.toString()}`, pressAction ],
[SettingGamepad.Button_Slow_Down]: [ `KEY ${Button.SLOW_DOWN.toString()}`, pressAction ], [SettingGamepad.Button_Slow_Down]: [ `KEY ${Button.SLOW_DOWN.toString()}`, pressAction ],
[SettingGamepad.Button_Submit]: [ `KEY ${Button.SUBMIT.toString()}`, pressAction ], [SettingGamepad.Button_Submit]: [ `KEY ${Button.SUBMIT.toString()}`, pressAction ],
@ -67,7 +67,7 @@ export const settingGamepadDefaults = {
[SettingGamepad.Button_Cycle_Gender]: 0, [SettingGamepad.Button_Cycle_Gender]: 0,
[SettingGamepad.Button_Cycle_Ability]: 0, [SettingGamepad.Button_Cycle_Ability]: 0,
[SettingGamepad.Button_Cycle_Nature]: 0, [SettingGamepad.Button_Cycle_Nature]: 0,
[SettingGamepad.Button_Cycle_Variant]: 0, [SettingGamepad.Button_Cycle_Tera]: 0,
[SettingGamepad.Button_Speed_Up]: 0, [SettingGamepad.Button_Speed_Up]: 0,
[SettingGamepad.Button_Slow_Down]: 0, [SettingGamepad.Button_Slow_Down]: 0,
[SettingGamepad.Button_Submit]: 0, [SettingGamepad.Button_Submit]: 0,
@ -96,7 +96,7 @@ export function setSettingGamepad(setting: SettingGamepad, value: number): boole
case SettingGamepad.Button_Cycle_Gender: case SettingGamepad.Button_Cycle_Gender:
case SettingGamepad.Button_Cycle_Ability: case SettingGamepad.Button_Cycle_Ability:
case SettingGamepad.Button_Cycle_Nature: case SettingGamepad.Button_Cycle_Nature:
case SettingGamepad.Button_Cycle_Variant: case SettingGamepad.Button_Cycle_Tera:
case SettingGamepad.Button_Speed_Up: case SettingGamepad.Button_Speed_Up:
case SettingGamepad.Button_Slow_Down: case SettingGamepad.Button_Slow_Down:
case SettingGamepad.Button_Submit: case SettingGamepad.Button_Submit:

View File

@ -32,8 +32,8 @@ export enum SettingKeyboard {
Alt_Button_Cycle_Ability = "ALT_BUTTON_CYCLE_ABILITY", Alt_Button_Cycle_Ability = "ALT_BUTTON_CYCLE_ABILITY",
Button_Cycle_Nature = "BUTTON_CYCLE_NATURE", Button_Cycle_Nature = "BUTTON_CYCLE_NATURE",
Alt_Button_Cycle_Nature = "ALT_BUTTON_CYCLE_NATURE", Alt_Button_Cycle_Nature = "ALT_BUTTON_CYCLE_NATURE",
Button_Cycle_Variant = "BUTTON_CYCLE_VARIANT", Button_Cycle_Tera = "BUTTON_CYCLE_TERA",
Alt_Button_Cycle_Variant = "ALT_BUTTON_CYCLE_VARIANT", Alt_Button_Cycle_Tera = "ALT_BUTTON_CYCLE_TERA",
Button_Speed_Up = "BUTTON_SPEED_UP", Button_Speed_Up = "BUTTON_SPEED_UP",
Alt_Button_Speed_Up = "ALT_BUTTON_SPEED_UP", Alt_Button_Speed_Up = "ALT_BUTTON_SPEED_UP",
Button_Slow_Down = "BUTTON_SLOW_DOWN", Button_Slow_Down = "BUTTON_SLOW_DOWN",
@ -73,8 +73,8 @@ export const settingKeyboardOptions = {
[SettingKeyboard.Alt_Button_Cycle_Ability]: [ `KEY ${Button.CYCLE_ABILITY.toString()}`, pressAction ], [SettingKeyboard.Alt_Button_Cycle_Ability]: [ `KEY ${Button.CYCLE_ABILITY.toString()}`, pressAction ],
[SettingKeyboard.Button_Cycle_Nature]: [ `KEY ${Button.CYCLE_NATURE.toString()}`, pressAction ], [SettingKeyboard.Button_Cycle_Nature]: [ `KEY ${Button.CYCLE_NATURE.toString()}`, pressAction ],
[SettingKeyboard.Alt_Button_Cycle_Nature]: [ `KEY ${Button.CYCLE_NATURE.toString()}`, pressAction ], [SettingKeyboard.Alt_Button_Cycle_Nature]: [ `KEY ${Button.CYCLE_NATURE.toString()}`, pressAction ],
[SettingKeyboard.Button_Cycle_Variant]: [ `KEY ${Button.V.toString()}`, pressAction ], [SettingKeyboard.Button_Cycle_Tera]: [ `KEY ${Button.CYCLE_TERA.toString()}`, pressAction ],
[SettingKeyboard.Alt_Button_Cycle_Variant]: [ `KEY ${Button.V.toString()}`, pressAction ], [SettingKeyboard.Alt_Button_Cycle_Tera]: [ `KEY ${Button.CYCLE_TERA.toString()}`, pressAction ],
[SettingKeyboard.Button_Speed_Up]: [ `KEY ${Button.SPEED_UP.toString()}`, pressAction ], [SettingKeyboard.Button_Speed_Up]: [ `KEY ${Button.SPEED_UP.toString()}`, pressAction ],
[SettingKeyboard.Alt_Button_Speed_Up]: [ `KEY ${Button.SPEED_UP.toString()}`, pressAction ], [SettingKeyboard.Alt_Button_Speed_Up]: [ `KEY ${Button.SPEED_UP.toString()}`, pressAction ],
[SettingKeyboard.Button_Slow_Down]: [ `KEY ${Button.SLOW_DOWN.toString()}`, pressAction ], [SettingKeyboard.Button_Slow_Down]: [ `KEY ${Button.SLOW_DOWN.toString()}`, pressAction ],
@ -112,8 +112,8 @@ export const settingKeyboardDefaults = {
[SettingKeyboard.Alt_Button_Cycle_Ability]: 0, [SettingKeyboard.Alt_Button_Cycle_Ability]: 0,
[SettingKeyboard.Button_Cycle_Nature]: 0, [SettingKeyboard.Button_Cycle_Nature]: 0,
[SettingKeyboard.Alt_Button_Cycle_Nature]: 0, [SettingKeyboard.Alt_Button_Cycle_Nature]: 0,
[SettingKeyboard.Button_Cycle_Variant]: 0, [SettingKeyboard.Button_Cycle_Tera]: 0,
[SettingKeyboard.Alt_Button_Cycle_Variant]: 0, [SettingKeyboard.Alt_Button_Cycle_Tera]: 0,
[SettingKeyboard.Button_Speed_Up]: 0, [SettingKeyboard.Button_Speed_Up]: 0,
[SettingKeyboard.Alt_Button_Speed_Up]: 0, [SettingKeyboard.Alt_Button_Speed_Up]: 0,
[SettingKeyboard.Button_Slow_Down]: 0, [SettingKeyboard.Button_Slow_Down]: 0,
@ -148,7 +148,7 @@ export function setSettingKeyboard(setting: SettingKeyboard, value: number): boo
case SettingKeyboard.Button_Cycle_Gender: case SettingKeyboard.Button_Cycle_Gender:
case SettingKeyboard.Button_Cycle_Ability: case SettingKeyboard.Button_Cycle_Ability:
case SettingKeyboard.Button_Cycle_Nature: case SettingKeyboard.Button_Cycle_Nature:
case SettingKeyboard.Button_Cycle_Variant: case SettingKeyboard.Button_Cycle_Tera:
case SettingKeyboard.Button_Speed_Up: case SettingKeyboard.Button_Speed_Up:
case SettingKeyboard.Button_Slow_Down: case SettingKeyboard.Button_Slow_Down:
case SettingKeyboard.Alt_Button_Up: case SettingKeyboard.Alt_Button_Up:
@ -164,7 +164,7 @@ export function setSettingKeyboard(setting: SettingKeyboard, value: number): boo
case SettingKeyboard.Alt_Button_Cycle_Gender: case SettingKeyboard.Alt_Button_Cycle_Gender:
case SettingKeyboard.Alt_Button_Cycle_Ability: case SettingKeyboard.Alt_Button_Cycle_Ability:
case SettingKeyboard.Alt_Button_Cycle_Nature: case SettingKeyboard.Alt_Button_Cycle_Nature:
case SettingKeyboard.Alt_Button_Cycle_Variant: case SettingKeyboard.Alt_Button_Cycle_Tera:
case SettingKeyboard.Alt_Button_Speed_Up: case SettingKeyboard.Alt_Button_Speed_Up:
case SettingKeyboard.Alt_Button_Slow_Down: case SettingKeyboard.Alt_Button_Slow_Down:
case SettingKeyboard.Alt_Button_Submit: case SettingKeyboard.Alt_Button_Submit:

View File

@ -391,7 +391,7 @@ describe("Abilities - Unburden", () => {
await game.forceEnemyMove(Moves.THIEF, BattlerIndex.PLAYER); await game.forceEnemyMove(Moves.THIEF, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.SPLASH); await game.forceEnemyMove(Moves.SPLASH);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2 ]); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2 ]);
game.doSelectPartyPokemon(0, "MoveEffectPhase"); game.doSelectPartyPokemon(0, "RevivalBlessingPhase");
await game.toNextTurn(); await game.toNextTurn();
expect(game.scene.getPlayerField()[0]).toBe(treecko); expect(game.scene.getPlayerField()[0]).toBe(treecko);

View File

@ -1,4 +1,6 @@
import { allMoves } from "#app/data/move"; import { allMoves } from "#app/data/move";
import type { EnemyPersistentModifier } from "#app/modifier/modifier";
import { modifierTypes } from "#app/modifier/modifier-type";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
@ -65,6 +67,28 @@ describe("Battle Mechanics - Damage Calculation", () => {
expect(aggron.hp).toBe(aggron.getMaxHp() - 1); expect(aggron.hp).toBe(aggron.getMaxHp() - 1);
}); });
it("Attacks deal 1 damage at minimum even with many tokens", async () => {
game.override
.startingLevel(1)
.enemySpecies(Species.AGGRON)
.enemyAbility(Abilities.STURDY)
.enemyLevel(10000);
await game.classicMode.startBattle([ Species.SHUCKLE ]);
const dmg_redux_modifier = modifierTypes.ENEMY_DAMAGE_REDUCTION().newModifier() as EnemyPersistentModifier;
dmg_redux_modifier.stackCount = 1000;
await game.scene.addEnemyModifier(modifierTypes.ENEMY_DAMAGE_REDUCTION().newModifier() as EnemyPersistentModifier);
const aggron = game.scene.getEnemyPokemon()!;
game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to("BerryPhase", false);
expect(aggron.hp).toBe(aggron.getMaxHp() - 1);
});
it("Fixed-damage moves ignore damage multipliers", async () => { it("Fixed-damage moves ignore damage multipliers", async () => {
game.override game.override
.enemySpecies(Species.DRAGONITE) .enemySpecies(Species.DRAGONITE)

View File

@ -0,0 +1,99 @@
import { Species } from "#enums/species";
import { Moves } from "#enums/moves";
import { Abilities } from "#app/enums/abilities";
import { BattlerIndex } from "#app/battle";
import { WeatherType } from "#enums/weather-type";
import { MoveResult } from "#app/field/pokemon";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { describe, beforeAll, afterEach, beforeEach, it, expect } from "vitest";
describe("Moves - Quash", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.battleType("double")
.enemyLevel(1)
.enemySpecies(Species.SLOWPOKE)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset([ Moves.RAIN_DANCE, Moves.SPLASH ])
.ability(Abilities.BALL_FETCH)
.moveset([ Moves.QUASH, Moves.SUNNY_DAY, Moves.RAIN_DANCE, Moves.SPLASH ]);
});
it("makes the target move last in a turn, ignoring priority", async () => {
await game.classicMode.startBattle([ Species.ACCELGOR, Species.RATTATA ]);
game.move.select(Moves.QUASH, 0, BattlerIndex.PLAYER_2);
game.move.select(Moves.SUNNY_DAY, 1);
await game.forceEnemyMove(Moves.SPLASH);
await game.forceEnemyMove(Moves.RAIN_DANCE);
await game.phaseInterceptor.to("TurnEndPhase", false);
// will be sunny if player_2 moved last because of quash, rainy otherwise
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SUNNY);
});
it("fails if the target has already moved", async () => {
await game.classicMode.startBattle([ Species.ACCELGOR, Species.RATTATA ]);
game.move.select(Moves.SPLASH, 0);
game.move.select(Moves.QUASH, 1, BattlerIndex.PLAYER);
await game.phaseInterceptor.to("MoveEndPhase");
await game.phaseInterceptor.to("MoveEndPhase");
expect(game.scene.getPlayerField()[1].getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
});
it("makes multiple quashed targets move in speed order at the end of the turn", async () => {
game.override.enemySpecies(Species.NINJASK)
.enemyLevel(100);
await game.classicMode.startBattle([ Species.ACCELGOR, Species.RATTATA ]);
// both users are quashed - rattata is slower so sun should be up at end of turn
game.move.select(Moves.RAIN_DANCE, 0);
game.move.select(Moves.SUNNY_DAY, 1);
await game.forceEnemyMove(Moves.QUASH, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.QUASH, BattlerIndex.PLAYER_2);
await game.phaseInterceptor.to("TurnEndPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SUNNY);
});
it("respects trick room", async () => {
game.override.enemyMoveset([ Moves.RAIN_DANCE, Moves.SPLASH, Moves.TRICK_ROOM ]);
await game.classicMode.startBattle([ Species.ACCELGOR, Species.RATTATA ]);
game.move.select(Moves.SPLASH, 0);
game.move.select(Moves.SPLASH, 1);
await game.forceEnemyMove(Moves.TRICK_ROOM);
await game.forceEnemyMove(Moves.SPLASH);
await game.phaseInterceptor.to("TurnInitPhase");
// both users are quashed - accelgor should move last w/ TR so rain should be up at end of turn
game.move.select(Moves.RAIN_DANCE, 0);
game.move.select(Moves.SUNNY_DAY, 1);
await game.forceEnemyMove(Moves.QUASH, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.QUASH, BattlerIndex.PLAYER_2);
await game.phaseInterceptor.to("TurnEndPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.RAIN);
});
});

View File

@ -0,0 +1,117 @@
import { BattlerIndex } from "#app/battle";
import { MoveResult } from "#app/field/pokemon";
import { toDmgValue } from "#app/utils";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Revival Blessing", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.SPLASH, Moves.REVIVAL_BLESSING, Moves.MEMENTO ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should revive a selected fainted Pokemon when used by the player", async () => {
await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]);
game.move.select(Moves.MEMENTO);
game.doSelectPartyPokemon(1, "SwitchPhase");
await game.toNextTurn();
const player = game.scene.getPlayerPokemon()!;
expect(player.species.speciesId).toBe(Species.MAGIKARP);
game.move.select(Moves.REVIVAL_BLESSING);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
game.doSelectPartyPokemon(1, "RevivalBlessingPhase");
await game.phaseInterceptor.to("MoveEndPhase", false);
const revivedPokemon = game.scene.getPlayerParty()[1];
expect(revivedPokemon.status?.effect).toBeFalsy();
expect(revivedPokemon.hp).toBe(Math.floor(revivedPokemon.getMaxHp() / 2));
});
it("should revive a random fainted enemy when used by an enemy Trainer", async () => {
game.override.enemyMoveset(Moves.REVIVAL_BLESSING).startingWave(8);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.SPLASH);
await game.doKillOpponents();
await game.toNextTurn();
game.move.select(Moves.SPLASH);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("MoveEndPhase", false);
const revivedPokemon = game.scene.getEnemyParty()[1];
expect(revivedPokemon.status?.effect).toBeFalsy();
expect(revivedPokemon.hp).toBe(Math.floor(revivedPokemon.getMaxHp() / 2));
});
it("should fail when there are no fainted Pokemon to target", async () => {
await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]);
game.move.select(Moves.REVIVAL_BLESSING);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("MoveEndPhase", false);
const player = game.scene.getPlayerPokemon()!;
expect(player.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
it("should revive a player pokemon and immediately send it back out if used in the same turn it fainted in doubles", async () => {
game.override
.battleType("double")
.enemyMoveset([ Moves.SPLASH, Moves.FISSURE ])
.enemyAbility(Abilities.NO_GUARD)
.enemyLevel(100);
await game.classicMode.startBattle([ Species.FEEBAS, Species.MILOTIC, Species.GYARADOS ]);
const feebas = game.scene.getPlayerField()[0];
game.move.select(Moves.SPLASH);
game.move.select(Moves.REVIVAL_BLESSING, 1);
await game.forceEnemyMove(Moves.FISSURE, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.SPLASH);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2 ]);
await game.phaseInterceptor.to("MoveEndPhase");
await game.phaseInterceptor.to("MoveEndPhase");
expect(feebas.isFainted()).toBe(true);
game.doSelectPartyPokemon(0, "RevivalBlessingPhase");
await game.toNextTurn();
expect(feebas.isFainted()).toBe(false);
expect(feebas.hp).toBe(toDmgValue(0.5 * feebas.getMaxHp()));
expect(game.scene.getPlayerField()[0]).toBe(feebas);
});
});

View File

@ -375,6 +375,6 @@ describe("Clowning Around - Mystery Encounter", () => {
async function addItemToPokemon(scene: BattleScene, pokemon: Pokemon, stackCount: number, itemType: PokemonHeldItemModifierType) { async function addItemToPokemon(scene: BattleScene, pokemon: Pokemon, stackCount: number, itemType: PokemonHeldItemModifierType) {
const itemMod = itemType.newModifier(pokemon) as PokemonHeldItemModifier; const itemMod = itemType.newModifier(pokemon) as PokemonHeldItemModifier;
itemMod.stackCount = stackCount; itemMod.stackCount = stackCount;
await scene.addModifier(itemMod, true, false, false, true); scene.addModifier(itemMod, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
} }

View File

@ -123,8 +123,6 @@ describe("Dancing Lessons - Mystery Encounter", () => {
partyLead.level = 1000; partyLead.level = 1000;
partyLead.calculateStats(); partyLead.calculateStats();
await runMysteryEncounterToEnd(game, 1, undefined, true); await runMysteryEncounterToEnd(game, 1, undefined, true);
// For some reason updateModifiers breaks in this test and does not resolve promise
vi.spyOn(game.scene, "updateModifiers").mockImplementation(() => new Promise(resolve => resolve()));
await skipBattleRunMysteryEncounterRewardsPhase(game); await skipBattleRunMysteryEncounterRewardsPhase(game);
await game.phaseInterceptor.to(SelectModifierPhase, false); await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name);

View File

@ -123,7 +123,7 @@ describe("Delibird-y - Mystery Encounter", () => {
scene.modifiers = []; scene.modifiers = [];
const amuletCoin = generateModifierType(modifierTypes.AMULET_COIN)!.newModifier() as MoneyMultiplierModifier; const amuletCoin = generateModifierType(modifierTypes.AMULET_COIN)!.newModifier() as MoneyMultiplierModifier;
amuletCoin.stackCount = 5; amuletCoin.stackCount = 5;
await scene.addModifier(amuletCoin, true, false, false, true); scene.addModifier(amuletCoin, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 1); await runMysteryEncounterToEnd(game, 1);
@ -193,7 +193,7 @@ describe("Delibird-y - Mystery Encounter", () => {
const sitrus = generateModifierType(modifierTypes.BERRY, [ BerryType.SITRUS ])!; const sitrus = generateModifierType(modifierTypes.BERRY, [ BerryType.SITRUS ])!;
const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier;
sitrusMod.stackCount = 2; sitrusMod.stackCount = 2;
await scene.addModifier(sitrusMod, true, false, false, true); scene.addModifier(sitrusMod, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 });
@ -214,7 +214,7 @@ describe("Delibird-y - Mystery Encounter", () => {
const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!; const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!;
const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier;
modifier.stackCount = 1; modifier.stackCount = 1;
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 });
@ -234,13 +234,13 @@ describe("Delibird-y - Mystery Encounter", () => {
scene.modifiers = []; scene.modifiers = [];
const candyJar = generateModifierType(modifierTypes.CANDY_JAR)!.newModifier() as LevelIncrementBoosterModifier; const candyJar = generateModifierType(modifierTypes.CANDY_JAR)!.newModifier() as LevelIncrementBoosterModifier;
candyJar.stackCount = 99; candyJar.stackCount = 99;
await scene.addModifier(candyJar, true, false, false, true); scene.addModifier(candyJar, true, false, false, true);
const sitrus = generateModifierType(modifierTypes.BERRY, [ BerryType.SITRUS ])!; const sitrus = generateModifierType(modifierTypes.BERRY, [ BerryType.SITRUS ])!;
// Sitrus berries on party // Sitrus berries on party
const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier;
sitrusMod.stackCount = 2; sitrusMod.stackCount = 2;
await scene.addModifier(sitrusMod, true, false, false, true); scene.addModifier(sitrusMod, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 });
@ -263,13 +263,13 @@ describe("Delibird-y - Mystery Encounter", () => {
scene.modifiers = []; scene.modifiers = [];
const healingCharm = generateModifierType(modifierTypes.BERRY_POUCH)!.newModifier() as PreserveBerryModifier; const healingCharm = generateModifierType(modifierTypes.BERRY_POUCH)!.newModifier() as PreserveBerryModifier;
healingCharm.stackCount = 3; healingCharm.stackCount = 3;
await scene.addModifier(healingCharm, true, false, false, true); scene.addModifier(healingCharm, true, false, false, true);
// Set 1 Reviver Seed on party lead // Set 1 Reviver Seed on party lead
const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!; const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!;
const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier;
modifier.stackCount = 1; modifier.stackCount = 1;
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 });
@ -292,7 +292,7 @@ describe("Delibird-y - Mystery Encounter", () => {
scene.modifiers = []; scene.modifiers = [];
const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!;
const modifier = soulDew.newModifier(scene.getPlayerParty()[0]); const modifier = soulDew.newModifier(scene.getPlayerParty()[0]);
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await game.phaseInterceptor.to(MysteryEncounterPhase, false); await game.phaseInterceptor.to(MysteryEncounterPhase, false);
@ -321,7 +321,7 @@ describe("Delibird-y - Mystery Encounter", () => {
const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!; const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!;
const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier;
modifier.stackCount = 1; modifier.stackCount = 1;
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 });
@ -355,7 +355,7 @@ describe("Delibird-y - Mystery Encounter", () => {
const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!;
const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier;
modifier.stackCount = 2; modifier.stackCount = 2;
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
@ -376,7 +376,7 @@ describe("Delibird-y - Mystery Encounter", () => {
const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!;
const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier;
modifier.stackCount = 1; modifier.stackCount = 1;
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
@ -396,13 +396,13 @@ describe("Delibird-y - Mystery Encounter", () => {
scene.modifiers = []; scene.modifiers = [];
const healingCharm = generateModifierType(modifierTypes.HEALING_CHARM)!.newModifier() as HealingBoosterModifier; const healingCharm = generateModifierType(modifierTypes.HEALING_CHARM)!.newModifier() as HealingBoosterModifier;
healingCharm.stackCount = 5; healingCharm.stackCount = 5;
await scene.addModifier(healingCharm, true, false, false, true); scene.addModifier(healingCharm, true, false, false, true);
// Set 1 Soul Dew on party lead // Set 1 Soul Dew on party lead
const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!;
const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier;
modifier.stackCount = 1; modifier.stackCount = 1;
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
@ -425,7 +425,7 @@ describe("Delibird-y - Mystery Encounter", () => {
scene.modifiers = []; scene.modifiers = [];
const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!; const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!;
const modifier = revSeed.newModifier(scene.getPlayerParty()[0]); const modifier = revSeed.newModifier(scene.getPlayerParty()[0]);
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await game.phaseInterceptor.to(MysteryEncounterPhase, false); await game.phaseInterceptor.to(MysteryEncounterPhase, false);
@ -455,7 +455,7 @@ describe("Delibird-y - Mystery Encounter", () => {
const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!;
const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier;
modifier.stackCount = 1; modifier.stackCount = 1;
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });

View File

@ -224,7 +224,7 @@ describe("Global Trade System - Mystery Encounter", () => {
const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!;
const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier;
modifier.stackCount = 2; modifier.stackCount = 2;
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
@ -249,7 +249,7 @@ describe("Global Trade System - Mystery Encounter", () => {
const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!;
const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier;
modifier.stackCount = 1; modifier.stackCount = 1;
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });

View File

@ -216,11 +216,11 @@ describe("Uncommon Breed - Mystery Encounter", () => {
const sitrus = generateModifierType(modifierTypes.BERRY, [ BerryType.SITRUS ])!; const sitrus = generateModifierType(modifierTypes.BERRY, [ BerryType.SITRUS ])!;
const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier;
sitrusMod.stackCount = 2; sitrusMod.stackCount = 2;
await scene.addModifier(sitrusMod, true, false, false, true); scene.addModifier(sitrusMod, true, false, false, true);
const ganlon = generateModifierType(modifierTypes.BERRY, [ BerryType.GANLON ])!; const ganlon = generateModifierType(modifierTypes.BERRY, [ BerryType.GANLON ])!;
const ganlonMod = ganlon.newModifier(scene.getPlayerParty()[0]) as BerryModifier; const ganlonMod = ganlon.newModifier(scene.getPlayerParty()[0]) as BerryModifier;
ganlonMod.stackCount = 3; ganlonMod.stackCount = 3;
await scene.addModifier(ganlonMod, true, false, false, true); scene.addModifier(ganlonMod, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 2); await runMysteryEncounterToEnd(game, 2);

View File

@ -406,9 +406,9 @@ describe("Test Rebinding", () => {
}); });
it("check to delete all the binds of an action", () => { it("check to delete all the binds of an action", () => {
inGame.whenWePressOnKeyboard("V").weShouldTriggerTheButton("Button_Cycle_Variant"); inGame.whenWePressOnKeyboard("V").weShouldTriggerTheButton("Button_Cycle_Tera");
inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Cycle_Variant").thereShouldBeNoIcon().weWantThisBindInstead("K").confirm(); inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Cycle_Tera").thereShouldBeNoIcon().weWantThisBindInstead("K").confirm();
inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Cycle_Variant").iconDisplayedIs("KEY_K").whenWeDelete().thereShouldBeNoIconAnymore(); inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Cycle_Tera").iconDisplayedIs("KEY_K").whenWeDelete().thereShouldBeNoIconAnymore();
inTheSettingMenu.whenCursorIsOnSetting("Button_Cycle_Variant").iconDisplayedIs("KEY_V").whenWeDelete().thereShouldBeNoIconAnymore(); inTheSettingMenu.whenCursorIsOnSetting("Button_Cycle_Tera").iconDisplayedIs("KEY_V").whenWeDelete().thereShouldBeNoIconAnymore();
}); });
}); });

View File

@ -35,7 +35,7 @@
<div id="apadCycleShiny" class="apad-button apad-square apad-small" data-key="CYCLE_SHINY"> <div id="apadCycleShiny" class="apad-button apad-square apad-small" data-key="CYCLE_SHINY">
<span class="apad-label">R</span> <span class="apad-label">R</span>
</div> </div>
<div id="apadCycleVariant" class="apad-button apad-square apad-small" data-key="V"> <div id="apadCycleTera" class="apad-button apad-square apad-small" data-key="CYCLE_TERA">
<span class="apad-label">V</span> <span class="apad-label">V</span>
</div> </div>
<div id="apadStats" class="apad-button apad-rectangle apad-small" data-key="STATS"> <div id="apadStats" class="apad-button apad-rectangle apad-small" data-key="STATS">
@ -59,7 +59,7 @@
<div id="apadCycleNature" class="apad-button apad-square apad-small" data-key="CYCLE_NATURE"> <div id="apadCycleNature" class="apad-button apad-square apad-small" data-key="CYCLE_NATURE">
<span class="apad-label">N</span> <span class="apad-label">N</span>
</div> </div>
<div id="apadInfo" class="apad-button apad-rectangle apad-small" data-key="V"> <div id="apadInfo" class="apad-button apad-rectangle apad-small" data-key="CYCLE_TERA">
<span class="apad-label">V</span> <span class="apad-label">V</span>
</div> </div>
</div> </div>

View File

@ -91,6 +91,7 @@ export default class GameWrapper {
Pokemon.prototype.updateFusionPalette = () => null; Pokemon.prototype.updateFusionPalette = () => null;
Pokemon.prototype.cry = () => null; Pokemon.prototype.cry = () => null;
Pokemon.prototype.faintCry = (cb) => { if (cb) cb(); }; Pokemon.prototype.faintCry = (cb) => { if (cb) cb(); };
BattleScene.prototype.addPokemonIcon = () => new Phaser.GameObjects.Container(this.scene);
} }
setScene(scene: BattleScene) { setScene(scene: BattleScene) {

View File

@ -60,6 +60,7 @@ import { RibbonModifierRewardPhase } from "#app/phases/ribbon-modifier-reward-ph
import { GameOverModifierRewardPhase } from "#app/phases/game-over-modifier-reward-phase"; import { GameOverModifierRewardPhase } from "#app/phases/game-over-modifier-reward-phase";
import { UnlockPhase } from "#app/phases/unlock-phase"; import { UnlockPhase } from "#app/phases/unlock-phase";
import { PostGameOverPhase } from "#app/phases/post-game-over-phase"; import { PostGameOverPhase } from "#app/phases/post-game-over-phase";
import { RevivalBlessingPhase } from "#app/phases/revival-blessing-phase";
export interface PromptHandler { export interface PromptHandler {
phaseTarget?: string; phaseTarget?: string;
@ -126,7 +127,8 @@ type PhaseClass =
| typeof EncounterPhase | typeof EncounterPhase
| typeof GameOverPhase | typeof GameOverPhase
| typeof UnlockPhase | typeof UnlockPhase
| typeof PostGameOverPhase; | typeof PostGameOverPhase
| typeof RevivalBlessingPhase;
type PhaseString = type PhaseString =
| "LoginPhase" | "LoginPhase"
@ -185,7 +187,8 @@ type PhaseString =
| "EncounterPhase" | "EncounterPhase"
| "GameOverPhase" | "GameOverPhase"
| "UnlockPhase" | "UnlockPhase"
| "PostGameOverPhase"; | "PostGameOverPhase"
| "RevivalBlessingPhase";
type PhaseInterceptorPhase = PhaseClass | PhaseString; type PhaseInterceptorPhase = PhaseClass | PhaseString;
@ -269,6 +272,7 @@ export default class PhaseInterceptor {
[ GameOverPhase, this.startPhase ], [ GameOverPhase, this.startPhase ],
[ UnlockPhase, this.startPhase ], [ UnlockPhase, this.startPhase ],
[ PostGameOverPhase, this.startPhase ], [ PostGameOverPhase, this.startPhase ],
[ RevivalBlessingPhase, this.startPhase ],
]; ];
private endBySetMode = [ private endBySetMode = [
@ -511,11 +515,11 @@ export default class PhaseInterceptor {
if (expireFn) { if (expireFn) {
this.prompts.shift(); this.prompts.shift();
} else if ( } else if (
currentMode === actionForNextPrompt.mode currentMode === actionForNextPrompt.mode &&
&& currentPhase === actionForNextPrompt.phaseTarget currentPhase === actionForNextPrompt.phaseTarget &&
&& currentHandler.active currentHandler.active &&
&& (!actionForNextPrompt.awaitingActionInput (!actionForNextPrompt.awaitingActionInput ||
|| (actionForNextPrompt.awaitingActionInput && currentHandler.awaitingActionInput)) (actionForNextPrompt.awaitingActionInput && currentHandler.awaitingActionInput))
) { ) {
const prompt = this.prompts.shift(); const prompt = this.prompts.shift();
if (prompt?.callback) { if (prompt?.callback) {

View File

@ -86,7 +86,7 @@ export class UiInputs {
[Button.CYCLE_GENDER]: () => this.buttonCycleOption(Button.CYCLE_GENDER), [Button.CYCLE_GENDER]: () => this.buttonCycleOption(Button.CYCLE_GENDER),
[Button.CYCLE_ABILITY]: () => this.buttonCycleOption(Button.CYCLE_ABILITY), [Button.CYCLE_ABILITY]: () => this.buttonCycleOption(Button.CYCLE_ABILITY),
[Button.CYCLE_NATURE]: () => this.buttonCycleOption(Button.CYCLE_NATURE), [Button.CYCLE_NATURE]: () => this.buttonCycleOption(Button.CYCLE_NATURE),
[Button.V]: () => this.buttonCycleOption(Button.V), [Button.CYCLE_TERA]: () => this.buttonCycleOption(Button.CYCLE_TERA),
[Button.SPEED_UP]: () => this.buttonSpeedChange(), [Button.SPEED_UP]: () => this.buttonSpeedChange(),
[Button.SLOW_DOWN]: () => this.buttonSpeedChange(false), [Button.SLOW_DOWN]: () => this.buttonSpeedChange(false),
}; };
@ -109,7 +109,7 @@ export class UiInputs {
[Button.CYCLE_GENDER]: () => undefined, [Button.CYCLE_GENDER]: () => undefined,
[Button.CYCLE_ABILITY]: () => undefined, [Button.CYCLE_ABILITY]: () => undefined,
[Button.CYCLE_NATURE]: () => undefined, [Button.CYCLE_NATURE]: () => undefined,
[Button.V]: () => this.buttonInfo(false), [Button.CYCLE_TERA]: () => undefined,
[Button.SPEED_UP]: () => undefined, [Button.SPEED_UP]: () => undefined,
[Button.SLOW_DOWN]: () => undefined, [Button.SLOW_DOWN]: () => undefined,
}; };
@ -197,7 +197,7 @@ export class UiInputs {
const uiHandler = globalScene.ui?.getHandler(); const uiHandler = globalScene.ui?.getHandler();
if (whitelist.some(handler => uiHandler instanceof handler)) { if (whitelist.some(handler => uiHandler instanceof handler)) {
globalScene.ui.processInput(button); globalScene.ui.processInput(button);
} else if (button === Button.V) { } else if (button === Button.CYCLE_TERA) {
this.buttonInfo(true); this.buttonInfo(true);
} }
} }

View File

@ -1,6 +1,7 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type Pokemon from "../field/pokemon"; import type Pokemon from "../field/pokemon";
import { TextStyle, addTextObject } from "./text"; import { TextStyle, addTextObject } from "./text";
import i18next from "i18next";
export default class PartyExpBar extends Phaser.GameObjects.Container { export default class PartyExpBar extends Phaser.GameObjects.Container {
private bg: Phaser.GameObjects.NineSlice; private bg: Phaser.GameObjects.NineSlice;
@ -43,9 +44,9 @@ export default class PartyExpBar extends Phaser.GameObjects.Container {
// if we want to only display the level in the small frame // if we want to only display the level in the small frame
if (showOnlyLevelUp) { if (showOnlyLevelUp) {
if (newLevel > 200) { // if the level is greater than 200, we only display Lv. UP if (newLevel > 200) { // if the level is greater than 200, we only display Lv. UP
this.expText.setText("Lv. UP"); this.expText.setText(i18next.t("battleScene:levelUp"));
} else { // otherwise we display Lv. Up and the new level } else { // otherwise we display Lv. Up and the new level
this.expText.setText(`Lv. UP: ${newLevel.toString()}`); this.expText.setText(i18next.t("battleScene:levelUpWithLevel", { level: newLevel }));
} }
} else { } else {
// if we want to display the exp // if we want to display the exp

View File

@ -45,7 +45,6 @@ import { EggSourceType } from "#enums/egg-source-types";
import { getPassiveCandyCount, getValueReductionCandyCounts, getSameSpeciesEggCandyCounts } from "#app/data/balance/starters"; import { getPassiveCandyCount, getValueReductionCandyCounts, getSameSpeciesEggCandyCounts } from "#app/data/balance/starters";
import { BooleanHolder, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, rgbHexToRgba, toReadableString } from "#app/utils"; import { BooleanHolder, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, rgbHexToRgba, toReadableString } from "#app/utils";
import type { Nature } from "#enums/nature"; import type { Nature } from "#enums/nature";
import BgmBar from "./bgm-bar";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { speciesTmMoves } from "#app/data/balance/tms"; import { speciesTmMoves } from "#app/data/balance/tms";
import type { BiomeTierTod } from "#app/data/balance/biomes"; import type { BiomeTierTod } from "#app/data/balance/biomes";
@ -242,7 +241,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
private menuContainer: Phaser.GameObjects.Container; private menuContainer: Phaser.GameObjects.Container;
private menuBg: Phaser.GameObjects.NineSlice; private menuBg: Phaser.GameObjects.NineSlice;
protected optionSelectText: Phaser.GameObjects.Text; protected optionSelectText: Phaser.GameObjects.Text;
public bgmBar: BgmBar;
private menuOptions: MenuOptions[]; private menuOptions: MenuOptions[];
protected scale: number = 0.1666666667; protected scale: number = 0.1666666667;
private menuDescriptions: string[]; private menuDescriptions: string[];
@ -480,10 +478,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.menuContainer.setName("menu"); this.menuContainer.setName("menu");
this.menuContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6), Phaser.Geom.Rectangle.Contains); this.menuContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6), Phaser.Geom.Rectangle.Contains);
this.bgmBar = new BgmBar();
this.bgmBar.setup();
ui.bgmBar = this.bgmBar;
this.menuContainer.add(this.bgmBar);
this.menuContainer.setVisible(false); this.menuContainer.setVisible(false);
this.menuOptions = Utils.getEnumKeys(MenuOptions).map(m => parseInt(MenuOptions[m]) as MenuOptions); this.menuOptions = Utils.getEnumKeys(MenuOptions).map(m => parseInt(MenuOptions[m]) as MenuOptions);
@ -983,13 +977,23 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
ui.setModeWithoutClear(Mode.OPTION_SELECT, { ui.setModeWithoutClear(Mode.OPTION_SELECT, {
options: this.levelMoves.map(m => { options: this.levelMoves.map(m => {
const levelNumber = m[0] > 0 ? String(m[0]) : "";
const option: OptionSelectItem = { const option: OptionSelectItem = {
label: String(m[0]).padEnd(4, " ") + allMoves[m[1]].name, label: levelNumber.padEnd(4, " ") + allMoves[m[1]].name,
handler: () => { handler: () => {
return false; return false;
}, },
onHover: () => { onHover: () => {
this.moveInfoOverlay.show(allMoves[m[1]]); this.moveInfoOverlay.show(allMoves[m[1]]);
if (m[0] === 0) {
this.showText(i18next.t("pokedexUiHandler:onlyEvolutionMove"));
} else if (m[0] === -1) {
this.showText(i18next.t("pokedexUiHandler:onlyRecallMove"));
} else if (m[0] <= 5) {
this.showText(i18next.t("pokedexUiHandler:onStarterSelectMove"));
} else {
this.showText(i18next.t("pokedexUiHandler:byLevelUpMove"));
}
}, },
}; };
return option; return option;

View File

@ -919,7 +919,7 @@ export default class PokedexUiHandler extends MessageUiHandler {
} else { } else {
error = true; error = true;
} }
} else if (button === Button.V) { } else if (button === Button.CYCLE_TERA) {
if (!this.filterTextMode && !this.showingTray) { if (!this.filterTextMode && !this.showingTray) {
this.cursorObj.setVisible(false); this.cursorObj.setVisible(false);
this.setSpecies(null); this.setSpecies(null);
@ -1170,9 +1170,6 @@ export default class PokedexUiHandler extends MessageUiHandler {
case SettingKeyboard.Button_Cycle_Shiny: case SettingKeyboard.Button_Cycle_Shiny:
iconPath = "R.png"; iconPath = "R.png";
break; break;
case SettingKeyboard.Button_Cycle_Variant:
iconPath = "V.png";
break;
case SettingKeyboard.Button_Cycle_Form: case SettingKeyboard.Button_Cycle_Form:
iconPath = "F.png"; iconPath = "F.png";
break; break;

View File

@ -93,9 +93,9 @@ export default class MoveTouchControlsHandler {
toolbar.innerHTML = ` toolbar.innerHTML = `
<div class="column"> <div class="column">
<div class="button-row"> <div class="button-row">
<div id="resetButton" class="button">${i18next.t("settings:reset")}</div> <div id="resetButton" class="button">${i18next.t("settings:touchReset")}</div>
<div id="saveButton" class="button">${i18next.t("settings:saveClose")}</div> <div id="saveButton" class="button">${i18next.t("settings:touchSaveClose")}</div>
<div id="cancelButton" class="button">${i18next.t("settings:cancel")}</div> <div id="cancelButton" class="button">${i18next.t("settings:touchCancel")}</div>
</div> </div>
<div class="info-row"> <div class="info-row">
<div class="orientation-label"> <div class="orientation-label">

View File

@ -56,6 +56,8 @@ import { getPassiveCandyCount, getValueReductionCandyCounts, getSameSpeciesEggCa
import { BooleanHolder, fixedInt, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, randIntRange, rgbHexToRgba, toReadableString } from "#app/utils"; import { BooleanHolder, fixedInt, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, randIntRange, rgbHexToRgba, toReadableString } from "#app/utils";
import type { Nature } from "#enums/nature"; import type { Nature } from "#enums/nature";
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
import { achvs } from "#app/system/achv";
import * as Utils from "../utils";
export type StarterSelectCallback = (starters: Starter[]) => void; export type StarterSelectCallback = (starters: Starter[]) => void;
@ -68,6 +70,7 @@ export interface Starter {
moveset?: StarterMoveset; moveset?: StarterMoveset;
pokerus: boolean; pokerus: boolean;
nickname?: string; nickname?: string;
teraType?: Type;
} }
interface LanguageSetting { interface LanguageSetting {
@ -212,6 +215,7 @@ interface SpeciesDetails {
abilityIndex?: number, abilityIndex?: number,
natureIndex?: number, natureIndex?: number,
forSeen?: boolean, // default = false forSeen?: boolean, // default = false
teraType?: Type,
} }
export default class StarterSelectUiHandler extends MessageUiHandler { export default class StarterSelectUiHandler extends MessageUiHandler {
@ -262,6 +266,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
private pokemonShinyIcon: Phaser.GameObjects.Sprite; private pokemonShinyIcon: Phaser.GameObjects.Sprite;
private pokemonPassiveDisabledIcon: Phaser.GameObjects.Sprite; private pokemonPassiveDisabledIcon: Phaser.GameObjects.Sprite;
private pokemonPassiveLockedIcon: Phaser.GameObjects.Sprite; private pokemonPassiveLockedIcon: Phaser.GameObjects.Sprite;
private teraIcon: Phaser.GameObjects.Sprite;
private activeTooltip: "ABILITY" | "PASSIVE" | "CANDY" | undefined; private activeTooltip: "ABILITY" | "PASSIVE" | "CANDY" | undefined;
private instructionsContainer: Phaser.GameObjects.Container; private instructionsContainer: Phaser.GameObjects.Container;
@ -271,12 +276,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
private abilityIconElement: Phaser.GameObjects.Sprite; private abilityIconElement: Phaser.GameObjects.Sprite;
private genderIconElement: Phaser.GameObjects.Sprite; private genderIconElement: Phaser.GameObjects.Sprite;
private natureIconElement: Phaser.GameObjects.Sprite; private natureIconElement: Phaser.GameObjects.Sprite;
private teraIconElement: Phaser.GameObjects.Sprite;
private goFilterIconElement: Phaser.GameObjects.Sprite; private goFilterIconElement: Phaser.GameObjects.Sprite;
private shinyLabel: Phaser.GameObjects.Text; private shinyLabel: Phaser.GameObjects.Text;
private formLabel: Phaser.GameObjects.Text; private formLabel: Phaser.GameObjects.Text;
private genderLabel: Phaser.GameObjects.Text; private genderLabel: Phaser.GameObjects.Text;
private abilityLabel: Phaser.GameObjects.Text; private abilityLabel: Phaser.GameObjects.Text;
private natureLabel: Phaser.GameObjects.Text; private natureLabel: Phaser.GameObjects.Text;
private teraLabel: Phaser.GameObjects.Text;
private goFilterLabel: Phaser.GameObjects.Text; private goFilterLabel: Phaser.GameObjects.Text;
private starterSelectMessageBox: Phaser.GameObjects.NineSlice; private starterSelectMessageBox: Phaser.GameObjects.NineSlice;
@ -292,6 +299,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
private dexAttrCursor: bigint = 0n; private dexAttrCursor: bigint = 0n;
private abilityCursor: number = -1; private abilityCursor: number = -1;
private natureCursor: number = -1; private natureCursor: number = -1;
private teraCursor: Type = Type.UNKNOWN;
private filterBarCursor: number = 0; private filterBarCursor: number = 0;
private starterMoveset: StarterMoveset | null; private starterMoveset: StarterMoveset | null;
private scrollCursor: number; private scrollCursor: number;
@ -304,6 +312,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
private starterAttr: bigint[] = []; private starterAttr: bigint[] = [];
private starterAbilityIndexes: number[] = []; private starterAbilityIndexes: number[] = [];
private starterNatures: Nature[] = []; private starterNatures: Nature[] = [];
private starterTeras: Type[] = [];
private starterMovesets: StarterMoveset[] = []; private starterMovesets: StarterMoveset[] = [];
private speciesStarterDexEntry: DexEntry | null; private speciesStarterDexEntry: DexEntry | null;
private speciesStarterMoves: Moves[]; private speciesStarterMoves: Moves[];
@ -312,6 +321,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
private canCycleGender: boolean; private canCycleGender: boolean;
private canCycleAbility: boolean; private canCycleAbility: boolean;
private canCycleNature: boolean; private canCycleNature: boolean;
private canCycleTera: boolean;
private assetLoadCancelled: BooleanHolder | null; private assetLoadCancelled: BooleanHolder | null;
public cursorObj: Phaser.GameObjects.Image; public cursorObj: Phaser.GameObjects.Image;
@ -823,6 +833,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.starterSelectContainer.add(this.pokemonEggMovesContainer); this.starterSelectContainer.add(this.pokemonEggMovesContainer);
this.teraIcon = globalScene.add.sprite(85, 63, "button_tera");
this.teraIcon.setName("terrastallize-icon");
this.teraIcon.setFrame("fire");
this.starterSelectContainer.add(this.teraIcon);
// The font size should be set per language // The font size should be set per language
const instructionTextSize = textSettings.instructionTextSize; const instructionTextSize = textSettings.instructionTextSize;
@ -867,6 +882,13 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.natureLabel = addTextObject(this.instructionRowX + this.instructionRowTextOffset, this.instructionRowY, i18next.t("starterSelectUiHandler:cycleNature"), TextStyle.PARTY, { fontSize: instructionTextSize }); this.natureLabel = addTextObject(this.instructionRowX + this.instructionRowTextOffset, this.instructionRowY, i18next.t("starterSelectUiHandler:cycleNature"), TextStyle.PARTY, { fontSize: instructionTextSize });
this.natureLabel.setName("text-nature-label"); this.natureLabel.setName("text-nature-label");
this.teraIconElement = new Phaser.GameObjects.Sprite(globalScene, this.instructionRowX, this.instructionRowY, "keyboard", "V.png");
this.teraIconElement.setName("sprite-tera-icon-element");
this.teraIconElement.setScale(0.675);
this.teraIconElement.setOrigin(0.0, 0.0);
this.teraLabel = addTextObject(this.instructionRowX + this.instructionRowTextOffset, this.instructionRowY, i18next.t("starterSelectUiHandler:cycleTera"), TextStyle.PARTY, { fontSize: instructionTextSize });
this.teraLabel.setName("text-tera-label");
this.goFilterIconElement = new Phaser.GameObjects.Sprite(globalScene, this.filterInstructionRowX, this.filterInstructionRowY, "keyboard", "C.png"); this.goFilterIconElement = new Phaser.GameObjects.Sprite(globalScene, this.filterInstructionRowX, this.filterInstructionRowY, "keyboard", "C.png");
this.goFilterIconElement.setName("sprite-goFilter-icon-element"); this.goFilterIconElement.setName("sprite-goFilter-icon-element");
this.goFilterIconElement.setScale(0.675); this.goFilterIconElement.setScale(0.675);
@ -1497,6 +1519,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
const props = globalScene.gameData.getSpeciesDexAttrProps(randomSpecies, dexAttr); const props = globalScene.gameData.getSpeciesDexAttrProps(randomSpecies, dexAttr);
const abilityIndex = this.abilityCursor; const abilityIndex = this.abilityCursor;
const nature = this.natureCursor as unknown as Nature; const nature = this.natureCursor as unknown as Nature;
const teraType = this.teraCursor;
const moveset = this.starterMoveset?.slice(0) as StarterMoveset; const moveset = this.starterMoveset?.slice(0) as StarterMoveset;
const starterCost = globalScene.gameData.getSpeciesStarterValue(randomSpecies.speciesId); const starterCost = globalScene.gameData.getSpeciesStarterValue(randomSpecies.speciesId);
const speciesForm = getPokemonSpeciesForm(randomSpecies.speciesId, props.formIndex); const speciesForm = getPokemonSpeciesForm(randomSpecies.speciesId, props.formIndex);
@ -1505,7 +1528,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
.loadAssets(props.female, props.formIndex, props.shiny, props.variant, true) .loadAssets(props.female, props.formIndex, props.shiny, props.variant, true)
.then(() => { .then(() => {
if (this.tryUpdateValue(starterCost, true)) { if (this.tryUpdateValue(starterCost, true)) {
this.addToParty(randomSpecies, dexAttr, abilityIndex, nature, moveset, true); this.addToParty(randomSpecies, dexAttr, abilityIndex, nature, moveset, teraType, true);
ui.playSelect(); ui.playSelect();
} }
}); });
@ -1585,7 +1608,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
const cursorObj = this.starterCursorObjs[this.starterSpecies.length]; const cursorObj = this.starterCursorObjs[this.starterSpecies.length];
cursorObj.setVisible(true); cursorObj.setVisible(true);
cursorObj.setPosition(this.cursorObj.x, this.cursorObj.y); cursorObj.setPosition(this.cursorObj.x, this.cursorObj.y);
this.addToParty(this.lastSpecies, this.dexAttrCursor, this.abilityCursor, this.natureCursor as unknown as Nature, this.starterMoveset?.slice(0) as StarterMoveset); this.addToParty(this.lastSpecies, this.dexAttrCursor, this.abilityCursor, this.natureCursor as unknown as Nature, this.starterMoveset?.slice(0) as StarterMoveset, this.teraCursor);
ui.playSelect(); ui.playSelect();
} else { } else {
ui.playError(); // this should be redundant as there is now a trigger for when a pokemon can't be added to party ui.playError(); // this should be redundant as there is now a trigger for when a pokemon can't be added to party
@ -1961,15 +1984,6 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
yOffset: 47 yOffset: 47
}); });
}; };
if (!pokemonPrevolutions.hasOwnProperty(this.lastSpecies.speciesId)) {
options.push({
label: i18next.t("starterSelectUiHandler:useCandies"),
handler: () => {
ui.setMode(Mode.STARTER_SELECT).then(() => showUseCandies());
return true;
}
});
}
options.push({ options.push({
label: i18next.t("menuUiHandler:POKEDEX"), label: i18next.t("menuUiHandler:POKEDEX"),
handler: () => { handler: () => {
@ -1985,6 +1999,15 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
return true; return true;
} }
}); });
if (!pokemonPrevolutions.hasOwnProperty(this.lastSpecies.speciesId)) {
options.push({
label: i18next.t("starterSelectUiHandler:useCandies"),
handler: () => {
ui.setMode(Mode.STARTER_SELECT).then(() => showUseCandies());
return true;
}
});
}
options.push({ options.push({
label: i18next.t("menu:cancel"), label: i18next.t("menu:cancel"),
handler: () => { handler: () => {
@ -2066,7 +2089,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
} }
} while (newFormIndex !== props.formIndex); } while (newFormIndex !== props.formIndex);
starterAttributes.form = newFormIndex; // store the selected form starterAttributes.form = newFormIndex; // store the selected form
this.setSpeciesDetails(this.lastSpecies, { formIndex: newFormIndex }); starterAttributes.tera = this.lastSpecies.forms[newFormIndex].type1;
this.setSpeciesDetails(this.lastSpecies, { formIndex: newFormIndex, teraType: starterAttributes.tera });
success = true; success = true;
} }
break; break;
@ -2125,6 +2149,19 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
success = true; success = true;
} }
break; break;
case Button.CYCLE_TERA:
if (this.canCycleTera) {
const speciesForm = getPokemonSpeciesForm(this.lastSpecies.speciesId, starterAttributes.form ?? 0);
if (speciesForm.type1 === this.teraCursor && !Utils.isNullOrUndefined(speciesForm.type2)) {
starterAttributes.tera = speciesForm.type2!;
this.setSpeciesDetails(this.lastSpecies, { teraType: speciesForm.type2! });
} else {
starterAttributes.tera = speciesForm.type1;
this.setSpeciesDetails(this.lastSpecies, { teraType: speciesForm.type1 });
}
success = true;
}
break;
case Button.UP: case Button.UP:
if (!this.starterIconsCursorObj.visible) { if (!this.starterIconsCursorObj.visible) {
if (currentRow > 0) { if (currentRow > 0) {
@ -2289,7 +2326,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
return [ isDupe, removeIndex ]; return [ isDupe, removeIndex ];
} }
addToParty(species: PokemonSpecies, dexAttr: bigint, abilityIndex: number, nature: Nature, moveset: StarterMoveset, randomSelection: boolean = false) { addToParty(species: PokemonSpecies, dexAttr: bigint, abilityIndex: number, nature: Nature, moveset: StarterMoveset, teraType: Type, randomSelection: boolean = false) {
const props = globalScene.gameData.getSpeciesDexAttrProps(species, dexAttr); const props = globalScene.gameData.getSpeciesDexAttrProps(species, dexAttr);
this.starterIcons[this.starterSpecies.length].setTexture(species.getIconAtlasKey(props.formIndex, props.shiny, props.variant)); this.starterIcons[this.starterSpecies.length].setTexture(species.getIconAtlasKey(props.formIndex, props.shiny, props.variant));
this.starterIcons[this.starterSpecies.length].setFrame(species.getIconId(props.female, props.formIndex, props.shiny, props.variant)); this.starterIcons[this.starterSpecies.length].setFrame(species.getIconId(props.female, props.formIndex, props.shiny, props.variant));
@ -2299,6 +2336,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.starterAttr.push(dexAttr); this.starterAttr.push(dexAttr);
this.starterAbilityIndexes.push(abilityIndex); this.starterAbilityIndexes.push(abilityIndex);
this.starterNatures.push(nature); this.starterNatures.push(nature);
this.starterTeras.push(teraType);
this.starterMovesets.push(moveset); this.starterMovesets.push(moveset);
if (this.speciesLoaded.get(species.speciesId) || randomSelection ) { if (this.speciesLoaded.get(species.speciesId) || randomSelection ) {
getPokemonSpeciesForm(species.speciesId, props.formIndex).cry(); getPokemonSpeciesForm(species.speciesId, props.formIndex).cry();
@ -2379,6 +2417,9 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
case SettingKeyboard.Button_Cycle_Nature: case SettingKeyboard.Button_Cycle_Nature:
iconPath = "N.png"; iconPath = "N.png";
break; break;
case SettingKeyboard.Button_Cycle_Tera:
iconPath = "V.png";
break;
case SettingKeyboard.Button_Stats: case SettingKeyboard.Button_Stats:
iconPath = "C.png"; iconPath = "C.png";
break; break;
@ -2459,6 +2500,9 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
if (this.canCycleNature) { if (this.canCycleNature) {
this.updateButtonIcon(SettingKeyboard.Button_Cycle_Nature, gamepadType, this.natureIconElement, this.natureLabel); this.updateButtonIcon(SettingKeyboard.Button_Cycle_Nature, gamepadType, this.natureIconElement, this.natureLabel);
} }
if (this.canCycleTera) {
this.updateButtonIcon(SettingKeyboard.Button_Cycle_Tera, gamepadType, this.teraIconElement, this.teraLabel);
}
} }
// if filter mode is inactivated and gamepadType is not undefined, update the button icons // if filter mode is inactivated and gamepadType is not undefined, update the button icons
@ -2876,6 +2920,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.dexAttrCursor = species ? this.getCurrentDexProps(species.speciesId) : 0n; this.dexAttrCursor = species ? this.getCurrentDexProps(species.speciesId) : 0n;
this.abilityCursor = species ? globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species) : 0; this.abilityCursor = species ? globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species) : 0;
this.natureCursor = species ? globalScene.gameData.getSpeciesDefaultNature(species) : 0; this.natureCursor = species ? globalScene.gameData.getSpeciesDefaultNature(species) : 0;
this.teraCursor = species ? species.type1 : Type.UNKNOWN;
if (!species && globalScene.ui.getTooltip().visible) { if (!species && globalScene.ui.getTooltip().visible) {
globalScene.ui.hideTooltip(); globalScene.ui.hideTooltip();
@ -2894,6 +2939,10 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
// load default ability from stater save data, if set // load default ability from stater save data, if set
this.abilityCursor = starterAttributes.ability; this.abilityCursor = starterAttributes.ability;
} }
if (starterAttributes?.tera) {
// load default tera from starter save data, if set
this.teraCursor = starterAttributes.tera;
}
if (this.statsMode) { if (this.statsMode) {
if (this.speciesStarterDexEntry?.caughtAttr) { if (this.speciesStarterDexEntry?.caughtAttr) {
@ -3035,7 +3084,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
female: props.female, female: props.female,
variant: props.variant, variant: props.variant,
abilityIndex: this.starterAbilityIndexes[starterIndex], abilityIndex: this.starterAbilityIndexes[starterIndex],
natureIndex: this.starterNatures[starterIndex] natureIndex: this.starterNatures[starterIndex],
teraType: this.starterTeras[starterIndex]
}); });
} else { } else {
const defaultDexAttr = this.getCurrentDexProps(species.speciesId); const defaultDexAttr = this.getCurrentDexProps(species.speciesId);
@ -3083,6 +3133,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonCaughtHatchedContainer.setVisible(false); this.pokemonCaughtHatchedContainer.setVisible(false);
this.pokemonCandyContainer.setVisible(false); this.pokemonCandyContainer.setVisible(false);
this.pokemonFormText.setVisible(false); this.pokemonFormText.setVisible(false);
this.teraIcon.setVisible(false);
const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, true, true); const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, true, true);
const defaultAbilityIndex = globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species); const defaultAbilityIndex = globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species);
@ -3117,6 +3168,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonCaughtHatchedContainer.setVisible(false); this.pokemonCaughtHatchedContainer.setVisible(false);
this.pokemonCandyContainer.setVisible(false); this.pokemonCandyContainer.setVisible(false);
this.pokemonFormText.setVisible(false); this.pokemonFormText.setVisible(false);
this.teraIcon.setVisible(false);
this.setSpeciesDetails(species!, { // TODO: is this bang correct? this.setSpeciesDetails(species!, { // TODO: is this bang correct?
shiny: false, shiny: false,
@ -3131,7 +3183,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
} }
setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}): void { setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}): void {
let { shiny, formIndex, female, variant, abilityIndex, natureIndex } = options; let { shiny, formIndex, female, variant, abilityIndex, natureIndex, teraType } = options;
const forSeen: boolean = options.forSeen ?? false; const forSeen: boolean = options.forSeen ?? false;
const oldProps = species ? globalScene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor) : null; const oldProps = species ? globalScene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor) : null;
const oldAbilityIndex = this.abilityCursor > -1 ? this.abilityCursor : globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species); const oldAbilityIndex = this.abilityCursor > -1 ? this.abilityCursor : globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species);
@ -3139,6 +3191,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.dexAttrCursor = 0n; this.dexAttrCursor = 0n;
this.abilityCursor = -1; this.abilityCursor = -1;
this.natureCursor = -1; this.natureCursor = -1;
this.teraCursor = Type.UNKNOWN;
// We will only update the sprite if there is a change to form, shiny/variant // We will only update the sprite if there is a change to form, shiny/variant
// or gender for species with gender sprite differences // or gender for species with gender sprite differences
const shouldUpdateSprite = (species?.genderDiffs && !isNullOrUndefined(female)) const shouldUpdateSprite = (species?.genderDiffs && !isNullOrUndefined(female))
@ -3168,6 +3221,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.dexAttrCursor |= globalScene.gameData.getFormAttr(formIndex !== undefined ? formIndex : (formIndex = oldProps!.formIndex)); // TODO: is this bang correct? this.dexAttrCursor |= globalScene.gameData.getFormAttr(formIndex !== undefined ? formIndex : (formIndex = oldProps!.formIndex)); // TODO: is this bang correct?
this.abilityCursor = abilityIndex !== undefined ? abilityIndex : (abilityIndex = oldAbilityIndex); this.abilityCursor = abilityIndex !== undefined ? abilityIndex : (abilityIndex = oldAbilityIndex);
this.natureCursor = natureIndex !== undefined ? natureIndex : (natureIndex = oldNatureIndex); this.natureCursor = natureIndex !== undefined ? natureIndex : (natureIndex = oldNatureIndex);
this.teraCursor = !Utils.isNullOrUndefined(teraType) ? teraType : (teraType = species.type1);
const [ isInParty, partyIndex ]: [boolean, number] = this.isInParty(species); // we use this to firstly check if the pokemon is in the party, and if so, to get the party index in order to update the icon image const [ isInParty, partyIndex ]: [boolean, number] = this.isInParty(species); // we use this to firstly check if the pokemon is in the party, and if so, to get the party index in order to update the icon image
if (isInParty) { if (isInParty) {
this.updatePartyIcon(species, partyIndex); this.updatePartyIcon(species, partyIndex);
@ -3179,6 +3233,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonPassiveText.setVisible(false); this.pokemonPassiveText.setVisible(false);
this.pokemonPassiveDisabledIcon.setVisible(false); this.pokemonPassiveDisabledIcon.setVisible(false);
this.pokemonPassiveLockedIcon.setVisible(false); this.pokemonPassiveLockedIcon.setVisible(false);
this.teraIcon.setVisible(false);
if (this.assetLoadCancelled) { if (this.assetLoadCancelled) {
this.assetLoadCancelled.value = true; this.assetLoadCancelled.value = true;
@ -3230,6 +3285,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.starterAttr[starterIndex] = this.dexAttrCursor; this.starterAttr[starterIndex] = this.dexAttrCursor;
this.starterAbilityIndexes[starterIndex] = this.abilityCursor; this.starterAbilityIndexes[starterIndex] = this.abilityCursor;
this.starterNatures[starterIndex] = this.natureCursor; this.starterNatures[starterIndex] = this.natureCursor;
this.starterTeras[starterIndex] = this.teraCursor;
} }
const assetLoadCancelled = new BooleanHolder(false); const assetLoadCancelled = new BooleanHolder(false);
@ -3288,7 +3344,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.canCycleForm = species.forms.filter(f => f.isStarterSelectable || !pokemonFormChanges[species.speciesId]?.find(fc => fc.formKey)) this.canCycleForm = species.forms.filter(f => f.isStarterSelectable || !pokemonFormChanges[species.speciesId]?.find(fc => fc.formKey))
.map((_, f) => dexEntry.caughtAttr & globalScene.gameData.getFormAttr(f)).filter(f => f).length > 1; .map((_, f) => dexEntry.caughtAttr & globalScene.gameData.getFormAttr(f)).filter(f => f).length > 1;
this.canCycleNature = globalScene.gameData.getNaturesForAttr(dexEntry.natureAttr).length > 1; this.canCycleNature = globalScene.gameData.getNaturesForAttr(dexEntry.natureAttr).length > 1;
this.canCycleTera = globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id) && !Utils.isNullOrUndefined(getPokemonSpeciesForm(species.speciesId, formIndex ?? 0).type2);
} }
if (dexEntry.caughtAttr && species.malePercent !== null) { if (dexEntry.caughtAttr && species.malePercent !== null) {
@ -3412,10 +3468,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonFormText.setText(formText); this.pokemonFormText.setText(formText);
this.setTypeIcons(speciesForm.type1, speciesForm.type2); this.setTypeIcons(speciesForm.type1, speciesForm.type2);
this.teraIcon.setFrame(Type[this.teraCursor].toLowerCase());
this.teraIcon.setVisible(!this.statsMode && globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id));
} else { } else {
this.pokemonAbilityText.setText(""); this.pokemonAbilityText.setText("");
this.pokemonPassiveText.setText(""); this.pokemonPassiveText.setText("");
this.pokemonNatureText.setText(""); this.pokemonNatureText.setText("");
this.teraIcon.setVisible(false);
this.setTypeIcons(null, null); this.setTypeIcons(null, null);
} }
} else { } else {
@ -3426,6 +3486,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonAbilityText.setText(""); this.pokemonAbilityText.setText("");
this.pokemonPassiveText.setText(""); this.pokemonPassiveText.setText("");
this.pokemonNatureText.setText(""); this.pokemonNatureText.setText("");
this.teraIcon.setVisible(false);
this.setTypeIcons(null, null); this.setTypeIcons(null, null);
} }
@ -3479,6 +3540,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.starterAttr.splice(index, 1); this.starterAttr.splice(index, 1);
this.starterAbilityIndexes.splice(index, 1); this.starterAbilityIndexes.splice(index, 1);
this.starterNatures.splice(index, 1); this.starterNatures.splice(index, 1);
this.starterTeras.splice(index, 1);
this.starterMovesets.splice(index, 1); this.starterMovesets.splice(index, 1);
for (let s = 0; s < this.starterSpecies.length; s++) { for (let s = 0; s < this.starterSpecies.length; s++) {
@ -3690,6 +3752,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
abilityIndex: thisObj.starterAbilityIndexes[i], abilityIndex: thisObj.starterAbilityIndexes[i],
passive: !(globalScene.gameData.starterData[starterSpecies.speciesId].passiveAttr ^ (PassiveAttr.ENABLED | PassiveAttr.UNLOCKED)), passive: !(globalScene.gameData.starterData[starterSpecies.speciesId].passiveAttr ^ (PassiveAttr.ENABLED | PassiveAttr.UNLOCKED)),
nature: thisObj.starterNatures[i] as Nature, nature: thisObj.starterNatures[i] as Nature,
teraType: thisObj.starterTeras[i] as Type,
moveset: thisObj.starterMovesets[i], moveset: thisObj.starterMovesets[i],
pokerus: thisObj.pokerusSpecies.includes(starterSpecies), pokerus: thisObj.pokerusSpecies.includes(starterSpecies),
nickname: thisObj.starterPreferences[starterSpecies.speciesId]?.nickname, nickname: thisObj.starterPreferences[starterSpecies.speciesId]?.nickname,
@ -3816,6 +3879,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.abilityLabel.setVisible(false); this.abilityLabel.setVisible(false);
this.natureIconElement.setVisible(false); this.natureIconElement.setVisible(false);
this.natureLabel.setVisible(false); this.natureLabel.setVisible(false);
this.teraIconElement.setVisible(false);
this.teraLabel.setVisible(false);
this.goFilterIconElement.setVisible(false); this.goFilterIconElement.setVisible(false);
this.goFilterLabel.setVisible(false); this.goFilterLabel.setVisible(false);
} }

View File

@ -225,7 +225,7 @@ export default class SummaryUiHandler extends UiHandler {
this.summaryContainer.add(this.championRibbon); this.summaryContainer.add(this.championRibbon);
this.championRibbon.setVisible(false); this.championRibbon.setVisible(false);
this.levelText = addTextObject(36, -17, "", TextStyle.SUMMARY_ALT); this.levelText = addTextObject(24, -17, "", TextStyle.SUMMARY_ALT);
this.levelText.setOrigin(0, 1); this.levelText.setOrigin(0, 1);
this.summaryContainer.add(this.levelText); this.summaryContainer.add(this.levelText);
@ -413,7 +413,7 @@ export default class SummaryUiHandler extends UiHandler {
} }
this.pokeball.setFrame(getPokeballAtlasKey(this.pokemon.pokeball)); this.pokeball.setFrame(getPokeballAtlasKey(this.pokemon.pokeball));
this.levelText.setText(this.pokemon.level.toString()); this.levelText.setText(`${i18next.t("pokemonSummary:lv")}${this.pokemon.level.toString()}`);
this.genderText.setText(getGenderSymbol(this.pokemon.getGender(true))); this.genderText.setText(getGenderSymbol(this.pokemon.getGender(true)));
this.genderText.setColor(getGenderColor(this.pokemon.getGender(true))); this.genderText.setColor(getGenderColor(this.pokemon.getGender(true)));
this.genderText.setShadowColor(getGenderColor(this.pokemon.getGender(true), true)); this.genderText.setShadowColor(getGenderColor(this.pokemon.getGender(true), true));
@ -756,7 +756,7 @@ export default class SummaryUiHandler extends UiHandler {
trainerText.setOrigin(0, 0); trainerText.setOrigin(0, 0);
profileContainer.add(trainerText); profileContainer.add(trainerText);
const trainerIdText = addTextObject(174, 12, globalScene.gameData.trainerId.toString(), TextStyle.SUMMARY_ALT); const trainerIdText = addTextObject(141, 12, `${i18next.t("pokemonSummary:idNo")}${globalScene.gameData.trainerId.toString()}`, TextStyle.SUMMARY_ALT);
trainerIdText.setOrigin(0, 0); trainerIdText.setOrigin(0, 0);
profileContainer.add(trainerIdText); profileContainer.add(trainerIdText);