@optolith/entity-descriptions 0.13.2 → 0.14.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/lib/creator.js +1 -1
  3. package/lib/entities/activatable.js +22 -14
  4. package/lib/entities/culture.js +3 -3
  5. package/lib/entities/curriculum.js +1 -1
  6. package/lib/entities/elixir.js +1 -1
  7. package/lib/entities/equipment.js +1 -1
  8. package/lib/entities/partial/adventurePointsValue.js +8 -7
  9. package/lib/entities/partial/dice.js +2 -2
  10. package/lib/entities/partial/enhancements.js +2 -2
  11. package/lib/entities/partial/herbary.js +1 -1
  12. package/lib/entities/partial/prerequisites/single/personalityTrait.js +1 -1
  13. package/lib/entities/partial/prerequisites/single/ratedSum.js +1 -1
  14. package/lib/entities/partial/profession/options/combatTechniquesForAdventurePoints.d.ts +16 -0
  15. package/lib/entities/partial/profession/options/combatTechniquesForAdventurePoints.js +43 -0
  16. package/lib/entities/partial/profession/options/numberOfSpellworksTotalingAdventurePoints.d.ts +16 -0
  17. package/lib/entities/partial/profession/options/numberOfSpellworksTotalingAdventurePoints.js +39 -0
  18. package/lib/entities/partial/profession/packages.d.ts +67 -0
  19. package/lib/entities/partial/profession/packages.js +84 -0
  20. package/lib/entities/partial/profession/variants.d.ts +21 -0
  21. package/lib/entities/partial/profession/variants.js +31 -0
  22. package/lib/entities/partial/rated/activatable/cost.js +11 -9
  23. package/lib/entities/partial/rated/activatable/effect.js +2 -2
  24. package/lib/entities/partial/reader.d.ts +25 -1
  25. package/lib/entities/partial/reader.js +20 -0
  26. package/lib/entities/partial/units/energy.js +2 -1
  27. package/lib/entities/partial/units/length.js +6 -3
  28. package/lib/entities/partial/units/timeSpan.js +11 -7
  29. package/lib/entities/partial/unknown.d.ts +4 -0
  30. package/lib/entities/partial/unknown.js +4 -0
  31. package/lib/entities/poison.js +1 -1
  32. package/lib/entities/profession.js +44 -62
  33. package/lib/tsconfig.tsbuildinfo +1 -1
  34. package/package.json +2 -2
@@ -0,0 +1,31 @@
1
+ import { nullableToArray } from "@elyukai/utils/nullable";
2
+ import { Reader } from "@elyukai/utils/reader";
3
+ import { assertExhaustive } from "@elyukai/utils/typeSafety";
4
+ import { fixedNumberOrString, translateR } from "../reader.js";
5
+ import { MISSING_VALUE } from "../unknown.js";
6
+ /**
7
+ * Renders an option for a profession variant, using the base profession package and the variant's options.
8
+ */
9
+ export const renderVariantOption = (base, variant, key, render) => (variant.options?.[key] === undefined
10
+ ? Reader.of(undefined)
11
+ : render(base.options?.[key], variant.options[key])).map(text => nullableToArray(text));
12
+ /**
13
+ * Render a variant option based on whether it is a removal, addition, or override of the base option.
14
+ */
15
+ export const renderVariantOptionPaths = (base, variant, paths) => {
16
+ switch (variant.kind) {
17
+ case "Remove":
18
+ return base === undefined ? Reader.of(MISSING_VALUE) : paths.remove(base);
19
+ case "Override":
20
+ return base === undefined ? paths.add(variant.Override) : paths.update(base, variant.Override);
21
+ default:
22
+ return assertExhaustive(variant);
23
+ }
24
+ };
25
+ /**
26
+ * Helper for "X instead of Y" text.
27
+ */
28
+ export const insteadOfR = (replacement, base) => translateR("{$replacement} instead of {$base}", {
29
+ replacement: fixedNumberOrString(replacement),
30
+ base: fixedNumberOrString(base),
31
+ });
@@ -3,7 +3,7 @@ import { Reader } from "@elyukai/utils/reader";
3
3
  import { mapNullable } from "@optolith/helpers/nullable";
4
4
  import { assertExhaustive } from "@optolith/helpers/typeSafety";
5
5
  import { renderParameterMap } from "../../map.js";
6
- import { formatEnergyFnR, formatEnergyR, getInstanceByIdFnR, modifiableBySpeedR, responsiveLocaleJoinR, responsiveR, responsiveTextR, responsiveTranslateR, translateMapR, translateR, } from "../../reader.js";
6
+ import { fixedNumberOrString, formatEnergyFnR, formatEnergyR, getInstanceByIdFnR, modifiableBySpeedR, responsiveLocaleJoinR, responsiveR, responsiveTextR, responsiveTranslateR, responsiveTranslateSplitR, translateMapR, translateR, } from "../../reader.js";
7
7
  import { appendNoteIfNeeded, replaceTextIfNeeded } from "../../responsiveText.js";
8
8
  import { formatCombinedTimeSpanR } from "../../units/timeSpan.js";
9
9
  import { MISSING_VALUE } from "../../unknown.js";
@@ -33,9 +33,11 @@ const appendPerCountableToCostIfNeeded = (value, baseCost) => {
33
33
  })));
34
34
  });
35
35
  };
