Skip to content

Commit

Permalink
Merge branch 'beta' into qol/load-i18n-en-locales-during-test
Browse files Browse the repository at this point in the history
  • Loading branch information
flx-sta authored Oct 3, 2024
2 parents 5b3b27d + ea9e0c7 commit abfe736
Show file tree
Hide file tree
Showing 13 changed files with 270 additions and 13 deletions.
Binary file added public/images/ui/friendship.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/ui/friendship_overlay.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/ui/legacy/friendship.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/ui/legacy/friendship_overlay.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 27 additions & 6 deletions src/data/move.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2668,20 +2668,42 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
}
}

/**
* Attribute used for moves that change stat stages
* @param stats {@linkcode BattleStat} array of stats to be changed
* @param stages stages by which to change the stats, from -6 to 6
* @param selfTarget whether the changes are applied to the user (true) or the target (false)
* @param condition {@linkcode MoveConditionFunc} optional condition to trigger the stat change
* @param firstHitOnly whether the stat change only applies on the first hit of a multi hit move
* @param moveEffectTrigger {@linkcode MoveEffectTrigger} the trigger for the effect to take place
* @param firstTargetOnly whether, if this is a multi target move, to only apply the effect after the first target is hit, rather than once for each target
* @param lastHitOnly whether the effect should only apply after the last hit of a multi hit move
*
* @extends MoveEffectAttr
* @see {@linkcode apply}
*/
export class StatStageChangeAttr extends MoveEffectAttr {
public stats: BattleStat[];
public stages: integer;
private condition: MoveConditionFunc | null;
private showMessage: boolean;

constructor(stats: BattleStat[], stages: integer, selfTarget?: boolean, condition?: MoveConditionFunc | null, showMessage: boolean = true, firstHitOnly: boolean = false, moveEffectTrigger: MoveEffectTrigger = MoveEffectTrigger.HIT, firstTargetOnly: boolean = false) {
super(selfTarget, moveEffectTrigger, firstHitOnly, false, firstTargetOnly);
constructor(stats: BattleStat[], stages: integer, selfTarget?: boolean, condition?: MoveConditionFunc | null, showMessage: boolean = true, firstHitOnly: boolean = false, moveEffectTrigger: MoveEffectTrigger = MoveEffectTrigger.HIT, firstTargetOnly: boolean = false, lastHitOnly: boolean = false) {
super(selfTarget, moveEffectTrigger, firstHitOnly, lastHitOnly, firstTargetOnly);
this.stats = stats;
this.stages = stages;
this.condition = condition!; // TODO: is this bang correct?
this.showMessage = showMessage;
}

/**
* Attempts to change stats of the user or target (depending on value of selfTarget) if conditions are met
* @param user {@linkcode Pokemon} the user of the move
* @param target {@linkcode Pokemon} the target of the move
* @param move {@linkcode Move} the move
* @param args unused
* @returns whether stat stages were changed
*/
apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean | Promise<boolean> {
if (!super.apply(user, target, move, args) || (this.condition && !this.condition(user, target, move))) {
return false;
Expand Down Expand Up @@ -9154,11 +9176,10 @@ export function initMoves() {
.attr(ClearTerrainAttr)
.condition((user, target, move) => !!user.scene.arena.terrain),
new AttackMove(Moves.SCALE_SHOT, Type.DRAGON, MoveCategory.PHYSICAL, 25, 90, 20, -1, 0, 8)
//.attr(StatStageChangeAttr, Stat.SPD, 1, true) // TODO: Have boosts only apply at end of move, not after every hit
//.attr(StatStageChangeAttr, Stat.DEF, -1, true)
.attr(StatStageChangeAttr, [Stat.SPD], 1, true, null, true, false, MoveEffectTrigger.HIT, false, true)
.attr(StatStageChangeAttr, [Stat.DEF], -1, true, null, true, false, MoveEffectTrigger.HIT, false, true)
.attr(MultiHitAttr)
.makesContact(false)
.partial(),
.makesContact(false),
new AttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, 100, 0, 8)
.attr(ChargeAttr, ChargeAnim.METEOR_BEAM_CHARGING, i18next.t("moveTriggers:isOverflowingWithSpacePower", {pokemonName: "{USER}"}), null, true)
.attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true)
Expand Down
2 changes: 2 additions & 0 deletions src/loading-scene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export class LoadingScene extends SceneBase {
this.loadAtlas("prompt", "ui");
this.loadImage("candy", "ui");
this.loadImage("candy_overlay", "ui");
this.loadImage("friendship", "ui");
this.loadImage("friendship_overlay", "ui");
this.loadImage("cursor", "ui");
this.loadImage("cursor_reverse", "ui");
for (const wv of Utils.getEnumValues(WindowVariant)) {
Expand Down
8 changes: 6 additions & 2 deletions src/phases/move-effect-phase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr,
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
import { MoveAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags";
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr } from "#app/data/move";
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } from "#app/data/move";
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Moves } from "#app/enums/moves";
Expand All @@ -14,6 +14,7 @@ import { PokemonMultiHitModifier, FlinchChanceModifier, EnemyAttackStatusEffectC
import i18next from "i18next";
import * as Utils from "#app/utils";
import { PokemonPhase } from "./pokemon-phase";
import { Type } from "#app/data/type";

export class MoveEffectPhase extends PokemonPhase {
public move: PokemonMove;
Expand Down Expand Up @@ -404,7 +405,10 @@ export class MoveEffectPhase extends PokemonPhase {
}

const semiInvulnerableTag = target.getTag(SemiInvulnerableTag);
if (semiInvulnerableTag && !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)) {
if (semiInvulnerableTag
&& !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)
&& !(this.move.getMove().getAttrs(ToxicAccuracyAttr) && user.isOfType(Type.POISON))
) {
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion src/phases/weather-effect-phase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export class WeatherEffectPhase extends CommonAnimPhase {
return;
}

const damage = Math.ceil(pokemon.getMaxHp() / 16);
const damage = Utils.toDmgValue(pokemon.getMaxHp() / 16);

this.scene.queueMessage(getWeatherDamageMessage(this.weather?.weatherType!, pokemon)!); // TODO: are those bangs correct?
pokemon.damageAndUpdate(damage, HitResult.EFFECTIVE, false, false, true);
Expand Down
18 changes: 16 additions & 2 deletions src/test/arena/weather_hail.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe("Weather - Hail", () => {
await game.phaseInterceptor.to("TurnEndPhase");

game.scene.getField(true).forEach(pokemon => {
expect(pokemon.hp).toBeLessThan(pokemon.getMaxHp() - Math.floor(pokemon.getMaxHp() / 16));
expect(pokemon.hp).toBe(pokemon.getMaxHp() - Math.max(Math.floor(pokemon.getMaxHp() / 16), 1));
});
});

Expand All @@ -56,6 +56,20 @@ describe("Weather - Hail", () => {
const enemyPokemon = game.scene.getEnemyPokemon()!;

expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp() - Math.floor(enemyPokemon.getMaxHp() / 16));
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp() - Math.max(Math.floor(enemyPokemon.getMaxHp() / 16), 1));
});

it("does not inflict damage to Ice type Pokemon", async () => {
await game.classicMode.startBattle([Species.CLOYSTER]);

game.move.select(Moves.SPLASH);

await game.phaseInterceptor.to("TurnEndPhase");

const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;

expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp() - Math.max(Math.floor(enemyPokemon.getMaxHp() / 16), 1));
});
});
37 changes: 35 additions & 2 deletions src/test/arena/weather_sandstorm.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { WeatherType } from "#app/data/weather";
import { Abilities } from "#app/enums/abilities";
import { Stat } from "#app/enums/stat";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
Expand Down Expand Up @@ -37,7 +39,7 @@ describe("Weather - Sandstorm", () => {
await game.phaseInterceptor.to("TurnEndPhase");

game.scene.getField(true).forEach(pokemon => {
expect(pokemon.hp).toBeLessThan(pokemon.getMaxHp() - Math.floor(pokemon.getMaxHp() / 16));
expect(pokemon.hp).toBe(pokemon.getMaxHp() - Math.max(Math.floor(pokemon.getMaxHp() / 16), 1));
});
});

