mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-21 09:02:47 +02:00
[Ability][Move] Rewrite Type Resolution and Effectiveness Calculation Functions (#3704)
* Make type/category read-only * Fix protean/libero tests * Refactor Pokemon type effectiveness calculation * Merge getMoveEffectiveness and getAttackMoveEffectiveness * Move priority-blocking ability check * Fix incorrect early stopping implementation in MultiHitAttr * Fix Aerilate, etc. affecting variable-type moves * Thunder Wave now respects Attack type immunities * Use final move types for pre-defend abilities * Steal some things from flx's PR hehe * Fix Thousand Arrows + "No effect" messages * Fix status type effectiveness check * Another status move effectiveness update + some docs * changing status logic again... * Fix unnecessary "No Effect" message for Volt Absorb, etc * Add type effectiveness unit test * Add Galvanize integration tests * Add multi-hit test to galvanize tests * Add power check to first Galvanize test * Add missing doc line Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com> * Resolve torranx's nits * Apply suggestions from Kev's code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * More suggestions I missed Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Optimize effectiveness test and make others more stylish (#3) * Resolve Kev's remaining nits and some test issues --------- Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: flx-sta Co-authored-by: frutescens
This commit is contained in:
parent
443e4bd24c
commit
0221c9faba
@ -8,7 +8,7 @@ import { Weather, WeatherType } from "./weather";
|
|||||||
import { BattlerTag, GroundedTag, GulpMissileTag, SemiInvulnerableTag } from "./battler-tags";
|
import { BattlerTag, GroundedTag, GulpMissileTag, SemiInvulnerableTag } from "./battler-tags";
|
||||||
import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
|
import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
|
||||||
import { Gender } from "./gender";
|
import { Gender } from "./gender";
|
||||||
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit } from "./move";
|
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr } from "./move";
|
||||||
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
|
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
|
||||||
import { Stat, getStatName } from "./pokemon-stat";
|
import { Stat, getStatName } from "./pokemon-stat";
|
||||||
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier";
|
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier";
|
||||||
@ -349,7 +349,7 @@ export class TypeImmunityAbAttr extends PreDefendAbAttr {
|
|||||||
if ([ MoveTarget.BOTH_SIDES, MoveTarget.ENEMY_SIDE, MoveTarget.USER_SIDE ].includes(move.moveTarget)) {
|
if ([ MoveTarget.BOTH_SIDES, MoveTarget.ENEMY_SIDE, MoveTarget.USER_SIDE ].includes(move.moveTarget)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (attacker !== pokemon && move.type === this.immuneType) {
|
if (attacker !== pokemon && attacker.getMoveType(move) === this.immuneType) {
|
||||||
(args[0] as Utils.NumberHolder).value = 0;
|
(args[0] as Utils.NumberHolder).value = 0;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -372,7 +372,8 @@ export class AttackTypeImmunityAbAttr extends TypeImmunityAbAttr {
|
|||||||
* Example: Levitate
|
* Example: Levitate
|
||||||
*/
|
*/
|
||||||
applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean {
|
applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean {
|
||||||
if (move.category !== MoveCategory.STATUS) {
|
// this is a hacky way to fix the Levitate/Thousand Arrows interaction, but it works for now...
|
||||||
|
if (move.category !== MoveCategory.STATUS && !move.hasAttr(NeutralDamageAgainstFlyingTypeMultiplierAttr)) {
|
||||||
return super.applyPreDefend(pokemon, passive, simulated, attacker, move, cancelled, args);
|
return super.applyPreDefend(pokemon, passive, simulated, attacker, move, cancelled, args);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -392,6 +393,7 @@ export class TypeImmunityHealAbAttr extends TypeImmunityAbAttr {
|
|||||||
const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name;
|
const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name;
|
||||||
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(),
|
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(),
|
||||||
Utils.toDmgValue(pokemon.getMaxHp() / 4), i18next.t("abilityTriggers:typeImmunityHeal", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }), true));
|
Utils.toDmgValue(pokemon.getMaxHp() / 4), i18next.t("abilityTriggers:typeImmunityHeal", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }), true));
|
||||||
|
cancelled.value = true; // Suppresses "No Effect" message
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -415,7 +417,7 @@ class TypeImmunityStatChangeAbAttr extends TypeImmunityAbAttr {
|
|||||||
const ret = super.applyPreDefend(pokemon, passive, simulated, attacker, move, cancelled, args);
|
const ret = super.applyPreDefend(pokemon, passive, simulated, attacker, move, cancelled, args);
|
||||||
|
|
||||||
if (ret) {
|
if (ret) {
|
||||||
cancelled.value = true;
|
cancelled.value = true; // Suppresses "No Effect" message
|
||||||
if (!simulated) {
|
if (!simulated) {
|
||||||
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ this.stat ], this.levels));
|
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ this.stat ], this.levels));
|
||||||
}
|
}
|
||||||
@ -440,7 +442,7 @@ class TypeImmunityAddBattlerTagAbAttr extends TypeImmunityAbAttr {
|
|||||||
const ret = super.applyPreDefend(pokemon, passive, simulated, attacker, move, cancelled, args);
|
const ret = super.applyPreDefend(pokemon, passive, simulated, attacker, move, cancelled, args);
|
||||||
|
|
||||||
if (ret) {
|
if (ret) {
|
||||||
cancelled.value = true;
|
cancelled.value = true; // Suppresses "No Effect" message
|
||||||
if (!simulated) {
|
if (!simulated) {
|
||||||
pokemon.addTag(this.tagType, this.turnCount, undefined, pokemon.id);
|
pokemon.addTag(this.tagType, this.turnCount, undefined, pokemon.id);
|
||||||
}
|
}
|
||||||
@ -456,8 +458,8 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean {
|
applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean {
|
||||||
if (move instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.type, attacker) < 2) {
|
if (move instanceof AttackMove && pokemon.getAttackTypeEffectiveness(pokemon.getMoveType(move), attacker) < 2) {
|
||||||
cancelled.value = true;
|
cancelled.value = true; // Suppresses "No Effect" message
|
||||||
(args[0] as Utils.NumberHolder).value = 0;
|
(args[0] as Utils.NumberHolder).value = 0;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -764,7 +766,7 @@ export class PostDefendTypeChangeAbAttr extends PostDefendAbAttr {
|
|||||||
if (simulated) {
|
if (simulated) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const type = move.type;
|
const type = attacker.getMoveType(move);
|
||||||
const pokemonTypes = pokemon.getTypes(true);
|
const pokemonTypes = pokemon.getTypes(true);
|
||||||
if (pokemonTypes.length !== 1 || pokemonTypes[0] !== type) {
|
if (pokemonTypes.length !== 1 || pokemonTypes[0] !== type) {
|
||||||
pokemon.summonData.types = [ type ];
|
pokemon.summonData.types = [ type ];
|
||||||
@ -1212,7 +1214,7 @@ export class FieldMultiplyBattleStatAbAttr extends AbAttr {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MoveTypeChangeAttr extends PreAttackAbAttr {
|
export class MoveTypeChangeAbAttr extends PreAttackAbAttr {
|
||||||
constructor(
|
constructor(
|
||||||
private newType: Type,
|
private newType: Type,
|
||||||
private powerMultiplier: number,
|
private powerMultiplier: number,
|
||||||
@ -1221,11 +1223,14 @@ export class MoveTypeChangeAttr extends PreAttackAbAttr {
|
|||||||
super(true);
|
super(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Decouple this into two attributes (type change / power boost)
|
||||||
applyPreAttack(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, args: any[]): boolean {
|
applyPreAttack(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, args: any[]): boolean {
|
||||||
if (this.condition && this.condition(pokemon, defender, move)) {
|
if (this.condition && this.condition(pokemon, defender, move)) {
|
||||||
move.type = this.newType;
|
|
||||||
if (args[0] && args[0] instanceof Utils.NumberHolder) {
|
if (args[0] && args[0] instanceof Utils.NumberHolder) {
|
||||||
args[0].value *= this.powerMultiplier;
|
args[0].value = this.newType;
|
||||||
|
}
|
||||||
|
if (args[1] && args[1] instanceof Utils.NumberHolder) {
|
||||||
|
args[1].value *= this.powerMultiplier;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -1257,22 +1262,12 @@ export class PokemonTypeChangeAbAttr extends PreAttackAbAttr {
|
|||||||
attr instanceof CopyMoveAttr
|
attr instanceof CopyMoveAttr
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// TODO remove this copy when phase order is changed so that damage, type, category, etc.
|
const moveType = pokemon.getMoveType(move);
|
||||||
// TODO are all calculated prior to playing the move animation.
|
|
||||||
const moveCopy = new Move(move.id, move.type, move.category, move.moveTarget, move.power, move.accuracy, move.pp, move.chance, move.priority, move.generation);
|
|
||||||
moveCopy.attrs = move.attrs;
|
|
||||||
|
|
||||||
// Moves like Weather Ball ignore effects of abilities like Normalize and Refrigerate
|
if (pokemon.getTypes().some((t) => t !== moveType)) {
|
||||||
if (move.findAttr(attr => attr instanceof VariableMoveTypeAttr)) {
|
|
||||||
applyMoveAttrs(VariableMoveTypeAttr, pokemon, null, moveCopy);
|
|
||||||
} else {
|
|
||||||
applyPreAttackAbAttrs(MoveTypeChangeAttr, pokemon, null, moveCopy);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pokemon.getTypes().some((t) => t !== moveCopy.type)) {
|
|
||||||
if (!simulated) {
|
if (!simulated) {
|
||||||
this.moveType = moveCopy.type;
|
this.moveType = moveType;
|
||||||
pokemon.summonData.types = [moveCopy.type];
|
pokemon.summonData.types = [moveType];
|
||||||
pokemon.updateInfo();
|
pokemon.updateInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2978,16 +2973,20 @@ function getAnticipationCondition(): AbAttrCondition {
|
|||||||
return (pokemon: Pokemon) => {
|
return (pokemon: Pokemon) => {
|
||||||
for (const opponent of pokemon.getOpponents()) {
|
for (const opponent of pokemon.getOpponents()) {
|
||||||
for (const move of opponent.moveset) {
|
for (const move of opponent.moveset) {
|
||||||
// move is super effective
|
// ignore null/undefined moves
|
||||||
if (move!.getMove() instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move!.getMove().type, opponent, true) >= 2) { // TODO: is this bang correct?
|
if (!move) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// the move's base type (not accounting for variable type changes) is super effective
|
||||||
|
if (move.getMove() instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.getMove().type, opponent, true) >= 2) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// move is a OHKO
|
// move is a OHKO
|
||||||
if (move?.getMove().hasAttr(OneHitKOAttr)) {
|
if (move.getMove().hasAttr(OneHitKOAttr)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// edge case for hidden power, type is computed
|
// edge case for hidden power, type is computed
|
||||||
if (move?.getMove().id === Moves.HIDDEN_POWER) {
|
if (move.getMove().id === Moves.HIDDEN_POWER) {
|
||||||
const iv_val = Math.floor(((opponent.ivs[Stat.HP] & 1)
|
const iv_val = Math.floor(((opponent.ivs[Stat.HP] & 1)
|
||||||
+(opponent.ivs[Stat.ATK] & 1) * 2
|
+(opponent.ivs[Stat.ATK] & 1) * 2
|
||||||
+(opponent.ivs[Stat.DEF] & 1) * 4
|
+(opponent.ivs[Stat.DEF] & 1) * 4
|
||||||
@ -5019,7 +5018,7 @@ export function initAbilities() {
|
|||||||
.conditionalAttr(pokemon => pokemon.status ? pokemon.status.effect === StatusEffect.PARALYSIS : false, BattleStatMultiplierAbAttr, BattleStat.SPD, 2)
|
.conditionalAttr(pokemon => pokemon.status ? pokemon.status.effect === StatusEffect.PARALYSIS : false, BattleStatMultiplierAbAttr, BattleStat.SPD, 2)
|
||||||
.conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(Abilities.COMATOSE), BattleStatMultiplierAbAttr, BattleStat.SPD, 1.5),
|
.conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(Abilities.COMATOSE), BattleStatMultiplierAbAttr, BattleStat.SPD, 1.5),
|
||||||
new Ability(Abilities.NORMALIZE, 4)
|
new Ability(Abilities.NORMALIZE, 4)
|
||||||
.attr(MoveTypeChangeAttr, Type.NORMAL, 1.2, (user, target, move) => {
|
.attr(MoveTypeChangeAbAttr, Type.NORMAL, 1.2, (user, target, move) => {
|
||||||
return ![Moves.HIDDEN_POWER, Moves.WEATHER_BALL, Moves.NATURAL_GIFT, Moves.JUDGMENT, Moves.TECHNO_BLAST].includes(move.id);
|
return ![Moves.HIDDEN_POWER, Moves.WEATHER_BALL, Moves.NATURAL_GIFT, Moves.JUDGMENT, Moves.TECHNO_BLAST].includes(move.id);
|
||||||
}),
|
}),
|
||||||
new Ability(Abilities.SNIPER, 4)
|
new Ability(Abilities.SNIPER, 4)
|
||||||
@ -5260,7 +5259,7 @@ export function initAbilities() {
|
|||||||
new Ability(Abilities.STRONG_JAW, 6)
|
new Ability(Abilities.STRONG_JAW, 6)
|
||||||
.attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.BITING_MOVE), 1.5),
|
.attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.BITING_MOVE), 1.5),
|
||||||
new Ability(Abilities.REFRIGERATE, 6)
|
new Ability(Abilities.REFRIGERATE, 6)
|
||||||
.attr(MoveTypeChangeAttr, Type.ICE, 1.2, (user, target, move) => move.type === Type.NORMAL),
|
.attr(MoveTypeChangeAbAttr, Type.ICE, 1.2, (user, target, move) => move.type === Type.NORMAL && !move.hasAttr(VariableMoveTypeAttr)),
|
||||||
new Ability(Abilities.SWEET_VEIL, 6)
|
new Ability(Abilities.SWEET_VEIL, 6)
|
||||||
.attr(UserFieldStatusEffectImmunityAbAttr, StatusEffect.SLEEP)
|
.attr(UserFieldStatusEffectImmunityAbAttr, StatusEffect.SLEEP)
|
||||||
.attr(UserFieldBattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
|
.attr(UserFieldBattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
|
||||||
@ -5283,11 +5282,11 @@ export function initAbilities() {
|
|||||||
new Ability(Abilities.TOUGH_CLAWS, 6)
|
new Ability(Abilities.TOUGH_CLAWS, 6)
|
||||||
.attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), 1.3),
|
.attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), 1.3),
|
||||||
new Ability(Abilities.PIXILATE, 6)
|
new Ability(Abilities.PIXILATE, 6)
|
||||||
.attr(MoveTypeChangeAttr, Type.FAIRY, 1.2, (user, target, move) => move.type === Type.NORMAL),
|
.attr(MoveTypeChangeAbAttr, Type.FAIRY, 1.2, (user, target, move) => move.type === Type.NORMAL && !move.hasAttr(VariableMoveTypeAttr)),
|
||||||
new Ability(Abilities.GOOEY, 6)
|
new Ability(Abilities.GOOEY, 6)
|
||||||
.attr(PostDefendStatChangeAbAttr, (target, user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), BattleStat.SPD, -1, false),
|
.attr(PostDefendStatChangeAbAttr, (target, user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), BattleStat.SPD, -1, false),
|
||||||
new Ability(Abilities.AERILATE, 6)
|
new Ability(Abilities.AERILATE, 6)
|
||||||
.attr(MoveTypeChangeAttr, Type.FLYING, 1.2, (user, target, move) => move.type === Type.NORMAL),
|
.attr(MoveTypeChangeAbAttr, Type.FLYING, 1.2, (user, target, move) => move.type === Type.NORMAL && !move.hasAttr(VariableMoveTypeAttr)),
|
||||||
new Ability(Abilities.PARENTAL_BOND, 6)
|
new Ability(Abilities.PARENTAL_BOND, 6)
|
||||||
.attr(AddSecondStrikeAbAttr, 0.25),
|
.attr(AddSecondStrikeAbAttr, 0.25),
|
||||||
new Ability(Abilities.DARK_AURA, 6)
|
new Ability(Abilities.DARK_AURA, 6)
|
||||||
@ -5359,11 +5358,11 @@ export function initAbilities() {
|
|||||||
new Ability(Abilities.LONG_REACH, 7)
|
new Ability(Abilities.LONG_REACH, 7)
|
||||||
.attr(IgnoreContactAbAttr),
|
.attr(IgnoreContactAbAttr),
|
||||||
new Ability(Abilities.LIQUID_VOICE, 7)
|
new Ability(Abilities.LIQUID_VOICE, 7)
|
||||||
.attr(MoveTypeChangeAttr, Type.WATER, 1, (user, target, move) => move.hasFlag(MoveFlags.SOUND_BASED)),
|
.attr(MoveTypeChangeAbAttr, Type.WATER, 1, (user, target, move) => move.hasFlag(MoveFlags.SOUND_BASED)),
|
||||||
new Ability(Abilities.TRIAGE, 7)
|
new Ability(Abilities.TRIAGE, 7)
|
||||||
.attr(ChangeMovePriorityAbAttr, (pokemon, move) => move.hasFlag(MoveFlags.TRIAGE_MOVE), 3),
|
.attr(ChangeMovePriorityAbAttr, (pokemon, move) => move.hasFlag(MoveFlags.TRIAGE_MOVE), 3),
|
||||||
new Ability(Abilities.GALVANIZE, 7)
|
new Ability(Abilities.GALVANIZE, 7)
|
||||||
.attr(MoveTypeChangeAttr, Type.ELECTRIC, 1.2, (user, target, move) => move.type === Type.NORMAL),
|
.attr(MoveTypeChangeAbAttr, Type.ELECTRIC, 1.2, (user, target, move) => move.type === Type.NORMAL && !move.hasAttr(VariableMoveTypeAttr)),
|
||||||
new Ability(Abilities.SURGE_SURFER, 7)
|
new Ability(Abilities.SURGE_SURFER, 7)
|
||||||
.conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), BattleStatMultiplierAbAttr, BattleStat.SPD, 2),
|
.conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), BattleStatMultiplierAbAttr, BattleStat.SPD, 2),
|
||||||
new Ability(Abilities.SCHOOLING, 7)
|
new Ability(Abilities.SCHOOLING, 7)
|
||||||
|
202
src/data/move.ts
202
src/data/move.ts
@ -9,7 +9,7 @@ import { Constructor } from "#app/utils";
|
|||||||
import * as Utils from "../utils";
|
import * as Utils from "../utils";
|
||||||
import { WeatherType } from "./weather";
|
import { WeatherType } from "./weather";
|
||||||
import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag";
|
import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag";
|
||||||
import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr, IgnoreProtectOnContactAbAttr, IgnoreMoveEffectsAbAttr, applyPreDefendAbAttrs, MoveEffectChanceMultiplierAbAttr, WonderSkinAbAttr, applyPreAttackAbAttrs, MoveTypeChangeAttr, UserFieldMoveTypePowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AllyMoveCategoryPowerBoostAbAttr, VariableMovePowerAbAttr } from "./ability";
|
import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr, IgnoreProtectOnContactAbAttr, IgnoreMoveEffectsAbAttr, applyPreDefendAbAttrs, MoveEffectChanceMultiplierAbAttr, WonderSkinAbAttr, applyPreAttackAbAttrs, MoveTypeChangeAbAttr, UserFieldMoveTypePowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AllyMoveCategoryPowerBoostAbAttr, VariableMovePowerAbAttr } from "./ability";
|
||||||
import { allAbilities } from "./ability";
|
import { allAbilities } from "./ability";
|
||||||
import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier, PokemonMoveAccuracyBoosterModifier, AttackTypeBoosterModifier, PokemonMultiHitModifier } from "../modifier/modifier";
|
import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier, PokemonMoveAccuracyBoosterModifier, AttackTypeBoosterModifier, PokemonMultiHitModifier } from "../modifier/modifier";
|
||||||
import { BattlerIndex, BattleType } from "../battle";
|
import { BattlerIndex, BattleType } from "../battle";
|
||||||
@ -113,9 +113,8 @@ type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean;
|
|||||||
export default class Move implements Localizable {
|
export default class Move implements Localizable {
|
||||||
public id: Moves;
|
public id: Moves;
|
||||||
public name: string;
|
public name: string;
|
||||||
public type: Type;
|
private _type: Type;
|
||||||
public defaultType: Type;
|
private _category: MoveCategory;
|
||||||
public category: MoveCategory;
|
|
||||||
public moveTarget: MoveTarget;
|
public moveTarget: MoveTarget;
|
||||||
public power: integer;
|
public power: integer;
|
||||||
public accuracy: integer;
|
public accuracy: integer;
|
||||||
@ -133,9 +132,8 @@ export default class Move implements Localizable {
|
|||||||
this.id = id;
|
this.id = id;
|
||||||
|
|
||||||
this.nameAppend = "";
|
this.nameAppend = "";
|
||||||
this.type = type;
|
this._type = type;
|
||||||
this.defaultType = type;
|
this._category = category;
|
||||||
this.category = category;
|
|
||||||
this.moveTarget = defaultMoveTarget;
|
this.moveTarget = defaultMoveTarget;
|
||||||
this.power = power;
|
this.power = power;
|
||||||
this.accuracy = accuracy;
|
this.accuracy = accuracy;
|
||||||
@ -158,6 +156,13 @@ export default class Move implements Localizable {
|
|||||||
this.localize();
|
this.localize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get type() {
|
||||||
|
return this._type;
|
||||||
|
}
|
||||||
|
get category() {
|
||||||
|
return this._category;
|
||||||
|
}
|
||||||
|
|
||||||
localize(): void {
|
localize(): void {
|
||||||
const i18nKey = Moves[this.id].split("_").filter(f => f).map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join("") as unknown as string;
|
const i18nKey = Moves[this.id].split("_").filter(f => f).map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join("") as unknown as string;
|
||||||
|
|
||||||
@ -733,7 +738,7 @@ export default class Move implements Localizable {
|
|||||||
const power = new Utils.NumberHolder(this.power);
|
const power = new Utils.NumberHolder(this.power);
|
||||||
const typeChangeMovePowerMultiplier = new Utils.NumberHolder(1);
|
const typeChangeMovePowerMultiplier = new Utils.NumberHolder(1);
|
||||||
|
|
||||||
applyPreAttackAbAttrs(MoveTypeChangeAttr, source, target, this, simulated, typeChangeMovePowerMultiplier);
|
applyPreAttackAbAttrs(MoveTypeChangeAbAttr, source, target, this, true, null, typeChangeMovePowerMultiplier);
|
||||||
|
|
||||||
const sourceTeraType = source.getTeraType();
|
const sourceTeraType = source.getTeraType();
|
||||||
if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === this.type && power.value < 60 && this.priority <= 0 && !this.hasAttr(MultiHitAttr) && !source.scene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id)) {
|
if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === this.type && power.value < 60 && this.priority <= 0 && !this.hasAttr(MultiHitAttr) && !source.scene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id)) {
|
||||||
@ -1083,15 +1088,12 @@ export class PreMoveMessageAttr extends MoveAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StatusMoveTypeImmunityAttr extends MoveAttr {
|
/**
|
||||||
public immuneType: Type;
|
* Attribute for Status moves that take attack type effectiveness
|
||||||
|
* into consideration (i.e. {@linkcode https://bulbapedia.bulbagarden.net/wiki/Thunder_Wave_(move) | Thunder Wave})
|
||||||
constructor(immuneType: Type) {
|
* @extends MoveAttr
|
||||||
super(false);
|
*/
|
||||||
|
export class RespectAttackTypeImmunityAttr extends MoveAttr { }
|
||||||
this.immuneType = immuneType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class IgnoreOpponentStatChangesAttr extends MoveAttr {
|
export class IgnoreOpponentStatChangesAttr extends MoveAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
@ -1851,19 +1853,11 @@ export class MultiHitAttr extends MoveAttr {
|
|||||||
* @returns True
|
* @returns True
|
||||||
*/
|
*/
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
let hitTimes: integer;
|
const hitType = new Utils.NumberHolder(this.multiHitType);
|
||||||
|
applyMoveAttrs(ChangeMultiHitTypeAttr, user, target, move, hitType);
|
||||||
|
this.multiHitType = hitType.value;
|
||||||
|
|
||||||
if (target.getAttackMoveEffectiveness(user, new PokemonMove(move.id)) === 0) {
|
(args[0] as Utils.NumberHolder).value = this.getHitCount(user, target);
|
||||||
// If there is a type immunity, the attack will stop no matter what
|
|
||||||
hitTimes = 1;
|
|
||||||
} else {
|
|
||||||
const hitType = new Utils.IntegerHolder(this.multiHitType);
|
|
||||||
applyMoveAttrs(ChangeMultiHitTypeAttr, user, target, move, hitType);
|
|
||||||
this.multiHitType = hitType.value;
|
|
||||||
hitTimes = this.getHitCount(user, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
(args[0] as Utils.IntegerHolder).value = hitTimes;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3762,7 +3756,7 @@ export class VariableMoveCategoryAttr extends MoveAttr {
|
|||||||
|
|
||||||
export class PhotonGeyserCategoryAttr extends VariableMoveCategoryAttr {
|
export class PhotonGeyserCategoryAttr extends VariableMoveCategoryAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
const category = (args[0] as Utils.IntegerHolder);
|
const category = (args[0] as Utils.NumberHolder);
|
||||||
|
|
||||||
if (user.getBattleStat(Stat.ATK, target, move) > user.getBattleStat(Stat.SPATK, target, move)) {
|
if (user.getBattleStat(Stat.ATK, target, move) > user.getBattleStat(Stat.SPATK, target, move)) {
|
||||||
category.value = MoveCategory.PHYSICAL;
|
category.value = MoveCategory.PHYSICAL;
|
||||||
@ -3775,7 +3769,7 @@ export class PhotonGeyserCategoryAttr extends VariableMoveCategoryAttr {
|
|||||||
|
|
||||||
export class TeraBlastCategoryAttr extends VariableMoveCategoryAttr {
|
export class TeraBlastCategoryAttr extends VariableMoveCategoryAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
const category = (args[0] as Utils.IntegerHolder);
|
const category = (args[0] as Utils.NumberHolder);
|
||||||
|
|
||||||
if (user.isTerastallized() && user.getBattleStat(Stat.ATK, target, move) > user.getBattleStat(Stat.SPATK, target, move)) {
|
if (user.isTerastallized() && user.getBattleStat(Stat.ATK, target, move) > user.getBattleStat(Stat.SPATK, target, move)) {
|
||||||
category.value = MoveCategory.PHYSICAL;
|
category.value = MoveCategory.PHYSICAL;
|
||||||
@ -3791,18 +3785,21 @@ export class TeraBlastCategoryAttr extends VariableMoveCategoryAttr {
|
|||||||
* @extends VariablePowerAttr
|
* @extends VariablePowerAttr
|
||||||
*/
|
*/
|
||||||
export class TeraBlastPowerAttr extends VariablePowerAttr {
|
export class TeraBlastPowerAttr extends VariablePowerAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
|
||||||
/**
|
/**
|
||||||
* @param user {@linkcode Pokemon} Pokemon using the move
|
* Sets Tera Blast's power to 100 if the user is terastallized with
|
||||||
* @param target {@linkcode Pokemon} N/A
|
* the Stellar tera type.
|
||||||
* @param move {@linkcode Move} {@linkcode Move.TERA_BLAST}
|
* @param user {@linkcode Pokemon} the Pokemon using this move
|
||||||
* @param {any[]} args N/A
|
* @param target n/a
|
||||||
* @returns true or false
|
* @param move {@linkcode Move} the Move with this attribute (i.e. Tera Blast)
|
||||||
|
* @param args
|
||||||
|
* - [0] {@linkcode Utils.NumberHolder} the applied move's power, factoring in
|
||||||
|
* previously applied power modifiers.
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
const power = args[0] as Utils.NumberHolder;
|
const power = args[0] as Utils.NumberHolder;
|
||||||
if (user.isTerastallized() && move.type === Type.STELLAR) {
|
if (user.isTerastallized() && user.getTeraType() === Type.STELLAR) {
|
||||||
//200 instead of 100 to reflect lack of stellar being 2x dmg on any type
|
power.value = 100;
|
||||||
power.value = 200;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3862,10 +3859,15 @@ export class VariableMoveTypeAttr extends MoveAttr {
|
|||||||
|
|
||||||
export class FormChangeItemTypeAttr extends VariableMoveTypeAttr {
|
export class FormChangeItemTypeAttr extends VariableMoveTypeAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
const moveType = args[0];
|
||||||
|
if (!(moveType instanceof Utils.NumberHolder)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if ([user.species.speciesId, user.fusionSpecies?.speciesId].includes(Species.ARCEUS) || [user.species.speciesId, user.fusionSpecies?.speciesId].includes(Species.SILVALLY)) {
|
if ([user.species.speciesId, user.fusionSpecies?.speciesId].includes(Species.ARCEUS) || [user.species.speciesId, user.fusionSpecies?.speciesId].includes(Species.SILVALLY)) {
|
||||||
const form = user.species.speciesId === Species.ARCEUS || user.species.speciesId === Species.SILVALLY ? user.formIndex : user.fusionSpecies?.formIndex!; // TODO: is this bang correct?
|
const form = user.species.speciesId === Species.ARCEUS || user.species.speciesId === Species.SILVALLY ? user.formIndex : user.fusionSpecies?.formIndex!; // TODO: is this bang correct?
|
||||||
|
|
||||||
move.type = Type[Type[form]];
|
moveType.value = Type[Type[form]];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3875,24 +3877,29 @@ export class FormChangeItemTypeAttr extends VariableMoveTypeAttr {
|
|||||||
|
|
||||||
export class TechnoBlastTypeAttr extends VariableMoveTypeAttr {
|
export class TechnoBlastTypeAttr extends VariableMoveTypeAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
const moveType = args[0];
|
||||||
|
if (!(moveType instanceof Utils.NumberHolder)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if ([user.species.speciesId, user.fusionSpecies?.speciesId].includes(Species.GENESECT)) {
|
if ([user.species.speciesId, user.fusionSpecies?.speciesId].includes(Species.GENESECT)) {
|
||||||
const form = user.species.speciesId === Species.GENESECT ? user.formIndex : user.fusionSpecies?.formIndex;
|
const form = user.species.speciesId === Species.GENESECT ? user.formIndex : user.fusionSpecies?.formIndex;
|
||||||
|
|
||||||
switch (form) {
|
switch (form) {
|
||||||
case 1: // Shock Drive
|
case 1: // Shock Drive
|
||||||
move.type = Type.ELECTRIC;
|
moveType.value = Type.ELECTRIC;
|
||||||
break;
|
break;
|
||||||
case 2: // Burn Drive
|
case 2: // Burn Drive
|
||||||
move.type = Type.FIRE;
|
moveType.value = Type.FIRE;
|
||||||
break;
|
break;
|
||||||
case 3: // Chill Drive
|
case 3: // Chill Drive
|
||||||
move.type = Type.ICE;
|
moveType.value = Type.ICE;
|
||||||
break;
|
break;
|
||||||
case 4: // Douse Drive
|
case 4: // Douse Drive
|
||||||
move.type = Type.WATER;
|
moveType.value = Type.WATER;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
move.type = Type.NORMAL;
|
moveType.value = Type.NORMAL;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -3904,15 +3911,20 @@ export class TechnoBlastTypeAttr extends VariableMoveTypeAttr {
|
|||||||
|
|
||||||
export class AuraWheelTypeAttr extends VariableMoveTypeAttr {
|
export class AuraWheelTypeAttr extends VariableMoveTypeAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
const moveType = args[0];
|
||||||
|
if (!(moveType instanceof Utils.NumberHolder)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if ([user.species.speciesId, user.fusionSpecies?.speciesId].includes(Species.MORPEKO)) {
|
if ([user.species.speciesId, user.fusionSpecies?.speciesId].includes(Species.MORPEKO)) {
|
||||||
const form = user.species.speciesId === Species.MORPEKO ? user.formIndex : user.fusionSpecies?.formIndex;
|
const form = user.species.speciesId === Species.MORPEKO ? user.formIndex : user.fusionSpecies?.formIndex;
|
||||||
|
|
||||||
switch (form) {
|
switch (form) {
|
||||||
case 1: // Hangry Mode
|
case 1: // Hangry Mode
|
||||||
move.type = Type.DARK;
|
moveType.value = Type.DARK;
|
||||||
break;
|
break;
|
||||||
default: // Full Belly Mode
|
default: // Full Belly Mode
|
||||||
move.type = Type.ELECTRIC;
|
moveType.value = Type.ELECTRIC;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -3924,18 +3936,23 @@ export class AuraWheelTypeAttr extends VariableMoveTypeAttr {
|
|||||||
|
|
||||||
export class RagingBullTypeAttr extends VariableMoveTypeAttr {
|
export class RagingBullTypeAttr extends VariableMoveTypeAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
const moveType = args[0];
|
||||||
|
if (!(moveType instanceof Utils.NumberHolder)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if ([user.species.speciesId, user.fusionSpecies?.speciesId].includes(Species.PALDEA_TAUROS)) {
|
if ([user.species.speciesId, user.fusionSpecies?.speciesId].includes(Species.PALDEA_TAUROS)) {
|
||||||
const form = user.species.speciesId === Species.PALDEA_TAUROS ? user.formIndex : user.fusionSpecies?.formIndex;
|
const form = user.species.speciesId === Species.PALDEA_TAUROS ? user.formIndex : user.fusionSpecies?.formIndex;
|
||||||
|
|
||||||
switch (form) {
|
switch (form) {
|
||||||
case 1: // Blaze breed
|
case 1: // Blaze breed
|
||||||
move.type = Type.FIRE;
|
moveType.value = Type.FIRE;
|
||||||
break;
|
break;
|
||||||
case 2: // Aqua breed
|
case 2: // Aqua breed
|
||||||
move.type = Type.WATER;
|
moveType.value = Type.WATER;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
move.type = Type.FIGHTING;
|
moveType.value = Type.FIGHTING;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -3947,25 +3964,30 @@ export class RagingBullTypeAttr extends VariableMoveTypeAttr {
|
|||||||
|
|
||||||
export class IvyCudgelTypeAttr extends VariableMoveTypeAttr {
|
export class IvyCudgelTypeAttr extends VariableMoveTypeAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
const moveType = args[0];
|
||||||
|
if (!(moveType instanceof Utils.NumberHolder)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if ([user.species.speciesId, user.fusionSpecies?.speciesId].includes(Species.OGERPON)) {
|
if ([user.species.speciesId, user.fusionSpecies?.speciesId].includes(Species.OGERPON)) {
|
||||||
const form = user.species.speciesId === Species.OGERPON ? user.formIndex : user.fusionSpecies?.formIndex;
|
const form = user.species.speciesId === Species.OGERPON ? user.formIndex : user.fusionSpecies?.formIndex;
|
||||||
|
|
||||||
switch (form) {
|
switch (form) {
|
||||||
case 1: // Wellspring Mask
|
case 1: // Wellspring Mask
|
||||||
case 5: // Wellspring Mask Tera
|
case 5: // Wellspring Mask Tera
|
||||||
move.type = Type.WATER;
|
moveType.value = Type.WATER;
|
||||||
break;
|
break;
|
||||||
case 2: // Hearthflame Mask
|
case 2: // Hearthflame Mask
|
||||||
case 6: // Hearthflame Mask Tera
|
case 6: // Hearthflame Mask Tera
|
||||||
move.type = Type.FIRE;
|
moveType.value = Type.FIRE;
|
||||||
break;
|
break;
|
||||||
case 3: // Cornerstone Mask
|
case 3: // Cornerstone Mask
|
||||||
case 7: // Cornerstone Mask Tera
|
case 7: // Cornerstone Mask Tera
|
||||||
move.type = Type.ROCK;
|
moveType.value = Type.ROCK;
|
||||||
break;
|
break;
|
||||||
case 4: // Teal Mask Tera
|
case 4: // Teal Mask Tera
|
||||||
default:
|
default:
|
||||||
move.type = Type.GRASS;
|
moveType.value = Type.GRASS;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -3977,22 +3999,27 @@ export class IvyCudgelTypeAttr extends VariableMoveTypeAttr {
|
|||||||
|
|
||||||
export class WeatherBallTypeAttr extends VariableMoveTypeAttr {
|
export class WeatherBallTypeAttr extends VariableMoveTypeAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
const moveType = args[0];
|
||||||
|
if (!(moveType instanceof Utils.NumberHolder)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!user.scene.arena.weather?.isEffectSuppressed(user.scene)) {
|
if (!user.scene.arena.weather?.isEffectSuppressed(user.scene)) {
|
||||||
switch (user.scene.arena.weather?.weatherType) {
|
switch (user.scene.arena.weather?.weatherType) {
|
||||||
case WeatherType.SUNNY:
|
case WeatherType.SUNNY:
|
||||||
case WeatherType.HARSH_SUN:
|
case WeatherType.HARSH_SUN:
|
||||||
move.type = Type.FIRE;
|
moveType.value = Type.FIRE;
|
||||||
break;
|
break;
|
||||||
case WeatherType.RAIN:
|
case WeatherType.RAIN:
|
||||||
case WeatherType.HEAVY_RAIN:
|
case WeatherType.HEAVY_RAIN:
|
||||||
move.type = Type.WATER;
|
moveType.value = Type.WATER;
|
||||||
break;
|
break;
|
||||||
case WeatherType.SANDSTORM:
|
case WeatherType.SANDSTORM:
|
||||||
move.type = Type.ROCK;
|
moveType.value = Type.ROCK;
|
||||||
break;
|
break;
|
||||||
case WeatherType.HAIL:
|
case WeatherType.HAIL:
|
||||||
case WeatherType.SNOW:
|
case WeatherType.SNOW:
|
||||||
move.type = Type.ICE;
|
moveType.value = Type.ICE;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@ -4015,10 +4042,15 @@ export class TerrainPulseTypeAttr extends VariableMoveTypeAttr {
|
|||||||
* @param user {@linkcode Pokemon} using this move
|
* @param user {@linkcode Pokemon} using this move
|
||||||
* @param target N/A
|
* @param target N/A
|
||||||
* @param move N/A
|
* @param move N/A
|
||||||
* @param args [0] {@linkcode Utils.IntegerHolder} The move's type to be modified
|
* @param args [0] {@linkcode Utils.NumberHolder} The move's type to be modified
|
||||||
* @returns true if the function succeeds
|
* @returns true if the function succeeds
|
||||||
*/
|
*/
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
const moveType = args[0];
|
||||||
|
if (!(moveType instanceof Utils.NumberHolder)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!user.isGrounded()) {
|
if (!user.isGrounded()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -4026,16 +4058,16 @@ export class TerrainPulseTypeAttr extends VariableMoveTypeAttr {
|
|||||||
const currentTerrain = user.scene.arena.getTerrainType();
|
const currentTerrain = user.scene.arena.getTerrainType();
|
||||||
switch (currentTerrain) {
|
switch (currentTerrain) {
|
||||||
case TerrainType.MISTY:
|
case TerrainType.MISTY:
|
||||||
move.type = Type.FAIRY;
|
moveType.value = Type.FAIRY;
|
||||||
break;
|
break;
|
||||||
case TerrainType.ELECTRIC:
|
case TerrainType.ELECTRIC:
|
||||||
move.type = Type.ELECTRIC;
|
moveType.value = Type.ELECTRIC;
|
||||||
break;
|
break;
|
||||||
case TerrainType.GRASSY:
|
case TerrainType.GRASSY:
|
||||||
move.type = Type.GRASS;
|
moveType.value = Type.GRASS;
|
||||||
break;
|
break;
|
||||||
case TerrainType.PSYCHIC:
|
case TerrainType.PSYCHIC:
|
||||||
move.type = Type.PSYCHIC;
|
moveType.value = Type.PSYCHIC;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@ -4044,8 +4076,17 @@ export class TerrainPulseTypeAttr extends VariableMoveTypeAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes type based on the user's IVs
|
||||||
|
* @extends VariableMoveTypeAttr
|
||||||
|
*/
|
||||||
export class HiddenPowerTypeAttr extends VariableMoveTypeAttr {
|
export class HiddenPowerTypeAttr extends VariableMoveTypeAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
const moveType = args[0];
|
||||||
|
if (!(moveType instanceof Utils.NumberHolder)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const iv_val = Math.floor(((user.ivs[Stat.HP] & 1)
|
const iv_val = Math.floor(((user.ivs[Stat.HP] & 1)
|
||||||
+(user.ivs[Stat.ATK] & 1) * 2
|
+(user.ivs[Stat.ATK] & 1) * 2
|
||||||
+(user.ivs[Stat.DEF] & 1) * 4
|
+(user.ivs[Stat.DEF] & 1) * 4
|
||||||
@ -4053,7 +4094,7 @@ export class HiddenPowerTypeAttr extends VariableMoveTypeAttr {
|
|||||||
+(user.ivs[Stat.SPATK] & 1) * 16
|
+(user.ivs[Stat.SPATK] & 1) * 16
|
||||||
+(user.ivs[Stat.SPDEF] & 1) * 32) * 15/63);
|
+(user.ivs[Stat.SPDEF] & 1) * 32) * 15/63);
|
||||||
|
|
||||||
move.type = [
|
moveType.value = [
|
||||||
Type.FIGHTING, Type.FLYING, Type.POISON, Type.GROUND,
|
Type.FIGHTING, Type.FLYING, Type.POISON, Type.GROUND,
|
||||||
Type.ROCK, Type.BUG, Type.GHOST, Type.STEEL,
|
Type.ROCK, Type.BUG, Type.GHOST, Type.STEEL,
|
||||||
Type.FIRE, Type.WATER, Type.GRASS, Type.ELECTRIC,
|
Type.FIRE, Type.WATER, Type.GRASS, Type.ELECTRIC,
|
||||||
@ -4068,16 +4109,21 @@ export class HiddenPowerTypeAttr extends VariableMoveTypeAttr {
|
|||||||
* @extends VariableMoveTypeAttr
|
* @extends VariableMoveTypeAttr
|
||||||
*/
|
*/
|
||||||
export class TeraBlastTypeAttr extends VariableMoveTypeAttr {
|
export class TeraBlastTypeAttr extends VariableMoveTypeAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
|
||||||
/**
|
/**
|
||||||
* @param user {@linkcode Pokemon} the user's type is checked
|
* @param user {@linkcode Pokemon} the user of the move
|
||||||
* @param target {@linkcode Pokemon} N/A
|
* @param target {@linkcode Pokemon} N/A
|
||||||
* @param move {@linkcode Move} {@linkcode Move.TeraBlastTypeAttr}
|
* @param move {@linkcode Move} the move with this attribute
|
||||||
* @param {any[]} args N/A
|
* @param args `[0]` the move's type to be modified
|
||||||
* @returns true or false
|
* @returns `true` if the move's type was modified; `false` otherwise
|
||||||
*/
|
*/
|
||||||
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
const moveType = args[0];
|
||||||
|
if (!(moveType instanceof Utils.NumberHolder)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isTerastallized()) {
|
if (user.isTerastallized()) {
|
||||||
move.type = user.getTeraType(); //changes move type to tera type
|
moveType.value = user.getTeraType(); // changes move type to tera type
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4087,14 +4133,18 @@ export class TeraBlastTypeAttr extends VariableMoveTypeAttr {
|
|||||||
|
|
||||||
export class MatchUserTypeAttr extends VariableMoveTypeAttr {
|
export class MatchUserTypeAttr extends VariableMoveTypeAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
const moveType = args[0];
|
||||||
|
if (!(moveType instanceof Utils.NumberHolder)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const userTypes = user.getTypes(true);
|
const userTypes = user.getTypes(true);
|
||||||
|
|
||||||
if (userTypes.includes(Type.STELLAR)) { // will not change to stellar type
|
if (userTypes.includes(Type.STELLAR)) { // will not change to stellar type
|
||||||
const nonTeraTypes = user.getTypes();
|
const nonTeraTypes = user.getTypes();
|
||||||
move.type = nonTeraTypes[0];
|
moveType.value = nonTeraTypes[0];
|
||||||
return true;
|
return true;
|
||||||
} else if (userTypes.length > 0) {
|
} else if (userTypes.length > 0) {
|
||||||
move.type = userTypes[0];
|
moveType.value = userTypes[0];
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
@ -4113,8 +4163,8 @@ export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTy
|
|||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
if (!target.getTag(BattlerTagType.IGNORE_FLYING)) {
|
if (!target.getTag(BattlerTagType.IGNORE_FLYING)) {
|
||||||
const multiplier = args[0] as Utils.NumberHolder;
|
const multiplier = args[0] as Utils.NumberHolder;
|
||||||
//When a flying type is hit, the first hit is always 1x multiplier. Levitating pokemon are instantly affected by typing
|
//When a flying type is hit, the first hit is always 1x multiplier.
|
||||||
if (target.isOfType(Type.FLYING) || target.hasAbility(Abilities.LEVITATE)) {
|
if (target.isOfType(Type.FLYING)) {
|
||||||
multiplier.value = 1;
|
multiplier.value = 1;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -6505,7 +6555,7 @@ export function initMoves() {
|
|||||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS),
|
.attr(StatusEffectAttr, StatusEffect.PARALYSIS),
|
||||||
new StatusMove(Moves.THUNDER_WAVE, Type.ELECTRIC, 90, 20, -1, 0, 1)
|
new StatusMove(Moves.THUNDER_WAVE, Type.ELECTRIC, 90, 20, -1, 0, 1)
|
||||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
||||||
.attr(StatusMoveTypeImmunityAttr, Type.GROUND),
|
.attr(RespectAttackTypeImmunityAttr),
|
||||||
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)
|
||||||
|
@ -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, StatusMoveTypeImmunityAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, OneHitKOAccuracyAttr } from "../data/move";
|
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr } 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 } from "#app/utils";
|
import { Constructor } from "#app/utils";
|
||||||
import * as Utils from "../utils";
|
import * as Utils from "../utils";
|
||||||
@ -22,7 +22,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoo
|
|||||||
import { WeatherType } from "../data/weather";
|
import { WeatherType } from "../data/weather";
|
||||||
import { TempBattleStat } from "../data/temp-battle-stat";
|
import { TempBattleStat } from "../data/temp-battle-stat";
|
||||||
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag";
|
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag";
|
||||||
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr } from "../data/ability";
|
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr } from "../data/ability";
|
||||||
import PokemonData from "../system/pokemon-data";
|
import PokemonData from "../system/pokemon-data";
|
||||||
import { BattlerIndex } from "../battle";
|
import { BattlerIndex } from "../battle";
|
||||||
import { Mode } from "../ui/ui";
|
import { Mode } from "../ui/ui";
|
||||||
@ -1208,60 +1208,83 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the effectiveness of a move against the Pokémon.
|
* Calculates the type of a move when used by this Pokemon after
|
||||||
*
|
* type-changing move and ability attributes have applied.
|
||||||
* @param source - The Pokémon using the move.
|
* @param move {@linkcode Move} The move being used.
|
||||||
* @param move - The move being used.
|
* @param simulated If `true`, prevents showing abilities applied in this calculation.
|
||||||
* @returns The type damage multiplier or 1 if it's a status move
|
* @returns the {@linkcode Type} of the move after attributes are applied
|
||||||
*/
|
*/
|
||||||
getMoveEffectiveness(source: Pokemon, move: PokemonMove): TypeDamageMultiplier {
|
getMoveType(move: Move, simulated: boolean = true): Type {
|
||||||
if (move.getMove().category === MoveCategory.STATUS) {
|
const moveTypeHolder = new Utils.NumberHolder(move.type);
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.getAttackMoveEffectiveness(source, move, !this.battleData?.abilityRevealed);
|
applyMoveAttrs(VariableMoveTypeAttr, this, null, move, moveTypeHolder);
|
||||||
|
applyPreAttackAbAttrs(MoveTypeChangeAbAttr, this, null, move, simulated, moveTypeHolder);
|
||||||
|
|
||||||
|
return moveTypeHolder.value as Type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the effectiveness of an attack move against the Pokémon.
|
* Calculates the effectiveness of a move against the Pokémon.
|
||||||
*
|
*
|
||||||
* @param source - The attacking Pokémon.
|
* @param source {@linkcode Pokemon} The attacking Pokémon.
|
||||||
* @param pokemonMove - The move being used by the attacking Pokémon.
|
* @param move {@linkcode Move} The move being used by the attacking Pokémon.
|
||||||
* @param ignoreAbility - Whether to check for abilities that might affect type effectiveness or immunity.
|
* @param ignoreAbility Whether to ignore abilities that might affect type effectiveness or immunity (defaults to `false`).
|
||||||
|
* @param simulated Whether to apply abilities via simulated calls (defaults to `true`)
|
||||||
|
* @param cancelled {@linkcode Utils.BooleanHolder} Stores whether the move was cancelled by a non-type-based immunity.
|
||||||
|
* Currently only used by {@linkcode Pokemon.apply} to determine whether a "No effect" message should be shown.
|
||||||
* @returns The type damage multiplier, indicating the effectiveness of the move
|
* @returns The type damage multiplier, indicating the effectiveness of the move
|
||||||
*/
|
*/
|
||||||
getAttackMoveEffectiveness(source: Pokemon, pokemonMove: PokemonMove, ignoreAbility: boolean = false): TypeDamageMultiplier {
|
getMoveEffectiveness(source: Pokemon, move: Move, ignoreAbility: boolean = false, simulated: boolean = true, cancelled?: Utils.BooleanHolder): TypeDamageMultiplier {
|
||||||
const move = pokemonMove.getMove();
|
if (move.hasAttr(TypelessAttr)) {
|
||||||
const typeless = move.hasAttr(TypelessAttr);
|
return 1;
|
||||||
const typeMultiplier = new Utils.NumberHolder(this.getAttackTypeEffectiveness(move, source));
|
|
||||||
const cancelled = new Utils.BooleanHolder(false);
|
|
||||||
applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier);
|
|
||||||
if (!typeless && !ignoreAbility) {
|
|
||||||
applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelled, true, typeMultiplier);
|
|
||||||
}
|
}
|
||||||
if (!cancelled.value && !ignoreAbility) {
|
const moveType = source.getMoveType(move);
|
||||||
applyPreDefendAbAttrs(MoveImmunityAbAttr, this, source, move, cancelled, true, typeMultiplier);
|
|
||||||
|
const typeMultiplier = new Utils.NumberHolder((move.category !== MoveCategory.STATUS || move.hasAttr(RespectAttackTypeImmunityAttr))
|
||||||
|
? this.getAttackTypeEffectiveness(moveType, source, false, simulated)
|
||||||
|
: 1);
|
||||||
|
|
||||||
|
applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier);
|
||||||
|
if (this.getTypes().find(t => move.isTypeImmune(source, this, t))) {
|
||||||
|
typeMultiplier.value = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (!cancelled.value ? Number(typeMultiplier.value) : 0) as TypeDamageMultiplier;
|
const cancelledHolder = cancelled ?? new Utils.BooleanHolder(false);
|
||||||
|
if (!ignoreAbility) {
|
||||||
|
applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelledHolder, simulated, typeMultiplier);
|
||||||
|
|
||||||
|
if (!cancelledHolder.value) {
|
||||||
|
applyPreDefendAbAttrs(MoveImmunityAbAttr, this, source, move, cancelledHolder, simulated, typeMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cancelledHolder.value) {
|
||||||
|
const defendingSidePlayField = this.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField();
|
||||||
|
defendingSidePlayField.forEach((p) => applyPreDefendAbAttrs(FieldPriorityMoveImmunityAbAttr, p, source, move, cancelledHolder));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const immuneTags = this.findTags(tag => tag instanceof TypeImmuneTag && tag.immuneType === moveType);
|
||||||
|
for (const tag of immuneTags) {
|
||||||
|
if (move && !move.getAttrs(HitsTagAttr).some(attr => attr.tagType === tag.tagType)) {
|
||||||
|
typeMultiplier.value = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (!cancelledHolder.value ? typeMultiplier.value : 0) as TypeDamageMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the type effectiveness multiplier for an attack type
|
* Calculates the type effectiveness multiplier for an attack type
|
||||||
* @param moveOrType The move being used, or a type if the move is unknown
|
* @param moveType {@linkcode Type} the type of the move being used
|
||||||
* @param source the Pokemon using the move
|
* @param source {@linkcode Pokemon} the Pokemon using the move
|
||||||
* @param ignoreStrongWinds whether or not this ignores strong winds (anticipation, forewarn, stealth rocks)
|
* @param ignoreStrongWinds whether or not this ignores strong winds (anticipation, forewarn, stealth rocks)
|
||||||
* @param simulated tag to only apply the strong winds effect message when the move is used
|
* @param simulated tag to only apply the strong winds effect message when the move is used
|
||||||
* @returns a multiplier for the type effectiveness
|
* @returns a multiplier for the type effectiveness
|
||||||
*/
|
*/
|
||||||
getAttackTypeEffectiveness(moveOrType: Move | Type, source?: Pokemon, ignoreStrongWinds: boolean = false, simulated: boolean = true): TypeDamageMultiplier {
|
getAttackTypeEffectiveness(moveType: Type, source?: Pokemon, ignoreStrongWinds: boolean = false, simulated: boolean = true): TypeDamageMultiplier {
|
||||||
const move = (moveOrType instanceof Move)
|
|
||||||
? moveOrType
|
|
||||||
: undefined;
|
|
||||||
const moveType = (moveOrType instanceof Move)
|
|
||||||
? move!.type // TODO: is this bang correct?
|
|
||||||
: moveOrType;
|
|
||||||
|
|
||||||
if (moveType === Type.STELLAR) {
|
if (moveType === Type.STELLAR) {
|
||||||
return this.isTerastallized() ? 2 : 1;
|
return this.isTerastallized() ? 2 : 1;
|
||||||
}
|
}
|
||||||
@ -1281,7 +1304,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
if (source) {
|
if (source) {
|
||||||
const ignoreImmunity = new Utils.BooleanHolder(false);
|
const ignoreImmunity = new Utils.BooleanHolder(false);
|
||||||
if (source.isActive(true) && source.hasAbilityWithAttr(IgnoreTypeImmunityAbAttr)) {
|
if (source.isActive(true) && source.hasAbilityWithAttr(IgnoreTypeImmunityAbAttr)) {
|
||||||
applyAbAttrs(IgnoreTypeImmunityAbAttr, source, ignoreImmunity, false, moveType, defType);
|
applyAbAttrs(IgnoreTypeImmunityAbAttr, source, ignoreImmunity, simulated, moveType, defType);
|
||||||
}
|
}
|
||||||
if (ignoreImmunity.value) {
|
if (ignoreImmunity.value) {
|
||||||
return 1;
|
return 1;
|
||||||
@ -1303,15 +1326,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
this.scene.queueMessage(i18next.t("weather:strongWindsEffectMessage"));
|
this.scene.queueMessage(i18next.t("weather:strongWindsEffectMessage"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const immuneTags = this.findTags(tag => tag instanceof TypeImmuneTag && tag.immuneType === moveType);
|
|
||||||
for (const tag of immuneTags) {
|
|
||||||
if (move && !move.getAttrs(HitsTagAttr).some(attr => attr.tagType === tag.tagType)) {
|
|
||||||
multiplier = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return multiplier as TypeDamageMultiplier;
|
return multiplier as TypeDamageMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1959,29 +1973,23 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
let result: HitResult;
|
let result: HitResult;
|
||||||
const damage = new Utils.NumberHolder(0);
|
const damage = new Utils.NumberHolder(0);
|
||||||
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||||
const defendingSidePlayField = this.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField();
|
|
||||||
|
|
||||||
const variableCategory = new Utils.IntegerHolder(move.category);
|
const variableCategory = new Utils.NumberHolder(move.category);
|
||||||
applyMoveAttrs(VariableMoveCategoryAttr, source, this, move, variableCategory);
|
applyMoveAttrs(VariableMoveCategoryAttr, source, this, move, variableCategory);
|
||||||
const moveCategory = variableCategory.value as MoveCategory;
|
const moveCategory = variableCategory.value as MoveCategory;
|
||||||
|
|
||||||
applyMoveAttrs(VariableMoveTypeAttr, source, this, move);
|
/** The move's type after type-changing effects are applied */
|
||||||
const types = this.getTypes(true, true);
|
const moveType = source.getMoveType(move);
|
||||||
|
|
||||||
|
/** If `value` is `true`, cancels the move and suppresses "No Effect" messages */
|
||||||
const cancelled = new Utils.BooleanHolder(false);
|
const cancelled = new Utils.BooleanHolder(false);
|
||||||
const power = move.calculateBattlePower(source, this);
|
|
||||||
const typeless = move.hasAttr(TypelessAttr);
|
|
||||||
|
|
||||||
const typeMultiplier = new Utils.NumberHolder(!typeless && (moveCategory !== MoveCategory.STATUS || move.getAttrs(StatusMoveTypeImmunityAttr).find(attr => types.includes(attr.immuneType)))
|
/**
|
||||||
? this.getAttackTypeEffectiveness(move, source, false, false)
|
* The effectiveness of the move being used. Along with type matchups, this
|
||||||
: 1);
|
* accounts for changes in effectiveness from the move's attributes and the
|
||||||
applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier);
|
* abilities of both the source and this Pokemon.
|
||||||
if (typeless) {
|
*/
|
||||||
typeMultiplier.value = 1;
|
const typeMultiplier = this.getMoveEffectiveness(source, move, false, false, cancelled);
|
||||||
}
|
|
||||||
if (types.find(t => move.isTypeImmune(source, this, t))) {
|
|
||||||
typeMultiplier.value = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (moveCategory) {
|
switch (moveCategory) {
|
||||||
case MoveCategory.PHYSICAL:
|
case MoveCategory.PHYSICAL:
|
||||||
@ -1989,27 +1997,44 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
const isPhysical = moveCategory === MoveCategory.PHYSICAL;
|
const isPhysical = moveCategory === MoveCategory.PHYSICAL;
|
||||||
const sourceTeraType = source.getTeraType();
|
const sourceTeraType = source.getTeraType();
|
||||||
|
|
||||||
if (!typeless) {
|
const power = move.calculateBattlePower(source, this);
|
||||||
applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelled, false, typeMultiplier);
|
|
||||||
applyMoveAttrs(NeutralDamageAgainstFlyingTypeMultiplierAttr, source, this, move, typeMultiplier);
|
|
||||||
}
|
|
||||||
if (!cancelled.value) {
|
|
||||||
applyPreDefendAbAttrs(MoveImmunityAbAttr, this, source, move, cancelled, false, typeMultiplier);
|
|
||||||
defendingSidePlayField.forEach((p) => applyPreDefendAbAttrs(FieldPriorityMoveImmunityAbAttr, p, source, move, cancelled, false, typeMultiplier));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancelled.value) {
|
if (cancelled.value) {
|
||||||
|
// Cancelled moves fail silently
|
||||||
source.stopMultiHit(this);
|
source.stopMultiHit(this);
|
||||||
result = HitResult.NO_EFFECT;
|
return HitResult.NO_EFFECT;
|
||||||
} else {
|
} else {
|
||||||
const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === move.type) as TypeBoostTag;
|
const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === moveType) as TypeBoostTag;
|
||||||
if (typeBoost?.oneUse) {
|
if (typeBoost?.oneUse) {
|
||||||
source.removeTag(typeBoost.tagType);
|
source.removeTag(typeBoost.tagType);
|
||||||
}
|
}
|
||||||
|
|
||||||
const arenaAttackTypeMultiplier = new Utils.NumberHolder(this.scene.arena.getAttackTypeMultiplier(move.type, source.isGrounded()));
|
/** Combined damage multiplier from field effects such as weather, terrain, etc. */
|
||||||
|
const arenaAttackTypeMultiplier = new Utils.NumberHolder(this.scene.arena.getAttackTypeMultiplier(moveType, source.isGrounded()));
|
||||||
applyMoveAttrs(IgnoreWeatherTypeDebuffAttr, source, this, move, arenaAttackTypeMultiplier);
|
applyMoveAttrs(IgnoreWeatherTypeDebuffAttr, source, this, move, arenaAttackTypeMultiplier);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not this Pokemon is immune to the incoming move.
|
||||||
|
* Note that this isn't fully resolved in `getMoveEffectiveness` because
|
||||||
|
* of possible type-suppressing field effects (e.g. Desolate Land's effect on Water-type attacks).
|
||||||
|
*/
|
||||||
|
const isTypeImmune = (typeMultiplier * arenaAttackTypeMultiplier.value) === 0;
|
||||||
|
if (isTypeImmune) {
|
||||||
|
// Moves with no effect that were not cancelled queue a "no effect" message before failing
|
||||||
|
source.stopMultiHit(this);
|
||||||
|
result = (move.id === Moves.SHEER_COLD)
|
||||||
|
? HitResult.IMMUNE
|
||||||
|
: HitResult.NO_EFFECT;
|
||||||
|
|
||||||
|
if (result === HitResult.IMMUNE) {
|
||||||
|
this.scene.queueMessage(i18next.t("battle:hitResultImmune", { pokemonName: this.name }));
|
||||||
|
} else {
|
||||||
|
this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
const glaiveRushModifier = new Utils.IntegerHolder(1);
|
const glaiveRushModifier = new Utils.IntegerHolder(1);
|
||||||
if (this.getTag(BattlerTagType.RECEIVE_DOUBLE_DAMAGE)) {
|
if (this.getTag(BattlerTagType.RECEIVE_DOUBLE_DAMAGE)) {
|
||||||
glaiveRushModifier.value = 2;
|
glaiveRushModifier.value = 2;
|
||||||
@ -2059,13 +2084,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
if (!isCritical) {
|
if (!isCritical) {
|
||||||
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);
|
||||||
}
|
}
|
||||||
const isTypeImmune = (typeMultiplier.value * arenaAttackTypeMultiplier.value) === 0;
|
|
||||||
const sourceTypes = source.getTypes();
|
const sourceTypes = source.getTypes();
|
||||||
const matchesSourceType = sourceTypes[0] === move.type || (sourceTypes.length > 1 && sourceTypes[1] === move.type);
|
const matchesSourceType = sourceTypes[0] === moveType || (sourceTypes.length > 1 && sourceTypes[1] === moveType);
|
||||||
const stabMultiplier = new Utils.NumberHolder(1);
|
const stabMultiplier = new Utils.NumberHolder(1);
|
||||||
if (sourceTeraType === Type.UNKNOWN && matchesSourceType) {
|
if (sourceTeraType === Type.UNKNOWN && matchesSourceType) {
|
||||||
stabMultiplier.value += 0.5;
|
stabMultiplier.value += 0.5;
|
||||||
} else if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === move.type) {
|
} else if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === moveType) {
|
||||||
stabMultiplier.value += 0.5;
|
stabMultiplier.value += 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2095,7 +2119,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
const randomMultiplier = ((this.scene.randBattleSeedInt(16) + 85) / 100);
|
const randomMultiplier = ((this.scene.randBattleSeedInt(16) + 85) / 100);
|
||||||
damage.value = Utils.toDmgValue((((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2)
|
damage.value = Utils.toDmgValue((((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2)
|
||||||
* stabMultiplier.value
|
* stabMultiplier.value
|
||||||
* typeMultiplier.value
|
* typeMultiplier
|
||||||
* arenaAttackTypeMultiplier.value
|
* arenaAttackTypeMultiplier.value
|
||||||
* screenMultiplier.value
|
* screenMultiplier.value
|
||||||
* twoStrikeMultiplier.value
|
* twoStrikeMultiplier.value
|
||||||
@ -2129,7 +2153,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.scene.arena.terrain?.terrainType === TerrainType.MISTY && this.isGrounded() && move.type === Type.DRAGON) {
|
if (this.scene.arena.terrain?.terrainType === TerrainType.MISTY && this.isGrounded() && moveType === Type.DRAGON) {
|
||||||
damage.value = Utils.toDmgValue(damage.value / 2);
|
damage.value = Utils.toDmgValue(damage.value / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2143,22 +2167,18 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
result = result!; // telling TS compiler that result is defined!
|
result = result!; // telling TS compiler that result is defined!
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
if (!typeMultiplier.value) {
|
const isOneHitKo = new Utils.BooleanHolder(false);
|
||||||
result = move.id === Moves.SHEER_COLD ? HitResult.IMMUNE : HitResult.NO_EFFECT;
|
applyMoveAttrs(OneHitKOAttr, source, this, move, isOneHitKo);
|
||||||
|
if (isOneHitKo.value) {
|
||||||
|
result = HitResult.ONE_HIT_KO;
|
||||||
|
isCritical = false;
|
||||||
|
damage.value = this.hp;
|
||||||
|
} else if (typeMultiplier >= 2) {
|
||||||
|
result = HitResult.SUPER_EFFECTIVE;
|
||||||
|
} else if (typeMultiplier >= 1) {
|
||||||
|
result = HitResult.EFFECTIVE;
|
||||||
} else {
|
} else {
|
||||||
const isOneHitKo = new Utils.BooleanHolder(false);
|
result = HitResult.NOT_VERY_EFFECTIVE;
|
||||||
applyMoveAttrs(OneHitKOAttr, source, this, move, isOneHitKo);
|
|
||||||
if (isOneHitKo.value) {
|
|
||||||
result = HitResult.ONE_HIT_KO;
|
|
||||||
isCritical = false;
|
|
||||||
damage.value = this.hp;
|
|
||||||
} else if (typeMultiplier.value >= 2) {
|
|
||||||
result = HitResult.SUPER_EFFECTIVE;
|
|
||||||
} else if (typeMultiplier.value >= 1) {
|
|
||||||
result = HitResult.EFFECTIVE;
|
|
||||||
} else {
|
|
||||||
result = HitResult.NOT_VERY_EFFECTIVE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2225,15 +2245,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
case HitResult.NOT_VERY_EFFECTIVE:
|
case HitResult.NOT_VERY_EFFECTIVE:
|
||||||
this.scene.queueMessage(i18next.t("battle:hitResultNotVeryEffective"));
|
this.scene.queueMessage(i18next.t("battle:hitResultNotVeryEffective"));
|
||||||
break;
|
break;
|
||||||
case HitResult.NO_EFFECT:
|
|
||||||
this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) }));
|
|
||||||
break;
|
|
||||||
case HitResult.IMMUNE:
|
|
||||||
this.scene.queueMessage(i18next.t("battle:hitResultImmune", { pokemonName: this.name }));
|
|
||||||
break;
|
|
||||||
case HitResult.ONE_HIT_KO:
|
case HitResult.ONE_HIT_KO:
|
||||||
this.scene.queueMessage(i18next.t("battle:hitResultOneHitKO"));
|
this.scene.queueMessage(i18next.t("battle:hitResultOneHitKO"));
|
||||||
break;
|
break;
|
||||||
|
case HitResult.IMMUNE:
|
||||||
|
case HitResult.NO_EFFECT:
|
||||||
|
console.error("Unhandled move immunity!");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2245,23 +2263,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (damage) {
|
if (damage) {
|
||||||
const attacker = this.scene.getPokemonById(source.id)!; // TODO: is this bang correct?
|
destinyTag?.lapse(source, BattlerTagLapseType.CUSTOM);
|
||||||
destinyTag?.lapse(attacker, BattlerTagLapseType.CUSTOM);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case MoveCategory.STATUS:
|
case MoveCategory.STATUS:
|
||||||
if (!typeless) {
|
if (!cancelled.value && typeMultiplier === 0) {
|
||||||
applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelled, false, typeMultiplier);
|
|
||||||
}
|
|
||||||
if (!cancelled.value) {
|
|
||||||
applyPreDefendAbAttrs(MoveImmunityAbAttr, this, source, move, cancelled, false, typeMultiplier);
|
|
||||||
defendingSidePlayField.forEach((p) => applyPreDefendAbAttrs(FieldPriorityMoveImmunityAbAttr, p, source, move, cancelled, false, typeMultiplier));
|
|
||||||
}
|
|
||||||
if (!typeMultiplier.value) {
|
|
||||||
this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) }));
|
this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) }));
|
||||||
}
|
}
|
||||||
result = cancelled.value || !typeMultiplier.value ? HitResult.NO_EFFECT : HitResult.STATUS;
|
result = (typeMultiplier === 0) ? HitResult.NO_EFFECT : HitResult.STATUS;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3918,7 +3928,7 @@ export class EnemyPokemon extends Pokemon {
|
|||||||
* Attack moves are given extra multipliers to their base benefit score based on
|
* Attack moves are given extra multipliers to their base benefit score based on
|
||||||
* the move's type effectiveness against the target and whether the move is a STAB move.
|
* the move's type effectiveness against the target and whether the move is a STAB move.
|
||||||
*/
|
*/
|
||||||
const effectiveness = target.getAttackMoveEffectiveness(this, pokemonMove);
|
const effectiveness = target.getMoveEffectiveness(this, move, !target.battleData?.abilityRevealed);
|
||||||
if (target.isPlayer() !== this.isPlayer()) {
|
if (target.isPlayer() !== this.isPlayer()) {
|
||||||
targetScore *= effectiveness;
|
targetScore *= effectiveness;
|
||||||
if (this.isOfType(move.type)) {
|
if (this.isOfType(move.type)) {
|
||||||
|
@ -311,8 +311,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
end() {
|
end() {
|
||||||
const move = this.move.getMove();
|
|
||||||
move.type = move.defaultType;
|
|
||||||
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,
|
||||||
|
133
src/test/abilities/galvanize.test.ts
Normal file
133
src/test/abilities/galvanize.test.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { BattlerIndex } from "#app/battle";
|
||||||
|
import { allMoves } from "#app/data/move";
|
||||||
|
import { Type } from "#app/data/type";
|
||||||
|
import { Abilities } from "#app/enums/abilities";
|
||||||
|
import { Moves } from "#app/enums/moves";
|
||||||
|
import { Species } from "#app/enums/species";
|
||||||
|
import { HitResult } from "#app/field/pokemon";
|
||||||
|
import GameManager from "#test/utils/gameManager";
|
||||||
|
import { SPLASH_ONLY } from "#test/utils/testUtils";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const TIMEOUT = 20 * 1000;
|
||||||
|
|
||||||
|
describe("Abilities - Galvanize", () => {
|
||||||
|
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")
|
||||||
|
.startingLevel(100)
|
||||||
|
.ability(Abilities.GALVANIZE)
|
||||||
|
.moveset([Moves.TACKLE, Moves.REVELATION_DANCE, Moves.FURY_SWIPES])
|
||||||
|
.enemySpecies(Species.DUSCLOPS)
|
||||||
|
.enemyAbility(Abilities.BALL_FETCH)
|
||||||
|
.enemyMoveset(SPLASH_ONLY)
|
||||||
|
.enemyLevel(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should change Normal-type attacks to Electric type and boost their power", async () => {
|
||||||
|
await game.startBattle();
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||||
|
vi.spyOn(playerPokemon, "getMoveType");
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
vi.spyOn(enemyPokemon, "apply");
|
||||||
|
|
||||||
|
const move = allMoves[Moves.TACKLE];
|
||||||
|
vi.spyOn(move, "calculateBattlePower");
|
||||||
|
|
||||||
|
game.move.select(Moves.TACKLE);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to("BerryPhase", false);
|
||||||
|
|
||||||
|
expect(playerPokemon.getMoveType).toHaveLastReturnedWith(Type.ELECTRIC);
|
||||||
|
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.EFFECTIVE);
|
||||||
|
expect(move.calculateBattlePower).toHaveReturnedWith(48);
|
||||||
|
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||||
|
}, TIMEOUT);
|
||||||
|
|
||||||
|
it("should cause Normal-type attacks to activate Volt Absorb", async () => {
|
||||||
|
game.override.enemyAbility(Abilities.VOLT_ABSORB);
|
||||||
|
|
||||||
|
await game.startBattle();
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||||
|
vi.spyOn(playerPokemon, "getMoveType");
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
vi.spyOn(enemyPokemon, "apply");
|
||||||
|
|
||||||
|
enemyPokemon.hp = Math.floor(enemyPokemon.getMaxHp() * 0.8);
|
||||||
|
|
||||||
|
game.move.select(Moves.TACKLE);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to("BerryPhase", false);
|
||||||
|
|
||||||
|
expect(playerPokemon.getMoveType).toHaveLastReturnedWith(Type.ELECTRIC);
|
||||||
|
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.NO_EFFECT);
|
||||||
|
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||||
|
}, TIMEOUT);
|
||||||
|
|
||||||
|
it("should not change the type of variable-type moves", async () => {
|
||||||
|
game.override.enemySpecies(Species.MIGHTYENA);
|
||||||
|
|
||||||
|
await game.startBattle([Species.ESPEON]);
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||||
|
vi.spyOn(playerPokemon, "getMoveType");
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
vi.spyOn(enemyPokemon, "apply");
|
||||||
|
|
||||||
|
game.move.select(Moves.REVELATION_DANCE);
|
||||||
|
await game.phaseInterceptor.to("BerryPhase", false);
|
||||||
|
|
||||||
|
expect(playerPokemon.getMoveType).not.toHaveLastReturnedWith(Type.ELECTRIC);
|
||||||
|
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.NO_EFFECT);
|
||||||
|
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||||
|
}, TIMEOUT);
|
||||||
|
|
||||||
|
it("should affect all hits of a Normal-type multi-hit move", async () => {
|
||||||
|
await game.startBattle();
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||||
|
vi.spyOn(playerPokemon, "getMoveType");
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
vi.spyOn(enemyPokemon, "apply");
|
||||||
|
|
||||||
|
game.move.select(Moves.FURY_SWIPES);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.move.forceHit();
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||||
|
expect(playerPokemon.turnData.hitCount).toBeGreaterThan(1);
|
||||||
|
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||||
|
|
||||||
|
while (playerPokemon.turnData.hitsLeft > 0) {
|
||||||
|
const enemyStartingHp = enemyPokemon.hp;
|
||||||
|
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||||
|
|
||||||
|
expect(playerPokemon.getMoveType).toHaveLastReturnedWith(Type.ELECTRIC);
|
||||||
|
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(enemyPokemon.apply).not.toHaveReturnedWith(HitResult.NO_EFFECT);
|
||||||
|
}, TIMEOUT);
|
||||||
|
});
|
@ -76,7 +76,7 @@ describe("Abilities - Libero", () => {
|
|||||||
|
|
||||||
expect(leadPokemon.summonData.abilitiesApplied.filter((a) => a === Abilities.LIBERO)).toHaveLength(1);
|
expect(leadPokemon.summonData.abilitiesApplied.filter((a) => a === Abilities.LIBERO)).toHaveLength(1);
|
||||||
const leadPokemonType = Type[leadPokemon.getTypes()[0]];
|
const leadPokemonType = Type[leadPokemon.getTypes()[0]];
|
||||||
const moveType = Type[allMoves[Moves.AGILITY].defaultType];
|
const moveType = Type[allMoves[Moves.AGILITY].type];
|
||||||
expect(leadPokemonType).not.toBe(moveType);
|
expect(leadPokemonType).not.toBe(moveType);
|
||||||
|
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
@ -249,7 +249,7 @@ describe("Abilities - Libero", () => {
|
|||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
leadPokemon.summonData.types = [allMoves[Moves.SPLASH].defaultType];
|
leadPokemon.summonData.types = [allMoves[Moves.SPLASH].type];
|
||||||
game.move.select(Moves.SPLASH);
|
game.move.select(Moves.SPLASH);
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
@ -357,6 +357,6 @@ function testPokemonTypeMatchesDefaultMoveType(pokemon: PlayerPokemon, move: Mov
|
|||||||
expect(pokemon.summonData.abilitiesApplied).toContain(Abilities.LIBERO);
|
expect(pokemon.summonData.abilitiesApplied).toContain(Abilities.LIBERO);
|
||||||
expect(pokemon.getTypes()).toHaveLength(1);
|
expect(pokemon.getTypes()).toHaveLength(1);
|
||||||
const pokemonType = Type[pokemon.getTypes()[0]],
|
const pokemonType = Type[pokemon.getTypes()[0]],
|
||||||
moveType = Type[allMoves[move].defaultType];
|
moveType = Type[allMoves[move].type];
|
||||||
expect(pokemonType).toBe(moveType);
|
expect(pokemonType).toBe(moveType);
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ describe("Abilities - Protean", () => {
|
|||||||
|
|
||||||
expect(leadPokemon.summonData.abilitiesApplied.filter((a) => a === Abilities.PROTEAN)).toHaveLength(1);
|
expect(leadPokemon.summonData.abilitiesApplied.filter((a) => a === Abilities.PROTEAN)).toHaveLength(1);
|
||||||
const leadPokemonType = Type[leadPokemon.getTypes()[0]];
|
const leadPokemonType = Type[leadPokemon.getTypes()[0]];
|
||||||
const moveType = Type[allMoves[Moves.AGILITY].defaultType];
|
const moveType = Type[allMoves[Moves.AGILITY].type];
|
||||||
expect(leadPokemonType).not.toBe(moveType);
|
expect(leadPokemonType).not.toBe(moveType);
|
||||||
|
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
@ -249,7 +249,7 @@ describe("Abilities - Protean", () => {
|
|||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
leadPokemon.summonData.types = [allMoves[Moves.SPLASH].defaultType];
|
leadPokemon.summonData.types = [allMoves[Moves.SPLASH].type];
|
||||||
game.move.select(Moves.SPLASH);
|
game.move.select(Moves.SPLASH);
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
@ -357,6 +357,6 @@ function testPokemonTypeMatchesDefaultMoveType(pokemon: PlayerPokemon, move: Mov
|
|||||||
expect(pokemon.summonData.abilitiesApplied).toContain(Abilities.PROTEAN);
|
expect(pokemon.summonData.abilitiesApplied).toContain(Abilities.PROTEAN);
|
||||||
expect(pokemon.getTypes()).toHaveLength(1);
|
expect(pokemon.getTypes()).toHaveLength(1);
|
||||||
const pokemonType = Type[pokemon.getTypes()[0]],
|
const pokemonType = Type[pokemon.getTypes()[0]],
|
||||||
moveType = Type[allMoves[move].defaultType];
|
moveType = Type[allMoves[move].type];
|
||||||
expect(pokemonType).toBe(moveType);
|
expect(pokemonType).toBe(moveType);
|
||||||
}
|
}
|
||||||
|
70
src/test/moves/effectiveness.test.ts
Normal file
70
src/test/moves/effectiveness.test.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { allMoves } from "#app/data/move";
|
||||||
|
import { getPokemonSpecies } from "#app/data/pokemon-species";
|
||||||
|
import { TrainerSlot } from "#app/data/trainer-config";
|
||||||
|
import { Abilities } from "#app/enums/abilities";
|
||||||
|
import { Moves } from "#app/enums/moves";
|
||||||
|
import { Species } from "#app/enums/species";
|
||||||
|
import * as Messages from "#app/messages";
|
||||||
|
import GameManager from "#test/utils/gameManager";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
function testMoveEffectiveness(game: GameManager, move: Moves, targetSpecies: Species,
|
||||||
|
expected: number, targetAbility: Abilities = Abilities.BALL_FETCH): void {
|
||||||
|
// Suppress getPokemonNameWithAffix because it calls on a null battle spec
|
||||||
|
vi.spyOn(Messages, "getPokemonNameWithAffix").mockReturnValue("");
|
||||||
|
game.override.enemyAbility(targetAbility);
|
||||||
|
const user = game.scene.addPlayerPokemon(getPokemonSpecies(Species.SNORLAX), 5);
|
||||||
|
const target = game.scene.addEnemyPokemon(getPokemonSpecies(targetSpecies), 5, TrainerSlot.NONE);
|
||||||
|
|
||||||
|
expect(target.getMoveEffectiveness(user, allMoves[move])).toBe(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Moves - Type Effectiveness", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
game.override.ability(Abilities.BALL_FETCH);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Normal-type attacks are neutrally effective against Normal-type Pokemon",
|
||||||
|
() => testMoveEffectiveness(game, Moves.TACKLE, Species.SNORLAX, 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
it("Normal-type attacks are not very effective against Steel-type Pokemon",
|
||||||
|
() => testMoveEffectiveness(game, Moves.TACKLE, Species.REGISTEEL, 0.5)
|
||||||
|
);
|
||||||
|
|
||||||
|
it("Normal-type attacks are doubly resisted by Steel/Rock-type Pokemon",
|
||||||
|
() => testMoveEffectiveness(game, Moves.TACKLE, Species.AGGRON, 0.25)
|
||||||
|
);
|
||||||
|
|
||||||
|
it("Normal-type attacks have no effect on Ghost-type Pokemon",
|
||||||
|
() => testMoveEffectiveness(game, Moves.TACKLE, Species.DUSCLOPS, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
it("Normal-type status moves are not affected by type matchups",
|
||||||
|
() => testMoveEffectiveness(game, Moves.GROWL, Species.DUSCLOPS, 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
it("Electric-type attacks are super-effective against Water-type Pokemon",
|
||||||
|
() => testMoveEffectiveness(game, Moves.THUNDERBOLT, Species.BLASTOISE, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
it("Electric-type attacks are doubly super-effective against Water/Flying-type Pokemon",
|
||||||
|
() => testMoveEffectiveness(game, Moves.THUNDERBOLT, Species.GYARADOS, 4)
|
||||||
|
);
|
||||||
|
|
||||||
|
it("Electric-type attacks are negated by Volt Absorb",
|
||||||
|
() => testMoveEffectiveness(game, Moves.THUNDERBOLT, Species.GYARADOS, 0, Abilities.VOLT_ABSORB)
|
||||||
|
);
|
||||||
|
});
|
@ -62,9 +62,6 @@ describe("Moves - Tera Blast", () => {
|
|||||||
|
|
||||||
it("increases power if user is Stellar tera type", async () => {
|
it("increases power if user is Stellar tera type", async () => {
|
||||||
game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]);
|
game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]);
|
||||||
const stellarTypeMultiplier = 2;
|
|
||||||
const stellarTypeDmgBonus = 20;
|
|
||||||
const basePower = moveToCheck.power;
|
|
||||||
|
|
||||||
await game.startBattle();
|
await game.startBattle();
|
||||||
|
|
||||||
@ -72,9 +69,25 @@ describe("Moves - Tera Blast", () => {
|
|||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||||
|
|
||||||
expect(moveToCheck.calculateBattlePower).toHaveReturnedWith((basePower + stellarTypeDmgBonus) * stellarTypeMultiplier);
|
expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(100);
|
||||||
}, 20000);
|
}, 20000);
|
||||||
|
|
||||||
|
it("is super effective against terastallized targets if user is Stellar tera type", async () => {
|
||||||
|
game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]);
|
||||||
|
|
||||||
|
await game.startBattle();
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
vi.spyOn(enemyPokemon, "apply");
|
||||||
|
vi.spyOn(enemyPokemon, "isTerastallized").mockReturnValue(true);
|
||||||
|
|
||||||
|
game.move.select(Moves.TERA_BLAST);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||||
|
|
||||||
|
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.SUPER_EFFECTIVE);
|
||||||
|
});
|
||||||
|
|
||||||
// Currently abilities are bugged and can't see when a move's category is changed
|
// Currently abilities are bugged and can't see when a move's category is changed
|
||||||
it.skip("uses the higher stat of the user's Atk and SpAtk for damage calculation", async () => {
|
it.skip("uses the higher stat of the user's Atk and SpAtk for damage calculation", async () => {
|
||||||
game.override.enemyAbility(Abilities.TOXIC_DEBRIS);
|
game.override.enemyAbility(Abilities.TOXIC_DEBRIS);
|
||||||
|
102
src/test/moves/thunder_wave.test.ts
Normal file
102
src/test/moves/thunder_wave.test.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { StatusEffect } from "#app/data/status-effect";
|
||||||
|
import { Abilities } from "#app/enums/abilities";
|
||||||
|
import { EnemyPokemon } from "#app/field/pokemon";
|
||||||
|
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 } from "vitest";
|
||||||
|
import { SPLASH_ONLY } from "../utils/testUtils";
|
||||||
|
|
||||||
|
const TIMEOUT = 20 * 1000;
|
||||||
|
|
||||||
|
describe("Moves - Thunder Wave", () => {
|
||||||
|
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")
|
||||||
|
.starterSpecies(Species.PIKACHU)
|
||||||
|
.moveset([Moves.THUNDER_WAVE])
|
||||||
|
.enemyMoveset(SPLASH_ONLY);
|
||||||
|
});
|
||||||
|
|
||||||
|
// References: https://bulbapedia.bulbagarden.net/wiki/Thunder_Wave_(move)
|
||||||
|
|
||||||
|
it("paralyzes non-statused Pokemon that are not Ground types", async () => {
|
||||||
|
game.override.enemySpecies(Species.MAGIKARP);
|
||||||
|
await game.startBattle();
|
||||||
|
|
||||||
|
const enemyPokemon: EnemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
|
||||||
|
game.move.select(Moves.THUNDER_WAVE);
|
||||||
|
await game.move.forceHit();
|
||||||
|
await game.phaseInterceptor.to("BerryPhase", false);
|
||||||
|
|
||||||
|
expect(enemyPokemon.status?.effect).toBe(StatusEffect.PARALYSIS);
|
||||||
|
}, TIMEOUT);
|
||||||
|
|
||||||
|
it("does not paralyze if the Pokemon is a Ground-type", async () => {
|
||||||
|
game.override.enemySpecies(Species.DIGLETT);
|
||||||
|
await game.startBattle();
|
||||||
|
|
||||||
|
const enemyPokemon: EnemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
|
||||||
|
game.move.select(Moves.THUNDER_WAVE);
|
||||||
|
await game.move.forceHit();
|
||||||
|
await game.phaseInterceptor.to("BerryPhase", false);
|
||||||
|
|
||||||
|
expect(enemyPokemon.status).toBeUndefined();
|
||||||
|
}, TIMEOUT);
|
||||||
|
|
||||||
|
it("does not paralyze if the Pokemon already has a status effect", async () => {
|
||||||
|
game.override.enemySpecies(Species.MAGIKARP).enemyStatusEffect(StatusEffect.BURN);
|
||||||
|
await game.startBattle();
|
||||||
|
|
||||||
|
const enemyPokemon: EnemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
|
||||||
|
game.move.select(Moves.THUNDER_WAVE);
|
||||||
|
await game.move.forceHit();
|
||||||
|
await game.phaseInterceptor.to("BerryPhase", false);
|
||||||
|
|
||||||
|
expect(enemyPokemon.status?.effect).not.toBe(StatusEffect.PARALYSIS);
|
||||||
|
}, TIMEOUT);
|
||||||
|
|
||||||
|
it("affects Ground types if the user has Normalize", async () => {
|
||||||
|
game.override.ability(Abilities.NORMALIZE).enemySpecies(Species.DIGLETT);
|
||||||
|
await game.startBattle();
|
||||||
|
|
||||||
|
const enemyPokemon: EnemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
|
||||||
|
game.move.select(Moves.THUNDER_WAVE);
|
||||||
|
await game.move.forceHit();
|
||||||
|
await game.phaseInterceptor.to("BerryPhase", false);
|
||||||
|
|
||||||
|
expect(enemyPokemon.status?.effect).toBe(StatusEffect.PARALYSIS);
|
||||||
|
}, TIMEOUT);
|
||||||
|
|
||||||
|
it("does not affect Ghost types if the user has Normalize", async () => {
|
||||||
|
game.override.ability(Abilities.NORMALIZE).enemySpecies(Species.HAUNTER);
|
||||||
|
await game.startBattle();
|
||||||
|
|
||||||
|
const enemyPokemon: EnemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
|
||||||
|
game.move.select(Moves.THUNDER_WAVE);
|
||||||
|
await game.move.forceHit();
|
||||||
|
await game.phaseInterceptor.to("BerryPhase", false);
|
||||||
|
|
||||||
|
expect(enemyPokemon.status).toBeUndefined();
|
||||||
|
}, TIMEOUT);
|
||||||
|
});
|
@ -143,7 +143,7 @@ export default class GameChallengesUiHandler extends UiHandler {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.monoTypeValue = this.scene.add.sprite(8, 98, `types${Utils.verifyLang(i18next.resolvedLanguage) ? `_${i18next.resolvedLanguage}` : ""}`);
|
this.monoTypeValue = this.scene.add.sprite(8, 98, Utils.getLocalizedSpriteKey("types"));
|
||||||
this.monoTypeValue.setName("challenge-value-monotype-sprite");
|
this.monoTypeValue.setName("challenge-value-monotype-sprite");
|
||||||
this.monoTypeValue.setScale(0.86);
|
this.monoTypeValue.setScale(0.86);
|
||||||
this.monoTypeValue.setVisible(false);
|
this.monoTypeValue.setVisible(false);
|
||||||
|
@ -44,7 +44,7 @@ export default class FightUiHandler extends UiHandler {
|
|||||||
this.moveInfoContainer.setName("move-info");
|
this.moveInfoContainer.setName("move-info");
|
||||||
ui.add(this.moveInfoContainer);
|
ui.add(this.moveInfoContainer);
|
||||||
|
|
||||||
this.typeIcon = this.scene.add.sprite(this.scene.scaledCanvas.width - 57, -36, `types${Utils.verifyLang(i18next.resolvedLanguage) ? `_${i18next.resolvedLanguage}` : ""}`, "unknown");
|
this.typeIcon = this.scene.add.sprite(this.scene.scaledCanvas.width - 57, -36, Utils.getLocalizedSpriteKey("types"), "unknown");
|
||||||
this.typeIcon.setVisible(false);
|
this.typeIcon.setVisible(false);
|
||||||
this.moveInfoContainer.add(this.typeIcon);
|
this.moveInfoContainer.add(this.typeIcon);
|
||||||
|
|
||||||
@ -179,15 +179,20 @@ export default class FightUiHandler extends UiHandler {
|
|||||||
|
|
||||||
if (hasMove) {
|
if (hasMove) {
|
||||||
const pokemonMove = moveset[cursor]!; // TODO: is the bang correct?
|
const pokemonMove = moveset[cursor]!; // TODO: is the bang correct?
|
||||||
this.typeIcon.setTexture(`types${Utils.verifyLang(i18next.resolvedLanguage) ? `_${i18next.resolvedLanguage}` : ""}`, Type[pokemonMove.getMove().type].toLowerCase()).setScale(0.8);
|
const moveType = pokemon.getMoveType(pokemonMove.getMove());
|
||||||
this.moveCategoryIcon.setTexture("categories", MoveCategory[pokemonMove.getMove().category].toLowerCase()).setScale(1.0);
|
const textureKey = Utils.getLocalizedSpriteKey("types");
|
||||||
|
this.typeIcon.setTexture(textureKey, Type[moveType].toLowerCase()).setScale(0.8);
|
||||||
|
|
||||||
|
const moveCategory = pokemonMove.getMove().category;
|
||||||
|
this.moveCategoryIcon.setTexture("categories", MoveCategory[moveCategory].toLowerCase()).setScale(1.0);
|
||||||
const power = pokemonMove.getMove().power;
|
const power = pokemonMove.getMove().power;
|
||||||
const accuracy = pokemonMove.getMove().accuracy;
|
const accuracy = pokemonMove.getMove().accuracy;
|
||||||
const maxPP = pokemonMove.getMovePp();
|
const maxPP = pokemonMove.getMovePp();
|
||||||
const pp = maxPP - pokemonMove.ppUsed;
|
const pp = maxPP - pokemonMove.ppUsed;
|
||||||
|
|
||||||
this.ppText.setText(`${Utils.padInt(pp, 2, " ")}/${Utils.padInt(maxPP, 2, " ")}`);
|
const ppLeftStr = Utils.padInt(pp, 2, " ");
|
||||||
|
const ppMaxStr = Utils.padInt(maxPP, 2, " ");
|
||||||
|
this.ppText.setText(`${ppLeftStr}/${ppMaxStr}`);
|
||||||
this.powerText.setText(`${power >= 0 ? power : "---"}`);
|
this.powerText.setText(`${power >= 0 ? power : "---"}`);
|
||||||
this.accuracyText.setText(`${accuracy >= 0 ? accuracy : "---"}`);
|
this.accuracyText.setText(`${accuracy >= 0 ? accuracy : "---"}`);
|
||||||
|
|
||||||
@ -231,7 +236,7 @@ export default class FightUiHandler extends UiHandler {
|
|||||||
* Returns undefined if it's a status move
|
* Returns undefined if it's a status move
|
||||||
*/
|
*/
|
||||||
private getEffectivenessText(pokemon: Pokemon, opponent: Pokemon, pokemonMove: PokemonMove): string | undefined {
|
private getEffectivenessText(pokemon: Pokemon, opponent: Pokemon, pokemonMove: PokemonMove): string | undefined {
|
||||||
const effectiveness = opponent.getMoveEffectiveness(pokemon, pokemonMove);
|
const effectiveness = opponent.getMoveEffectiveness(pokemon, pokemonMove.getMove(), !opponent.battleData?.abilityRevealed);
|
||||||
if (effectiveness === undefined) {
|
if (effectiveness === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -274,7 +279,7 @@ export default class FightUiHandler extends UiHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const moveColors = opponents
|
const moveColors = opponents
|
||||||
.map((opponent) => opponent.getMoveEffectiveness(pokemon, pokemonMove))
|
.map((opponent) => opponent.getMoveEffectiveness(pokemon, pokemonMove.getMove(), !opponent.battleData.abilityRevealed))
|
||||||
.sort((a, b) => b - a)
|
.sort((a, b) => b - a)
|
||||||
.map((effectiveness) => getTypeDamageMultiplierColor(effectiveness ?? 0, "offense"));
|
.map((effectiveness) => getTypeDamageMultiplierColor(effectiveness ?? 0, "offense"));
|
||||||
|
|
||||||
|
@ -410,7 +410,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
if (index === 0 || index === 19) {
|
if (index === 0 || index === 19) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const typeSprite = this.scene.add.sprite(0, 0, `types${Utils.verifyLang(i18next.resolvedLanguage) ? `_${i18next.resolvedLanguage}` : ""}`);
|
const typeSprite = this.scene.add.sprite(0, 0, Utils.getLocalizedSpriteKey("types"));
|
||||||
typeSprite.setScale(0.5);
|
typeSprite.setScale(0.5);
|
||||||
typeSprite.setFrame(type.toLowerCase());
|
typeSprite.setFrame(type.toLowerCase());
|
||||||
typeOptions.push(new DropDownOption(this.scene, index, new DropDownLabel("", typeSprite)));
|
typeOptions.push(new DropDownOption(this.scene, index, new DropDownLabel("", typeSprite)));
|
||||||
@ -668,12 +668,12 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
this.pokemonSprite.setPipeline(this.scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], ignoreTimeTint: true });
|
this.pokemonSprite.setPipeline(this.scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], ignoreTimeTint: true });
|
||||||
this.starterSelectContainer.add(this.pokemonSprite);
|
this.starterSelectContainer.add(this.pokemonSprite);
|
||||||
|
|
||||||
this.type1Icon = this.scene.add.sprite(8, 98, `types${Utils.verifyLang(i18next.resolvedLanguage) ? `_${i18next.resolvedLanguage}` : ""}`);
|
this.type1Icon = this.scene.add.sprite(8, 98, Utils.getLocalizedSpriteKey("types"));
|
||||||
this.type1Icon.setScale(0.5);
|
this.type1Icon.setScale(0.5);
|
||||||
this.type1Icon.setOrigin(0, 0);
|
this.type1Icon.setOrigin(0, 0);
|
||||||
this.starterSelectContainer.add(this.type1Icon);
|
this.starterSelectContainer.add(this.type1Icon);
|
||||||
|
|
||||||
this.type2Icon = this.scene.add.sprite(26, 98, `types${Utils.verifyLang(i18next.resolvedLanguage) ? `_${i18next.resolvedLanguage}` : ""}`);
|
this.type2Icon = this.scene.add.sprite(26, 98, Utils.getLocalizedSpriteKey("types"));
|
||||||
this.type2Icon.setScale(0.5);
|
this.type2Icon.setScale(0.5);
|
||||||
this.type2Icon.setOrigin(0, 0);
|
this.type2Icon.setOrigin(0, 0);
|
||||||
this.starterSelectContainer.add(this.type2Icon);
|
this.starterSelectContainer.add(this.type2Icon);
|
||||||
|
@ -716,7 +716,8 @@ export default class SummaryUiHandler extends UiHandler {
|
|||||||
const getTypeIcon = (index: integer, type: Type, tera: boolean = false) => {
|
const getTypeIcon = (index: integer, type: Type, tera: boolean = false) => {
|
||||||
const xCoord = typeLabel.width * typeLabel.scale + 9 + 34 * index;
|
const xCoord = typeLabel.width * typeLabel.scale + 9 + 34 * index;
|
||||||
const typeIcon = !tera
|
const typeIcon = !tera
|
||||||
? this.scene.add.sprite(xCoord, 42, `types${Utils.verifyLang(i18next.resolvedLanguage) ? `_${i18next.resolvedLanguage}` : ""}`, Type[type].toLowerCase()) : this.scene.add.sprite(xCoord, 42, "type_tera");
|
? this.scene.add.sprite(xCoord, 42, Utils.getLocalizedSpriteKey("types"), Type[type].toLowerCase())
|
||||||
|
: this.scene.add.sprite(xCoord, 42, "type_tera");
|
||||||
if (tera) {
|
if (tera) {
|
||||||
typeIcon.setScale(0.5);
|
typeIcon.setScale(0.5);
|
||||||
const typeRgb = getTypeRgb(type);
|
const typeRgb = getTypeRgb(type);
|
||||||
@ -934,10 +935,14 @@ export default class SummaryUiHandler extends UiHandler {
|
|||||||
|
|
||||||
if (this.summaryUiMode === SummaryUiMode.LEARN_MOVE) {
|
if (this.summaryUiMode === SummaryUiMode.LEARN_MOVE) {
|
||||||
this.extraMoveRowContainer.setVisible(true);
|
this.extraMoveRowContainer.setVisible(true);
|
||||||
const newMoveTypeIcon = this.scene.add.sprite(0, 0, `types${Utils.verifyLang(i18next.resolvedLanguage) ? `_${i18next.resolvedLanguage}` : ""}`, Type[this.newMove?.type!].toLowerCase()); // TODO: is this bang correct?
|
|
||||||
newMoveTypeIcon.setOrigin(0, 1);
|
|
||||||
this.extraMoveRowContainer.add(newMoveTypeIcon);
|
|
||||||
|
|
||||||
|
if (this.newMove && this.pokemon) {
|
||||||
|
const spriteKey = Utils.getLocalizedSpriteKey("types");
|
||||||
|
const moveType = this.pokemon.getMoveType(this.newMove);
|
||||||
|
const newMoveTypeIcon = this.scene.add.sprite(0, 0, spriteKey, Type[moveType].toLowerCase());
|
||||||
|
newMoveTypeIcon.setOrigin(0, 1);
|
||||||
|
this.extraMoveRowContainer.add(newMoveTypeIcon);
|
||||||
|
}
|
||||||
const ppOverlay = this.scene.add.image(163, -1, "summary_moves_overlay_pp");
|
const ppOverlay = this.scene.add.image(163, -1, "summary_moves_overlay_pp");
|
||||||
ppOverlay.setOrigin(0, 1);
|
ppOverlay.setOrigin(0, 1);
|
||||||
this.extraMoveRowContainer.add(ppOverlay);
|
this.extraMoveRowContainer.add(ppOverlay);
|
||||||
@ -956,8 +961,11 @@ export default class SummaryUiHandler extends UiHandler {
|
|||||||
const moveRowContainer = this.scene.add.container(0, 16 * m);
|
const moveRowContainer = this.scene.add.container(0, 16 * m);
|
||||||
this.moveRowsContainer.add(moveRowContainer);
|
this.moveRowsContainer.add(moveRowContainer);
|
||||||
|
|
||||||
if (move) {
|
if (move && this.pokemon) {
|
||||||
const typeIcon = this.scene.add.sprite(0, 0, `types${Utils.verifyLang(i18next.resolvedLanguage) ? `_${i18next.resolvedLanguage}` : ""}`, Type[move.getMove().type].toLowerCase()); typeIcon.setOrigin(0, 1);
|
const spriteKey = Utils.getLocalizedSpriteKey("types");
|
||||||
|
const moveType = this.pokemon.getMoveType(move.getMove());
|
||||||
|
const typeIcon = this.scene.add.sprite(0, 0, spriteKey, Type[moveType].toLowerCase());
|
||||||
|
typeIcon.setOrigin(0, 1);
|
||||||
moveRowContainer.add(typeIcon);
|
moveRowContainer.add(typeIcon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -574,3 +574,12 @@ export function isNullOrUndefined(object: any): boolean {
|
|||||||
export function toDmgValue(value: number, minValue: number = 1) {
|
export function toDmgValue(value: number, minValue: number = 1) {
|
||||||
return Math.max(Math.floor(value), minValue);
|
return Math.max(Math.floor(value), minValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to localize a sprite key (e.g. for types)
|
||||||
|
* @param baseKey the base key of the sprite (e.g. `type`)
|
||||||
|
* @returns the localized sprite key
|
||||||
|
*/
|
||||||
|
export function getLocalizedSpriteKey(baseKey: string) {
|
||||||
|
return `${baseKey}${verifyLang(i18next.resolvedLanguage) ? `_${i18next.resolvedLanguage}` : ""}`;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user