From 06c3611d06afd9b37a2d4341c3732ea183eb8c46 Mon Sep 17 00:00:00 2001 From: Brandon Rodgers Date: Tue, 14 May 2024 14:00:37 -0400 Subject: [PATCH 1/2] Ability Corrosion (#744) * Ability Corrosion Implemented Corrosion Partially. Tested against: - Poison Powder - Toxic - Dire Claw - Sludge Bomb - Psycho Shift They all work as expected Missing ability Magic Bounce to test against. * Added TSDoc Documentation Added documentation to the new IgnoreTypeStatusEffectImunnityAbAttr and added comments to the checks for this ability attribute. * Added More Documentation Add comment into Phases for what sourcePokemon is for. Renamed source to sourcePokemon onto trySetStatus and canSetStatus. Added TSDoc head for what sourcePokemon is and anything else I am aware of what they are used for. * Removed unfinished TSDoc Removed TSDoc headers due to not having enough understanding to fill out all of the parameters * Fix Formatting and Reorder Parameters * Update arena-tag.ts * Update phases.ts * Update ability.ts Added access modifiers to my class and the class I compared to. --------- Co-authored-by: Benjamin Odom --- src/data/ability.ts | 37 +++++++++++++++++++++++++++++++------ src/data/arena-tag.ts | 2 +- src/data/battler-tags.ts | 2 +- src/data/move.ts | 12 ++++++------ src/field/pokemon.ts | 35 ++++++++++++++++++++++++++++------- src/phases.ts | 6 ++++-- 6 files changed, 71 insertions(+), 23 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 22867ee13..0ff03ed67 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -715,7 +715,7 @@ export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr { applyPostDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: PokemonMove, hitResult: HitResult, args: any[]): boolean { if (move.getMove().checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.status && (this.chance === -1 || pokemon.randSeedInt(100) < this.chance)) { const effect = this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randSeedInt(this.effects.length)]; - return attacker.trySetStatus(effect, true); + return attacker.trySetStatus(effect, true, pokemon); } return false; @@ -1177,7 +1177,7 @@ export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr { applyPostAttack(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: PokemonMove, hitResult: HitResult, args: any[]): boolean { if (pokemon != attacker && (!this.contactRequired || move.getMove().checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) && pokemon.randSeedInt(100) < this.chance && !pokemon.status) { const effect = this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randSeedInt(this.effects.length)]; - return attacker.trySetStatus(effect, true); + return attacker.trySetStatus(effect, true, pokemon); } return false; @@ -2648,8 +2648,8 @@ export class NoFusionAbilityAbAttr extends AbAttr { } export class IgnoreTypeImmunityAbAttr extends AbAttr { - defenderType: Type; - allowedMoveTypes: Type[]; + private defenderType: Type; + private allowedMoveTypes: Type[]; constructor(defenderType: Type, allowedMoveTypes: Type[]) { super(true); @@ -2666,6 +2666,30 @@ export class IgnoreTypeImmunityAbAttr extends AbAttr { } } +/** + * Ignores the type immunity to Status Effects of the defender if the defender is of a certain type + */ +export class IgnoreTypeStatusEffectImmunityAbAttr extends AbAttr { + private statusEffect: StatusEffect[]; + private defenderType: Type[]; + + constructor(statusEffect: StatusEffect[], defenderType: Type[]) { + super(true); + + this.statusEffect = statusEffect; + this.defenderType = defenderType; + } + + apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { + if (this.statusEffect.includes(args[0] as StatusEffect) && this.defenderType.includes(args[1] as Type)) { + cancelled.value = true; + return true; + } + + return false; + } +} + function applyAbAttrsInternal(attrType: { new(...args: any[]): TAttr }, pokemon: Pokemon, applyFunc: AbAttrApplyFunc, args: any[], isAsync: boolean = false, showAbilityInstant: boolean = false, quiet: boolean = false, passive: boolean = false): Promise { return new Promise(resolve => { @@ -3489,8 +3513,9 @@ export function initAbilities() { .attr(UnsuppressableAbilityAbAttr) .attr(NoFusionAbilityAbAttr) .partial(), - new Ability(Abilities.CORROSION, 7) - .unimplemented(), + new Ability(Abilities.CORROSION, 7) // TODO: Test Corrosion against Magic Bounce once it is implemented + .attr(IgnoreTypeStatusEffectImmunityAbAttr, [StatusEffect.POISON, StatusEffect.TOXIC], [Type.STEEL, Type.POISON]) + .partial(), new Ability(Abilities.COMATOSE, 7) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 8942cfe4b..63ed157b5 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -316,7 +316,7 @@ class ToxicSpikesTag extends ArenaTrapTag { } } else if (!pokemon.status) { const toxic = this.layers > 1; - if (pokemon.trySetStatus(!toxic ? StatusEffect.POISON : StatusEffect.TOXIC, true, null, `the ${this.getMoveName()}`)) + if (pokemon.trySetStatus(!toxic ? StatusEffect.POISON : StatusEffect.TOXIC, true, null, 0, `the ${this.getMoveName()}`)) return true; } } diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index ba00b0906..257f56d46 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -819,7 +819,7 @@ export class ContactPoisonProtectedTag extends ProtectedTag { const effectPhase = pokemon.scene.getCurrentPhase(); if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) { const attacker = effectPhase.getPokemon(); - attacker.trySetStatus(StatusEffect.POISON, true); + attacker.trySetStatus(StatusEffect.POISON, true, pokemon); } } diff --git a/src/data/move.ts b/src/data/move.ts index 6a6bee468..8b2133090 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1156,13 +1156,13 @@ export class StatusEffectAttr extends MoveEffectAttr { return false; } if (!pokemon.status || (pokemon.status.effect === this.effect && move.chance < 0)) - return pokemon.trySetStatus(this.effect, true, this.cureTurn); + return pokemon.trySetStatus(this.effect, true, user, this.cureTurn); } return false; } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { - return !(this.selfTarget ? user : target).status && (this.selfTarget ? user : target).canSetStatus(this.effect, true) ? Math.floor(move.chance * -0.1) : 0; + return !(this.selfTarget ? user : target).status && (this.selfTarget ? user : target).canSetStatus(this.effect, true, false, user) ? Math.floor(move.chance * -0.1) : 0; } } @@ -1181,7 +1181,7 @@ export class MultiStatusEffectAttr extends StatusEffectAttr { } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { - return !(this.selfTarget ? user : target).status && (this.selfTarget ? user : target).canSetStatus(this.effect, true) ? Math.floor(move.chance * -0.1) : 0; + return !(this.selfTarget ? user : target).status && (this.selfTarget ? user : target).canSetStatus(this.effect, true, false, user) ? Math.floor(move.chance * -0.1) : 0; } } @@ -1197,7 +1197,7 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr { return false; } if (!target.status || (target.status.effect === statusToApply && move.chance < 0)) { - var statusAfflictResult = target.trySetStatus(statusToApply, true); + var statusAfflictResult = target.trySetStatus(statusToApply, true, user); if (statusAfflictResult) { user.scene.queueMessage(getPokemonMessage(user, getStatusEffectHealText(user.status.effect))); user.resetStatus(); @@ -1210,7 +1210,7 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr { } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { - return !(this.selfTarget ? user : target).status && (this.selfTarget ? user : target).canSetStatus(user.status?.effect, true) ? Math.floor(move.chance * -0.1) : 0; + return !(this.selfTarget ? user : target).status && (this.selfTarget ? user : target).canSetStatus(user.status?.effect, true, false, user) ? Math.floor(move.chance * -0.1) : 0; } } @@ -5226,7 +5226,7 @@ export function initMoves() { || user.status?.effect === StatusEffect.TOXIC || user.status?.effect === StatusEffect.PARALYSIS || user.status?.effect === StatusEffect.SLEEP) - && target.canSetStatus(user.status?.effect) + && target.canSetStatus(user.status?.effect, false, false, user) ), new AttackMove(Moves.TRUMP_CARD, Type.NORMAL, MoveCategory.SPECIAL, -1, -1, 5, -1, 0, 4) .makesContact() diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index fd1613df1..573fc907b 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -27,7 +27,7 @@ import { TempBattleStat } from '../data/temp-battle-stat'; import { ArenaTagSide, WeakenMoveScreenTag, WeakenMoveTypeTag } from '../data/arena-tag'; import { ArenaTagType } from "../data/enums/arena-tag-type"; import { Biome } from "../data/enums/biome"; -import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, FieldVariableMovePowerAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MoveTypeChangeAttr, NonSuperEffectiveImmunityAbAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, VariableMoveTypeAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPostDefendAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr } from '../data/ability'; +import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, FieldVariableMovePowerAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MoveTypeChangeAttr, NonSuperEffectiveImmunityAbAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, VariableMoveTypeAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPostDefendAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr } from '../data/ability'; import { Abilities } from "#app/data/enums/abilities"; import PokemonData from '../system/pokemon-data'; import Battle, { BattlerIndex } from '../battle'; @@ -2024,7 +2024,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.gender !== Gender.GENDERLESS && pokemon.gender === (this.gender === Gender.MALE ? Gender.FEMALE : Gender.MALE); } - canSetStatus(effect: StatusEffect, quiet: boolean = false, overrideStatus: boolean = false): boolean { + canSetStatus(effect: StatusEffect, quiet: boolean = false, overrideStatus: boolean = false, sourcePokemon: Pokemon = null): boolean { if (effect !== StatusEffect.FAINT) { if (overrideStatus ? this.status?.effect === effect : this.status) return false; @@ -2032,11 +2032,32 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return false; } + const types = this.getTypes(true, true); + switch (effect) { case StatusEffect.POISON: case StatusEffect.TOXIC: - if (this.isOfType(Type.POISON) || this.isOfType(Type.STEEL)) - return false; + // Check if the Pokemon is immune to Poison/Toxic or if the source pokemon is canceling the immunity + let poisonImmunity = types.map(defType => { + // Check if the Pokemon is not immune to Poison/Toxic + if (defType !== Type.POISON && defType !== Type.STEEL) + return false; + + // Check if the source Pokemon has an ability that cancels the Poison/Toxic immunity + const cancelImmunity = new Utils.BooleanHolder(false); + if (sourcePokemon) { + applyAbAttrs(IgnoreTypeStatusEffectImmunityAbAttr, sourcePokemon, cancelImmunity, effect, defType); + if (cancelImmunity.value) + return false; + } + + return true; + }) + + if (this.isOfType(Type.POISON) || this.isOfType(Type.STEEL)) { + if (poisonImmunity.includes(true)) + return false; + } break; case StatusEffect.PARALYSIS: if (this.isOfType(Type.ELECTRIC)) @@ -2065,12 +2086,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return true; } - trySetStatus(effect: StatusEffect, asPhase: boolean = false, cureTurn: integer = 0, sourceText: string = null): boolean { - if (!this.canSetStatus(effect, asPhase)) + trySetStatus(effect: StatusEffect, asPhase: boolean = false, sourcePokemon: Pokemon = null, cureTurn: integer = 0, sourceText: string = null): boolean { + if (!this.canSetStatus(effect, asPhase, false, sourcePokemon)) return false; if (asPhase) { - this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, cureTurn, sourceText)); + this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, cureTurn, sourceText, sourcePokemon)); return true; } diff --git a/src/phases.ts b/src/phases.ts index 271e5350c..8aa179d35 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -2931,19 +2931,21 @@ export class ObtainStatusEffectPhase extends PokemonPhase { private statusEffect: StatusEffect; private cureTurn: integer; private sourceText: string; + private sourcePokemon: Pokemon; - constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect: StatusEffect, cureTurn?: integer, sourceText?: string) { + constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect: StatusEffect, cureTurn?: integer, sourceText?: string, sourcePokemon?: Pokemon) { super(scene, battlerIndex); this.statusEffect = statusEffect; this.cureTurn = cureTurn; this.sourceText = sourceText; + this.sourcePokemon = sourcePokemon; // For tracking which Pokemon caused the status effect } start() { const pokemon = this.getPokemon(); if (!pokemon.status) { - if (pokemon.trySetStatus(this.statusEffect)) { + if (pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) { if (this.cureTurn) pokemon.status.cureTurn = this.cureTurn; pokemon.updateInfo(true); From 7d3cf577a635419bc254a752bd728abb3a4419c9 Mon Sep 17 00:00:00 2001 From: Ethan <71776311+EvasiveAce@users.noreply.github.com> Date: Tue, 14 May 2024 14:01:37 -0400 Subject: [PATCH 2/2] Implented Gen VII Sheer Cold Changes (#349) * Made Sheer Cold not affect Ice Types, as well as implementing the Gen VII change of 20% for non ice types. * Pushed accurancy change * Updated and separated the accuracy attribute and the Ice no effect attribute * Fixed the OHKO attribute (accidentally removed) and fixed multiplier * Updated attribute names, as well as making the move cancelled instead of 0x multiplier * Added TSDoc comments * Updated accuracy logic * Changed the text response for Sheer Cold immunity * Added immune to the HitResult enum --- src/data/move.ts | 46 ++++++++++++++++++++++++++++++++++++++++++-- src/field/pokemon.ts | 8 ++++++-- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 8b2133090..3c99ad8c1 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -2676,6 +2676,24 @@ export class WaterSuperEffectTypeMultiplierAttr extends VariableMoveTypeMultipli } } +export class IceNoEffectTypeAttr extends VariableMoveTypeMultiplierAttr { + /** + * Checks to see if the Target is Ice-Type or not. If so, the move will have no effect. + * @param {Pokemon} user N/A + * @param {Pokemon} target Pokemon that is being checked whether Ice-Type or not. + * @param {Move} move N/A + * @param {any[]} args Sets to false if the target is Ice-Type, so it should do no damage/no effect. + * @returns {boolean} Returns true if move is successful, false if Ice-Type. + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + if (target.isOfType(Type.ICE)) { + (args[0] as Utils.BooleanHolder).value = false; + return false; + } + return true; + } +} + export class FlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const multiplier = args[0] as Utils.NumberHolder; @@ -2695,6 +2713,29 @@ export class OneHitKOAccuracyAttr extends VariableAccuracyAttr { } } +export class SheerColdAccuracyAttr extends OneHitKOAccuracyAttr { + /** + * Changes the normal One Hit KO Accuracy Attr to implement the Gen VII changes, + * where if the user is Ice-Type, it has more accuracy. + * @param {Pokemon} user Pokemon that is using the move; checks the Pokemon's level. + * @param {Pokemon} target Pokemon that is receiving the move; checks the Pokemon's level. + * @param {Move} move N/A + * @param {any[]} args Uses the accuracy argument, allowing to change it from either 0 if it doesn't pass + * the first if/else, or 30/20 depending on the type of the user Pokemon. + * @returns Returns true if move is successful, false if misses. + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + const accuracy = args[0] as Utils.NumberHolder; + if (user.level < target.level) { + accuracy.value = 0; + } else { + const baseAccuracy = user.isOfType(Type.ICE) ? 30 : 20; + accuracy.value = Math.min(Math.max(baseAccuracy + 100 * (1 - target.level / user.level), 0), 100); + } + return true; + } +} + export class MissEffectAttr extends MoveAttr { private missEffectFunc: UserMoveConditionFunc; @@ -5095,9 +5136,10 @@ export function initMoves() { new AttackMove(Moves.SAND_TOMB, Type.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, 100, 0, 3) .attr(TrapAttr, BattlerTagType.SAND_TOMB) .makesContact(false), - new AttackMove(Moves.SHEER_COLD, Type.ICE, MoveCategory.SPECIAL, 200, 30, 5, -1, 0, 3) + new AttackMove(Moves.SHEER_COLD, Type.ICE, MoveCategory.SPECIAL, 200, 20, 5, -1, 0, 3) + .attr(IceNoEffectTypeAttr) .attr(OneHitKOAttr) - .attr(OneHitKOAccuracyAttr), + .attr(SheerColdAccuracyAttr), new AttackMove(Moves.MUDDY_WATER, Type.WATER, MoveCategory.SPECIAL, 90, 85, 10, 30, 0, 3) .attr(StatChangeAttr, BattleStat.ACC, -1) .target(MoveTarget.ALL_NEAR_ENEMIES), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 573fc907b..97d94bdba 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1563,7 +1563,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (!result) { if (!typeMultiplier.value) - result = HitResult.NO_EFFECT; + result = move.id == Moves.SHEER_COLD ? HitResult.IMMUNE : HitResult.NO_EFFECT; else { const oneHitKo = new Utils.BooleanHolder(false); applyMoveAttrs(OneHitKOAttr, source, this, move, oneHitKo); @@ -1631,6 +1631,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { case HitResult.NO_EFFECT: this.scene.queueMessage(i18next.t('battle:hitResultNoEffect', { pokemonName: this.name })); break; + case HitResult.IMMUNE: + this.scene.queueMessage(`${this.name} is unaffected!`); + break; case HitResult.ONE_HIT_KO: this.scene.queueMessage(i18next.t('battle:hitResultOneHitKO')); break; @@ -3360,7 +3363,8 @@ export enum HitResult { HEAL, FAIL, MISS, - OTHER + OTHER, + IMMUNE } export type DamageResult = HitResult.EFFECTIVE | HitResult.SUPER_EFFECTIVE | HitResult.NOT_VERY_EFFECTIVE | HitResult.ONE_HIT_KO | HitResult.OTHER;