diff --git a/public/images/pokemon/332.png b/public/images/pokemon/332.png index a9979480673..44e374426b4 100644 Binary files a/public/images/pokemon/332.png and b/public/images/pokemon/332.png differ diff --git a/public/images/pokemon/646.png b/public/images/pokemon/646.png index 64b0af2d151..e54083bfc73 100644 Binary files a/public/images/pokemon/646.png and b/public/images/pokemon/646.png differ diff --git a/public/images/pokemon/back/332.png b/public/images/pokemon/back/332.png index a432b1af4b2..b251a35e468 100644 Binary files a/public/images/pokemon/back/332.png and b/public/images/pokemon/back/332.png differ diff --git a/public/images/pokemon/back/female/332.png b/public/images/pokemon/back/female/332.png index a432b1af4b2..b251a35e468 100644 Binary files a/public/images/pokemon/back/female/332.png and b/public/images/pokemon/back/female/332.png differ diff --git a/public/images/pokemon/female/332.png b/public/images/pokemon/female/332.png index 2100e4b9a10..c1f7c8ec3f0 100644 Binary files a/public/images/pokemon/female/332.png and b/public/images/pokemon/female/332.png differ diff --git a/public/images/pokemon/variant/332.json b/public/images/pokemon/variant/332.json index e9b487bca25..336ef4a22f3 100644 --- a/public/images/pokemon/variant/332.json +++ b/public/images/pokemon/variant/332.json @@ -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" } } \ No newline at end of file diff --git a/public/images/pokemon/variant/back/332.json b/public/images/pokemon/variant/back/332.json index c13c07c34b4..fbfb3705202 100644 --- a/public/images/pokemon/variant/back/332.json +++ b/public/images/pokemon/variant/back/332.json @@ -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" } } \ No newline at end of file diff --git a/public/images/pokemon/variant/back/female/332.json b/public/images/pokemon/variant/back/female/332.json index 9ec50cb7e92..17f8d3c5f74 100644 --- a/public/images/pokemon/variant/back/female/332.json +++ b/public/images/pokemon/variant/back/female/332.json @@ -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" } } \ No newline at end of file diff --git a/public/images/pokemon/variant/female/332.json b/public/images/pokemon/variant/female/332.json index c86429d13c4..7a1dc0f1457 100644 --- a/public/images/pokemon/variant/female/332.json +++ b/public/images/pokemon/variant/female/332.json @@ -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" } } \ No newline at end of file diff --git a/public/locales b/public/locales index 42cd5cf577f..e9ccbadb6ea 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit 42cd5cf577f475c22bc82d55e7ca358eb4f3184f +Subproject commit e9ccbadb6eaa3b797f3dec919745befda2ec74bd diff --git a/scripts/decrypt-save.js b/scripts/decrypt-save.js new file mode 100644 index 00000000000..a7239a40df6 --- /dev/null +++ b/scripts/decrypt-save.js @@ -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 [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(); diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 2ce6dbb254b..62cfd7e2290 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -7551,7 +7551,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)); } } diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 59167ba47f6..5c97f360094 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -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), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index f6376dea23a..27e1c205ec3 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -5,7 +5,9 @@ import { globalScene } from "#app/global-scene"; import type { Variant } from "#app/sprites/variant"; import { populateVariantColors, variantColorCache } from "#app/sprites/variant"; import { variantData } from "#app/sprites/variant"; -import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info"; +import BattleInfo from "#app/ui/battle-info/battle-info"; +import { EnemyBattleInfo } from "#app/ui/battle-info/enemy-battle-info"; +import { PlayerBattleInfo } from "#app/ui/battle-info/player-battle-info"; import type Move from "#app/data/moves/move"; import { HighCritAttr, @@ -3389,22 +3391,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.battleInfo.updateInfo(this, instant); } - /** - * Show or hide the type effectiveness multiplier window - * Passing undefined will hide the window - */ - updateEffectiveness(effectiveness?: string) { - this.battleInfo.updateEffectiveness(effectiveness); - } - toggleStats(visible: boolean): void { this.battleInfo.toggleStats(visible); } - toggleFlyout(visible: boolean): void { - this.battleInfo.toggleFlyout(visible); - } - /** * Adds experience to this PlayerPokemon, subject to wave based level caps. * @param exp The amount of experience to add @@ -5569,6 +5559,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } export class PlayerPokemon extends Pokemon { + protected battleInfo: PlayerBattleInfo; public compatibleTms: Moves[]; constructor( @@ -6089,6 +6080,7 @@ export class PlayerPokemon extends Pokemon { } export class EnemyPokemon extends Pokemon { + protected battleInfo: EnemyBattleInfo; public trainerSlot: TrainerSlot; public aiType: AiType; public bossSegments: number; @@ -6761,6 +6753,19 @@ export class EnemyPokemon extends Pokemon { return ret; } + + + /** + * Show or hide the type effectiveness multiplier window + * Passing undefined will hide the window + */ + updateEffectiveness(effectiveness?: string) { + this.battleInfo.updateEffectiveness(effectiveness); + } + + toggleFlyout(visible: boolean): void { + this.battleInfo.toggleFlyout(visible); + } } /** diff --git a/src/main.ts b/src/main.ts index 7db663d14c7..38bfcbe5636 100644 --- a/src/main.ts +++ b/src/main.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; diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index c61eb73118f..6bdbb66be14 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -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) => diff --git a/src/typings/phaser/index.d.ts b/src/typings/phaser/index.d.ts index f3665768cec..26fbcff75bd 100644 --- a/src/typings/phaser/index.d.ts +++ b/src/typings/phaser/index.d.ts @@ -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; } } diff --git a/src/ui-inputs.ts b/src/ui-inputs.ts index 0c13cdb9512..e4f11e1c93c 100644 --- a/src/ui-inputs.ts +++ b/src/ui-inputs.ts @@ -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); } } diff --git a/src/ui/battle-flyout.ts b/src/ui/battle-flyout.ts index e590bebcf5a..f8ef5fc1ec4 100644 --- a/src/ui/battle-flyout.ts +++ b/src/ui/battle-flyout.ts @@ -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)}`; diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts deleted file mode 100644 index 2822e8364ec..00000000000 --- a/src/ui/battle-info.ts +++ /dev/null @@ -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(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 { - 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 { - 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 -} diff --git a/src/ui/battle-info/battle-info.ts b/src/ui/battle-info/battle-info.ts new file mode 100644 index 00000000000..71596bf0f43 --- /dev/null +++ b/src/ui/battle-info/battle-info.ts @@ -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, 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 { + let resolve: (r: void | PromiseLike) => void = () => {}; + const promise = new Promise(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; + } +} diff --git a/src/ui/battle-info/enemy-battle-info.ts b/src/ui/battle-info/enemy-battle-info.ts new file mode 100644 index 00000000000..e8f5434dcf2 --- /dev/null +++ b/src/ui/battle-info/enemy-battle-info.ts @@ -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(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, + instant?: boolean, + ): void { + super.updatePokemonHp(pokemon, resolve, instant); + this.lastHp = pokemon.hp; + } +} diff --git a/src/ui/battle-info/player-battle-info.ts b/src/ui/battle-info/player-battle-info.ts new file mode 100644 index 00000000000..634f89b7922 --- /dev/null +++ b/src/ui/battle-info/player-battle-info.ts @@ -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 { + 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 { + 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"); + } +} diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index ae38d7c7249..b05f6e7ab9d 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -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(); }); } diff --git a/src/ui/pokemon-info-container.ts b/src/ui/pokemon-info-container.ts index 18b5d2384ef..d8012a58875 100644 --- a/src/ui/pokemon-info-container.ts +++ b/src/ui/pokemon-info-container.ts @@ -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)); diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 80acac6a6b4..a971ba86479 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -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, }, }; diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index f93a1826b3e..24e790f7c61 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -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); diff --git a/src/utils/common.ts b/src/utils/common.ts index b9111578e2f..a018b49da3c 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -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"); + } +} diff --git a/test/moves/gastro_acid.test.ts b/test/moves/gastro_acid.test.ts index 37a79f83fa3..333619d16db 100644 --- a/test/moves/gastro_acid.test.ts +++ b/test/moves/gastro_acid.test.ts @@ -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(); + }); }); diff --git a/test/testUtils/mocks/mockGameObject.ts b/test/testUtils/mocks/mockGameObject.ts index 4c243ec9ca1..0dff7c48c73 100644 --- a/test/testUtils/mocks/mockGameObject.ts +++ b/test/testUtils/mocks/mockGameObject.ts @@ -1,3 +1,4 @@ export interface MockGameObject { name: string; + destroy?(): void; } diff --git a/test/testUtils/mocks/mocksContainer/mockContainer.ts b/test/testUtils/mocks/mocksContainer/mockContainer.ts index 5e739fbe3cc..883c3856c26 100644 --- a/test/testUtils/mocks/mocksContainer/mockContainer.ts +++ b/test/testUtils/mocks/mocksContainer/mockContainer.ts @@ -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; } } diff --git a/test/testUtils/mocks/mocksContainer/mockGraphics.ts b/test/testUtils/mocks/mocksContainer/mockGraphics.ts index ebf84e935e3..1650d2f9f60 100644 --- a/test/testUtils/mocks/mocksContainer/mockGraphics.ts +++ b/test/testUtils/mocks/mocksContainer/mockGraphics.ts @@ -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; + } } diff --git a/test/testUtils/mocks/mocksContainer/mockRectangle.ts b/test/testUtils/mocks/mocksContainer/mockRectangle.ts index 7bdf343759d..c11af824c56 100644 --- a/test/testUtils/mocks/mocksContainer/mockRectangle.ts +++ b/test/testUtils/mocks/mocksContainer/mockRectangle.ts @@ -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; + } } diff --git a/test/testUtils/mocks/mocksContainer/mockSprite.ts b/test/testUtils/mocks/mocksContainer/mockSprite.ts index dcc3588f127..ad70b6ced07 100644 --- a/test/testUtils/mocks/mocksContainer/mockSprite.ts +++ b/test/testUtils/mocks/mocksContainer/mockSprite.ts @@ -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; + } } diff --git a/test/testUtils/mocks/mocksContainer/mockText.ts b/test/testUtils/mocks/mocksContainer/mockText.ts index 1f3f0ad792f..8f72cd0d34f 100644 --- a/test/testUtils/mocks/mocksContainer/mockText.ts +++ b/test/testUtils/mocks/mocksContainer/mockText.ts @@ -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) {} } diff --git a/test/testUtils/testFileInitialization.ts b/test/testUtils/testFileInitialization.ts index 15635289e6f..ba90cba7d5b 100644 --- a/test/testUtils/testFileInitialization.ts +++ b/test/testUtils/testFileInitialization.ts @@ -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;