Merge remote-tracking branch 'upstream/beta' into type-move

This commit is contained in:
Bertie690 2025-05-29 12:01:25 -04:00
commit 77ed552ecc
42 changed files with 3651 additions and 3890 deletions

View File

@ -32,7 +32,6 @@
// TODO: these files are too big and complex, ignore them until their respective refactors
"src/data/moves/move.ts",
"src/data/abilities/ability.ts",
"src/field/pokemon.ts",
// this file is just too big:
"src/data/balance/tms.ts"
@ -58,7 +57,7 @@
},
"style": {
"noVar": "error",
"useEnumInitializers": "off", // large enums like Moves/Species would make this cumbersome
"useEnumInitializers": "off", // large enums like Moves/Species would make this cumbersome
"useBlockStatements": "error",
"useConst": "error",
"useImportType": "error",
@ -73,9 +72,9 @@
},
"suspicious": {
"noDoubleEquals": "error",
// While this would be a nice rule to enable, the current structure of the codebase makes this infeasible
// While this would be a nice rule to enable, the current structure of the codebase makes this infeasible
// due to being used for move/ability `args` params and save data-related code.
// This can likely be enabled for all non-utils files once these are eventually reworked, but until then we leave it off.
// This can likely be enabled for all non-utils files once these are eventually reworked, but until then we leave it off.
"noExplicitAny": "off",
"noAssignInExpressions": "off",
"noPrototypeBuiltins": "off",
@ -92,6 +91,19 @@
"noUselessSwitchCase": "off", // Explicit > Implicit
"noUselessConstructor": "warn", // TODO: Refactor and make this an error
"noBannedTypes": "warn" // TODO: Refactor and make this an error
},
"nursery": {
"noRestrictedTypes": {
"level": "error",
"options": {
"types": {
"integer": {
"message": "This is an alias for 'number' that can provide false impressions of what values can actually be contained in this variable. Use 'number' instead.",
"use": "number"
}
}
}
}
}
}
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -1,34 +1,36 @@
{
"1": {
"319452": "831a1f",
"4a7310": "982443",
"7ba563": "b44040",
"bdef84": "ec8c8c",
"8cbd63": "c54b4b",
"215200": "710f2e",
"a5d670": "df5252",
"4aa552": "9f2f2c",
"a5d674": "e16363",
"196b21": "891222",
"7aa953": "c54b4b",
"7ba563": "b44040",
"215200": "710f2e",
"f7ce00": "7aa1df",
"525252": "123a5a",
"63b56b": "b2332f",
"a5d673": "df5252",
"8c6b3a": "448bc3",
"4aa552": "9f2f2c"
"bdef84": "ec8c8c",
"63b56b": "b2332f",
"319452": "831a1f",
"196b21": "891222",
"4a7310": "982443"
},
"2": {
"319452": "b08d72",
"4a7310": "4f3956",
"7ba563": "704e7e",
"bdef84": "a779ba",
"8cbd63": "e3d7a6",
"215200": "583823",
"a5d670": "d7cda7",
"4aa552": "c5a77f",
"a5d674": "8c669b",
"196b21": "78582c",
"7aa953": "704e7e",
"7ba563": "704e7e",
"215200": "583823",
"f7ce00": "f2aacd",
"525252": "a53b6f",
"63b56b": "cfc191",
"a5d673": "d7cda7",
"8c6b3a": "df87bb",
"4aa552": "c5a77f"
"bdef84": "a779ba",
"63b56b": "cfc191",
"319452": "b08d72",
"196b21": "78582c",
"4a7310": "4f3956"
}
}

View File

@ -1,28 +1,28 @@
{
"1": {
"196b21": "831a1f",
"7ba563": "b44040",
"215201": "630d28",
"215200": "710f2f",
"a5d674": "df5252",
"8cbd63": "c54b4b",
"63b56b": "b2332f",
"a5d670": "e16363",
"319452": "831a1f",
"4aa552": "9f2f2c",
"7ba563": "b44040",
"8cbd63": "c54b4b",
"215200": "710f2f",
"196b21": "831a1f",
"a5d674": "df5252",
"4a7310": "982443",
"a5d673": "e16363",
"63b56b": "b2332f",
"215201": "630d28"
"4a7310": "982443"
},
"2": {
"196b21": "b08d72",
"7ba563": "704e7e",
"215201": "583823",
"215200": "3f3249",
"a5d674": "d7cda7",
"8cbd63": "e3d7a6",
"63b56b": "cfc191",
"a5d670": "8c669b",
"319452": "b08d72",
"4aa552": "c5a77f",
"7ba563": "704e7e",
"8cbd63": "e3d7a6",
"215200": "3f3249",
"196b21": "b08d72",
"a5d674": "d7cda7",
"4a7310": "4f3956",
"a5d673": "8c669b",
"63b56b": "cfc191",
"215201": "583823"
"4a7310": "4f3956"
}
}

View File

@ -1,28 +1,28 @@
{
"1": {
"196b21": "780d4a",
"7ba563": "b44040",
"215201": "710f2e",
"215200": "710f2f",
"a5d674": "de5b6f",
"8cbd63": "bf3d64",
"63b56b": "9e2056",
"a5d670": "e16363",
"319452": "780d4a",
"4aa552": "8a1652",
"7ba563": "b44040",
"8cbd63": "bf3d64",
"215200": "710f2f",
"196b21": "780d4a",
"a5d674": "de5b6f",
"4a7310": "982443",
"a5d673": "e16363",
"63b56b": "9e2056",
"215201": "710f2e"
"4a7310": "982443"
},
"2": {
"196b21": "b59c72",
"7ba563": "805a9c",
"215201": "694d37",
"215200": "41334d",
"a5d674": "f6f7df",
"8cbd63": "ebe9ca",
"63b56b": "e3ddb8",
"a5d670": "a473ba",
"319452": "b59c72",
"4aa552": "c9b991",
"7ba563": "805a9c",
"8cbd63": "ebe9ca",
"215200": "41334d",
"196b21": "b59c72",
"a5d674": "f6f7df",
"4a7310": "4f3956",
"a5d673": "a473ba",
"63b56b": "e3ddb8",
"215201": "694d37"
"4a7310": "4f3956"
}
}

View File

@ -1,34 +1,36 @@
{
"1": {
"319452": "780d4a",
"4a7310": "982443",
"7ba563": "b44040",
"bdef84": "ec8c8c",
"8cbd63": "bf3d64",
"215200": "710f2e",
"a5d670": "de5b6f",
"4aa552": "8a1652",
"a5d674": "e16363",
"196b21": "7d1157",
"7aa953": "bf3d64",
"7ba563": "b44040",
"215200": "710f2e",
"f7ce00": "5bcfc3",
"525252": "20668c",
"63b56b": "9e2056",
"a5d673": "de5b6f",
"8c6b3a": "33a3b0",
"4aa552": "8a1652"
"bdef84": "ec8c8c",
"63b56b": "9e2056",
"319452": "780d4a",
"196b21": "7d1157",
"4a7310": "982443"
},
"2": {
"319452": "b59c72",
"4a7310": "4f3956",
"7ba563": "805a9c",
"bdef84": "c193cf",
"8cbd63": "f6f7df",
"215200": "694d37",
"a5d670": "ebe9ca",
"4aa552": "c9b991",
"a5d674": "a473ba",
"196b21": "9c805f",
"7aa953": "805a9c",
"7ba563": "805a9c",
"215200": "694d37",
"f7ce00": "f2aab6",
"525252": "983364",
"63b56b": "e3ddb8",
"a5d673": "ebe9ca",
"8c6b3a": "df879f",
"4aa552": "c9b991"
"bdef84": "c193cf",
"63b56b": "e3ddb8",
"319452": "b59c72",
"196b21": "9c805f",
"4a7310": "4f3956"
}
}

@ -1 +1 @@
Subproject commit 42cd5cf577f475c22bc82d55e7ca358eb4f3184f
Subproject commit e9ccbadb6eaa3b797f3dec919745befda2ec74bd

149
scripts/decrypt-save.js Normal file
View File

@ -0,0 +1,149 @@
import pkg from "crypto-js";
const { AES, enc } = pkg;
// biome-ignore lint: This is how you import fs from node
import * as fs from "node:fs";
const SAVE_KEY = "x0i2O7WRiANTqPmZ";
/**
* A map of condensed keynames to their associated full names
* NOTE: Update this if `src/system/game-data#systemShortKeys` ever changes!
*/
const systemShortKeys = {
seenAttr: "$sa",
caughtAttr: "$ca",
natureAttr: "$na",
seenCount: "$s",
caughtCount: "$c",
hatchedCount: "$hc",
ivs: "$i",
moveset: "$m",
eggMoves: "$em",
candyCount: "$x",
friendship: "$f",
abilityAttr: "$a",
passiveAttr: "$pa",
valueReduction: "$vr",
classicWinCount: "$wc",
};
/**
* Replace the shortened key names with their full names
* @param {string} dataStr - The string to convert
* @returns {string} The string with shortened keynames replaced with full names
*/
function convertSystemDataStr(dataStr) {
const fromKeys = Object.values(systemShortKeys);
const toKeys = Object.keys(systemShortKeys);
for (const k in fromKeys) {
dataStr = dataStr.replace(new RegExp(`${fromKeys[k].replace("$", "\\$")}`, "g"), toKeys[k]);
}
return dataStr;
}
/**
* Decrypt a save
* @param {string} path - The path to the encrypted save file
* @returns {string} The decrypted save data
*/
function decryptSave(path) {
// Check if the file exists
if (!fs.existsSync(path)) {
console.error(`File not found: ${path}`);
process.exit(1);
}
let fileData;
try {
fileData = fs.readFileSync(path, "utf8");
} catch (e) {
switch (e.code) {
case "ENOENT":
console.error(`File not found: ${path}`);
break;
case "EACCES":
console.error(`Could not open ${path}: Permission denied`);
break;
case "EISDIR":
console.error(`Unable to read ${path} as it is a directory`);
break;
default:
console.error(`Error reading file: ${e.message}`);
}
process.exit(1);
}
return convertSystemDataStr(AES.decrypt(fileData, SAVE_KEY).toString(enc.Utf8));
}
/* Print the usage message and exits */
function printUsage() {
console.log(`
Usage: node decrypt-save.js <encrypted-file> [save-file]
Arguments:
file-path Path to the encrypted save file to decrypt.
save-file Path to where the decrypted data should be written. If not provided, the decrypted data will be printed to the console.
Options:
-h, --help Show this help message and exit.
Description:
This script decrypts an encrypted pokerogue save file
`);
}
/**
* Write `data` to `filePath`, gracefully communicating errors that arise
* @param {string} filePath
* @param {string} data
*/
function writeToFile(filePath, data) {
try {
fs.writeFileSync(filePath, data);
} catch (e) {
switch (e.code) {
case "EACCES":
console.error(`Could not open ${filePath}: Permission denied`);
break;
case "EISDIR":
console.error(`Unable to write to ${filePath} as it is a directory`);
break;
default:
console.error(`Error writing file: ${e.message}`);
}
process.exit(1);
}
}
function main() {
let args = process.argv.slice(2);
// Get options
const options = args.filter(arg => arg.startsWith("-"));
// get args
args = args.filter(arg => !arg.startsWith("-"));
if (args.length === 0 || options.includes("-h") || options.includes("--help") || args.length > 2) {
printUsage();
process.exit(0);
}
// If the user provided a second argument, check if the file exists already and refuse to write to it.
if (args.length === 2) {
const destPath = args[1];
if (fs.existsSync(destPath)) {
console.error(`Refusing to overwrite ${destPath}`);
process.exit(1);
}
}
// Otherwise, commence decryption.
const decrypt = decryptSave(args[0]);
if (args.length === 1) {
process.stdout.write(decrypt);
process.exit(0);
}
writeToFile(destPath, decrypt);
}
main();

View File

@ -7522,7 +7522,7 @@ export class SuppressAbilitiesAttr extends MoveEffectAttr {
/** Causes the effect to fail when the target's ability is unsupressable or already suppressed. */
getCondition(): MoveConditionFunc {
return (user, target, move) => target.getAbility().isSuppressable && !target.summonData.abilitySuppressed;
return (_user, target, _move) => !target.summonData.abilitySuppressed && (target.getAbility().isSuppressable || (target.hasPassive() && target.getPassiveAbility().isSuppressable));
}
}

View File

@ -3,7 +3,7 @@ import {
transitionMysteryEncounterIntroVisuals,
updatePlayerMoney,
} from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { isNullOrUndefined, NumberHolder, randSeedInt, randSeedItem } from "#app/utils/common";
import { isNullOrUndefined, randSeedInt, randSeedItem } from "#app/utils/common";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { globalScene } from "#app/global-scene";
import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
@ -88,7 +88,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui
const r = randSeedInt(SHINY_MAGIKARP_WEIGHT);
let validEventEncounters = timedEventManager
const validEventEncounters = timedEventManager
.getEventEncounters()
.filter(
s =>
@ -111,22 +111,26 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui
if (
r === 0 ||
((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE) &&
(validEventEncounters.length === 0))
validEventEncounters.length === 0)
) {
// If you roll 1%, give shiny Magikarp with random variant
species = getPokemonSpecies(Species.MAGIKARP);
pokemon = new PlayerPokemon(species, 5, 2, undefined, undefined, true);
}
else if (
(validEventEncounters.length > 0 && (r <= EVENT_THRESHOLD ||
(isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE)))
} else if (
validEventEncounters.length > 0 &&
(r <= EVENT_THRESHOLD || isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE)
) {
tries = 0;
do {
// If you roll 20%, give event encounter with 3 extra shiny rolls and its HA, if it has one
const enc = randSeedItem(validEventEncounters);
species = getPokemonSpecies(enc.species);
pokemon = new PlayerPokemon(species, 5, species.abilityHidden === Abilities.NONE ? undefined : 2, enc.formIndex);
pokemon = new PlayerPokemon(
species,
5,
species.abilityHidden === Abilities.NONE ? undefined : 2,
enc.formIndex,
);
pokemon.trySetShinySeed();
pokemon.trySetShinySeed();
pokemon.trySetShinySeed();
@ -145,15 +149,13 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui
pokemon.trySetShinySeed();
pokemon.trySetShinySeed();
pokemon.trySetShinySeed();
}
else {
} else {
// If there's, and this would never happen, no eligible event encounters with a hidden ability, just do Magikarp
species = getPokemonSpecies(Species.MAGIKARP);
pokemon = new PlayerPokemon(species, 5, 2, undefined, undefined, true);
}
}
}
else {
} else {
pokemon = new PlayerPokemon(species, 5, 2, species.formIndex);
}
pokemon.generateAndPopulateMoveset();

View File

