@optolith/entity-descriptions 0.11.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
4
4
 
5
+ ## [0.13.0](https://github.com/Optolith/entity-descriptions/compare/v0.12.0...v0.13.0) (2026-09-16)
6
+
7
+ ### ⚠ BREAKING CHANGES
8
+
9
+ * **deps:** upgrade to schema 0.63.0
10
+
11
+ ### build
12
+
13
+ * **deps:** upgrade to schema 0.63.0 ([e879198](https://github.com/Optolith/entity-descriptions/commit/e8791981d739ad4ea786d566eeeb80c918f6e7d5))
14
+ ## [0.12.0](https://github.com/Optolith/entity-descriptions/compare/v0.11.2...v0.12.0) (2026-09-15)
15
+
16
+ ### ⚠ BREAKING CHANGES
17
+
18
+ * **deps:** upgrade to schema 0.62.0
19
+
20
+ ### build
21
+
22
+ * **deps:** upgrade to schema 0.62.0 ([13badb3](https://github.com/Optolith/entity-descriptions/commit/13badb304c61a1f4b48241435d5e19400bbbd145))
23
+
24
+ ### Bug Fixes
25
+
26
+ * nested attributed strings in professions ([cdecedd](https://github.com/Optolith/entity-descriptions/commit/cdecedd5552bb22af4bfec73c4d06ca4b30e3121))
5
27
  ## [0.11.2](https://github.com/Optolith/entity-descriptions/compare/v0.11.1...v0.11.2) (2026-09-14)
6
28
 
7
29
  ### Bug Fixes
@@ -15,7 +15,7 @@ import { Case, fromUniformCase } from "tsondb/schema/gen";
15
15
  import { createEntityDescriptionCreator } from "../creator.js";
16
16
  import { renderActivatableNameComponents } from "./partial/activatableNameChunks.js";
17
17
  import { renderAdventurePointsValue } from "./partial/adventurePointsValue.js";
18
- import { renderResponsiveMap } from "./partial/map.js";
18
+ import { renderParameterMap } from "./partial/map.js";
19
19
  import { attributedName } from "./partial/markdown.js";
20
20
  import { additionFormatter } from "./partial/mathOperation.js";
21
21
  import { printAdvantageDisadvantagePrerequisites, printGeneralPrerequisites, } from "./partial/prerequisites/index.js";
@@ -311,7 +311,7 @@ const renderVolumeValue = (translate, translateMap, getAllResolvedSelectOptions,
311
311
  levels: volume.ByLevel.list.map((_, index) => romanize(index + 1)).join("/"),
312
312
  });
313
313
  case "Map":
314
- return renderResponsiveMap(volume.Map, option => Reader.of(option.points), values => translateR("{$points} points", { points: values })).run({
314
+ return renderParameterMap(volume.Map, option => Reader.of(option.points), values => translateR("{$points} points", { points: values })).run({
315
315
  translate,
316
316
  translateMap,
317
317
  responsiveTextSize,
@@ -454,7 +454,7 @@ const renderBindingCost = (translate, translateMap, localeCompare, getAllResolve
454
454
  }),
455
455
  });
456
456
  case "Map":
457
- return renderResponsiveMap(cost.Map, option => Reader.of(option.permanentValue), values => translateR("{$value} permanent AE", { value: values })).run({
457
+ return renderParameterMap(cost.Map, option => Reader.of(option.permanentValue), values => translateR("{$value} permanent AE", { value: values })).run({
458
458
  translate,
459
459
  translateMap,
460
460
  responsiveTextSize,
@@ -136,7 +136,7 @@ export const renderCombinedActivatableNameComponents = (translateMap, components
136
136
  useParenthesis: true,
137
137
  },
138
138
  options: join(components.options.map(chunk => renderActivatableNameChunk(translateMap, normalizeChunks(chunk)))),
139
- }, formatAsPrerequisite), components.id.kind, fromUniformCase(components.id), { context });
139
+ }, formatAsPrerequisite), components.id.kind, fromUniformCase(components.id), { context: `"${context}"` });
140
140
  /**
141
141
  * Renders the name components of multiple activatable entries.
142
142
  */
@@ -1,10 +1,14 @@
1
1
  import { on } from "@elyukai/utils/function";
2
2
  import { isNotNullish } from "@elyukai/utils/nullable";
3
+ import { Reader } from "@elyukai/utils/reader";
3
4
  import { unique } from "@optolith/helpers/array";
4
5
  import { deepEqual, numAsc } from "@optolith/helpers/compare";
5
6
  import { romanize } from "@optolith/helpers/roman";
6
7
  import { assertExhaustive } from "@optolith/helpers/typeSafety";
8
+ import { renderDefaultValueMapLabel, renderValueMap } from "./map.js";
7
9
  import { evaluateMathOperation } from "./mathOperation.js";
10
+ import { translateR } from "./reader.js";
11
+ import { ResponsiveTextSize } from "./responsiveText.js";
8
12
  import { MISSING_VALUE } from "./unknown.js";
9
13
  const typographicSign = (num) => (num > 0 ? "" : "−") + Math.abs(num).toFixed();
10
14
  const renderFixedSelectOptionsAdventurePointsValue = (locale, getAllSelectOptions, getNameForSelectOptionId, getApValueForSelectOption, applyNegative) => {
@@ -250,8 +254,10 @@ export const renderAdventurePointsValue = (locale, getNameForId, getNameForSelec
250
254
  });
251
255
  }
252
256
  case "Expression": {
253
- const expression = value.DependingOnActiveInstances.Expression;
254
- const values = Array.from({ length: entry.maximum ?? 3 }, (_, index) => applyNegative(evaluateMathOperation(expression, exprValue => {
257
+ const { expression, customLabels } = value.DependingOnActiveInstances.Expression;
258
+ const values = Array.from({
259
+ length: entry.maximum ?? (customLabels === undefined ? 3 : customLabels.options.length),
260
+ }, (_, index) => applyNegative(evaluateMathOperation(expression, exprValue => {
255
261
  switch (exprValue.kind) {
256
262
  case "Constant":
257
263
  return exprValue.Constant;
@@ -260,7 +266,18 @@ export const renderAdventurePointsValue = (locale, getNameForId, getNameForSelec
260
266
  default:
261
267
  return assertExhaustive(exprValue);
262
268
  }
263
- }))).join("/");
269
+ })));
270
+ if (customLabels !== undefined) {
271
+ return renderValueMap(entry.maximum === undefined
272
+ ? customLabels
273
+ : { ...customLabels, options: customLabels.options.slice(0, entry.maximum) },
274
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- customLabels.options is guaranteed to have at least as many entries as values
275
+ (_, index) => Reader.of(values[index]), renderDefaultValueMapLabel, singleValue => typeof singleValue === "number"
276
+ ? translateR(".input {$value :number} {{{$value} Adventure Points}}", {
277
+ value: singleValue,
278
+ })
279
+ : translateR("{$value} Adventure Points", { value: singleValue })).run({ translate, translateMap, responsiveTextSize: ResponsiveTextSize.Full });
280
+ }
264
281
  const labels = Array.from({ length: entry.maximum ?? 3 }, (_, index) => `${(index + 1).toFixed()}.`).join("/");
265
282
  if (entry.maximum !== undefined) {
266
283
  return translate("{$values} Adventure Points for the {$labels} purchase", {
@@ -5,7 +5,7 @@ import { assertExhaustive } from "@elyukai/utils/typeSafety";
5
5
  import { ensureNonEmpty } from "@optolith/helpers/array";
6
6
  import { Case } from "tsondb/schema/gen";
7
7
  import { makeNameBuilderRulesWithDefaults, renderCombinedActivatableNameComponents, renderNameComponentsOptions, } from "./activatableNameChunks.js";
8
- import { attributedCustomName } from "./markdown.js";
8
+ import { attributedNameFromText, customName } from "./markdown.js";
9
9
  import { MISSING_VALUE } from "./unknown.js";
10
10
  const convertCommonnessRatedAdvantageOrDisadvantageLevel = (level) => {
11
11
  switch (level.kind) {
@@ -29,9 +29,9 @@ const makeActivatableIdentifierForCommonnessRatedAdvantageOrDisadvantage = (enti
29
29
  };
30
30
  const renderCommonnessRatedAdvantageOrDisadvantageName = (translateMap, getInstanceById, getResolvedSelectOptionById, localeCompare, entity, item) => {
31
31
  const customTranslation = translateMap(item.translations);
32
- return (attributedCustomName(translateMap, getInstanceById, entity, (translation, instance) => {
32
+ return (customName(translateMap, getInstanceById, (translation, instance) => {
33
33
  if (customTranslation?.full !== undefined) {
34
- return customTranslation.full;
34
+ return attributedNameFromText(customTranslation.full, "commonness", entity, item.id);
35
35
  }
36
36
  const id = makeActivatableIdentifierForCommonnessRatedAdvantageOrDisadvantage(entity, item.id);
37
37
  const level = item.level === undefined
@@ -1,40 +1,26 @@
1
1
  import { Reader } from "@elyukai/utils/reader";
2
- import type { ParameterMapStyle, ResponsiveTextOptional } from "@optolith/database-schema/gen";
2
+ import type { ParameterMap, ParameterMapOption, ResponsiveTextOptional, ValueMap } from "@optolith/database-schema/gen";
3
3
  import type { StdEnv, StdReader } from "../../env.js";
4
4
  import type { LocaleMap } from "../../helpers/translate.js";
5
5
  /**
6
6
  * Renders a map of options to a string.
7
7
  */
8
- export declare const renderMap: <V, T, OT, VEnv, SVEnv>(map: {
9
- options: {
10
- value: V;
11
- translations?: LocaleMap<OT>;
12
- }[];
13
- style: ParameterMapStyle;
14
- translations?: LocaleMap<T>;
15
- }, getValue: (option: V) => Reader<VEnv, string | number>, surroundValues: ((values: string) => Reader<SVEnv, string>) | undefined, getLabel: (optionTranslation: OT) => string, getListPrefix: (translation: T) => string | undefined, getListSuffix: (translation: T) => string | undefined, getReplacement: (translation: T) => string | undefined, ...moreToAppend: ({
16
- surround: (values: string) => StdReader<string, "t">;
17
- getAdditionalValue: (option: V, optionTranslation: OT) => string | number;
18
- } | undefined)[]) => Reader<StdEnv<"t" | "tm"> & VEnv & SVEnv, string>;
8
+ export declare const renderValueMap: <T, TE1, TE2, TE3, TE4, TE5, V extends string | number>(map: ValueMap<T>, renderValueMapOptionValue: (option: T, index: number) => Reader<TE1, V>, renderValueMapOptionLabel: (option: T, index: number) => Reader<TE2, string>, formatValue: (value: V | string) => Reader<TE3, string>, ...moreToAppend: ({
9
+ render: (option: T, index: number) => Reader<TE4, V>;
10
+ format: (value: V | string) => Reader<TE5, string>;
11
+ } | undefined)[]) => Reader<TE1 & TE2 & TE3 & TE4 & TE5 & StdEnv<"t" | "tm" | "rts">, string>;
19
12
  /**
20
- * Renders a map of options to a string.
13
+ * Render the label of a value map option.
21
14
  */
22
- export declare const renderResponsiveMap: <T, VEnv, SVEnv>(map: {
23
- options: {
24
- value: T;
25
- translations?: LocaleMap<{
26
- label: ResponsiveTextOptional;
27
- }>;
28
- }[];
29
- style: ParameterMapStyle;
30
- translations?: LocaleMap<{
31
- listPrefix?: ResponsiveTextOptional;
32
- listSuffix?: ResponsiveTextOptional;
33
- replacement?: ResponsiveTextOptional;
34
- }>;
35
- }, getValue: (option: T) => Reader<VEnv, string | number>, surroundValues: ((values: string) => Reader<SVEnv, string>) | undefined, ...moreToAppend: ({
36
- surround: (values: string) => StdReader<string, "t">;
37
- getAdditionalValue: (option: T, optionTranslation: {
15
+ export declare const renderDefaultValueMapLabel: (option: {
16
+ translations: LocaleMap<{
38
17
  label: ResponsiveTextOptional;
39
- }) => string | number;
40
- } | undefined)[]) => Reader<StdEnv<"t" | "tm" | "rts"> & VEnv & SVEnv, string>;
18
+ }>;
19
+ }) => StdReader<string, "tm" | "rts">;
20
+ /**
21
+ * Renders a map of parameter options to a string.
22
+ */
23
+ export declare const renderParameterMap: <T, TE1, TE2, TE3, TE4, V extends string | number>(map: ParameterMap<T>, renderParameterMapValue: (value: T, index: number) => Reader<TE1, V>, formatValue: (value: V | string) => Reader<TE2, string>, ...moreToAppend: ({
24
+ render: (option: ParameterMapOption<T>, index: number) => Reader<TE3, V>;
25
+ format: (value: V | string) => Reader<TE4, string>;
26
+ } | undefined)[]) => Reader<TE1 & StdEnv<"tm" | "rts"> & TE2 & TE3 & TE4 & StdEnv<"t" | "tm" | "rts">, string>;
@@ -1,57 +1,44 @@
1
- import { mapNullableDefault } from "@elyukai/utils/nullable";
2
1
  import { Reader } from "@elyukai/utils/reader";
3
- import { isNotNullish, mapNullable } from "@optolith/helpers/nullable";
4
- import { getResponsiveTextOptional } from "./responsiveText.js";
2
+ import { assertExhaustive } from "@elyukai/utils/typeSafety";
3
+ import { isNotNullish } from "@optolith/helpers/nullable";
4
+ import { responsiveTextOptionalR, sequence, translateMapR, translateR } from "./reader.js";
5
5
  import { MISSING_VALUE } from "./unknown.js";
6
+ const renderReplacement = (translation, alt) => translation?.replacement !== undefined
7
+ ? responsiveTextOptionalR(translation.replacement).thenW(replacement => replacement === undefined ? alt() : Reader.of(replacement))
8
+ : alt();
9
+ const renderListJoiner = (joiner, spacePosition) => joiner === undefined
10
+ ? ""
11
+ : responsiveTextOptionalR(joiner).map(text => text === undefined ? "" : spacePosition === "before" ? ` ${text}` : `${text} `);
6
12
  /**
7
13
  * Renders a map of options to a string.
8
14
  */
9
- export const renderMap = (map, getValue, surroundValues = values => Reader.of(values), getLabel, getListPrefix, getListSuffix, getReplacement, ...moreToAppend) => Reader.asks(env => {
10
- const translation = env.translateMap(map.translations);
11
- const replacement = mapNullable(translation, t => getReplacement(t));
12
- if (isNotNullish(replacement)) {
13
- return replacement;
14
- }
15
- if (map.style.kind === "Verbose") {
16
- return map.options
17
- .map(option => {
18
- const value = getValue(option.value).run(env);
19
- const optionTranslation = env.translateMap(option.translations);
20
- return `${surroundValues(typeof value === "number" ? value.toFixed() : value).run(env)} ${env.translate("for")} ${mapNullableDefault(mapNullable(translation, getListPrefix), listPrefix => `${listPrefix} `, "")}${optionTranslation === undefined ? MISSING_VALUE : getLabel(optionTranslation)}${mapNullableDefault(mapNullable(translation, getListSuffix), listSuffix => ` ${listSuffix}`, "")}${moreToAppend
21
- .filter(isNotNullish)
22
- .map(({ surround, getAdditionalValue }) => {
23
- const additionalValue = optionTranslation === undefined
24
- ? MISSING_VALUE
25
- : getAdditionalValue(option.value, optionTranslation);
26
- return surround(typeof additionalValue === "number" ? additionalValue.toFixed() : additionalValue).run(env);
27
- })
28
- .join("")}`;
29
- })
30
- .join(", ");
31
- }
32
- return `${surroundValues(map.options.map(option => getValue(option.value).run(env)).join("/")).run(env)} ${env.translate("for")} ${mapNullableDefault(mapNullable(translation, getListPrefix), listPrefix => `${listPrefix} `, "")}${map.options
33
- .map(option => {
34
- const optionTranslation = env.translateMap(option.translations);
35
- if (optionTranslation === undefined) {
36
- return MISSING_VALUE;
15
+ export const renderValueMap = (map, renderValueMapOptionValue, renderValueMapOptionLabel, formatValue, ...moreToAppend) => translateMapR(map.translations).thenW(translation => renderReplacement(translation, () => {
16
+ const { listPrefix, listSuffix } = translation ?? {};
17
+ const listPrefixR = renderListJoiner(listPrefix, "after");
18
+ const listSuffixR = renderListJoiner(listSuffix, "before");
19
+ const moreToAppendFns = moreToAppend.filter(isNotNullish);
20
+ switch (map.style.kind) {
21
+ case "Compressed": {
22
+ return Reader.traverse(map.options, (option, index) => renderValueMapOptionValue(option, index).thenW(value => renderValueMapOptionLabel(option, index).map(label => ({ value, label })))).thenW(renderedOptions => sequence `${formatValue(renderedOptions.map(option => (typeof option.value === "number" ? option.value.toFixed() : option.value)).join("/"))} ${translateR("for")} ${listPrefixR}${renderedOptions
23
+ .map(option => option.label)
24
+ .join("/")}${listSuffixR}${Reader.traverse(moreToAppendFns, more => Reader.traverse(map.options, more.render).thenW(values => more.format(values.join("/")))).map(values => values.join(""))}`);
37
25
  }
38
- return getLabel(optionTranslation);
39
- })
40
- .join("/")}${mapNullableDefault(mapNullable(translation, getListSuffix), listSuffix => ` ${listSuffix}`, "")}${moreToAppend
41
- .filter(isNotNullish)
42
- .map(({ surround, getAdditionalValue }) => surround(map.options
43
- .map(option => {
44
- const optionTranslation = env.translateMap(option.translations);
45
- if (optionTranslation === undefined) {
46
- return MISSING_VALUE;
26
+ case "Verbose": {
27
+ return Reader.traverse(map.options, (option, index) => renderValueMapOptionValue(option, index)
28
+ .thenW(value => renderValueMapOptionLabel(option, index).map(label => ({ value, label })))
29
+ .thenW(({ value, label }) => sequence `${formatValue(value)} ${translateR("for")} ${listPrefixR}${label}${listSuffixR}${Reader.traverse(moreToAppendFns, more => more.render(option, index).thenW(more.format)).map(values => values.join(""))}`)).map(values => values.join(", "));
47
30
  }
48
- return getAdditionalValue(option.value, optionTranslation);
49
- })
50
- .join("/")).run(env))
51
- .join("")}`;
52
- });
31
+ default:
32
+ return assertExhaustive(map.style);
33
+ }
34
+ }));
53
35
  /**
54
- * Renders a map of options to a string.
36
+ * Render the label of a value map option.
37
+ */
38
+ export const renderDefaultValueMapLabel = (option) => translateMapR(option.translations).thenW(translation => translation?.label === undefined
39
+ ? Reader.of(MISSING_VALUE)
40
+ : responsiveTextOptionalR(translation.label).map(text => text ?? translation.label.full));
41
+ /**
42
+ * Renders a map of parameter options to a string.
55
43
  */
56
- export const renderResponsiveMap = (map, getValue, surroundValues, ...moreToAppend) => Reader.asks(env => renderMap(map, getValue, surroundValues, translation => getResponsiveTextOptional(translation.label, env.responsiveTextSize) ??
57
- translation.label.full, translation => mapNullable(translation.listPrefix, text => getResponsiveTextOptional(text, env.responsiveTextSize)), translation => mapNullable(translation.listSuffix, text => getResponsiveTextOptional(text, env.responsiveTextSize)), translation => mapNullable(translation.replacement, text => getResponsiveTextOptional(text, env.responsiveTextSize)), ...moreToAppend).run(env));
44
+ export const renderParameterMap = (map, renderParameterMapValue, formatValue, ...moreToAppend) => renderValueMap(map, (option, index) => renderParameterMapValue(option.value, index), renderDefaultValueMapLabel, formatValue, ...moreToAppend);
@@ -26,7 +26,7 @@ export declare const attributedName: <E extends { [K in keyof EntityMap]: Entity
26
26
  */
27
27
  export declare const customName: <E extends { [K in keyof EntityMap]: EntityMap[K] extends {
28
28
  translations: LocaleMap<object>;
29
- } ? K : never; }[keyof EntityMap]>(translateMap: TranslateMap, getInstanceById: GetInstanceById<E>, fn: (translation: EntityMap[E]["translations"][string]) => string, ...args: IdArgsVariant<EntityMap, E>) => string | undefined;
29
+ } ? K : never; }[keyof EntityMap]>(translateMap: TranslateMap, getInstanceById: GetInstanceById<E>, fn: (translation: EntityMap[E]["translations"][string], instance: EntityMap[E]) => string, ...args: IdArgsVariant<EntityMap, E>) => string | undefined;
30
30
  /**
31
31
  * Renders the name of an instance according to a custom function in an attributed string.
32
32
  */
@@ -47,6 +47,10 @@ export declare const attributedNameFromInstance: (translateMap: TranslateMap, in
47
47
  export declare const attributedNameFromTranslation: (translation: {
48
48
  name: string;
49
49
  } | undefined, context: string, ...args: IdArgsVariant<EntityMap, keyof EntityMap>) => string | undefined;
50
+ /**
51
+ * Renders the name of an instance in an attributed string.
52
+ */
53
+ export declare const attributedNameFromText: (text: string, context: string, ...args: IdArgsVariant<EntityMap, keyof EntityMap>) => string;
50
54
  /**
51
55
  * Renders the name of an instance in an attributed string.
52
56
  */
@@ -28,7 +28,12 @@ export const attributedName = (translateMap, getInstanceById, context, ...args)
28
28
  /**
29
29
  * Renders the name of an instance according to a custom function.
30
30
  */
31
- export const customName = (translateMap, getInstanceById, fn, ...args) => mapNullable(translateMap(getInstanceById(...args)?.translations), translation => fn(translation));
31
+ export const customName = (translateMap, getInstanceById, fn, ...args) => {
32
+ const instance = getInstanceById(...args);
33
+ return mapNullable(translateMap(instance?.translations), translation =>
34
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- instance must be non-nullish if translation is non-nullish
35
+ fn(translation, instance));
36
+ };
32
37
  /**
33
38
  * Renders the name of an instance according to a custom function in an attributed string.
34
39
  */
@@ -53,6 +58,13 @@ export const attributedNameFromTranslation = (translation, context, ...args) =>
53
58
  const id = normalizedIdArgs(args);
54
59
  return mapNullable(translation?.name, name => attributedInstance(name, id.entityName, id.id, { context: `"${context}"` }));
55
60
  };
61
+ /**
62
+ * Renders the name of an instance in an attributed string.
63
+ */
64
+ export const attributedNameFromText = (text, context, ...args) => {
65
+ const id = normalizedIdArgs(args);
66
+ return attributedInstance(text, id.entityName, id.id, { context: `"${context}"` });
67
+ };
56
68
  /**
57
69
  * Renders the name of an instance in an attributed string.
58
70
  */
@@ -1,5 +1,5 @@
1
1
  import { Reader } from "@elyukai/utils/reader";
2
- import type { ActivatableIdentifier, AdvantageDisadvantagePrerequisites, AnimistPowerPrerequisites, ArcaneTraditionPrerequisites, DerivedCharacteristicPrerequisites, EnhancementPrerequisites, GeneralPrerequisites, GeodeRitualPrerequisites, InfluencePrerequisites, LanguagePrerequisites, LiturgyPrerequisites, PersonalityTraitPrerequisites, PlainGeneralPrerequisites, ProfessionPrerequisites, PublicationPrerequisites, SpellworkPrerequisites } from "@optolith/database-schema/gen";
2
+ import type { ActivatableIdentifier, AdvantageDisadvantagePrerequisites, AnimistPowerPrerequisites, ArcaneTraditionPrerequisites, DerivedCharacteristicPrerequisites, EnhancementPrerequisites, GeneralPrerequisites, GeodeRitualPrerequisites, InfluencePrerequisites, LanguagePrerequisites, LiturgyPrerequisites, PersonalityTraitPrerequisites, PlainGeneralPrerequisites, ProfessionPrerequisites, ProfessionVariantPrerequisites, PublicationPrerequisites, SpellworkPrerequisites } from "@optolith/database-schema/gen";
3
3
  import type { StdEnv } from "../../../env.js";
4
4
  import { printAdvantageDisadvantagePrerequisiteGroup, printAnimistPowerPrerequisiteGroup, printArcaneTraditionPrerequisiteGroup, printDerivedCharacteristicPrerequisiteGroup, printEnhancementPrerequisiteGroup, printGeneralPrerequisiteGroup, printGeodeRitualPrerequisiteGroup, printInfluencePrerequisiteGroup, printLanguagePrerequisiteGroup, printLiturgyPrerequisiteGroup, printPersonalityTraitPrerequisiteGroup, printProfessionPrerequisiteGroup, printPublicationPrerequisiteGroup, printSpellworkPrerequisiteGroup } from "./prerequisiteGroups.js";
5
5
  type DeriveEnvF<T extends (...args: any[]) => any> = ReturnType<T> extends Reader<infer E, unknown> ? E : never;
@@ -26,6 +26,10 @@ export declare const printGeneralPrerequisites: (value: GeneralPrerequisites, pr
26
26
  * Print profession prerequisites as a string.
27
27
  */
28
28
  export declare const printProfessionPrerequisites: (value: ProfessionPrerequisites) => Reader<DeriveEnvF<typeof printProfessionPrerequisiteGroup> & StdEnv<"lj" | "lc">, string>;
29
+ /**
30
+ * Print profession prerequisites as a string.
31
+ */
32
+ export declare const printProfessionVariantPrerequisites: (value: ProfessionVariantPrerequisites) => Reader<DeriveEnvF<typeof printProfessionPrerequisiteGroup> & StdEnv<"lj" | "lc">, string>;
29
33
  /**
30
34
  * Print advantage disadvantage prerequisites as a string.
31
35
  */
@@ -76,6 +76,23 @@ const printPlainPrerequisites = (printPrerequisite, prerequisites) => Reader.tra
76
76
  type: element.kind === "Single" ? element.Single.kind : element.kind,
77
77
  part,
78
78
  })))).thenW(list => joinPrerequisiteParts(list.filter(isNotNullish)));
79
+ const printPrerequisiteDiff = (printPrerequisite, map, element) => {
80
+ switch (element.kind) {
81
+ case "Add":
82
+ return printPrerequisite(element.Add).map(part => map(element.Add, part));
83
+ case "Remove":
84
+ return printPrerequisite(element.Remove).map(part => map(element.Remove, part === undefined ? undefined : { ...part, isRemoved: true }));
85
+ default:
86
+ return assertExhaustive(element);
87
+ }
88
+ };
89
+ /**
90
+ * Print plain prerequisites as a string.
91
+ */
92
+ const printPlainDiffPrerequisites = (printPrerequisite, prerequisites) => Reader.traverse(prerequisites, diffElement => printPrerequisiteDiff(element => printPrerequisitesElement(printPrerequisite, element), (element, nullablePart) => mapNullable(nullablePart, part => ({
93
+ type: element.kind === "Single" ? element.Single.kind : element.kind,
94
+ part,
95
+ })), diffElement)).thenW(list => joinPrerequisiteParts(list.filter(isNotNullish)));
79
96
  /**
80
97
  * Print prerequisite for level as a string.
81
98
  */
@@ -169,6 +186,10 @@ export const printGeneralPrerequisites = (value, printPreviousLevelPrerequisites
169
186
  * Print profession prerequisites as a string.
170
187
  */
171
188
  export const printProfessionPrerequisites = (value) => printPlainPrerequisites(printProfessionPrerequisiteGroup, value);
189
+ /**
190
+ * Print profession prerequisites as a string.
191
+ */
192
+ export const printProfessionVariantPrerequisites = (value) => printPlainDiffPrerequisites(printProfessionPrerequisiteGroup, value);
172
193
  /**
173
194
  * Print advantage disadvantage prerequisites as a string.
174
195
  */
@@ -9,6 +9,7 @@ export type PrerequisitePart = {
9
9
  value: string | ActivatableNameComponents;
10
10
  sentenceType: SentenceType | undefined;
11
11
  isMeta: boolean;
12
+ isRemoved?: true;
12
13
  };
13
14
  /**
14
15
  * Type guard for prerequisite parts with a value object.
@@ -15,7 +15,8 @@ export const hasPartValueObject = (part) => typeof part.value !== "string";
15
15
  const countFurtherToCombined = (part, remainingParts) => {
16
16
  const indexHasDifferentBaseValues = remainingParts.findIndex(remaining => !(part.label === remaining.label &&
17
17
  deepEqual(part.value.id, remaining.value.id) &&
18
- part.value.level === remaining.value.level));
18
+ part.value.level === remaining.value.level &&
19
+ part.isRemoved === remaining.isRemoved));
19
20
  return indexHasDifferentBaseValues === -1
20
21
  ? // if -1 is returned, all remaining parts have the same base values, so we can combine with all of them
21
22
  remainingParts.length
@@ -59,8 +60,11 @@ const appendPrerequisitePartGroup = (previous, parts, isLast) => {
59
60
  if (parts.length === 0) {
60
61
  return Reader.of(previous);
61
62
  }
62
- else if (parts.every(hasPartValueObject)) {
63
- return Reader.asks(({ translate, translateMap, localeCompare }) => appendBySentenceType(previous, parts
63
+ const wrapInNoPrerequisite = (text) => parts[0]?.isRemoved === true
64
+ ? translateR("no prerequisite {$prerequisite}", { prerequisite: text })
65
+ : Reader.of(text);
66
+ if (parts.every(hasPartValueObject)) {
67
+ return Reader.asks(({ translate, translateMap, localeCompare }) => appendBySentenceType(previous, wrapInNoPrerequisite(parts
64
68
  .toSorted(on(part => fromUniformCase(part.value.id), localeCompare))
65
69
  .reduce((acc, current, i, arr) => {
66
70
  if (acc[1] > 0) {
@@ -70,20 +74,23 @@ const appendPrerequisitePartGroup = (previous, parts, isLast) => {
70
74
  return [
71
75
  [
72
76
  ...acc[0],
73
- [activatableKindToGroup(current.value.id.kind), (current.label ?? "") + rendered],
77
+ [
78
+ activatableKindToGroup(current.value.id.kind),
79
+ (current.label ?? "") + rendered,
80
+ ],
74
81
  ],
75
82
  furtherIncluded,
76
83
  ];
77
84
  }, [[], 0])[0]
78
85
  .toSorted(sortByActivatableGroupAndName(localeCompare))
79
86
  .map(grouped => grouped[1])
80
- .join(", "), undefined, isLast));
87
+ .join(", ")).run({ translate }), undefined, isLast));
81
88
  }
82
89
  else {
83
- return Reader.asks(({ translateMap }) => parts.reduce((acc, current, i, arr) => appendBySentenceType(acc, (current.label ?? "") +
90
+ return Reader.asks(({ translate, translateMap }) => parts.reduce((acc, current, i, arr) => appendBySentenceType(acc, wrapInNoPrerequisite((current.label ?? "") +
84
91
  (typeof current.value === "string"
85
92
  ? current.value
86
- : wrapActivatableInAttributedString(renderActivatableNameComponents(translateMap, current.value, true), current.value.id)), current.sentenceType, isLast && i === arr.length - 1), previous));
93
+ : wrapActivatableInAttributedString(renderActivatableNameComponents(translateMap, current.value, true), current.value.id))).run({ translate }), current.sentenceType, isLast && i === arr.length - 1), previous));
87
94
  }
88
95
  };
89
96
  /**
@@ -42,10 +42,12 @@ export declare const renderIndefiniteCost: (value: IndefiniteCost, shouldAppendN
42
42
  /**
43
43
  * Returns the text for a standalone one-time cost map of an activatable skill.
44
44
  */
45
- export declare const renderStandaloneCostMap: (value: StandaloneCostMap) => Reader<StdEnv<"t" | "tm" | "rts"> & {
45
+ export declare const renderStandaloneCostMap: (value: StandaloneCostMap) => Reader<StdEnv<"tm" | "rts"> & {
46
46
  translate: import("../../../../helpers/translate.js").Translate;
47
47
  energyUnit: import("../../units/energy.js").EnergyUnit;
48
- }, string>;
48
+ } & {
49
+ translate: import("../../../../helpers/translate.js").Translate;
50
+ } & StdEnv<"t" | "tm" | "rts">, string>;
49
51
  /**
50
52
  * Returns the text for the cost of a one-time activatable skill.
51
53
  */
@@ -2,7 +2,7 @@ import { identity } from "@elyukai/utils/function";
2
2
  import { Reader } from "@elyukai/utils/reader";
3
3
  import { mapNullable } from "@optolith/helpers/nullable";
4
4
  import { assertExhaustive } from "@optolith/helpers/typeSafety";
5
- import { renderResponsiveMap } from "../../map.js";
5
+ import { renderParameterMap } from "../../map.js";
6
6
  import { formatEnergyFnR, formatEnergyR, getInstanceByIdFnR, modifiableBySpeedR, responsiveLocaleJoinR, responsiveR, responsiveTextR, responsiveTranslateR, translateMapR, translateR, } from "../../reader.js";
7
7
  import { appendNoteIfNeeded, replaceTextIfNeeded } from "../../responsiveText.js";
8
8
  import { formatCombinedTimeSpanR } from "../../units/timeSpan.js";
@@ -114,13 +114,13 @@ const renderMultipleOneTimeCosts = (type, value) => {
114
114
  /**
115
115
  * Returns the text for a standalone one-time cost map of an activatable skill.
116
116
  */
117
- export const renderStandaloneCostMap = (value) => renderResponsiveMap(value, option => Reader.of(option.value), formatEnergyR, value.options.every(option => option.value.permanentValue !== undefined)
117
+ export const renderStandaloneCostMap = (value) => renderParameterMap(value, option => Reader.of(option.value), formatEnergyR, value.options.every(option => option.value.permanentValue !== undefined)
118
118
  ? {
119
- surround: values => translateR(", {$value} of which are permanent", {
119
+ format: values => translateR(", {$value} of which are permanent", {
120
120
  value: values,
121
121
  }),
122
122
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- is checked beforehand
123
- getAdditionalValue: option => option.permanentValue,
123
+ render: option => Reader.of(option.value.permanentValue),
124
124
  }
125
125
  : undefined);
126
126
  /**
@@ -129,23 +129,23 @@ export const renderStandaloneCostMap = (value) => renderResponsiveMap(value, opt
129
129
  const renderOneTimeCostMap = (value) => {
130
130
  switch (value.kind) {
131
131
  case "Modifiable":
132
- return renderResponsiveMap(value.Modifiable.map, option => deriveModifiableCost(option.initialModificationLevel).map(cost => cost ?? MISSING_VALUE), formatEnergyR, value.Modifiable.map.options.every(option => option.value.permanentValue !== undefined)
132
+ return renderParameterMap(value.Modifiable.map, option => deriveModifiableCost(option.initialModificationLevel).map(cost => cost ?? MISSING_VALUE), singleValue => formatEnergyR(singleValue), value.Modifiable.map.options.every(option => option.value.permanentValue !== undefined)
133
133
  ? {
134
- surround: values => translateR(", {$value} of which are permanent", {
134
+ format: values => translateR(", {$value} of which are permanent", {
135
135
  value: values,
136
136
  }),
137
137
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- is checked beforehand
138
- getAdditionalValue: option => option.permanentValue,
138
+ render: option => Reader.of(option.value.permanentValue),
139
139
  }
140
140
  : undefined);
141
141
  case "NonModifiable":
142
- return renderResponsiveMap(value.NonModifiable.map, option => Reader.of(option.value), formatEnergyR, value.NonModifiable.map.options.every(option => option.value.permanentValue !== undefined)
142
+ return renderParameterMap(value.NonModifiable.map, option => Reader.of(option.value), formatEnergyR, value.NonModifiable.map.options.every(option => option.value.permanentValue !== undefined)
143
143
  ? {
144
- surround: values => translateR(", {$value} of which are permanent", {
144
+ format: values => translateR(", {$value} of which are permanent", {
145
145
  value: values,
146
146
  }),
147
147
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- is checked beforehand
148
- getAdditionalValue: option => option.permanentValue,
148
+ render: option => Reader.of(option.value.permanentValue),
149
149
  }
150
150
  : undefined).then(text => appendNonModifiableSuffix(ModifiableParameter.Cost, text));
151
151
  default:
@@ -155,9 +155,9 @@ const renderOneTimeCostMap = (value) => {
155
155
  const renderSustainedCostMap = (value) => {
156
156
  switch (value.kind) {
157
157
  case "Modifiable":
158
- return renderResponsiveMap(value.Modifiable.map, option => deriveModifiableCost(option.initialModificationLevel).map(cost => cost ?? MISSING_VALUE), singleValue => formatEnergyR(singleValue).thenW(energy => appendIntervalToCostR(value.Modifiable.interval, energy)));
158
+ return renderParameterMap(value.Modifiable.map, option => deriveModifiableCost(option.initialModificationLevel).map(cost => cost ?? MISSING_VALUE), singleValue => formatEnergyR(singleValue).thenW(energy => appendIntervalToCostR(value.Modifiable.interval, energy)));
159
159
  case "NonModifiable":
160
- return renderResponsiveMap(value.NonModifiable.map, option => Reader.of(option.value), singleValue => formatEnergyR(singleValue).thenW(energy => appendIntervalToCostR(value.NonModifiable.interval, energy))).then(text => appendNonModifiableSuffix(ModifiableParameter.Cost, text));
160
+ return renderParameterMap(value.NonModifiable.map, option => Reader.of(option.value.toFixed()), singleValue => formatEnergyR(singleValue).thenW(energy => appendIntervalToCostR(value.NonModifiable.interval, energy))).then(text => appendNonModifiableSuffix(ModifiableParameter.Cost, text));
161
161
  default:
162
162
  return assertExhaustive(value);
163
163
  }
@@ -262,4 +262,8 @@ export declare const modifyBySpeedR: Reader<{
262
262
  * 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.
263
263
  */
264
264
  export declare const mapNullableR: <T, U, R>(defaultValue: U, fn: (value: NonNullable<T>) => Reader<U, R>) => (value: T) => Reader<U, R> | Reader<unknown, U>;
265
+ /**
266
+ * Use `Reader` instances directly in a template string.
267
+ */
268
+ export declare const sequence: <E>(string: TemplateStringsArray, ...values: (Reader<E, string> | string)[]) => Reader<E, string>;
265
269
  export {};
@@ -194,3 +194,7 @@ export const modifyBySpeedR = Reader.asks(({ speed }) => (key, level) => {
194
194
  * 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.
195
195
  */
196
196
  export const mapNullableR = (defaultValue, fn) => (value) => isNotNullish(value) ? fn(value) : Reader.of(defaultValue);
197
+ /**
198
+ * Use `Reader` instances directly in a template string.
199
+ */
200
+ export const sequence = (string, ...values) => Reader.traverse(values, value => (value instanceof Reader ? value : Reader.of(value))).map(resolvedValues => string.reduce((acc, str, i) => acc + str + (resolvedValues[i] ?? ""), ""));