Merge remote-tracking branch 'upstream/beta' into type-move
20
biome.jsonc
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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
@ -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();
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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),
|
||||
|
@ -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+
|
||||
|
2779
src/field/pokemon.ts
@ -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;
|
||||
|
@ -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()) {
|
||||
|
@ -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) =>
|
||||
|
12
src/typings/phaser/index.d.ts
vendored
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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)}`;
|
||||
|
@ -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
|
||||
}
|
688
src/ui/battle-info/battle-info.ts
Normal 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;
|
||||
}
|
||||
}
|
235
src/ui/battle-info/enemy-battle-info.ts
Normal 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;
|
||||
}
|
||||
}
|
242
src/ui/battle-info/player-battle-info.ts
Normal 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");
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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));
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
export interface MockGameObject {
|
||||
name: string;
|
||||
destroy?(): void;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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) {}
|
||||
}
|
||||
|
@ -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;
|
||||
|