Merge branch 'beta' into refactor-move-file

This commit is contained in:
Sirz Benjie 2025-03-07 18:58:57 -06:00 committed by GitHub
commit e630e06256
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 220 additions and 23 deletions

4
.gitignore vendored
View File

@ -41,3 +41,7 @@ coverage
/dependency-graph.svg
/.vs
# Script outputs
./*.csv

@ -1 +1 @@
Subproject commit 0e5c6096ba26f6b87aed1aab3fe9b0b23f6cbb7b
Subproject commit 6b3f37cb351552721232f4dabefa17bddb5b9004

View File

@ -0,0 +1,98 @@
"""
Validates the contents of the variant's masterlist file and identifies
any mismatched entries for the sprite of the same key between front, back, exp, exp back, and female.
This will create a csv file that contains all of the entries with mismatches.
An empty entry means that there was not a mismatch for that version of the sprite (meaning it matches front).
"""
import sys
if sys.version_info < (3, 7):
msg = "This script requires Python 3.7+"
raise RuntimeError(msg)
import json
import os
import csv
from dataclasses import dataclass, field
from typing import Literal as L
MASTERLIST_PATH = os.path.join(
os.path.dirname(os.path.dirname(__file__)), "public", "images", "pokemon", "variant", "_masterlist.json"
)
DEFAULT_OUTPUT_PATH = "sprite-mismatches.csv"
@dataclass(order=True)
class Sprite:
key: str = field(compare=False)
front: list[int] = field(default_factory=list, compare=False)
back: list[int] = field(default_factory=list, compare=False)
female: list[int] = field(default_factory=list, compare=False)
exp: list[int] = field(default_factory=list, compare=False)
expback: list[int] = field(default_factory=list, compare=False)
sortedKey: tuple[int] | tuple[int, str] = field(init=False, repr=False, compare=True)
def as_row(self) -> tuple[str, list[int] | L[""], list[int] | L[""], list[int] | L[""], list[int] | L[""], list[int] | L[""]]:
"""return sprite information as a tuple for csv writing"""
return (self.key, self.front or "", self.back or "", self.exp or "", self.expback or "", self.female or "")
def is_mismatch(self) -> bool:
"""return True if the female, back, or exp sprites do not match the front"""
for val in [self.back, self.exp, self.expback, self.female]:
if val != [] and val != self.front:
return True
return False
def __post_init__(self):
split = self.key.split("-", maxsplit=1)
self.sortedKey = (int(split[0]), split[1]) if len(split) == 2 else (int(split[0]),)
def make_mismatch_sprite_list(path):
with open(path, "r") as f:
masterlist: dict = json.load(f)
# Go through the keys in "front" and "back" and make sure they match the masterlist
back_data: dict[str, list[int]] = masterlist.pop("back", {})
exp_data: dict[str, list[int]] = masterlist.pop("exp", {})
exp_back_data: dict[str, list[int]] = exp_data.get("back", [])
female_data: dict[str, list[int]] = masterlist.pop("female", {})
sprites: list[Sprite] = []
for key, item in masterlist.items():
sprite = Sprite(
key, front=item, back=back_data.get(key, []), exp=exp_data.get(key, []), expback=exp_back_data.get(key, []), female=female_data.get(key, [])
)
if sprite.is_mismatch():
sprites.append(sprite)
return sprites
def write_mismatch_csv(filename: str, mismatches: list[Sprite]):
with open(filename, "w", newline="") as csvfile:
writer = csv.writer(csvfile)
writer.writerow(["key", "front", "back", "exp", "expback", "female"])
for sprite in sorted(mismatches):
writer.writerow(sprite.as_row())
if __name__ == "__main__":
import argparse
p = argparse.ArgumentParser("find_sprite_variant_mismatches", description=__doc__)
p.add_argument(
"-o",
"--output",
default=DEFAULT_OUTPUT_PATH,
help=f"The path to a file to save the output file. If not specified, will write to {DEFAULT_OUTPUT_PATH}.",
)
p.add_argument("--masterlist", default=MASTERLIST_PATH, help=f"The path to the masterlist file to validate. Defaults to {MASTERLIST_PATH}.")
args = p.parse_args()
mismatches = make_mismatch_sprite_list(args.masterlist)
write_mismatch_csv(args.output, mismatches)

View File

@ -205,6 +205,28 @@ export function getWeatherClearMessage(weatherType: WeatherType): string | null
return null;
}
export function getLegendaryWeatherContinuesMessage(weatherType: WeatherType): string | null {
switch (weatherType) {
case WeatherType.HARSH_SUN:
return i18next.t("weather:harshSunContinueMessage");
case WeatherType.HEAVY_RAIN:
return i18next.t("weather:heavyRainContinueMessage");
case WeatherType.STRONG_WINDS:
return i18next.t("weather:strongWindsContinueMessage");
}
return null;
}
export function getWeatherBlockMessage(weatherType: WeatherType): string {
switch (weatherType) {
case WeatherType.HARSH_SUN:
return i18next.t("weather:harshSunEffectMessage");
case WeatherType.HEAVY_RAIN:
return i18next.t("weather:heavyRainEffectMessage");
}
return i18next.t("weather:defaultEffectMessage");
}
export function getTerrainStartMessage(terrainType: TerrainType): string | null {
switch (terrainType) {
case TerrainType.MISTY:

View File

@ -5,7 +5,7 @@ import type { Constructor } from "#app/utils";
import * as Utils from "#app/utils";
import type PokemonSpecies from "#app/data/pokemon-species";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { getTerrainClearMessage, getTerrainStartMessage, getWeatherClearMessage, getWeatherStartMessage, Weather } from "#app/data/weather";
import { getTerrainClearMessage, getTerrainStartMessage, getWeatherClearMessage, getWeatherStartMessage, getLegendaryWeatherContinuesMessage, Weather } from "#app/data/weather";
import { CommonAnim } from "#app/data/battle-anims";
import type { PokemonType } from "#enums/type";
import type Move from "#app/data/moves/move";
@ -274,6 +274,12 @@ export class Arena {
const oldWeatherType = this.weather?.weatherType || WeatherType.NONE;
if (this.weather?.isImmutable() && ![ WeatherType.HARSH_SUN, WeatherType.HEAVY_RAIN, WeatherType.STRONG_WINDS, WeatherType.NONE ].includes(weather)) {
globalScene.unshiftPhase(new CommonAnimPhase(undefined, undefined, CommonAnim.SUNNY + (oldWeatherType - 1), true));
globalScene.queueMessage(getLegendaryWeatherContinuesMessage(oldWeatherType)!);
return false;
}
this.weather = weather ? new Weather(weather, hasPokemonSource ? 5 : 0) : null;
this.eventTarget.dispatchEvent(new WeatherChangedEvent(oldWeatherType, this.weather?.weatherType!, this.weather?.turnsLeft!)); // TODO: is this bang correct?
@ -358,6 +364,10 @@ export class Arena {
return !!this.terrain && this.terrain.isMoveTerrainCancelled(user, targets, move);
}
public getWeatherType(): WeatherType {
return this.weather?.weatherType ?? WeatherType.NONE;
}
public getTerrainType(): TerrainType {
return this.terrain?.terrainType ?? TerrainType.NONE;
}

View File

@ -2886,7 +2886,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/** Reduces damage if this Pokemon has a relevant screen (e.g. Light Screen for special attacks) */
const screenMultiplier = new Utils.NumberHolder(1);
globalScene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, simulated, source, moveCategory, screenMultiplier);
// Critical hits should bypass screens
if (!isCritical) {
globalScene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, simulated, source, moveCategory, screenMultiplier);
}
/**
* For each {@linkcode HitsTagAttr} the move has, doubles the damage of the move if:

View File

@ -30,7 +30,7 @@ import { MoveFlags } from "#enums/MoveFlags";
import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms";
import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect";
import { PokemonType } from "#enums/type";
import { getTerrainBlockMessage } from "#app/data/weather";
import { getTerrainBlockMessage, getWeatherBlockMessage } from "#app/data/weather";
import { MoveUsedEvent } from "#app/events/battle-scene";
import type { PokemonMove } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon";
@ -342,9 +342,10 @@ export class MovePhase extends BattlePhase {
* TODO: is this sustainable?
*/
let failedDueToTerrain: boolean = false;
let failedDueToWeather: boolean = false;
if (success) {
const passesConditions = move.applyConditions(this.pokemon, targets[0], move);
const failedDueToWeather: boolean = globalScene.arena.isMoveWeatherCancelled(this.pokemon, move);
failedDueToWeather = globalScene.arena.isMoveWeatherCancelled(this.pokemon, move);
failedDueToTerrain = globalScene.arena.isMoveTerrainCancelled(this.pokemon, this.targets, move);
success = passesConditions && !failedDueToWeather && !failedDueToTerrain;
}
@ -381,6 +382,8 @@ export class MovePhase extends BattlePhase {
failedText = failureMessage;
} else if (failedDueToTerrain) {
failedText = getTerrainBlockMessage(targets[0], globalScene.arena.getTerrainType());
} else if (failedDueToWeather) {
failedText = getWeatherBlockMessage(globalScene.arena.getWeatherType());
}
this.showFailedText(failedText);

