Compare commits

..

6 Commits

Author SHA1 Message Date
DustinLin e2543cb9ac
Merge 836964300d into 6030b780f2 2024-09-17 20:07:28 -07:00
flx-sta 6030b780f2
[Move][Mirror] Update HitTagAttr attributes v2 (#4297)
* [Move] Updated HitAttr tags

Affects Whirlwind/Fly, Steamroller/Minimize, and Malicious Moonsault/Minimize

* [Move] Update for MinimizeAccuracyAttr

Affects Steamroller and Malicious Moonsault

* add: whirlwind test

* add: steamroller test

* rename: `AlwaysHitMinimizeAttr` (from `MinimizeAccuracyAttr`)

* rename: `DealsDoubleDamageToTagAttr` (from `HitsTagAttr`)

---------

Co-authored-by: chaosgrimmon <31082757+chaosgrimmon@users.noreply.github.com>
2024-09-17 22:41:46 -04:00
Jannik Tappert e386504977
[BUG][Beta] Fix English Status Symbols (#4293)
* Have English Status Symbols show up again :)

* It now uses the function instead

---------

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
2024-09-17 22:26:47 -04:00
podar 106ed6b27b
[Bug] Using default animation for errors that occur. (#4266)
* Using default animation for errors that occur.

* Renaming function to make it clear that logging happens

* Updating logging for missing animations

* Missed committing linter changes

* Update src/data/battle-anims.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update src/data/battle-anims.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update src/data/battle-anims.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
2024-09-17 22:15:47 -04:00
flx-sta 4389bff5d0
[Bug] Fix stat-protection-attribute not taking inverts (e.g. Contrary) into account (#4031)
* add generic types to`Pokemon.getAbilityAttrs()`

* add invert check to `ProtectStatAbAttr.apply...`

This makes sure that a stat is only protected if no other ability inverts the change. E.g. `Contrary` inverts any decrease to an increase

* migrate contrary.test.ts to game.classicMode

* move `StatStageChangeMultiplierAbAttr` resolve above `ProtectStatAbAttr`

The effect of StatStageChangeMultiplierAbAttr is now applied before resolving any ProtectStatAbAttr. Thus the stage (level) of the BattleStat change was properly altered at the time of resolving the protection

* revert ability.ts changes

* add automated tests for `Clear Body` + `Contrary`

* StateStageChangePhase replace ~~`IntegerHolder`~~ with `NumberHolder`

Update Utils import and replace all occurcences of `Utils.`

* contrary.test.ts: remove `js` import
2024-09-17 19:14:41 -07:00
Madmadness65 00ba2eebc8
Add new biome BGM by Firel (#4301) 2024-09-17 21:57:34 +01:00
21 changed files with 271 additions and 70 deletions

View File

@ -55,7 +55,7 @@ Check out [Github Issues](https://github.com/pagefaultgames/pokerogue/issues) to
- Pokémon Sword/Shield - Pokémon Sword/Shield
- Pokémon Legends: Arceus - Pokémon Legends: Arceus
- Pokémon Scarlet/Violet - Pokémon Scarlet/Violet
- Firel (Custom Laboratory, Metropolis, Seabed, and Space biome music) - Firel (Custom Ice Cave, Laboratory, Metropolis, Plains, Power Plant, Seabed, Space, and Volcano biome music)
- Lmz (Custom Jungle biome music) - Lmz (Custom Jungle biome music)
- Andr06 (Custom Slum and Sea biome music) - Andr06 (Custom Slum and Sea biome music)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -2625,7 +2625,11 @@ export class PreStatStageChangeAbAttr extends AbAttr {
} }
} }
/**
* Protect one or all {@linkcode BattleStat} from reductions caused by other Pokémon's moves and Abilities
*/
export class ProtectStatAbAttr extends PreStatStageChangeAbAttr { export class ProtectStatAbAttr extends PreStatStageChangeAbAttr {
/** {@linkcode BattleStat} to protect or `undefined` if **all** {@linkcode BattleStat} are protected */
private protectedStat?: BattleStat; private protectedStat?: BattleStat;
constructor(protectedStat?: BattleStat) { constructor(protectedStat?: BattleStat) {
@ -2634,7 +2638,17 @@ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr {
this.protectedStat = protectedStat; this.protectedStat = protectedStat;
} }
applyPreStatStageChange(_pokemon: Pokemon, _passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, _args: any[]): boolean { /**
* Apply the {@linkcode ProtectedStatAbAttr} to an interaction
* @param _pokemon
* @param _passive
* @param simulated
* @param stat the {@linkcode BattleStat} being affected
* @param cancelled The {@linkcode Utils.BooleanHolder} that will be set to true if the stat is protected
* @param _args
* @returns true if the stat is protected, false otherwise
*/
applyPreStatStageChange(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, _args: any[]): boolean {
if (Utils.isNullOrUndefined(this.protectedStat) || stat === this.protectedStat) { if (Utils.isNullOrUndefined(this.protectedStat) || stat === this.protectedStat) {
cancelled.value = true; cancelled.value = true;
return true; return true;
@ -3757,7 +3771,7 @@ export class StatStageChangeMultiplierAbAttr extends AbAttr {
this.multiplier = multiplier; this.multiplier = multiplier;
} }
apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
(args[0] as Utils.IntegerHolder).value *= this.multiplier; (args[0] as Utils.IntegerHolder).value *= this.multiplier;
return true; return true;

View File

@ -488,14 +488,14 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise<void> {
} else { } else {
moveAnims.set(move, null); moveAnims.set(move, null);
const defaultMoveAnim = allMoves[move] instanceof AttackMove ? Moves.TACKLE : allMoves[move] instanceof SelfStatusMove ? Moves.FOCUS_ENERGY : Moves.TAIL_WHIP; const defaultMoveAnim = allMoves[move] instanceof AttackMove ? Moves.TACKLE : allMoves[move] instanceof SelfStatusMove ? Moves.FOCUS_ENERGY : Moves.TAIL_WHIP;
const moveName = Moves[move].toLowerCase().replace(/\_/g, "-");
const fetchAnimAndResolve = (move: Moves) => { const fetchAnimAndResolve = (move: Moves) => {
scene.cachedFetch(`./battle-anims/${moveName}.json`) scene.cachedFetch(`./battle-anims/${Utils.animationFileName(move)}.json`)
.then(response => { .then(response => {
const contentType = response.headers.get("content-type"); const contentType = response.headers.get("content-type");
if (!response.ok || contentType?.indexOf("application/json") === -1) { if (!response.ok || contentType?.indexOf("application/json") === -1) {
console.error(`Could not load animation file for move '${moveName}'`, response.status, response.statusText); useDefaultAnim(move, defaultMoveAnim);
populateMoveAnim(move, moveAnims.get(defaultMoveAnim)); logMissingMoveAnim(move, response.status, response.statusText);
return resolve(); return resolve();
} }
return response.json(); return response.json();
@ -515,6 +515,11 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise<void> {
} else { } else {
resolve(); resolve();
} }
})
.catch(error => {
useDefaultAnim(move, defaultMoveAnim);
logMissingMoveAnim(move, error);
return resolve();
}); });
}; };
fetchAnimAndResolve(move); fetchAnimAndResolve(move);
@ -522,6 +527,29 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise<void> {
}); });
} }
/**
* Populates the default animation for the given move.
*
* @param move the move to populate an animation for
* @param defaultMoveAnim the move to use as the default animation
*/
function useDefaultAnim(move: Moves, defaultMoveAnim: Moves) {
populateMoveAnim(move, moveAnims.get(defaultMoveAnim));
}
/**
* Helper method for printing a warning to the console when a move animation is missing.
*
* @param move the move to populate an animation for
* @param optionalParams parameters to add to the error logging
*
* @remarks use {@linkcode useDefaultAnim} to use a default animation
*/
function logMissingMoveAnim(move: Moves, ...optionalParams: any[]) {
const moveName = Utils.animationFileName(move);
console.warn(`Could not load animation file for move '${moveName}'`, ...optionalParams);
}
/** /**
* Fetches animation configs to be used in a Mystery Encounter * Fetches animation configs to be used in a Mystery Encounter
* @param scene * @param scene

View File

@ -3839,7 +3839,7 @@ export class StormAccuracyAttr extends VariableAccuracyAttr {
* @extends VariableAccuracyAttr * @extends VariableAccuracyAttr
* @see {@linkcode apply} * @see {@linkcode apply}
*/ */
export class MinimizeAccuracyAttr extends VariableAccuracyAttr { export class AlwaysHitMinimizeAttr extends VariableAccuracyAttr {
/** /**
* @see {@linkcode apply} * @see {@linkcode apply}
* @param user N/A * @param user N/A
@ -4855,10 +4855,10 @@ export class RemoveAllSubstitutesAttr extends MoveEffectAttr {
* Attribute used when a move hits a {@linkcode BattlerTagType} for double damage * Attribute used when a move hits a {@linkcode BattlerTagType} for double damage
* @extends MoveAttr * @extends MoveAttr
*/ */
export class HitsTagAttr extends MoveAttr { export class DealsDoubleDamageToTagAttr extends MoveAttr {
/** The {@linkcode BattlerTagType} this move hits */ /** The {@linkcode BattlerTagType} this move hits */
public tagType: BattlerTagType; public tagType: BattlerTagType;
/** Should this move deal double damage against {@linkcode HitsTagAttr.tagType}? */ /** Should this move deal double damage against {@linkcode DealsDoubleDamageToTagAttr.tagType}? */
public doubleDamage: boolean; public doubleDamage: boolean;
constructor(tagType: BattlerTagType, doubleDamage?: boolean) { constructor(tagType: BattlerTagType, doubleDamage?: boolean) {
@ -6790,12 +6790,11 @@ export function initMoves() {
new AttackMove(Moves.CUT, Type.NORMAL, MoveCategory.PHYSICAL, 50, 95, 30, -1, 0, 1) new AttackMove(Moves.CUT, Type.NORMAL, MoveCategory.PHYSICAL, 50, 95, 30, -1, 0, 1)
.slicingMove(), .slicingMove(),
new AttackMove(Moves.GUST, Type.FLYING, MoveCategory.SPECIAL, 40, 100, 35, -1, 0, 1) new AttackMove(Moves.GUST, Type.FLYING, MoveCategory.SPECIAL, 40, 100, 35, -1, 0, 1)
.attr(HitsTagAttr, BattlerTagType.FLYING, true) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, true)
.windMove(), .windMove(),
new AttackMove(Moves.WING_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 35, -1, 0, 1), new AttackMove(Moves.WING_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 35, -1, 0, 1),
new StatusMove(Moves.WHIRLWIND, Type.NORMAL, -1, 20, -1, -6, 1) new StatusMove(Moves.WHIRLWIND, Type.NORMAL, -1, 20, -1, -6, 1)
.attr(ForceSwitchOutAttr) .attr(ForceSwitchOutAttr)
.attr(HitsTagAttr, BattlerTagType.FLYING, false)
.ignoresSubstitute() .ignoresSubstitute()
.hidesTarget() .hidesTarget()
.windMove(), .windMove(),
@ -6808,8 +6807,8 @@ export function initMoves() {
new AttackMove(Moves.SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 80, 75, 20, -1, 0, 1), new AttackMove(Moves.SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 80, 75, 20, -1, 0, 1),
new AttackMove(Moves.VINE_WHIP, Type.GRASS, MoveCategory.PHYSICAL, 45, 100, 25, -1, 0, 1), new AttackMove(Moves.VINE_WHIP, Type.GRASS, MoveCategory.PHYSICAL, 45, 100, 25, -1, 0, 1),
new AttackMove(Moves.STOMP, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 1) new AttackMove(Moves.STOMP, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 1)
.attr(MinimizeAccuracyAttr) .attr(AlwaysHitMinimizeAttr)
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true)
.attr(FlinchAttr), .attr(FlinchAttr),
new AttackMove(Moves.DOUBLE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 30, 100, 30, -1, 0, 1) new AttackMove(Moves.DOUBLE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 30, 100, 30, -1, 0, 1)
.attr(MultiHitAttr, MultiHitType._2), .attr(MultiHitAttr, MultiHitType._2),
@ -6833,8 +6832,8 @@ export function initMoves() {
.attr(OneHitKOAccuracyAttr), .attr(OneHitKOAccuracyAttr),
new AttackMove(Moves.TACKLE, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, 0, 1), new AttackMove(Moves.TACKLE, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, 0, 1),
new AttackMove(Moves.BODY_SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 85, 100, 15, 30, 0, 1) new AttackMove(Moves.BODY_SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 85, 100, 15, 30, 0, 1)
.attr(MinimizeAccuracyAttr) .attr(AlwaysHitMinimizeAttr)
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS), .attr(StatusEffectAttr, StatusEffect.PARALYSIS),
new AttackMove(Moves.WRAP, Type.NORMAL, MoveCategory.PHYSICAL, 15, 90, 20, -1, 0, 1) new AttackMove(Moves.WRAP, Type.NORMAL, MoveCategory.PHYSICAL, 15, 90, 20, -1, 0, 1)
.attr(TrapAttr, BattlerTagType.WRAP), .attr(TrapAttr, BattlerTagType.WRAP),
@ -6902,7 +6901,7 @@ export function initMoves() {
new AttackMove(Moves.HYDRO_PUMP, Type.WATER, MoveCategory.SPECIAL, 110, 80, 5, -1, 0, 1), new AttackMove(Moves.HYDRO_PUMP, Type.WATER, MoveCategory.SPECIAL, 110, 80, 5, -1, 0, 1),
new AttackMove(Moves.SURF, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 1) new AttackMove(Moves.SURF, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 1)
.target(MoveTarget.ALL_NEAR_OTHERS) .target(MoveTarget.ALL_NEAR_OTHERS)
.attr(HitsTagAttr, BattlerTagType.UNDERWATER, true) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERWATER, true)
.attr(GulpMissileTagAttr), .attr(GulpMissileTagAttr),
new AttackMove(Moves.ICE_BEAM, Type.ICE, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1) new AttackMove(Moves.ICE_BEAM, Type.ICE, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1)
.attr(StatusEffectAttr, StatusEffect.FREEZE), .attr(StatusEffectAttr, StatusEffect.FREEZE),
@ -6985,18 +6984,18 @@ export function initMoves() {
new AttackMove(Moves.THUNDER, Type.ELECTRIC, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 1) new AttackMove(Moves.THUNDER, Type.ELECTRIC, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 1)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.attr(ThunderAccuracyAttr) .attr(ThunderAccuracyAttr)
.attr(HitsTagAttr, BattlerTagType.FLYING, false), .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false),
new AttackMove(Moves.ROCK_THROW, Type.ROCK, MoveCategory.PHYSICAL, 50, 90, 15, -1, 0, 1) new AttackMove(Moves.ROCK_THROW, Type.ROCK, MoveCategory.PHYSICAL, 50, 90, 15, -1, 0, 1)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.EARTHQUAKE, Type.GROUND, MoveCategory.PHYSICAL, 100, 100, 10, -1, 0, 1) new AttackMove(Moves.EARTHQUAKE, Type.GROUND, MoveCategory.PHYSICAL, 100, 100, 10, -1, 0, 1)
.attr(HitsTagAttr, BattlerTagType.UNDERGROUND, true) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERGROUND, true)
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1)
.makesContact(false) .makesContact(false)
.target(MoveTarget.ALL_NEAR_OTHERS), .target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.FISSURE, Type.GROUND, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1) new AttackMove(Moves.FISSURE, Type.GROUND, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1)
.attr(OneHitKOAttr) .attr(OneHitKOAttr)
.attr(OneHitKOAccuracyAttr) .attr(OneHitKOAccuracyAttr)
.attr(HitsTagAttr, BattlerTagType.UNDERGROUND, false) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERGROUND, false)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1) new AttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1)
.attr(ChargeAttr, ChargeAnim.DIG_CHARGING, i18next.t("moveTriggers:dugAHole", {pokemonName: "{USER}"}), BattlerTagType.UNDERGROUND) .attr(ChargeAttr, ChargeAnim.DIG_CHARGING, i18next.t("moveTriggers:dugAHole", {pokemonName: "{USER}"}), BattlerTagType.UNDERGROUND)
@ -7385,7 +7384,7 @@ export function initMoves() {
.attr(PreMoveMessageAttr, magnitudeMessageFunc) .attr(PreMoveMessageAttr, magnitudeMessageFunc)
.attr(MagnitudePowerAttr) .attr(MagnitudePowerAttr)
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1)
.attr(HitsTagAttr, BattlerTagType.UNDERGROUND, true) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERGROUND, true)
.makesContact(false) .makesContact(false)
.target(MoveTarget.ALL_NEAR_OTHERS), .target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.DYNAMIC_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 50, 5, 100, 0, 2) new AttackMove(Moves.DYNAMIC_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 50, 5, 100, 0, 2)
@ -7441,7 +7440,7 @@ export function initMoves() {
new AttackMove(Moves.CROSS_CHOP, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 80, 5, -1, 0, 2) new AttackMove(Moves.CROSS_CHOP, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 80, 5, -1, 0, 2)
.attr(HighCritAttr), .attr(HighCritAttr),
new AttackMove(Moves.TWISTER, Type.DRAGON, MoveCategory.SPECIAL, 40, 100, 20, 20, 0, 2) new AttackMove(Moves.TWISTER, Type.DRAGON, MoveCategory.SPECIAL, 40, 100, 20, 20, 0, 2)
.attr(HitsTagAttr, BattlerTagType.FLYING, true) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, true)
.attr(FlinchAttr) .attr(FlinchAttr)
.windMove() .windMove()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
@ -7473,7 +7472,7 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.DEF ], -1), .attr(StatStageChangeAttr, [ Stat.DEF ], -1),
new AttackMove(Moves.WHIRLPOOL, Type.WATER, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 2) new AttackMove(Moves.WHIRLPOOL, Type.WATER, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 2)
.attr(TrapAttr, BattlerTagType.WHIRLPOOL) .attr(TrapAttr, BattlerTagType.WHIRLPOOL)
.attr(HitsTagAttr, BattlerTagType.UNDERWATER, true), .attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERWATER, true),
new AttackMove(Moves.BEAT_UP, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 2) new AttackMove(Moves.BEAT_UP, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 2)
.attr(MultiHitAttr, MultiHitType.BEAT_UP) .attr(MultiHitAttr, MultiHitType.BEAT_UP)
.attr(BeatUpAttr) .attr(BeatUpAttr)
@ -7696,7 +7695,7 @@ export function initMoves() {
new AttackMove(Moves.EXTRASENSORY, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 20, 10, 0, 3) new AttackMove(Moves.EXTRASENSORY, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 20, 10, 0, 3)
.attr(FlinchAttr), .attr(FlinchAttr),
new AttackMove(Moves.SKY_UPPERCUT, Type.FIGHTING, MoveCategory.PHYSICAL, 85, 90, 15, -1, 0, 3) new AttackMove(Moves.SKY_UPPERCUT, Type.FIGHTING, MoveCategory.PHYSICAL, 85, 90, 15, -1, 0, 3)
.attr(HitsTagAttr, BattlerTagType.FLYING) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING)
.punchingMove(), .punchingMove(),
new AttackMove(Moves.SAND_TOMB, Type.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 3) new AttackMove(Moves.SAND_TOMB, Type.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 3)
.attr(TrapAttr, BattlerTagType.SAND_TOMB) .attr(TrapAttr, BattlerTagType.SAND_TOMB)
@ -7927,8 +7926,8 @@ export function initMoves() {
new AttackMove(Moves.DRAGON_PULSE, Type.DRAGON, MoveCategory.SPECIAL, 85, 100, 10, -1, 0, 4) new AttackMove(Moves.DRAGON_PULSE, Type.DRAGON, MoveCategory.SPECIAL, 85, 100, 10, -1, 0, 4)
.pulseMove(), .pulseMove(),
new AttackMove(Moves.DRAGON_RUSH, Type.DRAGON, MoveCategory.PHYSICAL, 100, 75, 10, 20, 0, 4) new AttackMove(Moves.DRAGON_RUSH, Type.DRAGON, MoveCategory.PHYSICAL, 100, 75, 10, 20, 0, 4)
.attr(MinimizeAccuracyAttr) .attr(AlwaysHitMinimizeAttr)
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true)
.attr(FlinchAttr), .attr(FlinchAttr),
new AttackMove(Moves.POWER_GEM, Type.ROCK, MoveCategory.SPECIAL, 80, 100, 20, -1, 0, 4), new AttackMove(Moves.POWER_GEM, Type.ROCK, MoveCategory.SPECIAL, 80, 100, 20, -1, 0, 4),
new AttackMove(Moves.DRAIN_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 4) new AttackMove(Moves.DRAIN_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 4)
@ -8125,7 +8124,7 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true) .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true)
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
.attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN]) .attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
.attr(HitsTagAttr, BattlerTagType.FLYING, false) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.STORM_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5) new AttackMove(Moves.STORM_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
.attr(CritOnlyAttr), .attr(CritOnlyAttr),
@ -8138,9 +8137,9 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true) .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
.danceMove(), .danceMove(),
new AttackMove(Moves.HEAVY_SLAM, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) new AttackMove(Moves.HEAVY_SLAM, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5)
.attr(MinimizeAccuracyAttr) .attr(AlwaysHitMinimizeAttr)
.attr(CompareWeightPowerAttr) .attr(CompareWeightPowerAttr)
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true), .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true),
new AttackMove(Moves.SYNCHRONOISE, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 5) new AttackMove(Moves.SYNCHRONOISE, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 5)
.target(MoveTarget.ALL_NEAR_OTHERS) .target(MoveTarget.ALL_NEAR_OTHERS)
.condition(unknownTypeCondition) .condition(unknownTypeCondition)
@ -8291,12 +8290,14 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.DEF ], -1) .attr(StatStageChangeAttr, [ Stat.DEF ], -1)
.slicingMove(), .slicingMove(),
new AttackMove(Moves.HEAT_CRASH, Type.FIRE, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) new AttackMove(Moves.HEAT_CRASH, Type.FIRE, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5)
.attr(MinimizeAccuracyAttr) .attr(AlwaysHitMinimizeAttr)
.attr(CompareWeightPowerAttr) .attr(CompareWeightPowerAttr)
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true), .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true),
new AttackMove(Moves.LEAF_TORNADO, Type.GRASS, MoveCategory.SPECIAL, 65, 90, 10, 50, 0, 5) new AttackMove(Moves.LEAF_TORNADO, Type.GRASS, MoveCategory.SPECIAL, 65, 90, 10, 50, 0, 5)
.attr(StatStageChangeAttr, [ Stat.ACC ], -1), .attr(StatStageChangeAttr, [ Stat.ACC ], -1),
new AttackMove(Moves.STEAMROLLER, Type.BUG, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 5) new AttackMove(Moves.STEAMROLLER, Type.BUG, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 5)
.attr(AlwaysHitMinimizeAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true)
.attr(FlinchAttr), .attr(FlinchAttr),
new SelfStatusMove(Moves.COTTON_GUARD, Type.GRASS, -1, 10, -1, 0, 5) new SelfStatusMove(Moves.COTTON_GUARD, Type.GRASS, -1, 10, -1, 0, 5)
.attr(StatStageChangeAttr, [ Stat.DEF ], 3, true), .attr(StatStageChangeAttr, [ Stat.DEF ], 3, true),
@ -8309,7 +8310,7 @@ export function initMoves() {
new AttackMove(Moves.HURRICANE, Type.FLYING, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 5) new AttackMove(Moves.HURRICANE, Type.FLYING, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 5)
.attr(ThunderAccuracyAttr) .attr(ThunderAccuracyAttr)
.attr(ConfuseAttr) .attr(ConfuseAttr)
.attr(HitsTagAttr, BattlerTagType.FLYING, false) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false)
.windMove(), .windMove(),
new AttackMove(Moves.HEAD_CHARGE, Type.NORMAL, MoveCategory.PHYSICAL, 120, 100, 15, -1, 0, 5) new AttackMove(Moves.HEAD_CHARGE, Type.NORMAL, MoveCategory.PHYSICAL, 120, 100, 15, -1, 0, 5)
.attr(RecoilAttr) .attr(RecoilAttr)
@ -8363,9 +8364,9 @@ export function initMoves() {
.attr(LastMoveDoublePowerAttr, Moves.FUSION_FLARE) .attr(LastMoveDoublePowerAttr, Moves.FUSION_FLARE)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.FLYING_PRESS, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 95, 10, -1, 0, 6) new AttackMove(Moves.FLYING_PRESS, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 95, 10, -1, 0, 6)
.attr(MinimizeAccuracyAttr) .attr(AlwaysHitMinimizeAttr)
.attr(FlyingTypeMultiplierAttr) .attr(FlyingTypeMultiplierAttr)
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true)
.condition(failOnGravityCondition), .condition(failOnGravityCondition),
new StatusMove(Moves.MAT_BLOCK, Type.FIGHTING, -1, 10, -1, 0, 6) new StatusMove(Moves.MAT_BLOCK, Type.FIGHTING, -1, 10, -1, 0, 6)
.target(MoveTarget.USER_SIDE) .target(MoveTarget.USER_SIDE)
@ -8535,8 +8536,8 @@ export function initMoves() {
new AttackMove(Moves.THOUSAND_ARROWS, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) new AttackMove(Moves.THOUSAND_ARROWS, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
.attr(NeutralDamageAgainstFlyingTypeMultiplierAttr) .attr(NeutralDamageAgainstFlyingTypeMultiplierAttr)
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true) .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true)
.attr(HitsTagAttr, BattlerTagType.FLYING, false) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false)
.attr(HitsTagAttr, BattlerTagType.MAGNET_RISEN, false) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MAGNET_RISEN, false)
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
.attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN]) .attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
.makesContact(false) .makesContact(false)
@ -8793,6 +8794,8 @@ export function initMoves() {
.partial() .partial()
.ignoresVirtual(), .ignoresVirtual(),
new AttackMove(Moves.MALICIOUS_MOONSAULT, Type.DARK, MoveCategory.PHYSICAL, 180, -1, 1, -1, 0, 7) new AttackMove(Moves.MALICIOUS_MOONSAULT, Type.DARK, MoveCategory.PHYSICAL, 180, -1, 1, -1, 0, 7)
.attr(AlwaysHitMinimizeAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true)
.partial() .partial()
.ignoresVirtual(), .ignoresVirtual(),
new AttackMove(Moves.OCEANIC_OPERETTA, Type.WATER, MoveCategory.SPECIAL, 195, -1, 1, -1, 0, 7) new AttackMove(Moves.OCEANIC_OPERETTA, Type.WATER, MoveCategory.SPECIAL, 195, -1, 1, -1, 0, 7)

View File

@ -746,7 +746,7 @@ export class Arena {
case Biome.TOWN: case Biome.TOWN:
return 7.288; return 7.288;
case Biome.PLAINS: case Biome.PLAINS:
return 7.693; return 17.485;
case Biome.GRASS: case Biome.GRASS:
return 1.995; return 1.995;
case Biome.TALL_GRASS: case Biome.TALL_GRASS:
@ -774,13 +774,13 @@ export class Arena {
case Biome.DESERT: case Biome.DESERT:
return 1.143; return 1.143;
case Biome.ICE_CAVE: case Biome.ICE_CAVE:
return 15.010; return 0.000;
case Biome.MEADOW: case Biome.MEADOW:
return 3.891; return 3.891;
case Biome.POWER_PLANT: case Biome.POWER_PLANT:
return 2.810; return 9.447;
case Biome.VOLCANO: case Biome.VOLCANO:
return 5.116; return 17.637;
case Biome.GRAVEYARD: case Biome.GRAVEYARD:
return 3.232; return 3.232;
case Biome.DOJO: case Biome.DOJO:

View File

@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "../battle-scene";
import { Variant, VariantSet, variantColorCache } from "#app/data/variant"; import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
import { variantData } from "#app/data/variant"; import { variantData } from "#app/data/variant";
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info";
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget } from "../data/move"; import Move, { HighCritAttr, DealsDoubleDamageToTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget } from "../data/move";
import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species"; import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species";
import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils"; import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils";
import * as Utils from "../utils"; import * as Utils from "../utils";
@ -1273,13 +1273,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param attrType {@linkcode AbAttr} The ability attribute to check for. * @param attrType {@linkcode AbAttr} The ability attribute to check for.
* @param canApply {@linkcode Boolean} If false, it doesn't check whether the ability is currently active * @param canApply {@linkcode Boolean} If false, it doesn't check whether the ability is currently active
* @param ignoreOverride {@linkcode Boolean} If true, it ignores ability changing effects * @param ignoreOverride {@linkcode Boolean} If true, it ignores ability changing effects
* @returns {AbAttr[]} A list of all the ability attributes on this ability. * @returns A list of all the ability attributes on this ability.
*/ */
getAbilityAttrs(attrType: { new(...args: any[]): AbAttr }, canApply: boolean = true, ignoreOverride?: boolean): AbAttr[] { getAbilityAttrs<T extends AbAttr = AbAttr>(attrType: { new(...args: any[]): T }, canApply: boolean = true, ignoreOverride?: boolean): T[] {
const abilityAttrs: AbAttr[] = []; const abilityAttrs: T[] = [];
if (!canApply || this.canApplyAbility()) { if (!canApply || this.canApplyAbility()) {
abilityAttrs.push(...this.getAbility(ignoreOverride).getAttrs(attrType)); abilityAttrs.push(...this.getAbility(ignoreOverride).getAttrs<T>(attrType));
} }
if (!canApply || this.canApplyAbility(true)) { if (!canApply || this.canApplyAbility(true)) {
@ -1513,7 +1513,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const immuneTags = this.findTags(tag => tag instanceof TypeImmuneTag && tag.immuneType === moveType); const immuneTags = this.findTags(tag => tag instanceof TypeImmuneTag && tag.immuneType === moveType);
for (const tag of immuneTags) { for (const tag of immuneTags) {
if (move && !move.getAttrs(HitsTagAttr).some(attr => attr.tagType === tag.tagType)) { if (move && !move.getAttrs(DealsDoubleDamageToTagAttr).some(attr => attr.tagType === tag.tagType)) {
typeMultiplier.value = 0; typeMultiplier.value = 0;
break; break;
} }
@ -2489,13 +2489,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, move.category, this.scene.currentBattle.double, screenMultiplier); this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, move.category, this.scene.currentBattle.double, screenMultiplier);
/** /**
* For each {@linkcode HitsTagAttr} the move has, doubles the damage of the move if: * For each {@linkcode DealsDoubleDamageToTagAttr} the move has, doubles the damage of the move if:
* The target has a {@linkcode BattlerTagType} that this move interacts with * The target has a {@linkcode BattlerTagType} that this move interacts with
* AND * AND
* The move doubles damage when used against that tag * The move doubles damage when used against that tag
*/ */
const hitsTagMultiplier = new Utils.NumberHolder(1); const hitsTagMultiplier = new Utils.NumberHolder(1);
move.getAttrs(HitsTagAttr).filter(hta => hta.doubleDamage).forEach(hta => { move.getAttrs(DealsDoubleDamageToTagAttr).filter(hta => hta.doubleDamage).forEach(hta => {
if (this.getTag(hta.tagType)) { if (this.getTag(hta.tagType)) {
hitsTagMultiplier.value *= 2; hitsTagMultiplier.value *= 2;
} }

View File

@ -108,7 +108,7 @@
"forest": "PMD EoS Dusk Forest", "forest": "PMD EoS Dusk Forest",
"grass": "PMD EoS Apple Woods", "grass": "PMD EoS Apple Woods",
"graveyard": "PMD EoS Mystifying Forest", "graveyard": "PMD EoS Mystifying Forest",
"ice_cave": "PMD EoS Vast Ice Mountain", "ice_cave": "Firel - -60F",
"island": "PMD EoS Craggy Coast", "island": "PMD EoS Craggy Coast",
"jungle": "Lmz - Jungle", "jungle": "Lmz - Jungle",
"laboratory": "Firel - Laboratory", "laboratory": "Firel - Laboratory",
@ -116,8 +116,8 @@
"meadow": "PMD EoS Sky Peak Forest", "meadow": "PMD EoS Sky Peak Forest",
"metropolis": "Firel - Metropolis", "metropolis": "Firel - Metropolis",
"mountain": "PMD EoS Mt. Horn", "mountain": "PMD EoS Mt. Horn",
"plains": "PMD EoS Sky Peak Prairie", "plains": "Firel - Route 888",
"power_plant": "PMD EoS Far Amp Plains", "power_plant": "Firel - The Klink",
"ruins": "PMD EoS Deep Sealed Ruin", "ruins": "PMD EoS Deep Sealed Ruin",
"sea": "Andr06 - Marine Mystique", "sea": "Andr06 - Marine Mystique",
"seabed": "Firel - Seabed", "seabed": "Firel - Seabed",
@ -128,7 +128,7 @@
"tall_grass": "PMD EoS Foggy Forest", "tall_grass": "PMD EoS Foggy Forest",
"temple": "PMD EoS Aegis Cave", "temple": "PMD EoS Aegis Cave",
"town": "PMD EoS Random Dungeon Theme 3", "town": "PMD EoS Random Dungeon Theme 3",
"volcano": "PMD EoS Steam Cave", "volcano": "Firel - Twisturn Volcano",
"wasteland": "PMD EoS Hidden Highland", "wasteland": "PMD EoS Hidden Highland",
"encounter_ace_trainer": "BW Trainers' Eyes Meet (Ace Trainer)", "encounter_ace_trainer": "BW Trainers' Eyes Meet (Ace Trainer)",
"encounter_backpacker": "BW Trainers' Eyes Meet (Backpacker)", "encounter_backpacker": "BW Trainers' Eyes Meet (Backpacker)",

View File

@ -4,7 +4,7 @@ import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr,
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 { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags";
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr } from "#app/data/move"; import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, DealsDoubleDamageToTagAttr } 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 { BattlerTagType } from "#app/enums/battler-tag-type";
import { Moves } from "#app/enums/moves"; import { Moves } from "#app/enums/moves";
@ -394,7 +394,7 @@ export class MoveEffectPhase extends PokemonPhase {
} }
const semiInvulnerableTag = target.getTag(SemiInvulnerableTag); const semiInvulnerableTag = target.getTag(SemiInvulnerableTag);
if (semiInvulnerableTag && !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)) { if (semiInvulnerableTag && !this.move.getMove().getAttrs(DealsDoubleDamageToTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)) {
return false; return false;
} }

View File

@ -6,7 +6,7 @@ import Pokemon from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { ResetNegativeStatStageModifier } from "#app/modifier/modifier"; import { ResetNegativeStatStageModifier } from "#app/modifier/modifier";
import { handleTutorial, Tutorial } from "#app/tutorial"; import { handleTutorial, Tutorial } from "#app/tutorial";
import * as Utils from "#app/utils"; import { NumberHolder, BooleanHolder } from "#app/utils";
import i18next from "i18next"; import i18next from "i18next";
import { PokemonPhase } from "./pokemon-phase"; import { PokemonPhase } from "./pokemon-phase";
import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat"; import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat";
@ -42,17 +42,23 @@ export class StatStageChangePhase extends PokemonPhase {
return this.end(); return this.end();
} }
const stages = new NumberHolder(this.stages);
if (!this.ignoreAbilities) {
applyAbAttrs(StatStageChangeMultiplierAbAttr, pokemon, null, false, stages);
}
let simulate = false; let simulate = false;
const filteredStats = this.stats.filter(stat => { const filteredStats = this.stats.filter(stat => {
const cancelled = new Utils.BooleanHolder(false); const cancelled = new BooleanHolder(false);
if (!this.selfTarget && this.stages < 0) { if (!this.selfTarget && stages.value < 0) {
// TODO: Include simulate boolean when tag applications can be simulated // TODO: Include simulate boolean when tag applications can be simulated
this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled); this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled);
} }
if (!cancelled.value && !this.selfTarget && this.stages < 0) { if (!cancelled.value && !this.selfTarget && stages.value < 0) {
applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled, simulate); applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled, simulate);
} }
@ -64,12 +70,6 @@ export class StatStageChangePhase extends PokemonPhase {
return !cancelled.value; return !cancelled.value;
}); });
const stages = new Utils.IntegerHolder(this.stages);
if (!this.ignoreAbilities) {
applyAbAttrs(StatStageChangeMultiplierAbAttr, pokemon, null, false, stages);
}
const relLevels = filteredStats.map(s => (stages.value >= 1 ? Math.min(pokemon.getStatStage(s) + stages.value, 6) : Math.max(pokemon.getStatStage(s) + stages.value, -6)) - pokemon.getStatStage(s)); const relLevels = filteredStats.map(s => (stages.value >= 1 ? Math.min(pokemon.getStatStage(s) + stages.value, 6) : Math.max(pokemon.getStatStage(s) + stages.value, -6)) - pokemon.getStatStage(s));
this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels); this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels);

View File

@ -31,7 +31,7 @@ describe("Abilities - Contrary", () => {
}); });
it("should invert stat changes when applied", async() => { it("should invert stat changes when applied", async() => {
await game.startBattle([ await game.classicMode.startBattle([
Species.SLOWBRO Species.SLOWBRO
]); ]);
@ -39,4 +39,39 @@ describe("Abilities - Contrary", () => {
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
}, 20000); }, 20000);
describe("With Clear Body", () => {
it("should apply positive effects", async () => {
game.override
.enemyPassiveAbility(Abilities.CLEAR_BODY)
.moveset([Moves.TAIL_WHIP]);
await game.classicMode.startBattle([Species.SLOWBRO]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
game.move.select(Moves.TAIL_WHIP);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(1);
});
it("should block negative effects", async () => {
game.override
.enemyPassiveAbility(Abilities.CLEAR_BODY)
.enemyMoveset([Moves.HOWL, Moves.HOWL, Moves.HOWL, Moves.HOWL])
.moveset([Moves.SPLASH]);
await game.classicMode.startBattle([Species.SLOWBRO]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
});
});
}); });

