[Enhancement] Add support for simulated calls to Abilities (#3529)

* Adds simulated tag support to all abilities

* Fix compiler errors in ability.ts

Most things are still on fire 😢

* Fix errors left over after merge

* Another pass through simulated ability call logic

* Fix leftover errors from merge resolution

* Another gh pages issue :pikamad:

* Simulated call fixes based on Kev's feedback

* RIP phases.ts

---------

Co-authored-by: Xavion3 <xavion333@gmail.com>
This commit is contained in:
innerthunder 2024-08-21 18:29:21 -07:00 committed by Jesse Chung
parent 0567b4db73
commit 754d377539
7 changed files with 82 additions and 116 deletions

View File

@ -372,8 +372,7 @@ 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 {
// this is a hacky way to fix the Levitate/Thousand Arrows interaction, but it works for now... if (move.category !== MoveCategory.STATUS) {
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,8 +391,7 @@ export class TypeImmunityHealAbAttr extends TypeImmunityAbAttr {
if (!pokemon.isFullHp() && !simulated) { if (!pokemon.isFullHp() && !simulated) {
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)); Math.max(Math.floor(pokemon.getMaxHp() / 4), 1), i18next.t("abilityTriggers:typeImmunityHeal", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }), true));
cancelled.value = true; // Suppresses "No Effect" message
} }
return true; return true;
} }
@ -417,7 +415,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; // Suppresses "No Effect" message cancelled.value = true;
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));
} }
@ -442,7 +440,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; // Suppresses "No Effect" message cancelled.value = true;
if (!simulated) { if (!simulated) {
pokemon.addTag(this.tagType, this.turnCount, undefined, pokemon.id); pokemon.addTag(this.tagType, this.turnCount, undefined, pokemon.id);
} }
@ -458,8 +456,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(pokemon.getMoveType(move), attacker) < 2) { if (move instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.type, attacker) < 2) {
cancelled.value = true; // Suppresses "No Effect" message cancelled.value = true;
(args[0] as Utils.NumberHolder).value = 0; (args[0] as Utils.NumberHolder).value = 0;
return true; return true;
} }
@ -807,7 +805,7 @@ export class PostDefendTypeChangeAbAttr extends PostDefendAbAttr {
if (simulated) { if (simulated) {
return true; return true;
} }
const type = attacker.getMoveType(move); const type = move.type;
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 ];
@ -948,8 +946,8 @@ export class PostDefendContactDamageAbAttr extends PostDefendAbAttr {
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (!simulated && move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) { if (!simulated && move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) {
attacker.damageAndUpdate(Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER); attacker.damageAndUpdate(Math.ceil(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER);
attacker.turnData.damageTaken += Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)); attacker.turnData.damageTaken += Math.ceil(attacker.getMaxHp() * (1 / this.damageRatio));
return true; return true;
} }
@ -1264,7 +1262,6 @@ export class MoveTypeChangeAbAttr 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)) {
if (args[0] && args[0] instanceof Utils.NumberHolder) { if (args[0] && args[0] instanceof Utils.NumberHolder) {
@ -1305,10 +1302,17 @@ export class PokemonTypeChangeAbAttr extends PreAttackAbAttr {
) { ) {
const moveType = pokemon.getMoveType(move); const moveType = pokemon.getMoveType(move);
if (pokemon.getTypes().some((t) => t !== moveType)) { // 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 (!simulated) { if (!simulated) {
this.moveType = moveType; this.moveType = moveCopy.type;
pokemon.summonData.types = [moveType]; pokemon.summonData.types = [moveCopy.type];
pokemon.updateInfo(); pokemon.updateInfo();
} }
@ -2086,7 +2090,7 @@ export class PostSummonAllyHealAbAttr extends PostSummonAbAttr {
if (target?.isActive(true)) { if (target?.isActive(true)) {
if (!simulated) { if (!simulated) {
target.scene.unshiftPhase(new PokemonHealPhase(target.scene, target.getBattlerIndex(), target.scene.unshiftPhase(new PokemonHealPhase(target.scene, target.getBattlerIndex(),
Utils.toDmgValue(pokemon.getMaxHp() / this.healRatio), i18next.t("abilityTriggers:postSummonAllyHeal", { pokemonNameWithAffix: getPokemonNameWithAffix(target), pokemonName: pokemon.name }), true, !this.showAnim)); Math.max(Math.floor(pokemon.getMaxHp() / this.healRatio), 1), i18next.t("abilityTriggers:postSummonAllyHeal", { pokemonNameWithAffix: getPokemonNameWithAffix(target), pokemonName: pokemon.name }), true, !this.showAnim));
} }
return true; return true;
@ -2547,7 +2551,7 @@ export class PreSwitchOutHealAbAttr extends PreSwitchOutAbAttr {
applyPreSwitchOut(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise<boolean> { applyPreSwitchOut(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise<boolean> {
if (!pokemon.isFullHp()) { if (!pokemon.isFullHp()) {
if (!simulated) { if (!simulated) {
const healAmount = Utils.toDmgValue(pokemon.getMaxHp() * 0.33); const healAmount = Math.floor(pokemon.getMaxHp() * 0.33);
pokemon.heal(healAmount); pokemon.heal(healAmount);
pokemon.updateInfo(); pokemon.updateInfo();
} }
@ -2658,7 +2662,7 @@ export class ConfusionOnStatusEffectAbAttr extends PostAttackAbAttr {
if (simulated) { if (simulated) {
return defender.canAddTag(BattlerTagType.CONFUSED); return defender.canAddTag(BattlerTagType.CONFUSED);
} else { } else {
return defender.addTag(BattlerTagType.CONFUSED, pokemon.randSeedInt(3, 2), move.id, defender.id); return defender.addTag(BattlerTagType.CONFUSED, pokemon.randSeedInt(3,2), move.id, defender.id);
} }
} }
return false; return false;
@ -3125,53 +3129,6 @@ export class PostWeatherChangeAbAttr extends AbAttr {
} }
} }
/**
* Triggers weather-based form change when weather changes.
* Used by Forecast and Flower Gift.
* @extends PostWeatherChangeAbAttr
*/
export class PostWeatherChangeFormChangeAbAttr extends PostWeatherChangeAbAttr {
private ability: Abilities;
private formRevertingWeathers: WeatherType[];
constructor(ability: Abilities, formRevertingWeathers: WeatherType[]) {
super(false);
this.ability = ability;
this.formRevertingWeathers = formRevertingWeathers;
}
/**
* Calls {@linkcode Arena.triggerWeatherBasedFormChangesToNormal | triggerWeatherBasedFormChangesToNormal} when the
* weather changed to form-reverting weather, otherwise calls {@linkcode Arena.triggerWeatherBasedFormChanges | triggerWeatherBasedFormChanges}
* @param {Pokemon} pokemon the Pokemon with this ability
* @param passive n/a
* @param weather n/a
* @param args n/a
* @returns whether the form change was triggered
*/
applyPostWeatherChange(pokemon: Pokemon, passive: boolean, simulated: boolean, weather: WeatherType, args: any[]): boolean {
const isCastformWithForecast = (pokemon.species.speciesId === Species.CASTFORM && this.ability === Abilities.FORECAST);
const isCherrimWithFlowerGift = (pokemon.species.speciesId === Species.CHERRIM && this.ability === Abilities.FLOWER_GIFT);
if (isCastformWithForecast || isCherrimWithFlowerGift) {
if (simulated) {
return simulated;
}
const weatherType = pokemon.scene.arena.weather?.weatherType;
if (weatherType && this.formRevertingWeathers.includes(weatherType)) {
pokemon.scene.arena.triggerWeatherBasedFormChangesToNormal();
} else {
pokemon.scene.arena.triggerWeatherBasedFormChanges();
}
return true;
}
return false;
}
}
export class PostWeatherChangeAddBattlerTagAttr extends PostWeatherChangeAbAttr { export class PostWeatherChangeAddBattlerTagAttr extends PostWeatherChangeAbAttr {
private tagType: BattlerTagType; private tagType: BattlerTagType;
private turnCount: integer; private turnCount: integer;
@ -3232,7 +3189,7 @@ export class PostWeatherLapseHealAbAttr extends PostWeatherLapseAbAttr {
const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name; const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name;
if (!simulated) { if (!simulated) {
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(), scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(),
Utils.toDmgValue(pokemon.getMaxHp() / (16 / this.healFactor)), i18next.t("abilityTriggers:postWeatherLapseHeal", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }), true)); Math.max(Math.floor(pokemon.getMaxHp() / (16 / this.healFactor)), 1), i18next.t("abilityTriggers:postWeatherLapseHeal", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }), true));
} }
return true; return true;
} }
@ -3259,7 +3216,7 @@ export class PostWeatherLapseDamageAbAttr extends PostWeatherLapseAbAttr {
if (!simulated) { if (!simulated) {
const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name; const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name;
scene.queueMessage(i18next.t("abilityTriggers:postWeatherLapseDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName })); scene.queueMessage(i18next.t("abilityTriggers:postWeatherLapseDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }));
pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / (16 / this.damageFactor)), HitResult.OTHER); pokemon.damageAndUpdate(Math.ceil(pokemon.getMaxHp() / (16 / this.damageFactor)), HitResult.OTHER);
} }
return true; return true;
@ -3339,7 +3296,7 @@ export class PostTurnStatusHealAbAttr extends PostTurnAbAttr {
const scene = pokemon.scene; const scene = pokemon.scene;
const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name; const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name;
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(), scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(),
Utils.toDmgValue(pokemon.getMaxHp() / 8), i18next.t("abilityTriggers:poisonHeal", { pokemonName: getPokemonNameWithAffix(pokemon), abilityName }), true)); Math.max(Math.floor(pokemon.getMaxHp() / 8), 1), i18next.t("abilityTriggers:poisonHeal", { pokemonName: getPokemonNameWithAffix(pokemon), abilityName }), true));
} }
return true; return true;
} }
@ -3464,18 +3421,6 @@ export class MoodyAbAttr extends PostTurnAbAttr {
constructor() { constructor() {
super(true); super(true);
} }
/**
* Randomly increases one BattleStat by 2 stages and decreases a different BattleStat by 1 stage
* @param {Pokemon} pokemon Pokemon that has this ability
* @param passive N/A
* @param simulated true if applying in a simulated call.
* @param args N/A
* @returns true
*
* Any BattleStats at +6 or -6 are excluded from being increased or decreased, respectively
* If the pokemon already has all BattleStats raised to stage 6, it will only decrease one BattleStat by 1 stage
* If the pokemon already has all BattleStats lowered to stage -6, it will only increase one BattleStat by 2 stages
*/
applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
const selectableStats = [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD]; const selectableStats = [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD];
const increaseStatArray = selectableStats.filter(s => pokemon.summonData.battleStats[s] < 6); const increaseStatArray = selectableStats.filter(s => pokemon.summonData.battleStats[s] < 6);
@ -3487,7 +3432,7 @@ export class MoodyAbAttr extends PostTurnAbAttr {
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [increaseStat], 2)); pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [increaseStat], 2));
} }
if (!simulated && decreaseStatArray.length > 0) { if (!simulated && decreaseStatArray.length > 0) {
const decreaseStat = decreaseStatArray[Utils.randInt(decreaseStatArray.length)]; const decreaseStat = selectableStats[Utils.randInt(selectableStats.length)];
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [decreaseStat], -1)); pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [decreaseStat], -1));
} }
return true; return true;
@ -3522,7 +3467,7 @@ export class PostTurnHealAbAttr extends PostTurnAbAttr {
const scene = pokemon.scene; const scene = pokemon.scene;
const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name; const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name;
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(), scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(),
Utils.toDmgValue(pokemon.getMaxHp() / 16), i18next.t("abilityTriggers:postTurnHeal", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }), true)); Math.max(Math.floor(pokemon.getMaxHp() / 16), 1), i18next.t("abilityTriggers:postTurnHeal", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }), true));
} }
return true; return true;
@ -3574,7 +3519,7 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr {
for (const opp of pokemon.getOpponents()) { for (const opp of pokemon.getOpponents()) {
if ((opp.status?.effect === StatusEffect.SLEEP || opp.hasAbility(Abilities.COMATOSE)) && !opp.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) { if ((opp.status?.effect === StatusEffect.SLEEP || opp.hasAbility(Abilities.COMATOSE)) && !opp.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) {
if (!simulated) { if (!simulated) {
opp.damageAndUpdate(Utils.toDmgValue(opp.getMaxHp() / 8), HitResult.OTHER); opp.damageAndUpdate(Math.floor(Math.max(1, opp.getMaxHp() / 8)), HitResult.OTHER);
pokemon.scene.queueMessage(i18next.t("abilityTriggers:badDreams", {pokemonName: getPokemonNameWithAffix(opp)})); pokemon.scene.queueMessage(i18next.t("abilityTriggers:badDreams", {pokemonName: getPokemonNameWithAffix(opp)}));
} }
hadEffect = true; hadEffect = true;
@ -3776,7 +3721,7 @@ export class ReduceBurnDamageAbAttr extends AbAttr {
* @returns `true` * @returns `true`
*/ */
apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
(args[0] as Utils.NumberHolder).value = Utils.toDmgValue((args[0] as Utils.NumberHolder).value * this.multiplier); (args[0] as Utils.NumberHolder).value = Math.max(Math.floor((args[0] as Utils.NumberHolder).value * this.multiplier), 1);
return true; return true;
} }
@ -3821,7 +3766,7 @@ export class HealFromBerryUseAbAttr extends AbAttr {
new PokemonHealPhase( new PokemonHealPhase(
pokemon.scene, pokemon.scene,
pokemon.getBattlerIndex(), pokemon.getBattlerIndex(),
Utils.toDmgValue(pokemon.getMaxHp() * this.healPercent), Math.max(Math.floor(pokemon.getMaxHp() * this.healPercent), 1),
i18next.t("abilityTriggers:healFromBerryUse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }), i18next.t("abilityTriggers:healFromBerryUse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }),
true true
) )
@ -4044,8 +3989,8 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr {
return false; return false;
} }
if (!simulated) { if (!simulated) {
attacker.damageAndUpdate(Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER); attacker.damageAndUpdate(Math.ceil(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER);
attacker.turnData.damageTaken += Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)); attacker.turnData.damageTaken += Math.ceil(attacker.getMaxHp() * (1 / this.damageRatio));
} }
return true; return true;
} }
@ -4433,7 +4378,7 @@ export class FormBlockDamageAbAttr extends ReceivedMoveDamageMultiplierAbAttr {
(args[0] as Utils.NumberHolder).value = this.multiplier; (args[0] as Utils.NumberHolder).value = this.multiplier;
pokemon.removeTag(this.tagType); pokemon.removeTag(this.tagType);
if (this.recoilDamageFunc) { if (this.recoilDamageFunc) {
pokemon.damageAndUpdate(this.recoilDamageFunc(pokemon), HitResult.OTHER, false, false, true, true); pokemon.damageAndUpdate(this.recoilDamageFunc(pokemon), HitResult.OTHER);
} }
} }
return true; return true;
@ -4546,7 +4491,7 @@ async function applyAbAttrsInternal<TAttr extends AbAttr>(
applyFunc: AbAttrApplyFunc<TAttr>, applyFunc: AbAttrApplyFunc<TAttr>,
args: any[], args: any[],
showAbilityInstant: boolean = false, showAbilityInstant: boolean = false,
simulated: boolean = false, quiet: boolean = false,
messages: string[] = [], messages: string[] = [],
) { ) {
for (const passive of [false, true]) { for (const passive of [false, true]) {
@ -4572,20 +4517,23 @@ async function applyAbAttrsInternal<TAttr extends AbAttr>(
if (pokemon.summonData && !pokemon.summonData.abilitiesApplied.includes(ability.id)) { if (pokemon.summonData && !pokemon.summonData.abilitiesApplied.includes(ability.id)) {
pokemon.summonData.abilitiesApplied.push(ability.id); pokemon.summonData.abilitiesApplied.push(ability.id);
} }
if (pokemon.battleData && !simulated && !pokemon.battleData.abilitiesApplied.includes(ability.id)) { if (pokemon.battleData && !quiet && !pokemon.battleData.abilitiesApplied.includes(ability.id)) {
pokemon.battleData.abilitiesApplied.push(ability.id); pokemon.battleData.abilitiesApplied.push(ability.id);
} }
if (attr.showAbility && !simulated) {
if (attr.showAbility && !quiet) {
if (showAbilityInstant) { if (showAbilityInstant) {
pokemon.scene.abilityBar.showAbility(pokemon, passive); pokemon.scene.abilityBar.showAbility(pokemon, passive);
} else { } else {
queueShowAbility(pokemon, passive); queueShowAbility(pokemon, passive);
} }
} }
if (!quiet) {
const message = attr.getTriggerMessage(pokemon, ability.name, args); const message = attr.getTriggerMessage(pokemon, ability.name, args);
if (message) { if (message) {
if (!simulated) {
pokemon.scene.queueMessage(message); pokemon.scene.queueMessage(message);
messages.push(message);
} }
} }
messages.push(message!); messages.push(message!);

View File

@ -70,7 +70,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
if (pokemon.battleData) { if (pokemon.battleData) {
pokemon.battleData.berriesEaten.push(berryType); pokemon.battleData.berriesEaten.push(berryType);
} }
const hpHealed = new Utils.NumberHolder(Utils.toDmgValue(pokemon.getMaxHp() / 4)); const hpHealed = new Utils.NumberHolder(Math.floor(pokemon.getMaxHp() / 4));
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, hpHealed); applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, hpHealed);
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(),
hpHealed.value, i18next.t("battle:hpHealBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), berryName: getBerryName(berryType) }), true)); hpHealed.value, i18next.t("battle:hpHealBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), berryName: getBerryName(berryType) }), true));

