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.
*/
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(),

View File

@ -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

View File

@ -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();
});
});

View File

@ -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);

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 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,