Compare commits
61 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
536b018dae | ||
|
43f8b78c35 | ||
|
8fdd5043c3 | ||
|
e175bbfb28 | ||
|
344e9463cc | ||
|
7001f78beb | ||
|
309e31e196 | ||
|
ddde977a0a | ||
|
4a28773929 | ||
|
9fc31350f8 | ||
|
848c1f01e0 | ||
|
2636f59c1e | ||
|
9f19c6bea2 | ||
|
bc0e2662fa | ||
|
eb8c0c0243 | ||
|
bd7de61a56 | ||
|
17c28c4024 | ||
|
231cfd040c | ||
|
8a69c628d1 | ||
|
9a00bc2f10 | ||
|
2a5e66d85f | ||
|
e0a752aa70 | ||
|
320641eaa2 | ||
|
2c89295e3d | ||
|
a9f6801ecb | ||
|
929f721ee0 | ||
|
5c22d9ccac | ||
|
264dd6b2d0 | ||
|
4dc067daa2 | ||
|
dadc7b9598 | ||
|
98e65b9b8b | ||
|
58ba29a1be | ||
|
1f2788a438 | ||
|
7447602146 | ||
|
e06980519d | ||
|
95ddcae543 | ||
|
6745ce7839 | ||
|
701eecf947 | ||
|
cc7391448a | ||
|
63c1c34746 | ||
|
88e42ba4c4 | ||
|
4aac5472a9 | ||
|
c8a66b2e59 | ||
|
f0c24cd16e | ||
|
622ee5ce80 | ||
|
1b6a52e520 | ||
|
56752d6f4a | ||
|
1f4efa0e27 | ||
|
6ef57e52e7 | ||
|
d8fe5ad753 | ||
|
e139f714de | ||
|
4b8c064335 | ||
|
6fac1a5052 | ||
|
a85d8cd5de | ||
|
cd610ff2c5 | ||
|
30b7a62988 | ||
|
efaf7760e4 | ||
|
0f8b1f63b5 | ||
|
049932c001 | ||
|
ae58f9de4f | ||
|
8dea0ce840 |
2
.github/workflows/github-pages.yml
vendored
@ -69,7 +69,7 @@ jobs:
|
||||
pnpm exec typedoc --out /tmp/docs --githubPages false --entryPoints ./src/
|
||||
|
||||
- name: Commit & Push docs
|
||||
if: github.event_name == 'push'
|
||||
if: github.event_name == 'push' && (github.ref_name == 'beta' || github.ref_name == 'main')
|
||||
run: |
|
||||
cd pokerogue_gh
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pokemon-rogue-battle",
|
||||
"private": true,
|
||||
"version": "1.10.2",
|
||||
"version": "1.10.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
|
Before Width: | Height: | Size: 197 B After Width: | Height: | Size: 195 B |
Before Width: | Height: | Size: 279 B After Width: | Height: | Size: 276 B |
Before Width: | Height: | Size: 290 B After Width: | Height: | Size: 289 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 218 B After Width: | Height: | Size: 215 B |
Before Width: | Height: | Size: 341 B After Width: | Height: | Size: 283 B |
Before Width: | Height: | Size: 349 B After Width: | Height: | Size: 261 B |
Before Width: | Height: | Size: 336 B After Width: | Height: | Size: 331 B |
Before Width: | Height: | Size: 364 B After Width: | Height: | Size: 289 B |
Before Width: | Height: | Size: 280 B After Width: | Height: | Size: 278 B |
Before Width: | Height: | Size: 206 B After Width: | Height: | Size: 205 B |
Before Width: | Height: | Size: 347 B After Width: | Height: | Size: 283 B |
Before Width: | Height: | Size: 235 B After Width: | Height: | Size: 230 B |
Before Width: | Height: | Size: 645 B After Width: | Height: | Size: 610 B |
Before Width: | Height: | Size: 419 B After Width: | Height: | Size: 417 B |
Before Width: | Height: | Size: 363 B After Width: | Height: | Size: 296 B |
Before Width: | Height: | Size: 208 B After Width: | Height: | Size: 206 B |
Before Width: | Height: | Size: 338 B After Width: | Height: | Size: 335 B |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 290 B After Width: | Height: | Size: 276 B |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 471 B After Width: | Height: | Size: 247 B |
Before Width: | Height: | Size: 296 B After Width: | Height: | Size: 295 B |
Before Width: | Height: | Size: 691 B After Width: | Height: | Size: 689 B |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 183 B After Width: | Height: | Size: 152 B |
Before Width: | Height: | Size: 201 B After Width: | Height: | Size: 154 B |
Before Width: | Height: | Size: 201 B After Width: | Height: | Size: 162 B |
Before Width: | Height: | Size: 183 B After Width: | Height: | Size: 152 B |
Before Width: | Height: | Size: 201 B After Width: | Height: | Size: 154 B |
Before Width: | Height: | Size: 201 B After Width: | Height: | Size: 162 B |
@ -1 +1 @@
|
||||
Subproject commit a73ea68fdda09bb5018f524cbe6b7e73a3ddf4e0
|
||||
Subproject commit 090bfefaf7e9d4efcbca61fa78a9cdf5d701830b
|
@ -1,3 +1,7 @@
|
||||
self.addEventListener('install', function () {
|
||||
console.log('Service worker installing...');
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(self.clients.claim());
|
||||
})
|
@ -863,6 +863,8 @@ export class BattleScene extends SceneBase {
|
||||
* @param pokemonId - The ID whose Pokemon will be retrieved.
|
||||
* @returns The {@linkcode Pokemon} associated with the given id.
|
||||
* Returns `null` if the ID is `undefined` or not present in either party.
|
||||
* @todo Change the `null` to `undefined` and update callers' signatures -
|
||||
* this is weird and causes a lot of random jank
|
||||
*/
|
||||
getPokemonById(pokemonId: number | undefined): Pokemon | null {
|
||||
if (isNullOrUndefined(pokemonId)) {
|
||||
@ -2318,7 +2320,7 @@ export class BattleScene extends SceneBase {
|
||||
});
|
||||
}
|
||||
|
||||
playSound(sound: string | AnySound, config?: object): AnySound {
|
||||
playSound(sound: string | AnySound, config?: object): AnySound | null {
|
||||
const key = typeof sound === "string" ? sound : sound.key;
|
||||
config = config ?? {};
|
||||
try {
|
||||
@ -2354,16 +2356,19 @@ export class BattleScene extends SceneBase {
|
||||
this.sound.play(key, config);
|
||||
return this.sound.get(key) as AnySound;
|
||||
} catch {
|
||||
console.log(`${key} not found`);
|
||||
return sound as AnySound;
|
||||
console.warn(`${key} not found`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
playSoundWithoutBgm(soundName: string, pauseDuration?: number): AnySound {
|
||||
playSoundWithoutBgm(soundName: string, pauseDuration?: number): AnySound | null {
|
||||
this.bgmCache.add(soundName);
|
||||
const resumeBgm = this.pauseBgm();
|
||||
this.playSound(soundName);
|
||||
const sound = this.sound.get(soundName) as AnySound;
|
||||
const sound = this.sound.get(soundName);
|
||||
if (!sound) {
|
||||
return sound;
|
||||
}
|
||||
if (this.bgmResumeTimer) {
|
||||
this.bgmResumeTimer.destroy();
|
||||
}
|
||||
@ -2373,7 +2378,7 @@ export class BattleScene extends SceneBase {
|
||||
this.bgmResumeTimer = null;
|
||||
});
|
||||
}
|
||||
return sound;
|
||||
return sound as AnySound;
|
||||
}
|
||||
|
||||
/** The loop point of any given battle, mystery encounter, or title track, read as seconds and milliseconds. */
|
||||
|
@ -396,7 +396,23 @@ export abstract class AbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
export class BlockRecoilDamageAttr extends AbAttr {
|
||||
/**
|
||||
* Abstract class for ability attributes that simply cancel an interaction
|
||||
*
|
||||
* @remarks
|
||||
* Abilities that have simple cancel interactions (e.g. {@linkcode BlockRecoilDamageAttr}) can extend this class to reuse the `canApply` and `apply` logic
|
||||
*/
|
||||
abstract class CancelInteractionAbAttr extends AbAttr {
|
||||
override canApply({ cancelled }: AbAttrParamsWithCancel): boolean {
|
||||
return !cancelled.value;
|
||||
}
|
||||
|
||||
override apply({ cancelled }: AbAttrParamsWithCancel): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
export class BlockRecoilDamageAttr extends CancelInteractionAbAttr {
|
||||
private declare readonly _: never;
|
||||
constructor() {
|
||||
super(false);
|
||||
@ -592,11 +608,7 @@ export class PreDefendFullHpEndureAbAttr extends PreDefendAbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
export class BlockItemTheftAbAttr extends AbAttr {
|
||||
override apply({ cancelled }: AbAttrParamsWithCancel): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
|
||||
export class BlockItemTheftAbAttr extends CancelInteractionAbAttr {
|
||||
getTriggerMessage({ pokemon }: AbAttrBaseParams, abilityName: string) {
|
||||
return i18next.t("abilityTriggers:blockItemTheft", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
@ -869,8 +881,9 @@ export interface FieldPriorityMoveImmunityAbAttrParams extends AugmentMoveIntera
|
||||
}
|
||||
|
||||
export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr {
|
||||
override canApply({ move, opponent: attacker }: FieldPriorityMoveImmunityAbAttrParams): boolean {
|
||||
override canApply({ move, opponent: attacker, cancelled }: FieldPriorityMoveImmunityAbAttrParams): boolean {
|
||||
return (
|
||||
!cancelled.value &&
|
||||
!(move.moveTarget === MoveTarget.USER || move.moveTarget === MoveTarget.NEAR_ALLY) &&
|
||||
move.getPriority(attacker) > 0 &&
|
||||
!move.isMultiTarget()
|
||||
@ -897,10 +910,8 @@ export class MoveImmunityAbAttr extends PreDefendAbAttr {
|
||||
this.immuneCondition = immuneCondition;
|
||||
}
|
||||
|
||||
override canApply({ pokemon, opponent: attacker, move }: MoveImmunityAbAttrParams): boolean {
|
||||
// TODO: Investigate whether this method should be checking against `cancelled`, specifically
|
||||
// if not checking this results in multiple flyouts showing when multiple abilities block the move.
|
||||
return this.immuneCondition(pokemon, attacker, move);
|
||||
override canApply({ pokemon, opponent: attacker, move, cancelled }: MoveImmunityAbAttrParams): boolean {
|
||||
return !cancelled.value && this.immuneCondition(pokemon, attacker, move);
|
||||
}
|
||||
|
||||
override apply({ cancelled }: MoveImmunityAbAttrParams): void {
|
||||
@ -970,6 +981,8 @@ export class MoveImmunityStatStageChangeAbAttr extends MoveImmunityAbAttr {
|
||||
export interface PostMoveInteractionAbAttrParams extends AugmentMoveInteractionAbAttrParams {
|
||||
/** Stores the hit result of the move used in the interaction */
|
||||
readonly hitResult: HitResult;
|
||||
/** The amount of damage dealt in the interaction */
|
||||
readonly damage: number;
|
||||
}
|
||||
|
||||
export class PostDefendAbAttr extends AbAttr {
|
||||
@ -1079,20 +1092,16 @@ export class PostDefendHpGatedStatStageChangeAbAttr extends PostDefendAbAttr {
|
||||
this.selfTarget = selfTarget;
|
||||
}
|
||||
|
||||
override canApply({ pokemon, opponent: attacker, move }: PostMoveInteractionAbAttrParams): boolean {
|
||||
override canApply({ pokemon, opponent: attacker, move, damage }: PostMoveInteractionAbAttrParams): boolean {
|
||||
const hpGateFlat: number = Math.ceil(pokemon.getMaxHp() * this.hpGate);
|
||||
const lastAttackReceived = pokemon.turnData.attacksReceived[pokemon.turnData.attacksReceived.length - 1];
|
||||
const damageReceived = lastAttackReceived?.damage || 0;
|
||||
return (
|
||||
this.condition(pokemon, attacker, move) && pokemon.hp <= hpGateFlat && pokemon.hp + damageReceived > hpGateFlat
|
||||
);
|
||||
return this.condition(pokemon, attacker, move) && pokemon.hp <= hpGateFlat && pokemon.hp + damage > hpGateFlat;
|
||||
}
|
||||
|
||||
override apply({ simulated, pokemon, opponent: attacker }: PostMoveInteractionAbAttrParams): void {
|
||||
override apply({ simulated, pokemon, opponent }: PostMoveInteractionAbAttrParams): void {
|
||||
if (!simulated) {
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"StatStageChangePhase",
|
||||
(this.selfTarget ? pokemon : attacker).getBattlerIndex(),
|
||||
(this.selfTarget ? pokemon : opponent).getBattlerIndex(),
|
||||
true,
|
||||
this.stats,
|
||||
this.stages,
|
||||
@ -1263,17 +1272,17 @@ export class PostDefendContactApplyTagChanceAbAttr extends PostDefendAbAttr {
|
||||
this.turnCount = turnCount;
|
||||
}
|
||||
|
||||
override canApply({ move, pokemon, opponent: attacker }: PostMoveInteractionAbAttrParams): boolean {
|
||||
override canApply({ move, pokemon, opponent }: PostMoveInteractionAbAttrParams): boolean {
|
||||
return (
|
||||
move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon }) &&
|
||||
move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: opponent, target: pokemon }) &&
|
||||
pokemon.randBattleSeedInt(100) < this.chance &&
|
||||
attacker.canAddTag(this.tagType)
|
||||
opponent.canAddTag(this.tagType)
|
||||
);
|
||||
}
|
||||
|
||||
override apply({ simulated, opponent: attacker, move }: PostMoveInteractionAbAttrParams): void {
|
||||
override apply({ pokemon, simulated, opponent, move }: PostMoveInteractionAbAttrParams): void {
|
||||
if (!simulated) {
|
||||
attacker.addTag(this.tagType, this.turnCount, move.id, attacker.id);
|
||||
opponent.addTag(this.tagType, this.turnCount, move.id, pokemon.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1593,12 +1602,7 @@ export interface FieldPreventExplosiveMovesAbAttrParams extends AbAttrBaseParams
|
||||
cancelled: BooleanHolder;
|
||||
}
|
||||
|
||||
export class FieldPreventExplosiveMovesAbAttr extends AbAttr {
|
||||
// TODO: investigate whether we need to check against `cancelled` in a `canApply` method
|
||||
override apply({ cancelled }: FieldPreventExplosiveMovesAbAttrParams): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
}
|
||||
export class FieldPreventExplosiveMovesAbAttr extends CancelInteractionAbAttr {}
|
||||
|
||||
export interface FieldMultiplyStatAbAttrParams extends AbAttrBaseParams {
|
||||
/** The kind of stat that is being checked for modification */
|
||||
@ -2537,15 +2541,11 @@ export class IgnoreOpponentStatStagesAbAttr extends AbAttr {
|
||||
* Abilities with this attribute prevent the user from being affected by Intimidate.
|
||||
* @sealed
|
||||
*/
|
||||
export class IntimidateImmunityAbAttr extends AbAttr {
|
||||
export class IntimidateImmunityAbAttr extends CancelInteractionAbAttr {
|
||||
constructor() {
|
||||
super(false);
|
||||
}
|
||||
|
||||
override apply({ cancelled }: AbAttrParamsWithCancel): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
|
||||
getTriggerMessage({ pokemon }: AbAttrParamsWithCancel, abilityName: string, ..._args: any[]): string {
|
||||
return i18next.t("abilityTriggers:intimidateImmunity", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
@ -3014,41 +3014,44 @@ export class PostSummonFormChangeAbAttr extends PostSummonAbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/** Attempts to copy a pokemon's ability */
|
||||
/**
|
||||
* Attempts to copy a pokemon's ability
|
||||
*
|
||||
* @remarks
|
||||
* Hardcodes idiosyncrasies specific to trace, so should not be used for other abilities
|
||||
* that might copy abilities in the future
|
||||
* @sealed
|
||||
*/
|
||||
export class PostSummonCopyAbilityAbAttr extends PostSummonAbAttr {
|
||||
private target: Pokemon;
|
||||
private targetAbilityName: string;
|
||||
|
||||
override canApply({ pokemon }: AbAttrBaseParams): boolean {
|
||||
const targets = pokemon.getOpponents();
|
||||
override canApply({ pokemon, simulated }: AbAttrBaseParams): boolean {
|
||||
const targets = pokemon
|
||||
.getOpponents()
|
||||
.filter(t => t.getAbility().isCopiable || t.getAbility().id === AbilityId.WONDER_GUARD);
|
||||
if (!targets.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let target: Pokemon;
|
||||
if (targets.length > 1) {
|
||||
globalScene.executeWithSeedOffset(() => (target = randSeedItem(targets)), globalScene.currentBattle.waveIndex);
|
||||
// simulated call always chooses first target so as to not advance RNG
|
||||
if (targets.length > 1 && !simulated) {
|
||||
target = targets[randSeedInt(targets.length)];
|
||||
} else {
|
||||
target = targets[0];
|
||||
}
|
||||
|
||||
if (
|
||||
!target!.getAbility().isCopiable &&
|
||||
// Wonder Guard is normally uncopiable so has the attribute, but Trace specifically can copy it
|
||||
!(pokemon.hasAbility(AbilityId.TRACE) && target!.getAbility().id === AbilityId.WONDER_GUARD)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.target = target!;
|
||||
this.targetAbilityName = allAbilities[target!.getAbility().id].name;
|
||||
this.target = target;
|
||||
this.targetAbilityName = allAbilities[target.getAbility().id].name;
|
||||
return true;
|
||||
}
|
||||
|
||||
override apply({ pokemon, simulated }: AbAttrBaseParams): void {
|
||||
if (!simulated) {
|
||||
pokemon.setTempAbility(this.target!.getAbility());
|
||||
setAbilityRevealed(this.target!);
|
||||
// Protect against this somehow being called before canApply by ensuring target is defined
|
||||
if (!simulated && this.target) {
|
||||
pokemon.setTempAbility(this.target.getAbility());
|
||||
setAbilityRevealed(this.target);
|
||||
pokemon.updateInfo();
|
||||
}
|
||||
}
|
||||
@ -3576,8 +3579,8 @@ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr {
|
||||
this.protectedStat = protectedStat;
|
||||
}
|
||||
|
||||
override canApply({ stat }: PreStatStageChangeAbAttrParams): boolean {
|
||||
return isNullOrUndefined(this.protectedStat) || stat === this.protectedStat;
|
||||
override canApply({ stat, cancelled }: PreStatStageChangeAbAttrParams): boolean {
|
||||
return !cancelled.value && (isNullOrUndefined(this.protectedStat) || stat === this.protectedStat);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3668,8 +3671,11 @@ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
|
||||
this.immuneEffects = immuneEffects;
|
||||
}
|
||||
|
||||
override canApply({ effect }: PreSetStatusAbAttrParams): boolean {
|
||||
return (this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) || this.immuneEffects.includes(effect);
|
||||
override canApply({ effect, cancelled }: PreSetStatusAbAttrParams): boolean {
|
||||
return (
|
||||
!cancelled.value &&
|
||||
((this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) || this.immuneEffects.includes(effect))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3719,7 +3725,8 @@ export interface UserFieldStatusEffectImmunityAbAttrParams extends AbAttrBasePar
|
||||
/**
|
||||
* Provides immunity to status effects to the user's field.
|
||||
*/
|
||||
export class UserFieldStatusEffectImmunityAbAttr extends AbAttr {
|
||||
export class UserFieldStatusEffectImmunityAbAttr extends CancelInteractionAbAttr {
|
||||
private declare readonly _: never;
|
||||
protected immuneEffects: StatusEffect[];
|
||||
|
||||
/**
|
||||
@ -3739,12 +3746,8 @@ export class UserFieldStatusEffectImmunityAbAttr extends AbAttr {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the `cancelled` value to true, indicating that the status effect is prevented.
|
||||
*/
|
||||
override apply({ cancelled }: UserFieldStatusEffectImmunityAbAttrParams): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
// declare here to allow typescript to allow us to override `canApply` method without adjusting params
|
||||
declare apply: (params: UserFieldStatusEffectImmunityAbAttrParams) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3775,14 +3778,7 @@ export class ConditionalUserFieldStatusEffectImmunityAbAttr extends UserFieldSta
|
||||
* @returns Whether the ability can be applied to cancel the status effect.
|
||||
*/
|
||||
override canApply(params: UserFieldStatusEffectImmunityAbAttrParams): boolean {
|
||||
return this.condition(params.target, params.source) && super.canApply(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the `cancelled` value to true, indicating that the status effect is prevented.
|
||||
*/
|
||||
override apply({ cancelled }: UserFieldStatusEffectImmunityAbAttrParams): void {
|
||||
cancelled.value = true;
|
||||
return !params.cancelled.value && this.condition(params.target, params.source) && super.canApply(params);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4018,20 +4014,16 @@ export class ConditionalCritAbAttr extends AbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
export class BlockNonDirectDamageAbAttr extends AbAttr {
|
||||
export class BlockNonDirectDamageAbAttr extends CancelInteractionAbAttr {
|
||||
constructor() {
|
||||
super(false);
|
||||
}
|
||||
|
||||
override apply({ cancelled }: AbAttrParamsWithCancel): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This attribute will block any status damage that you put in the parameter.
|
||||
*/
|
||||
export class BlockStatusDamageAbAttr extends AbAttr {
|
||||
export class BlockStatusDamageAbAttr extends CancelInteractionAbAttr {
|
||||
private effects: StatusEffect[];
|
||||
|
||||
/**
|
||||
@ -4043,20 +4035,12 @@ export class BlockStatusDamageAbAttr extends AbAttr {
|
||||
this.effects = effects;
|
||||
}
|
||||
|
||||
override canApply({ pokemon }: AbAttrParamsWithCancel): boolean {
|
||||
return !!pokemon.status?.effect && this.effects.includes(pokemon.status.effect);
|
||||
}
|
||||
|
||||
override apply({ cancelled }: AbAttrParamsWithCancel): void {
|
||||
cancelled.value = true;
|
||||
override canApply({ pokemon, cancelled }: AbAttrParamsWithCancel): boolean {
|
||||
return !cancelled.value && !!pokemon.status?.effect && this.effects.includes(pokemon.status.effect);
|
||||
}
|
||||
}
|
||||
|
||||
export class BlockOneHitKOAbAttr extends AbAttr {
|
||||
override apply({ cancelled }: AbAttrParamsWithCancel): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
}
|
||||
export class BlockOneHitKOAbAttr extends CancelInteractionAbAttr {}
|
||||
|
||||
export interface ChangeMovePriorityAbAttrParams extends AbAttrBaseParams {
|
||||
/** The move being used */
|
||||
@ -4130,8 +4114,8 @@ export class BlockWeatherDamageAttr extends PreWeatherDamageAbAttr {
|
||||
this.weatherTypes = weatherTypes;
|
||||
}
|
||||
|
||||
override canApply({ weather }: PreWeatherEffectAbAttrParams): boolean {
|
||||
if (!weather) {
|
||||
override canApply({ weather, cancelled }: PreWeatherEffectAbAttrParams): boolean {
|
||||
if (!weather || cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
const weatherType = weather.weatherType;
|
||||
@ -4152,8 +4136,8 @@ export class SuppressWeatherEffectAbAttr extends PreWeatherEffectAbAttr {
|
||||
this.affectsImmutable = affectsImmutable;
|
||||
}
|
||||
|
||||
override canApply({ weather }: PreWeatherEffectAbAttrParams): boolean {
|
||||
if (!weather) {
|
||||
override canApply({ weather, cancelled }: PreWeatherEffectAbAttrParams): boolean {
|
||||
if (!weather || cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
return this.affectsImmutable || weather.isImmutable();
|
||||
@ -5150,15 +5134,11 @@ export class StatStageChangeCopyAbAttr extends AbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
export class BypassBurnDamageReductionAbAttr extends AbAttr {
|
||||
export class BypassBurnDamageReductionAbAttr extends CancelInteractionAbAttr {
|
||||
private declare readonly _: never;
|
||||
constructor() {
|
||||
super(false);
|
||||
}
|
||||
|
||||
override apply({ cancelled }: AbAttrParamsWithCancel): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ReduceBurnDamageAbAttrParams extends AbAttrBaseParams {
|
||||
@ -5198,14 +5178,7 @@ export class DoubleBerryEffectAbAttr extends AbAttr {
|
||||
* Attribute to prevent opposing berry use while on the field.
|
||||
* Used by {@linkcode AbilityId.UNNERVE}, {@linkcode AbilityId.AS_ONE_GLASTRIER} and {@linkcode AbilityId.AS_ONE_SPECTRIER}
|
||||
*/
|
||||
export class PreventBerryUseAbAttr extends AbAttr {
|
||||
/**
|
||||
* Prevent use of opposing berries.
|
||||
*/
|
||||
override apply({ cancelled }: AbAttrParamsWithCancel): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
}
|
||||
export class PreventBerryUseAbAttr extends CancelInteractionAbAttr {}
|
||||
|
||||
/**
|
||||
* A Pokemon with this ability heals by a percentage of their maximum hp after eating a berry
|
||||
@ -5663,11 +5636,7 @@ export class IncreasePpAbAttr extends AbAttr {
|
||||
}
|
||||
|
||||
/** @sealed */
|
||||
export class ForceSwitchOutImmunityAbAttr extends AbAttr {
|
||||
override apply({ cancelled }: AbAttrParamsWithCancel): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
}
|
||||
export class ForceSwitchOutImmunityAbAttr extends CancelInteractionAbAttr {}
|
||||
|
||||
export interface ReduceBerryUseThresholdAbAttrParams extends AbAttrBaseParams {
|
||||
/** Holds the hp ratio for the berry to proc, which may be modified by ability application */
|
||||
@ -5746,8 +5715,8 @@ export class MoveAbilityBypassAbAttr extends AbAttr {
|
||||
this.moveIgnoreFunc = moveIgnoreFunc || ((_pokemon, _move) => true);
|
||||
}
|
||||
|
||||
override canApply({ pokemon, move }: MoveAbilityBypassAbAttrParams): boolean {
|
||||
return this.moveIgnoreFunc(pokemon, move);
|
||||
override canApply({ pokemon, move, cancelled }: MoveAbilityBypassAbAttrParams): boolean {
|
||||
return !cancelled.value && this.moveIgnoreFunc(pokemon, move);
|
||||
}
|
||||
|
||||
override apply({ cancelled }: MoveAbilityBypassAbAttrParams): void {
|
||||
@ -5841,8 +5810,8 @@ export class IgnoreTypeImmunityAbAttr extends AbAttr {
|
||||
this.allowedMoveTypes = allowedMoveTypes;
|
||||
}
|
||||
|
||||
override canApply({ moveType, defenderType }: IgnoreTypeImmunityAbAttrParams): boolean {
|
||||
return this.defenderType === defenderType && this.allowedMoveTypes.includes(moveType);
|
||||
override canApply({ moveType, defenderType, cancelled }: IgnoreTypeImmunityAbAttrParams): boolean {
|
||||
return !cancelled.value && this.defenderType === defenderType && this.allowedMoveTypes.includes(moveType);
|
||||
}
|
||||
|
||||
override apply({ cancelled }: IgnoreTypeImmunityAbAttrParams): void {
|
||||
|
@ -77,7 +77,8 @@ export enum EvolutionItem {
|
||||
LEADERS_CREST
|
||||
}
|
||||
|
||||
type TyrogueMove = MoveId.LOW_SWEEP | MoveId.MACH_PUNCH | MoveId.RAPID_SPIN;
|
||||
const tyrogueMoves = [MoveId.LOW_SWEEP, MoveId.MACH_PUNCH, MoveId.RAPID_SPIN] as const;
|
||||
type TyrogueMove = typeof tyrogueMoves[number];
|
||||
|
||||
/**
|
||||
* Pokemon Evolution tuple type consisting of:
|
||||
@ -192,7 +193,7 @@ export class SpeciesEvolutionCondition {
|
||||
case EvoCondKey.WEATHER:
|
||||
return cond.weather.includes(globalScene.arena.getWeatherType());
|
||||
case EvoCondKey.TYROGUE:
|
||||
return pokemon.getMoveset(true).find(m => m.moveId as TyrogueMove)?.moveId === cond.move;
|
||||
return pokemon.getMoveset(true).find(m => (tyrogueMoves as readonly MoveId[]) .includes(m.moveId))?.moveId === cond.move;
|
||||
case EvoCondKey.NATURE:
|
||||
return cond.nature.includes(pokemon.getNature());
|
||||
case EvoCondKey.RANDOM_FORM: {
|
||||
|
@ -68875,27 +68875,27 @@ interface TmPoolTiers {
|
||||
|
||||
export const tmPoolTiers: TmPoolTiers = {
|
||||
[MoveId.MEGA_PUNCH]: ModifierTier.GREAT,
|
||||
[MoveId.PAY_DAY]: ModifierTier.ULTRA,
|
||||
[MoveId.PAY_DAY]: ModifierTier.COMMON,
|
||||
[MoveId.FIRE_PUNCH]: ModifierTier.GREAT,
|
||||
[MoveId.ICE_PUNCH]: ModifierTier.GREAT,
|
||||
[MoveId.THUNDER_PUNCH]: ModifierTier.GREAT,
|
||||
[MoveId.SWORDS_DANCE]: ModifierTier.COMMON,
|
||||
[MoveId.SWORDS_DANCE]: ModifierTier.GREAT,
|
||||
[MoveId.CUT]: ModifierTier.COMMON,
|
||||
[MoveId.FLY]: ModifierTier.COMMON,
|
||||
[MoveId.FLY]: ModifierTier.GREAT,
|
||||
[MoveId.MEGA_KICK]: ModifierTier.GREAT,
|
||||
[MoveId.BODY_SLAM]: ModifierTier.GREAT,
|
||||
[MoveId.TAKE_DOWN]: ModifierTier.GREAT,
|
||||
[MoveId.DOUBLE_EDGE]: ModifierTier.ULTRA,
|
||||
[MoveId.PIN_MISSILE]: ModifierTier.COMMON,
|
||||
[MoveId.PIN_MISSILE]: ModifierTier.GREAT,
|
||||
[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.GREAT,
|
||||
[MoveId.PSYBEAM]: ModifierTier.COMMON,
|
||||
[MoveId.HYPER_BEAM]: ModifierTier.ULTRA,
|
||||
[MoveId.LOW_KICK]: ModifierTier.COMMON,
|
||||
[MoveId.LOW_KICK]: ModifierTier.GREAT,
|
||||
[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.COMMON,
|
||||
[MoveId.AGILITY]: ModifierTier.GREAT,
|
||||
[MoveId.NIGHT_SHADE]: ModifierTier.COMMON,
|
||||
[MoveId.SCREECH]: ModifierTier.COMMON,
|
||||
[MoveId.SCREECH]: ModifierTier.GREAT,
|
||||
[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.COMMON,
|
||||
[MoveId.AMNESIA]: ModifierTier.GREAT,
|
||||
[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.COMMON,
|
||||
[MoveId.CURSE]: ModifierTier.GREAT,
|
||||
[MoveId.REVERSAL]: ModifierTier.COMMON,
|
||||
[MoveId.SPITE]: ModifierTier.COMMON,
|
||||
[MoveId.PROTECT]: ModifierTier.COMMON,
|
||||
[MoveId.SCARY_FACE]: ModifierTier.COMMON,
|
||||
[MoveId.SCARY_FACE]: ModifierTier.GREAT,
|
||||
[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.COMMON,
|
||||
[MoveId.NATURE_POWER]: ModifierTier.COMMON,
|
||||
[MoveId.FOCUS_PUNCH]: ModifierTier.GREAT,
|
||||
[MoveId.NATURE_POWER]: ModifierTier.GREAT,
|
||||
[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.COMMON,
|
||||
[MoveId.SECRET_POWER]: ModifierTier.GREAT,
|
||||
[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.COMMON,
|
||||
[MoveId.FAKE_TEARS]: ModifierTier.GREAT,
|
||||
[MoveId.AIR_CUTTER]: ModifierTier.GREAT,
|
||||
[MoveId.OVERHEAT]: ModifierTier.ULTRA,
|
||||
[MoveId.ROCK_TOMB]: ModifierTier.GREAT,
|
||||
[MoveId.METAL_SOUND]: ModifierTier.COMMON,
|
||||
[MoveId.COSMIC_POWER]: ModifierTier.COMMON,
|
||||
[MoveId.METAL_SOUND]: ModifierTier.GREAT,
|
||||
[MoveId.COSMIC_POWER]: ModifierTier.GREAT,
|
||||
[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.COMMON,
|
||||
[MoveId.BULK_UP]: ModifierTier.GREAT,
|
||||
[MoveId.BOUNCE]: ModifierTier.GREAT,
|
||||
[MoveId.MUD_SHOT]: ModifierTier.GREAT,
|
||||
[MoveId.POISON_TAIL]: ModifierTier.GREAT,
|
||||
[MoveId.POISON_TAIL]: ModifierTier.COMMON,
|
||||
[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.COMMON,
|
||||
[MoveId.ROCK_POLISH]: ModifierTier.GREAT,
|
||||
[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.GREAT,
|
||||
[MoveId.NASTY_PLOT]: ModifierTier.COMMON,
|
||||
[MoveId.GIGA_IMPACT]: ModifierTier.ULTRA,
|
||||
[MoveId.NASTY_PLOT]: ModifierTier.GREAT,
|
||||
[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.ULTRA,
|
||||
[MoveId.GRASS_KNOT]: ModifierTier.GREAT,
|
||||
[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.COMMON,
|
||||
[MoveId.STORED_POWER]: ModifierTier.GREAT,
|
||||
[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.GREAT,
|
||||
[MoveId.DISARMING_VOICE]: ModifierTier.COMMON,
|
||||
[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.COMMON,
|
||||
[MoveId.STEEL_ROLLER]: ModifierTier.GREAT,
|
||||
[MoveId.SCALE_SHOT]: ModifierTier.ULTRA,
|
||||
[MoveId.METEOR_BEAM]: ModifierTier.GREAT,
|
||||
[MoveId.MISTY_EXPLOSION]: ModifierTier.COMMON,
|
||||
[MoveId.MISTY_EXPLOSION]: ModifierTier.GREAT,
|
||||
[MoveId.GRASSY_GLIDE]: ModifierTier.COMMON,
|
||||
[MoveId.RISING_VOLTAGE]: ModifierTier.COMMON,
|
||||
[MoveId.RISING_VOLTAGE]: ModifierTier.GREAT,
|
||||
[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.COMMON,
|
||||
[MoveId.DUAL_WINGBEAT]: ModifierTier.COMMON,
|
||||
[MoveId.TRIPLE_AXEL]: ModifierTier.ULTRA,
|
||||
[MoveId.DUAL_WINGBEAT]: ModifierTier.GREAT,
|
||||
[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.COMMON,
|
||||
[MoveId.TRAILBLAZE]: ModifierTier.GREAT,
|
||||
[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.GREAT,
|
||||
[MoveId.SUPERCELL_SLAM]: ModifierTier.ULTRA,
|
||||
[MoveId.PSYCHIC_NOISE]: ModifierTier.GREAT,
|
||||
[MoveId.UPPER_HAND]: ModifierTier.COMMON,
|
||||
};
|
||||
|
@ -291,9 +291,17 @@ class AnimTimedSoundEvent extends AnimTimedEvent {
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
return Math.ceil((globalScene.sound.get(`battle_anims/${this.resourceName}`).totalDuration * 1000) / 33.33);
|
||||
const sound = globalScene.sound.get(`battle_anims/${this.resourceName}`);
|
||||
if (!sound) {
|
||||
return 0;
|
||||
}
|
||||
return Math.ceil((sound.totalDuration * 1000) / 33.33);
|
||||
}
|
||||
return Math.ceil((battleAnim.user!.cry(soundConfig).totalDuration * 1000) / 33.33); // TODO: is the bang behind user correct?
|
||||
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);
|
||||
}
|
||||
|
||||
getEventType(): string {
|
||||
@ -827,7 +835,7 @@ export abstract class BattleAnim {
|
||||
// biome-ignore lint/complexity/noBannedTypes: callback is used liberally
|
||||
play(onSubstitute?: boolean, callback?: Function) {
|
||||
const isOppAnim = this.isOppAnim();
|
||||
const user = !isOppAnim ? this.user! : this.target!; // TODO: are those bangs correct?
|
||||
const user = !isOppAnim ? this.user! : this.target!; // TODO: These bangs are LITERALLY not correct at all
|
||||
const target = !isOppAnim ? this.target! : this.user!;
|
||||
|
||||
if (!target?.isOnField() && !this.playRegardlessOfIssues) {
|
||||
|
@ -1058,8 +1058,7 @@ export class SeedTag extends SerializableBattlerTag {
|
||||
// Check which opponent to restore HP to
|
||||
const source = pokemon.getOpponents().find(o => o.getBattlerIndex() === this.sourceIndex);
|
||||
if (!source) {
|
||||
console.warn(`Failed to get source Pokemon for SeedTag lapse; id: ${this.sourceId}`);
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const cancelled = new BooleanHolder(false);
|
||||
|
@ -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, toSnakeCase } from "#utils/strings";
|
||||
import { toCamelCase } 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 toSnakeCase(PokemonType[overrideValue - 1]);
|
||||
return PokemonType[overrideValue - 1].toLowerCase();
|
||||
}
|
||||
|
||||
getDescription(overrideValue: number = this.value): string {
|
||||
|
@ -6856,12 +6856,15 @@ 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;
|
||||
}
|
||||
|
||||
@ -6869,7 +6872,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.${PokemonType[this.type]}`) }));
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:transformedIntoType", { pokemonName: getPokemonNameWithAffix(target), typeName: i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[this.type])}`) }));
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -8137,9 +8140,12 @@ const failIfSingleBattle: MoveConditionFunc = (user, target, move) => globalScen
|
||||
|
||||
const failIfDampCondition: MoveConditionFunc = (user, target, move) => {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
globalScene.getField(true).map(p=>applyAbAttrs("FieldPreventExplosiveMovesAbAttr", {pokemon: p, cancelled}));
|
||||
// temporary workaround to prevent displaying the message during enemy command phase
|
||||
// TODO: either move this, or make the move condition func have a `simulated` param
|
||||
const simulated = globalScene.phaseManager.getCurrentPhase()?.is('EnemyCommandPhase');
|
||||
globalScene.getField(true).map(p=>applyAbAttrs("FieldPreventExplosiveMovesAbAttr", {pokemon: p, cancelled, simulated}));
|
||||
// Queue a message if an ability prevented usage of the move
|
||||
if (cancelled.value) {
|
||||
if (!simulated && cancelled.value) {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:cannotUseMove", { pokemonName: getPokemonNameWithAffix(user), moveName: move.name }));
|
||||
}
|
||||
return !cancelled.value;
|
||||
@ -8161,6 +8167,9 @@ const failIfGhostTypeCondition: MoveConditionFunc = (user: Pokemon, target: Poke
|
||||
const failIfNoTargetHeldItemsCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.getHeldItems().filter(i => i.isTransferable)?.length > 0;
|
||||
|
||||
const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
||||
if (isNullOrUndefined(target)) { // Fix bug when used against targets that have both fainted
|
||||
return "";
|
||||
}
|
||||
const heldItems = target.getHeldItems().filter(i => i.isTransferable);
|
||||
if (heldItems.length === 0) {
|
||||
return "";
|
||||
|
@ -2,6 +2,7 @@ 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";
|
||||
@ -10,8 +11,9 @@ 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 { PokemonType } from "#enums/pokemon-type";
|
||||
import { MAX_POKEMON_TYPE, 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";
|
||||
@ -219,6 +221,7 @@ 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,
|
||||
@ -230,7 +233,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
|
||||
],
|
||||
fillRemaining: false,
|
||||
});
|
||||
leaveEncounterWithoutBattle(true);
|
||||
leaveEncounterWithoutBattle(false);
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
@ -431,6 +434,8 @@ function getTeamTransformations(): PokemonTransformation[] {
|
||||
newAbilityIndex,
|
||||
undefined,
|
||||
);
|
||||
|
||||
transformation.newPokemon.teraType = randSeedInt(MAX_POKEMON_TYPE);
|
||||
}
|
||||
|
||||
return pokemonTransformations;
|
||||
@ -440,6 +445,8 @@ 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();
|
||||
|
||||
@ -462,6 +469,19 @@ 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();
|
||||
}
|
||||
|
||||
|
@ -44,6 +44,34 @@ export abstract class PhasePriorityQueue {
|
||||
public clear(): void {
|
||||
this.queue.splice(0, this.queue.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to remove one or more Phases from the current queue.
|
||||
* @param phaseFilter - The function to select phases for removal
|
||||
* @param removeCount - The maximum number of phases to remove, or `all` to remove all matching phases;
|
||||
* default `1`
|
||||
* @returns The number of successfully removed phases
|
||||
* @todo Remove this eventually once the patchwork bug this is used for is fixed
|
||||
*/
|
||||
public tryRemovePhase(phaseFilter: (phase: Phase) => boolean, removeCount: number | "all" = 1): number {
|
||||
if (removeCount === "all") {
|
||||
removeCount = this.queue.length;
|
||||
} else if (removeCount < 1) {
|
||||
return 0;
|
||||
}
|
||||
let numRemoved = 0;
|
||||
|
||||
do {
|
||||
const phaseIndex = this.queue.findIndex(phaseFilter);
|
||||
if (phaseIndex === -1) {
|
||||
break;
|
||||
}
|
||||
this.queue.splice(phaseIndex, 1);
|
||||
numRemoved++;
|
||||
} while (numRemoved < removeCount && this.queue.length > 0);
|
||||
|
||||
return numRemoved;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -593,14 +593,14 @@ export abstract class PokemonSpeciesForm {
|
||||
});
|
||||
}
|
||||
|
||||
cry(soundConfig?: Phaser.Types.Sound.SoundConfig, ignorePlay?: boolean): AnySound {
|
||||
cry(soundConfig?: Phaser.Types.Sound.SoundConfig, ignorePlay?: boolean): AnySound | null {
|
||||
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 (ignorePlay) {
|
||||
if (cry && ignorePlay) {
|
||||
cry.stop();
|
||||
}
|
||||
return cry;
|
||||
|
@ -11,6 +11,7 @@ import type { MoveId } from "#enums/move-id";
|
||||
import type { Nature } from "#enums/nature";
|
||||
import type { PokemonType } from "#enums/pokemon-type";
|
||||
import type { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import type { AttackMoveResult } from "#types/attack-move-result";
|
||||
import type { IllusionData } from "#types/illusion-data";
|
||||
import type { TurnMove } from "#types/turn-move";
|
||||
@ -326,6 +327,14 @@ export class PokemonTurnData {
|
||||
public switchedInThisTurn = false;
|
||||
public failedRunAway = false;
|
||||
public joinedRound = false;
|
||||
/** Tracker for a pending status effect
|
||||
*
|
||||
* @remarks
|
||||
* Set whenever {@linkcode Pokemon#trySetStatus} succeeds in order to prevent subsequent status effects
|
||||
* from being applied. Necessary because the status is not actually set until the {@linkcode ObtainStatusEffectPhase} runs,
|
||||
* which may not happen before another status effect is attempted to be applied.
|
||||
*/
|
||||
public pendingStatus: StatusEffect = StatusEffect.NONE;
|
||||
/**
|
||||
* The amount of times this Pokemon has acted again and used a move in the current turn.
|
||||
* Used to make sure multi-hits occur properly when the user is
|
||||
|
@ -126,7 +126,9 @@ export class DelayedAttackTag extends PositionalTag implements DelayedAttackArgs
|
||||
// Silently disappear if either source or target are missing or happen to be the same pokemon
|
||||
// (i.e. targeting oneself)
|
||||
// We also need to check for fainted targets as they don't technically leave the field until _after_ the turn ends
|
||||
return !!source && !!target && source !== target && !target.isFainted();
|
||||
// TODO: Figure out a way to store the target's offensive stat if they faint to allow pending attacks to persist
|
||||
// TODO: Remove the `?.scene` checks once battle anims are cleaned up - needed to avoid catch+release crash
|
||||
return !!source?.scene && !!target?.scene && source !== target && !target.isFainted();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,3 +20,6 @@ export enum PokemonType {
|
||||
FAIRY,
|
||||
STELLAR
|
||||
}
|
||||
|
||||
/** The largest legal value for a {@linkcode PokemonType} (includes Stellar) */
|
||||
export const MAX_POKEMON_TYPE = PokemonType.STELLAR;
|
@ -2234,8 +2234,16 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return this.hasPassive() && (!canApply || this.canApplyAbility(true)) && this.getPassiveAbility().hasAttr(attrType);
|
||||
}
|
||||
|
||||
public getAbilityPriorities(): [number, number] {
|
||||
return [this.getAbility().postSummonPriority, this.getPassiveAbility().postSummonPriority];
|
||||
/**
|
||||
* Return the ability priorities of the pokemon's ability and, if enabled, its passive ability
|
||||
* @returns A tuple containing the ability priorities of the pokemon
|
||||
*/
|
||||
public getAbilityPriorities(): [number] | [activePriority: number, passivePriority: number] {
|
||||
const abilityPriority = this.getAbility().postSummonPriority;
|
||||
if (this.hasPassive()) {
|
||||
return [abilityPriority, this.getPassiveAbility().postSummonPriority];
|
||||
}
|
||||
return [abilityPriority];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3235,6 +3243,18 @@ 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) {
|
||||
@ -4527,28 +4547,36 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
});
|
||||
}
|
||||
|
||||
cry(soundConfig?: Phaser.Types.Sound.SoundConfig, sceneOverride?: BattleScene): AnySound {
|
||||
cry(soundConfig?: Phaser.Types.Sound.SoundConfig, sceneOverride?: BattleScene): AnySound | null {
|
||||
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)) {
|
||||
let fusionCry = this.getFusionSpeciesForm(undefined, true).cry(soundConfig, true);
|
||||
const fusionCry = this.getFusionSpeciesForm(undefined, true).cry(soundConfig, true);
|
||||
if (!fusionCry) {
|
||||
return cry;
|
||||
}
|
||||
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)));
|
||||
fusionCry = this.getFusionSpeciesForm(undefined, true).cry({
|
||||
const fusionCryInner = this.getFusionSpeciesForm(undefined, true).cry({
|
||||
seek: Math.max(fusionCry.totalDuration * 0.4, 0),
|
||||
...soundConfig,
|
||||
});
|
||||
SoundFade.fadeIn(
|
||||
scene,
|
||||
fusionCry,
|
||||
fixedInt(Math.ceil(duration * 0.2)),
|
||||
scene.masterVolume * scene.fieldVolume,
|
||||
0,
|
||||
);
|
||||
if (fusionCryInner) {
|
||||
SoundFade.fadeIn(
|
||||
scene,
|
||||
fusionCryInner,
|
||||
fixedInt(Math.ceil(duration * 0.2)),
|
||||
scene.masterVolume * scene.fieldVolume,
|
||||
0,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
@ -4576,14 +4604,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
crySoundConfig.rate = 0.7;
|
||||
}
|
||||
}
|
||||
const cry = globalScene.playSound(key, crySoundConfig) as AnySound;
|
||||
const cry = globalScene.playSound(key, crySoundConfig);
|
||||
if (!cry || globalScene.fieldVolume === 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
const sprite = this.getSprite();
|
||||
const tintSprite = this.getTintSprite();
|
||||
const delay = Math.max(globalScene.sound.get(key).totalDuration * 50, 25);
|
||||
const delay = Math.max(cry.totalDuration * 50, 25);
|
||||
|
||||
let frameProgress = 0;
|
||||
let frameThreshold: number;
|
||||
@ -4636,20 +4664,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 }) as AnySound;
|
||||
const cry = globalScene.playSound(key, { rate: rate });
|
||||
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);
|
||||
@ -4692,16 +4720,20 @@ 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, {
|
||||
seek: Math.max(fusionCry.totalDuration * 0.4, 0),
|
||||
// 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),
|
||||
rate: rate,
|
||||
});
|
||||
SoundFade.fadeIn(
|
||||
globalScene,
|
||||
fusionCry,
|
||||
fixedInt(Math.ceil((duration / rate) * 0.2)),
|
||||
globalScene.masterVolume * globalScene.fieldVolume,
|
||||
0,
|
||||
);
|
||||
if (fusionCry) {
|
||||
SoundFade.fadeIn(
|
||||
globalScene,
|
||||
fusionCry,
|
||||
fixedInt(Math.ceil((duration / rate) * 0.2)),
|
||||
globalScene.masterVolume * globalScene.fieldVolume,
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
rate *= 0.99;
|
||||
if (cry && !cry.pendingRemove) {
|
||||
@ -4803,7 +4835,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
if (effect !== StatusEffect.FAINT) {
|
||||
// Status-overriding moves (i.e. Rest) fail if their respective status already exists;
|
||||
// all other moves fail if the target already has _any_ status
|
||||
if (overrideStatus ? this.status?.effect === effect : this.status) {
|
||||
if (overrideStatus ? this.status?.effect === effect : this.status || this.turnData.pendingStatus) {
|
||||
this.queueStatusImmuneMessage(quiet, overrideStatus ? "overlap" : "other"); // having different status displays generic fail message
|
||||
return false;
|
||||
}
|
||||
@ -4955,6 +4987,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
if (overrideStatus) {
|
||||
this.resetStatus(false);
|
||||
} else {
|
||||
this.turnData.pendingStatus = effect;
|
||||
}
|
||||
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
@ -4974,6 +5008,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect.
|
||||
* @param effect - The {@linkcode StatusEffect} to set
|
||||
* @remarks
|
||||
* Clears this pokemon's `pendingStatus` in its {@linkcode Pokemon.turnData | turnData}.
|
||||
*
|
||||
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||
*/
|
||||
doSetStatus(effect: Exclude<StatusEffect, StatusEffect.SLEEP>): void;
|
||||
@ -4982,6 +5018,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* @param effect - {@linkcode StatusEffect.SLEEP}
|
||||
* @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4
|
||||
* @remarks
|
||||
* Clears this pokemon's `pendingStatus` in its {@linkcode Pokemon#turnData}.
|
||||
*
|
||||
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||
*/
|
||||
doSetStatus(effect: StatusEffect.SLEEP, sleepTurnsRemaining?: number): void;
|
||||
@ -4991,6 +5029,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4
|
||||
* and is unused for all non-sleep Statuses
|
||||
* @remarks
|
||||
* Clears this pokemon's `pendingStatus` in its {@linkcode Pokemon#turnData}.
|
||||
*
|
||||
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||
*/
|
||||
doSetStatus(effect: StatusEffect, sleepTurnsRemaining?: number): void;
|
||||
@ -5000,6 +5040,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4
|
||||
* and is unused for all non-sleep Statuses
|
||||
* @remarks
|
||||
* Clears this pokemon's `pendingStatus` in its {@linkcode Pokemon#turnData}.
|
||||
*
|
||||
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||
* @todo Make this and all related fields private and change tests to use a field-based helper or similar
|
||||
*/
|
||||
@ -5007,6 +5049,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
effect: StatusEffect,
|
||||
sleepTurnsRemaining = effect !== StatusEffect.SLEEP ? 0 : this.randBattleSeedIntRange(2, 4),
|
||||
): void {
|
||||
// Reset any pending status
|
||||
this.turnData.pendingStatus = StatusEffect.NONE;
|
||||
switch (effect) {
|
||||
case StatusEffect.POISON:
|
||||
case StatusEffect.TOXIC:
|
||||
@ -6546,6 +6590,7 @@ export class EnemyPokemon extends Pokemon {
|
||||
ignoreAllyAbility: !p.getAlly()?.waveData.abilityRevealed,
|
||||
ignoreSourceAllyAbility: false,
|
||||
isCritical,
|
||||
simulated: true,
|
||||
}).damage >= p.hp
|
||||
);
|
||||
})
|
||||
@ -6884,7 +6929,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;
|
||||
let boostedStat: EffectiveStat | undefined;
|
||||
const statThresholds: number[] = [];
|
||||
let totalWeight = 0;
|
||||
|
||||
@ -6902,6 +6947,11 @@ 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
|
||||
@ -6917,7 +6967,7 @@ export class EnemyPokemon extends Pokemon {
|
||||
"StatStageChangePhase",
|
||||
this.getBattlerIndex(),
|
||||
true,
|
||||
[boostedStat!],
|
||||
[boostedStat],
|
||||
stages,
|
||||
true,
|
||||
true,
|
||||
|
@ -355,14 +355,23 @@ export class PhaseManager {
|
||||
if (this.phaseQueuePrependSpliceIndex > -1) {
|
||||
this.clearPhaseQueueSplice();
|
||||
}
|
||||
if (this.phaseQueuePrepend.length) {
|
||||
while (this.phaseQueuePrepend.length) {
|
||||
const poppedPhase = this.phaseQueuePrepend.pop();
|
||||
if (poppedPhase) {
|
||||
this.phaseQueue.unshift(poppedPhase);
|
||||
}
|
||||
this.phaseQueue.unshift(...this.phaseQueuePrepend);
|
||||
this.phaseQueuePrepend.splice(0);
|
||||
|
||||
const unactivatedConditionalPhases: [() => boolean, Phase][] = [];
|
||||
// Check if there are any conditional phases queued
|
||||
for (const [condition, phase] of this.conditionalQueue) {
|
||||
// Evaluate the condition associated with the phase
|
||||
if (condition()) {
|
||||
// If the condition is met, add the phase to the phase queue
|
||||
this.pushPhase(phase);
|
||||
} else {
|
||||
// If the condition is not met, re-add the phase back to the end of the conditional queue
|
||||
unactivatedConditionalPhases.push([condition, phase]);
|
||||
}
|
||||
}
|
||||
this.conditionalQueue = unactivatedConditionalPhases;
|
||||
|
||||
if (!this.phaseQueue.length) {
|
||||
this.populatePhaseQueue();
|
||||
// Clear the conditionalQueue if there are no phases left in the phaseQueue
|
||||
@ -371,24 +380,6 @@ export class PhaseManager {
|
||||
|
||||
this.currentPhase = this.phaseQueue.shift() ?? null;
|
||||
|
||||
const unactivatedConditionalPhases: [() => boolean, Phase][] = [];
|
||||
// Check if there are any conditional phases queued
|
||||
while (this.conditionalQueue?.length) {
|
||||
// Retrieve the first conditional phase from the queue
|
||||
const conditionalPhase = this.conditionalQueue.shift();
|
||||
// Evaluate the condition associated with the phase
|
||||
if (conditionalPhase?.[0]()) {
|
||||
// If the condition is met, add the phase to the phase queue
|
||||
this.pushPhase(conditionalPhase[1]);
|
||||
} else if (conditionalPhase) {
|
||||
// If the condition is not met, re-add the phase back to the front of the conditional queue
|
||||
unactivatedConditionalPhases.push(conditionalPhase);
|
||||
} else {
|
||||
console.warn("condition phase is undefined/null!", conditionalPhase);
|
||||
}
|
||||
}
|
||||
this.conditionalQueue.push(...unactivatedConditionalPhases);
|
||||
|
||||
if (this.currentPhase) {
|
||||
console.log(`%cStart Phase ${this.currentPhase.constructor.name}`, "color:green;");
|
||||
this.currentPhase.start();
|
||||
@ -520,6 +511,25 @@ export class PhaseManager {
|
||||
this.dynamicPhaseQueues[type].push(phase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to remove one or more Phases from the given DynamicPhaseQueue, removing the equivalent amount of {@linkcode ActivatePriorityQueuePhase}s from the queue.
|
||||
* @param type - The {@linkcode DynamicPhaseType} to check
|
||||
* @param phaseFilter - The function to select phases for removal
|
||||
* @param removeCount - The maximum number of phases to remove, or `all` to remove all matching phases;
|
||||
* default `1`
|
||||
* @todo Remove this eventually once the patchwork bug this is used for is fixed
|
||||
*/
|
||||
public tryRemoveDynamicPhase(
|
||||
type: DynamicPhaseType,
|
||||
phaseFilter: (phase: Phase) => boolean,
|
||||
removeCount: number | "all" = 1,
|
||||
): void {
|
||||
const numRemoved = this.dynamicPhaseQueues[type].tryRemovePhase(phaseFilter, removeCount);
|
||||
for (let x = 0; x < numRemoved; x++) {
|
||||
this.tryRemovePhase(p => p.is("ActivatePriorityQueuePhase"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unshifts the top phase from the corresponding dynamic queue onto {@linkcode phaseQueue}
|
||||
* @param type {@linkcode DynamicPhaseType} The type of dynamic phase to start
|
||||
|
@ -456,7 +456,7 @@ export class CommandPhase extends FieldPhase {
|
||||
|
||||
const numBallTypes = 5;
|
||||
if (cursor < numBallTypes) {
|
||||
const targetPokemon = globalScene.getEnemyPokemon();
|
||||
const targetPokemon = globalScene.getEnemyPokemon(false);
|
||||
if (
|
||||
targetPokemon?.isBoss() &&
|
||||
targetPokemon?.bossSegmentIndex >= 1 &&
|
||||
|
@ -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;
|
||||
private evolutionBgm: AnySound | null;
|
||||
private eggLapsePhase: EggLapsePhase;
|
||||
|
||||
constructor(hatchScene: EggLapsePhase, egg: Egg, eggsToHatchCount: number) {
|
||||
@ -230,6 +230,7 @@ export class EggHatchPhase extends Phase {
|
||||
} else {
|
||||
globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true));
|
||||
}
|
||||
this.pokemon?.destroy();
|
||||
super.end();
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,10 @@ export class EggSummaryPhase extends Phase {
|
||||
}
|
||||
|
||||
end() {
|
||||
this.eggHatchData.forEach(data => {
|
||||
data.pokemon?.destroy();
|
||||
});
|
||||
this.eggHatchData = [];
|
||||
globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true));
|
||||
globalScene.ui.setModeForceTransition(UiMode.MESSAGE).then(() => {
|
||||
super.end();
|
||||
|
@ -28,9 +28,10 @@ export class EvolutionPhase extends Phase {
|
||||
|
||||
private evolution: SpeciesFormEvolution | null;
|
||||
private fusionSpeciesEvolved: boolean; // Whether the evolution is of the fused species
|
||||
private evolutionBgm: AnySound;
|
||||
private evolutionBgm: AnySound | null;
|
||||
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;
|
||||
@ -297,7 +298,9 @@ export class EvolutionPhase extends Phase {
|
||||
this.evolutionBg.setVisible(false);
|
||||
},
|
||||
});
|
||||
SoundFade.fadeOut(globalScene, this.evolutionBgm, 100);
|
||||
if (this.evolutionBgm) {
|
||||
SoundFade.fadeOut(globalScene, this.evolutionBgm, 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -377,7 +380,9 @@ export class EvolutionPhase extends Phase {
|
||||
* Fadeout evolution music, play the cry, show the evolution completed text, and end the phase
|
||||
*/
|
||||
private onEvolutionComplete(evolvedPokemon: Pokemon) {
|
||||
SoundFade.fadeOut(globalScene, this.evolutionBgm, 100);
|
||||
if (this.evolutionBgm) {
|
||||
SoundFade.fadeOut(globalScene, this.evolutionBgm, 100);
|
||||
}
|
||||
globalScene.time.delayedCall(250, () => {
|
||||
this.pokemon.cry();
|
||||
globalScene.time.delayedCall(1250, () => {
|
||||
@ -522,6 +527,7 @@ export class EvolutionPhase extends Phase {
|
||||
return;
|
||||
}
|
||||
if (i === lastCycle) {
|
||||
this.pokemonTintSprite.setVisible(false).setActive(false);
|
||||
this.pokemonEvoTintSprite.setScale(1);
|
||||
}
|
||||
},
|
||||
|
@ -204,7 +204,7 @@ export class GameOverPhase extends BattlePhase {
|
||||
}
|
||||
this.getRunHistoryEntry().then(runHistoryEntry => {
|
||||
globalScene.gameData.saveRunHistory(runHistoryEntry, this.isVictory);
|
||||
globalScene.phaseManager.pushNew("PostGameOverPhase", endCardPhase);
|
||||
globalScene.phaseManager.pushNew("PostGameOverPhase", globalScene.sessionSlotId, endCardPhase);
|
||||
this.end();
|
||||
});
|
||||
};
|
||||
|
@ -400,10 +400,17 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
|
||||
* @param hitResult - The {@linkcode HitResult} of the attempted move
|
||||
* @param damage - The amount of damage dealt to the target in the interaction
|
||||
* @param wasCritical - `true` if the move was a critical hit
|
||||
*/
|
||||
protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult, wasCritical = false): void {
|
||||
const params = { pokemon: target, opponent: user, move: this.move, hitResult };
|
||||
protected applyOnGetHitAbEffects(
|
||||
user: Pokemon,
|
||||
target: Pokemon,
|
||||
hitResult: HitResult,
|
||||
damage: number,
|
||||
wasCritical = false,
|
||||
): void {
|
||||
const params = { pokemon: target, opponent: user, move: this.move, hitResult, damage };
|
||||
applyAbAttrs("PostDefendAbAttr", params);
|
||||
|
||||
if (wasCritical) {
|
||||
@ -763,12 +770,12 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
|
||||
this.triggerMoveEffects(MoveEffectTrigger.PRE_APPLY, user, target);
|
||||
|
||||
const [hitResult, wasCritical] = this.applyMove(user, target, effectiveness);
|
||||
const [hitResult, wasCritical, dmg] = this.applyMove(user, target, effectiveness);
|
||||
|
||||
// Apply effects to the user (always) and the target (if not blocked by substitute).
|
||||
this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, true);
|
||||
if (!this.move.hitsSubstitute(user, target)) {
|
||||
this.applyOnTargetEffects(user, target, hitResult, firstTarget, wasCritical);
|
||||
this.applyOnTargetEffects(user, target, hitResult, firstTarget, dmg, wasCritical);
|
||||
}
|
||||
if (this.lastHit) {
|
||||
globalScene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
|
||||
@ -788,9 +795,13 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||
* @param target - The {@linkcode Pokemon} targeted by the move
|
||||
* @param effectiveness - The effectiveness of the move against the target
|
||||
* @returns The {@linkcode HitResult} of the move against the target and a boolean indicating whether the target was crit
|
||||
* @returns The {@linkcode HitResult} of the move against the target, a boolean indicating whether the target was crit, and the amount of damage dealt
|
||||
*/
|
||||
protected applyMoveDamage(user: Pokemon, target: Pokemon, effectiveness: TypeDamageMultiplier): [HitResult, boolean] {
|
||||
protected applyMoveDamage(
|
||||
user: Pokemon,
|
||||
target: Pokemon,
|
||||
effectiveness: TypeDamageMultiplier,
|
||||
): [result: HitResult, critical: boolean, damage: number] {
|
||||
const isCritical = target.getCriticalHitResult(user, this.move);
|
||||
|
||||
/*
|
||||
@ -821,7 +832,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
const isOneHitKo = result === HitResult.ONE_HIT_KO;
|
||||
|
||||
if (!dmg) {
|
||||
return [result, false];
|
||||
return [result, false, 0];
|
||||
}
|
||||
|
||||
target.lapseTags(BattlerTagLapseType.HIT);
|
||||
@ -850,7 +861,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
}
|
||||
|
||||
if (damage <= 0) {
|
||||
return [result, isCritical];
|
||||
return [result, isCritical, damage];
|
||||
}
|
||||
|
||||
if (user.isPlayer()) {
|
||||
@ -879,7 +890,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
globalScene.applyModifiers(DamageMoneyRewardModifier, true, user, new NumberHolder(damage));
|
||||
}
|
||||
|
||||
return [result, isCritical];
|
||||
return [result, isCritical, damage];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -932,12 +943,17 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||
* @param target - The {@linkcode Pokemon} struck by the move
|
||||
* @param effectiveness - The effectiveness of the move against the target
|
||||
* @returns The {@linkcode HitResult} of the move against the target, a boolean indicating whether the target was crit, and the amount of damage dealt
|
||||
*/
|
||||
protected applyMove(user: Pokemon, target: Pokemon, effectiveness: TypeDamageMultiplier): [HitResult, boolean] {
|
||||
protected applyMove(
|
||||
user: Pokemon,
|
||||
target: Pokemon,
|
||||
effectiveness: TypeDamageMultiplier,
|
||||
): [HitResult, critical: boolean, damage: number] {
|
||||
const moveCategory = user.getMoveCategory(target, this.move);
|
||||
|
||||
if (moveCategory === MoveCategory.STATUS) {
|
||||
return [HitResult.STATUS, false];
|
||||
return [HitResult.STATUS, false, 0];
|
||||
}
|
||||
|
||||
const result = this.applyMoveDamage(user, target, effectiveness);
|
||||
@ -960,6 +976,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* @param target - The {@linkcode Pokemon} targeted by the move
|
||||
* @param hitResult - The {@linkcode HitResult} obtained from applying the move
|
||||
* @param firstTarget - `true` if the target is the first Pokemon hit by the attack
|
||||
* @param damage - The amount of damage dealt to the target in the interaction
|
||||
* @param wasCritical - `true` if the move was a critical hit
|
||||
*/
|
||||
protected applyOnTargetEffects(
|
||||
@ -967,6 +984,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
target: Pokemon,
|
||||
hitResult: HitResult,
|
||||
firstTarget: boolean,
|
||||
damage: number,
|
||||
wasCritical = false,
|
||||
): void {
|
||||
/** Does {@linkcode hitResult} indicate that damage was dealt to the target? */
|
||||
@ -979,8 +997,8 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
|
||||
this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, false);
|
||||
this.applyHeldItemFlinchCheck(user, target, dealsDamage);
|
||||
this.applyOnGetHitAbEffects(user, target, hitResult, wasCritical);
|
||||
applyAbAttrs("PostAttackAbAttr", { pokemon: user, opponent: target, move: this.move, hitResult });
|
||||
this.applyOnGetHitAbEffects(user, target, hitResult, damage, wasCritical);
|
||||
applyAbAttrs("PostAttackAbAttr", { pokemon: user, opponent: target, move: this.move, hitResult, damage: damage });
|
||||
|
||||
// We assume only enemy Pokemon are able to have the EnemyAttackStatusEffectChanceModifier from tokens
|
||||
if (!user.isPlayer() && this.move.is("AttackMove")) {
|
||||
|
@ -24,6 +24,7 @@ import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||
import { frenzyMissFunc } from "#moves/move-utils";
|
||||
import type { PokemonMove } from "#moves/pokemon-move";
|
||||
import { BattlePhase } from "#phases/battle-phase";
|
||||
import type { TurnMove } from "#types/turn-move";
|
||||
import { NumberHolder } from "#utils/common";
|
||||
import { enumValueToKey } from "#utils/enums";
|
||||
import i18next from "i18next";
|
||||
@ -41,6 +42,13 @@ export class MovePhase extends BattlePhase {
|
||||
/** Whether the current move should fail and retain PP. */
|
||||
protected cancelled = false;
|
||||
|
||||
/** The move history entry object that is pushed to the pokemon's move history
|
||||
*
|
||||
* @remarks
|
||||
* Can be edited _after_ being pushed to the history to adjust the result, targets, etc, for this move phase.
|
||||
*/
|
||||
protected moveHistoryEntry: TurnMove;
|
||||
|
||||
public get pokemon(): Pokemon {
|
||||
return this._pokemon;
|
||||
}
|
||||
@ -82,6 +90,11 @@ export class MovePhase extends BattlePhase {
|
||||
this.move = move;
|
||||
this.useMode = useMode;
|
||||
this.forcedLast = forcedLast;
|
||||
this.moveHistoryEntry = {
|
||||
move: MoveId.NONE,
|
||||
targets,
|
||||
useMode,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -410,13 +423,9 @@ export class MovePhase extends BattlePhase {
|
||||
if (showText) {
|
||||
this.showMoveText();
|
||||
}
|
||||
|
||||
this.pokemon.pushMoveHistory({
|
||||
move: this.move.moveId,
|
||||
targets: this.targets,
|
||||
result: MoveResult.FAIL,
|
||||
useMode: this.useMode,
|
||||
});
|
||||
const moveHistoryEntry = this.moveHistoryEntry;
|
||||
moveHistoryEntry.result = MoveResult.FAIL;
|
||||
this.pokemon.pushMoveHistory(moveHistoryEntry);
|
||||
|
||||
// Use move-specific failure messages if present before checking terrain/weather blockage
|
||||
// and falling back to the classic "But it failed!".
|
||||
@ -630,12 +639,9 @@ export class MovePhase extends BattlePhase {
|
||||
frenzyMissFunc(this.pokemon, this.move.getMove());
|
||||
}
|
||||
|
||||
this.pokemon.pushMoveHistory({
|
||||
move: MoveId.NONE,
|
||||
result: MoveResult.FAIL,
|
||||
targets: this.targets,
|
||||
useMode: this.useMode,
|
||||
});
|
||||
const moveHistoryEntry = this.moveHistoryEntry;
|
||||
moveHistoryEntry.result = MoveResult.FAIL;
|
||||
this.pokemon.pushMoveHistory(moveHistoryEntry);
|
||||
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
|
||||
@ -649,13 +655,16 @@ export class MovePhase extends BattlePhase {
|
||||
* Displays the move's usage text to the player as applicable for the move being used.
|
||||
*/
|
||||
public showMoveText(): void {
|
||||
const moveId = this.move.moveId;
|
||||
if (
|
||||
this.move.moveId === MoveId.NONE ||
|
||||
moveId === MoveId.NONE ||
|
||||
this.pokemon.getTag(BattlerTagType.RECHARGING) ||
|
||||
this.pokemon.getTag(BattlerTagType.INTERRUPTED)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Showing move text always adjusts the move history entry's move id
|
||||
this.moveHistoryEntry.move = moveId;
|
||||
|
||||
// TODO: This should be done by the move...
|
||||
globalScene.phaseManager.queueMessage(
|
||||
@ -668,7 +677,7 @@ export class MovePhase extends BattlePhase {
|
||||
|
||||
// Moves with pre-use messages (Magnitude, Chilly Reception, Fickle Beam, etc.) always display their messages even on failure
|
||||
// TODO: This assumes single target for message funcs - is this sustainable?
|
||||
applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.pokemon.getOpponents(false)[0], this.move.getMove());
|
||||
applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.getActiveTargetPokemon()[0], this.move.getMove());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -38,13 +38,15 @@ export class PartyHealPhase extends BattlePhase {
|
||||
pokemon.updateInfo(true);
|
||||
}
|
||||
const healSong = globalScene.playSoundWithoutBgm("heal");
|
||||
globalScene.time.delayedCall(fixedInt(healSong.totalDuration * 1000), () => {
|
||||
healSong.destroy();
|
||||
if (this.resumeBgm && bgmPlaying) {
|
||||
globalScene.playBgm();
|
||||
}
|
||||
globalScene.ui.fadeIn(500).then(() => this.end());
|
||||
});
|
||||
if (healSong) {
|
||||
globalScene.time.delayedCall(fixedInt(healSong.totalDuration * 1000), () => {
|
||||
healSong.destroy();
|
||||
if (this.resumeBgm && bgmPlaying) {
|
||||
globalScene.playBgm();
|
||||
}
|
||||
globalScene.ui.fadeIn(500).then(() => this.end());
|
||||
});
|
||||
}
|
||||
});
|
||||
globalScene.arena.playerTerasUsed = 0;
|
||||
}
|
||||
|
@ -5,10 +5,11 @@ import type { EndCardPhase } from "#phases/end-card-phase";
|
||||
export class PostGameOverPhase extends Phase {
|
||||
public readonly phaseName = "PostGameOverPhase";
|
||||
private endCardPhase?: EndCardPhase;
|
||||
private slotId: number;
|
||||
|
||||
constructor(endCardPhase?: EndCardPhase) {
|
||||
constructor(slotId: number, endCardPhase?: EndCardPhase) {
|
||||
super();
|
||||
|
||||
this.slotId = slotId;
|
||||
this.endCardPhase = endCardPhase;
|
||||
}
|
||||
|
||||
@ -20,16 +21,14 @@ export class PostGameOverPhase extends Phase {
|
||||
if (!success) {
|
||||
return globalScene.reset(true);
|
||||
}
|
||||
globalScene.gameData
|
||||
.tryClearSession(globalScene.sessionSlotId)
|
||||
.then((success: boolean | [boolean, boolean]) => {
|
||||
if (!success[0]) {
|
||||
return globalScene.reset(true);
|
||||
}
|
||||
globalScene.reset();
|
||||
globalScene.phaseManager.unshiftNew("TitlePhase");
|
||||
this.end();
|
||||
});
|
||||
globalScene.gameData.tryClearSession(this.slotId).then((success: boolean | [boolean, boolean]) => {
|
||||
if (!success[0]) {
|
||||
return globalScene.reset(true);
|
||||
}
|
||||
globalScene.reset();
|
||||
globalScene.phaseManager.unshiftNew("TitlePhase");
|
||||
this.end();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { DynamicPhaseType } from "#enums/dynamic-phase-type";
|
||||
import { SwitchType } from "#enums/switch-type";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
import { BattlePhase } from "#phases/battle-phase";
|
||||
@ -75,8 +76,11 @@ export class SwitchPhase extends BattlePhase {
|
||||
if (slotIndex >= globalScene.currentBattle.getBattlerCount() && slotIndex < 6) {
|
||||
// Remove any pre-existing PostSummonPhase under the same field index.
|
||||
// Pre-existing PostSummonPhases may occur when this phase is invoked during a prompt to switch at the start of a wave.
|
||||
globalScene.phaseManager.tryRemovePhase(
|
||||
// TODO: Separate the animations from `SwitchSummonPhase` and co. into another phase and use that on initial switch - this is a band-aid fix
|
||||
globalScene.phaseManager.tryRemoveDynamicPhase(
|
||||
DynamicPhaseType.POST_SUMMON,
|
||||
p => p.is("PostSummonPhase") && p.player && p.fieldIndex === this.fieldIndex,
|
||||
"all",
|
||||
);
|
||||
const switchType = option === PartyOption.PASS_BATON ? SwitchType.BATON_PASS : this.switchType;
|
||||
globalScene.phaseManager.unshiftNew("SwitchSummonPhase", switchType, fieldIndex, slotIndex, this.doReturn);
|
||||
|
@ -177,6 +177,9 @@ export class TitlePhase extends Phase {
|
||||
.then((success: boolean) => {
|
||||
if (success) {
|
||||
this.loaded = true;
|
||||
if (loggedInUser) {
|
||||
loggedInUser.lastSessionSlot = slotId;
|
||||
}
|
||||
globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end());
|
||||
} else {
|
||||
this.end();
|
||||
|
@ -179,12 +179,11 @@ 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");
|
||||
|
||||
/*
|
||||
|
@ -82,6 +82,7 @@ export class PokerogueSessionSavedataApi extends ApiBase {
|
||||
try {
|
||||
const urlSearchParams = this.toUrlSearchParams(params);
|
||||
const response = await this.doGet(`/savedata/session/delete?${urlSearchParams}`);
|
||||
console.debug("%cSending a request to delete session in slot %d", "color: blue", params.slot);
|
||||
|
||||
if (response.ok) {
|
||||
return null;
|
||||
|
@ -68,6 +68,7 @@ import { executeIf, fixedInt, isLocal, NumberHolder, randInt, randSeedItem } fro
|
||||
import { decrypt, encrypt } from "#utils/data";
|
||||
import { getEnumKeys } from "#utils/enums";
|
||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||
import { isBeta } from "#utils/utility-vars";
|
||||
import { AES, enc } from "crypto-js";
|
||||
import i18next from "i18next";
|
||||
|
||||
@ -419,7 +420,15 @@ export class GameData {
|
||||
}
|
||||
}
|
||||
|
||||
console.debug(systemData);
|
||||
if (isLocal || isBeta) {
|
||||
try {
|
||||
console.debug(
|
||||
this.parseSystemData(JSON.stringify(systemData, (_, v: any) => (typeof v === "bigint" ? v.toString() : v))),
|
||||
);
|
||||
} catch (err) {
|
||||
console.debug("Attempt to log system data failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem(`data_${loggedInUser?.username}`, encrypt(systemDataStr, bypassLogin));
|
||||
|
||||
@ -945,45 +954,46 @@ export class GameData {
|
||||
} as SessionSaveData;
|
||||
}
|
||||
|
||||
getSession(slotId: number): Promise<SessionSaveData | null> {
|
||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (slotId < 0) {
|
||||
return resolve(null);
|
||||
async getSession(slotId: number): Promise<SessionSaveData | null> {
|
||||
const { promise, resolve, reject } = Promise.withResolvers<SessionSaveData | null>();
|
||||
if (slotId < 0) {
|
||||
resolve(null);
|
||||
return promise;
|
||||
}
|
||||
const handleSessionData = async (sessionDataStr: string) => {
|
||||
try {
|
||||
const sessionData = this.parseSessionData(sessionDataStr);
|
||||
resolve(sessionData);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
const handleSessionData = async (sessionDataStr: string) => {
|
||||
try {
|
||||
const sessionData = this.parseSessionData(sessionDataStr);
|
||||
resolve(sessionData);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`)) {
|
||||
pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId }).then(async response => {
|
||||
if (!response || response?.length === 0 || response?.[0] !== "{") {
|
||||
console.error(response);
|
||||
return resolve(null);
|
||||
}
|
||||
if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`)) {
|
||||
const response = await pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId });
|
||||
|
||||
localStorage.setItem(
|
||||
`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`,
|
||||
encrypt(response, bypassLogin),
|
||||
);
|
||||
|
||||
await handleSessionData(response);
|
||||
});
|
||||
} else {
|
||||
const sessionData = localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`);
|
||||
if (sessionData) {
|
||||
await handleSessionData(decrypt(sessionData, bypassLogin));
|
||||
} else {
|
||||
return resolve(null);
|
||||
}
|
||||
if (!response || response?.length === 0 || response?.[0] !== "{") {
|
||||
console.error(response);
|
||||
resolve(null);
|
||||
return promise;
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem(
|
||||
`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`,
|
||||
encrypt(response, bypassLogin),
|
||||
);
|
||||
|
||||
await handleSessionData(response);
|
||||
return promise;
|
||||
}
|
||||
const sessionData = localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`);
|
||||
if (sessionData) {
|
||||
await handleSessionData(decrypt(sessionData, bypassLogin));
|
||||
return promise;
|
||||
}
|
||||
resolve(null);
|
||||
return promise;
|
||||
}
|
||||
|
||||
async renameSession(slotId: number, newName: string): Promise<boolean> {
|
||||
@ -1028,166 +1038,177 @@ export class GameData {
|
||||
return !(success !== null && !success);
|
||||
}
|
||||
|
||||
loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> {
|
||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const initSessionFromData = async (sessionData: SessionSaveData) => {
|
||||
console.debug(sessionData);
|
||||
|
||||
globalScene.gameMode = getGameMode(sessionData.gameMode || GameModes.CLASSIC);
|
||||
if (sessionData.challenges) {
|
||||
globalScene.gameMode.challenges = sessionData.challenges.map(c => c.toChallenge());
|
||||
async loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> {
|
||||
const { promise, resolve, reject } = Promise.withResolvers<boolean>();
|
||||
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);
|
||||
}
|
||||
|
||||
globalScene.setSeed(sessionData.seed || globalScene.game.config.seed[0]);
|
||||
globalScene.resetSeed();
|
||||
|
||||
console.log("Seed:", globalScene.seed);
|
||||
|
||||
globalScene.sessionPlayTime = sessionData.playTime || 0;
|
||||
globalScene.lastSavePlayTime = 0;
|
||||
|
||||
const loadPokemonAssets: Promise<void>[] = [];
|
||||
|
||||
const party = globalScene.getPlayerParty();
|
||||
party.splice(0, party.length);
|
||||
|
||||
for (const p of sessionData.party) {
|
||||
const pokemon = p.toPokemon() as PlayerPokemon;
|
||||
pokemon.setVisible(false);
|
||||
loadPokemonAssets.push(pokemon.loadAssets(false));
|
||||
party.push(pokemon);
|
||||
}
|
||||
|
||||
Object.keys(globalScene.pokeballCounts).forEach((key: string) => {
|
||||
globalScene.pokeballCounts[key] = sessionData.pokeballCounts[key] || 0;
|
||||
});
|
||||
if (Overrides.POKEBALL_OVERRIDE.active) {
|
||||
globalScene.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs;
|
||||
}
|
||||
|
||||
globalScene.money = Math.floor(sessionData.money || 0);
|
||||
globalScene.updateMoneyText();
|
||||
|
||||
if (globalScene.money > this.gameStats.highestMoney) {
|
||||
this.gameStats.highestMoney = globalScene.money;
|
||||
}
|
||||
|
||||
globalScene.score = sessionData.score;
|
||||
globalScene.updateScoreText();
|
||||
|
||||
globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(sessionData.mysteryEncounterSaveData);
|
||||
|
||||
globalScene.newArena(sessionData.arena.biome, sessionData.playerFaints);
|
||||
|
||||
const battleType = sessionData.battleType || 0;
|
||||
const trainerConfig = sessionData.trainer ? trainerConfigs[sessionData.trainer.trainerType] : null;
|
||||
const mysteryEncounterType =
|
||||
sessionData.mysteryEncounterType !== -1 ? sessionData.mysteryEncounterType : undefined;
|
||||
const battle = globalScene.newBattle(
|
||||
sessionData.waveIndex,
|
||||
battleType,
|
||||
sessionData.trainer,
|
||||
battleType === BattleType.TRAINER
|
||||
? trainerConfig?.doubleOnly || sessionData.trainer?.variant === TrainerVariant.DOUBLE
|
||||
: sessionData.enemyParty.length > 1,
|
||||
mysteryEncounterType,
|
||||
);
|
||||
battle.enemyLevels = sessionData.enemyParty.map(p => p.level);
|
||||
|
||||
globalScene.arena.init();
|
||||
|
||||
sessionData.enemyParty.forEach((enemyData, e) => {
|
||||
const enemyPokemon = enemyData.toPokemon(
|
||||
battleType,
|
||||
e,
|
||||
sessionData.trainer?.variant === TrainerVariant.DOUBLE,
|
||||
) as EnemyPokemon;
|
||||
battle.enemyParty[e] = enemyPokemon;
|
||||
if (battleType === BattleType.WILD) {
|
||||
battle.seenEnemyPartyMemberIds.add(enemyPokemon.id);
|
||||
}
|
||||
|
||||
loadPokemonAssets.push(enemyPokemon.loadAssets());
|
||||
});
|
||||
|
||||
globalScene.arena.weather = sessionData.arena.weather;
|
||||
globalScene.arena.eventTarget.dispatchEvent(
|
||||
new WeatherChangedEvent(
|
||||
WeatherType.NONE,
|
||||
globalScene.arena.weather?.weatherType!,
|
||||
globalScene.arena.weather?.turnsLeft!,
|
||||
),
|
||||
); // TODO: is this bang correct?
|
||||
|
||||
globalScene.arena.terrain = sessionData.arena.terrain;
|
||||
globalScene.arena.eventTarget.dispatchEvent(
|
||||
new TerrainChangedEvent(
|
||||
TerrainType.NONE,
|
||||
globalScene.arena.terrain?.terrainType!,
|
||||
globalScene.arena.terrain?.turnsLeft!,
|
||||
),
|
||||
); // TODO: is this bang correct?
|
||||
|
||||
globalScene.arena.playerTerasUsed = sessionData.arena.playerTerasUsed;
|
||||
|
||||
globalScene.arena.tags = sessionData.arena.tags;
|
||||
if (globalScene.arena.tags) {
|
||||
for (const tag of globalScene.arena.tags) {
|
||||
if (tag instanceof EntryHazardTag) {
|
||||
const { tagType, side, turnCount, layers, maxLayers } = tag as EntryHazardTag;
|
||||
globalScene.arena.eventTarget.dispatchEvent(
|
||||
new TagAddedEvent(tagType, side, turnCount, layers, maxLayers),
|
||||
);
|
||||
} else {
|
||||
globalScene.arena.eventTarget.dispatchEvent(new TagAddedEvent(tag.tagType, tag.side, tag.turnCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
globalScene.arena.positionalTagManager.tags = sessionData.arena.positionalTags.map(tag =>
|
||||
loadPositionalTag(tag),
|
||||
);
|
||||
|
||||
if (globalScene.modifiers.length) {
|
||||
console.warn("Existing modifiers not cleared on session load, deleting...");
|
||||
globalScene.modifiers = [];
|
||||
}
|
||||
for (const modifierData of sessionData.modifiers) {
|
||||
const modifier = modifierData.toModifier(Modifier[modifierData.className]);
|
||||
if (modifier) {
|
||||
globalScene.addModifier(modifier, true);
|
||||
}
|
||||
}
|
||||
globalScene.updateModifiers(true);
|
||||
|
||||
for (const enemyModifierData of sessionData.enemyModifiers) {
|
||||
const modifier = enemyModifierData.toModifier(Modifier[enemyModifierData.className]);
|
||||
if (modifier) {
|
||||
globalScene.addEnemyModifier(modifier, true);
|
||||
}
|
||||
}
|
||||
|
||||
globalScene.updateModifiers(false);
|
||||
|
||||
Promise.all(loadPokemonAssets).then(() => resolve(true));
|
||||
};
|
||||
if (sessionData) {
|
||||
initSessionFromData(sessionData);
|
||||
} else {
|
||||
this.getSession(slotId)
|
||||
.then(data => data && initSessionFromData(data))
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
return;
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
return;
|
||||
|
||||
globalScene.gameMode = getGameMode(fromSession.gameMode || GameModes.CLASSIC);
|
||||
if (fromSession.challenges) {
|
||||
globalScene.gameMode.challenges = fromSession.challenges.map(c => c.toChallenge());
|
||||
}
|
||||
|
||||
globalScene.setSeed(fromSession.seed || globalScene.game.config.seed[0]);
|
||||
globalScene.resetSeed();
|
||||
|
||||
console.log("Seed:", globalScene.seed);
|
||||
|
||||
globalScene.sessionPlayTime = fromSession.playTime || 0;
|
||||
globalScene.lastSavePlayTime = 0;
|
||||
|
||||
const loadPokemonAssets: Promise<void>[] = [];
|
||||
|
||||
const party = globalScene.getPlayerParty();
|
||||
party.splice(0, party.length);
|
||||
|
||||
for (const p of fromSession.party) {
|
||||
const pokemon = p.toPokemon() as PlayerPokemon;
|
||||
pokemon.setVisible(false);
|
||||
loadPokemonAssets.push(pokemon.loadAssets(false));
|
||||
party.push(pokemon);
|
||||
}
|
||||
|
||||
Object.keys(globalScene.pokeballCounts).forEach((key: string) => {
|
||||
globalScene.pokeballCounts[key] = fromSession.pokeballCounts[key] || 0;
|
||||
});
|
||||
if (Overrides.POKEBALL_OVERRIDE.active) {
|
||||
globalScene.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs;
|
||||
}
|
||||
|
||||
globalScene.money = Math.floor(fromSession.money || 0);
|
||||
globalScene.updateMoneyText();
|
||||
|
||||
if (globalScene.money > this.gameStats.highestMoney) {
|
||||
this.gameStats.highestMoney = globalScene.money;
|
||||
}
|
||||
|
||||
globalScene.score = fromSession.score;
|
||||
globalScene.updateScoreText();
|
||||
|
||||
globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(fromSession.mysteryEncounterSaveData);
|
||||
|
||||
globalScene.newArena(fromSession.arena.biome, fromSession.playerFaints);
|
||||
|
||||
const battleType = fromSession.battleType || 0;
|
||||
const trainerConfig = fromSession.trainer ? trainerConfigs[fromSession.trainer.trainerType] : null;
|
||||
const mysteryEncounterType =
|
||||
fromSession.mysteryEncounterType !== -1 ? fromSession.mysteryEncounterType : undefined;
|
||||
const battle = globalScene.newBattle(
|
||||
fromSession.waveIndex,
|
||||
battleType,
|
||||
fromSession.trainer,
|
||||
battleType === BattleType.TRAINER
|
||||
? trainerConfig?.doubleOnly || fromSession.trainer?.variant === TrainerVariant.DOUBLE
|
||||
: fromSession.enemyParty.length > 1,
|
||||
mysteryEncounterType,
|
||||
);
|
||||
battle.enemyLevels = fromSession.enemyParty.map(p => p.level);
|
||||
|
||||
globalScene.arena.init();
|
||||
|
||||
fromSession.enemyParty.forEach((enemyData, e) => {
|
||||
const enemyPokemon = enemyData.toPokemon(
|
||||
battleType,
|
||||
e,
|
||||
fromSession.trainer?.variant === TrainerVariant.DOUBLE,
|
||||
) as EnemyPokemon;
|
||||
battle.enemyParty[e] = enemyPokemon;
|
||||
if (battleType === BattleType.WILD) {
|
||||
battle.seenEnemyPartyMemberIds.add(enemyPokemon.id);
|
||||
}
|
||||
|
||||
loadPokemonAssets.push(enemyPokemon.loadAssets());
|
||||
});
|
||||
|
||||
globalScene.arena.weather = fromSession.arena.weather;
|
||||
globalScene.arena.eventTarget.dispatchEvent(
|
||||
new WeatherChangedEvent(
|
||||
WeatherType.NONE,
|
||||
globalScene.arena.weather?.weatherType!,
|
||||
globalScene.arena.weather?.turnsLeft!,
|
||||
),
|
||||
); // TODO: is this bang correct?
|
||||
|
||||
globalScene.arena.terrain = fromSession.arena.terrain;
|
||||
globalScene.arena.eventTarget.dispatchEvent(
|
||||
new TerrainChangedEvent(
|
||||
TerrainType.NONE,
|
||||
globalScene.arena.terrain?.terrainType!,
|
||||
globalScene.arena.terrain?.turnsLeft!,
|
||||
),
|
||||
); // TODO: is this bang correct?
|
||||
|
||||
globalScene.arena.playerTerasUsed = fromSession.arena.playerTerasUsed;
|
||||
|
||||
globalScene.arena.tags = fromSession.arena.tags;
|
||||
if (globalScene.arena.tags) {
|
||||
for (const tag of globalScene.arena.tags) {
|
||||
if (tag instanceof EntryHazardTag) {
|
||||
const { tagType, side, turnCount, layers, maxLayers } = tag as EntryHazardTag;
|
||||
globalScene.arena.eventTarget.dispatchEvent(
|
||||
new TagAddedEvent(tagType, side, turnCount, layers, maxLayers),
|
||||
);
|
||||
} else {
|
||||
globalScene.arena.eventTarget.dispatchEvent(new TagAddedEvent(tag.tagType, tag.side, tag.turnCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
globalScene.arena.positionalTagManager.tags = fromSession.arena.positionalTags.map(tag =>
|
||||
loadPositionalTag(tag),
|
||||
);
|
||||
|
||||
if (globalScene.modifiers.length) {
|
||||
console.warn("Existing modifiers not cleared on session load, deleting...");
|
||||
globalScene.modifiers = [];
|
||||
}
|
||||
for (const modifierData of fromSession.modifiers) {
|
||||
const modifier = modifierData.toModifier(Modifier[modifierData.className]);
|
||||
if (modifier) {
|
||||
globalScene.addModifier(modifier, true);
|
||||
}
|
||||
}
|
||||
globalScene.updateModifiers(true);
|
||||
|
||||
for (const enemyModifierData of fromSession.enemyModifiers) {
|
||||
const modifier = enemyModifierData.toModifier(Modifier[enemyModifierData.className]);
|
||||
if (modifier) {
|
||||
globalScene.addEnemyModifier(modifier, true);
|
||||
}
|
||||
}
|
||||
|
||||
globalScene.updateModifiers(false);
|
||||
|
||||
Promise.all(loadPokemonAssets).then(() => resolve(true));
|
||||
};
|
||||
if (sessionData) {
|
||||
initSessionFromData(sessionData);
|
||||
} else {
|
||||
this.getSession(slotId)
|
||||
.then(data => {
|
||||
return data && initSessionFromData(data);
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
return;
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -61,12 +61,12 @@ export class TouchControl {
|
||||
* event, removes the keydown state, and removes the 'active' class from the node and the last touched element.
|
||||
*/
|
||||
bindKey(node: HTMLElement, key: string) {
|
||||
node.addEventListener("touchstart", event => {
|
||||
node.addEventListener("pointerdown", event => {
|
||||
event.preventDefault();
|
||||
this.touchButtonDown(node, key);
|
||||
});
|
||||
|
||||
node.addEventListener("touchend", event => {
|
||||
node.addEventListener("pointerup", event => {
|
||||
event.preventDefault();
|
||||
this.touchButtonUp(node, key, event.target?.["id"]);
|
||||
});
|
||||
|
@ -13,7 +13,7 @@ import { SettingsGamepadUiHandler } from "#ui/settings-gamepad-ui-handler";
|
||||
import { SettingsKeyboardUiHandler } from "#ui/settings-keyboard-ui-handler";
|
||||
import { SettingsUiHandler } from "#ui/settings-ui-handler";
|
||||
import { StarterSelectUiHandler } from "#ui/starter-select-ui-handler";
|
||||
import type Phaser from "phaser";
|
||||
import Phaser from "phaser";
|
||||
|
||||
type ActionKeys = Record<Button, () => void>;
|
||||
|
||||
@ -224,25 +224,26 @@ export class UiInputs {
|
||||
|
||||
buttonSpeedChange(up = true): void {
|
||||
const settingGameSpeed = settingIndex(SettingKeys.Game_Speed);
|
||||
const settingOptions = Setting[settingGameSpeed].options;
|
||||
let currentSetting = settingOptions.findIndex(item => item.value === globalScene.gameSpeed.toString());
|
||||
// if current setting is -1, then the current game speed is not a valid option, so default to index 5 (3x)
|
||||
if (currentSetting === -1) {
|
||||
currentSetting = 5;
|
||||
}
|
||||
let direction: number;
|
||||
if (up && globalScene.gameSpeed < 5) {
|
||||
globalScene.gameData.saveSetting(
|
||||
SettingKeys.Game_Speed,
|
||||
Setting[settingGameSpeed].options.findIndex(item => item.label === `${globalScene.gameSpeed}x`) + 1,
|
||||
);
|
||||
if (globalScene.ui?.getMode() === UiMode.SETTINGS) {
|
||||
(globalScene.ui.getHandler() as SettingsUiHandler).show([]);
|
||||
}
|
||||
direction = 1;
|
||||
} else if (!up && globalScene.gameSpeed > 1) {
|
||||
globalScene.gameData.saveSetting(
|
||||
SettingKeys.Game_Speed,
|
||||
Math.max(
|
||||
Setting[settingGameSpeed].options.findIndex(item => item.label === `${globalScene.gameSpeed}x`) - 1,
|
||||
0,
|
||||
),
|
||||
);
|
||||
if (globalScene.ui?.getMode() === UiMode.SETTINGS) {
|
||||
(globalScene.ui.getHandler() as SettingsUiHandler).show([]);
|
||||
}
|
||||
direction = -1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
globalScene.gameData.saveSetting(
|
||||
SettingKeys.Game_Speed,
|
||||
Phaser.Math.Clamp(currentSetting + direction, 0, settingOptions.length - 1),
|
||||
);
|
||||
if (globalScene.ui?.getMode() === UiMode.SETTINGS) {
|
||||
(globalScene.ui.getHandler() as SettingsUiHandler).show([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -287,9 +287,6 @@ export abstract class BattleInfo extends Phaser.GameObjects.Container {
|
||||
2.5,
|
||||
);
|
||||
this.splicedIcon.setVisible(pokemon.isFusion(true));
|
||||
if (!this.splicedIcon.visible) {
|
||||
return;
|
||||
}
|
||||
this.splicedIcon
|
||||
.on("pointerover", () =>
|
||||
globalScene.ui.showTooltip(
|
||||
@ -323,6 +320,10 @@ export abstract class BattleInfo extends Phaser.GameObjects.Container {
|
||||
.setVisible(pokemon.isShiny())
|
||||
.setTint(getVariantTint(baseVariant));
|
||||
|
||||
this.shinyIcon
|
||||
.on("pointerover", () => globalScene.ui.showTooltip("", i18next.t("common:shinyOnHover") + shinyDescriptor))
|
||||
.on("pointerout", () => globalScene.ui.hideTooltip());
|
||||
|
||||
if (!this.shinyIcon.visible) {
|
||||
return;
|
||||
}
|
||||
@ -335,10 +336,6 @@ export abstract class BattleInfo extends Phaser.GameObjects.Container {
|
||||
}
|
||||
shinyDescriptor += ")";
|
||||
}
|
||||
|
||||
this.shinyIcon
|
||||
.on("pointerover", () => globalScene.ui.showTooltip("", i18next.t("common:shinyOnHover") + shinyDescriptor))
|
||||
.on("pointerout", () => globalScene.ui.hideTooltip());
|
||||
}
|
||||
|
||||
initInfo(pokemon: Pokemon) {
|
||||
|
@ -36,7 +36,7 @@ export class EnemyBattleInfo extends BattleInfo {
|
||||
override constructTypeIcons(): void {
|
||||
this.type1Icon = globalScene.add.sprite(-15, -15.5, "pbinfo_enemy_type1").setName("icon_type_1").setOrigin(0);
|
||||
this.type2Icon = globalScene.add.sprite(-15, -2.5, "pbinfo_enemy_type2").setName("icon_type_2").setOrigin(0);
|
||||
this.type3Icon = globalScene.add.sprite(0, 15.5, "pbinfo_enemy_type3").setName("icon_type_3").setOrigin(0);
|
||||
this.type3Icon = globalScene.add.sprite(0, -15.5, "pbinfo_enemy_type").setName("icon_type_3").setOrigin(0);
|
||||
this.add([this.type1Icon, this.type2Icon, this.type3Icon]);
|
||||
}
|
||||
|
||||
@ -115,6 +115,9 @@ 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);
|
||||
}
|
||||
|
||||
@ -180,12 +183,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`);
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ export class PlayerBattleInfo extends BattleInfo {
|
||||
override constructTypeIcons(): void {
|
||||
this.type1Icon = globalScene.add.sprite(-139, -17, "pbinfo_player_type1").setName("icon_type_1").setOrigin(0);
|
||||
this.type2Icon = globalScene.add.sprite(-139, -1, "pbinfo_player_type2").setName("icon_type_2").setOrigin(0);
|
||||
this.type3Icon = globalScene.add.sprite(-154, -17, "pbinfo_player_type3").setName("icon_type_3").setOrigin(0);
|
||||
this.type3Icon = globalScene.add.sprite(-154, -17, "pbinfo_player_type").setName("icon_type_3").setOrigin(0);
|
||||
this.add([this.type1Icon, this.type2Icon, this.type3Icon]);
|
||||
}
|
||||
|
||||
|
@ -136,6 +136,11 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
||||
this.submitAction = config.buttonActions.length ? config.buttonActions[0] : null;
|
||||
this.cancelAction = config.buttonActions[1] ?? null;
|
||||
|
||||
// Auto focus the first input field after a short delay, to prevent accidental inputs
|
||||
setTimeout(() => {
|
||||
this.inputs[0].setFocus();
|
||||
}, 50);
|
||||
|
||||
// #region: Override button pointerDown
|
||||
// Override the pointerDown event for the buttonBgs to call the `submitAction` and `cancelAction`
|
||||
// properties that we set above, allowing their behavior to change after this method terminates
|
||||
|
@ -613,6 +613,20 @@ 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;
|
||||
}
|
||||
|
||||
@ -2041,12 +2055,13 @@ class PartySlot extends Phaser.GameObjects.Container {
|
||||
|
||||
if (this.pokemon.isShiny()) {
|
||||
const doubleShiny = this.pokemon.isDoubleShiny(false);
|
||||
const largeIconTint = doubleShiny ? this.pokemon.getBaseVariant() : this.pokemon.getVariant();
|
||||
|
||||
const shinyStar = globalScene.add
|
||||
.image(0, 0, `shiny_star_small${doubleShiny ? "_1" : ""}`)
|
||||
.setOrigin(0)
|
||||
.setPositionRelative(this.slotName, shinyIconToNameOffset.x, shinyIconToNameOffset.y)
|
||||
.setTint(getVariantTint(this.pokemon.getBaseVariant()));
|
||||
.setTint(getVariantTint(largeIconTint));
|
||||
slotInfoContainer.add(shinyStar);
|
||||
|
||||
if (doubleShiny) {
|
||||
|
@ -106,10 +106,6 @@ export class PokedexScanUiHandler extends FormModalUiHandler {
|
||||
|
||||
this.reduceKeys();
|
||||
|
||||
setTimeout(() => {
|
||||
input.setFocus(); // Focus after a short delay to avoid unwanted input
|
||||
}, 50);
|
||||
|
||||
input.on("keydown", (inputObject, evt: KeyboardEvent) => {
|
||||
if (
|
||||
["escape", "space"].some(v => v === evt.key.toLowerCase() || v === evt.code.toLowerCase()) &&
|
||||
|
@ -9,9 +9,9 @@ export const TOUCH_CONTROL_POSITIONS_PORTRAIT = "touchControlPositionsPortrait";
|
||||
type ControlPosition = { id: string; x: number; y: number };
|
||||
|
||||
type ConfigurationEventListeners = {
|
||||
touchstart: EventListener[];
|
||||
touchmove: EventListener[];
|
||||
touchend: EventListener[];
|
||||
pointerdown: EventListener[];
|
||||
pointermove: EventListener[];
|
||||
pointerup: EventListener[];
|
||||
};
|
||||
|
||||
type ToolbarRefs = {
|
||||
@ -39,9 +39,9 @@ export class MoveTouchControlsHandler {
|
||||
* These are used to remove the event listeners when the configuration mode is disabled.
|
||||
*/
|
||||
private configurationEventListeners: ConfigurationEventListeners = {
|
||||
touchstart: [],
|
||||
touchmove: [],
|
||||
touchend: [],
|
||||
pointerdown: [],
|
||||
pointermove: [],
|
||||
pointerup: [],
|
||||
};
|
||||
|
||||
private overlay: Phaser.GameObjects.Container;
|
||||
@ -165,34 +165,33 @@ export class MoveTouchControlsHandler {
|
||||
/**
|
||||
* Start dragging the given button.
|
||||
* @param controlGroup The button that is being dragged.
|
||||
* @param touch The touch event that started the drag.
|
||||
* @param event The pointer event that started the drag.
|
||||
*/
|
||||
private startDrag = (controlGroup: HTMLElement): void => {
|
||||
this.draggingElement = controlGroup;
|
||||
};
|
||||
|
||||
/**
|
||||
* Drags the currently dragged element to the given touch position.
|
||||
* @param touch The touch event that is currently happening.
|
||||
* @param isLeft Whether the dragged element is a left button.
|
||||
* Drags the currently dragged element to the given pointer position.
|
||||
* @param event The pointer event that is currently happening.
|
||||
*/
|
||||
private drag = (touch: Touch): void => {
|
||||
private drag = (event: PointerEvent): void => {
|
||||
if (!this.draggingElement) {
|
||||
return;
|
||||
}
|
||||
const rect = this.draggingElement.getBoundingClientRect();
|
||||
// Map the touch position to the center of the dragged element.
|
||||
// Map the pointer position to the center of the dragged element.
|
||||
const xOffset = this.isLeft(this.draggingElement)
|
||||
? touch.clientX - rect.width / 2
|
||||
: window.innerWidth - touch.clientX - rect.width / 2;
|
||||
const yOffset = window.innerHeight - touch.clientY - rect.height / 2;
|
||||
? event.clientX - rect.width / 2
|
||||
: window.innerWidth - event.clientX - rect.width / 2;
|
||||
const yOffset = window.innerHeight - event.clientY - rect.height / 2;
|
||||
this.setPosition(this.draggingElement, xOffset, yOffset);
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops dragging the currently dragged element.
|
||||
*/
|
||||
private stopDrag = () => {
|
||||
private stopDrag = (): void => {
|
||||
this.draggingElement = null;
|
||||
};
|
||||
|
||||
@ -303,19 +302,19 @@ export class MoveTouchControlsHandler {
|
||||
*/
|
||||
private createConfigurationEventListeners(controlGroups: HTMLDivElement[]): ConfigurationEventListeners {
|
||||
return {
|
||||
touchstart: controlGroups.map((element: HTMLDivElement) => {
|
||||
pointerdown: controlGroups.map((element: HTMLDivElement) => {
|
||||
const startDrag = () => this.startDrag(element);
|
||||
element.addEventListener("touchstart", startDrag, { passive: true });
|
||||
element.addEventListener("pointerdown", startDrag, { passive: true });
|
||||
return startDrag;
|
||||
}),
|
||||
touchmove: controlGroups.map(() => {
|
||||
const drag = event => this.drag(event.touches[0]);
|
||||
window.addEventListener("touchmove", drag, { passive: true });
|
||||
pointermove: controlGroups.map(() => {
|
||||
const drag = (event: PointerEvent) => this.drag(event);
|
||||
window.addEventListener("pointermove", drag, { passive: true });
|
||||
return drag;
|
||||
}),
|
||||
touchend: controlGroups.map(() => {
|
||||
pointerup: controlGroups.map(() => {
|
||||
const stopDrag = () => this.stopDrag();
|
||||
window.addEventListener("touchend", stopDrag, { passive: true });
|
||||
window.addEventListener("pointerup", stopDrag, { passive: true });
|
||||
return stopDrag;
|
||||
}),
|
||||
};
|
||||
@ -373,12 +372,12 @@ export class MoveTouchControlsHandler {
|
||||
this.draggingElement = null;
|
||||
|
||||
// Remove event listeners
|
||||
const { touchstart, touchmove, touchend } = this.configurationEventListeners;
|
||||
const { pointerdown, pointermove, pointerup } = this.configurationEventListeners;
|
||||
this.getControlGroupElements().forEach((element, index) =>
|
||||
element.removeEventListener("touchstart", touchstart[index]),
|
||||
element.removeEventListener("pointerdown", pointerdown[index]),
|
||||
);
|
||||
touchmove.forEach(listener => window.removeEventListener("touchmove", listener));
|
||||
touchend.forEach(listener => window.removeEventListener("touchend", listener));
|
||||
pointermove.forEach(listener => window.removeEventListener("pointermove", listener));
|
||||
pointerup.forEach(listener => window.removeEventListener("pointerup", listener));
|
||||
|
||||
// Remove configuration toolbar
|
||||
const toolbar = document.querySelector("#touchControls #configToolbar");
|
||||
|
@ -72,7 +72,7 @@ import {
|
||||
rgbHexToRgba,
|
||||
} from "#utils/common";
|
||||
import type { StarterPreferences } from "#utils/data";
|
||||
import { loadStarterPreferences, saveStarterPreferences } from "#utils/data";
|
||||
import { deepCopy, 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,7 +1148,8 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
||||
this.starterSelectContainer.setVisible(true);
|
||||
|
||||
this.starterPreferences = loadStarterPreferences();
|
||||
this.originalStarterPreferences = loadStarterPreferences();
|
||||
// Deep copy the JSON (avoid re-loading from disk)
|
||||
this.originalStarterPreferences = deepCopy(this.starterPreferences);
|
||||
|
||||
this.allSpecies.forEach((species, s) => {
|
||||
const icon = this.starterContainers[s].icon;
|
||||
@ -1212,6 +1213,8 @@ 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);
|
||||
|
||||
@ -1828,9 +1831,15 @@ 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
|
||||
let starterAttributes = this.starterPreferences[this.lastSpecies.speciesId];
|
||||
// The original starter preferences
|
||||
const originalStarterAttributes = this.originalStarterPreferences[this.lastSpecies.speciesId];
|
||||
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]!;
|
||||
|
||||
// this gets the correct pokemon cursor depending on whether you're in the starter screen or the party icons
|
||||
if (!this.starterIconsCursorObj.visible) {
|
||||
@ -2050,10 +2059,6 @@ 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();
|
||||
@ -3408,8 +3413,9 @@ 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);
|
||||
@ -3634,15 +3640,19 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
||||
|
||||
if (starterIndex > -1) {
|
||||
props = globalScene.gameData.getSpeciesDexAttrProps(species, this.starterAttr[starterIndex]);
|
||||
this.setSpeciesDetails(species, {
|
||||
shiny: props.shiny,
|
||||
formIndex: props.formIndex,
|
||||
female: props.female,
|
||||
variant: props.variant,
|
||||
abilityIndex: this.starterAbilityIndexes[starterIndex],
|
||||
natureIndex: this.starterNatures[starterIndex],
|
||||
teraType: this.starterTeras[starterIndex],
|
||||
});
|
||||
this.setSpeciesDetails(
|
||||
species,
|
||||
{
|
||||
shiny: props.shiny,
|
||||
formIndex: props.formIndex,
|
||||
female: props.female,
|
||||
variant: props.variant,
|
||||
abilityIndex: this.starterAbilityIndexes[starterIndex],
|
||||
natureIndex: this.starterNatures[starterIndex],
|
||||
teraType: this.starterTeras[starterIndex],
|
||||
},
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
const defaultAbilityIndex =
|
||||
starterAttributes?.ability ?? globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species);
|
||||
@ -3659,15 +3669,19 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
||||
props.formIndex = starterAttributes?.form ?? props.formIndex;
|
||||
props.female = starterAttributes?.female ?? props.female;
|
||||
|
||||
this.setSpeciesDetails(species, {
|
||||
shiny: props.shiny,
|
||||
formIndex: props.formIndex,
|
||||
female: props.female,
|
||||
variant: props.variant,
|
||||
abilityIndex: defaultAbilityIndex,
|
||||
natureIndex: defaultNature,
|
||||
teraType: starterAttributes?.tera,
|
||||
});
|
||||
this.setSpeciesDetails(
|
||||
species,
|
||||
{
|
||||
shiny: props.shiny,
|
||||
formIndex: props.formIndex,
|
||||
female: props.female,
|
||||
variant: props.variant,
|
||||
abilityIndex: defaultAbilityIndex,
|
||||
natureIndex: defaultNature,
|
||||
teraType: starterAttributes?.tera,
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isNullOrUndefined(props.formIndex)) {
|
||||
@ -3704,15 +3718,19 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
||||
const defaultNature = globalScene.gameData.getSpeciesDefaultNature(species);
|
||||
const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
|
||||
|
||||
this.setSpeciesDetails(species, {
|
||||
shiny: props.shiny,
|
||||
formIndex: props.formIndex,
|
||||
female: props.female,
|
||||
variant: props.variant,
|
||||
abilityIndex: defaultAbilityIndex,
|
||||
natureIndex: defaultNature,
|
||||
forSeen: true,
|
||||
});
|
||||
this.setSpeciesDetails(
|
||||
species,
|
||||
{
|
||||
shiny: props.shiny,
|
||||
formIndex: props.formIndex,
|
||||
female: props.female,
|
||||
variant: props.variant,
|
||||
abilityIndex: defaultAbilityIndex,
|
||||
natureIndex: defaultNature,
|
||||
forSeen: true,
|
||||
},
|
||||
false,
|
||||
);
|
||||
this.pokemonSprite.setTint(0x808080);
|
||||
}
|
||||
} else {
|
||||
@ -3734,15 +3752,19 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
||||
this.pokemonFormText.setVisible(false);
|
||||
this.teraIcon.setVisible(false);
|
||||
|
||||
this.setSpeciesDetails(species!, {
|
||||
// TODO: is this bang correct?
|
||||
shiny: false,
|
||||
formIndex: 0,
|
||||
female: false,
|
||||
variant: 0,
|
||||
abilityIndex: 0,
|
||||
natureIndex: 0,
|
||||
});
|
||||
this.setSpeciesDetails(
|
||||
species!,
|
||||
{
|
||||
// TODO: is this bang correct?
|
||||
shiny: false,
|
||||
formIndex: 0,
|
||||
female: false,
|
||||
variant: 0,
|
||||
abilityIndex: 0,
|
||||
natureIndex: 0,
|
||||
},
|
||||
false,
|
||||
);
|
||||
this.pokemonSprite.clearTint();
|
||||
}
|
||||
}
|
||||
@ -3764,7 +3786,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
||||
return { dexEntry: { ...copiedDexEntry }, starterDataEntry: { ...copiedStarterDataEntry } };
|
||||
}
|
||||
|
||||
setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}): void {
|
||||
setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}, save = true): 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;
|
||||
@ -4176,7 +4198,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
||||
|
||||
this.updateInstructions();
|
||||
|
||||
saveStarterPreferences(this.originalStarterPreferences);
|
||||
if (save) {
|
||||
saveStarterPreferences(this.originalStarterPreferences);
|
||||
}
|
||||
}
|
||||
|
||||
setTypeIcons(type1: PokemonType | null, type2: PokemonType | null): void {
|
||||
|
@ -430,20 +430,21 @@ export class SummaryUiHandler extends UiHandler {
|
||||
this.friendshipShadow.setCrop(0, 0, 16, 16 - 16 * ((this.pokemon?.friendship || 0) / 255));
|
||||
|
||||
const doubleShiny = this.pokemon.isDoubleShiny(false);
|
||||
const baseVariant = this.pokemon.getBaseVariant(doubleShiny);
|
||||
const bigIconVariant = doubleShiny ? this.pokemon.getBaseVariant(doubleShiny) : this.pokemon.getVariant();
|
||||
|
||||
this.shinyIcon.setPositionRelative(
|
||||
this.nameText,
|
||||
this.nameText.displayWidth + (this.splicedIcon.visible ? this.splicedIcon.displayWidth + 1 : 0) + 1,
|
||||
3,
|
||||
);
|
||||
this.shinyIcon.setTexture(`shiny_star${doubleShiny ? "_1" : ""}`);
|
||||
this.shinyIcon.setVisible(this.pokemon.isShiny(false));
|
||||
this.shinyIcon.setTint(getVariantTint(baseVariant));
|
||||
this.shinyIcon
|
||||
.setTexture(`shiny_star${doubleShiny ? "_1" : ""}`)
|
||||
.setVisible(this.pokemon.isShiny(false))
|
||||
.setTint(getVariantTint(bigIconVariant));
|
||||
if (this.shinyIcon.visible) {
|
||||
let shinyDescriptor = "";
|
||||
if (doubleShiny || baseVariant) {
|
||||
shinyDescriptor = " (" + getShinyDescriptor(baseVariant);
|
||||
if (doubleShiny || bigIconVariant) {
|
||||
shinyDescriptor = " (" + getShinyDescriptor(bigIconVariant);
|
||||
if (doubleShiny) {
|
||||
shinyDescriptor += "/" + getShinyDescriptor(this.pokemon.fusionVariant);
|
||||
}
|
||||
|
@ -349,6 +349,15 @@ 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 };
|
||||
|
@ -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(values: object): object {
|
||||
export function deepCopy<T extends object>(values: T): T {
|
||||
// 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,13 +58,28 @@ 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;
|
||||
[key: number]: StarterAttributes | undefined;
|
||||
}
|
||||
// called on starter selection show once
|
||||
|
||||
@ -74,11 +89,17 @@ export function loadStarterPreferences(): StarterPreferences {
|
||||
localStorage.getItem(`starterPrefs_${loggedInUser?.username}`) || StarterPrefers_DEFAULT),
|
||||
);
|
||||
}
|
||||
// called on starter selection clear, always
|
||||
|
||||
export function saveStarterPreferences(prefs: StarterPreferences): void {
|
||||
const pStr: string = JSON.stringify(prefs);
|
||||
// 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));
|
||||
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
|
||||
|
@ -35,13 +35,43 @@ describe("Abilities - Intimidate", () => {
|
||||
it("should lower all opponents' ATK by 1 stage on entry and switch", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]);
|
||||
|
||||
const [mightyena, poochyena] = game.scene.getPlayerParty();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
expect(enemy.getStatStage(Stat.ATK)).toBe(-1);
|
||||
expect(mightyena).toHaveAbilityApplied(AbilityId.INTIMIDATE);
|
||||
|
||||
game.doSwitchPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(poochyena.isActive()).toBe(true);
|
||||
expect(enemy.getStatStage(Stat.ATK)).toBe(-2);
|
||||
expect(poochyena).toHaveAbilityApplied(AbilityId.INTIMIDATE);
|
||||
});
|
||||
|
||||
it("should trigger once on initial switch prompt without cancelling opposing abilities", async () => {
|
||||
await game.classicMode.runToSummon([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]);
|
||||
await game.classicMode.startBattleWithSwitch(1);
|
||||
|
||||
const [poochyena, mightyena] = game.scene.getPlayerParty();
|
||||
expect(poochyena.species.speciesId).toBe(SpeciesId.POOCHYENA);
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
expect(enemy).toHaveStatStage(Stat.ATK, -1);
|
||||
expect(poochyena).toHaveStatStage(Stat.ATK, -1);
|
||||
|
||||
expect(poochyena).toHaveAbilityApplied(AbilityId.INTIMIDATE);
|
||||
expect(mightyena).not.toHaveAbilityApplied(AbilityId.INTIMIDATE);
|
||||
});
|
||||
|
||||
it("should activate on reload with single party", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MIGHTYENA]);
|
||||
|
||||
expect(game.field.getEnemyPokemon()).toHaveStatStage(Stat.ATK, -1);
|
||||
|
||||
await game.reload.reloadSession();
|
||||
|
||||
expect(game.field.getEnemyPokemon()).toHaveStatStage(Stat.ATK, -1);
|
||||
});
|
||||
|
||||
it("should lower ATK of all opponents in a double battle", async () => {
|
||||
|
@ -175,4 +175,27 @@ 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);
|
||||
});
|
||||
});
|
||||
|
@ -4,12 +4,15 @@ import { allMoves } from "#data/data-lists";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { Button } from "#enums/buttons";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { PokeballType } from "#enums/pokeball";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { PositionalTagType } from "#enums/positional-tag-type";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import i18next from "i18next";
|
||||
import Phaser from "phaser";
|
||||
@ -95,7 +98,7 @@ describe("Moves - Delayed Attacks", () => {
|
||||
|
||||
expectFutureSightActive(0);
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||
expect(enemy).not.toHaveFullHp();
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("moveTriggers:tookMoveAttack", {
|
||||
pokemonName: getPokemonNameWithAffix(enemy),
|
||||
@ -130,12 +133,12 @@ describe("Moves - Delayed Attacks", () => {
|
||||
|
||||
expectFutureSightActive();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
expect(enemy.hp).toBe(enemy.getMaxHp());
|
||||
expect(enemy).toHaveFullHp();
|
||||
|
||||
await passTurns(2);
|
||||
|
||||
expectFutureSightActive(0);
|
||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||
expect(enemy).not.toHaveFullHp();
|
||||
});
|
||||
|
||||
it("should work when used against different targets in doubles", async () => {
|
||||
@ -149,15 +152,15 @@ describe("Moves - Delayed Attacks", () => {
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expectFutureSightActive(2);
|
||||
expect(enemy1.hp).toBe(enemy1.getMaxHp());
|
||||
expect(enemy2.hp).toBe(enemy2.getMaxHp());
|
||||
expect(enemy1).toHaveFullHp();
|
||||
expect(enemy2).toHaveFullHp();
|
||||
expect(karp.getLastXMoves()[0].result).toBe(MoveResult.OTHER);
|
||||
expect(feebas.getLastXMoves()[0].result).toBe(MoveResult.OTHER);
|
||||
|
||||
await passTurns(2);
|
||||
|
||||
expect(enemy1.hp).toBeLessThan(enemy1.getMaxHp());
|
||||
expect(enemy2.hp).toBeLessThan(enemy2.getMaxHp());
|
||||
expect(enemy1).not.toHaveFullHp();
|
||||
expect(enemy2).not.toHaveFullHp();
|
||||
});
|
||||
|
||||
it("should trigger multiple pending attacks in order of creation, even if that order changes later on", async () => {
|
||||
@ -222,8 +225,8 @@ describe("Moves - Delayed Attacks", () => {
|
||||
|
||||
expect(game.scene.getPlayerParty()).toEqual([milotic, karp, feebas]);
|
||||
|
||||
expect(karp.hp).toBe(karp.getMaxHp());
|
||||
expect(feebas.hp).toBe(feebas.getMaxHp());
|
||||
expect(karp).toHaveFullHp();
|
||||
expect(feebas).toHaveFullHp();
|
||||
expect(game.textInterceptor.logs).not.toContain(
|
||||
i18next.t("moveTriggers:tookMoveAttack", {
|
||||
pokemonName: getPokemonNameWithAffix(karp),
|
||||
@ -245,15 +248,14 @@ describe("Moves - Delayed Attacks", () => {
|
||||
expect(enemy2.isFainted()).toBe(true);
|
||||
expectFutureSightActive();
|
||||
|
||||
const attack = game.scene.arena.positionalTagManager.tags.find(
|
||||
t => t.tagType === PositionalTagType.DELAYED_ATTACK,
|
||||
)!;
|
||||
expect(attack).toBeDefined();
|
||||
expect(attack.targetIndex).toBe(enemy1.getBattlerIndex());
|
||||
expect(game).toHavePositionalTag({
|
||||
tagType: PositionalTagType.DELAYED_ATTACK,
|
||||
targetIndex: enemy1.getBattlerIndex(),
|
||||
});
|
||||
|
||||
await passTurns(2);
|
||||
|
||||
expect(enemy1.hp).toBeLessThan(enemy1.getMaxHp());
|
||||
expect(enemy1).not.toHaveFullHp();
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("moveTriggers:tookMoveAttack", {
|
||||
pokemonName: getPokemonNameWithAffix(enemy1),
|
||||
@ -281,7 +283,7 @@ describe("Moves - Delayed Attacks", () => {
|
||||
await game.toNextTurn();
|
||||
|
||||
expectFutureSightActive(0);
|
||||
expect(enemy1.hp).toBe(enemy1.getMaxHp());
|
||||
expect(enemy1).toHaveFullHp();
|
||||
expect(game.textInterceptor.logs).not.toContain(
|
||||
i18next.t("moveTriggers:tookMoveAttack", {
|
||||
pokemonName: getPokemonNameWithAffix(enemy1),
|
||||
@ -317,8 +319,8 @@ describe("Moves - Delayed Attacks", () => {
|
||||
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(enemy1.hp).toBe(enemy1.getMaxHp());
|
||||
expect(enemy2.hp).toBeLessThan(enemy2.getMaxHp());
|
||||
expect(enemy1).toHaveFullHp();
|
||||
expect(enemy2).not.toHaveFullHp();
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("moveTriggers:tookMoveAttack", {
|
||||
pokemonName: getPokemonNameWithAffix(enemy2),
|
||||
@ -351,7 +353,7 @@ describe("Moves - Delayed Attacks", () => {
|
||||
|
||||
// Player Normalize was not applied due to being off field
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||
expect(enemy).not.toHaveFullHp();
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("moveTriggers:tookMoveAttack", {
|
||||
pokemonName: getPokemonNameWithAffix(enemy),
|
||||
@ -384,6 +386,35 @@ describe("Moves - Delayed Attacks", () => {
|
||||
expect(typeBoostSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not crash when catching & releasing a Pokemon on the same turn its delayed attack expires", async () => {
|
||||
game.override.startingModifier([{ name: "MASTER_BALL", count: 1 }]);
|
||||
await game.classicMode.startBattle([
|
||||
SpeciesId.FEEBAS,
|
||||
SpeciesId.FEEBAS,
|
||||
SpeciesId.FEEBAS,
|
||||
SpeciesId.FEEBAS,
|
||||
SpeciesId.FEEBAS,
|
||||
SpeciesId.FEEBAS,
|
||||
]);
|
||||
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(MoveId.FUTURE_SIGHT);
|
||||
await game.toNextTurn();
|
||||
|
||||
expectFutureSightActive(1);
|
||||
|
||||
await passTurns(1);
|
||||
|
||||
// Throw master ball and release the enemy
|
||||
game.doThrowPokeball(PokeballType.MASTER_BALL);
|
||||
game.onNextPrompt("AttemptCapturePhase", UiMode.CONFIRM, () => {
|
||||
game.scene.ui.processInput(Button.CANCEL);
|
||||
});
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expectFutureSightActive(0);
|
||||
});
|
||||
|
||||
// TODO: Implement and move to a power spot's test file
|
||||
it.todo("Should activate ally's power spot when switched in during single battles");
|
||||
});
|
||||
|
50
test/moves/poltergeist.test.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Move - Poltergeist", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
it("should not crash when used after both opponents have fainted", async () => {
|
||||
game.override.battleStyle("double").enemyLevel(5);
|
||||
await game.classicMode.startBattle([SpeciesId.STARYU, SpeciesId.SLOWPOKE]);
|
||||
|
||||
game.move.use(MoveId.DAZZLING_GLEAM);
|
||||
game.move.use(MoveId.POLTERGEIST, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY);
|
||||
const [_, poltergeistUser] = game.scene.getPlayerField();
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
|
||||
await game.toEndOfTurn();
|
||||
// Expect poltergeist to have failed
|
||||
expect(poltergeistUser).toHaveUsedMove({ move: MoveId.POLTERGEIST, result: MoveResult.FAIL });
|
||||
// If the test makes it to the end of turn, no crash occurred. Nothing to assert
|
||||
});
|
||||
});
|
60
test/status-effects/general-status-effect.test.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { allAbilities } from "#data/data-lists";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { ObtainStatusEffectPhase } from "#phases/obtain-status-effect-phase";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import type { PostAttackContactApplyStatusEffectAbAttr } from "#types/ability-types";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
describe("Status Effects - General", () => {
|
||||
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
|
||||
.battleStyle("single")
|
||||
.enemyLevel(5)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.ability(AbilityId.BALL_FETCH);
|
||||
});
|
||||
|
||||
test("multiple status effects from the same interaction should not overwrite each other", async () => {
|
||||
game.override.ability(AbilityId.POISON_TOUCH).moveset([MoveId.NUZZLE]);
|
||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
||||
|
||||
// Force poison touch to always apply
|
||||
vi.spyOn(
|
||||
allAbilities[AbilityId.POISON_TOUCH].getAttrs(
|
||||
"PostAttackContactApplyStatusEffectAbAttr",
|
||||
// expose chance, which is private, for testing purpose, but keep type safety otherwise
|
||||
)[0] as unknown as Omit<PostAttackContactApplyStatusEffectAbAttr, "chance"> & { chance: number },
|
||||
"chance",
|
||||
"get",
|
||||
).mockReturnValue(100);
|
||||
const statusEffectPhaseSpy = vi.spyOn(ObtainStatusEffectPhase.prototype, "start");
|
||||
|
||||
game.move.select(MoveId.NUZZLE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(statusEffectPhaseSpy).toHaveBeenCalledOnce();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
// This test does not care which status effect is applied, as long as one is.
|
||||
expect(enemy.status?.effect).toBeOneOf([StatusEffect.POISON, StatusEffect.PARALYSIS]);
|
||||
});
|
||||
});
|
@ -1,6 +1,7 @@
|
||||
import { getGameMode } from "#app/game-mode";
|
||||
import overrides from "#app/overrides";
|
||||
import { BattleStyle } from "#enums/battle-style";
|
||||
import { Button } from "#enums/buttons";
|
||||
import { GameModes } from "#enums/game-modes";
|
||||
import { Nature } from "#enums/nature";
|
||||
import type { SpeciesId } from "#enums/species-id";
|
||||
@ -100,4 +101,33 @@ export class ClassicModeHelper extends GameManagerHelper {
|
||||
await this.game.phaseInterceptor.to(CommandPhase);
|
||||
console.log("==================[New Turn]==================");
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue inputs to switch at the start of the next battle, and then start it.
|
||||
* @param pokemonIndex - The 0-indexed position of the party pokemon to switch to.
|
||||
* Should never be called with 0 as that will select the currently active pokemon and freeze
|
||||
* @returns A Promise that resolves once the battle has been started and the switch prompt resolved
|
||||
* @todo Make this work for double battles
|
||||
* @example
|
||||
* ```ts
|
||||
* await game.classicMode.runToSummon([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA])
|
||||
* await game.startBattleWithSwitch(1);
|
||||
* ```
|
||||
*/
|
||||
public async startBattleWithSwitch(pokemonIndex: number): Promise<void> {
|
||||
this.game.scene.battleStyle = BattleStyle.SWITCH;
|
||||
this.game.onNextPrompt(
|
||||
"CheckSwitchPhase",
|
||||
UiMode.CONFIRM,
|
||||
() => {
|
||||
this.game.scene.ui.getHandler().setCursor(0);
|
||||
this.game.scene.ui.getHandler().processInput(Button.ACTION);
|
||||
},
|
||||
() => this.game.isCurrentPhase("CommandPhase") || this.game.isCurrentPhase("TurnInitPhase"),
|
||||
);
|
||||
this.game.doSelectPartyPokemon(pokemonIndex);
|
||||
|
||||
await this.game.phaseInterceptor.to("CommandPhase");
|
||||
console.log("==================[New Battle (Initial Switch)]==================");
|
||||
}
|
||||
}
|
||||
|
@ -111,7 +111,7 @@ class FakeMobile {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const event = new Event("touchstart");
|
||||
const event = new Event("pointerdown");
|
||||
node.dispatchEvent(event);
|
||||
}
|
||||
|
||||
@ -120,7 +120,7 @@ class FakeMobile {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const event = new Event("touchend");
|
||||
const event = new Event("pointerup");
|
||||
node.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import type { BattleScene } from "#app/battle-scene";
|
||||
import { Phase } from "#app/phase";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
import { AttemptCapturePhase } from "#phases/attempt-capture-phase";
|
||||
import { AttemptRunPhase } from "#phases/attempt-run-phase";
|
||||
import { BattleEndPhase } from "#phases/battle-end-phase";
|
||||
import { BerryPhase } from "#phases/berry-phase";
|
||||
@ -183,6 +184,7 @@ export class PhaseInterceptor {
|
||||
PostGameOverPhase,
|
||||
RevivalBlessingPhase,
|
||||
PokemonHealPhase,
|
||||
AttemptCapturePhase,
|
||||
];
|
||||
|
||||
private endBySetMode = [
|
||||
|
235
test/ui/transfer-item-options.test.ts
Normal file
@ -0,0 +1,235 @@
|
||||
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);
|
||||
});
|
||||
});
|
39
test/utils/data.test.ts
Normal file
@ -0,0 +1,39 @@
|
||||
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;
|
||||
});
|
||||
});
|
||||
});
|
@ -59,5 +59,12 @@
|
||||
"outDir": "./build",
|
||||
"noEmit": true
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "vite.config.ts", "vitest.config.ts", "vitest.workspace.ts"]
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"vite.config.ts",
|
||||
"vitest.config.ts",
|
||||
"vitest.workspace.ts",
|
||||
"public/service-worker.js"
|
||||
]
|
||||
}
|
||||
|