Skip to content

Commit

Permalink
[Move][Ability] Fully Implement Forest's Curse / Trick Or Treat / Mim…
Browse files Browse the repository at this point in the history
…icry (#4682)

* addedType variable

* basic mimicry implementation

* eslint

* rage

* quick change

* made files

* added mimicry activation message

* test for moves done

* hahahhaha

* done? for now?

* laklhaflhasd

* Apply suggestions from code review

Co-authored-by: NightKev <[email protected]>

* time to start... ughhh

* reflect type

* Added new message

* Update src/field/pokemon.ts

Co-authored-by: PigeonBar <[email protected]>

* Update src/data/ability.ts

Co-authored-by: flx-sta <[email protected]>

* added overrides

* some checks

* removed comments

* Apply suggestions from code review

Co-authored-by: NightKev <[email protected]>

---------

Co-authored-by: frutescens <info@laptop>
Co-authored-by: NightKev <[email protected]>
Co-authored-by: PigeonBar <[email protected]>
Co-authored-by: flx-sta <[email protected]>
  • Loading branch information
5 people authored Oct 29, 2024
1 parent 38a6bf0 commit fb2d3e4
Show file tree
Hide file tree
Showing 8 changed files with 350 additions and 13 deletions.
80 changes: 79 additions & 1 deletion src/data/ability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4703,6 +4703,84 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr {
}
}

/**
* This applies a terrain-based type change to the Pokemon.
* Used by Mimicry.
*/
export class TerrainEventTypeChangeAbAttr extends PostSummonAbAttr {
constructor() {
super(true);
}

override apply(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _cancelled: Utils.BooleanHolder, _args: any[]): boolean {
if (pokemon.isTerastallized()) {
return false;
}
const currentTerrain = pokemon.scene.arena.getTerrainType();
const typeChange: Type[] = this.determineTypeChange(pokemon, currentTerrain);
if (typeChange.length !== 0) {
if (pokemon.summonData.addedType && typeChange.includes(pokemon.summonData.addedType)) {
pokemon.summonData.addedType = null;
}
pokemon.summonData.types = typeChange;
pokemon.updateInfo();
}
return true;
}

/**
* Retrieves the type(s) the Pokemon should change to in response to a terrain
* @param pokemon
* @param currentTerrain {@linkcode TerrainType}
* @returns a list of type(s)
*/
private determineTypeChange(pokemon: Pokemon, currentTerrain: TerrainType): Type[] {
const typeChange: Type[] = [];
switch (currentTerrain) {
case TerrainType.ELECTRIC:
typeChange.push(Type.ELECTRIC);
break;
case TerrainType.MISTY:
typeChange.push(Type.FAIRY);
break;
case TerrainType.GRASSY:
typeChange.push(Type.GRASS);
break;
case TerrainType.PSYCHIC:
typeChange.push(Type.PSYCHIC);
break;
default:
pokemon.getTypes(false, false, true).forEach(t => {
typeChange.push(t);
});
break;
}
return typeChange;
}

/**
* Checks if the Pokemon should change types if summoned into an active terrain
* @returns `true` if there is an active terrain requiring a type change | `false` if not
*/
override applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise<boolean> {
if (pokemon.scene.arena.getTerrainType() !== TerrainType.NONE) {
return this.apply(pokemon, passive, simulated, new Utils.BooleanHolder(false), []);
}
return false;
}

override getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]) {
const currentTerrain = pokemon.scene.arena.getTerrainType();
const pokemonNameWithAffix = getPokemonNameWithAffix(pokemon);
if (currentTerrain === TerrainType.NONE) {
return i18next.t("abilityTriggers:pokemonTypeChangeRevert", { pokemonNameWithAffix });
} else {
const moveType = i18next.t(`pokemonInfo:Type.${Type[this.determineTypeChange(pokemon, currentTerrain)[0]]}`);
return i18next.t("abilityTriggers:pokemonTypeChange", { pokemonNameWithAffix, moveType });
}
}
}

