mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-09-23 23:13:42 +02:00
Merge pull request #6401 from pagefaultgames/hotfix-1.10.3
Hotfix 1.10.3 to main
This commit is contained in:
commit
6ef57e52e7
2
.github/workflows/github-pages.yml
vendored
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.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit a73ea68fdda09bb5018f524cbe6b7e73a3ddf4e0
|
||||
Subproject commit 102cbdcd924e2a7cdc7eab64d1ce79f6ec7604ff
|
@ -970,6 +970,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 +1081,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 +1261,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3014,41 +3012,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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -4803,7 +4811,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 +4963,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
if (overrideStatus) {
|
||||
this.resetStatus(false);
|
||||
} else {
|
||||
this.turnData.pendingStatus = effect;
|
||||
}
|
||||
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
@ -4974,6 +4984,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 +4994,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 +5005,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 +5016,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 +5025,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:
|
||||
|
@ -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
|
||||
|
@ -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")) {
|
||||
|
@ -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);
|
||||
|
@ -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]);
|
||||
}
|
||||
|
||||
|
@ -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]);
|
||||
}
|
||||
|
||||
|
@ -2041,12 +2041,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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 () => {
|
||||
|
60
test/status-effects/general-status-effect.test.ts
Normal file
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)]==================");
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user