Compare commits

...

24 Commits

Author SHA1 Message Date
Dean c6d2203353
Merge a27ccbf9c0 into ca0522436a 2025-01-16 20:37:09 -08:00
Scooom ca0522436a
[Challenge] Make the Flip Inverse Challenge Secret (#5133)
Co-authored-by: Scooom <jerry@hosterslice.com>
2025-01-16 16:50:19 -08:00
Dean a27ccbf9c0 Use globalScene 2025-01-16 11:26:43 -08:00
Dean 59d1dcedd6
Merge branch 'beta' into quash 2025-01-16 11:11:37 -08:00
Dean 8efe67c3c7
Update move.ts (readability)
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2025-01-09 12:34:30 -08:00
Dean 054bd031ce
Merge branch 'beta' into quash 2025-01-07 20:07:54 -08:00
Dean 50bd949a40 Add move text 2024-12-27 16:57:22 -08:00
Dean f4ad6381d2 Quash does fail in a single battle despite this not being documented anywhere 2024-12-27 15:27:13 -08:00
Dean ccf704d97d Spacing 2024-12-27 15:22:26 -08:00
Dean 71d5d23dcd Avoid reapplying if a move is already forced last 2024-12-27 15:19:42 -08:00
Dean cee65c4808 Allow for quashed speed ties 2024-12-27 15:09:43 -08:00
Dean 66e7db82de Add comments, fix var name 2024-12-27 14:53:57 -08:00
Dean 2fbcd28ea8 Test for respecting TR 2024-12-27 14:47:54 -08:00
Dean 22a2905cf8 Respect trick room in quash turn order 2024-12-27 14:47:34 -08:00
Dean c1f6303260 Fix ForceLastAttr to properly respect speed order 2024-12-27 14:35:48 -08:00
Dean 2a05517010 Fix speed test comment 2024-12-27 13:49:16 -08:00
Dean 71ecc683e4 Speed order test 2024-12-27 13:48:39 -08:00
Dean bde258bdfb Test for failure on a already moved target 2024-12-27 13:23:25 -08:00
Dean 88724b83eb Basic test case 2024-12-27 13:01:07 -08:00
Dean a1600161c1 Use findPhase instead of looping to search 2024-12-27 12:04:17 -08:00
Dean fadc2e526d Start searching from front of phaseQueue instead of weather 2024-12-27 11:35:07 -08:00
Dean f81c713380 Update MovePhase constructor 2024-12-27 11:27:35 -08:00
Dean bdbeaac104 Multi-squash power 2024-12-27 01:38:04 -08:00
Dean f6836b26a4 Add quash logic for single targets 2024-12-26 23:48:20 -08:00
4 changed files with 165 additions and 3 deletions

View File

@ -7922,6 +7922,56 @@ export class AfterYouAttr extends MoveEffectAttr {
} }
} }
/**
* Move effect to force the target to move last, ignoring priority.
* If applied to multiple targets, they move in speed order after all other moves.
* @extends MoveEffectAttr
*/
export class ForceLastAttr extends MoveEffectAttr {
/**
* Forces the target of this move to move last.
*
* @param user {@linkcode Pokemon} that is using the move.
* @param target {@linkcode Pokemon} that will be forced to move last.
* @param move {@linkcode Move} {@linkcode Moves.QUASH}
* @param _args N/A
* @returns true
*/
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
globalScene.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) }));
const targetMovePhase = globalScene.findPhase<MovePhase>((phase) => phase.pokemon === target);
if (targetMovePhase && !targetMovePhase.isForcedLast() && globalScene.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
// Finding the phase to insert the move in front of -
// Either the end of the turn or in front of another, slower move which has also been forced last
const prependPhase = globalScene.findPhase((phase) =>
[ MovePhase, MoveEndPhase ].every(cls => !(phase instanceof cls))
|| (phase instanceof MovePhase) && phaseForcedSlower(phase, target, !!globalScene.arena.getTag(ArenaTagType.TRICK_ROOM))
);
if (prependPhase) {
globalScene.phaseQueue.splice(
globalScene.phaseQueue.indexOf(prependPhase),
0,
new MovePhase(target, [ ...targetMovePhase.targets ], targetMovePhase.move, false, false, true)
);
}
}
return true;
}
}
/** Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target} */
const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean): boolean => {
let slower: boolean;
// quashed pokemon still have speed ties
if (phase.pokemon.getEffectiveStat(Stat.SPD) === target.getEffectiveStat(Stat.SPD)) {
slower = !!target.randSeedInt(2);
} else {
slower = !trickRoom ? phase.pokemon.getEffectiveStat(Stat.SPD) < target.getEffectiveStat(Stat.SPD) : phase.pokemon.getEffectiveStat(Stat.SPD) > target.getEffectiveStat(Stat.SPD);
}
return phase.isForcedLast() && slower;
};
const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY); const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY);
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune(); const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
@ -9725,7 +9775,8 @@ export function initMoves() {
.attr(RemoveHeldItemAttr, true), .attr(RemoveHeldItemAttr, true),
new StatusMove(Moves.QUASH, Type.DARK, 100, 15, -1, 0, 5) new StatusMove(Moves.QUASH, Type.DARK, 100, 15, -1, 0, 5)
.condition(failIfSingleBattle) .condition(failIfSingleBattle)
.unimplemented(), .condition((user, target, move) => !target.turnData.acted)
.attr(ForceLastAttr),
new AttackMove(Moves.ACROBATICS, Type.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5) new AttackMove(Moves.ACROBATICS, Type.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5)
.attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferable).reduce((v, m) => v + m.stackCount, 0))), .attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferable).reduce((v, m) => v + m.stackCount, 0))),
new StatusMove(Moves.REFLECT_TYPE, Type.NORMAL, -1, 15, -1, 0, 5) new StatusMove(Moves.REFLECT_TYPE, Type.NORMAL, -1, 15, -1, 0, 5)

