@optolith/entity-descriptions 0.4.8 → 0.4.11

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 (42) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/lib/entities/activatable.js +17 -14
  3. package/lib/entities/curriculum.js +14 -21
  4. package/lib/entities/derivedCharacteristic.d.ts +0 -2
  5. package/lib/entities/derivedCharacteristic.js +10 -17
  6. package/lib/entities/disease.js +5 -1
  7. package/lib/entities/elixir.js +12 -10
  8. package/lib/entities/equipment.d.ts +6 -5
  9. package/lib/entities/equipment.js +19 -6
  10. package/lib/entities/equipmentPackage.js +2 -1
  11. package/lib/entities/liturgicalChant.js +7 -3
  12. package/lib/entities/partial/commonnessRatedAdvantagesAndDisadvantages.js +8 -12
  13. package/lib/entities/partial/herbary.js +5 -8
  14. package/lib/entities/partial/markdown.d.ts +55 -0
  15. package/lib/entities/partial/markdown.js +59 -0
  16. package/lib/entities/partial/prerequisites/index.d.ts +3 -3
  17. package/lib/entities/partial/prerequisites/index.js +3 -3
  18. package/lib/entities/partial/prerequisites/part.js +8 -3
  19. package/lib/entities/partial/prerequisites/prerequisiteGroups.d.ts +1 -1
  20. package/lib/entities/partial/prerequisites/prerequisiteGroups.js +10 -7
  21. package/lib/entities/partial/prerequisites/single/culture.js +2 -1
  22. package/lib/entities/partial/prerequisites/single/personalityTrait.js +2 -1
  23. package/lib/entities/partial/prerequisites/single/profession.js +4 -1
  24. package/lib/entities/partial/prerequisites/single/race.js +2 -1
  25. package/lib/entities/partial/prerequisites/single/rated.js +2 -27
  26. package/lib/entities/partial/prerequisites/single/ratedMinimumNumber.js +2 -1
  27. package/lib/entities/partial/prerequisites/single/ratedSum.js +2 -1
  28. package/lib/entities/partial/prerequisites/single/state.js +2 -1
  29. package/lib/entities/partial/rated/activatable/checkResultBased.js +3 -1
  30. package/lib/entities/partial/reader.d.ts +33 -0
  31. package/lib/entities/partial/reader.js +13 -0
  32. package/lib/entities/personalityTrait.js +9 -2
  33. package/lib/entities/poison.js +20 -9
  34. package/lib/entities/profession.d.ts +2 -2
  35. package/lib/entities/profession.js +133 -38
  36. package/lib/entities/race.d.ts +1 -1
  37. package/lib/entities/race.js +23 -23
  38. package/lib/entities/spell.d.ts +4 -2
  39. package/lib/entities/spell.js +17 -29
  40. package/lib/index.d.ts +64 -64
  41. package/lib/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +5 -5