async function applyAbAttrsInternal<TAttr extends AbAttr>(
attrType: Constructor<TAttr>,
pokemon: Pokemon | null,
Expand Down Expand Up @@ -5767,7 +5845,7 @@ export function initAbilities() {
new Ability(Abilities.POWER_SPOT, 8)
.attr(AllyMoveCategoryPowerBoostAbAttr, [ MoveCategory.SPECIAL, MoveCategory.PHYSICAL ], 1.3),
new Ability(Abilities.MIMICRY, 8)
.unimplemented(),
.attr(TerrainEventTypeChangeAbAttr),
new Ability(Abilities.SCREEN_CLEANER, 8)
.attr(PostSummonRemoveArenaTagAbAttr, [ ArenaTagType.AURORA_VEIL, ArenaTagType.LIGHT_SCREEN, ArenaTagType.REFLECT ]),
new Ability(Abilities.STEELY_SPIRIT, 8)
Expand Down
23 changes: 12 additions & 11 deletions src/data/move.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5858,6 +5858,9 @@ export class RemoveTypeAttr extends MoveEffectAttr {

const userTypes = user.getTypes(true);
const modifiedTypes = userTypes.filter(type => type !== this.removedType);
if (modifiedTypes.length === 0) {
modifiedTypes.push(Type.UNKNOWN);
}
user.summonData.types = modifiedTypes;
user.updateInfo();

Expand All @@ -5880,7 +5883,11 @@ export class CopyTypeAttr extends MoveEffectAttr {
return false;
}

user.summonData.types = target.getTypes(true);
const targetTypes = target.getTypes(true);
if (targetTypes.includes(Type.UNKNOWN) && targetTypes.indexOf(Type.UNKNOWN) > -1) {
targetTypes[targetTypes.indexOf(Type.UNKNOWN)] = Type.NORMAL;
}
user.summonData.types = targetTypes;
user.updateInfo();

user.scene.queueMessage(i18next.t("moveTriggers:copyType", { pokemonName: getPokemonNameWithAffix(user), targetPokemonName: getPokemonNameWithAffix(target) }));
Expand All @@ -5889,7 +5896,7 @@ export class CopyTypeAttr extends MoveEffectAttr {
}

getCondition(): MoveConditionFunc {
return (user, target, move) => target.getTypes()[0] !== Type.UNKNOWN;
return (user, target, move) => target.getTypes()[0] !== Type.UNKNOWN || target.summonData.addedType !== null;
}
}

Expand Down Expand Up @@ -5947,11 +5954,7 @@ export class AddTypeAttr extends MoveEffectAttr {
}

apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const types = target.getTypes().slice(0, 2).filter(t => t !== Type.UNKNOWN); // TODO: Figure out some way to actually check if another version of this effect is already applied
if (this.type !== Type.UNKNOWN) {
types.push(this.type);
}
target.summonData.types = types;
target.summonData.addedType = this.type;
target.updateInfo();

user.scene.queueMessage(i18next.t("moveTriggers:addType", { typeName: i18next.t(`pokemonInfo:Type.${Type[this.type]}`), pokemonName: getPokemonNameWithAffix(target) }));
Expand Down Expand Up @@ -8983,8 +8986,7 @@ export function initMoves() {
.ignoresProtect()
.ignoresVirtual(),
new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6)
.attr(AddTypeAttr, Type.GHOST)
.edgeCase(), // Weird interaction with Forest's Curse, reflect type, burn up
.attr(AddTypeAttr, Type.GHOST),
new StatusMove(Moves.NOBLE_ROAR, Type.NORMAL, 100, 30, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1)
.soundBased(),
Expand All @@ -8996,8 +8998,7 @@ export function initMoves() {
.target(MoveTarget.ALL_NEAR_OTHERS)
.triageMove(),
new StatusMove(Moves.FORESTS_CURSE, Type.GRASS, 100, 20, -1, 0, 6)
.attr(AddTypeAttr, Type.GRASS)
.edgeCase(), // Weird interaction with Trick or Treat, reflect type, burn up
.attr(AddTypeAttr, Type.GRASS),
new AttackMove(Moves.PETAL_BLIZZARD, Type.GRASS, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 6)
.windMove()
.makesContact(false)
Expand Down
10 changes: 9 additions & 1 deletion src/field/arena.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ import Move from "#app/data/move";
import { ArenaTag, ArenaTagSide, ArenaTrapTag, getArenaTag } from "#app/data/arena-tag";
import { BattlerIndex } from "#app/battle";
import { Terrain, TerrainType } from "#app/data/terrain";
import { applyPostTerrainChangeAbAttrs, applyPostWeatherChangeAbAttrs, PostTerrainChangeAbAttr, PostWeatherChangeAbAttr } from "#app/data/ability";
import {
applyAbAttrs,
applyPostTerrainChangeAbAttrs,
applyPostWeatherChangeAbAttrs,
PostTerrainChangeAbAttr,
PostWeatherChangeAbAttr,
TerrainEventTypeChangeAbAttr
} from "#app/data/ability";
import Pokemon from "#app/field/pokemon";
import Overrides from "#app/overrides";
import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena";
Expand Down Expand Up @@ -387,6 +394,7 @@ export class Arena {
this.scene.getField(true).filter(p => p.isOnField()).map(pokemon => {
pokemon.findAndRemoveTags(t => "terrainTypes" in t && !(t.terrainTypes as TerrainType[]).find(t => t === terrain));
applyPostTerrainChangeAbAttrs(PostTerrainChangeAbAttr, pokemon, terrain);
applyAbAttrs(TerrainEventTypeChangeAbAttr, pokemon, null, false);
});

return true;
Expand Down
6 changes: 6 additions & 0 deletions src/field/pokemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}

// the type added to Pokemon from moves like Forest's Curse or Trick Or Treat
if (!ignoreOverride && this.summonData && this.summonData.addedType && !types.includes(this.summonData.addedType)) {
types.push(this.summonData.addedType);
}

// If both types are the same (can happen in weird custom typing scenarios), reduce to single type
if (types.length > 1 && types[0] === types[1]) {
types.splice(0, 1);
Expand Down Expand Up @@ -5100,6 +5105,7 @@ export class PokemonSummonData {
public moveset: (PokemonMove | null)[];
// If not initialized this value will not be populated from save data.
public types: Type[] = [];
public addedType: Type | null = null;
}

export class PokemonBattleData {
Expand Down
91 changes: 91 additions & 0 deletions src/test/abilities/mimicry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Type } from "#app/data/type";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";

describe("Abilities - Mimicry", () => {
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
.moveset([ Moves.SPLASH ])
.ability(Abilities.MIMICRY)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyMoveset(Moves.SPLASH);
});

it("Mimicry activates after the Pokémon with Mimicry is switched in while terrain is present, or whenever there is a change in terrain", async () => {
game.override.enemyAbility(Abilities.MISTY_SURGE);
await game.classicMode.startBattle([ Species.FEEBAS, Species.ABRA ]);

const [ playerPokemon1, playerPokemon2 ] = game.scene.getParty();
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(playerPokemon1.getTypes().includes(Type.FAIRY)).toBe(true);

game.doSwitchPokemon(1);
await game.toNextTurn();

expect(playerPokemon2.getTypes().includes(Type.FAIRY)).toBe(true);
});

it("Pokemon should revert back to its original, root type once terrain ends", async () => {
game.override
.moveset([ Moves.SPLASH, Moves.TRANSFORM ])
.enemyAbility(Abilities.MIMICRY)
.enemyMoveset([ Moves.SPLASH, Moves.PSYCHIC_TERRAIN ]);
await game.classicMode.startBattle([ Species.REGIELEKI ]);

const playerPokemon = game.scene.getPlayerPokemon();
game.move.select(Moves.TRANSFORM);
await game.forceEnemyMove(Moves.PSYCHIC_TERRAIN);
await game.toNextTurn();
expect(playerPokemon?.getTypes().includes(Type.PSYCHIC)).toBe(true);

if (game.scene.arena.terrain) {
game.scene.arena.terrain.turnsLeft = 1;
}

game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
expect(playerPokemon?.getTypes().includes(Type.ELECTRIC)).toBe(true);
});

it("If the Pokemon is under the effect of a type-adding move and an equivalent terrain activates, the move's effect disappears", async () => {
game.override
.enemyMoveset([ Moves.FORESTS_CURSE, Moves.GRASSY_TERRAIN ]);
await game.classicMode.startBattle([ Species.FEEBAS ]);

const playerPokemon = game.scene.getPlayerPokemon();
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.FORESTS_CURSE);
await game.toNextTurn();

expect(playerPokemon?.summonData.addedType).toBe(Type.GRASS);

game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.GRASSY_TERRAIN);
await game.phaseInterceptor.to("TurnEndPhase");

expect(playerPokemon?.summonData.addedType).toBeNull();
expect(playerPokemon?.getTypes().includes(Type.GRASS)).toBe(true);
});
});
47 changes: 47 additions & 0 deletions src/test/moves/forests_curse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Type } from "#app/data/type";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";

describe("Moves - Forest's Curse", () => {
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
.moveset([ Moves.FORESTS_CURSE, Moves.TRICK_OR_TREAT ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});

it("will replace the added type from Trick Or Treat", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);

const enemyPokemon = game.scene.getEnemyPokemon();
game.move.select(Moves.TRICK_OR_TREAT);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemyPokemon!.summonData.addedType).toBe(Type.GHOST);

game.move.select(Moves.FORESTS_CURSE);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemyPokemon?.summonData.addedType).toBe(Type.GRASS);
});
});
Loading

0 comments on commit fb2d3e4

Please sign in to comment.