mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2024-11-26 00:36:25 +00:00
Merge branch 'beta' into trick
This commit is contained in:
commit
876d0894be
@ -70,7 +70,6 @@ const content = `import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import { SPLASH_ONLY } from "#test/utils/testUtils";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest";
|
||||
|
||||
@ -95,7 +94,7 @@ describe("${description}", () => {
|
||||
.moveset([Moves.SPLASH])
|
||||
.battleType("single")
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(SPLASH_ONLY);
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
|
||||
it("test case", async () => {
|
||||
|
@ -2193,8 +2193,14 @@ export default class BattleScene extends SceneBase {
|
||||
return true;
|
||||
}
|
||||
|
||||
findPhase(phaseFilter: (phase: Phase) => boolean): Phase | undefined {
|
||||
return this.phaseQueue.find(phaseFilter);
|
||||
/**
|
||||
* Find a specific {@linkcode Phase} in the phase queue.
|
||||
*
|
||||
* @param phaseFilter filter function to use to find the wanted phase
|
||||
* @returns the found phase or undefined if none found
|
||||
*/
|
||||
findPhase<P extends Phase = Phase>(phaseFilter: (phase: P) => boolean): P | undefined {
|
||||
return this.phaseQueue.find(phaseFilter) as P;
|
||||
}
|
||||
|
||||
tryReplacePhase(phaseFilter: (phase: Phase) => boolean, phase: Phase): boolean {
|
||||
|
@ -1595,8 +1595,8 @@ export class PostAttackAbAttr extends AbAttr {
|
||||
private attackCondition: PokemonAttackCondition;
|
||||
|
||||
/** The default attackCondition requires that the selected move is a damaging move */
|
||||
constructor(attackCondition: PokemonAttackCondition = (user, target, move) => (move.category !== MoveCategory.STATUS)) {
|
||||
super();
|
||||
constructor(attackCondition: PokemonAttackCondition = (user, target, move) => (move.category !== MoveCategory.STATUS), showAbility: boolean = true) {
|
||||
super(showAbility);
|
||||
|
||||
this.attackCondition = attackCondition;
|
||||
}
|
||||
@ -1624,6 +1624,40 @@ export class PostAttackAbAttr extends AbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ability attribute for Gorilla Tactics
|
||||
* @extends PostAttackAbAttr
|
||||
*/
|
||||
export class GorillaTacticsAbAttr extends PostAttackAbAttr {
|
||||
constructor() {
|
||||
super((user, target, move) => true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Pokemon} pokemon the {@linkcode Pokemon} with this ability
|
||||
* @param passive n/a
|
||||
* @param simulated whether the ability is being simulated
|
||||
* @param defender n/a
|
||||
* @param move n/a
|
||||
* @param hitResult n/a
|
||||
* @param args n/a
|
||||
* @returns `true` if the ability is applied
|
||||
*/
|
||||
applyPostAttackAfterMoveTypeCheck(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean | Promise<boolean> {
|
||||
if (simulated) {
|
||||
return simulated;
|
||||
}
|
||||
|
||||
if (pokemon.getTag(BattlerTagType.GORILLA_TACTICS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pokemon.addTag(BattlerTagType.GORILLA_TACTICS);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr {
|
||||
private stealCondition: PokemonAttackCondition | null;
|
||||
|
||||
@ -5597,7 +5631,7 @@ export function initAbilities() {
|
||||
.bypassFaint()
|
||||
.partial(),
|
||||
new Ability(Abilities.GORILLA_TACTICS, 8)
|
||||
.unimplemented(),
|
||||
.attr(GorillaTacticsAbAttr),
|
||||
new Ability(Abilities.NEUTRALIZING_GAS, 8)
|
||||
.attr(SuppressFieldAbilitiesAbAttr)
|
||||
.attr(UncopiableAbilityAbAttr)
|
||||
|
@ -119,7 +119,9 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag {
|
||||
const move = phase.move;
|
||||
|
||||
if (this.isMoveRestricted(move.moveId)) {
|
||||
pokemon.scene.queueMessage(this.interruptedText(pokemon, move.moveId));
|
||||
if (this.interruptedText(pokemon, move.moveId)) {
|
||||
pokemon.scene.queueMessage(this.interruptedText(pokemon, move.moveId));
|
||||
}
|
||||
phase.cancel();
|
||||
}
|
||||
|
||||
@ -155,7 +157,9 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag {
|
||||
* @param {Moves} move {@linkcode Moves} ID of the move being interrupted
|
||||
* @returns {string} text to display when the move is interrupted
|
||||
*/
|
||||
abstract interruptedText(pokemon: Pokemon, move: Moves): string;
|
||||
interruptedText(pokemon: Pokemon, move: Moves): string {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -221,7 +225,7 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
|
||||
/**
|
||||
* @override
|
||||
*
|
||||
* Ensures that move history exists on `pokemon` and has a valid move. If so, sets the {@link moveId} and shows a message.
|
||||
* Ensures that move history exists on `pokemon` and has a valid move. If so, sets the {@linkcode moveId} and shows a message.
|
||||
* Otherwise the move ID will not get assigned and this tag will get removed next turn.
|
||||
*/
|
||||
override onAdd(pokemon: Pokemon): void {
|
||||
@ -250,7 +254,12 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
|
||||
return i18next.t("battle:moveDisabled", { moveName: allMoves[move].name });
|
||||
}
|
||||
|
||||
/** @override */
|
||||
/**
|
||||
* @override
|
||||
* @param {Pokemon} pokemon {@linkcode Pokemon} attempting to use the restricted move
|
||||
* @param {Moves} move {@linkcode Moves} ID of the move being interrupted
|
||||
* @returns {string} text to display when the move is interrupted
|
||||
*/
|
||||
override interruptedText(pokemon: Pokemon, move: Moves): string {
|
||||
return i18next.t("battle:disableInterruptedMove", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[move].name });
|
||||
}
|
||||
@ -262,6 +271,72 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag used by Gorilla Tactics to restrict the user to using only one move.
|
||||
* @extends MoveRestrictionBattlerTag
|
||||
*/
|
||||
export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
|
||||
private moveId = Moves.NONE;
|
||||
|
||||
constructor() {
|
||||
super(BattlerTagType.GORILLA_TACTICS, BattlerTagLapseType.CUSTOM, 0);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
override isMoveRestricted(move: Moves): boolean {
|
||||
return move !== this.moveId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Pokemon} pokemon the {@linkcode Pokemon} to check if the tag can be added
|
||||
* @returns `true` if the pokemon has a valid move and no existing {@linkcode GorillaTacticsTag}; `false` otherwise
|
||||
*/
|
||||
override canAdd(pokemon: Pokemon): boolean {
|
||||
return (this.getLastValidMove(pokemon) !== undefined) && !pokemon.getTag(GorillaTacticsTag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that move history exists on {@linkcode Pokemon} and has a valid move.
|
||||
* If so, sets the {@linkcode moveId} and increases the user's Attack by 50%.
|
||||
* @override
|
||||
* @param {Pokemon} pokemon the {@linkcode Pokemon} to add the tag to
|
||||
*/
|
||||
override onAdd(pokemon: Pokemon): void {
|
||||
const lastValidMove = this.getLastValidMove(pokemon);
|
||||
|
||||
if (!lastValidMove) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.moveId = lastValidMove;
|
||||
pokemon.setStat(Stat.ATK, pokemon.getStat(Stat.ATK, false) * 1.5, false);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
* @param {Pokemon} pokemon n/a
|
||||
* @param {Moves} move {@linkcode Moves} ID of the move being denied
|
||||
* @returns {string} text to display when the move is denied
|
||||
*/
|
||||
override selectionDeniedText(pokemon: Pokemon, move: Moves): string {
|
||||
return i18next.t("battle:canOnlyUseMove", { moveName: allMoves[this.moveId].name, pokemonName: getPokemonNameWithAffix(pokemon) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last valid move from the pokemon's move history.
|
||||
* @param {Pokemon} pokemon {@linkcode Pokemon} to get the last valid move from
|
||||
* @returns {Moves | undefined} the last valid move from the pokemon's move history
|
||||
*/
|
||||
getLastValidMove(pokemon: Pokemon): Moves | undefined {
|
||||
const move = pokemon.getLastXMoves()
|
||||
.find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual);
|
||||
|
||||
return move?.move;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BattlerTag that represents the "recharge" effects of moves like Hyper Beam.
|
||||
*/
|
||||
@ -2203,6 +2278,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
|
||||
return new TarShotTag();
|
||||
case BattlerTagType.THROAT_CHOPPED:
|
||||
return new ThroatChoppedTag();
|
||||
case BattlerTagType.GORILLA_TACTICS:
|
||||
return new GorillaTacticsTag();
|
||||
case BattlerTagType.NONE:
|
||||
default:
|
||||
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
|
||||
|
@ -6273,12 +6273,42 @@ export class VariableTargetAttr extends MoveAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute for {@linkcode Moves.AFTER_YOU}
|
||||
*
|
||||
* [After You - Move | Bulbapedia](https://bulbapedia.bulbagarden.net/wiki/After_You_(move))
|
||||
*/
|
||||
export class AfterYouAttr extends MoveEffectAttr {
|
||||
/**
|
||||
* Allows the target of this move to act right after the user.
|
||||
*
|
||||
* @param user {@linkcode Pokemon} that is using the move.
|
||||
* @param target {@linkcode Pokemon} that will move right after this move is used.
|
||||
* @param move {@linkcode Move} {@linkcode Moves.AFTER_YOU}
|
||||
* @param _args N/A
|
||||
* @returns true
|
||||
*/
|
||||
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||
user.scene.queueMessage(i18next.t("moveTriggers:afterYou", {targetName: getPokemonNameWithAffix(target)}));
|
||||
|
||||
//Will find next acting phase of the targeted pokémon, delete it and queue it next on successful delete.
|
||||
const nextAttackPhase = target.scene.findPhase<MovePhase>((phase) => phase.pokemon === target);
|
||||
if (nextAttackPhase && target.scene.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
|
||||
target.scene.prependToPhase(new MovePhase(target.scene, target, [...nextAttackPhase.targets], nextAttackPhase.move), MovePhase);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !user.scene.arena.getTag(ArenaTagType.GRAVITY);
|
||||
|
||||
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
|
||||
|
||||
const failOnMaxCondition: MoveConditionFunc = (user, target, move) => !target.isMax();
|
||||
|
||||
const failIfSingleBattle: MoveConditionFunc = (user, target, move) => user.scene.currentBattle.double;
|
||||
|
||||
const failIfDampCondition: MoveConditionFunc = (user, target, move) => {
|
||||
const cancelled = new Utils.BooleanHolder(false);
|
||||
user.scene.getField(true).map(p=>applyAbAttrs(FieldPreventExplosiveMovesAbAttr, p, cancelled));
|
||||
@ -8010,7 +8040,10 @@ export function initMoves() {
|
||||
.attr(AbilityGiveAttr),
|
||||
new StatusMove(Moves.AFTER_YOU, Type.NORMAL, -1, 15, -1, 0, 5)
|
||||
.ignoresProtect()
|
||||
.unimplemented(),
|
||||
.target(MoveTarget.NEAR_OTHER)
|
||||
.condition(failIfSingleBattle)
|
||||
.condition((user, target, move) => !target.turnData.acted)
|
||||
.attr(AfterYouAttr),
|
||||
new AttackMove(Moves.ROUND, Type.NORMAL, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 5)
|
||||
.soundBased()
|
||||
.partial(),
|
||||
|
@ -73,6 +73,7 @@ export enum BattlerTagType {
|
||||
SHELL_TRAP = "SHELL_TRAP",
|
||||
DRAGON_CHEER = "DRAGON_CHEER",
|
||||
NO_RETREAT = "NO_RETREAT",
|
||||
GORILLA_TACTICS = "GORILLA_TACTICS",
|
||||
THROAT_CHOPPED = "THROAT_CHOPPED",
|
||||
TAR_SHOT = "TAR_SHOT",
|
||||
}
|
||||
|
@ -3,5 +3,6 @@
|
||||
"power": "Stärke",
|
||||
"accuracy": "Genauigkeit",
|
||||
"abilityFlyInText": "{{passive}}{{abilityName}} von {{pokemonName}} wirkt!",
|
||||
"passive": "Passive Fähigkeit "
|
||||
"passive": "Passive Fähigkeit ",
|
||||
"teraHover": "Tera-Typ {{type}}"
|
||||
}
|
@ -67,5 +67,7 @@
|
||||
"swapArenaTags": "{{pokemonName}} hat die Effekte, die auf den beiden Seiten des Kampffeldes wirken, miteinander getauscht!",
|
||||
"trickOnSwap": "{{pokemonNameWithAffix}} tauscht Items mit dem Ziel!",
|
||||
"trickFoeNewItem": "{{pokemonNameWithAffix}} erhält {{itemName}}.",
|
||||
"safeguard": "{{targetName}} wird durch Bodyguard geschützt!"
|
||||
"exposedMove": "{{pokemonName}} erkennt {{targetPokemonName}}!",
|
||||
"safeguard": "{{targetName}} wird durch Bodyguard geschützt!",
|
||||
"afterYou": "{{targetName}} lässt sich auf Galanterie ein!"
|
||||
}
|
||||
|
@ -44,6 +44,7 @@
|
||||
"moveNotImplemented": "{{moveName}} is not yet implemented and cannot be selected.",
|
||||
"moveNoPP": "There's no PP left for\nthis move!",
|
||||
"moveDisabled": "{{moveName}} is disabled!",
|
||||
"canOnlyUseMove": "{{pokemonName}} can only use {{moveName}}!",
|
||||
"moveCannotBeSelected": "{{moveName}} cannot be selected!",
|
||||
"disableInterruptedMove": "{{pokemonNameWithAffix}}'s {{moveName}}\nis disabled!",
|
||||
"throatChopInterruptedMove": "The effects of Throat Chop prevent\n{{pokemonName}} from using certain moves!",
|
||||
|
@ -3,5 +3,6 @@
|
||||
"power": "Power",
|
||||
"accuracy": "Accuracy",
|
||||
"abilityFlyInText": " {{pokemonName}}'s {{passive}}{{abilityName}}",
|
||||
"passive": "Passive "
|
||||
"passive": "Passive ",
|
||||
"teraHover": "{{type}} Terastallized"
|
||||
}
|
@ -68,5 +68,6 @@
|
||||
"swapArenaTags": "{{pokemonName}} swapped the battle effects affecting each side of the field!",
|
||||
"trickOnSwap": "{{pokemonNameWithAffix}} switched items with its target!",
|
||||
"exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!",
|
||||
"safeguard": "{{targetName}} is protected by Safeguard!"
|
||||
"safeguard": "{{targetName}} is protected by Safeguard!",
|
||||
"afterYou": "{{pokemonName}} took the kind offer!"
|
||||
}
|
||||
|
83
src/test/abilities/gorilla_tactics.test.ts
Normal file
83
src/test/abilities/gorilla_tactics.test.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
import { Species } from "#app/enums/species";
|
||||
import { Stat } from "#app/enums/stat";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Abilities - Gorilla Tactics", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
const TIMEOUT = 20 * 1000;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleType("single")
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset([Moves.SPLASH, Moves.DISABLE])
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyLevel(30)
|
||||
.moveset([Moves.SPLASH, Moves.TACKLE, Moves.GROWL])
|
||||
.ability(Abilities.GORILLA_TACTICS);
|
||||
});
|
||||
|
||||
it("boosts the Pokémon's Attack by 50%, but limits the Pokémon to using only one move", async () => {
|
||||
await game.classicMode.startBattle([Species.GALAR_DARMANITAN]);
|
||||
|
||||
const darmanitan = game.scene.getPlayerPokemon()!;
|
||||
const initialAtkStat = darmanitan.getStat(Stat.ATK);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.SPLASH);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(darmanitan.getStat(Stat.ATK, false)).toBeCloseTo(initialAtkStat * 1.5);
|
||||
// Other moves should be restricted
|
||||
expect(darmanitan.isMoveRestricted(Moves.TACKLE)).toBe(true);
|
||||
expect(darmanitan.isMoveRestricted(Moves.SPLASH)).toBe(false);
|
||||
}, TIMEOUT);
|
||||
|
||||
it("should struggle if the only usable move is disabled", async () => {
|
||||
await game.classicMode.startBattle([Species.GALAR_DARMANITAN]);
|
||||
|
||||
const darmanitan = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
// First turn, lock move to Growl
|
||||
game.move.select(Moves.GROWL);
|
||||
await game.forceEnemyMove(Moves.SPLASH);
|
||||
|
||||
// Second turn, Growl is interrupted by Disable
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(Moves.GROWL);
|
||||
await game.forceEnemyMove(Moves.DISABLE);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemy.getStatStage(Stat.ATK)).toBe(-1); // Only the effect of the first Growl should be applied
|
||||
|
||||
// Third turn, Struggle is used
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(Moves.TACKLE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(darmanitan.hp).toBeLessThan(darmanitan.getMaxHp());
|
||||
}, TIMEOUT);
|
||||
});
|
65
src/test/moves/after_you.test.ts
Normal file
65
src/test/moves/after_you.test.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { Abilities } from "#app/enums/abilities";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
import { MovePhase } from "#app/phases/move-phase";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
const TIMEOUT = 20 * 1000;
|
||||
|
||||
describe("Moves - After You", () => {
|
||||
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(5)
|
||||
.enemySpecies(Species.PIKACHU)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.moveset([Moves.AFTER_YOU, Moves.SPLASH]);
|
||||
});
|
||||
|
||||
it("makes the target move immediately after the user", async () => {
|
||||
await game.classicMode.startBattle([Species.REGIELEKI, Species.SHUCKLE]);
|
||||
|
||||
game.move.select(Moves.AFTER_YOU, 0, BattlerIndex.PLAYER_2);
|
||||
game.move.select(Moves.SPLASH, 1);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
await game.phaseInterceptor.to(MovePhase, false);
|
||||
const phase = game.scene.getCurrentPhase() as MovePhase;
|
||||
expect(phase.pokemon).toBe(game.scene.getPlayerField()[1]);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
}, TIMEOUT);
|
||||
|
||||
it("fails if target already moved", async () => {
|
||||
game.override.enemySpecies(Species.SHUCKLE);
|
||||
await game.classicMode.startBattle([Species.REGIELEKI, Species.PIKACHU]);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
game.move.select(Moves.AFTER_YOU, 1, BattlerIndex.PLAYER);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
await game.phaseInterceptor.to(MovePhase);
|
||||
|
||||
expect(game.scene.getPlayerField()[1].getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
|
||||
}, TIMEOUT);
|
||||
});
|
@ -3,7 +3,6 @@ import { Species } from "#app/enums/species";
|
||||
import { Stat } from "#app/enums/stat";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import { SPLASH_ONLY } from "#test/utils/testUtils";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
@ -29,7 +28,7 @@ describe("Moves - Power Shift", () => {
|
||||
.battleType("single")
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(SPLASH_ONLY);
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
|
||||
it("switches the user's raw Attack stat with its raw Defense stat", async () => {
|
||||
|
@ -5,7 +5,6 @@ import { Species } from "#app/enums/species";
|
||||
import { Stat } from "#app/enums/stat";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import { SPLASH_ONLY } from "#test/utils/testUtils";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
@ -29,7 +28,7 @@ describe("Moves - Tar Shot", () => {
|
||||
game.override
|
||||
.battleType("single")
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(SPLASH_ONLY)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.enemySpecies(Species.TANGELA)
|
||||
.enemyLevel(1000)
|
||||
.moveset([Moves.TAR_SHOT, Moves.FIRE_PUNCH])
|
||||
|
@ -36,12 +36,14 @@ describe("Moves - Throat Chop", () => {
|
||||
it("prevents the target from using sound-based moves for two turns", async () => {
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.GROWL);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
|
||||
// First turn, move is interrupted
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(enemy.getStatStage(Stat.ATK)).toBe(0);
|
||||
|
||||
// Second turn, struggle if no valid moves
|
||||
await game.toNextTurn();
|
||||
@ -50,6 +52,6 @@ describe("Moves - Throat Chop", () => {
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(game.scene.getEnemyPokemon()!.isFullHp()).toBe(false);
|
||||
expect(enemy.isFullHp()).toBe(false);
|
||||
}, TIMEOUT);
|
||||
});
|
||||
|
@ -52,9 +52,8 @@ export default class MockContainer implements MockGameObject {
|
||||
/// Sets the position of this Game Object to be a relative position from the source Game Object.
|
||||
}
|
||||
|
||||
setInteractive(hitArea?, callback?, dropZone?) {
|
||||
/// Sets the InteractiveObject to be a drop zone for a drag and drop operation.
|
||||
}
|
||||
setInteractive = vi.fn();
|
||||
|
||||
setOrigin(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Phaser from "phaser";
|
||||
import { MockGameObject } from "../mockGameObject";
|
||||
import { vi } from "vitest";
|
||||
import Sprite = Phaser.GameObjects.Sprite;
|
||||
import Frame = Phaser.Textures.Frame;
|
||||
|
||||
@ -101,9 +102,7 @@ export default class MockSprite implements MockGameObject {
|
||||
return this.phaserSprite.stop();
|
||||
}
|
||||
|
||||
setInteractive(hitArea, hitAreaCallback, dropZone) {
|
||||
return null;
|
||||
}
|
||||
setInteractive = vi.fn();
|
||||
|
||||
on(event, callback, source) {
|
||||
return this.phaserSprite.on(event, callback, source);
|
||||
|
@ -197,6 +197,8 @@ export default class MockText implements MockGameObject {
|
||||
this.color = color;
|
||||
});
|
||||
|
||||
setInteractive = vi.fn();
|
||||
|
||||
setShadowColor(color) {
|
||||
// Sets the shadow color.
|
||||
// return this.phaserText.setShadowColor(color);
|
||||
|
@ -323,7 +323,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
|
||||
this.teraIcon.setVisible(this.lastTeraType !== Type.UNKNOWN);
|
||||
this.teraIcon.on("pointerover", () => {
|
||||
if (this.lastTeraType !== Type.UNKNOWN) {
|
||||
(this.scene as BattleScene).ui.showTooltip("", `${Utils.toReadableString(Type[this.lastTeraType])} Terastallized`);
|
||||
(this.scene as BattleScene).ui.showTooltip("", i18next.t("fightUiHandler:teraHover", {type: i18next.t(`pokemonInfo:Type.${Type[this.lastTeraType]}`) }));
|
||||
}
|
||||
});
|
||||
this.teraIcon.on("pointerout", () => (this.scene as BattleScene).ui.hideTooltip());
|
||||
|
@ -266,6 +266,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||
private pokemonPassiveDisabledIcon: Phaser.GameObjects.Sprite;
|
||||
private pokemonPassiveLockedIcon: Phaser.GameObjects.Sprite;
|
||||
|
||||
private activeTooltip: "ABILITY" | "PASSIVE" | "CANDY" | undefined;
|
||||
private instructionsContainer: Phaser.GameObjects.Container;
|
||||
private filterInstructionsContainer: Phaser.GameObjects.Container;
|
||||
private shinyIconElement: Phaser.GameObjects.Sprite;
|
||||
@ -561,10 +562,13 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||
this.pokemonAbilityLabelText = addTextObject(this.scene, 6, 127 + starterInfoYOffset, i18next.t("starterSelectUiHandler:ability"), TextStyle.SUMMARY_ALT, { fontSize: starterInfoTextSize });
|
||||
this.pokemonAbilityLabelText.setOrigin(0, 0);
|
||||
this.pokemonAbilityLabelText.setVisible(false);
|
||||
|
||||
this.starterSelectContainer.add(this.pokemonAbilityLabelText);
|
||||
|
||||
this.pokemonAbilityText = addTextObject(this.scene, starterInfoXPos, 127 + starterInfoYOffset, "", TextStyle.SUMMARY_ALT, { fontSize: starterInfoTextSize });
|
||||
this.pokemonAbilityText.setOrigin(0, 0);
|
||||
this.pokemonAbilityText.setInteractive(new Phaser.Geom.Rectangle(0, 0, 250, 55), Phaser.Geom.Rectangle.Contains);
|
||||
|
||||
this.starterSelectContainer.add(this.pokemonAbilityText);
|
||||
|
||||
this.pokemonPassiveLabelText = addTextObject(this.scene, 6, 136 + starterInfoYOffset, i18next.t("starterSelectUiHandler:passive"), TextStyle.SUMMARY_ALT, { fontSize: starterInfoTextSize });
|
||||
@ -574,6 +578,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||
|
||||
this.pokemonPassiveText = addTextObject(this.scene, starterInfoXPos, 136 + starterInfoYOffset, "", TextStyle.SUMMARY_ALT, { fontSize: starterInfoTextSize });
|
||||
this.pokemonPassiveText.setOrigin(0, 0);
|
||||
this.pokemonPassiveText.setInteractive(new Phaser.Geom.Rectangle(0, 0, 250, 55), Phaser.Geom.Rectangle.Contains);
|
||||
this.starterSelectContainer.add(this.pokemonPassiveText);
|
||||
|
||||
this.pokemonPassiveDisabledIcon = this.scene.add.sprite(starterInfoXPos, 137 + starterInfoYOffset, "icon_stop");
|
||||
@ -1921,6 +1926,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||
}
|
||||
} while (newAbilityIndex !== this.abilityCursor);
|
||||
starterAttributes.ability = newAbilityIndex; // store the selected ability
|
||||
|
||||
const { visible: tooltipVisible } = this.scene.ui.getTooltip();
|
||||
|
||||
if (tooltipVisible && this.activeTooltip === "ABILITY") {
|
||||
const newAbility = allAbilities[this.lastSpecies.getAbility(newAbilityIndex)];
|
||||
this.scene.ui.editTooltip(`${newAbility.name}`, `${newAbility.description}`);
|
||||
}
|
||||
|
||||
this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, undefined, newAbilityIndex, undefined);
|
||||
success = true;
|
||||
}
|
||||
@ -2687,12 +2700,30 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
getFriendship(speciesId: number) {
|
||||
let currentFriendship = this.scene.gameData.starterData[speciesId].friendship;
|
||||
if (!currentFriendship || currentFriendship === undefined) {
|
||||
currentFriendship = 0;
|
||||
}
|
||||
|
||||
const friendshipCap = getStarterValueFriendshipCap(speciesStarters[speciesId]);
|
||||
|
||||
return { currentFriendship, friendshipCap };
|
||||
}
|
||||
|
||||
setSpecies(species: PokemonSpecies | null) {
|
||||
this.speciesStarterDexEntry = species ? this.scene.gameData.dexData[species.speciesId] : null;
|
||||
this.dexAttrCursor = species ? this.getCurrentDexProps(species.speciesId) : 0n;
|
||||
this.abilityCursor = species ? this.scene.gameData.getStarterSpeciesDefaultAbilityIndex(species) : 0;
|
||||
this.natureCursor = species ? this.scene.gameData.getSpeciesDefaultNature(species) : 0;
|
||||
|
||||
if (!species && this.scene.ui.getTooltip().visible) {
|
||||
this.scene.ui.hideTooltip();
|
||||
}
|
||||
|
||||
this.pokemonAbilityText.off("pointerover");
|
||||
this.pokemonPassiveText.off("pointerover");
|
||||
|
||||
const starterAttributes : StarterAttributes | null = species ? {...this.starterPreferences[species.speciesId]} : null;
|
||||
|
||||
if (starterAttributes?.nature) {
|
||||
@ -2807,17 +2838,18 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||
this.pokemonHatchedIcon.setVisible(true);
|
||||
this.pokemonHatchedCountText.setVisible(true);
|
||||
|
||||
let currentFriendship = this.scene.gameData.starterData[this.lastSpecies.speciesId].friendship;
|
||||
if (!currentFriendship || currentFriendship === undefined) {
|
||||
currentFriendship = 0;
|
||||
}
|
||||
|
||||
const friendshipCap = getStarterValueFriendshipCap(speciesStarters[this.lastSpecies.speciesId]);
|
||||
const { currentFriendship, friendshipCap } = this.getFriendship(this.lastSpecies.speciesId);
|
||||
const candyCropY = 16 - (16 * (currentFriendship / friendshipCap));
|
||||
|
||||
if (this.pokemonCandyDarknessOverlay.visible) {
|
||||
this.pokemonCandyDarknessOverlay.on("pointerover", () => (this.scene as BattleScene).ui.showTooltip("", `${currentFriendship}/${friendshipCap}`, true));
|
||||
this.pokemonCandyDarknessOverlay.on("pointerout", () => (this.scene as BattleScene).ui.hideTooltip());
|
||||
this.pokemonCandyDarknessOverlay.on("pointerover", () => {
|
||||
this.scene.ui.showTooltip("", `${currentFriendship}/${friendshipCap}`, true);
|
||||
this.activeTooltip = "CANDY";
|
||||
});
|
||||
this.pokemonCandyDarknessOverlay.on("pointerout", () => {
|
||||
this.scene.ui.hideTooltip();
|
||||
this.activeTooltip = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
this.pokemonCandyDarknessOverlay.setCrop(0, 0, 16, candyCropY);
|
||||
@ -2932,6 +2964,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||
this.abilityCursor = -1;
|
||||
this.natureCursor = -1;
|
||||
|
||||
if (this.activeTooltip === "CANDY") {
|
||||
const { currentFriendship, friendshipCap } = this.getFriendship(this.lastSpecies.speciesId);
|
||||
this.scene.ui.editTooltip("", `${currentFriendship}/${friendshipCap}`);
|
||||
}
|
||||
|
||||
if (species?.forms?.find(f => f.formKey === "female")) {
|
||||
if (female !== undefined) {
|
||||
formIndex = female ? 1 : 0;
|
||||
@ -3081,8 +3118,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||
}
|
||||
|
||||
if (dexEntry.caughtAttr) {
|
||||
const ability = this.lastSpecies.getAbility(abilityIndex!); // TODO: is this bang correct?
|
||||
this.pokemonAbilityText.setText(allAbilities[ability].name);
|
||||
const ability = allAbilities[this.lastSpecies.getAbility(abilityIndex!)]; // TODO: is this bang correct?
|
||||
this.pokemonAbilityText.setText(ability.name);
|
||||
|
||||
const isHidden = abilityIndex === (this.lastSpecies.ability2 ? 2 : 1);
|
||||
this.pokemonAbilityText.setColor(this.getTextColor(!isHidden ? TextStyle.SUMMARY_ALT : TextStyle.SUMMARY_GOLD));
|
||||
@ -3091,6 +3128,21 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||
const passiveAttr = this.scene.gameData.starterData[species.speciesId].passiveAttr;
|
||||
const passiveAbility = allAbilities[starterPassiveAbilities[this.lastSpecies.speciesId]];
|
||||
|
||||
if (this.pokemonAbilityText.visible) {
|
||||
if (this.activeTooltip === "ABILITY") {
|
||||
this.scene.ui.editTooltip(`${ability.name}`, `${ability.description}`);
|
||||
}
|
||||
|
||||
this.pokemonAbilityText.on("pointerover", () => {
|
||||
this.scene.ui.showTooltip(`${ability.name}`, `${ability.description}`, true);
|
||||
this.activeTooltip = "ABILITY";
|
||||
});
|
||||
this.pokemonAbilityText.on("pointerout", () => {
|
||||
this.scene.ui.hideTooltip();
|
||||
this.activeTooltip = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
if (passiveAbility) {
|
||||
const isUnlocked = !!(passiveAttr & PassiveAttr.UNLOCKED);
|
||||
const isEnabled = !!(passiveAttr & PassiveAttr.ENABLED);
|
||||
@ -3107,6 +3159,21 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||
this.pokemonPassiveText.setAlpha(textAlpha);
|
||||
this.pokemonPassiveText.setShadowColor(this.getTextColor(textStyle, true));
|
||||
|
||||
if (this.activeTooltip === "PASSIVE") {
|
||||
this.scene.ui.editTooltip(`${passiveAbility.name}`, `${passiveAbility.description}`);
|
||||
}
|
||||
|
||||
if (this.pokemonPassiveText.visible) {
|
||||
this.pokemonPassiveText.on("pointerover", () => {
|
||||
this.scene.ui.showTooltip(`${passiveAbility.name}`, `${passiveAbility.description}`, true);
|
||||
this.activeTooltip = "PASSIVE";
|
||||
});
|
||||
this.pokemonPassiveText.on("pointerout", () => {
|
||||
this.scene.ui.hideTooltip();
|
||||
this.activeTooltip = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
const iconPosition = {
|
||||
x: this.pokemonPassiveText.x + this.pokemonPassiveText.displayWidth + 1,
|
||||
y: this.pokemonPassiveText.y + this.pokemonPassiveText.displayHeight / 2
|
||||
|
30
src/ui/ui.ts
30
src/ui/ui.ts
@ -244,7 +244,7 @@ export default class UI extends Phaser.GameObjects.Container {
|
||||
|
||||
this.tooltipContent = addTextObject(this.scene, 6, 16, "", TextStyle.TOOLTIP_CONTENT);
|
||||
this.tooltipContent.setName("text-tooltip-content");
|
||||
this.tooltipContent.setWordWrapWidth(696);
|
||||
this.tooltipContent.setWordWrapWidth(850);
|
||||
|
||||
this.tooltipContainer.add(this.tooltipBg);
|
||||
this.tooltipContainer.add(this.tooltipTitle);
|
||||
@ -368,14 +368,13 @@ export default class UI extends Phaser.GameObjects.Container {
|
||||
return false;
|
||||
}
|
||||
|
||||
getTooltip(): { visible: boolean; title: string; content: string } {
|
||||
return { visible: this.tooltipContainer.visible, title: this.tooltipTitle.text, content: this.tooltipContent.text };
|
||||
}
|
||||
|
||||
showTooltip(title: string, content: string, overlap?: boolean): void {
|
||||
this.tooltipContainer.setVisible(true);
|
||||
this.tooltipTitle.setText(title || "");
|
||||
const wrappedContent = this.tooltipContent.runWordWrap(content);
|
||||
this.tooltipContent.setText(wrappedContent);
|
||||
this.tooltipContent.y = title ? 16 : 4;
|
||||
this.tooltipBg.width = Math.min(Math.max(this.tooltipTitle.displayWidth, this.tooltipContent.displayWidth) + 12, 684);
|
||||
this.tooltipBg.height = (title ? 31 : 19) + 10.5 * (wrappedContent.split("\n").length - 1);
|
||||
this.editTooltip(title, content);
|
||||
if (overlap) {
|
||||
(this.scene as BattleScene).uiContainer.moveAbove(this.tooltipContainer, this);
|
||||
} else {
|
||||
@ -383,6 +382,15 @@ export default class UI extends Phaser.GameObjects.Container {
|
||||
}
|
||||
}
|
||||
|
||||
editTooltip(title: string, content: string): void {
|
||||
this.tooltipTitle.setText(title || "");
|
||||
const wrappedContent = this.tooltipContent.runWordWrap(content);
|
||||
this.tooltipContent.setText(wrappedContent);
|
||||
this.tooltipContent.y = title ? 16 : 4;
|
||||
this.tooltipBg.width = Math.min(Math.max(this.tooltipTitle.displayWidth, this.tooltipContent.displayWidth) + 12, 838);
|
||||
this.tooltipBg.height = (title ? 31 : 19) + 10.5 * (wrappedContent.split("\n").length - 1);
|
||||
}
|
||||
|
||||
hideTooltip(): void {
|
||||
this.tooltipContainer.setVisible(false);
|
||||
this.tooltipTitle.clearTint();
|
||||
@ -390,8 +398,12 @@ export default class UI extends Phaser.GameObjects.Container {
|
||||
|
||||
update(): void {
|
||||
if (this.tooltipContainer.visible) {
|
||||
const reverse = this.scene.game.input.mousePointer && this.scene.game.input.mousePointer.x >= this.scene.game.canvas.width - this.tooltipBg.width * 6 - 12;
|
||||
this.tooltipContainer.setPosition(!reverse ? this.scene.game.input.mousePointer!.x / 6 + 2 : this.scene.game.input.mousePointer!.x / 6 - this.tooltipBg.width - 2, this.scene.game.input.mousePointer!.y / 6 + 2); // TODO: are these bangs correct?
|
||||
const xReverse = this.scene.game.input.mousePointer && this.scene.game.input.mousePointer.x >= this.scene.game.canvas.width - this.tooltipBg.width * 6 - 12;
|
||||
const yReverse = this.scene.game.input.mousePointer && this.scene.game.input.mousePointer.y >= this.scene.game.canvas.height - this.tooltipBg.height * 6 - 12;
|
||||
this.tooltipContainer.setPosition(
|
||||
!xReverse ? this.scene.game.input.mousePointer!.x / 6 + 2 : this.scene.game.input.mousePointer!.x / 6 - this.tooltipBg.width - 2,
|
||||
!yReverse ? this.scene.game.input.mousePointer!.y / 6 + 2 : this.scene.game.input.mousePointer!.y / 6 - this.tooltipBg.height - 2,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user