Expand All @@ -53,6 +55,37 @@ describe("Weather - Sandstorm", () => {
const enemyPokemon = game.scene.getEnemyPokemon()!;

expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp() - Math.floor(enemyPokemon.getMaxHp() / 16));
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp() - Math.max(Math.floor(enemyPokemon.getMaxHp() / 16), 1));
});

it("does not inflict damage to Rock, Ground and Steel type Pokemon", async () => {
game.override
.battleType("double")
.enemySpecies(Species.SANDSHREW)
.ability(Abilities.BALL_FETCH)
.enemyAbility(Abilities.BALL_FETCH);

await game.classicMode.startBattle([Species.ROCKRUFF, Species.KLINK]);

game.move.select(Moves.SPLASH, 0);
game.move.select(Moves.SPLASH, 1);

await game.phaseInterceptor.to("TurnEndPhase");

game.scene.getField(true).forEach(pokemon => {
expect(pokemon.hp).toBe(pokemon.getMaxHp());
});
});

it("increases Rock type Pokemon Sp.Def by 50%", async () => {
await game.classicMode.startBattle([Species.ROCKRUFF]);

const playerPokemon = game.scene.getPlayerPokemon()!;
const playerSpdef = playerPokemon.getStat(Stat.SPDEF);
expect(playerPokemon.getEffectiveStat(Stat.SPDEF)).toBe(Math.floor(playerSpdef * 1.5));

const enemyPokemon = game.scene.getEnemyPokemon()!;
const enemySpdef = enemyPokemon.getStat(Stat.SPDEF);
expect(enemyPokemon.getEffectiveStat(Stat.SPDEF)).toBe(enemySpdef);
});
});
74 changes: 74 additions & 0 deletions src/test/moves/scale_shot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { DamagePhase } from "#app/phases/damage-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Stat } from "#enums/stat";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest";