View File

@ -0,0 +1,58 @@
import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { DamageCalculationResult } from "#app/field/pokemon";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Moves - Steamroller", () => {
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.STEAMROLLER]).battleType("single").enemyAbility(Abilities.BALL_FETCH);
});
it("should always hit a minimzed target with double damage", async () => {
game.override.enemySpecies(Species.DITTO).enemyMoveset(Moves.MINIMIZE);
await game.classicMode.startBattle([Species.IRON_BOULDER]);
const ditto = game.scene.getEnemyPokemon()!;
vi.spyOn(ditto, "getAttackDamage");
ditto.hp = 5000;
const steamroller = allMoves[Moves.STEAMROLLER];
vi.spyOn(steamroller, "calculateBattleAccuracy");
const ironBoulder = game.scene.getPlayerPokemon()!;
vi.spyOn(ironBoulder, "getAccuracyMultiplier");
// Turn 1
game.move.select(Moves.STEAMROLLER);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.toNextTurn();
// Turn 2
game.move.select(Moves.STEAMROLLER);
await game.toNextTurn();
const [dmgCalcTurn1, dmgCalcTurn2]: DamageCalculationResult[] = vi
.mocked(ditto.getAttackDamage)
.mock.results.map((r) => r.value);
expect(dmgCalcTurn2.damage).toBeGreaterThanOrEqual(dmgCalcTurn1.damage * 2);
expect(ditto.getTag(BattlerTagType.MINIMIZED)).toBeDefined();
expect(steamroller.calculateBattleAccuracy).toHaveReturnedWith(-1);
});
});

