From 6b98afa34fa9ac15da94b73d47b69de9bd704197 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Mon, 18 Aug 2025 21:00:03 -0400 Subject: [PATCH] Fixed main repo code to not expect snake cased locale strings --- .../global-trade-system-encounter.ts | 26 ++++--- src/field/trainer.ts | 70 +++++++++---------- src/utils/i18n.ts | 17 +++++ 3 files changed, 66 insertions(+), 47 deletions(-) create mode 100644 src/utils/i18n.ts diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index ed49fccf190..28cbf190e67 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -44,7 +44,10 @@ import { PokemonData } from "#system/pokemon-data"; import { MusicPreference } from "#system/settings"; import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import { isNullOrUndefined, NumberHolder, randInt, randSeedInt, randSeedItem, randSeedShuffle } from "#utils/common"; +import { getEnumKeys } from "#utils/enums"; +import { getRandomLocaleKey } from "#utils/i18n"; import { getPokemonSpecies } from "#utils/pokemon-utils"; +import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; /** the i18n namespace for the encounter */ @@ -984,14 +987,17 @@ function doTradeReceivedSequence( } function generateRandomTraderName() { - const length = TrainerType.YOUNGSTER - TrainerType.ACE_TRAINER + 1; - // +1 avoids TrainerType.UNKNOWN - const classKey = `trainersCommon:${TrainerType[randInt(length) + 1]}`; - // Some trainers have 2 gendered pools, some do not - const genderKey = i18next.exists(`${classKey}.MALE`) ? (randInt(2) === 0 ? ".MALE" : ".FEMALE") : ""; - const trainerNameKey = randSeedItem(Object.keys(i18next.t(`${classKey}${genderKey}`, { returnObjects: true }))); - const trainerNameString = i18next.t(`${classKey}${genderKey}.${trainerNameKey}`); - // Some names have an '&' symbol and need to be trimmed to a single name instead of a double name - const trainerNames = trainerNameString.split(" & "); - return trainerNames[randInt(trainerNames.length)]; + const allTrainerNames = getEnumKeys(TrainerType); + // Exclude TrainerType.UNKNOWN and everything after Ace Trainers (grunts and unique trainers) + const eligibleNames = allTrainerNames.slice( + 1, + allTrainerNames.indexOf(TrainerType[TrainerType.YOUNGSTER] as keyof typeof TrainerType), + ); + const randomTrainer = toCamelCase(randSeedItem(eligibleNames)); + const classKey = `trainersCommon:${randomTrainer}`; + // Pick a random gender for ones with gendered pools, or access the raw object for ones without. + const genderKey = i18next.exists(`${classKey}.male`) ? randSeedItem([".male", ".female"]) : ""; + const trainerNameString = getRandomLocaleKey(`${classKey}${genderKey}`)[1]; + // Split the string by &s (for duo trainers) + return randSeedItem(trainerNameString.split(" & ")); } diff --git a/src/field/trainer.ts b/src/field/trainer.ts index 584c9310932..71660be524f 100644 --- a/src/field/trainer.ts +++ b/src/field/trainer.ts @@ -16,14 +16,11 @@ import type { PersistentModifier } from "#modifiers/modifier"; import { getIsInitialized, initI18n } from "#plugins/i18n"; import type { TrainerConfig } from "#trainers/trainer-config"; import { trainerConfigs } from "#trainers/trainer-config"; -import { - TrainerPartyCompoundTemplate, - type TrainerPartyTemplate, - trainerPartyTemplates, -} from "#trainers/trainer-party-template"; +import { TrainerPartyCompoundTemplate, type TrainerPartyTemplate } from "#trainers/trainer-party-template"; import { randSeedInt, randSeedItem, randSeedWeightedItem } from "#utils/common"; +import { getRandomLocaleKey } from "#utils/i18n"; import { getPokemonSpecies } from "#utils/pokemon-utils"; -import { toSnakeCase } from "#utils/strings"; +import { toCamelCase, toSnakeCase } from "#utils/strings"; import i18next from "i18next"; export class Trainer extends Phaser.GameObjects.Container { @@ -35,6 +32,18 @@ export class Trainer extends Phaser.GameObjects.Container { public partnerNameKey: string | undefined; public originalIndexes: { [key: number]: number } = {}; + /** + * Create a new Trainer. + * @param trainerType - The {@linkcode TrainerType} for this trainer, used to determine + * name, sprite, party contents and other details. + * @param variant - The {@linkcode TrainerVariant} for this trainer (if any are available) + * @param partyTemplateIndex - If provided, will override the trainer's party template with the given + * version. + * @param nameKey - If provided, will override the name key of the trainer + * @param partnerNameKey - If provided, will override the + * @param trainerConfigOverride - If provided, will override the trainer config for the given trainer type + * @todo Review how many of these parameters we actually need + */ constructor( trainerType: TrainerType, variant: TrainerVariant, @@ -44,13 +53,11 @@ export class Trainer extends Phaser.GameObjects.Container { trainerConfigOverride?: TrainerConfig, ) { super(globalScene, -72, 80); - this.config = trainerConfigs.hasOwnProperty(trainerType) - ? trainerConfigs[trainerType] - : trainerConfigs[TrainerType.ACE_TRAINER]; - - if (trainerConfigOverride) { - this.config = trainerConfigOverride; - } + this.config = + trainerConfigOverride ?? + (trainerConfigs.hasOwnProperty(trainerType) + ? trainerConfigs[trainerType] + : trainerConfigs[TrainerType.ACE_TRAINER]); this.variant = variant; this.partyTemplateIndex = Math.min( @@ -59,20 +66,21 @@ export class Trainer extends Phaser.GameObjects.Container { : randSeedWeightedItem(this.config.partyTemplates.map((_, i) => i)), this.config.partyTemplates.length - 1, ); - const classKey = `trainersCommon:${TrainerType[trainerType]}`; + // TODO: Rework this and add actual error handling for missing names + const classKey = `trainersCommon:${toCamelCase(TrainerType[trainerType])}`; if (i18next.exists(classKey, { returnObjects: true })) { if (nameKey) { this.nameKey = nameKey; + this.name = i18next.t(nameKey); } else { - const genderKey = i18next.exists(`${classKey}.MALE`) + const genderKey = i18next.exists(`${classKey}.male`) ? variant === TrainerVariant.FEMALE - ? ".FEMALE" - : ".MALE" + ? ".female" + : ".male" : ""; - const trainerKey = randSeedItem(Object.keys(i18next.t(`${classKey}${genderKey}`, { returnObjects: true }))); - this.nameKey = `${classKey}${genderKey}.${trainerKey}`; + [this.nameKey, this.name] = getRandomLocaleKey(`${classKey}${genderKey}`); } - this.name = i18next.t(this.nameKey); + if (variant === TrainerVariant.DOUBLE) { if (this.config.doubleOnly) { if (partnerNameKey) { @@ -82,16 +90,8 @@ export class Trainer extends Phaser.GameObjects.Container { [this.name, this.partnerName] = this.name.split(" & "); } } else { - const partnerGenderKey = i18next.exists(`${classKey}.FEMALE`) ? ".FEMALE" : ""; - const partnerTrainerKey = randSeedItem( - Object.keys( - i18next.t(`${classKey}${partnerGenderKey}`, { - returnObjects: true, - }), - ), - ); - this.partnerNameKey = `${classKey}${partnerGenderKey}.${partnerTrainerKey}`; - this.partnerName = i18next.t(this.partnerNameKey); + const partnerGenderKey = i18next.exists(`${classKey}.fenale`) ? ".fenale" : ""; + [this.partnerNameKey, this.partnerName] = getRandomLocaleKey(`${classKey}${partnerGenderKey}`); } } } @@ -109,10 +109,6 @@ export class Trainer extends Phaser.GameObjects.Container { break; } - console.log( - Object.keys(trainerPartyTemplates)[Object.values(trainerPartyTemplates).indexOf(this.getPartyTemplate())], - ); - const getSprite = (hasShadow?: boolean, forceFemale?: boolean) => { const ret = globalScene.addFieldSprite( 0, @@ -157,9 +153,9 @@ export class Trainer extends Phaser.GameObjects.Container { /** * Returns the name of the trainer based on the provided trainer slot and the option to include a title. - * @param {TrainerSlot} trainerSlot - The slot to determine which name to use. Defaults to TrainerSlot.NONE. - * @param {boolean} includeTitle - Whether to include the title in the returned name. Defaults to false. - * @returns {string} - The formatted name of the trainer. + * @param rainerSlot - The slot to determine which name to use; default `TrainerSlot.NONE` + * @param includeTitle - Whether to include the title in the returned name; default `false` + * @returns - The formatted name of the trainer */ getName(trainerSlot: TrainerSlot = TrainerSlot.NONE, includeTitle = false): string { // Get the base title based on the trainer slot and variant. diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts new file mode 100644 index 00000000000..cd5f8d1ee4f --- /dev/null +++ b/src/utils/i18n.ts @@ -0,0 +1,17 @@ +import { randSeedItem } from "#utils/common"; +import i18next from "i18next"; + +/** + * Select a random i18n key from all nested keys in the given object. + * @param key - The i18n key to retrieve a random value of. + * The key's value should be an object containing numerical keys (starting from 1). + * @returns A typle containing the key and value pair. + * @privateRemarks + * The reason such "array-like" keys are not stored as actual arrays is due to the + * translation software used by the Translation Team (Mozilla Pontoon) + * not supporting arrays in any capacity. + */ +export function getRandomLocaleKey(key: string): [key: string, value: string] { + const keyName = `${key}.${randSeedItem(Object.keys(i18next.t("key", { returnObjects: true })))}`; + return [keyName, i18next.t(keyName)]; +}