@optolith/entity-descriptions 0.4.8 → 0.4.10

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 (28) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/lib/entities/activatable.js +16 -13
  3. package/lib/entities/curriculum.js +14 -21
  4. package/lib/entities/derivedCharacteristic.js +5 -7
  5. package/lib/entities/equipmentPackage.js +2 -1
  6. package/lib/entities/liturgicalChant.js +7 -3
  7. package/lib/entities/partial/commonnessRatedAdvantagesAndDisadvantages.js +8 -12
  8. package/lib/entities/partial/herbary.js +5 -8
  9. package/lib/entities/partial/markdown.d.ts +55 -0
  10. package/lib/entities/partial/markdown.js +59 -0
  11. package/lib/entities/partial/prerequisites/index.js +2 -2
  12. package/lib/entities/partial/prerequisites/part.js +8 -3
  13. package/lib/entities/partial/prerequisites/single/culture.js +2 -1
  14. package/lib/entities/partial/prerequisites/single/personalityTrait.js +2 -1
  15. package/lib/entities/partial/prerequisites/single/profession.js +4 -1
  16. package/lib/entities/partial/prerequisites/single/race.js +2 -1
  17. package/lib/entities/partial/prerequisites/single/rated.js +2 -27
  18. package/lib/entities/partial/prerequisites/single/ratedMinimumNumber.js +2 -1
  19. package/lib/entities/partial/prerequisites/single/ratedSum.js +2 -1
  20. package/lib/entities/partial/prerequisites/single/state.js +2 -1
  21. package/lib/entities/partial/reader.d.ts +33 -0
  22. package/lib/entities/partial/reader.js +13 -0
  23. package/lib/entities/personalityTrait.js +9 -2
  24. package/lib/entities/profession.js +25 -25
  25. package/lib/entities/race.js +4 -6
  26. package/lib/entities/spell.js +13 -25
  27. package/lib/tsconfig.tsbuildinfo +1 -1
  28. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
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.10](https://github.com/Optolith/entity-descriptions/compare/v0.4.9...v0.4.10) (2026-03-17)
6
+
7
+ ## [0.4.9](https://github.com/Optolith/entity-descriptions/compare/v0.4.8...v0.4.9) (2026-03-17)
8
+
9
+
10
+ ### Features
11
+
12
+ * mark generated instances with attributed strings ([85cce91](https://github.com/Optolith/entity-descriptions/commit/85cce917d16ac10833d4dccf7031c2dca4e75d26))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * always append level to activatable name ([10ca82a](https://github.com/Optolith/entity-descriptions/commit/10ca82a1e57afd2d11ba7c32b55a49172e676398))
18
+ * require previous level and fix function typo ([2b12a01](https://github.com/Optolith/entity-descriptions/commit/2b12a01beaaf786843edbbcf42def3d1b1ecb673))
19
+
5
20
  ## [0.4.8](https://github.com/Optolith/entity-descriptions/compare/v0.4.7...v0.4.8) (2026-03-15)
6
21
 
7
22
 
@@ -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
@@ -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,21 +1,19 @@
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
  };
16
+ const getAttribute = (getInstanceById, translateMap, attributeId, style = "full") => attributedCustomName(translateMap, getInstanceById, "derived-characteristic-calculation", translation => renderAttributeNameForStyle(style, translation), "Attribute", attributeId) ?? MISSING_VALUE;
19
17
  const getRaceBaseValue = (translate, translateMap, getInstanceById, idMap, raceBaseValue, style = "full") => {
20
18
  const instance = getInstanceById("DerivedCharacteristic", idMap.DerivedCharacteristic[raceBaseValue.kind]);
21
19
  const translation = translateMap(instance?.translations);
@@ -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,
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Some helpers to create Markdown content.
3
+ */
4
+ import type { EntityMap } from "optolith-database-schema/gen";
5
+ import { type IdArgsVariant } from "tsondb/schema/gen";
6
+ import type { GetInstanceById } from "../../helpers/getTypes.js";
7
+ import type { LocaleMap, TranslateMap } from "../../helpers/translate.js";
8
+ /**
9
+ * Creates an attributed string in Markdown format.
10
+ */
11
+ export declare const attributedString: (text: string, rawAttributes: Partial<Record<string, string>>) => string;
12
+ /**
13
+ * Display an instance of an entity in Markdown format.
14
+ */
15
+ export declare const attributedInstance: (name: string, entity: string, id?: string, other?: Record<string, string>) => string;
16
+ /**
17
+ * Renders the name of an instance in an attributed string.
18
+ */
19
+ export declare const attributedName: <E extends { [K in keyof EntityMap]: EntityMap[K] extends {
20
+ translations: LocaleMap<{
21
+ name: string;
22
+ }>;
23
+ } ? K : never; }[keyof EntityMap]>(translateMap: TranslateMap, getInstanceById: GetInstanceById<E>, context: string, ...args: IdArgsVariant<EntityMap, E>) => string | undefined;
24
+ /**
25
+ * Renders the name of an instance according to a custom function.
26
+ */
27
+ export declare const customName: <E extends { [K in keyof EntityMap]: EntityMap[K] extends {
28
+ translations: LocaleMap<object>;
29
+ } ? K : never; }[keyof EntityMap]>(translateMap: TranslateMap, getInstanceById: GetInstanceById<E>, fn: (translation: EntityMap[E]["translations"][string]) => string, ...args: IdArgsVariant<EntityMap, E>) => string | undefined;
30
+ /**
31
+ * Renders the name of an instance according to a custom function in an attributed string.
32
+ */
33
+ export declare const attributedCustomName: <E extends { [K in keyof EntityMap]: EntityMap[K] extends {
34
+ translations: LocaleMap<object>;
35
+ } ? K : never; }[keyof EntityMap]>(translateMap: TranslateMap, getInstanceById: GetInstanceById<E>, context: string, fn: (translation: EntityMap[E]["translations"][string]) => string, ...args: IdArgsVariant<EntityMap, E>) => string | undefined;
36
+ /**
37
+ * Renders the name of an instance in an attributed string.
38
+ */
39
+ export declare const attributedNameFromInstance: (translateMap: TranslateMap, instance: {
40
+ translations: LocaleMap<{
41
+ name: string;
42
+ }>;
43
+ } | undefined, context: string, ...args: IdArgsVariant<EntityMap, keyof EntityMap>) => string | undefined;
44
+ /**
45
+ * Renders the name of an instance in an attributed string.
46
+ */
47
+ export declare const attributedNameFromTranslation: (translation: {
48
+ name: string;
49
+ } | undefined, context: string, ...args: IdArgsVariant<EntityMap, keyof EntityMap>) => string | undefined;
50
+ /**
51
+ * Renders the name of an instance in an attributed string.
52
+ */
53
+ export declare const attributedNameFromSafeTranslation: (translation: {
54
+ name: string;
55
+ }, context: string, ...args: IdArgsVariant<EntityMap, keyof EntityMap>) => string;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Some helpers to create Markdown content.
3
+ */
4
+ import { mapNullable } from "@elyukai/utils/nullable";
5
+ import { normalizedIdArgs } from "tsondb/schema/gen";
6
+ /**
7
+ * Creates an attributed string in Markdown format.
8
+ */
9
+ export const attributedString = (text, rawAttributes) => `^[${text}](${Object.entries(rawAttributes)
10
+ .filter((pair) => pair[1] !== undefined)
11
+ .map(([key, value]) => `${key}: ${value}`)
12
+ .join(", ")})`;
13
+ /**
14
+ * Display an instance of an entity in Markdown format.
15
+ */
16
+ export const attributedInstance = (name, entity, id, other) => attributedString(name, {
17
+ ...other,
18
+ entity: `"${entity}"`,
19
+ instance: id === undefined ? undefined : `"${id}"`,
20
+ });
21
+ /**
22
+ * Renders the name of an instance in an attributed string.
23
+ */
24
+ export const attributedName = (translateMap, getInstanceById, context, ...args) => {
25
+ const id = normalizedIdArgs(args);
26
+ return mapNullable(translateMap(getInstanceById(...args)?.translations)?.name, name => attributedInstance(name, id.entityName, id.id, { context: `"${context}"` }));
27
+ };
28
+ /**
29
+ * Renders the name of an instance according to a custom function.
30
+ */
31
+ export const customName = (translateMap, getInstanceById, fn, ...args) => mapNullable(translateMap(getInstanceById(...args)?.translations), translation => fn(translation));
32
+ /**
33
+ * Renders the name of an instance according to a custom function in an attributed string.
34
+ */
35
+ export const attributedCustomName = (translateMap, getInstanceById, context, fn, ...args) => {
36
+ const id = normalizedIdArgs(args);
37
+ return mapNullable(translateMap(getInstanceById(...args)?.translations), translation => attributedInstance(fn(translation), id.entityName, id.id, { context: `"${context}"` }));
38
+ };
39
+ /**
40
+ * Renders the name of an instance in an attributed string.
41
+ */
42
+ export const attributedNameFromInstance = (translateMap, instance, context, ...args) => {
43
+ const id = normalizedIdArgs(args);
44
+ return mapNullable(translateMap(instance?.translations)?.name, name => attributedInstance(name, id.entityName, id.id, { context: `"${context}"` }));
45
+ };
46
+ /**
47
+ * Renders the name of an instance in an attributed string.
48
+ */
49
+ export const attributedNameFromTranslation = (translation, context, ...args) => {
50
+ const id = normalizedIdArgs(args);
51
+ return mapNullable(translation?.name, name => attributedInstance(name, id.entityName, id.id, { context: `"${context}"` }));
52
+ };
53
+ /**
54
+ * Renders the name of an instance in an attributed string.
55
+ */
56
+ export const attributedNameFromSafeTranslation = (translation, context, ...args) => {
57
+ const id = normalizedIdArgs(args);
58
+ return attributedInstance(translation.name, id.entityName, id.id, { context: `"${context}"` });
59
+ };
@@ -85,7 +85,7 @@ const printPrerequisitesForLevels = (printPrerequisite, locale, value, printPrev
85
85
  level: i + 2,
86
86
  prerequisite: {
87
87
  kind: "Single",
88
- Single: printPreviousLevelPrerequisites.createPreerequisite(i + 2),
88
+ Single: printPreviousLevelPrerequisites.createPrerequisite(i + 1),
89
89
  },
90
90
  }));
91
91
  const groupedByLevel = Map.groupBy([...value, ...previousLevelPrerequisites], prerequisite => prerequisite.level);
@@ -153,7 +153,7 @@ export const printGeneralPrerequisites = (getInstanceById, getResolvedSelectOpti
153
153
  ? undefined
154
154
  : {
155
155
  levels: printPreviousLevelPrerequisites.levels,
156
- createPreerequisite: (level) => ({
156
+ createPrerequisite: (level) => ({
157
157
  kind: "Activatable",
158
158
  Activatable: {
159
159
  id: printPreviousLevelPrerequisites.id,
@@ -5,6 +5,7 @@ import { compareNumber, reduceCompare } from "@elyukai/utils/ordering";
5
5
  import { assertExhaustive } from "@optolith/helpers/typeSafety";
6
6
  import { fromUniformCase } from "tsondb/schema/gen";
7
7
  import { renderActivatableNameComponents, renderActivatableNameComponentsCombinedIfPossible, } from "../activatableNameChunks.js";
8
+ import { attributedInstance } from "../markdown.js";
8
9
  /**
9
10
  * Type guard for prerequisite parts with a value object.
10
11
  */
@@ -18,15 +19,19 @@ const countFurtherToCombined = (part, remainingParts) => {
18
19
  remainingParts.length
19
20
  : indexHasDifferentBaseValues;
20
21
  };
22
+ const wrapActivatableInAttributedString = (text, id) => attributedInstance(text, id.kind, fromUniformCase(id), { context: '"prerequisite"' });
21
23
  const joinAdjacentParts = (translateMap, localeCompare, part, remainingParts) => {
22
24
  const valuesWithSameBase = remainingParts.slice(0, countFurtherToCombined(part, remainingParts));
23
25
  if (valuesWithSameBase.length > 0) {
24
26
  return [
25
- renderActivatableNameComponentsCombinedIfPossible(translateMap, [part.value, ...valuesWithSameBase.map(p => p.value)], true, list => list.toSorted(localeCompare).join(", ")),
27
+ wrapActivatableInAttributedString(renderActivatableNameComponentsCombinedIfPossible(translateMap, [part.value, ...valuesWithSameBase.map(p => p.value)], true, list => list.toSorted(localeCompare).join(", ")), part.value.id),
26
28
  valuesWithSameBase.length,
27
29
  ];
28
30
  }
29
- return [renderActivatableNameComponents(translateMap, part.value, true), 0];
31
+ return [
32
+ wrapActivatableInAttributedString(renderActivatableNameComponents(translateMap, part.value, true), part.value.id),
33
+ 0,
34
+ ];
30
35
  };
31
36
  const appendBySentenceType = (previouslyRendered, currentRendered, sentenceType, isLast) => {
32
37
  switch (sentenceType?.kind) {
@@ -76,7 +81,7 @@ const appendPrerequisitePartGroup = (previous, translateMap, localeCompare, part
76
81
  return parts.reduce((acc, current, i, arr) => appendBySentenceType(acc, (current.label ?? "") +
77
82
  (typeof current.value === "string"
78
83
  ? current.value
79
- : renderActivatableNameComponents(translateMap, current.value, true)), current.sentenceType, isLast && i === arr.length - 1), previous);
84
+ : wrapActivatableInAttributedString(renderActivatableNameComponents(translateMap, current.value, true), current.value.id)), current.sentenceType, isLast && i === arr.length - 1), previous);
80
85
  }
81
86
  };
82
87
  /**
@@ -1,3 +1,4 @@
1
+ import { attributedNameFromSafeTranslation } from "../../markdown.js";
1
2
  import { printDisplayOption } from "../displayOption.js";
2
3
  /**
3
4
  * Get the translation of a culture prerequisite.
@@ -13,7 +14,7 @@ export const printCulturePrerequisite = (getInstanceById, locale, prerequisite)
13
14
  }
14
15
  return {
15
16
  label: `${locale.translate("Culture")} `,
16
- value: cultureTranslation.name,
17
+ value: attributedNameFromSafeTranslation(cultureTranslation, "prerequisite", "Culture", prerequisite.id),
17
18
  sentenceType: undefined,
18
19
  isMeta: false,
19
20
  };
@@ -1,3 +1,4 @@
1
+ import { attributedNameFromSafeTranslation } from "../../markdown.js";
1
2
  import { printDisplayOption } from "../displayOption.js";
2
3
  /**
3
4
  * Get the translation of a personality trait prerequisite.
@@ -11,7 +12,7 @@ export const printPersonalityTraitPrerequisite = (getInstanceById, locale, prere
11
12
  if (personalityTrait === undefined || personalityTraitTranslation === undefined) {
12
13
  return undefined;
13
14
  }
14
- const name = `${personalityTraitTranslation.name} (${locale.translate("Level {$level}", {
15
+ const name = `${attributedNameFromSafeTranslation(personalityTraitTranslation, "prerequisite", "PersonalityTrait", prerequisite.id)} (${locale.translate("Level {$level}", {
15
16
  level: personalityTrait.level,
16
17
  })})`;
17
18
  return {
@@ -1,3 +1,4 @@
1
+ import { attributedInstance } from "../../markdown.js";
1
2
  import { getProfessionName } from "../../professions.js";
2
3
  import { printDisplayOption } from "../displayOption.js";
3
4
  /**
@@ -12,7 +13,9 @@ export const printProfessionPrerequisite = (getChildInstancesForInstanceId, loca
12
13
  return undefined;
13
14
  }
14
15
  return {
15
- value: professionName,
16
+ value: attributedInstance(professionName, "Profession", prerequisite.id, {
17
+ context: '"prerequisite"',
18
+ }),
16
19
  sentenceType: undefined,
17
20
  isMeta: false,
18
21
  };
@@ -1,3 +1,4 @@
1
+ import { attributedNameFromSafeTranslation } from "../../markdown.js";
1
2
  import { printDisplayOption } from "../displayOption.js";
2
3
  /**
3
4
  * Get the translation of a race prerequisite.
@@ -13,7 +14,7 @@ export const printRacePrerequisite = (getInstanceById, locale, prerequisite) =>
13
14
  }
14
15
  return {
15
16
  label: `${locale.translate("Race")} `,
16
- value: raceTranslation.name,
17
+ value: attributedNameFromSafeTranslation(raceTranslation, "prerequisite", "Race", prerequisite.id),
17
18
  sentenceType: undefined,
18
19
  isMeta: false,
19
20
  };