View File

@ -0,0 +1,53 @@
import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest";
describe("Moves - Whirlwind", () => {
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
.battleType("single")
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.WHIRLWIND)
.enemySpecies(Species.PIDGEY);
});
it.each([
{ move: Moves.FLY, name: "Fly" },
{ move: Moves.BOUNCE, name: "Bounce" },
{ move: Moves.SKY_DROP, name: "Sky Drop" },
])("should not hit a flying target: $name (=$move)", async ({ move }) => {
game.override.moveset([move]);
await game.classicMode.startBattle([Species.STARAPTOR]);
const staraptor = game.scene.getPlayerPokemon()!;
const whirlwind = allMoves[Moves.WHIRLWIND];
vi.spyOn(whirlwind, "getFailedText");
game.move.select(move);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.toNextTurn();
expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined();
expect(whirlwind.getFailedText).toHaveBeenCalledOnce();
});
});

View File

@ -162,7 +162,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
this.splicedIcon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 12, 15), Phaser.Geom.Rectangle.Contains); this.splicedIcon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 12, 15), Phaser.Geom.Rectangle.Contains);
this.add(this.splicedIcon); this.add(this.splicedIcon);
this.statusIndicator = this.scene.add.sprite(0, 0, `statuses_${i18next.resolvedLanguage}`); this.statusIndicator = this.scene.add.sprite(0, 0, Utils.getLocalizedSpriteKey("statuses"));
this.statusIndicator.setName("icon_status"); this.statusIndicator.setName("icon_status");
this.statusIndicator.setVisible(false); this.statusIndicator.setVisible(false);
this.statusIndicator.setOrigin(0, 0); this.statusIndicator.setOrigin(0, 0);

