From cf9be74548bf0a98d682815d3389aba814b707ef Mon Sep 17 00:00:00 2001 From: Lukas Obermann Date: Wed, 25 Feb 2026 13:31:36 +0100 Subject: [PATCH] feat: profession description generator --- .prettierrc.yml | 1 + scripts/testOutput.ts | 2 + src/entities/influence.ts | 70 +- src/entities/partial/activatableNameChunks.ts | 848 ++++++----- .../partial/prerequisites/displayOption.ts | 8 +- src/entities/partial/prerequisites/index.ts | 148 +- src/entities/partial/prerequisites/part.ts | 221 ++- .../prerequisites/prerequisiteGroups.ts | 133 +- .../prerequisites/single/activatable.ts | 56 +- .../prerequisites/single/animistPower.ts | 6 +- .../prerequisites/single/blessedTradition.ts | 2 +- .../partial/prerequisites/single/culture.ts | 4 +- .../partial/prerequisites/single/influence.ts | 12 +- .../prerequisites/single/magicalTradition.ts | 2 +- .../partial/prerequisites/single/pact.ts | 10 +- .../prerequisites/single/personalityTrait.ts | 18 +- .../prerequisites/single/primaryAttribute.ts | 2 +- .../prerequisites/single/publication.ts | 2 +- .../partial/prerequisites/single/race.ts | 4 +- .../partial/prerequisites/single/rated.ts | 56 +- .../single/ratedMinimumNumber.ts | 57 +- .../partial/prerequisites/single/ratedSum.ts | 19 +- .../partial/prerequisites/single/sex.ts | 12 +- .../prerequisites/single/socialStatus.ts | 6 +- .../partial/prerequisites/single/state.ts | 2 +- src/entities/partial/professions.ts | 3 +- .../partial/rated/activatable/castingTime.ts | 4 +- .../partial/rated/activatable/cost.ts | 4 +- .../partial/rated/activatable/range.ts | 4 +- .../rated/activatable/targetCategory.ts | 4 +- src/entities/partial/rated/skillCheck.ts | 4 +- src/entities/partial/reader.ts | 217 ++- src/entities/profession.ts | 1321 +++++++++++++++++ src/entities/spell.ts | 847 ++++------- src/helpers/locale.ts | 3 +- src/helpers/translate.ts | 5 + src/index.ts | 3 +- test/entities/partial/prerequisites/part.ts | 36 +- test/helpers/locale.ts | 3 +- test/helpers/translate.ts | 12 +- 40 files changed, 2751 insertions(+), 1420 deletions(-) create mode 100644 src/entities/profession.ts diff --git a/.prettierrc.yml b/.prettierrc.yml index 2f45000..f7875fd 100644 --- a/.prettierrc.yml +++ b/.prettierrc.yml @@ -2,3 +2,4 @@ arrowParens: avoid semi: false tabWidth: 2 trailingComma: all +printWidth: 100 diff --git a/scripts/testOutput.ts b/scripts/testOutput.ts index 09f33cc..80fa840 100644 --- a/scripts/testOutput.ts +++ b/scripts/testOutput.ts @@ -73,6 +73,8 @@ const collator = new Intl.Collator(localeId, { usage: "sort" }) const localeEnv: LocaleEnvironment = { id: localeId, + format: (text, args) => + new MessageFormat(localeId, text, { bidiIsolation: "none" }).format(args), compare: collator.compare.bind(collator), translate: (key, ...rest) => new MessageFormat(localeId, localeInstance.translations?.[key] ?? key, { diff --git a/src/entities/influence.ts b/src/entities/influence.ts index 88ea232..e53f353 100644 --- a/src/entities/influence.ts +++ b/src/entities/influence.ts @@ -1,44 +1,44 @@ import { createEntityDescriptionCreator } from "../creator.js" +import type { GetInstanceById } from "../helpers/getTypes.js" import { printInfluencePrerequisites } from "./partial/prerequisites/index.js" /** * Get a JSON representation of the rules text for an influence. */ -export const getInfluenceEntityDescription = - createEntityDescriptionCreator<"Influence">( - (_, locale, { content: entry }) => { - const { translate, translateMap } = locale - const translation = translateMap(entry.translations) +export const getInfluenceEntityDescription = createEntityDescriptionCreator< + "Influence", + { + getInstanceById: GetInstanceById<"Publication" | "Influence"> + } +>(({ getInstanceById }, locale, { content: entry }) => { + const { translate, translateMap } = locale + const translation = translateMap(entry.translations) - if (translation === undefined) { - return undefined - } + if (translation === undefined) { + return undefined + } - return { - title: translation.name, - className: "influence", - body: [ - { - type: "definitionList", - items: [ - ...(translation.effects?.map(effect => ({ - label: effect.label, - value: effect.text, - })) ?? []), - entry.prerequisites === undefined - ? undefined - : { - label: translate("Prerequisites"), - value: printInfluencePrerequisites( - locale, - entry.prerequisites, - ), - }, - ], - }, + return { + title: translation.name, + className: "influence", + body: [ + { + type: "definitionList", + items: [ + ...(translation.effects?.map(effect => ({ + label: effect.label, + value: effect.text, + })) ?? []), + entry.prerequisites === undefined + ? undefined + : { + label: translate("Prerequisites"), + value: printInfluencePrerequisites(getInstanceById, locale, entry.prerequisites), + }, ], - errata: translation.errata, - references: entry.src, - } - }, - ) + }, + ], + errata: translation.errata, + references: entry.src, + } +}) diff --git a/src/entities/partial/activatableNameChunks.ts b/src/entities/partial/activatableNameChunks.ts index c2ee145..2eaea0f 100644 --- a/src/entities/partial/activatableNameChunks.ts +++ b/src/entities/partial/activatableNameChunks.ts @@ -1,34 +1,50 @@ +import { allSame } from "@elyukai/utils/array/filters" +import { ensureNonEmpty, isNotEmpty } from "@elyukai/utils/array/nonEmpty" +import { deepEqual } from "@elyukai/utils/equality" +import { identity, on } from "@elyukai/utils/function" +import { isNotNullish } from "@elyukai/utils/nullable" import { mapObject } from "@optolith/helpers/object" import { romanize } from "@optolith/helpers/roman" import { assertExhaustive } from "@optolith/helpers/typeSafety" -import type { ResolvedSelectOption } from "optolith-database-schema/cache" import type { ActivatableIdentifier, + ActivatableNameBuilderRules, RequirableSelectOptionIdentifier, } from "optolith-database-schema/gen" -import { type GetInstanceById } from "../../helpers/getTypes.js" -import { - AdvantageIdentifier, - DisadvantageIdentifier, - KarmaSpecialAbilityIdentifier, -} from "../../helpers/identifiers.js" -import { LocaleEnvironment } from "../../helpers/locale.js" -import type { LocaleMap } from "../../helpers/translate.js" +import type { + LocaleMap, + Translate, + TranslateMap, +} from "../../helpers/translate.js" +import type { GetResolvedSelectOptionById } from "./prerequisites/single/activatable.js" import { MISSING_VALUE } from "./unknown.js" /** * Returns the full name of the activatable entry as well as its components. */ export type ActivatableNameComponents = { - full: ActivatableNameChunk - fullWithoutLevel: + id: ActivatableIdentifier + base: ActivatableNameChunk + options: | ActivatableNameChunk | [ActivatableNameChunk, ActivatableNameChunk] + | undefined + level: number | undefined + nameBuilderRules: Required +} + +/** + * Returns the full name of the activatable entry as well as its components. + */ +export type CombinedActivatableNameComponents = { + id: ActivatableIdentifier base: ActivatableNameChunk options: ( | ActivatableNameChunk | [ActivatableNameChunk, ActivatableNameChunk] )[] + level: number | undefined + nameBuilderRules: Required } /** @@ -38,7 +54,7 @@ export type ActivatableNameComponents = { export type ActivatableNameChunk = | LocaleMap | string - | ((locale: LocaleEnvironment) => string) + | ((translateMap: TranslateMap) => string) const combineChunks = ( a: ActivatableNameChunk, @@ -60,14 +76,16 @@ const combineChunks = ( } else if (typeof b === "function") { return fs => join(a(fs), b(fs)) } else if (typeof b === "object") { - return fs => join(a(fs), fs.translateMap(b) ?? MISSING_VALUE) + return translateMap => + join(a(translateMap), translateMap(b) ?? MISSING_VALUE) } return b } else if (typeof a === "object") { if (typeof b === "string") { return mapObject(a, aValue => join(aValue, b)) } else if (typeof b === "function") { - return fs => join(fs.translateMap(a) ?? MISSING_VALUE, b(fs)) + return translateMap => + join(translateMap(a) ?? MISSING_VALUE, b(translateMap)) } else if (typeof b === "object") { const ret: LocaleMap = {} for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) { @@ -80,431 +98,459 @@ const combineChunks = ( return a } -const mapChunk = ( - chunk: ActivatableNameChunk, - map: (str: string) => string, -): ActivatableNameChunk => { - if (typeof chunk === "string") { - return map(chunk) - } else if (typeof chunk === "function") { - return fs => map(chunk(fs)) - } else if (typeof chunk === "object") { - return mapObject(chunk, chunkValue => map(chunkValue)) - } - return chunk -} +const combinePair = (pair: [ActivatableNameChunk, ActivatableNameChunk]) => + combineChunks(pair[0], pair[1], (a, b) => `${a}: ${b}`) +const normalizeChunks = ( + chunk: ActivatableNameChunk | [ActivatableNameChunk, ActivatableNameChunk], +): ActivatableNameChunk => (Array.isArray(chunk) ? combinePair(chunk) : chunk) + +/** + * Zips multiple name chunks together, separating them with commas. Pairs of chunks are combined with a colon. + */ const zipChunks = ( chunks: ( | ActivatableNameChunk | [ActivatableNameChunk, ActivatableNameChunk] )[], -): ActivatableNameChunk => { - const withNormalizedPairs = chunks.map(chunk => { - if (Array.isArray(chunk)) { - return combineChunks(chunk[0], chunk[1], (a, b) => `${a}: ${b}`) - } +): ActivatableNameChunk => + chunks + .map(normalizeChunks) + .reduce( + (acc, chunk) => + acc === "" ? chunk : combineChunks(acc, chunk, (a, b) => `${a}, ${b}`), + "", + ) +const renderLevel = ( + formatAsPrerequisite: boolean, + id: ActivatableIdentifier, + level: number | undefined, +) => + level === undefined + ? undefined + : formatAsPrerequisite || + id.kind === "Advantage" || + id.kind === "Disadvantage" || + level === 1 + ? romanize(level) + : `I–${romanize(level)}` + +/** + * Converts a name chunk to a displayable string. + */ +const renderActivatableNameChunk = ( + translateMap: TranslateMap, + chunk: ActivatableNameChunk, +): string => { + if (typeof chunk === "string") { return chunk - }) - - return withNormalizedPairs.reduce( - (acc, chunk) => - acc === "" ? chunk : combineChunks(acc, chunk, (a, b) => `${a}, ${b}`), - "", - ) + } else if (typeof chunk === "function") { + return chunk(translateMap) + } else if (typeof chunk === "object") { + return translateMap(chunk) ?? MISSING_VALUE + } + return chunk } -const combineBaseName = ( - base: ActivatableNameChunk, - level: number | undefined, - options: ActivatableNameComponents["options"], - config: { - levelPlacement?: "before" | "after" - useParenthesis?: boolean - } = {}, -): Pick => { - const { levelPlacement = "after", useParenthesis = true } = config - const appendLevel: (str: string) => string = - level === undefined ? x => x : x => `${x} ${romanize(level)}` +/** + * This can be used to render an activatable entry with a selectable level, as it splits the text where the level should be inserted. + */ +export const renderActivatableNameComponentsWithoutLevel = ( + translateMap: TranslateMap, + components: ActivatableNameComponents, +): [beforeLevel: string, afterLevel?: string] => { + const { levelPlacement, useParenthesis } = components.nameBuilderRules - const wrapParens = (str: string) => `(${str})` + const wrapParens: (text: string) => string = useParenthesis + ? str => `(${str})` + : identity - const appendOptions = useParenthesis - ? (baseStr: string, optionsStr: string) => - optionsStr === "" ? baseStr : `${baseStr} ${wrapParens(optionsStr)}` - : (baseStr: string, optionsStr: string) => - optionsStr === "" ? baseStr : `${baseStr} ${optionsStr}` + const base = renderActivatableNameChunk(translateMap, components.base) + const options = + components.options === undefined + ? undefined + : wrapParens( + renderActivatableNameChunk( + translateMap, + normalizeChunks(components.options), + ), + ) - const full = combineChunks( - base, - zipChunks(options), - (() => { - switch (levelPlacement) { - case "before": - return (a, b) => appendOptions(appendLevel(a), b) - case "after": - return (a, b) => appendLevel(appendOptions(a, b)) - default: - return assertExhaustive(levelPlacement) - } - })(), - ) - - switch (levelPlacement) { - case "before": - return { - full, - fullWithoutLevel: [base, mapChunk(zipChunks(options), wrapParens)], - } - case "after": - return { - full, - fullWithoutLevel: combineChunks( - base, - zipChunks(options), - appendOptions, - ), - } + switch (levelPlacement.kind) { + case "BeforeOptions": + return [base, options] + case "AfterOptions": + return [[base, options].filter(isNotNullish).join(" ")] default: return assertExhaustive(levelPlacement) } } -const getEntrySpecificFullName = ( - getInstanceById: GetInstanceById<"Aspect">, - locale: LocaleEnvironment, - id: ActivatableIdentifier, - base: ActivatableNameChunk, - level: number | undefined, - options: RequirableSelectOptionIdentifier[] | undefined, - printedOptions: ActivatableNameComponents["options"], -): Pick | undefined => { - switch (id.kind) { - case "Advantage": - switch (id.Advantage) { - case AdvantageIdentifier.HatredOf: { - const [firstOption, ...rest] = printedOptions +/** + * Renders the name components of an activatable entry. + */ +export const renderActivatableNameComponents = ( + translateMap: TranslateMap, + components: ActivatableNameComponents, + formatAsPrerequisite: boolean, +): string => { + const levelText = renderLevel( + formatAsPrerequisite, + components.id, + components.level, + ) - if (firstOption === undefined || Array.isArray(firstOption)) { - return undefined - } + const [beforeLevel, afterLevel] = renderActivatableNameComponentsWithoutLevel( + translateMap, + components, + ) - return combineBaseName( - combineChunks(base, firstOption, (a, b) => `${a} ${b}`), - level, - rest, - ) - } - case AdvantageIdentifier.ImmunityToPoison: - case AdvantageIdentifier.ImmunityToDisease: - return combineBaseName(base, level, printedOptions, { - useParenthesis: false, - }) - default: - return undefined - } - case "Disadvantage": - switch (id.Disadvantage) { - case DisadvantageIdentifier.PersonalityFlaw: { - const [selection, optionalText, ...rest] = printedOptions + return [beforeLevel, levelText, afterLevel].filter(isNotNullish).join(" ") +} - if ( - selection === undefined || - Array.isArray(selection) || - Array.isArray(optionalText) - ) { - return undefined - } - - return combineBaseName(base, level, [ - optionalText === undefined ? selection : [selection, optionalText], - ...rest, - ]) - } - case DisadvantageIdentifier.AfraidOf: - return combineBaseName(base, level, printedOptions, { - useParenthesis: false, - }) - case DisadvantageIdentifier.Principles: - case DisadvantageIdentifier.Obligations: - return combineBaseName(base, level, printedOptions, { - levelPlacement: "before", - }) - default: - return undefined - } - case "AdvancedCombatSpecialAbility": - case "AdvancedKarmaSpecialAbility": - case "AdvancedMagicalSpecialAbility": - return undefined - case "AdvancedSkillSpecialAbility": - switch (id.AdvancedSkillSpecialAbility) { - // case AdvancedSkillSpecialAbilityIdentifier.Fachwissen: { - // const [skillId, firstApplicationId, secondApplicationId] = options ?? [] - // const aspect = - // aspectId?.tag === "Aspect" - // ? getAspectById(aspectId.aspect) - // : undefined - // const aspectTranslations = locale.translateMap(aspect?.translations) - - // if (aspectTranslations === undefined) { - // return undefined - // } - - // return combineBaseName( - // combineChunks( - // base, - // aspectTranslations.master_of_aspect_suffix ?? - // aspectTranslations.name, - // (a, b) => `${a} ${b}`, - // ), - // level, - // [], - // ) - // const getApp = ( - // getSid: (r: Record) => Maybe, - // ) => - // pipe( - // SA.applications, - // filter(pipe(AA.prerequisite, isNothing)), - // find(pipe(AA.id, elemF(getSid(hero_entry)))), - // fmap(AA.name), - // ) - // return pipe_( - // hero_entry, - // AOWIA.sid, - // misStringM, - // bindF(lookupF(SDA.skills(staticData))), - // bindF(skill => - // pipe_( - // List(getApp(AOWIA.sid2)(skill), getApp(AOWIA.sid3)(skill)), - // catMaybes, - // ensure(xs => flength(xs) === 2), - // fmap( - // pipe( - // sortStrings(staticData), - // formatList("conjunction")(staticData), - // apps => `${SA.name(skill)}: ${apps}`, - // ), - // ), - // ), - // ), - // ) - // } - - default: - return undefined - } - case "AncestorGlyph": - case "ArcaneOrbEnchantment": - case "AttireEnchantment": - case "Beutelzauber": - case "BlessedTradition": - case "BowlEnchantment": - case "BrawlingSpecialAbility": - case "CauldronEnchantment": - case "CeremonialItemSpecialAbility": - case "ChronicleEnchantment": - case "CombatSpecialAbility": - case "CombatStyleSpecialAbility": - case "CommandSpecialAbility": - case "DaggerRitual": - case "FamiliarSpecialAbility": - case "FatePointSexSpecialAbility": - case "FatePointSpecialAbility": - case "FoolsHatEnchantment": - return undefined - case "GeneralSpecialAbility": - switch (id.GeneralSpecialAbility) { - // case GeneralSpecialAbilityIdentifier.LanguageSpecialization: { - // const [languageId, specializationId] = options ?? [] - - // const language = - // languageId?.tag === "Language" - // ? getLanguageById(languageId.language) - // : undefined - - // const specializations = - // language?.specializations?.tag === "Specific" - // ? language.specializations.specific.list - // : [] - - // const specialization = - // specializationId?.tag === "General" - // ? specializations.find( - // spec => spec.id === specializationId.general, - // ) - // : undefined - - // if (aspectTranslations === undefined) { - // return undefined - // } - - // return combineBaseName( - // combineChunks( - // base, - // aspectTranslations.master_of_aspect_suffix ?? - // aspectTranslations.name, - // (a, b) => `${a} ${b}`, - // ), - // level, - // [], - // ) - - // return pipe( - // SDA.specialAbilities, - // lookup(SpecialAbilityId.Language), - // bindF(pipe(findSelectOption, thrush(AOWIA.sid(hero_entry)))), - // bindF(lang => - // pipe( - // AOWIA.sid2, - // bindF( - // ifElse(isString)>( - // Just, - // )(spec_id => - // bind(SOA.specializations(lang))(subscriptF(spec_id - 1)), - // ), - // ), - // fmap(spec => `${SOA.name(lang)}: ${spec}`), - // )(hero_entry), - // ), - // )(staticData) - // } - - default: - return undefined - } - case "Haubenzauber": - case "InstrumentEnchantment": - return undefined - case "KarmaSpecialAbility": - switch (id.KarmaSpecialAbility) { - case KarmaSpecialAbilityIdentifier.MasterOfAspect: { - const [aspectId] = options ?? [] - const aspect = - aspectId?.kind === "Aspect" - ? getInstanceById("Aspect", aspectId.Aspect) - : undefined - const aspectTranslations = locale.translateMap(aspect?.translations) - - if (aspectTranslations === undefined) { - return undefined - } - - return combineBaseName( - combineChunks( - base, - aspectTranslations.master_of_aspect_suffix ?? - aspectTranslations.name, - (a, b) => `${a} ${b}`, - ), - level, - [], - ) - } - - default: - return undefined - } - case "Krallenkettenzauber": - case "Kristallkugelzauber": - case "LiturgicalStyleSpecialAbility": - case "LycantropicGift": - case "MagicalSign": - case "MagicalSpecialAbility": - case "MagicalTradition": - case "MagicStyleSpecialAbility": - case "OrbEnchantment": - case "PactGift": - case "ProtectiveWardingCircleSpecialAbility": - case "RingEnchantment": - case "Sermon": - case "SexSpecialAbility": - case "SickleRitual": - case "SikaryanDrainSpecialAbility": - case "SkillStyleSpecialAbility": - case "SpellSwordEnchantment": - case "StaffEnchantment": - case "ToyEnchantment": - case "Trinkhornzauber": - case "VampiricGift": - case "Vision": - case "WandEnchantment": - case "WeaponEnchantment": - return undefined - default: - return assertExhaustive(id) +/** + * Combines multiple name components into a single one when all values except for options are the same, joining options according to the passed function. + * + * Returns `undefined` if the components cannot be combined. + */ +export const combineNameComponents = ( + elements: ActivatableNameComponents[], +): CombinedActivatableNameComponents | undefined => { + if ( + isNotEmpty(elements) && + allSame( + elements, + on(item => [item.id, item.level], deepEqual), + ) + ) { + return { + ...elements[0], + options: + ensureNonEmpty(elements.map(e => e.options).filter(isNotNullish)) ?? [], + } } + + return undefined +} + +/** + * Renders the name components of an activatable entry, combining multiple options into one string. + */ +export const renderCombinedActivatableNameComponents = ( + translateMap: TranslateMap, + components: CombinedActivatableNameComponents, + formatAsPrerequisite: boolean, + join: (list: string[]) => string = list => list.join(", "), +): string => + renderActivatableNameComponents( + translateMap, + { + ...components, + options: join( + components.options.map(chunk => + renderActivatableNameChunk(translateMap, normalizeChunks(chunk)), + ), + ), + }, + formatAsPrerequisite, + ) + +/** + * Renders the name components of multiple activatable entries. + */ +export const renderMultipleStandaloneActivatableNameComponents = ( + translateMap: TranslateMap, + components: ActivatableNameComponents[], + formatAsPrerequisite: boolean, + join: (list: string[]) => string = list => list.join(", "), +): string => + join( + components.map(item => + renderActivatableNameComponents(translateMap, item, formatAsPrerequisite), + ), + ) + +/** + * Renders the name components of multiple activatable entries, combining parts if possible. + */ +export const renderActivatableNameComponentsCombinedIfPossible = ( + translateMap: TranslateMap, + components: ActivatableNameComponents[], + formatAsPrerequisite: boolean, + join: (list: string[]) => string = list => list.join(", "), +): string => { + const combined = combineNameComponents(components) + if (combined === undefined) { + return renderMultipleStandaloneActivatableNameComponents( + translateMap, + components, + formatAsPrerequisite, + join, + ) + } else { + return renderCombinedActivatableNameComponents( + translateMap, + combined, + formatAsPrerequisite, + join, + ) + } +} + +// const getEntrySpecificFullName = ( +// getInstanceById: GetInstanceById<"Aspect">, +// locale: LocaleEnvironment, +// id: ActivatableIdentifier, +// base: ActivatableNameChunk, +// level: number | undefined, +// printedOptions: ActivatableNameComponents["options"], +// ): +// | Pick +// | undefined => { +// switch (id.kind) { +// case "Advantage": +// switch (id.Advantage) { +// case AdvantageIdentifier.HatredOf: { +// const [firstOption, ...rest] = printedOptions + +// if (firstOption === undefined || Array.isArray(firstOption)) { +// return undefined +// } + +// return combineBaseName( +// combineChunks(base, firstOption, (a, b) => `${a} ${b}`), +// level, +// rest, +// ) +// } +// default: +// return undefined +// } +// case "Disadvantage": +// switch (id.Disadvantage) { +// case DisadvantageIdentifier.PersonalityFlaw: { +// const [selection, optionalText, ...rest] = printedOptions + +// if ( +// selection === undefined || +// Array.isArray(selection) || +// Array.isArray(optionalText) +// ) { +// return undefined +// } + +// return combineBaseName(base, level, [ +// optionalText === undefined ? selection : [selection, optionalText], +// ...rest, +// ]) +// } +// default: +// return undefined +// } +// case "AdvancedCombatSpecialAbility": +// case "AdvancedKarmaSpecialAbility": +// case "AdvancedMagicalSpecialAbility": +// case "AdvancedSkillSpecialAbility": +// switch (id.AdvancedSkillSpecialAbility) { +// case AdvancedSkillSpecialAbilityIdentifier.Fachwissen: { +// const [skillId, firstApplicationId, secondApplicationId] = options ?? [] +// const aspect = +// aspectId?.tag === "Aspect" +// ? getAspectById(aspectId.aspect) +// : undefined +// const aspectTranslations = locale.translateMap(aspect?.translations) + +// if (aspectTranslations === undefined) { +// return undefined +// } + +// return combineBaseName( +// combineChunks( +// base, +// aspectTranslations.master_of_aspect_suffix ?? +// aspectTranslations.name, +// (a, b) => `${a} ${b}`, +// ), +// level, +// [], +// ) +// const getApp = ( +// getSid: (r: Record) => Maybe, +// ) => +// pipe( +// SA.applications, +// filter(pipe(AA.prerequisite, isNothing)), +// find(pipe(AA.id, elemF(getSid(hero_entry)))), +// fmap(AA.name), +// ) +// return pipe_( +// hero_entry, +// AOWIA.sid, +// misStringM, +// bindF(lookupF(SDA.skills(staticData))), +// bindF(skill => +// pipe_( +// List(getApp(AOWIA.sid2)(skill), getApp(AOWIA.sid3)(skill)), +// catMaybes, +// ensure(xs => flength(xs) === 2), +// fmap( +// pipe( +// sortStrings(staticData), +// formatList("conjunction")(staticData), +// apps => `${SA.name(skill)}: ${apps}`, +// ), +// ), +// ), +// ), +// ) +// } + +// default: +// return undefined +// } +// case "AncestorGlyph": +// case "ArcaneOrbEnchantment": +// case "AttireEnchantment": +// case "Beutelzauber": +// case "BlessedTradition": +// case "BowlEnchantment": +// case "BrawlingSpecialAbility": +// case "CauldronEnchantment": +// case "CeremonialItemSpecialAbility": +// case "ChronicleEnchantment": +// case "CombatSpecialAbility": +// case "CombatStyleSpecialAbility": +// case "CommandSpecialAbility": +// case "DaggerRitual": +// case "FamiliarSpecialAbility": +// case "FatePointSexSpecialAbility": +// case "FatePointSpecialAbility": +// case "FoolsHatEnchantment": +// return undefined +// case "GeneralSpecialAbility": +// switch (id.GeneralSpecialAbility) { +// case GeneralSpecialAbilityIdentifier.LanguageSpecialization: { +// const [languageId, specializationId] = options ?? [] + +// const language = +// languageId?.tag === "Language" +// ? getLanguageById(languageId.language) +// : undefined + +// const specializations = +// language?.specializations?.tag === "Specific" +// ? language.specializations.specific.list +// : [] + +// const specialization = +// specializationId?.tag === "General" +// ? specializations.find( +// spec => spec.id === specializationId.general, +// ) +// : undefined + +// if (aspectTranslations === undefined) { +// return undefined +// } + +// return pipe( +// SDA.specialAbilities, +// lookup(SpecialAbilityId.Language), +// bindF(pipe(findSelectOption, thrush(AOWIA.sid(hero_entry)))), +// bindF(lang => +// pipe( +// AOWIA.sid2, +// bindF( +// ifElse(isString)>( +// Just, +// )(spec_id => +// bind(SOA.specializations(lang))(subscriptF(spec_id - 1)), +// ), +// ), +// fmap(spec => `${SOA.name(lang)}: ${spec}`), +// )(hero_entry), +// ), +// )(staticData) +// } + +const renderOptions = ( + displayedInProfession: boolean, + getResolvedSelectOptionById: GetResolvedSelectOptionById, + id: ActivatableIdentifier, + options: RequirableSelectOptionIdentifier[] | undefined, +): + | ActivatableNameChunk + | [ActivatableNameChunk, ActivatableNameChunk] + | undefined => { + const arr = + options?.map(optionId => { + const optTranslations = getResolvedSelectOptionById(id, optionId)?.content + .translations + return optTranslations === undefined + ? MISSING_VALUE + : mapObject(optTranslations, t10n => + displayedInProfession + ? (t10n.name_in_profession ?? t10n.name) + : t10n.name, + ) + }) ?? [] + + if (isNotEmpty(arr) && arr.length > 1) { + const [first, ...rest] = arr + return [first, zipChunks(rest)] + } + + return arr[0] } /** * Gets the name components for an activatable entry. */ export const getNameComponents = ( - getInstanceById: GetInstanceById<"Aspect">, - locale: LocaleEnvironment, + translate: Translate, id: ActivatableIdentifier, options: RequirableSelectOptionIdentifier[] | undefined, level: number | undefined, + nameBuilderRules: ActivatableNameBuilderRules | undefined, translations: LocaleMap, getBaseName: (translation: T) => string, - getSelectOptionById: ( - id: RequirableSelectOptionIdentifier, - ) => ResolvedSelectOption | undefined, + getResolvedSelectOptionById: GetResolvedSelectOptionById, displayedInProfession: boolean, ): ActivatableNameComponents => { - const base = mapObject(translations, getBaseName) - const nameOptions: ActivatableNameComponents["options"] = (() => { - const arr = - options?.map(optionId => { - const optTranslations = - getSelectOptionById(optionId)?.content.translations - return optTranslations === undefined - ? MISSING_VALUE - : mapObject(optTranslations, t10n => - displayedInProfession - ? (t10n.name_in_profession ?? t10n.name) - : t10n.name, - ) - }) ?? [] + const nameBuilderRulesWithDefaults: Required = { + levelPlacement: nameBuilderRules?.levelPlacement ?? { + kind: "AfterOptions", + }, + useParenthesis: nameBuilderRules?.useParenthesis ?? true, + } - if (arr.length > 1) { - const [first, ...rest] = arr - return [[first!, zipChunks(rest)]] - } + const renderedBase = mapObject(translations, getBaseName) + const renderedOptions = renderOptions( + displayedInProfession, + getResolvedSelectOptionById, + id, + options, + ) - return arr - })() + const isTradition = + id.kind === "MagicalTradition" || id.kind === "BlessedTradition" + + const actualBase = isTradition ? translate("Tradition") : renderedBase + const actualOptions: + | ActivatableNameChunk + | [ActivatableNameChunk, ActivatableNameChunk] + | undefined = isTradition + ? renderedOptions === undefined + ? renderedBase + : [renderedBase, normalizeChunks(renderedOptions)] + : renderedOptions return { - ...(getEntrySpecificFullName( - getInstanceById, - locale, - id, - base, - level, - options, - nameOptions, - ) ?? combineBaseName(base, level, nameOptions)), - base, - options: nameOptions, + id, + base: actualBase, + options: actualOptions, + level, + nameBuilderRules: nameBuilderRulesWithDefaults, } } - -/** - * Converts a name chunk to a displayable string. - */ -export const printActivatableNameChunk = ( - locale: LocaleEnvironment, - chunk: ActivatableNameChunk, -): string => { - if (typeof chunk === "string") { - return chunk - } else if (typeof chunk === "function") { - return chunk(locale) - } else if (typeof chunk === "object") { - return locale.translateMap(chunk) ?? MISSING_VALUE - } - return chunk -} diff --git a/src/entities/partial/prerequisites/displayOption.ts b/src/entities/partial/prerequisites/displayOption.ts index ea2afd2..cc7508c 100644 --- a/src/entities/partial/prerequisites/displayOption.ts +++ b/src/entities/partial/prerequisites/displayOption.ts @@ -1,6 +1,6 @@ import { assertExhaustive } from "@optolith/helpers/typeSafety" import { DisplayOption } from "optolith-database-schema/gen" -import { LocaleEnvironment } from "../../../helpers/locale.js" +import type { TranslateMap } from "../../../helpers/translate.js" import { MISSING_VALUE } from "../unknown.js" import { PrerequisitePart } from "./part.js" @@ -8,7 +8,7 @@ import { PrerequisitePart } from "./part.js" * Get the translation of a display option. */ export const printDisplayOption = ( - locale: LocaleEnvironment, + translateMap: TranslateMap, displayOption: DisplayOption, ): PrerequisitePart | undefined => { switch (displayOption.kind) { @@ -16,9 +16,7 @@ export const printDisplayOption = ( return undefined case "ReplaceWith": return { - value: - locale.translateMap(displayOption.ReplaceWith.translations) - ?.replacement ?? MISSING_VALUE, + value: translateMap(displayOption.ReplaceWith.translations)?.replacement ?? MISSING_VALUE, sentenceType: displayOption.ReplaceWith.sentence_type, isMeta: false, } diff --git a/src/entities/partial/prerequisites/index.ts b/src/entities/partial/prerequisites/index.ts index 332b4a3..8d7129a 100644 --- a/src/entities/partial/prerequisites/index.ts +++ b/src/entities/partial/prerequisites/index.ts @@ -1,4 +1,5 @@ import { on } from "@elyukai/utils/function" +import { mapNullable } from "@elyukai/utils/nullable" import { numAsc } from "@optolith/helpers/compare" import { isNotNullish } from "@optolith/helpers/nullable" import { romanize } from "@optolith/helpers/roman" @@ -32,9 +33,14 @@ import { } from "optolith-database-schema/gen" import type { GetInstanceById } from "../../../helpers/getTypes.js" import { LocaleEnvironment } from "../../../helpers/locale.js" +import type { TranslateMap } from "../../../helpers/translate.js" +import { + renderActivatableNameComponents, + renderActivatableNameComponentsCombinedIfPossible, +} from "../activatableNameChunks.js" import { MISSING_VALUE } from "../unknown.js" import { printDisplayOption } from "./displayOption.js" -import { joinPrerequisiteParts, PrerequisitePart } from "./part.js" +import { hasPartValueObject, joinPrerequisiteParts, PrerequisitePart } from "./part.js" import { printAdvantageDisadvantagePrerequisiteGroup, printAnimistPowerPrerequisiteGroup, @@ -56,26 +62,24 @@ import { GetResolvedSelectOptionById } from "./single/activatable.js" type Prerequisite = { kind: string } const printPrerequisiteGroup = ( - locale: LocaleEnvironment, + translateMap: TranslateMap, group: PrerequisiteGroup, ): PrerequisitePart => ({ - value: locale.translateMap(group.translations)?.text ?? MISSING_VALUE, + value: translateMap(group.translations)?.text ?? MISSING_VALUE, sentenceType: undefined, isMeta: false, }) const printPrerequisitesDisjunction = ( getPrerequisiteTranslation: (prerequisite: T) => PrerequisitePart | undefined, - locale: LocaleEnvironment, + locale: Pick, disjunction: PrerequisitesDisjunction, ): PrerequisitePart | undefined => { if (disjunction.display_option !== undefined) { - return printDisplayOption(locale, disjunction.display_option) + return printDisplayOption(locale.translateMap, disjunction.display_option) } - const [first, ...other] = disjunction.list - .map(getPrerequisiteTranslation) - .filter(isNotNullish) + const [first, ...other] = disjunction.list.map(getPrerequisiteTranslation).filter(isNotNullish) if (first === undefined) { return undefined @@ -83,16 +87,26 @@ const printPrerequisitesDisjunction = ( if ( disjunction.list.length < 2 || - disjunction.list - .slice(1) - .every(part => part.kind === disjunction.list[0]!.kind) + disjunction.list.slice(1).every(part => part.kind === disjunction.list[0]!.kind) ) { return { label: first.label, - value: locale.join( - [first, ...other].map(part => part.value), - "disjunction", - ), + value: + hasPartValueObject(first) && other.every(hasPartValueObject) + ? renderActivatableNameComponentsCombinedIfPossible( + locale.translateMap, + [first.value, ...other.map(part => part.value)], + true, + list => locale.join(list, "disjunction"), + ) + : locale.join( + [first, ...other].map(part => + typeof part.value === "string" + ? part.value + : renderActivatableNameComponents(locale.translateMap, part.value, true), + ), + "disjunction", + ), sentenceType: undefined, isMeta: false, } @@ -113,20 +127,16 @@ const printPrerequisitesDisjunction = ( */ const printPrerequisitesElement = ( printPrerequisite: (prerequisite: T) => PrerequisitePart | undefined, - locale: LocaleEnvironment, + locale: Pick, element: PrerequisitesElement, ): PrerequisitePart | undefined => { switch (element.kind) { case "Single": return printPrerequisite(element.Single) case "Disjunction": - return printPrerequisitesDisjunction( - printPrerequisite, - locale, - element.Disjunction, - ) + return printPrerequisitesDisjunction(printPrerequisite, locale, element.Disjunction) case "Group": - return printPrerequisiteGroup(locale, element.Group) + return printPrerequisiteGroup(locale.translateMap, element.Group) default: return assertExhaustive(element) } @@ -137,14 +147,19 @@ const printPrerequisitesElement = ( */ const printPlainPrerequisites = ( printPrerequisite: (prerequisite: T) => PrerequisitePart | undefined, - locale: LocaleEnvironment, + locale: Pick, prerequisites: PlainPrerequisites, ): string => joinPrerequisiteParts( - locale, + locale.translate, + locale.translateMap, + locale.compare, prerequisites .map(element => - printPrerequisitesElement(printPrerequisite, locale, element), + mapNullable(printPrerequisitesElement(printPrerequisite, locale, element), part => ({ + type: element.kind === "Single" ? element.Single.kind : element.kind, + part, + })), ) .filter(isNotNullish), ) @@ -173,18 +188,13 @@ const printPrerequisitesForLevels = ( const previousLevelPrerequisites: PrerequisitesForLevels = printPreviousLevelPrerequisites === undefined ? [] - : Array.from( - { length: printPreviousLevelPrerequisites.levels - 1 }, - (_, i) => ({ - level: i + 2, - prerequisite: { - kind: "Single", - Single: printPreviousLevelPrerequisites.createPreerequisite( - i + 2, - ), - }, - }), - ) + : Array.from({ length: printPreviousLevelPrerequisites.levels - 1 }, (_, i) => ({ + level: i + 2, + prerequisite: { + kind: "Single", + Single: printPreviousLevelPrerequisites.createPreerequisite(i + 2), + }, + })) const groupedByLevel = Map.groupBy( [...value, ...previousLevelPrerequisites], @@ -198,8 +208,7 @@ const printPrerequisitesForLevels = ( .toArray() .sort(on(item => item[0], numAsc)) - const hasOnlyBasePrerequisites = - groupedByLevel.size === 1 && hasBasePrerequisites + const hasOnlyBasePrerequisites = groupedByLevel.size === 1 && hasBasePrerequisites const printedParts = [ ...(hasBasePrerequisites @@ -211,10 +220,18 @@ const printPrerequisitesForLevels = ( ]), ...sortedByLevel.map(([levelNumber, prerequisites]) => { const prerequisitesString = joinPrerequisiteParts( - locale, + locale.translate, + locale.translateMap, + locale.compare, prerequisites - .map(prerequisite => - printPrerequisiteForLevel(printPrerequisite, locale, prerequisite), + .map(element => + mapNullable(printPrerequisiteForLevel(printPrerequisite, locale, element), part => ({ + type: + element.prerequisite.kind === "Single" + ? element.prerequisite.Single.kind + : element.prerequisite.kind, + part, + })), ) .filter(isNotNullish), ) @@ -238,8 +255,7 @@ export const printDerivedCharacteristicPrerequisites = ( value: DerivedCharacteristicPrerequisites, ): string => printPlainPrerequisites( - prerequisite => - printDerivedCharacteristicPrerequisiteGroup(locale, prerequisite), + prerequisite => printDerivedCharacteristicPrerequisiteGroup(locale, prerequisite), locale, value, ) @@ -253,8 +269,7 @@ export const printPublicationPrerequisites = ( value: PublicationPrerequisites, ): string => printPlainPrerequisites( - prerequisite => - printPublicationPrerequisiteGroup(getInstanceById, locale, prerequisite), + prerequisite => printPublicationPrerequisiteGroup(getInstanceById, locale, prerequisite), locale, value, ) @@ -347,14 +362,10 @@ export const printGeneralPrerequisites = ( */ export const printProfessionPrerequisites = ( getInstanceById: GetInstanceById< - | "Race" - | "Culture" - | ActivatableIdentifier["kind"] - | RatedIdentifier["kind"] - | "Aspect" + "Race" | "Culture" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "Aspect" >, getResolvedSelectOptionById: GetResolvedSelectOptionById, - locale: LocaleEnvironment, + locale: Pick, value: ProfessionPrerequisites, ): string => printPlainPrerequisites( @@ -415,12 +426,7 @@ export const printArcaneTraditionPrerequisites = ( value: ArcaneTraditionPrerequisites, ): string => printPlainPrerequisites( - prerequisite => - printArcaneTraditionPrerequisiteGroup( - getInstanceById, - locale, - prerequisite, - ), + prerequisite => printArcaneTraditionPrerequisiteGroup(getInstanceById, locale, prerequisite), locale, value, ) @@ -434,12 +440,7 @@ export const printPersonalityTraitPrerequisites = ( value: PersonalityTraitPrerequisites, ): string => printPlainPrerequisites( - prerequisite => - printPersonalityTraitPrerequisiteGroup( - getInstanceById, - locale, - prerequisite, - ), + prerequisite => printPersonalityTraitPrerequisiteGroup(getInstanceById, locale, prerequisite), locale, value, ) @@ -453,8 +454,7 @@ export const printSpellworkPrerequisites = ( value: SpellworkPrerequisites, ): string => printPlainPrerequisites( - prerequisite => - printSpellworkPrerequisiteGroup(getInstanceById, locale, prerequisite), + prerequisite => printSpellworkPrerequisiteGroup(getInstanceById, locale, prerequisite), locale, value, ) @@ -476,11 +476,12 @@ export const printLiturgyPrerequisites = ( * Print influence prerequisites as a string. */ export const printInfluencePrerequisites = ( + getInstanceById: GetInstanceById<"Influence">, locale: LocaleEnvironment, value: InfluencePrerequisites, ): string => printPlainPrerequisites( - prerequisite => printInfluencePrerequisiteGroup(locale, prerequisite), + prerequisite => printInfluencePrerequisiteGroup(getInstanceById, locale, prerequisite), locale, value, ) @@ -489,9 +490,7 @@ export const printInfluencePrerequisites = ( * Print language prerequisites as a string. */ export const printLanguagePrerequisites = ( - getInstanceById: GetInstanceById< - "Race" | ActivatableIdentifier["kind"] | "Aspect" - >, + getInstanceById: GetInstanceById<"Race" | ActivatableIdentifier["kind"] | "Aspect">, getResolvedSelectOptionById: GetResolvedSelectOptionById, locale: LocaleEnvironment, value: LanguagePrerequisites, @@ -517,8 +516,7 @@ export const printAnimistPowerPrerequisites = ( value: AnimistPowerPrerequisites, ): string => printPlainPrerequisites( - prerequisite => - printAnimistPowerPrerequisiteGroup(getInstanceById, locale, prerequisite), + prerequisite => printAnimistPowerPrerequisiteGroup(getInstanceById, locale, prerequisite), locale, value, ) @@ -527,11 +525,12 @@ export const printAnimistPowerPrerequisites = ( * Print geode ritual prerequisites as a string. */ export const printGeodeRitualPrerequisites = ( + getInstanceById: GetInstanceById<"Influence">, locale: LocaleEnvironment, value: GeodeRitualPrerequisites, ): string => printPlainPrerequisites( - prerequisite => printGeodeRitualPrerequisiteGroup(locale, prerequisite), + prerequisite => printGeodeRitualPrerequisiteGroup(getInstanceById, locale, prerequisite), locale, value, ) @@ -545,8 +544,7 @@ export const printEnhancementPrerequisites = ( value: EnhancementPrerequisites, ): string => printPlainPrerequisites( - prerequisite => - printEnhancementPrerequisiteGroup(getInstanceById, locale, prerequisite), + prerequisite => printEnhancementPrerequisiteGroup(getInstanceById, locale, prerequisite), locale, value, ) diff --git a/src/entities/partial/prerequisites/part.ts b/src/entities/partial/prerequisites/part.ts index 40d59dd..555dbcd 100644 --- a/src/entities/partial/prerequisites/part.ts +++ b/src/entities/partial/prerequisites/part.ts @@ -1,47 +1,208 @@ +import { groupBy } from "@elyukai/utils/array/groups" +import { deepEqual, equal } from "@elyukai/utils/equality" +import { on } from "@elyukai/utils/function" +import { compareNumber, reduceCompare, type Compare } from "@elyukai/utils/ordering" import { assertExhaustive } from "@optolith/helpers/typeSafety" -import { SentenceType } from "optolith-database-schema/gen" -import { LocaleEnvironment } from "../../../helpers/locale.js" +import { SentenceType, type ActivatableIdentifier } from "optolith-database-schema/gen" +import type { LocaleCompare } from "../../../helpers/locale.js" +import type { Translate, TranslateMap } from "../../../helpers/translate.js" +import { + renderActivatableNameComponents, + renderActivatableNameComponentsCombinedIfPossible, + type ActivatableNameComponents, +} from "../activatableNameChunks.js" /** * A part of the total list of prerequisites. */ export type PrerequisitePart = { label?: string - value: string + value: string | ActivatableNameComponents sentenceType: SentenceType | undefined isMeta: boolean } +/** + * Type guard for prerequisite parts with a value object. + */ +export const hasPartValueObject = ( + part: PrerequisitePart, +): part is PrerequisitePart & { value: ActivatableNameComponents } => typeof part.value !== "string" + +const countFurtherToCombined = ( + part: PrerequisitePart & { value: ActivatableNameComponents }, + remainingParts: (PrerequisitePart & { value: ActivatableNameComponents })[], +) => { + const indexHasDifferentBaseValues = remainingParts.findIndex( + remaining => + !( + part.label === remaining.label && + deepEqual(part.value.id, remaining.value.id) && + part.value.level === remaining.value.level + ), + ) + + return indexHasDifferentBaseValues === -1 + ? // if -1 is returned, all remaining parts have the same base values, so we can combine with all of them + remainingParts.length + : indexHasDifferentBaseValues +} + +const joinAdjacentParts = ( + translateMap: TranslateMap, + localeCompare: LocaleCompare, + part: PrerequisitePart & { value: ActivatableNameComponents }, + remainingParts: (PrerequisitePart & { value: ActivatableNameComponents })[], +): [renderedValue: string, furtherIncluded: number] => { + const valuesWithSameBase = remainingParts.slice(0, countFurtherToCombined(part, remainingParts)) + + if (valuesWithSameBase.length > 0) { + return [ + renderActivatableNameComponentsCombinedIfPossible( + translateMap, + [part.value, ...valuesWithSameBase.map(p => p.value)], + true, + list => list.toSorted(localeCompare).join(", "), + ), + valuesWithSameBase.length, + ] + } + + return [renderActivatableNameComponents(translateMap, part.value, true), 0] +} + +const appendBySentenceType = ( + previouslyRendered: string, + currentRendered: string, + sentenceType: SentenceType | undefined, + isLast: boolean, +): string => { + switch (sentenceType?.kind) { + case "Standalone": + return `${ + /[.;]$/u.test(previouslyRendered) + ? `${previouslyRendered.slice(0, -1)}. ` + : `${previouslyRendered}. ` + }${currentRendered}${currentRendered.endsWith(".") ? "" : "."}` + case "Connected": + return `${previouslyRendered === "" ? "" : /[.;]$/u.test(previouslyRendered) ? `${previouslyRendered} ` : `${previouslyRendered}; `}${currentRendered}${ + isLast ? "" : ";" + }` + case undefined: + return previouslyRendered === "" + ? currentRendered + : `${previouslyRendered}${/[.;]$/u.test(previouslyRendered) ? " " : ", "}${currentRendered}` + default: + return assertExhaustive(sentenceType) + } +} + +type ActivatableGroup = "Advantage" | "Disadvantage" | "SpecialAbility" +const activatableKindToGroup = (kind: ActivatableIdentifier["kind"]): ActivatableGroup => + kind === "Advantage" ? "Advantage" : kind === "Disadvantage" ? "Disadvantage" : "SpecialAbility" +const activatableGroupOrder: ActivatableGroup[] = ["Advantage", "Disadvantage", "SpecialAbility"] +const sortByActivatableGroup: Compare = on( + group => activatableGroupOrder.indexOf(group), + compareNumber, +) + +const sortByActivatableGroupAndName = ( + localeCompare: LocaleCompare, +): Compare<[ActivatableGroup, string]> => + reduceCompare( + on(item => item[0], sortByActivatableGroup), + on(item => item[1], localeCompare), + ) + +const appendPrerequisitePartGroup = ( + previous: string, + translateMap: TranslateMap, + localeCompare: LocaleCompare, + parts: PrerequisitePart[], + isLast: boolean, +) => { + if (parts.length === 0) { + return previous + } else if (parts.every(hasPartValueObject)) { + return appendBySentenceType( + previous, + parts + .reduce( + ( + acc: [[ActivatableGroup, string][], number], + current, + i, + arr, + ): [[ActivatableGroup, string][], number] => { + if (acc[1] > 0) { + return [acc[0], acc[1] - 1] + } + + const [rendered, furtherIncluded] = joinAdjacentParts( + translateMap, + localeCompare, + current, + arr.slice(i + 1), + ) + + return [ + [ + ...acc[0], + [activatableKindToGroup(current.value.id.kind), (current.label ?? "") + rendered], + ], + furtherIncluded, + ] + }, + [[], 0], + )[0] + .toSorted(sortByActivatableGroupAndName(localeCompare)) + .map(grouped => grouped[1]) + .join(", "), + undefined, + isLast, + ) + } else { + return parts.reduce( + (acc, current, i, arr) => + appendBySentenceType( + acc, + (current.label ?? "") + + (typeof current.value === "string" + ? current.value + : renderActivatableNameComponents(translateMap, current.value, true)), + current.sentenceType, + isLast && i === arr.length - 1, + ), + previous, + ) + } +} + /** * Join prerequisite parts using their configuration. */ export const joinPrerequisiteParts = ( - locale: LocaleEnvironment, - parts: PrerequisitePart[], + translate: Translate, + translateMap: TranslateMap, + localeCompare: LocaleCompare, + parts: { type: string; part: PrerequisitePart }[], ): string => - parts.reduce( - (acc, part, i, arr) => { - const text = - part.label === undefined ? part.value : part.label + part.value - - if (acc === "") { - return text - } - - switch (part.sentenceType?.kind) { - case "Standalone": - return `${ - /[.;]$/u.test(acc) ? `${acc.slice(0, -1)}. ` : `${acc}. ` - }${text}${text.endsWith(".") ? "" : "."}` - case "Connected": - return `${/[.;]$/u.test(acc) ? `${acc} ` : `${acc}; `}${text}${ - i < arr.length - 1 ? ";" : "" - }` - case undefined: - return `${acc}${/[.;]$/u.test(acc) ? " " : ", "}${text}` - default: - return assertExhaustive(part.sentenceType) - } - }, - parts.every(part => part.isMeta) ? locale.translate("none") : "", + groupBy( + parts, + on(part => part.type, equal), ) + .map(group => ({ + type: group[0]!.type, + parts: group.map(groupItem => groupItem.part), + })) + .reduce( + (acc, partGroup, i, arr) => + appendPrerequisitePartGroup( + acc, + translateMap, + localeCompare, + partGroup.parts, + i === arr.length - 1, + ), + parts.every(({ part }) => part.isMeta) ? translate("none") : "", + ) diff --git a/src/entities/partial/prerequisites/prerequisiteGroups.ts b/src/entities/partial/prerequisites/prerequisiteGroups.ts index d1a8fc7..0d63e15 100644 --- a/src/entities/partial/prerequisites/prerequisiteGroups.ts +++ b/src/entities/partial/prerequisites/prerequisiteGroups.ts @@ -21,10 +21,7 @@ import { import type { GetInstanceById } from "../../../helpers/getTypes.js" import { LocaleEnvironment } from "../../../helpers/locale.js" import { PrerequisitePart } from "./part.js" -import { - GetResolvedSelectOptionById, - printActivatablePrerequisite, -} from "./single/activatable.js" +import { GetResolvedSelectOptionById, printActivatablePrerequisite } from "./single/activatable.js" import { printAnimistPowerPrerequisite } from "./single/animistPower.js" import { printBlessedTraditionPrerequisite } from "./single/blessedTradition.js" import { printCommonSuggestedByRCPPrerequisite } from "./single/commonSuggestedByRCP.js" @@ -59,15 +56,9 @@ export const printDerivedCharacteristicPrerequisiteGroup = ( case "Rule": return printRulePrerequisite(locale, prerequisite.Rule) case "BlessedTradition": - return printBlessedTraditionPrerequisite( - locale, - prerequisite.BlessedTradition, - ) + return printBlessedTraditionPrerequisite(locale, prerequisite.BlessedTradition) case "MagicalTradition": - return printMagicalTraditionPrerequisite( - locale, - prerequisite.MagicalTradition, - ) + return printMagicalTraditionPrerequisite(locale, prerequisite.MagicalTradition) default: return assertExhaustive(prerequisite) } @@ -86,11 +77,7 @@ export const printPublicationPrerequisiteGroup = ( // default: // return assertExhaustive(prerequisite) // } - printPublicationPrerequisite( - getInstanceById, - locale, - prerequisite.Publication, - ) + printPublicationPrerequisite(getInstanceById, locale, prerequisite.Publication) /** * Print the translation of a general prerequisite group. @@ -115,49 +102,33 @@ export const printGeneralPrerequisiteGroup = ( ): PrerequisitePart | undefined => { switch (prerequisite.kind) { case "Sex": - return printBinarySexPrerequisite(locale, prerequisite.Sex) + return printBinarySexPrerequisite(locale.translate, prerequisite.Sex) case "Race": return printRacePrerequisite(getInstanceById, locale, prerequisite.Race) case "Culture": - return printCulturePrerequisite( - getInstanceById, - locale, - prerequisite.Culture, - ) + return printCulturePrerequisite(getInstanceById, locale, prerequisite.Culture) case "Pact": return printPactPrerequisite(getInstanceById, locale, prerequisite.Pact) case "SocialStatus": - return printSocialStatusPrerequisite( - getInstanceById, - locale, - prerequisite.SocialStatus, - ) + return printSocialStatusPrerequisite(getInstanceById, locale, prerequisite.SocialStatus) case "State": return printStatePrerequisite(getInstanceById, locale, prerequisite.State) case "Rule": return printRulePrerequisite(locale, prerequisite.Rule) case "PrimaryAttribute": - return printPrimaryAttributePrerequisite( - locale, - prerequisite.PrimaryAttribute, - ) + return printPrimaryAttributePrerequisite(locale, prerequisite.PrimaryAttribute) case "Activatable": return printActivatablePrerequisite( getInstanceById, getResolvedSelectOptionById, locale, prerequisite.Activatable, + false, ) case "BlessedTradition": - return printBlessedTraditionPrerequisite( - locale, - prerequisite.BlessedTradition, - ) + return printBlessedTraditionPrerequisite(locale, prerequisite.BlessedTradition) case "MagicalTradition": - return printMagicalTraditionPrerequisite( - locale, - prerequisite.MagicalTradition, - ) + return printMagicalTraditionPrerequisite(locale, prerequisite.MagicalTradition) case "Rated": return printRatedPrerequisite(getInstanceById, locale, prerequisite.Rated) case "RatedMinimumNumber": @@ -167,24 +138,13 @@ export const printGeneralPrerequisiteGroup = ( prerequisite.RatedMinimumNumber, ) case "RatedSum": - return printRatedSumPrerequisite( - getInstanceById, - locale, - prerequisite.RatedSum, - ) + return printRatedSumPrerequisite(getInstanceById, locale, prerequisite.RatedSum) case "Enhancement": - return printEnhancementPrerequisite( - getInstanceById, - locale, - prerequisite.Enhancement, - ) + return printEnhancementPrerequisite(getInstanceById, locale, prerequisite.Enhancement) case "Text": return printTextPrerequisite(locale, prerequisite.Text) case "SexualCharacteristic": - return printSexualCharacteristicPrerequisite( - locale, - prerequisite.SexualCharacteristic, - ) + return printSexualCharacteristicPrerequisite(locale, prerequisite.SexualCharacteristic) default: return assertExhaustive(prerequisite) } @@ -195,33 +155,26 @@ export const printGeneralPrerequisiteGroup = ( */ export const printProfessionPrerequisiteGroup = ( getInstanceById: GetInstanceById< - | "Race" - | "Culture" - | ActivatableIdentifier["kind"] - | RatedIdentifier["kind"] - | "Aspect" + "Race" | "Culture" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "Aspect" >, getResolvedSelectOptionById: GetResolvedSelectOptionById, - locale: LocaleEnvironment, + locale: Pick, prerequisite: ProfessionPrerequisiteGroup, ): PrerequisitePart | undefined => { switch (prerequisite.kind) { case "Sex": - return printBinarySexPrerequisite(locale, prerequisite.Sex) + return printBinarySexPrerequisite(locale.translate, prerequisite.Sex) case "Race": return printRacePrerequisite(getInstanceById, locale, prerequisite.Race) case "Culture": - return printCulturePrerequisite( - getInstanceById, - locale, - prerequisite.Culture, - ) + return printCulturePrerequisite(getInstanceById, locale, prerequisite.Culture) case "Activatable": return printActivatablePrerequisite( getInstanceById, getResolvedSelectOptionById, locale, prerequisite.Activatable, + true, ) case "Rated": return printRatedPrerequisite(getInstanceById, locale, prerequisite.Rated) @@ -296,13 +249,9 @@ export const printArcaneTraditionPrerequisiteGroup = ( ): PrerequisitePart | undefined => { switch (prerequisite.kind) { case "Sex": - return printBinarySexPrerequisite(locale, prerequisite.Sex) + return printBinarySexPrerequisite(locale.translate, prerequisite.Sex) case "Culture": - return printCulturePrerequisite( - getInstanceById, - locale, - prerequisite.Culture, - ) + return printCulturePrerequisite(getInstanceById, locale, prerequisite.Culture) default: return assertExhaustive(prerequisite) } @@ -320,11 +269,7 @@ export const printPersonalityTraitPrerequisiteGroup = ( case "Race": return printRacePrerequisite(getInstanceById, locale, prerequisite.Race) case "Culture": - return printCulturePrerequisite( - getInstanceById, - locale, - prerequisite.Culture, - ) + return printCulturePrerequisite(getInstanceById, locale, prerequisite.Culture) case "PersonalityTrait": return printPersonalityTraitPrerequisite( getInstanceById, @@ -374,12 +319,13 @@ export const printLiturgyPrerequisiteGroup = ( * Print the translation of an influence prerequisite group. */ export const printInfluencePrerequisiteGroup = ( + getInstanceById: GetInstanceById<"Influence">, locale: LocaleEnvironment, prerequisite: InfluencePrerequisiteGroup, ): PrerequisitePart | undefined => { switch (prerequisite.kind) { case "Influence": - return printInfluencePrerequisite(locale, prerequisite.Influence) + return printInfluencePrerequisite(getInstanceById, locale, prerequisite.Influence) case "Text": return printTextPrerequisite(locale, prerequisite.Text) default: @@ -391,9 +337,7 @@ export const printInfluencePrerequisiteGroup = ( * Print the translation of a language prerequisite group. */ export const printLanguagePrerequisiteGroup = ( - getInstanceById: GetInstanceById< - "Race" | ActivatableIdentifier["kind"] | "Aspect" - >, + getInstanceById: GetInstanceById<"Race" | ActivatableIdentifier["kind"] | "Aspect">, getResolvedSelectOptionById: GetResolvedSelectOptionById, locale: LocaleEnvironment, prerequisite: LanguagePrerequisiteGroup, @@ -407,6 +351,7 @@ export const printLanguagePrerequisiteGroup = ( getResolvedSelectOptionById, locale, prerequisite.Activatable, + false, ) case "Text": return printTextPrerequisite(locale, prerequisite.Text) @@ -428,16 +373,13 @@ export const printAnimistPowerPrerequisiteGroup = ( // default: // return assertExhaustive(prerequisite) // } - printAnimistPowerPrerequisite( - getInstanceById, - locale, - prerequisite.AnimistPower, - ) + printAnimistPowerPrerequisite(getInstanceById, locale, prerequisite.AnimistPower) /** * Print the translation of a geode ritual prerequisite group. */ export const printGeodeRitualPrerequisiteGroup = ( + getInstanceById: GetInstanceById<"Influence">, locale: LocaleEnvironment, prerequisite: GeodeRitualPrerequisiteGroup, ): PrerequisitePart | undefined => @@ -446,7 +388,7 @@ export const printGeodeRitualPrerequisiteGroup = ( // default: // return assertExhaustive(prerequisite) // } - printInfluencePrerequisite(locale, prerequisite.Influence) + printInfluencePrerequisite(getInstanceById, locale, prerequisite.Influence) /** * Print the translation of an enhancement prerequisite group. @@ -460,11 +402,7 @@ export const printEnhancementPrerequisiteGroup = ( case "Rated": return printRatedPrerequisite(getInstanceById, locale, prerequisite.Rated) case "Enhancement": - return printEnhancementPrerequisite( - getInstanceById, - locale, - prerequisite.Enhancement, - ) + return printEnhancementPrerequisite(getInstanceById, locale, prerequisite.Enhancement) default: return assertExhaustive(prerequisite) } @@ -480,16 +418,9 @@ export const printPreconditionGroup = ( ): PrerequisitePart | undefined => { switch (prerequisite.kind) { case "Publication": - return printPublicationPrerequisite( - getInstanceById, - locale, - prerequisite.Publication, - ) + return printPublicationPrerequisite(getInstanceById, locale, prerequisite.Publication) case "SexualCharacteristic": - return printSexualCharacteristicPrerequisite( - locale, - prerequisite.SexualCharacteristic, - ) + return printSexualCharacteristicPrerequisite(locale, prerequisite.SexualCharacteristic) default: return assertExhaustive(prerequisite) } diff --git a/src/entities/partial/prerequisites/single/activatable.ts b/src/entities/partial/prerequisites/single/activatable.ts index 7a07e0a..3ae1e91 100644 --- a/src/entities/partial/prerequisites/single/activatable.ts +++ b/src/entities/partial/prerequisites/single/activatable.ts @@ -4,16 +4,14 @@ import type { } from "optolith-database-schema/cache" import type { ActivatableIdentifier, + ActivatableNameBuilderRules, ActivatablePrerequisite, RequirableSelectOptionIdentifier, } from "optolith-database-schema/gen" import { type GetInstanceById } from "../../../../helpers/getTypes.js" import { LocaleEnvironment } from "../../../../helpers/locale.js" -import type { LocaleMap } from "../../../../helpers/translate.js" -import { - getNameComponents, - printActivatableNameChunk, -} from "../../activatableNameChunks.js" +import type { LocaleMap, Translate } from "../../../../helpers/translate.js" +import { getNameComponents } from "../../activatableNameChunks.js" import { printDisplayOption } from "../displayOption.js" import { PrerequisitePart } from "../part.js" @@ -25,31 +23,39 @@ export type GetResolvedSelectOptionById = ( selectOptionId: ResolvedSelectOptionIdentifier, ) => ResolvedSelectOption | undefined -const printActivatableName = ( +/** + * Get the name components of an activatable. + */ +export const printActivatableName = ( getInstanceById: GetInstanceById, - locale: LocaleEnvironment, + translate: Translate, id: ActivatableIdentifier, options: RequirableSelectOptionIdentifier[] | undefined, level: number | undefined, getResolvedSelectOptionById: GetResolvedSelectOptionById, + displayedInProfession: boolean, ) => { - const entry: { translations: LocaleMap<{ name: string }> } | undefined = - getInstanceById(id) + const entry: + | { + nameBuilderRules?: ActivatableNameBuilderRules + translations: LocaleMap<{ name: string }> + } + | undefined = getInstanceById(id) if (entry === undefined) { return undefined } return getNameComponents( - getInstanceById, - locale, + translate, id, options, level, + entry.nameBuilderRules, entry.translations, t => t.name, - selectOptionId => getResolvedSelectOptionById(id, selectOptionId), - false, + getResolvedSelectOptionById, + displayedInProfession, ) } @@ -59,20 +65,22 @@ const printActivatableName = ( export const printActivatablePrerequisite = ( getInstanceById: GetInstanceById, getResolvedSelectOptionById: GetResolvedSelectOptionById, - locale: LocaleEnvironment, + locale: Pick, prerequisite: ActivatablePrerequisite, + displayedInProfession: boolean, ): PrerequisitePart | undefined => { if (prerequisite.display_option !== undefined) { - return printDisplayOption(locale, prerequisite.display_option) + return printDisplayOption(locale.translateMap, prerequisite.display_option) } const nameComponents = printActivatableName( getInstanceById, - locale, + locale.translate, prerequisite.id, prerequisite.options, prerequisite.level, getResolvedSelectOptionById, + displayedInProfession, ) if (nameComponents === undefined) { @@ -81,11 +89,19 @@ export const printActivatablePrerequisite = ( return { label: `${ - prerequisite.active - ? locale.translate("special ability") - : locale.translate("no special ability") + prerequisite.id.kind === "Advantage" + ? prerequisite.active + ? locale.translate("advantage") + : locale.translate("no advantage") + : prerequisite.id.kind === "Disadvantage" + ? prerequisite.active + ? locale.translate("disadvantage") + : locale.translate("no disadvantage") + : prerequisite.active + ? locale.translate("special ability") + : locale.translate("no special ability") } `, - value: printActivatableNameChunk(locale, nameComponents.full), + value: nameComponents, sentenceType: undefined, isMeta: false, } diff --git a/src/entities/partial/prerequisites/single/animistPower.ts b/src/entities/partial/prerequisites/single/animistPower.ts index 697c5e7..bd32ffc 100644 --- a/src/entities/partial/prerequisites/single/animistPower.ts +++ b/src/entities/partial/prerequisites/single/animistPower.ts @@ -15,7 +15,7 @@ export const printAnimistPowerPrerequisite = ( prerequisite: AnimistPowerPrerequisite, ): PrerequisitePart | undefined => { if (prerequisite.display_option !== undefined) { - return printDisplayOption(locale, prerequisite.display_option) + return printDisplayOption(locale.translateMap, prerequisite.display_option) } const animistPower = getInstanceById("AnimistPower", prerequisite.id) @@ -23,9 +23,7 @@ export const printAnimistPowerPrerequisite = ( return { value: [ locale.translateMap(animistPower?.translations)?.name ?? "MISSING_VALUE", - prerequisite.level === undefined - ? undefined - : romanize(prerequisite.level), + prerequisite.level === undefined ? undefined : romanize(prerequisite.level), prerequisite.value.toString(), ] .filter(isNotNullish) diff --git a/src/entities/partial/prerequisites/single/blessedTradition.ts b/src/entities/partial/prerequisites/single/blessedTradition.ts index df013ac..347f6a7 100644 --- a/src/entities/partial/prerequisites/single/blessedTradition.ts +++ b/src/entities/partial/prerequisites/single/blessedTradition.ts @@ -35,7 +35,7 @@ export const printBlessedTraditionPrerequisite = ( prerequisite: BlessedTraditionPrerequisite, ): PrerequisitePart | undefined => { if (prerequisite.display_option !== undefined) { - return printDisplayOption(locale, prerequisite.display_option) + return printDisplayOption(locale.translateMap, prerequisite.display_option) } return { diff --git a/src/entities/partial/prerequisites/single/culture.ts b/src/entities/partial/prerequisites/single/culture.ts index ed33326..4e807c1 100644 --- a/src/entities/partial/prerequisites/single/culture.ts +++ b/src/entities/partial/prerequisites/single/culture.ts @@ -9,11 +9,11 @@ import { PrerequisitePart } from "../part.js" */ export const printCulturePrerequisite = ( getInstanceById: GetInstanceById<"Culture">, - locale: LocaleEnvironment, + locale: Pick, prerequisite: CulturePrerequisite, ): PrerequisitePart | undefined => { if (prerequisite.display_option !== undefined) { - return printDisplayOption(locale, prerequisite.display_option) + return printDisplayOption(locale.translateMap, prerequisite.display_option) } const culture = getInstanceById("Culture", prerequisite.id) diff --git a/src/entities/partial/prerequisites/single/influence.ts b/src/entities/partial/prerequisites/single/influence.ts index 4bc9c5e..4b0177d 100644 --- a/src/entities/partial/prerequisites/single/influence.ts +++ b/src/entities/partial/prerequisites/single/influence.ts @@ -1,4 +1,5 @@ import { InfluencePrerequisite } from "optolith-database-schema/gen" +import type { GetInstanceById } from "../../../../helpers/getTypes.js" import { LocaleEnvironment } from "../../../../helpers/locale.js" import { MISSING_VALUE } from "../../unknown.js" import { printDisplayOption } from "../displayOption.js" @@ -8,17 +9,20 @@ import { PrerequisitePart } from "../part.js" * Get the translation of a culture prerequisite. */ export const printInfluencePrerequisite = ( - locale: LocaleEnvironment, + getInstanceById: GetInstanceById<"Influence">, + locale: Pick, prerequisite: InfluencePrerequisite, ): PrerequisitePart | undefined => { if (prerequisite.display_option !== undefined) { - return printDisplayOption(locale, prerequisite.display_option) + return printDisplayOption(locale.translateMap, prerequisite.display_option) } - // TODO + const name = + locale.translateMap(getInstanceById("Influence", prerequisite.id)?.translations)?.name ?? + MISSING_VALUE return { - value: MISSING_VALUE, + value: `${locale.translate("no influence")} ${name}`, sentenceType: undefined, isMeta: false, } diff --git a/src/entities/partial/prerequisites/single/magicalTradition.ts b/src/entities/partial/prerequisites/single/magicalTradition.ts index d8d7db3..1c53554 100644 --- a/src/entities/partial/prerequisites/single/magicalTradition.ts +++ b/src/entities/partial/prerequisites/single/magicalTradition.ts @@ -31,7 +31,7 @@ export const printMagicalTraditionPrerequisite = ( prerequisite: MagicalTraditionPrerequisite, ): PrerequisitePart | undefined => { if (prerequisite.display_option !== undefined) { - return printDisplayOption(locale, prerequisite.display_option) + return printDisplayOption(locale.translateMap, prerequisite.display_option) } return { diff --git a/src/entities/partial/prerequisites/single/pact.ts b/src/entities/partial/prerequisites/single/pact.ts index 66cc872..acc2197 100644 --- a/src/entities/partial/prerequisites/single/pact.ts +++ b/src/entities/partial/prerequisites/single/pact.ts @@ -16,7 +16,7 @@ export const printPactPrerequisite = ( prerequisite: PactPrerequisite, ): PrerequisitePart | undefined => { if (prerequisite.display_option !== undefined) { - return printDisplayOption(locale, prerequisite.display_option) + return printDisplayOption(locale.translateMap, prerequisite.display_option) } const pactCategory = getInstanceById("PactCategory", prerequisite.category) @@ -28,16 +28,14 @@ export const printPactPrerequisite = ( domain: locale.join( prerequisite.domain.map( id => - locale.translateMap( - getInstanceById("PactDomain", id)?.translations, - )?.name ?? MISSING_VALUE, + locale.translateMap(getInstanceById("PactDomain", id)?.translations)?.name ?? + MISSING_VALUE, ), "disjunction", ), }), locale.translate("{$pact} level {$pactLevel}", { - pact: - locale.translateMap(pactCategory?.translations)?.name ?? MISSING_VALUE, + pact: locale.translateMap(pactCategory?.translations)?.name ?? MISSING_VALUE, pactLevel: romanize(prerequisite.level ?? 1), }), ].filter(isNotNullish) diff --git a/src/entities/partial/prerequisites/single/personalityTrait.ts b/src/entities/partial/prerequisites/single/personalityTrait.ts index 21e0256..291d8e6 100644 --- a/src/entities/partial/prerequisites/single/personalityTrait.ts +++ b/src/entities/partial/prerequisites/single/personalityTrait.ts @@ -13,25 +13,19 @@ export const printPersonalityTraitPrerequisite = ( prerequisite: PersonalityTraitPrerequisite, ): PrerequisitePart | undefined => { if (prerequisite.display_option !== undefined) { - return printDisplayOption(locale, prerequisite.display_option) + return printDisplayOption(locale.translateMap, prerequisite.display_option) } const personalityTrait = getInstanceById("PersonalityTrait", prerequisite.id) - const personalityTraitTranslation = locale.translateMap( - personalityTrait?.translations, - ) + const personalityTraitTranslation = locale.translateMap(personalityTrait?.translations) - if ( - personalityTrait === undefined || - personalityTraitTranslation === undefined - ) { + if (personalityTrait === undefined || personalityTraitTranslation === undefined) { return undefined } - const name = `${personalityTraitTranslation.name} (${locale.translate( - "Level {$level}", - { level: personalityTrait.level }, - )})` + const name = `${personalityTraitTranslation.name} (${locale.translate("Level {$level}", { + level: personalityTrait.level, + })})` return { value: prerequisite.active diff --git a/src/entities/partial/prerequisites/single/primaryAttribute.ts b/src/entities/partial/prerequisites/single/primaryAttribute.ts index 256216f..f26a0fe 100644 --- a/src/entities/partial/prerequisites/single/primaryAttribute.ts +++ b/src/entities/partial/prerequisites/single/primaryAttribute.ts @@ -11,7 +11,7 @@ export const printPrimaryAttributePrerequisite = ( prerequisite: PrimaryAttributePrerequisite, ): PrerequisitePart | undefined => { if (prerequisite.display_option !== undefined) { - return printDisplayOption(locale, prerequisite.display_option) + return printDisplayOption(locale.translateMap, prerequisite.display_option) } return { diff --git a/src/entities/partial/prerequisites/single/publication.ts b/src/entities/partial/prerequisites/single/publication.ts index 28134bf..820b37c 100644 --- a/src/entities/partial/prerequisites/single/publication.ts +++ b/src/entities/partial/prerequisites/single/publication.ts @@ -13,7 +13,7 @@ export const printPublicationPrerequisite = ( prerequisite: PublicationPrerequisite, ): PrerequisitePart | undefined => { if (prerequisite.display_option !== undefined) { - return printDisplayOption(locale, prerequisite.display_option) + return printDisplayOption(locale.translateMap, prerequisite.display_option) } const publication = getInstanceById("Publication", prerequisite.id) diff --git a/src/entities/partial/prerequisites/single/race.ts b/src/entities/partial/prerequisites/single/race.ts index 4ec811d..b4a4b00 100644 --- a/src/entities/partial/prerequisites/single/race.ts +++ b/src/entities/partial/prerequisites/single/race.ts @@ -9,11 +9,11 @@ import { PrerequisitePart } from "../part.js" */ export const printRacePrerequisite = ( getInstanceById: GetInstanceById<"Race">, - locale: LocaleEnvironment, + locale: Pick, prerequisite: RacePrerequisite, ): PrerequisitePart | undefined => { if (prerequisite.display_option !== undefined) { - return printDisplayOption(locale, prerequisite.display_option) + return printDisplayOption(locale.translateMap, prerequisite.display_option) } const race = getInstanceById("Race", prerequisite.id) diff --git a/src/entities/partial/prerequisites/single/rated.ts b/src/entities/partial/prerequisites/single/rated.ts index 3ee2d5f..1549552 100644 --- a/src/entities/partial/prerequisites/single/rated.ts +++ b/src/entities/partial/prerequisites/single/rated.ts @@ -1,10 +1,8 @@ import { assertExhaustive } from "@optolith/helpers/typeSafety" -import type { - RatedIdentifier, - RatedPrerequisite, -} from "optolith-database-schema/gen" +import type { RatedIdentifier, RatedPrerequisite } from "optolith-database-schema/gen" import { type GetInstanceById } from "../../../../helpers/getTypes.js" import { LocaleEnvironment } from "../../../../helpers/locale.js" +import type { TranslateMap } from "../../../../helpers/translate.js" import { MISSING_VALUE } from "../../unknown.js" import { printDisplayOption } from "../displayOption.js" import { PrerequisitePart } from "../part.js" @@ -20,56 +18,40 @@ const printRatedName = ( | "LiturgicalChant" | "Ceremony" >, - locale: LocaleEnvironment, + translateMap: TranslateMap, id: RatedIdentifier, ) => { switch (id.kind) { case "Attribute": return ( - locale.translateMap( - getInstanceById("Attribute", id.Attribute)?.translations, - )?.name ?? MISSING_VALUE + translateMap(getInstanceById("Attribute", id.Attribute)?.translations)?.name ?? + MISSING_VALUE ) case "Skill": - return ( - locale.translateMap(getInstanceById("Skill", id.Skill)?.translations) - ?.name ?? MISSING_VALUE - ) + return translateMap(getInstanceById("Skill", id.Skill)?.translations)?.name ?? MISSING_VALUE case "CloseCombatTechnique": return ( - locale.translateMap( - getInstanceById("CloseCombatTechnique", id.CloseCombatTechnique) - ?.translations, - )?.name ?? MISSING_VALUE + translateMap(getInstanceById("CloseCombatTechnique", id.CloseCombatTechnique)?.translations) + ?.name ?? MISSING_VALUE ) case "RangedCombatTechnique": return ( - locale.translateMap( - getInstanceById("RangedCombatTechnique", id.RangedCombatTechnique) - ?.translations, + translateMap( + getInstanceById("RangedCombatTechnique", id.RangedCombatTechnique)?.translations, )?.name ?? MISSING_VALUE ) case "Spell": - return ( - locale.translateMap(getInstanceById("Spell", id.Spell)?.translations) - ?.name ?? MISSING_VALUE - ) + return translateMap(getInstanceById("Spell", id.Spell)?.translations)?.name ?? MISSING_VALUE case "Ritual": - return ( - locale.translateMap(getInstanceById("Ritual", id.Ritual)?.translations) - ?.name ?? MISSING_VALUE - ) + return translateMap(getInstanceById("Ritual", id.Ritual)?.translations)?.name ?? MISSING_VALUE case "LiturgicalChant": return ( - locale.translateMap( - getInstanceById("LiturgicalChant", id.LiturgicalChant)?.translations, - )?.name ?? MISSING_VALUE + translateMap(getInstanceById("LiturgicalChant", id.LiturgicalChant)?.translations)?.name ?? + MISSING_VALUE ) case "Ceremony": return ( - locale.translateMap( - getInstanceById("Ceremony", id.Ceremony)?.translations, - )?.name ?? MISSING_VALUE + translateMap(getInstanceById("Ceremony", id.Ceremony)?.translations)?.name ?? MISSING_VALUE ) default: return assertExhaustive(id) @@ -90,17 +72,15 @@ export const printRatedPrerequisite = ( | "LiturgicalChant" | "Ceremony" >, - locale: LocaleEnvironment, + locale: Pick, prerequisite: RatedPrerequisite, ): PrerequisitePart | undefined => { if (prerequisite.display_option !== undefined) { - return printDisplayOption(locale, prerequisite.display_option) + return printDisplayOption(locale.translateMap, prerequisite.display_option) } return { - value: `${printRatedName(getInstanceById, locale, prerequisite.id)} ${ - prerequisite.value - }`, + value: `${printRatedName(getInstanceById, locale.translateMap, prerequisite.id)} ${prerequisite.value}`, sentenceType: undefined, isMeta: false, } diff --git a/src/entities/partial/prerequisites/single/ratedMinimumNumber.ts b/src/entities/partial/prerequisites/single/ratedMinimumNumber.ts index 49cabe7..12e0c9a 100644 --- a/src/entities/partial/prerequisites/single/ratedMinimumNumber.ts +++ b/src/entities/partial/prerequisites/single/ratedMinimumNumber.ts @@ -10,40 +10,25 @@ import { MISSING_VALUE } from "../../unknown.js" import { printDisplayOption } from "../displayOption.js" import { PrerequisitePart } from "../part.js" -const printNumberOfTheFollowingSkills = ( - locale: LocaleEnvironment, - number: number, -): string => - locale.translate( - ".input {$count :number} {{{$count} of the following skills}}", - { count: number }, - ) +const printNumberOfTheFollowingSkills = (locale: LocaleEnvironment, number: number): string => + locale.translate(".input {$count :number} {{{$count} of the following skills}}", { + count: number, + }) -const printNumberOfAllCombatTechniques = ( - locale: LocaleEnvironment, - number: number, -): string => +const printNumberOfAllCombatTechniques = (locale: LocaleEnvironment, number: number): string => locale.translate(".input {$count :number} {{{$count} combat techniques}}", { count: number, }) -const printNumberOfCloseCombatTechniques = ( - locale: LocaleEnvironment, - number: number, -): string => - locale.translate( - ".input {$count :number} {{{$count} close combat techniques}}", - { count: number }, - ) +const printNumberOfCloseCombatTechniques = (locale: LocaleEnvironment, number: number): string => + locale.translate(".input {$count :number} {{{$count} close combat techniques}}", { + count: number, + }) -const printNumberOfRangedCombatTechniques = ( - locale: LocaleEnvironment, - number: number, -): string => - locale.translate( - ".input {$count :number} {{{$count} ranged combat techniques}}", - { count: number }, - ) +const printNumberOfRangedCombatTechniques = (locale: LocaleEnvironment, number: number): string => + locale.translate(".input {$count :number} {{{$count} ranged combat techniques}}", { + count: number, + }) const printNumberOfCombatTechniques = ( locale: LocaleEnvironment, @@ -71,17 +56,13 @@ export const printRatedMinimumNumberPrerequisite = ( prerequisite: RatedMinimumNumberPrerequisite, ): PrerequisitePart | undefined => { if (prerequisite.display_option !== undefined) { - return printDisplayOption(locale, prerequisite.display_option) + return printDisplayOption(locale.translateMap, prerequisite.display_option) } switch (prerequisite.targets.kind) { case "Skills": { const skills = prerequisite.targets.Skills.targets - .map( - id => - locale.translateMap(getInstanceById("Skill", id)?.translations) - ?.name, - ) + .map(id => locale.translateMap(getInstanceById("Skill", id)?.translations)?.name) .filter(isNotNullish) return { @@ -124,10 +105,7 @@ export const printRatedMinimumNumberPrerequisite = ( count: prerequisite.number, property: locale.translateMap( - getInstanceById( - "Property", - prerequisite.targets.Spellworks.property, - )?.translations, + getInstanceById("Property", prerequisite.targets.Spellworks.property)?.translations, )?.name ?? MISSING_VALUE, minRating: prerequisite.value, }, @@ -145,8 +123,7 @@ export const printRatedMinimumNumberPrerequisite = ( count: prerequisite.number, aspect: locale.translateMap( - getInstanceById("Aspect", prerequisite.targets.Liturgies.aspect) - ?.translations, + getInstanceById("Aspect", prerequisite.targets.Liturgies.aspect)?.translations, )?.name ?? MISSING_VALUE, minRating: prerequisite.value, }, diff --git a/src/entities/partial/prerequisites/single/ratedSum.ts b/src/entities/partial/prerequisites/single/ratedSum.ts index 67c5a3b..8bc1c62 100644 --- a/src/entities/partial/prerequisites/single/ratedSum.ts +++ b/src/entities/partial/prerequisites/single/ratedSum.ts @@ -14,25 +14,18 @@ export const printRatedSumPrerequisite = ( prerequisite: RatedSumPrerequisite, ): PrerequisitePart | undefined => { if (prerequisite.display_option !== undefined) { - return printDisplayOption(locale, prerequisite.display_option) + return printDisplayOption(locale.translateMap, prerequisite.display_option) } const skills = prerequisite.targets - .map( - skillId => - locale.translateMap(getInstanceById("Skill", skillId)?.translations) - ?.name, - ) + .map(skillId => locale.translateMap(getInstanceById("Skill", skillId)?.translations)?.name) .filter(isNotNullish) return { - value: locale.translate( - "the SR for {$skill} combined must add up to at least {$minRating}", - { - skill: locale.join(skills, "conjunction"), - minRating: prerequisite.sum, - }, - ), + value: locale.translate("the SR for {$skill} combined must add up to at least {$minRating}", { + skill: locale.join(skills, "conjunction"), + minRating: prerequisite.sum, + }), sentenceType: undefined, isMeta: false, } diff --git a/src/entities/partial/prerequisites/single/sex.ts b/src/entities/partial/prerequisites/single/sex.ts index 24a9e62..e35859a 100644 --- a/src/entities/partial/prerequisites/single/sex.ts +++ b/src/entities/partial/prerequisites/single/sex.ts @@ -1,14 +1,14 @@ import { assertExhaustive } from "@optolith/helpers/typeSafety" import type { BinarySex, SexPrerequisite } from "optolith-database-schema/gen" -import { LocaleEnvironment } from "../../../../helpers/locale.js" +import type { Translate } from "../../../../helpers/translate.js" import { PrerequisitePart } from "../part.js" -const printId = (locale: LocaleEnvironment, id: BinarySex): string => { +const printId = (translate: Translate, id: BinarySex): string => { switch (id.kind) { case "Male": - return locale.translate("Male") + return translate("Male") case "Female": - return locale.translate("Female") + return translate("Female") default: return assertExhaustive(id) } @@ -18,10 +18,10 @@ const printId = (locale: LocaleEnvironment, id: BinarySex): string => { * Get the translation of a (binary) sex prerequisite. */ export const printBinarySexPrerequisite = ( - locale: LocaleEnvironment, + translate: Translate, prerequisite: SexPrerequisite, ): PrerequisitePart | undefined => ({ - value: printId(locale, prerequisite.id), + value: printId(translate, prerequisite.id), sentenceType: undefined, isMeta: false, }) diff --git a/src/entities/partial/prerequisites/single/socialStatus.ts b/src/entities/partial/prerequisites/single/socialStatus.ts index 0ca6ff0..97941fc 100644 --- a/src/entities/partial/prerequisites/single/socialStatus.ts +++ b/src/entities/partial/prerequisites/single/socialStatus.ts @@ -13,13 +13,11 @@ export const printSocialStatusPrerequisite = ( prerequisite: SocialStatusPrerequisite, ): PrerequisitePart | undefined => { if (prerequisite.display_option !== undefined) { - return printDisplayOption(locale, prerequisite.display_option) + return printDisplayOption(locale.translateMap, prerequisite.display_option) } const socialStatus = getInstanceById("SocialStatus", prerequisite.id) - const socialStatusTranslation = locale.translateMap( - socialStatus?.translations, - ) + const socialStatusTranslation = locale.translateMap(socialStatus?.translations) if (socialStatusTranslation === undefined) { return undefined diff --git a/src/entities/partial/prerequisites/single/state.ts b/src/entities/partial/prerequisites/single/state.ts index 640dfd5..c4346e8 100644 --- a/src/entities/partial/prerequisites/single/state.ts +++ b/src/entities/partial/prerequisites/single/state.ts @@ -13,7 +13,7 @@ export const printStatePrerequisite = ( prerequisite: StatePrerequisite, ): PrerequisitePart | undefined => { if (prerequisite.display_option !== undefined) { - return printDisplayOption(locale, prerequisite.display_option) + return printDisplayOption(locale.translateMap, prerequisite.display_option) } const state = getInstanceById("State", prerequisite.id) diff --git a/src/entities/partial/professions.ts b/src/entities/partial/professions.ts index 45489b0..9cc28ec 100644 --- a/src/entities/partial/professions.ts +++ b/src/entities/partial/professions.ts @@ -37,9 +37,8 @@ export const getBaseProfessionPackageForCurriculum = ( (_acc: { id: string; content: ProfessionPackage } | undefined, version) => getChildInstancesForInstanceId("ProfessionPackage", version.id).find( professionPackage => - professionPackage.content.experience_level === undefined || professionPackage.content.experience_level === - idMap.ExperienceLevel.Experienced, + idMap.ExperienceLevel.Experienced, ), isNotNullish, undefined, diff --git a/src/entities/partial/rated/activatable/castingTime.ts b/src/entities/partial/rated/activatable/castingTime.ts index b107740..f1fa6ee 100644 --- a/src/entities/partial/rated/activatable/castingTime.ts +++ b/src/entities/partial/rated/activatable/castingTime.ts @@ -15,7 +15,7 @@ import { } from "optolith-database-schema/gen" import { Case } from "../../../../helpers/enums.js" import { - getInstanceByIdR, + getInstanceByIdFnR, modifiableBySpeedR, type StdReader, } from "../../reader.js" @@ -37,7 +37,7 @@ const deriveModifiableCastingTime = ( "s" | "ibi", "SkillModificationLevel" > => - getInstanceByIdR<"SkillModificationLevel">().thenW( + getInstanceByIdFnR<"SkillModificationLevel">().thenW( getInstanceById => mapNullable( getInstanceById("SkillModificationLevel", modificationLevelId), diff --git a/src/entities/partial/rated/activatable/cost.ts b/src/entities/partial/rated/activatable/cost.ts index 809db55..5290984 100644 --- a/src/entities/partial/rated/activatable/cost.ts +++ b/src/entities/partial/rated/activatable/cost.ts @@ -26,7 +26,7 @@ import { additionFormatter } from "../../mathOperation.js" import { formatEnergyFnR, formatEnergyR, - getInstanceByIdR, + getInstanceByIdFnR, modifiableBySpeedR, responsiveLocaleJoinR, responsiveR, @@ -53,7 +53,7 @@ import { const deriveModifiableCost = ( modificationLevelId: SkillModificationLevel_ID, ): StdReader => - getInstanceByIdR<"SkillModificationLevel">().thenW( + getInstanceByIdFnR<"SkillModificationLevel">().thenW( getInstanceById => mapNullable( getInstanceById("SkillModificationLevel", modificationLevelId), diff --git a/src/entities/partial/rated/activatable/range.ts b/src/entities/partial/rated/activatable/range.ts index 8c7bba3..5f0335b 100644 --- a/src/entities/partial/rated/activatable/range.ts +++ b/src/entities/partial/rated/activatable/range.ts @@ -10,7 +10,7 @@ import type { SkillModificationLevel_ID, } from "optolith-database-schema/gen" import { - getInstanceByIdR, + getInstanceByIdFnR, modifiableBySpeedOptionalR, modifiableBySpeedR, translateFnR, @@ -38,7 +38,7 @@ const deriveModifiableRange = ( "s" | "tm" | "ibi", "SkillModificationLevel" > => - getInstanceByIdR<"SkillModificationLevel">().thenW( + getInstanceByIdFnR<"SkillModificationLevel">().thenW( getInstanceById => mapNullable( getInstanceById("SkillModificationLevel", modificationLevelId), diff --git a/src/entities/partial/rated/activatable/targetCategory.ts b/src/entities/partial/rated/activatable/targetCategory.ts index d268398..85f8c58 100644 --- a/src/entities/partial/rated/activatable/targetCategory.ts +++ b/src/entities/partial/rated/activatable/targetCategory.ts @@ -8,7 +8,7 @@ import type { } from "optolith-database-schema/gen" import { type RawDefinitionListEntityDescriptionSectionItem } from "../../../../index.js" import { - getInstanceByIdR, + getInstanceByIdFnR, translateMapR, translateR, type StdReader, @@ -17,7 +17,7 @@ import { MISSING_VALUE } from "../../unknown.js" import { appendInParensIfNotEmpty } from "./parensIf.js" const renderPredefined = (targetCategoryId: TargetCategory_ID) => - getInstanceByIdR<"TargetCategory">() + getInstanceByIdFnR<"TargetCategory">() .thenW( getInstanceById => mapNullable( diff --git a/src/entities/partial/rated/skillCheck.ts b/src/entities/partial/rated/skillCheck.ts index 5dd0bb2..19f27bd 100644 --- a/src/entities/partial/rated/skillCheck.ts +++ b/src/entities/partial/rated/skillCheck.ts @@ -7,7 +7,7 @@ import type { } from "optolith-database-schema/gen" import { type IdMap } from "../../../index.js" import { - getInstanceByIdR, + getInstanceByIdFnR, responsiveR, responsiveThenR, responsiveTranslateR, @@ -50,7 +50,7 @@ const renderSkillCheckPenalty = ( penalty: SkillCheckPenalty, ): StdReader => { const getDerivedCharacteristicTranslation = (id: string) => - getInstanceByIdR<"DerivedCharacteristic">() + getInstanceByIdFnR<"DerivedCharacteristic">() .map(getInstanceById => getInstanceById("DerivedCharacteristic", id)) .thenW(dc => dc === undefined diff --git a/src/entities/partial/reader.ts b/src/entities/partial/reader.ts index 579bf6e..f5c2276 100644 --- a/src/entities/partial/reader.ts +++ b/src/entities/partial/reader.ts @@ -2,6 +2,7 @@ import { isNotNullish } from "@elyukai/utils/nullable" import { Reader } from "@elyukai/utils/reader" import { assertExhaustive } from "@elyukai/utils/typeSafety" import type { + ChildEntityMap, EntityMap, FastSkillModificationLevelConfig, ResponsiveText, @@ -9,13 +10,15 @@ import type { SkillModificationLevel, SlowSkillModificationLevelConfig, } from "optolith-database-schema/gen" -import type { GetInstanceById } from "../../helpers/getTypes.js" +import type { IdArgsVariant } from "tsondb/schema/gen" import type { - LocaleCompare, - LocaleJoin, - LocaleJoinType, -} from "../../helpers/locale.js" + GetAllChildInstancesForParent, + GetAllInstances, + GetInstanceById, +} from "../../helpers/getTypes.js" +import type { LocaleCompare, LocaleJoin, LocaleJoinType } from "../../helpers/locale.js" import type { + Format, LocaleMap, Translate, TranslateMap, @@ -24,15 +27,22 @@ import type { TranslationParamsInArray, Translations, } from "../../helpers/translate.js" +import type { GetResolvedSelectOptionById } from "./prerequisites/single/activatable.js" import type { ModifiableParameter } from "./rated/activatable/nonModifiableSuffix.js" import { Speed } from "./rated/activatable/speed.js" import { responsive, ResponsiveTextSize } from "./responsiveText.js" import { formatEnergy, type EnergyUnit } from "./units/energy.js" +import { MISSING_VALUE } from "./unknown.js" /** * The standard set of environment properties for readers in this project. */ -export type EnvMap = { +export type EnvMap< + E extends keyof EntityMap = never, + AE extends keyof EntityMap = never, + CE extends keyof ChildEntityMap = never, +> = { + format: Format translate: Translate translateMap: TranslateMap localeJoin: LocaleJoin @@ -40,16 +50,18 @@ export type EnvMap = { responsiveTextSize: ResponsiveTextSize speed: Speed energyUnit: EnergyUnit - nonModifiableSuffix?: ( - param: ModifiableParameter, - ) => TranslationKeysWithoutParams + nonModifiableSuffix?: (param: ModifiableParameter) => TranslationKeysWithoutParams getInstanceById: GetInstanceById + getAllInstances: GetAllInstances + getChildInstancesForInstanceId: GetAllChildInstancesForParent + getResolvedSelectOptionById: GetResolvedSelectOptionById } /** * Shortcuts for selecting keys of the shared environment map type. */ export type EnvMapAbbr = { + f: "format" t: "translate" tm: "translateMap" lj: "localeJoin" @@ -59,6 +71,9 @@ export type EnvMapAbbr = { eu: "energyUnit" nms: "nonModifiableSuffix" ibi: "getInstanceById" + ai: "getAllInstances" + acibp: "getChildInstancesForInstanceId" + rso: "getResolvedSelectOptionById" } /** @@ -67,9 +82,11 @@ export type EnvMapAbbr = { * The keys are abbreviated to keep type annotations short. */ export type StdEnv< - in K extends keyof EnvMapAbbr = keyof EnvMapAbbr, - in E extends keyof EntityMap = never, -> = Pick, EnvMapAbbr[K]> + K extends keyof EnvMapAbbr = keyof EnvMapAbbr, + E extends keyof EntityMap = never, + AE extends keyof EntityMap = never, + CE extends keyof ChildEntityMap = never, +> = Pick, EnvMapAbbr[K]> /** * Shortcut for a reader with common environment properties. @@ -80,24 +97,34 @@ export type StdReader< T, K extends keyof EnvMapAbbr, E extends keyof EntityMap = never, -> = Reader, T> + AE extends keyof EntityMap = never, + CE extends keyof ChildEntityMap = never, +> = Reader, T> // Specialized constructors for common contexts +/** + * Treats the text as a translation string and applies supplied arguments. + */ +export const formatR = ( + text: string, + args?: Record | undefined, +): Reader<{ format: Format }, string> => Reader.asks(env => env.format(text, args)) + /** * Creates a value from a translation key. */ export const translateR = ( key: K, ...rest: TranslationParamsInArray -): Reader<{ translate: Translate }, string> => - Reader.asks(env => env.translate(key, ...rest)) +): Reader<{ translate: Translate }, string> => Reader.asks(env => env.translate(key, ...rest)) /** * Returns the `translate` function from the context. */ -export const translateFnR: Reader<{ translate: Translate }, Translate> = - Reader.asks(env => env.translate) +export const translateFnR: Reader<{ translate: Translate }, Translate> = Reader.asks( + env => env.translate, +) /** * Takes the appropriate translation from a locale map. @@ -110,10 +137,80 @@ export const translateMapR = ( /** * Returns the `translateMap` function from the context. */ -export const translateMapFnR: Reader< - { translateMap: TranslateMap }, - TranslateMap -> = Reader.asks(env => env.translateMap) +export const translateMapFnR: Reader<{ translateMap: TranslateMap }, TranslateMap> = Reader.asks( + env => env.translateMap, +) + +/** + * Retrieves the translation for the current locale from the given value’s `translations` property. + * @returns `undefined` if the value does not exist or does not have a translation for the current locale, otherwise the translation. + */ +export const translationR = ( + value: { translations?: LocaleMap } | undefined, +): Reader<{ translateMap: TranslateMap }, T | undefined> => + Reader.asks(env => env.translateMap(value?.translations)) + +/** + * Retrieves a specific property of the specified entry. + */ +export const mapTranslationR = < + E extends keyof EntityMap & + { + [K in keyof EntityMap]: EntityMap[K] extends { translations: LocaleMap } ? K : never + }[keyof EntityMap], + R, +>( + ...args: [ + ...IdArgsVariant, + fn: ( + translation: EntityMap[E] extends { translations: LocaleMap } + ? T | undefined + : never, + ) => R, + ] +): Reader<{ translateMap: TranslateMap; getInstanceById: GetInstanceById }, R | undefined> => + Reader.asks(env => { + const idArgs = args.length === 3 ? ([args[0], args[1]] as const) : ([args[0]] as const) + const fn = args.length === 3 ? args[2] : args[1] + const translation = env.translateMap(env.getInstanceById(...idArgs)?.translations) + return fn( + translation as EntityMap[E] extends { translations: LocaleMap } + ? T | undefined + : never, + ) + }) + +/** + * Retrieves the `name` property of the specified entry. + */ +export const nameR = < + E extends { + [K in keyof EntityMap]: EntityMap[K] extends { translations: LocaleMap<{ name: string }> } + ? K + : never + }[keyof EntityMap], +>( + ...args: IdArgsVariant +): Reader< + { translateMap: TranslateMap; getInstanceById: GetInstanceById }, + string | undefined +> => Reader.asks(env => env.translateMap(env.getInstanceById(...args)?.translations)?.name) + +/** + * Retrieves the `name` property of the specified entry. + * + * Returns {@link MISSING_VALUE} if there is no name for the entry in the locale. This can happen if the entry itself does not exist, or it does not have a translation for the current locale. + */ +export const strictNameR = < + E extends { + [K in keyof EntityMap]: EntityMap[K] extends { translations: LocaleMap<{ name: string }> } + ? K + : never + }[keyof EntityMap], +>( + ...args: IdArgsVariant +): Reader<{ translateMap: TranslateMap; getInstanceById: GetInstanceById }, string> => + nameR(...args).map(name => name ?? MISSING_VALUE) /** * Joins a list of strings according to the locale’s rules for the given type. @@ -121,27 +218,50 @@ export const translateMapFnR: Reader< export const localeJoinR = ( arr: string[], type: LocaleJoinType, -): Reader<{ localeJoin: LocaleJoin }, string> => - Reader.asks(env => env.localeJoin(arr, type)) +): Reader<{ localeJoin: LocaleJoin }, string> => Reader.asks(env => env.localeJoin(arr, type)) /** * Returns a function to retrieve an instance from the database by its entity name and ID. */ -export const getInstanceByIdR = (): Reader< +export const getInstanceByIdFnR = (): Reader< { getInstanceById: GetInstanceById }, GetInstanceById > => Reader.asks(env => env.getInstanceById) +/** + * Retrieves an instance from the database by its entity name and ID. + */ +export const getInstanceByIdR = ( + ...args: IdArgsVariant +): Reader<{ getInstanceById: GetInstanceById }, EntityMap[E] | undefined> => + Reader.asks(env => env.getInstanceById(...args)) + +/** + * Retrieves all instances of an entity from the database by their entity name. + */ +export const getAllInstancesR = ( + entityName: E, +): Reader<{ getAllInstances: GetAllInstances }, { id: string; content: EntityMap[E] }[]> => + Reader.asks(env => env.getAllInstances(entityName)) + +/** + * Retrieves all child instances of an entity from the database by their child entity name and their parent’s identifier. + */ +export const getChildInstancesForInstanceIdR = ( + entityName: CE, + parentId: ChildEntityMap[CE][2], +): Reader< + { getChildInstancesForInstanceId: GetAllChildInstancesForParent }, + { id: string; content: ChildEntityMap[CE][0] }[] +> => Reader.asks(env => env.getChildInstancesForInstanceId(entityName, parentId)) + /** * Joins a list of strings according to the locale’s rules for the given type. */ export const responsiveLocaleJoinR = ( arr: string[], type: LocaleJoinType, -): Reader< - { localeJoin: LocaleJoin; responsiveTextSize: ResponsiveTextSize }, - string -> => +): Reader<{ localeJoin: LocaleJoin; responsiveTextSize: ResponsiveTextSize }, string> => Reader.asks(({ localeJoin, responsiveTextSize }) => responsive( responsiveTextSize, @@ -164,10 +284,17 @@ export const responsiveLocaleJoinR = ( /** * Compares two strings according to the locale’s sorting rules. */ -export const localeCompareR: Reader< - { localeCompare: LocaleCompare }, - LocaleCompare -> = Reader.asks(env => env.localeCompare) +export const localeCompareR: Reader<{ localeCompare: LocaleCompare }, LocaleCompare> = Reader.asks( + env => env.localeCompare, +) + +/** + * Sorts an array of strings according to the locale’s sorting rules. + */ +export const localeSortR = ( + arr: T[], +): Reader<{ localeCompare: LocaleCompare }, T[]> => + Reader.asks(({ localeCompare }) => arr.toSorted(localeCompare)) /** * Creates a responsive value from two functions that return the value for the full and compressed version, respectively. @@ -176,9 +303,7 @@ export const responsiveR = ( full: () => T, compressed: () => T, ): Reader<{ responsiveTextSize: ResponsiveTextSize }, T> => - Reader.asks(({ responsiveTextSize }) => - responsive(responsiveTextSize, full, compressed), - ) + Reader.asks(({ responsiveTextSize }) => responsive(responsiveTextSize, full, compressed)) /** * Creates a responsive value from two functions that return the value for the full and compressed version, respectively. @@ -208,10 +333,7 @@ export const responsiveTranslateR = < fullKey: K, compressedKey: K2, ...rest: TranslationParamsInArray & TranslationParamsInArray -): Reader< - { translate: Translate; responsiveTextSize: ResponsiveTextSize }, - string -> => +): Reader<{ translate: Translate; responsiveTextSize: ResponsiveTextSize }, string> => Reader.asks(({ translate, responsiveTextSize }) => responsive( responsiveTextSize, @@ -254,9 +376,7 @@ export const responsiveTextOptionalR = ( export const formatEnergyR = ( value: string | number, ): Reader<{ translate: Translate; energyUnit: EnergyUnit }, string> => - Reader.asks(({ translate, energyUnit }) => - formatEnergy(translate, energyUnit, value), - ) + Reader.asks(({ translate, energyUnit }) => formatEnergy(translate, energyUnit, value)) /** * Formats the given energy cost value with the appropriate unit based on the entity type. @@ -330,19 +450,12 @@ export const modifyBySpeedR: Reader< ) => SpeedMap[S][K] > = Reader.asks( ({ speed }) => - ( - key: K, - level: SkillModificationLevel, - ) => { + (key: K, level: SkillModificationLevel) => { switch (speed) { case Speed.Fast: - return level.fast[ - key as keyof FastSkillModificationLevelConfig - ] as SpeedMap[S][K] + return level.fast[key as keyof FastSkillModificationLevelConfig] as SpeedMap[S][K] case Speed.Slow: - return level.slow[ - key as keyof SlowSkillModificationLevelConfig - ] as SpeedMap[S][K] + return level.slow[key as keyof SlowSkillModificationLevelConfig] as SpeedMap[S][K] default: return assertExhaustive(speed) } diff --git a/src/entities/profession.ts b/src/entities/profession.ts new file mode 100644 index 0000000..5bade84 --- /dev/null +++ b/src/entities/profession.ts @@ -0,0 +1,1321 @@ +import { allSame } from "@elyukai/utils/array/filters" +import { + ensureNonEmpty, + isEmpty, + isNotEmpty, + type NonEmptyArray, +} from "@elyukai/utils/array/nonEmpty" +import { deepEqual, equal, type Equality } from "@elyukai/utils/equality" +import { on } from "@elyukai/utils/function" +import { + isNotNullish, + isNullish, + mapNullable, + nullableToArray, + type AnyNonNullish, +} from "@elyukai/utils/nullable" +import { compareNumber } from "@elyukai/utils/ordering" +import { Reader } from "@elyukai/utils/reader" +import { assertExhaustive } from "@elyukai/utils/typeSafety" +import { + CombatTechniqueIdentifier, + type ActivatableIdentifier, + type ActivatableNameBuilderRules, + type BlessedTradition_ID, + type Blessing_ID, + type Cantrip_ID, + type CantripsOptions, + type CombatTechniquesOptions, + type ConstantProfessionSpecialAbility, + type CursesOptions, + type ExperienceLevel, + type LanguagesScriptsOptions, + type LiturgiesOptions, + type LiturgyIdentifier, + type MagicalActionIdentifier, + type Profession_ID, + type ProfessionMagicalSkillIdentifier, + type ProfessionPackage, + type ProfessionPackageOptions, + type ProfessionPrerequisiteGroup, + type ProfessionPrerequisites, + type ProfessionSpecialAbility, + type ProfessionVariant, + type ProfessionVariantPackageOptions, + type ProfessionVariantTranslation, + type RatedIdentifier, + type RestrictedBlessings, + type SkillsOptions, + type SkillSpecializationOptions, + type SpecialAbilityIdentifier, + type VariantOptionAction, +} from "optolith-database-schema/gen" +import { createEntityDescriptionCreator } from "../creator.js" +import type { + GetAllChildInstancesForParent, + GetAllInstances, + GetInstanceById, +} from "../helpers/getTypes.js" +import type { LocaleMap, Translate } from "../helpers/translate.js" +import type { + RawDefinitionListEntityDescriptionSectionItem, + RawEntityDescription, + RawNestedDefinitionListEntityDescriptionSection, +} from "../index.js" +import { + combineNameComponents, + getNameComponents, + renderActivatableNameComponents, + renderCombinedActivatableNameComponents, +} from "./partial/activatableNameChunks.js" +import { + renderCommonnessRatedAdvantagesOrDisadvantages, + renderValueWithPossibleTranslation, +} from "./partial/commonnessRatedAdvantagesAndDisadvantages.js" +import { printProfessionPrerequisites } from "./partial/prerequisites/index.js" +import { type GetResolvedSelectOptionById } from "./partial/prerequisites/single/activatable.js" +import { parensIf } from "./partial/rated/activatable/parensIf.js" +import { + formatR, + getChildInstancesForInstanceIdR, + getInstanceByIdR, + localeCompareR, + localeJoinR, + localeSortR, + nameR, + strictNameR, + translateR, + translationR, + type EnvMap, + type StdEnv, + type StdReader, +} from "./partial/reader.js" +import { MISSING_VALUE, UNHANDLED_VALUE } from "./partial/unknown.js" + +type PreparedProfessionPackage = { + id: string + content: ProfessionPackage + experienceLevel: ExperienceLevel +} + +const prepareProfessionPackages = ( + professionId: Profession_ID, +): StdReader< + PreparedProfessionPackage[], + "ibi" | "acibp", + "ExperienceLevel", + never, + "ProfessionPackage" +> => + Reader.asks(({ getInstanceById, getChildInstancesForInstanceId }) => + getChildInstancesForInstanceId("ProfessionPackage", professionId) + .map( + ( + professionPackage, + ): { + id: string + content: ProfessionPackage + experienceLevel?: ExperienceLevel + } => ({ + ...professionPackage, + experienceLevel: getInstanceById( + "ExperienceLevel", + professionPackage.content.experience_level, + ), + }), + ) + .filter( + (professionPackage): professionPackage is Required => + professionPackage.experienceLevel !== undefined, + ) + .toSorted(on(item => item.experienceLevel.adventure_points, compareNumber)), + ) + +const renderNumericListAcrossPackages = ( + packages: PreparedProfessionPackage[], + selector: (pkg: PreparedProfessionPackage) => Reader, + equalityFn: Equality, + renderText: (value: T) => Reader, + defaultValue: number, +): Reader & SelectorEnv & RenderEnv, string | undefined> => + packages + .reduce>( + (accR, pkg, pkgIndex): Reader => + accR.map2(selector(pkg), (acc, selected) => + selected.reduce<[T, number[]][]>( + (filledAcc, [values, number]) => { + const existing = filledAcc.findIndex(([value]) => equalityFn(value, values)) + if (existing >= 0) { + filledAcc[existing]![1][pkgIndex] = number + defaultValue + return filledAcc + } else { + return [ + ...filledAcc, + [ + values, + Array(pkgIndex) + .fill(defaultValue) + .concat(number + defaultValue), + ], + ] + } + }, + acc.map(([value, numbers]): [T, number[]] => [value, [...numbers, defaultValue]]), + ), + ), + Reader.of([]), + ) + .thenW(list => + Reader.traverse(list, ([value, numbers]) => + renderText(value).map(text => `${text} ${numbers.join("/")}`), + ), + ) + .thenW(list => localeSortR(list)) + .map(list => ensureNonEmpty(list)?.join(", ")) + +const renderSinglePackageValue = < + T, + SelectorEnv, + RenderEnv, + R extends string | string[] | undefined, +>( + professionPackages: NonEmptyArray, + selector: (pkg: PreparedProfessionPackage) => Reader, + equalityFn: Equality, + renderText: (value: NonEmptyArray) => Reader, +): Reader => + (Reader.traverse(professionPackages, selector) as Reader>).thenW( + selected => (allSame(selected, equalityFn) ? renderText([selected[0]]) : renderText(selected)), + ) + +type SpecialAbilities = { + languagesScripts: LanguagesScriptsOptions | undefined + skillSpecialization: SkillSpecializationOptions | undefined + curses: CursesOptions | undefined + list: ProfessionSpecialAbility[] | undefined +} + +const insteadOfR = (replacement: string | number, base: string | number) => + translateR("{$replacement} instead of {$base}", { replacement, base }) + +/** + * If the selected values of the base and the variant are equal, only renders the value once. Otherwise, renders both values as the variant value replacing the base value. + */ +const plainOrInsteadOfR = ( + base: T, + variant: T, + selector: (value: T) => U, + equality: Equality, + render: (value: U) => Reader, +) => { + const baseValue = selector(base) + const variantValue = selector(variant) + return equality(baseValue, variantValue) + ? render(baseValue) + : render(variantValue).thenW(replacement => + render(baseValue).thenW(baseRendered => insteadOfR(replacement, baseRendered)), + ) +} + +const renderVariantOptionPaths = ( + base: T | undefined, + variant: VariantOptionAction, + paths: { + remove: (base: T) => Reader + add: (variant: T) => Reader + update: (base: T, variant: T) => Reader + }, +): Reader => { + switch (variant.kind) { + case "Remove": + return base === undefined ? Reader.of(MISSING_VALUE) : paths.remove(base) + case "Override": + return base === undefined ? paths.add(variant.Override) : paths.update(base, variant.Override) + default: + return assertExhaustive(variant) + } +} + +const renderVariantLanguagesScriptsOption = ( + baseOptions: LanguagesScriptsOptions | undefined, + variantOptions: VariantOptionAction, +) => + renderVariantOptionPaths(baseOptions, variantOptions, { + remove: base => + translateR("no Languages and Literacy totaling {$apValue} AP", { apValue: base.ap_value }), + add: override => + translateR("Languages and Literacy totaling {$apValue} AP", { apValue: override.ap_value }), + update: (base, override) => + translateR("{$replacement} instead of {$base}", { + replacement: override.ap_value, + base: base.ap_value, + }).then(apValue => + translateR("Languages and Literacy totaling {$apValue} AP", { + apValue, + }), + ), + }) + +const renderLanguagesScriptsOption = ( + option: LanguagesScriptsOptions, +): Reader, string> => + renderVariantLanguagesScriptsOption(undefined, { kind: "Override", Override: option }) + +const renderSkillSpecializationOption = ( + option: SkillSpecializationOptions, +): Reader, string> => + Reader.asks(({ format, translate, translateMap, localeJoin, getInstanceById }) => { + switch (option.kind) { + case "Specific": + return translate("Skill Specialization {$possibleSkills}", { + possibleSkills: localeJoin( + option.Specific.options.map( + id => translateMap(getInstanceById("Skill", id)?.translations)?.name ?? MISSING_VALUE, + ), + "disjunction", + ), + }) + case "Group": + return translate("Skill Specialization for a {$skillOfGroup}", { + skillOfGroup: format( + translateMap(getInstanceById("SkillGroup", option.Group)?.translations)?.longName ?? + MISSING_VALUE, + { hiddenCount: 1 }, + ), + }) + default: + return assertExhaustive(option) + } + }) + +const renderCombatTechniquesOption = (option: CombatTechniquesOptions) => + isNotEmpty(option.fixed) + ? (() => { + const [first, ...others] = option.fixed + + const firstTextR = translateR( + ".input {$count :number} {{{$count} of the following combat techniques {$rating}}}", + { count: first.number, rating: first.rating_modifier + 6 }, + ) + + const fixedTextR = others.reduce( + (accTextR, other) => + accTextR.then(accText => + translateR(".input {$count :number} {{{$previous}, {$count} others {$rating}}}", { + count: other.number, + previous: accText, + rating: other.rating_modifier + 6, + }), + ), + firstTextR, + ) + + const completeTextR = fixedTextR.then(fixedText => + option.rest_rating_modifier === undefined + ? Reader.of(fixedText) + : translateR("{$previous}, all others {$rating}", { + previous: fixedText, + rating: option.rest_rating_modifier + 6, + }), + ) + + const listR = Reader.traverse(option.options, strictNameR) + .thenW(list => localeSortR(list)) + .thenW(list => localeJoinR(list, "disjunction")) + + return completeTextR.thenW(completeText => listR.map(list => `${completeText}: ${list}`)) + })() + : Reader.of(MISSING_VALUE) + +const getTotalingAPValues = ( + professionPackages: NonEmptyArray, + selector: (professionPackage: PreparedProfessionPackage) => { ap_value: number } | undefined, +): string | number | undefined => { + const options = professionPackages.map(selector) + + if (!isNotEmpty(options) || options.every(isNullish)) { + return undefined + } + + return allSame(options, deepEqual) + ? options[0]!.ap_value + : options.map(option => option?.ap_value ?? 0).join("/") +} + +const renderSkillsOption = ( + professionPackages: NonEmptyArray, + skillGroup: { id: string; nameOfSkillsOfGroup: string }, +): StdReader => + mapNullable( + getTotalingAPValues(professionPackages, pkg => + pkg.content.options?.skills?.group === skillGroup.id + ? pkg.content.options?.skills + : undefined, + ), + apValue => + translateR("{$apValue} AP to improve other {$skillsOfGroup}", { + apValue, + skillsOfGroup: skillGroup.nameOfSkillsOfGroup, + }), + ) ?? Reader.of(undefined) + +const renderCantripList = ( + cantripIds: Cantrip_ID[], +): StdReader => + Reader.traverse(cantripIds, id => strictNameR("Cantrip", id)) + .thenW(localeSortR) + .thenW(list => localeJoinR(list, "disjunction")) + +const renderVariantCantripsOption = ( + baseOptions: CantripsOptions | undefined, + variantOptions: VariantOptionAction, +): StdReader => + renderVariantOptionPaths(baseOptions, variantOptions, { + remove: base => + renderCantripList(base.options).thenW(list => + translateR(".input {$count :number} {{{$count} cantrips}}", { count: 0 }) + .then(count => translateR("{$count} from the following list", { count })) + .map(text => `${text}: ${list}`), + ), + add: override => + renderCantripList(override.options).thenW(list => + translateR(".input {$count :number} {{{$count} cantrips}}", { count: override.number }) + .then(count => translateR("{$count} from the following list", { count })) + .map(text => `${text}: ${list}`), + ), + update: (base, override) => + deepEqual(base.options, override.options) + ? renderCantripList(override.options).thenW(list => + translateR(".input {$count :number} {{{$count} cantrips}}", { + count: override.number, + }).then(baseCount => + translateR(".input {$count :number} {{{$count} cantrips}}", { + count: override.number, + }) + .then(replacementCount => + translateR("{$replacement} instead of {$base}", { + replacement: replacementCount, + base: baseCount, + }).then(count => translateR("{$count} from the following list", { count })), + ) + .map(text => `${text}: ${list}`), + ), + ) + : renderCantripList(override.options) + .thenW(list => + translateR(".input {$count :number} {{{$count} cantrips}}", { + count: override.number, + }) + .then(count => translateR("{$count} from the following list", { count })) + .map(text => `${text}: ${list}`), + ) + .then(replacementText => + renderCantripList(base.options) + .thenW(list => + translateR(".input {$count :number} {{{$count} cantrips}}", { + count: base.number, + }) + .then(count => translateR("{$count} from the following list", { count })) + .map(text => `${text}: ${list}`), + ) + .then(baseText => + translateR("{$replacement} instead of {$base}", { + replacement: replacementText, + base: baseText, + }), + ), + ), + }) + +const renderSingleCantripsOption = ( + option: CantripsOptions, +): StdReader => + renderVariantCantripsOption(undefined, { kind: "Override", Override: option }) + +const renderCantripsOption = ( + professionPackages: NonEmptyArray, +): StdReader => { + const cantripsOptions = professionPackages.map(pkg => pkg.content.options?.cantrips) + + if (!isNotEmpty(cantripsOptions) || cantripsOptions.every(isNullish)) { + return Reader.of(undefined) + } + + if (allSame(cantripsOptions, deepEqual)) { + return renderSingleCantripsOption(cantripsOptions[0]!) + } else { + return Reader.traverse(cantripsOptions, option => + option === undefined ? Reader.of("—") : renderSingleCantripsOption(option), + ).map(list => list.join(" / ")) + } +} + +const renderLiturgiesOption = ( + professionPackages: NonEmptyArray, +): StdReader => + mapNullable( + getTotalingAPValues(professionPackages, pkg => pkg.content.options?.liturgies), + apValue => + translateR("Liturgies totaling {$apValue} AP", { + apValue, + }), + ) ?? Reader.of(undefined) + +const renderVariantSkillSpecializationOption = ( + baseOptions: SkillSpecializationOptions | undefined, + variantOptions: VariantOptionAction, +): StdReader => + renderVariantOptionPaths(baseOptions, variantOptions, { + remove: ( + base, + ): StdReader => { + switch (base.kind) { + case "Specific": + return Reader.traverse(base.Specific.options, id => strictNameR("Skill", id)) + .thenW(localeSortR) + .thenW(list => localeJoinR(list, "disjunction")) + .thenW(skillsText => + translateR("no Skill Specialization {$possibleSkills}", { + possibleSkills: skillsText, + }), + ) + case "Group": + return getInstanceByIdR("SkillGroup", base.Group) + .thenW(translationR) + .map(t => t?.longName ?? MISSING_VALUE) + .thenW(longName => formatR(longName, { hiddenCount: 1 })) + .thenW(skillOfGroup => + translateR("no Skill Specialization for a {$skillOfGroup}", { + skillOfGroup, + }), + ) + default: + return assertExhaustive(base) + } + }, + add: override => renderSkillSpecializationOption(override), + update: (base, override) => { + if (base.kind === "Specific" && override.kind === "Specific") { + const overrideSkills = Reader.traverse(override.Specific.options, id => + strictNameR("Skill", id), + ).thenW(list => localeJoinR(list, "disjunction")) + + const baseSkills = Reader.traverse(base.Specific.options, id => + strictNameR("Skill", id), + ).thenW(list => localeJoinR(list, "disjunction")) + + return overrideSkills + .thenW(overrideSkillsText => + baseSkills.thenW(baseSkillsText => insteadOfR(overrideSkillsText, baseSkillsText)), + ) + .then(skillsText => + translateR("Skill Specialization {$possibleSkills}", { + possibleSkills: skillsText, + }), + ) + } else { + return renderSkillSpecializationOption(override).then(overrideText => + renderSkillSpecializationOption(base).then(baseText => + insteadOfR(overrideText, baseText), + ), + ) + } + }, + }) + +const renderVariantCombatTechniquesOption = ( + baseOptions: CombatTechniquesOptions | undefined, + variantOptions: VariantOptionAction, +): StdReader< + string, + "t" | "tm" | "lc" | "lj" | "ibi", + "CloseCombatTechnique" | "RangedCombatTechnique" +> => + renderVariantOptionPaths(baseOptions, variantOptions, { + remove: base => + renderCombatTechniquesOption({ + ...base, + fixed: base.fixed.map(option => ({ ...option, number: 0 })), + }), + add: override => renderCombatTechniquesOption(override), + update: (base, override) => + renderCombatTechniquesOption(override).then(overrideText => + renderCombatTechniquesOption(base).then(baseText => insteadOfR(overrideText, baseText)), + ), + }) + +const renderVariantCursesOption = ( + baseOptions: CursesOptions | undefined, + variantOptions: VariantOptionAction, +): StdReader => + renderVariantOptionPaths(baseOptions, variantOptions, { + remove: base => translateR("no Curses totaling {$apValue} AP", { apValue: base.ap_value }), + add: override => translateR("Curses totaling {$apValue} AP", { apValue: override.ap_value }), + update: (base, override) => + insteadOfR(override.ap_value, base.ap_value).then(apValue => + translateR("Curses totaling {$apValue} AP", { + apValue, + }), + ), + }) + +const renderCursesOption = (option: CursesOptions) => + renderVariantCursesOption(undefined, { kind: "Override", Override: option }) + +const nameOfSkillsOfGroupR = (skillGroupId: string) => + getInstanceByIdR("SkillGroup", skillGroupId) + .thenW(translationR) + .map(t => t?.longName ?? MISSING_VALUE) + +const renderVariantSkillsOption = ( + baseOptions: SkillsOptions | undefined, + variantOptions: VariantOptionAction, +): StdReader => + renderVariantOptionPaths(baseOptions, variantOptions, { + remove: base => + nameOfSkillsOfGroupR(base.group).thenW(nameOfSkillsOfGroup => + translateR("no AP to improve other {$skillsOfGroup}", { + skillsOfGroup: nameOfSkillsOfGroup, + }), + ), + add: override => + nameOfSkillsOfGroupR(override.group).thenW(nameOfSkillsOfGroup => + translateR("{$apValue} AP to improve other {$skillsOfGroup}", { + apValue: override.ap_value, + skillsOfGroup: nameOfSkillsOfGroup, + }), + ), + update: (base, override) => + plainOrInsteadOfR(base, override, b => b.ap_value, equal, Reader.of).thenW(apValue => + plainOrInsteadOfR(base, override, b => b.group, equal, nameOfSkillsOfGroupR).thenW( + nameOfSkillsOfGroup => + translateR("{$apValue} AP to improve other {$skillsOfGroup}", { + apValue, + skillsOfGroup: nameOfSkillsOfGroup, + }), + ), + ), + }) + +const renderVariantLiturgiesOption = ( + baseOptions: LiturgiesOptions | undefined, + variantOptions: VariantOptionAction, +): StdReader => + renderVariantOptionPaths(baseOptions, variantOptions, { + remove: base => translateR("no Liturgies totaling {$apValue} AP", { apValue: base.ap_value }), + add: override => translateR("Liturgies totaling {$apValue} AP", { apValue: override.ap_value }), + update: (base, override) => + insteadOfR(override.ap_value, base.ap_value).then(apValue => + translateR("Liturgies totaling {$apValue} AP", { + apValue, + }), + ), + }) + +const getSpecialAbilityNameComponents = ( + getInstanceById: GetInstanceById, + getResolvedSelectOptionById: GetResolvedSelectOptionById, + translate: Translate, + option: ConstantProfessionSpecialAbility, +) => { + const instance: + | { + nameBuilderRules?: ActivatableNameBuilderRules + translations: LocaleMap<{ name: string }> + } + | undefined = getInstanceById(option.id) + + if (instance === undefined) { + return undefined + } + + return getNameComponents( + translate, + option.id, + option.options, + option.level, + "nameBuilderRules" in instance ? instance.nameBuilderRules : undefined, + instance.translations, + t => t.name, + getResolvedSelectOptionById, + true, + ) +} + +const renderSpecialAbilityName = (specialAbility: ProfessionSpecialAbility) => + Reader.asks( + ({ + translate, + translateMap, + localeJoin, + getInstanceById, + getResolvedSelectOptionById, + }: StdEnv<"t" | "tm" | "lc" | "lj" | "ibi" | "rso", SpecialAbilityIdentifier["kind"]>) => { + switch (specialAbility.kind) { + case "Constant": { + const chunk = getSpecialAbilityNameComponents( + getInstanceById, + getResolvedSelectOptionById, + translate, + specialAbility.Constant, + ) + + return chunk === undefined + ? MISSING_VALUE + : renderActivatableNameComponents(translateMap, chunk, false) + } + case "Selection": { + const chunks = specialAbility.Selection.options.map(option => + getSpecialAbilityNameComponents( + getInstanceById, + getResolvedSelectOptionById, + translate, + option, + ), + ) + + const possiblyCombined = + isNotEmpty(specialAbility.Selection.options) && chunks.every(isNotNullish) + ? combineNameComponents(chunks) + : undefined + + if (possiblyCombined === undefined) { + return localeJoin( + chunks.map(chunk => + chunk === undefined + ? MISSING_VALUE + : renderActivatableNameComponents(translateMap, chunk, false), + ), + "disjunction", + ) + } else { + return renderCombinedActivatableNameComponents( + translateMap, + possiblyCombined, + false, + list => localeJoin(list, "disjunction"), + ) + } + } + default: + return assertExhaustive(specialAbility) + } + }, + ) + +const renderSpecialAbilities = (specialAbilities: SpecialAbilities) => + Reader.sequence< + StdEnv< + "f" | "t" | "tm" | "lj" | "lc" | "ibi" | "rso", + "Skill" | "SkillGroup" | SpecialAbilityIdentifier["kind"] + >, + string | NonEmptyArray | undefined + >([ + specialAbilities.languagesScripts === undefined + ? Reader.of(undefined) + : renderLanguagesScriptsOption(specialAbilities.languagesScripts), + specialAbilities.curses === undefined + ? Reader.of(undefined) + : renderCursesOption(specialAbilities.curses), + specialAbilities.skillSpecialization === undefined + ? Reader.of(undefined) + : renderSkillSpecializationOption(specialAbilities.skillSpecialization), + specialAbilities.list === undefined + ? Reader.of(undefined) + : Reader.traverse(specialAbilities.list, renderSpecialAbilityName) + .thenW(localeSortR) + .map(ensureNonEmpty), + ]).then(rendered => { + const result = ensureNonEmpty(rendered.filter(isNotNullish).flat())?.join(", ") + return result === undefined ? translateR("none") : Reader.of(result) + }) + +const renderCombatTechniques = (professionPackages: NonEmptyArray) => + Reader.sequence< + StdEnv< + "f" | "t" | "tm" | "lj" | "lc" | "ibi", + "CloseCombatTechnique" | "RangedCombatTechnique" + >, + string | undefined + >([ + renderNumericListAcrossPackages( + professionPackages, + pkg => Reader.of(pkg.content.combat_techniques?.map(ct => [ct.id, ct.rating_modifier]) ?? []), + deepEqual, + (ctId: CombatTechniqueIdentifier) => strictNameR(ctId), + 6, + ), + professionPackages.some(pkg => pkg.content.options?.combat_techniques !== undefined) + ? renderSinglePackageValue( + professionPackages, + pkg => Reader.of(pkg.content.options?.combat_techniques), + deepEqual, + options => + Reader.traverse(options, option => + option === undefined ? Reader.of("—") : renderCombatTechniquesOption(option), + ).map(renderedOptions => renderedOptions.join(" / ")), + ) + : Reader.of(undefined), + ]).map(list => ensureNonEmpty(list.filter(isNotNullish))?.join(", ") ?? "—") + +const renderSkills = ( + professionPackages: NonEmptyArray, +): StdReader< + RawDefinitionListEntityDescriptionSectionItem[], + "t" | "tm" | "lc" | "ibi" | "ai" | "acibp", + "Skill", + "SkillGroup" +> => + Reader.asks(({ getAllInstances }: StdEnv<"ai", never, "SkillGroup">) => + getAllInstances("SkillGroup"), + ).thenW(skillGroups => + Reader.traverse( + skillGroups.toSorted(on(group => group.content.position, compareNumber)), + ({ id: groupId, content: group }) => + translationR(group).thenW(groupTranslation => + Reader.sequence, string | undefined>([ + renderNumericListAcrossPackages( + professionPackages, + pkg => + Reader.asks( + ({ getInstanceById }: StdEnv<"ibi", "Skill">) => + pkg.content.skills + ?.filter(skill => getInstanceById("Skill", skill.id)?.group === groupId) + .map(skill => [skill.id, skill.rating_modifier]) ?? [], + ), + equal, + skillId => strictNameR("Skill", skillId), + 0, + ), + renderSkillsOption(professionPackages, { + id: groupId, + nameOfSkillsOfGroup: groupTranslation?.longName ?? MISSING_VALUE, + }), + ]).map(value => ({ + label: groupTranslation?.name ?? MISSING_VALUE, + value: ensureNonEmpty(value.filter(isNotNullish))?.join(", ") ?? "—", + })), + ), + ), + ) + +const renderSpellworkName = (spellworkIds: ProfessionMagicalSkillIdentifier[]) => + Reader.traverse(spellworkIds, spellworkId => { + switch (spellworkId.kind) { + case "Spellwork": + return strictNameR(spellworkId.Spellwork.id).thenW(baseName => + (spellworkId.Spellwork.tradition === undefined + ? Reader.of(undefined) + : nameR("MagicalTradition", spellworkId.Spellwork.tradition) + ).map(traditionName => baseName + parensIf(traditionName)), + ) + case "MagicalAction": + return strictNameR(spellworkId.MagicalAction.id) + default: + return spellworkId + } + }).thenW(list => localeJoinR(list, "disjunction")) + +const renderSpellworks = (professionPackages: NonEmptyArray) => + Reader.sequence< + StdEnv<"t" | "tm" | "lc" | "lj" | "ibi", "Cantrip" | "Spell" | "Ritual" | "MagicalTradition">, + string | undefined + >([ + renderCantripsOption(professionPackages), + renderNumericListAcrossPackages( + professionPackages, + (pkg: PreparedProfessionPackage) => + Reader.of(pkg.content.spells?.map(ct => [ct.id, ct.rating_modifier]) ?? []), + deepEqual, + renderSpellworkName, + 0, + ), + ]).map(list => ensureNonEmpty(list.filter(isNotNullish))?.join("; ")) + +const retrieveBlessedTraditionIdentifierFromPrerequisiteGroup = ( + prerequisite: ProfessionPrerequisiteGroup, +) => + prerequisite.kind === "Activatable" && prerequisite.Activatable.id.kind === "BlessedTradition" + ? [prerequisite.Activatable.id.BlessedTradition] + : [] + +const retrieveBlessedTraditionIdentifierFromPrerequisites = ( + prerequisites: ProfessionPrerequisites | undefined, +) => + prerequisites?.flatMap(part => { + switch (part.kind) { + case "Single": + return retrieveBlessedTraditionIdentifierFromPrerequisiteGroup(part.Single) + case "Disjunction": + return part.Disjunction.list.flatMap( + retrieveBlessedTraditionIdentifierFromPrerequisiteGroup, + ) + case "Group": + return part.Group.list.flatMap(retrieveBlessedTraditionIdentifierFromPrerequisiteGroup) + default: + return assertExhaustive(part) + } + }) ?? [] + +const renderBlessingList = (blessings: Blessing_ID[]) => + Reader.traverse(blessings, id => strictNameR("Blessing", id)).thenW(localeSortR) + +const renderRestrictedBlessings = (restrictedBlessings: RestrictedBlessings) => { + switch (restrictedBlessings.kind) { + case "Three": + return renderBlessingList(restrictedBlessings.Three) + case "Six": + return renderBlessingList(restrictedBlessings.Six) + default: + return assertExhaustive(restrictedBlessings) + } +} + +const renderSingleBlessings = (traditions: BlessedTradition_ID[]) => + Reader.traverse(traditions, traditionId => + getInstanceByIdR("BlessedTradition", traditionId).map( + tradition => tradition?.restricted_blessings, + ), + ).thenW(restrictedBlessingsList => + !isNotEmpty(restrictedBlessingsList) + ? Reader.of(undefined) + : restrictedBlessingsList.length === 1 + ? (() => { + const [restrictedBlessings] = restrictedBlessingsList + return restrictedBlessings === undefined + ? translateR("The Twelve Blessings") + : translateR("The Twelve Blessings").thenW(base => + renderRestrictedBlessings(restrictedBlessings).thenW(list => + translateR("except for {$list :list type=conjunction}", { + list, + }).map(note => base + parensIf(note)), + ), + ) + })() + : restrictedBlessingsList.some(isNotNullish) + ? translateR("The Twelve Blessings").map2( + translateR("depends on selected tradition"), + (base, note) => base + parensIf(note), + ) + : translateR("The Twelve Blessings"), + ) + +const renderBlessings = (professionPackages: NonEmptyArray) => { + const traditions = professionPackages.map(pkg => + retrieveBlessedTraditionIdentifierFromPrerequisites(pkg.content.prerequisites), + ) + if (traditions.every(isEmpty)) { + return Reader.of(undefined) + } else if (allSame(traditions, deepEqual)) { + return renderSingleBlessings(traditions[0]!) + } else { + return Reader.traverse(traditions, option => + option === undefined ? Reader.of("—") : renderSingleBlessings(option), + ).map(list => list.join(" / ")) + } +} + +const renderLiturgicalChantName = (liturgyIds: LiturgyIdentifier[]) => + Reader.traverse(liturgyIds, strictNameR).thenW(list => localeJoinR(list, "disjunction")) + +const renderLiturgicalChants = (professionPackages: NonEmptyArray) => + Reader.sequence< + StdEnv< + "t" | "tm" | "lc" | "lj" | "ibi", + "BlessedTradition" | "Blessing" | "LiturgicalChant" | "Ceremony" + >, + string | undefined + >([ + renderBlessings(professionPackages), + renderNumericListAcrossPackages( + professionPackages, + (pkg: PreparedProfessionPackage) => + Reader.of(pkg.content.liturgical_chants?.map(ct => [ct.id, ct.rating_modifier]) ?? []), + deepEqual, + renderLiturgicalChantName, + 0, + ), + renderLiturgiesOption(professionPackages), + ]).map(list => ensureNonEmpty(list.filter(isNotNullish))?.join(", ")) + +const renderRatedVariantChanges = ( + baseList: { id: ID; rating_modifier: number }[] | undefined, + variantList: { id: ID; rating_modifier: number }[] | undefined, + renderInstance: (id: ID) => Reader, +) => + Reader.traverse(variantList ?? [], ({ id, rating_modifier }) => { + const baseValue = baseList?.find(item => deepEqual(item.id, id))?.rating_modifier ?? 0 + return renderInstance(id).thenW(name => + translateR("{$replacement} instead of {$base}", { + replacement: `${name} ${baseValue + rating_modifier}`, + base: baseValue, + }), + ) + }).thenW(localeSortR) + +const renderVariantOption = ( + base: ProfessionPackage, + variant: ProfessionVariant, + key: K, + render: ( + base: ProfessionPackageOptions[K] | undefined, + variant: NonNullable, + ) => Reader, +): Reader => + (variant.options?.[key] === undefined + ? Reader.of(undefined) + : render(base.options?.[key], variant.options[key]) + ).map(text => nullableToArray(text)) + +const renderProfessionVariantLabel = ( + base: ProfessionPackage, + variant: ProfessionVariant, + translation: ProfessionVariantTranslation, +) => + translateR("{$value} AP", { + value: base.ap_value + (variant.ap_value ?? 0), + }).map(apValueText => translation.name.default + parensIf(apValueText)) + +const renderProfessionVariantText = ( + base: ProfessionPackage, + variant: ProfessionVariant, + translation: ProfessionVariantTranslation, +) => + translation.full_text !== undefined + ? Reader.of(translation.full_text) + : Reader.sequence< + StdEnv< + "f" | "t" | "tm" | "lc" | "lj" | "ibi" | "rso", + | RatedIdentifier["kind"] + | MagicalActionIdentifier["kind"] + | ActivatableIdentifier["kind"] + | "Aspect" + | "Race" + | "Culture" + | "Cantrip" + | "SkillGroup" + >, + string[] + >([ + Reader.asks(env => + variant.prerequisites === undefined + ? [] + : [ + printProfessionPrerequisites( + env.getInstanceById, + env.getResolvedSelectOptionById, + { + ...env, + join: env.localeJoin, + compare: env.localeCompare, + }, + variant.prerequisites, + ), + ], + ), + renderVariantOption( + base, + variant, + "skill_specialization", + renderVariantSkillSpecializationOption, + ), + renderVariantOption( + base, + variant, + "languages_scripts", + renderVariantLanguagesScriptsOption, + ), + renderVariantOption( + base, + variant, + "combat_techniques", + renderVariantCombatTechniquesOption, + ), + renderVariantOption(base, variant, "cantrips", renderVariantCantripsOption), + renderVariantOption(base, variant, "curses", renderVariantCursesOption), + renderVariantOption(base, variant, "skills", renderVariantSkillsOption), + renderVariantOption(base, variant, "liturgies", renderVariantLiturgiesOption), + variant.special_abilities === undefined + ? Reader.of([]) + : Reader.traverse(variant.special_abilities, specialAbility => { + switch (specialAbility.action.kind) { + case "Remove": + return translateR("no special ability").thenW(prefix => + renderSpecialAbilityName(specialAbility.value).map(name => `${prefix} ${name}`), + ) + case "Override": + return renderSpecialAbilityName(specialAbility.value) + default: + return assertExhaustive(specialAbility.action) + } + }), + renderRatedVariantChanges(base.combat_techniques, variant.combat_techniques, id => + strictNameR(id), + ), + renderRatedVariantChanges(base.skills, variant.skills, id => strictNameR("Skill", id)), + renderRatedVariantChanges(base.spells, variant.spells, renderSpellworkName), + renderRatedVariantChanges( + base.liturgical_chants, + variant.liturgical_chants, + renderLiturgicalChantName, + ), + ]).map( + lists => + lists + .filter(isNotEmpty) + .map(list => list.join(", ")) + .join("; ") + (translation.concluding_text ?? ""), + ) + +const renderProfessionVariant = (base: ProfessionPackage, variant: ProfessionVariant) => + translationR(variant).thenW(translation => + translation === undefined + ? Reader.of(undefined) + : renderProfessionVariantLabel(base, variant, translation).thenW(label => + renderProfessionVariantText(base, variant, translation).map( + (value): RawDefinitionListEntityDescriptionSectionItem => ({ + label, + value, + }), + ), + ), + ) + +const renderProfessionVariants = (professionPackages: NonEmptyArray) => + professionPackages.length > 1 + ? Reader.of(UNHANDLED_VALUE) + : getChildInstancesForInstanceIdR("ProfessionVariant", professionPackages[0].id).thenW( + variants => + Reader.traverse(variants, variant => + renderProfessionVariant(professionPackages[0].content, variant.content), + ) + .map(list => list.filter(isNotNullish)) + .thenW(list => + list.length === 0 + ? Reader.of(undefined) + : localeCompareR.map( + (localeCompare): RawNestedDefinitionListEntityDescriptionSection[] => [ + { + type: "definitionList", + style: "nested", + items: list.toSorted(on(item => item.label, localeCompare)), + }, + ], + ), + ), + ) + +/** + * Get a JSON representation of the rules text for a profession version. + */ +export const getProfessionVersionEntityDescription = createEntityDescriptionCreator< + "ProfessionVersion", + { + getInstanceById: GetInstanceById< + | "Publication" + | "ExperienceLevel" + | "Advantage" + | "Disadvantage" + | SpecialAbilityIdentifier["kind"] + | "Aspect" + | RatedIdentifier["kind"] + | "Race" + | "Culture" + | MagicalActionIdentifier["kind"] + | "SkillGroup" + | "Cantrip" + | "Blessing" + > + getAllInstances: GetAllInstances<"SkillGroup"> + getChildInstancesForInstanceId: GetAllChildInstancesForParent< + "ProfessionPackage" | "ProfessionVariant" + > + getResolvedSelectOptionById: GetResolvedSelectOptionById + } +>( + ( + { + getInstanceById, + getAllInstances, + getChildInstancesForInstanceId, + getResolvedSelectOptionById, + }, + locale, + { id, content: entry }, + ): RawEntityDescription | undefined => { + const { format, translate, translateMap, compare: localeCompare, join: localeJoin } = locale + + const translation = translateMap(entry.translations) + + const env = { + format, + translate, + translateMap, + localeCompare, + localeJoin, + getInstanceById, + getAllInstances, + getChildInstancesForInstanceId, + getResolvedSelectOptionById, + } satisfies Partial + + const professionPackages = prepareProfessionPackages(id).run(env) + + if (translation === undefined || !isNotEmpty(professionPackages)) { + return undefined + } + + return { + title: + translation.name.default + + (professionPackages.length > 1 + ? ` (${professionPackages.map(pkg => translateMap(pkg.experienceLevel.translations)?.name).join("/")})` + : ""), + className: "profession", + body: [ + { + type: "definitionList", + items: [ + { + label: translate("AP Value"), + value: renderSinglePackageValue( + professionPackages, + pkg => Reader.of(pkg.content.ap_value), + equal, + apValues => + apValues.length === 1 + ? translateR(".input {$value :number} {{{$value} Adventure Points}}", { + value: apValues[0], + }) + : translateR("{$value} Adventure Points", { + value: apValues.join("/"), + }), + ).run(env), + }, + { + label: translate("Prerequisites"), + value: renderSinglePackageValue( + professionPackages, + pkg => Reader.of(pkg.content.prerequisites), + deepEqual, + prerequisitesLists => + Reader.of( + prerequisitesLists + .map(prerequisites => + prerequisites === undefined + ? translate("none") + : printProfessionPrerequisites( + getInstanceById, + getResolvedSelectOptionById, + locale, + prerequisites, + ), + ) + .join(" / "), + ), + ).run(env), + }, + { + label: translate("Special Abilities"), + value: renderSinglePackageValue( + professionPackages, + pkg => + Reader.of({ + skillSpecialization: pkg.content.options?.skill_specialization, + languagesScripts: pkg.content.options?.languages_scripts, + curses: pkg.content.options?.curses, + list: pkg.content.special_abilities, + }), + deepEqual, + specialAbilityLists => + Reader.of( + specialAbilityLists + .map(specialAbilities => + specialAbilities === undefined + ? translate("none") + : renderSpecialAbilities(specialAbilities).run(env), + ) + .join(" / "), + ), + ).run(env), + }, + { + label: translate("Combat Techniques"), + value: renderCombatTechniques(professionPackages).run(env), + }, + { + label: translate("Skills"), + value: [ + { + type: "definitionList", + style: "nested", + items: renderSkills(professionPackages).run(env), + }, + ], + }, + renderSpellworks(professionPackages) + .map(value => + value === undefined + ? undefined + : { + label: translate("Spellworks"), + value, + }, + ) + .run(env), + renderLiturgicalChants(professionPackages) + .map(value => + value === undefined + ? undefined + : { + label: translate("Liturgical Chants"), + value, + }, + ) + .run(env), + renderValueWithPossibleTranslation( + "Suggested Advantages", + entry.suggested_advantages, + values => + renderCommonnessRatedAdvantagesOrDisadvantages("Advantage", values).run(env), + translation.suggested_advantages, + ).run(env), + renderValueWithPossibleTranslation( + "Suggested Disadvantages", + entry.suggested_disadvantages, + values => + renderCommonnessRatedAdvantagesOrDisadvantages("Disadvantage", values).run(env), + translation.suggested_disadvantages, + ).run(env), + renderValueWithPossibleTranslation( + "Unsuitable Advantages", + entry.unsuitable_advantages, + values => + renderCommonnessRatedAdvantagesOrDisadvantages("Advantage", values).run(env), + translation.unsuitable_advantages, + ).run(env), + renderValueWithPossibleTranslation( + "Unsuitable Disadvantages", + entry.unsuitable_disadvantages, + values => + renderCommonnessRatedAdvantagesOrDisadvantages("Disadvantage", values).run(env), + translation.unsuitable_disadvantages, + ).run(env), + renderProfessionVariants(professionPackages) + .map(value => + value === undefined + ? undefined + : { + label: translate("Variants"), + value, + }, + ) + .run(env), + ], + }, + ], + errata: translation.errata, + references: entry.src, + } + }, +) diff --git a/src/entities/spell.ts b/src/entities/spell.ts index 0b26e7f..ec52be5 100644 --- a/src/entities/spell.ts +++ b/src/entities/spell.ts @@ -29,20 +29,10 @@ import { } from "optolith-database-schema/gen" import { Case } from "tsondb/schema/gen" import { createEntityDescriptionCreator } from "../creator.js" -import type { - GetAllChildInstancesForParent, - GetInstanceById, -} from "../helpers/getTypes.js" +import type { GetAllChildInstancesForParent, GetInstanceById } from "../helpers/getTypes.js" import type { LocaleCompare } from "../helpers/locale.js" -import { - Translate, - TranslateMap, - type TranslationKeysWithoutParams, -} from "../helpers/translate.js" -import { - type IdMap, - type RawDefinitionListEntityDescriptionSectionItem, -} from "../index.js" +import { Translate, TranslateMap, type TranslationKeysWithoutParams } from "../helpers/translate.js" +import { type IdMap, type RawDefinitionListEntityDescriptionSectionItem } from "../index.js" import { renderAnimalTypesSection } from "./partial/animalTypes.js" import { printAnimistPowerPrerequisites, @@ -79,10 +69,7 @@ import { renderImprovementCost, renderImprovementCostValue, } from "./partial/rated/improvementCost.js" -import { - renderSkillCheck, - renderSkillCheckWithPenalty, -} from "./partial/rated/skillCheck.js" +import { renderSkillCheck, renderSkillCheckWithPenalty } from "./partial/rated/skillCheck.js" import { formatEnergyR, localeJoinR, @@ -112,14 +99,12 @@ const combineGeneratedTextWithStaticTranslation = ( return undefined } - const normalizedStaticText = - typeof staticText === "string" ? staticText : staticText?.full + const normalizedStaticText = typeof staticText === "string" ? staticText : staticText?.full return { label, value: - normalizedStaticText !== undefined && - generatedText !== normalizedStaticText + normalizedStaticText !== undefined && generatedText !== normalizedStaticText ? `***${generatedText}*** (${normalizedStaticText})` : generatedText, } @@ -127,11 +112,7 @@ const combineGeneratedTextWithStaticTranslation = ( const renderProperty = ( id: Property_ID, -): StdReader< - RawDefinitionListEntityDescriptionSectionItem, - "t" | "tm" | "ibi", - "Property" -> => +): StdReader => Reader.asks(({ translate, translateMap, getInstanceById }) => { const text = (() => { const staticEntry = getInstanceById("Property", id) @@ -165,9 +146,7 @@ const getTextForTraditions = ( return deps.translate("General") case "Specific": return value.Specific.map(trad => - deps.translateMap( - deps.getInstanceById("MagicalTradition", trad)?.translations, - ), + deps.translateMap(deps.getInstanceById("MagicalTradition", trad)?.translations), ) .filter(isNotNullish) .map(trad => trad.name_for_arcane_spellworks ?? trad.name) @@ -189,9 +168,7 @@ const getTraditionNameForArcaneSpellworksById = ( getInstanceById: GetInstanceById<"MagicalTradition">, translateMap: TranslateMap, ) => { - const translation = translateMap( - getInstanceById("MagicalTradition", id)?.translations, - ) + const translation = translateMap(getInstanceById("MagicalTradition", id)?.translations) return translation?.name_for_arcane_spellworks ?? translation?.name } @@ -202,11 +179,7 @@ export const getCantripEntityDescription = createEntityDescriptionCreator< "Cantrip", { getInstanceById: GetInstanceById< - | "Publication" - | "TargetCategory" - | "Property" - | "MagicalTradition" - | "Curriculum" + "Publication" | "TargetCategory" | "Property" | "MagicalTradition" | "Curriculum" > } >(({ getInstanceById }, locale, { content: entry }) => { @@ -238,11 +211,7 @@ export const getCantripEntityDescription = createEntityDescriptionCreator< label: translate("Effect"), value: translation.effect, }, - combineGeneratedTextWithStaticTranslation( - translate("Range"), - range, - translation.range, - ), + combineGeneratedTextWithStaticTranslation(translate("Range"), range, translation.range), combineGeneratedTextWithStaticTranslation( translate("Duration"), duration, @@ -260,10 +229,7 @@ export const getCantripEntityDescription = createEntityDescriptionCreator< switch (academyOrTradition.kind) { case "Academy": return translateMap( - getInstanceById( - "Curriculum", - academyOrTradition.Academy, - )?.translations, + getInstanceById("Curriculum", academyOrTradition.Academy)?.translations, )?.name case "Tradition": { return mapNullable( @@ -275,9 +241,7 @@ export const getCantripEntityDescription = createEntityDescriptionCreator< name => name + parensIf( - translateMap( - academyOrTradition.Tradition.translations, - )?.note, + translateMap(academyOrTradition.Tradition.translations)?.note, ), ) } @@ -347,9 +311,7 @@ export const getSpellEntityDescription = createEntityDescriptionCreator< localeJoin: locale.join, energyUnit: "ArcaneEnergy", responsiveTextSize: ResponsiveTextSize.Full, - nonModifiableSuffix: ( - param: ModifiableParameter, - ): TranslationKeysWithoutParams => { + nonModifiableSuffix: (param: ModifiableParameter): TranslationKeysWithoutParams => { switch (param) { case ModifiableParameter.CastingTime: return " (you cannot use a modification on this spell’s casting time)" @@ -363,8 +325,9 @@ export const getSpellEntityDescription = createEntityDescriptionCreator< }, } satisfies Partial - const { castingTime, cost, range, duration } = - renderFastPerformanceParameters(entry.parameters).run(env) + const { castingTime, cost, range, duration } = renderFastPerformanceParameters( + entry.parameters, + ).run(env) return { title: translation.name, @@ -373,27 +336,15 @@ export const getSpellEntityDescription = createEntityDescriptionCreator< { type: "definitionList", items: [ - renderSkillCheckWithPenalty( - entry.check, - entry.check_penalty, - idMap, - ).run(env), + renderSkillCheckWithPenalty(entry.check, entry.check_penalty, idMap).run(env), renderEffect(translation.effect).run(env), combineGeneratedTextWithStaticTranslation( translate("Casting Time"), castingTime, translation.casting_time, ), - combineGeneratedTextWithStaticTranslation( - translate("AE Cost"), - cost, - translation.cost, - ), - combineGeneratedTextWithStaticTranslation( - translate("Range"), - range, - translation.range, - ), + combineGeneratedTextWithStaticTranslation(translate("AE Cost"), cost, translation.cost), + combineGeneratedTextWithStaticTranslation(translate("Range"), range, translation.range), combineGeneratedTextWithStaticTranslation( translate("Duration"), duration, @@ -446,9 +397,7 @@ export const getRitualEntityDescription = createEntityDescriptionCreator< localeJoin: locale.join, energyUnit: "ArcaneEnergy", responsiveTextSize: ResponsiveTextSize.Full, - nonModifiableSuffix: ( - param: ModifiableParameter, - ): TranslationKeysWithoutParams => { + nonModifiableSuffix: (param: ModifiableParameter): TranslationKeysWithoutParams => { switch (param) { case ModifiableParameter.CastingTime: return " (you cannot use a modification on this ritual’s ritual time)" @@ -462,8 +411,9 @@ export const getRitualEntityDescription = createEntityDescriptionCreator< }, } satisfies Partial - const { castingTime, cost, range, duration } = - renderSlowPerformanceParameters(entry.parameters).run(env) + const { castingTime, cost, range, duration } = renderSlowPerformanceParameters( + entry.parameters, + ).run(env) return { title: translation.name, @@ -472,27 +422,15 @@ export const getRitualEntityDescription = createEntityDescriptionCreator< { type: "definitionList", items: [ - renderSkillCheckWithPenalty( - entry.check, - entry.check_penalty, - idMap, - ).run(env), + renderSkillCheckWithPenalty(entry.check, entry.check_penalty, idMap).run(env), renderEffect(translation.effect).run(env), combineGeneratedTextWithStaticTranslation( translate("Ritual Time"), castingTime, translation.casting_time, ), - combineGeneratedTextWithStaticTranslation( - translate("AE Cost"), - cost, - translation.cost, - ), - combineGeneratedTextWithStaticTranslation( - translate("Range"), - range, - translation.range, - ), + combineGeneratedTextWithStaticTranslation(translate("AE Cost"), cost, translation.cost), + combineGeneratedTextWithStaticTranslation(translate("Range"), range, translation.range), combineGeneratedTextWithStaticTranslation( translate("Duration"), duration, @@ -551,17 +489,9 @@ export const getCurseEntityDescription = createEntityDescriptionCreator< { type: "definitionList", items: [ - renderSkillCheckWithPenalty( - entry.check, - entry.check_penalty, - idMap, - ).run(env), + renderSkillCheckWithPenalty(entry.check, entry.check_penalty, idMap).run(env), renderEffect(translation.effect).run(env), - combineGeneratedTextWithStaticTranslation( - translate("AE Cost"), - cost, - translation.cost, - ), + combineGeneratedTextWithStaticTranslation(translate("AE Cost"), cost, translation.cost), combineGeneratedTextWithStaticTranslation( translate("Duration"), duration, @@ -587,188 +517,140 @@ const renderMagicalActionSkill = ( "t" | "tm" | "lc" | "lj" | "ibi", "Skill" > => - Reader.asks( - ({ - translate, - translateMap, - localeCompare, - localeJoin, - getInstanceById, - }) => ({ - label: translate("Skill"), - value: localeJoin( - skill - .map( - id => - translateMap(getInstanceById("Skill", id)?.translations)?.name ?? - MISSING_VALUE, - ) - .toSorted(localeCompare), - "disjunction", - ), - }), - ) + Reader.asks(({ translate, translateMap, localeCompare, localeJoin, getInstanceById }) => ({ + label: translate("Skill"), + value: localeJoin( + skill + .map(id => translateMap(getInstanceById("Skill", id)?.translations)?.name ?? MISSING_VALUE) + .toSorted(localeCompare), + "disjunction", + ), + })) /** * Get a JSON representation of the rules text for an Elven magical song. */ -export const getElvenMagicalSongEntityDescription = - createEntityDescriptionCreator< - "ElvenMagicalSong", - { - getInstanceById: GetInstanceById< - | "Publication" - | "Attribute" - | "Property" - | "DerivedCharacteristic" - | "Skill" - > - idMap: IdMap - } - >(({ getInstanceById, idMap }, locale, { content: entry }) => { - const { translate, translateMap } = locale - const translation = translateMap(entry.translations) +export const getElvenMagicalSongEntityDescription = createEntityDescriptionCreator< + "ElvenMagicalSong", + { + getInstanceById: GetInstanceById< + "Publication" | "Attribute" | "Property" | "DerivedCharacteristic" | "Skill" + > + idMap: IdMap + } +>(({ getInstanceById, idMap }, locale, { content: entry }) => { + const { translate, translateMap } = locale + const translation = translateMap(entry.translations) - if (translation === undefined) { - return undefined - } + if (translation === undefined) { + return undefined + } - const env = { - translate, - translateMap, - getInstanceById, - localeJoin: locale.join, - localeCompare: locale.compare, - energyUnit: "ArcaneEnergy", - responsiveTextSize: ResponsiveTextSize.Full, - } satisfies Partial + const env = { + translate, + translateMap, + getInstanceById, + localeJoin: locale.join, + localeCompare: locale.compare, + energyUnit: "ArcaneEnergy", + responsiveTextSize: ResponsiveTextSize.Full, + } satisfies Partial - const cost = renderNonModifiableOneTimeCost( - entry.parameters.cost, - false, - ).run(env) + const cost = renderNonModifiableOneTimeCost(entry.parameters.cost, false).run(env) - return { - title: translation.name, - className: "elven-magical-song", - body: [ - { - type: "definitionList", - items: [ - renderSkillCheckWithPenalty( - entry.check, - entry.check_penalty, - idMap, - ).run(env), - renderEffect(translation.effect).run(env), - renderMagicalActionSkill(entry.skill).run(env), - combineGeneratedTextWithStaticTranslation( - translate("AE Cost"), - cost, - translation.cost, - ), - renderProperty(entry.property).run(env), - renderImprovementCost(entry.improvement_cost).run(env), - ], - }, - ], - errata: translation.errata, - references: entry.src, - } - }) + return { + title: translation.name, + className: "elven-magical-song", + body: [ + { + type: "definitionList", + items: [ + renderSkillCheckWithPenalty(entry.check, entry.check_penalty, idMap).run(env), + renderEffect(translation.effect).run(env), + renderMagicalActionSkill(entry.skill).run(env), + combineGeneratedTextWithStaticTranslation(translate("AE Cost"), cost, translation.cost), + renderProperty(entry.property).run(env), + renderImprovementCost(entry.improvement_cost).run(env), + ], + }, + ], + errata: translation.errata, + references: entry.src, + } +}) /** * Get a JSON representation of the rules text for a domination ritual. */ -export const getDominationRitualEntityDescription = - createEntityDescriptionCreator< - "DominationRitual", - { - getInstanceById: GetInstanceById< - | "Publication" - | "Attribute" - | "Property" - | "DerivedCharacteristic" - | "SkillModificationLevel" - > - idMap: IdMap - } - >(({ getInstanceById, idMap }, locale, { content: entry }) => { - const { translate, translateMap } = locale - const translation = translateMap(entry.translations) +export const getDominationRitualEntityDescription = createEntityDescriptionCreator< + "DominationRitual", + { + getInstanceById: GetInstanceById< + "Publication" | "Attribute" | "Property" | "DerivedCharacteristic" | "SkillModificationLevel" + > + idMap: IdMap + } +>(({ getInstanceById, idMap }, locale, { content: entry }) => { + const { translate, translateMap } = locale + const translation = translateMap(entry.translations) - if (translation === undefined) { - return undefined - } + if (translation === undefined) { + return undefined + } - const env = { - translate, - translateMap, - getInstanceById, - speed: Speed.Slow, - energyUnit: "ArcaneEnergy", - responsiveTextSize: ResponsiveTextSize.Full, - } satisfies Partial + const env = { + translate, + translateMap, + getInstanceById, + speed: Speed.Slow, + energyUnit: "ArcaneEnergy", + responsiveTextSize: ResponsiveTextSize.Full, + } satisfies Partial - const cost = renderModifiableOneTimeCost(entry.parameters.cost).run(env) - const duration = renderOneTimeDuration(entry.parameters.duration).run(env) + const cost = renderModifiableOneTimeCost(entry.parameters.cost).run(env) + const duration = renderOneTimeDuration(entry.parameters.duration).run(env) - return { - title: translation.name, - className: "domination-ritual", - body: [ - { - type: "definitionList", - items: [ - renderSkillCheckWithPenalty( - entry.check, - entry.check_penalty, - idMap, - ).run(env), - renderEffect(translation.effect).run(env), - combineGeneratedTextWithStaticTranslation( - translate("AE Cost"), - cost, - translation.cost, - ), - combineGeneratedTextWithStaticTranslation( - translate("Duration"), - duration, - translation.duration, - ), - renderProperty(entry.property).run(env), - { - label: translate("Improvement Cost"), - value: "B", - }, - ], - }, - ], - errata: translation.errata, - references: entry.src, - } - }) + return { + title: translation.name, + className: "domination-ritual", + body: [ + { + type: "definitionList", + items: [ + renderSkillCheckWithPenalty(entry.check, entry.check_penalty, idMap).run(env), + renderEffect(translation.effect).run(env), + combineGeneratedTextWithStaticTranslation(translate("AE Cost"), cost, translation.cost), + combineGeneratedTextWithStaticTranslation( + translate("Duration"), + duration, + translation.duration, + ), + renderProperty(entry.property).run(env), + { + label: translate("Improvement Cost"), + value: "B", + }, + ], + }, + ], + errata: translation.errata, + references: entry.src, + } +}) const renderMusicTradition = ( translate: Translate, translateMap: TranslateMap, localeCompare: LocaleCompare, - getInstanceById: GetInstanceById< - "ArcaneBardTradition" | "ArcaneDancerTradition" - >, + getInstanceById: GetInstanceById<"ArcaneBardTradition" | "ArcaneDancerTradition">, entity: "ArcaneBardTradition" | "ArcaneDancerTradition", - musicTraditions: - | ArcaneBardTraditionReference[] - | ArcaneDancerTraditionReference[], + musicTraditions: ArcaneBardTraditionReference[] | ArcaneDancerTraditionReference[], ): RawDefinitionListEntityDescriptionSectionItem => ({ label: translate("Music Tradition"), value: ensureNonEmpty( musicTraditions - .map( - trad => - translateMap(getInstanceById(entity, trad.id)?.translations)?.name, - ) + .map(trad => translateMap(getInstanceById(entity, trad.id)?.translations)?.name) .filter(isNotNullish) .toSorted(localeCompare), )?.join(", ") ?? MISSING_VALUE, @@ -823,11 +705,7 @@ export const getMagicalDanceEntityDescription = createEntityDescriptionCreator< duration, translation.duration, ), - combineGeneratedTextWithStaticTranslation( - translate("AE Cost"), - cost, - translation.cost, - ), + combineGeneratedTextWithStaticTranslation(translate("AE Cost"), cost, translation.cost), renderProperty(entry.property).run(env), renderMusicTradition( translate, @@ -898,11 +776,7 @@ export const getMagicalMelodyEntityDescription = createEntityDescriptionCreator< translation.duration, ), renderMagicalActionSkill(entry.skill).run(env), - combineGeneratedTextWithStaticTranslation( - translate("AE Cost"), - cost, - translation.cost, - ), + combineGeneratedTextWithStaticTranslation(translate("AE Cost"), cost, translation.cost), renderProperty(entry.property).run(env), renderMusicTradition( translate, @@ -930,8 +804,8 @@ const renderFamiliarsTrickProperty = ( switch (property.kind) { case "Fixed": return ( - translateMap(getInstanceById("Property", property.Fixed)?.translations) - ?.name ?? MISSING_VALUE + translateMap(getInstanceById("Property", property.Fixed)?.translations)?.name ?? + MISSING_VALUE ) case "Indefinite": return getResponsiveText( @@ -961,22 +835,18 @@ const renderFamiliarsTrickPerformanceParameters = ( })), ) case "OneTimeInterval": - return renderNonModifiableOneTimeCost( - params.OneTimeInterval.cost, - false, - ).then(cost => + return renderNonModifiableOneTimeCost(params.OneTimeInterval.cost, false).then(cost => translateR("depends on spent AE").map(duration => ({ cost, duration, })), ) case "Sustained": - return renderNonModifiableOneTimeCost(params.Sustained.cost, false).then( - cost => - renderSustainedDuration(undefined).map(duration => ({ - cost, - duration, - })), + return renderNonModifiableOneTimeCost(params.Sustained.cost, false).then(cost => + renderSustainedDuration(undefined).map(duration => ({ + cost, + duration, + })), ) default: return assertExhaustive(params) @@ -986,93 +856,81 @@ const renderFamiliarsTrickPerformanceParameters = ( /** * Get a JSON representation of the rules text for a familiar’s trick. */ -export const getFamiliarsTrickEntityDescription = - createEntityDescriptionCreator< - "FamiliarsTrick", - { - getInstanceById: GetInstanceById< - | "Publication" - | "Attribute" - | "Property" - | "DerivedCharacteristic" - | "AnimalType" - > - } - >(({ getInstanceById }, locale, { content: entry }) => { - const { translate, translateMap } = locale - const translation = translateMap(entry.translations) +export const getFamiliarsTrickEntityDescription = createEntityDescriptionCreator< + "FamiliarsTrick", + { + getInstanceById: GetInstanceById< + "Publication" | "Attribute" | "Property" | "DerivedCharacteristic" | "AnimalType" + > + } +>(({ getInstanceById }, locale, { content: entry }) => { + const { translate, translateMap } = locale + const translation = translateMap(entry.translations) - if (translation === undefined) { - return undefined - } + if (translation === undefined) { + return undefined + } - const env = { - translate, - translateMap, - getInstanceById, - localeJoin: locale.join, - energyUnit: "ArcaneEnergy", - responsiveTextSize: ResponsiveTextSize.Full, - } satisfies Partial + const env = { + translate, + translateMap, + getInstanceById, + localeJoin: locale.join, + energyUnit: "ArcaneEnergy", + responsiveTextSize: ResponsiveTextSize.Full, + } satisfies Partial - const { cost, duration } = renderFamiliarsTrickPerformanceParameters( - entry.parameters, - ).run(env) + const { cost, duration } = renderFamiliarsTrickPerformanceParameters(entry.parameters).run(env) - return { - title: translation.name, - className: "magical-dance", - body: [ - { - type: "definitionList", - items: [ - { - label: locale.translate("Effect"), - value: translation.effect, - }, - renderAnimalTypesSection( - translate, + return { + title: translation.name, + className: "magical-dance", + body: [ + { + type: "definitionList", + items: [ + { + label: locale.translate("Effect"), + value: translation.effect, + }, + renderAnimalTypesSection( + translate, + translateMap, + locale.compare, + getInstanceById, + entry.animal_types, + ), + combineGeneratedTextWithStaticTranslation(translate("AE Cost"), cost, translation.cost), + combineGeneratedTextWithStaticTranslation( + translate("Duration"), + duration, + translation.duration, + ), + { + label: translate("Property"), + value: renderFamiliarsTrickProperty( translateMap, - locale.compare, getInstanceById, - entry.animal_types, + ResponsiveTextSize.Full, + entry.property, ), - combineGeneratedTextWithStaticTranslation( - translate("AE Cost"), - cost, - translation.cost, - ), - combineGeneratedTextWithStaticTranslation( - translate("Duration"), - duration, - translation.duration, - ), - { - label: translate("Property"), - value: renderFamiliarsTrickProperty( - translateMap, - getInstanceById, - ResponsiveTextSize.Full, - entry.property, - ), - }, - { - label: translate("AP Value"), - value: - entry.ap_value === undefined - ? translate("All familiars know this trick by default.") - : translate( - ".input {$value :number} {{{$value} Adventure Points}}", - { value: entry.ap_value }, - ), - }, - ], - }, - ], - errata: translation.errata, - references: entry.src, - } - }) + }, + { + label: translate("AP Value"), + value: + entry.ap_value === undefined + ? translate("All familiars know this trick by default.") + : translate(".input {$value :number} {{{$value} Adventure Points}}", { + value: entry.ap_value, + }), + }, + ], + }, + ], + errata: translation.errata, + references: entry.src, + } +}) const renderAnimistPowerPerformanceParameters = ( params: AnimistPowerPerformanceParameters, @@ -1117,9 +975,7 @@ const renderAnimistPowerTribeTradition = ( return ( ensureNonEmpty( tribeTradition - .map( - id => translateMap(getInstanceById("Tribe", id)?.translations)?.name, - ) + .map(id => translateMap(getInstanceById("Tribe", id)?.translations)?.name) .filter(isNotNullish) .toSorted(localeCompare), )?.join(", ") ?? MISSING_VALUE @@ -1177,20 +1033,14 @@ export const getAnimistPowerEntityDescription = createEntityDescriptionCreator< responsiveTextSize: ResponsiveTextSize.Full, } satisfies Partial - const { cost, duration } = renderAnimistPowerPerformanceParameters( - entry.parameters, - ).run(env) + const { cost, duration } = renderAnimistPowerPerformanceParameters(entry.parameters).run(env) const levels = entry.levels?.length ?? 1 const prerequisites = entry.prerequisites === undefined ? undefined - : printAnimistPowerPrerequisites( - getInstanceById, - locale, - entry.prerequisites, - ) + : printAnimistPowerPrerequisites(getInstanceById, locale, entry.prerequisites) const additionalEffectsFromLevels = entry.levels @@ -1245,11 +1095,7 @@ export const getAnimistPowerEntityDescription = createEntityDescriptionCreator< items: [ renderSkillCheck(entry.check).run(env), renderEffect(mergedEffect).run(env), - combineGeneratedTextWithStaticTranslation( - translate("AE Cost"), - cost, - translation.cost, - ), + combineGeneratedTextWithStaticTranslation(translate("AE Cost"), cost, translation.cost), combineGeneratedTextWithStaticTranslation( translate("Duration"), duration, @@ -1294,6 +1140,7 @@ export const getGeodeRitualEntityDescription = createEntityDescriptionCreator< | "Property" | "MagicalTradition" | "DerivedCharacteristic" + | "Influence" > } >(({ getInstanceById }, locale, { content: entry }) => { @@ -1313,9 +1160,9 @@ export const getGeodeRitualEntityDescription = createEntityDescriptionCreator< responsiveTextSize: ResponsiveTextSize.Full, } satisfies Partial - const castingTime = renderSlowSkillNonModifiableCastingTime( - entry.parameters.casting_time, - ).run(env) + const castingTime = renderSlowSkillNonModifiableCastingTime(entry.parameters.casting_time).run( + env, + ) const cost = renderMagicalActionCost(entry.parameters.cost).run(env) @@ -1345,16 +1192,8 @@ export const getGeodeRitualEntityDescription = createEntityDescriptionCreator< castingTime, translation.casting_time, ), - combineGeneratedTextWithStaticTranslation( - translate("AE Cost"), - cost, - translation.cost, - ), - combineGeneratedTextWithStaticTranslation( - translate("Range"), - range, - translation.range, - ), + combineGeneratedTextWithStaticTranslation(translate("AE Cost"), cost, translation.cost), + combineGeneratedTextWithStaticTranslation(translate("Range"), range, translation.range), combineGeneratedTextWithStaticTranslation( translate("Duration"), duration, @@ -1365,10 +1204,7 @@ export const getGeodeRitualEntityDescription = createEntityDescriptionCreator< ? undefined : { label: translate("Prerequisites"), - value: printGeodeRitualPrerequisites( - locale, - entry.prerequisites, - ), + value: printGeodeRitualPrerequisites(getInstanceById, locale, entry.prerequisites), }, renderProperty(entry.property).run(env), { @@ -1423,9 +1259,7 @@ export const getJesterTrickEntityDescription = createEntityDescriptionCreator< actions, }).run(env) - const cost = renderNonModifiableOneTimeCost(entry.parameters.cost, false).run( - env, - ) + const cost = renderNonModifiableOneTimeCost(entry.parameters.cost, false).run(env) const range = renderNonModifiableRange( entry.parameters.range.kind === "Fixed" @@ -1446,27 +1280,15 @@ export const getJesterTrickEntityDescription = createEntityDescriptionCreator< { type: "definitionList", items: [ - renderSkillCheckWithPenalty( - entry.check, - entry.check_penalty, - idMap, - ).run(env), + renderSkillCheckWithPenalty(entry.check, entry.check_penalty, idMap).run(env), renderEffect(translation.effect).run(env), combineGeneratedTextWithStaticTranslation( translate("Casting Time"), castingTime, translation.casting_time, ), - combineGeneratedTextWithStaticTranslation( - translate("AE Cost"), - cost, - translation.cost, - ), - combineGeneratedTextWithStaticTranslation( - translate("Range"), - range, - translation.range, - ), + combineGeneratedTextWithStaticTranslation(translate("AE Cost"), cost, translation.cost), + combineGeneratedTextWithStaticTranslation(translate("Range"), range, translation.range), combineGeneratedTextWithStaticTranslation( translate("Duration"), duration, @@ -1518,15 +1340,13 @@ export const getZibiljaRitualEntityDescription = createEntityDescriptionCreator< responsiveTextSize: ResponsiveTextSize.Full, } satisfies Partial - const { castingTime, cost, range, duration } = - renderSlowOneTimePerformanceParameters( - nestedCastingTime => - renderCastingTime( - renderSlowSkillNonModifiableCastingTime, - nestedCastingTime, - ).with(nestedEnv => ({ ...nestedEnv, speed: Speed.Slow })), - entry.parameters, - ).run(env) + const { castingTime, cost, range, duration } = renderSlowOneTimePerformanceParameters( + nestedCastingTime => + renderCastingTime(renderSlowSkillNonModifiableCastingTime, nestedCastingTime).with( + nestedEnv => ({ ...nestedEnv, speed: Speed.Slow }), + ), + entry.parameters, + ).run(env) return { title: translation.name, @@ -1535,27 +1355,15 @@ export const getZibiljaRitualEntityDescription = createEntityDescriptionCreator< { type: "definitionList", items: [ - renderSkillCheckWithPenalty( - entry.check, - entry.check_penalty, - idMap, - ).run(env), + renderSkillCheckWithPenalty(entry.check, entry.check_penalty, idMap).run(env), renderEffect(translation.effect).run(env), combineGeneratedTextWithStaticTranslation( translate("Ritual Time"), castingTime, translation.casting_time, ), - combineGeneratedTextWithStaticTranslation( - translate("AE Cost"), - cost, - translation.cost, - ), - combineGeneratedTextWithStaticTranslation( - translate("Range"), - range, - translation.range, - ), + combineGeneratedTextWithStaticTranslation(translate("AE Cost"), cost, translation.cost), + combineGeneratedTextWithStaticTranslation(translate("Range"), range, translation.range), combineGeneratedTextWithStaticTranslation( translate("Duration"), duration, @@ -1585,17 +1393,10 @@ const deriveValueGroupsFromMagicalRuneOptions = ( .toSorted(on(item => item[0], comparator)) .map(([value, matchingOptions]) => Reader.asks( - ({ - translateMap, - localeJoin, - localeCompare, - }: StdEnv<"tm" | "lj" | "lc">) => + ({ translateMap, localeJoin, localeCompare }: StdEnv<"tm" | "lj" | "lc">) => `${printValue(value)} (${localeJoin( matchingOptions - .map( - option => - translateMap(option.translations)?.name ?? MISSING_VALUE, - ) + .map(option => translateMap(option.translations)?.name ?? MISSING_VALUE) .toSorted(localeCompare), "conjunction", )})`, @@ -1603,10 +1404,7 @@ const deriveValueGroupsFromMagicalRuneOptions = ( ), ).then(formattedOptions => localeJoinR(formattedOptions, "disjunction")) -const renderMagicalRuneCost = ( - options: Lazy, - cost: MagicalRuneCost, -) => { +const renderMagicalRuneCost = (options: Lazy, cost: MagicalRuneCost) => { switch (cost.kind) { case "Single": return formatEnergyR(cost.Single.value).thenW(text => @@ -1663,11 +1461,10 @@ const renderMagicalRunCraftingTimePart = ( } return responsiveTextR(translation.countable).thenW(countable => - responsiveTranslateR( - "{$cost} per {$countable}", - "{$cost}/{$countable}", - { cost: text, countable }, - ), + responsiveTranslateR("{$cost} per {$countable}", "{$cost}/{$countable}", { + cost: text, + countable, + }), ) }) }) @@ -1687,10 +1484,7 @@ const renderMagicalRuneDuration = (duration: MagicalRuneDuration) => const renderMagicalRuneImprovementCost = ( options: Lazy, improvementCost: MagicalRuneImprovementCost, -): StdReader< - RawDefinitionListEntityDescriptionSectionItem, - "t" | "tm" | "lc" | "lj" | "eu" -> => { +): StdReader => { switch (improvementCost.kind) { case "Constant": return renderImprovementCost(improvementCost.Constant) @@ -1705,9 +1499,7 @@ const renderMagicalRuneImprovementCost = ( selectedImprovementCost => selectedImprovementCost ?? MISSING_VALUE, ) .thenW(formatEnergyR) - .then(value => - translateR("Improvement Cost").map(label => ({ label, value })), - ) + .then(value => translateR("Improvement Cost").map(label => ({ label, value }))) default: return assertExhaustive(improvementCost) } @@ -1720,100 +1512,67 @@ export const getMagicalRuneEntityDescription = createEntityDescriptionCreator< "MagicalRune", { getInstanceById: GetInstanceById< - | "Publication" - | "Attribute" - | "Property" - | "DerivedCharacteristic" - | "Skill" + "Publication" | "Attribute" | "Property" | "DerivedCharacteristic" | "Skill" > getChildInstancesForInstanceId: GetAllChildInstancesForParent<"MagicalRuneOption"> idMap: IdMap } ->( - ( - { getInstanceById, getChildInstancesForInstanceId, idMap }, - locale, - { id, content: entry }, - ) => { - const { translate, translateMap } = locale - const translation = translateMap(entry.translations) +>(({ getInstanceById, getChildInstancesForInstanceId, idMap }, locale, { id, content: entry }) => { + const { translate, translateMap } = locale + const translation = translateMap(entry.translations) - if (translation === undefined) { - return undefined - } + if (translation === undefined) { + return undefined + } - const env = { - translate, - translateMap, - getInstanceById, - localeCompare: locale.compare, - localeJoin: locale.join, - energyUnit: "ArcaneEnergy", - responsiveTextSize: ResponsiveTextSize.Full, - } satisfies Partial + const env = { + translate, + translateMap, + getInstanceById, + localeCompare: locale.compare, + localeJoin: locale.join, + energyUnit: "ArcaneEnergy", + responsiveTextSize: ResponsiveTextSize.Full, + } satisfies Partial - const options = Lazy.of(() => - getChildInstancesForInstanceId("MagicalRuneOption", id).map( - item => item.content, - ), - ) + const options = Lazy.of(() => + getChildInstancesForInstanceId("MagicalRuneOption", id).map(item => item.content), + ) - const cost = renderMagicalRuneCost(options, entry.parameters.cost).run(env) - const craftingTime = renderMagicalRuneCraftingTime( - entry.parameters.crafting_time, - ).run(env) - const duration = renderMagicalRuneDuration(entry.parameters.duration).run( - env, - ) + const cost = renderMagicalRuneCost(options, entry.parameters.cost).run(env) + const craftingTime = renderMagicalRuneCraftingTime(entry.parameters.crafting_time).run(env) + const duration = renderMagicalRuneDuration(entry.parameters.duration).run(env) - return { - title: - (translation.name_in_library ?? translation.name) + - parensIf(translation.native_name), - className: "magical-rune", - body: [ - { - type: "definitionList", - items: [ - renderSkillCheckWithPenalty( - entry.check, - entry.check_penalty, - idMap, - ).run(env), - renderEffect(translation.effect).run(env), - combineGeneratedTextWithStaticTranslation( - translate("AE Cost"), - cost, - translation.cost, - ), - combineGeneratedTextWithStaticTranslation( - translate("Crafting Time (slow / fast)"), - craftingTime, - translation.crafting_time === undefined - ? undefined - : renderSplitMagicalRuneParameterTranslation( - translation.crafting_time, - ).run(env), - ), - combineGeneratedTextWithStaticTranslation( - translate("Duration (slow / fast)"), - duration, - translation.duration === undefined - ? undefined - : renderSplitMagicalRuneParameterTranslation( - translation.duration, - ).run(env), - ), - renderProperty(entry.property).run(env), - renderMagicalRuneImprovementCost( - options, - entry.improvement_cost, - ).run(env), - ], - }, - ], - errata: translation.errata, - references: entry.src, - } - }, -) + return { + title: (translation.name_in_library ?? translation.name) + parensIf(translation.native_name), + className: "magical-rune", + body: [ + { + type: "definitionList", + items: [ + renderSkillCheckWithPenalty(entry.check, entry.check_penalty, idMap).run(env), + renderEffect(translation.effect).run(env), + combineGeneratedTextWithStaticTranslation(translate("AE Cost"), cost, translation.cost), + combineGeneratedTextWithStaticTranslation( + translate("Crafting Time (slow / fast)"), + craftingTime, + translation.crafting_time === undefined + ? undefined + : renderSplitMagicalRuneParameterTranslation(translation.crafting_time).run(env), + ), + combineGeneratedTextWithStaticTranslation( + translate("Duration (slow / fast)"), + duration, + translation.duration === undefined + ? undefined + : renderSplitMagicalRuneParameterTranslation(translation.duration).run(env), + ), + renderProperty(entry.property).run(env), + renderMagicalRuneImprovementCost(options, entry.improvement_cost).run(env), + ], + }, + ], + errata: translation.errata, + references: entry.src, + } +}) diff --git a/src/helpers/locale.ts b/src/helpers/locale.ts index d6cf542..a6be57e 100644 --- a/src/helpers/locale.ts +++ b/src/helpers/locale.ts @@ -1,6 +1,6 @@ import { Compare } from "@optolith/helpers/compare" import type { LocaleMeasurementAdjustments } from "optolith-database-schema/gen" -import { Translate, TranslateMap } from "./translate.js" +import { Translate, TranslateMap, type Format } from "./translate.js" /** * The type of list to join in a locale-aware way. @@ -12,6 +12,7 @@ export type LocaleJoinType = "conjunction" | "disjunction" | "unit" */ export type LocaleEnvironment = { id: string + format: Format translate: Translate translateMap: TranslateMap compare: LocaleCompare diff --git a/src/helpers/translate.ts b/src/helpers/translate.ts index 3bf8616..1e1385c 100644 --- a/src/helpers/translate.ts +++ b/src/helpers/translate.ts @@ -7,6 +7,11 @@ import { ResponsiveTextSize } from "../entities/partial/responsiveText.js" */ export type Translations = NonNullable +/** + * A function that formats a string with the given arguments. + */ +export type Format = (text: string, args?: Record) => string + /** * Translates a given key into a string, optionally with parameters. */ diff --git a/src/index.ts b/src/index.ts index 0a1658a..256e797 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,6 +42,7 @@ import { getOptionalRuleEntityDescription } from "./entities/optionalRule.js" import type { GetResolvedSelectOptionById } from "./entities/partial/prerequisites/single/activatable.js" import { getPersonalityTraitEntityDescription } from "./entities/personalityTrait.js" import { getPoisonEntityDescription } from "./entities/poison.js" +import { getProfessionVersionEntityDescription } from "./entities/profession.js" import { getRaceEntityDescription } from "./entities/race.js" import { getSexPracticeEntityDescription } from "./entities/sexPractice.js" import { getSkillEntityDescription } from "./entities/skill.js" @@ -260,7 +261,7 @@ const registeredEntityDescriptionCreators = { DerivedCharacteristic: getDerivedCharacteristicEntityDescription, Race: getRaceEntityDescription, Culture: getCultureEntityDescription, - // ProfessionVersion: getProfessionVersionEntityDescription, + ProfessionVersion: getProfessionVersionEntityDescription, Advantage: getActivatableEntityDescription, Disadvantage: getActivatableEntityDescription, // core values diff --git a/test/entities/partial/prerequisites/part.ts b/test/entities/partial/prerequisites/part.ts index 8c9a023..42e3ecd 100644 --- a/test/entities/partial/prerequisites/part.ts +++ b/test/entities/partial/prerequisites/part.ts @@ -19,7 +19,12 @@ describe("joinPrerequisiteParts", () => { }, { value: "C", sentenceType: undefined, isMeta: false }, ] - const result = joinPrerequisiteParts(defaultLocaleEnvironment, parts) + const result = joinPrerequisiteParts( + defaultLocaleEnvironment.translate, + defaultLocaleEnvironment.translateMap, + defaultLocaleEnvironment.compare, + parts.map(part => ({ type: "test", part })), + ) assert.equal(result, "A, Label for B, C") }) @@ -32,7 +37,12 @@ describe("joinPrerequisiteParts", () => { { value: "E", sentenceType: undefined, isMeta: false }, { value: "F", sentenceType: Case("Connected"), isMeta: false }, ] - const result = joinPrerequisiteParts(defaultLocaleEnvironment, parts) + const result = joinPrerequisiteParts( + defaultLocaleEnvironment.translate, + defaultLocaleEnvironment.translateMap, + defaultLocaleEnvironment.compare, + parts.map(part => ({ type: "test", part })), + ) assert.equal(result, "A; B; C; D, E; F") }) @@ -49,7 +59,12 @@ describe("joinPrerequisiteParts", () => { { value: "I", sentenceType: undefined, isMeta: false }, { value: "J", sentenceType: Case("Standalone"), isMeta: false }, ] - const result = joinPrerequisiteParts(defaultLocaleEnvironment, parts) + const result = joinPrerequisiteParts( + defaultLocaleEnvironment.translate, + defaultLocaleEnvironment.translateMap, + defaultLocaleEnvironment.compare, + parts.map(part => ({ type: "test", part })), + ) assert.equal(result, "A. B. C. D, E. F. G. H, I. J.") }) @@ -59,7 +74,12 @@ describe("joinPrerequisiteParts", () => { { value: "B", sentenceType: undefined, isMeta: true }, { value: "C", sentenceType: undefined, isMeta: true }, ] - const result = joinPrerequisiteParts(defaultLocaleEnvironment, parts) + const result = joinPrerequisiteParts( + defaultLocaleEnvironment.translate, + defaultLocaleEnvironment.translateMap, + defaultLocaleEnvironment.compare, + parts.map(part => ({ type: "test", part })), + ) assert.equal(result, "none, A, B, C") }) @@ -68,7 +88,13 @@ describe("joinPrerequisiteParts", () => { // @ts-expect-error Testing invalid input { value: "A", sentenceType: Case("any"), isMeta: true }, ] - const block = () => joinPrerequisiteParts(defaultLocaleEnvironment, parts) + const block = () => + joinPrerequisiteParts( + defaultLocaleEnvironment.translate, + defaultLocaleEnvironment.translateMap, + defaultLocaleEnvironment.compare, + parts.map(part => ({ type: "test", part })), + ) assert.throws(block) }) }) diff --git a/test/helpers/locale.ts b/test/helpers/locale.ts index 3f1869e..cf088d1 100644 --- a/test/helpers/locale.ts +++ b/test/helpers/locale.ts @@ -1,6 +1,6 @@ import { assertExhaustive } from "@elyukai/utils/typeSafety" import { LocaleEnvironment } from "../../src/helpers/locale.js" -import { translateMapMock, translateMock } from "./translate.js" +import { formatMock, translateMapMock, translateMock } from "./translate.js" const localeId = "en-US" @@ -21,6 +21,7 @@ const unitListFormat = new Intl.ListFormat(localeId, { */ export const defaultLocaleEnvironment: LocaleEnvironment = { id: "en-US", + format: formatMock, translate: translateMock, translateMap: translateMapMock, compare: (x, y) => collator.compare(x, y), diff --git a/test/helpers/translate.ts b/test/helpers/translate.ts index 9116b40..4389740 100644 --- a/test/helpers/translate.ts +++ b/test/helpers/translate.ts @@ -1,5 +1,15 @@ import { MessageFormat } from "messageformat" -import { Translate, TranslateMap } from "../../src/helpers/translate.js" +import { + Translate, + TranslateMap, + type Format, +} from "../../src/helpers/translate.js" + +/** + * A mocked format function. + */ +export const formatMock: Format = (text, args) => + new MessageFormat("en", text).format(args) /** * A mocked translate function.