Merge branch 'beta' into move/fairylock

This commit is contained in:
Tempoanon 2024-11-02 22:56:01 -04:00 committed by GitHub
commit 3440d2d52f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 682 additions and 317 deletions

@ -1 +1 @@
Subproject commit 3cf6d553541d79ba165387bc73fb06544d00f1f9 Subproject commit fc4a1effd5170def3c8314208a52cd0d8e6913ef

View File

@ -323,6 +323,7 @@ export default class BattleScene extends SceneBase {
this.conditionalQueue = []; this.conditionalQueue = [];
this.phaseQueuePrependSpliceIndex = -1; this.phaseQueuePrependSpliceIndex = -1;
this.nextCommandPhaseQueue = []; this.nextCommandPhaseQueue = [];
this.eventManager = new TimedEventManager();
this.updateGameInfo(); this.updateGameInfo();
} }
@ -378,7 +379,6 @@ export default class BattleScene extends SceneBase {
this.fieldSpritePipeline = new FieldSpritePipeline(this.game); this.fieldSpritePipeline = new FieldSpritePipeline(this.game);
(this.renderer as Phaser.Renderer.WebGL.WebGLRenderer).pipelines.add("FieldSprite", this.fieldSpritePipeline); (this.renderer as Phaser.Renderer.WebGL.WebGLRenderer).pipelines.add("FieldSprite", this.fieldSpritePipeline);
this.eventManager = new TimedEventManager();
this.launchBattle(); this.launchBattle();
} }

View File

