refactor: use Reader for activatable skill helpers

Use Reader to inject environment functions and values. This keeps
function parameter lists tidy and also functions more readable since far
less arguments have to be passed around.
This commit is contained in:
Lukas Obermann
2026-02-20 23:18:13 +01:00
parent 2c4af0eae6
commit 52abf19a3d
42 changed files with 2233 additions and 2620 deletions
+4 -4
View File
@@ -9,7 +9,7 @@
"version": "0.2.1",
"license": "MPL-2.0",
"dependencies": {
"@elyukai/utils": "^0.2.2",
"@elyukai/utils": "^0.2.4",
"@optolith/helpers": "^0.2.2",
"messageformat": "^4.0.0",
"tsondb": "^0.19.8"
@@ -69,9 +69,9 @@
}
},
"node_modules/@elyukai/utils": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@elyukai/utils/-/utils-0.2.2.tgz",
"integrity": "sha512-nHzQGG7Oi7qTMIwCaWA9LKQJWRYCRu00V/gaMX22GeQkSgi6eNXaGmT0WA4TOSCEinw7wjdfiVkel55SmdUDqA==",
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@elyukai/utils/-/utils-0.2.4.tgz",
"integrity": "sha512-E1NnNUCo23Pn8J3arDjWqkJ7yikr1ddPG/zPcuGAE/O0kGAMkJbFnBDbNb+eelqq+/J5zZhNmOZBgACD3ObCFg==",
"license": "MPL-2.0"
},
"node_modules/@es-joy/jsdoccomment": {
+1 -1
View File
@@ -30,7 +30,7 @@
},
"homepage": "https://github.com/Optolith/entity-descriptions#readme",
"dependencies": {
"@elyukai/utils": "^0.2.2",
"@elyukai/utils": "^0.2.4",
"@optolith/helpers": "^0.2.2",
"messageformat": "^4.0.0",
"tsondb": "^0.19.8"
+26 -24
View File
@@ -33,6 +33,7 @@ import type {
PropertyDeclaration,
PublicationRefs,
RangedCombatTechnique,
RatedIdentifier,
SelectOptions,
SpecialRule,
Volume,
@@ -544,17 +545,14 @@ const renderVolumeValue = (
})
case "Map":
return renderResponsiveMap(
translate,
translateMap,
responsiveTextSize,
volume.Map,
option => option.points,
values => translate("{$points} points", { points: values }),
optionTranslation => optionTranslation.label,
translation => translation.list_prepend,
translation => translation.list_append,
translation => translation.replacement,
)
).run({
translate,
translateMap,
responsiveTextSize,
})
case "DerivedFromSelection": {
const groups = Map.groupBy(
getAllResolvedSelectOptions(),
@@ -736,16 +734,9 @@ const renderArcaneEnergyCost = (
})
case "Map":
return renderResponsiveMap(
translate,
translateMap,
responsiveTextSize,
cost.Map,
option => option.value,
values => translate("{$value} AE", { value: values }),
optionTranslation => optionTranslation.label,
translation => translation.list_prepend,
translation => translation.list_append,
translation => translation.replacement,
cost.Map.options.every(option => option.permanent_value !== undefined)
? {
surround: values =>
@@ -755,7 +746,11 @@ const renderArcaneEnergyCost = (
getAdditionalValue: option => option.permanent_value!,
}
: undefined,
)
).run({
translate,
translateMap,
responsiveTextSize,
})
case "Variable":
return translate("Variable")
case "ByLevel":
@@ -809,17 +804,14 @@ const renderBindingCost = (
})
case "Map":
return renderResponsiveMap(
translate,
translateMap,
responsiveTextSize,
cost.Map,
option => option.permanent_value,
values => translate("{$value} permanent AE", { value: values }),
optionTranslation => optionTranslation.label,
translation => translation.list_prepend,
translation => translation.list_append,
translation => translation.replacement,
)
).run({
translate,
translateMap,
responsiveTextSize,
})
case "DerivedFromSelection": {
const groups = Map.groupBy(
getAllResolvedSelectOptions(),
@@ -937,11 +929,21 @@ export const getActivatableEntityDescription = createEntityDescriptionCreator<
ActivatableIdentifier["kind"],
{
getInstanceById: GetInstanceById<
| "Publication"
| "Subject"
| ActivatableIdentifier["kind"]
| RatedIdentifier["kind"]
| "TradeSecret"
| "Aspect"
| "Property"
| "Race"
| "Culture"
| "State"
| "Enhancement"
| "PactCategory"
| "PactDomain"
| "SocialStatus"
| "Weapon"
>
getAllInstances: GetAllInstances<"Script">
getResolvedSelectOptionById: GetResolvedSelectOptionById
+1 -1
View File
@@ -8,7 +8,7 @@ import { MISSING_VALUE } from "./partial/unknown.js"
export const getAlternativeRuleEntityDescription =
createEntityDescriptionCreator<
"AlternativeRule",
{ getInstanceById: GetInstanceById<"PlayerType"> }
{ getInstanceById: GetInstanceById<"Publication" | "PlayerType"> }
>(({ getInstanceById }, { translateMap }, { content: entry }) => {
const translation = translateMap(entry.translations)
+5 -5
View File
@@ -1,7 +1,7 @@
import { mapNullable } from "@optolith/helpers/nullable"
import { createEntityDescriptionCreator } from "../creator.js"
import type { GetInstanceById } from "../helpers/getTypes.js"
import { createImprovementCost } from "./partial/rated/improvementCost.js"
import { renderImprovementCost } from "./partial/rated/improvementCost.js"
/**
* Get a JSON representation of the rules text for a close combat technique.
@@ -10,7 +10,7 @@ export const getCloseCombatTechniqueEntityDescription =
createEntityDescriptionCreator<
"CloseCombatTechnique",
{
getInstanceById: GetInstanceById<"Attribute">
getInstanceById: GetInstanceById<"Publication" | "Attribute">
}
>(({ getInstanceById }, { translate, translateMap }, { content: entry }) => {
const translation = translateMap(entry.translations)
@@ -37,7 +37,7 @@ export const getCloseCombatTechniqueEntityDescription =
)
.join("/"),
},
createImprovementCost(translate, entry.improvement_cost),
renderImprovementCost(entry.improvement_cost).run({ translate }),
],
errata: translation.errata,
references: entry.src,
@@ -51,7 +51,7 @@ export const getRangedCombatTechniqueEntityDescription =
createEntityDescriptionCreator<
"RangedCombatTechnique",
{
getInstanceById: GetInstanceById<"Attribute">
getInstanceById: GetInstanceById<"Publication" | "Attribute">
}
>(({ getInstanceById }, { translate, translateMap }, { content: entry }) => {
const translation = translateMap(entry.translations)
@@ -78,7 +78,7 @@ export const getRangedCombatTechniqueEntityDescription =
)
.join("/"),
},
createImprovementCost(translate, entry.improvement_cost),
renderImprovementCost(entry.improvement_cost).run({ translate }),
],
errata: translation.errata,
references: entry.src,
+3
View File
@@ -464,6 +464,7 @@ export const getCurriculumEntityDescription = createEntityDescriptionCreator<
"Curriculum",
{
getInstanceById: GetInstanceById<
| "Publication"
| "Skill"
| "CloseCombatTechnique"
| "RangedCombatTechnique"
@@ -471,6 +472,8 @@ export const getCurriculumEntityDescription = createEntityDescriptionCreator<
| "Spell"
| "Ritual"
| "MagicalTradition"
| "Property"
| "Element"
>
getAllInstances: GetAllInstances<"Profession">
getChildInstancesForInstanceId: GetAllChildInstancesForParent<
+3 -1
View File
@@ -121,7 +121,9 @@ export const getDerivedCharacteristicEntityDescription =
createEntityDescriptionCreator<
"DerivedCharacteristic",
{
getInstanceById: GetInstanceById<"Attribute" | "DerivedCharacteristic">
getInstanceById: GetInstanceById<
"Publication" | "Attribute" | "DerivedCharacteristic"
>
idMap: IdMap
}
>(
+3 -1
View File
@@ -79,7 +79,9 @@ const renderCauses = (
export const getDiseaseEntityDescription = createEntityDescriptionCreator<
"AnimalDisease" | "Disease",
{
getInstanceById: GetInstanceById<"AnimalType" | "DerivedCharacteristic">
getInstanceById: GetInstanceById<
"Publication" | "AnimalType" | "DerivedCharacteristic"
>
idMap: IdMap
}
>(
+19 -1
View File
@@ -1,5 +1,9 @@
import { mapNullable } from "@elyukai/utils/nullable"
import { sign } from "@elyukai/utils/string/number"
import type {
ActivatableIdentifier,
RatedIdentifier,
} from "optolith-database-schema/gen"
import { createEntityDescriptionCreator } from "../creator.js"
import type { GetInstanceById } from "../helpers/getTypes.js"
import {
@@ -16,7 +20,21 @@ import { parensIf } from "./partial/rated/activatable/parensIf.js"
export const getElixirEntityDescription = createEntityDescriptionCreator<
"Elixir",
{
getInstanceById: GetInstanceById<"DerivedCharacteristic">
getInstanceById: GetInstanceById<
| "Publication"
| "DerivedCharacteristic"
| ActivatableIdentifier["kind"]
| RatedIdentifier["kind"]
| "Race"
| "Culture"
| "State"
| "Enhancement"
| "PactCategory"
| "PactDomain"
| "SocialStatus"
| "Aspect"
| "Property"
>
getResolvedSelectOptionById: GetResolvedSelectOptionById
}
>(
+5
View File
@@ -884,6 +884,7 @@ export const getEquipmentEntityDescription = createEntityDescriptionCreator<
Exclude<EquipmentIdentifier["kind"], "Elixir" | "Poison">,
{
getInstanceById: GetInstanceById<
| "Publication"
| "Attribute"
| "Reach"
| "SocialStatus"
@@ -892,6 +893,10 @@ export const getEquipmentEntityDescription = createEntityDescriptionCreator<
| "MagicalTradition"
| "BlessedTradition"
| "DerivedCharacteristic"
| "Ammunition"
| "Race"
| "Culture"
| "Profession"
>
idMap: IdMap
}
+1 -1
View File
@@ -316,7 +316,7 @@ export const getEquipmentPackageEntityDescription =
"EquipmentPackage",
{
getInstanceById: GetInstanceById<
EquipmentIdentifier["kind"] | "SocialStatus"
"Publication" | EquipmentIdentifier["kind"] | "SocialStatus"
>
}
>(({ getInstanceById }, locale, { content }) => {
+1 -1
View File
@@ -7,7 +7,7 @@ import type { GetInstanceById } from "../helpers/getTypes.js"
*/
export const getFocusRuleEntityDescription = createEntityDescriptionCreator<
"FocusRule",
{ getInstanceById: GetInstanceById<"Subject"> }
{ getInstanceById: GetInstanceById<"Publication" | "Subject"> }
>(({ getInstanceById }, { translate, translateMap }, { content: entry }) => {
const translation = translateMap(entry.translations)
const topicTranslation = translateMap(
+87 -97
View File
@@ -4,22 +4,24 @@ import { assertExhaustive } from "@optolith/helpers/typeSafety"
import { type LiturgyTradition } from "optolith-database-schema/gen"
import { createEntityDescriptionCreator } from "../creator.js"
import type { GetInstanceById } from "../helpers/getTypes.js"
import { Translate, TranslateMap } from "../helpers/translate.js"
import { EntityDescriptionSection, type IdMap } from "../index.js"
import { getSlowCastingTimeTranslation } from "./partial/rated/activatable/castingTime.js"
import { getDurationTranslationForBlessing } from "./partial/rated/activatable/duration.js"
import { getTextForEffect } from "./partial/rated/activatable/effect.js"
import { Entity } from "./partial/rated/activatable/entity.js"
import {
getFastOneTimePerformanceParametersTranslations,
getFastSustainedPerformanceParametersTranslations,
getSlowOneTimePerformanceParametersTranslations,
getSlowSustainedPerformanceParametersTranslations,
Translate,
TranslateMap,
type TranslationKeysWithoutParams,
} from "../helpers/translate.js"
import { EntityDescriptionSection, type IdMap } from "../index.js"
import { renderOneTimeDuration } from "./partial/rated/activatable/duration.js"
import { renderEffect } from "./partial/rated/activatable/effect.js"
import {
renderFastPerformanceParameters,
renderSlowPerformanceParameters,
} from "./partial/rated/activatable/index.js"
import { getTextForBlessingRange } from "./partial/rated/activatable/range.js"
import { getTargetCategoryTranslation } from "./partial/rated/activatable/targetCategory.js"
import { createImprovementCost } from "./partial/rated/improvementCost.js"
import { getTextForCheck } from "./partial/rated/skillCheck.js"
import { ModifiableParameter } from "./partial/rated/activatable/nonModifiableSuffix.js"
import { renderNonModifiableRange } from "./partial/rated/activatable/range.js"
import { renderTargetCategory } from "./partial/rated/activatable/targetCategory.js"
import { renderImprovementCost } from "./partial/rated/improvementCost.js"
import { renderSkillCheckWithPenalty } from "./partial/rated/skillCheck.js"
import type { EnvMap } from "./partial/reader.js"
import { ResponsiveTextSize } from "./partial/responsiveText.js"
const getTextForTraditions = (
@@ -83,7 +85,7 @@ const getTextForTraditions = (
export const getBlessingEntityDescription = createEntityDescriptionCreator<
"Blessing",
{
getInstanceById: GetInstanceById<"TargetCategory">
getInstanceById: GetInstanceById<"Publication" | "TargetCategory">
}
>(({ getInstanceById }, locale, { content: entry }) => {
const { translate, translateMap } = locale
@@ -93,18 +95,15 @@ export const getBlessingEntityDescription = createEntityDescriptionCreator<
return undefined
}
const range = getTextForBlessingRange(
translate,
ResponsiveTextSize.Full,
entry.parameters.range,
)
const duration = getDurationTranslationForBlessing(
const env = {
translate,
translateMap,
ResponsiveTextSize.Full,
entry.parameters.duration,
)
getInstanceById,
responsiveTextSize: ResponsiveTextSize.Full,
} satisfies Partial<EnvMap>
const range = renderNonModifiableRange(entry.parameters.range, false).run(env)
const duration = renderOneTimeDuration(entry.parameters.duration).run(env)
return {
title: translation.name,
@@ -128,7 +127,7 @@ export const getBlessingEntityDescription = createEntityDescriptionCreator<
? `***${duration}*** (${translation.duration})`
: duration,
},
getTargetCategoryTranslation(getInstanceById, locale, entry.target),
renderTargetCategory(entry.target).run(env),
],
errata: translation.errata,
references: entry.src,
@@ -143,6 +142,7 @@ export const getLiturgicalChantEntityDescription =
"LiturgicalChant",
{
getInstanceById: GetInstanceById<
| "Publication"
| "Attribute"
| "SkillModificationLevel"
| "TargetCategory"
@@ -160,46 +160,42 @@ export const getLiturgicalChantEntityDescription =
return undefined
}
const { castingTime, cost, range, duration } = (() => {
switch (entry.parameters.kind) {
case "OneTime":
return getFastOneTimePerformanceParametersTranslations(
getInstanceById,
locale,
Entity.LiturgicalChant,
ResponsiveTextSize.Full,
entry.parameters.OneTime,
)
const env = {
translate,
translateMap,
getInstanceById,
localeJoin: locale.join,
energyUnit: "KarmaPoints",
responsiveTextSize: ResponsiveTextSize.Full,
nonModifiableSuffix: (
param: ModifiableParameter,
): TranslationKeysWithoutParams => {
switch (param) {
case ModifiableParameter.CastingTime:
return " (you cannot use a modification on this chants liturgical time)"
case ModifiableParameter.Cost:
return " (you cannot use a modification on this chants cost)"
case ModifiableParameter.Range:
return " (you cannot use a modification on this chants range)"
default:
return assertExhaustive(param)
}
},
} satisfies Partial<EnvMap>
case "Sustained":
return getFastSustainedPerformanceParametersTranslations(
getInstanceById,
locale,
Entity.LiturgicalChant,
ResponsiveTextSize.Full,
entry.parameters.Sustained,
)
default:
return assertExhaustive(entry.parameters)
}
})()
const { castingTime, cost, range, duration } =
renderFastPerformanceParameters(entry.parameters).run(env)
return {
title: translation.name,
className: "liturgical-chant",
body: [
getTextForCheck(
{ translate, translateMap, getInstanceById },
renderSkillCheckWithPenalty(
entry.check,
{
value: entry.check_penalty,
responsiveText: ResponsiveTextSize.Full,
getInstanceById,
idMap,
},
),
...getTextForEffect(locale, translation.effect),
entry.check_penalty,
idMap,
).run(env),
...renderEffect(translation.effect).run(env),
{
label: translate("Liturgical Time"),
value:
@@ -229,7 +225,7 @@ export const getLiturgicalChantEntityDescription =
? `***${duration}*** (${translation.duration.full})`
: duration,
},
getTargetCategoryTranslation(getInstanceById, locale, entry.target),
renderTargetCategory(entry.target).run(env),
getTextForTraditions(
{
translate,
@@ -239,7 +235,7 @@ export const getLiturgicalChantEntityDescription =
},
entry.traditions,
),
createImprovementCost(translate, entry.improvement_cost),
renderImprovementCost(entry.improvement_cost).run(env),
],
errata: translation.errata,
references: entry.src,
@@ -253,6 +249,7 @@ export const getCeremonyEntityDescription = createEntityDescriptionCreator<
"Ceremony",
{
getInstanceById: GetInstanceById<
| "Publication"
| "Attribute"
| "SkillModificationLevel"
| "TargetCategory"
@@ -270,47 +267,40 @@ export const getCeremonyEntityDescription = createEntityDescriptionCreator<
return undefined
}
const { castingTime, cost, range, duration } = (() => {
switch (entry.parameters.kind) {
case "OneTime":
return getSlowOneTimePerformanceParametersTranslations(
getInstanceById,
locale,
Entity.Ritual,
ResponsiveTextSize.Full,
entry.parameters.OneTime,
getSlowCastingTimeTranslation,
)
const env = {
translate,
translateMap,
getInstanceById,
localeJoin: locale.join,
energyUnit: "KarmaPoints",
responsiveTextSize: ResponsiveTextSize.Full,
nonModifiableSuffix: (
param: ModifiableParameter,
): TranslationKeysWithoutParams => {
switch (param) {
case ModifiableParameter.CastingTime:
return " (you cannot use a modification on this ceremonys ceremonial time)"
case ModifiableParameter.Cost:
return " (you cannot use a modification on this ceremonys cost)"
case ModifiableParameter.Range:
return " (you cannot use a modification on this ceremonys range)"
default:
return assertExhaustive(param)
}
},
} satisfies Partial<EnvMap>
case "Sustained":
return getSlowSustainedPerformanceParametersTranslations(
getInstanceById,
locale,
Entity.Ceremony,
ResponsiveTextSize.Full,
entry.parameters.Sustained,
)
default:
return assertExhaustive(entry.parameters)
}
})()
const { castingTime, cost, range, duration } =
renderSlowPerformanceParameters(entry.parameters).run(env)
return {
title: translation.name,
className: "ceremony",
body: [
getTextForCheck(
{ translate, translateMap, getInstanceById },
entry.check,
{
value: entry.check_penalty,
responsiveText: ResponsiveTextSize.Full,
getInstanceById,
idMap,
},
renderSkillCheckWithPenalty(entry.check, entry.check_penalty, idMap).run(
env,
),
...getTextForEffect(locale, translation.effect),
...renderEffect(translation.effect).run(env),
{
label: translate("Ceremonial Time"),
value:
@@ -340,7 +330,7 @@ export const getCeremonyEntityDescription = createEntityDescriptionCreator<
? `***${duration}*** (${translation.duration.full})`
: duration,
},
getTargetCategoryTranslation(getInstanceById, locale, entry.target),
renderTargetCategory(entry.target).run(env),
getTextForTraditions(
{
translate,
@@ -350,7 +340,7 @@ export const getCeremonyEntityDescription = createEntityDescriptionCreator<
},
entry.traditions,
),
createImprovementCost(translate, entry.improvement_cost),
renderImprovementCost(entry.improvement_cost).run(env),
],
errata: translation.errata,
references: entry.src,
+56 -49
View File
@@ -1,4 +1,5 @@
import { mapNullableDefault } from "@elyukai/utils/nullable"
import { Reader } from "@elyukai/utils/reader"
import { isNotNullish, mapNullable } from "@optolith/helpers/nullable"
import type { ResponsiveTextOptional } from "optolith-database-schema/gen"
import type {
@@ -6,10 +7,8 @@ import type {
Translate,
TranslateMap,
} from "../../helpers/translate.js"
import {
getResponsiveTextOptional,
type ResponsiveTextSize,
} from "./responsiveText.js"
import { type StdReader } from "./reader.js"
import { getResponsiveTextOptional } from "./responsiveText.js"
import { MISSING_VALUE } from "./unknown.js"
/**
@@ -95,62 +94,70 @@ export const renderMap = <
* Renders a map of options to a string.
*/
export const renderResponsiveMap = <
M extends { options: MO[]; translations?: LocaleMap<MT> },
MOT = M extends { options: { translations?: LocaleMap<infer MOT_> }[] }
? MOT_
: never,
MT = M extends { translations?: LocaleMap<infer MT_> } ? MT_ : never,
MO extends { translations?: LocaleMap<MOT> } = M extends {
options: (infer MO_ & { translations?: LocaleMap<MOT> })[]
}
? MO_
: never,
M extends {
options: MO[]
translations?: LocaleMap<{
list_prepend?: ResponsiveTextOptional
listPrefix?: ResponsiveTextOptional
list_append?: ResponsiveTextOptional
listSuffix?: ResponsiveTextOptional
replacement?: ResponsiveTextOptional
}>
},
MO extends { translations?: LocaleMap<{ label: ResponsiveTextOptional }> } =
M extends {
options: (infer MO_)[]
}
? MO_
: never,
>(
translate: Translate,
translateMap: TranslateMap,
responsiveTextSize: ResponsiveTextSize,
map: M,
getValue: (option: MO) => string | number,
surroundValues: ((values: string) => string) | undefined,
getLabel: (optionTranslation: MOT) => ResponsiveTextOptional,
getListPrefix: (translation: MT) => ResponsiveTextOptional | undefined,
getListSuffix: (translation: MT) => ResponsiveTextOptional | undefined,
getReplacement: (translation: MT) => ResponsiveTextOptional | undefined,
...moreToAppend: (
| {
surround: (values: string) => string
getAdditionalValue: (
option: MO,
optionTranslation: MOT,
optionTranslation: { label: ResponsiveTextOptional },
) => string | number
}
| undefined
)[]
): string =>
renderMap<M, MOT, MT, MO>(
translate,
translateMap,
map,
getValue,
surroundValues,
translation => {
const responsiveLabel = getLabel(translation)
return (
getResponsiveTextOptional(responsiveLabel, responsiveTextSize) ??
responsiveLabel.full
)
},
translation =>
mapNullable(getListPrefix(translation), text =>
getResponsiveTextOptional(text, responsiveTextSize),
),
translation =>
mapNullable(getListSuffix(translation), text =>
getResponsiveTextOptional(text, responsiveTextSize),
),
translation =>
mapNullable(getReplacement(translation), text =>
getResponsiveTextOptional(text, responsiveTextSize),
),
...moreToAppend,
): StdReader<string, "t" | "tm" | "rts"> =>
Reader.asks(({ translate, translateMap, responsiveTextSize }) =>
renderMap<
M,
{ label: ResponsiveTextOptional },
{
list_prepend?: ResponsiveTextOptional
listPrefix?: ResponsiveTextOptional
list_append?: ResponsiveTextOptional
listSuffix?: ResponsiveTextOptional
replacement?: ResponsiveTextOptional
},
MO
>(
translate,
translateMap,
map,
getValue,
surroundValues,
translation =>
getResponsiveTextOptional(translation.label, responsiveTextSize) ??
translation.label.full,
translation =>
mapNullable(translation.listPrefix ?? translation.list_prepend, text =>
getResponsiveTextOptional(text, responsiveTextSize),
),
translation =>
mapNullable(translation.listSuffix ?? translation.list_append, text =>
getResponsiveTextOptional(text, responsiveTextSize),
),
translation =>
mapNullable(translation.replacement, text =>
getResponsiveTextOptional(text, responsiveTextSize),
),
...moreToAppend,
),
)
+8 -3
View File
@@ -29,7 +29,6 @@ import {
type ActivatableIdentifier,
type GeneralPrerequisiteGroup,
type RatedIdentifier,
type SkillWithEnhancementsIdentifier,
} from "optolith-database-schema/gen"
import type { GetInstanceById } from "../../../helpers/getTypes.js"
import { LocaleEnvironment } from "../../../helpers/locale.js"
@@ -268,12 +267,14 @@ export const printPlainGeneralPrerequisites = (
| "Race"
| "Culture"
| "PactCategory"
| "PactDomain"
| "SocialStatus"
| "State"
| ActivatableIdentifier["kind"]
| RatedIdentifier["kind"]
| "Property"
| "Aspect"
| "Enhancement"
>,
getResolvedSelectOptionById: GetResolvedSelectOptionById,
locale: LocaleEnvironment,
@@ -299,12 +300,14 @@ export const printGeneralPrerequisites = (
| "Race"
| "Culture"
| "PactCategory"
| "PactDomain"
| "SocialStatus"
| "State"
| ActivatableIdentifier["kind"]
| RatedIdentifier["kind"]
| "Property"
| "Aspect"
| "Enhancement"
>,
getResolvedSelectOptionById: GetResolvedSelectOptionById,
locale: LocaleEnvironment,
@@ -374,12 +377,14 @@ export const printAdvantageDisadvantagePrerequisites = (
| "Race"
| "Culture"
| "PactCategory"
| "PactDomain"
| "SocialStatus"
| "State"
| ActivatableIdentifier["kind"]
| RatedIdentifier["kind"]
| "Property"
| "Aspect"
| "Enhancement"
>,
getResolvedSelectOptionById: GetResolvedSelectOptionById,
locale: LocaleEnvironment,
@@ -424,7 +429,7 @@ export const printArcaneTraditionPrerequisites = (
* Print personality trait prerequisites as a string.
*/
export const printPersonalityTraitPrerequisites = (
getInstanceById: GetInstanceById<"Culture">,
getInstanceById: GetInstanceById<"Race" | "Culture" | "PersonalityTrait">,
locale: LocaleEnvironment,
value: PersonalityTraitPrerequisites,
): string =>
@@ -535,7 +540,7 @@ export const printGeodeRitualPrerequisites = (
* Print enhancement prerequisites as a string.
*/
export const printEnhancementPrerequisites = (
getInstanceById: GetInstanceById<SkillWithEnhancementsIdentifier["kind"]>,
getInstanceById: GetInstanceById<RatedIdentifier["kind"] | "Enhancement">,
locale: LocaleEnvironment,
value: EnhancementPrerequisites,
): string =>
@@ -17,7 +17,6 @@ import {
SpellworkPrerequisiteGroup,
type ActivatableIdentifier,
type RatedIdentifier,
type SkillWithEnhancementsIdentifier,
} from "optolith-database-schema/gen"
import type { GetInstanceById } from "../../../helpers/getTypes.js"
import { LocaleEnvironment } from "../../../helpers/locale.js"
@@ -101,12 +100,14 @@ export const printGeneralPrerequisiteGroup = (
| "Race"
| "Culture"
| "PactCategory"
| "PactDomain"
| "SocialStatus"
| "State"
| ActivatableIdentifier["kind"]
| RatedIdentifier["kind"]
| "Property"
| "Aspect"
| "Enhancement"
>,
getResolvedSelectOptionById: GetResolvedSelectOptionById,
locale: LocaleEnvironment,
@@ -237,12 +238,14 @@ export const printAdvantageDisadvantagePrerequisiteGroup = (
| "Race"
| "Culture"
| "PactCategory"
| "PactDomain"
| "SocialStatus"
| "State"
| ActivatableIdentifier["kind"]
| RatedIdentifier["kind"]
| "Property"
| "Aspect"
| "Enhancement"
>,
getResolvedSelectOptionById: GetResolvedSelectOptionById,
locale: LocaleEnvironment,
@@ -449,7 +452,7 @@ export const printGeodeRitualPrerequisiteGroup = (
* Print the translation of an enhancement prerequisite group.
*/
export const printEnhancementPrerequisiteGroup = (
getInstanceById: GetInstanceById<SkillWithEnhancementsIdentifier["kind"]>,
getInstanceById: GetInstanceById<RatedIdentifier["kind"] | "Enhancement">,
locale: LocaleEnvironment,
prerequisite: EnhancementPrerequisiteGroup,
): PrerequisitePart | undefined => {
@@ -1,3 +1,4 @@
import { Reader } from "@elyukai/utils/reader"
import { isNotNullish, mapNullable } from "@optolith/helpers/nullable"
import { assertExhaustive } from "@optolith/helpers/typeSafety"
import {
@@ -9,192 +10,133 @@ import {
ModifiableCastingTime,
SlowCastingTime,
SlowSkillNonModifiableCastingTime,
type DurationUnitValue,
type SkillModificationLevel_ID,
} from "optolith-database-schema/gen"
import { Case } from "../../../../helpers/enums.js"
import type { GetInstanceById } from "../../../../helpers/getTypes.js"
import type { Translate } from "../../../../helpers/translate.js"
import { ResponsiveTextSize } from "../../responsiveText.js"
import { formatTimeSpan } from "../../units/timeSpan.js"
import { MISSING_VALUE } from "../../unknown.js"
import { Entity } from "./entity.js"
import {
getNonModifiableSuffixTranslation,
getInstanceByIdR,
modifiableBySpeedR,
type StdReader,
} from "../../reader.js"
import {
formatCombinedTimeSpanR,
formatTimeSpanR,
} from "../../units/timeSpan.js"
import { MISSING_VALUE } from "../../unknown.js"
import {
appendNonModifiableSuffix,
ModifiableParameter,
} from "./nonModifiableSuffix.js"
import { getMapModifiableBySpeed, Speed } from "./speed.js"
import { Speed } from "./speed.js"
const getModifiableCastingTimeTranslation = (
getInstanceById: GetInstanceById<"SkillModificationLevel">,
translate: Translate,
speed: Speed,
responsiveTextSize: ResponsiveTextSize,
const deriveModifiableCastingTime = (
modificationLevelId: SkillModificationLevel_ID,
): StdReader<
DurationUnitValue | undefined,
"s" | "ibi",
"SkillModificationLevel"
> =>
getInstanceByIdR<"SkillModificationLevel">().thenW(
getInstanceById =>
mapNullable(
getInstanceById("SkillModificationLevel", modificationLevelId),
modificationLevel =>
modifiableBySpeedR("casting_time", modificationLevel).map(
castingTime =>
typeof castingTime === "number"
? { value: castingTime, unit: Case("Actions") }
: castingTime,
),
) ?? Reader.of(undefined),
)
const renderModifiableCastingTime = (
value: ModifiableCastingTime,
): string =>
mapNullable(
getInstanceById("SkillModificationLevel", value.initial_modification_level),
modificationLevel =>
getMapModifiableBySpeed(
config =>
formatTimeSpan(
translate,
responsiveTextSize,
Case("Actions"),
config.casting_time,
),
config =>
formatTimeSpan(
translate,
responsiveTextSize,
config.casting_time.unit,
config.casting_time.value,
),
speed,
modificationLevel,
),
) ?? MISSING_VALUE
): StdReader<string, "t" | "rts" | "s" | "ibi", "SkillModificationLevel"> =>
deriveModifiableCastingTime(value.initial_modification_level).thenW(
castingTime =>
castingTime === undefined
? Reader.of(MISSING_VALUE)
: formatCombinedTimeSpanR(castingTime),
)
const getFastSkillNonModifiableCastingTimeTranslation = (
translate: Translate,
responsiveTextSize: ResponsiveTextSize,
const renderCastingTimeDuringLovemaking = (
value: CastingTimeDuringLovemaking,
): StdReader<string, "t" | "rts"> => formatCombinedTimeSpanR(value)
/**
* Renders the text for a non-modifiable casting time of a fast activatable skill.
*/
export const renderFastSkillNonModifiableCastingTime = (
value: FastSkillNonModifiableCastingTime,
): string =>
formatTimeSpan(translate, responsiveTextSize, Case("Actions"), value.actions)
): StdReader<string, "t" | "rts"> =>
formatTimeSpanR(Case("Actions"), value.actions)
/**
* Get the text for a non-modifiable casting time of a slow activatable skill.
*/
export const getSlowSkillNonModifiableCastingTimeTranslation = (
translate: Translate,
responsiveTextSize: ResponsiveTextSize,
export const renderSlowSkillNonModifiableCastingTime = (
value: SlowSkillNonModifiableCastingTime,
): string =>
formatTimeSpan(translate, responsiveTextSize, value.unit, value.value)
): StdReader<string, "t" | "rts"> => formatCombinedTimeSpanR(value)
/**
* Translate casting time.
*/
export const getCastingTimeTranslation = <NonModifiable extends object>(
getNonModifiableCastingTimeTranslation: (
translate: Translate,
responsiveTextSize: ResponsiveTextSize,
export const renderCastingTime = <NonModifiable extends object>(
renderNonModifiableCastingTime: (
value: NonModifiable,
) => string,
speed: Speed,
getInstanceById: GetInstanceById<"SkillModificationLevel">,
translate: Translate,
entity: Entity,
responsiveTextSize: ResponsiveTextSize,
) => StdReader<string, "t" | "rts" | "s" | "nms">,
value: CastingTime<NonModifiable>,
): string => {
): StdReader<
string,
"t" | "rts" | "s" | "nms" | "ibi",
"SkillModificationLevel"
> => {
switch (value.kind) {
case "Modifiable":
return getModifiableCastingTimeTranslation(
getInstanceById,
translate,
speed,
responsiveTextSize,
value.Modifiable,
)
return renderModifiableCastingTime(value.Modifiable)
case "NonModifiable":
return (
getNonModifiableCastingTimeTranslation(
translate,
responsiveTextSize,
value.NonModifiable,
) +
getNonModifiableSuffixTranslation(
translate,
entity,
ModifiableParameter.CastingTime,
responsiveTextSize,
)
return renderNonModifiableCastingTime(value.NonModifiable).then(base =>
appendNonModifiableSuffix(ModifiableParameter.CastingTime, base),
)
default:
return assertExhaustive(value)
}
}
const getCastingTimeDuringLovemakingTranslation = (
translate: Translate,
responsiveTextSize: ResponsiveTextSize,
value: CastingTimeDuringLovemaking,
): string =>
formatTimeSpan(translate, responsiveTextSize, value.unit, value.value)
const getCastingTimeIncludingLovemakingTranslation = <
NonModifiable extends object,
>(
getNonModifiableCastingTimeTranslation: (
translate: Translate,
responsiveTextSize: ResponsiveTextSize,
const renderCastingTimeIncludingLovemaking = <NonModifiable extends object>(
renderNonModifiableCastingTime: (
value: NonModifiable,
) => string,
getInstanceById: GetInstanceById<"SkillModificationLevel">,
translate: Translate,
speed: Speed,
entity: Entity,
responsiveTextSize: ResponsiveTextSize,
) => StdReader<string, "t" | "rts" | "s" | "nms">,
value: CastingTimeIncludingLovemaking<NonModifiable>,
) =>
[
Reader.sequence([
mapNullable(value.default, def =>
getCastingTimeTranslation(
getNonModifiableCastingTimeTranslation,
speed,
getInstanceById,
translate,
entity,
responsiveTextSize,
def,
),
),
mapNullable(value.during_lovemaking, duringLovemaking =>
getCastingTimeDuringLovemakingTranslation(
translate,
responsiveTextSize,
duringLovemaking,
),
),
]
.filter(isNotNullish)
.join(" / ")
renderCastingTime(renderNonModifiableCastingTime, def),
) ?? Reader.of(undefined),
mapNullable(value.during_lovemaking, renderCastingTimeDuringLovemaking) ??
Reader.of(undefined),
]).map(texts => texts.filter(isNotNullish).join(" / "))
/**
* Get the text for the casting time of a fast activatable skill.
*/
export const getFastCastingTimeTranslation = (
getInstanceById: GetInstanceById<"SkillModificationLevel">,
translate: Translate,
entity: Entity,
responsiveTextSize: ResponsiveTextSize,
export const renderFastCastingTime = (
value: FastCastingTime,
): string =>
getCastingTimeIncludingLovemakingTranslation(
getFastSkillNonModifiableCastingTimeTranslation,
getInstanceById,
translate,
Speed.Fast,
entity,
responsiveTextSize,
): StdReader<string, "t" | "rts" | "nms" | "ibi", "SkillModificationLevel"> =>
renderCastingTimeIncludingLovemaking(
renderFastSkillNonModifiableCastingTime,
value,
)
).with(env => ({ ...env, speed: Speed.Fast }))
/**
* Get the text for the casting time of a slow activatable skill.
*/
export const getSlowCastingTimeTranslation = (
getInstanceById: GetInstanceById<"SkillModificationLevel">,
translate: Translate,
entity: Entity,
responsiveTextSize: ResponsiveTextSize,
export const renderSlowCastingTime = (
value: SlowCastingTime,
): string =>
getCastingTimeIncludingLovemakingTranslation(
getSlowSkillNonModifiableCastingTimeTranslation,
getInstanceById,
translate,
Speed.Slow,
entity,
responsiveTextSize,
): StdReader<string, "t" | "rts" | "nms" | "ibi", "SkillModificationLevel"> =>
renderCastingTimeIncludingLovemaking(
renderSlowSkillNonModifiableCastingTime,
value,
)
).with(env => ({ ...env, speed: Speed.Slow }))
@@ -5,21 +5,18 @@ import type {
CheckResultBasedModifier,
CheckResultValue,
} from "optolith-database-schema/gen"
import { Translate } from "../../../../helpers/translate.js"
import {
divisionFormatter,
multiplicationFormatter,
} from "../../mathOperation.js"
import { translateR, type StdReader } from "../../reader.js"
const getCheckResultBaseValueTranslation = (
translate: Translate,
baseValue: CheckResultValue,
) => {
const renderCheckResultBaseValue = (baseValue: CheckResultValue) => {
switch (baseValue.kind) {
case "QualityLevels":
return translate("QL")
return translateR("QL")
case "SkillPoints":
return translate("SP")
return translateR("SP")
default:
return assertExhaustive(baseValue)
}
@@ -56,21 +53,20 @@ export const appendCheckResultModifier = (
modifier: CheckResultBasedModifier,
) => {
const formatter = getArithmeticFormatter(modifier.arithmetic)
return formatter(left, modifier.value.toString())
return formatter(left, modifier.value)
}
/**
* Returns the value text for a check-result-based parameter of an activatable
* skill.
*/
export const getCheckResultBasedValueTranslation = (
translate: Translate,
export const renderCheckResultBasedValue = (
value: CheckResultBased,
): string => {
const base = getCheckResultBaseValueTranslation(translate, value.base)
return mapNullableDefault(
value.modifier,
modifier => appendCheckResultModifier(base, modifier),
base,
): StdReader<string, "t"> =>
renderCheckResultBaseValue(value.base).map(base =>
mapNullableDefault(
value.modifier,
modifier => appendCheckResultModifier(base, modifier),
base,
),
)
}
File diff suppressed because it is too large Load Diff
+150 -262
View File
@@ -1,204 +1,156 @@
import { Reader } from "@elyukai/utils/reader"
import { mapNullable } from "@optolith/helpers/nullable"
import { assertExhaustive } from "@optolith/helpers/typeSafety"
import type {
BlessingDuration,
CantripDuration,
CastingTimeDuringLovemaking,
CheckResultBasedDuration,
DurationForSustained,
FixedDuration,
Immediate,
IndefiniteBlessingDuration,
IndefiniteBlessingDurationTranslation,
IndefiniteDuration,
IndefiniteDurationTranslation,
MusicDuration,
PermanentDuration,
ResponsiveText,
} from "optolith-database-schema/gen"
import type { Translate, TranslateMap } from "../../../../helpers/translate.js"
import type { LocaleMap } from "../../../../helpers/translate.js"
import {
getResponsiveText,
replaceTextIfRequested,
responsive,
ResponsiveTextSize,
} from "../../responsiveText.js"
import { formatTimeSpan } from "../../units/timeSpan.js"
import { getCheckResultBasedValueTranslation } from "./checkResultBased.js"
responsiveTextR,
responsiveTranslateR,
translateMapR,
translateR,
type StdReader,
} from "../../reader.js"
import { replaceTextIfNeeded } from "../../responsiveText.js"
import {
formatCombinedTimeSpanR,
formatTimeSpanR,
} from "../../units/timeSpan.js"
import { MISSING_VALUE } from "../../unknown.js"
import { renderCheckResultBasedValue } from "./checkResultBased.js"
import { wrapAsMaximum, wrapIfMaximum } from "./isMinimumMaximum.js"
import { appendInParensIfNotEmpty } from "./parensIf.js"
const getImmediateDurationTranslation = (
translate: Translate,
translateMap: TranslateMap,
responsiveTextSize: ResponsiveTextSize,
const renderImmediateDuration = (
value?: Immediate,
): string => {
const text = appendInParensIfNotEmpty(
mapNullable(value?.maximum, max => {
const maxText = formatTimeSpan(
translate,
responsiveTextSize,
max.unit,
max.value,
)
): StdReader<string, "t" | "tm" | "rts"> =>
translateR("Immediate")
.thenW(
base =>
mapNullable(value?.maximum, max =>
formatTimeSpanR(max.unit, max.value)
.then(wrapAsMaximum)
.map(maxText => appendInParensIfNotEmpty(maxText, base)),
) ?? Reader.of(base),
)
.thenW(text => replaceTextIfNeeded(value?.translations, text))
return responsive(
responsiveTextSize,
() => translate("no more than {$value}", { value: maxText }),
() => translate("max. {$value}", { value: maxText }),
)
}),
translate("Immediate"),
)
return replaceTextIfRequested(
"replacement",
value?.translations,
translateMap,
responsiveTextSize,
text,
)
}
const getPermanentDurationTranslation = (
translate: Translate,
translateMap: TranslateMap,
responsiveTextSize: ResponsiveTextSize,
const renderPermanentDuration = (
value: PermanentDuration,
): string =>
replaceTextIfRequested(
"replacement",
value.translations,
translateMap,
responsiveTextSize,
translate("Permanent"),
): StdReader<string, "t" | "tm" | "rts"> =>
translateR("Permanent").thenW(text =>
replaceTextIfNeeded(value.translations, text),
)
const getFixedDurationTranslation = (
translate: Translate,
translateMap: TranslateMap,
responsiveTextSize: ResponsiveTextSize,
const renderFixedDuration = (
value: FixedDuration,
): string => {
const duration = formatTimeSpan(
translate,
responsiveTextSize,
value.unit,
value.value,
)
): StdReader<string, "t" | "tm" | "rts"> =>
formatCombinedTimeSpanR(value)
.then(text => wrapIfMaximum(value.is_maximum, text))
.thenW(text => replaceTextIfNeeded(value.translations, text))
const durationWrappedIfMaximum = wrapIfMaximum(
translate,
responsiveTextSize,
value.is_maximum,
duration,
)
return replaceTextIfRequested(
"replacement",
value.translations,
translateMap,
responsiveTextSize,
durationWrappedIfMaximum,
)
}
const getCheckResultBasedDurationTranslation = (
translate: Translate,
responsiveTextSize: ResponsiveTextSize,
const renderCheckResultBasedDuration = (
value: CheckResultBasedDuration,
): string => {
const duration = formatTimeSpan(
translate,
responsiveTextSize,
value.unit,
getCheckResultBasedValueTranslation(translate, value),
)
): StdReader<string, "t" | "tm" | "rts"> =>
renderCheckResultBasedValue(value)
.thenW(text => formatTimeSpanR(value.unit, text))
.then(text => wrapIfMaximum(value.is_maximum, text))
return wrapIfMaximum(
translate,
responsiveTextSize,
value.is_maximum,
duration,
)
const renderIndefiniteDuration = (value: {
maximum?: OneTimeDuration
translations: LocaleMap<{
description: ResponsiveText | string
}>
}): StdReader<string, "t" | "tm" | "rts"> => {
const { maximum, translations } = value
return translateMapR<{
description: ResponsiveText | string
}>(translations)
.thenW(translation =>
typeof translation?.description === "object"
? responsiveTextR(translation.description)
: Reader.of(translation?.description ?? MISSING_VALUE),
)
.thenW(
maximum === undefined
? Reader.of
: text =>
// eslint-disable-next-line @typescript-eslint/no-use-before-define
renderOneTimeDuration(maximum).then(maximumText =>
translateR(
"{$defaultDuration}, but no more than {$maximumDuration}",
{
defaultDuration: text,
maximumDuration: maximumText,
},
),
),
)
}
const getIndefiniteDurationTranslation = (
translateMap: TranslateMap,
responsiveTextSize: ResponsiveTextSize,
value: IndefiniteDuration | IndefiniteBlessingDuration,
) => {
const description = translateMap<
IndefiniteDurationTranslation | IndefiniteBlessingDurationTranslation
>(value.translations)?.description
const renderDurationDuringLovemaking = (
value: CastingTimeDuringLovemaking,
): StdReader<string, "t" | "rts"> => formatCombinedTimeSpanR(value)
return typeof description === "string"
? description
: getResponsiveText(description, responsiveTextSize)
}
type OneTimeDuration =
| {
kind: "Immediate"
Immediate?: Immediate
}
| {
kind: "Permanent"
Permanent: PermanentDuration
}
| {
kind: "Fixed"
Fixed: FixedDuration
}
| {
kind: "CheckResultBased"
CheckResultBased: CheckResultBasedDuration
}
| {
kind: "Indefinite"
Indefinite: {
maximum?: OneTimeDuration
translations: LocaleMap<{
description: ResponsiveText | string
}>
}
}
// used for cantrips:
| {
kind: "DuringLovemaking"
DuringLovemaking: CastingTimeDuringLovemaking
}
/**
* Returns the text for the duration of a one-time activatable skill.
*/
export const getDurationForOneTimeTranslation = (
translate: Translate,
translateMap: TranslateMap,
responsiveTextSize: ResponsiveTextSize,
value:
| {
kind: "Immediate"
Immediate?: Immediate
}
| {
kind: "Permanent"
Permanent: PermanentDuration
}
| {
kind: "Fixed"
Fixed: FixedDuration
}
| {
kind: "CheckResultBased"
CheckResultBased: CheckResultBasedDuration
}
| {
kind: "Indefinite"
Indefinite: IndefiniteDuration
},
): string => {
export const renderOneTimeDuration = (
value: OneTimeDuration,
): StdReader<string, "t" | "tm" | "rts"> => {
switch (value.kind) {
case "Immediate":
return getImmediateDurationTranslation(
translate,
translateMap,
responsiveTextSize,
value.Immediate,
)
return renderImmediateDuration(value.Immediate)
case "Permanent":
return getPermanentDurationTranslation(
translate,
translateMap,
responsiveTextSize,
value.Permanent,
)
return renderPermanentDuration(value.Permanent)
case "Fixed":
return getFixedDurationTranslation(
translate,
translateMap,
responsiveTextSize,
value.Fixed,
)
return renderFixedDuration(value.Fixed)
case "CheckResultBased":
return getCheckResultBasedDurationTranslation(
translate,
responsiveTextSize,
value.CheckResultBased,
)
return renderCheckResultBasedDuration(value.CheckResultBased)
case "Indefinite":
return getIndefiniteDurationTranslation(
translateMap,
responsiveTextSize,
value.Indefinite,
)
return renderIndefiniteDuration(value.Indefinite)
case "DuringLovemaking":
return renderDurationDuringLovemaking(value.DuringLovemaking)
default:
return assertExhaustive(value)
}
@@ -207,107 +159,43 @@ export const getDurationForOneTimeTranslation = (
/**
* Returns the text for the duration of a sustained activatable skill.
*/
export const getDurationForSustainedTranslation = (
translate: Translate,
responsiveTextSize: ResponsiveTextSize,
export const renderSustainedDuration = (
value: DurationForSustained | undefined,
): string =>
): StdReader<string, "t" | "rts"> =>
value === undefined
? responsive(
responsiveTextSize,
() => translate("Sustained"),
() => translate("(S)"),
? responsiveTranslateR("Sustained", "(S)")
: formatCombinedTimeSpanR(value.maximum).then(maxText =>
wrapAsMaximum(maxText),
)
: wrapAsMaximum(
translate,
responsiveTextSize,
formatTimeSpan(
translate,
responsiveTextSize,
value.maximum.unit,
value.maximum.value,
),
)
const getDurationDuringLovemakingTranslation = (
translate: Translate,
responsiveTextSize: ResponsiveTextSize,
value: CastingTimeDuringLovemaking,
): string =>
formatTimeSpan(translate, responsiveTextSize, value.unit, value.value)
/**
* Returns the text for the duration of a cantrip.
* Returns the text for the duration of a musical activatable skill.
*/
export const getDurationTranslationForCantrip = (
translate: Translate,
translateMap: TranslateMap,
responsiveTextSize: ResponsiveTextSize,
value: CantripDuration,
): string => {
switch (value.kind) {
case "Immediate":
return getImmediateDurationTranslation(
translate,
translateMap,
responsiveTextSize,
{},
)
case "Fixed":
return getFixedDurationTranslation(
translate,
translateMap,
responsiveTextSize,
value.Fixed,
)
case "Indefinite":
return getIndefiniteDurationTranslation(
translateMap,
responsiveTextSize,
value.Indefinite,
)
case "DuringLovemaking":
return getDurationDuringLovemakingTranslation(
translate,
responsiveTextSize,
value.DuringLovemaking,
)
default:
return assertExhaustive(value)
}
}
export const renderMusicDuration = (
duration: MusicDuration,
): StdReader<string, "t"> =>
Reader.asks(({ translate }) => {
const length = (() => {
switch (duration.length.kind) {
case "Long":
return translate("long")
case "Short":
return translate("short")
default:
return assertExhaustive(duration.length)
}
})()
/**
* Returns the text for the duration of a blessing.
*/
export const getDurationTranslationForBlessing = (
translate: Translate,
translateMap: TranslateMap,
responsiveTextSize: ResponsiveTextSize,
value: BlessingDuration,
): string => {
switch (value.kind) {
case "Immediate":
return getImmediateDurationTranslation(
translate,
translateMap,
responsiveTextSize,
{},
)
case "Fixed":
return getFixedDurationTranslation(
translate,
translateMap,
responsiveTextSize,
value.Fixed,
)
case "Indefinite":
return getIndefiniteDurationTranslation(
translateMap,
responsiveTextSize,
value.Indefinite,
)
default:
return assertExhaustive(value)
}
}
const reusability = (() => {
switch (duration.reusability.kind) {
case "OneTime":
return translate("one-time")
case "Sustainable":
return translate("sustainable")
default:
return assertExhaustive(duration.reusability)
}
})()
return `${length}, ${reusability}`
})
@@ -2,60 +2,58 @@ import { filterNonNullable } from "@optolith/helpers/array"
import { mapNullable } from "@optolith/helpers/nullable"
import { assertExhaustive } from "@optolith/helpers/typeSafety"
import type { ActivatableSkillEffect } from "optolith-database-schema/gen"
import { LocaleEnvironment } from "../../../../helpers/locale.js"
import { EntityDescriptionSection } from "../../../../index.js"
import { translateFnR, type StdReader } from "../../reader.js"
const getContentPartsForQualityLevels = (
getQualityLevelString: (index: number) => string | number,
locale: LocaleEnvironment,
source: {
text_before: string
quality_levels: string[]
text_after?: string
},
): EntityDescriptionSection[] =>
filterNonNullable([
{
label: locale.translate("Effect"),
value: source.text_before,
},
...source.quality_levels.map((text, index) => ({
value: text,
label: locale.translate("QL {$value}", {
value: getQualityLevelString(index),
}),
})),
mapNullable(source.text_after, textAfter => ({
value: textAfter,
className: "effect-after",
})),
])
): StdReader<EntityDescriptionSection[], "t"> =>
translateFnR.map(translate =>
filterNonNullable([
{
label: translate("Effect"),
value: source.text_before,
},
...source.quality_levels.map((text, index) => ({
value: text,
label: translate("QL {$value}", {
value: getQualityLevelString(index),
}),
})),
mapNullable(source.text_after, textAfter => ({
value: textAfter,
className: "effect-after",
})),
]),
)
/**
* Gets the text for the effect of an activatable skill.
*/
export const getTextForEffect = (
locale: LocaleEnvironment,
export const renderEffect = (
effect: ActivatableSkillEffect,
): EntityDescriptionSection[] => {
): StdReader<EntityDescriptionSection[], "t"> => {
switch (effect.kind) {
case "Plain":
return [
return translateFnR.map(translate => [
{
label: locale.translate("Effect"),
label: translate("Effect"),
value: effect.Plain.text,
},
]
])
case "ForEachQualityLevel":
return getContentPartsForQualityLevels(
index => index + 1,
locale,
effect.ForEachQualityLevel,
)
case "ForEachTwoQualityLevels":
return getContentPartsForQualityLevels(
index => `${index * 2 + 1}${index * 2 + 2}`,
locale,
effect.ForEachTwoQualityLevels,
)
default:
@@ -1,11 +0,0 @@
/**
* The entity type of an activatable skill.
*/
export enum Entity {
Cantrip,
Spell,
Ritual,
Blessing,
LiturgicalChant,
Ceremony,
}
+143 -190
View File
@@ -1,218 +1,171 @@
import { assertExhaustive } from "@elyukai/utils/typeSafety"
import type {
FastOneTimePerformanceParameters,
FastSustainedPerformanceParameters,
OneTimePerformanceParameters,
SlowOneTimePerformanceParameters,
SlowSustainedPerformanceParameters,
} from "optolith-database-schema/gen"
import type { GetInstanceById } from "../../../../helpers/getTypes.js"
import { LocaleEnvironment } from "../../../../helpers/locale.js"
import type { Translate } from "../../../../helpers/translate.js"
import { ResponsiveTextSize } from "../../responsiveText.js"
import {
getFastCastingTimeTranslation,
getSlowCastingTimeTranslation,
} from "./castingTime.js"
import {
getOneTimeCostTranslation,
getSustainedCostTranslation,
} from "./cost.js"
import {
getDurationForOneTimeTranslation,
getDurationForSustainedTranslation,
} from "./duration.js"
import { Entity } from "./entity.js"
import { getTextForActivatableSkillRange } from "./range.js"
import type { StdReader } from "../../reader.js"
import { renderFastCastingTime, renderSlowCastingTime } from "./castingTime.js"
import { renderOneTimeCost, renderSustainedCost } from "./cost.js"
import { renderOneTimeDuration, renderSustainedDuration } from "./duration.js"
import { renderRange } from "./range.js"
import { Speed } from "./speed.js"
type RenderedPerformanceParameters = {
castingTime: string
cost: string
range: string
duration: string
}
/**
* Get the texts for all fast one-time performance parameters.
*/
export const getFastOneTimePerformanceParametersTranslations = (
getInstanceById: GetInstanceById<"SkillModificationLevel">,
locale: LocaleEnvironment,
entity: Entity,
responsiveTextSize: ResponsiveTextSize,
export const renderFastOneTimePerformanceParameters = (
value: FastOneTimePerformanceParameters,
): {
castingTime: string
cost: string
range: string
duration: string
} => ({
castingTime: getFastCastingTimeTranslation(
getInstanceById,
locale.translate,
entity,
responsiveTextSize,
value.casting_time,
),
cost: getOneTimeCostTranslation(
getInstanceById,
locale,
Speed.Fast,
entity,
responsiveTextSize,
value.cost,
),
range: getTextForActivatableSkillRange(
getInstanceById,
locale.translate,
locale.translateMap,
Speed.Fast,
responsiveTextSize,
entity,
value.range,
),
duration: getDurationForOneTimeTranslation(
locale.translate,
locale.translateMap,
responsiveTextSize,
value.duration,
),
})
): StdReader<
RenderedPerformanceParameters,
"t" | "tm" | "lj" | "eu" | "rts" | "nms" | "ibi",
"SkillModificationLevel"
> =>
renderFastCastingTime(value.casting_time)
.thenW(castingTime =>
renderOneTimeCost(value.cost).then(cost =>
renderRange(value.range).then(range =>
renderOneTimeDuration(value.duration).map(duration => ({
castingTime,
cost,
range,
duration,
})),
),
),
)
.with(env => ({ ...env, speed: Speed.Fast }))
/**
* Get the texts for all fast sustained performance parameters.
*/
export const getFastSustainedPerformanceParametersTranslations = (
getInstanceById: GetInstanceById<"SkillModificationLevel">,
locale: LocaleEnvironment,
entity: Entity,
responsiveTextSize: ResponsiveTextSize,
export const renderFastSustainedPerformanceParameters = (
value: FastSustainedPerformanceParameters,
): {
castingTime: string
cost: string
range: string
duration: string
} => ({
castingTime: getFastCastingTimeTranslation(
getInstanceById,
locale.translate,
entity,
responsiveTextSize,
value.casting_time,
),
cost: getSustainedCostTranslation(
getInstanceById,
locale,
Speed.Fast,
entity,
responsiveTextSize,
value.cost,
),
range: getTextForActivatableSkillRange(
getInstanceById,
locale.translate,
locale.translateMap,
Speed.Fast,
responsiveTextSize,
entity,
value.range,
),
duration: getDurationForSustainedTranslation(
locale.translate,
responsiveTextSize,
value.duration,
),
})
): StdReader<
RenderedPerformanceParameters,
"t" | "tm" | "lj" | "eu" | "rts" | "nms" | "ibi",
"SkillModificationLevel"
> =>
renderFastCastingTime(value.casting_time)
.thenW(castingTime =>
renderSustainedCost(value.cost).then(cost =>
renderRange(value.range).then(range =>
renderSustainedDuration(value.duration).map(duration => ({
castingTime,
cost,
range,
duration,
})),
),
),
)
.with(env => ({ ...env, speed: Speed.Fast }))
/**
* Renders the performance parameters for a fast activatable skill.
*/
export const renderFastPerformanceParameters = (
value:
| { kind: "OneTime"; OneTime: FastOneTimePerformanceParameters }
| { kind: "Sustained"; Sustained: FastSustainedPerformanceParameters },
): StdReader<
RenderedPerformanceParameters,
"t" | "tm" | "lj" | "eu" | "rts" | "nms" | "ibi",
"SkillModificationLevel"
> => {
switch (value.kind) {
case "OneTime":
return renderFastOneTimePerformanceParameters(value.OneTime)
case "Sustained":
return renderFastSustainedPerformanceParameters(value.Sustained)
default:
return assertExhaustive(value)
}
}
/**
* Get the texts for all slow one-time performance parameters.
*/
export const getSlowOneTimePerformanceParametersTranslations = <CastingTime>(
getInstanceById: GetInstanceById<"SkillModificationLevel">,
locale: LocaleEnvironment,
entity: Entity,
responsiveTextSize: ResponsiveTextSize,
value: OneTimePerformanceParameters<CastingTime>,
export const renderSlowOneTimePerformanceParameters = <CastingTime>(
renderCastingTime: (
getInstanceById: GetInstanceById<"SkillModificationLevel">,
translate: Translate,
entity: Entity,
responsiveTextSize: ResponsiveTextSize,
value: CastingTime,
) => string,
): {
castingTime: string
cost: string
range: string
duration: string
} => ({
castingTime: renderCastingTime(
getInstanceById,
locale.translate,
entity,
responsiveTextSize,
value.casting_time,
),
cost: getOneTimeCostTranslation(
getInstanceById,
locale,
Speed.Slow,
entity,
responsiveTextSize,
value.cost,
),
range: getTextForActivatableSkillRange(
getInstanceById,
locale.translate,
locale.translateMap,
Speed.Slow,
responsiveTextSize,
entity,
value.range,
),
duration: getDurationForOneTimeTranslation(
locale.translate,
locale.translateMap,
responsiveTextSize,
value.duration,
),
})
) => StdReader<string, "t" | "rts" | "nms" | "ibi", "SkillModificationLevel">,
value: OneTimePerformanceParameters<CastingTime>,
): StdReader<
RenderedPerformanceParameters,
"t" | "tm" | "lj" | "eu" | "rts" | "nms" | "ibi",
"SkillModificationLevel"
> =>
renderCastingTime(value.casting_time)
.thenW(castingTime =>
renderOneTimeCost(value.cost).then(cost =>
renderRange(value.range).then(range =>
renderOneTimeDuration(value.duration).map(duration => ({
castingTime,
cost,
range,
duration,
})),
),
),
)
.with(env => ({ ...env, speed: Speed.Slow }))
/**
* Get the texts for all slow sustained performance parameters.
*/
export const getSlowSustainedPerformanceParametersTranslations = (
getInstanceById: GetInstanceById<"SkillModificationLevel">,
locale: LocaleEnvironment,
entity: Entity,
responsiveTextSize: ResponsiveTextSize,
export const renderSlowSustainedPerformanceParameters = (
value: SlowSustainedPerformanceParameters,
): {
castingTime: string
cost: string
range: string
duration: string
} => ({
castingTime: getSlowCastingTimeTranslation(
getInstanceById,
locale.translate,
entity,
responsiveTextSize,
value.casting_time,
),
cost: getSustainedCostTranslation(
getInstanceById,
locale,
Speed.Slow,
entity,
responsiveTextSize,
value.cost,
),
range: getTextForActivatableSkillRange(
getInstanceById,
locale.translate,
locale.translateMap,
Speed.Slow,
responsiveTextSize,
entity,
value.range,
),
duration: getDurationForSustainedTranslation(
locale.translate,
responsiveTextSize,
value.duration,
),
})
): StdReader<
RenderedPerformanceParameters,
"t" | "tm" | "lj" | "eu" | "rts" | "nms" | "ibi",
"SkillModificationLevel"
> =>
renderSlowCastingTime(value.casting_time)
.thenW(castingTime =>
renderSustainedCost(value.cost).then(cost =>
renderRange(value.range).then(range =>
renderSustainedDuration(value.duration).map(duration => ({
castingTime,
cost,
range,
duration,
})),
),
),
)
.with(env => ({ ...env, speed: Speed.Slow }))
/**
* Renders the performance parameters for a slow activatable skill.
*/
export const renderSlowPerformanceParameters = (
value:
| { kind: "OneTime"; OneTime: SlowOneTimePerformanceParameters }
| { kind: "Sustained"; Sustained: SlowSustainedPerformanceParameters },
): StdReader<
RenderedPerformanceParameters,
"t" | "tm" | "lj" | "eu" | "rts" | "nms" | "ibi",
"SkillModificationLevel"
> => {
switch (value.kind) {
case "OneTime":
return renderSlowOneTimePerformanceParameters(
renderSlowCastingTime,
value.OneTime,
)
case "Sustained":
return renderSlowSustainedPerformanceParameters(value.Sustained)
default:
return assertExhaustive(value)
}
}
@@ -1,55 +1,36 @@
import { LocaleEnvironment } from "../../../../helpers/locale.js"
import type { Translate } from "../../../../helpers/translate.js"
import { responsive, ResponsiveTextSize } from "../../responsiveText.js"
import { Reader } from "@elyukai/utils/reader"
import { responsiveTranslateR, type StdReader } from "../../reader.js"
/**
* Wraps the text in a translation that indicates its a minimum value.
*/
export const wrapAsMinimum = (
locale: LocaleEnvironment,
responsiveTextSize: ResponsiveTextSize,
text: string,
): string =>
responsive(
responsiveTextSize,
() => locale.translate("at least {$value}", { value: text }),
() => locale.translate("min. {$value}", { value: text }),
)
export const wrapAsMinimum = (text: string): StdReader<string, "t" | "rts"> =>
responsiveTranslateR("at least {$value}", "min. {$value}", { value: text })
/**
* Wraps the text in a translation that indicates its a minimum value if the
* `is_minimum` property says its a minimum value.
*/
export const wrapIfMinimum = (
locale: LocaleEnvironment,
responsiveTextSize: ResponsiveTextSize,
is_minimum: boolean | undefined,
text: string,
) =>
is_minimum === true ? wrapAsMinimum(locale, responsiveTextSize, text) : ""
): StdReader<string, "t" | "rts"> =>
is_minimum === true ? wrapAsMinimum(text) : Reader.of("")
/**
* Wraps the text in a translation that indicates its a maximum value.
*/
export const wrapAsMaximum = (
translate: Translate,
responsiveTextSize: ResponsiveTextSize,
text: string,
): string =>
responsive(
responsiveTextSize,
() => translate("no more than {$value}", { value: text }),
() => translate("max. {$value}", { value: text }),
)
export const wrapAsMaximum = (text: string): StdReader<string, "t" | "rts"> =>
responsiveTranslateR("no more than {$value}", "max. {$value}", {
value: text,
})
/**
* Wraps the text in a translation that indicates its a maximum value if the
* `is_maximum` property says its a maximum value.
*/
export const wrapIfMaximum = (
translate: Translate,
responsiveTextSize: ResponsiveTextSize,
is_maximum: boolean | undefined,
text: string,
): string =>
is_maximum === true ? wrapAsMaximum(translate, responsiveTextSize, text) : ""
): StdReader<string, "t" | "rts"> =>
is_maximum === true ? wrapAsMaximum(text) : Reader.of("")
@@ -1,8 +1,9 @@
import { mapNullable } from "@optolith/helpers/nullable"
import { assertExhaustive } from "@optolith/helpers/typeSafety"
import type { Translate, Translations } from "../../../../helpers/translate.js"
import { responsive, ResponsiveTextSize } from "../../responsiveText.js"
import { Entity } from "./entity.js"
import { Reader } from "@elyukai/utils/reader"
import {
responsiveTranslateR,
type StdEnv,
type StdReader,
} from "../../reader.js"
/**
* A parameter that is designed to be modifiable.
@@ -13,77 +14,18 @@ export enum ModifiableParameter {
Range,
}
const translationKeyForNonModifiableSuffix = {
[Entity.Spell]: {
[ModifiableParameter.CastingTime]:
" (you cannot use a modification on this spells casting time)",
[ModifiableParameter.Cost]:
" (you cannot use a modification on this spells cost)",
[ModifiableParameter.Range]:
" (you cannot use a modification on this spells range)",
},
[Entity.Ritual]: {
[ModifiableParameter.CastingTime]:
" (you cannot use a modification on this rituals ritual time)",
[ModifiableParameter.Cost]:
" (you cannot use a modification on this rituals cost)",
[ModifiableParameter.Range]:
" (you cannot use a modification on this rituals range)",
},
[Entity.LiturgicalChant]: {
[ModifiableParameter.CastingTime]:
" (you cannot use a modification on this chants liturgical time)",
[ModifiableParameter.Cost]:
" (you cannot use a modification on this chants cost)",
[ModifiableParameter.Range]:
" (you cannot use a modification on this chants range)",
},
[Entity.Ceremony]: {
[ModifiableParameter.CastingTime]:
" (you cannot use a modification on this ceremonys ceremonial time)",
[ModifiableParameter.Cost]:
" (you cannot use a modification on this ceremonys cost)",
[ModifiableParameter.Range]:
" (you cannot use a modification on this ceremonys range)",
},
[Entity.Cantrip]: undefined,
[Entity.Blessing]: undefined,
} satisfies {
[entityKey in Entity]:
| {
[paramKey in ModifiableParameter]: keyof Translations
}
| undefined
}
/**
* Returns the suffix for the text of a non-modifiable parameter that indicates
* that the parameter cannot be modified.
*/
export const getNonModifiableSuffixTranslation = (
translate: Translate,
entity: Entity,
export const appendNonModifiableSuffix = (
param: ModifiableParameter,
responsiveTextSize: ResponsiveTextSize,
): string =>
responsive(
responsiveTextSize,
() =>
mapNullable(translationKeyForNonModifiableSuffix[entity]?.[param], key =>
translate(key),
) ?? "",
() => {
switch (entity) {
case Entity.Spell:
case Entity.Ritual:
case Entity.LiturgicalChant:
case Entity.Ceremony:
return translate(" (cannot modify)")
case Entity.Cantrip:
case Entity.Blessing:
return ""
default:
return assertExhaustive(entity)
}
},
)
base: string,
): StdReader<string, "t" | "rts" | "nms"> =>
Reader.asks((ctx: StdEnv<"nms">) => ctx.nonModifiableSuffix)
.thenW(suffix =>
suffix === undefined
? Reader.of("")
: responsiveTranslateR(suffix(param), " (cannot modify)"),
)
.map(suffix => base + suffix)
+128 -205
View File
@@ -1,166 +1,140 @@
import { Reader } from "@elyukai/utils/reader"
import { mapNullable } from "@optolith/helpers/nullable"
import { assertExhaustive } from "@optolith/helpers/typeSafety"
import type {
BlessingRange,
CantripRange,
CheckResultBasedRange,
FixedRange,
ModifiableRange,
Range,
RangeValue,
SkillModificationLevel_ID,
} from "optolith-database-schema/gen"
import { type GetInstanceById } from "../../../../helpers/getTypes.js"
import type { Translate, TranslateMap } from "../../../../helpers/translate.js"
import {
appendNoteIfRequested,
replaceTextIfRequested,
ResponsiveTextSize,
getInstanceByIdR,
modifiableBySpeedOptionalR,
modifiableBySpeedR,
translateFnR,
translateMapR,
translateR,
type StdReader,
} from "../../reader.js"
import {
appendNoteIfNeeded,
replaceTextIfNeeded,
} from "../../responsiveText.js"
import { formatLength } from "../../units/length.js"
import { formatCombinedLengthR, formatLengthR } from "../../units/length.js"
import { MISSING_VALUE } from "../../unknown.js"
import { getCheckResultBasedValueTranslation } from "./checkResultBased.js"
import { Entity } from "./entity.js"
import { renderCheckResultBasedValue } from "./checkResultBased.js"
import { wrapIfMaximum } from "./isMinimumMaximum.js"
import {
getNonModifiableSuffixTranslation,
appendNonModifiableSuffix,
ModifiableParameter,
} from "./nonModifiableSuffix.js"
import { getModifiableBySpeed, Speed } from "./speed.js"
const getModifiableRangeTranslation = (
getInstanceById: GetInstanceById<"SkillModificationLevel">,
translate: Translate,
speed: Speed,
responsiveTextSize: ResponsiveTextSize,
value: ModifiableRange,
) =>
mapNullable(
getInstanceById("SkillModificationLevel", value.initial_modification_level),
modificationLevel => {
const range = getModifiableBySpeed(speed, "range", modificationLevel)
if (range === 1) {
return translate("Touch")
}
return formatLength(translate, responsiveTextSize, "Steps", range)
},
) ?? MISSING_VALUE
const getSightTranslation = (translate: Translate) => translate("Sight")
const getSelfTranslation = (translate: Translate) => translate("Self")
const getGlobalTranslation = (translate: Translate) => translate("Global")
const getTouchTranslation = (
translate: Translate,
entity: Entity,
responsiveTextSize: ResponsiveTextSize,
) =>
translate("Touch") +
getNonModifiableSuffixTranslation(
translate,
entity,
ModifiableParameter.Range,
responsiveTextSize,
const deriveModifiableRange = (
modificationLevelId: SkillModificationLevel_ID,
): StdReader<
{ value: number; translation: string | undefined } | undefined,
"s" | "tm" | "ibi",
"SkillModificationLevel"
> =>
getInstanceByIdR<"SkillModificationLevel">().thenW(
getInstanceById =>
mapNullable(
getInstanceById("SkillModificationLevel", modificationLevelId),
modificationLevel =>
modifiableBySpeedR("range", modificationLevel).thenW(value =>
translateMapR(modificationLevel.translations).thenW(translation =>
translation === undefined
? Reader.of({ value, translation: undefined })
: modifiableBySpeedOptionalR("range", translation).map(
valueTranslation => ({
value,
translation: valueTranslation,
}),
),
),
),
) ?? Reader.of(undefined),
)
const getFixedRangeTranslation = (
translate: Translate,
entity: Entity,
responsiveTextSize: ResponsiveTextSize,
value: FixedRange,
) =>
formatLength(translate, responsiveTextSize, value.unit.kind, value.value) +
getNonModifiableSuffixTranslation(
translate,
entity,
ModifiableParameter.Range,
responsiveTextSize,
const wrapIfRadius = (is_radius: boolean | undefined, text: string) =>
translateFnR.map(translate =>
is_radius === true ? `${text} ${translate("Radius")}` : text,
)
const wrapIfRadius = (
translate: Translate,
is_radius: boolean | undefined,
text: string,
) => (is_radius === true ? `${text} ${translate("Radius")}` : text)
const getCheckResultBasedRangeTranslation = (
translate: Translate,
entity: Entity,
responsiveTextSize: ResponsiveTextSize,
value: CheckResultBasedRange,
) => {
const range = formatLength(
translate,
responsiveTextSize,
value.unit.kind,
getCheckResultBasedValueTranslation(translate, value),
)
const rangeWrappedIfRadius = wrapIfRadius(translate, value.is_radius, range)
const rangeWrappedIfRadiusAndIfMaximum = wrapIfMaximum(
translate,
responsiveTextSize,
value.is_maximum,
rangeWrappedIfRadius,
)
return (
rangeWrappedIfRadiusAndIfMaximum +
getNonModifiableSuffixTranslation(
translate,
entity,
ModifiableParameter.Range,
responsiveTextSize,
const renderModifiableRange = (value: ModifiableRange) =>
deriveModifiableRange(value.initial_modification_level)
.thenW(range =>
range === undefined
? Reader.of(MISSING_VALUE)
: typeof range === "string"
? Reader.of(range)
: formatLengthR("Steps", range.value),
)
)
}
.then(text => wrapIfRadius(value.is_radius, text))
.then(text => wrapIfMaximum(value.is_maximum, text))
const renderFixedRange = (value: FixedRange) =>
formatCombinedLengthR(value)
.then(text => wrapIfRadius(value.is_radius, text))
.then(text => wrapIfMaximum(value.is_maximum, text))
const getCheckResultBasedRangeTranslation = (value: CheckResultBasedRange) =>
renderCheckResultBasedValue(value)
.thenW(text => formatLengthR(value.unit, text))
.then(text => wrapIfRadius(value.is_radius, text))
.then(text => wrapIfMaximum(value.is_maximum, text))
/**
* Returns the text for the range of an activatable skill.
* Returns the text for the non-modifiable range of an activatable skill.
*/
export const getRangeValueTranslation = (
getInstanceById: GetInstanceById<"SkillModificationLevel">,
translate: Translate,
speed: Speed,
responsiveTextSize: ResponsiveTextSize,
entity: Entity,
value: RangeValue,
) => {
export const renderNonModifiableRange = (
value:
| {
kind: "Sight"
}
| {
kind: "Self"
}
| {
kind: "Global"
}
| {
kind: "Touch"
}
| {
kind: "Fixed"
Fixed: FixedRange
}
| {
kind: "CheckResultBased"
CheckResultBased: CheckResultBasedRange
},
shouldAppendNonModifiableSuffix: boolean,
): StdReader<string, "t" | "tm" | "rts" | "nms"> => {
const appendNonModifiableSuffixIfNeeded = shouldAppendNonModifiableSuffix
? (text: string) =>
appendNonModifiableSuffix(ModifiableParameter.Range, text)
: Reader.of
switch (value.kind) {
case "Modifiable":
return getModifiableRangeTranslation(
getInstanceById,
translate,
speed,
responsiveTextSize,
value.Modifiable,
)
case "Sight":
return getSightTranslation(translate)
return translateR("Sight")
case "Self":
return getSelfTranslation(translate)
return translateR("Self")
case "Global":
return getGlobalTranslation(translate)
return translateR("Global")
case "Touch":
return getTouchTranslation(translate, entity, responsiveTextSize)
return translateR("Touch").thenW(appendNonModifiableSuffixIfNeeded)
case "Fixed": {
return getFixedRangeTranslation(
translate,
entity,
responsiveTextSize,
value.Fixed,
return renderFixedRange(value.Fixed).thenW(
appendNonModifiableSuffixIfNeeded,
)
}
case "CheckResultBased":
return getCheckResultBasedRangeTranslation(
translate,
entity,
responsiveTextSize,
value.CheckResultBased,
return getCheckResultBasedRangeTranslation(value.CheckResultBased).then(
appendNonModifiableSuffixIfNeeded,
)
default:
return assertExhaustive(value)
@@ -170,90 +144,39 @@ export const getRangeValueTranslation = (
/**
* Returns the text for the range of an activatable skill.
*/
export const getTextForActivatableSkillRange = (
getInstanceById: GetInstanceById<"SkillModificationLevel">,
translate: Translate,
translateMap: TranslateMap,
speed: Speed,
responsiveTextSize: ResponsiveTextSize,
entity: Entity,
value: Range,
): string => {
const rangeValue = getRangeValueTranslation(
getInstanceById,
translate,
speed,
responsiveTextSize,
entity,
value.value,
)
const withReplacement = replaceTextIfRequested(
"replacement",
value.translations,
translateMap,
responsiveTextSize,
rangeValue,
)
return appendNoteIfRequested(
"note",
value.translations,
translateMap,
responsiveTextSize,
withReplacement,
)
}
const getTextForTinyActivatableRange = (
translate: Translate,
responsiveTextSize: ResponsiveTextSize,
entity: Entity,
value: CantripRange | BlessingRange,
): string => {
export const renderRangeValue = (
value: RangeValue,
shouldAppendNonModifiableSuffix: boolean,
): StdReader<
string,
"t" | "tm" | "rts" | "s" | "nms" | "ibi",
"SkillModificationLevel"
> => {
switch (value.kind) {
case "Modifiable":
return renderModifiableRange(value.Modifiable)
case "Sight":
case "Self":
return getSelfTranslation(translate)
case "Global":
case "Touch":
return getTouchTranslation(translate, entity, responsiveTextSize)
case "Fixed":
return getFixedRangeTranslation(
translate,
entity,
responsiveTextSize,
value.Fixed,
)
case "CheckResultBased":
return renderNonModifiableRange(value, shouldAppendNonModifiableSuffix)
default:
return assertExhaustive(value)
}
}
/**
* Returns the text for the range of a cantrip.
* Returns the text for the range of an activatable skill.
*/
export const getTextForCantripRange = (
translate: Translate,
responsiveTextSize: ResponsiveTextSize,
value: CantripRange,
): string =>
getTextForTinyActivatableRange(
translate,
responsiveTextSize,
Entity.Cantrip,
value,
)
/**
* Returns the text for the range of a blessing.
*/
export const getTextForBlessingRange = (
translate: Translate,
responsiveTextSize: ResponsiveTextSize,
value: BlessingRange,
): string =>
getTextForTinyActivatableRange(
translate,
responsiveTextSize,
Entity.Blessing,
value,
)
export const renderRange = (
value: Range,
): StdReader<
string,
"t" | "tm" | "rts" | "s" | "nms" | "ibi",
"SkillModificationLevel"
> =>
renderRangeValue(value.value, true)
.then(text => replaceTextIfNeeded(value.translations, text))
.then(text => appendNoteIfNeeded(value.translations, text))
@@ -1,59 +1,51 @@
import { Reader } from "@elyukai/utils/reader"
import { mapNullable } from "@optolith/helpers/nullable"
import { assertExhaustive } from "@optolith/helpers/typeSafety"
import type {
AffectedTargetCategories,
SpecificAffectedTargetCategoryIdentifier,
TargetCategory_ID,
} from "optolith-database-schema/gen"
import { type GetInstanceById } from "../../../../helpers/getTypes.js"
import { LocaleEnvironment } from "../../../../helpers/locale.js"
import { EntityDescriptionSection } from "../../../../index.js"
import {
getInstanceByIdR,
translateMapR,
translateR,
type StdReader,
} from "../../reader.js"
import { MISSING_VALUE } from "../../unknown.js"
import { appendInParensIfNotEmpty } from "./parensIf.js"
const getSelfTranslation = (locale: LocaleEnvironment) =>
locale.translate("Self")
const getZoneTranslation = (locale: LocaleEnvironment) =>
locale.translate("Zone")
const getLiturgicalChantsAndCeremoniesTranslation = (
locale: LocaleEnvironment,
) => locale.translate("Liturgical Chants and Ceremonies")
const getSpellworksTranslation = (locale: LocaleEnvironment) =>
locale.translate("Spellworks")
const getCantripsTranslation = (locale: LocaleEnvironment) =>
locale.translate("Cantrips")
const getPredefinedTranslation = (
getInstanceById: GetInstanceById<"TargetCategory">,
locale: LocaleEnvironment,
id: string,
) =>
mapNullable(
locale.translateMap(getInstanceById("TargetCategory", id)?.translations),
translation => translation.name,
) ?? MISSING_VALUE
const renderPredefined = (targetCategoryId: TargetCategory_ID) =>
getInstanceByIdR<"TargetCategory">()
.thenW(
getInstanceById =>
mapNullable(
getInstanceById("TargetCategory", targetCategoryId),
targetCategory =>
translateMapR(targetCategory.translations).map(
translation => translation?.name,
),
) ?? Reader.of(undefined),
)
.map(translation => translation ?? MISSING_VALUE)
const getTargetCategoryTranslationByType = (
getInstanceById: GetInstanceById<"TargetCategory">,
locale: LocaleEnvironment,
id: SpecificAffectedTargetCategoryIdentifier,
) => {
): StdReader<string, "t" | "tm" | "ibi", "TargetCategory"> => {
switch (id.kind) {
case "Self":
return getSelfTranslation(locale)
return translateR("Self")
case "Zone":
return getZoneTranslation(locale)
return translateR("Zone")
case "LiturgicalChantsAndCeremonies":
return getLiturgicalChantsAndCeremoniesTranslation(locale)
return translateR("Liturgical Chants and Ceremonies")
case "Spellworks":
return getSpellworksTranslation(locale)
return translateR("Spellworks")
case "Cantrips":
return getCantripsTranslation(locale)
return translateR("Cantrips")
case "Predefined":
return getPredefinedTranslation(getInstanceById, locale, id.Predefined)
return renderPredefined(id.Predefined)
default:
return assertExhaustive(id)
}
@@ -62,21 +54,21 @@ const getTargetCategoryTranslationByType = (
/**
* Get the text for the target category.
*/
export const getTargetCategoryTranslation = (
getInstanceById: GetInstanceById<"TargetCategory">,
locale: LocaleEnvironment,
export const renderTargetCategory = (
values: AffectedTargetCategories,
): EntityDescriptionSection => ({
label: locale.translate("Target Category"),
value:
values.length === 0
? locale.translate("all")
: values
.map(({ id, translations }) =>
appendInParensIfNotEmpty(
locale.translateMap(translations)?.note,
getTargetCategoryTranslationByType(getInstanceById, locale, id),
): StdReader<EntityDescriptionSection, "t" | "tm" | "ibi", "TargetCategory"> =>
translateR("Target Category").thenW(label =>
(values.length === 0
? translateR("all")
: Reader.sequence(
values.map(({ id, translations }) =>
getTargetCategoryTranslationByType(id).then(
text =>
translateMapR(translations)
.map(translation => translation?.note)
.map(note => appendInParensIfNotEmpty(note, text)) ?? text,
),
)
.join(", "),
})
),
).map(texts => texts.join(", "))
).map(value => ({ label, value })),
)
@@ -1,14 +1,14 @@
import type { ImprovementCost } from "optolith-database-schema/gen"
import { Translate } from "../../../helpers/translate.js"
import { EntityDescriptionSection } from "../../../index.js"
import { translateR, type StdReader } from "../reader.js"
/**
* Returns the improvement cost as an inline library property.
*/
export const createImprovementCost = (
translate: Translate,
export const renderImprovementCost = (
improvementCost: ImprovementCost,
): EntityDescriptionSection => ({
label: translate("Improvement Cost"),
value: improvementCost.kind,
})
): StdReader<EntityDescriptionSection, "t"> =>
translateR("Improvement Cost").map(label => ({
label,
value: improvementCost.kind,
}))
+110 -120
View File
@@ -1,145 +1,135 @@
import { Reader } from "@elyukai/utils/reader"
import { assertExhaustive } from "@optolith/helpers/typeSafety"
import type {
SkillCheck,
SkillCheckPenalty,
} from "optolith-database-schema/gen"
import { type GetInstanceById } from "../../../helpers/getTypes.js"
import { Translate, TranslateMap } from "../../../helpers/translate.js"
import { EntityDescriptionSection, type IdMap } from "../../../index.js"
import { responsive, ResponsiveTextSize } from "../responsiveText.js"
import { type IdMap } from "../../../index.js"
import {
getInstanceByIdR,
responsiveR,
responsiveThenR,
responsiveTranslateR,
translateMapR,
translateR,
type StdReader,
} from "../reader.js"
import { MISSING_VALUE } from "../unknown.js"
/**
* Returns the skill check as an inline library property.
* Renders a skill check.
*/
export const getTextForCheck = (
deps: {
translate: Translate
translateMap: TranslateMap
getInstanceById: GetInstanceById<"Attribute">
},
export const renderSkillCheck = (
check: SkillCheck,
checkPenalty?: {
value: SkillCheckPenalty | undefined
responsiveText: ResponsiveTextSize
getInstanceById: GetInstanceById<"DerivedCharacteristic">
idMap: IdMap
},
): EntityDescriptionSection => ({
label: deps.translate("Check"),
value:
check
): StdReader<
{ label: string; value: string },
"t" | "tm" | "ibi",
"Attribute"
> =>
Reader.asks(({ translate, translateMap, getInstanceById }) => ({
label: translate("Check"),
value: check
.map(
id =>
deps.translateMap(deps.getInstanceById("Attribute", id)?.translations)
?.abbreviation ?? "??",
translateMap(getInstanceById("Attribute", id)?.translations)
?.abbreviation ?? MISSING_VALUE,
)
.join("/") +
(() => {
if (checkPenalty?.value === undefined) {
return ""
}
.join("/"),
}))
const { responsiveText } = checkPenalty
const renderSkillCheckPenalty = (
idMap: IdMap,
penalty: SkillCheckPenalty,
): StdReader<string, "t" | "tm" | "rts" | "ibi", "DerivedCharacteristic"> => {
const getDerivedCharacteristicTranslation = (id: string) =>
getInstanceByIdR<"DerivedCharacteristic">()
.map(getInstanceById => getInstanceById("DerivedCharacteristic", id))
.thenW(dc =>
dc === undefined
? Reader.of(undefined)
: translateMapR(dc.translations).thenW(translation =>
translation === undefined
? Reader.of(undefined)
: responsiveR(
() => translation.name,
() => translation.abbreviation,
),
),
)
.map(translation => translation ?? MISSING_VALUE)
const getDerivedCharacteristicTranslation = (id: string) =>
deps.translateMap(
checkPenalty.getInstanceById("DerivedCharacteristic", id)
?.translations,
)
const getSpiritTranslation = () =>
getDerivedCharacteristicTranslation(idMap.DerivedCharacteristic.Spirit)
const getSpiritTranslation = () =>
getDerivedCharacteristicTranslation(
checkPenalty.idMap.DerivedCharacteristic.Spirit,
)
const getToughnessTranslation = () =>
getDerivedCharacteristicTranslation(idMap.DerivedCharacteristic.Toughness)
const getToughnessTranslation = () =>
getDerivedCharacteristicTranslation(
checkPenalty.idMap.DerivedCharacteristic.Toughness,
)
switch (penalty.kind) {
case "Spirit":
return getSpiritTranslation()
const penalty = (() => {
switch (checkPenalty.value.kind) {
case "Spirit": {
const translation = getSpiritTranslation()
return translation === undefined
? ""
: responsive(
responsiveText,
() => translation.name,
() => translation.abbreviation,
)
}
case "HalfOfSpirit":
return getSpiritTranslation().map(translation => `${translation}/2`)
case "HalfOfSpirit": {
const translation = getSpiritTranslation()
return translation === undefined
? ""
: responsive(
responsiveText,
() => `${translation.name}/2`,
() => `${translation.abbreviation}/2`,
)
}
case "Toughness":
return getToughnessTranslation()
case "Toughness": {
const translation = getToughnessTranslation()
return translation === undefined
? ""
: responsive(
responsiveText,
() => `${translation.name}/2`,
() => `${translation.abbreviation}/2`,
)
}
case "HigherOfSpiritAndToughness":
return getSpiritTranslation().thenW(spirit =>
getToughnessTranslation().thenW(toughness =>
responsiveThenR(
() =>
translateR(
"{$first} or {$second}, depending on which value is higher",
{
first: spirit,
second: toughness,
},
),
() => Reader.of(`${spirit}/${toughness}`),
),
),
)
case "HigherOfSpiritAndToughness": {
const spiritTranslation = getSpiritTranslation()
const toughnessTranslation = getToughnessTranslation()
return spiritTranslation === undefined ||
toughnessTranslation === undefined
? ""
: responsive(
responsiveText,
() =>
deps.translate(
"{$first} or {$second}, depending on which value is higher",
{
first: spiritTranslation.abbreviation,
second: toughnessTranslation.abbreviation,
},
),
() =>
`${spiritTranslation.abbreviation}/${toughnessTranslation.abbreviation}`,
)
}
case "SummoningDifficulty":
return responsiveTranslateR("Invocation Difficulty", "ID")
case "SummoningDifficulty":
return responsive(
responsiveText,
() => deps.translate("Invocation Difficulty"),
() => deps.translate("ID"),
)
case "CreationDifficulty":
return responsiveTranslateR("Creation Difficulty", "CD")
case "CreationDifficulty":
return responsive(
responsiveText,
() => deps.translate("Creation Difficulty"),
() => deps.translate("CD"),
)
case "Object":
return translateR("Object")
case "Object":
return deps.translate("Object")
default:
return assertExhaustive(penalty)
}
}
default:
return assertExhaustive(checkPenalty.value)
}
})()
return responsive(
responsiveText,
() =>
deps.translate(" (modified by {$modifier})", { modifier: penalty }),
() => deps.translate(" ({$modifier})", { modifier: penalty }),
/**
* Renders a skill check with a possible penalty.
*/
export const renderSkillCheckWithPenalty = (
check: SkillCheck,
checkPenalty: SkillCheckPenalty | undefined,
idMap: IdMap,
): StdReader<
{ label: string; value: string },
"t" | "tm" | "rts" | "ibi",
"Attribute" | "DerivedCharacteristic"
> =>
checkPenalty === undefined
? renderSkillCheck(check)
: renderSkillCheck(check).thenW(checkText =>
renderSkillCheckPenalty(idMap, checkPenalty)
.then(penaltyText =>
responsiveTranslateR(
" (modified by {$modifier})",
" ({$modifier})",
{ modifier: penaltyText },
),
)
.map(penaltyText => ({
...checkText,
value: checkText.value + penaltyText,
})),
)
})(),
})
+355
View File
@@ -0,0 +1,355 @@
import { isNotNullish } from "@elyukai/utils/nullable"
import { Reader } from "@elyukai/utils/reader"
import { assertExhaustive } from "@elyukai/utils/typeSafety"
import type {
EntityMap,
FastSkillModificationLevelConfig,
ResponsiveText,
ResponsiveTextOptional,
SkillModificationLevel,
SlowSkillModificationLevelConfig,
} from "optolith-database-schema/gen"
import type { GetInstanceById } from "../../helpers/getTypes.js"
import type {
LocaleCompare,
LocaleJoin,
LocaleJoinType,
} from "../../helpers/locale.js"
import type {
LocaleMap,
Translate,
TranslateMap,
TranslationKeyMatchingParamsOfKey,
TranslationKeysWithoutParams,
TranslationParamsInArray,
Translations,
} from "../../helpers/translate.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"
/**
* The standard set of environment properties for readers in this project.
*/
export type EnvMap<E extends keyof EntityMap = never> = {
translate: Translate
translateMap: TranslateMap
localeJoin: LocaleJoin
localeCompare: LocaleCompare
responsiveTextSize: ResponsiveTextSize
speed: Speed
energyUnit: EnergyUnit
nonModifiableSuffix?: (
param: ModifiableParameter,
) => TranslationKeysWithoutParams
getInstanceById: GetInstanceById<E>
}
type EnvMapAbbr = {
t: "translate"
tm: "translateMap"
lj: "localeJoin"
lc: "localeCompare"
rts: "responsiveTextSize"
s: "speed"
eu: "energyUnit"
nms: "nonModifiableSuffix"
ibi: "getInstanceById"
}
/**
* The standard set of environment properties for readers in this project.
*
* 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<EnvMap<E>, EnvMapAbbr[K]>
/**
* Shortcut for a reader with common environment properties.
*
* The keys are abbreviated to keep return type annotations short.
*/
export type StdReader<
T,
K extends keyof EnvMapAbbr,
E extends keyof EntityMap = never,
> = Reader<StdEnv<K, E>, T>
// Specialized constructors for common contexts
/**
* Creates a value from a translation key.
*/
export const translateR = <K extends keyof Translations>(
key: K,
...rest: TranslationParamsInArray<K>
): 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)
/**
* Takes the appropriate translation from a locale map.
*/
export const translateMapR = <T>(
map: LocaleMap<T> | undefined,
): Reader<{ translateMap: TranslateMap }, T | undefined> =>
Reader.asks(env => env.translateMap(map))
/**
* Returns the `translateMap` function from the context.
*/
export const translateMapFnR: Reader<
{ translateMap: TranslateMap },
TranslateMap
> = Reader.asks(env => env.translateMap)
/**
* Joins a list of strings according to the locales rules for the given type.
*/
export const localeJoinR = (
arr: string[],
type: LocaleJoinType,
): 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 = <E extends keyof EntityMap = never>(): Reader<
{ getInstanceById: GetInstanceById<E> },
GetInstanceById<E>
> => Reader.asks(env => env.getInstanceById)
/**
* Joins a list of strings according to the locales rules for the given type.
*/
export const responsiveLocaleJoinR = (
arr: string[],
type: LocaleJoinType,
): Reader<
{ localeJoin: LocaleJoin; responsiveTextSize: ResponsiveTextSize },
string
> =>
Reader.asks(({ localeJoin, responsiveTextSize }) =>
responsive(
responsiveTextSize,
() => localeJoin(arr, type),
() => {
switch (type) {
case "conjunction":
return " + "
case "disjunction":
return " / "
case "unit":
return " "
default:
return assertExhaustive(type)
}
},
),
)
/**
* Compares two strings according to the locales sorting rules.
*/
export const localeCompareR: Reader<
{ localeCompare: LocaleCompare },
LocaleCompare
> = Reader.asks(env => env.localeCompare)
/**
* Creates a responsive value from two functions that return the value for the full and compressed version, respectively.
*/
export const responsiveR = <T>(
full: () => T,
compressed: () => T,
): Reader<{ responsiveTextSize: ResponsiveTextSize }, T> =>
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.
*/
export const responsiveThenR = <T, RF extends object, RC extends object>(
full: () => Reader<RF, T>,
compressed: () => Reader<RC, T>,
): Reader<{ responsiveTextSize: ResponsiveTextSize } & RF & RC, T> =>
Reader.asks(env => {
switch (env.responsiveTextSize) {
case ResponsiveTextSize.Full:
return full().run(env)
case ResponsiveTextSize.Compressed:
return compressed().run(env)
default:
return assertExhaustive(env.responsiveTextSize)
}
})
/**
* Creates a responsive value from a translation key that has a full and a compressed version.
*/
export const responsiveTranslateR = <
K extends keyof Translations,
K2 extends TranslationKeyMatchingParamsOfKey<K>,
>(
fullKey: K,
compressedKey: K2,
...rest: TranslationParamsInArray<K> & TranslationParamsInArray<K2>
): Reader<
{ translate: Translate; responsiveTextSize: ResponsiveTextSize },
string
> =>
Reader.asks(({ translate, responsiveTextSize }) =>
responsive(
responsiveTextSize,
() => translate(fullKey, ...(rest as TranslationParamsInArray<K>)),
() => translate(compressedKey, ...(rest as TranslationParamsInArray<K2>)),
),
)
/**
* Creates a responsive value from a responsive text.
*/
export const responsiveTextR = (
responsiveText: ResponsiveText,
): Reader<{ responsiveTextSize: ResponsiveTextSize }, string> =>
Reader.asks(({ responsiveTextSize }) =>
responsive(
responsiveTextSize,
() => responsiveText.full,
() => responsiveText.compressed,
),
)
/**
* Creates a responsive value from a responsive text with an optional compressed variant.
*/
export const responsiveTextOptionalR = (
responsiveText: ResponsiveTextOptional,
): Reader<{ responsiveTextSize: ResponsiveTextSize }, string | undefined> =>
Reader.asks(({ responsiveTextSize }) =>
responsive(
responsiveTextSize,
() => responsiveText.full,
() => responsiveText.compressed,
),
)
/**
* Formats the given energy cost value with the appropriate unit.
*/
export const formatEnergyR = (
value: string | number,
): Reader<{ translate: Translate; energyUnit: EnergyUnit }, string> =>
Reader.asks(({ translate, energyUnit }) =>
formatEnergy(translate, energyUnit, value),
)
/**
* Formats the given energy cost value with the appropriate unit based on the entity type.
*/
export const formatEnergyFnR: Reader<
{ translate: Translate; energyUnit: EnergyUnit },
(value: string | number) => string
> = Reader.asks(
({ translate, energyUnit }) =>
value =>
formatEnergy(translate, energyUnit, value),
)
type SpeedMap = {
[Speed.Fast]: FastSkillModificationLevelConfig
[Speed.Slow]: SlowSkillModificationLevelConfig
}
/**
* Returns a common value for a skill modification level depending on the speed.
*/
export const modifiableBySpeedR = <
Fast extends object,
Slow extends { [K_ in keyof Fast]: unknown },
K extends keyof Fast,
>(
key: K,
level: { fast: Fast; slow: Slow },
): Reader<{ speed: Speed }, (Fast | Slow)[K]> =>
Reader.asks(({ speed }) => {
switch (speed) {
case Speed.Fast:
return level.fast[key] as (Fast | Slow)[K]
case Speed.Slow:
return level.slow[key] as (Fast | Slow)[K]
default:
return assertExhaustive(speed)
}
})
/**
* Returns a common value for a skill modification level depending on the speed.
*/
export const modifiableBySpeedOptionalR = <
Fast extends object,
Slow extends { [K_ in keyof Fast]: unknown },
K extends keyof Fast,
>(
key: K,
level: { fast?: Fast; slow?: Slow },
): Reader<{ speed: Speed }, (Fast | Slow)[K] | undefined> =>
Reader.asks(({ speed }) => {
switch (speed) {
case Speed.Fast:
return level.fast?.[key] as (Fast | Slow)[K] | undefined
case Speed.Slow:
return level.slow?.[key] as (Fast | Slow)[K] | undefined
default:
return assertExhaustive(speed)
}
})
/**
* Returns a common value for a skill modification level depending on the speed.
*/
export const modifyBySpeedR: Reader<
{ speed: Speed },
<S extends Speed, K extends keyof SpeedMap[S]>(
key: K,
level: SkillModificationLevel,
) => SpeedMap[S][K]
> = Reader.asks(
({ speed }) =>
<S extends Speed, K extends keyof SpeedMap[S]>(
key: K,
level: SkillModificationLevel,
) => {
switch (speed) {
case Speed.Fast:
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]
default:
return assertExhaustive(speed)
}
},
)
/**
* Returns the default values wrapped in a reader if the value is nullish, otherwise applies the given function to the value and returns the result.
*/
export const mapNullableR =
<T, U, R>(defaultValue: U, fn: (value: NonNullable<T>) => Reader<U, R>) =>
(value: T) =>
isNotNullish(value) ? fn(value) : Reader.of(defaultValue)
+35 -24
View File
@@ -1,12 +1,19 @@
import { mapNullable } from "@optolith/helpers/nullable"
import { identity } from "@elyukai/utils/function"
import { Reader } from "@elyukai/utils/reader"
import { assertExhaustive } from "@optolith/helpers/typeSafety"
import type {
ResponsiveText,
ResponsiveTextOptional,
ResponsiveTextReplace,
} from "optolith-database-schema/gen"
import { TranslateMap, type LocaleMap } from "../../helpers/translate.js"
import { type LocaleMap } from "../../helpers/translate.js"
import { appendInParensIfNotEmpty } from "./rated/activatable/parensIf.js"
import {
responsiveTextOptionalR,
responsiveTextR,
translateMapR,
type StdEnv,
} from "./reader.js"
import { MISSING_VALUE } from "./unknown.js"
/**
@@ -78,34 +85,38 @@ export const getResponsiveTextOptional = (
* Replaces a text with a given value if a replacement is requested, otherwise
* just return the given value.
*/
export const replaceTextIfRequested = <Key extends string>(
key: Key,
translation: LocaleMap<{ [K in Key]?: ResponsiveTextReplace }> | undefined,
translateMap: TranslateMap,
responsiveText: ResponsiveTextSize,
export const replaceTextIfNeeded = (
translations: LocaleMap<{ replacement?: ResponsiveTextReplace }> | undefined,
valueToReplace: string,
) =>
mapNullable(translateMap(translation)?.[key], replacement =>
getResponsiveText(replacement, responsiveText).replace(
"$1",
valueToReplace,
),
) ?? valueToReplace
translateMapR(translations)
.with<StdEnv<"tm" | "rts">>(identity)
.then(translation => {
if (translation?.replacement === undefined) {
return Reader.of(valueToReplace)
}
return responsiveTextR(translation.replacement).map(note =>
note.replace("$1", valueToReplace),
)
})
/**
* Appends a note to a given value if a note is requested, otherwise just return
* the given value.
*/
export const appendNoteIfRequested = <Key extends string>(
key: Key,
translation: LocaleMap<{ [K in Key]?: ResponsiveTextOptional }> | undefined,
translateMap: TranslateMap,
responsiveText: ResponsiveTextSize,
export const appendNoteIfNeeded = (
translations: LocaleMap<{ note?: ResponsiveTextOptional }> | undefined,
valueToAppendTo: string,
) =>
mapNullable(translateMap(translation)?.[key], note =>
appendInParensIfNotEmpty(
getResponsiveTextOptional(note, responsiveText),
valueToAppendTo,
),
) ?? valueToAppendTo
translateMapR(translations)
.with<StdEnv<"tm" | "rts">>(identity)
.then(translation => {
if (translation?.note === undefined) {
return Reader.of(valueToAppendTo)
}
return responsiveTextOptionalR(translation.note).map(note =>
appendInParensIfNotEmpty(note, valueToAppendTo),
)
})
+4 -25
View File
@@ -1,8 +1,9 @@
import { assertExhaustive } from "@optolith/helpers/typeSafety"
import type { Translate, Translations } from "../../../helpers/translate.js"
import { Entity } from "../rated/activatable/entity.js"
type EnergyUnit = "ArcaneEnergy" | "KarmaPoints"
/**
* The unit of an energy cost.
*/
export type EnergyUnit = "ArcaneEnergy" | "KarmaPoints"
const lengthUnitTranslationKeys = {
ArcaneEnergy: "{$value} AE",
@@ -23,25 +24,3 @@ export const formatEnergy = (
return translate(key, { value })
}
/**
* Returns the text for an energy unit that is based on the entity type.
*/
export const formatEnergyByEntity = (
translate: Translate,
entity: Entity,
value: number | string,
) => {
switch (entity) {
case Entity.Cantrip:
case Entity.Spell:
case Entity.Ritual:
return formatEnergy(translate, "ArcaneEnergy", value)
case Entity.Blessing:
case Entity.LiturgicalChant:
case Entity.Ceremony:
return formatEnergy(translate, "KarmaPoints", value)
default:
return assertExhaustive(entity)
}
}
+31 -2
View File
@@ -1,4 +1,6 @@
import { Reader } from "@elyukai/utils/reader"
import type { Translate, Translations } from "../../../helpers/translate.js"
import type { StdReader } from "../reader.js"
import { responsive, ResponsiveTextSize } from "../responsiveText.js"
type LengthUnit = "Steps" | "Miles"
@@ -28,11 +30,11 @@ const lengthUnitTranslationKeys = {
export const formatLength = (
translate: Translate,
responsiveTextSize: ResponsiveTextSize,
unit: LengthUnit,
unit: LengthUnit | { kind: LengthUnit },
value: number | string,
) => {
const [fullNumberKey, fullKey, compressedKey] =
lengthUnitTranslationKeys[unit]
lengthUnitTranslationKeys[typeof unit === "string" ? unit : unit.kind]
return responsive(
responsiveTextSize,
@@ -43,3 +45,30 @@ export const formatLength = (
() => translate(compressedKey, { value }),
)
}
/**
* Returns the text for a length unit.
*/
export const formatLengthR = (
unit: LengthUnit | { kind: LengthUnit },
value: number | string,
): StdReader<string, "t" | "rts"> =>
Reader.asks(env =>
formatLength(env.translate, env.responsiveTextSize, unit, value),
)
/**
* Returns the text for a length unit.
*/
export const formatCombinedLengthR = (object: {
unit: LengthUnit | { kind: LengthUnit }
value: number | string
}): StdReader<string, "t" | "rts"> =>
Reader.asks(env =>
formatLength(
env.translate,
env.responsiveTextSize,
object.unit,
object.value,
),
)
+28 -4
View File
@@ -1,4 +1,9 @@
import type { Translate, Translations } from "../../../helpers/translate.js"
import { Reader } from "@elyukai/utils/reader"
import type {
Translate,
TranslationKeyMatchingParams,
} from "../../../helpers/translate.js"
import type { StdEnv } from "../reader.js"
import { ResponsiveTextSize, responsive } from "../responsiveText.js"
type TimeSpanUnit =
@@ -31,9 +36,9 @@ const timeSpanUnitTranslationKeys = {
Rounds: [".input {$value :number} {{{$value} rounds}}", "{$value} rounds", "{$value} rnds"],
} as const satisfies {
[key in TimeSpanUnit]: [
fullNumber: keyof Translations,
full: keyof Translations,
compressed: keyof Translations,
fullNumber: TranslationKeyMatchingParams<{ value: number }>,
full: TranslationKeyMatchingParams<{ value: number }>,
compressed: TranslationKeyMatchingParams<{ value: number }>,
]
}
@@ -58,3 +63,22 @@ export const formatTimeSpan = (
() => translate(compressedKey, { value }),
)
}
/**
* Returns the text for a time span unit.
*/
export const formatTimeSpanR = (
unit: { kind: TimeSpanUnit },
value: number | string,
): Reader<StdEnv<"t" | "rts">, string> =>
Reader.asks(({ translate, responsiveTextSize }) =>
formatTimeSpan(translate, responsiveTextSize, unit, value),
)
/**
* Returns the text for a time span unit.
*/
export const formatCombinedTimeSpanR = (object: {
unit: { kind: TimeSpanUnit }
value: number | string
}): Reader<StdEnv<"t" | "rts">, string> =>
formatTimeSpanR(object.unit, object.value)
+5 -1
View File
@@ -13,7 +13,11 @@ import { MISSING_VALUE } from "./partial/unknown.js"
export const getPersonalityTraitEntityDescription =
createEntityDescriptionCreator<
"PersonalityTrait",
{ getInstanceById: GetInstanceById<"Culture" | "PersonalityTrait"> }
{
getInstanceById: GetInstanceById<
"Publication" | "Race" | "Culture" | "PersonalityTrait"
>
}
>(({ getInstanceById }, locale, { content: entry }) => {
const { translate, translateMap, join: localeJoin } = locale
const translation = translateMap(entry.translations)
+18 -1
View File
@@ -3,6 +3,7 @@ import { isNotNullish, mapNullable } from "@elyukai/utils/nullable"
import { sign } from "@elyukai/utils/string/number"
import { assertExhaustive } from "@elyukai/utils/typeSafety"
import type {
ActivatableIdentifier,
AnimalVenomLevel,
DemonicPoisonLevel,
Intoxicant,
@@ -13,6 +14,7 @@ import type {
PoisonDuration,
PoisonSourceType,
PoisonStart,
RatedIdentifier,
} from "optolith-database-schema/gen"
import { createEntityDescriptionCreator } from "../creator.js"
import type { GetInstanceById } from "../helpers/getTypes.js"
@@ -423,7 +425,22 @@ const renderValueCost = (
export const getPoisonEntityDescription = createEntityDescriptionCreator<
"Poison",
{
getInstanceById: GetInstanceById<"DerivedCharacteristic" | "Disease">
getInstanceById: GetInstanceById<
| "Publication"
| "DerivedCharacteristic"
| "Disease"
| ActivatableIdentifier["kind"]
| RatedIdentifier["kind"]
| "Race"
| "Culture"
| "State"
| "Enhancement"
| "PactCategory"
| "PactDomain"
| "SocialStatus"
| "Aspect"
| "Property"
>
getResolvedSelectOptionById: GetResolvedSelectOptionById
idMap: IdMap
}
+9 -8
View File
@@ -18,8 +18,8 @@ import type {
GetAllResolvedSkillUses,
} from "../index.js"
import { BaseActivatableTranslation } from "./activatable.js"
import { createImprovementCost } from "./partial/rated/improvementCost.js"
import { getTextForCheck } from "./partial/rated/skillCheck.js"
import { renderImprovementCost } from "./partial/rated/improvementCost.js"
import { renderSkillCheck } from "./partial/rated/skillCheck.js"
const getUsesOrNewApplications = <
T extends ResolvedNewSkillApplication | ResolvedSkillUse,
@@ -69,7 +69,7 @@ export const getSkillEntityDescription = createEntityDescriptionCreator<
"Skill",
{
getInstanceById: GetInstanceById<
"Attribute" | ActivatableIdentifier["kind"]
"Publication" | "Attribute" | ActivatableIdentifier["kind"] | "Aspect"
>
getAllInstances: GetAllInstances<
| "BlessedTradition"
@@ -159,10 +159,11 @@ export const getSkillEntityDescription = createEntityDescriptionCreator<
label: translate("Uses"),
value: uses.join(", "),
},
getTextForCheck(
{ translate, translateMap, getInstanceById },
entry.check,
),
renderSkillCheck(entry.check).run({
translate,
translateMap,
getInstanceById,
}),
{
label: translate("Applications"),
value: applications.join(", "),
@@ -198,7 +199,7 @@ export const getSkillEntityDescription = createEntityDescriptionCreator<
label: translate("Botch"),
value: translation.botch,
},
createImprovementCost(translate, entry.improvement_cost),
renderImprovementCost(entry.improvement_cost).run({ translate }),
],
}
},
+330 -662
View File
File diff suppressed because it is too large Load Diff
+13 -1
View File
@@ -3,10 +3,22 @@
import * as Database from "optolith-database-schema/gen"
import { IdArgsVariant } from "tsondb/schema/gen"
export type GetInstanceById<T extends keyof Database.EntityMap> = <U extends T>(
export type GetInstanceById<
in T extends Extract<keyof Database.EntityMap, string>,
> = <U extends T>(
...args: IdArgsVariant<Database.EntityMap, U>
) => Database.EntityMap[U] | undefined
// export type GetInstanceById<T extends Extract<keyof Database.EntityMap, string>> = {
// <U extends T>(entity: U, id: string): Database.EntityMap[U] | undefined
// <U extends T>(enumCase: Case<U, string>): Database.EntityMap[U] | undefined
// }
// const test = (() =>
// null) as unknown as GetInstanceById<"AdvancedCombatSpecialAbility">
// const test2: GetInstanceById<"Advantage"> = test
export type GetAllInstances<T extends keyof Database.EntityMap> = <U extends T>(
entity: U,
) => { id: string; content: Database.EntityMap[U] }[]
+7 -5
View File
@@ -2,6 +2,11 @@ import { Compare } from "@optolith/helpers/compare"
import type { LocaleMeasurementAdjustments } from "optolith-database-schema/gen"
import { Translate, TranslateMap } from "./translate.js"
/**
* The type of list to join in a locale-aware way.
*/
export type LocaleJoinType = "conjunction" | "disjunction" | "unit"
/**
* The environment for a locale.
*/
@@ -10,7 +15,7 @@ export type LocaleEnvironment = {
translate: Translate
translateMap: TranslateMap
compare: LocaleCompare
join: (list: string[], type: "conjunction" | "disjunction" | "unit") => string
join: (list: string[], type: LocaleJoinType) => string
measurementAdjustments: Required<LocaleMeasurementAdjustments>
}
@@ -22,7 +27,4 @@ export type LocaleCompare = Compare<string>
/**
* A function that joins a list of strings according to the locale's rules for the given type.
*/
export type LocaleJoin = (
list: string[],
type: "conjunction" | "disjunction" | "unit",
) => string
export type LocaleJoin = (list: string[], type: LocaleJoinType) => string
+31 -17
View File
@@ -23,7 +23,10 @@ export type TranslationParams<K extends keyof Translations> =
? Params
: undefined
type TranslationParamsInArray<K extends keyof Translations> =
/**
* Extracts the parameters for a given translation key as an array, or an empty array if the key does not have parameters.
*/
export type TranslationParamsInArray<K extends keyof Translations> =
Translations[K] extends string & { __params: infer Params }
? [params: Params]
: []
@@ -38,7 +41,10 @@ export type LocaleMap<T> = Record<string, T>
*/
export type TranslateMap = <T>(map: LocaleMap<T> | undefined) => T | undefined
type TranslationKeysWithoutParams = {
/**
* The set of translation keys that do not have parameters.
*/
export type TranslationKeysWithoutParams = {
[K in keyof Translations]-?: Translations[K] extends string & {
__params: object
}
@@ -46,37 +52,45 @@ type TranslationKeysWithoutParams = {
: K
}[keyof Translations]
type TranslationKeyMatchingParams<Params> = Params extends object
/**
* Extracts the translation keys that match the parameters.
*/
export type TranslationKeyMatchingParams<Params> = Params extends object
? {
[K in keyof Translations]-?: Translations[K] extends string & {
__params: Params
}
? K
[K in keyof Translations]-?: keyof Params extends keyof TranslationParams<K>
? keyof TranslationParams<K> extends keyof Params
? K
: never
: never
}[keyof Translations]
: TranslationKeysWithoutParams
type TranslationKeyMatchingParamsOfKey<K extends keyof Translations> =
TranslationKeyMatchingParams<TranslationParams<K>>
/**
* Extracts the translation keys that match the parameters of the given key.
*/
export type TranslationKeyMatchingParamsOfKey<T extends keyof Translations> =
Translations[T] extends object
? TranslationKeyMatchingParams<TranslationParams<T>>
: TranslationKeysWithoutParams
/**
* Translates a key that has different translations based on the responsive text size, e.g. a full and a compressed version of the same translation.
*/
export const responsiveTranslate = <K extends keyof Translations>(
export const responsiveTranslate = <
K extends keyof Translations,
K2 extends TranslationKeyMatchingParamsOfKey<K>,
>(
translate: Translate,
responsiveTextSize: ResponsiveTextSize,
fullKey: K,
compressedKey: TranslationKeyMatchingParamsOfKey<K>,
...rest: TranslationParamsInArray<K>
compressedKey: K2,
...rest: TranslationParamsInArray<K> & TranslationParamsInArray<K2>
): string => {
switch (responsiveTextSize) {
case ResponsiveTextSize.Full:
return translate(fullKey, ...rest)
return translate(fullKey, ...(rest as TranslationParamsInArray<K>))
case ResponsiveTextSize.Compressed:
return translate(
compressedKey,
...(rest as unknown as TranslationParamsInArray<typeof compressedKey>),
)
return translate(compressedKey, ...(rest as TranslationParamsInArray<K2>))
default:
return assertExhaustive(responsiveTextSize)
}
@@ -1,36 +1,36 @@
import assert from "node:assert/strict"
import { describe, it } from "node:test"
import { getCheckResultBasedValueTranslation } from "../../../../../src/entities/partial/rated/activatable/checkResultBased.js"
import { renderCheckResultBasedValue } from "../../../../../src/entities/partial/rated/activatable/checkResultBased.js"
import { Case } from "../../../../../src/helpers/enums.js"
import { translateMock } from "../../../../helpers/translate.js"
describe("getTextForCheckResultBased", () => {
it("should return the value text for a check-result-based parameter of an activatable skill", () => {
assert.equal(
getCheckResultBasedValueTranslation(translateMock, {
renderCheckResultBasedValue({
base: Case("QualityLevels"),
}),
}).run({ translate: translateMock }),
"QL",
)
assert.equal(
getCheckResultBasedValueTranslation(translateMock, {
renderCheckResultBasedValue({
base: Case("SkillPoints"),
}),
}).run({ translate: translateMock }),
"SP",
)
assert.equal(
getCheckResultBasedValueTranslation(translateMock, {
renderCheckResultBasedValue({
base: Case("QualityLevels"),
modifier: { arithmetic: Case("Divide"), value: 2 },
}),
"QL / 2",
}).run({ translate: translateMock }),
"QL/2",
)
assert.equal(
getCheckResultBasedValueTranslation(translateMock, {
renderCheckResultBasedValue({
base: Case("SkillPoints"),
modifier: { arithmetic: Case("Multiply"), value: 3 },
}),
"SP × 3",
}).run({ translate: translateMock }),
"SP×3",
)
})
})