View File

@ -743,7 +743,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(MoveTypeChangeAbAttr, source, target, this, true, null, typeChangeMovePowerMultiplier); applyPreAttackAbAttrs(MoveTypeChangeAttr, source, target, this, simulated, 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)) {
@ -1955,7 +1955,7 @@ export class StatusEffectAttr extends MoveEffectAttr {
} }
if ((!pokemon.status || (pokemon.status.effect === this.effect && moveChance < 0)) if ((!pokemon.status || (pokemon.status.effect === this.effect && moveChance < 0))
&& pokemon.checkIfCanSetStatus(this.effect, false, false, user) && pokemon.trySetStatus(this.effect, true, user, this.cureTurn)) { && pokemon.trySetStatus(this.effect, true, user, this.cureTurn)) {
applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect); applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect);
return true; return true;
} }

View File

@ -1261,19 +1261,17 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* Currently only used by {@linkcode Pokemon.apply} to determine whether a "No effect" message should be shown. * 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
*/ */
getMoveEffectiveness(source: Pokemon, move: Move, ignoreAbility: boolean = false, simulated: boolean = true, cancelled?: Utils.BooleanHolder): TypeDamageMultiplier { getAttackMoveEffectiveness(source: Pokemon, pokemonMove: PokemonMove, ignoreAbility: boolean = false): TypeDamageMultiplier {
if (move.hasAttr(TypelessAttr)) { const move = pokemonMove.getMove();
return 1; const typeless = move.hasAttr(TypelessAttr);
} const typeMultiplier = new Utils.NumberHolder(this.getAttackTypeEffectiveness(move, source));
const moveType = source.getMoveType(move); const cancelled = new Utils.BooleanHolder(false);
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); applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier);
if (this.getTypes().find(t => move.isTypeImmune(source, this, t))) { if (!typeless && !ignoreAbility) {
typeMultiplier.value = 0; applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelled, true, typeMultiplier);
}
if (!cancelled.value && !ignoreAbility) {
applyPreDefendAbAttrs(MoveImmunityAbAttr, this, source, move, cancelled, true, typeMultiplier);
} }
const cancelledHolder = cancelled ?? new Utils.BooleanHolder(false); const cancelledHolder = cancelled ?? new Utils.BooleanHolder(false);
@ -1334,7 +1332,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, simulated, moveType, defType); applyAbAttrs(IgnoreTypeImmunityAbAttr, source, ignoreImmunity, false, moveType, defType);
} }
if (ignoreImmunity.value) { if (ignoreImmunity.value) {
return 1; return 1;
@ -2030,7 +2028,14 @@ 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();
const power = move.calculateBattlePower(source, this); 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));
}
if (cancelled.value) { if (cancelled.value) {
// Cancelled moves fail silently // Cancelled moves fail silently
@ -2308,7 +2313,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
break; break;
case MoveCategory.STATUS: case MoveCategory.STATUS:
if (!cancelled.value && typeMultiplier === 0) { 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) {
this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) })); this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) }));
} }
result = (typeMultiplier === 0) ? HitResult.NO_EFFECT : HitResult.STATUS; result = (typeMultiplier === 0) ? HitResult.NO_EFFECT : HitResult.STATUS;