@ -1049,31 +1049,80 @@ export enum MoveEffectTrigger {
POST_TARGET, POST_TARGET,
} }
interface MoveEffectAttrOptions {
/**
* Defines when this effect should trigger in the move's effect order
* @see {@linkcode MoveEffectPhase}
*/
trigger?: MoveEffectTrigger;
/** Should this effect only apply on the first hit? */
firstHitOnly?: boolean;
/** Should this effect only apply on the last hit? */
lastHitOnly?: boolean;
/** Should this effect only apply on the first target hit? */
firstTargetOnly?: boolean;
/** Overrides the secondary effect chance for this attr if set. */
effectChanceOverride?: number;
}
/** Base class defining all Move Effect Attributes /** Base class defining all Move Effect Attributes
* @extends MoveAttr * @extends MoveAttr
* @see {@linkcode apply} * @see {@linkcode apply}
*/ */
export class MoveEffectAttr extends MoveAttr { export class MoveEffectAttr extends MoveAttr {
/** Defines when this effect should trigger in the move's effect order /**
* @see {@linkcode phases.MoveEffectPhase.start} * A container for this attribute's optional parameters
* @see {@linkcode MoveEffectAttrOptions} for supported params.
*/ */
public trigger: MoveEffectTrigger; protected options?: MoveEffectAttrOptions;
/** Should this effect only apply on the first hit? */
public firstHitOnly: boolean;
/** Should this effect only apply on the last hit? */
public lastHitOnly: boolean;
/** Should this effect only apply on the first target hit? */
public firstTargetOnly: boolean;
/** Overrides the secondary effect chance for this attr if set. */
public effectChanceOverride?: number;
constructor(selfTarget?: boolean, trigger?: MoveEffectTrigger, firstHitOnly: boolean = false, lastHitOnly: boolean = false, firstTargetOnly: boolean = false, effectChanceOverride?: number) { constructor(selfTarget?: boolean, options?: MoveEffectAttrOptions) {
super(selfTarget); super(selfTarget);
this.trigger = trigger ?? MoveEffectTrigger.POST_APPLY; this.options = options;
this.firstHitOnly = firstHitOnly; }
this.lastHitOnly = lastHitOnly;
this.firstTargetOnly = firstTargetOnly; /**
this.effectChanceOverride = effectChanceOverride; * Defines when this effect should trigger in the move's effect order.
* @default MoveEffectTrigger.POST_APPLY
* @see {@linkcode MoveEffectTrigger}
*/
public get trigger () {
return this.options?.trigger ?? MoveEffectTrigger.POST_APPLY;
}
/**
* `true` if this effect should only trigger on the first hit of
* multi-hit moves.
* @default false
*/
public get firstHitOnly () {
return this.options?.firstHitOnly ?? false;
}
/**
* `true` if this effect should only trigger on the last hit of
* multi-hit moves.
* @default false
*/
public get lastHitOnly () {
return this.options?.lastHitOnly ?? false;
}
/**
* `true` if this effect should apply only upon hitting a target
* for the first time when targeting multiple {@linkcode Pokemon}.
* @default false
*/
public get firstTargetOnly () {
return this.options?.firstTargetOnly ?? false;
}
/**
* If defined, overrides the move's base chance for this
* secondary effect to trigger.
*/
public get effectChanceOverride () {
return this.options?.effectChanceOverride;
} }
/** /**
@ -1398,7 +1447,7 @@ export class RecoilAttr extends MoveEffectAttr {
private unblockable: boolean; private unblockable: boolean;
constructor(useHp: boolean = false, damageRatio: number = 0.25, unblockable: boolean = false) { constructor(useHp: boolean = false, damageRatio: number = 0.25, unblockable: boolean = false) {
super(true, MoveEffectTrigger.POST_APPLY, false, true); super(true, { lastHitOnly: true });
this.useHp = useHp; this.useHp = useHp;
this.damageRatio = damageRatio; this.damageRatio = damageRatio;
@ -1456,7 +1505,7 @@ export class RecoilAttr extends MoveEffectAttr {
**/ **/
export class SacrificialAttr extends MoveEffectAttr { export class SacrificialAttr extends MoveEffectAttr {
constructor() { constructor() {
super(true, MoveEffectTrigger.POST_TARGET); super(true, { trigger: MoveEffectTrigger.POST_TARGET });
} }
/** /**
@ -1489,7 +1538,7 @@ export class SacrificialAttr extends MoveEffectAttr {
**/ **/
export class SacrificialAttrOnHit extends MoveEffectAttr { export class SacrificialAttrOnHit extends MoveEffectAttr {
constructor() { constructor() {
super(true, MoveEffectTrigger.HIT); super(true, { trigger: MoveEffectTrigger.HIT });
} }
/** /**
@ -1528,7 +1577,7 @@ export class SacrificialAttrOnHit extends MoveEffectAttr {
*/ */
export class HalfSacrificialAttr extends MoveEffectAttr { export class HalfSacrificialAttr extends MoveEffectAttr {
constructor() { constructor() {
super(true, MoveEffectTrigger.POST_TARGET); super(true, { trigger: MoveEffectTrigger.POST_TARGET });
} }
/** /**
@ -1932,7 +1981,7 @@ export class HitHealAttr extends MoveEffectAttr {
private healStat: EffectiveStat | null; private healStat: EffectiveStat | null;
constructor(healRatio?: number | null, healStat?: EffectiveStat) { constructor(healRatio?: number | null, healStat?: EffectiveStat) {
super(true, MoveEffectTrigger.HIT); super(true, { trigger: MoveEffectTrigger.HIT });
this.healRatio = healRatio ?? 0.5; this.healRatio = healRatio ?? 0.5;
this.healStat = healStat ?? null; this.healStat = healStat ?? null;
@ -2141,7 +2190,7 @@ export class StatusEffectAttr extends MoveEffectAttr {
public overrideStatus: boolean = false; public overrideStatus: boolean = false;
constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) { constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) {
super(selfTarget, MoveEffectTrigger.HIT); super(selfTarget, { trigger: MoveEffectTrigger.HIT });
this.effect = effect; this.effect = effect;
this.turnsRemaining = turnsRemaining; this.turnsRemaining = turnsRemaining;
@ -2214,7 +2263,7 @@ export class MultiStatusEffectAttr extends StatusEffectAttr {
export class PsychoShiftEffectAttr extends MoveEffectAttr { export class PsychoShiftEffectAttr extends MoveEffectAttr {
constructor() { constructor() {
super(false, MoveEffectTrigger.HIT); super(false, { trigger: MoveEffectTrigger.HIT });
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
@ -2251,7 +2300,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
private chance: number; private chance: number;
constructor(chance: number) { constructor(chance: number) {
super(false, MoveEffectTrigger.HIT); super(false, { trigger: MoveEffectTrigger.HIT });
this.chance = chance; this.chance = chance;
} }
@ -2312,7 +2361,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
private berriesOnly: boolean; private berriesOnly: boolean;
constructor(berriesOnly: boolean) { constructor(berriesOnly: boolean) {
super(false, MoveEffectTrigger.HIT); super(false, { trigger: MoveEffectTrigger.HIT });
this.berriesOnly = berriesOnly; this.berriesOnly = berriesOnly;
} }
@ -2386,7 +2435,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
export class EatBerryAttr extends MoveEffectAttr { export class EatBerryAttr extends MoveEffectAttr {
protected chosenBerry: BerryModifier | undefined; protected chosenBerry: BerryModifier | undefined;
constructor() { constructor() {
super(true, MoveEffectTrigger.HIT); super(true, { trigger: MoveEffectTrigger.HIT });
} }
/** /**
* Causes the target to eat a berry. * Causes the target to eat a berry.
@ -2489,7 +2538,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
* @param ...effects - List of status effects to cure * @param ...effects - List of status effects to cure
*/ */
constructor(selfTarget: boolean, ...effects: StatusEffect[]) { constructor(selfTarget: boolean, ...effects: StatusEffect[]) {
super(selfTarget, MoveEffectTrigger.POST_APPLY, false, true); super(selfTarget, { lastHitOnly: true });
this.effects = effects; this.effects = effects;
} }
@ -2819,35 +2868,67 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
} }
} }
/**
* Set of optional parameters that may be applied to stat stage changing effects
* @extends MoveEffectAttrOptions
* @see {@linkcode StatStageChangeAttr}
*/
interface StatStageChangeAttrOptions extends MoveEffectAttrOptions {
/** If defined, needs to be met in order for the stat change to apply */
condition?: MoveConditionFunc,
/** `true` to display a message */
showMessage?: boolean
}
/** /**
* Attribute used for moves that change stat stages * Attribute used for moves that change stat stages
* *
* @param stats {@linkcode BattleStat} Array of stat(s) to change * @param stats {@linkcode BattleStat} Array of stat(s) to change
* @param stages How many stages to change the stat(s) by, [-6, 6] * @param stages How many stages to change the stat(s) by, [-6, 6]
* @param selfTarget `true` if the move is self-targetting * @param selfTarget `true` if the move is self-targetting
* @param condition {@linkcode MoveConditionFunc} Optional condition to be checked in order to apply the changes * @param options {@linkcode StatStageChangeAttrOptions} Container for any optional parameters for this attribute.
* @param showMessage `true` to display a message; default `true`
* @param firstHitOnly `true` if only the first hit of a multi hit move should cause a stat stage change; default `false`
* @param moveEffectTrigger {@linkcode MoveEffectTrigger} When the stat change should trigger; default {@linkcode MoveEffectTrigger.HIT}
* @param firstTargetOnly `true` if a move that hits multiple pokemon should only trigger the stat change if it hits at least one pokemon, rather than once per hit pokemon; default `false`
* @param lastHitOnly `true` if the effect should only apply after the last hit of a multi hit move; default `false`
* @param effectChanceOverride Will override the move's normal secondary effect chance if specified
* *
* @extends MoveEffectAttr * @extends MoveEffectAttr
* @see {@linkcode apply} * @see {@linkcode apply}
*/ */
export class StatStageChangeAttr extends MoveEffectAttr { export class StatStageChangeAttr extends MoveEffectAttr {
public stats: BattleStat[]; public stats: BattleStat[];
public stages: integer; public stages: number;
private condition?: MoveConditionFunc | null; /**
private showMessage: boolean; * Container for optional parameters to this attribute.
* @see {@linkcode StatStageChangeAttrOptions} for available optional params
*/
protected override options?: StatStageChangeAttrOptions;
constructor(stats: BattleStat[], stages: integer, selfTarget?: boolean, condition?: MoveConditionFunc | null, showMessage: boolean = true, firstHitOnly: boolean = false, moveEffectTrigger: MoveEffectTrigger = MoveEffectTrigger.HIT, firstTargetOnly: boolean = false, lastHitOnly: boolean = false, effectChanceOverride?: number) { constructor(stats: BattleStat[], stages: number, selfTarget?: boolean, options?: StatStageChangeAttrOptions) {
super(selfTarget, moveEffectTrigger, firstHitOnly, lastHitOnly, firstTargetOnly, effectChanceOverride); super(selfTarget, options);
this.stats = stats; this.stats = stats;
this.stages = stages; this.stages = stages;
this.condition = condition; this.options = options;
this.showMessage = showMessage; }
/**
* The condition required for the stat stage change to apply.
* Defaults to `null` (i.e. no condition required).
*/
private get condition () {
return this.options?.condition ?? null;
}
/**
* `true` to display a message for the stat change.
* @default true
*/
private get showMessage () {
return this.options?.showMessage ?? true;
}
/**
* Indicates when the stat change should trigger
* @default MoveEffectTrigger.HIT
*/
public override get trigger () {
return this.options?.trigger ?? MoveEffectTrigger.HIT;
} }
/** /**
@ -2932,20 +3013,6 @@ export class SecretPowerAttr extends MoveEffectAttr {
super(false); super(false);
} }
/**
* Used to determine if the move should apply a secondary effect based on Secret Power's 30% chance
* @returns `true` if the move's secondary effect should apply
*/
override canApply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
this.effectChanceOverride = move.chance;
const moveChance = this.getMoveChance(user, target, move, this.selfTarget);
if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) {
return true;
} else {
return false;
}
}
/** /**
* Used to apply the secondary effect to the target Pokemon * Used to apply the secondary effect to the target Pokemon
* @returns `true` if a secondary effect is successfully applied * @returns `true` if a secondary effect is successfully applied
@ -2962,8 +3029,6 @@ export class SecretPowerAttr extends MoveEffectAttr {
const biome = user.scene.arena.biomeType; const biome = user.scene.arena.biomeType;
secondaryEffect = this.determineBiomeEffect(biome); secondaryEffect = this.determineBiomeEffect(biome);
} }
// effectChanceOverride used in the application of the actual secondary effect
secondaryEffect.effectChanceOverride = 100;
return secondaryEffect.apply(user, target, move, []); return secondaryEffect.apply(user, target, move, []);
} }
@ -3139,7 +3204,7 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr {
private messageCallback: ((user: Pokemon) => void) | undefined; private messageCallback: ((user: Pokemon) => void) | undefined;
constructor(stat: BattleStat[], levels: integer, cutRatio: integer, messageCallback?: ((user: Pokemon) => void) | undefined) { constructor(stat: BattleStat[], levels: integer, cutRatio: integer, messageCallback?: ((user: Pokemon) => void) | undefined) {
super(stat, levels, true, null, true); super(stat, levels, true);
this.cutRatio = cutRatio; this.cutRatio = cutRatio;
this.messageCallback = messageCallback; this.messageCallback = messageCallback;
@ -4889,7 +4954,7 @@ export class BypassRedirectAttr extends MoveAttr {
export class FrenzyAttr extends MoveEffectAttr { export class FrenzyAttr extends MoveEffectAttr {
constructor() { constructor() {
super(true, MoveEffectTrigger.HIT, false, true); super(true, { trigger: MoveEffectTrigger.HIT, lastHitOnly: true });
} }
canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]) { canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]) {
@ -4962,7 +5027,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
private failOnOverlap: boolean; private failOnOverlap: boolean;
constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: integer = 0, turnCountMax?: integer, lastHitOnly: boolean = false, cancelOnFail: boolean = false) { constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: integer = 0, turnCountMax?: integer, lastHitOnly: boolean = false, cancelOnFail: boolean = false) {
super(selfTarget, MoveEffectTrigger.POST_APPLY, false, lastHitOnly); super(selfTarget, { lastHitOnly: lastHitOnly });
this.tagType = tagType; this.tagType = tagType;
this.turnCountMin = turnCountMin; this.turnCountMin = turnCountMin;
@ -5397,7 +5462,7 @@ export class AddArenaTagAttr extends MoveEffectAttr {
public selfSideTarget: boolean; public selfSideTarget: boolean;
constructor(tagType: ArenaTagType, turnCount?: integer | null, failOnOverlap: boolean = false, selfSideTarget: boolean = false) { constructor(tagType: ArenaTagType, turnCount?: integer | null, failOnOverlap: boolean = false, selfSideTarget: boolean = false) {
super(true, MoveEffectTrigger.POST_APPLY); super(true);
this.tagType = tagType; this.tagType = tagType;
this.turnCount = turnCount!; // TODO: is the bang correct? this.turnCount = turnCount!; // TODO: is the bang correct?
@ -5435,7 +5500,7 @@ export class RemoveArenaTagsAttr extends MoveEffectAttr {
public selfSideTarget: boolean; public selfSideTarget: boolean;
constructor(tagTypes: ArenaTagType[], selfSideTarget: boolean) { constructor(tagTypes: ArenaTagType[], selfSideTarget: boolean) {
super(true, MoveEffectTrigger.POST_APPLY); super(true);
this.tagTypes = tagTypes; this.tagTypes = tagTypes;
this.selfSideTarget = selfSideTarget; this.selfSideTarget = selfSideTarget;
@ -5501,7 +5566,7 @@ export class RemoveArenaTrapAttr extends MoveEffectAttr {
private targetBothSides: boolean; private targetBothSides: boolean;
constructor(targetBothSides: boolean = false) { constructor(targetBothSides: boolean = false) {
super(true, MoveEffectTrigger.PRE_APPLY); super(true, { trigger: MoveEffectTrigger.PRE_APPLY });
this.targetBothSides = targetBothSides; this.targetBothSides = targetBothSides;
} }
@ -5537,7 +5602,7 @@ export class RemoveScreensAttr extends MoveEffectAttr {
private targetBothSides: boolean; private targetBothSides: boolean;
constructor(targetBothSides: boolean = false) { constructor(targetBothSides: boolean = false) {
super(true, MoveEffectTrigger.PRE_APPLY); super(true, { trigger: MoveEffectTrigger.PRE_APPLY });
this.targetBothSides = targetBothSides; this.targetBothSides = targetBothSides;
} }
@ -5575,7 +5640,7 @@ export class SwapArenaTagsAttr extends MoveEffectAttr {
constructor(SwapTags: ArenaTagType[]) { constructor(SwapTags: ArenaTagType[]) {
super(true, MoveEffectTrigger.POST_APPLY); super(true);
this.SwapTags = SwapTags; this.SwapTags = SwapTags;
} }
@ -5701,7 +5766,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
private selfSwitch: boolean = false, private selfSwitch: boolean = false,
private switchType: SwitchType = SwitchType.SWITCH private switchType: SwitchType = SwitchType.SWITCH
) { ) {
super(false, MoveEffectTrigger.POST_APPLY, false, true); super(false, { lastHitOnly: true });
} }
isBatonPass() { isBatonPass() {
@ -5750,6 +5815,11 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
return false; return false;
} }
// Don't allow wild mons to flee with U-turn et al
if (this.selfSwitch && !user.isPlayer() && move.category !== MoveCategory.STATUS) {
return false;
}
if (switchOutTarget.hp > 0) { if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(false); switchOutTarget.leaveField(false);
user.scene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500); user.scene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500);
@ -5834,7 +5904,6 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
} }
} }
export class ChillyReceptionAttr extends ForceSwitchOutAttr { export class ChillyReceptionAttr extends ForceSwitchOutAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
user.scene.arena.trySetWeather(WeatherType.SNOW, true); user.scene.arena.trySetWeather(WeatherType.SNOW, true);
@ -5852,7 +5921,7 @@ export class RemoveTypeAttr extends MoveEffectAttr {
private messageCallback: ((user: Pokemon) => void) | undefined; private messageCallback: ((user: Pokemon) => void) | undefined;
constructor(removedType: Type, messageCallback?: (user: Pokemon) => void) { constructor(removedType: Type, messageCallback?: (user: Pokemon) => void) {
super(true, MoveEffectTrigger.POST_TARGET); super(true, { trigger: MoveEffectTrigger.POST_TARGET });
this.removedType = removedType; this.removedType = removedType;
this.messageCallback = messageCallback; this.messageCallback = messageCallback;
@ -5921,22 +5990,114 @@ export class CopyBiomeTypeAttr extends MoveEffectAttr {
return false; return false;
} }
const biomeType = user.scene.arena.getTypeForBiome(); const terrainType = user.scene.arena.getTerrainType();
let typeChange: Type;
if (terrainType !== TerrainType.NONE) {
typeChange = this.getTypeForTerrain(user.scene.arena.getTerrainType());
} else {
typeChange = this.getTypeForBiome(user.scene.arena.biomeType);
}
user.summonData.types = [ biomeType ]; user.summonData.types = [ typeChange ];
user.updateInfo(); user.updateInfo();
user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), typeName: i18next.t(`pokemonInfo:Type.${Type[biomeType]}`) })); user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), typeName: i18next.t(`pokemonInfo:Type.${Type[typeChange]}`) }));
return true; return true;
} }
/**
* Retrieves a type from the current terrain
* @param terrainType {@linkcode TerrainType}
* @returns {@linkcode Type}
*/
private getTypeForTerrain(terrainType: TerrainType): Type {
switch (terrainType) {
case TerrainType.ELECTRIC:
return Type.ELECTRIC;
case TerrainType.MISTY:
return Type.FAIRY;
case TerrainType.GRASSY:
return Type.GRASS;
case TerrainType.PSYCHIC:
return Type.PSYCHIC;
case TerrainType.NONE:
default:
return Type.UNKNOWN;
}
}
/**
* Retrieves a type from the current biome
* @param biomeType {@linkcode Biome}
* @returns {@linkcode Type}
*/
private getTypeForBiome(biomeType: Biome): Type {
switch (biomeType) {
case Biome.TOWN:
case Biome.PLAINS:
case Biome.METROPOLIS:
return Type.NORMAL;
case Biome.GRASS:
case Biome.TALL_GRASS:
return Type.GRASS;
case Biome.FOREST:
case Biome.JUNGLE:
return Type.BUG;
case Biome.SLUM:
case Biome.SWAMP:
return Type.POISON;
case Biome.SEA:
case Biome.BEACH:
case Biome.LAKE:
case Biome.SEABED:
return Type.WATER;
case Biome.MOUNTAIN:
return Type.FLYING;
case Biome.BADLANDS:
return Type.GROUND;
case Biome.CAVE:
case Biome.DESERT:
return Type.ROCK;
case Biome.ICE_CAVE:
case Biome.SNOWY_FOREST:
return Type.ICE;
case Biome.MEADOW:
case Biome.FAIRY_CAVE:
case Biome.ISLAND:
return Type.FAIRY;
case Biome.POWER_PLANT:
return Type.ELECTRIC;
case Biome.VOLCANO:
return Type.FIRE;
case Biome.GRAVEYARD:
case Biome.TEMPLE:
return Type.GHOST;
case Biome.DOJO:
case Biome.CONSTRUCTION_SITE:
return Type.FIGHTING;
case Biome.FACTORY:
case Biome.LABORATORY:
return Type.STEEL;
case Biome.RUINS:
case Biome.SPACE:
return Type.PSYCHIC;
case Biome.WASTELAND:
case Biome.END:
return Type.DRAGON;
case Biome.ABYSS:
return Type.DARK;
default:
return Type.UNKNOWN;
}
}
} }
export class ChangeTypeAttr extends MoveEffectAttr { export class ChangeTypeAttr extends MoveEffectAttr {
private type: Type; private type: Type;
constructor(type: Type) { constructor(type: Type) {
super(false, MoveEffectTrigger.HIT); super(false, { trigger: MoveEffectTrigger.HIT });
this.type = type; this.type = type;
} }
@ -5959,7 +6120,7 @@ export class AddTypeAttr extends MoveEffectAttr {
private type: Type; private type: Type;
constructor(type: Type) { constructor(type: Type) {
super(false, MoveEffectTrigger.HIT); super(false, { trigger: MoveEffectTrigger.HIT });
this.type = type; this.type = type;
} }
@ -6486,7 +6647,7 @@ export class AbilityChangeAttr extends MoveEffectAttr {
public ability: Abilities; public ability: Abilities;
constructor(ability: Abilities, selfTarget?: boolean) { constructor(ability: Abilities, selfTarget?: boolean) {
super(selfTarget, MoveEffectTrigger.HIT); super(selfTarget, { trigger: MoveEffectTrigger.HIT });
this.ability = ability; this.ability = ability;
} }
@ -6515,7 +6676,7 @@ export class AbilityCopyAttr extends MoveEffectAttr {
public copyToPartner: boolean; public copyToPartner: boolean;
constructor(copyToPartner: boolean = false) { constructor(copyToPartner: boolean = false) {
super(false, MoveEffectTrigger.HIT); super(false, { trigger: MoveEffectTrigger.HIT });
this.copyToPartner = copyToPartner; this.copyToPartner = copyToPartner;
} }
@ -6554,7 +6715,7 @@ export class AbilityGiveAttr extends MoveEffectAttr {
public copyToPartner: boolean; public copyToPartner: boolean;
constructor() { constructor() {
super(false, MoveEffectTrigger.HIT); super(false, { trigger: MoveEffectTrigger.HIT });
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
@ -6866,7 +7027,7 @@ export class DiscourageFrequentUseAttr extends MoveAttr {
export class MoneyAttr extends MoveEffectAttr { export class MoneyAttr extends MoveEffectAttr {
constructor() { constructor() {
super(true, MoveEffectTrigger.HIT, true); super(true, { trigger: MoveEffectTrigger.HIT, firstHitOnly: true });
} }
apply(user: Pokemon, target: Pokemon, move: Move): boolean { apply(user: Pokemon, target: Pokemon, move: Move): boolean {
@ -6883,7 +7044,7 @@ export class MoneyAttr extends MoveEffectAttr {
*/ */
export class DestinyBondAttr extends MoveEffectAttr { export class DestinyBondAttr extends MoveEffectAttr {
constructor() { constructor() {
super(true, MoveEffectTrigger.PRE_APPLY); super(true, { trigger: MoveEffectTrigger.PRE_APPLY });
} }
/** /**
@ -6933,7 +7094,7 @@ export class StatusIfBoostedAttr extends MoveEffectAttr {
public effect: StatusEffect; public effect: StatusEffect;
constructor(effect: StatusEffect) { constructor(effect: StatusEffect) {
super(true, MoveEffectTrigger.HIT); super(true, { trigger: MoveEffectTrigger.HIT });
this.effect = effect; this.effect = effect;
} }
@ -7058,6 +7219,11 @@ const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target
const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.scene.phaseQueue.find(phase => phase instanceof MovePhase) !== undefined; const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.scene.phaseQueue.find(phase => phase instanceof MovePhase) !== undefined;
const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => {
const party: Pokemon[] = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty();
return party.some(pokemon => pokemon.isActive() && !pokemon.isOnField());
};
export type MoveAttrFilter = (attr: MoveAttr) => boolean; export type MoveAttrFilter = (attr: MoveAttr) => boolean;
function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise<void> { function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise<void> {
@ -7967,6 +8133,7 @@ export function initMoves() {
.attr(StatusEffectAttr, StatusEffect.PARALYSIS), .attr(StatusEffectAttr, StatusEffect.PARALYSIS),
new SelfStatusMove(Moves.BATON_PASS, Type.NORMAL, -1, 40, -1, 0, 2) new SelfStatusMove(Moves.BATON_PASS, Type.NORMAL, -1, 40, -1, 0, 2)
.attr(ForceSwitchOutAttr, true, SwitchType.BATON_PASS) .attr(ForceSwitchOutAttr, true, SwitchType.BATON_PASS)
.condition(failIfLastInPartyCondition)
.hidesUser(), .hidesUser(),
new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2) new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2)
.attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true) .attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true)
@ -8985,7 +9152,7 @@ export function initMoves() {
// If any fielded pokémon is grass-type and grounded. // If any fielded pokémon is grass-type and grounded.
return [ ...user.scene.getEnemyParty(), ...user.scene.getParty() ].some((poke) => poke.isOfType(Type.GRASS) && poke.isGrounded()); return [ ...user.scene.getEnemyParty(), ...user.scene.getParty() ].some((poke) => poke.isOfType(Type.GRASS) && poke.isGrounded());
}) })
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded()), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded() }),
new StatusMove(Moves.STICKY_WEB, Type.BUG, -1, 20, -1, 0, 6) new StatusMove(Moves.STICKY_WEB, Type.BUG, -1, 20, -1, 0, 6)
.attr(AddArenaTrapTagAttr, ArenaTagType.STICKY_WEB) .attr(AddArenaTrapTagAttr, ArenaTagType.STICKY_WEB)
.target(MoveTarget.ENEMY_SIDE), .target(MoveTarget.ENEMY_SIDE),
@ -9022,7 +9189,7 @@ export function initMoves() {
.soundBased() .soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6) new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, null, true, true, MoveEffectTrigger.PRE_APPLY) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, { trigger: MoveEffectTrigger.PRE_APPLY })
.attr(ForceSwitchOutAttr, true) .attr(ForceSwitchOutAttr, true)
.soundBased(), .soundBased(),
new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6) new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6)
@ -9037,7 +9204,7 @@ export function initMoves() {
.condition(failIfLastCondition), .condition(failIfLastCondition),
new StatusMove(Moves.FLOWER_SHIELD, Type.FAIRY, -1, 10, -1, 0, 6) new StatusMove(Moves.FLOWER_SHIELD, Type.FAIRY, -1, 10, -1, 0, 6)
.target(MoveTarget.ALL) .target(MoveTarget.ALL)
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag)), .attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, { condition: (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag) }),
new StatusMove(Moves.GRASSY_TERRAIN, Type.GRASS, -1, 10, -1, 0, 6) new StatusMove(Moves.GRASSY_TERRAIN, Type.GRASS, -1, 10, -1, 0, 6)
.attr(TerrainChangeAttr, TerrainType.GRASSY) .attr(TerrainChangeAttr, TerrainType.GRASSY)
.target(MoveTarget.BOTH_SIDES), .target(MoveTarget.BOTH_SIDES),
@ -9070,7 +9237,7 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.SPATK ], -1) .attr(StatStageChangeAttr, [ Stat.SPATK ], -1)
.soundBased(), .soundBased(),
new AttackMove(Moves.DIAMOND_STORM, Type.ROCK, MoveCategory.PHYSICAL, 100, 95, 5, 50, 0, 6) new AttackMove(Moves.DIAMOND_STORM, Type.ROCK, MoveCategory.PHYSICAL, 100, 95, 5, 50, 0, 6)
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, undefined, undefined, undefined, undefined, true) .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, { firstTargetOnly: true })
.makesContact(false) .makesContact(false)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.STEAM_ERUPTION, Type.WATER, MoveCategory.SPECIAL, 110, 95, 5, 30, 0, 6) new AttackMove(Moves.STEAM_ERUPTION, Type.WATER, MoveCategory.SPECIAL, 110, 95, 5, 30, 0, 6)
@ -9096,7 +9263,7 @@ export function initMoves() {
new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6) new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2), .attr(StatStageChangeAttr, [ Stat.SPATK ], -2),
new StatusMove(Moves.VENOM_DRENCH, Type.POISON, 100, 20, -1, 0, 6) new StatusMove(Moves.VENOM_DRENCH, Type.POISON, 100, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, { condition: (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC })
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6) new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6)
.ignoresSubstitute() .ignoresSubstitute()
@ -9107,7 +9274,7 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true) .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true)
.ignoresVirtual(), .ignoresVirtual(),
new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6) new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false))) .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, { condition: (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)) })
.ignoresSubstitute() .ignoresSubstitute()
.target(MoveTarget.USER_AND_ALLIES) .target(MoveTarget.USER_AND_ALLIES)
.condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p.hasAbility(a, false)))), .condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p.hasAbility(a, false)))),
@ -9327,7 +9494,7 @@ export function initMoves() {
new SelfStatusMove(Moves.LASER_FOCUS, Type.NORMAL, -1, 30, -1, 0, 7) new SelfStatusMove(Moves.LASER_FOCUS, Type.NORMAL, -1, 30, -1, 0, 7)
.attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false), .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false),
new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7) new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false))) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)) })
.ignoresSubstitute() .ignoresSubstitute()
.target(MoveTarget.USER_AND_ALLIES) .target(MoveTarget.USER_AND_ALLIES)
.condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p.hasAbility(a, false)))), .condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p.hasAbility(a, false)))),
@ -9384,7 +9551,7 @@ export function initMoves() {
.ballBombMove() .ballBombMove()
.makesContact(false), .makesContact(false),
new AttackMove(Moves.CLANGING_SCALES, Type.DRAGON, MoveCategory.SPECIAL, 110, 100, 5, -1, 0, 7) new AttackMove(Moves.CLANGING_SCALES, Type.DRAGON, MoveCategory.SPECIAL, 110, 100, 5, -1, 0, 7)
.attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, true) .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, { firstTargetOnly: true })
.soundBased() .soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.DRAGON_HAMMER, Type.DRAGON, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 7), new AttackMove(Moves.DRAGON_HAMMER, Type.DRAGON, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 7),
@ -9498,7 +9665,7 @@ export function initMoves() {
.makesContact(false) .makesContact(false)
.ignoresVirtual(), .ignoresVirtual(),
new AttackMove(Moves.CLANGOROUS_SOULBLAZE, Type.DRAGON, MoveCategory.SPECIAL, 185, -1, 1, 100, 0, 7) new AttackMove(Moves.CLANGOROUS_SOULBLAZE, Type.DRAGON, MoveCategory.SPECIAL, 185, -1, 1, 100, 0, 7)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true, undefined, undefined, undefined, undefined, true) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true, { firstTargetOnly: true })
.soundBased() .soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES) .target(MoveTarget.ALL_NEAR_ENEMIES)
.edgeCase() // I assume it needs clanging scales and Kommo-O .edgeCase() // I assume it needs clanging scales and Kommo-O
@ -9736,8 +9903,8 @@ export function initMoves() {
.attr(ClearTerrainAttr) .attr(ClearTerrainAttr)
.condition((user, target, move) => !!user.scene.arena.terrain), .condition((user, target, move) => !!user.scene.arena.terrain),
new AttackMove(Moves.SCALE_SHOT, Type.DRAGON, MoveCategory.PHYSICAL, 25, 90, 20, -1, 0, 8) new AttackMove(Moves.SCALE_SHOT, Type.DRAGON, MoveCategory.PHYSICAL, 25, 90, 20, -1, 0, 8)
.attr(StatStageChangeAttr, [ Stat.SPD ], 1, true, null, true, false, MoveEffectTrigger.HIT, false, true) .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true, { lastHitOnly: true })
.attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, false, true) .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, { lastHitOnly: true })
.attr(MultiHitAttr) .attr(MultiHitAttr)
.makesContact(false), .makesContact(false),
new ChargingAttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 8) new ChargingAttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 8)
@ -9871,7 +10038,7 @@ export function initMoves() {
new AttackMove(Moves.TRIPLE_ARROWS, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 10, 30, 0, 8) new AttackMove(Moves.TRIPLE_ARROWS, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 10, 30, 0, 8)
.makesContact(false) .makesContact(false)
.attr(HighCritAttr) .attr(HighCritAttr)
.attr(StatStageChangeAttr, [ Stat.DEF ], -1, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 50) .attr(StatStageChangeAttr, [ Stat.DEF ], -1, false, { effectChanceOverride: 50 })
.attr(FlinchAttr), .attr(FlinchAttr),
new AttackMove(Moves.INFERNAL_PARADE, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 15, 30, 0, 8) new AttackMove(Moves.INFERNAL_PARADE, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 15, 30, 0, 8)
.attr(StatusEffectAttr, StatusEffect.BURN) .attr(StatusEffectAttr, StatusEffect.BURN)
@ -10007,7 +10174,7 @@ export function initMoves() {
.attr(TeraMoveCategoryAttr) .attr(TeraMoveCategoryAttr)
.attr(TeraBlastTypeAttr) .attr(TeraBlastTypeAttr)
.attr(TeraBlastPowerAttr) .attr(TeraBlastPowerAttr)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR)) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, { condition: (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR) })
.partial(), /** Does not ignore abilities that affect stats, relevant in determining the move's category {@see TeraMoveCategoryAttr} */ .partial(), /** Does not ignore abilities that affect stats, relevant in determining the move's category {@see TeraMoveCategoryAttr} */
new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9) new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9)
.attr(ProtectAttr, BattlerTagType.SILK_TRAP) .attr(ProtectAttr, BattlerTagType.SILK_TRAP)
@ -10091,7 +10258,7 @@ export function initMoves() {
.attr(RemoveScreensAttr), .attr(RemoveScreensAttr),
new AttackMove(Moves.MAKE_IT_RAIN, Type.STEEL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9) new AttackMove(Moves.MAKE_IT_RAIN, Type.STEEL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9)
.attr(MoneyAttr) .attr(MoneyAttr)
.attr(StatStageChangeAttr, [ Stat.SPATK ], -1, true, null, true, false, MoveEffectTrigger.HIT, true) .attr(StatStageChangeAttr, [ Stat.SPATK ], -1, true, { firstTargetOnly: true })
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.PSYBLADE, Type.PSYCHIC, MoveCategory.PHYSICAL, 80, 100, 15, -1, 0, 9) new AttackMove(Moves.PSYBLADE, Type.PSYCHIC, MoveCategory.PHYSICAL, 80, 100, 15, -1, 0, 9)
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.ELECTRIC && user.isGrounded() ? 1.5 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.ELECTRIC && user.isGrounded() ? 1.5 : 1)
@ -10108,12 +10275,13 @@ export function initMoves() {
.makesContact(), .makesContact(),
new SelfStatusMove(Moves.SHED_TAIL, Type.NORMAL, -1, 10, -1, 0, 9) new SelfStatusMove(Moves.SHED_TAIL, Type.NORMAL, -1, 10, -1, 0, 9)
.attr(AddSubstituteAttr, 0.5) .attr(AddSubstituteAttr, 0.5)
.attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL), .attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL)
.condition(failIfLastInPartyCondition),
new SelfStatusMove(Moves.CHILLY_RECEPTION, Type.ICE, -1, 10, -1, 0, 9) new SelfStatusMove(Moves.CHILLY_RECEPTION, Type.ICE, -1, 10, -1, 0, 9)
.attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) })) .attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) }))
.attr(ChillyReceptionAttr, true), .attr(ChillyReceptionAttr, true),
new SelfStatusMove(Moves.TIDY_UP, Type.NORMAL, -1, 10, -1, 0, 9) new SelfStatusMove(Moves.TIDY_UP, Type.NORMAL, -1, 10, -1, 0, 9)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true, null, true, true) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true)
.attr(RemoveArenaTrapAttr, true) .attr(RemoveArenaTrapAttr, true)
.attr(RemoveAllSubstitutesAttr), .attr(RemoveAllSubstitutesAttr),
new StatusMove(Moves.SNOWSCAPE, Type.ICE, -1, 10, -1, 0, 9) new StatusMove(Moves.SNOWSCAPE, Type.ICE, -1, 10, -1, 0, 9)

View File

@ -1,7 +1,7 @@
import { EnemyPartyConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { EnemyPartyConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { trainerConfigs, TrainerPartyCompoundTemplate, TrainerPartyTemplate, } from "#app/data/trainer-config"; import { trainerConfigs, TrainerPartyCompoundTemplate, TrainerPartyTemplate, } from "#app/data/trainer-config";
import { ModifierTier } from "#app/modifier/modifier-tier"; import { ModifierTier } from "#app/modifier/modifier-tier";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { ModifierPoolType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PartyMemberStrength } from "#enums/party-member-strength"; import { PartyMemberStrength } from "#enums/party-member-strength";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
@ -280,7 +280,7 @@ export const ClowningAroundEncounter: MysteryEncounter =
let numRogue = 0; let numRogue = 0;
items.filter(m => m.isTransferable && !(m instanceof BerryModifier)) items.filter(m => m.isTransferable && !(m instanceof BerryModifier))
.forEach(m => { .forEach(m => {
const type = m.type.withTierFromPool(); const type = m.type.withTierFromPool(ModifierPoolType.PLAYER, party);
const tier = type.tier ?? ModifierTier.ULTRA; const tier = type.tier ?? ModifierTier.ULTRA;
if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) { if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) {
numRogue += m.stackCount; numRogue += m.stackCount;

View File

@ -418,7 +418,7 @@ export function generateModifierType(scene: BattleScene, modifier: () => Modifie
// Populates item id and tier (order matters) // Populates item id and tier (order matters)
result = result result = result
.withIdFromFunc(modifierTypes[modifierId]) .withIdFromFunc(modifierTypes[modifierId])
.withTierFromPool(); .withTierFromPool(ModifierPoolType.PLAYER, scene.getParty());
return result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result; return result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result;
} }

View File

@ -224,66 +224,6 @@ export class Arena {
return 0; return 0;
} }
getTypeForBiome() {
switch (this.biomeType) {
case Biome.TOWN:
case Biome.PLAINS:
case Biome.METROPOLIS:
return Type.NORMAL;
case Biome.GRASS:
case Biome.TALL_GRASS:
return Type.GRASS;
case Biome.FOREST:
case Biome.JUNGLE:
return Type.BUG;
case Biome.SLUM:
case Biome.SWAMP:
return Type.POISON;
case Biome.SEA:
case Biome.BEACH:
case Biome.LAKE:
case Biome.SEABED:
return Type.WATER;
case Biome.MOUNTAIN:
return Type.FLYING;
case Biome.BADLANDS:
return Type.GROUND;
case Biome.CAVE:
case Biome.DESERT:
return Type.ROCK;
case Biome.ICE_CAVE:
case Biome.SNOWY_FOREST:
return Type.ICE;
case Biome.MEADOW:
case Biome.FAIRY_CAVE:
case Biome.ISLAND:
return Type.FAIRY;
case Biome.POWER_PLANT:
return Type.ELECTRIC;
case Biome.VOLCANO:
return Type.FIRE;
case Biome.GRAVEYARD:
case Biome.TEMPLE:
return Type.GHOST;
case Biome.DOJO:
case Biome.CONSTRUCTION_SITE:
return Type.FIGHTING;
case Biome.FACTORY:
case Biome.LABORATORY:
return Type.STEEL;
case Biome.RUINS:
case Biome.SPACE:
return Type.PSYCHIC;
case Biome.WASTELAND:
case Biome.END:
return Type.DRAGON;
case Biome.ABYSS:
return Type.DARK;
default:
return Type.UNKNOWN;
}
}
getBgTerrainColorRatioForBiome(): number { getBgTerrainColorRatioForBiome(): number {
switch (this.biomeType) { switch (this.biomeType) {
case Biome.SPACE: case Biome.SPACE:

View File

@ -19,7 +19,7 @@ import { Unlockables } from "#app/system/unlockables";
import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system/voucher"; import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system/voucher";
import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler"; import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler";
import { getModifierTierTextTint } from "#app/ui/text"; import { getModifierTierTextTint } from "#app/ui/text";
import { formatMoney, getEnumKeys, getEnumValues, IntegerHolder, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils"; import { formatMoney, getEnumKeys, getEnumValues, IntegerHolder, isNullOrUndefined, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
@ -121,17 +121,40 @@ export class ModifierType {
* Populates item tier for ModifierType instance * Populates item tier for ModifierType instance
* Tier is a necessary field for items that appear in player shop (determines the Pokeball visual they use) * Tier is a necessary field for items that appear in player shop (determines the Pokeball visual they use)
* To find the tier, this function performs a reverse lookup of the item type in modifier pools * To find the tier, this function performs a reverse lookup of the item type in modifier pools
* It checks the weight of the item and will use the first tier for which the weight is greater than 0
* This is to allow items to be in multiple item pools depending on the conditions, for example for events
* If all tiers have a weight of 0 for the item, the first tier where the item was found is used
* @param poolType Default 'ModifierPoolType.PLAYER'. Which pool to lookup item tier from * @param poolType Default 'ModifierPoolType.PLAYER'. Which pool to lookup item tier from
* @param party optional. Needed to check the weight of modifiers with conditional weight (see {@linkcode WeightedModifierTypeWeightFunc})
* if not provided or empty, the weight check will be ignored
* @param rerollCount Default `0`. Used to check the weight of modifiers with conditional weight (see {@linkcode WeightedModifierTypeWeightFunc})
*/ */
withTierFromPool(poolType: ModifierPoolType = ModifierPoolType.PLAYER): ModifierType { withTierFromPool(poolType: ModifierPoolType = ModifierPoolType.PLAYER, party?: PlayerPokemon[], rerollCount: number = 0): ModifierType {
let defaultTier: undefined | ModifierTier;
for (const tier of Object.values(getModifierPoolForType(poolType))) { for (const tier of Object.values(getModifierPoolForType(poolType))) {
for (const modifier of tier) { for (const modifier of tier) {
if (this.id === modifier.modifierType.id) { if (this.id === modifier.modifierType.id) {
let weight: number;
if (modifier.weight instanceof Function) {
weight = party ? modifier.weight(party, rerollCount) : 0;
} else {
weight = modifier.weight;
}
if (weight > 0) {
this.tier = modifier.modifierType.tier; this.tier = modifier.modifierType.tier;
return this; return this;
} else if (isNullOrUndefined(defaultTier)) {
// If weight is 0, keep track of the first tier where the item was found
defaultTier = modifier.modifierType.tier;
} }
} }
} }
}
// Didn't find a pool with weight > 0, fallback to first tier where the item was found, if any
if (defaultTier) {
this.tier = defaultTier;
}
return this; return this;
} }
@ -2117,7 +2140,7 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo
// Populates item id and tier // Populates item id and tier
guaranteedMod = guaranteedMod guaranteedMod = guaranteedMod
.withIdFromFunc(modifierTypes[modifierId]) .withIdFromFunc(modifierTypes[modifierId])
.withTierFromPool(); .withTierFromPool(ModifierPoolType.PLAYER, party);
const modType = guaranteedMod instanceof ModifierTypeGenerator ? guaranteedMod.generateType(party) : guaranteedMod; const modType = guaranteedMod instanceof ModifierTypeGenerator ? guaranteedMod.generateType(party) : guaranteedMod;
if (modType) { if (modType) {
@ -2186,7 +2209,7 @@ export function overridePlayerModifierTypeOptions(options: ModifierTypeOption[],
} }
if (modifierType) { if (modifierType) {
options[i].type = modifierType.withIdFromFunc(modifierFunc).withTierFromPool(); options[i].type = modifierType.withIdFromFunc(modifierFunc).withTierFromPool(ModifierPoolType.PLAYER, party);
} }
} }
} }

View File

@ -1,20 +1,62 @@
import BattleScene from "#app/battle-scene";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, applyPostDefendAbAttrs, PostDefendAbAttr, applyPostAttackAbAttrs, PostAttackAbAttr, MaxMultiHitAbAttr, AlwaysHitAbAttr, TypeImmunityAbAttr } from "#app/data/ability"; import BattleScene from "#app/battle-scene";
import {
AddSecondStrikeAbAttr,
AlwaysHitAbAttr,
applyPostAttackAbAttrs,
applyPostDefendAbAttrs,
applyPreAttackAbAttrs,
IgnoreMoveEffectsAbAttr,
MaxMultiHitAbAttr,
PostAttackAbAttr,
PostDefendAbAttr,
TypeImmunityAbAttr,
} from "#app/data/ability";
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
import { MoveAnim } from "#app/data/battle-anims"; import { MoveAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags"; import {
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, OneHitKOAttr, MoveEffectTrigger, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } from "#app/data/move"; BattlerTagLapseType,
DamageProtectedTag,
ProtectedTag,
SemiInvulnerableTag,
SubstituteTag,
} from "#app/data/battler-tags";
import {
applyFilteredMoveAttrs,
applyMoveAttrs,
AttackMove,
FixedDamageAttr,
HitsTagAttr,
MissEffectAttr,
MoveAttr,
MoveCategory,
MoveEffectAttr,
MoveEffectTrigger,
MoveFlags,
MoveTarget,
MultiHitAttr,
NoEffectAttr,
OneHitKOAttr,
OverrideMoveEffectAttr,
ToxicAccuracyAttr,
VariableTargetAttr,
} from "#app/data/move";
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Moves } from "#app/enums/moves";
import Pokemon, { PokemonMove, MoveResult, HitResult } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonMultiHitModifier, FlinchChanceModifier, EnemyAttackStatusEffectChanceModifier, ContactHeldItemTransferChanceModifier, HitHealModifier } from "#app/modifier/modifier";
import i18next from "i18next";
import * as Utils from "#app/utils";
import { PokemonPhase } from "./pokemon-phase";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import Pokemon, { HitResult, MoveResult, PokemonMove } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import {
ContactHeldItemTransferChanceModifier,
EnemyAttackStatusEffectChanceModifier,
FlinchChanceModifier,
HitHealModifier,
PokemonMultiHitModifier,
} from "#app/modifier/modifier";
import { BooleanHolder, executeIf, NumberHolder } from "#app/utils";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import i18next from "i18next";
import { PokemonPhase } from "./pokemon-phase";
export class MoveEffectPhase extends PokemonPhase { export class MoveEffectPhase extends PokemonPhase {
public move: PokemonMove; public move: PokemonMove;
@ -35,7 +77,7 @@ export class MoveEffectPhase extends PokemonPhase {
this.targets = targets; this.targets = targets;
} }
start() { public override start(): void {
super.start(); super.start();
/** The Pokemon using this phase's invoked move */ /** The Pokemon using this phase's invoked move */
@ -52,12 +94,12 @@ export class MoveEffectPhase extends PokemonPhase {
* Does an effect from this move override other effects on this turn? * Does an effect from this move override other effects on this turn?
* e.g. Charging moves (Fly, etc.) on their first turn of use. * e.g. Charging moves (Fly, etc.) on their first turn of use.
*/ */
const overridden = new Utils.BooleanHolder(false); const overridden = new BooleanHolder(false);
/** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */ /** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */
const move = this.move.getMove(); const move = this.move.getMove();
// Assume single target for override // Assume single target for override
applyMoveAttrs(OverrideMoveEffectAttr, user, this.getTarget() ?? null, move, overridden, this.move.virtual).then(() => { applyMoveAttrs(OverrideMoveEffectAttr, user, this.getFirstTarget() ?? null, move, overridden, this.move.virtual).then(() => {
// If other effects were overriden, stop this phase before they can be applied // If other effects were overriden, stop this phase before they can be applied
if (overridden.value) { if (overridden.value) {
return this.end(); return this.end();
@ -71,14 +113,14 @@ export class MoveEffectPhase extends PokemonPhase {
* effects of the move itself, Parental Bond, and Multi-Lens to do so. * effects of the move itself, Parental Bond, and Multi-Lens to do so.
*/ */
if (user.turnData.hitsLeft === -1) { if (user.turnData.hitsLeft === -1) {
const hitCount = new Utils.IntegerHolder(1); const hitCount = new NumberHolder(1);
// Assume single target for multi hit // Assume single target for multi hit
applyMoveAttrs(MultiHitAttr, user, this.getTarget() ?? null, move, hitCount); applyMoveAttrs(MultiHitAttr, user, this.getFirstTarget() ?? null, move, hitCount);
// If Parental Bond is applicable, double the hit count // If Parental Bond is applicable, double the hit count
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new Utils.IntegerHolder(0)); applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new NumberHolder(0));
// If Multi-Lens is applicable, multiply the hit count by 1 + the number of Multi-Lenses held by the user // If Multi-Lens is applicable, multiply the hit count by 1 + the number of Multi-Lenses held by the user
if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) { if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) {
this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new Utils.IntegerHolder(0)); this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new NumberHolder(0));
} }
// Set the user's relevant turnData fields to reflect the final hit count // Set the user's relevant turnData fields to reflect the final hit count
user.turnData.hitCount = hitCount.value; user.turnData.hitCount = hitCount.value;
@ -100,7 +142,8 @@ export class MoveEffectPhase extends PokemonPhase {
const hasActiveTargets = targets.some(t => t.isActive(true)); const hasActiveTargets = targets.some(t => t.isActive(true));
/** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */ /** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */
const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr)
&& (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
&& !targets[0].getTag(SemiInvulnerableTag); && !targets[0].getTag(SemiInvulnerableTag);
/** /**
@ -111,7 +154,7 @@ export class MoveEffectPhase extends PokemonPhase {
if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) { if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) {
this.stopMultiHit(); this.stopMultiHit();
if (hasActiveTargets) { if (hasActiveTargets) {
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getTarget() ? getPokemonNameWithAffix(this.getTarget()!) : "" })); this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" }));
moveHistoryEntry.result = MoveResult.MISS; moveHistoryEntry.result = MoveResult.MISS;
applyMoveAttrs(MissEffectAttr, user, null, move); applyMoveAttrs(MissEffectAttr, user, null, move);
} else { } else {
@ -127,29 +170,39 @@ export class MoveEffectPhase extends PokemonPhase {
const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false; const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false;
// Move animation only needs one target // Move animation only needs one target
new MoveAnim(move.id as Moves, user, this.getTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getTarget()!), () => { new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getFirstTarget()!), () => {
/** Has the move successfully hit a target (for damage) yet? */ /** Has the move successfully hit a target (for damage) yet? */
let hasHit: boolean = false; let hasHit: boolean = false;
for (const target of targets) { for (const target of targets) {
// Prevent ENEMY_SIDE targeted moves from occurring twice in double battles
if (move.moveTarget === MoveTarget.ENEMY_SIDE && target !== targets[targets.length - 1]) {
continue;
}
/** The {@linkcode ArenaTagSide} to which the target belongs */ /** The {@linkcode ArenaTagSide} to which the target belongs */
const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
/** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */ /** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */
const hasConditionalProtectApplied = new Utils.BooleanHolder(false); const hasConditionalProtectApplied = new BooleanHolder(false);
/** Does the applied conditional protection bypass Protect-ignoring effects? */ /** Does the applied conditional protection bypass Protect-ignoring effects? */
const bypassIgnoreProtect = new Utils.BooleanHolder(false); const bypassIgnoreProtect = new BooleanHolder(false);
/** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */ /** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */
if (!this.move.getMove().isAllyTarget()) { if (!this.move.getMove().isAllyTarget()) {
this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, false, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect); this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, false, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect);
} }
/** Is the target protected by Protect, etc. or a relevant conditional protection effect? */ /** Is the target protected by Protect, etc. or a relevant conditional protection effect? */
const isProtected = (bypassIgnoreProtect.value || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target)) const isProtected = (
&& (hasConditionalProtectApplied.value || (!target.findTags(t => t instanceof DamageProtectedTag).length && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType))) bypassIgnoreProtect.value
|| (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target))
&& (hasConditionalProtectApplied.value
|| (!target.findTags(t => t instanceof DamageProtectedTag).length
&& target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)))
|| (this.move.getMove().category !== MoveCategory.STATUS
&& target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType))));
/** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */ /** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */
const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr)
&& (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
&& !target.getTag(SemiInvulnerableTag); && !target.getTag(SemiInvulnerableTag);
/** /**
@ -218,7 +271,7 @@ export class MoveEffectPhase extends PokemonPhase {
} }
/** Does this phase represent the invoked move's last strike? */ /** Does this phase represent the invoked move's last strike? */
const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()); const lastHit = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive());
/** /**
* If the user can change forms by using the invoked move, * If the user can change forms by using the invoked move,
@ -234,85 +287,48 @@ export class MoveEffectPhase extends PokemonPhase {
* These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger * These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger
* type requires different conditions to be met with respect to the move's hit result. * type requires different conditions to be met with respect to the move's hit result.
*/ */
applyAttrs.push(new Promise(resolve => { const k = new Promise<void>((resolve) => {
// Apply all effects with PRE_MOVE triggers (if the target isn't immune to the move) //Start promise chain and apply PRE_APPLY move attributes
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && hitResult !== HitResult.NO_EFFECT, let promiseChain: Promise<void | null> = applyFilteredMoveAttrs((attr: MoveAttr) =>
user, target, move).then(() => { attr instanceof MoveEffectAttr
// All other effects require the move to not have failed or have been cancelled to trigger && attr.trigger === MoveEffectTrigger.PRE_APPLY
if (hitResult !== HitResult.FAIL) { && (!attr.firstHitOnly || firstHit)
/** && (!attr.lastHitOnly || lastHit)
* If the invoked move's effects are meant to trigger during the move's "charge turn," && hitResult !== HitResult.NO_EFFECT, user, target, move);
* ignore all effects after this point.
* Otherwise, apply all self-targeted POST_APPLY effects. /** Don't complete if the move failed */
*/ if (hitResult === HitResult.FAIL) {
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY
&& attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move).then(() => {
// All effects past this point require the move to have hit the target
if (hitResult !== HitResult.NO_EFFECT) {
// Apply all non-self-targeted POST_APPLY effects
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY
&& !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()).then(() => {
/**
* If the move hit, and the target doesn't have Shield Dust,
* apply the chance to flinch the target gained from King's Rock
*/
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !move.hitsSubstitute(user, target)) {
const flinched = new Utils.BooleanHolder(false);
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
if (flinched.value) {
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
}
}
// If the move was not protected against, apply all HIT effects
Utils.executeIf(!isProtected, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT
&& (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => {
// Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them)
return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => {
// Only apply the following effects if the move was not deflected by a substitute
if (move.hitsSubstitute(user, target)) {
return resolve(); return resolve();
} }
// If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tokens /** Apply Move/Ability Effects in correct order */
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { promiseChain = promiseChain
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); .then(this.applySelfTargetEffects(user, target, firstHit, lastHit));
}
target.lapseTags(BattlerTagLapseType.AFTER_HIT);
})).then(() => { if (hitResult !== HitResult.NO_EFFECT) {
// Apply the user's post-attack ability effects promiseChain
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => { .then(this.applyPostApplyEffects(user, target, firstHit, lastHit))
/** .then(this.applyHeldItemFlinchCheck(user, target, dealsDamage))
* If the invoked move is an attack, apply the user's chance to .then(this.applySuccessfulAttackEffects(user, target, firstHit, lastHit, !!isProtected, hitResult, firstTarget))
* steal an item from the target granted by Grip Claw .then(() => resolve());
*/
if (this.move.getMove() instanceof AttackMove) {
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
}
resolve();
});
});
})
).then(() => resolve());
});
} else { } else {
applyMoveAttrs(NoEffectAttr, user, null, move).then(() => resolve()); promiseChain
.then(() => applyMoveAttrs(NoEffectAttr, user, null, move))
.then(resolve);
} }
}); });
} else {
resolve(); applyAttrs.push(k);
}
});
}));
} }
// Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved // Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved
const postTarget = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()) ? const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ?
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) : applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) :
null; null;
if (!!postTarget) { if (postTarget) {
if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after
applyAttrs[applyAttrs.length - 1]?.then(() => postTarget); applyAttrs[applyAttrs.length - 1].then(() => postTarget);
} else { // Otherwise, push a new asynchronous move effect } else { // Otherwise, push a new asynchronous move effect
applyAttrs.push(postTarget); applyAttrs.push(postTarget);
} }
@ -327,7 +343,7 @@ export class MoveEffectPhase extends PokemonPhase {
*/ */
targets.forEach(target => { targets.forEach(target => {
const substitute = target.getTag(SubstituteTag); const substitute = target.getTag(SubstituteTag);
if (!!substitute && substitute.hp <= 0) { if (substitute && substitute.hp <= 0) {
target.lapseTag(BattlerTagType.SUBSTITUTE); target.lapseTag(BattlerTagType.SUBSTITUTE);
} }
}); });
@ -337,7 +353,7 @@ export class MoveEffectPhase extends PokemonPhase {
}); });
} }
end() { public override end(): void {
const user = this.getUserPokemon(); const user = this.getUserPokemon();
/** /**
* If this phase isn't for the invoked move's last strike, * If this phase isn't for the invoked move's last strike,
@ -347,7 +363,7 @@ export class MoveEffectPhase extends PokemonPhase {
* to the user. * to the user.
*/ */
if (user) { if (user) {
if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) { if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getFirstTarget()?.isActive()) {
this.scene.unshiftPhase(this.getNewHitPhase()); this.scene.unshiftPhase(this.getNewHitPhase());
} else { } else {
// Queue message for number of hits made by multi-move // Queue message for number of hits made by multi-move
@ -367,11 +383,135 @@ export class MoveEffectPhase extends PokemonPhase {
} }
/** /**
* Resolves whether this phase's invoked move hits or misses the given target * Apply self-targeted effects that trigger `POST_APPLY`
* @param target {@linkcode Pokemon} the Pokemon targeted by the invoked move *
* @returns `true` if the move does not miss the target; `false` otherwise * @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 firstHit - `true` if this is the first hit in a multi-hit attack
* @param lastHit - `true` if this is the last hit in a multi-hit attack
* @returns a function intended to be passed into a `then()` call.
*/ */
hitCheck(target: Pokemon): boolean { protected applySelfTargetEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise<void | null> {
return () => applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.POST_APPLY
&& attr.selfTarget
&& (!attr.firstHitOnly || firstHit)
&& (!attr.lastHitOnly || lastHit), user, target, this.move.getMove());
}
/**
* Applies non-self-targeted effects that trigger `POST_APPLY`
* (i.e. Smelling Salts curing Paralysis, and the forced switch from U-Turn, Dragon Tail, etc)
* @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 firstHit - `true` if this is the first hit in a multi-hit attack
* @param lastHit - `true` if this is the last hit in a multi-hit attack
* @returns a function intended to be passed into a `then()` call.
*/
protected applyPostApplyEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise<void | null> {
return () => applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.POST_APPLY
&& !attr.selfTarget
&& (!attr.firstHitOnly || firstHit)
&& (!attr.lastHitOnly || lastHit), user, target, this.move.getMove());
}
/**
* Applies effects that trigger on HIT
* (i.e. Final Gambit, Power-Up Punch, Drain Punch)
* @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 firstHit - `true` if this is the first hit in a multi-hit attack
* @param lastHit - `true` if this is the last hit in a multi-hit attack
* @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move}
* @returns a function intended to be passed into a `then()` call.
*/
protected applyOnHitEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, firstTarget: boolean): Promise<void> {
return applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.HIT
&& (!attr.firstHitOnly || firstHit)
&& (!attr.lastHitOnly || lastHit)
&& (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove());
}
/**
* Applies reactive effects that occur when a Pokémon is hit.
* (i.e. Effect Spore, Disguise, Liquid Ooze, Beak Blast)
* @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
* @returns a `Promise` intended to be passed into a `then()` call.
*/
protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): Promise<void | null> {
return executeIf(!target.isFainted() || target.canApplyAbility(), () =>
applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult)
.then(() => {
if (!this.move.getMove().hitsSubstitute(user, target)) {
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
}
target.lapseTags(BattlerTagLapseType.AFTER_HIT);
}
})
);
}
/**
* Applies all effects and attributes that require a move to connect with a target,
* namely reactive effects like Weak Armor, on-hit effects like that of Power-Up Punch, and item stealing effects
* @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 firstHit - `true` if this is the first hit in a multi-hit attack
* @param lastHit - `true` if this is the last hit in a multi-hit attack
* @param isProtected - `true` if the target is protected by effects such as Protect
* @param hitResult - The {@linkcode HitResult} of the attempted move
* @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move}
* @returns a function intended to be passed into a `then()` call.
*/
protected applySuccessfulAttackEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, isProtected : boolean, hitResult: HitResult, firstTarget: boolean) : () => Promise<void | null> {
return () => executeIf(!isProtected, () =>
this.applyOnHitEffects(user, target, firstHit, lastHit, firstTarget).then(() =>
this.applyOnGetHitAbEffects(user, target, hitResult)).then(() =>
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult)).then(() => { // Item Stealing Effects
if (this.move.getMove() instanceof AttackMove) {
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
}
})
);
}
/**
* Handles checking for and applying Flinches
* @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 dealsDamage - `true` if the attempted move successfully dealt damage
* @returns a function intended to be passed into a `then()` call.
*/
protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : () => void {
return () => {
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) {
const flinched = new BooleanHolder(false);
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
if (flinched.value) {
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
}
}
};
}
/**
* Resolves whether this phase's invoked move hits the given target
* @param target - The {@linkcode Pokemon} targeted by the invoked move
* @returns `true` if the move hits the target
*/
public hitCheck(target: Pokemon): boolean {
// Moves targeting the user and entry hazards can't miss // Moves targeting the user and entry hazards can't miss
if ([ MoveTarget.USER, MoveTarget.ENEMY_SIDE ].includes(this.move.getMove().moveTarget)) { if ([ MoveTarget.USER, MoveTarget.ENEMY_SIDE ].includes(this.move.getMove().moveTarget)) {
return true; return true;
@ -425,29 +565,29 @@ export class MoveEffectPhase extends PokemonPhase {
return rand < (moveAccuracy * accuracyMultiplier); return rand < (moveAccuracy * accuracyMultiplier);
} }
/** Returns the {@linkcode Pokemon} using this phase's invoked move */ /** @returns The {@linkcode Pokemon} using this phase's invoked move */
getUserPokemon(): Pokemon | undefined { public getUserPokemon(): Pokemon | undefined {
if (this.battlerIndex > BattlerIndex.ENEMY_2) { if (this.battlerIndex > BattlerIndex.ENEMY_2) {
return this.scene.getPokemonById(this.battlerIndex) ?? undefined; return this.scene.getPokemonById(this.battlerIndex) ?? undefined;
} }
return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex]; return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex];
} }
/** Returns an array of all {@linkcode Pokemon} targeted by this phase's invoked move */ /** @returns An array of all {@linkcode Pokemon} targeted by this phase's invoked move */
getTargets(): Pokemon[] { public getTargets(): Pokemon[] {
return this.scene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1); return this.scene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1);
} }
/** Returns the first target of this phase's invoked move */ /** @returns The first target of this phase's invoked move */
getTarget(): Pokemon | undefined { public getFirstTarget(): Pokemon | undefined {
return this.getTargets()[0]; return this.getTargets()[0];
} }
/** /**
* Removes the given {@linkcode Pokemon} from this phase's target list * Removes the given {@linkcode Pokemon} from this phase's target list
* @param target {@linkcode Pokemon} the Pokemon to be removed * @param target - The {@linkcode Pokemon} to be removed
*/ */
removeTarget(target: Pokemon): void { protected removeTarget(target: Pokemon): void {
const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex()); const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex());
if (targetIndex !== -1) { if (targetIndex !== -1) {
this.targets.splice(this.targets.findIndex(ind => ind === target.getBattlerIndex()), 1); this.targets.splice(this.targets.findIndex(ind => ind === target.getBattlerIndex()), 1);
@ -459,23 +599,25 @@ export class MoveEffectPhase extends PokemonPhase {
* @param target {@linkcode Pokemon} if defined, only stop subsequent * @param target {@linkcode Pokemon} if defined, only stop subsequent
* strikes against this Pokemon * strikes against this Pokemon
*/ */
stopMultiHit(target?: Pokemon): void { public stopMultiHit(target?: Pokemon): void {
/** If given a specific target, remove the target from subsequent strikes */ // If given a specific target, remove the target from subsequent strikes
if (target) { if (target) {
this.removeTarget(target); this.removeTarget(target);
} }
/** const user = this.getUserPokemon();
* If no target specified, or the specified target was the last of this move's if (!user) {
* targets, completely cancel all subsequent strikes. return;
*/ }
// If no target specified, or the specified target was the last of this move's
// targets, completely cancel all subsequent strikes.
if (!target || this.targets.length === 0 ) { if (!target || this.targets.length === 0 ) {
this.getUserPokemon()!.turnData.hitCount = 1; // TODO: is the bang correct here? user.turnData.hitCount = 1;
this.getUserPokemon()!.turnData.hitsLeft = 1; // TODO: is the bang correct here? user.turnData.hitsLeft = 1;
} }
} }
/** Returns a new MoveEffectPhase with the same properties as this phase */ /** @returns A new `MoveEffectPhase` with the same properties as this phase */
getNewHitPhase() { protected getNewHitPhase(): MoveEffectPhase {
return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move); return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move);
} }
} }

