diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 4f6a73f759f..7c54ff6c0bc 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -5352,27 +5352,26 @@ export class CombinedPledgeTypeAttr extends VariableMoveTypeAttr { /** * Attribute for moves which have a custom type chart interaction. */ -export class VariableMoveTypeChartAttr extends MoveAttr { +export abstract class VariableMoveTypeChartAttr extends MoveAttr { /** + * Apply the attribute to change the move's type effectiveness multiplier. * @param user - The {@linkcode Pokemon} using the move * @param target - The {@linkcode Pokemon} targeted by the move * @param move - The {@linkcode Move} with this attribute * @param args - - * `[0]`: A {@linkcode NumberHolder} holding the type effectiveness + * `[0]`: A {@linkcode NumberHolder} holding the current type effectiveness * `[1]`: The target's entire defensive type profile * `[2]`: The current {@linkcode PokemonType} of the move * @returns `true` if application of the attribute succeeds */ - apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean { - return false; - } + abstract public override apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean; } /** * Attribute to implement {@linkcode MoveId.FREEZE_DRY}'s guaranteed water type super effectiveness. */ export class FreezeDryAttr extends VariableMoveTypeChartAttr { - apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean { + public override apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean { const [multiplier, types, moveType] = args; if (!types.includes(PokemonType.WATER)) { return false; @@ -5390,7 +5389,7 @@ export class FreezeDryAttr extends VariableMoveTypeChartAttr { * against all ungrounded flying types. */ export class NeutralDamageAgainstFlyingTypeAttr extends VariableMoveTypeChartAttr { - apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean { + public override apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean { const [multiplier, types] = args; if (target.isGrounded() || !types.includes(PokemonType.FLYING)) { return false; @@ -5401,6 +5400,26 @@ export class NeutralDamageAgainstFlyingTypeAttr extends VariableMoveTypeChartAtt } } +/** + * Attribute used by {@linkcode MoveId.SYNCHRONOISE} to render the move ineffective + * against all targets who do not share a type with the user. + */ +export class HitsSameTypeAttr extends VariableMoveTypeChartAttr { + public override apply(user: Pokemon, _target: Pokemon, _move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean { + const [multiplier, types] = args; + if (user.getTypes(true).eev(type => types.includes(type))) { + return false; + } + multiplier.value = 0; + return true; + } + + getCondition(): MoveConditionFunc { + return unknownTypeCondition; + } +} + + /** * Attribute used by {@linkcode MoveId.FLYING_PRESS} to add the Flying Type to its type effectiveness. */ @@ -8140,19 +8159,6 @@ export class UpperHandCondition extends MoveCondition { } } -// TODO: Does this need to extend from this? -// The only reason it might is to show ineffectiveness text but w/e -export class HitsSameTypeAttr extends VariableMoveTypeChartAttr { - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const multiplier = args[0] as NumberHolder; - if (!user.getTypes(true).some(type => target.getTypes(true).includes(type))) { - multiplier.value = 0; - return true; - } - return false; - } -} - /** * Attribute used for Conversion 2, to convert the user's type to a random type that resists the target's last used move. * Fails if the user already has ALL types that resist the target's last used move. @@ -8419,6 +8425,7 @@ const MoveAttrs = Object.freeze({ VariableMoveTypeChartAttr, FreezeDryAttr, OneHitKOAccuracyAttr, + HitsSameTypeAttr, SheerColdAccuracyAttr, MissEffectAttr, NoEffectAttr, @@ -8487,7 +8494,7 @@ const MoveAttrs = Object.freeze({ VariableTargetAttr, AfterYouAttr, ForceLastAttr, - HitsSameTypeAttr, + , ResistLastMoveTypeAttr, ExposedMoveAttr, }); @@ -10010,9 +10017,8 @@ export function initMoves() { .attr(CompareWeightPowerAttr) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED), new AttackMove(MoveId.SYNCHRONOISE, PokemonType.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 5) - .target(MoveTarget.ALL_NEAR_OTHERS) - .condition(unknownTypeCondition) - .attr(HitsSameTypeAttr), + .attr(HitsSameTypeAttr) + .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(MoveId.ELECTRO_BALL, PokemonType.ELECTRIC, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 5) .attr(ElectroBallPowerAttr) .ballBombMove(), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index e5c4cb3a46a..d8dcd07442d 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2547,7 +2547,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { // Apply any typing changes from Freeze-Dry, etc. if (move) { - applyMoveAttrs("VariableMoveTypeChartAttr", null, this, move, multi, types, moveType); + applyMoveAttrs("VariableMoveTypeChartAttr", source ?? null, this, move, multi, types, moveType); } // Handle strong winds lowering effectiveness of types super effective against pure flying diff --git a/test/moves/synchronoise.test.ts b/test/moves/synchronoise.test.ts index 4fb1a7ca161..547bb077952 100644 --- a/test/moves/synchronoise.test.ts +++ b/test/moves/synchronoise.test.ts @@ -1,10 +1,12 @@ import { AbilityId } from "#enums/ability-id"; +import { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; +import { MoveResult } from "#enums/move-result"; import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Moves - Synchronoise", () => { let phaserGame: Phaser.Game; @@ -23,7 +25,6 @@ describe("Moves - Synchronoise", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .moveset([MoveId.SYNCHRONOISE]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") .criticalHits(false) @@ -32,25 +33,65 @@ describe("Moves - Synchronoise", () => { .enemyMoveset(MoveId.SPLASH); }); - // TODO: Write test - it.todo("should affect all opponents that share a type with the user"); + it("should affect all opponents that share a type with the user", async () => { + game.override.battleStyle("double"); + await game.classicMode.startBattle([SpeciesId.BIBAREL, SpeciesId.STARLY]); + + const [bidoof, starly] = game.scene.getPlayerField(); + const [karp1, karp2] = game.scene.getEnemyField(); + // Mock 2nd magikarp to be a completely different type + vi.spyOn(karp2, "getTypes").mockReturnValue([PokemonType.GRASS]); + + game.move.use(MoveId.SYNCHRONOISE, BattlerIndex.PLAYER); + game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2); + await game.toEndOfTurn(); + + expect(bidoof).toHaveUsedMove({ move: MoveId.SYNCHRONOISE, result: MoveResult.SUCCESS }); + expect(starly).not.toHaveFullHp(); + expect(karp1).not.toHaveFullHp(); + expect(karp2).toHaveFullHp(); + }); it("should consider the user's Tera Type if it is Terastallized", async () => { await game.classicMode.startBattle([SpeciesId.BIDOOF]); - const playerPokemon = game.field.getPlayerPokemon(); - const enemyPokemon = game.field.getEnemyPokemon(); + const bidoof = game.field.getPlayerPokemon(); + const karp = game.field.getEnemyPokemon(); - playerPokemon.teraType = PokemonType.WATER; - game.move.selectWithTera(MoveId.SYNCHRONOISE); + game.field.forceTera(bidoof, PokemonType.WATER); + game.move.use(MoveId.SYNCHRONOISE); await game.toEndOfTurn(); - expect(enemyPokemon).not.toHaveFullHp(); + expect(bidoof).toHaveUsedMove({ move: MoveId.SYNCHRONOISE, result: MoveResult.SUCCESS }); + expect(karp).not.toHaveFullHp(); }); - // TODO: Write test - it.todo("should fail if no opponents share a type with the user"); + it("should consider the user/target's normal types if Terastallized into Tera Stellar", async () => { + await game.classicMode.startBattle([SpeciesId.ABRA]); - // TODO: Write test - it.todo("should fail if the user is typeless"); + const abra = game.field.getPlayerPokemon(); + const karp = game.field.getEnemyPokemon(); + + game.field.forceTera(abra, PokemonType.STELLAR); + game.field.forceTera(karp, PokemonType.STELLAR); + game.move.use(MoveId.SYNCHRONOISE); + await game.toEndOfTurn(); + + expect(abra).toHaveUsedMove({ move: MoveId.SYNCHRONOISE, result: MoveResult.MISS }); + expect(karp).toHaveFullHp(); + }); + + it("should count as ineffective if no enemies share types with the user", async () => { + await game.classicMode.startBattle([SpeciesId.BIBAREL]); + + const bibarel = game.field.getPlayerPokemon(); + const karp = game.field.getEnemyPokemon(); + bibarel.summonData.types = [PokemonType.UNKNOWN]; + + game.move.use(MoveId.SYNCHRONOISE); + await game.toEndOfTurn(); + + expect(bibarel).toHaveUsedMove({ move: MoveId.SYNCHRONOISE, result: MoveResult.MISS }); + expect(karp).toHaveFullHp(); + }); }); diff --git a/test/test-utils/helpers/field-helper.ts b/test/test-utils/helpers/field-helper.ts index 2d8fd8ee701..4809a5b1106 100644 --- a/test/test-utils/helpers/field-helper.ts +++ b/test/test-utils/helpers/field-helper.ts @@ -1,5 +1,6 @@ /* biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */ import type { globalScene } from "#app/global-scene"; +import type { MoveHelper } from "#test/test-utils/helpers/move-helper"; /* biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */ import type { Ability } from "#abilities/ability"; @@ -75,12 +76,14 @@ export class FieldHelper extends GameManagerHelper { } /** - * Force a given Pokemon to be terastallized to the given type. + * Force a given Pokemon to be Terastallized to the given type. * - * @param pokemon - The pokemon to terastallize. - * @param teraType - The {@linkcode PokemonType} to terastallize into; defaults to the pokemon's primary type. + * @param pokemon - The pokemon to Terastallize + * @param teraType - The {@linkcode PokemonType} to Terastallize into; defaults to `pokemon`'s primary type if not provided * @remarks * This function only mocks the Pokemon's tera-related variables; it does NOT activate any tera-related abilities. + * If activating on-Terastallize effects is desired, use either {@linkcode MoveHelper.use} with `useTera=true`, + * or {@linkcode MoveHelper.selectWithTera} instead. */ public forceTera(pokemon: Pokemon, teraType: PokemonType = pokemon.getSpeciesForm(true).type1): void { vi.spyOn(pokemon, "isTerastallized", "get").mockReturnValue(true); diff --git a/test/test-utils/helpers/move-helper.ts b/test/test-utils/helpers/move-helper.ts index 6a01e4110da..928cffdd8b1 100644 --- a/test/test-utils/helpers/move-helper.ts +++ b/test/test-utils/helpers/move-helper.ts @@ -97,11 +97,15 @@ export class MoveHelper extends GameManagerHelper { } /** - * Select a move _already in the player's moveset_ to be used during the next {@linkcode CommandPhase}, **which will also terastallize on this turn**. + * Select a move _already in the player's moveset_ to be used during the next {@linkcode CommandPhase}, + * **which will also terastallize on this turn**. + * Activates all relevant abilities and effects on Terastallizing (equivalent to inputting the command manually) * @param move - The {@linkcode MoveId} to use. * @param pkmIndex - The {@linkcode BattlerIndex} of the player Pokemon using the move. Relevant for double battles only and defaults to {@linkcode BattlerIndex.PLAYER} if not specified. * @param targetIndex - The {@linkcode BattlerIndex} of the Pokemon to target for single-target moves; should be omitted for multi-target moves. * If set to `null`, will forgo normal target selection entirely (useful for UI tests) + * @remarks + * Will fail the current test if the move being selected is not in the user's moveset. */ public selectWithTera( move: MoveId,