Fixed synchronoise and added tests

This commit is contained in:
Bertie690 2025-08-18 23:59:07 -04:00
parent 7e402d02b0
commit 9187047b94
5 changed files with 96 additions and 42 deletions

View File

@ -5352,27 +5352,26 @@ export class CombinedPledgeTypeAttr extends VariableMoveTypeAttr {
/** /**
* Attribute for moves which have a custom type chart interaction. * 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 user - The {@linkcode Pokemon} using the move
* @param target - The {@linkcode Pokemon} targeted by the move * @param target - The {@linkcode Pokemon} targeted by the move
* @param move - The {@linkcode Move} with this attribute * @param move - The {@linkcode Move} with this attribute
* @param args - * @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 * `[1]`: The target's entire defensive type profile
* `[2]`: The current {@linkcode PokemonType} of the move * `[2]`: The current {@linkcode PokemonType} of the move
* @returns `true` if application of the attribute succeeds * @returns `true` if application of the attribute succeeds
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean { abstract public override apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean;
return false;
}
} }
/** /**
* Attribute to implement {@linkcode MoveId.FREEZE_DRY}'s guaranteed water type super effectiveness. * Attribute to implement {@linkcode MoveId.FREEZE_DRY}'s guaranteed water type super effectiveness.
*/ */
export class FreezeDryAttr extends VariableMoveTypeChartAttr { 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; const [multiplier, types, moveType] = args;
if (!types.includes(PokemonType.WATER)) { if (!types.includes(PokemonType.WATER)) {
return false; return false;
@ -5390,7 +5389,7 @@ export class FreezeDryAttr extends VariableMoveTypeChartAttr {
* against all ungrounded flying types. * against all ungrounded flying types.
*/ */
export class NeutralDamageAgainstFlyingTypeAttr extends VariableMoveTypeChartAttr { 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; const [multiplier, types] = args;
if (target.isGrounded() || !types.includes(PokemonType.FLYING)) { if (target.isGrounded() || !types.includes(PokemonType.FLYING)) {
return false; 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. * 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. * 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. * 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, VariableMoveTypeChartAttr,
FreezeDryAttr, FreezeDryAttr,
OneHitKOAccuracyAttr, OneHitKOAccuracyAttr,
HitsSameTypeAttr,
SheerColdAccuracyAttr, SheerColdAccuracyAttr,
MissEffectAttr, MissEffectAttr,
NoEffectAttr, NoEffectAttr,
@ -8487,7 +8494,7 @@ const MoveAttrs = Object.freeze({
VariableTargetAttr, VariableTargetAttr,
AfterYouAttr, AfterYouAttr,
ForceLastAttr, ForceLastAttr,
HitsSameTypeAttr, ,
ResistLastMoveTypeAttr, ResistLastMoveTypeAttr,
ExposedMoveAttr, ExposedMoveAttr,
}); });
@ -10010,9 +10017,8 @@ export function initMoves() {
.attr(CompareWeightPowerAttr) .attr(CompareWeightPowerAttr)
.attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED), .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED),
new AttackMove(MoveId.SYNCHRONOISE, PokemonType.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 5) new AttackMove(MoveId.SYNCHRONOISE, PokemonType.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 5)
.target(MoveTarget.ALL_NEAR_OTHERS) .attr(HitsSameTypeAttr)
.condition(unknownTypeCondition) .target(MoveTarget.ALL_NEAR_OTHERS),
.attr(HitsSameTypeAttr),
new AttackMove(MoveId.ELECTRO_BALL, PokemonType.ELECTRIC, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 5) new AttackMove(MoveId.ELECTRO_BALL, PokemonType.ELECTRIC, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 5)
.attr(ElectroBallPowerAttr) .attr(ElectroBallPowerAttr)
.ballBombMove(), .ballBombMove(),

View File

@ -2547,7 +2547,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
// Apply any typing changes from Freeze-Dry, etc. // Apply any typing changes from Freeze-Dry, etc.
if (move) { 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 // Handle strong winds lowering effectiveness of types super effective against pure flying

View File

@ -1,10 +1,12 @@
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { BattlerIndex } from "#enums/battler-index";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { MoveResult } from "#enums/move-result";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { GameManager } from "#test/test-utils/game-manager"; import { GameManager } from "#test/test-utils/game-manager";
import Phaser from "phaser"; 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", () => { describe("Moves - Synchronoise", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -23,7 +25,6 @@ describe("Moves - Synchronoise", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override game.override
.moveset([MoveId.SYNCHRONOISE])
.ability(AbilityId.BALL_FETCH) .ability(AbilityId.BALL_FETCH)
.battleStyle("single") .battleStyle("single")
.criticalHits(false) .criticalHits(false)
@ -32,25 +33,65 @@ describe("Moves - Synchronoise", () => {
.enemyMoveset(MoveId.SPLASH); .enemyMoveset(MoveId.SPLASH);
}); });
// TODO: Write test it("should affect all opponents that share a type with the user", async () => {
it.todo("should affect all opponents that share a type with the user"); 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 () => { it("should consider the user's Tera Type if it is Terastallized", async () => {
await game.classicMode.startBattle([SpeciesId.BIDOOF]); await game.classicMode.startBattle([SpeciesId.BIDOOF]);
const playerPokemon = game.field.getPlayerPokemon(); const bidoof = game.field.getPlayerPokemon();
const enemyPokemon = game.field.getEnemyPokemon(); const karp = game.field.getEnemyPokemon();
playerPokemon.teraType = PokemonType.WATER; game.field.forceTera(bidoof, PokemonType.WATER);
game.move.selectWithTera(MoveId.SYNCHRONOISE); game.move.use(MoveId.SYNCHRONOISE);
await game.toEndOfTurn(); await game.toEndOfTurn();
expect(enemyPokemon).not.toHaveFullHp(); expect(bidoof).toHaveUsedMove({ move: MoveId.SYNCHRONOISE, result: MoveResult.SUCCESS });
expect(karp).not.toHaveFullHp();
}); });
// TODO: Write test it("should consider the user/target's normal types if Terastallized into Tera Stellar", async () => {
it.todo("should fail if no opponents share a type with the user"); await game.classicMode.startBattle([SpeciesId.ABRA]);
// TODO: Write test const abra = game.field.getPlayerPokemon();
it.todo("should fail if the user is typeless"); 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();
});
}); });

View File

@ -1,5 +1,6 @@
/* biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */ /* biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */
import type { globalScene } from "#app/global-scene"; 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 */ /* biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
import type { Ability } from "#abilities/ability"; 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 pokemon - The pokemon to Terastallize
* @param teraType - The {@linkcode PokemonType} to terastallize into; defaults to the pokemon's primary type. * @param teraType - The {@linkcode PokemonType} to Terastallize into; defaults to `pokemon`'s primary type if not provided
* @remarks * @remarks
* This function only mocks the Pokemon's tera-related variables; it does NOT activate any tera-related abilities. * 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 { public forceTera(pokemon: Pokemon, teraType: PokemonType = pokemon.getSpeciesForm(true).type1): void {
vi.spyOn(pokemon, "isTerastallized", "get").mockReturnValue(true); vi.spyOn(pokemon, "isTerastallized", "get").mockReturnValue(true);

View File

@ -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 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 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. * @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) * 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( public selectWithTera(
move: MoveId, move: MoveId,