View File

@ -57,7 +57,7 @@ describe("Abilities - Serene Grace", () => {
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
console.log(move.chance + " Their ability is " + phase.getUserPokemon()!.getAbility().name); console.log(move.chance + " Their ability is " + phase.getUserPokemon()!.getAbility().name);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
expect(chance.value).toBe(30); expect(chance.value).toBe(30);
}, 20000); }, 20000);
@ -83,7 +83,7 @@ describe("Abilities - Serene Grace", () => {
expect(move.id).toBe(Moves.AIR_SLASH); expect(move.id).toBe(Moves.AIR_SLASH);
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
expect(chance.value).toBe(60); expect(chance.value).toBe(60);
}, 20000); }, 20000);

View File

@ -60,8 +60,8 @@ describe("Abilities - Sheer Force", () => {
const power = new Utils.IntegerHolder(move.power); const power = new Utils.IntegerHolder(move.power);
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power);
expect(chance.value).toBe(0); expect(chance.value).toBe(0);
expect(power.value).toBe(move.power * 5461 / 4096); expect(power.value).toBe(move.power * 5461 / 4096);
@ -93,8 +93,8 @@ describe("Abilities - Sheer Force", () => {
const power = new Utils.IntegerHolder(move.power); const power = new Utils.IntegerHolder(move.power);
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power);
expect(chance.value).toBe(-1); expect(chance.value).toBe(-1);
expect(power.value).toBe(move.power); expect(power.value).toBe(move.power);
@ -126,8 +126,8 @@ describe("Abilities - Sheer Force", () => {
const power = new Utils.IntegerHolder(move.power); const power = new Utils.IntegerHolder(move.power);
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power);
expect(chance.value).toBe(-1); expect(chance.value).toBe(-1);
expect(power.value).toBe(move.power); expect(power.value).toBe(move.power);
@ -161,7 +161,7 @@ describe("Abilities - Sheer Force", () => {
const power = new Utils.IntegerHolder(move.power); const power = new Utils.IntegerHolder(move.power);
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
const user = phase.getUserPokemon()!; const user = phase.getUserPokemon()!;
const target = phase.getTarget()!; const target = phase.getFirstTarget()!;
const opponentType = target.getTypes()[0]; const opponentType = target.getTypes()[0];
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false);

View File

@ -57,8 +57,8 @@ describe("Abilities - Shield Dust", () => {
expect(move.id).toBe(Moves.AIR_SLASH); expect(move.id).toBe(Moves.AIR_SLASH);
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getTarget()!, phase.getUserPokemon()!, null, null, false, chance); applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getFirstTarget()!, phase.getUserPokemon()!, null, null, false, chance);
expect(chance.value).toBe(0); expect(chance.value).toBe(0);
}, 20000); }, 20000);

