[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 { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
|
||||
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 { Stat, getStatName } from "./pokemon-stat";
|
||||
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)) {
|
||||
return false;
|
||||
}
|
||||
if (attacker !== pokemon && move.type === this.immuneType) {
|
||||
if (attacker !== pokemon && attacker.getMoveType(move) === this.immuneType) {
|
||||
(args[0] as Utils.NumberHolder).value = 0;
|
||||
return true;
|
||||
}
|
||||
|
@ -372,7 +372,8 @@ export class AttackTypeImmunityAbAttr extends TypeImmunityAbAttr {
|
|||
* Example: Levitate
|
||||
*/
|
||||
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 false;
|
||||
|
@ -392,6 +393,7 @@ export class TypeImmunityHealAbAttr extends TypeImmunityAbAttr {
|
|||
const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name;
|
||||
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(),
|
||||
Utils.toDmgValue(pokemon.getMaxHp() / 4), i18next.t("abilityTriggers:typeImmunityHeal", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }), true));
|
||||
cancelled.value = true; // Suppresses "No Effect" message
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -415,7 +417,7 @@ class TypeImmunityStatChangeAbAttr extends TypeImmunityAbAttr {
|
|||
const ret = super.applyPreDefend(pokemon, passive, simulated, attacker, move, cancelled, args);
|
||||
|
||||
if (ret) {
|
||||
cancelled.value = true;
|
||||
cancelled.value = true; // Suppresses "No Effect" message
|
||||
if (!simulated) {
|
||||
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);
|
||||
|
||||
if (ret) {
|
||||
cancelled.value = true;
|
||||
cancelled.value = true; // Suppresses "No Effect" message
|
||||
if (!simulated) {
|
||||
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 {
|
||||
if (move instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.type, attacker) < 2) {
|
||||
cancelled.value = true;
|
||||
if (move instanceof AttackMove && pokemon.getAttackTypeEffectiveness(pokemon.getMoveType(move), attacker) < 2) {
|
||||
cancelled.value = true; // Suppresses "No Effect" message
|
||||
(args[0] as Utils.NumberHolder).value = 0;
|
||||
return true;
|
||||
}
|
||||
|
@ -764,7 +766,7 @@ export class PostDefendTypeChangeAbAttr extends PostDefendAbAttr {
|
|||
if (simulated) {
|
||||
return true;
|
||||
}
|
||||
const type = move.type;
|
||||
const type = attacker.getMoveType(move);
|
||||
const pokemonTypes = pokemon.getTypes(true);
|
||||
if (pokemonTypes.length !== 1 || pokemonTypes[0] !== 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(
|
||||
private newType: Type,
|
||||
private powerMultiplier: number,
|
||||
|
@ -1221,11 +1223,14 @@ export class MoveTypeChangeAttr extends PreAttackAbAttr {
|
|||
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 {
|
||||
if (this.condition && this.condition(pokemon, defender, move)) {
|
||||
move.type = this.newType;
|
||||
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;
|
||||
}
|
||||
|
@ -1257,22 +1262,12 @@ export class PokemonTypeChangeAbAttr extends PreAttackAbAttr {
|
|||
attr instanceof CopyMoveAttr
|
||||
)
|
||||
) {
|
||||
// TODO remove this copy when phase order is changed so that damage, type, category, etc.
|
||||
// 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;
|
||||
const moveType = pokemon.getMoveType(move);
|
||||
|
||||
// Moves like Weather Ball ignore effects of abilities like Normalize and Refrigerate
|
||||
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 (pokemon.getTypes().some((t) => t !== moveType)) {
|
||||
if (!simulated) {
|
||||
this.moveType = moveCopy.type;
|
||||
pokemon.summonData.types = [moveCopy.type];
|
||||
this.moveType = moveType;
|
||||
pokemon.summonData.types = [moveType];
|
||||
pokemon.updateInfo();
|
||||
}
|
||||
|
||||
|
@ -2978,16 +2973,20 @@ function getAnticipationCondition(): AbAttrCondition {
|
|||
return (pokemon: Pokemon) => {
|
||||
for (const opponent of pokemon.getOpponents()) {
|
||||
for (const move of opponent.moveset) {
|
||||
// move is super effective
|
||||
if (move!.getMove() instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move!.getMove().type, opponent, true) >= 2) { // TODO: is this bang correct?
|
||||
// ignore null/undefined moves
|
||||
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;
|
||||
}
|
||||
// move is a OHKO
|
||||
if (move?.getMove().hasAttr(OneHitKOAttr)) {
|
||||
if (move.getMove().hasAttr(OneHitKOAttr)) {
|
||||
return true;
|
||||
}
|
||||
// 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)
|
||||
+(opponent.ivs[Stat.ATK] & 1) * 2
|
||||
+(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.hasAbility(Abilities.COMATOSE), BattleStatMultiplierAbAttr, BattleStat.SPD, 1.5),
|
||||
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);
|
||||
}),
|
||||
new Ability(Abilities.SNIPER, 4)
|
||||
|
@ -5260,7 +5259,7 @@ export function initAbilities() {
|
|||
new Ability(Abilities.STRONG_JAW, 6)
|
||||
.attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.BITING_MOVE), 1.5),
|
||||
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)
|
||||
.attr(UserFieldStatusEffectImmunityAbAttr, StatusEffect.SLEEP)
|
||||
.attr(UserFieldBattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
|
||||
|
@ -5283,11 +5282,11 @@ export function initAbilities() {
|
|||
new Ability(Abilities.TOUGH_CLAWS, 6)
|
||||
.attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), 1.3),
|
||||
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)
|
||||
.attr(PostDefendStatChangeAbAttr, (target, user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), BattleStat.SPD, -1, false),
|
||||
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)
|
||||
.attr(AddSecondStrikeAbAttr, 0.25),
|
||||
new Ability(Abilities.DARK_AURA, 6)
|
||||
|
@ -5359,11 +5358,11 @@ export function initAbilities() {
|
|||
new Ability(Abilities.LONG_REACH, 7)
|
||||
.attr(IgnoreContactAbAttr),
|
||||
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)
|
||||
.attr(ChangeMovePriorityAbAttr, (pokemon, move) => move.hasFlag(MoveFlags.TRIAGE_MOVE), 3),
|
||||
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)
|
||||
.conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), BattleStatMultiplierAbAttr, BattleStat.SPD, 2),
|
||||
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 { WeatherType } from "./weather";
|
||||
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 { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier, PokemonMoveAccuracyBoosterModifier, AttackTypeBoosterModifier, PokemonMultiHitModifier } from "../modifier/modifier";
|
||||
import { BattlerIndex, BattleType } from "../battle";
|
||||
|
@ -113,9 +113,8 @@ type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean;
|
|||
export default class Move implements Localizable {
|
||||
public id: Moves;
|
||||
public name: string;
|
||||
public type: Type;
|
||||
public defaultType: Type;
|
||||
public category: MoveCategory;
|
||||
private _type: Type;
|
||||
private _category: MoveCategory;
|
||||
public moveTarget: MoveTarget;
|
||||
public power: integer;
|
||||
public accuracy: integer;
|
||||
|
@ -133,9 +132,8 @@ export default class Move implements Localizable {
|
|||
this.id = id;
|
||||
|
||||
this.nameAppend = "";
|
||||
this.type = type;
|
||||
this.defaultType = type;
|
||||
this.category = category;
|
||||
this._type = type;
|
||||
this._category = category;
|
||||
this.moveTarget = defaultMoveTarget;
|
||||
this.power = power;
|
||||
this.accuracy = accuracy;
|
||||
|
@ -158,6 +156,13 @@ export default class Move implements Localizable {
|
|||
this.localize();
|
||||
}
|
||||
|
||||
get type() {
|
||||
return this._type;
|
||||
}
|
||||
get category() {
|
||||
return this._category;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
@ -733,7 +738,7 @@ export default class Move implements Localizable {
|
|||
const power = new Utils.NumberHolder(this.power);
|
||||
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();
|
||||
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;
|
||||
|
||||
constructor(immuneType: Type) {
|
||||
super(false);
|
||||
|
||||
this.immuneType = immuneType;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Attribute for Status moves that take attack type effectiveness
|
||||
* into consideration (i.e. {@linkcode https://bulbapedia.bulbagarden.net/wiki/Thunder_Wave_(move) | Thunder Wave})
|
||||
* @extends MoveAttr
|
||||
*/
|
||||
export class RespectAttackTypeImmunityAttr extends MoveAttr { }
|
||||
|
||||
export class IgnoreOpponentStatChangesAttr extends MoveAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
|
@ -1851,19 +1853,11 @@ export class MultiHitAttr extends MoveAttr {
|
|||
* @returns True
|
||||
*/
|
||||
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) {
|
||||
// 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;
|
||||
(args[0] as Utils.NumberHolder).value = this.getHitCount(user, target);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -3762,7 +3756,7 @@ export class VariableMoveCategoryAttr extends MoveAttr {
|
|||
|
||||
export class PhotonGeyserCategoryAttr extends VariableMoveCategoryAttr {
|
||||
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)) {
|
||||
category.value = MoveCategory.PHYSICAL;
|
||||
|
@ -3775,7 +3769,7 @@ export class PhotonGeyserCategoryAttr extends VariableMoveCategoryAttr {
|
|||
|
||||
export class TeraBlastCategoryAttr extends VariableMoveCategoryAttr {
|
||||
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)) {
|
||||
category.value = MoveCategory.PHYSICAL;
|
||||
|
@ -3791,18 +3785,21 @@ export class TeraBlastCategoryAttr extends VariableMoveCategoryAttr {
|
|||
* @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
|
||||
* @param target {@linkcode Pokemon} N/A
|
||||
* @param move {@linkcode Move} {@linkcode Move.TERA_BLAST}
|
||||
* @param {any[]} args N/A
|
||||
* @returns true or false
|
||||
* Sets Tera Blast's power to 100 if the user is terastallized with
|
||||
* the Stellar tera type.
|
||||
* @param user {@linkcode Pokemon} the Pokemon using this move
|
||||
* @param target n/a
|
||||
* @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;
|
||||
if (user.isTerastallized() && move.type === Type.STELLAR) {
|
||||
//200 instead of 100 to reflect lack of stellar being 2x dmg on any type
|
||||
power.value = 200;
|
||||
if (user.isTerastallized() && user.getTeraType() === Type.STELLAR) {
|
||||
power.value = 100;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -3862,10 +3859,15 @@ export class VariableMoveTypeAttr extends MoveAttr {
|
|||
|
||||
export class FormChangeItemTypeAttr extends VariableMoveTypeAttr {
|
||||
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)) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -3875,24 +3877,29 @@ export class FormChangeItemTypeAttr extends VariableMoveTypeAttr {
|
|||
|
||||
export class TechnoBlastTypeAttr extends VariableMoveTypeAttr {
|
||||
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)) {
|
||||
const form = user.species.speciesId === Species.GENESECT ? user.formIndex : user.fusionSpecies?.formIndex;
|
||||
|
||||
switch (form) {
|
||||
case 1: // Shock Drive
|
||||
move.type = Type.ELECTRIC;
|
||||
moveType.value = Type.ELECTRIC;
|
||||
break;
|
||||
case 2: // Burn Drive
|
||||
move.type = Type.FIRE;
|
||||
moveType.value = Type.FIRE;
|
||||
break;
|
||||
case 3: // Chill Drive
|
||||
move.type = Type.ICE;
|
||||
moveType.value = Type.ICE;
|
||||
break;
|
||||
case 4: // Douse Drive
|
||||
move.type = Type.WATER;
|
||||
moveType.value = Type.WATER;
|
||||
break;
|
||||
default:
|
||||
move.type = Type.NORMAL;
|
||||
moveType.value = Type.NORMAL;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
|
@ -3904,15 +3911,20 @@ export class TechnoBlastTypeAttr extends VariableMoveTypeAttr {
|
|||
|
||||
export class AuraWheelTypeAttr extends VariableMoveTypeAttr {
|
||||
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)) {
|
||||
const form = user.species.speciesId === Species.MORPEKO ? user.formIndex : user.fusionSpecies?.formIndex;
|
||||
|
||||
switch (form) {
|
||||
case 1: // Hangry Mode
|
||||
move.type = Type.DARK;
|
||||
moveType.value = Type.DARK;
|
||||
break;
|
||||
default: // Full Belly Mode
|
||||
move.type = Type.ELECTRIC;
|
||||
moveType.value = Type.ELECTRIC;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
|
@ -3924,18 +3936,23 @@ export class AuraWheelTypeAttr extends VariableMoveTypeAttr {
|
|||
|
||||
export class RagingBullTypeAttr extends VariableMoveTypeAttr {
|
||||
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)) {
|
||||
const form = user.species.speciesId === Species.PALDEA_TAUROS ? user.formIndex : user.fusionSpecies?.formIndex;
|
||||
|
||||
switch (form) {
|
||||
case 1: // Blaze breed
|
||||
move.type = Type.FIRE;
|
||||
moveType.value = Type.FIRE;
|
||||
break;
|
||||
case 2: // Aqua breed
|
||||
move.type = Type.WATER;
|
||||
moveType.value = Type.WATER;
|
||||
break;
|
||||
default:
|
||||
move.type = Type.FIGHTING;
|
||||
moveType.value = Type.FIGHTING;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
|
@ -3947,25 +3964,30 @@ export class RagingBullTypeAttr extends VariableMoveTypeAttr {
|
|||
|
||||
export class IvyCudgelTypeAttr extends VariableMoveTypeAttr {
|
||||
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)) {
|
||||
const form = user.species.speciesId === Species.OGERPON ? user.formIndex : user.fusionSpecies?.formIndex;
|
||||
|
||||
switch (form) {
|
||||
case 1: // Wellspring Mask
|
||||
case 5: // Wellspring Mask Tera
|
||||
move.type = Type.WATER;
|
||||
moveType.value = Type.WATER;
|
||||
break;
|
||||
case 2: // Hearthflame Mask
|
||||
case 6: // Hearthflame Mask Tera
|
||||
move.type = Type.FIRE;
|
||||
moveType.value = Type.FIRE;
|
||||
break;
|
||||
case 3: // Cornerstone Mask
|
||||
case 7: // Cornerstone Mask Tera
|
||||
move.type = Type.ROCK;
|
||||
moveType.value = Type.ROCK;
|
||||
break;
|
||||
case 4: // Teal Mask Tera
|
||||
default:
|
||||
move.type = Type.GRASS;
|
||||
moveType.value = Type.GRASS;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
|
@ -3977,22 +3999,27 @@ export class IvyCudgelTypeAttr extends VariableMoveTypeAttr {
|
|||
|
||||
export class WeatherBallTypeAttr extends VariableMoveTypeAttr {
|
||||
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)) {
|
||||
switch (user.scene.arena.weather?.weatherType) {
|
||||
case WeatherType.SUNNY:
|
||||
case WeatherType.HARSH_SUN:
|
||||
move.type = Type.FIRE;
|
||||
moveType.value = Type.FIRE;
|
||||
break;
|
||||
case WeatherType.RAIN:
|
||||
case WeatherType.HEAVY_RAIN:
|
||||
move.type = Type.WATER;
|
||||
moveType.value = Type.WATER;
|
||||
break;
|
||||
case WeatherType.SANDSTORM:
|
||||
move.type = Type.ROCK;
|
||||
moveType.value = Type.ROCK;
|
||||
break;
|
||||
case WeatherType.HAIL:
|
||||
case WeatherType.SNOW:
|
||||
move.type = Type.ICE;
|
||||
moveType.value = Type.ICE;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
|
@ -4015,10 +4042,15 @@ export class TerrainPulseTypeAttr extends VariableMoveTypeAttr {
|
|||
* @param user {@linkcode Pokemon} using this move
|
||||
* @param target 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
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const moveType = args[0];
|
||||
if (!(moveType instanceof Utils.NumberHolder)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!user.isGrounded()) {
|
||||
return false;
|
||||
}
|
||||
|
@ -4026,16 +4058,16 @@ export class TerrainPulseTypeAttr extends VariableMoveTypeAttr {
|
|||
const currentTerrain = user.scene.arena.getTerrainType();
|
||||
switch (currentTerrain) {
|
||||
case TerrainType.MISTY:
|
||||
move.type = Type.FAIRY;
|
||||
moveType.value = Type.FAIRY;
|
||||
break;
|
||||
case TerrainType.ELECTRIC:
|
||||
move.type = Type.ELECTRIC;
|
||||
moveType.value = Type.ELECTRIC;
|
||||
break;
|
||||
case TerrainType.GRASSY:
|
||||
move.type = Type.GRASS;
|
||||
moveType.value = Type.GRASS;
|
||||
break;
|
||||
case TerrainType.PSYCHIC:
|
||||
move.type = Type.PSYCHIC;
|
||||
moveType.value = Type.PSYCHIC;
|
||||
break;
|
||||
default:
|
||||
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 {
|
||||
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)
|
||||
+(user.ivs[Stat.ATK] & 1) * 2
|
||||
+(user.ivs[Stat.DEF] & 1) * 4
|
||||
|
@ -4053,7 +4094,7 @@ export class HiddenPowerTypeAttr extends VariableMoveTypeAttr {
|
|||
+(user.ivs[Stat.SPATK] & 1) * 16
|
||||
+(user.ivs[Stat.SPDEF] & 1) * 32) * 15/63);
|
||||
|
||||
move.type = [
|
||||
moveType.value = [
|
||||
Type.FIGHTING, Type.FLYING, Type.POISON, Type.GROUND,
|
||||
Type.ROCK, Type.BUG, Type.GHOST, Type.STEEL,
|
||||
Type.FIRE, Type.WATER, Type.GRASS, Type.ELECTRIC,
|
||||
|
@ -4068,16 +4109,21 @@ export class HiddenPowerTypeAttr extends VariableMoveTypeAttr {
|
|||
* @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 move {@linkcode Move} {@linkcode Move.TeraBlastTypeAttr}
|
||||
* @param {any[]} args N/A
|
||||
* @returns true or false
|
||||
* @param move {@linkcode Move} the move with this attribute
|
||||
* @param args `[0]` the move's type to be modified
|
||||
* @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()) {
|
||||
move.type = user.getTeraType(); //changes move type to tera type
|
||||
moveType.value = user.getTeraType(); // changes move type to tera type
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -4087,14 +4133,18 @@ export class TeraBlastTypeAttr extends VariableMoveTypeAttr {
|
|||
|
||||
export class MatchUserTypeAttr extends VariableMoveTypeAttr {
|
||||
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);
|
||||
|
||||
if (userTypes.includes(Type.STELLAR)) { // will not change to stellar type
|
||||
const nonTeraTypes = user.getTypes();
|
||||
move.type = nonTeraTypes[0];
|
||||
moveType.value = nonTeraTypes[0];
|
||||
return true;
|
||||
} else if (userTypes.length > 0) {
|
||||
move.type = userTypes[0];
|
||||
moveType.value = userTypes[0];
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -4113,8 +4163,8 @@ export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTy
|
|||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!target.getTag(BattlerTagType.IGNORE_FLYING)) {
|
||||
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
|
||||
if (target.isOfType(Type.FLYING) || target.hasAbility(Abilities.LEVITATE)) {
|
||||
//When a flying type is hit, the first hit is always 1x multiplier.
|
||||
if (target.isOfType(Type.FLYING)) {
|
||||
multiplier.value = 1;
|
||||
}
|
||||
return true;
|
||||
|
@ -6505,7 +6555,7 @@ export function initMoves() {
|
|||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS),
|
||||
new StatusMove(Moves.THUNDER_WAVE, Type.ELECTRIC, 90, 20, -1, 0, 1)
|
||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
||||
.attr(StatusMoveTypeImmunityAttr, Type.GROUND),
|
||||
.attr(RespectAttackTypeImmunityAttr),
|
||||
new AttackMove(Moves.THUNDER, Type.ELECTRIC, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 1)
|
||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
||||
.attr(ThunderAccuracyAttr)
|
||||
|
|
|
@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "../battle-scene";
|
|||
import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
|
||||
import { variantData } from "#app/data/variant";
|
||||
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 { Constructor } from "#app/utils";
|
||||
import * as Utils from "../utils";
|
||||
|
@ -22,7 +22,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoo
|
|||
import { WeatherType } from "../data/weather";
|
||||
import { TempBattleStat } from "../data/temp-battle-stat";
|
||||
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 { BattlerIndex } from "../battle";
|
||||
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.
|
||||
*
|
||||
* @param source - The Pokémon using the move.
|
||||
* @param move - The move being used.
|
||||
* @returns The type damage multiplier or 1 if it's a status move
|
||||
* Calculates the type of a move when used by this Pokemon after
|
||||
* type-changing move and ability attributes have applied.
|
||||
* @param move {@linkcode Move} The move being used.
|
||||
* @param simulated If `true`, prevents showing abilities applied in this calculation.
|
||||
* @returns the {@linkcode Type} of the move after attributes are applied
|
||||
*/
|
||||
getMoveEffectiveness(source: Pokemon, move: PokemonMove): TypeDamageMultiplier {
|
||||
if (move.getMove().category === MoveCategory.STATUS) {
|
||||
return 1;
|
||||
}
|
||||
getMoveType(move: Move, simulated: boolean = true): Type {
|
||||
const moveTypeHolder = new Utils.NumberHolder(move.type);
|
||||
|
||||
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 pokemonMove - The move being used by the attacking Pokémon.
|
||||
* @param ignoreAbility - Whether to check for abilities that might affect type effectiveness or immunity.
|
||||
* @param source {@linkcode Pokemon} The attacking Pokémon.
|
||||
* @param move {@linkcode Move} The move being used by the attacking Pokémon.
|
||||
* @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
|
||||
*/
|
||||
getAttackMoveEffectiveness(source: Pokemon, pokemonMove: PokemonMove, ignoreAbility: boolean = false): TypeDamageMultiplier {
|
||||
const move = pokemonMove.getMove();
|
||||
const typeless = move.hasAttr(TypelessAttr);
|
||||
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);
|
||||
getMoveEffectiveness(source: Pokemon, move: Move, ignoreAbility: boolean = false, simulated: boolean = true, cancelled?: Utils.BooleanHolder): TypeDamageMultiplier {
|
||||
if (move.hasAttr(TypelessAttr)) {
|
||||
return 1;
|
||||
}
|
||||
if (!cancelled.value && !ignoreAbility) {
|
||||
applyPreDefendAbAttrs(MoveImmunityAbAttr, this, source, move, cancelled, true, typeMultiplier);
|
||||
const moveType = source.getMoveType(move);
|
||||
|
||||
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
|
||||
* @param moveOrType The move being used, or a type if the move is unknown
|
||||
* @param source the Pokemon using the move
|
||||
* @param moveType {@linkcode Type} the type of the move being used
|
||||
* @param source {@linkcode Pokemon} the Pokemon using the move
|
||||
* @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
|
||||
* @returns a multiplier for the type effectiveness
|
||||
*/
|
||||
getAttackTypeEffectiveness(moveOrType: Move | 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;
|
||||
|
||||
getAttackTypeEffectiveness(moveType: Type, source?: Pokemon, ignoreStrongWinds: boolean = false, simulated: boolean = true): TypeDamageMultiplier {
|
||||
if (moveType === Type.STELLAR) {
|
||||
return this.isTerastallized() ? 2 : 1;
|
||||
}
|
||||
|
@ -1281,7 +1304,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
if (source) {
|
||||
const ignoreImmunity = new Utils.BooleanHolder(false);
|
||||
if (source.isActive(true) && source.hasAbilityWithAttr(IgnoreTypeImmunityAbAttr)) {
|
||||
applyAbAttrs(IgnoreTypeImmunityAbAttr, source, ignoreImmunity, false, moveType, defType);
|
||||
applyAbAttrs(IgnoreTypeImmunityAbAttr, source, ignoreImmunity, simulated, moveType, defType);
|
||||
}
|
||||
if (ignoreImmunity.value) {
|
||||
return 1;
|
||||
|
@ -1303,15 +1326,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
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;
|
||||
}
|
||||
|
||||
|
@ -1959,29 +1973,23 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
let result: HitResult;
|
||||
const damage = new Utils.NumberHolder(0);
|
||||
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);
|
||||
const moveCategory = variableCategory.value as MoveCategory;
|
||||
|
||||
applyMoveAttrs(VariableMoveTypeAttr, source, this, move);
|
||||
const types = this.getTypes(true, true);
|
||||
/** The move's type after type-changing effects are applied */
|
||||
const moveType = source.getMoveType(move);
|
||||
|
||||
/** If `value` is `true`, cancels the move and suppresses "No Effect" messages */
|
||||
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)
|
||||
: 1);
|
||||
applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier);
|
||||
if (typeless) {
|
||||
typeMultiplier.value = 1;
|
||||
}
|
||||
if (types.find(t => move.isTypeImmune(source, this, t))) {
|
||||
typeMultiplier.value = 0;
|
||||
}
|
||||
/**
|
||||
* The effectiveness of the move being used. Along with type matchups, this
|
||||
* accounts for changes in effectiveness from the move's attributes and the
|
||||
* abilities of both the source and this Pokemon.
|
||||
*/
|
||||
const typeMultiplier = this.getMoveEffectiveness(source, move, false, false, cancelled);
|
||||
|
||||
switch (moveCategory) {
|
||||
case MoveCategory.PHYSICAL:
|
||||
|
@ -1989,27 +1997,44 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
const isPhysical = moveCategory === MoveCategory.PHYSICAL;
|
||||
const sourceTeraType = source.getTeraType();
|
||||
|
||||
if (!typeless) {
|
||||
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));
|
||||
}
|
||||
const power = move.calculateBattlePower(source, this);
|
||||
|
||||
if (cancelled.value) {
|
||||
// Cancelled moves fail silently
|
||||
source.stopMultiHit(this);
|
||||
result = HitResult.NO_EFFECT;
|
||||
return HitResult.NO_EFFECT;
|
||||
} 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) {
|
||||
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);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
if (this.getTag(BattlerTagType.RECEIVE_DOUBLE_DAMAGE)) {
|
||||
glaiveRushModifier.value = 2;
|
||||
|
@ -2059,13 +2084,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
if (!isCritical) {
|
||||
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 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);
|
||||
if (sourceTeraType === Type.UNKNOWN && matchesSourceType) {
|
||||
stabMultiplier.value += 0.5;
|
||||
} else if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === move.type) {
|
||||
} else if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === moveType) {
|
||||
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);
|
||||
damage.value = Utils.toDmgValue((((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2)
|
||||
* stabMultiplier.value
|
||||
* typeMultiplier.value
|
||||
* typeMultiplier
|
||||
* arenaAttackTypeMultiplier.value
|
||||
* screenMultiplier.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);
|
||||
}
|
||||
|
||||
|
@ -2143,22 +2167,18 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
result = result!; // telling TS compiler that result is defined!
|
||||
|
||||
if (!result) {
|
||||
if (!typeMultiplier.value) {
|
||||
result = move.id === Moves.SHEER_COLD ? HitResult.IMMUNE : HitResult.NO_EFFECT;
|
||||
const isOneHitKo = new Utils.BooleanHolder(false);
|
||||
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 {
|
||||
const isOneHitKo = new Utils.BooleanHolder(false);
|
||||
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;
|
||||
}
|
||||
result = HitResult.NOT_VERY_EFFECTIVE;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2225,15 +2245,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
case HitResult.NOT_VERY_EFFECTIVE:
|
||||
this.scene.queueMessage(i18next.t("battle:hitResultNotVeryEffective"));
|
||||
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:
|
||||
this.scene.queueMessage(i18next.t("battle:hitResultOneHitKO"));
|
||||
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) {
|
||||
const attacker = this.scene.getPokemonById(source.id)!; // TODO: is this bang correct?
|
||||
destinyTag?.lapse(attacker, BattlerTagLapseType.CUSTOM);
|
||||
destinyTag?.lapse(source, BattlerTagLapseType.CUSTOM);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MoveCategory.STATUS:
|
||||
if (!typeless) {
|
||||
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) {
|
||||
if (!cancelled.value && typeMultiplier === 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -3918,7 +3928,7 @@ export class EnemyPokemon extends Pokemon {
|
|||
* 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.
|
||||
*/
|
||||
const effectiveness = target.getAttackMoveEffectiveness(this, pokemonMove);
|
||||
const effectiveness = target.getMoveEffectiveness(this, move, !target.battleData?.abilityRevealed);
|
||||
if (target.isPlayer() !== this.isPlayer()) {
|
||||
targetScore *= effectiveness;
|
||||
if (this.isOfType(move.type)) {
|
||||
|
|
|
@ -311,8 +311,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||
}
|
||||
|
||||
end() {
|
||||
const move = this.move.getMove();
|
||||
move.type = move.defaultType;
|
||||
const user = this.getUserPokemon();
|
||||
/**
|
||||
* If this phase isn't for the invoked move's last strike,
|
||||
|
|
|
@ -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);
|
||||
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);
|
||||
|
||||
await game.toNextTurn();
|
||||
|
@ -249,7 +249,7 @@ describe("Abilities - Libero", () => {
|
|||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
leadPokemon.summonData.types = [allMoves[Moves.SPLASH].defaultType];
|
||||
leadPokemon.summonData.types = [allMoves[Moves.SPLASH].type];
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
|
@ -357,6 +357,6 @@ function testPokemonTypeMatchesDefaultMoveType(pokemon: PlayerPokemon, move: Mov
|
|||
expect(pokemon.summonData.abilitiesApplied).toContain(Abilities.LIBERO);
|
||||
expect(pokemon.getTypes()).toHaveLength(1);
|
||||
const pokemonType = Type[pokemon.getTypes()[0]],
|
||||
moveType = Type[allMoves[move].defaultType];
|
||||
moveType = Type[allMoves[move].type];
|
||||
expect(pokemonType).toBe(moveType);
|
||||
}
|
||||
|
|
|
@ -76,7 +76,7 @@ describe("Abilities - Protean", () => {
|
|||
|
||||
expect(leadPokemon.summonData.abilitiesApplied.filter((a) => a === Abilities.PROTEAN)).toHaveLength(1);
|
||||
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);
|
||||
|
||||
await game.toNextTurn();
|
||||
|
@ -249,7 +249,7 @@ describe("Abilities - Protean", () => {
|
|||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
leadPokemon.summonData.types = [allMoves[Moves.SPLASH].defaultType];
|
||||
leadPokemon.summonData.types = [allMoves[Moves.SPLASH].type];
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
|
@ -357,6 +357,6 @@ function testPokemonTypeMatchesDefaultMoveType(pokemon: PlayerPokemon, move: Mov
|
|||
expect(pokemon.summonData.abilitiesApplied).toContain(Abilities.PROTEAN);
|
||||
expect(pokemon.getTypes()).toHaveLength(1);
|
||||
const pokemonType = Type[pokemon.getTypes()[0]],
|
||||
moveType = Type[allMoves[move].defaultType];
|
||||
moveType = Type[allMoves[move].type];
|
||||
expect(pokemonType).toBe(moveType);
|
||||
}
|
||||
|
|
|
@ -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 () => {
|
||||
game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]);
|
||||
const stellarTypeMultiplier = 2;
|
||||
const stellarTypeDmgBonus = 20;
|
||||
const basePower = moveToCheck.power;
|
||||
|
||||
await game.startBattle();
|
||||
|
||||
|
@ -72,9 +69,25 @@ describe("Moves - Tera Blast", () => {
|
|||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(moveToCheck.calculateBattlePower).toHaveReturnedWith((basePower + stellarTypeDmgBonus) * stellarTypeMultiplier);
|
||||
expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(100);
|
||||
}, 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
|
||||
it.skip("uses the higher stat of the user's Atk and SpAtk for damage calculation", async () => {
|
||||
game.override.enemyAbility(Abilities.TOXIC_DEBRIS);
|
||||
|
|
|
@ -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.setScale(0.86);
|
||||
this.monoTypeValue.setVisible(false);
|
||||
|
|
|
@ -44,7 +44,7 @@ export default class FightUiHandler extends UiHandler {
|
|||
this.moveInfoContainer.setName("move-info");
|
||||
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.moveInfoContainer.add(this.typeIcon);
|
||||
|
||||
|
@ -179,15 +179,20 @@ export default class FightUiHandler extends UiHandler {
|
|||
|
||||
if (hasMove) {
|
||||
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);
|
||||
this.moveCategoryIcon.setTexture("categories", MoveCategory[pokemonMove.getMove().category].toLowerCase()).setScale(1.0);
|
||||
const moveType = pokemon.getMoveType(pokemonMove.getMove());
|
||||
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 accuracy = pokemonMove.getMove().accuracy;
|
||||
const maxPP = pokemonMove.getMovePp();
|
||||
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.accuracyText.setText(`${accuracy >= 0 ? accuracy : "---"}`);
|
||||
|
||||
|
@ -231,7 +236,7 @@ export default class FightUiHandler extends UiHandler {
|
|||
* Returns undefined if it's a status move
|
||||
*/
|
||||
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) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -274,7 +279,7 @@ export default class FightUiHandler extends UiHandler {
|
|||
}
|
||||
|
||||
const moveColors = opponents
|
||||
.map((opponent) => opponent.getMoveEffectiveness(pokemon, pokemonMove))
|
||||
.map((opponent) => opponent.getMoveEffectiveness(pokemon, pokemonMove.getMove(), !opponent.battleData.abilityRevealed))
|
||||
.sort((a, b) => b - a)
|
||||
.map((effectiveness) => getTypeDamageMultiplierColor(effectiveness ?? 0, "offense"));
|
||||
|
||||
|
|
|
@ -410,7 +410,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
|||
if (index === 0 || index === 19) {
|
||||
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.setFrame(type.toLowerCase());
|
||||
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.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.setOrigin(0, 0);
|
||||
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.setOrigin(0, 0);
|
||||
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 xCoord = typeLabel.width * typeLabel.scale + 9 + 34 * index;
|
||||
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) {
|
||||
typeIcon.setScale(0.5);
|
||||
const typeRgb = getTypeRgb(type);
|
||||
|
@ -934,10 +935,14 @@ export default class SummaryUiHandler extends UiHandler {
|
|||
|
||||
if (this.summaryUiMode === SummaryUiMode.LEARN_MOVE) {
|
||||
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");
|
||||
ppOverlay.setOrigin(0, 1);
|
||||
this.extraMoveRowContainer.add(ppOverlay);
|
||||
|
@ -956,8 +961,11 @@ export default class SummaryUiHandler extends UiHandler {
|
|||
const moveRowContainer = this.scene.add.container(0, 16 * m);
|
||||
this.moveRowsContainer.add(moveRowContainer);
|
||||
|
||||
if (move) {
|
||||
const typeIcon = this.scene.add.sprite(0, 0, `types${Utils.verifyLang(i18next.resolvedLanguage) ? `_${i18next.resolvedLanguage}` : ""}`, Type[move.getMove().type].toLowerCase()); typeIcon.setOrigin(0, 1);
|
||||
if (move && this.pokemon) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -574,3 +574,12 @@ export function isNullOrUndefined(object: any): boolean {
|
|||
export function toDmgValue(value: number, minValue: number = 1) {
|
||||
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