@optolith/entity-descriptions 0.4.13 → 0.4.15

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.4.15](https://github.com/Optolith/entity-descriptions/compare/v0.4.14...v0.4.15) (2026-06-07)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * add sustained cost map interval ([f7c1b31](https://github.com/Optolith/entity-descriptions/commit/f7c1b31452befa0c898a6f21e4624c8084ea5df5))
11
+ * display full tradition name ([2d07eff](https://github.com/Optolith/entity-descriptions/commit/2d07eff1d4b6a5b72ce705e24969e4a384a1d486))
12
+ * ignore rating in prerequisite when zero ([b375702](https://github.com/Optolith/entity-descriptions/commit/b375702ea644fc21b92bb64b811e204aeda7370c))
13
+ * render ap value from explicit select options ([df2178f](https://github.com/Optolith/entity-descriptions/commit/df2178fe131ed9e9d3427ee092a46b2448b09650))
14
+
15
+ ## [0.4.14](https://github.com/Optolith/entity-descriptions/compare/v0.4.13...v0.4.14) (2026-06-07)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * display negative disadvantage AP values ([8dd72a0](https://github.com/Optolith/entity-descriptions/commit/8dd72a04ef10aeb300b54ed8fa25ddfb60318a3e))
21
+ * render shorter variant of commonness-rated options ([9f9eae0](https://github.com/Optolith/entity-descriptions/commit/9f9eae0af0800b17247aa2db8c300e0f693eb336))
22
+ * separate variant values and concluding text with a period ([d58ebeb](https://github.com/Optolith/entity-descriptions/commit/d58ebeb35839ced633ba7a47bf6c69283522803d))
23
+ * sort commonness-rated options alphabetically ([25c50dd](https://github.com/Optolith/entity-descriptions/commit/25c50dde77a34a207eb129fbe9ef63bb41e589cc))
24
+ * sort skill spec skill options ([5b63ea8](https://github.com/Optolith/entity-descriptions/commit/5b63ea8244bc499027069e2edd1f48064bf98df4))
25
+ * wrap skill spec options in parens if needed ([dd0562b](https://github.com/Optolith/entity-descriptions/commit/dd0562bfd09b7cf11d8e9c277b3fdcfba11204bc))
26
+
5
27
  ## [0.4.13](https://github.com/Optolith/entity-descriptions/compare/v0.4.12...v0.4.13) (2026-06-01)
6
28
 
7
29
 
@@ -47,7 +47,7 @@ export type BaseActivatableTranslation = {
47
47
  * Get a JSON representation of the rules text for a special ability.
48
48
  */
49
49
  export declare const getActivatableEntityDescription: import("../creator.js").EntityDescriptionCreator<"Advantage" | "Disadvantage" | "AdvancedCombatSpecialAbility" | "AdvancedKarmaSpecialAbility" | "AdvancedMagicalSpecialAbility" | "AdvancedSkillSpecialAbility" | "AncestorGlyph" | "ArcaneOrbEnchantment" | "AttireEnchantment" | "Beutelzauber" | "BlessedTradition" | "BowlEnchantment" | "BrawlingSpecialAbility" | "CauldronEnchantment" | "CeremonialItemSpecialAbility" | "ChronicleEnchantment" | "CombatSpecialAbility" | "CombatStyleSpecialAbility" | "CommandSpecialAbility" | "DaggerRitual" | "FamiliarSpecialAbility" | "FatePointSexSpecialAbility" | "FatePointSpecialAbility" | "FoolsHatEnchantment" | "GeneralSpecialAbility" | "Haubenzauber" | "InstrumentEnchantment" | "KarmaSpecialAbility" | "Krallenkettenzauber" | "Kristallkugelzauber" | "LiturgicalStyleSpecialAbility" | "LycantropicGift" | "MagicalSign" | "MagicalSpecialAbility" | "MagicalTradition" | "MagicStyleSpecialAbility" | "OrbEnchantment" | "PactGift" | "ProtectiveWardingCircleSpecialAbility" | "RingEnchantment" | "Sermon" | "SexSpecialAbility" | "SickleRitual" | "SikaryanDrainSpecialAbility" | "SkillStyleSpecialAbility" | "SpellSwordEnchantment" | "StaffEnchantment" | "ToyEnchantment" | "Trinkhornzauber" | "VampiricGift" | "Vision" | "WandEnchantment" | "WeaponEnchantment", {
50
- getInstanceById: GetInstanceById<"Publication" | "Subject" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "TradeSecret" | "Aspect" | "Property" | "Race" | "Culture" | "State" | "Enhancement" | "PactCategory" | "PactDomain" | "SocialStatus" | "Weapon" | "Patron">;
50
+ getInstanceById: GetInstanceById<"Publication" | "Subject" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "TradeSecret" | "Aspect" | "Property" | "Race" | "Culture" | "State" | "Enhancement" | "PactCategory" | "PactDomain" | "SocialStatus" | "Weapon" | "Patron" | "PersonalityTrait">;
51
51
  getAllInstances: GetAllInstances<"Script">;
52
52
  getResolvedSelectOptionById: GetResolvedSelectOptionById;
53
53
  getAllResolvedSelectOptions: GetAllResolvedSelectOptions;
@@ -705,8 +705,11 @@ export const getActivatableEntityDescription = createEntityDescriptionCreator(({
705
705
  : entityName === "MagicalTradition"
706
706
  ? renderAdditionalSpecialRules(translation.name, translation.nameOfSpellcasters, undefined, undefined, undefined, entry.primary).run(env)
707
707
  : [];
708
+ const makeTraditionName = entityName === "BlessedTradition" || entityName === "MagicalTradition"
709
+ ? name => translate("Tradition ({$tradition})", { tradition: name })
710
+ : name => name;
708
711
  return {
709
- title: (translation.name_in_library ?? translation.name) +
712
+ title: makeTraditionName(translation.name_in_library ?? translation.name) +
710
713
  (baseEntry.levels !== undefined ? ` I–${romanize(baseEntry.levels)}` : ""),
711
714
  subtitle: mapNullable(baseEntry.usage_type, usageType => {
712
715
  switch (usageType.kind) {
@@ -807,7 +810,7 @@ export const getActivatableEntityDescription = createEntityDescriptionCreator(({
807
810
  }
808
811
  const { parent: parentId } = selectOption.content;
809
812
  return locale.translateMap(getInstanceById(parentId.kind, fromUniformCase(parentId))?.translations)?.name;
810
- }, () => getAllResolvedSelectOptions(wrappedId), getAllInstances, apValue, entry, translation) + append,
813
+ }, () => getAllResolvedSelectOptions(wrappedId), getAllInstances, apValue, entry, translation, entityName === "Disadvantage") + append,
811
814
  };
812
815
  }),
813
816
  ],
@@ -109,7 +109,16 @@ const renderCommonProfessions = (getChildInstancesForInstanceId, commonProfessio
109
109
  default:
110
110
  return assertExhaustive(constraint);
111
111
  }
112
- }).then(magic => renderCommonProfessionGroup("Blessed Professions", commonProfessions.Grouped.blessed, constraint => renderTraditionConstraint(getChildInstancesForInstanceId, "BlessedTradition", constraint.Tradition)).map(blessed => [
112
+ }).then(magic => renderCommonProfessionGroup("Blessed Professions", commonProfessions.Grouped.blessed, constraint => {
113
+ switch (constraint.kind) {
114
+ case "Profession":
115
+ return renderProfessionConstraint(getChildInstancesForInstanceId, constraint.Profession);
116
+ case "Tradition":
117
+ return renderTraditionConstraint(getChildInstancesForInstanceId, "BlessedTradition", constraint.Tradition);
118
+ default:
119
+ return assertExhaustive(constraint);
120
+ }
121
+ }).map(blessed => [
113
122
  {
114
123
  type: "definitionList",
115
124
  style: "nested",
@@ -5,6 +5,6 @@ import type { GetResolvedSelectOptionById } from "./partial/prerequisites/single
5
5
  * Get a JSON representation of the rules text for an elixir.
6
6
  */
7
7
  export declare const getElixirEntityDescription: import("../creator.js").EntityDescriptionCreator<"Elixir", {
8
- getInstanceById: GetInstanceById<"Publication" | "DerivedCharacteristic" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "Race" | "Culture" | "State" | "Enhancement" | "PactCategory" | "PactDomain" | "SocialStatus" | "Aspect" | "Property">;
8
+ getInstanceById: GetInstanceById<"Publication" | "DerivedCharacteristic" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "Race" | "Culture" | "State" | "Enhancement" | "PactCategory" | "PactDomain" | "SocialStatus" | "Aspect" | "Property" | "PersonalityTrait">;
9
9
  getResolvedSelectOptionById: GetResolvedSelectOptionById;
10
10
  }, import("../index.js").EntityDescription>;
@@ -6,4 +6,4 @@ import type { BaseActivatable, BaseActivatableTranslation } from "../activatable
6
6
  /**
7
7
  * Renders an adventure points value to a human-readable string, using the provided locale for translations and formatting. For values that depend on select options or other instances, the necessary lookup functions are provided as arguments.
8
8
  */
9
- export declare const renderAdventurePointsValue: (locale: LocaleEnvironment, getNameForId: (id: ActivatableIdentifier) => string | undefined, getNameForSelectOptionId: (id: ResolvedSelectOptionIdentifier) => string | undefined, getAllSelectOptions: () => ResolvedSelectOption[], getAllInstances: GetAllInstances<"Script" | "AnimalShapeSize">, value: AdventurePointsValue | number, entry: BaseActivatable, translation: BaseActivatableTranslation) => string;
9
+ export declare const renderAdventurePointsValue: (locale: LocaleEnvironment, getNameForId: (id: ActivatableIdentifier) => string | undefined, getNameForSelectOptionId: (id: ResolvedSelectOptionIdentifier) => string | undefined, getAllSelectOptions: () => ResolvedSelectOption[], getAllInstances: GetAllInstances<"Script" | "AnimalShapeSize">, value: AdventurePointsValue | number, entry: BaseActivatable, translation: BaseActivatableTranslation, negative: boolean) => string;
@@ -1,11 +1,41 @@
1
1
  import { on } from "@elyukai/utils/function";
2
+ import { isNotNullish } from "@elyukai/utils/nullable";
2
3
  import { unique } from "@optolith/helpers/array";
3
4
  import { deepEqual, numAsc } from "@optolith/helpers/compare";
4
5
  import { romanize } from "@optolith/helpers/roman";
5
6
  import { assertExhaustive } from "@optolith/helpers/typeSafety";
6
7
  import { evaluateMathOperation } from "./mathOperation.js";
7
8
  import { MISSING_VALUE } from "./unknown.js";
8
- const renderSelectOptionsAdventurePointsValue = (locale, derivedLabel, getAllSelectOptions, getNameForSelectOptionId, config) => {
9
+ const typographicSign = (num) => (num > 0 ? "" : "−") + Math.abs(num).toFixed();
10
+ const renderFixedSelectOptionsAdventurePointsValue = (locale, getAllSelectOptions, getNameForSelectOptionId, getApValueForSelectOption, applyNegative) => {
11
+ const entriesMap = Map.groupBy(getAllSelectOptions().map((item) => [
12
+ getApValueForSelectOption(item, item.id),
13
+ item,
14
+ ]), item => item[0])
15
+ .entries()
16
+ .toArray();
17
+ if (entriesMap.every((entry) => typeof entry[0] === "number" && entry[1].length === 1)) {
18
+ const entriesSortedByApValue = entriesMap.toSorted(on(item => item[0], numAsc));
19
+ return entriesSortedByApValue
20
+ .map(entry => locale.translateMap(entry[1][0][1].content.translations)?.name ?? MISSING_VALUE)
21
+ .join("/")
22
+ .concat(`: ${locale.translate("{$value} Adventure Points", { value: entriesSortedByApValue.map(entry => typographicSign(applyNegative(entry[0]))).join("/") })}`);
23
+ }
24
+ else {
25
+ return entriesMap
26
+ .map(([apValue, items]) => [
27
+ apValue,
28
+ locale.join(items
29
+ .map(item => getNameForSelectOptionId(item[1].id))
30
+ .filter(name => name !== undefined)
31
+ .toSorted(locale.compare), "conjunction"),
32
+ ])
33
+ .toSorted(on(item => item[1], locale.compare))
34
+ .map(([apValue, itemNames]) => `${itemNames}: ${locale.translate("{$value} Adventure Points", { value: typeof apValue === "number" ? applyNegative(apValue) : apValue })}`)
35
+ .join("; ");
36
+ }
37
+ };
38
+ const renderSelectOptionsAdventurePointsValue = (locale, derivedLabel, getAllSelectOptions, getNameForSelectOptionId, config, applyNegative) => {
9
39
  if (config === undefined) {
10
40
  return MISSING_VALUE;
11
41
  }
@@ -18,23 +48,8 @@ const renderSelectOptionsAdventurePointsValue = (locale, derivedLabel, getAllSel
18
48
  })}`;
19
49
  }
20
50
  case "Fixed":
21
- return Map.groupBy(getAllSelectOptions().map((item) => [
22
- config.Fixed.map.find(mapItem => deepEqual(mapItem.id, item.id))?.ap_value ??
23
- config.Fixed.default,
24
- item,
25
- ]), item => item[0])
26
- .entries()
27
- .map(([apValue, items]) => [
28
- apValue,
29
- locale.join(items
30
- .map(item => getNameForSelectOptionId(item[1].id))
31
- .filter(name => name !== undefined)
32
- .toSorted(locale.compare), "conjunction"),
33
- ])
34
- .toArray()
35
- .toSorted(on(item => item[1], locale.compare))
36
- .map(([apValue, itemNames]) => `${itemNames}: ${locale.translate("{$value} Adventure Points", { value: apValue })}`)
37
- .join("; ");
51
+ return renderFixedSelectOptionsAdventurePointsValue(locale, getAllSelectOptions, getNameForSelectOptionId, item => config.Fixed.map.find(mapItem => deepEqual(mapItem.id, item.id))?.ap_value ??
52
+ config.Fixed.default, applyNegative);
38
53
  default:
39
54
  return assertExhaustive(config);
40
55
  }
@@ -42,23 +57,26 @@ const renderSelectOptionsAdventurePointsValue = (locale, derivedLabel, getAllSel
42
57
  /**
43
58
  * Renders an adventure points value to a human-readable string, using the provided locale for translations and formatting. For values that depend on select options or other instances, the necessary lookup functions are provided as arguments.
44
59
  */
45
- export const renderAdventurePointsValue = (locale, getNameForId, getNameForSelectOptionId, getAllSelectOptions, getAllInstances, value, entry, translation) => {
60
+ export const renderAdventurePointsValue = (locale, getNameForId, getNameForSelectOptionId, getAllSelectOptions, getAllInstances, value, entry, translation, negative) => {
46
61
  const { translate, translateMap } = locale;
62
+ const applyNegative = negative
63
+ ? numValue => -numValue
64
+ : numValue => numValue;
47
65
  if (typeof value === "number") {
48
66
  return translate(".input {$value :number} {{{$value} Adventure Points}}", {
49
- value,
67
+ value: applyNegative(value),
50
68
  });
51
69
  }
52
70
  switch (value.kind) {
53
71
  case "Fixed": {
54
72
  if (entry.levels === undefined) {
55
73
  return translate(".input {$value :number} {{{$value} Adventure Points}}", {
56
- value: value.Fixed,
74
+ value: applyNegative(value.Fixed),
57
75
  });
58
76
  }
59
77
  else {
60
78
  return translate(".input {$value :number} {{{$value} Adventure Points per level}}", {
61
- value: value.Fixed,
79
+ value: applyNegative(value.Fixed),
62
80
  });
63
81
  }
64
82
  }
@@ -68,7 +86,7 @@ export const renderAdventurePointsValue = (locale, getNameForId, getNameForSelec
68
86
  }
69
87
  const mainValue = `${translate("Level {$level}", {
70
88
  level: Array.from({ length: entry.levels }, (_, index) => romanize(index + 1)).join("/"),
71
- })}: ${value.ByLevel.list.join("/")}`;
89
+ })}: ${value.ByLevel.list.map(applyNegative).join("/")}`;
72
90
  const { additionalBySizeCategory } = value.ByLevel;
73
91
  if (additionalBySizeCategory === undefined) {
74
92
  return mainValue;
@@ -76,7 +94,7 @@ export const renderAdventurePointsValue = (locale, getNameForId, getNameForSelec
76
94
  else {
77
95
  const sizeCategories = ["tiny", "small", "medium", "large", "huge"];
78
96
  const values = sizeCategories
79
- .map(sizeCategory => additionalBySizeCategory[sizeCategory])
97
+ .map(sizeCategory => applyNegative(additionalBySizeCategory[sizeCategory]))
80
98
  .join("/");
81
99
  const labels = sizeCategories.map(sizeCategory => translate(sizeCategory)).join("/");
82
100
  return `${mainValue} + ${translate("{$values} AP for size category {$labels} (per level)", { values, labels })}`;
@@ -85,7 +103,25 @@ export const renderAdventurePointsValue = (locale, getNameForId, getNameForSelec
85
103
  case "DerivedFromSelection": {
86
104
  const derivedSelectOptions = entry.select_options?.derived;
87
105
  if (derivedSelectOptions === undefined) {
88
- return MISSING_VALUE;
106
+ return renderFixedSelectOptionsAdventurePointsValue(locale, getAllSelectOptions, getNameForSelectOptionId, item => {
107
+ switch (item.content.ap_value?.kind) {
108
+ case "Fixed":
109
+ return item.content.ap_value.Fixed;
110
+ case "DependingOnActiveInstances":
111
+ switch (item.content.ap_value.DependingOnActiveInstances.kind) {
112
+ case "Threshold":
113
+ return item.content.ap_value.DependingOnActiveInstances.Threshold.normal;
114
+ case "Expression":
115
+ return MISSING_VALUE;
116
+ default:
117
+ return assertExhaustive(item.content.ap_value.DependingOnActiveInstances);
118
+ }
119
+ case undefined:
120
+ return MISSING_VALUE;
121
+ default:
122
+ return assertExhaustive(item.content.ap_value);
123
+ }
124
+ }, applyNegative);
89
125
  }
90
126
  switch (derivedSelectOptions.kind) {
91
127
  case "Blessings":
@@ -98,14 +134,15 @@ export const renderAdventurePointsValue = (locale, getNameForId, getNameForSelec
98
134
  return translate("{$value} Adventure Points", {
99
135
  value: unique(getAllInstances("Script")
100
136
  .map(script => script.content.ap_value)
101
- .filter(apValue => apValue !== undefined))
137
+ .filter(isNotNullish))
102
138
  .toSorted(numAsc)
139
+ .map(applyNegative)
103
140
  .join("/"),
104
141
  });
105
142
  case "AnimalShapes": {
106
143
  const sizes = getAllInstances("AnimalShapeSize").toSorted(on(x => x.content.ap_value, numAsc));
107
144
  return translate("{$values} adventure points for a {$sized} animal shape", {
108
- values: sizes.map(size => size.content.ap_value).join("/"),
145
+ values: sizes.map(size => applyNegative(size.content.ap_value)).join("/"),
109
146
  sized: sizes
110
147
  .map(size => translateMap(size.content.translations)?.name ?? MISSING_VALUE)
111
148
  .join("/"),
@@ -179,9 +216,9 @@ export const renderAdventurePointsValue = (locale, getNameForId, getNameForSelec
179
216
  default:
180
217
  return translate("A/B/C/D ability");
181
218
  }
182
- }, getAllSelectOptions, getNameForSelectOptionId, derivedSelectOptions.Skills.ap_value);
219
+ }, getAllSelectOptions, getNameForSelectOptionId, derivedSelectOptions.Skills.ap_value, applyNegative);
183
220
  case "CombatTechniques":
184
- return renderSelectOptionsAdventurePointsValue(locale, () => translate("B/C/D combat technique"), getAllSelectOptions, getNameForSelectOptionId, derivedSelectOptions.CombatTechniques.ap_value);
221
+ return renderSelectOptionsAdventurePointsValue(locale, () => translate("B/C/D combat technique"), getAllSelectOptions, getNameForSelectOptionId, derivedSelectOptions.CombatTechniques.ap_value, applyNegative);
185
222
  case "TargetCategories":
186
223
  return MISSING_VALUE;
187
224
  default:
@@ -27,7 +27,7 @@ const makeActivatableIdentifierForCommonnessRatedAdvantageOrDisadvantage = (enti
27
27
  return assertExhaustive(entity);
28
28
  }
29
29
  };
30
- const renderCommonnessRatedAdvantageOrDisadvantageName = (translateMap, getInstanceById, getResolvedSelectOptionById, entity, item) => {
30
+ const renderCommonnessRatedAdvantageOrDisadvantageName = (translateMap, getInstanceById, getResolvedSelectOptionById, localeCompare, entity, item) => {
31
31
  const customTranslation = translateMap(item.translations);
32
32
  return (attributedCustomName(translateMap, getInstanceById, entity, (translation, instance) => {
33
33
  if (customTranslation?.full !== undefined) {
@@ -38,7 +38,7 @@ const renderCommonnessRatedAdvantageOrDisadvantageName = (translateMap, getInsta
38
38
  ? undefined
39
39
  : convertCommonnessRatedAdvantageOrDisadvantageLevel(item.level);
40
40
  const options = customTranslation?.options === undefined
41
- ? (item.options?.map(option => renderNameComponentsOptions(false, getResolvedSelectOptionById, id, [option]) ??
41
+ ? (item.options?.map(option => renderNameComponentsOptions(true, getResolvedSelectOptionById, id, [option]) ??
42
42
  MISSING_VALUE) ?? [])
43
43
  : [customTranslation.options];
44
44
  const name = translation.name_in_library !== undefined && options.length === 0
@@ -49,12 +49,8 @@ const renderCommonnessRatedAdvantageOrDisadvantageName = (translateMap, getInsta
49
49
  base: name,
50
50
  level,
51
51
  nameBuilderRules: makeNameBuilderRulesWithDefaults(instance.nameBuilderRules),
52
- options: customTranslation?.options === undefined
53
- ? (item.options?.map(option => renderNameComponentsOptions(false, getResolvedSelectOptionById, id, [
54
- option,
55
- ]) ?? MISSING_VALUE) ?? [])
56
- : [customTranslation.options],
57
- }, false);
52
+ options,
53
+ }, false, list => list.toSorted(localeCompare).join(", "));
58
54
  }, entity, item.id) ?? MISSING_VALUE);
59
55
  };
60
56
  /**
@@ -63,13 +59,13 @@ const renderCommonnessRatedAdvantageOrDisadvantageName = (translateMap, getInsta
63
59
  export const renderCommonnessRatedAdvantagesOrDisadvantages = (entity, items, emptyString) => Reader.asks(({ translateMap, getInstanceById, getResolvedSelectOptionById, localeCompare }) => items === undefined || !isNotEmpty(items)
64
60
  ? emptyString
65
61
  : items
66
- .map(item => renderCommonnessRatedAdvantageOrDisadvantageName(translateMap, getInstanceById, getResolvedSelectOptionById, entity, item))
62
+ .map(item => renderCommonnessRatedAdvantageOrDisadvantageName(translateMap, getInstanceById, getResolvedSelectOptionById, localeCompare, entity, item))
67
63
  .toSorted(localeCompare)
68
64
  .join(", "));
69
65
  /**
70
66
  * Render the names of commonness-rated advantages and disadvantages together.
71
67
  */
72
- export const renderCommonnessRatedAdvantagesAndDisadvantages = (advantages, disadvantages, emptyString) => Reader.asks(({ translateMap, getInstanceById, getResolvedSelectOptionById, localeCompare }) => mapNullable(ensureNonEmpty([["Advantage", advantages], ["Disadvantage", disadvantages]].flatMap(([entity, items]) => items?.map(item => renderCommonnessRatedAdvantageOrDisadvantageName(translateMap, getInstanceById, getResolvedSelectOptionById, entity, item)) ?? [])), renderedItems => renderedItems.toSorted(localeCompare).join(", ")) ?? emptyString);
68
+ export const renderCommonnessRatedAdvantagesAndDisadvantages = (advantages, disadvantages, emptyString) => Reader.asks(({ translateMap, getInstanceById, getResolvedSelectOptionById, localeCompare }) => mapNullable(ensureNonEmpty([["Advantage", advantages], ["Disadvantage", disadvantages]].flatMap(([entity, items]) => items?.map(item => renderCommonnessRatedAdvantageOrDisadvantageName(translateMap, getInstanceById, getResolvedSelectOptionById, localeCompare, entity, item)) ?? [])), renderedItems => renderedItems.toSorted(localeCompare).join(", ")) ?? emptyString);
73
69
  /**
74
70
  * Render a value with a possible translation, falling back to explicity rendering the value if no translation is available.
75
71
  */
@@ -13,11 +13,11 @@ export declare const printPublicationPrerequisites: (getInstanceById: GetInstanc
13
13
  /**
14
14
  * Print plain general prerequisites as a string.
15
15
  */
16
- export declare const printPlainGeneralPrerequisites: (getInstanceById: GetInstanceById<"Race" | "Culture" | "PactCategory" | "PactDomain" | "SocialStatus" | "State" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "Property" | "Aspect" | "Enhancement">, getResolvedSelectOptionById: GetResolvedSelectOptionById, locale: LocaleEnvironment, value: PlainGeneralPrerequisites) => string;
16
+ export declare const printPlainGeneralPrerequisites: (getInstanceById: GetInstanceById<"Race" | "Culture" | "PactCategory" | "PactDomain" | "SocialStatus" | "State" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "Property" | "Aspect" | "Enhancement" | "PersonalityTrait">, getResolvedSelectOptionById: GetResolvedSelectOptionById, locale: LocaleEnvironment, value: PlainGeneralPrerequisites) => string;
17
17
  /**
18
18
  * Print general prerequisites as a string.
19
19
  */
20
- export declare const printGeneralPrerequisites: (getInstanceById: GetInstanceById<"Race" | "Culture" | "PactCategory" | "PactDomain" | "SocialStatus" | "State" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "Property" | "Aspect" | "Enhancement">, getResolvedSelectOptionById: GetResolvedSelectOptionById, locale: LocaleEnvironment, value: GeneralPrerequisites, printPreviousLevelPrerequisites?: {
20
+ export declare const printGeneralPrerequisites: (getInstanceById: GetInstanceById<"Race" | "Culture" | "PactCategory" | "PactDomain" | "SocialStatus" | "State" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "Property" | "Aspect" | "Enhancement" | "PersonalityTrait">, getResolvedSelectOptionById: GetResolvedSelectOptionById, locale: LocaleEnvironment, value: GeneralPrerequisites, printPreviousLevelPrerequisites?: {
21
21
  id: ActivatableIdentifier;
22
22
  levels: number;
23
23
  }, trailingText?: string) => string;
@@ -28,7 +28,7 @@ export declare const printProfessionPrerequisites: (getInstanceById: GetInstance
28
28
  /**
29
29
  * Print advantage disadvantage prerequisites as a string.
30
30
  */
31
- export declare const printAdvantageDisadvantagePrerequisites: (getInstanceById: GetInstanceById<"Race" | "Culture" | "PactCategory" | "PactDomain" | "SocialStatus" | "State" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "Property" | "Aspect" | "Enhancement">, getResolvedSelectOptionById: GetResolvedSelectOptionById, locale: LocaleEnvironment, value: AdvantageDisadvantagePrerequisites, name: string, type: "Advantage" | "Disadvantage") => string;
31
+ export declare const printAdvantageDisadvantagePrerequisites: (getInstanceById: GetInstanceById<"Race" | "Culture" | "PactCategory" | "PactDomain" | "SocialStatus" | "State" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "Property" | "Aspect" | "Enhancement" | "PersonalityTrait">, getResolvedSelectOptionById: GetResolvedSelectOptionById, locale: LocaleEnvironment, value: AdvantageDisadvantagePrerequisites, name: string, type: "Advantage" | "Disadvantage") => string;
32
32
  /**
33
33
  * Print arcane tradition prerequisites as a string.
34
34
  */
@@ -14,7 +14,7 @@ export declare const printPublicationPrerequisiteGroup: (getInstanceById: GetIns
14
14
  /**
15
15
  * Print the translation of a general prerequisite group.
16
16
  */
17
- export declare const printGeneralPrerequisiteGroup: (getInstanceById: GetInstanceById<"Race" | "Culture" | "PactCategory" | "PactDomain" | "SocialStatus" | "State" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "Property" | "Aspect" | "Enhancement">, getResolvedSelectOptionById: GetResolvedSelectOptionById, locale: LocaleEnvironment, prerequisite: GeneralPrerequisiteGroup) => PrerequisitePart | undefined;
17
+ export declare const printGeneralPrerequisiteGroup: (getInstanceById: GetInstanceById<"Race" | "Culture" | "PactCategory" | "PactDomain" | "SocialStatus" | "State" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "Property" | "Aspect" | "Enhancement" | "PersonalityTrait">, getResolvedSelectOptionById: GetResolvedSelectOptionById, locale: LocaleEnvironment, prerequisite: GeneralPrerequisiteGroup) => PrerequisitePart | undefined;
18
18
  /**
19
19
  * Print the translation of a profession prerequisite group.
20
20
  */
@@ -22,7 +22,7 @@ export declare const printProfessionPrerequisiteGroup: (getInstanceById: GetInst
22
22
  /**
23
23
  * Print the translation of an advantage/disadvantage prerequisite group.
24
24
  */
25
- export declare const printAdvantageDisadvantagePrerequisiteGroup: (getInstanceById: GetInstanceById<"Race" | "Culture" | "PactCategory" | "PactDomain" | "SocialStatus" | "State" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "Property" | "Aspect" | "Enhancement">, getResolvedSelectOptionById: GetResolvedSelectOptionById, locale: LocaleEnvironment, prerequisite: AdvantageDisadvantagePrerequisiteGroup, name: string, type: "Advantage" | "Disadvantage") => PrerequisitePart | undefined;
25
+ export declare const printAdvantageDisadvantagePrerequisiteGroup: (getInstanceById: GetInstanceById<"Race" | "Culture" | "PactCategory" | "PactDomain" | "SocialStatus" | "State" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "Property" | "Aspect" | "Enhancement" | "PersonalityTrait">, getResolvedSelectOptionById: GetResolvedSelectOptionById, locale: LocaleEnvironment, prerequisite: AdvantageDisadvantagePrerequisiteGroup, name: string, type: "Advantage" | "Disadvantage") => PrerequisitePart | undefined;
26
26
  /**
27
27
  * Print the translation of an arcane tradition prerequisite group.
28
28
  */
@@ -87,6 +87,8 @@ export const printGeneralPrerequisiteGroup = (getInstanceById, getResolvedSelect
87
87
  return printTextPrerequisite(locale, prerequisite.Text);
88
88
  case "SexualCharacteristic":
89
89
  return printSexualCharacteristicPrerequisite(locale, prerequisite.SexualCharacteristic);
90
+ case "PersonalityTrait":
91
+ return printPersonalityTraitPrerequisite(getInstanceById, locale, prerequisite.PersonalityTrait);
90
92
  default:
91
93
  return assertExhaustive(prerequisite);
92
94
  }
@@ -136,6 +138,7 @@ export const printAdvantageDisadvantagePrerequisiteGroup = (getInstanceById, get
136
138
  case "Enhancement":
137
139
  case "Text":
138
140
  case "SexualCharacteristic":
141
+ case "PersonalityTrait":
139
142
  return printGeneralPrerequisiteGroup(getInstanceById, getResolvedSelectOptionById, locale, prerequisite);
140
143
  default:
141
144
  return assertExhaustive(prerequisite);
@@ -8,8 +8,9 @@ export const printRatedPrerequisite = (getInstanceById, locale, prerequisite) =>
8
8
  if (prerequisite.display_option !== undefined) {
9
9
  return printDisplayOption(locale.translateMap, prerequisite.display_option);
10
10
  }
11
+ const name = attributedCustomName(locale.translateMap, getInstanceById, "prerequisite", (t) => t.abbreviation ?? t.name, prerequisite.id) ?? MISSING_VALUE;
11
12
  return {
12
- value: `${attributedCustomName(locale.translateMap, getInstanceById, "prerequisite", (t) => t.abbreviation ?? t.name, prerequisite.id) ?? MISSING_VALUE} ${prerequisite.value.toFixed()}`,
13
+ value: prerequisite.value > 0 ? `${name} ${prerequisite.value.toFixed()}` : name,
13
14
  sentenceType: undefined,
14
15
  isMeta: false,
15
16
  };
@@ -1,7 +1,8 @@
1
1
  import { Reader } from "@elyukai/utils/reader";
2
2
  import type { CheckResultBasedModifier, DurationUnitValue, ElvenMagicalSongPermanentCost, FirstPersonMagicalMelodyCost, NonModifiableOneTimeCostPerCountable, NonModifiableSustainedCost, OneTimeCost, OneTimeCostMap, ResponsiveText, ResponsiveTextOptional, ResponsiveTextReplace, SkillModificationLevel_ID, SustainedCost } from "@optolith/database-schema/gen";
3
- import { type LocaleMap } from "../../../../helpers/translate.js";
3
+ import { type LocaleMap, type Translate } from "../../../../helpers/translate.js";
4
4
  import { type StdEnv, type StdReader } from "../../reader.js";
5
+ import { type ResponsiveTextSize } from "../../responsiveText.js";
5
6
  /**
6
7
  * Returns the text for the modifiable one-time cost of an activatable skill.
7
8
  */
@@ -51,11 +52,11 @@ export declare const renderOneTimeCost: (value: OneTimeCost) => StdReader<string
51
52
  * Returns the text for the non-modifiable cost of a sustained activatable skill.
52
53
  */
53
54
  export declare const renderNonModifiableSustainedCost: (value: NonModifiableSustainedCost) => Reader<{
54
- translate: import("../../../../helpers/translate.js").Translate;
55
+ translate: Translate;
55
56
  energyUnit: import("../../units/energy.js").EnergyUnit;
56
57
  } & {
57
- translate: import("../../../../helpers/translate.js").Translate;
58
- responsiveTextSize: import("../../responsiveText.js").ResponsiveTextSize;
58
+ translate: Translate;
59
+ responsiveTextSize: ResponsiveTextSize;
59
60
  } & StdEnv<"t" | "rts" | "tm" | "eu", never, never, never>, string>;
60
61
  /**
61
62
  * Returns the text for the cost of a sustained activatable skill.
@@ -2,10 +2,11 @@ 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 { responsiveTranslate, } from "../../../../helpers/translate.js";
5
6
  import { renderResponsiveMap } from "../../map.js";
6
7
  import { formatEnergyFnR, formatEnergyR, getInstanceByIdFnR, modifiableBySpeedR, responsiveLocaleJoinR, responsiveR, responsiveTextR, responsiveTranslateR, translateMapR, translateR, } from "../../reader.js";
7
- import { appendNoteIfNeeded, replaceTextIfNeeded } from "../../responsiveText.js";
8
- import { formatCombinedTimeSpanR } from "../../units/timeSpan.js";
8
+ import { appendNoteIfNeeded, replaceTextIfNeeded, } from "../../responsiveText.js";
9
+ import { formatCombinedTimeSpan, formatCombinedTimeSpanR } from "../../units/timeSpan.js";
9
10
  import { MISSING_VALUE } from "../../unknown.js";
10
11
  import { appendCheckResultModifier } from "./checkResultBased.js";
11
12
  import { wrapIfMinimum } from "./isMinimumMaximum.js";
@@ -57,12 +58,18 @@ export const renderModifiableOneTimeCost = (value) => deriveModifiableCost(value
57
58
  }
58
59
  return responsiveTextR(translation.additional).map(additional => `${text} + ${additional}`);
59
60
  })));
60
- const appendIntervalToCost = (interval, baseCost) => interval === undefined
61
+ const appendIntervalToCostR = (interval, baseCost) => interval === undefined
61
62
  ? Reader.of(baseCost)
62
63
  : formatCombinedTimeSpanR(interval, true).then(formattedInterval => responsiveTranslateR("{$cost} per {$interval}", "{$cost}/{$interval}", {
63
64
  cost: baseCost,
64
65
  interval: formattedInterval,
65
66
  }));
67
+ const appendIntervalToCost = (translate, responsiveTextSize, interval, baseCost) => interval === undefined
68
+ ? baseCost
69
+ : responsiveTranslate(translate, responsiveTextSize, "{$cost} per {$interval}", "{$cost}/{$interval}", {
70
+ cost: baseCost,
71
+ interval: formatCombinedTimeSpan(translate, responsiveTextSize, interval, true),
72
+ });
66
73
  /**
67
74
  * Returns the text for a non-modifiable one-time cost of an activatable skill.
68
75
  */
@@ -70,7 +77,7 @@ export const renderNonModifiableOneTimeCost = (value, shouldAppendNonModifiableS
70
77
  .thenW(base => appendFamiliarsTrickLPCostIfNeeded(value.lp_value, base))
71
78
  .thenW(base => appendPerCountableToCostIfNeeded(value.per, base))
72
79
  .then(base => appendPermanentCostIfNeeded(value.permanent_value, base))
73
- .then(base => appendIntervalToCost(value.interval, base))
80
+ .then(base => appendIntervalToCostR(value.interval, base))
74
81
  .then(base => appendElvenPermanentCostIfNeeded(value.permanent, base))
75
82
  .then(base => wrapIfMinimum(value.is_minimum, base))
76
83
  .then(base => appendNoteIfNeeded(value.translations, base))
@@ -126,7 +133,7 @@ export const renderOneTimeCostMap = (value) => Reader.asks(({ translate }) => tr
126
133
  : undefined))
127
134
  .then(text => appendNonModifiableSuffix(ModifiableParameter.Cost, text)));
128
135
  const renderSustainedCostMap = (value) => formatEnergyFnR
129
- .thenW(formatEnergy => renderResponsiveMap(value, option => option.value, formatEnergy))
136
+ .thenW(formatEnergy => Reader.ask().thenW(({ translate, responsiveTextSize }) => renderResponsiveMap(value, option => option.value, singleValue => appendIntervalToCost(translate, responsiveTextSize, value.interval, formatEnergy(singleValue)))))
130
137
  .then(text => appendNonModifiableSuffix(ModifiableParameter.Cost, text));
131
138
  /**
132
139
  * Returns the text for the cost of a one-time activatable skill.
@@ -147,7 +154,7 @@ export const renderOneTimeCost = (value) => {
147
154
  };
148
155
  const buildSustainedCost = (activationCost, intervalCost, interval) => {
149
156
  const activationCostWithLabel = responsiveR(() => translateR("activation").map(label => `${activationCost} (${label})`), () => Reader.of(activationCost)).thenW(identity);
150
- const intervalCostWithLabel = appendIntervalToCost(interval, intervalCost);
157
+ const intervalCostWithLabel = appendIntervalToCostR(interval, intervalCost);
151
158
  return activationCostWithLabel.map2(intervalCostWithLabel, (activationStr, intervalStr) => `${activationStr} + ${intervalStr}`);
152
159
  };
153
160
  const renderModifiableSustainedCost = (value) => deriveModifiableCost(value.initial_modification_level).thenW(cost => cost === undefined
@@ -12,6 +12,15 @@ export type TimeSpanUnit = "Seconds" | "Minutes" | "Hours" | "Days" | "Weeks" |
12
12
  export declare const formatTimeSpan: (translate: Translate, responsiveTextSize: ResponsiveTextSize, unit: {
13
13
  kind: TimeSpanUnit;
14
14
  } | TimeSpanUnit, value: number | string, interval?: boolean) => string;
15
+ /**
16
+ * Returns the text for a time span unit.
17
+ */
18
+ export declare const formatCombinedTimeSpan: (translate: Translate, responsiveTextSize: ResponsiveTextSize, object: {
19
+ unit: {
20
+ kind: TimeSpanUnit;
21
+ };
22
+ value: number | string;
23
+ }, interval?: boolean) => string;
15
24
  /**
16
25
  * Returns the text for a time span unit.
17
26
  */
@@ -28,6 +28,10 @@ export const formatTimeSpan = (translate, responsiveTextSize, unit, value, inter
28
28
  style: interval ? "interval" : "default",
29
29
  }));
30
30
  };
31
+ /**
32
+ * Returns the text for a time span unit.
33
+ */
34
+ export const formatCombinedTimeSpan = (translate, responsiveTextSize, object, interval = false) => formatTimeSpan(translate, responsiveTextSize, object.unit, object.value, interval);
31
35
  /**
32
36
  * Returns the text for a time span unit.
33
37
  */
@@ -6,7 +6,7 @@ import type { GetResolvedSelectOptionById } from "./partial/prerequisites/single
6
6
  * Get a JSON representation of the rules text for a poison.
7
7
  */
8
8
  export declare const getPoisonEntityDescription: import("../creator.js").EntityDescriptionCreator<"Poison", {
9
- getInstanceById: GetInstanceById<"Publication" | "DerivedCharacteristic" | "Disease" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "Race" | "Culture" | "State" | "Enhancement" | "PactCategory" | "PactDomain" | "SocialStatus" | "Aspect" | "Property">;
9
+ getInstanceById: GetInstanceById<"Publication" | "DerivedCharacteristic" | "Disease" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "Race" | "Culture" | "State" | "Enhancement" | "PactCategory" | "PactDomain" | "SocialStatus" | "Aspect" | "Property" | "PersonalityTrait">;
10
10
  getResolvedSelectOptionById: GetResolvedSelectOptionById;
11
11
  idMap: IdMap;
12
12
  }, import("../index.js").EntityDescription>;
@@ -77,12 +77,15 @@ const renderVariantLanguagesScriptsOption = (baseOptions, variantOptions) => ren
77
77
  })),
78
78
  });
79
79
  const renderLanguagesScriptsOption = (option) => renderVariantLanguagesScriptsOption(undefined, { kind: "Override", Override: option });
80
- const renderSkillSpecializationOption = (option) => Reader.asks(({ format, translate, translateMap, localeJoin, getInstanceById }) => {
80
+ const wrapInParensIf = (text, condition) => (condition ? parensIf(text) : text);
81
+ const renderSkillSpecializationOption = (option) => Reader.asks(({ format, translate, translateMap, localeJoin, localeCompare, getInstanceById }) => {
81
82
  switch (option.kind) {
82
83
  case "Specific":
83
84
  return translate("Skill Specialization {$possibleSkills}", {
84
- possibleSkills: localeJoin(option.Specific.options.map(id => attributedName(translateMap, getInstanceById, "profession", "Skill", id) ??
85
- MISSING_VALUE), "disjunction"),
85
+ possibleSkills: wrapInParensIf(localeJoin(option.Specific.options
86
+ .map(id => attributedName(translateMap, getInstanceById, "profession", "Skill", id) ??
87
+ MISSING_VALUE)
88
+ .toSorted(localeCompare), "disjunction"), option.Specific.options.length > 2),
86
89
  });
87
90
  case "Group":
88
91
  return translate("Skill Specialization for a {$skillOfGroup}", {
@@ -563,10 +566,13 @@ const renderProfessionVariantText = (base, variant, translation) => translation.
563
566
  renderRatedVariantChanges(base.spells, variant.spells, renderSpellworkName),
564
567
  renderBlessingsForVariant(variant),
565
568
  renderRatedVariantChanges(baseHasNoTradition(base) ? "ignore" : base.liturgical_chants, variant.liturgical_chants, renderLiturgicalChantName),
566
- ]).map(lists => lists
567
- .filter(isNotEmpty)
568
- .map(list => list.join(", "))
569
- .join("; ") + (translation.concluding_text ?? ""));
569
+ ]).map(lists => {
570
+ const nonEmptyLists = lists.filter(isNotEmpty);
571
+ return (nonEmptyLists.map(list => list.join(", ")).join("; ") +
572
+ (translation.concluding_text !== undefined
573
+ ? `${nonEmptyLists.length > 0 ? ". " : ""}${translation.concluding_text}`
574
+ : ""));
575
+ });
570
576
  const renderProfessionVariant = (base, variants) => translateMapFnR.thenW(translateMap => {
571
577
  const translations = variants
572
578
  .map(variant => mapNullable(translateMap(variant.content.translations), t => ({