mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-09-23 06:53:27 +02:00
Update main to 1.10.7
This commit is contained in:
commit
536b018dae
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pokemon-rogue-battle",
|
||||
"private": true,
|
||||
"version": "1.10.6",
|
||||
"version": "1.10.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 2686cd3edc0bd2c7a7f12cc54c00c109e51a48d7
|
||||
Subproject commit 090bfefaf7e9d4efcbca61fa78a9cdf5d701830b
|
@ -863,6 +863,8 @@ export class BattleScene extends SceneBase {
|
||||
* @param pokemonId - The ID whose Pokemon will be retrieved.
|
||||
* @returns The {@linkcode Pokemon} associated with the given id.
|
||||
* Returns `null` if the ID is `undefined` or not present in either party.
|
||||
* @todo Change the `null` to `undefined` and update callers' signatures -
|
||||
* this is weird and causes a lot of random jank
|
||||
*/
|
||||
getPokemonById(pokemonId: number | undefined): Pokemon | null {
|
||||
if (isNullOrUndefined(pokemonId)) {
|
||||
|
@ -396,7 +396,23 @@ export abstract class AbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
export class BlockRecoilDamageAttr extends AbAttr {
|
||||
/**
|
||||
* Abstract class for ability attributes that simply cancel an interaction
|
||||
*
|
||||
* @remarks
|
||||
* Abilities that have simple cancel interactions (e.g. {@linkcode BlockRecoilDamageAttr}) can extend this class to reuse the `canApply` and `apply` logic
|
||||
*/
|
||||
abstract class CancelInteractionAbAttr extends AbAttr {
|
||||
override canApply({ cancelled }: AbAttrParamsWithCancel): boolean {
|
||||
return !cancelled.value;
|
||||
}
|
||||
|
||||
override apply({ cancelled }: AbAttrParamsWithCancel): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
export class BlockRecoilDamageAttr extends CancelInteractionAbAttr {
|
||||
private declare readonly _: never;
|
||||
constructor() {
|
||||
super(false);
|
||||
@ -592,11 +608,7 @@ export class PreDefendFullHpEndureAbAttr extends PreDefendAbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
export class BlockItemTheftAbAttr extends AbAttr {
|
||||
override apply({ cancelled }: AbAttrParamsWithCancel): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
|
||||
export class BlockItemTheftAbAttr extends CancelInteractionAbAttr {
|
||||
getTriggerMessage({ pokemon }: AbAttrBaseParams, abilityName: string) {
|
||||
return i18next.t("abilityTriggers:blockItemTheft", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
@ -869,8 +881,9 @@ export interface FieldPriorityMoveImmunityAbAttrParams extends AugmentMoveIntera
|
||||
}
|
||||
|
||||
export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr {
|
||||
override canApply({ move, opponent: attacker }: FieldPriorityMoveImmunityAbAttrParams): boolean {
|
||||
override canApply({ move, opponent: attacker, cancelled }: FieldPriorityMoveImmunityAbAttrParams): boolean {
|
||||
return (
|
||||
!cancelled.value &&
|
||||
!(move.moveTarget === MoveTarget.USER || move.moveTarget === MoveTarget.NEAR_ALLY) &&
|
||||
move.getPriority(attacker) > 0 &&
|
||||
!move.isMultiTarget()
|
||||
@ -897,10 +910,8 @@ export class MoveImmunityAbAttr extends PreDefendAbAttr {
|
||||
this.immuneCondition = immuneCondition;
|
||||
}
|
||||
|
||||
override canApply({ pokemon, opponent: attacker, move }: MoveImmunityAbAttrParams): boolean {
|
||||
// TODO: Investigate whether this method should be checking against `cancelled`, specifically
|
||||
// if not checking this results in multiple flyouts showing when multiple abilities block the move.
|
||||
return this.immuneCondition(pokemon, attacker, move);
|
||||
override canApply({ pokemon, opponent: attacker, move, cancelled }: MoveImmunityAbAttrParams): boolean {
|
||||
return !cancelled.value && this.immuneCondition(pokemon, attacker, move);
|
||||
}
|
||||
|
||||
override apply({ cancelled }: MoveImmunityAbAttrParams): void {
|
||||
@ -1591,12 +1602,7 @@ export interface FieldPreventExplosiveMovesAbAttrParams extends AbAttrBaseParams
|
||||
cancelled: BooleanHolder;
|
||||
}
|
||||
|
||||
export class FieldPreventExplosiveMovesAbAttr extends AbAttr {
|
||||
// TODO: investigate whether we need to check against `cancelled` in a `canApply` method
|
||||
override apply({ cancelled }: FieldPreventExplosiveMovesAbAttrParams): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
}
|
||||
export class FieldPreventExplosiveMovesAbAttr extends CancelInteractionAbAttr {}
|
||||
|
||||
export interface FieldMultiplyStatAbAttrParams extends AbAttrBaseParams {
|
||||
/** The kind of stat that is being checked for modification */
|
||||
@ -2535,15 +2541,11 @@ export class IgnoreOpponentStatStagesAbAttr extends AbAttr {
|
||||
* Abilities with this attribute prevent the user from being affected by Intimidate.
|
||||
* @sealed
|
||||
*/
|
||||
export class IntimidateImmunityAbAttr extends AbAttr {
|
||||
export class IntimidateImmunityAbAttr extends CancelInteractionAbAttr {
|
||||
constructor() {
|
||||
super(false);
|
||||
}
|
||||
|
||||
override apply({ cancelled }: AbAttrParamsWithCancel): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
|
||||
getTriggerMessage({ pokemon }: AbAttrParamsWithCancel, abilityName: string, ..._args: any[]): string {
|
||||
return i18next.t("abilityTriggers:intimidateImmunity", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
@ -3577,8 +3579,8 @@ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr {
|
||||
this.protectedStat = protectedStat;
|
||||
}
|
||||
|
||||
override canApply({ stat }: PreStatStageChangeAbAttrParams): boolean {
|
||||
return isNullOrUndefined(this.protectedStat) || stat === this.protectedStat;
|
||||
override canApply({ stat, cancelled }: PreStatStageChangeAbAttrParams): boolean {
|
||||
return !cancelled.value && (isNullOrUndefined(this.protectedStat) || stat === this.protectedStat);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3669,8 +3671,11 @@ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
|
||||
this.immuneEffects = immuneEffects;
|
||||
}
|
||||
|
||||
override canApply({ effect }: PreSetStatusAbAttrParams): boolean {
|
||||
return (this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) || this.immuneEffects.includes(effect);
|
||||
override canApply({ effect, cancelled }: PreSetStatusAbAttrParams): boolean {
|
||||
return (
|
||||
!cancelled.value &&
|
||||
((this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) || this.immuneEffects.includes(effect))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3720,7 +3725,8 @@ export interface UserFieldStatusEffectImmunityAbAttrParams extends AbAttrBasePar
|
||||
/**
|
||||
* Provides immunity to status effects to the user's field.
|
||||
*/
|
||||
export class UserFieldStatusEffectImmunityAbAttr extends AbAttr {
|
||||
export class UserFieldStatusEffectImmunityAbAttr extends CancelInteractionAbAttr {
|
||||
private declare readonly _: never;
|
||||
protected immuneEffects: StatusEffect[];
|
||||
|
||||
/**
|
||||
@ -3740,12 +3746,8 @@ export class UserFieldStatusEffectImmunityAbAttr extends AbAttr {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the `cancelled` value to true, indicating that the status effect is prevented.
|
||||
*/
|
||||
override apply({ cancelled }: UserFieldStatusEffectImmunityAbAttrParams): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
// declare here to allow typescript to allow us to override `canApply` method without adjusting params
|
||||
declare apply: (params: UserFieldStatusEffectImmunityAbAttrParams) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3776,14 +3778,7 @@ export class ConditionalUserFieldStatusEffectImmunityAbAttr extends UserFieldSta
|
||||
* @returns Whether the ability can be applied to cancel the status effect.
|
||||
*/
|
||||
override canApply(params: UserFieldStatusEffectImmunityAbAttrParams): boolean {
|
||||
return this.condition(params.target, params.source) && super.canApply(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the `cancelled` value to true, indicating that the status effect is prevented.
|
||||
*/
|
||||
override apply({ cancelled }: UserFieldStatusEffectImmunityAbAttrParams): void {
|
||||
cancelled.value = true;
|
||||
return !params.cancelled.value && this.condition(params.target, params.source) && super.canApply(params);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4019,20 +4014,16 @@ export class ConditionalCritAbAttr extends AbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
export class BlockNonDirectDamageAbAttr extends AbAttr {
|
||||
export class BlockNonDirectDamageAbAttr extends CancelInteractionAbAttr {
|
||||
constructor() {
|
||||
super(false);
|
||||
}
|
||||
|
||||
override apply({ cancelled }: AbAttrParamsWithCancel): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This attribute will block any status damage that you put in the parameter.
|
||||
*/
|
||||
export class BlockStatusDamageAbAttr extends AbAttr {
|
||||
export class BlockStatusDamageAbAttr extends CancelInteractionAbAttr {
|
||||
private effects: StatusEffect[];
|
||||
|
||||
/**
|
||||
@ -4044,20 +4035,12 @@ export class BlockStatusDamageAbAttr extends AbAttr {
|
||||
this.effects = effects;
|
||||
}
|
||||
|
||||
override canApply({ pokemon }: AbAttrParamsWithCancel): boolean {
|
||||
return !!pokemon.status?.effect && this.effects.includes(pokemon.status.effect);
|
||||
}
|
||||
|
||||
override apply({ cancelled }: AbAttrParamsWithCancel): void {
|
||||
cancelled.value = true;
|
||||
override canApply({ pokemon, cancelled }: AbAttrParamsWithCancel): boolean {
|
||||
return !cancelled.value && !!pokemon.status?.effect && this.effects.includes(pokemon.status.effect);
|
||||
}
|
||||
}
|
||||
|
||||
export class BlockOneHitKOAbAttr extends AbAttr {
|
||||
override apply({ cancelled }: AbAttrParamsWithCancel): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
}
|
||||
export class BlockOneHitKOAbAttr extends CancelInteractionAbAttr {}
|
||||
|
||||
export interface ChangeMovePriorityAbAttrParams extends AbAttrBaseParams {
|
||||
/** The move being used */
|
||||
@ -4131,8 +4114,8 @@ export class BlockWeatherDamageAttr extends PreWeatherDamageAbAttr {
|
||||
this.weatherTypes = weatherTypes;
|
||||
}
|
||||
|
||||
override canApply({ weather }: PreWeatherEffectAbAttrParams): boolean {
|
||||
if (!weather) {
|
||||
override canApply({ weather, cancelled }: PreWeatherEffectAbAttrParams): boolean {
|
||||
if (!weather || cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
const weatherType = weather.weatherType;
|
||||
@ -4153,8 +4136,8 @@ export class SuppressWeatherEffectAbAttr extends PreWeatherEffectAbAttr {
|
||||
this.affectsImmutable = affectsImmutable;
|
||||
}
|
||||
|
||||
override canApply({ weather }: PreWeatherEffectAbAttrParams): boolean {
|
||||
if (!weather) {
|
||||
override canApply({ weather, cancelled }: PreWeatherEffectAbAttrParams): boolean {
|
||||
if (!weather || cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
return this.affectsImmutable || weather.isImmutable();
|
||||
@ -5151,15 +5134,11 @@ export class StatStageChangeCopyAbAttr extends AbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
export class BypassBurnDamageReductionAbAttr extends AbAttr {
|
||||
export class BypassBurnDamageReductionAbAttr extends CancelInteractionAbAttr {
|
||||
private declare readonly _: never;
|
||||
constructor() {
|
||||
super(false);
|
||||
}
|
||||
|
||||
override apply({ cancelled }: AbAttrParamsWithCancel): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ReduceBurnDamageAbAttrParams extends AbAttrBaseParams {
|
||||
@ -5199,14 +5178,7 @@ export class DoubleBerryEffectAbAttr extends AbAttr {
|
||||
* Attribute to prevent opposing berry use while on the field.
|
||||
* Used by {@linkcode AbilityId.UNNERVE}, {@linkcode AbilityId.AS_ONE_GLASTRIER} and {@linkcode AbilityId.AS_ONE_SPECTRIER}
|
||||
*/
|
||||
export class PreventBerryUseAbAttr extends AbAttr {
|
||||
/**
|
||||
* Prevent use of opposing berries.
|
||||
*/
|
||||
override apply({ cancelled }: AbAttrParamsWithCancel): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
}
|
||||
export class PreventBerryUseAbAttr extends CancelInteractionAbAttr {}
|
||||
|
||||
/**
|
||||
* A Pokemon with this ability heals by a percentage of their maximum hp after eating a berry
|
||||
@ -5664,11 +5636,7 @@ export class IncreasePpAbAttr extends AbAttr {
|
||||
}
|
||||
|
||||
/** @sealed */
|
||||
export class ForceSwitchOutImmunityAbAttr extends AbAttr {
|
||||
override apply({ cancelled }: AbAttrParamsWithCancel): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
}
|
||||
export class ForceSwitchOutImmunityAbAttr extends CancelInteractionAbAttr {}
|
||||
|
||||
export interface ReduceBerryUseThresholdAbAttrParams extends AbAttrBaseParams {
|
||||
/** Holds the hp ratio for the berry to proc, which may be modified by ability application */
|
||||
@ -5747,8 +5715,8 @@ export class MoveAbilityBypassAbAttr extends AbAttr {
|
||||
this.moveIgnoreFunc = moveIgnoreFunc || ((_pokemon, _move) => true);
|
||||
}
|
||||
|
||||
override canApply({ pokemon, move }: MoveAbilityBypassAbAttrParams): boolean {
|
||||
return this.moveIgnoreFunc(pokemon, move);
|
||||
override canApply({ pokemon, move, cancelled }: MoveAbilityBypassAbAttrParams): boolean {
|
||||
return !cancelled.value && this.moveIgnoreFunc(pokemon, move);
|
||||
}
|
||||
|
||||
override apply({ cancelled }: MoveAbilityBypassAbAttrParams): void {
|
||||
@ -5842,8 +5810,8 @@ export class IgnoreTypeImmunityAbAttr extends AbAttr {
|
||||
this.allowedMoveTypes = allowedMoveTypes;
|
||||
}
|
||||
|
||||
override canApply({ moveType, defenderType }: IgnoreTypeImmunityAbAttrParams): boolean {
|
||||
return this.defenderType === defenderType && this.allowedMoveTypes.includes(moveType);
|
||||
override canApply({ moveType, defenderType, cancelled }: IgnoreTypeImmunityAbAttrParams): boolean {
|
||||
return !cancelled.value && this.defenderType === defenderType && this.allowedMoveTypes.includes(moveType);
|
||||
}
|
||||
|
||||
override apply({ cancelled }: IgnoreTypeImmunityAbAttrParams): void {
|
||||
|
@ -835,7 +835,7 @@ export abstract class BattleAnim {
|
||||
// biome-ignore lint/complexity/noBannedTypes: callback is used liberally
|
||||
play(onSubstitute?: boolean, callback?: Function) {
|
||||
const isOppAnim = this.isOppAnim();
|
||||
const user = !isOppAnim ? this.user! : this.target!; // TODO: are those bangs correct?
|
||||
const user = !isOppAnim ? this.user! : this.target!; // TODO: These bangs are LITERALLY not correct at all
|
||||
const target = !isOppAnim ? this.target! : this.user!;
|
||||
|
||||
if (!target?.isOnField() && !this.playRegardlessOfIssues) {
|
||||
|
@ -764,7 +764,7 @@ export class SingleTypeChallenge extends Challenge {
|
||||
}
|
||||
|
||||
getValue(overrideValue: number = this.value): string {
|
||||
return i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[overrideValue - 1])}`);
|
||||
return PokemonType[overrideValue - 1].toLowerCase();
|
||||
}
|
||||
|
||||
getDescription(overrideValue: number = this.value): string {
|
||||
|
@ -8140,9 +8140,12 @@ const failIfSingleBattle: MoveConditionFunc = (user, target, move) => globalScen
|
||||
|
||||
const failIfDampCondition: MoveConditionFunc = (user, target, move) => {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
globalScene.getField(true).map(p=>applyAbAttrs("FieldPreventExplosiveMovesAbAttr", {pokemon: p, cancelled}));
|
||||
// temporary workaround to prevent displaying the message during enemy command phase
|
||||
// TODO: either move this, or make the move condition func have a `simulated` param
|
||||
const simulated = globalScene.phaseManager.getCurrentPhase()?.is('EnemyCommandPhase');
|
||||
globalScene.getField(true).map(p=>applyAbAttrs("FieldPreventExplosiveMovesAbAttr", {pokemon: p, cancelled, simulated}));
|
||||
// Queue a message if an ability prevented usage of the move
|
||||
if (cancelled.value) {
|
||||
if (!simulated && cancelled.value) {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:cannotUseMove", { pokemonName: getPokemonNameWithAffix(user), moveName: move.name }));
|
||||
}
|
||||
return !cancelled.value;
|
||||
@ -8164,6 +8167,9 @@ const failIfGhostTypeCondition: MoveConditionFunc = (user: Pokemon, target: Poke
|
||||
const failIfNoTargetHeldItemsCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.getHeldItems().filter(i => i.isTransferable)?.length > 0;
|
||||
|
||||
const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
||||
if (isNullOrUndefined(target)) { // Fix bug when used against targets that have both fainted
|
||||
return "";
|
||||
}
|
||||
const heldItems = target.getHeldItems().filter(i => i.isTransferable);
|
||||
if (heldItems.length === 0) {
|
||||
return "";
|
||||
|
@ -126,7 +126,9 @@ export class DelayedAttackTag extends PositionalTag implements DelayedAttackArgs
|
||||
// Silently disappear if either source or target are missing or happen to be the same pokemon
|
||||
// (i.e. targeting oneself)
|
||||
// We also need to check for fainted targets as they don't technically leave the field until _after_ the turn ends
|
||||
return !!source && !!target && source !== target && !target.isFainted();
|
||||
// TODO: Figure out a way to store the target's offensive stat if they faint to allow pending attacks to persist
|
||||
// TODO: Remove the `?.scene` checks once battle anims are cleaned up - needed to avoid catch+release crash
|
||||
return !!source?.scene && !!target?.scene && source !== target && !target.isFainted();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6590,6 +6590,7 @@ export class EnemyPokemon extends Pokemon {
|
||||
ignoreAllyAbility: !p.getAlly()?.waveData.abilityRevealed,
|
||||
ignoreSourceAllyAbility: false,
|
||||
isCritical,
|
||||
simulated: true,
|
||||
}).damage >= p.hp
|
||||
);
|
||||
})
|
||||
|
@ -230,6 +230,7 @@ export class EggHatchPhase extends Phase {
|
||||
} else {
|
||||
globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true));
|
||||
}
|
||||
this.pokemon?.destroy();
|
||||
super.end();
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,10 @@ export class EggSummaryPhase extends Phase {
|
||||
}
|
||||
|
||||
end() {
|
||||
this.eggHatchData.forEach(data => {
|
||||
data.pokemon?.destroy();
|
||||
});
|
||||
this.eggHatchData = [];
|
||||
globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true));
|
||||
globalScene.ui.setModeForceTransition(UiMode.MESSAGE).then(() => {
|
||||
super.end();
|
||||
|
@ -204,7 +204,7 @@ export class GameOverPhase extends BattlePhase {
|
||||
}
|
||||
this.getRunHistoryEntry().then(runHistoryEntry => {
|
||||
globalScene.gameData.saveRunHistory(runHistoryEntry, this.isVictory);
|
||||
globalScene.phaseManager.pushNew("PostGameOverPhase", endCardPhase);
|
||||
globalScene.phaseManager.pushNew("PostGameOverPhase", globalScene.sessionSlotId, endCardPhase);
|
||||
this.end();
|
||||
});
|
||||
};
|
||||
|
@ -24,6 +24,7 @@ import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||
import { frenzyMissFunc } from "#moves/move-utils";
|
||||
import type { PokemonMove } from "#moves/pokemon-move";
|
||||
import { BattlePhase } from "#phases/battle-phase";
|
||||
import type { TurnMove } from "#types/turn-move";
|
||||
import { NumberHolder } from "#utils/common";
|
||||
import { enumValueToKey } from "#utils/enums";
|
||||
import i18next from "i18next";
|
||||
@ -41,6 +42,13 @@ export class MovePhase extends BattlePhase {
|
||||
/** Whether the current move should fail and retain PP. */
|
||||
protected cancelled = false;
|
||||
|
||||
/** The move history entry object that is pushed to the pokemon's move history
|
||||
*
|
||||
* @remarks
|
||||
* Can be edited _after_ being pushed to the history to adjust the result, targets, etc, for this move phase.
|
||||
*/
|
||||
protected moveHistoryEntry: TurnMove;
|
||||
|
||||
public get pokemon(): Pokemon {
|
||||
return this._pokemon;
|
||||
}
|
||||
@ -82,6 +90,11 @@ export class MovePhase extends BattlePhase {
|
||||
this.move = move;
|
||||
this.useMode = useMode;
|
||||
this.forcedLast = forcedLast;
|
||||
this.moveHistoryEntry = {
|
||||
move: MoveId.NONE,
|
||||
targets,
|
||||
useMode,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -410,13 +423,9 @@ export class MovePhase extends BattlePhase {
|
||||
if (showText) {
|
||||
this.showMoveText();
|
||||
}
|
||||
|
||||
this.pokemon.pushMoveHistory({
|
||||
move: this.move.moveId,
|
||||
targets: this.targets,
|
||||
result: MoveResult.FAIL,
|
||||
useMode: this.useMode,
|
||||
});
|
||||
const moveHistoryEntry = this.moveHistoryEntry;
|
||||
moveHistoryEntry.result = MoveResult.FAIL;
|
||||
this.pokemon.pushMoveHistory(moveHistoryEntry);
|
||||
|
||||
// Use move-specific failure messages if present before checking terrain/weather blockage
|
||||
// and falling back to the classic "But it failed!".
|
||||
@ -630,12 +639,9 @@ export class MovePhase extends BattlePhase {
|
||||
frenzyMissFunc(this.pokemon, this.move.getMove());
|
||||
}
|
||||
|
||||
this.pokemon.pushMoveHistory({
|
||||
move: MoveId.NONE,
|
||||
result: MoveResult.FAIL,
|
||||
targets: this.targets,
|
||||
useMode: this.useMode,
|
||||
});
|
||||
const moveHistoryEntry = this.moveHistoryEntry;
|
||||
moveHistoryEntry.result = MoveResult.FAIL;
|
||||
this.pokemon.pushMoveHistory(moveHistoryEntry);
|
||||
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
|
||||
@ -649,13 +655,16 @@ export class MovePhase extends BattlePhase {
|
||||
* Displays the move's usage text to the player as applicable for the move being used.
|
||||
*/
|
||||
public showMoveText(): void {
|
||||
const moveId = this.move.moveId;
|
||||
if (
|
||||
this.move.moveId === MoveId.NONE ||
|
||||
moveId === MoveId.NONE ||
|
||||
this.pokemon.getTag(BattlerTagType.RECHARGING) ||
|
||||
this.pokemon.getTag(BattlerTagType.INTERRUPTED)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Showing move text always adjusts the move history entry's move id
|
||||
this.moveHistoryEntry.move = moveId;
|
||||
|
||||
// TODO: This should be done by the move...
|
||||
globalScene.phaseManager.queueMessage(
|
||||
@ -668,7 +677,7 @@ export class MovePhase extends BattlePhase {
|
||||
|
||||
// Moves with pre-use messages (Magnitude, Chilly Reception, Fickle Beam, etc.) always display their messages even on failure
|
||||
// TODO: This assumes single target for message funcs - is this sustainable?
|
||||
applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.pokemon.getOpponents(false)[0], this.move.getMove());
|
||||
applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.getActiveTargetPokemon()[0], this.move.getMove());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -5,10 +5,11 @@ import type { EndCardPhase } from "#phases/end-card-phase";
|
||||
export class PostGameOverPhase extends Phase {
|
||||
public readonly phaseName = "PostGameOverPhase";
|
||||
private endCardPhase?: EndCardPhase;
|
||||
private slotId: number;
|
||||
|
||||
constructor(endCardPhase?: EndCardPhase) {
|
||||
constructor(slotId: number, endCardPhase?: EndCardPhase) {
|
||||
super();
|
||||
|
||||
this.slotId = slotId;
|
||||
this.endCardPhase = endCardPhase;
|
||||
}
|
||||
|
||||
@ -20,9 +21,7 @@ export class PostGameOverPhase extends Phase {
|
||||
if (!success) {
|
||||
return globalScene.reset(true);
|
||||
}
|
||||
globalScene.gameData
|
||||
.tryClearSession(globalScene.sessionSlotId)
|
||||
.then((success: boolean | [boolean, boolean]) => {
|
||||
globalScene.gameData.tryClearSession(this.slotId).then((success: boolean | [boolean, boolean]) => {
|
||||
if (!success[0]) {
|
||||
return globalScene.reset(true);
|
||||
}
|
||||
|
@ -177,6 +177,9 @@ export class TitlePhase extends Phase {
|
||||
.then((success: boolean) => {
|
||||
if (success) {
|
||||
this.loaded = true;
|
||||
if (loggedInUser) {
|
||||
loggedInUser.lastSessionSlot = slotId;
|
||||
}
|
||||
globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end());
|
||||
} else {
|
||||
this.end();
|
||||
|
@ -82,6 +82,7 @@ export class PokerogueSessionSavedataApi extends ApiBase {
|
||||
try {
|
||||
const urlSearchParams = this.toUrlSearchParams(params);
|
||||
const response = await this.doGet(`/savedata/session/delete?${urlSearchParams}`);
|
||||
console.debug("%cSending a request to delete session in slot %d", "color: blue", params.slot);
|
||||
|
||||
if (response.ok) {
|
||||
return null;
|
||||
|
@ -68,6 +68,7 @@ import { executeIf, fixedInt, isLocal, NumberHolder, randInt, randSeedItem } fro
|
||||
import { decrypt, encrypt } from "#utils/data";
|
||||
import { getEnumKeys } from "#utils/enums";
|
||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||
import { isBeta } from "#utils/utility-vars";
|
||||
import { AES, enc } from "crypto-js";
|
||||
import i18next from "i18next";
|
||||
|
||||
@ -419,7 +420,15 @@ export class GameData {
|
||||
}
|
||||
}
|
||||
|
||||
console.debug(systemData);
|
||||
if (isLocal || isBeta) {
|
||||
try {
|
||||
console.debug(
|
||||
this.parseSystemData(JSON.stringify(systemData, (_, v: any) => (typeof v === "bigint" ? v.toString() : v))),
|
||||
);
|
||||
} catch (err) {
|
||||
console.debug("Attempt to log system data failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem(`data_${loggedInUser?.username}`, encrypt(systemDataStr, bypassLogin));
|
||||
|
||||
@ -945,11 +954,11 @@ export class GameData {
|
||||
} as SessionSaveData;
|
||||
}
|
||||
|
||||
getSession(slotId: number): Promise<SessionSaveData | null> {
|
||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this
|
||||
return new Promise(async (resolve, reject) => {
|
||||
async getSession(slotId: number): Promise<SessionSaveData | null> {
|
||||
const { promise, resolve, reject } = Promise.withResolvers<SessionSaveData | null>();
|
||||
if (slotId < 0) {
|
||||
return resolve(null);
|
||||
resolve(null);
|
||||
return promise;
|
||||
}
|
||||
const handleSessionData = async (sessionDataStr: string) => {
|
||||
try {
|
||||
@ -962,10 +971,12 @@ export class GameData {
|
||||
};
|
||||
|
||||
if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`)) {
|
||||
pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId }).then(async response => {
|
||||
const response = await pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId });
|
||||
|
||||
if (!response || response?.length === 0 || response?.[0] !== "{") {
|
||||
console.error(response);
|
||||
return resolve(null);
|
||||
resolve(null);
|
||||
return promise;
|
||||
}
|
||||
|
||||
localStorage.setItem(
|
||||
@ -974,16 +985,15 @@ export class GameData {
|
||||
);
|
||||
|
||||
await handleSessionData(response);
|
||||
});
|
||||
} else {
|
||||
return promise;
|
||||
}
|
||||
const sessionData = localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`);
|
||||
if (sessionData) {
|
||||
await handleSessionData(decrypt(sessionData, bypassLogin));
|
||||
} else {
|
||||
return resolve(null);
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
});
|
||||
resolve(null);
|
||||
return promise;
|
||||
}
|
||||
|
||||
async renameSession(slotId: number, newName: string): Promise<boolean> {
|
||||
@ -1028,24 +1038,33 @@ export class GameData {
|
||||
return !(success !== null && !success);
|
||||
}
|
||||
|
||||
loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> {
|
||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this
|
||||
return new Promise(async (resolve, reject) => {
|
||||
async loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> {
|
||||
const { promise, resolve, reject } = Promise.withResolvers<boolean>();
|
||||
try {
|
||||
const initSessionFromData = async (sessionData: SessionSaveData) => {
|
||||
console.debug(sessionData);
|
||||
|
||||
globalScene.gameMode = getGameMode(sessionData.gameMode || GameModes.CLASSIC);
|
||||
if (sessionData.challenges) {
|
||||
globalScene.gameMode.challenges = sessionData.challenges.map(c => c.toChallenge());
|
||||
const initSessionFromData = (fromSession: SessionSaveData) => {
|
||||
if (isLocal || isBeta) {
|
||||
try {
|
||||
console.debug(
|
||||
this.parseSessionData(
|
||||
JSON.stringify(fromSession, (_, v: any) => (typeof v === "bigint" ? v.toString() : v)),
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
console.debug("Attempt to log session data failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
globalScene.setSeed(sessionData.seed || globalScene.game.config.seed[0]);
|
||||
globalScene.gameMode = getGameMode(fromSession.gameMode || GameModes.CLASSIC);
|
||||
if (fromSession.challenges) {
|
||||
globalScene.gameMode.challenges = fromSession.challenges.map(c => c.toChallenge());
|
||||
}
|
||||
|
||||
globalScene.setSeed(fromSession.seed || globalScene.game.config.seed[0]);
|
||||
globalScene.resetSeed();
|
||||
|
||||
console.log("Seed:", globalScene.seed);
|
||||
|
||||
globalScene.sessionPlayTime = sessionData.playTime || 0;
|
||||
globalScene.sessionPlayTime = fromSession.playTime || 0;
|
||||
globalScene.lastSavePlayTime = 0;
|
||||
|
||||
const loadPokemonAssets: Promise<void>[] = [];
|
||||
@ -1053,7 +1072,7 @@ export class GameData {
|
||||
const party = globalScene.getPlayerParty();
|
||||
party.splice(0, party.length);
|
||||
|
||||
for (const p of sessionData.party) {
|
||||
for (const p of fromSession.party) {
|
||||
const pokemon = p.toPokemon() as PlayerPokemon;
|
||||
pokemon.setVisible(false);
|
||||
loadPokemonAssets.push(pokemon.loadAssets(false));
|
||||
@ -1061,48 +1080,48 @@ export class GameData {
|
||||
}
|
||||
|
||||
Object.keys(globalScene.pokeballCounts).forEach((key: string) => {
|
||||
globalScene.pokeballCounts[key] = sessionData.pokeballCounts[key] || 0;
|
||||
globalScene.pokeballCounts[key] = fromSession.pokeballCounts[key] || 0;
|
||||
});
|
||||
if (Overrides.POKEBALL_OVERRIDE.active) {
|
||||
globalScene.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs;
|
||||
}
|
||||
|
||||
globalScene.money = Math.floor(sessionData.money || 0);
|
||||
globalScene.money = Math.floor(fromSession.money || 0);
|
||||
globalScene.updateMoneyText();
|
||||
|
||||
if (globalScene.money > this.gameStats.highestMoney) {
|
||||
this.gameStats.highestMoney = globalScene.money;
|
||||
}
|
||||
|
||||
globalScene.score = sessionData.score;
|
||||
globalScene.score = fromSession.score;
|
||||
globalScene.updateScoreText();
|
||||
|
||||
globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(sessionData.mysteryEncounterSaveData);
|
||||
globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(fromSession.mysteryEncounterSaveData);
|
||||
|
||||
globalScene.newArena(sessionData.arena.biome, sessionData.playerFaints);
|
||||
globalScene.newArena(fromSession.arena.biome, fromSession.playerFaints);
|
||||
|
||||
const battleType = sessionData.battleType || 0;
|
||||
const trainerConfig = sessionData.trainer ? trainerConfigs[sessionData.trainer.trainerType] : null;
|
||||
const battleType = fromSession.battleType || 0;
|
||||
const trainerConfig = fromSession.trainer ? trainerConfigs[fromSession.trainer.trainerType] : null;
|
||||
const mysteryEncounterType =
|
||||
sessionData.mysteryEncounterType !== -1 ? sessionData.mysteryEncounterType : undefined;
|
||||
fromSession.mysteryEncounterType !== -1 ? fromSession.mysteryEncounterType : undefined;
|
||||
const battle = globalScene.newBattle(
|
||||
sessionData.waveIndex,
|
||||
fromSession.waveIndex,
|
||||
battleType,
|
||||
sessionData.trainer,
|
||||
fromSession.trainer,
|
||||
battleType === BattleType.TRAINER
|
||||
? trainerConfig?.doubleOnly || sessionData.trainer?.variant === TrainerVariant.DOUBLE
|
||||
: sessionData.enemyParty.length > 1,
|
||||
? trainerConfig?.doubleOnly || fromSession.trainer?.variant === TrainerVariant.DOUBLE
|
||||
: fromSession.enemyParty.length > 1,
|
||||
mysteryEncounterType,
|
||||
);
|
||||
battle.enemyLevels = sessionData.enemyParty.map(p => p.level);
|
||||
battle.enemyLevels = fromSession.enemyParty.map(p => p.level);
|
||||
|
||||
globalScene.arena.init();
|
||||
|
||||
sessionData.enemyParty.forEach((enemyData, e) => {
|
||||
fromSession.enemyParty.forEach((enemyData, e) => {
|
||||
const enemyPokemon = enemyData.toPokemon(
|
||||
battleType,
|
||||
e,
|
||||
sessionData.trainer?.variant === TrainerVariant.DOUBLE,
|
||||
fromSession.trainer?.variant === TrainerVariant.DOUBLE,
|
||||
) as EnemyPokemon;
|
||||
battle.enemyParty[e] = enemyPokemon;
|
||||
if (battleType === BattleType.WILD) {
|
||||
@ -1112,7 +1131,7 @@ export class GameData {
|
||||
loadPokemonAssets.push(enemyPokemon.loadAssets());
|
||||
});
|
||||
|
||||
globalScene.arena.weather = sessionData.arena.weather;
|
||||
globalScene.arena.weather = fromSession.arena.weather;
|
||||
globalScene.arena.eventTarget.dispatchEvent(
|
||||
new WeatherChangedEvent(
|
||||
WeatherType.NONE,
|
||||
@ -1121,7 +1140,7 @@ export class GameData {
|
||||
),
|
||||
); // TODO: is this bang correct?
|
||||
|
||||
globalScene.arena.terrain = sessionData.arena.terrain;
|
||||
globalScene.arena.terrain = fromSession.arena.terrain;
|
||||
globalScene.arena.eventTarget.dispatchEvent(
|
||||
new TerrainChangedEvent(
|
||||
TerrainType.NONE,
|
||||
@ -1130,9 +1149,9 @@ export class GameData {
|
||||
),
|
||||
); // TODO: is this bang correct?
|
||||
|
||||
globalScene.arena.playerTerasUsed = sessionData.arena.playerTerasUsed;
|
||||
globalScene.arena.playerTerasUsed = fromSession.arena.playerTerasUsed;
|
||||
|
||||
globalScene.arena.tags = sessionData.arena.tags;
|
||||
globalScene.arena.tags = fromSession.arena.tags;
|
||||
if (globalScene.arena.tags) {
|
||||
for (const tag of globalScene.arena.tags) {
|
||||
if (tag instanceof EntryHazardTag) {
|
||||
@ -1146,7 +1165,7 @@ export class GameData {
|
||||
}
|
||||
}
|
||||
|
||||
globalScene.arena.positionalTagManager.tags = sessionData.arena.positionalTags.map(tag =>
|
||||
globalScene.arena.positionalTagManager.tags = fromSession.arena.positionalTags.map(tag =>
|
||||
loadPositionalTag(tag),
|
||||
);
|
||||
|
||||
@ -1154,7 +1173,7 @@ export class GameData {
|
||||
console.warn("Existing modifiers not cleared on session load, deleting...");
|
||||
globalScene.modifiers = [];
|
||||
}
|
||||
for (const modifierData of sessionData.modifiers) {
|
||||
for (const modifierData of fromSession.modifiers) {
|
||||
const modifier = modifierData.toModifier(Modifier[modifierData.className]);
|
||||
if (modifier) {
|
||||
globalScene.addModifier(modifier, true);
|
||||
@ -1162,7 +1181,7 @@ export class GameData {
|
||||
}
|
||||
globalScene.updateModifiers(true);
|
||||
|
||||
for (const enemyModifierData of sessionData.enemyModifiers) {
|
||||
for (const enemyModifierData of fromSession.enemyModifiers) {
|
||||
const modifier = enemyModifierData.toModifier(Modifier[enemyModifierData.className]);
|
||||
if (modifier) {
|
||||
globalScene.addEnemyModifier(modifier, true);
|
||||
@ -1177,7 +1196,9 @@ export class GameData {
|
||||
initSessionFromData(sessionData);
|
||||
} else {
|
||||
this.getSession(slotId)
|
||||
.then(data => data && initSessionFromData(data))
|
||||
.then(data => {
|
||||
return data && initSessionFromData(data);
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
return;
|
||||
@ -1185,9 +1206,9 @@ export class GameData {
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -61,12 +61,12 @@ export class TouchControl {
|
||||
* event, removes the keydown state, and removes the 'active' class from the node and the last touched element.
|
||||
*/
|
||||
bindKey(node: HTMLElement, key: string) {
|
||||
node.addEventListener("touchstart", event => {
|
||||
node.addEventListener("pointerdown", event => {
|
||||
event.preventDefault();
|
||||
this.touchButtonDown(node, key);
|
||||
});
|
||||
|
||||
node.addEventListener("touchend", event => {
|
||||
node.addEventListener("pointerup", event => {
|
||||
event.preventDefault();
|
||||
this.touchButtonUp(node, key, event.target?.["id"]);
|
||||
});
|
||||
|
@ -136,6 +136,11 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
||||
this.submitAction = config.buttonActions.length ? config.buttonActions[0] : null;
|
||||
this.cancelAction = config.buttonActions[1] ?? null;
|
||||
|
||||
// Auto focus the first input field after a short delay, to prevent accidental inputs
|
||||
setTimeout(() => {
|
||||
this.inputs[0].setFocus();
|
||||
}, 50);
|
||||
|
||||
// #region: Override button pointerDown
|
||||
// Override the pointerDown event for the buttonBgs to call the `submitAction` and `cancelAction`
|
||||
// properties that we set above, allowing their behavior to change after this method terminates
|
||||
|
@ -106,10 +106,6 @@ export class PokedexScanUiHandler extends FormModalUiHandler {
|
||||
|
||||
this.reduceKeys();
|
||||
|
||||
setTimeout(() => {
|
||||
input.setFocus(); // Focus after a short delay to avoid unwanted input
|
||||
}, 50);
|
||||
|
||||
input.on("keydown", (inputObject, evt: KeyboardEvent) => {
|
||||
if (
|
||||
["escape", "space"].some(v => v === evt.key.toLowerCase() || v === evt.code.toLowerCase()) &&
|
||||
|
@ -9,9 +9,9 @@ export const TOUCH_CONTROL_POSITIONS_PORTRAIT = "touchControlPositionsPortrait";
|
||||
type ControlPosition = { id: string; x: number; y: number };
|
||||
|
||||
type ConfigurationEventListeners = {
|
||||
touchstart: EventListener[];
|
||||
touchmove: EventListener[];
|
||||
touchend: EventListener[];
|
||||
pointerdown: EventListener[];
|
||||
pointermove: EventListener[];
|
||||
pointerup: EventListener[];
|
||||
};
|
||||
|
||||
type ToolbarRefs = {
|
||||
@ -39,9 +39,9 @@ export class MoveTouchControlsHandler {
|
||||
* These are used to remove the event listeners when the configuration mode is disabled.
|
||||
*/
|
||||
private configurationEventListeners: ConfigurationEventListeners = {
|
||||
touchstart: [],
|
||||
touchmove: [],
|
||||
touchend: [],
|
||||
pointerdown: [],
|
||||
pointermove: [],
|
||||
pointerup: [],
|
||||
};
|
||||
|
||||
private overlay: Phaser.GameObjects.Container;
|
||||
@ -165,34 +165,33 @@ export class MoveTouchControlsHandler {
|
||||
/**
|
||||
* Start dragging the given button.
|
||||
* @param controlGroup The button that is being dragged.
|
||||
* @param touch The touch event that started the drag.
|
||||
* @param event The pointer event that started the drag.
|
||||
*/
|
||||
private startDrag = (controlGroup: HTMLElement): void => {
|
||||
this.draggingElement = controlGroup;
|
||||
};
|
||||
|
||||
/**
|
||||
* Drags the currently dragged element to the given touch position.
|
||||
* @param touch The touch event that is currently happening.
|
||||
* @param isLeft Whether the dragged element is a left button.
|
||||
* Drags the currently dragged element to the given pointer position.
|
||||
* @param event The pointer event that is currently happening.
|
||||
*/
|
||||
private drag = (touch: Touch): void => {
|
||||
private drag = (event: PointerEvent): void => {
|
||||
if (!this.draggingElement) {
|
||||
return;
|
||||
}
|
||||
const rect = this.draggingElement.getBoundingClientRect();
|
||||
// Map the touch position to the center of the dragged element.
|
||||
// Map the pointer position to the center of the dragged element.
|
||||
const xOffset = this.isLeft(this.draggingElement)
|
||||
? touch.clientX - rect.width / 2
|
||||
: window.innerWidth - touch.clientX - rect.width / 2;
|
||||
const yOffset = window.innerHeight - touch.clientY - rect.height / 2;
|
||||
? event.clientX - rect.width / 2
|
||||
: window.innerWidth - event.clientX - rect.width / 2;
|
||||
const yOffset = window.innerHeight - event.clientY - rect.height / 2;
|
||||
this.setPosition(this.draggingElement, xOffset, yOffset);
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops dragging the currently dragged element.
|
||||
*/
|
||||
private stopDrag = () => {
|
||||
private stopDrag = (): void => {
|
||||
this.draggingElement = null;
|
||||
};
|
||||
|
||||
@ -303,19 +302,19 @@ export class MoveTouchControlsHandler {
|
||||
*/
|
||||
private createConfigurationEventListeners(controlGroups: HTMLDivElement[]): ConfigurationEventListeners {
|
||||
return {
|
||||
touchstart: controlGroups.map((element: HTMLDivElement) => {
|
||||
pointerdown: controlGroups.map((element: HTMLDivElement) => {
|
||||
const startDrag = () => this.startDrag(element);
|
||||
element.addEventListener("touchstart", startDrag, { passive: true });
|
||||
element.addEventListener("pointerdown", startDrag, { passive: true });
|
||||
return startDrag;
|
||||
}),
|
||||
touchmove: controlGroups.map(() => {
|
||||
const drag = event => this.drag(event.touches[0]);
|
||||
window.addEventListener("touchmove", drag, { passive: true });
|
||||
pointermove: controlGroups.map(() => {
|
||||
const drag = (event: PointerEvent) => this.drag(event);
|
||||
window.addEventListener("pointermove", drag, { passive: true });
|
||||
return drag;
|
||||
}),
|
||||
touchend: controlGroups.map(() => {
|
||||
pointerup: controlGroups.map(() => {
|
||||
const stopDrag = () => this.stopDrag();
|
||||
window.addEventListener("touchend", stopDrag, { passive: true });
|
||||
window.addEventListener("pointerup", stopDrag, { passive: true });
|
||||
return stopDrag;
|
||||
}),
|
||||
};
|
||||
@ -373,12 +372,12 @@ export class MoveTouchControlsHandler {
|
||||
this.draggingElement = null;
|
||||
|
||||
// Remove event listeners
|
||||
const { touchstart, touchmove, touchend } = this.configurationEventListeners;
|
||||
const { pointerdown, pointermove, pointerup } = this.configurationEventListeners;
|
||||
this.getControlGroupElements().forEach((element, index) =>
|
||||
element.removeEventListener("touchstart", touchstart[index]),
|
||||
element.removeEventListener("pointerdown", pointerdown[index]),
|
||||
);
|
||||
touchmove.forEach(listener => window.removeEventListener("touchmove", listener));
|
||||
touchend.forEach(listener => window.removeEventListener("touchend", listener));
|
||||
pointermove.forEach(listener => window.removeEventListener("pointermove", listener));
|
||||
pointerup.forEach(listener => window.removeEventListener("pointerup", listener));
|
||||
|
||||
// Remove configuration toolbar
|
||||
const toolbar = document.querySelector("#touchControls #configToolbar");
|
||||
|
@ -4,12 +4,15 @@ import { allMoves } from "#data/data-lists";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { Button } from "#enums/buttons";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { PokeballType } from "#enums/pokeball";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { PositionalTagType } from "#enums/positional-tag-type";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import i18next from "i18next";
|
||||
import Phaser from "phaser";
|
||||
@ -95,7 +98,7 @@ describe("Moves - Delayed Attacks", () => {
|
||||
|
||||
expectFutureSightActive(0);
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||
expect(enemy).not.toHaveFullHp();
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("moveTriggers:tookMoveAttack", {
|
||||
pokemonName: getPokemonNameWithAffix(enemy),
|
||||
@ -130,12 +133,12 @@ describe("Moves - Delayed Attacks", () => {
|
||||
|
||||
expectFutureSightActive();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
expect(enemy.hp).toBe(enemy.getMaxHp());
|
||||
expect(enemy).toHaveFullHp();
|
||||
|
||||
await passTurns(2);
|
||||
|
||||
expectFutureSightActive(0);
|
||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||
expect(enemy).not.toHaveFullHp();
|
||||
});
|
||||
|
||||
it("should work when used against different targets in doubles", async () => {
|
||||
@ -149,15 +152,15 @@ describe("Moves - Delayed Attacks", () => {
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expectFutureSightActive(2);
|
||||
expect(enemy1.hp).toBe(enemy1.getMaxHp());
|
||||
expect(enemy2.hp).toBe(enemy2.getMaxHp());
|
||||
expect(enemy1).toHaveFullHp();
|
||||
expect(enemy2).toHaveFullHp();
|
||||
expect(karp.getLastXMoves()[0].result).toBe(MoveResult.OTHER);
|
||||
expect(feebas.getLastXMoves()[0].result).toBe(MoveResult.OTHER);
|
||||
|
||||
await passTurns(2);
|
||||
|
||||
expect(enemy1.hp).toBeLessThan(enemy1.getMaxHp());
|
||||
expect(enemy2.hp).toBeLessThan(enemy2.getMaxHp());
|
||||
expect(enemy1).not.toHaveFullHp();
|
||||
expect(enemy2).not.toHaveFullHp();
|
||||
});
|
||||
|
||||
it("should trigger multiple pending attacks in order of creation, even if that order changes later on", async () => {
|
||||
@ -222,8 +225,8 @@ describe("Moves - Delayed Attacks", () => {
|
||||
|
||||
expect(game.scene.getPlayerParty()).toEqual([milotic, karp, feebas]);
|
||||
|
||||
expect(karp.hp).toBe(karp.getMaxHp());
|
||||
expect(feebas.hp).toBe(feebas.getMaxHp());
|
||||
expect(karp).toHaveFullHp();
|
||||
expect(feebas).toHaveFullHp();
|
||||
expect(game.textInterceptor.logs).not.toContain(
|
||||
i18next.t("moveTriggers:tookMoveAttack", {
|
||||
pokemonName: getPokemonNameWithAffix(karp),
|
||||
@ -245,15 +248,14 @@ describe("Moves - Delayed Attacks", () => {
|
||||
expect(enemy2.isFainted()).toBe(true);
|
||||
expectFutureSightActive();
|
||||
|
||||
const attack = game.scene.arena.positionalTagManager.tags.find(
|
||||
t => t.tagType === PositionalTagType.DELAYED_ATTACK,
|
||||
)!;
|
||||
expect(attack).toBeDefined();
|
||||
expect(attack.targetIndex).toBe(enemy1.getBattlerIndex());
|
||||
expect(game).toHavePositionalTag({
|
||||
tagType: PositionalTagType.DELAYED_ATTACK,
|
||||
targetIndex: enemy1.getBattlerIndex(),
|
||||
});
|
||||
|
||||
await passTurns(2);
|
||||
|
||||
expect(enemy1.hp).toBeLessThan(enemy1.getMaxHp());
|
||||
expect(enemy1).not.toHaveFullHp();
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("moveTriggers:tookMoveAttack", {
|
||||
pokemonName: getPokemonNameWithAffix(enemy1),
|
||||
@ -281,7 +283,7 @@ describe("Moves - Delayed Attacks", () => {
|
||||
await game.toNextTurn();
|
||||
|
||||
expectFutureSightActive(0);
|
||||
expect(enemy1.hp).toBe(enemy1.getMaxHp());
|
||||
expect(enemy1).toHaveFullHp();
|
||||
expect(game.textInterceptor.logs).not.toContain(
|
||||
i18next.t("moveTriggers:tookMoveAttack", {
|
||||
pokemonName: getPokemonNameWithAffix(enemy1),
|
||||
@ -317,8 +319,8 @@ describe("Moves - Delayed Attacks", () => {
|
||||
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(enemy1.hp).toBe(enemy1.getMaxHp());
|
||||
expect(enemy2.hp).toBeLessThan(enemy2.getMaxHp());
|
||||
expect(enemy1).toHaveFullHp();
|
||||
expect(enemy2).not.toHaveFullHp();
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("moveTriggers:tookMoveAttack", {
|
||||
pokemonName: getPokemonNameWithAffix(enemy2),
|
||||
@ -351,7 +353,7 @@ describe("Moves - Delayed Attacks", () => {
|
||||
|
||||
// Player Normalize was not applied due to being off field
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||
expect(enemy).not.toHaveFullHp();
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("moveTriggers:tookMoveAttack", {
|
||||
pokemonName: getPokemonNameWithAffix(enemy),
|
||||
@ -384,6 +386,35 @@ describe("Moves - Delayed Attacks", () => {
|
||||
expect(typeBoostSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not crash when catching & releasing a Pokemon on the same turn its delayed attack expires", async () => {
|
||||
game.override.startingModifier([{ name: "MASTER_BALL", count: 1 }]);
|
||||
await game.classicMode.startBattle([
|
||||
SpeciesId.FEEBAS,
|
||||
SpeciesId.FEEBAS,
|
||||
SpeciesId.FEEBAS,
|
||||
SpeciesId.FEEBAS,
|
||||
SpeciesId.FEEBAS,
|
||||
SpeciesId.FEEBAS,
|
||||
]);
|
||||
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(MoveId.FUTURE_SIGHT);
|
||||
await game.toNextTurn();
|
||||
|
||||
expectFutureSightActive(1);
|
||||
|
||||
await passTurns(1);
|
||||
|
||||
// Throw master ball and release the enemy
|
||||
game.doThrowPokeball(PokeballType.MASTER_BALL);
|
||||
game.onNextPrompt("AttemptCapturePhase", UiMode.CONFIRM, () => {
|
||||
game.scene.ui.processInput(Button.CANCEL);
|
||||
});
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expectFutureSightActive(0);
|
||||
});
|
||||
|
||||
// TODO: Implement and move to a power spot's test file
|
||||
it.todo("Should activate ally's power spot when switched in during single battles");
|
||||
});
|
||||
|
50
test/moves/poltergeist.test.ts
Normal file
50
test/moves/poltergeist.test.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Move - Poltergeist", () => {
|
||||
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
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
it("should not crash when used after both opponents have fainted", async () => {
|
||||
game.override.battleStyle("double").enemyLevel(5);
|
||||
await game.classicMode.startBattle([SpeciesId.STARYU, SpeciesId.SLOWPOKE]);
|
||||
|
||||
game.move.use(MoveId.DAZZLING_GLEAM);
|
||||
game.move.use(MoveId.POLTERGEIST, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY);
|
||||
const [_, poltergeistUser] = game.scene.getPlayerField();
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
|
||||
await game.toEndOfTurn();
|
||||
// Expect poltergeist to have failed
|
||||
expect(poltergeistUser).toHaveUsedMove({ move: MoveId.POLTERGEIST, result: MoveResult.FAIL });
|
||||
// If the test makes it to the end of turn, no crash occurred. Nothing to assert
|
||||
});
|
||||
});
|
@ -111,7 +111,7 @@ class FakeMobile {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const event = new Event("touchstart");
|
||||
const event = new Event("pointerdown");
|
||||
node.dispatchEvent(event);
|
||||
}
|
||||
|
||||
@ -120,7 +120,7 @@ class FakeMobile {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const event = new Event("touchend");
|
||||
const event = new Event("pointerup");
|
||||
node.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import type { BattleScene } from "#app/battle-scene";
|
||||
import { Phase } from "#app/phase";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
import { AttemptCapturePhase } from "#phases/attempt-capture-phase";
|
||||
import { AttemptRunPhase } from "#phases/attempt-run-phase";
|
||||
import { BattleEndPhase } from "#phases/battle-end-phase";
|
||||
import { BerryPhase } from "#phases/berry-phase";
|
||||
@ -183,6 +184,7 @@ export class PhaseInterceptor {
|
||||
PostGameOverPhase,
|
||||
RevivalBlessingPhase,
|
||||
PokemonHealPhase,
|
||||
AttemptCapturePhase,
|
||||
];
|
||||
|
||||
private endBySetMode = [
|
||||
|
Loading…
Reference in New Issue
Block a user