View File

@ -91,7 +91,7 @@ export default class MoveInfoOverlay extends Phaser.GameObjects.Container implem
valuesBg.setOrigin(0, 0); valuesBg.setOrigin(0, 0);
this.val.add(valuesBg); this.val.add(valuesBg);
this.typ = this.scene.add.sprite(25, EFF_HEIGHT - 35, `types${Utils.verifyLang(i18next.language) ? `_${i18next.language}` : ""}`, "unknown"); this.typ = this.scene.add.sprite(25, EFF_HEIGHT - 35, Utils.getLocalizedSpriteKey("types"), "unknown");
this.typ.setScale(0.8); this.typ.setScale(0.8);
this.val.add(this.typ); this.val.add(this.typ);
@ -138,7 +138,7 @@ export default class MoveInfoOverlay extends Phaser.GameObjects.Container implem
this.pow.setText(move.power >= 0 ? move.power.toString() : "---"); this.pow.setText(move.power >= 0 ? move.power.toString() : "---");
this.acc.setText(move.accuracy >= 0 ? move.accuracy.toString() : "---"); this.acc.setText(move.accuracy >= 0 ? move.accuracy.toString() : "---");
this.pp.setText(move.pp >= 0 ? move.pp.toString() : "---"); this.pp.setText(move.pp >= 0 ? move.pp.toString() : "---");
this.typ.setTexture(`types${Utils.verifyLang(i18next.language) ? `_${i18next.language}` : ""}`, Type[move.type].toLowerCase()); this.typ.setTexture(Utils.getLocalizedSpriteKey("types"), Type[move.type].toLowerCase());
this.cat.setFrame(MoveCategory[move.category].toLowerCase()); this.cat.setFrame(MoveCategory[move.category].toLowerCase());
this.desc.setText(move?.effect || ""); this.desc.setText(move?.effect || "");