View File

@ -1,7 +1,7 @@
import type BattleScene from "#app/battle-scene";
import { ArenaTagSide } from "#app/data/arena-tag";
import type Move from "#app/data/moves/move";
import { allMoves } from "#app/data/moves/move";
import { allMoves, CritOnlyAttr } from "#app/data/moves/move";
import { ArenaTagType } from "#app/enums/arena-tag-type";
import type Pokemon from "#app/field/pokemon";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
@ -12,7 +12,7 @@ import { Species } from "#enums/species";
import { WeatherType } from "#enums/weather-type";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
let globalScene: BattleScene;
@ -47,7 +47,7 @@ describe("Moves - Aurora Veil", () => {
it("reduces damage of physical attacks by half in a single battle", async () => {
const moveToUse = Moves.TACKLE;
await game.startBattle([ Species.SHUCKLE ]);
await game.classicMode.startBattle([ Species.SHUCKLE ]);
game.move.select(moveToUse);
@ -61,7 +61,7 @@ describe("Moves - Aurora Veil", () => {
game.override.battleType("double");
const moveToUse = Moves.ROCK_SLIDE;
await game.startBattle([ Species.SHUCKLE, Species.SHUCKLE ]);
await game.classicMode.startBattle([ Species.SHUCKLE, Species.SHUCKLE ]);
game.move.select(moveToUse);
game.move.select(moveToUse, 1);
@ -74,7 +74,7 @@ describe("Moves - Aurora Veil", () => {
it("reduces damage of special attacks by half in a single battle", async () => {
const moveToUse = Moves.ABSORB;
await game.startBattle([ Species.SHUCKLE ]);
await game.classicMode.startBattle([ Species.SHUCKLE ]);
game.move.select(moveToUse);
@ -89,7 +89,7 @@ describe("Moves - Aurora Veil", () => {
game.override.battleType("double");
const moveToUse = Moves.DAZZLING_GLEAM;
await game.startBattle([ Species.SHUCKLE, Species.SHUCKLE ]);
await game.classicMode.startBattle([ Species.SHUCKLE, Species.SHUCKLE ]);
game.move.select(moveToUse);
game.move.select(moveToUse, 1);
@ -99,6 +99,31 @@ describe("Moves - Aurora Veil", () => {
expect(mockedDmg).toBe(allMoves[moveToUse].power * doubleBattleMultiplier);
});
it("does not affect physical critical hits", async () => {
game.override.moveset([ Moves.WICKED_BLOW ]);
const moveToUse = Moves.WICKED_BLOW;
await game.classicMode.startBattle([ Species.SHUCKLE ]);
game.move.select(moveToUse);
await game.phaseInterceptor.to(TurnEndPhase);
const mockedDmg = getMockedMoveDamage(game.scene.getEnemyPokemon()!, game.scene.getPlayerPokemon()!, allMoves[moveToUse]);
expect(mockedDmg).toBe(allMoves[moveToUse].power);
});
it("does not affect critical hits", async () => {
game.override.moveset([ Moves.FROST_BREATH ]);
const moveToUse = Moves.FROST_BREATH;
vi.spyOn(allMoves[Moves.FROST_BREATH], "accuracy", "get").mockReturnValue(100);
await game.classicMode.startBattle([ Species.SHUCKLE ]);
game.move.select(moveToUse);
await game.phaseInterceptor.to(TurnEndPhase);
const mockedDmg = getMockedMoveDamage(game.scene.getEnemyPokemon()!, game.scene.getPlayerPokemon()!, allMoves[moveToUse]);
expect(mockedDmg).toBe(allMoves[moveToUse].power);
});
});
/**
@ -115,7 +140,9 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) =
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (globalScene.arena.getTagOnSide(ArenaTagType.AURORA_VEIL, side)) {
globalScene.arena.applyTagsForSide(ArenaTagType.AURORA_VEIL, side, false, attacker, move.category, multiplierHolder);
if (move.getAttrs(CritOnlyAttr).length === 0) {
globalScene.arena.applyTagsForSide(ArenaTagType.AURORA_VEIL, side, false, attacker, move.category, multiplierHolder);
}
}
return move.power * multiplierHolder.value;

View File

@ -1,7 +1,7 @@
import type BattleScene from "#app/battle-scene";
import { ArenaTagSide } from "#app/data/arena-tag";
import type Move from "#app/data/moves/move";
import { allMoves } from "#app/data/moves/move";
import { allMoves, CritOnlyAttr } from "#app/data/moves/move";
import { Abilities } from "#app/enums/abilities";
import { ArenaTagType } from "#app/enums/arena-tag-type";
import type Pokemon from "#app/field/pokemon";
@ -11,7 +11,7 @@ import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
let globalScene: BattleScene;
@ -45,7 +45,7 @@ describe("Moves - Light Screen", () => {
it("reduces damage of special attacks by half in a single battle", async () => {
const moveToUse = Moves.ABSORB;
await game.startBattle([ Species.SHUCKLE ]);
await game.classicMode.startBattle([ Species.SHUCKLE ]);
game.move.select(moveToUse);
@ -60,7 +60,7 @@ describe("Moves - Light Screen", () => {
game.override.battleType("double");
const moveToUse = Moves.DAZZLING_GLEAM;
await game.startBattle([ Species.SHUCKLE, Species.SHUCKLE ]);
await game.classicMode.startBattle([ Species.SHUCKLE, Species.SHUCKLE ]);
game.move.select(moveToUse);
game.move.select(moveToUse, 1);
@ -73,7 +73,7 @@ describe("Moves - Light Screen", () => {
it("does not affect physical attacks", async () => {
const moveToUse = Moves.TACKLE;
await game.startBattle([ Species.SHUCKLE ]);
await game.classicMode.startBattle([ Species.SHUCKLE ]);
game.move.select(moveToUse);
@ -82,6 +82,19 @@ describe("Moves - Light Screen", () => {
expect(mockedDmg).toBe(allMoves[moveToUse].power);
});
it("does not affect critical hits", async () => {
game.override.moveset([ Moves.FROST_BREATH ]);
const moveToUse = Moves.FROST_BREATH;
vi.spyOn(allMoves[Moves.FROST_BREATH], "accuracy", "get").mockReturnValue(100);
await game.classicMode.startBattle([ Species.SHUCKLE ]);
game.move.select(moveToUse);
await game.phaseInterceptor.to(TurnEndPhase);
const mockedDmg = getMockedMoveDamage(game.scene.getEnemyPokemon()!, game.scene.getPlayerPokemon()!, allMoves[moveToUse]);
expect(mockedDmg).toBe(allMoves[moveToUse].power);
});
});
/**
@ -98,7 +111,9 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) =
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (globalScene.arena.getTagOnSide(ArenaTagType.LIGHT_SCREEN, side)) {
globalScene.arena.applyTagsForSide(ArenaTagType.LIGHT_SCREEN, side, false, attacker, move.category, multiplierHolder);
if (move.getAttrs(CritOnlyAttr).length === 0) {
globalScene.arena.applyTagsForSide(ArenaTagType.LIGHT_SCREEN, side, false, attacker, move.category, multiplierHolder);
}
}
return move.power * multiplierHolder.value;

View File

@ -1,7 +1,7 @@
import type BattleScene from "#app/battle-scene";
import { ArenaTagSide } from "#app/data/arena-tag";
import type Move from "#app/data/moves/move";
import { allMoves } from "#app/data/moves/move";
import { allMoves, CritOnlyAttr } from "#app/data/moves/move";
import { Abilities } from "#app/enums/abilities";
import { ArenaTagType } from "#app/enums/arena-tag-type";
import type Pokemon from "#app/field/pokemon";
@ -45,7 +45,7 @@ describe("Moves - Reflect", () => {
it("reduces damage of physical attacks by half in a single battle", async () => {
const moveToUse = Moves.TACKLE;
await game.startBattle([ Species.SHUCKLE ]);
await game.classicMode.startBattle([ Species.SHUCKLE ]);
game.move.select(moveToUse);
@ -59,7 +59,7 @@ describe("Moves - Reflect", () => {
game.override.battleType("double");
const moveToUse = Moves.ROCK_SLIDE;
await game.startBattle([ Species.SHUCKLE, Species.SHUCKLE ]);
await game.classicMode.startBattle([ Species.SHUCKLE, Species.SHUCKLE ]);
game.move.select(moveToUse);
game.move.select(moveToUse, 1);
@ -72,7 +72,7 @@ describe("Moves - Reflect", () => {
it("does not affect special attacks", async () => {
const moveToUse = Moves.ABSORB;
await game.startBattle([ Species.SHUCKLE ]);
await game.classicMode.startBattle([ Species.SHUCKLE ]);
game.move.select(moveToUse);
@ -82,6 +82,18 @@ describe("Moves - Reflect", () => {
expect(mockedDmg).toBe(allMoves[moveToUse].power);
});
it("does not affect critical hits", async () => {
game.override.moveset([ Moves.WICKED_BLOW ]);
const moveToUse = Moves.WICKED_BLOW;
await game.classicMode.startBattle([ Species.SHUCKLE ]);
game.move.select(moveToUse);
await game.phaseInterceptor.to(TurnEndPhase);
const mockedDmg = getMockedMoveDamage(game.scene.getEnemyPokemon()!, game.scene.getPlayerPokemon()!, allMoves[moveToUse]);
expect(mockedDmg).toBe(allMoves[moveToUse].power);
});
});
/**
@ -98,7 +110,9 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) =
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (globalScene.arena.getTagOnSide(ArenaTagType.REFLECT, side)) {
globalScene.arena.applyTagsForSide(ArenaTagType.REFLECT, side, false, attacker, move.category, multiplierHolder);
if (move.getAttrs(CritOnlyAttr).length === 0) {
globalScene.arena.applyTagsForSide(ArenaTagType.REFLECT, side, false, attacker, move.category, multiplierHolder);
}
}
return move.power * multiplierHolder.value;