View File

@ -56,6 +56,7 @@ export class MovePhase extends BattlePhase {
protected _targets: BattlerIndex[]; protected _targets: BattlerIndex[];
protected followUp: boolean; protected followUp: boolean;
protected ignorePp: boolean; protected ignorePp: boolean;
protected forcedLast: boolean;
protected failed: boolean = false; protected failed: boolean = false;
protected cancelled: boolean = false; protected cancelled: boolean = false;
@ -87,7 +88,8 @@ export class MovePhase extends BattlePhase {
* @param followUp Indicates that the move being uses is a "follow-up" - for example, a move being used by Metronome or Dancer. * @param followUp Indicates that the move being uses is a "follow-up" - for example, a move being used by Metronome or Dancer.
* Follow-ups bypass a few failure conditions, including flinches, sleep/paralysis/freeze and volatile status checks, etc. * Follow-ups bypass a few failure conditions, including flinches, sleep/paralysis/freeze and volatile status checks, etc.
*/ */
constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp: boolean = false, ignorePp: boolean = false) {
constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp: boolean = false, ignorePp: boolean = false, forcedLast: boolean = false) {
super(); super();
this.pokemon = pokemon; this.pokemon = pokemon;
@ -95,6 +97,7 @@ export class MovePhase extends BattlePhase {
this.move = move; this.move = move;
this.followUp = followUp; this.followUp = followUp;
this.ignorePp = ignorePp; this.ignorePp = ignorePp;
this.forcedLast = forcedLast;
} }
/** /**
@ -116,6 +119,15 @@ export class MovePhase extends BattlePhase {
this.cancelled = true; this.cancelled = true;
} }
/**
* Shows whether the current move has been forced to the end of the turn
* Needed for speed order, see {@linkcode Moves.QUASH}
* */
public isForcedLast(): boolean {
return this.forcedLast;
}
public start(): void { public start(): void {
super.start(); super.start();

View File

@ -365,7 +365,7 @@ export const achvs = {
FRESH_START: new ChallengeAchv("FRESH_START", "", "FRESH_START.description", "reviver_seed", 100, (c) => c instanceof FreshStartChallenge && c.value > 0 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)), FRESH_START: new ChallengeAchv("FRESH_START", "", "FRESH_START.description", "reviver_seed", 100, (c) => c instanceof FreshStartChallenge && c.value > 0 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
INVERSE_BATTLE: new ChallengeAchv("INVERSE_BATTLE", "", "INVERSE_BATTLE.description", "inverse", 100, (c) => c instanceof InverseBattleChallenge && c.value > 0), INVERSE_BATTLE: new ChallengeAchv("INVERSE_BATTLE", "", "INVERSE_BATTLE.description", "inverse", 100, (c) => c instanceof InverseBattleChallenge && c.value > 0),
FLIP_STATS: new ChallengeAchv("FLIP_STATS", "", "FLIP_STATS.description", "dubious_disc", 100, (c) => c instanceof FlipStatChallenge && c.value > 0), FLIP_STATS: new ChallengeAchv("FLIP_STATS", "", "FLIP_STATS.description", "dubious_disc", 100, (c) => c instanceof FlipStatChallenge && c.value > 0),
FLIP_INVERSE: new ChallengeAchv("FLIP_INVERSE", "", "FLIP_INVERSE.description", "cracked_pot", 100, (c) => c instanceof FlipStatChallenge && c.value > 0 && globalScene.gameMode.challenges.every(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)), FLIP_INVERSE: new ChallengeAchv("FLIP_INVERSE", "", "FLIP_INVERSE.description", "cracked_pot", 100, (c) => c instanceof FlipStatChallenge && c.value > 0 && globalScene.gameMode.challenges.every(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)).setSecret(),
BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 50).setSecret(), BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 50).setSecret(),
}; };