View File

@ -1272,7 +1272,7 @@ class PartySlot extends Phaser.GameObjects.Container {
} }
if (this.pokemon.status) { if (this.pokemon.status) {
const statusIndicator = this.scene.add.sprite(0, 0, `statuses_${i18next.resolvedLanguage}`); const statusIndicator = this.scene.add.sprite(0, 0, Utils.getLocalizedSpriteKey("statuses"));
statusIndicator.setFrame(StatusEffect[this.pokemon.status?.effect].toLowerCase()); statusIndicator.setFrame(StatusEffect[this.pokemon.status?.effect].toLowerCase());
statusIndicator.setOrigin(0, 0); statusIndicator.setOrigin(0, 0);
statusIndicator.setPositionRelative(slotLevelLabel, this.slotIndex >= battlerCount ? 43 : 55, 0); statusIndicator.setPositionRelative(slotLevelLabel, this.slotIndex >= battlerCount ? 43 : 55, 0);

View File

@ -214,7 +214,7 @@ export default class SummaryUiHandler extends UiHandler {
this.statusContainer.add(statusLabel); this.statusContainer.add(statusLabel);
this.status = this.scene.add.sprite(91, 4, `statuses_${i18next.resolvedLanguage}`); this.status = this.scene.add.sprite(91, 4, Utils.getLocalizedSpriteKey("statuses"));
this.status.setOrigin(0.5, 0); this.status.setOrigin(0.5, 0);
this.statusContainer.add(this.status); this.statusContainer.add(this.status);

View File

@ -1,4 +1,5 @@
import { MoneyFormat } from "#enums/money-format"; import { MoneyFormat } from "#enums/money-format";
import { Moves } from "#enums/moves";
import i18next from "i18next"; import i18next from "i18next";
export const MissingTextureKey = "__MISSING"; export const MissingTextureKey = "__MISSING";
@ -628,3 +629,12 @@ export function getLocalizedSpriteKey(baseKey: string) {
export function isBetween(num: number, min: number, max: number): boolean { export function isBetween(num: number, min: number, max: number): boolean {
return num >= min && num <= max; return num >= min && num <= max;
} }
/**
* Helper method to return the animation filename for a given move
*
* @param move the move for which the animation filename is needed
*/
export function animationFileName(move: Moves): string {
return Moves[move].toLowerCase().replace(/\_/g, "-");
}