Compare commits

..

No commits in common. "main" and "v1.10.3" have entirely different histories.

77 changed files with 483 additions and 1036 deletions

View File

@ -1,7 +1,7 @@
{
"name": "pokemon-rogue-battle",
"private": true,
"version": "1.10.7",
"version": "1.10.3",
"type": "module",
"scripts": {
"start": "vite",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 B

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 B

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 B

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 B

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 B

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 B

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 B

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 B

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 B

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 B

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 B

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 B

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 610 B

After

Width:  |  Height:  |  Size: 645 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 B

After

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 B

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 B

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 B

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 B

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 B

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 296 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 689 B

After

Width:  |  Height:  |  Size: 691 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 B

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 B

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 B

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 B

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 B

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 B

After

Width:  |  Height:  |  Size: 201 B

@ -1 +1 @@
Subproject commit 090bfefaf7e9d4efcbca61fa78a9cdf5d701830b
Subproject commit 102cbdcd924e2a7cdc7eab64d1ce79f6ec7604ff

View File

@ -1,7 +1,3 @@
self.addEventListener('install', function () {
console.log('Service worker installing...');
});
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
})

View File

@ -863,8 +863,6 @@ 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)) {
@ -2320,7 +2318,7 @@ export class BattleScene extends SceneBase {
});
}
playSound(sound: string | AnySound, config?: object): AnySound | null {
playSound(sound: string | AnySound, config?: object): AnySound {
const key = typeof sound === "string" ? sound : sound.key;
config = config ?? {};
try {
@ -2356,19 +2354,16 @@ export class BattleScene extends SceneBase {
this.sound.play(key, config);
return this.sound.get(key) as AnySound;
} catch {
console.warn(`${key} not found`);
return null;
console.log(`${key} not found`);
return sound as AnySound;
}
}
playSoundWithoutBgm(soundName: string, pauseDuration?: number): AnySound | null {
playSoundWithoutBgm(soundName: string, pauseDuration?: number): AnySound {
this.bgmCache.add(soundName);
const resumeBgm = this.pauseBgm();
this.playSound(soundName);
const sound = this.sound.get(soundName);
if (!sound) {
return sound;
}
const sound = this.sound.get(soundName) as AnySound;
if (this.bgmResumeTimer) {
this.bgmResumeTimer.destroy();
}
@ -2378,7 +2373,7 @@ export class BattleScene extends SceneBase {
this.bgmResumeTimer = null;
});
}
return sound as AnySound;
return sound;
}
/** The loop point of any given battle, mystery encounter, or title track, read as seconds and milliseconds. */

View File

@ -396,23 +396,7 @@ export abstract class 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 {
export class BlockRecoilDamageAttr extends AbAttr {
private declare readonly _: never;
constructor() {
super(false);
@ -608,7 +592,11 @@ export class PreDefendFullHpEndureAbAttr extends PreDefendAbAttr {
}
}
export class BlockItemTheftAbAttr extends CancelInteractionAbAttr {
export class BlockItemTheftAbAttr extends AbAttr {
override apply({ cancelled }: AbAttrParamsWithCancel): void {
cancelled.value = true;
}
getTriggerMessage({ pokemon }: AbAttrBaseParams, abilityName: string) {
return i18next.t("abilityTriggers:blockItemTheft", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
@ -881,9 +869,8 @@ export interface FieldPriorityMoveImmunityAbAttrParams extends AugmentMoveIntera
}
export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr {
override canApply({ move, opponent: attacker, cancelled }: FieldPriorityMoveImmunityAbAttrParams): boolean {
override canApply({ move, opponent: attacker }: FieldPriorityMoveImmunityAbAttrParams): boolean {
return (
!cancelled.value &&
!(move.moveTarget === MoveTarget.USER || move.moveTarget === MoveTarget.NEAR_ALLY) &&
move.getPriority(attacker) > 0 &&
!move.isMultiTarget()
@ -910,8 +897,10 @@ export class MoveImmunityAbAttr extends PreDefendAbAttr {
this.immuneCondition = immuneCondition;
}
override canApply({ pokemon, opponent: attacker, move, cancelled }: MoveImmunityAbAttrParams): boolean {
return !cancelled.value && this.immuneCondition(pokemon, attacker, move);
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 apply({ cancelled }: MoveImmunityAbAttrParams): void {
@ -1602,7 +1591,12 @@ export interface FieldPreventExplosiveMovesAbAttrParams extends AbAttrBaseParams
cancelled: BooleanHolder;
}
export class FieldPreventExplosiveMovesAbAttr extends CancelInteractionAbAttr {}
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 interface FieldMultiplyStatAbAttrParams extends AbAttrBaseParams {
/** The kind of stat that is being checked for modification */
@ -2541,11 +2535,15 @@ export class IgnoreOpponentStatStagesAbAttr extends AbAttr {
* Abilities with this attribute prevent the user from being affected by Intimidate.
* @sealed
*/
export class IntimidateImmunityAbAttr extends CancelInteractionAbAttr {
export class IntimidateImmunityAbAttr extends AbAttr {
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),
@ -3579,8 +3577,8 @@ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr {
this.protectedStat = protectedStat;
}
override canApply({ stat, cancelled }: PreStatStageChangeAbAttrParams): boolean {
return !cancelled.value && (isNullOrUndefined(this.protectedStat) || stat === this.protectedStat);
override canApply({ stat }: PreStatStageChangeAbAttrParams): boolean {
return isNullOrUndefined(this.protectedStat) || stat === this.protectedStat;
}
/**
@ -3671,11 +3669,8 @@ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
this.immuneEffects = immuneEffects;
}
override canApply({ effect, cancelled }: PreSetStatusAbAttrParams): boolean {
return (
!cancelled.value &&
((this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) || this.immuneEffects.includes(effect))
);
override canApply({ effect }: PreSetStatusAbAttrParams): boolean {
return (this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) || this.immuneEffects.includes(effect);
}
/**
@ -3725,8 +3720,7 @@ export interface UserFieldStatusEffectImmunityAbAttrParams extends AbAttrBasePar
/**
* Provides immunity to status effects to the user's field.
*/
export class UserFieldStatusEffectImmunityAbAttr extends CancelInteractionAbAttr {
private declare readonly _: never;
export class UserFieldStatusEffectImmunityAbAttr extends AbAttr {
protected immuneEffects: StatusEffect[];
/**
@ -3746,8 +3740,12 @@ export class UserFieldStatusEffectImmunityAbAttr extends CancelInteractionAbAttr
);
}
// declare here to allow typescript to allow us to override `canApply` method without adjusting params
declare apply: (params: UserFieldStatusEffectImmunityAbAttrParams) => void;
/**
* Set the `cancelled` value to true, indicating that the status effect is prevented.
*/
override apply({ cancelled }: UserFieldStatusEffectImmunityAbAttrParams): void {
cancelled.value = true;
}
}
/**
@ -3778,7 +3776,14 @@ export class ConditionalUserFieldStatusEffectImmunityAbAttr extends UserFieldSta
* @returns Whether the ability can be applied to cancel the status effect.
*/
override canApply(params: UserFieldStatusEffectImmunityAbAttrParams): boolean {
return !params.cancelled.value && this.condition(params.target, params.source) && super.canApply(params);
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;
}
}
@ -4014,16 +4019,20 @@ export class ConditionalCritAbAttr extends AbAttr {
}
}
export class BlockNonDirectDamageAbAttr extends CancelInteractionAbAttr {
export class BlockNonDirectDamageAbAttr extends AbAttr {
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 CancelInteractionAbAttr {
export class BlockStatusDamageAbAttr extends AbAttr {
private effects: StatusEffect[];
/**
@ -4035,12 +4044,20 @@ export class BlockStatusDamageAbAttr extends CancelInteractionAbAttr {
this.effects = effects;
}
override canApply({ pokemon, cancelled }: AbAttrParamsWithCancel): boolean {
return !cancelled.value && !!pokemon.status?.effect && this.effects.includes(pokemon.status.effect);
override canApply({ pokemon }: AbAttrParamsWithCancel): boolean {
return !!pokemon.status?.effect && this.effects.includes(pokemon.status.effect);
}
override apply({ cancelled }: AbAttrParamsWithCancel): void {
cancelled.value = true;
}
}
export class BlockOneHitKOAbAttr extends CancelInteractionAbAttr {}
export class BlockOneHitKOAbAttr extends AbAttr {
override apply({ cancelled }: AbAttrParamsWithCancel): void {
cancelled.value = true;
}
}
export interface ChangeMovePriorityAbAttrParams extends AbAttrBaseParams {
/** The move being used */
@ -4114,8 +4131,8 @@ export class BlockWeatherDamageAttr extends PreWeatherDamageAbAttr {
this.weatherTypes = weatherTypes;
}
override canApply({ weather, cancelled }: PreWeatherEffectAbAttrParams): boolean {
if (!weather || cancelled.value) {
override canApply({ weather }: PreWeatherEffectAbAttrParams): boolean {
if (!weather) {
return false;
}
const weatherType = weather.weatherType;
@ -4136,8 +4153,8 @@ export class SuppressWeatherEffectAbAttr extends PreWeatherEffectAbAttr {
this.affectsImmutable = affectsImmutable;
}
override canApply({ weather, cancelled }: PreWeatherEffectAbAttrParams): boolean {
if (!weather || cancelled.value) {
override canApply({ weather }: PreWeatherEffectAbAttrParams): boolean {
if (!weather) {
return false;
}
return this.affectsImmutable || weather.isImmutable();
@ -5134,11 +5151,15 @@ export class StatStageChangeCopyAbAttr extends AbAttr {
}
}
export class BypassBurnDamageReductionAbAttr extends CancelInteractionAbAttr {
export class BypassBurnDamageReductionAbAttr extends AbAttr {
private declare readonly _: never;
constructor() {
super(false);
}
override apply({ cancelled }: AbAttrParamsWithCancel): void {
cancelled.value = true;
}
}
export interface ReduceBurnDamageAbAttrParams extends AbAttrBaseParams {
@ -5178,7 +5199,14 @@ 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 CancelInteractionAbAttr {}
export class PreventBerryUseAbAttr extends AbAttr {
/**
* Prevent use of opposing berries.
*/
override apply({ cancelled }: AbAttrParamsWithCancel): void {
cancelled.value = true;
}
}
/**
* A Pokemon with this ability heals by a percentage of their maximum hp after eating a berry
@ -5636,7 +5664,11 @@ export class IncreasePpAbAttr extends AbAttr {
}
/** @sealed */
export class ForceSwitchOutImmunityAbAttr extends CancelInteractionAbAttr {}
export class ForceSwitchOutImmunityAbAttr extends AbAttr {
override apply({ cancelled }: AbAttrParamsWithCancel): void {
cancelled.value = true;
}
}
export interface ReduceBerryUseThresholdAbAttrParams extends AbAttrBaseParams {
/** Holds the hp ratio for the berry to proc, which may be modified by ability application */
@ -5715,8 +5747,8 @@ export class MoveAbilityBypassAbAttr extends AbAttr {
this.moveIgnoreFunc = moveIgnoreFunc || ((_pokemon, _move) => true);
}
override canApply({ pokemon, move, cancelled }: MoveAbilityBypassAbAttrParams): boolean {
return !cancelled.value && this.moveIgnoreFunc(pokemon, move);
override canApply({ pokemon, move }: MoveAbilityBypassAbAttrParams): boolean {
return this.moveIgnoreFunc(pokemon, move);
}
override apply({ cancelled }: MoveAbilityBypassAbAttrParams): void {
@ -5810,8 +5842,8 @@ export class IgnoreTypeImmunityAbAttr extends AbAttr {
this.allowedMoveTypes = allowedMoveTypes;
}
override canApply({ moveType, defenderType, cancelled }: IgnoreTypeImmunityAbAttrParams): boolean {
return !cancelled.value && this.defenderType === defenderType && this.allowedMoveTypes.includes(moveType);
override canApply({ moveType, defenderType }: IgnoreTypeImmunityAbAttrParams): boolean {
return this.defenderType === defenderType && this.allowedMoveTypes.includes(moveType);
}
override apply({ cancelled }: IgnoreTypeImmunityAbAttrParams): void {

View File

@ -77,8 +77,7 @@ export enum EvolutionItem {
LEADERS_CREST
}
const tyrogueMoves = [MoveId.LOW_SWEEP, MoveId.MACH_PUNCH, MoveId.RAPID_SPIN] as const;
type TyrogueMove = typeof tyrogueMoves[number];
type TyrogueMove = MoveId.LOW_SWEEP | MoveId.MACH_PUNCH | MoveId.RAPID_SPIN;
/**
* Pokemon Evolution tuple type consisting of:
@ -193,7 +192,7 @@ export class SpeciesEvolutionCondition {
case EvoCondKey.WEATHER:
return cond.weather.includes(globalScene.arena.getWeatherType());
case EvoCondKey.TYROGUE:
return pokemon.getMoveset(true).find(m => (tyrogueMoves as readonly MoveId[]) .includes(m.moveId))?.moveId === cond.move;
return pokemon.getMoveset(true).find(m => m.moveId as TyrogueMove)?.moveId === cond.move;
case EvoCondKey.NATURE:
return cond.nature.includes(pokemon.getNature());
case EvoCondKey.RANDOM_FORM: {

View File

@ -68875,27 +68875,27 @@ interface TmPoolTiers {
export const tmPoolTiers: TmPoolTiers = {
[MoveId.MEGA_PUNCH]: ModifierTier.GREAT,
[MoveId.PAY_DAY]: ModifierTier.COMMON,
[MoveId.PAY_DAY]: ModifierTier.ULTRA,
[MoveId.FIRE_PUNCH]: ModifierTier.GREAT,
[MoveId.ICE_PUNCH]: ModifierTier.GREAT,
[MoveId.THUNDER_PUNCH]: ModifierTier.GREAT,
[MoveId.SWORDS_DANCE]: ModifierTier.GREAT,
[MoveId.SWORDS_DANCE]: ModifierTier.COMMON,
[MoveId.CUT]: ModifierTier.COMMON,
[MoveId.FLY]: ModifierTier.GREAT,
[MoveId.FLY]: ModifierTier.COMMON,
[MoveId.MEGA_KICK]: ModifierTier.GREAT,
[MoveId.BODY_SLAM]: ModifierTier.GREAT,
[MoveId.TAKE_DOWN]: ModifierTier.GREAT,
[MoveId.DOUBLE_EDGE]: ModifierTier.ULTRA,
[MoveId.PIN_MISSILE]: ModifierTier.GREAT,
[MoveId.PIN_MISSILE]: ModifierTier.COMMON,
[MoveId.ROAR]: ModifierTier.COMMON,
[MoveId.FLAMETHROWER]: ModifierTier.ULTRA,
[MoveId.HYDRO_PUMP]: ModifierTier.ULTRA,
[MoveId.SURF]: ModifierTier.ULTRA,
[MoveId.ICE_BEAM]: ModifierTier.ULTRA,
[MoveId.BLIZZARD]: ModifierTier.ULTRA,
[MoveId.PSYBEAM]: ModifierTier.COMMON,
[MoveId.PSYBEAM]: ModifierTier.GREAT,
[MoveId.HYPER_BEAM]: ModifierTier.ULTRA,
[MoveId.LOW_KICK]: ModifierTier.GREAT,
[MoveId.LOW_KICK]: ModifierTier.COMMON,
[MoveId.COUNTER]: ModifierTier.COMMON,
[MoveId.STRENGTH]: ModifierTier.GREAT,
[MoveId.SOLAR_BEAM]: ModifierTier.ULTRA,
@ -68907,9 +68907,9 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.DIG]: ModifierTier.GREAT,
[MoveId.TOXIC]: ModifierTier.GREAT,
[MoveId.PSYCHIC]: ModifierTier.ULTRA,
[MoveId.AGILITY]: ModifierTier.GREAT,
[MoveId.AGILITY]: ModifierTier.COMMON,
[MoveId.NIGHT_SHADE]: ModifierTier.COMMON,
[MoveId.SCREECH]: ModifierTier.GREAT,
[MoveId.SCREECH]: ModifierTier.COMMON,
[MoveId.DOUBLE_TEAM]: ModifierTier.COMMON,
[MoveId.CONFUSE_RAY]: ModifierTier.COMMON,
[MoveId.LIGHT_SCREEN]: ModifierTier.COMMON,
@ -68921,7 +68921,7 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.FIRE_BLAST]: ModifierTier.ULTRA,
[MoveId.WATERFALL]: ModifierTier.GREAT,
[MoveId.SWIFT]: ModifierTier.COMMON,
[MoveId.AMNESIA]: ModifierTier.GREAT,
[MoveId.AMNESIA]: ModifierTier.COMMON,
[MoveId.DREAM_EATER]: ModifierTier.GREAT,
[MoveId.LEECH_LIFE]: ModifierTier.ULTRA,
[MoveId.FLASH]: ModifierTier.COMMON,
@ -68933,11 +68933,11 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.SUBSTITUTE]: ModifierTier.COMMON,
[MoveId.THIEF]: ModifierTier.GREAT,
[MoveId.SNORE]: ModifierTier.COMMON,
[MoveId.CURSE]: ModifierTier.GREAT,
[MoveId.CURSE]: ModifierTier.COMMON,
[MoveId.REVERSAL]: ModifierTier.COMMON,
[MoveId.SPITE]: ModifierTier.COMMON,
[MoveId.PROTECT]: ModifierTier.COMMON,
[MoveId.SCARY_FACE]: ModifierTier.GREAT,
[MoveId.SCARY_FACE]: ModifierTier.COMMON,
[MoveId.SLUDGE_BOMB]: ModifierTier.GREAT,
[MoveId.MUD_SLAP]: ModifierTier.COMMON,
[MoveId.SPIKES]: ModifierTier.COMMON,
@ -68979,8 +68979,8 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.TORMENT]: ModifierTier.COMMON,
[MoveId.WILL_O_WISP]: ModifierTier.COMMON,
[MoveId.FACADE]: ModifierTier.GREAT,
[MoveId.FOCUS_PUNCH]: ModifierTier.GREAT,
[MoveId.NATURE_POWER]: ModifierTier.GREAT,
[MoveId.FOCUS_PUNCH]: ModifierTier.COMMON,
[MoveId.NATURE_POWER]: ModifierTier.COMMON,
[MoveId.CHARGE]: ModifierTier.COMMON,
[MoveId.TAUNT]: ModifierTier.COMMON,
[MoveId.HELPING_HAND]: ModifierTier.COMMON,
@ -68993,7 +68993,7 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.ENDEAVOR]: ModifierTier.COMMON,
[MoveId.SKILL_SWAP]: ModifierTier.COMMON,
[MoveId.IMPRISON]: ModifierTier.COMMON,
[MoveId.SECRET_POWER]: ModifierTier.GREAT,
[MoveId.SECRET_POWER]: ModifierTier.COMMON,
[MoveId.DIVE]: ModifierTier.GREAT,
[MoveId.FEATHER_DANCE]: ModifierTier.COMMON,
[MoveId.BLAZE_KICK]: ModifierTier.GREAT,
@ -69001,12 +69001,12 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.BLAST_BURN]: ModifierTier.ULTRA,
[MoveId.HYDRO_CANNON]: ModifierTier.ULTRA,
[MoveId.WEATHER_BALL]: ModifierTier.COMMON,
[MoveId.FAKE_TEARS]: ModifierTier.GREAT,
[MoveId.FAKE_TEARS]: ModifierTier.COMMON,
[MoveId.AIR_CUTTER]: ModifierTier.GREAT,
[MoveId.OVERHEAT]: ModifierTier.ULTRA,
[MoveId.ROCK_TOMB]: ModifierTier.GREAT,
[MoveId.METAL_SOUND]: ModifierTier.GREAT,
[MoveId.COSMIC_POWER]: ModifierTier.GREAT,
[MoveId.METAL_SOUND]: ModifierTier.COMMON,
[MoveId.COSMIC_POWER]: ModifierTier.COMMON,
[MoveId.SIGNAL_BEAM]: ModifierTier.GREAT,
[MoveId.SAND_TOMB]: ModifierTier.COMMON,
[MoveId.MUDDY_WATER]: ModifierTier.GREAT,
@ -69016,10 +69016,10 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.IRON_DEFENSE]: ModifierTier.GREAT,
[MoveId.DRAGON_CLAW]: ModifierTier.ULTRA,
[MoveId.FRENZY_PLANT]: ModifierTier.ULTRA,
[MoveId.BULK_UP]: ModifierTier.GREAT,
[MoveId.BULK_UP]: ModifierTier.COMMON,
[MoveId.BOUNCE]: ModifierTier.GREAT,
[MoveId.MUD_SHOT]: ModifierTier.GREAT,
[MoveId.POISON_TAIL]: ModifierTier.COMMON,
[MoveId.POISON_TAIL]: ModifierTier.GREAT,
[MoveId.COVET]: ModifierTier.GREAT,
[MoveId.MAGICAL_LEAF]: ModifierTier.GREAT,
[MoveId.CALM_MIND]: ModifierTier.GREAT,
@ -69047,7 +69047,7 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.TOXIC_SPIKES]: ModifierTier.GREAT,
[MoveId.FLARE_BLITZ]: ModifierTier.ULTRA,
[MoveId.AURA_SPHERE]: ModifierTier.GREAT,
[MoveId.ROCK_POLISH]: ModifierTier.GREAT,
[MoveId.ROCK_POLISH]: ModifierTier.COMMON,
[MoveId.POISON_JAB]: ModifierTier.GREAT,
[MoveId.DARK_PULSE]: ModifierTier.GREAT,
[MoveId.AQUA_TAIL]: ModifierTier.GREAT,
@ -69063,8 +69063,8 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.ENERGY_BALL]: ModifierTier.GREAT,
[MoveId.BRAVE_BIRD]: ModifierTier.ULTRA,
[MoveId.EARTH_POWER]: ModifierTier.ULTRA,
[MoveId.GIGA_IMPACT]: ModifierTier.ULTRA,
[MoveId.NASTY_PLOT]: ModifierTier.GREAT,
[MoveId.GIGA_IMPACT]: ModifierTier.GREAT,
[MoveId.NASTY_PLOT]: ModifierTier.COMMON,
[MoveId.AVALANCHE]: ModifierTier.GREAT,
[MoveId.SHADOW_CLAW]: ModifierTier.GREAT,
[MoveId.THUNDER_FANG]: ModifierTier.GREAT,
@ -69084,7 +69084,7 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.IRON_HEAD]: ModifierTier.GREAT,
[MoveId.STONE_EDGE]: ModifierTier.ULTRA,
[MoveId.STEALTH_ROCK]: ModifierTier.COMMON,
[MoveId.GRASS_KNOT]: ModifierTier.GREAT,
[MoveId.GRASS_KNOT]: ModifierTier.ULTRA,
[MoveId.BUG_BITE]: ModifierTier.GREAT,
[MoveId.CHARGE_BEAM]: ModifierTier.GREAT,
[MoveId.HONE_CLAWS]: ModifierTier.COMMON,
@ -69102,7 +69102,7 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.FOUL_PLAY]: ModifierTier.ULTRA,
[MoveId.ROUND]: ModifierTier.COMMON,
[MoveId.ECHOED_VOICE]: ModifierTier.COMMON,
[MoveId.STORED_POWER]: ModifierTier.GREAT,
[MoveId.STORED_POWER]: ModifierTier.COMMON,
[MoveId.ALLY_SWITCH]: ModifierTier.COMMON,
[MoveId.SCALD]: ModifierTier.GREAT,
[MoveId.HEX]: ModifierTier.GREAT,
@ -69130,7 +69130,7 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.SNARL]: ModifierTier.COMMON,
[MoveId.PHANTOM_FORCE]: ModifierTier.ULTRA,
[MoveId.PETAL_BLIZZARD]: ModifierTier.GREAT,
[MoveId.DISARMING_VOICE]: ModifierTier.COMMON,
[MoveId.DISARMING_VOICE]: ModifierTier.GREAT,
[MoveId.DRAINING_KISS]: ModifierTier.GREAT,
[MoveId.GRASSY_TERRAIN]: ModifierTier.COMMON,
[MoveId.MISTY_TERRAIN]: ModifierTier.COMMON,
@ -69161,12 +69161,12 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.BREAKING_SWIPE]: ModifierTier.GREAT,
[MoveId.STEEL_BEAM]: ModifierTier.ULTRA,
[MoveId.EXPANDING_FORCE]: ModifierTier.GREAT,
[MoveId.STEEL_ROLLER]: ModifierTier.GREAT,
[MoveId.STEEL_ROLLER]: ModifierTier.COMMON,
[MoveId.SCALE_SHOT]: ModifierTier.ULTRA,
[MoveId.METEOR_BEAM]: ModifierTier.GREAT,
[MoveId.MISTY_EXPLOSION]: ModifierTier.GREAT,
[MoveId.MISTY_EXPLOSION]: ModifierTier.COMMON,
[MoveId.GRASSY_GLIDE]: ModifierTier.COMMON,
[MoveId.RISING_VOLTAGE]: ModifierTier.GREAT,
[MoveId.RISING_VOLTAGE]: ModifierTier.COMMON,
[MoveId.TERRAIN_PULSE]: ModifierTier.COMMON,
[MoveId.SKITTER_SMACK]: ModifierTier.GREAT,
[MoveId.BURNING_JEALOUSY]: ModifierTier.GREAT,
@ -69175,20 +69175,20 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.CORROSIVE_GAS]: ModifierTier.COMMON,
[MoveId.COACHING]: ModifierTier.COMMON,
[MoveId.FLIP_TURN]: ModifierTier.COMMON,
[MoveId.TRIPLE_AXEL]: ModifierTier.ULTRA,
[MoveId.DUAL_WINGBEAT]: ModifierTier.GREAT,
[MoveId.TRIPLE_AXEL]: ModifierTier.COMMON,
[MoveId.DUAL_WINGBEAT]: ModifierTier.COMMON,
[MoveId.SCORCHING_SANDS]: ModifierTier.GREAT,
[MoveId.TERA_BLAST]: ModifierTier.GREAT,
[MoveId.ICE_SPINNER]: ModifierTier.GREAT,
[MoveId.SNOWSCAPE]: ModifierTier.COMMON,
[MoveId.POUNCE]: ModifierTier.COMMON,
[MoveId.TRAILBLAZE]: ModifierTier.GREAT,
[MoveId.TRAILBLAZE]: ModifierTier.COMMON,
[MoveId.CHILLING_WATER]: ModifierTier.COMMON,
[MoveId.HARD_PRESS]: ModifierTier.GREAT,
[MoveId.DRAGON_CHEER]: ModifierTier.COMMON,
[MoveId.ALLURING_VOICE]: ModifierTier.GREAT,
[MoveId.TEMPER_FLARE]: ModifierTier.GREAT,
[MoveId.SUPERCELL_SLAM]: ModifierTier.ULTRA,
[MoveId.SUPERCELL_SLAM]: ModifierTier.GREAT,
[MoveId.PSYCHIC_NOISE]: ModifierTier.GREAT,
[MoveId.UPPER_HAND]: ModifierTier.COMMON,
};

View File

@ -291,17 +291,9 @@ class AnimTimedSoundEvent extends AnimTimedEvent {
} catch (err) {
console.error(err);
}
const sound = globalScene.sound.get(`battle_anims/${this.resourceName}`);
if (!sound) {
return 0;
return Math.ceil((globalScene.sound.get(`battle_anims/${this.resourceName}`).totalDuration * 1000) / 33.33);
}
return Math.ceil((sound.totalDuration * 1000) / 33.33);
}
const cry = battleAnim.user!.cry(soundConfig); // TODO: is the bang behind user correct?
if (!cry) {
return 0;
}
return Math.ceil((cry.totalDuration * 1000) / 33.33);
return Math.ceil((battleAnim.user!.cry(soundConfig).totalDuration * 1000) / 33.33); // TODO: is the bang behind user correct?
}
getEventType(): string {
@ -835,7 +827,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: These bangs are LITERALLY not correct at all
const user = !isOppAnim ? this.user! : this.target!; // TODO: are those bangs correct?
const target = !isOppAnim ? this.target! : this.user!;
if (!target?.isOnField() && !this.playRegardlessOfIssues) {

View File

@ -27,7 +27,7 @@ import type { DexEntry } from "#types/dex-data";
import { type BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common";
import { deepCopy } from "#utils/data";
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
import { toCamelCase } from "#utils/strings";
import { toCamelCase, toSnakeCase } from "#utils/strings";
import i18next from "i18next";
/** A constant for the default max cost of the starting party before a run */
@ -764,7 +764,7 @@ export class SingleTypeChallenge extends Challenge {
}
getValue(overrideValue: number = this.value): string {
return PokemonType[overrideValue - 1].toLowerCase();
return toSnakeCase(PokemonType[overrideValue - 1]);
}
getDescription(overrideValue: number = this.value): string {

View File

@ -6856,15 +6856,12 @@ export class CopyBiomeTypeAttr extends MoveEffectAttr {
}
}
/**
* Attribute to override the target's current types to the given type.
* Used by {@linkcode MoveId.SOAK} and {@linkcode MoveId.MAGIC_POWDER}.
*/
export class ChangeTypeAttr extends MoveEffectAttr {
private type: PokemonType;
constructor(type: PokemonType) {
super(false);
this.type = type;
}
@ -6872,7 +6869,7 @@ export class ChangeTypeAttr extends MoveEffectAttr {
target.summonData.types = [ this.type ];
target.updateInfo();
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:transformedIntoType", { pokemonName: getPokemonNameWithAffix(target), typeName: i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[this.type])}`) }));
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:transformedIntoType", { pokemonName: getPokemonNameWithAffix(target), typeName: i18next.t(`pokemonInfo:Type.${PokemonType[this.type]}`) }));
return true;
}
@ -8140,12 +8137,9 @@ const failIfSingleBattle: MoveConditionFunc = (user, target, move) => globalScen
const failIfDampCondition: MoveConditionFunc = (user, target, move) => {
const cancelled = new BooleanHolder(false);
// 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}));
globalScene.getField(true).map(p=>applyAbAttrs("FieldPreventExplosiveMovesAbAttr", {pokemon: p, cancelled}));
// Queue a message if an ability prevented usage of the move
if (!simulated && cancelled.value) {
if (cancelled.value) {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:cannotUseMove", { pokemonName: getPokemonNameWithAffix(user), moveName: move.name }));
}
return !cancelled.value;
@ -8167,9 +8161,6 @@ 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 "";

View File

@ -2,7 +2,6 @@ import { globalScene } from "#app/global-scene";
import { allSpecies, modifierTypes } from "#data/data-lists";
import { getLevelTotalExp } from "#data/exp";
import type { PokemonSpecies } from "#data/pokemon-species";
import { AbilityId } from "#enums/ability-id";
import { Challenges } from "#enums/challenges";
import { ModifierTier } from "#enums/modifier-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
@ -11,9 +10,8 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Nature } from "#enums/nature";
import { PartyMemberStrength } from "#enums/party-member-strength";
import { PlayerGender } from "#enums/player-gender";
import { MAX_POKEMON_TYPE, PokemonType } from "#enums/pokemon-type";
import { PokemonType } from "#enums/pokemon-type";
import { SpeciesId } from "#enums/species-id";
import { StatusEffect } from "#enums/status-effect";
import { TrainerType } from "#enums/trainer-type";
import type { PlayerPokemon, Pokemon } from "#field/pokemon";
import type { PokemonHeldItemModifier } from "#modifiers/modifier";
@ -221,7 +219,6 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
await showEncounterText(`${namespace}:option.1.dreamComplete`);
await doNewTeamPostProcess(transformations);
globalScene.phaseManager.unshiftNew("PartyHealPhase", true);
setEncounterRewards({
guaranteedModifierTypeFuncs: [
modifierTypes.MEMORY_MUSHROOM,
@ -233,7 +230,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
],
fillRemaining: false,
});
leaveEncounterWithoutBattle(false);
leaveEncounterWithoutBattle(true);
})
.build(),
)
@ -434,8 +431,6 @@ function getTeamTransformations(): PokemonTransformation[] {
newAbilityIndex,
undefined,
);
transformation.newPokemon.teraType = randSeedInt(MAX_POKEMON_TYPE);
}
return pokemonTransformations;
@ -445,8 +440,6 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) {
let atLeastOneNewStarter = false;
for (const transformation of transformations) {
const previousPokemon = transformation.previousPokemon;
const oldHpRatio = previousPokemon.getHpRatio(true);
const oldStatus = previousPokemon.status;
const newPokemon = transformation.newPokemon;
const speciesRootForm = newPokemon.species.getRootSpeciesId();
@ -469,19 +462,6 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) {
}
newPokemon.calculateStats();
if (oldHpRatio > 0) {
newPokemon.hp = Math.ceil(oldHpRatio * newPokemon.getMaxHp());
// Assume that the `status` instance can always safely be transferred to the new pokemon
// This is the case (as of version 1.10.4)
// Safeguard against COMATOSE here
if (!newPokemon.hasAbility(AbilityId.COMATOSE, false, true)) {
newPokemon.status = oldStatus;
}
} else {
newPokemon.hp = 0;
newPokemon.doSetStatus(StatusEffect.FAINT);
}
await newPokemon.updateInfo();
}

View File

@ -593,14 +593,14 @@ export abstract class PokemonSpeciesForm {
});
}
cry(soundConfig?: Phaser.Types.Sound.SoundConfig, ignorePlay?: boolean): AnySound | null {
cry(soundConfig?: Phaser.Types.Sound.SoundConfig, ignorePlay?: boolean): AnySound {
const cryKey = this.getCryKey(this.formIndex);
let cry: AnySound | null = globalScene.sound.get(cryKey) as AnySound;
if (cry?.pendingRemove) {
cry = null;
}
cry = globalScene.playSound(cry ?? cryKey, soundConfig);
if (cry && ignorePlay) {
if (ignorePlay) {
cry.stop();
}
return cry;

View File

@ -126,9 +126,7 @@ 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
// 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();
return !!source && !!target && source !== target && !target.isFainted();
}
}

View File

@ -20,6 +20,3 @@ export enum PokemonType {
FAIRY,
STELLAR
}
/** The largest legal value for a {@linkcode PokemonType} (includes Stellar) */
export const MAX_POKEMON_TYPE = PokemonType.STELLAR;

View File

@ -3243,18 +3243,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
rand -= stabMovePool[index++][1];
}
this.moveset.push(new PokemonMove(stabMovePool[index][0]));
} else {
// If there are no damaging STAB moves, just force a random damaging move
const attackMovePool = baseWeights.filter(m => allMoves[m[0]].category !== MoveCategory.STATUS);
if (attackMovePool.length) {
const totalWeight = attackMovePool.reduce((v, m) => v + m[1], 0);
let rand = randSeedInt(totalWeight);
let index = 0;
while (rand > attackMovePool[index][1]) {
rand -= attackMovePool[index++][1];
}
this.moveset.push(new PokemonMove(attackMovePool[index][0], 0, 0));
}
}
while (baseWeights.length > this.moveset.length && this.moveset.length < 4) {
@ -4547,36 +4535,28 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
});
}
cry(soundConfig?: Phaser.Types.Sound.SoundConfig, sceneOverride?: BattleScene): AnySound | null {
cry(soundConfig?: Phaser.Types.Sound.SoundConfig, sceneOverride?: BattleScene): AnySound {
const scene = sceneOverride ?? globalScene; // TODO: is `sceneOverride` needed?
const cry = this.getSpeciesForm(undefined, true).cry(soundConfig);
if (!cry) {
return cry;
}
let duration = cry.totalDuration * 1000;
if (this.fusionSpecies && this.getSpeciesForm(undefined, true) !== this.getFusionSpeciesForm(undefined, true)) {
const fusionCry = this.getFusionSpeciesForm(undefined, true).cry(soundConfig, true);
if (!fusionCry) {
return cry;
}
let fusionCry = this.getFusionSpeciesForm(undefined, true).cry(soundConfig, true);
duration = Math.min(duration, fusionCry.totalDuration * 1000);
fusionCry.destroy();
scene.time.delayedCall(fixedInt(Math.ceil(duration * 0.4)), () => {
try {
SoundFade.fadeOut(scene, cry, fixedInt(Math.ceil(duration * 0.2)));
const fusionCryInner = this.getFusionSpeciesForm(undefined, true).cry({
fusionCry = this.getFusionSpeciesForm(undefined, true).cry({
seek: Math.max(fusionCry.totalDuration * 0.4, 0),
...soundConfig,
});
if (fusionCryInner) {
SoundFade.fadeIn(
scene,
fusionCryInner,
fusionCry,
fixedInt(Math.ceil(duration * 0.2)),
scene.masterVolume * scene.fieldVolume,
0,
);
}
} catch (err) {
console.error(err);
}
@ -4604,14 +4584,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
crySoundConfig.rate = 0.7;
}
}
const cry = globalScene.playSound(key, crySoundConfig);
const cry = globalScene.playSound(key, crySoundConfig) as AnySound;
if (!cry || globalScene.fieldVolume === 0) {
callback();
return;
}
const sprite = this.getSprite();
const tintSprite = this.getTintSprite();
const delay = Math.max(cry.totalDuration * 50, 25);
const delay = Math.max(globalScene.sound.get(key).totalDuration * 50, 25);
let frameProgress = 0;
let frameThreshold: number;
@ -4664,20 +4644,20 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
const key = this.species.getCryKey(this.formIndex);
let i = 0;
let rate = 0.85;
const cry = globalScene.playSound(key, { rate: rate });
const cry = globalScene.playSound(key, { rate: rate }) as AnySound;
const sprite = this.getSprite();
const tintSprite = this.getTintSprite();
let duration = cry.totalDuration * 1000;
const fusionCryKey = this.fusionSpecies!.getCryKey(this.fusionFormIndex);
let fusionCry = globalScene.playSound(fusionCryKey, {
rate: rate,
});
}) as AnySound;
if (!cry || !fusionCry || globalScene.fieldVolume === 0) {
callback();
return;
}
fusionCry.stop();
let duration = cry.totalDuration * 1000;
duration = Math.min(duration, fusionCry.totalDuration * 1000);
fusionCry.destroy();
const delay = Math.max(duration * 0.05, 25);
@ -4720,12 +4700,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
if (i === transitionIndex && fusionCryKey) {
SoundFade.fadeOut(globalScene, cry, fixedInt(Math.ceil((duration / rate) * 0.2)));
fusionCry = globalScene.playSound(fusionCryKey, {
// TODO: This bang is correct as this callback can only be called once, but
// this whole block with conditionally reassigning fusionCry needs a second lock.
seek: Math.max(fusionCry!.totalDuration * 0.4, 0),
seek: Math.max(fusionCry.totalDuration * 0.4, 0),
rate: rate,
});
if (fusionCry) {
SoundFade.fadeIn(
globalScene,
fusionCry,
@ -4734,7 +4711,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
0,
);
}
}
rate *= 0.99;
if (cry && !cry.pendingRemove) {
cry.setRate(rate);
@ -6590,7 +6566,6 @@ export class EnemyPokemon extends Pokemon {
ignoreAllyAbility: !p.getAlly()?.waveData.abilityRevealed,
ignoreSourceAllyAbility: false,
isCritical,
simulated: true,
}).damage >= p.hp
);
})
@ -6929,7 +6904,7 @@ export class EnemyPokemon extends Pokemon {
const leftoverStats = EFFECTIVE_STATS.filter((s: EffectiveStat) => this.getStatStage(s) < 6);
const statWeights = leftoverStats.map((s: EffectiveStat) => this.getStat(s, false));
let boostedStat: EffectiveStat | undefined;
let boostedStat: EffectiveStat;
const statThresholds: number[] = [];
let totalWeight = 0;
@ -6947,11 +6922,6 @@ export class EnemyPokemon extends Pokemon {
}
}
if (boostedStat === undefined) {
this.bossSegmentIndex--;
return;
}
let stages = 1;
// increase the boost if the boss has at least 3 segments and we passed last shield
@ -6967,7 +6937,7 @@ export class EnemyPokemon extends Pokemon {
"StatStageChangePhase",
this.getBattlerIndex(),
true,
[boostedStat],
[boostedStat!],
stages,
true,
true,

View File

@ -456,7 +456,7 @@ export class CommandPhase extends FieldPhase {
const numBallTypes = 5;
if (cursor < numBallTypes) {
const targetPokemon = globalScene.getEnemyPokemon(false);
const targetPokemon = globalScene.getEnemyPokemon();
if (
targetPokemon?.isBoss() &&
targetPokemon?.bossSegmentIndex >= 1 &&

View File

@ -64,7 +64,7 @@ export class EggHatchPhase extends Phase {
private canSkip: boolean;
private skipped: boolean;
/** The sound effect being played when the egg is hatched */
private evolutionBgm: AnySound | null;
private evolutionBgm: AnySound;
private eggLapsePhase: EggLapsePhase;
constructor(hatchScene: EggLapsePhase, egg: Egg, eggsToHatchCount: number) {
@ -230,7 +230,6 @@ export class EggHatchPhase extends Phase {
} else {
globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true));
}
this.pokemon?.destroy();
super.end();
}

View File

@ -39,10 +39,6 @@ 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();

View File

@ -28,10 +28,9 @@ export class EvolutionPhase extends Phase {
private evolution: SpeciesFormEvolution | null;
private fusionSpeciesEvolved: boolean; // Whether the evolution is of the fused species
private evolutionBgm: AnySound | null;
private evolutionBgm: AnySound;
private evolutionHandler: EvolutionSceneHandler;
/** Container for all assets used by the scene. When the scene is cleared, the children within this are destroyed. */
protected evolutionContainer: Phaser.GameObjects.Container;
protected evolutionBaseBg: Phaser.GameObjects.Image;
protected evolutionBg: Phaser.GameObjects.Video;
@ -298,10 +297,8 @@ export class EvolutionPhase extends Phase {
this.evolutionBg.setVisible(false);
},
});
if (this.evolutionBgm) {
SoundFade.fadeOut(globalScene, this.evolutionBgm, 100);
}
}
/**
* Show the confirmation prompt for pausing evolutions
@ -380,9 +377,7 @@ export class EvolutionPhase extends Phase {
* Fadeout evolution music, play the cry, show the evolution completed text, and end the phase
*/
private onEvolutionComplete(evolvedPokemon: Pokemon) {
if (this.evolutionBgm) {
SoundFade.fadeOut(globalScene, this.evolutionBgm, 100);
}
globalScene.time.delayedCall(250, () => {
this.pokemon.cry();
globalScene.time.delayedCall(1250, () => {
@ -527,7 +522,6 @@ export class EvolutionPhase extends Phase {
return;
}
if (i === lastCycle) {
this.pokemonTintSprite.setVisible(false).setActive(false);
this.pokemonEvoTintSprite.setScale(1);
}
},

View File

@ -204,7 +204,7 @@ export class GameOverPhase extends BattlePhase {
}
this.getRunHistoryEntry().then(runHistoryEntry => {
globalScene.gameData.saveRunHistory(runHistoryEntry, this.isVictory);
globalScene.phaseManager.pushNew("PostGameOverPhase", globalScene.sessionSlotId, endCardPhase);
globalScene.phaseManager.pushNew("PostGameOverPhase", endCardPhase);
this.end();
});
};

View File

@ -24,7 +24,6 @@ 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";
@ -42,13 +41,6 @@ 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;
}
@ -90,11 +82,6 @@ export class MovePhase extends BattlePhase {
this.move = move;
this.useMode = useMode;
this.forcedLast = forcedLast;
this.moveHistoryEntry = {
move: MoveId.NONE,
targets,
useMode,
};
}
/**
@ -423,9 +410,13 @@ export class MovePhase extends BattlePhase {
if (showText) {
this.showMoveText();
}
const moveHistoryEntry = this.moveHistoryEntry;
moveHistoryEntry.result = MoveResult.FAIL;
this.pokemon.pushMoveHistory(moveHistoryEntry);
this.pokemon.pushMoveHistory({
move: this.move.moveId,
targets: this.targets,
result: MoveResult.FAIL,
useMode: this.useMode,
});
// Use move-specific failure messages if present before checking terrain/weather blockage
// and falling back to the classic "But it failed!".
@ -639,9 +630,12 @@ export class MovePhase extends BattlePhase {
frenzyMissFunc(this.pokemon, this.move.getMove());
}
const moveHistoryEntry = this.moveHistoryEntry;
moveHistoryEntry.result = MoveResult.FAIL;
this.pokemon.pushMoveHistory(moveHistoryEntry);
this.pokemon.pushMoveHistory({
move: MoveId.NONE,
result: MoveResult.FAIL,
targets: this.targets,
useMode: this.useMode,
});
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
@ -655,16 +649,13 @@ 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 (
moveId === MoveId.NONE ||
this.move.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(
@ -677,7 +668,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.getActiveTargetPokemon()[0], this.move.getMove());
applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.pokemon.getOpponents(false)[0], this.move.getMove());
}
/**

View File

@ -38,7 +38,6 @@ export class PartyHealPhase extends BattlePhase {
pokemon.updateInfo(true);
}
const healSong = globalScene.playSoundWithoutBgm("heal");
if (healSong) {
globalScene.time.delayedCall(fixedInt(healSong.totalDuration * 1000), () => {
healSong.destroy();
if (this.resumeBgm && bgmPlaying) {
@ -46,7 +45,6 @@ export class PartyHealPhase extends BattlePhase {
}
globalScene.ui.fadeIn(500).then(() => this.end());
});
}
});
globalScene.arena.playerTerasUsed = 0;
}

View File

@ -5,11 +5,10 @@ import type { EndCardPhase } from "#phases/end-card-phase";
export class PostGameOverPhase extends Phase {
public readonly phaseName = "PostGameOverPhase";
private endCardPhase?: EndCardPhase;
private slotId: number;
constructor(slotId: number, endCardPhase?: EndCardPhase) {
constructor(endCardPhase?: EndCardPhase) {
super();
this.slotId = slotId;
this.endCardPhase = endCardPhase;
}
@ -21,7 +20,9 @@ export class PostGameOverPhase extends Phase {
if (!success) {
return globalScene.reset(true);
}
globalScene.gameData.tryClearSession(this.slotId).then((success: boolean | [boolean, boolean]) => {
globalScene.gameData
.tryClearSession(globalScene.sessionSlotId)
.then((success: boolean | [boolean, boolean]) => {
if (!success[0]) {
return globalScene.reset(true);
}

View File

@ -177,9 +177,6 @@ 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();

View File

@ -179,11 +179,12 @@ export class TurnStartPhase extends FieldPhase {
// https://www.smogon.com/forums/threads/sword-shield-battle-mechanics-research.3655528/page-64#post-9244179
phaseManager.pushNew("WeatherEffectPhase");
phaseManager.pushNew("PositionalTagPhase");
phaseManager.pushNew("BerryPhase");
/** Add a new phase to check who should be taking status damage */
phaseManager.pushNew("CheckStatusEffectPhase", moveOrder);
phaseManager.pushNew("PositionalTagPhase");
phaseManager.pushNew("TurnEndPhase");
/*

View File

@ -82,7 +82,6 @@ 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;

View File

@ -68,7 +68,6 @@ 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";
@ -420,15 +419,7 @@ export class GameData {
}
}
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);
}
}
console.debug(systemData);
localStorage.setItem(`data_${loggedInUser?.username}`, encrypt(systemDataStr, bypassLogin));
@ -954,11 +945,11 @@ export class GameData {
} as SessionSaveData;
}
async getSession(slotId: number): Promise<SessionSaveData | null> {
const { promise, resolve, reject } = Promise.withResolvers<SessionSaveData | null>();
getSession(slotId: number): Promise<SessionSaveData | null> {
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this
return new Promise(async (resolve, reject) => {
if (slotId < 0) {
resolve(null);
return promise;
return resolve(null);
}
const handleSessionData = async (sessionDataStr: string) => {
try {
@ -971,12 +962,10 @@ export class GameData {
};
if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`)) {
const response = await pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId });
pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId }).then(async response => {
if (!response || response?.length === 0 || response?.[0] !== "{") {
console.error(response);
resolve(null);
return promise;
return resolve(null);
}
localStorage.setItem(
@ -985,15 +974,16 @@ export class GameData {
);
await handleSessionData(response);
return promise;
}
});
} else {
const sessionData = localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`);
if (sessionData) {
await handleSessionData(decrypt(sessionData, bypassLogin));
return promise;
} else {
return resolve(null);
}
resolve(null);
return promise;
}
});
}
async renameSession(slotId: number, newName: string): Promise<boolean> {
@ -1038,33 +1028,24 @@ export class GameData {
return !(success !== null && !success);
}
async loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> {
const { promise, resolve, reject } = Promise.withResolvers<boolean>();
loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> {
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this
return new Promise(async (resolve, reject) => {
try {
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);
}
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());
}
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.setSeed(sessionData.seed || globalScene.game.config.seed[0]);
globalScene.resetSeed();
console.log("Seed:", globalScene.seed);
globalScene.sessionPlayTime = fromSession.playTime || 0;
globalScene.sessionPlayTime = sessionData.playTime || 0;
globalScene.lastSavePlayTime = 0;
const loadPokemonAssets: Promise<void>[] = [];
@ -1072,7 +1053,7 @@ export class GameData {
const party = globalScene.getPlayerParty();
party.splice(0, party.length);
for (const p of fromSession.party) {
for (const p of sessionData.party) {
const pokemon = p.toPokemon() as PlayerPokemon;
pokemon.setVisible(false);
loadPokemonAssets.push(pokemon.loadAssets(false));
@ -1080,48 +1061,48 @@ export class GameData {
}
Object.keys(globalScene.pokeballCounts).forEach((key: string) => {
globalScene.pokeballCounts[key] = fromSession.pokeballCounts[key] || 0;
globalScene.pokeballCounts[key] = sessionData.pokeballCounts[key] || 0;
});
if (Overrides.POKEBALL_OVERRIDE.active) {
globalScene.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs;
}
globalScene.money = Math.floor(fromSession.money || 0);
globalScene.money = Math.floor(sessionData.money || 0);
globalScene.updateMoneyText();
if (globalScene.money > this.gameStats.highestMoney) {
this.gameStats.highestMoney = globalScene.money;
}
globalScene.score = fromSession.score;
globalScene.score = sessionData.score;
globalScene.updateScoreText();
globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(fromSession.mysteryEncounterSaveData);
globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(sessionData.mysteryEncounterSaveData);
globalScene.newArena(fromSession.arena.biome, fromSession.playerFaints);
globalScene.newArena(sessionData.arena.biome, sessionData.playerFaints);
const battleType = fromSession.battleType || 0;
const trainerConfig = fromSession.trainer ? trainerConfigs[fromSession.trainer.trainerType] : null;
const battleType = sessionData.battleType || 0;
const trainerConfig = sessionData.trainer ? trainerConfigs[sessionData.trainer.trainerType] : null;
const mysteryEncounterType =
fromSession.mysteryEncounterType !== -1 ? fromSession.mysteryEncounterType : undefined;
sessionData.mysteryEncounterType !== -1 ? sessionData.mysteryEncounterType : undefined;
const battle = globalScene.newBattle(
fromSession.waveIndex,
sessionData.waveIndex,
battleType,
fromSession.trainer,
sessionData.trainer,
battleType === BattleType.TRAINER
? trainerConfig?.doubleOnly || fromSession.trainer?.variant === TrainerVariant.DOUBLE
: fromSession.enemyParty.length > 1,
? trainerConfig?.doubleOnly || sessionData.trainer?.variant === TrainerVariant.DOUBLE
: sessionData.enemyParty.length > 1,
mysteryEncounterType,
);
battle.enemyLevels = fromSession.enemyParty.map(p => p.level);
battle.enemyLevels = sessionData.enemyParty.map(p => p.level);
globalScene.arena.init();
fromSession.enemyParty.forEach((enemyData, e) => {
sessionData.enemyParty.forEach((enemyData, e) => {
const enemyPokemon = enemyData.toPokemon(
battleType,
e,
fromSession.trainer?.variant === TrainerVariant.DOUBLE,
sessionData.trainer?.variant === TrainerVariant.DOUBLE,
) as EnemyPokemon;
battle.enemyParty[e] = enemyPokemon;
if (battleType === BattleType.WILD) {
@ -1131,7 +1112,7 @@ export class GameData {
loadPokemonAssets.push(enemyPokemon.loadAssets());
});
globalScene.arena.weather = fromSession.arena.weather;
globalScene.arena.weather = sessionData.arena.weather;
globalScene.arena.eventTarget.dispatchEvent(
new WeatherChangedEvent(
WeatherType.NONE,
@ -1140,7 +1121,7 @@ export class GameData {
),
); // TODO: is this bang correct?
globalScene.arena.terrain = fromSession.arena.terrain;
globalScene.arena.terrain = sessionData.arena.terrain;
globalScene.arena.eventTarget.dispatchEvent(
new TerrainChangedEvent(
TerrainType.NONE,
@ -1149,9 +1130,9 @@ export class GameData {
),
); // TODO: is this bang correct?
globalScene.arena.playerTerasUsed = fromSession.arena.playerTerasUsed;
globalScene.arena.playerTerasUsed = sessionData.arena.playerTerasUsed;
globalScene.arena.tags = fromSession.arena.tags;
globalScene.arena.tags = sessionData.arena.tags;
if (globalScene.arena.tags) {
for (const tag of globalScene.arena.tags) {
if (tag instanceof EntryHazardTag) {
@ -1165,7 +1146,7 @@ export class GameData {
}
}
globalScene.arena.positionalTagManager.tags = fromSession.arena.positionalTags.map(tag =>
globalScene.arena.positionalTagManager.tags = sessionData.arena.positionalTags.map(tag =>
loadPositionalTag(tag),
);
@ -1173,7 +1154,7 @@ export class GameData {
console.warn("Existing modifiers not cleared on session load, deleting...");
globalScene.modifiers = [];
}
for (const modifierData of fromSession.modifiers) {
for (const modifierData of sessionData.modifiers) {
const modifier = modifierData.toModifier(Modifier[modifierData.className]);
if (modifier) {
globalScene.addModifier(modifier, true);
@ -1181,7 +1162,7 @@ export class GameData {
}
globalScene.updateModifiers(true);
for (const enemyModifierData of fromSession.enemyModifiers) {
for (const enemyModifierData of sessionData.enemyModifiers) {
const modifier = enemyModifierData.toModifier(Modifier[enemyModifierData.className]);
if (modifier) {
globalScene.addEnemyModifier(modifier, true);
@ -1196,9 +1177,7 @@ export class GameData {
initSessionFromData(sessionData);
} else {
this.getSession(slotId)
.then(data => {
return data && initSessionFromData(data);
})
.then(data => data && initSessionFromData(data))
.catch(err => {
reject(err);
return;
@ -1206,9 +1185,9 @@ export class GameData {
}
} catch (err) {
reject(err);
return;
}
return promise;
});
}
/**

View File

@ -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("pointerdown", event => {
node.addEventListener("touchstart", event => {
event.preventDefault();
this.touchButtonDown(node, key);
});
node.addEventListener("pointerup", event => {
node.addEventListener("touchend", event => {
event.preventDefault();
this.touchButtonUp(node, key, event.target?.["id"]);
});

View File

@ -115,9 +115,6 @@ export class EnemyBattleInfo extends BattleInfo {
globalScene.gameData.starterData[pokemon.species.getRootSpeciesId()].classicWinCount > 0 &&
globalScene.gameData.starterData[pokemon.species.getRootSpeciesId(true)].classicWinCount > 0
) {
// move the ribbon to the left if there is no owned icon
const championRibbonX = this.ownedIcon.visible ? 8 : 0;
this.championRibbon.setPositionRelative(this.nameText, championRibbonX, 11.75);
this.championRibbon.setVisible(true);
}
@ -183,12 +180,12 @@ export class EnemyBattleInfo extends BattleInfo {
this.ownedIcon,
this.championRibbon,
this.statusIndicator,
this.levelContainer,
this.statValuesContainer,
].map(e => (e.x += 48 * (boss ? -1 : 1)));
this.hpBar.x += 38 * (boss ? -1 : 1);
this.hpBar.y += 2 * (this.boss ? -1 : 1);
this.hpBar.setTexture(`overlay_hp${boss ? "_boss" : ""}`);
this.levelContainer.x += 2 * (boss ? -1 : 1);
this.box.setTexture(this.getTextureName());
this.statsBox.setTexture(`${this.getTextureName()}_stats`);
}

View File

@ -136,11 +136,6 @@ 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

View File

@ -613,20 +613,6 @@ export class PartyUiHandler extends MessageUiHandler {
ui.playSelect();
return true;
}
if (option === PartyOption.SUMMARY) {
return this.processSummaryOption(pokemon);
}
if (option === PartyOption.POKEDEX) {
return this.processPokedexOption(pokemon);
}
if (option === PartyOption.UNPAUSE_EVOLUTION) {
return this.processUnpauseEvolutionOption(pokemon);
}
if (option === PartyOption.RENAME) {
return this.processRenameOption(pokemon);
}
return false;
}

View File

@ -106,6 +106,10 @@ 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()) &&

View File

@ -9,9 +9,9 @@ export const TOUCH_CONTROL_POSITIONS_PORTRAIT = "touchControlPositionsPortrait";
type ControlPosition = { id: string; x: number; y: number };
type ConfigurationEventListeners = {
pointerdown: EventListener[];
pointermove: EventListener[];
pointerup: EventListener[];
touchstart: EventListener[];
touchmove: EventListener[];
touchend: 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 = {
pointerdown: [],
pointermove: [],
pointerup: [],
touchstart: [],
touchmove: [],
touchend: [],
};
private overlay: Phaser.GameObjects.Container;
@ -165,33 +165,34 @@ export class MoveTouchControlsHandler {
/**
* Start dragging the given button.
* @param controlGroup The button that is being dragged.
* @param event The pointer event that started the drag.
* @param touch The touch event that started the drag.
*/
private startDrag = (controlGroup: HTMLElement): void => {
this.draggingElement = controlGroup;
};
/**
* Drags the currently dragged element to the given pointer position.
* @param event The pointer event that is currently happening.
* 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.
*/
private drag = (event: PointerEvent): void => {
private drag = (touch: Touch): void => {
if (!this.draggingElement) {
return;
}
const rect = this.draggingElement.getBoundingClientRect();
// Map the pointer position to the center of the dragged element.
// Map the touch position to the center of the dragged element.
const xOffset = this.isLeft(this.draggingElement)
? event.clientX - rect.width / 2
: window.innerWidth - event.clientX - rect.width / 2;
const yOffset = window.innerHeight - event.clientY - rect.height / 2;
? touch.clientX - rect.width / 2
: window.innerWidth - touch.clientX - rect.width / 2;
const yOffset = window.innerHeight - touch.clientY - rect.height / 2;
this.setPosition(this.draggingElement, xOffset, yOffset);
};
/**
* Stops dragging the currently dragged element.
*/
private stopDrag = (): void => {
private stopDrag = () => {
this.draggingElement = null;
};
@ -302,19 +303,19 @@ export class MoveTouchControlsHandler {
*/
private createConfigurationEventListeners(controlGroups: HTMLDivElement[]): ConfigurationEventListeners {
return {
pointerdown: controlGroups.map((element: HTMLDivElement) => {
touchstart: controlGroups.map((element: HTMLDivElement) => {
const startDrag = () => this.startDrag(element);
element.addEventListener("pointerdown", startDrag, { passive: true });
element.addEventListener("touchstart", startDrag, { passive: true });
return startDrag;
}),
pointermove: controlGroups.map(() => {
const drag = (event: PointerEvent) => this.drag(event);
window.addEventListener("pointermove", drag, { passive: true });
touchmove: controlGroups.map(() => {
const drag = event => this.drag(event.touches[0]);
window.addEventListener("touchmove", drag, { passive: true });
return drag;
}),
pointerup: controlGroups.map(() => {
touchend: controlGroups.map(() => {
const stopDrag = () => this.stopDrag();
window.addEventListener("pointerup", stopDrag, { passive: true });
window.addEventListener("touchend", stopDrag, { passive: true });
return stopDrag;
}),
};
@ -372,12 +373,12 @@ export class MoveTouchControlsHandler {
this.draggingElement = null;
// Remove event listeners
const { pointerdown, pointermove, pointerup } = this.configurationEventListeners;
const { touchstart, touchmove, touchend } = this.configurationEventListeners;
this.getControlGroupElements().forEach((element, index) =>
element.removeEventListener("pointerdown", pointerdown[index]),
element.removeEventListener("touchstart", touchstart[index]),
);
pointermove.forEach(listener => window.removeEventListener("pointermove", listener));
pointerup.forEach(listener => window.removeEventListener("pointerup", listener));
touchmove.forEach(listener => window.removeEventListener("touchmove", listener));
touchend.forEach(listener => window.removeEventListener("touchend", listener));
// Remove configuration toolbar
const toolbar = document.querySelector("#touchControls #configToolbar");

View File

@ -72,7 +72,7 @@ import {
rgbHexToRgba,
} from "#utils/common";
import type { StarterPreferences } from "#utils/data";
import { deepCopy, loadStarterPreferences, saveStarterPreferences } from "#utils/data";
import { loadStarterPreferences, saveStarterPreferences } from "#utils/data";
import { getPokemonSpeciesForm, getPokerusStarters } from "#utils/pokemon-utils";
import { toCamelCase, toTitleCase } from "#utils/strings";
import { argbFromRgba } from "@material/material-color-utilities";
@ -1148,8 +1148,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
this.starterSelectContainer.setVisible(true);
this.starterPreferences = loadStarterPreferences();
// Deep copy the JSON (avoid re-loading from disk)
this.originalStarterPreferences = deepCopy(this.starterPreferences);
this.originalStarterPreferences = loadStarterPreferences();
this.allSpecies.forEach((species, s) => {
const icon = this.starterContainers[s].icon;
@ -1213,8 +1212,6 @@ export class StarterSelectUiHandler extends MessageUiHandler {
preferences: StarterPreferences,
ignoreChallenge = false,
): StarterAttributes {
// if preferences for the species is undefined, set it to an empty object
preferences[species.speciesId] ??= {};
const starterAttributes = preferences[species.speciesId];
const { dexEntry, starterDataEntry: starterData } = this.getSpeciesData(species.speciesId, !ignoreChallenge);
@ -1831,15 +1828,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
// The persistent starter data to apply e.g. candy upgrades
const persistentStarterData = globalScene.gameData.starterData[this.lastSpecies.speciesId];
// The sanitized starter preferences
if (this.starterPreferences[this.lastSpecies.speciesId] === undefined) {
this.starterPreferences[this.lastSpecies.speciesId] = {};
}
if (this.originalStarterPreferences[this.lastSpecies.speciesId] === undefined) {
this.originalStarterPreferences[this.lastSpecies.speciesId] = {};
}
// Bangs are safe here due to the above check
const starterAttributes = this.starterPreferences[this.lastSpecies.speciesId]!;
const originalStarterAttributes = this.originalStarterPreferences[this.lastSpecies.speciesId]!;
let starterAttributes = this.starterPreferences[this.lastSpecies.speciesId];
// The original starter preferences
const originalStarterAttributes = this.originalStarterPreferences[this.lastSpecies.speciesId];
// this gets the correct pokemon cursor depending on whether you're in the starter screen or the party icons
if (!this.starterIconsCursorObj.visible) {
@ -2059,6 +2050,10 @@ export class StarterSelectUiHandler extends MessageUiHandler {
const option: OptionSelectItem = {
label: getNatureName(n, true, true, true, globalScene.uiTheme),
handler: () => {
// update default nature in starter save data
if (!starterAttributes) {
starterAttributes = this.starterPreferences[this.lastSpecies.speciesId] = {};
}
starterAttributes.nature = n;
originalStarterAttributes.nature = starterAttributes.nature;
this.clearText();
@ -3413,9 +3408,8 @@ export class StarterSelectUiHandler extends MessageUiHandler {
if (species) {
const defaultDexAttr = this.getCurrentDexProps(species.speciesId);
const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
// Bang is correct due to the `?` before variant
const variant = this.starterPreferences[species.speciesId]?.variant
? (this.starterPreferences[species.speciesId]!.variant as Variant)
? (this.starterPreferences[species.speciesId].variant as Variant)
: defaultProps.variant;
const tint = getVariantTint(variant);
this.pokemonShinyIcon.setFrame(getVariantIcon(variant)).setTint(tint);
@ -3640,9 +3634,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
if (starterIndex > -1) {
props = globalScene.gameData.getSpeciesDexAttrProps(species, this.starterAttr[starterIndex]);
this.setSpeciesDetails(
species,
{
this.setSpeciesDetails(species, {
shiny: props.shiny,
formIndex: props.formIndex,
female: props.female,
@ -3650,9 +3642,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
abilityIndex: this.starterAbilityIndexes[starterIndex],
natureIndex: this.starterNatures[starterIndex],
teraType: this.starterTeras[starterIndex],
},
false,
);
});
} else {
const defaultAbilityIndex =
starterAttributes?.ability ?? globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species);
@ -3669,9 +3659,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
props.formIndex = starterAttributes?.form ?? props.formIndex;
props.female = starterAttributes?.female ?? props.female;
this.setSpeciesDetails(
species,
{
this.setSpeciesDetails(species, {
shiny: props.shiny,
formIndex: props.formIndex,
female: props.female,
@ -3679,9 +3667,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
abilityIndex: defaultAbilityIndex,
natureIndex: defaultNature,
teraType: starterAttributes?.tera,
},
false,
);
});
}
if (!isNullOrUndefined(props.formIndex)) {
@ -3718,9 +3704,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
const defaultNature = globalScene.gameData.getSpeciesDefaultNature(species);
const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
this.setSpeciesDetails(
species,
{
this.setSpeciesDetails(species, {
shiny: props.shiny,
formIndex: props.formIndex,
female: props.female,
@ -3728,9 +3712,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
abilityIndex: defaultAbilityIndex,
natureIndex: defaultNature,
forSeen: true,
},
false,
);
});
this.pokemonSprite.setTint(0x808080);
}
} else {
@ -3752,9 +3734,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonFormText.setVisible(false);
this.teraIcon.setVisible(false);
this.setSpeciesDetails(
species!,
{
this.setSpeciesDetails(species!, {
// TODO: is this bang correct?
shiny: false,
formIndex: 0,
@ -3762,9 +3742,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
variant: 0,
abilityIndex: 0,
natureIndex: 0,
},
false,
);
});
this.pokemonSprite.clearTint();
}
}
@ -3786,7 +3764,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
return { dexEntry: { ...copiedDexEntry }, starterDataEntry: { ...copiedStarterDataEntry } };
}
setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}, save = true): void {
setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}): void {
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;
@ -4198,10 +4176,8 @@ export class StarterSelectUiHandler extends MessageUiHandler {
this.updateInstructions();
if (save) {
saveStarterPreferences(this.originalStarterPreferences);
}
}
setTypeIcons(type1: PokemonType | null, type2: PokemonType | null): void {
if (type1 !== null) {

View File

@ -349,15 +349,6 @@ export function getTextStyleOptions(
styleOptions.fontSize = defaultFontSize - 42;
styleOptions.padding = { top: 4 };
break;
case "ko":
styleOptions.fontSize = defaultFontSize - 38;
styleOptions.padding = { top: 4, left: 6 };
break;
case "zh-CN":
case "zh-TW":
styleOptions.fontSize = defaultFontSize - 42;
styleOptions.padding = { top: 5, left: 14 };
break;
default:
styleOptions.fontSize = defaultFontSize - 30;
styleOptions.padding = { left: 12 };

View File

@ -8,7 +8,7 @@ import { AES, enc } from "crypto-js";
* @param values - The object to be deep copied.
* @returns A new object that is a deep copy of the input.
*/
export function deepCopy<T extends object>(values: T): T {
export function deepCopy(values: object): object {
// Convert the object to a JSON string and parse it back to an object to perform a deep copy
return JSON.parse(JSON.stringify(values));
}
@ -58,28 +58,13 @@ export function decrypt(data: string, bypassLogin: boolean): string {
return AES.decrypt(data, saveKey).toString(enc.Utf8);
}
/**
* Check if an object has no properties of its own (its shape is `{}`). An empty array is considered a bare object.
* @param obj - Object to check
* @returns - Whether the object is bare
*/
export function isBareObject(obj: any): boolean {
if (typeof obj !== "object") {
return false;
}
for (const _ in obj) {
return false;
}
return true;
}
// the latest data saved/loaded for the Starter Preferences. Required to reduce read/writes. Initialize as "{}", since this is the default value and no data needs to be stored if present.
// if they ever add private static variables, move this into StarterPrefs
const StarterPrefers_DEFAULT: string = "{}";
let StarterPrefers_private_latest: string = StarterPrefers_DEFAULT;
export interface StarterPreferences {
[key: number]: StarterAttributes | undefined;
[key: number]: StarterAttributes;
}
// called on starter selection show once
@ -89,17 +74,11 @@ export function loadStarterPreferences(): StarterPreferences {
localStorage.getItem(`starterPrefs_${loggedInUser?.username}`) || StarterPrefers_DEFAULT),
);
}
// called on starter selection clear, always
export function saveStarterPreferences(prefs: StarterPreferences): void {
// Fastest way to check if an object has any properties (does no allocation)
if (isBareObject(prefs)) {
console.warn("Refusing to save empty starter preferences");
return;
}
// no reason to store `{}` (for starters not customized)
const pStr: string = JSON.stringify(prefs, (_, value) => (isBareObject(value) ? undefined : value));
const pStr: string = JSON.stringify(prefs);
if (pStr !== StarterPrefers_private_latest) {
console.log("%cSaving starter preferences", "color: blue");
// something changed, store the update
localStorage.setItem(`starterPrefs_${loggedInUser?.username}`, pStr);
// update the latest prefs

View File

@ -175,27 +175,4 @@ describe("Evolution", () => {
expect(fourForm.evoFormKey).toBe("four"); // meanwhile, according to the pokemon-forms, the evoFormKey for a 4 family maushold is "four"
}
});
it("tyrogue should evolve if move is not in first slot", async () => {
game.override
.moveset([MoveId.TACKLE, MoveId.RAPID_SPIN, MoveId.LOW_KICK])
.enemySpecies(SpeciesId.GOLEM)
.enemyMoveset(MoveId.SPLASH)
.startingWave(41)
.startingLevel(19)
.enemyLevel(30);
await game.classicMode.startBattle([SpeciesId.TYROGUE]);
const tyrogue = game.field.getPlayerPokemon();
const golem = game.field.getEnemyPokemon();
golem.hp = 1;
expect(golem.hp).toBe(1);
game.move.select(MoveId.TACKLE);
await game.phaseInterceptor.to("EndEvolutionPhase");
expect(tyrogue.species.speciesId).toBe(SpeciesId.HITMONTOP);
});
});

View File

@ -4,15 +4,12 @@ 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";
@ -98,7 +95,7 @@ describe("Moves - Delayed Attacks", () => {
expectFutureSightActive(0);
const enemy = game.field.getEnemyPokemon();
expect(enemy).not.toHaveFullHp();
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
expect(game.textInterceptor.logs).toContain(
i18next.t("moveTriggers:tookMoveAttack", {
pokemonName: getPokemonNameWithAffix(enemy),
@ -133,12 +130,12 @@ describe("Moves - Delayed Attacks", () => {
expectFutureSightActive();
const enemy = game.field.getEnemyPokemon();
expect(enemy).toHaveFullHp();
expect(enemy.hp).toBe(enemy.getMaxHp());
await passTurns(2);
expectFutureSightActive(0);
expect(enemy).not.toHaveFullHp();
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
});
it("should work when used against different targets in doubles", async () => {
@ -152,15 +149,15 @@ describe("Moves - Delayed Attacks", () => {
await game.toEndOfTurn();
expectFutureSightActive(2);
expect(enemy1).toHaveFullHp();
expect(enemy2).toHaveFullHp();
expect(enemy1.hp).toBe(enemy1.getMaxHp());
expect(enemy2.hp).toBe(enemy2.getMaxHp());
expect(karp.getLastXMoves()[0].result).toBe(MoveResult.OTHER);
expect(feebas.getLastXMoves()[0].result).toBe(MoveResult.OTHER);
await passTurns(2);
expect(enemy1).not.toHaveFullHp();
expect(enemy2).not.toHaveFullHp();
expect(enemy1.hp).toBeLessThan(enemy1.getMaxHp());
expect(enemy2.hp).toBeLessThan(enemy2.getMaxHp());
});
it("should trigger multiple pending attacks in order of creation, even if that order changes later on", async () => {
@ -225,8 +222,8 @@ describe("Moves - Delayed Attacks", () => {
expect(game.scene.getPlayerParty()).toEqual([milotic, karp, feebas]);
expect(karp).toHaveFullHp();
expect(feebas).toHaveFullHp();
expect(karp.hp).toBe(karp.getMaxHp());
expect(feebas.hp).toBe(feebas.getMaxHp());
expect(game.textInterceptor.logs).not.toContain(
i18next.t("moveTriggers:tookMoveAttack", {
pokemonName: getPokemonNameWithAffix(karp),
@ -248,14 +245,15 @@ describe("Moves - Delayed Attacks", () => {
expect(enemy2.isFainted()).toBe(true);
expectFutureSightActive();
expect(game).toHavePositionalTag({
tagType: PositionalTagType.DELAYED_ATTACK,
targetIndex: enemy1.getBattlerIndex(),
});
const attack = game.scene.arena.positionalTagManager.tags.find(
t => t.tagType === PositionalTagType.DELAYED_ATTACK,
)!;
expect(attack).toBeDefined();
expect(attack.targetIndex).toBe(enemy1.getBattlerIndex());
await passTurns(2);
expect(enemy1).not.toHaveFullHp();
expect(enemy1.hp).toBeLessThan(enemy1.getMaxHp());
expect(game.textInterceptor.logs).toContain(
i18next.t("moveTriggers:tookMoveAttack", {
pokemonName: getPokemonNameWithAffix(enemy1),
@ -283,7 +281,7 @@ describe("Moves - Delayed Attacks", () => {
await game.toNextTurn();
expectFutureSightActive(0);
expect(enemy1).toHaveFullHp();
expect(enemy1.hp).toBe(enemy1.getMaxHp());
expect(game.textInterceptor.logs).not.toContain(
i18next.t("moveTriggers:tookMoveAttack", {
pokemonName: getPokemonNameWithAffix(enemy1),
@ -319,8 +317,8 @@ describe("Moves - Delayed Attacks", () => {
await game.toEndOfTurn();
expect(enemy1).toHaveFullHp();
expect(enemy2).not.toHaveFullHp();
expect(enemy1.hp).toBe(enemy1.getMaxHp());
expect(enemy2.hp).toBeLessThan(enemy2.getMaxHp());
expect(game.textInterceptor.logs).toContain(
i18next.t("moveTriggers:tookMoveAttack", {
pokemonName: getPokemonNameWithAffix(enemy2),
@ -353,7 +351,7 @@ describe("Moves - Delayed Attacks", () => {
// Player Normalize was not applied due to being off field
const enemy = game.field.getEnemyPokemon();
expect(enemy).not.toHaveFullHp();
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
expect(game.textInterceptor.logs).toContain(
i18next.t("moveTriggers:tookMoveAttack", {
pokemonName: getPokemonNameWithAffix(enemy),
@ -386,35 +384,6 @@ 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");
});

View File

@ -1,50 +0,0 @@
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
});
});

View File

@ -111,7 +111,7 @@ class FakeMobile {
if (!node) {
return;
}
const event = new Event("pointerdown");
const event = new Event("touchstart");
node.dispatchEvent(event);
}
@ -120,7 +120,7 @@ class FakeMobile {
if (!node) {
return;
}
const event = new Event("pointerup");
const event = new Event("touchend");
node.dispatchEvent(event);
}
}

View File

@ -1,7 +1,6 @@
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";
@ -184,7 +183,6 @@ export class PhaseInterceptor {
PostGameOverPhase,
RevivalBlessingPhase,
PokemonHealPhase,
AttemptCapturePhase,
];
private endBySetMode = [

View File

@ -1,235 +0,0 @@
import { BerryType } from "#enums/berry-type";
import { Button } from "#enums/buttons";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import { UiMode } from "#enums/ui-mode";
import { GameManager } from "#test/test-utils/game-manager";
import { type PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler";
import type { RenameFormUiHandler } from "#ui/rename-form-ui-handler";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
// todo: Some tests fail when running all tests at once, but pass when running individually. Seams like it's always the 2nd and 4th (non todo) tests that fail.
describe("UI - Transfer Item Options", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(async () => {
game = new GameManager(phaserGame);
game.override
.battleStyle("single")
.startingLevel(100)
.startingHeldItems([
{ name: "BERRY", count: 1, type: BerryType.SITRUS },
{ name: "BERRY", count: 2, type: BerryType.APICOT },
{ name: "BERRY", count: 2, type: BerryType.LUM },
])
.enemySpecies(SpeciesId.MAGIKARP)
.enemyMoveset(MoveId.SPLASH);
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.SQUIRTLE, SpeciesId.CHARMANDER]);
game.move.use(MoveId.DRAGON_CLAW);
await game.phaseInterceptor.to("SelectModifierPhase");
await game.scene.ui.setModeWithoutClear(UiMode.PARTY, PartyUiMode.MODIFIER_TRANSFER);
});
it.todo("should open the summary screen while transfering an item", async () => {
await new Promise<void>(resolve => {
game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, async () => {
await new Promise(r => setTimeout(r, 100));
const handler = game.scene.ui.getHandler() as PartyUiHandler;
// Select first party member
handler.setCursor(0);
handler.processInput(Button.ACTION);
resolve();
});
});
await new Promise<void>(resolve => {
game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, async () => {
await new Promise(r => setTimeout(r, 100));
const handler = game.scene.ui.getHandler() as PartyUiHandler;
// select item to transfer
handler.processInput(Button.ACTION);
resolve();
});
});
await new Promise(r => setTimeout(r, 100));
const handler = game.scene.ui.getHandler() as PartyUiHandler;
// move to second pokemon
handler.setCursor(1);
handler.processInput(Button.ACTION);
// select summary
handler.processInput(Button.DOWN);
handler.processInput(Button.ACTION);
await new Promise(r => setTimeout(r, 100));
expect(game.scene.ui.getMode()).toBe(UiMode.SUMMARY);
});
it.todo("should open the pokèdex screen while transfering an item", async () => {
await new Promise<void>(resolve => {
game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, async () => {
await new Promise(r => setTimeout(r, 100));
const handler = game.scene.ui.getHandler() as PartyUiHandler;
// Select first party member
handler.setCursor(0);
handler.processInput(Button.ACTION);
resolve();
});
});
await new Promise<void>(resolve => {
game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, async () => {
await new Promise(r => setTimeout(r, 100));
const handler = game.scene.ui.getHandler() as PartyUiHandler;
// select item to transfer
handler.processInput(Button.ACTION);
resolve();
});
});
await new Promise(r => setTimeout(r, 100));
const handler = game.scene.ui.getHandler() as PartyUiHandler;
// move to second pokemon
handler.setCursor(1);
handler.processInput(Button.ACTION);
// select pokèdex
handler.processInput(Button.DOWN);
handler.processInput(Button.DOWN);
handler.processInput(Button.ACTION);
await new Promise(r => setTimeout(r, 100));
expect(game.scene.ui.getMode()).toBe(UiMode.POKEDEX_PAGE);
});
it.todo("should open the rename screen and rename the pokemon while transfering an item", async () => {
await new Promise<void>(resolve => {
game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, async () => {
await new Promise(r => setTimeout(r, 100));
const handler = game.scene.ui.getHandler() as PartyUiHandler;
// Select first party member
handler.setCursor(0);
handler.processInput(Button.ACTION);
resolve();
});
});
await new Promise<void>(resolve => {
game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, async () => {
await new Promise(r => setTimeout(r, 100));
const handler = game.scene.ui.getHandler() as PartyUiHandler;
// select item to transfer
handler.processInput(Button.ACTION);
resolve();
});
});
await new Promise(r => setTimeout(r, 100));
let handler: PartyUiHandler | RenameFormUiHandler | undefined;
handler = game.scene.ui.getHandler() as PartyUiHandler;
// move to second pokemon
handler.setCursor(1);
handler.processInput(Button.ACTION);
// select rename
handler.processInput(Button.DOWN);
handler.processInput(Button.DOWN);
handler.processInput(Button.DOWN);
handler.processInput(Button.ACTION);
const pokemon = game.scene.getPlayerParty()[1];
if (!pokemon) {
expect.fail("Pokemon is undefined");
}
const nickname = pokemon.nickname;
expect(nickname).toBe(undefined);
await new Promise(r => setTimeout(r, 100));
expect(game.scene.ui.getMode()).toBe(UiMode.RENAME_POKEMON);
await new Promise(r => setTimeout(r, 100));
handler = game.scene.ui.getHandler() as RenameFormUiHandler;
handler["inputs"][0].setText("New nickname");
handler.processInput(Button.SUBMIT);
await new Promise(r => setTimeout(r, 100));
// get the sanitized name
const sanitizedName = btoa(unescape(encodeURIComponent("New nickname")));
expect(pokemon.nickname).toBe(sanitizedName);
});
it.todo("should pause the evolution while transfering an item", async () => {
await new Promise<void>(resolve => {
game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, async () => {
await new Promise(r => setTimeout(r, 100));
const handler = game.scene.ui.getHandler() as PartyUiHandler;
// Select first party member
handler.setCursor(0);
handler.processInput(Button.ACTION);
resolve();
});
});
await new Promise<void>(resolve => {
game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, async () => {
await new Promise(r => setTimeout(r, 100));
const handler = game.scene.ui.getHandler() as PartyUiHandler;
// select item to transfer
handler.processInput(Button.ACTION);
resolve();
});
});
await new Promise(r => setTimeout(r, 100));
const handler = game.scene.ui.getHandler() as PartyUiHandler;
// move to second pokemon
handler.setCursor(1);
handler.processInput(Button.ACTION);
const pokemon = game.scene.getPlayerParty()[1];
if (!pokemon) {
expect.fail("Pokemon is undefined");
}
if (pokemon.pauseEvolutions !== undefined) {
expect(pokemon.pauseEvolutions).toBe(false);
}
// select pause evolution
handler.processInput(Button.DOWN);
handler.processInput(Button.DOWN);
handler.processInput(Button.DOWN);
handler.processInput(Button.DOWN);
handler.processInput(Button.ACTION);
await new Promise(r => setTimeout(r, 100));
expect(game.scene.ui.getMode()).toBe(UiMode.PARTY);
expect(pokemon.pauseEvolutions).toBe(true);
});
});

View File

@ -1,39 +0,0 @@
import { deepCopy, isBareObject } from "#utils/data";
import { describe, expect, it } from "vitest";
describe("Utils - Data", () => {
describe("deepCopy", () => {
it("should create a deep copy of an object", () => {
const original = { a: 1, b: { c: 2 } };
const copy = deepCopy(original);
// ensure the references are different
expect(copy === original, "copied object should not compare equal").not;
expect(copy).toEqual(original);
// update copy's `a` to a different value and ensure original is unaffected
copy.a = 42;
expect(original.a, "adjusting property of copy should not affect original").toBe(1);
// update copy's nested `b.c` to a different value and ensure original is unaffected
copy.b.c = 99;
expect(original.b.c, "adjusting nested property of copy should not affect original").toBe(2);
});
});
describe("isBareObject", () => {
it("should properly identify bare objects", () => {
expect(isBareObject({}), "{} should be considered bare");
expect(isBareObject(new Object()), "new Object() should be considered bare");
expect(isBareObject(Object.create(null)));
expect(isBareObject([]), "an empty array should be considered bare");
});
it("should properly reject non-objects", () => {
expect(isBareObject(new Date())).not;
expect(isBareObject(null)).not;
expect(isBareObject(42)).not;
expect(isBareObject("")).not;
expect(isBareObject(undefined)).not;
expect(isBareObject(() => {})).not;
expect(isBareObject(new (class A {})())).not;
});
});
});

View File

@ -59,12 +59,5 @@
"outDir": "./build",
"noEmit": true
},
"exclude": [
"node_modules",
"dist",
"vite.config.ts",
"vitest.config.ts",
"vitest.workspace.ts",
"public/service-worker.js"
]
"exclude": ["node_modules", "dist", "vite.config.ts", "vitest.config.ts", "vitest.workspace.ts"]
}