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'], [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']) #apadCycleVariant {
#touchControls:not(.config-mode):not([data-ui-mode='STARTER_SELECT'], [data-ui-mode='POKEDEX']) #apadCycleTera {
display: none;
}

View File

@ -129,7 +129,7 @@
<div id="apadCycleNature" class="apad-button apad-square apad-small" data-key="CYCLE_NATURE">
<span class="apad-label">N</span>
</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>
</div>
<!-- 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_Ability]: Button.CYCLE_ABILITY,
[SettingKeyboard.Button_Cycle_Nature]: Button.CYCLE_NATURE,
[SettingKeyboard.Button_Cycle_Variant]: Button.V,
[SettingKeyboard.Button_Speed_Up]: Button.SPEED_UP,
[SettingKeyboard.Button_Slow_Down]: Button.SLOW_DOWN,
[SettingKeyboard.Button_Cycle_Tera]: Button.CYCLE_TERA,
[SettingKeyboard.Alt_Button_Up]: Button.UP,
[SettingKeyboard.Alt_Button_Down]: Button.DOWN,
[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_Ability]: Button.CYCLE_ABILITY,
[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_Slow_Down]: Button.SLOW_DOWN,
[SettingKeyboard.Alt_Button_Cycle_Tera]: Button.CYCLE_TERA,
},
default: {
KEY_ARROW_UP: SettingKeyboard.Button_Up,
@ -216,7 +216,7 @@ const cfg_keyboard_qwerty = {
KEY_G: SettingKeyboard.Button_Cycle_Gender,
KEY_E: SettingKeyboard.Button_Cycle_Ability,
KEY_N: SettingKeyboard.Button_Cycle_Nature,
KEY_V: SettingKeyboard.Button_Cycle_Variant,
KEY_V: SettingKeyboard.Button_Cycle_Tera,
KEY_PLUS: -1,
KEY_MINUS: -1,
KEY_A: SettingKeyboard.Alt_Button_Left,

View File

@ -53,7 +53,7 @@ const pad_dualshock = {
[SettingGamepad.Button_Action]: Button.ACTION,
[SettingGamepad.Button_Cancel]: Button.CANCEL,
[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_Stats]: Button.STATS,
[SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM,
@ -72,7 +72,7 @@ const pad_dualshock = {
RC_S: SettingGamepad.Button_Action,
RC_E: SettingGamepad.Button_Cancel,
RC_W: SettingGamepad.Button_Cycle_Nature,
RC_N: SettingGamepad.Button_Cycle_Variant,
RC_N: SettingGamepad.Button_Cycle_Tera,
START: SettingGamepad.Button_Menu,
SELECT: SettingGamepad.Button_Stats,
LB: SettingGamepad.Button_Cycle_Form,

View File

@ -51,7 +51,7 @@ const pad_generic = {
[SettingGamepad.Button_Action]: Button.ACTION,
[SettingGamepad.Button_Cancel]: Button.CANCEL,
[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_Stats]: Button.STATS,
[SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM,
@ -69,7 +69,7 @@ const pad_generic = {
RC_S: SettingGamepad.Button_Action,
RC_E: SettingGamepad.Button_Cancel,
RC_W: SettingGamepad.Button_Cycle_Nature,
RC_N: SettingGamepad.Button_Cycle_Variant,
RC_N: SettingGamepad.Button_Cycle_Tera,
START: SettingGamepad.Button_Menu,
SELECT: SettingGamepad.Button_Stats,
LB: SettingGamepad.Button_Cycle_Form,

View File

@ -52,7 +52,7 @@ const pad_procon = {
[SettingGamepad.Button_Action]: Button.ACTION,
[SettingGamepad.Button_Cancel]: Button.CANCEL,
[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_Stats]: Button.STATS,
[SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM,
@ -70,7 +70,7 @@ const pad_procon = {
RC_S: SettingGamepad.Button_Action,
RC_E: SettingGamepad.Button_Cancel,
RC_W: SettingGamepad.Button_Cycle_Nature,
RC_N: SettingGamepad.Button_Cycle_Variant,
RC_N: SettingGamepad.Button_Cycle_Tera,
START: SettingGamepad.Button_Menu,
SELECT: SettingGamepad.Button_Stats,
LB: SettingGamepad.Button_Cycle_Form,

View File

@ -43,7 +43,7 @@ const pad_unlicensedSNES = {
[SettingGamepad.Button_Action]: Button.ACTION,
[SettingGamepad.Button_Cancel]: Button.CANCEL,
[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_Stats]: Button.STATS,
[SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM,
@ -61,7 +61,7 @@ const pad_unlicensedSNES = {
RC_S: SettingGamepad.Button_Action,
RC_E: SettingGamepad.Button_Cancel,
RC_W: SettingGamepad.Button_Cycle_Nature,
RC_N: SettingGamepad.Button_Cycle_Variant,
RC_N: SettingGamepad.Button_Cycle_Tera,
START: SettingGamepad.Button_Menu,
SELECT: SettingGamepad.Button_Stats,
LB: SettingGamepad.Button_Cycle_Form,

View File

@ -51,7 +51,7 @@ const pad_xbox360 = {
[SettingGamepad.Button_Action]: Button.ACTION,
[SettingGamepad.Button_Cancel]: Button.CANCEL,
[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_Stats]: Button.STATS,
[SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM,
@ -69,7 +69,7 @@ const pad_xbox360 = {
RC_S: SettingGamepad.Button_Action,
RC_E: SettingGamepad.Button_Cancel,
RC_W: SettingGamepad.Button_Cycle_Nature,
RC_N: SettingGamepad.Button_Cycle_Variant,
RC_N: SettingGamepad.Button_Cycle_Tera,
START: SettingGamepad.Button_Menu,
SELECT: SettingGamepad.Button_Stats,
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.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.teraType = pokemon.species.type1; // Always primary tera type
return true;
}

View File

@ -1,5 +1,16 @@
import { ChargeAnim, initMoveAnim, loadMoveAnimAssets, MoveChargeAnim } from "./battle-anims";
import { CommandedTag, EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, SubstituteTag, TrappedTag, TypeBoostTag } from "./battler-tags";
import { ChargeAnim, MoveChargeAnim } from "./battle-anims";
import {
CommandedTag,
EncoreTag,
GulpMissileTag,
HelpingHandTag,
SemiInvulnerableTag,
ShellTrapTag,
StockpilingTag,
SubstituteTag,
TrappedTag,
TypeBoostTag,
} from "./battler-tags";
import { getPokemonNameWithAffix } from "../messages";
import type { AttackMoveResult, TurnMove } from "../field/pokemon";
import type Pokemon from "../field/pokemon";
@ -30,7 +41,7 @@ import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
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 { MoveEndPhase } from "#app/phases/move-end-phase";
import { MovePhase } from "#app/phases/move-phase";
@ -46,6 +57,10 @@ import { applyChallenges, ChallengeType } from "./challenge";
import { SwitchType } from "#enums/switch-type";
import { StatusEffect } from "#enums/status-effect";
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 {
PHYSICAL,
@ -1057,7 +1072,7 @@ export abstract class MoveAttr {
* @param args Set of unique arguments needed by this attribute
* @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;
}
@ -1200,7 +1215,7 @@ export class MoveEffectAttr extends MoveAttr {
}
/** 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);
}
@ -1866,7 +1881,7 @@ export class FlameBurstAttr extends MoveEffectAttr {
* @param args - n/a
* @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 cancelled = new Utils.BooleanHolder(false);
@ -2406,32 +2421,27 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
this.chance = chance;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise<boolean>(resolve => {
if (move.hitsSubstitute(user, target)) {
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;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (move.hitsSubstitute(user, target)) {
return false;
}
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[] {
@ -2875,9 +2885,7 @@ export class WeatherInstantChargeAttr extends InstantChargeAttr {
}
export class OverrideMoveEffectAttr extends MoveAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise<boolean> {
//const overridden = args[0] as Utils.BooleanHolder;
//const virtual = arg[1] as boolean;
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return true;
}
}
@ -2903,26 +2911,27 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
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
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);
});
} else {
globalScene.ui.showText(i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(target.id) ?? undefined), moveName: move.name }), null, () => resolve(true));
}
});
const overridden = args[0] as Utils.BooleanHolder;
const virtual = args[1] as boolean;
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
* @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))) {
return false;
}
@ -3131,7 +3140,7 @@ export class SecretPowerAttr extends MoveEffectAttr {
* Used to apply the secondary effect to the target Pokemon
* @returns `true` if a secondary effect is successfully applied
*/
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean | Promise<boolean> {
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
if (!super.apply(user, target, move, args)) {
return false;
}
@ -3286,8 +3295,8 @@ export class AcupressureStatStageChangeAttr extends MoveEffectAttr {
super();
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise<boolean> {
const randStats = BATTLE_STATS.filter(s => target.getStatStage(s) < 6);
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const randStats = BATTLE_STATS.filter((s) => target.getStatStage(s) < 6);
if (randStats.length > 0) {
const boostStat = [ randStats[user.randSeedInt(randStats.length)] ];
globalScene.unshiftPhase(new StatStageChangePhase(target.getBattlerIndex(), this.selfTarget, boostStat, 2));
@ -3324,17 +3333,14 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr {
this.messageCallback = messageCallback;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise<boolean>(resolve => {
user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / this.cutRatio), HitResult.OTHER, false, true);
user.updateInfo().then(() => {
const ret = super.apply(user, target, move, args);
if (this.messageCallback) {
this.messageCallback(user);
}
resolve(ret);
});
});
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / this.cutRatio), HitResult.OTHER, false, true);
user.updateInfo();
const ret = super.apply(user, target, move, args);
if (this.messageCallback) {
this.messageCallback(user);
}
return ret;
}
getCondition(): MoveConditionFunc {
@ -3426,28 +3432,27 @@ export class ResetStatsAttr extends MoveEffectAttr {
super();
this.targetAllPokemon = targetAllPokemon;
}
async apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
const promises: Promise<void>[] = [];
if (this.targetAllPokemon) { // Target all pokemon on the field when Freezy Frost or Haze are used
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
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"));
} else { // Affects only the single target when Clear Smog is used
if (!move.hitsSubstitute(user, target)) {
promises.push(this.resetStats(target));
this.resetStats(target);
globalScene.queueMessage(i18next.t("moveTriggers:resetStats", { pokemonName: getPokemonNameWithAffix(target) }));
}
}
await Promise.all(promises);
return true;
}
async resetStats(pokemon: Pokemon): Promise<void> {
private resetStats(pokemon: Pokemon): void {
for (const s of BATTLE_STATS) {
pokemon.setStatStage(s, 0);
}
return pokemon.updateInfo();
pokemon.updateInfo();
}
}
@ -3503,43 +3508,28 @@ export class SwapStatStagesAttr extends MoveEffectAttr {
}
export class HpSplitAttr extends MoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise(resolve => {
if (!super.apply(user, target, move, args)) {
return resolve(false);
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!super.apply(user, target, move, args)) {
return false;
}
const infoUpdates: Promise<void>[] = [];
const hpValue = Math.floor((target.hp + user.hp) / 2);
if (user.hp < hpValue) {
const healing = user.heal(hpValue - user.hp);
const hpValue = Math.floor((target.hp + user.hp) / 2);
[ user, target ].forEach((p) => {
if (p.hp < hpValue) {
const healing = p.heal(hpValue - p.hp);
if (healing) {
globalScene.damageNumberHandler.add(user, healing, HitResult.HEAL);
globalScene.damageNumberHandler.add(p, healing, HitResult.HEAL);
}
} else if (user.hp > hpValue) {
const damage = user.damage(user.hp - hpValue, true);
} else if (p.hp > hpValue) {
const damage = p.damage(p.hp - hpValue, true);
if (damage) {
globalScene.damageNumberHandler.add(user, damage);
globalScene.damageNumberHandler.add(p, damage);
}
}
infoUpdates.push(user.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));
p.updateInfo();
});
return true;
}
}
@ -6024,44 +6014,44 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
* @param args N/A
* @returns Promise, true if function succeeds.
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise(resolve => {
// If user is player, checks if the user has fainted pokemon
if (user instanceof PlayerPokemon
&& globalScene.getPlayerParty().findIndex(p => p.isFainted()) > -1) {
(user as PlayerPokemon).revivalBlessing().then(() => {
resolve(true);
});
// If user is enemy, checks that it is a trainer, and it has fainted non-boss pokemon in party
} else if (user instanceof EnemyPokemon
&& user.hasTrainer()
&& globalScene.getEnemyParty().findIndex(p => p.isFainted() && !p.isBoss()) > -1) {
// Selects a random fainted pokemon
const faintedPokemon = globalScene.getEnemyParty().filter(p => p.isFainted() && !p.isBoss());
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);
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
// If user is player, checks if the user has fainted pokemon
if (user instanceof PlayerPokemon) {
globalScene.unshiftPhase(new RevivalBlessingPhase(user));
return true;
} else if (user instanceof EnemyPokemon && user.hasTrainer() && globalScene.getEnemyParty().findIndex((p) => p.isFainted() && !p.isBoss()) > -1) {
// If used by an enemy trainer with at least one fainted non-boss Pokemon, this
// revives one of said Pokemon selected at random.
const faintedPokemon = globalScene.getEnemyParty().filter((p) => p.isFainted() && !p.isBoss());
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) {
const allyPokemon = user.getAlly();
if (slotIndex <= 1) {
globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, pokemon.getFieldIndex(), slotIndex, false, false));
} else if (allyPokemon.isFainted()) {
globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, false));
}
if (globalScene.currentBattle.double && globalScene.getEnemyParty().length > 1) {
const allyPokemon = user.getAlly();
if (slotIndex <= 1) {
globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, pokemon.getFieldIndex(), slotIndex, false, false));
} else if (allyPokemon.isFainted()) {
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 {
if (user.hasTrainer() && globalScene.getEnemyParty().findIndex(p => p.isFainted() && !p.isBoss()) > -1) {
getCondition(): MoveConditionFunc {
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;
}
@ -6579,7 +6569,7 @@ export class FirstMoveTypeAttr extends MoveEffectAttr {
class CallMoveAttr extends OverrideMoveEffectAttr {
protected invalidMoves: Moves[];
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 moveTargets = getMoveTargets(user, move.id, replaceMoveTarget);
if (moveTargets.targets.length === 0) {
@ -6589,11 +6579,8 @@ class CallMoveAttr extends OverrideMoveEffectAttr {
? moveTargets.targets
: [ 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 });
globalScene.unshiftPhase(new LoadMoveAnimPhase(move.id));
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;
}
}
@ -6626,7 +6613,7 @@ export class RandomMoveAttr extends CallMoveAttr {
* @param move Move being used
* @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);
let moveId: Moves = Moves.NONE;
do {
@ -6663,7 +6650,7 @@ export class RandomMovesetMoveAttr extends CallMoveAttr {
* @param move Move being used
* @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);
}
@ -6965,145 +6952,141 @@ const invalidCopycatMoves = [
];
export class NaturePowerAttr extends OverrideMoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise(resolve => {
let moveId;
switch (globalScene.arena.getTerrainType()) {
// this allows terrains to 'override' the biome move
case TerrainType.NONE:
switch (globalScene.arena.biomeType) {
case Biome.TOWN:
moveId = Moves.ROUND;
break;
case Biome.METROPOLIS:
moveId = Moves.TRI_ATTACK;
break;
case Biome.SLUM:
moveId = Moves.SLUDGE_BOMB;
break;
case Biome.PLAINS:
moveId = Moves.SILVER_WIND;
break;
case Biome.GRASS:
moveId = Moves.GRASS_KNOT;
break;
case Biome.TALL_GRASS:
moveId = Moves.POLLEN_PUFF;
break;
case Biome.MEADOW:
moveId = Moves.GIGA_DRAIN;
break;
case Biome.FOREST:
moveId = Moves.BUG_BUZZ;
break;
case Biome.JUNGLE:
moveId = Moves.LEAF_STORM;
break;
case Biome.SEA:
moveId = Moves.HYDRO_PUMP;
break;
case Biome.SWAMP:
moveId = Moves.MUD_BOMB;
break;
case Biome.BEACH:
moveId = Moves.SCALD;
break;
case Biome.LAKE:
moveId = Moves.BUBBLE_BEAM;
break;
case Biome.SEABED:
moveId = Moves.BRINE;
break;
case Biome.ISLAND:
moveId = Moves.LEAF_TORNADO;
break;
case Biome.MOUNTAIN:
moveId = Moves.AIR_SLASH;
break;
case Biome.BADLANDS:
moveId = Moves.EARTH_POWER;
break;
case Biome.DESERT:
moveId = Moves.SCORCHING_SANDS;
break;
case Biome.WASTELAND:
moveId = Moves.DRAGON_PULSE;
break;
case Biome.CONSTRUCTION_SITE:
moveId = Moves.STEEL_BEAM;
break;
case Biome.CAVE:
moveId = Moves.POWER_GEM;
break;
case Biome.ICE_CAVE:
moveId = Moves.ICE_BEAM;
break;
case Biome.SNOWY_FOREST:
moveId = Moves.FROST_BREATH;
break;
case Biome.VOLCANO:
moveId = Moves.LAVA_PLUME;
break;
case Biome.GRAVEYARD:
moveId = Moves.SHADOW_BALL;
break;
case Biome.RUINS:
moveId = Moves.ANCIENT_POWER;
break;
case Biome.TEMPLE:
moveId = Moves.EXTRASENSORY;
break;
case Biome.DOJO:
moveId = Moves.FOCUS_BLAST;
break;
case Biome.FAIRY_CAVE:
moveId = Moves.ALLURING_VOICE;
break;
case Biome.ABYSS:
moveId = Moves.OMINOUS_WIND;
break;
case Biome.SPACE:
moveId = Moves.DRACO_METEOR;
break;
case Biome.FACTORY:
moveId = Moves.FLASH_CANNON;
break;
case Biome.LABORATORY:
moveId = Moves.ZAP_CANNON;
break;
case Biome.POWER_PLANT:
moveId = Moves.CHARGE_BEAM;
break;
case Biome.END:
moveId = Moves.ETERNABEAM;
break;
}
break;
case TerrainType.MISTY:
moveId = Moves.MOONBLAST;
break;
case TerrainType.ELECTRIC:
moveId = Moves.THUNDERBOLT;
break;
case TerrainType.GRASSY:
moveId = Moves.ENERGY_BALL;
break;
case TerrainType.PSYCHIC:
moveId = Moves.PSYCHIC;
break;
default:
// Just in case there's no match
moveId = Moves.TRI_ATTACK;
break;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
let moveId;
switch (globalScene.arena.getTerrainType()) {
// this allows terrains to 'override' the biome move
case TerrainType.NONE:
switch (globalScene.arena.biomeType) {
case Biome.TOWN:
moveId = Moves.ROUND;
break;
case Biome.METROPOLIS:
moveId = Moves.TRI_ATTACK;
break;
case Biome.SLUM:
moveId = Moves.SLUDGE_BOMB;
break;
case Biome.PLAINS:
moveId = Moves.SILVER_WIND;
break;
case Biome.GRASS:
moveId = Moves.GRASS_KNOT;
break;
case Biome.TALL_GRASS:
moveId = Moves.POLLEN_PUFF;
break;
case Biome.MEADOW:
moveId = Moves.GIGA_DRAIN;
break;
case Biome.FOREST:
moveId = Moves.BUG_BUZZ;
break;
case Biome.JUNGLE:
moveId = Moves.LEAF_STORM;
break;
case Biome.SEA:
moveId = Moves.HYDRO_PUMP;
break;
case Biome.SWAMP:
moveId = Moves.MUD_BOMB;
break;
case Biome.BEACH:
moveId = Moves.SCALD;
break;
case Biome.LAKE:
moveId = Moves.BUBBLE_BEAM;
break;
case Biome.SEABED:
moveId = Moves.BRINE;
break;
case Biome.ISLAND:
moveId = Moves.LEAF_TORNADO;
break;
case Biome.MOUNTAIN:
moveId = Moves.AIR_SLASH;
break;
case Biome.BADLANDS:
moveId = Moves.EARTH_POWER;
break;
case Biome.DESERT:
moveId = Moves.SCORCHING_SANDS;
break;
case Biome.WASTELAND:
moveId = Moves.DRAGON_PULSE;
break;
case Biome.CONSTRUCTION_SITE:
moveId = Moves.STEEL_BEAM;
break;
case Biome.CAVE:
moveId = Moves.POWER_GEM;
break;
case Biome.ICE_CAVE:
moveId = Moves.ICE_BEAM;
break;
case Biome.SNOWY_FOREST:
moveId = Moves.FROST_BREATH;
break;
case Biome.VOLCANO:
moveId = Moves.LAVA_PLUME;
break;
case Biome.GRAVEYARD:
moveId = Moves.SHADOW_BALL;
break;
case Biome.RUINS:
moveId = Moves.ANCIENT_POWER;
break;
case Biome.TEMPLE:
moveId = Moves.EXTRASENSORY;
break;
case Biome.DOJO:
moveId = Moves.FOCUS_BLAST;
break;
case Biome.FAIRY_CAVE:
moveId = Moves.ALLURING_VOICE;
break;
case Biome.ABYSS:
moveId = Moves.OMINOUS_WIND;
break;
case Biome.SPACE:
moveId = Moves.DRACO_METEOR;
break;
case Biome.FACTORY:
moveId = Moves.FLASH_CANNON;
break;
case Biome.LABORATORY:
moveId = Moves.ZAP_CANNON;
break;
case Biome.POWER_PLANT:
moveId = Moves.CHARGE_BEAM;
break;
case Biome.END:
moveId = Moves.ETERNABEAM;
break;
}
break;
case TerrainType.MISTY:
moveId = Moves.MOONBLAST;
break;
case TerrainType.ELECTRIC:
moveId = Moves.THUNDERBOLT;
break;
case TerrainType.GRASSY:
moveId = Moves.ENERGY_BALL;
break;
case TerrainType.PSYCHIC:
moveId = Moves.PSYCHIC;
break;
default:
// Just in case there's no match
moveId = Moves.TRI_ATTACK;
break;
}
user.getMoveQueue().push({ move: moveId, targets: [ target.getBattlerIndex() ], ignorePP: true });
globalScene.unshiftPhase(new MovePhase(user, [ target.getBattlerIndex() ], new PokemonMove(moveId, 0, 0, true), true));
initMoveAnim(moveId).then(() => {
loadMoveAnimAssets([ moveId ], true)
.then(() => resolve(true));
});
});
user.getMoveQueue().push({ move: moveId, targets: [ target.getBattlerIndex() ], ignorePP: true });
globalScene.unshiftPhase(new LoadMoveAnimPhase(moveId));
globalScene.unshiftPhase(new MovePhase(user, [ target.getBattlerIndex() ], new PokemonMove(moveId, 0, 0, true), true));
return true;
}
}
@ -7121,7 +7104,7 @@ export class CopyMoveAttr extends CallMoveAttr {
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;
const lastMove = this.mirrorMove ? target.getLastXMoves()[0].move : globalScene.currentBattle.lastMove;
return super.apply(user, target, allMoves[lastMove], args);
@ -7682,50 +7665,15 @@ export class SuppressAbilitiesIfActedAttr extends MoveEffectAttr {
* Used by Transform
*/
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)) {
return false;
}
const promises: Promise<void>[] = [];
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.unshiftPhase(new PokemonTransformPhase(user.getBattlerIndex(), target.getBattlerIndex()));
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;
}
}
@ -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 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;
function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise<void> {
return new Promise(resolve => {
const attrPromises: Promise<boolean>[] = [];
const moveAttrs = move.attrs.filter(a => attrFilter(a));
for (const attr of moveAttrs) {
const result = attr.apply(user, target, move, args);
if (result instanceof Promise) {
attrPromises.push(result);
}
}
Promise.allSettled(attrPromises).then(() => resolve());
});
function applyMoveAttrsInternal(
attrFilter: MoveAttrFilter,
user: Pokemon | null,
target: Pokemon | null,
move: Move,
args: any[],
): void {
move.attrs.filter((attr) => attrFilter(attr)).forEach((attr) => attr.apply(user, target, move, args));
}
function applyMoveChargeAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: ChargingMove, args: any[]): Promise<void> {
return new Promise(resolve => {
const chargeAttrPromises: Promise<boolean>[] = [];
const chargeMoveAttrs = move.chargeAttrs.filter(a => attrFilter(a));
for (const attr of chargeMoveAttrs) {
const result = attr.apply(user, target, move, args);
if (result instanceof Promise) {
chargeAttrPromises.push(result);
}
}
Promise.allSettled(chargeAttrPromises).then(() => resolve());
});
function applyMoveChargeAttrsInternal(
attrFilter: MoveAttrFilter,
user: Pokemon | null,
target: Pokemon | null,
move: ChargingMove,
args: any[],
): void {
move.chargeAttrs.filter((attr) => attrFilter(attr)).forEach((attr) => attr.apply(user, target, move, args));
}
export function applyMoveAttrs(attrType: Constructor<MoveAttr>, user: Pokemon | null, target: Pokemon | null, move: Move, ...args: any[]): Promise<void> {
return applyMoveAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args);
export function applyMoveAttrs(
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> {
return applyMoveAttrsInternal(attrFilter, user, target, move, args);
export function applyFilteredMoveAttrs(
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> {
return applyMoveChargeAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args);
export function applyMoveChargeAttrs(
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 {
@ -9914,7 +9922,8 @@ export function initMoves() {
.attr(RemoveHeldItemAttr, true),
new StatusMove(Moves.QUASH, Type.DARK, 100, 15, -1, 0, 5)
.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)
.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)

View File

@ -151,7 +151,7 @@ async function spawnNextTrainerOrEndEncounter() {
// Give 10x Voucher
const newModifier = modifierTypes.VOUCHER_PREMIUM().newModifier();
await globalScene.addModifier(newModifier);
globalScene.addModifier(newModifier);
globalScene.playSound("item_fanfare");
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
for (const item of transformation.heldItems) {
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
if (shouldGetOldGateau(newPokemon)) {
@ -416,7 +416,7 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) {
?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU);
const modifier = modType?.newModifier(newPokemon);
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);
const modifier = modType?.newModifier(pokemon);
if (modifier) {
await globalScene.addModifier(modifier, false, false, false, true);
globalScene.addModifier(modifier, false, false, false, true);
pokemon.calculateStats();
}
}
@ -359,7 +359,7 @@ export async function applyModifierTypeToPlayerPokemon(pokemon: PlayerPokemon, m
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.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("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.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_ABILITY,
CYCLE_NATURE,
V,
CYCLE_TERA,
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 { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase";
import { Challenges } from "#enums/challenges";
import { PokemonAnimType } from "#enums/pokemon-anim-type";
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);
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 spAtk = this.getStat(Stat.SPATK);
const worseCategory: MoveCategory = atk > spAtk ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL;
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. */
let weightMultiplier = 0.9;
@ -4509,43 +4510,6 @@ export class PlayerPokemon extends Pokemon {
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> {
if (!evolution) {
@ -4727,70 +4691,62 @@ export class PlayerPokemon extends Pokemon {
}
/**
* Returns a Promise to fuse two PlayerPokemon together
* @param pokemon The PlayerPokemon to fuse to this one
*/
fuse(pokemon: PlayerPokemon): Promise<void> {
return new Promise(resolve => {
this.fusionSpecies = pokemon.species;
this.fusionFormIndex = pokemon.formIndex;
this.fusionAbilityIndex = pokemon.abilityIndex;
this.fusionShiny = pokemon.shiny;
this.fusionVariant = pokemon.variant;
this.fusionGender = pokemon.gender;
this.fusionLuck = pokemon.luck;
this.fusionCustomPokemonData = pokemon.customPokemonData;
if ((pokemon.pauseEvolutions) || (this.pauseEvolutions)) {
this.pauseEvolutions = true;
}
* Returns a Promise to fuse two PlayerPokemon together
* @param pokemon The PlayerPokemon to fuse to this one
*/
fuse(pokemon: PlayerPokemon): void {
this.fusionSpecies = pokemon.species;
this.fusionFormIndex = pokemon.formIndex;
this.fusionAbilityIndex = pokemon.abilityIndex;
this.fusionShiny = pokemon.shiny;
this.fusionVariant = pokemon.variant;
this.fusionGender = pokemon.gender;
this.fusionLuck = pokemon.luck;
this.fusionCustomPokemonData = pokemon.customPokemonData;
if (pokemon.pauseEvolutions || this.pauseEvolutions) {
this.pauseEvolutions = true;
}
globalScene.validateAchv(achvs.SPLICE);
globalScene.gameData.gameStats.pokemonFused++;
globalScene.validateAchv(achvs.SPLICE);
globalScene.gameData.gameStats.pokemonFused++;
// Store the average HP% that each Pokemon has
const maxHp = this.getMaxHp();
const newHpPercent = ((pokemon.hp / pokemon.getMaxHp()) + (this.hp / maxHp)) / 2;
// Store the average HP% that each Pokemon has
const maxHp = this.getMaxHp();
const newHpPercent = (pokemon.hp / pokemon.getMaxHp() + this.hp / maxHp) / 2;
this.generateName();
this.calculateStats();
this.generateName();
this.calculateStats();
// Set this Pokemon's HP to the average % of both fusion components
this.hp = Math.round(maxHp * newHpPercent);
if (!this.isFainted()) {
// 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.status = getRandomStatus(this.status, pokemon.status); // Get a random valid status between the two
} else if (!pokemon.isFainted()) {
// 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.status = pokemon.status; // Inherit the other Pokemon's status
}
// Set this Pokemon's HP to the average % of both fusion components
this.hp = Math.round(maxHp * newHpPercent);
if (!this.isFainted()) {
// 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.status = getRandomStatus(this.status, pokemon.status); // Get a random valid status between the two
} else if (!pokemon.isFainted()) {
// 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.status = pokemon.status; // Inherit the other Pokemon's status
}
this.generateCompatibleTms();
this.updateInfo(true);
const fusedPartyMemberIndex = globalScene.getPlayerParty().indexOf(pokemon);
let partyMemberIndex = globalScene.getPlayerParty().indexOf(this);
if (partyMemberIndex > fusedPartyMemberIndex) {
partyMemberIndex--;
}
const fusedPartyMemberHeldModifiers = globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier
&& m.pokemonId === pokemon.id, true) as PokemonHeldItemModifier[];
const transferModifiers: Promise<boolean>[] = [];
for (const modifier of fusedPartyMemberHeldModifiers) {
transferModifiers.push(globalScene.tryTransferHeldItemModifier(modifier, this, false, modifier.getStackCount(), true, true, false));
}
Promise.allSettled(transferModifiers).then(() => {
globalScene.updateModifiers(true, true).then(() => {
globalScene.removePartyMemberModifiers(fusedPartyMemberIndex);
globalScene.getPlayerParty().splice(fusedPartyMemberIndex, 1)[0];
const newPartyMemberIndex = globalScene.getPlayerParty().indexOf(this);
pokemon.getMoveset(true).map((m: PokemonMove) => globalScene.unshiftPhase(new LearnMovePhase(newPartyMemberIndex, m.getMove().id)));
pokemon.destroy();
this.updateFusionPalette();
resolve();
});
});
});
this.generateCompatibleTms();
this.updateInfo(true);
const fusedPartyMemberIndex = globalScene.getPlayerParty().indexOf(pokemon);
let partyMemberIndex = globalScene.getPlayerParty().indexOf(this);
if (partyMemberIndex > fusedPartyMemberIndex) {
partyMemberIndex--;
}
const fusedPartyMemberHeldModifiers = globalScene.findModifiers((m) => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id, true) as PokemonHeldItemModifier[];
for (const modifier of fusedPartyMemberHeldModifiers) {
globalScene.tryTransferHeldItemModifier(modifier, this, false, modifier.getStackCount(), true, true, false);
}
globalScene.updateModifiers(true, true);
globalScene.removePartyMemberModifiers(fusedPartyMemberIndex);
globalScene.getPlayerParty().splice(fusedPartyMemberIndex, 1)[0];
const newPartyMemberIndex = globalScene.getPlayerParty().indexOf(this);
pokemon.getMoveset(true).map((m: PokemonMove) => globalScene.unshiftPhase(new LearnMovePhase(newPartyMemberIndex, m.getMove().id)));
pokemon.destroy();
this.updateFusionPalette();
}
unfuse(): Promise<void> {

View File

@ -1822,7 +1822,7 @@ const modifierPool: ModifierPool = {
if (!isHoldingOrb) {
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
const hasStatusMoves = [ Moves.FACADE, Moves.PSYCHO_SHIFT ]

View File

@ -158,7 +158,7 @@ export abstract class Modifier {
* Handles applying of {@linkcode Modifier}
* @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 {
@ -1949,7 +1949,7 @@ export abstract class ConsumablePokemonModifier extends ConsumableModifier {
* @param playerPokemon The {@linkcode PlayerPokemon} that consumes the item
* @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() {
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}
* @returns always Promise<true>
*/
override async apply(playerPokemon: PlayerPokemon, playerPokemon2: PlayerPokemon): Promise<boolean> {
await playerPokemon.fuse(playerPokemon2);
override apply(playerPokemon: PlayerPokemon, playerPokemon2: PlayerPokemon): boolean {
playerPokemon.fuse(playerPokemon2);
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 tierItemModifiers = itemModifiers.filter(m => m.type.getOrInferTier(poolType) === highestItemTier);
const heldItemTransferPromises: Promise<void>[] = [];
for (let i = 0; i < transferredItemCount; i++) {
if (!tierItemModifiers.length) {
while (highestItemTier-- && !tierItemModifiers.length) {
@ -3149,19 +3147,15 @@ export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier {
}
const randItemIndex = pokemon.randSeedInt(itemModifiers.length);
const randItem = itemModifiers[randItemIndex];
heldItemTransferPromises.push(globalScene.tryTransferHeldItemModifier(randItem, pokemon, false).then(success => {
if (success) {
transferredModifierTypes.push(randItem.type);
itemModifiers.splice(randItemIndex, 1);
}
}));
if (globalScene.tryTransferHeldItemModifier(randItem, pokemon, false)) {
transferredModifierTypes.push(randItem.type);
itemModifiers.splice(randItemIndex, 1);
}
}
Promise.all(heldItemTransferPromises).then(() => {
for (const mt of transferredModifierTypes) {
globalScene.queueMessage(this.getTransferMessage(pokemon, targetPokemon, mt));
}
});
for (const mt of transferredModifierTypes) {
globalScene.queueMessage(this.getTransferMessage(pokemon, targetPokemon, mt));
}
return !!transferredModifierTypes.length;
}
@ -3390,7 +3384,7 @@ abstract class EnemyDamageMultiplierModifier extends EnemyPersistentModifier {
* @returns always `true`
*/
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;
}

View File

@ -21,6 +21,7 @@ export class AddEnemyBuffModifierPhase extends Phase {
for (let i = 0; i < count; i++) {
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 i18next from "i18next";
import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters";
import { Nature } from "#enums/nature";
export class EncounterPhase extends BattlePhase {
private loaded: boolean;
@ -156,7 +157,31 @@ export class EncounterPhase extends BattlePhase {
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;
});

View File

@ -12,16 +12,22 @@ export class GameOverModifierRewardPhase extends ModifierRewardPhase {
doReward(): Promise<void> {
return new Promise<void>(resolve => {
const newModifier = this.modifierType.newModifier();
globalScene.addModifier(newModifier).then(() => {
// Sound loaded into game as is
globalScene.playSound("level_up_fanfare");
globalScene.ui.setMode(Mode.MESSAGE);
globalScene.ui.fadeIn(250).then(() => {
globalScene.ui.showText(i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name }), null, () => {
globalScene.addModifier(newModifier);
// Sound loaded into game as is
globalScene.playSound("level_up_fanfare");
globalScene.ui.setMode(Mode.MESSAGE);
globalScene.ui.fadeIn(250).then(() => {
globalScene.ui.showText(
i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name }),
null,
() => {
globalScene.time.delayedCall(1500, () => globalScene.arenaBg.setVisible(true));
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> {
return new Promise<void>(resolve => {
const newModifier = this.modifierType.newModifier();
globalScene.addModifier(newModifier).then(() => {
globalScene.playSound("item_fanfare");
globalScene.ui.showText(i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name }), null, () => resolve(), null, true);
});
globalScene.addModifier(newModifier);
globalScene.playSound("item_fanfare");
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, () => {
move.showChargeText(user, target);
applyMoveChargeAttrs(MoveEffectAttr, user, target, move).then(() => {
user.addTag(BattlerTagType.CHARGING, 1, move.id, user.id);
this.end();
});
applyMoveChargeAttrs(MoveEffectAttr, user, target, move);
user.addTag(BattlerTagType.CHARGING, 1, move.id, user.id);
this.end();
});
}

View File

@ -61,7 +61,7 @@ import {
PokemonMultiHitModifier,
} from "#app/modifier/modifier";
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 { BattlerTagType } from "#enums/battler-tag-type";
import type { Moves } from "#enums/moves";
@ -143,86 +143,86 @@ export class MoveEffectPhase extends PokemonPhase {
const move = this.move.getMove();
// Assume single target for override
applyMoveAttrs(OverrideMoveEffectAttr, user, this.getFirstTarget() ?? null, move, overridden, this.move.virtual).then(() => {
// If other effects were overriden, stop this phase before they can be applied
if (overridden.value) {
return this.end();
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) {
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);
// 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();
}
/** 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()!), () => {
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? */
let hasHit: boolean = false;
@ -313,7 +313,7 @@ export class MoveEffectPhase extends PokemonPhase {
}
/** 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
if (firstHit) {
@ -363,7 +363,7 @@ export class MoveEffectPhase extends PokemonPhase {
}
/** 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,
@ -381,43 +381,29 @@ export class MoveEffectPhase extends PokemonPhase {
}
}
/**
* Create a Promise that applies *all* effects from the invoked move's MoveEffectAttrs.
* These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger
* type requires different conditions to be met with respect to the move's hit result.
*/
const k = new Promise<void>((resolve) => {
//Start promise chain and apply PRE_APPLY move attributes
let promiseChain: Promise<void | null> = applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.PRE_APPLY
&& (!attr.firstHitOnly || firstHit)
&& (!attr.lastHitOnly || lastHit)
&& hitResult !== HitResult.NO_EFFECT, user, target, move);
applyFilteredMoveAttrs(
(attr: MoveAttr) =>
attr instanceof MoveEffectAttr &&
attr.trigger === MoveEffectTrigger.PRE_APPLY &&
(!attr.firstHitOnly || firstHit) &&
(!attr.lastHitOnly || lastHit) &&
hitResult !== HitResult.NO_EFFECT,
user,
target,
move,
);
/** Don't complete if the move failed */
if (hitResult === HitResult.FAIL) {
return resolve();
}
/** Apply Move/Ability Effects in correct order */
promiseChain = promiseChain
.then(this.applySelfTargetEffects(user, target, firstHit, lastHit));
if (hitResult !== HitResult.FAIL) {
this.applySelfTargetEffects(user, target, firstHit, lastHit);
if (hitResult !== HitResult.NO_EFFECT) {
promiseChain
.then(this.applyPostApplyEffects(user, target, firstHit, lastHit))
.then(this.applyHeldItemFlinchCheck(user, target, dealsDamage))
.then(this.applySuccessfulAttackEffects(user, target, firstHit, lastHit, !!isProtected, hitResult, firstTarget))
.then(() => resolve());
this.applyPostApplyEffects(user, target, firstHit, lastHit);
this.applyHeldItemFlinchCheck(user, target, dealsDamage);
this.applySuccessfulAttackEffects(user, target, firstHit, lastHit, !!isProtected, hitResult, firstTarget);
} else {
promiseChain
.then(() => applyMoveAttrs(NoEffectAttr, user, null, move))
.then(resolve);
applyMoveAttrs(NoEffectAttr, user, null, move);
}
});
applyAttrs.push(k);
}
}
// Apply queued phases
@ -425,41 +411,35 @@ export class MoveEffectPhase extends PokemonPhase {
globalScene.appendToPhase(queuedPhases, MoveEndPhase);
}
// 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()) ?
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) :
null;
if (postTarget) {
if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after
applyAttrs[applyAttrs.length - 1].then(() => postTarget);
} else { // Otherwise, push a new asynchronous move effect
applyAttrs.push(postTarget);
}
if (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) {
applyFilteredMoveAttrs(
(attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET,
user,
null,
move,
);
}
// 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.
* This prevents blocked effects from applying until after this hit resolves.
*/
targets.forEach(target => {
const substitute = target.getTag(SubstituteTag);
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);
/**
* Remove the target's substitute (if it exists and has expired)
* 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);
if (substitute && substitute.hp <= 0) {
target.lapseTag(BattlerTagType.SUBSTITUTE);
}
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 {
@ -500,8 +480,8 @@ export class MoveEffectPhase extends PokemonPhase {
* @param lastHit - `true` if this is the last hit in a multi-hit attack
* @returns a function intended to be passed into a `then()` call.
*/
protected applySelfTargetEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise<void | null> {
return () => applyFilteredMoveAttrs((attr: MoveAttr) =>
protected applySelfTargetEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): void {
applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.POST_APPLY
&& 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
* @returns a function intended to be passed into a `then()` call.
*/
protected applyPostApplyEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise<void | null> {
return () => applyFilteredMoveAttrs((attr: MoveAttr) =>
protected applyPostApplyEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): void {
applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.POST_APPLY
&& !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}
* @returns a function intended to be passed into a `then()` call.
*/
protected applyOnHitEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, firstTarget: boolean): Promise<void> {
return applyFilteredMoveAttrs((attr: MoveAttr) =>
protected applyOnHitEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, firstTarget: boolean): void {
applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.HIT
&& (!attr.firstHitOnly || firstHit)
@ -554,21 +534,18 @@ export class MoveEffectPhase extends PokemonPhase {
* @param hitResult - The {@linkcode HitResult} of the attempted move
* @returns a `Promise` intended to be passed into a `then()` call.
*/
protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): Promise<void | null> {
return executeIf(!target.isFainted() || target.canApplyAbility(), () =>
applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult)
.then(() => {
protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): void {
if (!target.isFainted() || target.canApplyAbility()) {
applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult);
if (!this.move.getMove().hitsSubstitute(user, target)) {
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
globalScene.applyShuffledModifiers(EnemyAttackStatusEffectChanceModifier, false, target);
}
if (!this.move.getMove().hitsSubstitute(user, target)) {
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
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}
* @returns a function intended to be passed into a `then()` call.
*/
protected applySuccessfulAttackEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, isProtected : boolean, hitResult: HitResult, firstTarget: boolean) : () => Promise<void | null> {
return () => executeIf(!isProtected, () =>
this.applyOnHitEffects(user, target, firstHit, lastHit, firstTarget).then(() =>
this.applyOnGetHitAbEffects(user, target, hitResult)).then(() =>
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult)).then(() => { // Item Stealing Effects
if (this.move.getMove() instanceof AttackMove) {
globalScene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
}
})
);
protected applySuccessfulAttackEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean, isProtected: boolean, hitResult: HitResult, firstTarget: boolean): void {
if (!isProtected) {
this.applyOnHitEffects(user, target, firstHit, lastHit, firstTarget);
this.applyOnGetHitAbEffects(user, target, hitResult);
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult);
if (this.move.getMove() instanceof AttackMove) {
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
* @returns a function intended to be passed into a `then()` call.
*/
protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : () => void {
return () => {
if (this.move.getMove().hasAttr(FlinchAttr)) {
return;
}
protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : void {
if (this.move.getMove().hasAttr(FlinchAttr)) {
return;
}
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) {
const flinched = new BooleanHolder(false);
globalScene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
if (flinched.value) {
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
}
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) {
const flinched = new BooleanHolder(false);
globalScene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
if (flinched.value) {
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
}
};
}
}
/**

View File

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

View File

@ -56,6 +56,7 @@ export class MovePhase extends BattlePhase {
protected _targets: BattlerIndex[];
protected followUp: boolean;
protected ignorePp: boolean;
protected forcedLast: boolean;
protected failed: boolean = false;
protected cancelled: 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.
* 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();
this.pokemon = pokemon;
@ -99,6 +101,7 @@ export class MovePhase extends BattlePhase {
this.followUp = followUp;
this.ignorePp = ignorePp;
this.reflected = reflected;
this.forcedLast = forcedLast;
}
/**
@ -120,6 +123,15 @@ export class MovePhase extends BattlePhase {
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 {
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);
}
applyPostSummonAbAttrs(PostSummonAbAttr, pokemon)
.then(() => {
const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
field.forEach((p) => applyAbAttrs(CommanderAbAttr, p, null, false));
applyPostSummonAbAttrs(PostSummonAbAttr, pokemon);
const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
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> {
return new Promise<void>(resolve => {
const newModifier = this.modifierType.newModifier();
globalScene.addModifier(newModifier).then(() => {
globalScene.playSound("level_up_fanfare");
globalScene.ui.setMode(Mode.MESSAGE);
globalScene.ui.showText(i18next.t("battle:beatModeFirstTime", {
speciesName: this.species.name,
gameMode: globalScene.gameMode.getName(),
newModifier: newModifier?.type.name
}), null, () => {
resolve();
}, null, true, 1500);
});
globalScene.addModifier(newModifier);
globalScene.playSound("level_up_fanfare");
globalScene.ui.setMode(Mode.MESSAGE);
globalScene.ui.showText(i18next.t("battle:beatModeFirstTime", {
speciesName: this.species.name,
gameMode: globalScene.gameMode.getName(),
newModifier: newModifier?.type.name,
}), null, () => {
resolve();
}, null, true, 1500);
});
}
}

View File

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

View File

@ -12,6 +12,7 @@ import type { Starter } from "#app/ui/starter-select-ui-handler";
import { Mode } from "#app/ui/ui";
import type { Species } from "#enums/species";
import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
import * as Utils from "../utils";
export class SelectStarterPhase extends Phase {
@ -79,6 +80,12 @@ export class SelectStarterPhase extends Phase {
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) {
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 { pokerogueApi } from "#app/plugins/api/pokerogue-api";
import { ArenaTrapTag } from "#app/data/arena-tag";
import type { Type } from "#enums/type";
export const defaultStarterSpecies: Species[] = [
Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE,
@ -229,6 +230,7 @@ export interface StarterAttributes {
shiny?: boolean;
favorite?: boolean;
nickname?: string;
tera?: Type;
}
export interface StarterPreferences {

View File

@ -21,7 +21,7 @@ export enum SettingGamepad {
Button_Cycle_Gender = "BUTTON_CYCLE_GENDER",
Button_Cycle_Ability = "BUTTON_CYCLE_ABILITY",
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_Slow_Down = "BUTTON_SLOW_DOWN",
Button_Submit = "BUTTON_SUBMIT",
@ -45,7 +45,7 @@ export const settingGamepadOptions = {
[SettingGamepad.Button_Cycle_Gender]: [ `KEY ${Button.CYCLE_GENDER.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_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_Slow_Down]: [ `KEY ${Button.SLOW_DOWN.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_Ability]: 0,
[SettingGamepad.Button_Cycle_Nature]: 0,
[SettingGamepad.Button_Cycle_Variant]: 0,
[SettingGamepad.Button_Cycle_Tera]: 0,
[SettingGamepad.Button_Speed_Up]: 0,
[SettingGamepad.Button_Slow_Down]: 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_Ability:
case SettingGamepad.Button_Cycle_Nature:
case SettingGamepad.Button_Cycle_Variant:
case SettingGamepad.Button_Cycle_Tera:
case SettingGamepad.Button_Speed_Up:
case SettingGamepad.Button_Slow_Down:
case SettingGamepad.Button_Submit:

View File

@ -32,8 +32,8 @@ export enum SettingKeyboard {
Alt_Button_Cycle_Ability = "ALT_BUTTON_CYCLE_ABILITY",
Button_Cycle_Nature = "BUTTON_CYCLE_NATURE",
Alt_Button_Cycle_Nature = "ALT_BUTTON_CYCLE_NATURE",
Button_Cycle_Variant = "BUTTON_CYCLE_VARIANT",
Alt_Button_Cycle_Variant = "ALT_BUTTON_CYCLE_VARIANT",
Button_Cycle_Tera = "BUTTON_CYCLE_TERA",
Alt_Button_Cycle_Tera = "ALT_BUTTON_CYCLE_TERA",
Button_Speed_Up = "BUTTON_SPEED_UP",
Alt_Button_Speed_Up = "ALT_BUTTON_SPEED_UP",
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.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.Alt_Button_Cycle_Variant]: [ `KEY ${Button.V.toString()}`, pressAction ],
[SettingKeyboard.Button_Cycle_Tera]: [ `KEY ${Button.CYCLE_TERA.toString()}`, pressAction ],
[SettingKeyboard.Alt_Button_Cycle_Tera]: [ `KEY ${Button.CYCLE_TERA.toString()}`, pressAction ],
[SettingKeyboard.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 ],
@ -112,8 +112,8 @@ export const settingKeyboardDefaults = {
[SettingKeyboard.Alt_Button_Cycle_Ability]: 0,
[SettingKeyboard.Button_Cycle_Nature]: 0,
[SettingKeyboard.Alt_Button_Cycle_Nature]: 0,
[SettingKeyboard.Button_Cycle_Variant]: 0,
[SettingKeyboard.Alt_Button_Cycle_Variant]: 0,
[SettingKeyboard.Button_Cycle_Tera]: 0,
[SettingKeyboard.Alt_Button_Cycle_Tera]: 0,
[SettingKeyboard.Button_Speed_Up]: 0,
[SettingKeyboard.Alt_Button_Speed_Up]: 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_Ability:
case SettingKeyboard.Button_Cycle_Nature:
case SettingKeyboard.Button_Cycle_Variant:
case SettingKeyboard.Button_Cycle_Tera:
case SettingKeyboard.Button_Speed_Up:
case SettingKeyboard.Button_Slow_Down:
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_Ability:
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_Slow_Down:
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.SPLASH);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2 ]);
game.doSelectPartyPokemon(0, "MoveEffectPhase");
game.doSelectPartyPokemon(0, "RevivalBlessingPhase");
await game.toNextTurn();
expect(game.scene.getPlayerField()[0]).toBe(treecko);

View File

@ -1,4 +1,6 @@
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 { ArenaTagType } from "#enums/arena-tag-type";
import { Moves } from "#enums/moves";
@ -65,6 +67,28 @@ describe("Battle Mechanics - Damage Calculation", () => {
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 () => {
game.override
.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) {
const itemMod = itemType.newModifier(pokemon) as PokemonHeldItemModifier;
itemMod.stackCount = stackCount;
await scene.addModifier(itemMod, true, false, false, true);
scene.addModifier(itemMod, true, false, false, true);
await scene.updateModifiers(true);
}

View File

@ -123,8 +123,6 @@ describe("Dancing Lessons - Mystery Encounter", () => {
partyLead.level = 1000;
partyLead.calculateStats();
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 game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name);

View File

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

View File

@ -406,9 +406,9 @@ describe("Test Rebinding", () => {
});
it("check to delete all the binds of an action", () => {
inGame.whenWePressOnKeyboard("V").weShouldTriggerTheButton("Button_Cycle_Variant");
inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Cycle_Variant").thereShouldBeNoIcon().weWantThisBindInstead("K").confirm();
inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Cycle_Variant").iconDisplayedIs("KEY_K").whenWeDelete().thereShouldBeNoIconAnymore();
inTheSettingMenu.whenCursorIsOnSetting("Button_Cycle_Variant").iconDisplayedIs("KEY_V").whenWeDelete().thereShouldBeNoIconAnymore();
inGame.whenWePressOnKeyboard("V").weShouldTriggerTheButton("Button_Cycle_Tera");
inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Cycle_Tera").thereShouldBeNoIcon().weWantThisBindInstead("K").confirm();
inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Cycle_Tera").iconDisplayedIs("KEY_K").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">
<span class="apad-label">R</span>
</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>
</div>
<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">
<span class="apad-label">N</span>
</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>
</div>
</div>

View File

@ -91,6 +91,7 @@ export default class GameWrapper {
Pokemon.prototype.updateFusionPalette = () => null;
Pokemon.prototype.cry = () => null;
Pokemon.prototype.faintCry = (cb) => { if (cb) cb(); };
BattleScene.prototype.addPokemonIcon = () => new Phaser.GameObjects.Container(this.scene);
}
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 { UnlockPhase } from "#app/phases/unlock-phase";
import { PostGameOverPhase } from "#app/phases/post-game-over-phase";
import { RevivalBlessingPhase } from "#app/phases/revival-blessing-phase";
export interface PromptHandler {
phaseTarget?: string;
@ -126,7 +127,8 @@ type PhaseClass =
| typeof EncounterPhase
| typeof GameOverPhase
| typeof UnlockPhase
| typeof PostGameOverPhase;
| typeof PostGameOverPhase
| typeof RevivalBlessingPhase;
type PhaseString =
| "LoginPhase"
@ -185,7 +187,8 @@ type PhaseString =
| "EncounterPhase"
| "GameOverPhase"
| "UnlockPhase"
| "PostGameOverPhase";
| "PostGameOverPhase"
| "RevivalBlessingPhase";
type PhaseInterceptorPhase = PhaseClass | PhaseString;
@ -269,6 +272,7 @@ export default class PhaseInterceptor {
[ GameOverPhase, this.startPhase ],
[ UnlockPhase, this.startPhase ],
[ PostGameOverPhase, this.startPhase ],
[ RevivalBlessingPhase, this.startPhase ],
];
private endBySetMode = [
@ -511,11 +515,11 @@ export default class PhaseInterceptor {
if (expireFn) {
this.prompts.shift();
} else if (
currentMode === actionForNextPrompt.mode
&& currentPhase === actionForNextPrompt.phaseTarget
&& currentHandler.active
&& (!actionForNextPrompt.awaitingActionInput
|| (actionForNextPrompt.awaitingActionInput && currentHandler.awaitingActionInput))
currentMode === actionForNextPrompt.mode &&
currentPhase === actionForNextPrompt.phaseTarget &&
currentHandler.active &&
(!actionForNextPrompt.awaitingActionInput ||
(actionForNextPrompt.awaitingActionInput && currentHandler.awaitingActionInput))
) {
const prompt = this.prompts.shift();
if (prompt?.callback) {

View File

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

View File

@ -1,6 +1,7 @@
import { globalScene } from "#app/global-scene";
import type Pokemon from "../field/pokemon";
import { TextStyle, addTextObject } from "./text";
import i18next from "i18next";
export default class PartyExpBar extends Phaser.GameObjects.Container {
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 (showOnlyLevelUp) {
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
this.expText.setText(`Lv. UP: ${newLevel.toString()}`);
this.expText.setText(i18next.t("battleScene:levelUpWithLevel", { level: newLevel }));
}
} else {
// 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 { BooleanHolder, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, rgbHexToRgba, toReadableString } from "#app/utils";
import type { Nature } from "#enums/nature";
import BgmBar from "./bgm-bar";
import * as Utils from "../utils";
import { speciesTmMoves } from "#app/data/balance/tms";
import type { BiomeTierTod } from "#app/data/balance/biomes";
@ -242,7 +241,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
private menuContainer: Phaser.GameObjects.Container;
private menuBg: Phaser.GameObjects.NineSlice;
protected optionSelectText: Phaser.GameObjects.Text;
public bgmBar: BgmBar;
private menuOptions: MenuOptions[];
protected scale: number = 0.1666666667;
private menuDescriptions: string[];
@ -480,10 +478,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
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.bgmBar = new BgmBar();
this.bgmBar.setup();
ui.bgmBar = this.bgmBar;
this.menuContainer.add(this.bgmBar);
this.menuContainer.setVisible(false);
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, {
options: this.levelMoves.map(m => {
const levelNumber = m[0] > 0 ? String(m[0]) : "";
const option: OptionSelectItem = {
label: String(m[0]).padEnd(4, " ") + allMoves[m[1]].name,
label: levelNumber.padEnd(4, " ") + allMoves[m[1]].name,
handler: () => {
return false;
},
onHover: () => {
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;

View File

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

View File

@ -93,9 +93,9 @@ export default class MoveTouchControlsHandler {
toolbar.innerHTML = `
<div class="column">
<div class="button-row">
<div id="resetButton" class="button">${i18next.t("settings:reset")}</div>
<div id="saveButton" class="button">${i18next.t("settings:saveClose")}</div>
<div id="cancelButton" class="button">${i18next.t("settings:cancel")}</div>
<div id="resetButton" class="button">${i18next.t("settings:touchReset")}</div>
<div id="saveButton" class="button">${i18next.t("settings:touchSaveClose")}</div>
<div id="cancelButton" class="button">${i18next.t("settings:touchCancel")}</div>
</div>
<div class="info-row">
<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 type { Nature } from "#enums/nature";
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;
@ -68,6 +70,7 @@ export interface Starter {
moveset?: StarterMoveset;
pokerus: boolean;
nickname?: string;
teraType?: Type;
}
interface LanguageSetting {
@ -212,6 +215,7 @@ interface SpeciesDetails {
abilityIndex?: number,
natureIndex?: number,
forSeen?: boolean, // default = false
teraType?: Type,
}
export default class StarterSelectUiHandler extends MessageUiHandler {
@ -262,6 +266,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
private pokemonShinyIcon: Phaser.GameObjects.Sprite;
private pokemonPassiveDisabledIcon: Phaser.GameObjects.Sprite;
private pokemonPassiveLockedIcon: Phaser.GameObjects.Sprite;
private teraIcon: Phaser.GameObjects.Sprite;
private activeTooltip: "ABILITY" | "PASSIVE" | "CANDY" | undefined;
private instructionsContainer: Phaser.GameObjects.Container;
@ -271,12 +276,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
private abilityIconElement: Phaser.GameObjects.Sprite;
private genderIconElement: Phaser.GameObjects.Sprite;
private natureIconElement: Phaser.GameObjects.Sprite;
private teraIconElement: Phaser.GameObjects.Sprite;
private goFilterIconElement: Phaser.GameObjects.Sprite;
private shinyLabel: Phaser.GameObjects.Text;
private formLabel: Phaser.GameObjects.Text;
private genderLabel: Phaser.GameObjects.Text;
private abilityLabel: Phaser.GameObjects.Text;
private natureLabel: Phaser.GameObjects.Text;
private teraLabel: Phaser.GameObjects.Text;
private goFilterLabel: Phaser.GameObjects.Text;
private starterSelectMessageBox: Phaser.GameObjects.NineSlice;
@ -292,6 +299,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
private dexAttrCursor: bigint = 0n;
private abilityCursor: number = -1;
private natureCursor: number = -1;
private teraCursor: Type = Type.UNKNOWN;
private filterBarCursor: number = 0;
private starterMoveset: StarterMoveset | null;
private scrollCursor: number;
@ -304,6 +312,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
private starterAttr: bigint[] = [];
private starterAbilityIndexes: number[] = [];
private starterNatures: Nature[] = [];
private starterTeras: Type[] = [];
private starterMovesets: StarterMoveset[] = [];
private speciesStarterDexEntry: DexEntry | null;
private speciesStarterMoves: Moves[];
@ -312,6 +321,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
private canCycleGender: boolean;
private canCycleAbility: boolean;
private canCycleNature: boolean;
private canCycleTera: boolean;
private assetLoadCancelled: BooleanHolder | null;
public cursorObj: Phaser.GameObjects.Image;
@ -823,6 +833,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
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
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.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.setName("sprite-goFilter-icon-element");
this.goFilterIconElement.setScale(0.675);
@ -1497,6 +1519,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
const props = globalScene.gameData.getSpeciesDexAttrProps(randomSpecies, dexAttr);
const abilityIndex = this.abilityCursor;
const nature = this.natureCursor as unknown as Nature;
const teraType = this.teraCursor;
const moveset = this.starterMoveset?.slice(0) as StarterMoveset;
const starterCost = globalScene.gameData.getSpeciesStarterValue(randomSpecies.speciesId);
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)
.then(() => {
if (this.tryUpdateValue(starterCost, true)) {
this.addToParty(randomSpecies, dexAttr, abilityIndex, nature, moveset, true);
this.addToParty(randomSpecies, dexAttr, abilityIndex, nature, moveset, teraType, true);
ui.playSelect();
}
});
@ -1585,7 +1608,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
const cursorObj = this.starterCursorObjs[this.starterSpecies.length];
cursorObj.setVisible(true);
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();
} else {
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
});
};
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({
label: i18next.t("menuUiHandler:POKEDEX"),
handler: () => {
@ -1985,6 +1999,15 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
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({
label: i18next.t("menu:cancel"),
handler: () => {
@ -2066,7 +2089,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
}
} while (newFormIndex !== props.formIndex);
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;
}
break;
@ -2125,6 +2149,19 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
success = true;
}
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:
if (!this.starterIconsCursorObj.visible) {
if (currentRow > 0) {
@ -2289,7 +2326,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
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);
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));
@ -2299,6 +2336,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.starterAttr.push(dexAttr);
this.starterAbilityIndexes.push(abilityIndex);
this.starterNatures.push(nature);
this.starterTeras.push(teraType);
this.starterMovesets.push(moveset);
if (this.speciesLoaded.get(species.speciesId) || randomSelection ) {
getPokemonSpeciesForm(species.speciesId, props.formIndex).cry();
@ -2379,6 +2417,9 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
case SettingKeyboard.Button_Cycle_Nature:
iconPath = "N.png";
break;
case SettingKeyboard.Button_Cycle_Tera:
iconPath = "V.png";
break;
case SettingKeyboard.Button_Stats:
iconPath = "C.png";
break;
@ -2459,6 +2500,9 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
if (this.canCycleNature) {
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
@ -2876,6 +2920,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.dexAttrCursor = species ? this.getCurrentDexProps(species.speciesId) : 0n;
this.abilityCursor = species ? globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species) : 0;
this.natureCursor = species ? globalScene.gameData.getSpeciesDefaultNature(species) : 0;
this.teraCursor = species ? species.type1 : Type.UNKNOWN;
if (!species && globalScene.ui.getTooltip().visible) {
globalScene.ui.hideTooltip();
@ -2894,6 +2939,10 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
// load default ability from stater save data, if set
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.speciesStarterDexEntry?.caughtAttr) {
@ -3035,7 +3084,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
female: props.female,
variant: props.variant,
abilityIndex: this.starterAbilityIndexes[starterIndex],
natureIndex: this.starterNatures[starterIndex]
natureIndex: this.starterNatures[starterIndex],
teraType: this.starterTeras[starterIndex]
});
} else {
const defaultDexAttr = this.getCurrentDexProps(species.speciesId);
@ -3083,6 +3133,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonCaughtHatchedContainer.setVisible(false);
this.pokemonCandyContainer.setVisible(false);
this.pokemonFormText.setVisible(false);
this.teraIcon.setVisible(false);
const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, true, true);
const defaultAbilityIndex = globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species);
@ -3117,6 +3168,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonCaughtHatchedContainer.setVisible(false);
this.pokemonCandyContainer.setVisible(false);
this.pokemonFormText.setVisible(false);
this.teraIcon.setVisible(false);
this.setSpeciesDetails(species!, { // TODO: is this bang correct?
shiny: false,
@ -3131,7 +3183,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
}
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 oldProps = species ? globalScene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor) : null;
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.abilityCursor = -1;
this.natureCursor = -1;
this.teraCursor = Type.UNKNOWN;
// We will only update the sprite if there is a change to form, shiny/variant
// or gender for species with gender sprite differences
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.abilityCursor = abilityIndex !== undefined ? abilityIndex : (abilityIndex = oldAbilityIndex);
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
if (isInParty) {
this.updatePartyIcon(species, partyIndex);
@ -3179,6 +3233,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonPassiveText.setVisible(false);
this.pokemonPassiveDisabledIcon.setVisible(false);
this.pokemonPassiveLockedIcon.setVisible(false);
this.teraIcon.setVisible(false);
if (this.assetLoadCancelled) {
this.assetLoadCancelled.value = true;
@ -3230,6 +3285,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.starterAttr[starterIndex] = this.dexAttrCursor;
this.starterAbilityIndexes[starterIndex] = this.abilityCursor;
this.starterNatures[starterIndex] = this.natureCursor;
this.starterTeras[starterIndex] = this.teraCursor;
}
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))
.map((_, f) => dexEntry.caughtAttr & globalScene.gameData.getFormAttr(f)).filter(f => f).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) {
@ -3412,10 +3468,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonFormText.setText(formText);
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 {
this.pokemonAbilityText.setText("");
this.pokemonPassiveText.setText("");
this.pokemonNatureText.setText("");
this.teraIcon.setVisible(false);
this.setTypeIcons(null, null);
}
} else {
@ -3426,6 +3486,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonAbilityText.setText("");
this.pokemonPassiveText.setText("");
this.pokemonNatureText.setText("");
this.teraIcon.setVisible(false);
this.setTypeIcons(null, null);
}
@ -3479,6 +3540,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.starterAttr.splice(index, 1);
this.starterAbilityIndexes.splice(index, 1);
this.starterNatures.splice(index, 1);
this.starterTeras.splice(index, 1);
this.starterMovesets.splice(index, 1);
for (let s = 0; s < this.starterSpecies.length; s++) {
@ -3690,6 +3752,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
abilityIndex: thisObj.starterAbilityIndexes[i],
passive: !(globalScene.gameData.starterData[starterSpecies.speciesId].passiveAttr ^ (PassiveAttr.ENABLED | PassiveAttr.UNLOCKED)),
nature: thisObj.starterNatures[i] as Nature,
teraType: thisObj.starterTeras[i] as Type,
moveset: thisObj.starterMovesets[i],
pokerus: thisObj.pokerusSpecies.includes(starterSpecies),
nickname: thisObj.starterPreferences[starterSpecies.speciesId]?.nickname,
@ -3816,6 +3879,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.abilityLabel.setVisible(false);
this.natureIconElement.setVisible(false);
this.natureLabel.setVisible(false);
this.teraIconElement.setVisible(false);
this.teraLabel.setVisible(false);
this.goFilterIconElement.setVisible(false);
this.goFilterLabel.setVisible(false);
}

View File

@ -225,7 +225,7 @@ export default class SummaryUiHandler extends UiHandler {
this.summaryContainer.add(this.championRibbon);
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.summaryContainer.add(this.levelText);
@ -413,7 +413,7 @@ export default class SummaryUiHandler extends UiHandler {
}
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.setColor(getGenderColor(this.pokemon.getGender(true)));
this.genderText.setShadowColor(getGenderColor(this.pokemon.getGender(true), true));
@ -756,7 +756,7 @@ export default class SummaryUiHandler extends UiHandler {
trainerText.setOrigin(0, 0);
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);
profileContainer.add(trainerIdText);