View File

@ -0,0 +1,49 @@
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { TerrainType } from "#app/data/terrain";
import { Type } from "#app/data/type";
import { BattlerIndex } from "#app/battle";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Camouflage", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.CAMOUFLAGE ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.REGIELEKI)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.PSYCHIC_TERRAIN);
});
it("Camouflage should look at terrain first when selecting a type to change into", async () => {
await game.classicMode.startBattle([ Species.SHUCKLE ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.CAMOUFLAGE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.arena.getTerrainType()).toBe(TerrainType.PSYCHIC);
const pokemonType = playerPokemon.getTypes()[0];
expect(pokemonType).toBe(Type.PSYCHIC);
});
});

View File

@ -81,7 +81,7 @@ describe("Moves - Dynamax Cannon", () => {
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.moveId).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamagePhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(120); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(120);
}, 20000); }, 20000);
@ -98,7 +98,7 @@ describe("Moves - Dynamax Cannon", () => {
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.moveId).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamagePhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(140); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(140);
}, 20000); }, 20000);
@ -115,7 +115,7 @@ describe("Moves - Dynamax Cannon", () => {
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.moveId).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamagePhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(160); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(160);
}, 20000); }, 20000);
@ -132,7 +132,7 @@ describe("Moves - Dynamax Cannon", () => {
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.moveId).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamagePhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(180); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(180);
}, 20000); }, 20000);
@ -149,7 +149,7 @@ describe("Moves - Dynamax Cannon", () => {
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.moveId).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamagePhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000); }, 20000);