describe("Moves - Scale Shot", () => {
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
.moveset([Moves.SCALE_SHOT])
.battleType("single")
.disableCrits()
.starterSpecies(Species.MINCCINO)
.ability(Abilities.NO_GUARD)
.passiveAbility(Abilities.SKILL_LINK)
.enemyAbility(Abilities.SHEER_FORCE)
.enemyPassiveAbility(Abilities.STALL)
.enemyMoveset(Moves.SKILL_SWAP)
.enemyLevel(5);
});

it("applies stat changes after last hit", async () => {
await game.classicMode.startBattle([Species.FORRETRESS]);
const minccino = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SCALE_SHOT);
await game.phaseInterceptor.to(MoveEffectPhase);
await game.phaseInterceptor.to(DamagePhase);
await game.phaseInterceptor.to(MoveEffectPhase);
expect (minccino?.getStatStage(Stat.DEF)).toBe(0);
expect (minccino?.getStatStage(Stat.SPD)).toBe(0);
await game.phaseInterceptor.to(MoveEndPhase);
expect (minccino.getStatStage(Stat.DEF)).toBe(-1);
expect (minccino.getStatStage(Stat.SPD)).toBe(1);
});

it("unaffected by sheer force", async () => {
await game.classicMode.startBattle([Species.WOBBUFFET]);
const minccino = game.scene.getPlayerPokemon()!;
const wobbuffet = game.scene.getEnemyPokemon()!;
wobbuffet.setStat(Stat.HP, 100, true);
wobbuffet.hp = 100;
game.move.select(Moves.SCALE_SHOT);
await game.phaseInterceptor.to(TurnEndPhase);
const hpafter1 = wobbuffet.hp;
//effect not nullified by sheer force
expect (minccino.getStatStage(Stat.DEF)).toBe(-1);
expect (minccino.getStatStage(Stat.SPD)).toBe(1);
game.move.select(Moves.SCALE_SHOT);
await game.phaseInterceptor.to(MoveEndPhase);
const hpafter2 = wobbuffet.hp;
//check damage not boosted- make damage before sheer force a little lower than theoretical boosted sheer force damage
expect (100 - hpafter1).toBe(hpafter1 - hpafter2);
});
});
76 changes: 76 additions & 0 deletions src/test/moves/toxic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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, vi } from "vitest";
import { StatusEffect } from "#enums/status-effect";
import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move";

describe("Moves - Toxic", () => {
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("single")
.moveset(Moves.TOXIC)
.enemySpecies(Species.MAGIKARP)
.enemyMoveset(Moves.SPLASH);
});

it("should be guaranteed to hit if user is Poison-type", async () => {
vi.spyOn(allMoves[Moves.TOXIC], "accuracy", "get").mockReturnValue(0);
await game.classicMode.startBattle([Species.TOXAPEX]);

game.move.select(Moves.TOXIC);
await game.phaseInterceptor.to("BerryPhase", false);

expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.TOXIC);
});

it("may miss if user is not Poison-type", async () => {
vi.spyOn(allMoves[Moves.TOXIC], "accuracy", "get").mockReturnValue(0);
await game.classicMode.startBattle([Species.UMBREON]);

game.move.select(Moves.TOXIC);
await game.phaseInterceptor.to("BerryPhase", false);

expect(game.scene.getEnemyPokemon()!.status).toBeUndefined();
});

it("should hit semi-invulnerable targets if user is Poison-type", async () => {
vi.spyOn(allMoves[Moves.TOXIC], "accuracy", "get").mockReturnValue(0);
game.override.enemyMoveset(Moves.FLY);
await game.classicMode.startBattle([Species.TOXAPEX]);

game.move.select(Moves.TOXIC);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("BerryPhase", false);

expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.TOXIC);
});

it("should miss semi-invulnerable targets if user is not Poison-type", async () => {
vi.spyOn(allMoves[Moves.TOXIC], "accuracy", "get").mockReturnValue(-1);
game.override.enemyMoveset(Moves.FLY);
await game.classicMode.startBattle([Species.UMBREON]);

game.move.select(Moves.TOXIC);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("BerryPhase", false);

expect(game.scene.getEnemyPokemon()!.status).toBeUndefined();
});
});
Loading

0 comments on commit abfe736

Please sign in to comment.