View File

@ -0,0 +1,99 @@
import { Species } from "#enums/species";
import { Moves } from "#enums/moves";
import { Abilities } from "#app/enums/abilities";
import { BattlerIndex } from "#app/battle";
import { WeatherType } from "#enums/weather-type";
import { MoveResult } from "#app/field/pokemon";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { describe, beforeAll, afterEach, beforeEach, it, expect } from "vitest";
describe("Moves - Quash", () => {
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("double")
.enemyLevel(1)
.enemySpecies(Species.SLOWPOKE)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset([ Moves.RAIN_DANCE, Moves.SPLASH ])
.ability(Abilities.BALL_FETCH)
.moveset([ Moves.QUASH, Moves.SUNNY_DAY, Moves.RAIN_DANCE, Moves.SPLASH ]);
});
it("makes the target move last in a turn, ignoring priority", async () => {
await game.classicMode.startBattle([ Species.ACCELGOR, Species.RATTATA ]);
game.move.select(Moves.QUASH, 0, BattlerIndex.PLAYER_2);
game.move.select(Moves.SUNNY_DAY, 1);
await game.forceEnemyMove(Moves.SPLASH);
await game.forceEnemyMove(Moves.RAIN_DANCE);
await game.phaseInterceptor.to("TurnEndPhase", false);
// will be sunny if player_2 moved last because of quash, rainy otherwise
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SUNNY);
});
it("fails if the target has already moved", async () => {
await game.classicMode.startBattle([ Species.ACCELGOR, Species.RATTATA ]);
game.move.select(Moves.SPLASH, 0);
game.move.select(Moves.QUASH, 1, BattlerIndex.PLAYER);
await game.phaseInterceptor.to("MoveEndPhase");
await game.phaseInterceptor.to("MoveEndPhase");
expect(game.scene.getPlayerField()[1].getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
});
it("makes multiple quashed targets move in speed order at the end of the turn", async () => {
game.override.enemySpecies(Species.NINJASK)
.enemyLevel(100);
await game.classicMode.startBattle([ Species.ACCELGOR, Species.RATTATA ]);
// both users are quashed - rattata is slower so sun should be up at end of turn
game.move.select(Moves.RAIN_DANCE, 0);
game.move.select(Moves.SUNNY_DAY, 1);
await game.forceEnemyMove(Moves.QUASH, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.QUASH, BattlerIndex.PLAYER_2);
await game.phaseInterceptor.to("TurnEndPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SUNNY);
});
it("respects trick room", async () => {
game.override.enemyMoveset([ Moves.RAIN_DANCE, Moves.SPLASH, Moves.TRICK_ROOM ]);
await game.classicMode.startBattle([ Species.ACCELGOR, Species.RATTATA ]);
game.move.select(Moves.SPLASH, 0);
game.move.select(Moves.SPLASH, 1);
await game.forceEnemyMove(Moves.TRICK_ROOM);
await game.forceEnemyMove(Moves.SPLASH);
await game.phaseInterceptor.to("TurnInitPhase");
// both users are quashed - accelgor should move last w/ TR so rain should be up at end of turn
game.move.select(Moves.RAIN_DANCE, 0);
game.move.select(Moves.SUNNY_DAY, 1);
await game.forceEnemyMove(Moves.QUASH, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.QUASH, BattlerIndex.PLAYER_2);
await game.phaseInterceptor.to("TurnEndPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.RAIN);
});
});