View File

@ -182,7 +182,10 @@ export class CommandPhase extends FieldPhase {
} else { } else {
const batonPass = isSwitch && args[0] as boolean; const batonPass = isSwitch && args[0] as boolean;
const trappedAbMessages: string[] = []; const trappedAbMessages: string[] = [];
if (batonPass || !playerPokemon.isTrapped(trappedAbMessages)) { if (!batonPass) {
enemyField.forEach(enemyPokemon => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, enemyPokemon, trapped, playerPokemon, trappedAbMessages, true));
}
if (batonPass || (!trapTag && !trapped.value)) {
this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch
? { command: Command.POKEMON, cursor: cursor, args: args } ? { command: Command.POKEMON, cursor: cursor, args: args }
: { command: Command.RUN }; : { command: Command.RUN };

View File

@ -42,7 +42,10 @@ export class EnemyCommandPhase extends FieldPhase {
if (trainer && !enemyPokemon.getMoveQueue().length) { if (trainer && !enemyPokemon.getMoveQueue().length) {
const opponents = enemyPokemon.getOpponents(); const opponents = enemyPokemon.getOpponents();
if (!enemyPokemon.isTrapped()) { const trapTag = enemyPokemon.findTag(t => t instanceof TrappedTag) as TrappedTag;
const trapped = new Utils.BooleanHolder(false);
opponents.forEach(playerPokemon => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, playerPokemon, trapped, enemyPokemon, [], true));
if (!trapTag && !trapped.value) {
const partyMemberScores = trainer.getPartyMemberMatchupScores(enemyPokemon.trainerSlot, true); const partyMemberScores = trainer.getPartyMemberMatchupScores(enemyPokemon.trainerSlot, true);
if (partyMemberScores.length) { if (partyMemberScores.length) {

View File

@ -107,8 +107,8 @@ export class TurnStartPhase extends FieldPhase {
applyMoveAttrs(IncrementMovePriorityAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a)!, null, aMove, aPriority); applyMoveAttrs(IncrementMovePriorityAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a)!, null, aMove, aPriority);
applyMoveAttrs(IncrementMovePriorityAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b)!, null, bMove, bPriority); applyMoveAttrs(IncrementMovePriorityAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b)!, null, bMove, bPriority);
applyAbAttrs(ChangeMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a)!, null, false, aMove, aPriority); applyAbAttrs(ChangeMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a)!, null, false, aMove, aPriority); //TODO: is the bang correct here?
applyAbAttrs(ChangeMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b)!, null, false, bMove, bPriority); applyAbAttrs(ChangeMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b)!, null, false, bMove, bPriority); //TODO: is the bang correct here?
// The game now checks for differences in priority levels. // The game now checks for differences in priority levels.
// If the moves share the same original priority bracket, it can check for differences in battlerBypassSpeed and return the result. // If the moves share the same original priority bracket, it can check for differences in battlerBypassSpeed and return the result.