View File

@ -2,7 +2,7 @@ import { Abilities } from "#enums/abilities";
import { Biome } from "#enums/biome"; import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { allMoves, SecretPowerAttr } from "#app/data/move"; import { allMoves } from "#app/data/move";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
@ -11,6 +11,7 @@ import { StatusEffect } from "#enums/status-effect";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { ArenaTagSide } from "#app/data/arena-tag"; import { ArenaTagSide } from "#app/data/arena-tag";
import { allAbilities, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability";
describe("Moves - Secret Power", () => { describe("Moves - Secret Power", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -60,30 +61,38 @@ describe("Moves - Secret Power", () => {
expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-1); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-1);
}); });
it("the 'rainbow' effect of fire+water pledge does not double the chance of secret power's secondary effect", it("Secret Power's effect chance is doubled by Serene Grace, but not by the 'rainbow' effect from Fire/Water Pledge",
async () => { async () => {
game.override game.override
.moveset([ Moves.FIRE_PLEDGE, Moves.WATER_PLEDGE, Moves.SECRET_POWER, Moves.SPLASH ]) .moveset([ Moves.FIRE_PLEDGE, Moves.WATER_PLEDGE, Moves.SECRET_POWER, Moves.SPLASH ])
.ability(Abilities.SERENE_GRACE)
.enemyMoveset([ Moves.SPLASH ]) .enemyMoveset([ Moves.SPLASH ])
.battleType("double"); .battleType("double");
await game.classicMode.startBattle([ Species.BLASTOISE, Species.CHARIZARD ]); await game.classicMode.startBattle([ Species.BLASTOISE, Species.CHARIZARD ]);
const secretPowerAttr = allMoves[Moves.SECRET_POWER].getAttrs(SecretPowerAttr)[0]; const sereneGraceAttr = allAbilities[Abilities.SERENE_GRACE].getAttrs(MoveEffectChanceMultiplierAbAttr)[0];
vi.spyOn(secretPowerAttr, "getMoveChance"); vi.spyOn(sereneGraceAttr, "apply");
game.move.select(Moves.WATER_PLEDGE, 0, BattlerIndex.ENEMY); game.move.select(Moves.WATER_PLEDGE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.FIRE_PLEDGE, 1, BattlerIndex.ENEMY_2); game.move.select(Moves.FIRE_PLEDGE, 1, BattlerIndex.ENEMY_2);
await game.phaseInterceptor.to("TurnEndPhase"); await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.getTagOnSide(ArenaTagType.WATER_FIRE_PLEDGE, ArenaTagSide.PLAYER)).toBeDefined(); let rainbowEffect = game.scene.arena.getTagOnSide(ArenaTagType.WATER_FIRE_PLEDGE, ArenaTagSide.PLAYER);
expect(rainbowEffect).toBeDefined();
rainbowEffect = rainbowEffect!;
vi.spyOn(rainbowEffect, "apply");
game.move.select(Moves.SECRET_POWER, 0, BattlerIndex.ENEMY); game.move.select(Moves.SECRET_POWER, 0, BattlerIndex.ENEMY);
game.move.select(Moves.SPLASH, 1); game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to("BerryPhase", false); await game.phaseInterceptor.to("BerryPhase", false);
expect(secretPowerAttr.getMoveChance).toHaveLastReturnedWith(30); expect(sereneGraceAttr.apply).toHaveBeenCalledOnce();
expect(sereneGraceAttr.apply).toHaveLastReturnedWith(true);
expect(rainbowEffect.apply).toHaveBeenCalledTimes(0);
} }
); );
}); });

