From 0d894e9009788141e707c77491f75625ca10c270 Mon Sep 17 00:00:00 2001 From: muscode13 Date: Tue, 1 Oct 2024 11:40:48 -0600 Subject: [PATCH] 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 3ace872de3c..f1c41e9ffb4 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 59417f52e02..471260e3611 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 07525e92157..458243752b4 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 cf9cf78225e..221cbf8bcb5 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 00000000000..628ef83f8a2 --- /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); + }); +});