package/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
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.11](https://github.com/Optolith/entity-descriptions/compare/v0.4.10...v0.4.11) (2026-03-23)
6
+
7
+
8
+ ### Features
9
+
10
+ * support schema version 0.42.0 ([80b2eaa](https://github.com/Optolith/entity-descriptions/commit/80b2eaacbcf445cf3222c11b86c1be77b7d973d6))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * activatable cost by level separator ([a0119ff](https://github.com/Optolith/entity-descriptions/commit/a0119ffdafcc6027801c0ff7b502c9c65af107dd))
16
+ * render attribute prerequisite as abbreviation ([6f258b1](https://github.com/Optolith/entity-descriptions/commit/6f258b1f6f4fb1c88b09fe9fb2013b180d91fc4b))
17
+
18
+ ## [0.4.10](https://github.com/Optolith/entity-descriptions/compare/v0.4.9...v0.4.10) (2026-03-17)
19
+
20
+ ## [0.4.9](https://github.com/Optolith/entity-descriptions/compare/v0.4.8...v0.4.9) (2026-03-17)
21
+
22
+
23
+ ### Features
24
+
25
+ * mark generated instances with attributed strings ([85cce91](https://github.com/Optolith/entity-descriptions/commit/85cce917d16ac10833d4dccf7031c2dca4e75d26))
26
+
27
+
28
+ ### Bug Fixes
29
+
30
+ * always append level to activatable name ([10ca82a](https://github.com/Optolith/entity-descriptions/commit/10ca82a1e57afd2d11ba7c32b55a49172e676398))
31
+ * require previous level and fix function typo ([2b12a01](https://github.com/Optolith/entity-descriptions/commit/2b12a01beaaf786843edbbcf42def3d1b1ecb673))
32
+
5
33
  ## [0.4.8](https://github.com/Optolith/entity-descriptions/compare/v0.4.7...v0.4.8) (2026-03-15)
6
34
 
7
35
 
@@ -13,6 +13,7 @@ import { createEntityDescriptionCreator } from "../creator.js";
13
13
  import { renderActivatableNameComponents } from "./partial/activatableNameChunks.js";
14
14
  import { renderAdventurePointsValue } from "./partial/adventurePointsValue.js";
15
15
  import { renderResponsiveMap } from "./partial/map.js";
16
+ import { attributedName } from "./partial/markdown.js";
16
17
  import { additionFormatter } from "./partial/mathOperation.js";
17
18
  import { printAdvantageDisadvantagePrerequisites, printGeneralPrerequisites, } from "./partial/prerequisites/index.js";
18
19
  import { printActivatableName, } from "./partial/prerequisites/single/activatable.js";
@@ -142,7 +143,7 @@ const wrapInParens = (args, append = "") => {
142
143
  return ` (${filteredArgs.join(", ") + append})`;
143
144
  };
144
145
  const addSpaceIfNoCommaAtStart = (str) => (str.startsWith(",") ? str : ` ${str}`);
145
- const renderApplicableCombatTechniquesRestriction = (getInstanceById, locale, main, restriction, translation, weapons, getExcludedInstance) => {
146
+ const renderApplicableCombatTechniquesRestriction = (getInstanceById, locale, main, restriction, translation, weapons, normalizeExcludedId) => {
146
147
  switch (restriction.kind) {
147
148
  case "Improvised":
148
149
  return main + wrapInParens([locale.translate("only improvised weapons"), weapons]);
@@ -156,13 +157,12 @@ const renderApplicableCombatTechniquesRestriction = (getInstanceById, locale, ma
156
157
  return main + wrapInParens([weapons], locale.translate("while mounted"));
157
158
  }
158
159
  case "Race": {
159
- const race = getInstanceById("Race", restriction.Race);
160
- const raceName = locale.translateMap(race?.translations)?.name ?? MISSING_VALUE;
160
+ const raceName = attributedName(locale.translateMap, getInstanceById, "applicable-combat-technique", "Race", restriction.Race);
161
161
  return (main +
162
162
  wrapInParens([
163
163
  // originally "while {$racial} weapon", but different to parameterize without inflection support
164
164
  locale.translate("while weapon of race {$race}", {
165
- race: raceName,
165
+ race: raceName ?? MISSING_VALUE,
166
166
  }),
167
167
  weapons,
168
168
  ]));
@@ -172,7 +172,12 @@ const renderApplicableCombatTechniquesRestriction = (getInstanceById, locale, ma
172
172
  wrapInParens([
173
173
  locale.translate("except {$list}", {
174
174
  list: locale.join(restriction.ExcludeCombatTechniques.list
175
- .map(id => locale.translateMap(getExcludedInstance?.(id)?.translations)?.name ?? MISSING_VALUE)
175
+ .map(id => {
176
+ const normalizedId = normalizeExcludedId?.(id);
177
+ return normalizedId !== undefined
178
+ ? (attributedName(locale.translateMap, getInstanceById, "applicable-combat-technique", normalizedId) ?? MISSING_VALUE)
179
+ : MISSING_VALUE;
180
+ })
176
181
  .toSorted(locale.compare), "conjunction"),
177
182
  }),
178
183
  weapons,
@@ -207,28 +212,27 @@ const renderApplicableCombatTechniquesValue = (getInstanceById, locale, translat
207
212
  const main = locale.translate("All");
208
213
  const mainWithRestriction = applicableCombatTechniques.All.restriction === undefined
209
214
  ? main
210
- : renderApplicableCombatTechniquesRestriction(getInstanceById, locale, main, applicableCombatTechniques.All.restriction, translation, undefined, id => getInstanceById(id.kind, fromUniformCase(id)));
215
+ : renderApplicableCombatTechniquesRestriction(getInstanceById, locale, main, applicableCombatTechniques.All.restriction, translation, undefined, id => id);
211
216
  return mainWithRestriction;
212
217
  }
213
218
  case "AllClose": {
214
219
  const main = locale.translate("All Close Combat Techniques");
215
220
  const mainWithRestriction = applicableCombatTechniques.AllClose.restriction === undefined
216
221
  ? main
217
- : renderApplicableCombatTechniquesRestriction(getInstanceById, locale, main, applicableCombatTechniques.AllClose.restriction, translation, undefined, id => getInstanceById("CloseCombatTechnique", id));
222
+ : renderApplicableCombatTechniquesRestriction(getInstanceById, locale, main, applicableCombatTechniques.AllClose.restriction, translation, undefined, id => Case("CloseCombatTechnique", id));
218
223
  return mainWithRestriction;
219
224
  }
220
225
  case "AllRanged": {
221
226
  const main = locale.translate("All Ranged Combat Techniques");
222
227
  const mainWithRestriction = applicableCombatTechniques.AllRanged.restriction === undefined
223
228
  ? main
224
- : renderApplicableCombatTechniquesRestriction(getInstanceById, locale, main, applicableCombatTechniques.AllRanged.restriction, translation, undefined, id => getInstanceById("RangedCombatTechnique", id));
229
+ : renderApplicableCombatTechniquesRestriction(getInstanceById, locale, main, applicableCombatTechniques.AllRanged.restriction, translation, undefined, id => Case("RangedCombatTechnique", id));
225
230
  return mainWithRestriction;
226
231
  }
227
232
  case "Specific": {
228
233
  return applicableCombatTechniques.Specific.list
229
234
  .map(specific => {
230
- const entry = getInstanceById(specific.id.kind, fromUniformCase(specific.id));
231
- const main = locale.translateMap(entry?.translations)?.name ?? MISSING_VALUE;
235
+ const main = attributedName(locale.translateMap, getInstanceById, "applicable-combat-technique", specific.id) ?? MISSING_VALUE;
232
236
  const mainWithRestriction = specific.restriction === undefined
233
237
  ? main
234
238
  : renderApplicableCombatTechniquesRestriction(getInstanceById, locale, main, specific.restriction, translation, specific.weapons === undefined
@@ -388,7 +392,7 @@ const renderArcaneEnergyCost = (translate, translateMap, localeJoin, responsiveT
388
392
  cost: translate("{$value} AE", {
389
393
  value: cost.ByLevel.levels.map(level => level.value).join("/"),
390
394
  }),
391
- level: Array.from({ length: cost.ByLevel.levels.length }, (_, index) => romanize(index + 1)),
395
+ level: Array.from({ length: cost.ByLevel.levels.length }, (_, index) => romanize(index + 1)).join("/"),
392
396
  });
393
397
  case "Verbose":
394
398
  return cost.ByLevel.levels
@@ -635,9 +639,8 @@ export const getActivatableEntityDescription = createEntityDescriptionCreator(({
635
639
  const baseEntry = entry;
636
640
  const wrappedId = Case(entityName, id);
637
641
  return {
638
- title: translation.name_in_library ??
639
- translation.name +
640
- (baseEntry.levels !== undefined ? ` I–${romanize(baseEntry.levels)}` : ""),
642
+ title: (translation.name_in_library ?? translation.name) +
643
+ (baseEntry.levels !== undefined ? ` I–${romanize(baseEntry.levels)}` : ""),
641
644
  subtitle: mapNullable(baseEntry.usage_type, usageType => {
642
645
  switch (usageType.kind) {
643
646
  case "Passive":
@@ -5,6 +5,7 @@ import { sign } from "@elyukai/utils/string/number";
5
5
  import { assertExhaustive } from "@elyukai/utils/typeSafety";
6
6
  import { isNotNullish, mapNullable } from "@optolith/helpers/nullable";
7
7
  import { createEntityDescriptionCreator } from "../creator.js";
8
+ import { attributedName } from "./partial/markdown.js";
8
9
  import { getBaseProfessionPackageForCurriculum } from "./partial/professions.js";
9
10
  import { parensIf } from "./partial/rated/activatable/parensIf.js";
10
11
  import { MISSING_VALUE } from "./partial/unknown.js";
@@ -14,13 +15,12 @@ const renderElectiveSpellworks = (translate, translateMap, localeCompare, getIns
14
15
  return translate("All, at the GM’s discretion");
15
16
  case "Specific":
16
17
  return electiveSpellworks.Specific.list
17
- .map(item => mapNullable(translateMap(getInstanceById(item.id)?.translations)?.name, name => {
18
+ .map(item => mapNullable(attributedName(translateMap, getInstanceById, "curriculum", item.id), name => {
18
19
  if (item.restriction === undefined) {
19
20
  return name;
20
21
  }
21
22
  return (name +
22
- parensIf(translateMap(getInstanceById("Element", item.restriction.Element)?.translations)
23
- ?.name ?? MISSING_VALUE));
23
+ parensIf(attributedName(translateMap, getInstanceById, "curriculum", "Element", item.restriction.Element)));
24
24
  }))
25
25
  .filter(isNotNullish)
26
26
  .toSorted(localeCompare)
@@ -42,7 +42,7 @@ const renderRestrictedSpellworks = (translate, translateMap, localeCompare, loca
42
42
  return "";
43
43
  }
44
44
  const translatedSpellworks = excludedSpellworks
45
- .map(excludedSpellwork => translateMap(getInstanceById(excludedSpellwork)?.translations)?.name)
45
+ .map(excludedSpellwork => attributedName(translateMap, getInstanceById, "curriculum", excludedSpellwork))
46
46
  .filter(isNotNullish);
47
47
  if (translatedSpellworks.length === 0) {
48
48
  return "";
@@ -53,16 +53,14 @@ const renderRestrictedSpellworks = (translate, translateMap, localeCompare, loca
53
53
  };
54
54
  const restrictedProperties = restrictedGroups.get("restrictedProperty")?.map(restriction => translate(".input {$count :number} {{only {$count} additional spellworks with the Property {$property}}}", {
55
55
  count: restriction.Property.maximum,
56
- property: translateMap(getInstanceById("Property", restriction.Property.id)?.translations)
57
- ?.name ?? MISSING_VALUE,
56
+ property: attributedName(translateMap, getInstanceById, "curriculum", "Property", restriction.Property.id) ?? MISSING_VALUE,
58
57
  }) + addExclusion(restriction.Property.exclude)) ?? [];
59
58
  const propertyGroup = restrictedGroups.get("property");
60
59
  const translatedProperties = propertyGroup
61
60
  ?.map(restriction => {
62
61
  switch (restriction.kind) {
63
62
  case "Property":
64
- return translateMap(getInstanceById("Property", restriction.Property.id)?.translations)
65
- ?.name;
63
+ return attributedName(translateMap, getInstanceById, "curriculum", "Property", restriction.Property.id);
66
64
  case "DemonSummoning":
67
65
  return translate("Demon Summoning");
68
66
  default:
@@ -80,7 +78,7 @@ const renderRestrictedSpellworks = (translate, translateMap, localeCompare, loca
80
78
  : [];
81
79
  const spellworkGroup = restrictedGroups.get("spellwork");
82
80
  const translatedSpellworks = spellworkGroup
83
- ?.map(restriction => translateMap(getInstanceById(restriction.Spellwork)?.translations)?.name)
81
+ ?.map(restriction => attributedName(translateMap, getInstanceById, "curriculum", restriction.Spellwork))
84
82
  .filter(isNotNullish)
85
83
  .toSorted(localeCompare) ?? [];
86
84
  const spellworks = translatedSpellworks.length > 0
@@ -103,24 +101,20 @@ const renderRestrictedSpellworks = (translate, translateMap, localeCompare, loca
103
101
  }) ?? [];
104
102
  return [...restrictedProperties, ...properties, ...others, ...spellworks].join("; ");
105
103
  };
106
- const renderSpellworkAdjustment = (translateMap, getInstanceById, adjustment) => `${translateMap(getInstanceById(adjustment.id)?.translations)?.name ?? MISSING_VALUE} ${adjustment.points.toFixed()}${parensIf(adjustment.tradition === undefined
104
+ const renderSpellworkAdjustment = (translateMap, getInstanceById, adjustment) => `${attributedName(translateMap, getInstanceById, "curriculum", adjustment.id) ?? MISSING_VALUE} ${adjustment.points.toFixed()}${parensIf(adjustment.tradition === undefined
107
105
  ? undefined
108
- : translateMap(getInstanceById("MagicalTradition", adjustment.tradition)?.translations)?.name)}`;
106
+ : attributedName(translateMap, getInstanceById, "curriculum", "MagicalTradition", adjustment.tradition))}`;
109
107
  const renderAbilityAdjustmentName = (translateMap, getInstanceById, abilityAdjustment) => {
110
108
  switch (abilityAdjustment.kind) {
111
109
  case "Skill":
112
- return (translateMap(getInstanceById("Skill", abilityAdjustment.Skill.id)?.translations)?.name ??
113
- MISSING_VALUE);
110
+ return (attributedName(translateMap, getInstanceById, "curriculum", "Skill", abilityAdjustment.Skill.id) ?? MISSING_VALUE);
114
111
  case "CombatTechnique":
115
- return (translateMap(getInstanceById(abilityAdjustment.CombatTechnique.id)?.translations)?.name ??
116
- MISSING_VALUE);
112
+ return (attributedName(translateMap, getInstanceById, "curriculum", abilityAdjustment.CombatTechnique.id) ?? MISSING_VALUE);
117
113
  case "Spellwork":
118
- return ((translateMap(getInstanceById(abilityAdjustment.Spellwork.id)?.translations)?.name ??
119
- MISSING_VALUE) +
114
+ return ((attributedName(translateMap, getInstanceById, "curriculum", abilityAdjustment.Spellwork.id) ?? MISSING_VALUE) +
120
115
  parensIf(abilityAdjustment.Spellwork.tradition === undefined
121
116
  ? undefined
122
- : translateMap(getInstanceById("MagicalTradition", abilityAdjustment.Spellwork.tradition)
123
- ?.translations)?.name));
117
+ : attributedName(translateMap, getInstanceById, "curriculum", "MagicalTradition", abilityAdjustment.Spellwork.tradition)));
124
118
  default:
125
119
  return assertExhaustive(abilityAdjustment);
126
120
  }
@@ -195,8 +189,7 @@ export const getCurriculumEntityDescription = createEntityDescriptionCreator(({
195
189
  items: [
196
190
  {
197
191
  label: translate("Guideline"),
198
- value: translateMap(getInstanceById("Guideline", entry.guideline)?.translations)?.name ??
199
- MISSING_VALUE,
192
+ value: attributedName(translateMap, getInstanceById, "curriculum", "Guideline", entry.guideline) ?? MISSING_VALUE,
200
193
  },
201
194
  {
202
195
  label: translate("Elective Spellworks Package"),
@@ -1,9 +1,7 @@
1
1
  import type { GetInstanceById } from "../helpers/getTypes.js";
2
- import type { IdMap } from "../index.js";
3
2
  /**
4
3
  * Get a JSON representation of the rules text for a derived characteristic.
5
4
  */
6
5
  export declare const getDerivedCharacteristicEntityDescription: import("../creator.js").EntityDescriptionCreator<"DerivedCharacteristic", {
7
6
  getInstanceById: GetInstanceById<"Publication" | "Attribute" | "DerivedCharacteristic">;
8
- idMap: IdMap;
9
7
  }, import("../index.js").EntityDescription>;
@@ -1,27 +1,20 @@
1
1
  import { assertExhaustive } from "@optolith/helpers/typeSafety";
2
2
  import { createEntityDescriptionCreator } from "../creator.js";
3
+ import { attributedCustomName } from "./partial/markdown.js";
3
4
  import { renderMathOperation } from "./partial/mathOperation.js";
4
5
  import { MISSING_VALUE } from "./partial/unknown.js";
5
- const getAttribute = (getInstanceById, translateMap, attributeId, style = "full") => {
6
- const attribute = getInstanceById("Attribute", attributeId);
7
- if (attribute === undefined) {
8
- return MISSING_VALUE;
9
- }
6
+ const renderAttributeNameForStyle = (style, translation) => {
10
7
  switch (style) {
11
8
  case "full":
12
- return translateMap(attribute.translations)?.name ?? MISSING_VALUE;
9
+ return translation.name;
13
10
  case "compact":
14
- return translateMap(attribute.translations)?.abbreviation ?? MISSING_VALUE;
11
+ return translation.abbreviation;
15
12
  default:
16
13
  return assertExhaustive(style);
17
14
  }
18
15
  };
19
- const getRaceBaseValue = (translate, translateMap, getInstanceById, idMap, raceBaseValue, style = "full") => {
20
- const instance = getInstanceById("DerivedCharacteristic", idMap.DerivedCharacteristic[raceBaseValue.kind]);
21
- const translation = translateMap(instance?.translations);
22
- if (translation === undefined) {
23
- return MISSING_VALUE;
24
- }
16
+ const getAttribute = (getInstanceById, translateMap, attributeId, style = "full") => attributedCustomName(translateMap, getInstanceById, "derived-characteristic-calculation", translation => renderAttributeNameForStyle(style, translation), "Attribute", attributeId) ?? MISSING_VALUE;
17
+ const getRaceBaseValue = (translate, translation, style = "full") => {
25
18
  switch (style) {
26
19
  case "full":
27
20
  return translate("Base {$name} from Race", translation);
@@ -31,14 +24,14 @@ const getRaceBaseValue = (translate, translateMap, getInstanceById, idMap, raceB
31
24
  return assertExhaustive(style);
32
25
  }
33
26
  };
34
- const renderBaseCalculation = (getInstanceById, translate, translateMap, idMap, calculation, style = "full") => renderMathOperation(calculation, value => {
27
+ const renderBaseCalculation = (getInstanceById, translate, translateMap, translation, calculation, style = "full") => renderMathOperation(calculation, value => {
35
28
  switch (value.kind) {
36
29
  case "Constant":
37
30
  return value.Constant.toFixed();
38
31
  case "Attribute":
39
32
  return getAttribute(getInstanceById, translateMap, value.Attribute, style);
40
33
  case "RaceBaseValue":
41
- return getRaceBaseValue(translate, translateMap, getInstanceById, idMap, value.RaceBaseValue, style);
34
+ return getRaceBaseValue(translate, translation, style);
42
35
  case "PrimaryAttribute":
43
36
  switch (style) {
44
37
  case "full":
@@ -62,7 +55,7 @@ const renderBaseCalculation = (getInstanceById, translate, translateMap, idMap,
62
55
  /**
63
56
  * Get a JSON representation of the rules text for a derived characteristic.
64
57
  */
65
- export const getDerivedCharacteristicEntityDescription = createEntityDescriptionCreator(({ getInstanceById, idMap }, { translate, translateMap }, { content: entry }) => {
58
+ export const getDerivedCharacteristicEntityDescription = createEntityDescriptionCreator(({ getInstanceById }, { translate, translateMap }, { content: entry }) => {
66
59
  const translation = translateMap(entry.translations);
67
60
  if (translation === undefined) {
68
61
  return undefined;
@@ -82,7 +75,7 @@ export const getDerivedCharacteristicEntityDescription = createEntityDescription
82
75
  items: [
83
76
  {
84
77
  label: translate("Base Value"),
85
- value: renderBaseCalculation(getInstanceById, translate, translateMap, idMap, entry.calculation.base),
78
+ value: renderBaseCalculation(getInstanceById, translate, translateMap, translation, entry.calculation.base),
86
79
  },
87
80
  ],
88
81
  },
@@ -25,7 +25,11 @@ export const getDiseaseEntityDescription = createEntityDescriptionCreator(({ get
25
25
  return undefined;
26
26
  }
27
27
  return {
28
- title: translation.name,
28
+ title: translation.name +
29
+ parensIf(ensureNonEmpty([
30
+ baseEntry.isChildhoodDisease === true ? translate("childhood disease") : undefined,
31
+ baseEntry.isMagicalDisease === true ? translate("magical disease") : undefined,
32
+ ].filter(isNotNullish))?.join(", ")),
29
33
  className: "disease",
30
34
  body: [
31
35
  {
@@ -58,16 +58,18 @@ export const getElixirEntityDescription = createEntityDescriptionCreator(({ getI
58
58
  },
59
59
  {
60
60
  label: translate("Quality Levels"),
61
- value: [
62
- {
63
- type: "definitionList",
64
- style: "nested",
65
- items: translation.quality_levels.map((effectForLevel, index) => ({
66
- label: (index + 1).toFixed(),
67
- value: effectForLevel,
68
- })),
69
- },
70
- ],
61
+ value: translation.quality_levels.kind === "Plain"
62
+ ? translation.quality_levels.Plain.text
63
+ : [
64
+ {
65
+ type: "definitionList",
66
+ style: "nested",
67
+ items: translation.quality_levels.ForEachQualityLevel.qualityLevels.map((effectForLevel, index) => ({
68
+ label: (index + 1).toFixed(),
69
+ value: effectForLevel,
70
+ })),
71
+ },
72
+ ],
71
73
  },
72
74
  ],
73
75
  },
@@ -1,6 +1,7 @@
1
- import type { EquipmentIdentifier, GenMeleeWeapon, GenRangedWeapon, LocaleMeasurementAdjustments } from "optolith-database-schema/gen";
1
+ import type { AmmunitionishIdentifier, EquipmentIdentifier, GenMeleeWeapon, GenRangedWeapon, LocaleMeasurementAdjustments } from "optolith-database-schema/gen";
2
2
  import { type TaggedEntity } from "../creator.js";
3
3
  import type { GetInstanceById } from "../helpers/getTypes.js";
4
+ import type { LocaleJoin } from "../helpers/locale.js";
4
5
  import type { Translate, TranslateMap } from "../helpers/translate.js";
5
6
  import type { IdMap, LabeledEntityDescriptionSection, RawEntityDescriptionSectionContent } from "../index.js";
6
7
  /**
@@ -10,15 +11,15 @@ export declare const getEquipmentName: (translate: Translate, translateMap: Tran
10
11
  /**
11
12
  * Render combat values of a melee weapon.
12
13
  */
13
- export declare const renderMeleeWeapon: <Damage>(translate: Translate, translateMap: TranslateMap, getInstanceById: GetInstanceById<"Attribute" | "CloseCombatTechnique" | "Reach">, measurements: Required<LocaleMeasurementAdjustments>, renderDamage: (damage: Damage) => string, closeCombatTechniqueId: string, use: GenMeleeWeapon<Damage>) => LabeledEntityDescriptionSection<RawEntityDescriptionSectionContent>;
14
+ export declare const renderMeleeWeapon: <Damage>(translate: Translate, translateMap: TranslateMap, localeJoin: LocaleJoin, getInstanceById: GetInstanceById<"Attribute" | "CloseCombatTechnique" | "Reach">, measurements: Required<LocaleMeasurementAdjustments>, renderDamage: (damage: Damage) => string, closeCombatTechniqueId: string, use: GenMeleeWeapon<Damage>) => LabeledEntityDescriptionSection<RawEntityDescriptionSectionContent>;
14
15
  /**
15
16
  * Render combat values of a ranged weapon.
16
17
  */
17
- export declare const renderRangedWeapon: <Damage>(translate: Translate, translateMap: TranslateMap, getInstanceById: GetInstanceById<"RangedCombatTechnique" | "Ammunition">, measurements: Required<LocaleMeasurementAdjustments>, renderDamage: (damage: Damage) => string, rangedCombatTechniqueId: string, use: GenRangedWeapon<Damage>) => LabeledEntityDescriptionSection<RawEntityDescriptionSectionContent>;
18
+ export declare const renderRangedWeapon: <Damage>(translate: Translate, translateMap: TranslateMap, getInstanceById: GetInstanceById<"RangedCombatTechnique" | AmmunitionishIdentifier["kind"]>, measurements: Required<LocaleMeasurementAdjustments>, renderDamage: (damage: Damage) => string, rangedCombatTechniqueId: string, use: GenRangedWeapon<Damage>) => LabeledEntityDescriptionSection<RawEntityDescriptionSectionContent>;
18
19
  /**
19
20
  * Get a JSON representation of the rules text for equipment.
20
21
  */
21
- export declare const getEquipmentEntityDescription: import("../creator.js").EntityDescriptionCreator<"Weapon" | "Armor" | "Ammunition" | "Animal" | "AnimalCare" | "BandageOrRemedy" | "Book" | "CeremonialItem" | "Clothes" | "ClothingPackage" | "Container" | "EquipmentOfBlessedOnes" | "GemOrPreciousStone" | "IlluminationLightSource" | "IlluminationRefillOrSupply" | "Jewelry" | "Laboratory" | "Liebesspielzeug" | "LuxuryGood" | "MagicalArtifact" | "MusicalInstrument" | "Newspaper" | "OrienteeringAid" | "RopeOrChain" | "Stationery" | "ThievesTool" | "ToolOfTheTrade" | "TravelGearOrTool" | "Vehicle" | "WeaponAccessory" | "WorkingSupernaturalCreature", {
22
- getInstanceById: GetInstanceById<"Publication" | "Attribute" | "Reach" | "SocialStatus" | "CloseCombatTechnique" | "RangedCombatTechnique" | "MagicalTradition" | "BlessedTradition" | "DerivedCharacteristic" | "Ammunition" | "Race" | "Culture" | "Profession" | "Skill">;
22
+ export declare const getEquipmentEntityDescription: import("../creator.js").EntityDescriptionCreator<"Ammunition" | "Weapon" | "Armor" | "Animal" | "AnimalCare" | "BandageOrRemedy" | "Book" | "CeremonialItem" | "Clothes" | "ClothingPackage" | "Container" | "EquipmentOfBlessedOnes" | "GemOrPreciousStone" | "IlluminationLightSource" | "IlluminationRefillOrSupply" | "Jewelry" | "Laboratory" | "Liebesspielzeug" | "LuxuryGood" | "MagicalArtifact" | "MusicalInstrument" | "Newspaper" | "OrienteeringAid" | "RopeOrChain" | "Stationery" | "ThievesTool" | "ToolOfTheTrade" | "TravelGearOrTool" | "Vehicle" | "WeaponAccessory" | "WorkingSupernaturalCreature", {
23
+ getInstanceById: GetInstanceById<"Publication" | "Attribute" | "Reach" | "SocialStatus" | "CloseCombatTechnique" | "RangedCombatTechnique" | "MagicalTradition" | "BlessedTradition" | "DerivedCharacteristic" | AmmunitionishIdentifier["kind"] | "Race" | "Culture" | "Profession" | "Skill">;
23
24
  idMap: IdMap;
24
25
  }, import("../index.js").EntityDescription>;
@@ -1,11 +1,14 @@
1
1
  import { ensureNonEmpty, isNotEmpty } from "@elyukai/utils/array/nonEmpty";
2
+ import { on } from "@elyukai/utils/function";
2
3
  import { isNotNullish } from "@elyukai/utils/nullable";
4
+ import { compareNumber } from "@elyukai/utils/ordering";
3
5
  import { romanize } from "@elyukai/utils/roman";
4
6
  import { sign } from "@elyukai/utils/string/number";
5
7
  import { assertExhaustive } from "@elyukai/utils/typeSafety";
6
8
  import { mapNullable } from "@optolith/helpers/nullable";
7
9
  import { createEntityDescriptionCreator } from "../creator.js";
8
10
  import { renderDice, renderDiceAndFlat } from "./partial/dice.js";
11
+ import { attributedName, attributedNameFromInstance } from "./partial/markdown.js";
9
12
  import { additionFormatter, subtractionFormatter } from "./partial/mathOperation.js";
10
13
  import { parensIf } from "./partial/rated/activatable/parensIf.js";
11
14
  import { ResponsiveTextSize } from "./partial/responsiveText.js";
@@ -91,9 +94,19 @@ const renderPrimaryAttributeAndDamageThreshold = (translateMap, getInstanceById,
91
94
  const renderAttackParryModifier = (attackModifier, parryModifier) => attackModifier === undefined && parryModifier === undefined
92
95
  ? "—"
93
96
  : `${attackModifier === undefined ? "—" : sign(attackModifier)}/${parryModifier === undefined ? "—" : sign(parryModifier)}`;
94
- const renderReach = (translateMap, getInstanceById, reach) => reach === undefined
97
+ const renderReach = (translateMap, localeJoin, getInstanceById, reaches) => reaches === undefined
95
98
  ? "—"
96
- : (translateMap(getInstanceById("Reach", reach)?.translations)?.name ?? MISSING_VALUE);
99
+ : localeJoin(reaches
100
+ .map((reach) => {
101
+ const instance = getInstanceById("Reach", reach);
102
+ return [
103
+ instance?.position ?? 0,
104
+ attributedNameFromInstance(translateMap, instance, "equipment", "Reach", reach) ??
105
+ MISSING_VALUE,
106
+ ];
107
+ })
108
+ .toSorted(on(e => e[0], compareNumber))
109
+ .map(e => e[1]), "conjunction");
97
110
  const renderLength = (translate, measurements, length) => length === undefined
98
111
  ? "—"
99
112
  : translate(".input {$value :number} {{{$value} inches}}", {
@@ -102,7 +115,7 @@ const renderLength = (translate, measurements, length) => length === undefined
102
115
  /**
103
116
  * Render combat values of a melee weapon.
104
117
  */
105
- export const renderMeleeWeapon = (translate, translateMap, getInstanceById, measurements, renderDamage, closeCombatTechniqueId, use) => {
118
+ export const renderMeleeWeapon = (translate, translateMap, localeJoin, getInstanceById, measurements, renderDamage, closeCombatTechniqueId, use) => {
106
119
  const combatTechnique = getInstanceById("CloseCombatTechnique", closeCombatTechniqueId);
107
120
  const fields = combatTechnique?.special ?? {
108
121
  can_parry: { kind: "Prohibited" },
@@ -137,7 +150,7 @@ export const renderMeleeWeapon = (translate, translateMap, getInstanceById, meas
137
150
  ? undefined
138
151
  : {
139
152
  label: translate("Reach"),
140
- value: renderReach(translateMap, getInstanceById, use.reach),
153
+ value: renderReach(translateMap, localeJoin, getInstanceById, use.reach),
141
154
  },
142
155
  fields.has_length.kind === "Prohibited"
143
156
  ? undefined
@@ -159,7 +172,7 @@ const renderReloadTime = (translate, reloadTime) => isNotEmpty(reloadTime)
159
172
  const renderRangeBrackets = (rangeBrackets) => `${rangeBrackets.close.toFixed()}/${rangeBrackets.medium.toFixed()}/${rangeBrackets.far.toFixed()}`;
160
173
  const renderAmmunition = (translateMap, getInstanceById, ammunition) => ammunition === undefined
161
174
  ? "—"
162
- : (translateMap(getInstanceById("Ammunition", ammunition)?.translations)?.name ?? MISSING_VALUE);
175
+ : (attributedName(translateMap, getInstanceById, "equipment", ammunition) ?? MISSING_VALUE);
163
176
  const renderMeleeDamage = (translate) => (damage) => renderDiceAndFlat(translate, damage.dice, damage.flat);
164
177
  /**
165
178
  * Render combat values of a ranged weapon.
@@ -713,7 +726,7 @@ export const getEquipmentEntityDescription = createEntityDescriptionCreator(({ g
713
726
  className: "equipment",
714
727
  body: [
715
728
  ...(combatValues?.type === "Weapon"
716
- ? Object.entries(combatValues.values.melee_uses ?? {}).map(([combatTechniqueId, use]) => renderMeleeWeapon(translate, translateMap, getInstanceById, locale.measurementAdjustments, renderMeleeDamage(translate), combatTechniqueId, use))
729
+ ? Object.entries(combatValues.values.melee_uses ?? {}).map(([combatTechniqueId, use]) => renderMeleeWeapon(translate, translateMap, locale.join, getInstanceById, locale.measurementAdjustments, renderMeleeDamage(translate), combatTechniqueId, use))
717
730
  : []),
718
731
  ...(combatValues?.type === "Weapon"
719
732
  ? Object.entries(combatValues.values.ranged_uses ?? {}).map(([combatTechniqueId, use]) => renderRangedWeapon(translate, translateMap, getInstanceById, locale.measurementAdjustments, renderRangedDamage(translate), combatTechniqueId, use))
@@ -2,6 +2,7 @@ import { on } from "@elyukai/utils/function";
2
2
  import { assertExhaustive } from "@elyukai/utils/typeSafety";
3
3
  import { createEntityDescriptionCreator } from "../creator.js";
4
4
  import { getEquipmentName } from "./equipment.js";
5
+ import { attributedInstance } from "./partial/markdown.js";
5
6
  const sumAtomicEquipmentCost = (acc, current) => {
6
7
  if (typeof acc === "string") {
7
8
  return acc;
@@ -280,7 +281,7 @@ export const getEquipmentPackageEntityDescription = createEntityDescriptionCreat
280
281
  header: [translation.name, translate("Weight"), translate("Cost")],
281
282
  rows: actualItems
282
283
  .map((item) => [
283
- getEquipmentName(translate, translateMap, getInstanceById, item.content),
284
+ attributedInstance(getEquipmentName(translate, translateMap, getInstanceById, item.content), item.content.entity, item.content.id, { context: '"equipment-package"' }),
284
285
  renderAtomicWeight(translate, locale.measurementAdjustments, getAtomicEquipmentWeight(item.content)),
285
286
  renderAtomicCost(translate, getAtomicEquipmentCost(item.content)),
286
287
  ])
@@ -3,6 +3,7 @@ import { assertExhaustive } from "@optolith/helpers/typeSafety";
3
3
  import { Case } from "tsondb/schema/gen";
4
4
  import { createEntityDescriptionCreator } from "../creator.js";
5
5
  import { renderEnhancements } from "./partial/enhancements.js";
6
+ import { attributedInstance, attributedName } from "./partial/markdown.js";
6
7
  import { renderOneTimeDuration } from "./partial/rated/activatable/duration.js";
7
8
  import { renderEffect } from "./partial/rated/activatable/effect.js";
8
9
  import { combineGeneratedTextWithStaticTranslation, renderFastPerformanceParameters, renderSlowPerformanceParameters, } from "./partial/rated/activatable/index.js";
@@ -13,7 +14,7 @@ import { renderImprovementCost } from "./partial/rated/improvementCost.js";
13
14
  import { renderSkillCheckWithPenalty } from "./partial/rated/skillCheck.js";
14
15
  import { ResponsiveTextSize } from "./partial/responsiveText.js";
15
16
  const getTextForTraditions = (deps, values) => {
16
- const getAspectName = (aspectId) => deps.translateMap(deps.getInstanceById("Aspect", aspectId)?.translations)?.name;
17
+ const getAspectName = (aspectId) => attributedName(deps.translateMap, deps.getInstanceById, "traditions", "Aspect", aspectId);
17
18
  const text = values
18
19
  .map(trad => {
19
20
  switch (trad.kind) {
@@ -25,14 +26,17 @@ const getTextForTraditions = (deps, values) => {
25
26
  if (name === undefined) {
26
27
  return undefined;
27
28
  }
29
+ const attributedStringName = attributedInstance(name, "BlessedTradition", trad.Tradition.tradition, {
30
+ context: '"traditions"',
31
+ });
28
32
  const aspects = trad.Tradition.aspects
29
33
  ?.map(getAspectName)
30
34
  .filter(isNotNullish)
31
35
  .sort(deps.localeCompare) ?? [];
32
36
  if (aspects.length === 0) {
33
- return name;
37
+ return attributedStringName;
34
38
  }
35
- return `${name} (${aspects.join(" and ")})`;
39
+ return `${attributedStringName} (${aspects.join(" and ")})`;
36
40
  }
37
41
  default:
38
42
  return assertExhaustive(trad);
@@ -2,18 +2,17 @@ import { isNotEmpty } from "@elyukai/utils/array/nonEmpty";
2
2
  import { mapNullable } from "@elyukai/utils/nullable";
3
3
  import { Reader } from "@elyukai/utils/reader";
4
4
  import { ensureNonEmpty } from "@optolith/helpers/array";
5
+ import { attributedCustomName } from "./markdown.js";
5
6
  import { parensIf } from "./rated/activatable/parensIf.js";
6
7
  import { MISSING_VALUE } from "./unknown.js";
7
8
  const renderCommonnessRatedAdvantageOrDisadvantageName = (translateMap, getInstanceById, entity, item) => {
8
- const instance = getInstanceById(entity, item.id);
9
- const instanceTranslation = translateMap(instance?.translations);
10
9
  const customTranslation = translateMap(item.translations);
11
- const name = instanceTranslation?.name_in_library ?? instanceTranslation?.name;
12
- return name !== undefined &&
13
- customTranslation?.options !== undefined &&
14
- name.endsWith(")")
15
- ? `${name.slice(0, -1)}; ${customTranslation.options})`
16
- : (name ?? MISSING_VALUE) + parensIf(customTranslation?.options);
10
+ return (attributedCustomName(translateMap, getInstanceById, entity, translation => {
11
+ const name = translation.name_in_library ?? translation.name;
12
+ return customTranslation?.options !== undefined && name.endsWith(")")
13
+ ? `${name.slice(0, -1)}; ${customTranslation.options})`
14
+ : name + parensIf(customTranslation?.options);
15
+ }, entity, item.id) ?? MISSING_VALUE);
17
16
  };
18
17
  /**
19
18
  * Render the names of either commonness-rated advantages or commonness-rated disadvantages.
@@ -27,10 +26,7 @@ export const renderCommonnessRatedAdvantagesOrDisadvantages = (entity, items) =>
27
26
  /**
28
27
  * Render the names of commonness-rated advantages and disadvantages together.
29
28
  */
30
- export const renderCommonnessRatedAdvantagesAndDisadvantages = (advantages, disadvantages) => Reader.asks(({ translate, translateMap, getInstanceById, localeCompare }) => mapNullable(ensureNonEmpty([
31
- ["Advantage", advantages],
32
- ["Disadvantage", disadvantages],
33
- ].flatMap(([entity, items]) => items?.map(item => renderCommonnessRatedAdvantageOrDisadvantageName(translateMap, getInstanceById, entity, item)) ?? [])), renderedItems => renderedItems.toSorted(localeCompare).join(", ")) ?? translate("none"));
29
+ export const renderCommonnessRatedAdvantagesAndDisadvantages = (advantages, disadvantages) => Reader.asks(({ translate, translateMap, getInstanceById, localeCompare }) => mapNullable(ensureNonEmpty([["Advantage", advantages], ["Disadvantage", disadvantages]].flatMap(([entity, items]) => items?.map(item => renderCommonnessRatedAdvantageOrDisadvantageName(translateMap, getInstanceById, entity, item)) ?? [])), renderedItems => renderedItems.toSorted(localeCompare).join(", ")) ?? translate("none"));
34
30
  /**
35
31
  * Render a value with a possible translation, falling back to explicity rendering the value if no translation is available.
36
32
  */
@@ -1,6 +1,7 @@
1
1
  import { isNotEmpty } from "@elyukai/utils/array/nonEmpty";
2
2
  import { assertExhaustive } from "@elyukai/utils/typeSafety";
3
3
  import { renderDice } from "./dice.js";
4
+ import { attributedName } from "./markdown.js";
4
5
  import { parensIf } from "./rated/activatable/parensIf.js";
5
6
  import { MISSING_VALUE } from "./unknown.js";
6
7
  /**
@@ -24,16 +25,12 @@ export const renderLaboratoryLevel = (translate, level) => {
24
25
  export const renderResistance = (translate, translateMap, getInstanceById, idMap, resistance) => {
25
26
  switch (resistance.kind) {
26
27
  case "Spirit":
27
- return (translateMap(getInstanceById("DerivedCharacteristic", idMap.DerivedCharacteristic.Spirit)
28
- ?.translations)?.name ?? MISSING_VALUE);
28
+ return (attributedName(translateMap, getInstanceById, "resistance", "DerivedCharacteristic", idMap.DerivedCharacteristic.Spirit) ?? MISSING_VALUE);
29
29
  case "Toughness":
30
- return (translateMap(getInstanceById("DerivedCharacteristic", idMap.DerivedCharacteristic.Toughness)
31
- ?.translations)?.name ?? MISSING_VALUE);
30
+ return (attributedName(translateMap, getInstanceById, "resistance", "DerivedCharacteristic", idMap.DerivedCharacteristic.Toughness) ?? MISSING_VALUE);
32
31
  case "LowerOfSpiritAndToughness": {
33
- const spiritTranslation = translateMap(getInstanceById("DerivedCharacteristic", idMap.DerivedCharacteristic.Spirit)
34
- ?.translations)?.name ?? MISSING_VALUE;
35
- const toughnessTranslation = translateMap(getInstanceById("DerivedCharacteristic", idMap.DerivedCharacteristic.Toughness)
36
- ?.translations)?.name ?? MISSING_VALUE;
32
+ const spiritTranslation = attributedName(translateMap, getInstanceById, "resistance", "DerivedCharacteristic", idMap.DerivedCharacteristic.Spirit) ?? MISSING_VALUE;
33
+ const toughnessTranslation = attributedName(translateMap, getInstanceById, "resistance", "DerivedCharacteristic", idMap.DerivedCharacteristic.Toughness) ?? MISSING_VALUE;
37
34
  return translate("{$first} or {$second}, depending on which value is lower", {
38
35
  first: spiritTranslation,
39
36
  second: toughnessTranslation,