View File

@ -1,4 +1,5 @@
import { SubstituteTag } from "#app/data/battler-tags"; import { SubstituteTag } from "#app/data/battler-tags";
import { MoveResult } from "#app/field/pokemon";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
@ -53,4 +54,18 @@ describe("Moves - Shed Tail", () => {
expect(substituteTag).toBeDefined(); expect(substituteTag).toBeDefined();
expect(substituteTag?.hp).toBe(Math.floor(magikarp.getMaxHp() / 4)); expect(substituteTag?.hp).toBe(Math.floor(magikarp.getMaxHp() / 4));
}); });
it("should fail if no ally is available to switch in", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const magikarp = game.scene.getPlayerPokemon()!;
expect(game.scene.getParty().length).toBe(1);
game.move.select(Moves.SHED_TAIL);
await game.phaseInterceptor.to("TurnEndPhase", false);
expect(magikarp.isOnField()).toBeTruthy();
expect(magikarp.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
}); });

View File

@ -24,6 +24,7 @@ import GamepadPlugin = Phaser.Input.Gamepad.GamepadPlugin;
import EventEmitter = Phaser.Events.EventEmitter; import EventEmitter = Phaser.Events.EventEmitter;
import UpdateList = Phaser.GameObjects.UpdateList; import UpdateList = Phaser.GameObjects.UpdateList;
import { version } from "../../../package.json"; import { version } from "../../../package.json";
import { MockTimedEventManager } from "./mocks/mockTimedEventManager";
Object.defineProperty(window, "localStorage", { Object.defineProperty(window, "localStorage", {
value: mockLocalStorage(), value: mockLocalStorage(),
@ -232,6 +233,7 @@ export default class GameWrapper {
this.scene.make = new MockGameObjectCreator(mockTextureManager); this.scene.make = new MockGameObjectCreator(mockTextureManager);
this.scene.time = new MockClock(this.scene); this.scene.time = new MockClock(this.scene);
this.scene.remove = vi.fn(); // TODO: this should be stubbed differently this.scene.remove = vi.fn(); // TODO: this should be stubbed differently
this.scene.eventManager = new MockTimedEventManager(); // Disable Timed Events
} }
} }

View File

@ -0,0 +1,17 @@
import { TimedEventManager } from "#app/timed-event-manager";
/** Mock TimedEventManager so that ongoing events don't impact tests */
export class MockTimedEventManager extends TimedEventManager {
override activeEvent() {
return undefined;
}
override isEventActive(): boolean {
return false;
}
override getFriendshipMultiplier(): number {
return 1;
}
override getShinyMultiplier(): number {
return 1;
}
}