36
- const appendPermanentCostIfNeeded = (permanentValue, baseCost) => mapNullable(permanentValue, value => responsiveTranslateR(".input {$value :number} {{, {$value} of which are permanent}}", " ({$value} perm.)", { value }).map(text => baseCost + text)) ?? Reader.of(baseCost);
37
- const appendElvenPermanentCostIfNeeded = (permanent, baseCost) => mapNullable(permanent, value => responsiveTranslateR(".input {$value :number} {{{$value} permanent AE}}", "{$value} pAE", {
36
+ const appendPermanentCostIfNeeded = (permanentValue, baseCost) => mapNullable(permanentValue, value => responsiveTranslateSplitR(".input {$value :number} {{, {$value} of which are permanent}}", " ({$value} perm.)", { value }, { value: value.toFixed() }).map(text => baseCost + text)) ?? Reader.of(baseCost);
37
+ const appendElvenPermanentCostIfNeeded = (permanent, baseCost) => mapNullable(permanent, value => responsiveTranslateSplitR(".input {$value :number} {{{$value} permanent AE}}", "{$value} pAE", {
38
38
  value: value.value,
39
+ }, {
40
+ value: value.value.toFixed(),
39
41
  })
40
42
  .thenW(text => replaceTextIfNeeded(value.translations, text))
41
43
  .map(text => baseCost + text)) ?? Reader.of(baseCost);
@@ -117,7 +119,7 @@ const renderMultipleOneTimeCosts = (type, value) => {
117
119
  export const renderStandaloneCostMap = (value) => renderParameterMap(value, option => Reader.of(option.value), formatEnergyR, value.options.every(option => option.value.permanentValue !== undefined)
118
120
  ? {
119
121
  format: values => translateR(", {$value} of which are permanent", {
120
- value: values,
122
+ value: fixedNumberOrString(values),
121
123
  }),
122
124
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- is checked beforehand
123
125
  render: option => Reader.of(option.value.permanentValue),
@@ -132,7 +134,7 @@ const renderOneTimeCostMap = (value) => {
132
134
  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
135
  ? {
134
136
  format: values => translateR(", {$value} of which are permanent", {
135
- value: values,
137
+ value: fixedNumberOrString(values),
136
138
  }),
137
139
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- is checked beforehand
138
140
  render: option => Reader.of(option.value.permanentValue),
@@ -142,7 +144,7 @@ const renderOneTimeCostMap = (value) => {
142
144
  return renderParameterMap(value.NonModifiable.map, option => Reader.of(option.value), formatEnergyR, value.NonModifiable.map.options.every(option => option.value.permanentValue !== undefined)
143
145
  ? {
144
146
  format: values => translateR(", {$value} of which are permanent", {
145
- value: values,
147
+ value: fixedNumberOrString(values),
146
148
  }),
147
149
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- is checked beforehand
148
150
  render: option => Reader.of(option.value.permanentValue),
@@ -240,10 +242,10 @@ export const renderMagicalActionCost = (cost) => {
240
242
  case "FirstPerson":
241
243
  return Reader.asks(({ translate }) => translate("{$firstPersonValue} for the first person; {$additionalPersonValue} for each additional person", {
242
244
  firstPersonValue: translate("{$value} AE", {
243
- value: cost.FirstPerson.value,
245
+ value: cost.FirstPerson.value.toFixed(),
244
246
  }),
245
247
  additionalPersonValue: translate("{$value} AE", {
246
- value: cost.FirstPerson.value / 2,
248
+ value: (cost.FirstPerson.value / 2).toFixed(),
247
249
  }),
248
250
  }));
249
251
  case "ByPrimaryPatron":
@@ -252,7 +254,7 @@ export const renderMagicalActionCost = (cost) => {
252
254
  return Reader.asks(({ translate }) => cost.All.minimum === undefined
253
255
  ? translate("All AE")
254
256
  : translate("All AE, at least {$value} AE", {
255
- value: cost.All.minimum,
257
+ value: cost.All.minimum.toFixed(),
256
258
  }));
257
259
  case "Map":
258
260
  return renderStandaloneCostMap(cost.Map);
@@ -1,6 +1,6 @@
1
1
  import { mapNullable } from "@optolith/helpers/nullable";
2
2
  import { assertExhaustive } from "@optolith/helpers/typeSafety";
3
- import { translateFnR } from "../../reader.js";
3
+ import { fixedNumberOrString, translateFnR } from "../../reader.js";
4
4
  const getContentPartsForQualityLevels = (getQualityLevelString, source) => translateFnR.map((translate) => ({
5
5
  label: translate("Effect"),
6
6
  value: [
@@ -13,7 +13,7 @@ const getContentPartsForQualityLevels = (getQualityLevelString, source) => trans
13
13
  style: "hidden",
14
14
  items: source.quality_levels.map((text, index) => ({
15
15
  label: translate("QL {$value}", {
16
- value: getQualityLevelString(index),
16
+ value: fixedNumberOrString(getQualityLevelString(index)),
17
17
  }),
18
18
  value: text,
19
19
  })),
@@ -1,6 +1,7 @@
1
1
  import { Reader } from "@elyukai/utils/reader";
2
2
  import type { ChildEntityMap, EntityMap, FastSkillModificationLevelConfig, ResponsiveText, ResponsiveTextOptional, SkillModificationLevel, SlowSkillModificationLevelConfig } from "@optolith/database-schema/gen";
3
3
  import type { IdArgsVariant } from "tsondb/schema/gen";
4
+ import type { StdReader } from "../../env.js";
4
5
  import type { GetAllChildInstancesForParent, GetAllInstances, GetInstanceById } from "../../helpers/getTypes.js";
5
6
  import type { LocaleCompare, LocaleJoin, LocaleJoinType } from "../../helpers/locale.js";
6
7
  import type { Format, LocaleMap, Translate, TranslateMap, TranslationKeyMatchingParamsOfKey, TranslationParamsInArray, Translations } from "../../helpers/translate.js";
@@ -99,7 +100,7 @@ export declare const attributedNameFromInstanceR: (instance: {
99
100
  translations: LocaleMap<{
100
101
  name: string;
101
102
  }>;
102
- } | undefined, context: string, ...args: IdArgsVariant) => Reader<{
103
+ } | undefined, context: string, ...args: IdArgsVariant<EntityMap, keyof EntityMap>) => Reader<{
103
104
  translateMap: TranslateMap;
104
105
  }, string | undefined>;
105
106
  /**
@@ -204,6 +205,13 @@ export declare const responsiveTranslateR: <K extends keyof Translations, K2 ext
204
205
  translate: Translate;
205
206
  responsiveTextSize: ResponsiveTextSize;
206
207
  }, string>;
208
+ /**
209
+ * Creates a responsive value from a translation key that has a full and a compressed version.
210
+ */
211
+ export declare const responsiveTranslateSplitR: <K extends keyof Translations, K2 extends TranslationKeyMatchingParamsOfKey<K>>(fullKey: K, compressedKey: K2, ...rest: [...TranslationParamsInArray<K>, ...TranslationParamsInArray<K2>]) => Reader<{
212
+ translate: Translate;
213
+ responsiveTextSize: ResponsiveTextSize;
214
+ }, string>;
207
215
  /**
208
216
  * Creates a responsive value from a responsive text.
209
217
  */
@@ -266,4 +274,20 @@ export declare const mapNullableR: <T, U, R>(defaultValue: U, fn: (value: NonNul
266
274
  * Use `Reader` instances directly in a template string.
267
275
  */
268
276
  export declare const sequence: <E>(string: TemplateStringsArray, ...values: (Reader<E, string> | string)[]) => Reader<E, string>;
277
+ /**
278
+ * Formats a number according to the locale’s rules, returns a string as-is.
279
+ */
280
+ export declare const formatNumber: (value: number) => StdReader<string, "fn">;
281
+ /**
282
+ * Formats an AP value in an appreviated form.
283
+ */
284
+ export declare const formatAPValue: (value: number) => StdReader<string, "t" | "fn">;
285
+ /**
286
+ * Formats a number according to the locale’s rules, returns a string as-is.
287
+ */
288
+ export declare const formatNumberOrString: (value: number | string) => StdReader<string, "fn">;
289
+ /**
290
+ * Formats a number in fixed-point notation, returns a string as-is.
291
+ */
292
+ export declare const fixedNumberOrString: (value: number | string, fractionDigits?: number) => string;
269
293
  export {};
@@ -135,6 +135,10 @@ export const responsiveThenR = (full, compressed) => Reader.asks(env => {
135
135
  * Creates a responsive value from a translation key that has a full and a compressed version.
136
136
  */
137
137
  export const responsiveTranslateR = (fullKey, compressedKey, ...rest) => Reader.asks(({ translate, responsiveTextSize }) => responsive(responsiveTextSize, () => translate(fullKey, ...rest), () => translate(compressedKey, ...rest)));
138
+ /**
139
+ * Creates a responsive value from a translation key that has a full and a compressed version.
140
+ */
141
+ export const responsiveTranslateSplitR = (fullKey, compressedKey, ...rest) => Reader.asks(({ translate, responsiveTextSize }) => responsive(responsiveTextSize, () => translate(fullKey, ...rest.slice(0, 1)), () => translate(compressedKey, ...rest.slice(1))));
138
142
  /**
139
143
  * Creates a responsive value from a responsive text.
140
144
  */
@@ -198,3 +202,19 @@ export const mapNullableR = (defaultValue, fn) => (value) => isNotNullish(value)
198
202
  * Use `Reader` instances directly in a template string.
199
203
  */
200
204
  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] ?? ""), ""));
205
+ /**
206
+ * Formats a number according to the locale’s rules, returns a string as-is.
207
+ */
208
+ export const formatNumber = (value) => Reader.asks(env => env.formatNumber(value));
209
+ /**
210
+ * Formats an AP value in an appreviated form.
211
+ */
212
+ export const formatAPValue = (value) => formatNumber(value).thenW(formattedValue => translateR("{$value} AP", { value: formattedValue }));
213
+ /**
214
+ * Formats a number according to the locale’s rules, returns a string as-is.
215
+ */
216
+ export const formatNumberOrString = (value) => Reader.asks(env => (typeof value === "number" ? env.formatNumber(value) : value));
217
+ /**
218
+ * Formats a number in fixed-point notation, returns a string as-is.
219
+ */
220
+ export const fixedNumberOrString = (value, fractionDigits) => typeof value === "number" ? value.toFixed(fractionDigits) : value;
@@ -1,3 +1,4 @@
1
+ import { fixedNumberOrString } from "../reader.js";
1
2
  const lengthUnitTranslationKeys = {
2
3
  LifePoints: "{$value} LP",
3
4
  ArcaneEnergy: "{$value} AE",
@@ -8,5 +9,5 @@ const lengthUnitTranslationKeys = {
8
9
  */
9
10
  export const formatEnergy = (translate, unit, value) => {
10
11
  const key = lengthUnitTranslationKeys[unit];
11
- return translate(key, { value });
12
+ return translate(key, { value: fixedNumberOrString(value) });
12
13
  };
@@ -1,4 +1,5 @@
1
1
  import { Reader } from "@elyukai/utils/reader";
2
+ import { fixedNumberOrString } from "../reader.js";
2
3
  import { responsive } from "../responsiveText.js";
3
4
  const lengthUnitTranslationKeys = {
4
5
  Steps: [".input {$value :number} {{{$value} yards}}", "{$value} yards", "{$value} yd"],
@@ -9,9 +10,11 @@ const lengthUnitTranslationKeys = {
9
10
  */
10
11
  export const formatLength = (translate, responsiveTextSize, unit, value) => {
11
12
  const [fullNumberKey, fullKey, compressedKey] = lengthUnitTranslationKeys[typeof unit === "string" ? unit : unit.kind];
12
- return responsive(responsiveTextSize, () => translate(typeof value === "number" ? fullNumberKey : fullKey, {
13
- value,
14
- }), () => translate(compressedKey, { value }));
13
+ return responsive(responsiveTextSize, () => translate(typeof value === "number" ? fullNumberKey : fullKey, typeof value === "number"
14
+ ? {
15
+ value,
16
+ }
17
+ : { value }), () => translate(compressedKey, { value: fixedNumberOrString(value) }));
15
18
  };
16
19
  /**
17
20
  * Returns the text for a length unit.
@@ -35,13 +35,17 @@ export const formatTimeSpan = (translate, translateMap, format, responsiveTextSi
35
35
  }));
36
36
  }
37
37
  const [fullNumberKey, fullKey, compressedNumberKey, compressedKey] = timeSpanUnitTranslationKeys[typeof unit === "string" ? unit : unit.kind];
38
- return responsive(responsiveTextSize, () => translate(typeof value === "number" ? fullNumberKey : fullKey, {
39
- value,
40
- style: interval ? "interval" : "default",
41
- }), () => translate(typeof value === "number" ? compressedNumberKey : compressedKey, {
42
- value,
43
- style: interval ? "interval" : "default",
44
- }));
38
+ return responsive(responsiveTextSize, () => translate(typeof value === "number" ? fullNumberKey : fullKey, typeof value === "number"
39
+ ? {
40
+ value,
41
+ style: interval ? "interval" : "default",
42
+ }
43
+ : { value }), () => translate(typeof value === "number" ? compressedNumberKey : compressedKey, typeof value === "number"
44
+ ? {
45
+ value,
46
+ style: interval ? "interval" : "default",
47
+ }
48
+ : { value }));
45
49
  };
46
50
  /**
47
51
  * Returns the text for a time span unit.
@@ -6,3 +6,7 @@ export declare const MISSING_VALUE = "###MISSING###";
6
6
  * String to display when a translation that should be present produces an error.
7
7
  */
8
8
  export declare const UNHANDLED_VALUE = "###UNHANDLEDERROR###";
9
+ /**
10
+ * String to display when there is a logic error in text generation.
11
+ */
12
+ export declare const LOGIC_ERROR = "###LOGIC_ERROR###";
@@ -6,3 +6,7 @@ export const MISSING_VALUE = "###MISSING###";
6
6
  * String to display when a translation that should be present produces an error.
7
7
  */
8
8
  export const UNHANDLED_VALUE = "###UNHANDLEDERROR###";
9
+ /**
10
+ * String to display when there is a logic error in text generation.
11
+ */
12
+ export const LOGIC_ERROR = "###LOGIC_ERROR###";
@@ -379,7 +379,7 @@ export const getPoisonEntityDescription = createEntityDescriptionCreator(({ getI
379
379
  ? undefined
380
380
  : {
381
381
  label: `${translate("AP Value")} (${translate("Trade Secret")})`,
382
- value: translate("{$value} AP", { value: tradeSecret.apValue }) +
382
+ value: translate("{$value} AP", { value: tradeSecret.apValue.toFixed() }) +
383
383
  parensIf(mapNullable(tradeSecret.prerequisites, prerequisites => `${translate("Prerequisites")}: ${printPlainGeneralPrerequisites(prerequisites).run(env)}`)),
384
384
  },
385
385
  entry.source_type.kind === "AlchemicalPoison" ||
@@ -2,7 +2,7 @@ import { allSame } from "@elyukai/utils/array/filters";
2
2
  import { ensureNonEmpty, isEmpty, isNotEmpty, } from "@elyukai/utils/array/nonEmpty";
3
3
  import { deepEqual, equal } from "@elyukai/utils/equality";
4
4
  import { on } from "@elyukai/utils/function";
5
- import { isNotNullish, isNullish, mapNullable, nullableToArray, } from "@elyukai/utils/nullable";
5
+ import { isNotNullish, isNullish, mapNullable } from "@elyukai/utils/nullable";
6
6
  import { omitKeys } from "@elyukai/utils/object";
7
7
  import { compareNumber } from "@elyukai/utils/ordering";
8
8
  import { Reader } from "@elyukai/utils/reader";
@@ -13,16 +13,13 @@ import { combineNameComponents, getNameComponents, renderActivatableNameComponen
13
13
  import { renderCommonnessRatedAdvantagesOrDisadvantages, renderValueWithPossibleTranslation, } from "./partial/commonnessRatedAdvantagesAndDisadvantages.js";
14
14
  import { attributedCustomName, attributedInstance, attributedName, attributedNameFromInstance, } from "./partial/markdown.js";
15
15
  import { printProfessionPrerequisites, printProfessionVariantPrerequisites, } from "./partial/prerequisites/index.js";
16
+ import { renderBaseCombatTechniquesForAdventurePointsOption, renderVariantCombatTechniquesForAdventurePointsOption, } from "./partial/profession/options/combatTechniquesForAdventurePoints.js";
17
+ import { renderBaseNumberOfSpellworksTotalingAdventurePoints, renderVariantNumberOfSpellworksTotalingAdventurePoints, } from "./partial/profession/options/numberOfSpellworksTotalingAdventurePoints.js";
18
+ import { prepareProfessionPackages, } from "./partial/profession/packages.js";
19
+ import { insteadOfR, renderVariantOption, renderVariantOptionPaths, } from "./partial/profession/variants.js";
16
20
  import { parensIf } from "./partial/rated/activatable/parensIf.js";
17
- import { attributedCustomNameR, attributedNameR, customNameR, formatR, getChildInstancesForInstanceIdFnR, getChildInstancesForInstanceIdR, getInstanceByIdR, localeCompareR, localeJoinR, localeSortR, strictNameR, translateMapFnR, translateR, translationR, } from "./partial/reader.js";
21
+ import { attributedCustomNameR, attributedNameR, customNameR, fixedNumberOrString, formatAPValue, formatR, getChildInstancesForInstanceIdFnR, getChildInstancesForInstanceIdR, getInstanceByIdR, localeCompareR, localeJoinR, localeSortR, strictNameR, translateMapFnR, translateR, translationR, } from "./partial/reader.js";
18
22
  import { MISSING_VALUE, UNHANDLED_VALUE } from "./partial/unknown.js";
19
- const prepareProfessionPackages = (professionId) => Reader.asks(({ getInstanceById, getChildInstancesForInstanceId }) => getChildInstancesForInstanceId("ProfessionPackage", professionId)
20
- .map((professionPackage) => ({
21
- ...professionPackage,
22
- experienceLevel: getInstanceById("ExperienceLevel", professionPackage.content.experience_level),
23
- }))
24
- .filter((professionPackage) => professionPackage.experienceLevel !== undefined)
25
- .toSorted(on(item => item.experienceLevel.adventure_points, compareNumber)));
26
23
  const renderNumericListAcrossPackages = (packages, selector, equalityFn, renderText, defaultValue) => packages
27
24
  .reduce((accR, pkg, pkgIndex) => accR.map2(selector(pkg), (acc, selected) => selected.reduce((filledAcc, [values, number]) => {
28
25
  const existing = filledAcc.findIndex(([value]) => equalityFn(value, values));
@@ -48,7 +45,6 @@ const renderNumericListAcrossPackages = (packages, selector, equalityFn, renderT
48
45
  .thenW(list => localeSortR(list))
49
46
  .map(list => ensureNonEmpty(list)?.join(", "));
50
47
  const renderSinglePackageValue = (professionPackages, selector, equalityFn, renderText) => Reader.traverse(professionPackages, selector).thenW(selected => (allSame(selected, equalityFn) ? renderText([selected[0]]) : renderText(selected)));
51
- const insteadOfR = (replacement, base) => translateR("{$replacement} instead of {$base}", { replacement, base });
52
48
  /**
53
49
  * If the selected values of the base and the variant are equal, only renders the value once. Otherwise, renders both values as the variant value replacing the base value.
54
50
  */
@@ -59,23 +55,14 @@ const plainOrInsteadOfR = (base, variant, selector, equality, render) => {
59
55
  ? render(baseValue)
60
56
  : render(variantValue).thenW(replacement => render(baseValue).thenW(baseRendered => insteadOfR(replacement, baseRendered)));
61
57
  };
62
- const renderVariantOptionPaths = (base, variant, paths) => {
63
- switch (variant.kind) {
64
- case "Remove":
65
- return base === undefined ? Reader.of(MISSING_VALUE) : paths.remove(base);
66
- case "Override":
67
- return base === undefined ? paths.add(variant.Override) : paths.update(base, variant.Override);
68
- default:
69
- return assertExhaustive(variant);
70
- }
71
- };
72
58
  const renderVariantLanguagesScriptsOption = (baseOptions, variantOptions) => renderVariantOptionPaths(baseOptions, variantOptions, {
73
- remove: base => translateR("no Languages and Literacy totaling {$apValue} AP", { apValue: base.ap_value }),
74
- add: override => translateR("Languages and Literacy totaling {$apValue} AP", { apValue: override.ap_value }),
75
- update: (base, override) => translateR("{$replacement} instead of {$base}", {
76
- replacement: override.ap_value,
77
- base: base.ap_value,
78
- }).then(apValue => translateR("Languages and Literacy totaling {$apValue} AP", {
59
+ remove: base => translateR("no Languages and Literacy totaling {$apValue} AP", {
60
+ apValue: base.ap_value.toFixed(),
61
+ }),
62
+ add: override => translateR("Languages and Literacy totaling {$apValue} AP", {
63
+ apValue: override.ap_value.toFixed(),
64
+ }),
65
+ update: (base, override) => insteadOfR(override.ap_value, base.ap_value).then(apValue => translateR("Languages and Literacy totaling {$apValue} AP", {
79
66
  apValue,
80
67
  })),
81
68
  });
@@ -101,17 +88,17 @@ const renderSkillSpecializationOption = (option) => Reader.asks(({ format, trans
101
88
  const renderCombatTechniquesOption = (option) => isNotEmpty(option.fixed)
102
89
  ? (() => {
103
90
  const [first, ...others] = option.fixed;
104
- const firstTextR = translateR(".input {$count :number} {{{$count} of the following combat techniques {$rating}}}", { count: first.number, rating: first.rating_modifier + 6 });
91
+ const firstTextR = translateR(".input {$count :number} {{{$count} of the following combat techniques {$rating}}}", { count: first.number, rating: (first.rating_modifier + 6).toFixed() });
105
92
  const fixedTextR = others.reduce((accTextR, other) => accTextR.then(accText => translateR(".input {$count :number} {{{$previous}, {$count} others {$rating}}}", {
106
93
  count: other.number,
107
94
  previous: accText,
108
- rating: other.rating_modifier + 6,
95
+ rating: (other.rating_modifier + 6).toFixed(),
109
96
  })), firstTextR);
110
97
  const completeTextR = fixedTextR.then(fixedText => option.rest_rating_modifier === undefined
111
98
  ? Reader.of(fixedText)
112
99
  : translateR("{$previous}, the others {$rating}", {
113
100
  previous: fixedText,
114
- rating: option.rest_rating_modifier + 6,
101
+ rating: (option.rest_rating_modifier + 6).toFixed(),
115
102
  }));
116
103
  const listR = Reader.traverse(option.options, id => attributedNameR("profession", id).map(name => name ?? MISSING_VALUE))
117
104
  .thenW(list => localeSortR(list))
@@ -134,13 +121,13 @@ const renderSkillsOption = (professionPackages, skillGroup) => allSame(professio
134
121
  ? pkg.content.options.skills
135
122
  : undefined), apValue => professionPackages[0].content.options?.skills?.specific === undefined
136
123
  ? translateR("{$apValue} AP to improve other {$skillsOfGroup}", {
137
- apValue,
124
+ apValue: fixedNumberOrString(apValue),
138
125
  skillsOfGroup: skillGroup.nameOfSkillsOfGroup,
139
126
  })
140
127
  : Reader.traverse(professionPackages[0].content.options.skills.specific, id => attributedNameR("profession", "Skill", id).map(name => name ?? MISSING_VALUE))
141
128
  .thenW(localeSortR)
142
- .thenW(list => translateR("maximum SR per skill: {$maxSR}", { maxSR: 8 }).then(maxSRText => translateR("{$apValue} AP to distribute among the following skills: {$list}", {
143
- apValue,
129
+ .thenW(list => translateR("maximum SR per skill: {$maxSR}", { maxSR: "8" }).then(maxSRText => translateR("{$apValue} AP to distribute among the following skills: {$list}", {
130
+ apValue: fixedNumberOrString(apValue),
144
131
  list: list.join(", ") + parensIf(maxSRText),
145
132
  })))) ?? Reader.of(undefined))
146
133
  : Reader.traverse(professionPackages, pkg => {
@@ -149,13 +136,13 @@ const renderSkillsOption = (professionPackages, skillGroup) => allSame(professio
149
136
  ? Reader.of("—")
150
137
  : skillsOption.specific === undefined
151
138
  ? translateR("{$apValue} AP to improve other {$skillsOfGroup}", {
152
- apValue: skillsOption.ap_value,
139
+ apValue: skillsOption.ap_value.toFixed(),
153
140
  skillsOfGroup: skillGroup.nameOfSkillsOfGroup,
154
141
  })
155
142
  : Reader.traverse(skillsOption.specific, id => attributedNameR("profession", "Skill", id).map(name => name ?? MISSING_VALUE))
156
143
  .thenW(localeSortR)
157
- .thenW(list => translateR("maximum SR per skill: {$maxSR}", { maxSR: 8 }).then(maxSRText => translateR("{$apValue} AP to distribute among the following skills: {$list}", {
158
- apValue: skillsOption.ap_value,
144
+ .thenW(list => translateR("maximum SR per skill: {$maxSR}", { maxSR: "8" }).then(maxSRText => translateR("{$apValue} AP to distribute among the following skills: {$list}", {
145
+ apValue: skillsOption.ap_value.toFixed(),
159
146
  list: list.join(", ") + parensIf(maxSRText),
160
147
  })));
161
148
  }).map(list => list.join(" / "));
@@ -175,10 +162,7 @@ const renderVariantCantripsOption = (baseOptions, variantOptions) => renderVaria
175
162
  }).then(baseCount => translateR(".input {$count :number} {{{$count} cantrips}}", {
176
163
  count: override.number,
177
164
  })
178
- .then(replacementCount => translateR("{$replacement} instead of {$base}", {
179
- replacement: replacementCount,
180
- base: baseCount,
181
- }).then(count => translateR("{$count} from the following list", { count })))
165
+ .then(replacementCount => insteadOfR(replacementCount, baseCount).then(count => translateR("{$count} from the following list", { count })))
182
166
  .map(text => `${text}: ${list}`)))
183
167
  : renderCantripList(override.options)
184
168
  .thenW(list => translateR(".input {$count :number} {{{$count} cantrips}}", {
@@ -192,10 +176,7 @@ const renderVariantCantripsOption = (baseOptions, variantOptions) => renderVaria
192
176
  })
193
177
  .then(count => translateR("{$count} from the following list", { count }))
194
178
  .map(text => `${text}: ${list}`))
195
- .then(baseText => translateR("{$replacement} instead of {$base}", {
196
- replacement: replacementText,
197
- base: baseText,
198
- }))),
179
+ .then(baseText => insteadOfR(replacementText, baseText))),
199
180
  });
200
181
  const renderSingleCantripsOption = (option) => renderVariantCantripsOption(undefined, { kind: "Override", Override: option });
201
182
  const renderCantripsOption = (professionPackages) => {
@@ -212,7 +193,7 @@ const renderCantripsOption = (professionPackages) => {
212
193
  }
213
194
  };
214
195
  const renderLiturgiesOption = (professionPackages) => mapNullable(getTotalingAPValues(professionPackages, pkg => pkg.content.options?.liturgies), apValue => translateR("Liturgies totaling {$apValue} AP", {
215
- apValue,
196
+ apValue: fixedNumberOrString(apValue),
216
197
  })) ?? Reader.of(undefined);
217
198
  const renderVariantSkillSpecializationOption = (baseOptions, variantOptions) => renderVariantOptionPaths(baseOptions, variantOptions, {
218
199
  remove: (base) => {
@@ -260,8 +241,8 @@ const renderVariantCombatTechniquesOption = (baseOptions, variantOptions) => ren
260
241
  update: (base, override) => renderCombatTechniquesOption(override).then(overrideText => renderCombatTechniquesOption(base).then(baseText => insteadOfR(overrideText, baseText))),
261
242
  });
262
243
  const renderVariantCursesOption = (baseOptions, variantOptions) => renderVariantOptionPaths(baseOptions, variantOptions, {
263
- remove: base => translateR("no Curses totaling {$apValue} AP", { apValue: base.ap_value }),
264
- add: override => translateR("Curses totaling {$apValue} AP", { apValue: override.ap_value }),
244
+ remove: base => translateR("no Curses totaling {$apValue} AP", { apValue: base.ap_value.toFixed() }),
245
+ add: override => translateR("Curses totaling {$apValue} AP", { apValue: override.ap_value.toFixed() }),
265
246
  update: (base, override) => insteadOfR(override.ap_value, base.ap_value).then(apValue => translateR("Curses totaling {$apValue} AP", {
266
247
  apValue,
267
248
  })),
@@ -273,17 +254,17 @@ const renderVariantSkillsOption = (baseOptions, variantOptions) => renderVariant
273
254
  skillsOfGroup: nameOfSkillsOfGroup,
274
255
  })),
275
256
  add: override => nameOfSkillsOfGroupR(override.group).thenW(nameOfSkillsOfGroup => translateR("{$apValue} AP to improve other {$skillsOfGroup}", {
276
- apValue: override.ap_value,
257
+ apValue: override.ap_value.toFixed(),
277
258
  skillsOfGroup: nameOfSkillsOfGroup,
278
259
  })),
279
260
  update: (base, override) => plainOrInsteadOfR(base, override, b => b.ap_value, equal, b => Reader.of(b)).thenW(apValue => plainOrInsteadOfR(base, override, b => b.group, equal, nameOfSkillsOfGroupR).thenW(nameOfSkillsOfGroup => translateR("{$apValue} AP to improve other {$skillsOfGroup}", {
280
- apValue,
281
- skillsOfGroup: nameOfSkillsOfGroup,
261
+ apValue: fixedNumberOrString(apValue),
262
+ skillsOfGroup: fixedNumberOrString(nameOfSkillsOfGroup),
282
263
  }))),
283
264
  });
284
265
  const renderVariantLiturgiesOption = (baseOptions, variantOptions) => renderVariantOptionPaths(baseOptions, variantOptions, {
285
- remove: base => translateR("no Liturgies totaling {$apValue} AP", { apValue: base.ap_value }),
286
- add: override => translateR("Liturgies totaling {$apValue} AP", { apValue: override.ap_value }),
266
+ remove: base => translateR("no Liturgies totaling {$apValue} AP", { apValue: base.ap_value.toFixed() }),
267
+ add: override => translateR("Liturgies totaling {$apValue} AP", { apValue: override.ap_value.toFixed() }),
287
268
  update: (base, override) => insteadOfR(override.ap_value, base.ap_value).then(apValue => translateR("Liturgies totaling {$apValue} AP", {
288
269
  apValue,
289
270
  })),
@@ -416,6 +397,9 @@ const renderCombatTechniques = (professionPackages) => Reader.sequence([
416
397
  professionPackages.some(pkg => pkg.content.options?.combat_techniques !== undefined)
417
398
  ? renderSinglePackageValue(professionPackages, pkg => Reader.of(pkg.content.options?.combat_techniques), deepEqual, options => Reader.traverse(options, option => option === undefined ? Reader.of("—") : renderCombatTechniquesOption(option)).map(renderedOptions => renderedOptions.join(" / ")))
418
399
  : Reader.of(undefined),
400
+ professionPackages.some(pkg => pkg.content.options?.combatTechniquesForAdventurePoints !== undefined)
401
+ ? renderBaseCombatTechniquesForAdventurePointsOption(professionPackages)
402
+ : Reader.of(undefined),
419
403
  ]).map(list => ensureNonEmpty(list.filter(isNotNullish))?.join(", ") ?? "—");
420
404
  const renderSkills = (professionPackages) => Reader.asks(({ getAllInstances }) => getAllInstances("SkillGroup")).thenW(skillGroups => Reader.traverse(skillGroups.toSorted(on(group => group.content.position, compareNumber)), ({ id: groupId, content: group }) => translationR(group).thenW(groupTranslation => Reader.sequence([
421
405
  renderNumericListAcrossPackages(professionPackages, pkg => Reader.asks(({ getInstanceById }) => pkg.content.skills
@@ -444,6 +428,9 @@ const renderSpellworkName = (spellworkIds) => Reader.traverse(spellworkIds, spel
444
428
  const renderSpellworks = (professionPackages) => Reader.sequence([
445
429
  renderCantripsOption(professionPackages),
446
430
  renderNumericListAcrossPackages(professionPackages, (pkg) => Reader.of(pkg.content.spells?.map(ct => [ct.id, ct.rating_modifier]) ?? []), deepEqual, renderSpellworkName, undefined),
431
+ professionPackages.some(pkg => pkg.content.options?.spellworks !== undefined)
432
+ ? renderBaseNumberOfSpellworksTotalingAdventurePoints(professionPackages)
433
+ : Reader.of(undefined),
447
434
  ]).map(list => ensureNonEmpty(list.filter(isNotNullish))?.join("; "));
448
435
  const retrieveBlessedTraditionIdentifierFromPrerequisiteGroup = (prerequisite) => prerequisite.kind === "Activatable" && prerequisite.Activatable.id.kind === "BlessedTradition"
449
436
  ? [prerequisite.Activatable.id.BlessedTradition]
@@ -533,17 +520,9 @@ const renderRatedVariantChanges = (baseList, variantList, renderInstance) => Rea
533
520
  return renderInstance(id).map(name => `${name} ${rating_modifier.toFixed()}`);
534
521
  }
535
522
  const baseValue = baseList?.find(item => deepEqual(item.id, id))?.rating_modifier ?? 0;
536
- return renderInstance(id).thenW(name => translateR("{$replacement} instead of {$base}", {
537
- replacement: `${name} ${(baseValue + rating_modifier).toFixed()}`,
538
- base: baseValue,
539
- }));
523
+ return renderInstance(id).thenW(name => insteadOfR(`${name} ${(baseValue + rating_modifier).toFixed()}`, baseValue));
540
524
  }).thenW(localeSortR);
541
- const renderVariantOption = (base, variant, key, render) => (variant.options?.[key] === undefined
542
- ? Reader.of(undefined)
543
- : render(base.options?.[key], variant.options[key])).map(text => nullableToArray(text));
544
- const renderProfessionVariantLabel = (base, variant, translations) => translateR("{$value} AP", {
545
- value: base.ap_value + (variant.ap_value ?? 0),
546
- }).thenW(apValueText => localeSortR(translations.map(({ id, content }) => `^[${content.name.default}](entity: "ProfessionVariant", instance: "${id}")`))
525
+ const renderProfessionVariantLabel = (base, variant, translations) => formatAPValue(base.ap_value + (variant.ap_value ?? 0)).thenW(apValueText => localeSortR(translations.map(({ id, content }) => `^[${content.name.default}](entity: "ProfessionVariant", instance: "${id}")`))
547
526
  .thenW(list => localeJoinR(list, "conjunction"))
548
527
  .map(names => names + parensIf(apValueText)));
549
528
  const renderProfessionVariantText = (base, variant, translation) => translation.full_text !== undefined
@@ -557,9 +536,11 @@ const renderProfessionVariantText = (base, variant, translation) => translation.
557
536
  renderVariantOption(base, variant, "skill_specialization", renderVariantSkillSpecializationOption),
558
537
  renderVariantOption(base, variant, "languages_scripts", renderVariantLanguagesScriptsOption),
559
538
  renderVariantOption(base, variant, "combat_techniques", renderVariantCombatTechniquesOption),
539
+ renderVariantOption(base, variant, "combatTechniquesForAdventurePoints", renderVariantCombatTechniquesForAdventurePointsOption),
560
540
  renderVariantOption(base, variant, "cantrips", renderVariantCantripsOption),
561
541
  renderVariantOption(base, variant, "curses", renderVariantCursesOption),
562
542
  renderVariantOption(base, variant, "skills", renderVariantSkillsOption),
543
+ renderVariantOption(base, variant, "spellworks", renderVariantNumberOfSpellworksTotalingAdventurePoints),
563
544
  renderVariantOption(base, variant, "liturgies", renderVariantLiturgiesOption),
564
545
  variant.special_abilities === undefined
565
546
  ? Reader.of([])
@@ -635,10 +616,11 @@ const renderProfessionVariants = (professionPackages) => professionPackages.leng
635
616
  * Get a JSON representation of the rules text for a profession version.
636
617
  */
637
618
  export const getProfessionVersionEntityDescription = createEntityDescriptionCreator(({ getInstanceById, getAllInstances, getChildInstancesForInstanceId, getResolvedSelectOptionById, }, locale, { id, content: entry }) => {
638
- const { format, translate, translateMap, compare: localeCompare, join: localeJoin } = locale;
619
+ const { format, formatNumber, translate, translateMap, compare: localeCompare, join: localeJoin, } = locale;
639
620
  const translation = translateMap(entry.translations);
640
621
  const env = {
641
622
  format,
623
+ formatNumber,
642
624
  translate,
643
625
  translateMap,
644
626
  localeCompare,