diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 82ee785896c..b8c8d1d5a1f 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -2699,15 +2699,17 @@ export default class BattleScene extends SceneBase { * @param itemLost If `true`, treat the item's current holder as losing the item (for now, this simply enables Unburden). Default is `true`. * @returns `true` if the transfer was successful */ - tryTransferHeldItemModifier(itemModifier: PokemonHeldItemModifier, target: Pokemon, playSound: boolean, transferQuantity: number = 1, instant?: boolean, ignoreUpdate?: boolean, itemLost: boolean = true): Promise { + tryTransferHeldItemModifier(_itemModifier: PokemonHeldItemModifier, target: Pokemon, playSound: boolean, transferQuantity: number = 1, instant?: boolean, ignoreUpdate?: boolean, itemLost: boolean = true, simulated: boolean = false): Promise { return new Promise(resolve => { - const source = itemModifier.pokemonId ? itemModifier.getPokemon() : null; + const source = _itemModifier.pokemonId ? _itemModifier.getPokemon() : null; const cancelled = new Utils.BooleanHolder(false); + const removeFunc: (modifier: PersistentModifier, enemy: boolean | undefined) => boolean = simulated ? (modifier, enemy) => this.canRemoveModifier(modifier, enemy) : (modifier, enemy) => this.removeModifier(modifier, enemy); Utils.executeIf(!!source && source.isPlayer() !== target.isPlayer(), () => applyAbAttrs(BlockItemTheftAbAttr, source! /* checked in condition*/, cancelled)).then(() => { if (cancelled.value) { return resolve(false); } - const newItemModifier = itemModifier.clone() as PokemonHeldItemModifier; + const newItemModifier = _itemModifier.clone() as PokemonHeldItemModifier; + const itemModifier = simulated ? _itemModifier.clone() : _itemModifier; newItemModifier.pokemonId = target.id; const matchingModifier = this.findModifier(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).matchType(itemModifier) && m.pokemonId === target.id, target.isPlayer()) as PokemonHeldItemModifier; @@ -2727,9 +2729,12 @@ export default class BattleScene extends SceneBase { newItemModifier.stackCount = countTaken; } removeOld = !itemModifier.stackCount; - if (!removeOld || !source || this.removeModifier(itemModifier, !source.isPlayer())) { + if (!removeOld || !source || removeFunc(simulated ? _itemModifier : itemModifier, !source.isPlayer())) { const addModifier = () => { - if (!matchingModifier || this.removeModifier(matchingModifier, !target.isPlayer())) { + if (!matchingModifier || removeFunc(matchingModifier, !target.isPlayer())) { + if (simulated) { + return resolve(true); + } if (target.isPlayer()) { this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant).then(() => { if (source && itemLost) { @@ -2749,7 +2754,7 @@ export default class BattleScene extends SceneBase { resolve(false); } }; - if (source && source.isPlayer() !== target.isPlayer() && !ignoreUpdate) { + if (!simulated && source && source.isPlayer() !== target.isPlayer() && !ignoreUpdate) { this.updateModifiers(source.isPlayer(), instant).then(() => addModifier()); } else { addModifier(); @@ -2909,6 +2914,11 @@ export default class BattleScene extends SceneBase { }); } + canRemoveModifier(modifier: PersistentModifier, enemy: boolean = false): boolean { + const modifiers = !enemy ? this.modifiers : this.enemyModifiers; + return modifiers.indexOf(modifier) > -1; + } + /** * Removes a currently owned item. If the item is stacked, the entire item stack * gets removed. This function does NOT apply in-battle effects, such as Unburden. diff --git a/src/data/ability.ts b/src/data/ability.ts index 979552fbd31..19779348582 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -591,7 +591,7 @@ export class FullHpResistTypeAbAttr extends PreDefendAbAttr { } export class PostDefendAbAttr extends AbAttr { - canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean { + canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean | Promise { return true; } @@ -869,7 +869,7 @@ export class PostDefendTerrainChangeAbAttr extends PostDefendAbAttr { } override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - return hitResult < HitResult.NO_EFFECT && !move.hitsSubstitute(attacker, pokemon); + return hitResult < HitResult.NO_EFFECT && !move.hitsSubstitute(attacker, pokemon) && globalScene.arena.canSetTerrain(this.terrainType); } override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, _args: any[]): boolean { @@ -1048,7 +1048,8 @@ export class PostDefendWeatherChangeAbAttr extends PostDefendAbAttr { } override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean { - return (!(this.condition && !this.condition(pokemon, attacker, move) || move.hitsSubstitute(attacker, pokemon)) && !globalScene.arena.weather?.isImmutable()); + return (!(this.condition && !this.condition(pokemon, attacker, move) || move.hitsSubstitute(attacker, pokemon)) + && !globalScene.arena.weather?.isImmutable() && globalScene.arena.canSetWeather(this.weatherType)); } override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean { @@ -1619,7 +1620,7 @@ export class PostAttackAbAttr extends AbAttr { * applying the effect of any inherited class. This can be changed by providing a different {@link attackCondition} to the constructor. See {@link ConfusionOnStatusEffectAbAttr} * for an example of an effect that does not require a damaging move. */ - canApplyPostAttack(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean { + canApplyPostAttack(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean | Promise { // When attackRequired is true, we require the move to be an attack move and to deal damage before checking secondary requirements. // If attackRequired is false, we always defer to the secondary requirements. return this.attackCondition(pokemon, defender, move); @@ -1674,33 +1675,34 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { this.stealCondition = stealCondition ?? null; } - canApplyPostAttack(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean { - return super.canApplyPostAttack(pokemon, passive, simulated, defender, move, hitResult, args); // && SUCCESS CHECK + canApplyPostAttack(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, hitResult: HitResult, args: any[]): Promise { + return new Promise(resolve => { + if (!super.canApplyPostAttack(pokemon, passive, simulated, defender, move, hitResult, args)) { + return resolve(false); + } + + canStealHeldItem(pokemon, passive, simulated, defender, move, hitResult, this.stealCondition, args).then(success => { + resolve(success); + }); + }); } applyPostAttack(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, hitResult: HitResult, args: any[]): Promise { return new Promise(resolve => { - if (!simulated && hitResult < HitResult.NO_EFFECT && (!this.stealCondition || this.stealCondition(pokemon, defender, move))) { - const heldItems = this.getTargetHeldItems(defender).filter(i => i.isTransferable); - if (heldItems.length) { - const stolenItem = heldItems[pokemon.randSeedInt(heldItems.length)]; - globalScene.tryTransferHeldItemModifier(stolenItem, pokemon, false).then(success => { - if (success) { - globalScene.queueMessage(i18next.t("abilityTriggers:postAttackStealHeldItem", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), defenderName: defender.name, stolenItemType: stolenItem.type.name })); - } - resolve(success); - }); - return; - } + const heldItems = getTargetHeldItems(defender).filter(i => i.isTransferable); + if (heldItems.length) { + const stolenItem = heldItems[pokemon.randSeedInt(heldItems.length)]; + globalScene.tryTransferHeldItemModifier(stolenItem, pokemon, false).then(success => { + if (success) { + globalScene.queueMessage(i18next.t("abilityTriggers:postAttackStealHeldItem", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), defenderName: defender.name, stolenItemType: stolenItem.type.name })); + } + resolve(success); + }); + return; } resolve(simulated); }); } - - getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] { - return globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.pokemonId === target.id, target.isPlayer()) as PokemonHeldItemModifier[]; - } } export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr { @@ -1776,30 +1778,33 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { this.condition = condition; } - // SUCCESS CHECK - - override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, _args: any[]): Promise { + override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): Promise { return new Promise(resolve => { - if (!simulated && hitResult < HitResult.NO_EFFECT && (!this.condition || this.condition(pokemon, attacker, move)) && !move.hitsSubstitute(attacker, pokemon)) { - const heldItems = this.getTargetHeldItems(attacker).filter(i => i.isTransferable); - if (heldItems.length) { - const stolenItem = heldItems[pokemon.randSeedInt(heldItems.length)]; - globalScene.tryTransferHeldItemModifier(stolenItem, pokemon, false).then(success => { - if (success) { - globalScene.queueMessage(i18next.t("abilityTriggers:postDefendStealHeldItem", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), attackerName: attacker.name, stolenItemType: stolenItem.type.name })); - } - resolve(success); - }); - return; - } + if (move.hitsSubstitute(attacker, pokemon)) { + return resolve(false); } - resolve(simulated); + + canStealHeldItem(pokemon, passive, simulated, attacker, move, hitResult, this.condition, args).then(success => { + resolve(success); + }); }); } - getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] { - return globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.pokemonId === target.id, target.isPlayer()) as PokemonHeldItemModifier[]; + override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, _args: any[]): Promise { + return new Promise(resolve => { + const heldItems = getTargetHeldItems(attacker).filter(i => i.isTransferable); + if (heldItems.length) { + const stolenItem = heldItems[pokemon.randSeedInt(heldItems.length)]; + globalScene.tryTransferHeldItemModifier(stolenItem, pokemon, false).then(success => { + if (success) { + globalScene.queueMessage(i18next.t("abilityTriggers:postDefendStealHeldItem", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), attackerName: attacker.name, stolenItemType: stolenItem.type.name })); + } + resolve(success); + }); + return; + } + resolve(simulated); + }); } } @@ -1841,7 +1846,8 @@ export class SynchronizeStatusAbAttr extends PostSetStatusAbAttr { StatusEffect.TOXIC ]); - return (sourcePokemon && syncStatuses.has(effect)) ?? false; + // synchronize does not need to check canSetStatus because the ability shows even if it fails to set the status + return ((sourcePokemon ?? false) && syncStatuses.has(effect)); } /** @@ -2307,7 +2313,7 @@ export class PostSummonWeatherChangeAbAttr extends PostSummonAbAttr { const weatherReplaceable = (this.weatherType === WeatherType.HEAVY_RAIN || this.weatherType === WeatherType.HARSH_SUN || this.weatherType === WeatherType.STRONG_WINDS) || !globalScene.arena.weather?.isImmutable(); - return weatherReplaceable; + return weatherReplaceable && globalScene.arena.canSetWeather(this.weatherType); } applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { @@ -2328,6 +2334,10 @@ export class PostSummonTerrainChangeAbAttr extends PostSummonAbAttr { this.terrainType = terrainType; } + canApplyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { + return globalScene.arena.canSetTerrain(this.terrainType); + } + applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { if (simulated) { return globalScene.arena.terrain?.terrainType !== this.terrainType; @@ -3889,7 +3899,7 @@ export class PostBiomeChangeWeatherChangeAbAttr extends PostBiomeChangeAbAttr { } canApply(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { - return (globalScene.arena.weather?.isImmutable() && globalScene.arena.weather?.weatherType !== this.weatherType) ?? false; + return ((globalScene.arena.weather?.isImmutable() ?? false) && globalScene.arena.canSetWeather(this.weatherType)); } apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { @@ -3915,7 +3925,7 @@ export class PostBiomeChangeTerrainChangeAbAttr extends PostBiomeChangeAbAttr { } canApply(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { - return globalScene.arena.terrain?.terrainType !== this.terrainType; + return globalScene.arena.canSetTerrain(this.terrainType); } apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { @@ -5020,10 +5030,17 @@ async function applyAbAttrsInternal( const ability = passive ? pokemon.getPassiveAbility() : pokemon.getAbility(); for (const attr of ability.getAttrs(attrType)) { const condition = attr.getCondition(); - if ((condition && !condition(pokemon)) || successFunc && !successFunc(attr, passive)) { + if ((condition && !condition(pokemon))) { continue; } + let success = successFunc(attr, passive); + if (success instanceof Promise) { + success = await success; + } + if (!success) { + continue; + } globalScene.setPhaseQueueSplice(); if (attr.showAbility && !simulated) { @@ -5514,6 +5531,31 @@ function getPokemonWithWeatherBasedForms() { ); } +/** + * Returns if a target's held item can be stolen + * + * Common to attrs for {@linkcode Abilities.MAGICIAN} and {@linkcode Abilities.PICKPOCKET} + */ +async function canStealHeldItem(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, condition: any, args: any[]): Promise { + if (!simulated && hitResult < HitResult.NO_EFFECT && (!condition || condition(pokemon, attacker, move))) { + const heldItems = getTargetHeldItems(attacker).filter(i => i.isTransferable); + if (heldItems.length) { + return globalScene.tryTransferHeldItemModifier(heldItems[pokemon.randSeedInt(heldItems.length)], pokemon, false, 1, false, false, true, true); + } + } + return simulated; +} + +/** + * Gets the held items of a pokemon + * @param target Target Pokemon + * @returns List of {@linkcode target}'s held items + */ +function getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] { + return globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier + && m.pokemonId === target.id, target.isPlayer()) as PokemonHeldItemModifier[]; +} + export const allAbilities = [ new Ability(Abilities.NONE, 3) ]; export function initAbilities() { diff --git a/src/field/arena.ts b/src/field/arena.ts index acdf6171474..a46cce8c127 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -249,6 +249,11 @@ export class Arena { return true; } + /** Returns weather or not the weather can be changed to {@linkcode weather} */ + canSetWeather(weather: WeatherType): boolean { + return !(this.weather?.weatherType === (weather || undefined)); + } + /** * Attempts to set a new weather to the battle * @param weather {@linkcode WeatherType} new {@linkcode WeatherType} to set @@ -260,7 +265,7 @@ export class Arena { return this.trySetWeatherOverride(Overrides.WEATHER_OVERRIDE); } - if (this.weather?.weatherType === (weather || undefined)) { + if (!this.canSetWeather(weather)) { return false; } @@ -314,8 +319,13 @@ export class Arena { }); } + /** Returns whether or not the terrain can be set to {@linkcode terrain} */ + canSetTerrain(terrain: TerrainType): boolean { + return !(this.terrain?.terrainType === (terrain || undefined)); + } + trySetTerrain(terrain: TerrainType, hasPokemonSource: boolean, ignoreAnim: boolean = false): boolean { - if (this.terrain?.terrainType === (terrain || undefined)) { + if (!this.canSetTerrain(terrain)) { return false; } diff --git a/src/overrides.ts b/src/overrides.ts index 8f881ca59dd..b0a4d5c7d4d 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -2,6 +2,7 @@ import { type PokeballCounts } from "#app/battle-scene"; import { Gender } from "#app/data/gender"; import { Variant } from "#app/data/variant"; +import { BerryType } from "#app/enums/berry-type"; import { type ModifierOverride } from "#app/modifier/modifier-type"; import { Unlockables } from "#app/system/unlockables"; import { Abilities } from "#enums/abilities"; @@ -32,7 +33,14 @@ import { WeatherType } from "#enums/weather-type"; * } * ``` */ -const overrides = {} satisfies Partial>; +const overrides = { + OPP_HELD_ITEMS_OVERRIDE: [{ name: "BERRY", type: BerryType.GANLON, count: 3 }], + ABILITY_OVERRIDE: Abilities.PICKPOCKET, + OPP_LEVEL_OVERRIDE: 1, + STARTING_LEVEL_OVERRIDE: 100, + OPP_MOVESET_OVERRIDE: Moves.TACKLE, + MOVESET_OVERRIDE: Moves.SPLASH +} satisfies Partial>; /** * If you need to add Overrides values for local testing do that inside {@linkcode overrides}