@ -1697,8 +1697,8 @@ export function initSpecies() {
new PokemonSpecies(Species.CHINCHOU, 2, false, false, false, "Angler Pokémon", PokemonType.WATER, PokemonType.ELECTRIC, 0.5, 12, Abilities.VOLT_ABSORB, Abilities.ILLUMINATE, Abilities.WATER_ABSORB, 330, 75, 38, 38, 56, 56, 67, 190, 50, 66, GrowthRate.SLOW, 50, false),
new PokemonSpecies(Species.LANTURN, 2, false, false, false, "Light Pokémon", PokemonType.WATER, PokemonType.ELECTRIC, 1.2, 22.5, Abilities.VOLT_ABSORB, Abilities.ILLUMINATE, Abilities.WATER_ABSORB, 460, 125, 58, 58, 76, 76, 67, 75, 50, 161, GrowthRate.SLOW, 50, false),
new PokemonSpecies(Species.PICHU, 2, false, false, false, "Tiny Mouse Pokémon", PokemonType.ELECTRIC, null, 0.3, 2, Abilities.STATIC, Abilities.NONE, Abilities.LIGHTNING_ROD, 205, 20, 40, 15, 35, 35, 60, 190, 70, 41, GrowthRate.MEDIUM_FAST, 50, false, false,
new PokemonForm("Normal", "", PokemonType.ELECTRIC, null, 1.4, 61.5, Abilities.STATIC, Abilities.NONE, Abilities.LIGHTNING_ROD, 205, 20, 40, 15, 35, 35, 60, 190, 70, 41, false, null, true),
new PokemonForm("Spiky-Eared", "spiky", PokemonType.ELECTRIC, null, 1.4, 61.5, Abilities.STATIC, Abilities.NONE, Abilities.LIGHTNING_ROD, 205, 20, 40, 15, 35, 35, 60, 190, 70, 41, false, null, true),
new PokemonForm("Normal", "", PokemonType.ELECTRIC, null, 1.4, 2, Abilities.STATIC, Abilities.NONE, Abilities.LIGHTNING_ROD, 205, 20, 40, 15, 35, 35, 60, 190, 70, 41, false, null, true),
new PokemonForm("Spiky-Eared", "spiky", PokemonType.ELECTRIC, null, 1.4, 2, Abilities.STATIC, Abilities.NONE, Abilities.LIGHTNING_ROD, 205, 20, 40, 15, 35, 35, 60, 190, 70, 41, false, null, true),
),
new PokemonSpecies(Species.CLEFFA, 2, false, false, false, "Star Shape Pokémon", PokemonType.FAIRY, null, 0.3, 3, Abilities.CUTE_CHARM, Abilities.MAGIC_GUARD, Abilities.FRIEND_GUARD, 218, 50, 25, 28, 45, 55, 15, 150, 140, 44, GrowthRate.FAST, 25, false),
new PokemonSpecies(Species.IGGLYBUFF, 2, false, false, false, "Balloon Pokémon", PokemonType.NORMAL, PokemonType.FAIRY, 0.3, 1, Abilities.CUTE_CHARM, Abilities.COMPETITIVE, Abilities.FRIEND_GUARD, 210, 90, 30, 15, 40, 20, 15, 170, 50, 42, GrowthRate.FAST, 25, false),
@ -3121,7 +3121,7 @@ export function initSpecies() {
),
new PokemonSpecies(Species.WALKING_WAKE, 9, false, false, false, "Paradox Pokémon", PokemonType.WATER, PokemonType.DRAGON, 3.5, 280, Abilities.PROTOSYNTHESIS, Abilities.NONE, Abilities.NONE, 590, 99, 83, 91, 125, 83, 109, 10, 0, 295, GrowthRate.SLOW, null, false), //Custom Catchrate, matching Gouging Fire and Raging Bolt
new PokemonSpecies(Species.IRON_LEAVES, 9, false, false, false, "Paradox Pokémon", PokemonType.GRASS, PokemonType.PSYCHIC, 1.5, 125, Abilities.QUARK_DRIVE, Abilities.NONE, Abilities.NONE, 590, 90, 130, 88, 70, 108, 104, 10, 0, 295, GrowthRate.SLOW, null, false), //Custom Catchrate, matching Iron Boulder and Iron Crown
new PokemonSpecies(Species.DIPPLIN, 9, false, false, false, "Candy Apple Pokémon", PokemonType.GRASS, PokemonType.DRAGON, 0.4, 9.7, Abilities.SUPERSWEET_SYRUP, Abilities.GLUTTONY, Abilities.STICKY_HOLD, 485, 80, 80, 110, 95, 80, 40, 45, 50, 170, GrowthRate.ERRATIC, 50, false),
new PokemonSpecies(Species.DIPPLIN, 9, false, false, false, "Candy Apple Pokémon", PokemonType.GRASS, PokemonType.DRAGON, 0.4, 4.4, Abilities.SUPERSWEET_SYRUP, Abilities.GLUTTONY, Abilities.STICKY_HOLD, 485, 80, 80, 110, 95, 80, 40, 45, 50, 170, GrowthRate.ERRATIC, 50, false),
new PokemonSpecies(Species.POLTCHAGEIST, 9, false, false, false, "Matcha Pokémon", PokemonType.GRASS, PokemonType.GHOST, 0.1, 1.1, Abilities.HOSPITALITY, Abilities.NONE, Abilities.HEATPROOF, 308, 40, 45, 45, 74, 54, 50, 120, 50, 62, GrowthRate.SLOW, null, false, false,
new PokemonForm("Counterfeit Form", "counterfeit", PokemonType.GRASS, PokemonType.GHOST, 0.1, 1.1, Abilities.HOSPITALITY, Abilities.NONE, Abilities.HEATPROOF, 308, 40, 45, 45, 74, 54, 50, 120, 50, 62, false, null, true),
new PokemonForm("Artisan Form", "artisan", PokemonType.GRASS, PokemonType.GHOST, 0.1, 1.1, Abilities.HOSPITALITY, Abilities.NONE, Abilities.HEATPROOF, 308, 40, 45, 45, 74, 54, 50, 120, 50, 62, false, null, false, true),

View File

@ -224,16 +224,16 @@ export const trainerPartyTemplates = {
*/
export function getEvilGruntPartyTemplate(): TrainerPartyTemplate {
const waveIndex = globalScene.currentBattle?.waveIndex;
if (waveIndex <= ClassicFixedBossWaves.EVIL_GRUNT_1){
if (waveIndex <= ClassicFixedBossWaves.EVIL_GRUNT_1) {
return trainerPartyTemplates.TWO_AVG;
}
if (waveIndex <= ClassicFixedBossWaves.EVIL_GRUNT_2){
if (waveIndex <= ClassicFixedBossWaves.EVIL_GRUNT_2) {
return trainerPartyTemplates.THREE_AVG;
}
if (waveIndex <= ClassicFixedBossWaves.EVIL_GRUNT_3){
if (waveIndex <= ClassicFixedBossWaves.EVIL_GRUNT_3) {
return trainerPartyTemplates.TWO_AVG_ONE_STRONG;
}
if (waveIndex <= ClassicFixedBossWaves.EVIL_ADMIN_1){
if (waveIndex <= ClassicFixedBossWaves.EVIL_ADMIN_1) {
return trainerPartyTemplates.GYM_LEADER_4; // 3avg 1 strong 1 stronger
}
return trainerPartyTemplates.GYM_LEADER_5; // 3 avg 2 strong 1 stronger
@ -251,7 +251,7 @@ export function getGymLeaderPartyTemplate() {
switch (gameMode.modeId) {
case GameModes.DAILY:
if (currentBattle?.waveIndex <= 20) {
return trainerPartyTemplates.GYM_LEADER_2
return trainerPartyTemplates.GYM_LEADER_2;
}
return trainerPartyTemplates.GYM_LEADER_3;
case GameModes.CHALLENGE: // In the future, there may be a ChallengeType to call here. For now, use classic's.
@ -259,13 +259,15 @@ export function getGymLeaderPartyTemplate() {
if (currentBattle?.waveIndex <= 20) {
return trainerPartyTemplates.GYM_LEADER_1; // 1 avg 1 strong
}
else if (currentBattle?.waveIndex <= 30) {
if (currentBattle?.waveIndex <= 30) {
return trainerPartyTemplates.GYM_LEADER_2; // 1 avg 1 strong 1 stronger
}
else if (currentBattle?.waveIndex <= 60) { // 50 and 60
// 50 and 60
if (currentBattle?.waveIndex <= 60) {
return trainerPartyTemplates.GYM_LEADER_3; // 2 avg 1 strong 1 stronger
}
else if (currentBattle?.waveIndex <= 90) { // 80 and 90
// 80 and 90
if (currentBattle?.waveIndex <= 90) {
return trainerPartyTemplates.GYM_LEADER_4; // 3 avg 1 strong 1 stronger
}
// 110+

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,7 @@ window.addEventListener("unhandledrejection", event => {
const setPositionRelative = function (guideObject: Phaser.GameObjects.GameObject, x: number, y: number) {
const offsetX = guideObject.width * (-0.5 + (0.5 - guideObject.originX));
const offsetY = guideObject.height * (-0.5 + (0.5 - guideObject.originY));
this.setPosition(guideObject.x + offsetX + x, guideObject.y + offsetY + y);
return this.setPosition(guideObject.x + offsetX + x, guideObject.y + offsetY + y);
};
Phaser.GameObjects.Container.prototype.setPositionRelative = setPositionRelative;

View File

@ -24,7 +24,7 @@ export class RevivalBlessingPhase extends BattlePhase {
UiMode.PARTY,
PartyUiMode.REVIVAL_BLESSING,
this.user.getFieldIndex(),
(slotIndex: integer, _option: PartyOption) => {
(slotIndex: number, _option: PartyOption) => {
if (slotIndex >= 0 && slotIndex < 6) {
const pokemon = globalScene.getPlayerParty()[slotIndex];
if (!pokemon || !pokemon.isFainted()) {

View File

@ -125,6 +125,12 @@ export class SwitchSummonPhase extends SummonPhase {
const switchedInPokemon: Pokemon | undefined = party[this.slotIndex];
this.lastPokemon = this.getPokemon();
// Defensive programming: Overcome the bug where the summon data has somehow not been reset
// prior to switching in a new Pokemon.
// Force the switch to occur and load the assets for the new pokemon, ignoring override.
switchedInPokemon.resetSummonData();
switchedInPokemon.loadAssets(true);
applyPreSummonAbAttrs(PreSummonAbAttr, switchedInPokemon);
applyPreSwitchOutAbAttrs(PreSwitchOutAbAttr, this.lastPokemon);
if (!switchedInPokemon) {
@ -132,6 +138,7 @@ export class SwitchSummonPhase extends SummonPhase {
return;
}
if (this.switchType === SwitchType.BATON_PASS) {
// If switching via baton pass, update opposing tags coming from the prior pokemon
(this.player ? globalScene.getEnemyField() : globalScene.getPlayerField()).forEach((enemyPokemon: Pokemon) =>

View File

@ -20,37 +20,37 @@ declare module "phaser" {
/**
* Sets this object's position relative to another object with a given offset
*/
setPositionRelative(guideObject: any, x: number, y: number): void;
setPositionRelative(guideObject: any, x: number, y: number): this;
}
interface Sprite {
/**
* Sets this object's position relative to another object with a given offset
*/
setPositionRelative(guideObject: any, x: number, y: number): void;
setPositionRelative(guideObject: any, x: number, y: number): this;
}
interface Image {
/**
* Sets this object's position relative to another object with a given offset
*/
setPositionRelative(guideObject: any, x: number, y: number): void;
setPositionRelative(guideObject: any, x: number, y: number): this;
}
interface NineSlice {
/**
* Sets this object's position relative to another object with a given offset
*/
setPositionRelative(guideObject: any, x: number, y: number): void;
setPositionRelative(guideObject: any, x: number, y: number): this;
}
interface Text {
/**
* Sets this object's position relative to another object with a given offset
*/
setPositionRelative(guideObject: any, x: number, y: number): void;
setPositionRelative(guideObject: any, x: number, y: number): this;
}
interface Rectangle {
/**
* Sets this object's position relative to another object with a given offset
*/
setPositionRelative(guideObject: any, x: number, y: number): void;
setPositionRelative(guideObject: any, x: number, y: number): this;
}
}

View File

@ -161,7 +161,7 @@ export class UiInputs {
buttonInfo(pressed = true): void {
if (globalScene.showMovesetFlyout) {
for (const p of globalScene.getField().filter(p => p?.isActive(true))) {
for (const p of globalScene.getEnemyField().filter(p => p?.isActive(true))) {
p.toggleFlyout(pressed);
}
}

View File

@ -1,4 +1,4 @@
import type { default as Pokemon } from "../field/pokemon";
import type { EnemyPokemon, default as Pokemon } from "../field/pokemon";
import { addTextObject, TextStyle } from "./text";
import { fixedInt } from "#app/utils/common";
import { globalScene } from "#app/global-scene";
@ -126,7 +126,7 @@ export default class BattleFlyout extends Phaser.GameObjects.Container {
* Links the given {@linkcode Pokemon} and subscribes to the {@linkcode BattleSceneEventType.MOVE_USED} event
* @param pokemon {@linkcode Pokemon} to link to this flyout
*/
initInfo(pokemon: Pokemon) {
initInfo(pokemon: EnemyPokemon) {
this.pokemon = pokemon;
this.name = `Flyout ${getPokemonNameWithAffix(this.pokemon)}`;

View File

@ -1,986 +0,0 @@
import type { EnemyPokemon, default as Pokemon } from "../field/pokemon";
import { getLevelTotalExp, getLevelRelExp } from "../data/exp";
import { getLocalizedSpriteKey, fixedInt } from "#app/utils/common";
import { addTextObject, TextStyle } from "./text";
import { getGenderSymbol, getGenderColor, Gender } from "../data/gender";
import { StatusEffect } from "#enums/status-effect";
import { globalScene } from "#app/global-scene";
import { getTypeRgb } from "#app/data/type";
import { PokemonType } from "#enums/pokemon-type";
import { getVariantTint } from "#app/sprites/variant";
import { Stat } from "#enums/stat";
import BattleFlyout from "./battle-flyout";
import { WindowVariant, addWindow } from "./ui-theme";
import i18next from "i18next";
import { ExpGainsSpeed } from "#app/enums/exp-gains-speed";
export default class BattleInfo extends Phaser.GameObjects.Container {
public static readonly EXP_GAINS_DURATION_BASE = 1650;
private baseY: number;
private player: boolean;
private mini: boolean;
private boss: boolean;
private bossSegments: number;
private offset: boolean;
private lastName: string | null;
private lastTeraType: PokemonType;
private lastStatus: StatusEffect;
private lastHp: number;
private lastMaxHp: number;
private lastHpFrame: string | null;
private lastExp: number;
private lastLevelExp: number;
private lastLevel: number;
private lastLevelCapped: boolean;
private lastStats: string;
private box: Phaser.GameObjects.Sprite;
private nameText: Phaser.GameObjects.Text;
private genderText: Phaser.GameObjects.Text;
private ownedIcon: Phaser.GameObjects.Sprite;
private championRibbon: Phaser.GameObjects.Sprite;
private teraIcon: Phaser.GameObjects.Sprite;
private shinyIcon: Phaser.GameObjects.Sprite;
private fusionShinyIcon: Phaser.GameObjects.Sprite;
private splicedIcon: Phaser.GameObjects.Sprite;
private statusIndicator: Phaser.GameObjects.Sprite;
private levelContainer: Phaser.GameObjects.Container;
private hpBar: Phaser.GameObjects.Image;
private hpBarSegmentDividers: Phaser.GameObjects.Rectangle[];
private levelNumbersContainer: Phaser.GameObjects.Container;
private hpNumbersContainer: Phaser.GameObjects.Container;
private type1Icon: Phaser.GameObjects.Sprite;
private type2Icon: Phaser.GameObjects.Sprite;
private type3Icon: Phaser.GameObjects.Sprite;
private expBar: Phaser.GameObjects.Image;
// #region Type effectiveness hint objects
private effectivenessContainer: Phaser.GameObjects.Container;
private effectivenessWindow: Phaser.GameObjects.NineSlice;
private effectivenessText: Phaser.GameObjects.Text;
private currentEffectiveness?: string;
// #endregion
public expMaskRect: Phaser.GameObjects.Graphics;
private statsContainer: Phaser.GameObjects.Container;
private statsBox: Phaser.GameObjects.Sprite;
private statValuesContainer: Phaser.GameObjects.Container;
private statNumbers: Phaser.GameObjects.Sprite[];
public flyoutMenu?: BattleFlyout;
private statOrder: Stat[];
private readonly statOrderPlayer = [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.ACC, Stat.EVA, Stat.SPD];
private readonly statOrderEnemy = [Stat.HP, Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.ACC, Stat.EVA, Stat.SPD];
constructor(x: number, y: number, player: boolean) {
super(globalScene, x, y);
this.baseY = y;
this.player = player;
this.mini = !player;
this.boss = false;
this.offset = false;
this.lastName = null;
this.lastTeraType = PokemonType.UNKNOWN;
this.lastStatus = StatusEffect.NONE;
this.lastHp = -1;
this.lastMaxHp = -1;
this.lastHpFrame = null;
this.lastExp = -1;
this.lastLevelExp = -1;
this.lastLevel = -1;
// Initially invisible and shown via Pokemon.showInfo
this.setVisible(false);
this.box = globalScene.add.sprite(0, 0, this.getTextureName());
this.box.setName("box");
this.box.setOrigin(1, 0.5);
this.add(this.box);
this.nameText = addTextObject(player ? -115 : -124, player ? -15.2 : -11.2, "", TextStyle.BATTLE_INFO);
this.nameText.setName("text_name");
this.nameText.setOrigin(0, 0);
this.add(this.nameText);
this.genderText = addTextObject(0, 0, "", TextStyle.BATTLE_INFO);
this.genderText.setName("text_gender");
this.genderText.setOrigin(0, 0);
this.genderText.setPositionRelative(this.nameText, 0, 2);
this.add(this.genderText);
if (!this.player) {
this.ownedIcon = globalScene.add.sprite(0, 0, "icon_owned");
this.ownedIcon.setName("icon_owned");
this.ownedIcon.setVisible(false);
this.ownedIcon.setOrigin(0, 0);
this.ownedIcon.setPositionRelative(this.nameText, 0, 11.75);
this.add(this.ownedIcon);
this.championRibbon = globalScene.add.sprite(0, 0, "champion_ribbon");
this.championRibbon.setName("icon_champion_ribbon");
this.championRibbon.setVisible(false);
this.championRibbon.setOrigin(0, 0);
this.championRibbon.setPositionRelative(this.nameText, 8, 11.75);
this.add(this.championRibbon);
}
this.teraIcon = globalScene.add.sprite(0, 0, "icon_tera");
this.teraIcon.setName("icon_tera");
this.teraIcon.setVisible(false);
this.teraIcon.setOrigin(0, 0);
this.teraIcon.setScale(0.5);
this.teraIcon.setPositionRelative(this.nameText, 0, 2);
this.teraIcon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 12, 15), Phaser.Geom.Rectangle.Contains);
this.add(this.teraIcon);
this.shinyIcon = globalScene.add.sprite(0, 0, "shiny_star");
this.shinyIcon.setName("icon_shiny");
this.shinyIcon.setVisible(false);
this.shinyIcon.setOrigin(0, 0);
this.shinyIcon.setScale(0.5);
this.shinyIcon.setPositionRelative(this.nameText, 0, 2);
this.shinyIcon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 12, 15), Phaser.Geom.Rectangle.Contains);
this.add(this.shinyIcon);
this.fusionShinyIcon = globalScene.add.sprite(0, 0, "shiny_star_2");
this.fusionShinyIcon.setName("icon_fusion_shiny");
this.fusionShinyIcon.setVisible(false);
this.fusionShinyIcon.setOrigin(0, 0);
this.fusionShinyIcon.setScale(0.5);
this.fusionShinyIcon.setPosition(this.shinyIcon.x, this.shinyIcon.y);
this.add(this.fusionShinyIcon);
this.splicedIcon = globalScene.add.sprite(0, 0, "icon_spliced");
this.splicedIcon.setName("icon_spliced");
this.splicedIcon.setVisible(false);
this.splicedIcon.setOrigin(0, 0);
this.splicedIcon.setScale(0.5);
this.splicedIcon.setPositionRelative(this.nameText, 0, 2);
this.splicedIcon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 12, 15), Phaser.Geom.Rectangle.Contains);
this.add(this.splicedIcon);
this.statusIndicator = globalScene.add.sprite(0, 0, getLocalizedSpriteKey("statuses"));
this.statusIndicator.setName("icon_status");
this.statusIndicator.setVisible(false);
this.statusIndicator.setOrigin(0, 0);
this.statusIndicator.setPositionRelative(this.nameText, 0, 11.5);
this.add(this.statusIndicator);
this.levelContainer = globalScene.add.container(player ? -41 : -50, player ? -10 : -5);
this.levelContainer.setName("container_level");
this.add(this.levelContainer);
const levelOverlay = globalScene.add.image(0, 0, "overlay_lv");
this.levelContainer.add(levelOverlay);
this.hpBar = globalScene.add.image(player ? -61 : -71, player ? -1 : 4.5, "overlay_hp");
this.hpBar.setName("hp_bar");
this.hpBar.setOrigin(0);
this.add(this.hpBar);
this.hpBarSegmentDividers = [];
this.levelNumbersContainer = globalScene.add.container(9.5, globalScene.uiTheme ? 0 : -0.5);
this.levelNumbersContainer.setName("container_level");
this.levelContainer.add(this.levelNumbersContainer);
if (this.player) {
this.hpNumbersContainer = globalScene.add.container(-15, 10);
this.hpNumbersContainer.setName("container_hp");
this.add(this.hpNumbersContainer);
const expBar = globalScene.add.image(-98, 18, "overlay_exp");
expBar.setName("overlay_exp");
expBar.setOrigin(0);
this.add(expBar);
const expMaskRect = globalScene.make.graphics({});
expMaskRect.setScale(6);
expMaskRect.fillStyle(0xffffff);
expMaskRect.beginPath();
expMaskRect.fillRect(127, 126, 85, 2);
const expMask = expMaskRect.createGeometryMask();
expBar.setMask(expMask);
this.expBar = expBar;
this.expMaskRect = expMaskRect;
}
this.statsContainer = globalScene.add.container(0, 0);
this.statsContainer.setName("container_stats");
this.statsContainer.setAlpha(0);
this.add(this.statsContainer);
this.statsBox = globalScene.add.sprite(0, 0, `${this.getTextureName()}_stats`);
this.statsBox.setName("box_stats");
this.statsBox.setOrigin(1, 0.5);
this.statsContainer.add(this.statsBox);
const statLabels: Phaser.GameObjects.Sprite[] = [];
this.statNumbers = [];
this.statValuesContainer = globalScene.add.container(0, 0);
this.statsContainer.add(this.statValuesContainer);
// this gives us a different starting location from the left of the label and padding between stats for a player vs enemy
// since the player won't have HP to show, it doesn't need to change from the current version
const startingX = this.player ? -this.statsBox.width + 8 : -this.statsBox.width + 5;
const paddingX = this.player ? 4 : 2;
const statOverflow = this.player ? 1 : 0;
this.statOrder = this.player ? this.statOrderPlayer : this.statOrderEnemy; // this tells us whether or not to use the player or enemy battle stat order
this.statOrder.map((s, i) => {
// we do a check for i > statOverflow to see when the stat labels go onto the next column
// For enemies, we have HP (i=0) by itself then a new column, so we check for i > 0
// For players, we don't have HP, so we start with i = 0 and i = 1 for our first column, and so need to check for i > 1
const statX =
i > statOverflow
? this.statNumbers[Math.max(i - 2, 0)].x + this.statNumbers[Math.max(i - 2, 0)].width + paddingX
: startingX; // we have the Math.max(i - 2, 0) in there so for i===1 to not return a negative number; since this is now based on anything >0 instead of >1, we need to allow for i-2 < 0
const baseY = -this.statsBox.height / 2 + 4; // this is the baseline for the y-axis
let statY: number; // this will be the y-axis placement for the labels
if (this.statOrder[i] === Stat.SPD || this.statOrder[i] === Stat.HP) {
statY = baseY + 5;
} else {
statY = baseY + (!!(i % 2) === this.player ? 10 : 0); // we compare i % 2 against this.player to tell us where to place the label; because this.battleStatOrder for enemies has HP, this.battleStatOrder[1]=ATK, but for players this.battleStatOrder[0]=ATK, so this comparing i % 2 to this.player fixes this issue for us
}
const statLabel = globalScene.add.sprite(statX, statY, "pbinfo_stat", Stat[s]);
statLabel.setName("icon_stat_label_" + i.toString());
statLabel.setOrigin(0, 0);
statLabels.push(statLabel);
this.statValuesContainer.add(statLabel);
const statNumber = globalScene.add.sprite(
statX + statLabel.width,
statY,
"pbinfo_stat_numbers",
this.statOrder[i] !== Stat.HP ? "3" : "empty",
);
statNumber.setName("icon_stat_number_" + i.toString());
statNumber.setOrigin(0, 0);
this.statNumbers.push(statNumber);
this.statValuesContainer.add(statNumber);
if (this.statOrder[i] === Stat.HP) {
statLabel.setVisible(false);
statNumber.setVisible(false);
}
});
if (!this.player) {
this.flyoutMenu = new BattleFlyout(this.player);
this.add(this.flyoutMenu);
this.moveBelow<Phaser.GameObjects.GameObject>(this.flyoutMenu, this.box);
}
this.type1Icon = globalScene.add.sprite(
player ? -139 : -15,
player ? -17 : -15.5,
`pbinfo_${player ? "player" : "enemy"}_type1`,
);
this.type1Icon.setName("icon_type_1");
this.type1Icon.setOrigin(0, 0);
this.add(this.type1Icon);
this.type2Icon = globalScene.add.sprite(
player ? -139 : -15,
player ? -1 : -2.5,
`pbinfo_${player ? "player" : "enemy"}_type2`,
);
this.type2Icon.setName("icon_type_2");
this.type2Icon.setOrigin(0, 0);
this.add(this.type2Icon);
this.type3Icon = globalScene.add.sprite(
player ? -154 : 0,
player ? -17 : -15.5,
`pbinfo_${player ? "player" : "enemy"}_type`,
);
this.type3Icon.setName("icon_type_3");
this.type3Icon.setOrigin(0, 0);
this.add(this.type3Icon);
if (!this.player) {
this.effectivenessContainer = globalScene.add.container(0, 0);
this.effectivenessContainer.setPositionRelative(this.type1Icon, 22, 4);
this.effectivenessContainer.setVisible(false);
this.add(this.effectivenessContainer);
this.effectivenessText = addTextObject(5, 4.5, "", TextStyle.BATTLE_INFO);
this.effectivenessWindow = addWindow(0, 0, 0, 20, undefined, false, undefined, undefined, WindowVariant.XTHIN);
this.effectivenessContainer.add(this.effectivenessWindow);
this.effectivenessContainer.add(this.effectivenessText);
}
}
getStatsValueContainer(): Phaser.GameObjects.Container {
return this.statValuesContainer;
}
initInfo(pokemon: Pokemon) {
this.updateNameText(pokemon);
const nameTextWidth = this.nameText.displayWidth;
this.name = pokemon.getNameToRender();
this.box.name = pokemon.getNameToRender();
this.flyoutMenu?.initInfo(pokemon);
this.genderText.setText(getGenderSymbol(pokemon.gender));
this.genderText.setColor(getGenderColor(pokemon.gender));
this.genderText.setPositionRelative(this.nameText, nameTextWidth, 0);
this.lastTeraType = pokemon.getTeraType();
this.teraIcon.setPositionRelative(this.nameText, nameTextWidth + this.genderText.displayWidth + 1, 2);
this.teraIcon.setVisible(pokemon.isTerastallized);
this.teraIcon.on("pointerover", () => {
if (pokemon.isTerastallized) {
globalScene.ui.showTooltip(
"",
i18next.t("fightUiHandler:teraHover", {
type: i18next.t(`pokemonInfo:Type.${PokemonType[this.lastTeraType]}`),
}),
);
}
});
this.teraIcon.on("pointerout", () => globalScene.ui.hideTooltip());
const isFusion = pokemon.isFusion(true);
this.splicedIcon.setPositionRelative(
this.nameText,
nameTextWidth + this.genderText.displayWidth + 1 + (this.teraIcon.visible ? this.teraIcon.displayWidth + 1 : 0),
2.5,
);
this.splicedIcon.setVisible(isFusion);
if (this.splicedIcon.visible) {
this.splicedIcon.on("pointerover", () =>
globalScene.ui.showTooltip(
"",
`${pokemon.species.getName(pokemon.formIndex)}/${pokemon.fusionSpecies?.getName(pokemon.fusionFormIndex)}`,
),
);
this.splicedIcon.on("pointerout", () => globalScene.ui.hideTooltip());
}
const doubleShiny = isFusion && pokemon.shiny && pokemon.fusionShiny;
const baseVariant = !doubleShiny ? pokemon.getVariant(true) : pokemon.variant;
this.shinyIcon.setPositionRelative(
this.nameText,
nameTextWidth +
this.genderText.displayWidth +
1 +
(this.teraIcon.visible ? this.teraIcon.displayWidth + 1 : 0) +
(this.splicedIcon.visible ? this.splicedIcon.displayWidth + 1 : 0),
2.5,
);
this.shinyIcon.setTexture(`shiny_star${doubleShiny ? "_1" : ""}`);
this.shinyIcon.setVisible(pokemon.isShiny());
this.shinyIcon.setTint(getVariantTint(baseVariant));
if (this.shinyIcon.visible) {
const shinyDescriptor =
doubleShiny || baseVariant
? `${baseVariant === 2 ? i18next.t("common:epicShiny") : baseVariant === 1 ? i18next.t("common:rareShiny") : i18next.t("common:commonShiny")}${doubleShiny ? `/${pokemon.fusionVariant === 2 ? i18next.t("common:epicShiny") : pokemon.fusionVariant === 1 ? i18next.t("common:rareShiny") : i18next.t("common:commonShiny")}` : ""}`
: "";
this.shinyIcon.on("pointerover", () =>
globalScene.ui.showTooltip(
"",
`${i18next.t("common:shinyOnHover")}${shinyDescriptor ? ` (${shinyDescriptor})` : ""}`,
),
);
this.shinyIcon.on("pointerout", () => globalScene.ui.hideTooltip());
}
this.fusionShinyIcon.setPosition(this.shinyIcon.x, this.shinyIcon.y);
this.fusionShinyIcon.setVisible(doubleShiny);
if (isFusion) {
this.fusionShinyIcon.setTint(getVariantTint(pokemon.fusionVariant));
}
if (!this.player) {
if (this.nameText.visible) {
this.nameText.on("pointerover", () =>
globalScene.ui.showTooltip(
"",
i18next.t("battleInfo:generation", {
generation: i18next.t(`starterSelectUiHandler:gen${pokemon.species.generation}`),
}),
),
);
this.nameText.on("pointerout", () => globalScene.ui.hideTooltip());
}
const dexEntry = globalScene.gameData.dexData[pokemon.species.speciesId];
this.ownedIcon.setVisible(!!dexEntry.caughtAttr);
const opponentPokemonDexAttr = pokemon.getDexAttr();
if (globalScene.gameMode.isClassic) {
if (
globalScene.gameData.starterData[pokemon.species.getRootSpeciesId()].classicWinCount > 0 &&
globalScene.gameData.starterData[pokemon.species.getRootSpeciesId(true)].classicWinCount > 0
) {
this.championRibbon.setVisible(true);
}
}
// Check if Player owns all genders and forms of the Pokemon
const missingDexAttrs = (dexEntry.caughtAttr & opponentPokemonDexAttr) < opponentPokemonDexAttr;
const ownedAbilityAttrs = globalScene.gameData.starterData[pokemon.species.getRootSpeciesId()].abilityAttr;
// Check if the player owns ability for the root form
const playerOwnsThisAbility = pokemon.checkIfPlayerHasAbilityOfStarter(ownedAbilityAttrs);
if (missingDexAttrs || !playerOwnsThisAbility) {
this.ownedIcon.setTint(0x808080);
}
if (this.boss) {
this.updateBossSegmentDividers(pokemon as EnemyPokemon);
}
}
this.hpBar.setScale(pokemon.getHpRatio(true), 1);
this.lastHpFrame = this.hpBar.scaleX > 0.5 ? "high" : this.hpBar.scaleX > 0.25 ? "medium" : "low";
this.hpBar.setFrame(this.lastHpFrame);
if (this.player) {
this.setHpNumbers(pokemon.hp, pokemon.getMaxHp());
}
this.lastHp = pokemon.hp;
this.lastMaxHp = pokemon.getMaxHp();
this.setLevel(pokemon.level);
this.lastLevel = pokemon.level;
this.shinyIcon.setVisible(pokemon.isShiny());
const types = pokemon.getTypes(true, false, undefined, true);
this.type1Icon.setTexture(`pbinfo_${this.player ? "player" : "enemy"}_type${types.length > 1 ? "1" : ""}`);
this.type1Icon.setFrame(PokemonType[types[0]].toLowerCase());
this.type2Icon.setVisible(types.length > 1);
this.type3Icon.setVisible(types.length > 2);
if (types.length > 1) {
this.type2Icon.setFrame(PokemonType[types[1]].toLowerCase());
}
if (types.length > 2) {
this.type3Icon.setFrame(PokemonType[types[2]].toLowerCase());
}
if (this.player) {
this.expMaskRect.x = (pokemon.levelExp / getLevelTotalExp(pokemon.level, pokemon.species.growthRate)) * 510;
this.lastExp = pokemon.exp;
this.lastLevelExp = pokemon.levelExp;
this.statValuesContainer.setPosition(8, 7);
}
const stats = this.statOrder.map(() => 0);
this.lastStats = stats.join("");
this.updateStats(stats);
}
getTextureName(): string {
return `pbinfo_${this.player ? "player" : "enemy"}${!this.player && this.boss ? "_boss" : this.mini ? "_mini" : ""}`;
}
setMini(mini: boolean): void {
if (this.mini === mini) {
return;
}
this.mini = mini;
this.box.setTexture(this.getTextureName());
this.statsBox.setTexture(`${this.getTextureName()}_stats`);
if (this.player) {
this.y -= 12 * (mini ? 1 : -1);
this.baseY = this.y;
}
const offsetElements = [
this.nameText,
this.genderText,
this.teraIcon,
this.splicedIcon,
this.shinyIcon,
this.statusIndicator,
this.levelContainer,
];
offsetElements.forEach(el => (el.y += 1.5 * (mini ? -1 : 1)));
[this.type1Icon, this.type2Icon, this.type3Icon].forEach(el => {
el.x += 4 * (mini ? 1 : -1);
el.y += -8 * (mini ? 1 : -1);
});
this.statValuesContainer.x += 2 * (mini ? 1 : -1);
this.statValuesContainer.y += -7 * (mini ? 1 : -1);
const toggledElements = [this.hpNumbersContainer, this.expBar];
toggledElements.forEach(el => el.setVisible(!mini));
}
toggleStats(visible: boolean): void {
globalScene.tweens.add({
targets: this.statsContainer,
duration: fixedInt(125),
ease: "Sine.easeInOut",
alpha: visible ? 1 : 0,
});
}
updateBossSegments(pokemon: EnemyPokemon): void {
const boss = !!pokemon.bossSegments;
if (boss !== this.boss) {
this.boss = boss;
[
this.nameText,
this.genderText,
this.teraIcon,
this.splicedIcon,
this.shinyIcon,
this.ownedIcon,
this.championRibbon,
this.statusIndicator,
this.statValuesContainer,
].map(e => (e.x += 48 * (boss ? -1 : 1)));
this.hpBar.x += 38 * (boss ? -1 : 1);
this.hpBar.y += 2 * (this.boss ? -1 : 1);
this.levelContainer.x += 2 * (boss ? -1 : 1);
this.hpBar.setTexture(`overlay_hp${boss ? "_boss" : ""}`);
this.box.setTexture(this.getTextureName());
this.statsBox.setTexture(`${this.getTextureName()}_stats`);
}
this.bossSegments = boss ? pokemon.bossSegments : 0;
this.updateBossSegmentDividers(pokemon);
}
updateBossSegmentDividers(pokemon: EnemyPokemon): void {
while (this.hpBarSegmentDividers.length) {
this.hpBarSegmentDividers.pop()?.destroy();
}
if (this.boss && this.bossSegments > 1) {
const uiTheme = globalScene.uiTheme;
const maxHp = pokemon.getMaxHp();
for (let s = 1; s < this.bossSegments; s++) {
const dividerX = (Math.round((maxHp / this.bossSegments) * s) / maxHp) * this.hpBar.width;
const divider = globalScene.add.rectangle(
0,
0,
1,
this.hpBar.height - (uiTheme ? 0 : 1),
pokemon.bossSegmentIndex >= s ? 0xffffff : 0x404040,
);
divider.setOrigin(0.5, 0);
divider.setName("hpBar_divider_" + s.toString());
this.add(divider);
this.moveBelow(divider as Phaser.GameObjects.GameObject, this.statsContainer);
divider.setPositionRelative(this.hpBar, dividerX, uiTheme ? 0 : 1);
this.hpBarSegmentDividers.push(divider);
}
}
}
setOffset(offset: boolean): void {
if (this.offset === offset) {
return;
}
this.offset = offset;
this.x += 10 * (this.offset === this.player ? 1 : -1);
this.y += 27 * (this.offset ? 1 : -1);
this.baseY = this.y;
}
updateInfo(pokemon: Pokemon, instant?: boolean): Promise<void> {
return new Promise(resolve => {
if (!globalScene) {
return resolve();
}
const gender = pokemon.summonData.illusion?.gender ?? pokemon.gender;
this.genderText.setText(getGenderSymbol(gender));
this.genderText.setColor(getGenderColor(gender));
const nameUpdated = this.lastName !== pokemon.getNameToRender();
if (nameUpdated) {
this.updateNameText(pokemon);
this.genderText.setPositionRelative(this.nameText, this.nameText.displayWidth, 0);
}
const teraType = pokemon.isTerastallized ? pokemon.getTeraType() : PokemonType.UNKNOWN;
const teraTypeUpdated = this.lastTeraType !== teraType;
if (teraTypeUpdated) {
this.teraIcon.setVisible(teraType !== PokemonType.UNKNOWN);
this.teraIcon.setPositionRelative(
this.nameText,
this.nameText.displayWidth + this.genderText.displayWidth + 1,
2,
);
this.teraIcon.setTintFill(Phaser.Display.Color.GetColor(...getTypeRgb(teraType)));
this.lastTeraType = teraType;
}
const isFusion = pokemon.isFusion(true);
if (nameUpdated || teraTypeUpdated) {
this.splicedIcon.setVisible(isFusion);
this.teraIcon.setPositionRelative(
this.nameText,
this.nameText.displayWidth + this.genderText.displayWidth + 1,
2,
);
this.splicedIcon.setPositionRelative(
this.nameText,
this.nameText.displayWidth +
this.genderText.displayWidth +
1 +
(this.teraIcon.visible ? this.teraIcon.displayWidth + 1 : 0),
1.5,
);
this.shinyIcon.setPositionRelative(
this.nameText,
this.nameText.displayWidth +
this.genderText.displayWidth +
1 +
(this.teraIcon.visible ? this.teraIcon.displayWidth + 1 : 0) +
(this.splicedIcon.visible ? this.splicedIcon.displayWidth + 1 : 0),
2.5,
);
}
if (this.lastStatus !== (pokemon.status?.effect || StatusEffect.NONE)) {
this.lastStatus = pokemon.status?.effect || StatusEffect.NONE;
if (this.lastStatus !== StatusEffect.NONE) {
this.statusIndicator.setFrame(StatusEffect[this.lastStatus].toLowerCase());
}
const offsetX = !this.player ? (this.ownedIcon.visible ? 8 : 0) + (this.championRibbon.visible ? 8 : 0) : 0;
this.statusIndicator.setPositionRelative(this.nameText, offsetX, 11.5);
this.statusIndicator.setVisible(!!this.lastStatus);
}
const types = pokemon.getTypes(true, false, undefined, true);
this.type1Icon.setTexture(`pbinfo_${this.player ? "player" : "enemy"}_type${types.length > 1 ? "1" : ""}`);
this.type1Icon.setFrame(PokemonType[types[0]].toLowerCase());
this.type2Icon.setVisible(types.length > 1);
this.type3Icon.setVisible(types.length > 2);
if (types.length > 1) {
this.type2Icon.setFrame(PokemonType[types[1]].toLowerCase());
}
if (types.length > 2) {
this.type3Icon.setFrame(PokemonType[types[2]].toLowerCase());
}
const updateHpFrame = () => {
const hpFrame = this.hpBar.scaleX > 0.5 ? "high" : this.hpBar.scaleX > 0.25 ? "medium" : "low";
if (hpFrame !== this.lastHpFrame) {
this.hpBar.setFrame(hpFrame);
this.lastHpFrame = hpFrame;
}
};
const updatePokemonHp = () => {
let duration = !instant ? Phaser.Math.Clamp(Math.abs(this.lastHp - pokemon.hp) * 5, 250, 5000) : 0;
const speed = globalScene.hpBarSpeed;
if (speed) {
duration = speed >= 3 ? 0 : duration / Math.pow(2, speed);
}
globalScene.tweens.add({
targets: this.hpBar,
ease: "Sine.easeOut",
scaleX: pokemon.getHpRatio(true),
duration: duration,
onUpdate: () => {
if (this.player && this.lastHp !== pokemon.hp) {
const tweenHp = Math.ceil(this.hpBar.scaleX * pokemon.getMaxHp());
this.setHpNumbers(tweenHp, pokemon.getMaxHp());
this.lastHp = tweenHp;
}
updateHpFrame();
},
onComplete: () => {
updateHpFrame();
// If, after tweening, the hp is different from the original (due to rounding), force the hp number display
// to update to the correct value.
if (this.player && this.lastHp !== pokemon.hp) {
this.setHpNumbers(pokemon.hp, pokemon.getMaxHp());
this.lastHp = pokemon.hp;
}
resolve();
},
});
if (!this.player) {
this.lastHp = pokemon.hp;
}
this.lastMaxHp = pokemon.getMaxHp();
};
if (this.player) {
const isLevelCapped = pokemon.level >= globalScene.getMaxExpLevel();
if (this.lastExp !== pokemon.exp || this.lastLevel !== pokemon.level) {
const originalResolve = resolve;
const durationMultipler = Math.max(
Phaser.Tweens.Builders.GetEaseFunction("Cubic.easeIn")(
1 - Math.min(pokemon.level - this.lastLevel, 10) / 10,
),
0.1,
);
resolve = () => this.updatePokemonExp(pokemon, false, durationMultipler).then(() => originalResolve());
} else if (isLevelCapped !== this.lastLevelCapped) {
this.setLevel(pokemon.level);
}
this.lastLevelCapped = isLevelCapped;
}
if (this.lastHp !== pokemon.hp || this.lastMaxHp !== pokemon.getMaxHp()) {
return updatePokemonHp();
}
if (!this.player && this.lastLevel !== pokemon.level) {
this.setLevel(pokemon.level);
this.lastLevel = pokemon.level;
}
const stats = pokemon.getStatStages();
const statsStr = stats.join("");
if (this.lastStats !== statsStr) {
this.updateStats(stats);
this.lastStats = statsStr;
}
this.shinyIcon.setVisible(pokemon.isShiny(true));
const doubleShiny = isFusion && pokemon.shiny && pokemon.fusionShiny;
const baseVariant = !doubleShiny ? pokemon.getVariant(true) : pokemon.variant;
this.shinyIcon.setTint(getVariantTint(baseVariant));
this.fusionShinyIcon.setVisible(doubleShiny);
if (isFusion) {
this.fusionShinyIcon.setTint(getVariantTint(pokemon.fusionVariant));
}
this.fusionShinyIcon.setPosition(this.shinyIcon.x, this.shinyIcon.y);
resolve();
});
}
updateNameText(pokemon: Pokemon): void {
let displayName = pokemon.getNameToRender().replace(/[♂♀]/g, "");
let nameTextWidth: number;
const nameSizeTest = addTextObject(0, 0, displayName, TextStyle.BATTLE_INFO);
nameTextWidth = nameSizeTest.displayWidth;
const gender = pokemon.summonData.illusion?.gender ?? pokemon.gender;
while (
nameTextWidth >
(this.player || !this.boss ? 60 : 98) -
((gender !== Gender.GENDERLESS ? 6 : 0) +
(pokemon.fusionSpecies ? 8 : 0) +
(pokemon.isShiny() ? 8 : 0) +
(Math.min(pokemon.level.toString().length, 3) - 3) * 8)
) {
displayName = `${displayName.slice(0, displayName.endsWith(".") ? -2 : -1).trimEnd()}.`;
nameSizeTest.setText(displayName);
nameTextWidth = nameSizeTest.displayWidth;
}
nameSizeTest.destroy();
this.nameText.setText(displayName);
this.lastName = pokemon.getNameToRender();
if (this.nameText.visible) {
this.nameText.setInteractive(
new Phaser.Geom.Rectangle(0, 0, this.nameText.width, this.nameText.height),
Phaser.Geom.Rectangle.Contains,
);
}
}
updatePokemonExp(pokemon: Pokemon, instant?: boolean, levelDurationMultiplier = 1): Promise<void> {
return new Promise(resolve => {
const levelUp = this.lastLevel < pokemon.level;
const relLevelExp = getLevelRelExp(this.lastLevel + 1, pokemon.species.growthRate);
const levelExp = levelUp ? relLevelExp : pokemon.levelExp;
let ratio = relLevelExp ? levelExp / relLevelExp : 0;
if (this.lastLevel >= globalScene.getMaxExpLevel(true)) {
if (levelUp) {
ratio = 1;
} else {
ratio = 0;
}
instant = true;
}
const durationMultiplier = Phaser.Tweens.Builders.GetEaseFunction("Sine.easeIn")(
1 - Math.max(this.lastLevel - 100, 0) / 150,
);
let duration =
this.visible && !instant
? ((levelExp - this.lastLevelExp) / relLevelExp) *
BattleInfo.EXP_GAINS_DURATION_BASE *
durationMultiplier *
levelDurationMultiplier
: 0;
const speed = globalScene.expGainsSpeed;
if (speed && speed >= ExpGainsSpeed.DEFAULT) {
duration = speed >= ExpGainsSpeed.SKIP ? ExpGainsSpeed.DEFAULT : duration / Math.pow(2, speed);
}
if (ratio === 1) {
this.lastLevelExp = 0;
this.lastLevel++;
} else {
this.lastExp = pokemon.exp;
this.lastLevelExp = pokemon.levelExp;
}
if (duration) {
globalScene.playSound("se/exp");
}
globalScene.tweens.add({
targets: this.expMaskRect,
ease: "Sine.easeIn",
x: ratio * 510,
duration: duration,
onComplete: () => {
if (!globalScene) {
return resolve();
}
if (duration) {
globalScene.sound.stopByKey("se/exp");
}
if (ratio === 1) {
globalScene.playSound("se/level_up");
this.setLevel(this.lastLevel);
globalScene.time.delayedCall(500 * levelDurationMultiplier, () => {
this.expMaskRect.x = 0;
this.updateInfo(pokemon, instant).then(() => resolve());
});
return;
}
resolve();
},
});
});
}
setLevel(level: number): void {
const isCapped = level >= globalScene.getMaxExpLevel();
this.levelNumbersContainer.removeAll(true);
const levelStr = level.toString();
for (let i = 0; i < levelStr.length; i++) {
this.levelNumbersContainer.add(
globalScene.add.image(i * 8, 0, `numbers${isCapped && this.player ? "_red" : ""}`, levelStr[i]),
);
}
this.levelContainer.setX((this.player ? -41 : -50) - 8 * Math.max(levelStr.length - 3, 0));
}
setHpNumbers(hp: number, maxHp: number): void {
if (!this.player || !globalScene) {
return;
}
this.hpNumbersContainer.removeAll(true);
const hpStr = hp.toString();
const maxHpStr = maxHp.toString();
let offset = 0;
for (let i = maxHpStr.length - 1; i >= 0; i--) {
this.hpNumbersContainer.add(globalScene.add.image(offset++ * -8, 0, "numbers", maxHpStr[i]));
}
this.hpNumbersContainer.add(globalScene.add.image(offset++ * -8, 0, "numbers", "/"));
for (let i = hpStr.length - 1; i >= 0; i--) {
this.hpNumbersContainer.add(globalScene.add.image(offset++ * -8, 0, "numbers", hpStr[i]));
}
}
updateStats(stats: number[]): void {
this.statOrder.map((s, i) => {
if (s !== Stat.HP) {
this.statNumbers[i].setFrame(stats[s - 1].toString());
}
});
}
/**
* Request the flyoutMenu to toggle if available and hides or shows the effectiveness window where necessary
*/
toggleFlyout(visible: boolean): void {
this.flyoutMenu?.toggleFlyout(visible);
if (visible) {
this.effectivenessContainer?.setVisible(false);
} else {
this.updateEffectiveness(this.currentEffectiveness);
}
}
/**
* Show or hide the type effectiveness multiplier window
* Passing undefined will hide the window
*/
updateEffectiveness(effectiveness?: string) {
if (this.player) {
return;
}
this.currentEffectiveness = effectiveness;
if (!globalScene.typeHints || effectiveness === undefined || this.flyoutMenu?.flyoutVisible) {
this.effectivenessContainer.setVisible(false);
return;
}
this.effectivenessText.setText(effectiveness);
this.effectivenessWindow.width = 10 + this.effectivenessText.displayWidth;
this.effectivenessContainer.setVisible(true);
}
getBaseY(): number {
return this.baseY;
}
resetY(): void {
this.y = this.baseY;
}
}
export class PlayerBattleInfo extends BattleInfo {
constructor() {
super(Math.floor(globalScene.game.canvas.width / 6) - 10, -72, true);
}
}
export class EnemyBattleInfo extends BattleInfo {
constructor() {
super(140, -141, false);
}
setMini(_mini: boolean): void {} // Always mini
}

View File

@ -0,0 +1,688 @@
import type { default as Pokemon } from "../../field/pokemon";
import { getLocalizedSpriteKey, fixedInt, getShinyDescriptor } from "#app/utils/common";
import { addTextObject, TextStyle } from "../text";
import { getGenderSymbol, getGenderColor, Gender } from "../../data/gender";
import { StatusEffect } from "#enums/status-effect";
import { globalScene } from "#app/global-scene";
import { getTypeRgb } from "#app/data/type";
import { PokemonType } from "#enums/pokemon-type";
import { getVariantTint } from "#app/sprites/variant";
import { Stat } from "#enums/stat";
import i18next from "i18next";
/**
* Parameters influencing the position of elements within the battle info container
*/
export type BattleInfoParamList = {
/** X offset for the name text*/
nameTextX: number;
/** Y offset for the name text */
nameTextY: number;
/** X offset for the level container */
levelContainerX: number;
/** Y offset for the level container */
levelContainerY: number;
/** X offset for the hp bar */
hpBarX: number;
/** Y offset for the hp bar */
hpBarY: number;
/** Parameters for the stat box container */
statBox: {
/** The starting offset from the left of the label for the entries in the stat box */
xOffset: number;
/** The X padding between each number column */
paddingX: number;
/** The index of the stat entries at which paddingX is used instead of startingX */
statOverflow: number;
};
};
export default abstract class BattleInfo extends Phaser.GameObjects.Container {
public static readonly EXP_GAINS_DURATION_BASE = 1650;
protected baseY: number;
protected baseLvContainerX: number;
protected player: boolean;
protected mini: boolean;
protected boss: boolean;
protected bossSegments: number;
protected offset: boolean;
protected lastName: string | null;
protected lastTeraType: PokemonType;
protected lastStatus: StatusEffect;
protected lastHp: number;
protected lastMaxHp: number;
protected lastHpFrame: string | null;
protected lastExp: number;
protected lastLevelExp: number;
protected lastLevel: number;
protected lastLevelCapped: boolean;
protected lastStats: string;
protected box: Phaser.GameObjects.Sprite;
protected nameText: Phaser.GameObjects.Text;
protected genderText: Phaser.GameObjects.Text;
protected teraIcon: Phaser.GameObjects.Sprite;
protected shinyIcon: Phaser.GameObjects.Sprite;
protected fusionShinyIcon: Phaser.GameObjects.Sprite;
protected splicedIcon: Phaser.GameObjects.Sprite;
protected statusIndicator: Phaser.GameObjects.Sprite;
protected levelContainer: Phaser.GameObjects.Container;
protected hpBar: Phaser.GameObjects.Image;
protected levelNumbersContainer: Phaser.GameObjects.Container;
protected type1Icon: Phaser.GameObjects.Sprite;
protected type2Icon: Phaser.GameObjects.Sprite;
protected type3Icon: Phaser.GameObjects.Sprite;
protected expBar: Phaser.GameObjects.Image;
public expMaskRect: Phaser.GameObjects.Graphics;
protected statsContainer: Phaser.GameObjects.Container;
protected statsBox: Phaser.GameObjects.Sprite;
protected statValuesContainer: Phaser.GameObjects.Container;
protected statNumbers: Phaser.GameObjects.Sprite[];
get statOrder(): Stat[] {
return [];
}
/** Helper method used by the constructor to create the tera and shiny icons next to the name */
private constructIcons() {
const hitArea = new Phaser.Geom.Rectangle(0, 0, 12, 15);
const hitCallback = Phaser.Geom.Rectangle.Contains;
this.teraIcon = globalScene.add
.sprite(0, 0, "icon_tera")
.setName("icon_tera")
.setVisible(false)
.setOrigin(0)
.setScale(0.5)
.setInteractive(hitArea, hitCallback)
.setPositionRelative(this.nameText, 0, 2);
this.shinyIcon = globalScene.add
.sprite(0, 0, "shiny_star")
.setName("icon_shiny")
.setVisible(false)
.setOrigin(0)
.setScale(0.5)
.setInteractive(hitArea, hitCallback)
.setPositionRelative(this.nameText, 0, 2);
this.fusionShinyIcon = globalScene.add
.sprite(0, 0, "shiny_star_2")
.setName("icon_fusion_shiny")
.setVisible(false)
.setOrigin(0)
.setScale(0.5)
.copyPosition(this.shinyIcon);
this.splicedIcon = globalScene.add
.sprite(0, 0, "icon_spliced")
.setName("icon_spliced")
.setVisible(false)
.setOrigin(0)
.setScale(0.5)
.setInteractive(hitArea, hitCallback)
.setPositionRelative(this.nameText, 0, 2);
this.add([this.teraIcon, this.shinyIcon, this.fusionShinyIcon, this.splicedIcon]);
}
/**
* Submethod of the constructor that creates and adds the stats container to the battle info
*/
protected constructStatContainer({ xOffset, paddingX, statOverflow }: BattleInfoParamList["statBox"]): void {
this.statsContainer = globalScene.add.container(0, 0).setName("container_stats").setAlpha(0);
this.add(this.statsContainer);
this.statsBox = globalScene.add
.sprite(0, 0, `${this.getTextureName()}_stats`)
.setName("box_stats")
.setOrigin(1, 0.5);
this.statsContainer.add(this.statsBox);
const statLabels: Phaser.GameObjects.Sprite[] = [];
this.statNumbers = [];
this.statValuesContainer = globalScene.add.container();
this.statsContainer.add(this.statValuesContainer);
const startingX = -this.statsBox.width + xOffset;
// this gives us a different starting location from the left of the label and padding between stats for a player vs enemy
// since the player won't have HP to show, it doesn't need to change from the current version
for (const [i, s] of this.statOrder.entries()) {
const isHp = s === Stat.HP;
// we do a check for i > statOverflow to see when the stat labels go onto the next column
// For enemies, we have HP (i=0) by itself then a new column, so we check for i > 0
// For players, we don't have HP, so we start with i = 0 and i = 1 for our first column, and so need to check for i > 1
const statX =
i > statOverflow
? this.statNumbers[Math.max(i - 2, 0)].x + this.statNumbers[Math.max(i - 2, 0)].width + paddingX
: startingX; // we have the Math.max(i - 2, 0) in there so for i===1 to not return a negative number; since this is now based on anything >0 instead of >1, we need to allow for i-2 < 0
let statY = -this.statsBox.height / 2 + 4; // this is the baseline for the y-axis
if (isHp || s === Stat.SPD) {
statY += 5;
} else if (this.player === !!(i % 2)) {
// we compare i % 2 against this.player to tell us where to place the label
// because this.battleStatOrder for enemies has HP, this.battleStatOrder[1]=ATK, but for players
// this.battleStatOrder[0]=ATK, so this comparing i % 2 to this.player fixes this issue for us
statY += 10;
}
const statLabel = globalScene.add
.sprite(statX, statY, "pbinfo_stat", Stat[s])
.setName("icon_stat_label_" + i.toString())
.setOrigin(0);
statLabels.push(statLabel);
this.statValuesContainer.add(statLabel);
const statNumber = globalScene.add
.sprite(statX + statLabel.width, statY, "pbinfo_stat_numbers", !isHp ? "3" : "empty")
.setName("icon_stat_number_" + i.toString())
.setOrigin(0);
this.statNumbers.push(statNumber);
this.statValuesContainer.add(statNumber);
if (isHp) {
statLabel.setVisible(false);
statNumber.setVisible(false);
}
}
}
/**
* Submethod of the constructor that creates and adds the pokemon type icons to the battle info
*/
protected abstract constructTypeIcons(): void;
/**
* @param x - The x position of the battle info container
* @param y - The y position of the battle info container
* @param player - Whether this battle info belongs to a player or an enemy
* @param posParams - The parameters influencing the position of elements within the battle info container
*/
constructor(x: number, y: number, player: boolean, posParams: BattleInfoParamList) {
super(globalScene, x, y);
this.baseY = y;
this.player = player;
this.mini = !player;
this.boss = false;
this.offset = false;
this.lastName = null;
this.lastTeraType = PokemonType.UNKNOWN;
this.lastStatus = StatusEffect.NONE;
this.lastHp = -1;
this.lastMaxHp = -1;
this.lastHpFrame = null;
this.lastExp = -1;
this.lastLevelExp = -1;
this.lastLevel = -1;
this.baseLvContainerX = posParams.levelContainerX;
// Initially invisible and shown via Pokemon.showInfo
this.setVisible(false);
this.box = globalScene.add.sprite(0, 0, this.getTextureName()).setName("box").setOrigin(1, 0.5);
this.add(this.box);
this.nameText = addTextObject(player ? -115 : -124, player ? -15.2 : -11.2, "", TextStyle.BATTLE_INFO)
.setName("text_name")
.setOrigin(0);
this.add(this.nameText);
this.genderText = addTextObject(0, 0, "", TextStyle.BATTLE_INFO)
.setName("text_gender")
.setOrigin(0)
.setPositionRelative(this.nameText, 0, 2);
this.add(this.genderText);
this.constructIcons();
this.statusIndicator = globalScene.add
.sprite(0, 0, getLocalizedSpriteKey("statuses"))
.setName("icon_status")
.setVisible(false)
.setOrigin(0)
.setPositionRelative(this.nameText, 0, 11.5);
this.add(this.statusIndicator);
this.levelContainer = globalScene.add
.container(posParams.levelContainerX, posParams.levelContainerY)
.setName("container_level");
this.add(this.levelContainer);
const levelOverlay = globalScene.add.image(0, 0, "overlay_lv");
this.levelContainer.add(levelOverlay);
this.hpBar = globalScene.add.image(posParams.hpBarX, posParams.hpBarY, "overlay_hp").setName("hp_bar").setOrigin(0);
this.add(this.hpBar);
this.levelNumbersContainer = globalScene.add
.container(9.5, globalScene.uiTheme ? 0 : -0.5)
.setName("container_level");
this.levelContainer.add(this.levelNumbersContainer);
this.constructStatContainer(posParams.statBox);
this.constructTypeIcons();
}
getStatsValueContainer(): Phaser.GameObjects.Container {
return this.statValuesContainer;
}
//#region Initialization methods
initSplicedIcon(pokemon: Pokemon, baseWidth: number) {
this.splicedIcon.setPositionRelative(
this.nameText,
baseWidth + this.genderText.displayWidth + 1 + (this.teraIcon.visible ? this.teraIcon.displayWidth + 1 : 0),
2.5,
);
this.splicedIcon.setVisible(pokemon.isFusion(true));
if (!this.splicedIcon.visible) {
return;
}
this.splicedIcon
.on("pointerover", () =>
globalScene.ui.showTooltip(
"",
`${pokemon.species.getName(pokemon.formIndex)}/${pokemon.fusionSpecies?.getName(pokemon.fusionFormIndex)}`,
),
)
.on("pointerout", () => globalScene.ui.hideTooltip());
}
/**
* Called by {@linkcode initInfo} to initialize the shiny icon
* @param pokemon - The pokemon object attached to this battle info
* @param baseXOffset - The x offset to use for the shiny icon
* @param doubleShiny - Whether the pokemon is shiny and its fusion species is also shiny
*/
protected initShinyIcon(pokemon: Pokemon, xOffset: number, doubleShiny: boolean) {
const baseVariant = !doubleShiny ? pokemon.getVariant(true) : pokemon.variant;
this.shinyIcon.setPositionRelative(
this.nameText,
xOffset +
this.genderText.displayWidth +
1 +
(this.teraIcon.visible ? this.teraIcon.displayWidth + 1 : 0) +
(this.splicedIcon.visible ? this.splicedIcon.displayWidth + 1 : 0),
2.5,
);
this.shinyIcon
.setTexture(`shiny_star${doubleShiny ? "_1" : ""}`)
.setVisible(pokemon.isShiny())
.setTint(getVariantTint(baseVariant));
if (!this.shinyIcon.visible) {
return;
}
let shinyDescriptor = "";
if (doubleShiny || baseVariant) {
shinyDescriptor = " (" + getShinyDescriptor(baseVariant);
if (doubleShiny) {
shinyDescriptor += "/" + getShinyDescriptor(pokemon.fusionVariant);
}
shinyDescriptor += ")";
}
this.shinyIcon
.on("pointerover", () => globalScene.ui.showTooltip("", i18next.t("common:shinyOnHover") + shinyDescriptor))
.on("pointerout", () => globalScene.ui.hideTooltip());
}
initInfo(pokemon: Pokemon) {
this.updateNameText(pokemon);
const nameTextWidth = this.nameText.displayWidth;
this.name = pokemon.getNameToRender();
this.box.name = pokemon.getNameToRender();
this.genderText
.setText(getGenderSymbol(pokemon.gender))
.setColor(getGenderColor(pokemon.gender))
.setPositionRelative(this.nameText, nameTextWidth, 0);
this.lastTeraType = pokemon.getTeraType();
this.teraIcon
.setVisible(pokemon.isTerastallized)
.on("pointerover", () => {
if (pokemon.isTerastallized) {
globalScene.ui.showTooltip(
"",
i18next.t("fightUiHandler:teraHover", {
type: i18next.t(`pokemonInfo:Type.${PokemonType[this.lastTeraType]}`),
}),
);
}
})
.on("pointerout", () => globalScene.ui.hideTooltip())
.setPositionRelative(this.nameText, nameTextWidth + this.genderText.displayWidth + 1, 2);
const isFusion = pokemon.isFusion(true);
this.initSplicedIcon(pokemon, nameTextWidth);
const doubleShiny = isFusion && pokemon.shiny && pokemon.fusionShiny;
this.initShinyIcon(pokemon, nameTextWidth, doubleShiny);
this.fusionShinyIcon.setVisible(doubleShiny).copyPosition(this.shinyIcon);
if (isFusion) {
this.fusionShinyIcon.setTint(getVariantTint(pokemon.fusionVariant));
}
this.hpBar.setScale(pokemon.getHpRatio(true), 1);
this.lastHpFrame = this.hpBar.scaleX > 0.5 ? "high" : this.hpBar.scaleX > 0.25 ? "medium" : "low";
this.hpBar.setFrame(this.lastHpFrame);
this.lastHp = pokemon.hp;
this.lastMaxHp = pokemon.getMaxHp();
this.setLevel(pokemon.level);
this.lastLevel = pokemon.level;
this.shinyIcon.setVisible(pokemon.isShiny());
this.setTypes(pokemon.getTypes(true, false, undefined, true));
const stats = this.statOrder.map(() => 0);
this.lastStats = stats.join("");
this.updateStats(stats);
}
//#endregion
/**
* Return the texture name of the battle info box
*/
abstract getTextureName(): string;
setMini(_mini: boolean): void {}
toggleStats(visible: boolean): void {
globalScene.tweens.add({
targets: this.statsContainer,
duration: fixedInt(125),
ease: "Sine.easeInOut",
alpha: visible ? 1 : 0,
});
}
setOffset(offset: boolean): void {
if (this.offset === offset) {
return;
}
this.offset = offset;
this.x += 10 * (this.offset === this.player ? 1 : -1);
this.y += 27 * (this.offset ? 1 : -1);
this.baseY = this.y;
}
//#region Update methods and helpers
/**
* Update the status icon to match the pokemon's current status
* @param pokemon - The pokemon object attached to this battle info
* @param xOffset - The offset from the name text
*/
updateStatusIcon(pokemon: Pokemon, xOffset = 0) {
if (this.lastStatus !== (pokemon.status?.effect || StatusEffect.NONE)) {
this.lastStatus = pokemon.status?.effect || StatusEffect.NONE;
if (this.lastStatus !== StatusEffect.NONE) {
this.statusIndicator.setFrame(StatusEffect[this.lastStatus].toLowerCase());
}
this.statusIndicator.setVisible(!!this.lastStatus).setPositionRelative(this.nameText, xOffset, 11.5);
}
}
/** Update the pokemon name inside the container */
protected updateName(name: string): boolean {
if (this.lastName === name) {
return false;
}
this.nameText.setText(name).setPositionRelative(this.box, -this.nameText.displayWidth, 0);
this.lastName = name;
return true;
}
protected updateTeraType(ty: PokemonType): boolean {
if (this.lastTeraType === ty) {
return false;
}
this.teraIcon
.setVisible(ty !== PokemonType.UNKNOWN)
.setTintFill(Phaser.Display.Color.GetColor(...getTypeRgb(ty)))
.setPositionRelative(this.nameText, this.nameText.displayWidth + this.genderText.displayWidth + 1, 2);
this.lastTeraType = ty;
return true;
}
/**
* Update the type icons to match the pokemon's types
*/
setTypes(types: PokemonType[]): void {
this.type1Icon
.setTexture(`pbinfo_${this.player ? "player" : "enemy"}_type${types.length > 1 ? "1" : ""}`)
.setFrame(PokemonType[types[0]].toLowerCase());
this.type2Icon.setVisible(types.length > 1);
this.type3Icon.setVisible(types.length > 2);
if (types.length > 1) {
this.type2Icon.setFrame(PokemonType[types[1]].toLowerCase());
}
if (types.length > 2) {
this.type3Icon.setFrame(PokemonType[types[2]].toLowerCase());
}
}
/**
* Called by {@linkcode updateInfo} to update the position of the tera, spliced, and shiny icons
* @param isFusion - Whether the pokemon is a fusion or not
*/
protected updateIconDisplay(isFusion: boolean): void {
this.teraIcon.setPositionRelative(this.nameText, this.nameText.displayWidth + this.genderText.displayWidth + 1, 2);
this.splicedIcon
.setVisible(isFusion)
.setPositionRelative(
this.nameText,
this.nameText.displayWidth +
this.genderText.displayWidth +
1 +
(this.teraIcon.visible ? this.teraIcon.displayWidth + 1 : 0),
1.5,
);
this.shinyIcon.setPositionRelative(
this.nameText,
this.nameText.displayWidth +
this.genderText.displayWidth +
1 +
(this.teraIcon.visible ? this.teraIcon.displayWidth + 1 : 0) +
(this.splicedIcon.visible ? this.splicedIcon.displayWidth + 1 : 0),
2.5,
);
}
//#region Hp Bar Display handling
/**
* Called every time the hp frame is updated by the tween
* @param pokemon - The pokemon object attached to this battle info
*/
protected updateHpFrame(): void {
const hpFrame = this.hpBar.scaleX > 0.5 ? "high" : this.hpBar.scaleX > 0.25 ? "medium" : "low";
if (hpFrame !== this.lastHpFrame) {
this.hpBar.setFrame(hpFrame);
this.lastHpFrame = hpFrame;
}
}
/**
* Called by every frame in the hp animation tween created in {@linkcode updatePokemonHp}
* @param _pokemon - The pokemon the battle-info bar belongs to
*/
protected onHpTweenUpdate(_pokemon: Pokemon): void {
this.updateHpFrame();
}
/** Update the pokemonHp bar */
protected updatePokemonHp(pokemon: Pokemon, resolve: (r: void | PromiseLike<void>) => void, instant?: boolean): void {
let duration = !instant ? Phaser.Math.Clamp(Math.abs(this.lastHp - pokemon.hp) * 5, 250, 5000) : 0;
const speed = globalScene.hpBarSpeed;
if (speed) {
duration = speed >= 3 ? 0 : duration / Math.pow(2, speed);
}
globalScene.tweens.add({
targets: this.hpBar,
ease: "Sine.easeOut",
scaleX: pokemon.getHpRatio(true),
duration: duration,
onUpdate: () => {
this.onHpTweenUpdate(pokemon);
},
onComplete: () => {
this.updateHpFrame();
resolve();
},
});
this.lastMaxHp = pokemon.getMaxHp();
}
//#endregion
async updateInfo(pokemon: Pokemon, instant?: boolean): Promise<void> {
let resolve: (r: void | PromiseLike<void>) => void = () => {};
const promise = new Promise<void>(r => (resolve = r));
if (!globalScene) {
return resolve();
}
const gender: Gender = pokemon.summonData?.illusion?.gender ?? pokemon.gender;
this.genderText.setText(getGenderSymbol(gender)).setColor(getGenderColor(gender));
const nameUpdated = this.updateName(pokemon.getNameToRender());
const teraTypeUpdated = this.updateTeraType(pokemon.isTerastallized ? pokemon.getTeraType() : PokemonType.UNKNOWN);
const isFusion = pokemon.isFusion(true);
if (nameUpdated || teraTypeUpdated) {
this.updateIconDisplay(isFusion);
}
this.updateStatusIcon(pokemon);
if (this.lastHp !== pokemon.hp || this.lastMaxHp !== pokemon.getMaxHp()) {
return this.updatePokemonHp(pokemon, resolve, instant);
}
if (!this.player && this.lastLevel !== pokemon.level) {
this.setLevel(pokemon.level);
this.lastLevel = pokemon.level;
}
const stats = pokemon.getStatStages();
const statsStr = stats.join("");
if (this.lastStats !== statsStr) {
this.updateStats(stats);
this.lastStats = statsStr;
}
this.shinyIcon.setVisible(pokemon.isShiny(true));
const doubleShiny = isFusion && pokemon.shiny && pokemon.fusionShiny;
const baseVariant = !doubleShiny ? pokemon.getVariant(true) : pokemon.variant;
this.shinyIcon.setTint(getVariantTint(baseVariant));
this.fusionShinyIcon.setVisible(doubleShiny).setPosition(this.shinyIcon.x, this.shinyIcon.y);
if (isFusion) {
this.fusionShinyIcon.setTint(getVariantTint(pokemon.fusionVariant));
}
resolve();
await promise;
}
//#endregion
updateNameText(pokemon: Pokemon): void {
let displayName = pokemon.getNameToRender().replace(/[♂♀]/g, "");
let nameTextWidth: number;
const nameSizeTest = addTextObject(0, 0, displayName, TextStyle.BATTLE_INFO);
nameTextWidth = nameSizeTest.displayWidth;
const gender = pokemon.summonData.illusion?.gender ?? pokemon.gender;
while (
nameTextWidth >
(this.player || !this.boss ? 60 : 98) -
((gender !== Gender.GENDERLESS ? 6 : 0) +
(pokemon.fusionSpecies ? 8 : 0) +
(pokemon.isShiny() ? 8 : 0) +
(Math.min(pokemon.level.toString().length, 3) - 3) * 8)
) {
displayName = `${displayName.slice(0, displayName.endsWith(".") ? -2 : -1).trimEnd()}.`;
nameSizeTest.setText(displayName);
nameTextWidth = nameSizeTest.displayWidth;
}
nameSizeTest.destroy();
this.nameText.setText(displayName);
this.lastName = pokemon.getNameToRender();
if (this.nameText.visible) {
this.nameText.setInteractive(
new Phaser.Geom.Rectangle(0, 0, this.nameText.width, this.nameText.height),
Phaser.Geom.Rectangle.Contains,
);
}
}
/**
* Set the level numbers container to display the provided level
*
* @remarks
* The numbers in the pokemon's level uses images for each number rather than a text object with a special font.
* This method sets the images for each digit of the level number and then positions the level container based
* on the number of digits.
*
* @param level - The level to display
* @param textureKey - The texture key for the level numbers
*/
setLevel(level: number, textureKey: "numbers" | "numbers_red" = "numbers"): void {
this.levelNumbersContainer.removeAll(true);
const levelStr = level.toString();
for (let i = 0; i < levelStr.length; i++) {
this.levelNumbersContainer.add(globalScene.add.image(i * 8, 0, textureKey, levelStr[i]));
}
this.levelContainer.setX(this.baseLvContainerX - 8 * Math.max(levelStr.length - 3, 0));
}
updateStats(stats: number[]): void {
for (const [i, s] of this.statOrder.entries()) {
if (s !== Stat.HP) {
this.statNumbers[i].setFrame(stats[s - 1].toString());
}
}
}
getBaseY(): number {
return this.baseY;
}
resetY(): void {
this.y = this.baseY;
}
}

View File

@ -0,0 +1,235 @@
import { globalScene } from "#app/global-scene";
import BattleFlyout from "../battle-flyout";
import { addTextObject, TextStyle } from "#app/ui/text";
import { addWindow, WindowVariant } from "#app/ui/ui-theme";
import { Stat } from "#enums/stat";
import i18next from "i18next";
import type { EnemyPokemon } from "#app/field/pokemon";
import type { GameObjects } from "phaser";
import BattleInfo from "./battle-info";
import type { BattleInfoParamList } from "./battle-info";
export class EnemyBattleInfo extends BattleInfo {
protected player: false = false;
protected championRibbon: Phaser.GameObjects.Sprite;
protected ownedIcon: Phaser.GameObjects.Sprite;
protected flyoutMenu: BattleFlyout;
protected hpBarSegmentDividers: GameObjects.Rectangle[] = [];
// #region Type effectiveness hint objects
protected effectivenessContainer: Phaser.GameObjects.Container;
protected effectivenessWindow: Phaser.GameObjects.NineSlice;
protected effectivenessText: Phaser.GameObjects.Text;
protected currentEffectiveness?: string;
// #endregion
override get statOrder(): Stat[] {
return [Stat.HP, Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.ACC, Stat.EVA, Stat.SPD];
}
override getTextureName(): string {
return this.boss ? "pbinfo_enemy_boss_mini" : "pbinfo_enemy_mini";
}
override constructTypeIcons(): void {
this.type1Icon = globalScene.add.sprite(-15, -15.5, "pbinfo_enemy_type1").setName("icon_type_1").setOrigin(0);
this.type2Icon = globalScene.add.sprite(-15, -2.5, "pbinfo_enemy_type2").setName("icon_type_2").setOrigin(0);
this.type3Icon = globalScene.add.sprite(0, 15.5, "pbinfo_enemy_type3").setName("icon_type_3").setOrigin(0);
this.add([this.type1Icon, this.type2Icon, this.type3Icon]);
}
constructor() {
const posParams: BattleInfoParamList = {
nameTextX: -124,
nameTextY: -11.2,
levelContainerX: -50,
levelContainerY: -5,
hpBarX: -71,
hpBarY: 4.5,
statBox: {
xOffset: 5,
paddingX: 2,
statOverflow: 0,
},
};
super(140, -141, false, posParams);
this.ownedIcon = globalScene.add
.sprite(0, 0, "icon_owned")
.setName("icon_owned")
.setVisible(false)
.setOrigin(0, 0)
.setPositionRelative(this.nameText, 0, 11.75);
this.championRibbon = globalScene.add
.sprite(0, 0, "champion_ribbon")
.setName("icon_champion_ribbon")
.setVisible(false)
.setOrigin(0, 0)
.setPositionRelative(this.nameText, 8, 11.75);
// Ensure these two icons are positioned below the stats container
this.addAt([this.ownedIcon, this.championRibbon], this.getIndex(this.statsContainer));
this.flyoutMenu = new BattleFlyout(this.player);
this.add(this.flyoutMenu);
this.moveBelow<Phaser.GameObjects.GameObject>(this.flyoutMenu, this.box);
this.effectivenessContainer = globalScene.add
.container(0, 0)
.setVisible(false)
.setPositionRelative(this.type1Icon, 22, 4);
this.add(this.effectivenessContainer);
this.effectivenessText = addTextObject(5, 4.5, "", TextStyle.BATTLE_INFO);
this.effectivenessWindow = addWindow(0, 0, 0, 20, undefined, false, undefined, undefined, WindowVariant.XTHIN);
this.effectivenessContainer.add([this.effectivenessWindow, this.effectivenessText]);
}
override initInfo(pokemon: EnemyPokemon): void {
this.flyoutMenu.initInfo(pokemon);
super.initInfo(pokemon);
if (this.nameText.visible) {
this.nameText
.on("pointerover", () =>
globalScene.ui.showTooltip(
"",
i18next.t("battleInfo:generation", {
generation: i18next.t(`starterSelectUiHandler:gen${pokemon.species.generation}`),
}),
),
)
.on("pointerout", () => globalScene.ui.hideTooltip());
}
const dexEntry = globalScene.gameData.dexData[pokemon.species.speciesId];
this.ownedIcon.setVisible(!!dexEntry.caughtAttr);
const opponentPokemonDexAttr = pokemon.getDexAttr();
if (
globalScene.gameMode.isClassic &&
globalScene.gameData.starterData[pokemon.species.getRootSpeciesId()].classicWinCount > 0 &&
globalScene.gameData.starterData[pokemon.species.getRootSpeciesId(true)].classicWinCount > 0
) {
this.championRibbon.setVisible(true);
}
// Check if Player owns all genders and forms of the Pokemon
const missingDexAttrs = (dexEntry.caughtAttr & opponentPokemonDexAttr) < opponentPokemonDexAttr;
const ownedAbilityAttrs = globalScene.gameData.starterData[pokemon.species.getRootSpeciesId()].abilityAttr;
// Check if the player owns ability for the root form
const playerOwnsThisAbility = pokemon.checkIfPlayerHasAbilityOfStarter(ownedAbilityAttrs);
if (missingDexAttrs || !playerOwnsThisAbility) {
this.ownedIcon.setTint(0x808080);
}
if (this.boss) {
this.updateBossSegmentDividers(pokemon as EnemyPokemon);
}
}
/**
* Show or hide the type effectiveness multiplier window
* Passing undefined will hide the window
*/
updateEffectiveness(effectiveness?: string) {
this.currentEffectiveness = effectiveness;
if (!globalScene.typeHints || effectiveness === undefined || this.flyoutMenu.flyoutVisible) {
this.effectivenessContainer.setVisible(false);
return;
}
this.effectivenessText.setText(effectiveness);
this.effectivenessWindow.width = 10 + this.effectivenessText.displayWidth;
this.effectivenessContainer.setVisible(true);
}
/**
* Request the flyoutMenu to toggle if available and hides or shows the effectiveness window where necessary
*/
toggleFlyout(visible: boolean): void {
this.flyoutMenu.toggleFlyout(visible);
if (visible) {
this.effectivenessContainer.setVisible(false);
} else {
this.updateEffectiveness(this.currentEffectiveness);
}
}
updateBossSegments(pokemon: EnemyPokemon): void {
const boss = !!pokemon.bossSegments;
if (boss !== this.boss) {
this.boss = boss;
[
this.nameText,
this.genderText,
this.teraIcon,
this.splicedIcon,
this.shinyIcon,
this.ownedIcon,
this.championRibbon,
this.statusIndicator,
this.levelContainer,
this.statValuesContainer,
].map(e => (e.x += 48 * (boss ? -1 : 1)));
this.hpBar.x += 38 * (boss ? -1 : 1);
this.hpBar.y += 2 * (this.boss ? -1 : 1);
this.hpBar.setTexture(`overlay_hp${boss ? "_boss" : ""}`);
this.box.setTexture(this.getTextureName());
this.statsBox.setTexture(`${this.getTextureName()}_stats`);
}
this.bossSegments = boss ? pokemon.bossSegments : 0;
this.updateBossSegmentDividers(pokemon);
}
updateBossSegmentDividers(pokemon: EnemyPokemon): void {
while (this.hpBarSegmentDividers.length) {
this.hpBarSegmentDividers.pop()?.destroy();
}
if (this.boss && this.bossSegments > 1) {
const uiTheme = globalScene.uiTheme;
const maxHp = pokemon.getMaxHp();
for (let s = 1; s < this.bossSegments; s++) {
const dividerX = (Math.round((maxHp / this.bossSegments) * s) / maxHp) * this.hpBar.width;
const divider = globalScene.add.rectangle(
0,
0,
1,
this.hpBar.height - (uiTheme ? 0 : 1),
pokemon.bossSegmentIndex >= s ? 0xffffff : 0x404040,
);
divider.setOrigin(0.5, 0).setName("hpBar_divider_" + s.toString());
this.add(divider);
this.moveBelow(divider as Phaser.GameObjects.GameObject, this.statsContainer);
divider.setPositionRelative(this.hpBar, dividerX, uiTheme ? 0 : 1);
this.hpBarSegmentDividers.push(divider);
}
}
}
override updateStatusIcon(pokemon: EnemyPokemon): void {
super.updateStatusIcon(pokemon, (this.ownedIcon.visible ? 8 : 0) + (this.championRibbon.visible ? 8 : 0));
}
protected override updatePokemonHp(
pokemon: EnemyPokemon,
resolve: (r: void | PromiseLike<void>) => void,
instant?: boolean,
): void {
super.updatePokemonHp(pokemon, resolve, instant);
this.lastHp = pokemon.hp;
}
}

View File

@ -0,0 +1,242 @@
import { getLevelRelExp, getLevelTotalExp } from "#app/data/exp";
import type { PlayerPokemon } from "#app/field/pokemon";
import { globalScene } from "#app/global-scene";
import { ExpGainsSpeed } from "#enums/exp-gains-speed";
import { Stat } from "#enums/stat";
import BattleInfo from "./battle-info";
import type { BattleInfoParamList } from "./battle-info";
export class PlayerBattleInfo extends BattleInfo {
protected player: true = true;
protected hpNumbersContainer: Phaser.GameObjects.Container;
override get statOrder(): Stat[] {
return [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.ACC, Stat.EVA, Stat.SPD];
}
override getTextureName(): string {
return this.mini ? "pbinfo_player_mini" : "pbinfo_player";
}
override constructTypeIcons(): void {
this.type1Icon = globalScene.add.sprite(-139, -17, "pbinfo_player_type1").setName("icon_type_1").setOrigin(0);
this.type2Icon = globalScene.add.sprite(-139, -1, "pbinfo_player_type2").setName("icon_type_2").setOrigin(0);
this.type3Icon = globalScene.add.sprite(-154, -17, "pbinfo_player_type3").setName("icon_type_3").setOrigin(0);
this.add([this.type1Icon, this.type2Icon, this.type3Icon]);
}
constructor() {
const posParams: BattleInfoParamList = {
nameTextX: -115,
nameTextY: -15.2,
levelContainerX: -41,
levelContainerY: -10,
hpBarX: -61,
hpBarY: -1,
statBox: {
xOffset: 8,
paddingX: 4,
statOverflow: 1,
},
};
super(Math.floor(globalScene.game.canvas.width / 6) - 10, -72, true, posParams);
this.hpNumbersContainer = globalScene.add.container(-15, 10).setName("container_hp");
// hp number container must be beneath the stat container for overlay to display properly
this.addAt(this.hpNumbersContainer, this.getIndex(this.statsContainer));
const expBar = globalScene.add.image(-98, 18, "overlay_exp").setName("overlay_exp").setOrigin(0);
this.add(expBar);
const expMaskRect = globalScene.make
.graphics({})
.setScale(6)
.fillStyle(0xffffff)
.beginPath()
.fillRect(127, 126, 85, 2);
const expMask = expMaskRect.createGeometryMask();
expBar.setMask(expMask);
this.expBar = expBar;
this.expMaskRect = expMaskRect;
}
override initInfo(pokemon: PlayerPokemon): void {
super.initInfo(pokemon);
this.setHpNumbers(pokemon.hp, pokemon.getMaxHp());
this.expMaskRect.x = (pokemon.levelExp / getLevelTotalExp(pokemon.level, pokemon.species.growthRate)) * 510;
this.lastExp = pokemon.exp;
this.lastLevelExp = pokemon.levelExp;
this.statValuesContainer.setPosition(8, 7);
}
override setMini(mini: boolean): void {
if (this.mini === mini) {
return;
}
this.mini = mini;
this.box.setTexture(this.getTextureName());
this.statsBox.setTexture(`${this.getTextureName()}_stats`);
if (this.player) {
this.y -= 12 * (mini ? 1 : -1);
this.baseY = this.y;
}
const offsetElements = [
this.nameText,
this.genderText,
this.teraIcon,
this.splicedIcon,
this.shinyIcon,
this.statusIndicator,
this.levelContainer,
];
offsetElements.forEach(el => (el.y += 1.5 * (mini ? -1 : 1)));
[this.type1Icon, this.type2Icon, this.type3Icon].forEach(el => {
el.x += 4 * (mini ? 1 : -1);
el.y += -8 * (mini ? 1 : -1);
});
this.statValuesContainer.x += 2 * (mini ? 1 : -1);
this.statValuesContainer.y += -7 * (mini ? 1 : -1);
const toggledElements = [this.hpNumbersContainer, this.expBar];
toggledElements.forEach(el => el.setVisible(!mini));
}
/**
* Updates the Hp Number text (that is the "HP/Max HP" text that appears below the player's health bar)
* while the health bar is tweening.
* @param pokemon - The Pokemon the health bar belongs to.
*/
protected override onHpTweenUpdate(pokemon: PlayerPokemon): void {
const tweenHp = Math.ceil(this.hpBar.scaleX * pokemon.getMaxHp());
this.setHpNumbers(tweenHp, pokemon.getMaxHp());
this.lastHp = tweenHp;
this.updateHpFrame();
}
updatePokemonExp(pokemon: PlayerPokemon, instant?: boolean, levelDurationMultiplier = 1): Promise<void> {
const levelUp = this.lastLevel < pokemon.level;
const relLevelExp = getLevelRelExp(this.lastLevel + 1, pokemon.species.growthRate);
const levelExp = levelUp ? relLevelExp : pokemon.levelExp;
let ratio = relLevelExp ? levelExp / relLevelExp : 0;
if (this.lastLevel >= globalScene.getMaxExpLevel(true)) {
ratio = levelUp ? 1 : 0;
instant = true;
}
const durationMultiplier = Phaser.Tweens.Builders.GetEaseFunction("Sine.easeIn")(
1 - Math.max(this.lastLevel - 100, 0) / 150,
);
let duration =
this.visible && !instant
? ((levelExp - this.lastLevelExp) / relLevelExp) *
BattleInfo.EXP_GAINS_DURATION_BASE *
durationMultiplier *
levelDurationMultiplier
: 0;
const speed = globalScene.expGainsSpeed;
if (speed && speed >= ExpGainsSpeed.DEFAULT) {
duration = speed >= ExpGainsSpeed.SKIP ? ExpGainsSpeed.DEFAULT : duration / Math.pow(2, speed);
}
if (ratio === 1) {
this.lastLevelExp = 0;
this.lastLevel++;
} else {
this.lastExp = pokemon.exp;
this.lastLevelExp = pokemon.levelExp;
}
if (duration) {
globalScene.playSound("se/exp");
}
return new Promise(resolve => {
globalScene.tweens.add({
targets: this.expMaskRect,
ease: "Sine.easeIn",
x: ratio * 510,
duration: duration,
onComplete: () => {
if (!globalScene) {
return resolve();
}
if (duration) {
globalScene.sound.stopByKey("se/exp");
}
if (ratio === 1) {
globalScene.playSound("se/level_up");
this.setLevel(this.lastLevel);
globalScene.time.delayedCall(500 * levelDurationMultiplier, () => {
this.expMaskRect.x = 0;
this.updateInfo(pokemon, instant).then(() => resolve());
});
return;
}
resolve();
},
});
});
}
/**
* Updates the info on the info bar.
*
* In addition to performing all the steps of {@linkcode BattleInfo.updateInfo},
* it also updates the EXP Bar
*/
override async updateInfo(pokemon: PlayerPokemon, instant?: boolean): Promise<void> {
await super.updateInfo(pokemon, instant);
const isLevelCapped = pokemon.level >= globalScene.getMaxExpLevel();
const oldLevelCapped = this.lastLevelCapped;
this.lastLevelCapped = isLevelCapped;
if (this.lastExp !== pokemon.exp || this.lastLevel !== pokemon.level) {
const durationMultipler = Math.max(
Phaser.Tweens.Builders.GetEaseFunction("Cubic.easeIn")(1 - Math.min(pokemon.level - this.lastLevel, 10) / 10),
0.1,
);
await this.updatePokemonExp(pokemon, false, durationMultipler);
} else if (isLevelCapped !== oldLevelCapped) {
this.setLevel(pokemon.level);
}
}
/**
* Set the HP numbers text, that is the "HP/Max HP" text that appears below the player's health bar.
* @param hp - The current HP of the player.
* @param maxHp - The maximum HP of the player.
*/
setHpNumbers(hp: number, maxHp: number): void {
if (!globalScene) {
return;
}
this.hpNumbersContainer.removeAll(true);
const hpStr = hp.toString();
const maxHpStr = maxHp.toString();
let offset = 0;
for (let i = maxHpStr.length - 1; i >= 0; i--) {
this.hpNumbersContainer.add(globalScene.add.image(offset++ * -8, 0, "numbers", maxHpStr[i]));
}
this.hpNumbersContainer.add(globalScene.add.image(offset++ * -8, 0, "numbers", "/"));
for (let i = hpStr.length - 1; i >= 0; i--) {
this.hpNumbersContainer.add(globalScene.add.image(offset++ * -8, 0, "numbers", hpStr[i]));
}
}
/**
* Set the level numbers container to display the provided level
*
* Overrides the default implementation to handle displaying level capped numbers in red.
* @param level - The level to display
*/
override setLevel(level: number): void {
super.setLevel(level, level >= globalScene.getMaxExpLevel() ? "numbers_red" : "numbers");
}
}

View File

@ -10,7 +10,7 @@ import { getLocalizedSpriteKey, fixedInt, padInt } from "#app/utils/common";
import { MoveCategory } from "#enums/MoveCategory";
import i18next from "i18next";
import { Button } from "#enums/buttons";
import type { PokemonMove } from "#app/field/pokemon";
import type { EnemyPokemon, PokemonMove } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon";
import type { CommandPhase } from "#app/phases/command-phase";
import MoveInfoOverlay from "./move-info-overlay";
@ -279,7 +279,7 @@ export default class FightUiHandler extends UiHandler implements InfoToggle {
this.moveInfoOverlay.show(pokemonMove.getMove());
pokemon.getOpponents().forEach(opponent => {
opponent.updateEffectiveness(this.getEffectivenessText(pokemon, opponent, pokemonMove));
(opponent as EnemyPokemon).updateEffectiveness(this.getEffectivenessText(pokemon, opponent, pokemonMove));
});
}
@ -391,7 +391,7 @@ export default class FightUiHandler extends UiHandler implements InfoToggle {
const opponents = (globalScene.getCurrentPhase() as CommandPhase).getPokemon().getOpponents();
opponents.forEach(opponent => {
opponent.updateEffectiveness(undefined);
(opponent as EnemyPokemon).updateEffectiveness();
});
}

View File

@ -151,9 +151,9 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
// Prevent overlapping overrides on action modification
this.submitAction = originalLoginAction;
this.sanitizeInputs();
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] });
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] });
const onFail = error => {
globalScene.ui.setMode(UiMode.LOGIN_FORM, Object.assign(config, { errorMessage: error?.trim() }));
globalScene.ui.setMode(UiMode.LOGIN_FORM, Object.assign(config, { errorMessage: error?.trim() }));
globalScene.ui.playError();
};
if (!this.inputs[0].text) {
@ -243,7 +243,7 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
},
});
}
globalScene.ui.setOverlayMode(UiMode.OPTION_SELECT, {
globalScene.ui.setOverlayMode(UiMode.OPTION_SELECT, {
options: options,
delay: 1000,
});

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ import type Pokemon from "../field/pokemon";
import i18next from "i18next";
import type { DexEntry, StarterDataEntry } from "../system/game-data";
import { DexAttr } from "../system/game-data";
import { fixedInt } from "#app/utils/common";
import { fixedInt, getShinyDescriptor } from "#app/utils/common";
import ConfirmUiHandler from "./confirm-ui-handler";
import { StatsContainer } from "./stats-container";
import { TextStyle, addBBCodeTextObject, addTextObject, getTextColor } from "./text";
@ -343,18 +343,19 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container {
this.pokemonShinyIcon.setVisible(pokemon.isShiny());
this.pokemonShinyIcon.setTint(getVariantTint(baseVariant));
if (this.pokemonShinyIcon.visible) {
const shinyDescriptor =
doubleShiny || baseVariant
? `${baseVariant === 2 ? i18next.t("common:epicShiny") : baseVariant === 1 ? i18next.t("common:rareShiny") : i18next.t("common:commonShiny")}${doubleShiny ? `/${pokemon.fusionVariant === 2 ? i18next.t("common:epicShiny") : pokemon.fusionVariant === 1 ? i18next.t("common:rareShiny") : i18next.t("common:commonShiny")}` : ""}`
: "";
this.pokemonShinyIcon.on("pointerover", () =>
globalScene.ui.showTooltip(
"",
`${i18next.t("common:shinyOnHover")}${shinyDescriptor ? ` (${shinyDescriptor})` : ""}`,
true,
),
);
this.pokemonShinyIcon.on("pointerout", () => globalScene.ui.hideTooltip());
let shinyDescriptor = "";
if (doubleShiny || baseVariant) {
shinyDescriptor = " (" + getShinyDescriptor(baseVariant);
if (doubleShiny) {
shinyDescriptor += "/" + getShinyDescriptor(pokemon.fusionVariant);
}
shinyDescriptor += ")";
}
this.pokemonShinyIcon
.on("pointerover", () =>
globalScene.ui.showTooltip("", i18next.t("common:shinyOnHover") + shinyDescriptor, true),
)
.on("pointerout", () => globalScene.ui.hideTooltip());
const newShiny = BigInt(1 << (pokemon.shiny ? 1 : 0));
const newVariant = BigInt(1 << (pokemon.variant + 4));

View File

@ -108,17 +108,21 @@ const languageSettings: { [key: string]: LanguageSetting } = {
instructionTextSize: "38px",
},
de: {
starterInfoTextSize: "48px",
starterInfoTextSize: "54px",
instructionTextSize: "35px",
starterInfoXPos: 33,
starterInfoXPos: 35,
},
"es-ES": {
starterInfoTextSize: "52px",
instructionTextSize: "35px",
starterInfoTextSize: "50px",
instructionTextSize: "38px",
starterInfoYOffset: 0.5,
starterInfoXPos: 38,
},
"es-MX": {
starterInfoTextSize: "52px",
instructionTextSize: "35px",
starterInfoTextSize: "50px",
instructionTextSize: "38px",
starterInfoYOffset: 0.5,
starterInfoXPos: 38,
},
fr: {
starterInfoTextSize: "54px",
@ -128,21 +132,16 @@ const languageSettings: { [key: string]: LanguageSetting } = {
starterInfoTextSize: "56px",
instructionTextSize: "38px",
},
pt_BR: {
starterInfoTextSize: "47px",
instructionTextSize: "38px",
"pt-BR": {
starterInfoTextSize: "48px",
instructionTextSize: "42px",
starterInfoYOffset: 0.5,
starterInfoXPos: 33,
},
zh: {
starterInfoTextSize: "47px",
instructionTextSize: "38px",
starterInfoYOffset: 1,
starterInfoXPos: 24,
},
pt: {
starterInfoTextSize: "48px",
instructionTextSize: "42px",
starterInfoXPos: 33,
starterInfoTextSize: "56px",
instructionTextSize: "36px",
starterInfoXPos: 26,
},
ko: {
starterInfoTextSize: "60px",
@ -156,9 +155,11 @@ const languageSettings: { [key: string]: LanguageSetting } = {
starterInfoYOffset: 0.5,
starterInfoXPos: 33,
},
"ca-ES": {
starterInfoTextSize: "52px",
ca: {
starterInfoTextSize: "48px",
instructionTextSize: "38px",
starterInfoYOffset: 0.5,
starterInfoXPos: 29,
},
};

View File

@ -11,6 +11,7 @@ import {
isNullOrUndefined,
toReadableString,
formatStat,
getShinyDescriptor,
} from "#app/utils/common";
import type { PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters";
@ -444,18 +445,19 @@ export default class SummaryUiHandler extends UiHandler {
this.shinyIcon.setVisible(this.pokemon.isShiny(false));
this.shinyIcon.setTint(getVariantTint(baseVariant));
if (this.shinyIcon.visible) {
const shinyDescriptor =
doubleShiny || baseVariant
? `${baseVariant === 2 ? i18next.t("common:epicShiny") : baseVariant === 1 ? i18next.t("common:rareShiny") : i18next.t("common:commonShiny")}${doubleShiny ? `/${this.pokemon.fusionVariant === 2 ? i18next.t("common:epicShiny") : this.pokemon.fusionVariant === 1 ? i18next.t("common:rareShiny") : i18next.t("common:commonShiny")}` : ""}`
: "";
this.shinyIcon.on("pointerover", () =>
globalScene.ui.showTooltip(
"",
`${i18next.t("common:shinyOnHover")}${shinyDescriptor ? ` (${shinyDescriptor})` : ""}`,
true,
),
);
this.shinyIcon.on("pointerout", () => globalScene.ui.hideTooltip());
let shinyDescriptor = "";
if (doubleShiny || baseVariant) {
shinyDescriptor = " (" + getShinyDescriptor(baseVariant);
if (doubleShiny) {
shinyDescriptor += "/" + getShinyDescriptor(this.pokemon.fusionVariant);
}
shinyDescriptor += ")";
}
this.shinyIcon
.on("pointerover", () =>
globalScene.ui.showTooltip("", i18next.t("common:shinyOnHover") + shinyDescriptor, true),
)
.on("pointerout", () => globalScene.ui.hideTooltip());
}
this.fusionShinyIcon.setPosition(this.shinyIcon.x, this.shinyIcon.y);

View File

@ -2,6 +2,7 @@ import { MoneyFormat } from "#enums/money-format";
import { Moves } from "#enums/moves";
import i18next from "i18next";
import { pokerogueApi } from "#app/plugins/api/pokerogue-api";
import type { Variant } from "#app/sprites/variant";
export type nil = null | undefined;
@ -576,3 +577,18 @@ export function animationFileName(move: Moves): string {
export function camelCaseToKebabCase(str: string): string {
return str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, (s, o) => (o ? "-" : "") + s.toLowerCase());
}
/** Get the localized shiny descriptor for the provided variant
* @param variant - The variant to get the shiny descriptor for
* @returns The localized shiny descriptor
*/
export function getShinyDescriptor(variant: Variant): string {
switch (variant) {
case 2:
return i18next.t("common:epicShiny");
case 1:
return i18next.t("common:rareShiny");
case 0:
return i18next.t("common:commonShiny");
}
}

View File

@ -25,7 +25,7 @@ describe("Moves - Gastro Acid", () => {
game.override.battleStyle("double");
game.override.startingLevel(1);
game.override.enemyLevel(100);
game.override.ability(Abilities.NONE);
game.override.ability(Abilities.BALL_FETCH);
game.override.moveset([Moves.GASTRO_ACID, Moves.WATER_GUN, Moves.SPLASH, Moves.CORE_ENFORCER]);
game.override.enemySpecies(Species.BIDOOF);
game.override.enemyMoveset(Moves.SPLASH);
@ -40,7 +40,7 @@ describe("Moves - Gastro Acid", () => {
* - player mon 1 should have dealt damage, player mon 2 should have not
*/
await game.startBattle();
await game.classicMode.startBattle();
game.move.select(Moves.GASTRO_ACID, 0, BattlerIndex.ENEMY);
game.move.select(Moves.SPLASH, 1);
@ -63,7 +63,7 @@ describe("Moves - Gastro Acid", () => {
it("fails if used on an enemy with an already-suppressed ability", async () => {
game.override.battleStyle("single");
await game.startBattle();
await game.classicMode.startBattle();
game.move.select(Moves.CORE_ENFORCER);
// Force player to be slower to enable Core Enforcer to proc its suppression effect
@ -77,4 +77,27 @@ describe("Moves - Gastro Acid", () => {
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
it("should suppress the passive of a target even if its main ability is unsuppressable and not suppress main abli", async () => {
game.override
.enemyAbility(Abilities.COMATOSE)
.enemyPassiveAbility(Abilities.WATER_ABSORB)
.moveset([Moves.SPLASH, Moves.GASTRO_ACID, Moves.WATER_GUN]);
await game.classicMode.startBattle([Species.MAGIKARP]);
const enemyPokemon = game.scene.getEnemyPokemon();
game.move.select(Moves.GASTRO_ACID);
await game.toNextTurn();
expect(enemyPokemon?.summonData.abilitySuppressed).toBe(true);
game.move.select(Moves.WATER_GUN);
await game.toNextTurn();
expect(enemyPokemon?.getHpRatio()).toBeLessThan(1);
game.move.select(Moves.SPORE);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon?.status?.effect).toBeFalsy();
});
});

View File

@ -1,3 +1,4 @@
export interface MockGameObject {
name: string;
destroy?(): void;
}

View File

@ -2,199 +2,256 @@ import type MockTextureManager from "#test/testUtils/mocks/mockTextureManager";
import type { MockGameObject } from "../mockGameObject";
export default class MockContainer implements MockGameObject {
protected x;
protected y;
protected x: number;
protected y: number;
protected scene;
protected width;
protected height;
protected visible;
private alpha;
protected width: number;
protected height: number;
protected visible: boolean;
private alpha: number;
private style;
public frame;
protected textureManager;
public list: MockGameObject[] = [];
public name: string;
constructor(textureManager: MockTextureManager, x, y) {
constructor(textureManager: MockTextureManager, x: number, y: number) {
this.x = x;
this.y = y;
this.frame = {};
this.textureManager = textureManager;
}
setVisible(visible) {
setVisible(visible: boolean): this {
this.visible = visible;
return this;
}
once(_event, _callback, _source) {}
once(_event, _callback, _source): this {
return this;
}
off(_event, _callback, _source) {}
off(_event, _callback, _source): this {
return this;
}
removeFromDisplayList() {
removeFromDisplayList(): this {
// same as remove or destroy
return this;
}
removeBetween(_startIndex, _endIndex, _destroyChild) {
removeBetween(_startIndex, _endIndex, _destroyChild): this {
// Removes multiple children across an index range
return this;
}
addedToScene() {
// This callback is invoked when this Game Object is added to a Scene.
}
setSize(_width, _height) {
setSize(_width: number, _height: number): this {
// Sets the size of this Game Object.
return this;
}
setMask() {
setMask(): this {
/// Sets the mask that this Game Object will use to render with.
return this;
}
setPositionRelative(_source, _x, _y) {
setPositionRelative(_source, _x, _y): this {
/// Sets the position of this Game Object to be a relative position from the source Game Object.
return this;
}
setInteractive = () => null;
setInteractive(): this {
return this;
}
setOrigin(x, y) {
setOrigin(x = 0.5, y = x): this {
this.x = x;
this.y = y;
return this;
}
setAlpha(alpha) {
setAlpha(alpha = 1): this {
this.alpha = alpha;
return this;
}
setFrame(_frame, _updateSize?: boolean, _updateOrigin?: boolean) {
setFrame(_frame, _updateSize?: boolean, _updateOrigin?: boolean): this {
// Sets the frame this Game Object will use to render with.
return this;
}
setScale(_scale) {
setScale(_x = 1, _y = _x): this {
// Sets the scale of this Game Object.
return this;
}
setPosition(x, y) {
setPosition(x = 0, y = x, _z = 0, _w = 0): this {
this.x = x;
this.y = y;
return this;
}
setX(x) {
setX(x = 0): this {
this.x = x;
return this;
}
setY(y) {
setY(y = 0): this {
this.y = y;
return this;
}
destroy() {
this.list = [];
}
setShadow(_shadowXpos, _shadowYpos, _shadowColor) {
setShadow(_shadowXpos, _shadowYpos, _shadowColor): this {
// Sets the shadow settings for this Game Object.
return this;
}
setLineSpacing(_lineSpacing) {
setLineSpacing(_lineSpacing): this {
// Sets the line spacing value of this Game Object.
return this;
}
setText(_text) {
setText(_text): this {
// Sets the text this Game Object will display.
return this;
}
setAngle(_angle) {
setAngle(_angle): this {
// Sets the angle of this Game Object.
return this;
}
setShadowOffset(_offsetX, _offsetY) {
setShadowOffset(_offsetX, _offsetY): this {
// Sets the shadow offset values.
return this;
}
setWordWrapWidth(_width) {
// Sets the width (in pixels) to use for wrapping lines.
}
setFontSize(_fontSize) {
setFontSize(_fontSize): this {
// Sets the font size of this Game Object.
return this;
}
getBounds() {
return { width: this.width, height: this.height };
}
setColor(_color) {
setColor(_color): this {
// Sets the tint of this Game Object.
return this;
}
setShadowColor(_color) {
setShadowColor(_color): this {
// Sets the shadow color.
return this;
}
setTint(_color) {
setTint(_color: this) {
// Sets the tint of this Game Object.
return this;
}
setStrokeStyle(_thickness, _color) {
setStrokeStyle(_thickness, _color): this {
// Sets the stroke style for the graphics.
return this;
}
setDepth(_depth) {
// Sets the depth of this Game Object.
setDepth(_depth): this {
// Sets the depth of this Game Object.\
return this;
}
setTexture(_texture) {
// Sets the texture this Game Object will use to render with.
setTexture(_texture): this {
// Sets the texture this Game Object will use to render with.\
return this;
}
clearTint() {
// Clears any previously set tint.
clearTint(): this {
// Clears any previously set tint.\
return this;
}
sendToBack() {
// Sends this Game Object to the back of its parent's display list.
sendToBack(): this {
// Sends this Game Object to the back of its parent's display list.\
return this;
}
moveTo(_obj) {
// Moves this Game Object to the given index in the list.
moveTo(_obj): this {
// Moves this Game Object to the given index in the list.\
return this;
}
moveAbove(_obj) {
moveAbove(_obj): this {
// Moves this Game Object to be above the given Game Object in the display list.
return this;
}
moveBelow(_obj) {
moveBelow(_obj): this {
// Moves this Game Object to be below the given Game Object in the display list.
return this;
}
setName(name: string) {
setName(name: string): this {
this.name = name;
return this;
}
bringToTop(_obj) {
bringToTop(_obj): this {
// Brings this Game Object to the top of its parents display list.
return this;
}
on(_event, _callback, _source) {}
add(obj) {
// Adds a child to this Game Object.
this.list.push(obj);
on(_event, _callback, _source): this {
return this;
}
removeAll() {
add(obj: MockGameObject | MockGameObject[]): this {
if (Array.isArray(obj)) {
this.list.push(...obj);
} else {
this.list.push(obj);
}
return this;
}
removeAll(): this {
// Removes all Game Objects from this Container.
this.list = [];
return this;
}
addAt(obj, index) {
addAt(obj: MockGameObject | MockGameObject[], index = 0): this {
// Adds a Game Object to this Container at the given index.
this.list.splice(index, 0, obj);
if (!Array.isArray(obj)) {
obj = [obj];
}
this.list.splice(index, 0, ...obj);
return this;
}
remove(obj) {
const index = this.list.indexOf(obj);
if (index !== -1) {
this.list.splice(index, 1);
remove(obj: MockGameObject | MockGameObject[], destroyChild = false): this {
if (!Array.isArray(obj)) {
obj = [obj];
}
for (const item of obj) {
const index = this.list.indexOf(item);
if (index !== -1) {
this.list.splice(index, 1);
}
if (destroyChild) {
item.destroy?.();
}
}
return this;
}
getIndex(obj) {
@ -210,15 +267,43 @@ export default class MockContainer implements MockGameObject {
return this.list;
}
getByName(key: string) {
getByName(key: string): MockGameObject | null {
return this.list.find(v => v.name === key) ?? new MockContainer(this.textureManager, 0, 0);
}
disableInteractive = () => null;
disableInteractive(): this {
return this;
}
each(method) {
for (const item of this.list) {
method(item);
// biome-ignore lint/complexity/noBannedTypes: This matches the signature of the method it mocks
each(callback: Function, context?: object, ...args: any[]): this {
if (context !== undefined) {
callback = callback.bind(context);
}
for (const item of this.list.slice()) {
callback(item, ...args);
}
return this;
}
// biome-ignore lint/complexity/noBannedTypes: This matches the signature of the method it mocks
iterate(callback: Function, context?: object, ...args: any[]): this {
if (context !== undefined) {
callback = callback.bind(context);
}
for (const item of this.list) {
callback(item, ...args);
}
return this;
}
copyPosition(source: { x?: number; y?: number }): this {
if (source.x !== undefined) {
this.x = source.x;
}
if (source.y !== undefined) {
this.y = source.y;
}
return this;
}
}

View File

@ -8,57 +8,76 @@ export default class MockGraphics implements MockGameObject {
this.scene = textureManager.scene;
}
fillStyle(_color) {
fillStyle(_color): this {
// Sets the fill style to be used by the fill methods.
return this;
}
beginPath() {
beginPath(): this {
// Starts a new path by emptying the list of sub-paths. Call this method when you want to create a new path.
return this;
}
fillRect(_x, _y, _width, _height) {
fillRect(_x, _y, _width, _height): this {
// Adds a rectangle shape to the path which is filled when you call fill().
return this;
}
createGeometryMask() {
createGeometryMask(): this {
// Creates a geometry mask.
return this;
}
setOrigin(_x, _y) {}
setOrigin(_x, _y): this {
return this;
}
setAlpha(_alpha) {}
setAlpha(_alpha): this {
return this;
}
setVisible(_visible) {}
setVisible(_visible): this {
return this;
}
setName(_name) {}
setName(_name) {
return this;
}
once(_event, _callback, _source) {}
once(_event, _callback, _source) {
return this;
}
removeFromDisplayList() {
removeFromDisplayList(): this {
// same as remove or destroy
return this;
}
addedToScene() {
// This callback is invoked when this Game Object is added to a Scene.
}
setPositionRelative(_source, _x, _y) {
setPositionRelative(_source, _x, _y): this {
/// Sets the position of this Game Object to be a relative position from the source Game Object.
return this;
}
destroy() {
this.list = [];
}
setScale(_scale) {
// Sets the scale of this Game Object.
setScale(_scale): this {
return this;
}
off(_event, _callback, _source) {}
off(_event, _callback, _source): this {
return this;
}
add(obj) {
add(obj): this {
// Adds a child to this Game Object.
this.list.push(obj);
return this;
}
removeAll() {
@ -90,4 +109,8 @@ export default class MockGraphics implements MockGameObject {
getAll() {
return this.list;
}
copyPosition(_source): this {
return this;
}
}

View File

@ -10,34 +10,51 @@ export default class MockRectangle implements MockGameObject {
this.fillColor = fillColor;
this.scene = textureManager.scene;
}
setOrigin(_x, _y) {}
setOrigin(_x, _y): this {
return this;
}
setAlpha(_alpha) {}
setVisible(_visible) {}
setAlpha(_alpha): this {
return this;
}
setVisible(_visible): this {
return this;
}
setName(_name) {}
setName(_name): this {
return this;
}
once(_event, _callback, _source) {}
once(_event, _callback, _source): this {
return this;
}
removeFromDisplayList() {
removeFromDisplayList(): this {
// same as remove or destroy
return this;
}
addedToScene() {
// This callback is invoked when this Game Object is added to a Scene.
}
setPositionRelative(_source, _x, _y) {
setPositionRelative(_source, _x, _y): this {
/// Sets the position of this Game Object to be a relative position from the source Game Object.
return this;
}
destroy() {
this.list = [];
}
add(obj) {
add(obj: MockGameObject | MockGameObject[]): this {
// Adds a child to this Game Object.
this.list.push(obj);
if (Array.isArray(obj)) {
this.list.push(...obj);
} else {
this.list.push(obj);
}
return this;
}
removeAll() {
@ -45,16 +62,18 @@ export default class MockRectangle implements MockGameObject {
this.list = [];
}
addAt(obj, index) {
addAt(obj, index): this {
// Adds a Game Object to this Container at the given index.
this.list.splice(index, 0, obj);
return this;
}
remove(obj) {
remove(obj): this {
const index = this.list.indexOf(obj);
if (index !== -1) {
this.list.splice(index, 1);
}
return this;
}
getIndex(obj) {
@ -69,9 +88,12 @@ export default class MockRectangle implements MockGameObject {
getAll() {
return this.list;
}
setScale(_scale) {
setScale(_scale): this {
// return this.phaserText.setScale(scale);
return this;
}
off() {}
off(): this {
return this;
}
}

View File

@ -1,6 +1,5 @@
import Phaser from "phaser";
import type { MockGameObject } from "../mockGameObject";
import Sprite = Phaser.GameObjects.Sprite;
import Frame = Phaser.Textures.Frame;
export default class MockSprite implements MockGameObject {
@ -21,7 +20,9 @@ export default class MockSprite implements MockGameObject {
Phaser.GameObjects.Sprite.prototype.setInteractive = this.setInteractive;
// @ts-ignore
Phaser.GameObjects.Sprite.prototype.setTexture = this.setTexture;
// @ts-ignore
Phaser.GameObjects.Sprite.prototype.setSizeToFrame = this.setSizeToFrame;
// @ts-ignore
Phaser.GameObjects.Sprite.prototype.setFrame = this.setFrame;
// Phaser.GameObjects.Sprite.prototype.disable = this.disable;
@ -37,46 +38,55 @@ export default class MockSprite implements MockGameObject {
};
}
setTexture(_key: string, _frame?: string | number) {
setTexture(_key: string, _frame?: string | number): this {
return this;
}
setSizeToFrame(_frame?: boolean | Frame): Sprite {
return {} as Sprite;
setSizeToFrame(_frame?: boolean | Frame): this {
return this;
}
setPipeline(obj) {
setPipeline(obj): this {
// Sets the pipeline of this Game Object.
return this.phaserSprite.setPipeline(obj);
this.phaserSprite.setPipeline(obj);
return this;
}
off(_event, _callback, _source) {}
off(_event, _callback, _source): this {
return this;
}
setTintFill(color) {
setTintFill(color): this {
// Sets the tint fill color.
return this.phaserSprite.setTintFill(color);
this.phaserSprite.setTintFill(color);
return this;
}
setScale(scale) {
return this.phaserSprite.setScale(scale);
setScale(scale = 1): this {
this.phaserSprite.setScale(scale);
return this;
}
setOrigin(x, y) {
return this.phaserSprite.setOrigin(x, y);
setOrigin(x = 0.5, y = x): this {
this.phaserSprite.setOrigin(x, y);
return this;
}
setSize(width, height) {
setSize(width, height): this {
// Sets the size of this Game Object.
return this.phaserSprite.setSize(width, height);
this.phaserSprite.setSize(width, height);
return this;
}
once(event, callback, source) {
return this.phaserSprite.once(event, callback, source);
once(event, callback, source): this {
this.phaserSprite.once(event, callback, source);
return this;
}
removeFromDisplayList() {
removeFromDisplayList(): this {
// same as remove or destroy
return this.phaserSprite.removeFromDisplayList();
this.phaserSprite.removeFromDisplayList();
return this;
}
addedToScene() {
@ -84,97 +94,121 @@ export default class MockSprite implements MockGameObject {
return this.phaserSprite.addedToScene();
}
setVisible(visible) {
return this.phaserSprite.setVisible(visible);
setVisible(visible): this {
this.phaserSprite.setVisible(visible);
return this;
}
setPosition(x, y) {
return this.phaserSprite.setPosition(x, y);
setPosition(x, y): this {
this.phaserSprite.setPosition(x, y);
return this;
}
setRotation(radians) {
return this.phaserSprite.setRotation(radians);
setRotation(radians): this {
this.phaserSprite.setRotation(radians);
return this;
}
stop() {
return this.phaserSprite.stop();
stop(): this {
this.phaserSprite.stop();
return this;
}
setInteractive = () => null;
on(event, callback, source) {
return this.phaserSprite.on(event, callback, source);
setInteractive(): this {
return this;
}
setAlpha(alpha) {
return this.phaserSprite.setAlpha(alpha);
on(event, callback, source): this {
this.phaserSprite.on(event, callback, source);
return this;
}
setTint(color) {
setAlpha(alpha): this {
this.phaserSprite.setAlpha(alpha);
return this;
}
setTint(color): this {
// Sets the tint of this Game Object.
return this.phaserSprite.setTint(color);
this.phaserSprite.setTint(color);
return this;
}
setFrame(frame, _updateSize?: boolean, _updateOrigin?: boolean) {
setFrame(frame, _updateSize?: boolean, _updateOrigin?: boolean): this {
// Sets the frame this Game Object will use to render with.
this.frame = frame;
return frame;
return this;
}
setPositionRelative(source, x, y) {
setPositionRelative(source, x, y): this {
/// Sets the position of this Game Object to be a relative position from the source Game Object.
return this.phaserSprite.setPositionRelative(source, x, y);
this.phaserSprite.setPositionRelative(source, x, y);
return this;
}
setY(y) {
return this.phaserSprite.setY(y);
setY(y: number): this {
this.phaserSprite.setY(y);
return this;
}
setCrop(x, y, width, height) {
setCrop(x: number, y: number, width: number, height: number): this {
// Sets the crop size of this Game Object.
return this.phaserSprite.setCrop(x, y, width, height);
this.phaserSprite.setCrop(x, y, width, height);
return this;
}
clearTint() {
clearTint(): this {
// Clears any previously set tint.
return this.phaserSprite.clearTint();
this.phaserSprite.clearTint();
return this;
}
disableInteractive() {
disableInteractive(): this {
// Disables Interactive features of this Game Object.
return null;
return this;
}
apply() {
return this.phaserSprite.apply();
this.phaserSprite.apply();
return this;
}
play() {
play(): this {
// return this.phaserSprite.play();
return this;
}
setPipelineData(key, value) {
setPipelineData(key: string, value: any): this {
this.pipelineData[key] = value;
return this;
}
destroy() {
return this.phaserSprite.destroy();
}
setName(name) {
return this.phaserSprite.setName(name);
setName(name: string): this {
this.phaserSprite.setName(name);
return this;
}
setAngle(angle) {
return this.phaserSprite.setAngle(angle);
setAngle(angle): this {
this.phaserSprite.setAngle(angle);
return this;
}
setMask() {}
setMask(): this {
return this;
}
add(obj) {
add(obj: MockGameObject | MockGameObject[]): this {
// Adds a child to this Game Object.
this.list.push(obj);
if (Array.isArray(obj)) {
this.list.push(...obj);
} else {
this.list.push(obj);
}
return this;
}
removeAll() {
@ -182,16 +216,18 @@ export default class MockSprite implements MockGameObject {
this.list = [];
}
addAt(obj, index) {
addAt(obj, index): this {
// Adds a Game Object to this Container at the given index.
this.list.splice(index, 0, obj);
return this;
}
remove(obj) {
remove(obj): this {
const index = this.list.indexOf(obj);
if (index !== -1) {
this.list.splice(index, 1);
}
return this;
}
getIndex(obj) {
@ -206,4 +242,9 @@ export default class MockSprite implements MockGameObject {
getAll() {
return this.list;
}
copyPosition(obj): this {
this.phaserSprite.copyPosition(obj);
return this;
}
}

View File

@ -107,42 +107,51 @@ export default class MockText implements MockGameObject {
}
}
setScale(_scale) {
setScale(_scale): this {
// return this.phaserText.setScale(scale);
return this;
}
setShadow(_shadowXpos, _shadowYpos, _shadowColor) {
setShadow(_shadowXpos, _shadowYpos, _shadowColor): this {
// Sets the shadow settings for this Game Object.
// return this.phaserText.setShadow(shadowXpos, shadowYpos, shadowColor);
return this;
}
setLineSpacing(_lineSpacing) {
setLineSpacing(_lineSpacing): this {
// Sets the line spacing value of this Game Object.
// return this.phaserText.setLineSpacing(lineSpacing);
return this;
}
setOrigin(_x, _y) {
setOrigin(_x, _y): this {
// return this.phaserText.setOrigin(x, y);
return this;
}
once(_event, _callback, _source) {
once(_event, _callback, _source): this {
// return this.phaserText.once(event, callback, source);
return this;
}
off(_event, _callback, _obj) {}
removedFromScene() {}
addToDisplayList() {}
setStroke(_color, _thickness) {
// Sets the stroke color and thickness.
// return this.phaserText.setStroke(color, thickness);
addToDisplayList(): this {
return this;
}
removeFromDisplayList() {
setStroke(_color, _thickness): this {
// Sets the stroke color and thickness.
// return this.phaserText.setStroke(color, thickness);
return this;
}
removeFromDisplayList(): this {
// same as remove or destroy
// return this.phaserText.removeFromDisplayList();
return this;
}
addedToScene() {
@ -150,16 +159,18 @@ export default class MockText implements MockGameObject {
// return this.phaserText.addedToScene();
}
setVisible(_visible) {
// return this.phaserText.setVisible(visible);
setVisible(_visible): this {
return this;
}
setY(_y) {
setY(_y): this {
// return this.phaserText.setY(y);
return this;
}
setX(_x) {
setX(_x): this {
// return this.phaserText.setX(x);
return this;
}
/**
@ -169,37 +180,45 @@ export default class MockText implements MockGameObject {
* @param z The z position of this Game Object. Default 0.
* @param w The w position of this Game Object. Default 0.
*/
setPosition(_x?: number, _y?: number, _z?: number, _w?: number) {}
setPosition(_x?: number, _y?: number, _z?: number, _w?: number): this {
return this;
}
setText(text) {
setText(text): this {
// Sets the text this Game Object will display.
// return this.phaserText.setText\(text);
this.text = text;
return this;
}
setAngle(_angle) {
setAngle(_angle): this {
// Sets the angle of this Game Object.
// return this.phaserText.setAngle(angle);
return this;
}
setPositionRelative(_source, _x, _y) {
setPositionRelative(_source, _x, _y): this {
/// Sets the position of this Game Object to be a relative position from the source Game Object.
// return this.phaserText.setPositionRelative(source, x, y);
return this;
}
setShadowOffset(_offsetX, _offsetY) {
setShadowOffset(_offsetX, _offsetY): this {
// Sets the shadow offset values.
// return this.phaserText.setShadowOffset(offsetX, offsetY);
return this;
}
setWordWrapWidth(width) {
setWordWrapWidth(width): this {
// Sets the width (in pixels) to use for wrapping lines.
this.wordWrapWidth = width;
return this;
}
setFontSize(_fontSize) {
setFontSize(_fontSize): this {
// Sets the font size of this Game Object.
// return this.phaserText.setFontSize(fontSize);
return this;
}
getBounds() {
@ -209,25 +228,31 @@ export default class MockText implements MockGameObject {
};
}
setColor(color: string) {
setColor(color: string): this {
this.color = color;
return this;
}
setInteractive = () => null;
setInteractive(): this {
return this;
}
setShadowColor(_color) {
setShadowColor(_color): this {
// Sets the shadow color.
// return this.phaserText.setShadowColor(color);
return this;
}
setTint(_color) {
setTint(_color): this {
// Sets the tint of this Game Object.
// return this.phaserText.setTint(color);
return this;
}
setStrokeStyle(_thickness, _color) {
setStrokeStyle(_thickness, _color): this {
// Sets the stroke style for the graphics.
// return this.phaserText.setStrokeStyle(thickness, color);
return this;
}
destroy() {
@ -235,20 +260,24 @@ export default class MockText implements MockGameObject {
this.list = [];
}
setAlpha(_alpha) {
setAlpha(_alpha): this {
// return this.phaserText.setAlpha(alpha);
return this;
}
setName(name: string) {
setName(name: string): this {
this.name = name;
return this;
}
setAlign(_align) {
setAlign(_align): this {
// return this.phaserText.setAlign(align);
return this;
}
setMask() {
setMask(): this {
/// Sets the mask that this Game Object will use to render with.
return this;
}
getBottomLeft() {
@ -265,37 +294,43 @@ export default class MockText implements MockGameObject {
};
}
disableInteractive() {
disableInteractive(): this {
// Disables interaction with this Game Object.
return this;
}
clearTint() {
clearTint(): this {
// Clears tint on this Game Object.
return this;
}
add(obj) {
add(obj): this {
// Adds a child to this Game Object.
this.list.push(obj);
return this;
}
removeAll() {
removeAll(): this {
// Removes all Game Objects from this Container.
this.list = [];
return this;
}
addAt(obj, index) {
addAt(obj, index): this {
// Adds a Game Object to this Container at the given index.
this.list.splice(index, 0, obj);
return this;
}
remove(obj) {
remove(obj): this {
const index = this.list.indexOf(obj);
if (index !== -1) {
this.list.splice(index, 1);
}
return this;
}
getIndex(obj) {
getIndex(obj): number {
const index = this.list.indexOf(obj);
return index || -1;
}
@ -317,5 +352,6 @@ export default class MockText implements MockGameObject {
return this.runWordWrap(this.text).split("\n");
}
// biome-ignore lint/complexity/noBannedTypes: This matches the signature of the class this mocks
on(_event: string | symbol, _fn: Function, _context?: any) {}
}

View File

@ -70,10 +70,10 @@ export function initTestFile() {
* @param x The relative x position
* @param y The relative y position
*/
const setPositionRelative = function (guideObject: any, x: number, y: number) {
const setPositionRelative = function (guideObject: any, x: number, y: number): any {
const offsetX = guideObject.width * (-0.5 + (0.5 - guideObject.originX));
const offsetY = guideObject.height * (-0.5 + (0.5 - guideObject.originY));
this.setPosition(guideObject.x + offsetX + x, guideObject.y + offsetY + y);
return this.setPosition(guideObject.x + offsetX + x, guideObject.y + offsetY + y);
};
Phaser.GameObjects.Container.prototype.setPositionRelative = setPositionRelative;