From 0d894e9009788141e707c77491f75625ca10c270 Mon Sep 17 00:00:00 2001 From: muscode13 Date: Tue, 1 Oct 2024 11:40:48 -0600 Subject: [PATCH 1/7] unburden implemented --- src/data/ability.ts | 92 ++++++++++++++++++- src/data/move.ts | 3 + src/field/pokemon.ts | 1 + src/modifier/modifier.ts | 1 + src/test/abilities/unburden.test.ts | 136 ++++++++++++++++++++++++++++ 5 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 src/test/abilities/unburden.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 3ace872de3c2..f1c41e9ffb4c 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1692,6 +1692,7 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { const stolenItem = heldItems[pokemon.randSeedInt(heldItems.length)]; pokemon.scene.tryTransferHeldItemModifier(stolenItem, pokemon, false).then(success => { if (success) { + defender.turnData.itemsLost += 1; pokemon.scene.queueMessage(i18next.t("abilityTriggers:postAttackStealHeldItem", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), defenderName: defender.name, stolenItemType: stolenItem.type.name })); } resolve(success); @@ -1785,6 +1786,7 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { const stolenItem = heldItems[pokemon.randSeedInt(heldItems.length)]; pokemon.scene.tryTransferHeldItemModifier(stolenItem, pokemon, false).then(success => { if (success) { + attacker.turnData.itemsLost += 1; pokemon.scene.queueMessage(i18next.t("abilityTriggers:postDefendStealHeldItem", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), attackerName: attacker.name, stolenItemType: stolenItem.type.name })); } resolve(success); @@ -3835,6 +3837,92 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { } } +export class UnburdenBerryAbAttr extends PostTurnAbAttr { + private stats: BattleStat[]; + private stages: number; + + constructor(stats: BattleStat[], stages: number) { + super(true); + + this.stats = Array.isArray(stats) + ? stats + : [ stats ]; + this.stages = stages; + } + + applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { + const multipleItems = pokemon.battleData.berriesEaten.length * this.stages; + if (multipleItems > 6) { + this.stages = 6; + } else { + this.stages = multipleItems; + } + if (!simulated) { + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.stages)); + } + return true; + } + + getCondition(): AbAttrCondition { + return (pokemon: Pokemon) => pokemon.battleData.berriesEaten.length !== 0; + } + +} + +export class UnburdenDefStolenAbAttr extends PostDefendAbAttr { + private stats: BattleStat[]; + private stages: number; + + constructor(stats: BattleStat[], stages: number) { + super(true); + + this.stats = Array.isArray(stats) + ? stats + : [ stats ]; + this.stages = stages; + } + + applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { + if (!simulated) { + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.stages)); + pokemon.turnData.itemsLost -= 1; + } + return true; + } + + getCondition(): AbAttrCondition { + return (pokemon: Pokemon) => pokemon.turnData.itemsLost > 0; + } + +} + +export class UnburdenAtkStolenAbAttr extends PostAttackAbAttr { + private stats: BattleStat[]; + private stages: number; + + constructor(stats: BattleStat[], stages: number) { + super(); + + this.stats = Array.isArray(stats) + ? stats + : [ stats ]; + this.stages = stages; + } + + applyPostAttackAfterMoveTypeCheck(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { + if (!simulated) { + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.stages)); + pokemon.turnData.itemsLost -= 1; + } + return true; + } + + getCondition(): AbAttrCondition { + return (pokemon: Pokemon) => pokemon.turnData.itemsLost > 0; + } + +} + export class StatStageChangeMultiplierAbAttr extends AbAttr { private multiplier: integer; @@ -5146,7 +5234,9 @@ export function initAbilities() { new Ability(Abilities.ANGER_POINT, 4) .attr(PostDefendCritStatStageChangeAbAttr, Stat.ATK, 6), new Ability(Abilities.UNBURDEN, 4) - .unimplemented(), + .attr(UnburdenBerryAbAttr, [ Stat.SPD ], 2) + .attr(UnburdenAtkStolenAbAttr, [ Stat.SPD ], 2) + .attr(UnburdenDefStolenAbAttr, [ Stat.SPD ], 2), new Ability(Abilities.HEATPROOF, 4) .attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 0.5) .attr(ReduceBurnDamageAbAttr, 0.5) diff --git a/src/data/move.ts b/src/data/move.ts index 59417f52e023..471260e36115 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -2146,6 +2146,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr { const stolenItem = tierHeldItems[user.randSeedInt(tierHeldItems.length)]; user.scene.tryTransferHeldItemModifier(stolenItem, user, false).then(success => { if (success) { + target.turnData.itemsLost += 1; user.scene.queueMessage(i18next.t("moveTriggers:stoleItem", {pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: stolenItem.type.name})); } resolve(success); @@ -2227,6 +2228,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { // Decrease item amount and update icon !--removedItem.stackCount; target.scene.updateModifiers(target.isPlayer()); + target.turnData.itemsLost+=1; if (this.berriesOnly) { user.scene.queueMessage(i18next.t("moveTriggers:incineratedItem", {pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name})); @@ -2341,6 +2343,7 @@ export class StealEatBerryAttr extends EatBerryAttr { } // if the target has berries, pick a random berry and steal it this.chosenBerry = heldBerries[user.randSeedInt(heldBerries.length)]; + target.turnData.itemsLost+=1; const message = i18next.t("battle:stealEatBerry", {pokemonName: user.name, targetName: target.name, berryName: this.chosenBerry.type.name}); user.scene.queueMessage(message); this.reduceBerryModifier(target); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 07525e921574..458243752b43 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -5011,6 +5011,7 @@ export class PokemonTurnData { public order: number; public statStagesIncreased: boolean = false; public statStagesDecreased: boolean = false; + public itemsLost: number = 0; } export enum AiType { diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index cf9cf78225e9..221cbf8bcb5e 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -2719,6 +2719,7 @@ export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier { const randItem = itemModifiers[randItemIndex]; heldItemTransferPromises.push(pokemon.scene.tryTransferHeldItemModifier(randItem, pokemon, false).then(success => { if (success) { + targetPokemon.turnData.itemsLost += 1; transferredModifierTypes.push(randItem.type); itemModifiers.splice(randItemIndex, 1); } diff --git a/src/test/abilities/unburden.test.ts b/src/test/abilities/unburden.test.ts new file mode 100644 index 000000000000..628ef83f8a29 --- /dev/null +++ b/src/test/abilities/unburden.test.ts @@ -0,0 +1,136 @@ +import { Abilities } from "#enums/abilities"; +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, vi } from "vitest"; +import { Stat } from "#enums/stat"; +import { BerryType } from "#app/enums/berry-type"; +import { allMoves, StealHeldItemChanceAttr } from "#app/data/move"; + + +describe("Abilities - Unburden", () => { + 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.TREECKO) + .startingLevel(1) + .moveset([Moves.POPULATION_BOMB, Moves.KNOCK_OFF, Moves.PLUCK, Moves.THIEF]) + .ability(Abilities.UNBURDEN) + .startingHeldItems([ + { name: "BERRY", count: 1, type: BerryType.SITRUS }, + { name: "BERRY", count: 2, type: BerryType.APICOT }, + { name: "BERRY", count: 2, type: BerryType.LUM }, + ]) + .enemySpecies(Species.NINJASK) + .enemyLevel(100) + .enemyMoveset([Moves.FALSE_SWIPE]) + .enemyAbility(Abilities.UNBURDEN) + .enemyHeldItems([ + { name: "BERRY", type: BerryType.SITRUS, count: 1 }, + { name: "BERRY", type: BerryType.LUM, count: 1 }, + ]); + }); + + it("should activate when a berry is eaten", async () => { + await game.classicMode.startBattle(); + const playerPokemon = game.scene.getPlayerPokemon()!; + const playerHeldItems = playerPokemon.getHeldItems().length; + game.move.select(Moves.FALSE_SWIPE); + await game.toNextTurn(); + expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(4); + + }); + it("should activate when a berry is stolen", async () => { + await game.classicMode.startBattle(); + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + game.move.select(Moves.PLUCK); + await game.toNextTurn(); + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getStatStage(Stat.SPD)).toBe(2); + }); + it("should activate when an item is knocked off", async () => { + await game.classicMode.startBattle(); + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + game.move.select(Moves.KNOCK_OFF); + await game.toNextTurn(); + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getStatStage(Stat.SPD)).toBe(2); + }); + it("should activate when an item is stolen via attacking ability", async () => { + game.override + .ability(Abilities.MAGICIAN) + .startingHeldItems([ + { name: "MULTI_LENS", count: 3 }, + ]); + await game.classicMode.startBattle(); + vi.spyOn(allMoves[Moves.POPULATION_BOMB], "accuracy", "get").mockReturnValue(100); + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + game.move.select(Moves.POPULATION_BOMB); + await game.toNextTurn(); + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getStatStage(Stat.SPD)).toBe(4); + }); + it("should activate when an item is stolen via defending ability", async () => { + game.override + .enemyAbility(Abilities.PICKPOCKET) + .startingHeldItems([ + { name: "MULTI_LENS", count: 3 }, + { name: "SOUL_DEW", count: 1}, + { name: "LUCKY_EGG", count: 1 }, + ]); + await game.classicMode.startBattle(); + vi.spyOn(allMoves[Moves.POPULATION_BOMB], "accuracy", "get").mockReturnValue(100); + const playerPokemon = game.scene.getPlayerPokemon()!; + const playerHeldItems = playerPokemon.getHeldItems().length; + game.move.select(Moves.POPULATION_BOMB); + await game.toNextTurn(); + expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(6); + }); + it("should activate when an item is stolen via move", async () => { + vi.spyOn(allMoves[Moves.THIEF], "attrs", "get").mockReturnValue([new StealHeldItemChanceAttr(1.0)]); // give Thief 100% steal rate + game.override.startingHeldItems([ + { name: "MULTI_LENS", count: 3 }, + ]); + await game.classicMode.startBattle(); + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + game.move.select(Moves.THIEF); + await game.toNextTurn(); + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getStatStage(Stat.SPD)).toBe(4); + }); + it("should activate when an item is stolen via grip claw", async () => { + game.override.startingHeldItems([ + { name: "GRIP_CLAW", count: 5 }, + { name: "MULTI_LENS", count: 3 }, + ]); + await game.classicMode.startBattle(); + vi.spyOn(allMoves[Moves.POPULATION_BOMB], "accuracy", "get").mockReturnValue(100); + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + game.move.select(Moves.POPULATION_BOMB); + await game.toNextTurn(); + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getStatStage(Stat.SPD)).toBe(4); + }); +}); From 09d2cbb3600daa2a4e450a5ef38d6642ac1c69d5 Mon Sep 17 00:00:00 2001 From: muscode13 Date: Wed, 2 Oct 2024 08:59:58 -0600 Subject: [PATCH 2/7] Used tag instead of stat changes for Unburden --- src/data/ability.ts | 78 ++++++++++------------------- src/data/battler-tags.ts | 15 ++++++ src/enums/battler-tag-type.ts | 1 + src/test/abilities/unburden.test.ts | 21 +++++--- 4 files changed, 56 insertions(+), 59 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index e2ae7830cee2..0394a55dd951 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -3792,28 +3792,16 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { } export class UnburdenBerryAbAttr extends PostTurnAbAttr { - private stats: BattleStat[]; - private stages: number; - - constructor(stats: BattleStat[], stages: number) { - super(true); - - this.stats = Array.isArray(stats) - ? stats - : [ stats ]; - this.stages = stages; - } - applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { - const multipleItems = pokemon.battleData.berriesEaten.length * this.stages; - if (multipleItems > 6) { - this.stages = 6; - } else { - this.stages = multipleItems; + if (simulated) { + return simulated; } - if (!simulated) { - pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.stages)); + + if (pokemon.getTag(BattlerTagType.UNBURDEN)) { + return false; } + + pokemon.addTag(BattlerTagType.UNBURDEN); return true; } @@ -3824,23 +3812,16 @@ export class UnburdenBerryAbAttr extends PostTurnAbAttr { } export class UnburdenDefStolenAbAttr extends PostDefendAbAttr { - private stats: BattleStat[]; - private stages: number; - - constructor(stats: BattleStat[], stages: number) { - super(true); - - this.stats = Array.isArray(stats) - ? stats - : [ stats ]; - this.stages = stages; - } - applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (!simulated) { - pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.stages)); - pokemon.turnData.itemsLost -= 1; + if (simulated) { + return simulated; } + + if (pokemon.getTag(BattlerTagType.UNBURDEN)) { + return false; + } + + pokemon.addTag(BattlerTagType.UNBURDEN); return true; } @@ -3851,23 +3832,16 @@ export class UnburdenDefStolenAbAttr extends PostDefendAbAttr { } export class UnburdenAtkStolenAbAttr extends PostAttackAbAttr { - private stats: BattleStat[]; - private stages: number; - - constructor(stats: BattleStat[], stages: number) { - super(); - - this.stats = Array.isArray(stats) - ? stats - : [ stats ]; - this.stages = stages; - } - applyPostAttackAfterMoveTypeCheck(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (!simulated) { - pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.stages)); - pokemon.turnData.itemsLost -= 1; + if (simulated) { + return simulated; } + + if (pokemon.getTag(BattlerTagType.UNBURDEN)) { + return false; + } + + pokemon.addTag(BattlerTagType.UNBURDEN); return true; } @@ -5188,9 +5162,9 @@ export function initAbilities() { new Ability(Abilities.ANGER_POINT, 4) .attr(PostDefendCritStatStageChangeAbAttr, Stat.ATK, 6), new Ability(Abilities.UNBURDEN, 4) - .attr(UnburdenBerryAbAttr, [ Stat.SPD ], 2) - .attr(UnburdenAtkStolenAbAttr, [ Stat.SPD ], 2) - .attr(UnburdenDefStolenAbAttr, [ Stat.SPD ], 2), + .attr(UnburdenBerryAbAttr) + .attr(UnburdenAtkStolenAbAttr) + .attr(UnburdenDefStolenAbAttr), new Ability(Abilities.HEATPROOF, 4) .attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 0.5) .attr(ReduceBurnDamageAbAttr, 0.5) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index d8094f963681..8450a4ecfdc3 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1537,6 +1537,19 @@ export class AbilityBattlerTag extends BattlerTag { } } +/** + * Tag used by Unburden to double speed + * @extends AbilityBattlerTag + */ +export class UnburdenTag extends AbilityBattlerTag { + constructor() { + super(BattlerTagType.UNBURDEN, Abilities.UNBURDEN, BattlerTagLapseType.CUSTOM, 1); + } + onAdd(pokemon: Pokemon): void { + pokemon.setStat(Stat.SPD, pokemon.getStat(Stat.SPD, false) * 2, false); + } +} + export class TruantTag extends AbilityBattlerTag { constructor() { super(BattlerTagType.TRUANT, Abilities.TRUANT, BattlerTagLapseType.MOVE, 1); @@ -2815,6 +2828,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new ThroatChoppedTag(); case BattlerTagType.GORILLA_TACTICS: return new GorillaTacticsTag(); + case BattlerTagType.UNBURDEN: + return new UnburdenTag(); case BattlerTagType.SUBSTITUTE: return new SubstituteTag(sourceMove, sourceId); case BattlerTagType.AUTOTOMIZED: diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 209d36316f9f..e914d98ca37a 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -75,6 +75,7 @@ export enum BattlerTagType { DRAGON_CHEER = "DRAGON_CHEER", NO_RETREAT = "NO_RETREAT", GORILLA_TACTICS = "GORILLA_TACTICS", + UNBURDEN = "UNBURDEN", THROAT_CHOPPED = "THROAT_CHOPPED", TAR_SHOT = "TAR_SHOT", BURNED_UP = "BURNED_UP", diff --git a/src/test/abilities/unburden.test.ts b/src/test/abilities/unburden.test.ts index 628ef83f8a29..dc87ec923061 100644 --- a/src/test/abilities/unburden.test.ts +++ b/src/test/abilities/unburden.test.ts @@ -50,29 +50,32 @@ describe("Abilities - Unburden", () => { await game.classicMode.startBattle(); const playerPokemon = game.scene.getPlayerPokemon()!; const playerHeldItems = playerPokemon.getHeldItems().length; + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); game.move.select(Moves.FALSE_SWIPE); await game.toNextTurn(); expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); - expect(playerPokemon.getStatStage(Stat.SPD)).toBe(4); + expect(playerPokemon.getStat(Stat.SPD, false)).toBeCloseTo(initialPlayerSpeed * 2); }); it("should activate when a berry is stolen", async () => { await game.classicMode.startBattle(); const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); game.move.select(Moves.PLUCK); await game.toNextTurn(); expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getStatStage(Stat.SPD)).toBe(2); + expect(enemyPokemon.getStat(Stat.SPD, false)).toBeCloseTo(initialEnemySpeed * 2); }); it("should activate when an item is knocked off", async () => { await game.classicMode.startBattle(); const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); game.move.select(Moves.KNOCK_OFF); await game.toNextTurn(); expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getStatStage(Stat.SPD)).toBe(2); + expect(enemyPokemon.getStat(Stat.SPD, false)).toBeCloseTo(initialEnemySpeed * 2); }); it("should activate when an item is stolen via attacking ability", async () => { game.override @@ -84,10 +87,11 @@ describe("Abilities - Unburden", () => { vi.spyOn(allMoves[Moves.POPULATION_BOMB], "accuracy", "get").mockReturnValue(100); const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); game.move.select(Moves.POPULATION_BOMB); await game.toNextTurn(); expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getStatStage(Stat.SPD)).toBe(4); + expect(enemyPokemon.getStat(Stat.SPD, false)).toBeCloseTo(initialEnemySpeed * 2); }); it("should activate when an item is stolen via defending ability", async () => { game.override @@ -101,10 +105,11 @@ describe("Abilities - Unburden", () => { vi.spyOn(allMoves[Moves.POPULATION_BOMB], "accuracy", "get").mockReturnValue(100); const playerPokemon = game.scene.getPlayerPokemon()!; const playerHeldItems = playerPokemon.getHeldItems().length; + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); game.move.select(Moves.POPULATION_BOMB); await game.toNextTurn(); expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); - expect(playerPokemon.getStatStage(Stat.SPD)).toBe(6); + expect(playerPokemon.getStat(Stat.SPD, false)).toBeCloseTo(initialPlayerSpeed * 2); }); it("should activate when an item is stolen via move", async () => { vi.spyOn(allMoves[Moves.THIEF], "attrs", "get").mockReturnValue([new StealHeldItemChanceAttr(1.0)]); // give Thief 100% steal rate @@ -114,10 +119,11 @@ describe("Abilities - Unburden", () => { await game.classicMode.startBattle(); const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); game.move.select(Moves.THIEF); await game.toNextTurn(); expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getStatStage(Stat.SPD)).toBe(4); + expect(enemyPokemon.getStat(Stat.SPD, false)).toBeCloseTo(initialEnemySpeed * 2); }); it("should activate when an item is stolen via grip claw", async () => { game.override.startingHeldItems([ @@ -128,9 +134,10 @@ describe("Abilities - Unburden", () => { vi.spyOn(allMoves[Moves.POPULATION_BOMB], "accuracy", "get").mockReturnValue(100); const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); game.move.select(Moves.POPULATION_BOMB); await game.toNextTurn(); expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getStatStage(Stat.SPD)).toBe(4); + expect(enemyPokemon.getStat(Stat.SPD, false)).toBeCloseTo(initialEnemySpeed * 2); }); }); From b453b8eed65063ef178457ee0883ebdfb7b8246e Mon Sep 17 00:00:00 2001 From: muscode13 Date: Thu, 3 Oct 2024 13:25:59 -0600 Subject: [PATCH 3/7] added documentation and neutralizing gas test --- src/data/ability.ts | 63 ++++++++++++++++++++++++++--- src/test/abilities/unburden.test.ts | 12 ++++++ 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 7aa4de0c2b9b..c16a371265b8 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -3792,7 +3792,22 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { } } -export class UnburdenBerryAbAttr extends PostTurnAbAttr { +/** + * Ability attribute for Unburden, triggers upon berry consumption + * @extends PostTurnAbAttr + * @see {@linkcode applyPostTurn} + * @see {@linkcode getCondition} + */ +export class UnburdenBerryRemovedAbAttr extends PostTurnAbAttr { + + /** + * + * @param {Pokemon} pokemon the {@linkcode Pokemon} with this ability + * @param passive n/a + * @param simulated whether the ability is being simulated + * @param args n/a + * @returns `true` if the ability is applied + */ applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { if (simulated) { return simulated; @@ -3812,7 +3827,25 @@ export class UnburdenBerryAbAttr extends PostTurnAbAttr { } -export class UnburdenDefStolenAbAttr extends PostDefendAbAttr { +/** + * Ability attribute for Unburden, triggers upon an item being lost while defending (Knock Off, Thief, Pluck) + * @extends PostDefendAbAttr + * @see {@linkcode applyPostDefend} + * @see {@linkcode getCondition} + */ +export class UnburdenDefendingItemRemovedAbAttr extends PostDefendAbAttr { + + /** + * + * @param {Pokemon} pokemon the {@linkcode Pokemon} with this ability + * @param passive n/a + * @param simulated whether the ability is being simulated + * @param attacker n/a + * @param move n/a + * @param hitResult n/a + * @param args n/a + * @returns `true` if the ability is applied + */ applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { if (simulated) { return simulated; @@ -3832,7 +3865,25 @@ export class UnburdenDefStolenAbAttr extends PostDefendAbAttr { } -export class UnburdenAtkStolenAbAttr extends PostAttackAbAttr { +/** + * Ability attribute for Unburden, triggers upon an item being lost while attacking (Pickpocket) + * @extends PostAttackAbAttr + * @see {@linkcode applyPostAttackAfterMoveTypeCheck} + * @see {@linkcode getCondition} + */ +export class UnburdenAttackingItemRemovedAbAttr extends PostAttackAbAttr { + + /** + * + * @param {Pokemon} pokemon the {@linkcode Pokemon} with this ability + * @param passive n/a + * @param simulated whether the ability is being simulated + * @param defender n/a + * @param move n/a + * @param hitResult n/a + * @param args n/a + * @returns `true` if the ability is applied + */ applyPostAttackAfterMoveTypeCheck(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { if (simulated) { return simulated; @@ -5163,9 +5214,9 @@ export function initAbilities() { new Ability(Abilities.ANGER_POINT, 4) .attr(PostDefendCritStatStageChangeAbAttr, Stat.ATK, 6), new Ability(Abilities.UNBURDEN, 4) - .attr(UnburdenBerryAbAttr) - .attr(UnburdenAtkStolenAbAttr) - .attr(UnburdenDefStolenAbAttr), + .attr(UnburdenBerryRemovedAbAttr) + .attr(UnburdenAttackingItemRemovedAbAttr) + .attr(UnburdenDefendingItemRemovedAbAttr), new Ability(Abilities.HEATPROOF, 4) .attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 0.5) .attr(ReduceBurnDamageAbAttr, 0.5) diff --git a/src/test/abilities/unburden.test.ts b/src/test/abilities/unburden.test.ts index dc87ec923061..30327214839d 100644 --- a/src/test/abilities/unburden.test.ts +++ b/src/test/abilities/unburden.test.ts @@ -140,4 +140,16 @@ describe("Abilities - Unburden", () => { expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); expect(enemyPokemon.getStat(Stat.SPD, false)).toBeCloseTo(initialEnemySpeed * 2); }); + + it("should not activate when a neutralizing ability is present", async () => { + game.override.enemyAbility(Abilities.NEUTRALIZING_GAS); + await game.classicMode.startBattle(); + const playerPokemon = game.scene.getPlayerPokemon()!; + const playerHeldItems = playerPokemon.getHeldItems().length; + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + game.move.select(Moves.FALSE_SWIPE); + await game.toNextTurn(); + expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); + expect(playerPokemon.getStat(Stat.SPD, false)).toBe(initialPlayerSpeed); + }); }); From ee52607e44da3fed773af8a92dcba492f4110d16 Mon Sep 17 00:00:00 2001 From: muscode13 Date: Thu, 3 Oct 2024 13:30:35 -0600 Subject: [PATCH 4/7] accounted for unburden in getEffectiveStat --- src/field/pokemon.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index c7bb3083b2c3..431a764b2b25 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -936,6 +936,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (this.status && this.status.effect === StatusEffect.PARALYSIS) { ret >>= 1; } + if (this.getTag(BattlerTagType.UNBURDEN)) { + ret *=2; + } break; } From 54468d82ce6c5a92195f7fb5952427529bd28840 Mon Sep 17 00:00:00 2001 From: muscode13 Date: Thu, 3 Oct 2024 14:52:16 -0600 Subject: [PATCH 5/7] fixed doubling speed in two places --- src/data/battler-tags.ts | 5 +++- src/test/abilities/unburden.test.ts | 42 +++++++++++++++++++---------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 8450a4ecfdc3..cdf66bfaf7f1 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1546,7 +1546,10 @@ export class UnburdenTag extends AbilityBattlerTag { super(BattlerTagType.UNBURDEN, Abilities.UNBURDEN, BattlerTagLapseType.CUSTOM, 1); } onAdd(pokemon: Pokemon): void { - pokemon.setStat(Stat.SPD, pokemon.getStat(Stat.SPD, false) * 2, false); + super.onAdd(pokemon); + } + onRemove(pokemon: Pokemon): void { + super.onRemove(pokemon); } } diff --git a/src/test/abilities/unburden.test.ts b/src/test/abilities/unburden.test.ts index 30327214839d..f01501de0215 100644 --- a/src/test/abilities/unburden.test.ts +++ b/src/test/abilities/unburden.test.ts @@ -54,7 +54,7 @@ describe("Abilities - Unburden", () => { game.move.select(Moves.FALSE_SWIPE); await game.toNextTurn(); expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); - expect(playerPokemon.getStat(Stat.SPD, false)).toBeCloseTo(initialPlayerSpeed * 2); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed * 2); }); it("should activate when a berry is stolen", async () => { @@ -65,7 +65,7 @@ describe("Abilities - Unburden", () => { game.move.select(Moves.PLUCK); await game.toNextTurn(); expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getStat(Stat.SPD, false)).toBeCloseTo(initialEnemySpeed * 2); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); }); it("should activate when an item is knocked off", async () => { await game.classicMode.startBattle(); @@ -75,7 +75,7 @@ describe("Abilities - Unburden", () => { game.move.select(Moves.KNOCK_OFF); await game.toNextTurn(); expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getStat(Stat.SPD, false)).toBeCloseTo(initialEnemySpeed * 2); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); }); it("should activate when an item is stolen via attacking ability", async () => { game.override @@ -91,10 +91,11 @@ describe("Abilities - Unburden", () => { game.move.select(Moves.POPULATION_BOMB); await game.toNextTurn(); expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getStat(Stat.SPD, false)).toBeCloseTo(initialEnemySpeed * 2); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); }); it("should activate when an item is stolen via defending ability", async () => { game.override + .startingLevel(50) .enemyAbility(Abilities.PICKPOCKET) .startingHeldItems([ { name: "MULTI_LENS", count: 3 }, @@ -109,7 +110,7 @@ describe("Abilities - Unburden", () => { game.move.select(Moves.POPULATION_BOMB); await game.toNextTurn(); expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); - expect(playerPokemon.getStat(Stat.SPD, false)).toBeCloseTo(initialPlayerSpeed * 2); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed * 2); }); it("should activate when an item is stolen via move", async () => { vi.spyOn(allMoves[Moves.THIEF], "attrs", "get").mockReturnValue([new StealHeldItemChanceAttr(1.0)]); // give Thief 100% steal rate @@ -123,22 +124,35 @@ describe("Abilities - Unburden", () => { game.move.select(Moves.THIEF); await game.toNextTurn(); expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getStat(Stat.SPD, false)).toBeCloseTo(initialEnemySpeed * 2); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); }); it("should activate when an item is stolen via grip claw", async () => { - game.override.startingHeldItems([ - { name: "GRIP_CLAW", count: 5 }, - { name: "MULTI_LENS", count: 3 }, - ]); + game.override + .startingLevel(5) + .startingHeldItems([ + { name: "GRIP_CLAW", count: 5 }, + { name: "MULTI_LENS", count: 3 }, + ]) + .enemyHeldItems([ + { name: "SOUL_DEW", count: 1 }, + { name: "LUCKY_EGG", count: 1 }, + { name: "LEFTOVERS", count: 1 }, + { name: "GRIP_CLAW", count: 1 }, + { name: "MULTI_LENS", count: 1 }, + { name: "BERRY", type: BerryType.SITRUS, count: 1 }, + { name: "BERRY", type: BerryType.LUM, count: 1 }, + ]); await game.classicMode.startBattle(); vi.spyOn(allMoves[Moves.POPULATION_BOMB], "accuracy", "get").mockReturnValue(100); const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyHeldItemCt = enemyPokemon.getHeldItems().length; const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); - game.move.select(Moves.POPULATION_BOMB); - await game.toNextTurn(); + while (enemyPokemon.getHeldItems().length === enemyHeldItemCt) { + game.move.select(Moves.POPULATION_BOMB); + await game.toNextTurn(); + } expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getStat(Stat.SPD, false)).toBeCloseTo(initialEnemySpeed * 2); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); }); it("should not activate when a neutralizing ability is present", async () => { @@ -150,6 +164,6 @@ describe("Abilities - Unburden", () => { game.move.select(Moves.FALSE_SWIPE); await game.toNextTurn(); expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); - expect(playerPokemon.getStat(Stat.SPD, false)).toBe(initialPlayerSpeed); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed); }); }); From fc89850e0757dd7ad7cf179aef5f09a48fbdc22f Mon Sep 17 00:00:00 2001 From: muscode13 Date: Mon, 7 Oct 2024 17:02:34 -0600 Subject: [PATCH 6/7] merge conflict resolve --- public/locales | 2 +- src/data/move.ts | 4 ++-- src/field/pokemon.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/locales b/public/locales index b44ee2173788..3ccef8472dd7 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit b44ee2173788018ffd5dc6b7b7fa159be5b9d514 +Subproject commit 3ccef8472dd7cc7c362538489954cb8fdad27e5f diff --git a/src/data/move.ts b/src/data/move.ts index 6d622e1be4f9..4885d57c30a1 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -2237,7 +2237,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { // Decrease item amount and update icon !--removedItem.stackCount; target.scene.updateModifiers(target.isPlayer()); - target.turnData.itemsLost+=1; + target.turnData.itemsLost += 1; if (this.berriesOnly) { user.scene.queueMessage(i18next.t("moveTriggers:incineratedItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name })); @@ -2352,7 +2352,7 @@ export class StealEatBerryAttr extends EatBerryAttr { } // if the target has berries, pick a random berry and steal it this.chosenBerry = heldBerries[user.randSeedInt(heldBerries.length)]; - target.turnData.itemsLost+=1; + target.turnData.itemsLost += 1; const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: this.chosenBerry.type.name }); user.scene.queueMessage(message); this.reduceBerryModifier(target); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index a9b7df0d8e02..59cb503c1d49 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -939,7 +939,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { ret >>= 1; } if (this.getTag(BattlerTagType.UNBURDEN)) { - ret *=2; + ret *= 2; } break; } From 7ce52f9655b47e323fe4eef39a844f7c69db527e Mon Sep 17 00:00:00 2001 From: muscode13 Date: Wed, 9 Oct 2024 19:40:15 -0600 Subject: [PATCH 7/7] changed documentation wording, added test for stuff cheeks --- src/data/ability.ts | 2 +- src/test/abilities/unburden.test.ts | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 69df0d06dfd7..dd927573dce3 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -3793,7 +3793,7 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { } /** - * Ability attribute for Unburden, triggers upon berry consumption + * Ability attribute for Unburden, triggers when a Pokemon consumes a berry they are holding * @extends PostTurnAbAttr * @see {@linkcode applyPostTurn} * @see {@linkcode getCondition} diff --git a/src/test/abilities/unburden.test.ts b/src/test/abilities/unburden.test.ts index f01501de0215..c467c2495fec 100644 --- a/src/test/abilities/unburden.test.ts +++ b/src/test/abilities/unburden.test.ts @@ -29,7 +29,7 @@ describe("Abilities - Unburden", () => { .battleType("single") .starterSpecies(Species.TREECKO) .startingLevel(1) - .moveset([Moves.POPULATION_BOMB, Moves.KNOCK_OFF, Moves.PLUCK, Moves.THIEF]) + .moveset([ Moves.POPULATION_BOMB, Moves.KNOCK_OFF, Moves.PLUCK, Moves.THIEF ]) .ability(Abilities.UNBURDEN) .startingHeldItems([ { name: "BERRY", count: 1, type: BerryType.SITRUS }, @@ -38,7 +38,7 @@ describe("Abilities - Unburden", () => { ]) .enemySpecies(Species.NINJASK) .enemyLevel(100) - .enemyMoveset([Moves.FALSE_SWIPE]) + .enemyMoveset([ Moves.FALSE_SWIPE ]) .enemyAbility(Abilities.UNBURDEN) .enemyHeldItems([ { name: "BERRY", type: BerryType.SITRUS, count: 1 }, @@ -99,7 +99,7 @@ describe("Abilities - Unburden", () => { .enemyAbility(Abilities.PICKPOCKET) .startingHeldItems([ { name: "MULTI_LENS", count: 3 }, - { name: "SOUL_DEW", count: 1}, + { name: "SOUL_DEW", count: 1 }, { name: "LUCKY_EGG", count: 1 }, ]); await game.classicMode.startBattle(); @@ -113,7 +113,7 @@ describe("Abilities - Unburden", () => { expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed * 2); }); it("should activate when an item is stolen via move", async () => { - vi.spyOn(allMoves[Moves.THIEF], "attrs", "get").mockReturnValue([new StealHeldItemChanceAttr(1.0)]); // give Thief 100% steal rate + vi.spyOn(allMoves[Moves.THIEF], "attrs", "get").mockReturnValue([ new StealHeldItemChanceAttr(1.0) ]); // give Thief 100% steal rate game.override.startingHeldItems([ { name: "MULTI_LENS", count: 3 }, ]); @@ -166,4 +166,15 @@ describe("Abilities - Unburden", () => { expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed); }); + it("should activate when a move that consumes a berry is used", async () => { + game.override.enemyMoveset([ Moves.STUFF_CHEEKS ]); + await game.classicMode.startBattle(); + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + game.move.select(Moves.STUFF_CHEEKS); + await game.toNextTurn(); + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + }); });