@optolith/entity-descriptions 0.2.0 → 0.3.1

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 (187) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/lib/creator.d.ts +32 -0
  3. package/lib/creator.js +72 -0
  4. package/lib/entities/activatable.d.ts +52 -0
  5. package/lib/entities/activatable.js +609 -0
  6. package/lib/entities/alternativeRule.d.ts +7 -0
  7. package/lib/entities/alternativeRule.js +21 -0
  8. package/lib/entities/attribute.d.ts +6 -0
  9. package/lib/entities/attribute.js +15 -0
  10. package/lib/entities/combatTechnique.d.ts +5 -7
  11. package/lib/entities/combatTechnique.js +34 -24
  12. package/lib/entities/condition.d.ts +12 -0
  13. package/lib/entities/condition.js +63 -0
  14. package/lib/entities/culture.d.ts +8 -0
  15. package/lib/entities/culture.js +309 -0
  16. package/lib/entities/curriculum.d.ts +11 -0
  17. package/lib/entities/curriculum.js +266 -0
  18. package/lib/entities/derivedCharacteristic.d.ts +9 -0
  19. package/lib/entities/derivedCharacteristic.js +91 -0
  20. package/lib/entities/disease.d.ts +9 -0
  21. package/lib/entities/disease.js +88 -0
  22. package/lib/entities/elixir.d.ts +10 -0
  23. package/lib/entities/elixir.js +76 -0
  24. package/lib/entities/equipment.d.ts +24 -0
  25. package/lib/entities/equipment.js +613 -0
  26. package/lib/entities/equipmentPackage.d.ts +8 -0
  27. package/lib/entities/equipmentPackage.js +301 -0
  28. package/lib/entities/experienceLevel.d.ts +3 -2
  29. package/lib/entities/experienceLevel.js +33 -28
  30. package/lib/entities/focusRule.d.ts +3 -4
  31. package/lib/entities/focusRule.js +13 -5
  32. package/lib/entities/influence.d.ts +7 -0
  33. package/lib/entities/influence.js +35 -0
  34. package/lib/entities/liturgicalChant.d.ts +10 -23
  35. package/lib/entities/liturgicalChant.js +169 -131
  36. package/lib/entities/optionalRule.d.ts +3 -2
  37. package/lib/entities/optionalRule.js +4 -3
  38. package/lib/entities/partial/activatableNameChunks.d.ts +58 -0
  39. package/lib/entities/partial/activatableNameChunks.js +356 -0
  40. package/lib/entities/partial/adventurePointsValue.d.ts +9 -0
  41. package/lib/entities/partial/adventurePointsValue.js +243 -0
  42. package/lib/entities/partial/animalTypes.d.ts +11 -0
  43. package/lib/entities/partial/animalTypes.js +13 -0
  44. package/lib/entities/partial/commonnessRatedAdvantagesAndDisadvantages.d.ts +16 -0
  45. package/lib/entities/partial/commonnessRatedAdvantagesAndDisadvantages.js +40 -0
  46. package/lib/entities/partial/dice.d.ts +10 -0
  47. package/lib/entities/partial/dice.js +13 -0
  48. package/lib/entities/partial/herbary.d.ts +28 -0
  49. package/lib/entities/partial/herbary.js +65 -0
  50. package/lib/entities/partial/map.d.ts +49 -0
  51. package/lib/entities/partial/map.js +43 -0
  52. package/lib/entities/partial/mathOperation.d.ts +36 -0
  53. package/lib/entities/partial/mathOperation.js +107 -0
  54. package/lib/entities/partial/prerequisites/displayOption.d.ts +7 -0
  55. package/lib/entities/partial/prerequisites/displayOption.js +19 -0
  56. package/lib/entities/partial/prerequisites/index.d.ts +67 -0
  57. package/lib/entities/partial/prerequisites/index.js +189 -0
  58. package/lib/entities/partial/prerequisites/part.d.ts +26 -0
  59. package/lib/entities/partial/prerequisites/part.js +88 -0
  60. package/lib/entities/partial/prerequisites/prerequisiteGroups.d.ts +65 -0
  61. package/lib/entities/partial/prerequisites/prerequisiteGroups.js +269 -0
  62. package/lib/entities/partial/prerequisites/single/activatable.d.ts +18 -0
  63. package/lib/entities/partial/prerequisites/single/activatable.js +40 -0
  64. package/lib/entities/partial/prerequisites/single/animistPower.d.ts +8 -0
  65. package/lib/entities/partial/prerequisites/single/animistPower.js +23 -0
  66. package/lib/entities/partial/prerequisites/single/blessedTradition.d.ts +7 -0
  67. package/lib/entities/partial/prerequisites/single/blessedTradition.js +32 -0
  68. package/lib/entities/partial/prerequisites/single/commonSuggestedByRCP.d.ts +6 -0
  69. package/lib/entities/partial/prerequisites/single/commonSuggestedByRCP.js +19 -0
  70. package/lib/entities/partial/prerequisites/single/culture.d.ts +8 -0
  71. package/lib/entities/partial/prerequisites/single/culture.js +20 -0
  72. package/lib/entities/partial/prerequisites/single/enhancement.d.ts +8 -0
  73. package/lib/entities/partial/prerequisites/single/enhancement.js +41 -0
  74. package/lib/entities/partial/prerequisites/single/influence.d.ts +8 -0
  75. package/lib/entities/partial/prerequisites/single/influence.js +17 -0
  76. package/lib/entities/partial/prerequisites/single/magicalTradition.d.ts +7 -0
  77. package/lib/entities/partial/prerequisites/single/magicalTradition.js +27 -0
  78. package/lib/entities/partial/prerequisites/single/noOtherAncestorBloodAdvantage.d.ts +6 -0
  79. package/lib/entities/partial/prerequisites/single/noOtherAncestorBloodAdvantage.js +8 -0
  80. package/lib/entities/partial/prerequisites/single/pact.d.ts +8 -0
  81. package/lib/entities/partial/prerequisites/single/pact.js +30 -0
  82. package/lib/entities/partial/prerequisites/single/personalityTrait.d.ts +8 -0
  83. package/lib/entities/partial/prerequisites/single/personalityTrait.js +28 -0
  84. package/lib/entities/partial/prerequisites/single/primaryAttribute.d.ts +7 -0
  85. package/lib/entities/partial/prerequisites/single/primaryAttribute.js +15 -0
  86. package/lib/entities/partial/prerequisites/single/publication.d.ts +8 -0
  87. package/lib/entities/partial/prerequisites/single/publication.js +19 -0
  88. package/lib/entities/partial/prerequisites/single/race.d.ts +8 -0
  89. package/lib/entities/partial/prerequisites/single/race.js +20 -0
  90. package/lib/entities/partial/prerequisites/single/rated.d.ts +8 -0
  91. package/lib/entities/partial/prerequisites/single/rated.js +41 -0
  92. package/lib/entities/partial/prerequisites/single/ratedMinimumNumber.d.ts +8 -0
  93. package/lib/entities/partial/prerequisites/single/ratedMinimumNumber.js +89 -0
  94. package/lib/entities/partial/prerequisites/single/ratedSum.d.ts +8 -0
  95. package/lib/entities/partial/prerequisites/single/ratedSum.js +21 -0
  96. package/lib/entities/partial/prerequisites/single/rule.d.ts +7 -0
  97. package/lib/entities/partial/prerequisites/single/rule.js +4 -0
  98. package/lib/entities/partial/prerequisites/single/sex.d.ts +7 -0
  99. package/lib/entities/partial/prerequisites/single/sex.js +19 -0
  100. package/lib/entities/partial/prerequisites/single/sexualCharacteristic.d.ts +7 -0
  101. package/lib/entities/partial/prerequisites/single/sexualCharacteristic.js +21 -0
  102. package/lib/entities/partial/prerequisites/single/socialStatus.d.ts +8 -0
  103. package/lib/entities/partial/prerequisites/single/socialStatus.js +21 -0
  104. package/lib/entities/partial/prerequisites/single/state.d.ts +8 -0
  105. package/lib/entities/partial/prerequisites/single/state.js +20 -0
  106. package/lib/entities/partial/prerequisites/single/text.d.ts +7 -0
  107. package/lib/entities/partial/prerequisites/single/text.js +9 -0
  108. package/lib/entities/partial/professions.d.ts +15 -0
  109. package/lib/entities/partial/professions.js +19 -0
  110. package/lib/entities/partial/rated/activatable/castingTime.d.ts +16 -7
  111. package/lib/entities/partial/rated/activatable/castingTime.js +35 -20
  112. package/lib/entities/partial/rated/activatable/checkResultBased.d.ts +18 -3
  113. package/lib/entities/partial/rated/activatable/checkResultBased.js +18 -10
  114. package/lib/entities/partial/rated/activatable/cost.d.ts +88 -8
  115. package/lib/entities/partial/rated/activatable/cost.js +183 -115
  116. package/lib/entities/partial/rated/activatable/duration.d.ts +36 -13
  117. package/lib/entities/partial/rated/activatable/duration.js +72 -61
  118. package/lib/entities/partial/rated/activatable/effect.d.ts +4 -4
  119. package/lib/entities/partial/rated/activatable/effect.js +32 -25
  120. package/lib/entities/partial/rated/activatable/index.d.ts +31 -27
  121. package/lib/entities/partial/rated/activatable/index.js +63 -28
  122. package/lib/entities/partial/rated/activatable/isMinimumMaximum.d.ts +5 -6
  123. package/lib/entities/partial/rated/activatable/isMinimumMaximum.js +8 -5
  124. package/lib/entities/partial/rated/activatable/nonModifiableSuffix.d.ts +2 -4
  125. package/lib/entities/partial/rated/activatable/nonModifiableSuffix.js +7 -42
  126. package/lib/entities/partial/rated/activatable/range.d.ts +22 -14
  127. package/lib/entities/partial/rated/activatable/range.js +54 -54
  128. package/lib/entities/partial/rated/activatable/speed.d.ts +11 -2
  129. package/lib/entities/partial/rated/activatable/speed.js +14 -1
  130. package/lib/entities/partial/rated/activatable/targetCategory.d.ts +4 -5
  131. package/lib/entities/partial/rated/activatable/targetCategory.js +19 -24
  132. package/lib/entities/partial/rated/improvementCost.d.ts +16 -4
  133. package/lib/entities/partial/rated/improvementCost.js +63 -4
  134. package/lib/entities/partial/rated/skillCheck.d.ts +20 -17
  135. package/lib/entities/partial/rated/skillCheck.js +56 -54
  136. package/lib/entities/partial/reader.d.ts +266 -0
  137. package/lib/entities/partial/reader.js +175 -0
  138. package/lib/entities/partial/responsiveText.d.ts +10 -5
  139. package/lib/entities/partial/responsiveText.js +19 -3
  140. package/lib/entities/partial/units/energy.d.ts +5 -8
  141. package/lib/entities/partial/units/energy.js +5 -23
  142. package/lib/entities/partial/units/length.d.ts +20 -2
  143. package/lib/entities/partial/units/length.js +24 -5
  144. package/lib/entities/partial/units/timeSpan.d.ts +25 -4
  145. package/lib/entities/partial/units/timeSpan.js +27 -15
  146. package/lib/entities/partial/unknown.d.ts +5 -1
  147. package/lib/entities/partial/unknown.js +5 -1
  148. package/lib/entities/personalityTrait.d.ts +7 -0
  149. package/lib/entities/personalityTrait.js +56 -0
  150. package/lib/entities/poison.d.ts +12 -0
  151. package/lib/entities/poison.js +356 -0
  152. package/lib/entities/profession.d.ts +12 -0
  153. package/lib/entities/profession.js +585 -0
  154. package/lib/entities/race.d.ts +9 -0
  155. package/lib/entities/race.js +146 -0
  156. package/lib/entities/sexPractice.d.ts +6 -0
  157. package/lib/entities/sexPractice.js +33 -0
  158. package/lib/entities/skill.d.ts +9 -9
  159. package/lib/entities/skill.js +124 -91
  160. package/lib/entities/spell.d.ts +83 -26
  161. package/lib/entities/spell.js +835 -147
  162. package/lib/entities/state.d.ts +6 -0
  163. package/lib/entities/state.js +17 -0
  164. package/lib/helpers/enums.d.ts +11 -0
  165. package/lib/helpers/enums.js +6 -0
  166. package/lib/helpers/getTypes.d.ts +12 -482
  167. package/lib/helpers/getTypes.js +1 -0
  168. package/lib/helpers/identifiers.d.ts +314 -0
  169. package/lib/helpers/identifiers.js +333 -0
  170. package/lib/helpers/locale.d.ts +21 -2
  171. package/lib/helpers/translate.d.ts +47 -5
  172. package/lib/helpers/translate.js +13 -6
  173. package/lib/index.d.ts +848 -21
  174. package/lib/index.js +182 -17
  175. package/lib/references/index.d.ts +6 -3
  176. package/lib/references/index.js +25 -33
  177. package/lib/references/page.d.ts +1 -1
  178. package/lib/references/page.js +14 -14
  179. package/lib/references/pageRange.d.ts +1 -5
  180. package/lib/references/pageRange.js +7 -16
  181. package/lib/tsconfig.tsbuildinfo +1 -0
  182. package/package.json +30 -10
  183. package/.prettierrc.yml +0 -1
  184. package/lib/entities/partial/rated/activatable/entity.d.ts +0 -11
  185. package/lib/entities/partial/rated/activatable/entity.js +0 -12
  186. package/lib/references/occurrence.d.ts +0 -4
  187. package/lib/references/occurrence.js +0 -3
@@ -0,0 +1,613 @@
1
+ import { ensureNonEmpty, isNotEmpty } from "@elyukai/utils/array/nonEmpty";
2
+ import { isNotNullish } from "@elyukai/utils/nullable";
3
+ import { sign } from "@elyukai/utils/string/number";
4
+ import { assertExhaustive } from "@elyukai/utils/typeSafety";
5
+ import { mapNullable } from "@optolith/helpers/nullable";
6
+ import { createEntityDescriptionCreator, } from "../creator.js";
7
+ import { renderDice, renderDiceAndFlat } from "./partial/dice.js";
8
+ import { additionFormatter, subtractionFormatter, } from "./partial/mathOperation.js";
9
+ import { parensIf } from "./partial/rated/activatable/parensIf.js";
10
+ import { ResponsiveTextSize } from "./partial/responsiveText.js";
11
+ import { formatTimeSpan } from "./partial/units/timeSpan.js";
12
+ import { MISSING_VALUE, UNHANDLED_VALUE } from "./partial/unknown.js";
13
+ /**
14
+ * Get the name of an equipment item.
15
+ */
16
+ export const getEquipmentName = (translate, translateMap, getInstanceById, entry) => {
17
+ switch (entry.entity) {
18
+ case "ClothingPackage": {
19
+ const socialStatusName = translateMap(getInstanceById("SocialStatus", entry.content.socialStatus)
20
+ ?.translations)?.name ?? MISSING_VALUE;
21
+ return translate("Clothing Package {$socialStatus}", {
22
+ socialStatus: socialStatusName,
23
+ });
24
+ }
25
+ case "Ammunition":
26
+ case "Animal":
27
+ case "AnimalCare":
28
+ case "Armor":
29
+ case "BandageOrRemedy":
30
+ case "Book":
31
+ case "CeremonialItem":
32
+ case "Clothes":
33
+ case "Container":
34
+ case "Elixir":
35
+ case "EquipmentOfBlessedOnes":
36
+ case "GemOrPreciousStone":
37
+ case "IlluminationLightSource":
38
+ case "IlluminationRefillOrSupply":
39
+ case "Jewelry":
40
+ case "Laboratory":
41
+ case "Liebesspielzeug":
42
+ case "LuxuryGood":
43
+ case "MagicalArtifact":
44
+ case "MusicalInstrument":
45
+ case "Newspaper":
46
+ case "OrienteeringAid":
47
+ case "Poison":
48
+ case "RopeOrChain":
49
+ case "Stationery":
50
+ case "ThievesTool":
51
+ case "ToolOfTheTrade":
52
+ case "TravelGearOrTool":
53
+ case "Vehicle":
54
+ case "Weapon":
55
+ case "WeaponAccessory":
56
+ return (translateMap(entry.content.translations)?.name ??
57
+ MISSING_VALUE);
58
+ default:
59
+ return assertExhaustive(entry);
60
+ }
61
+ };
62
+ const renderPrimaryAttributeAndDamageThreshold = (translateMap, getInstanceById, closeCombatTechnique, damageThreshold) => {
63
+ if (damageThreshold === undefined) {
64
+ return "—";
65
+ }
66
+ const getAttrAbbrv = (attrId) => translateMap(getInstanceById("Attribute", attrId)?.translations)
67
+ ?.abbreviation ?? MISSING_VALUE;
68
+ switch (damageThreshold.kind) {
69
+ case "Default":
70
+ return `${closeCombatTechnique?.primary_attribute.map(getAttrAbbrv).join("/") ??
71
+ MISSING_VALUE} ${damageThreshold.Default.threshold}`;
72
+ case "List":
73
+ if (isNotEmpty(damageThreshold.List.list)) {
74
+ const { list } = damageThreshold.List;
75
+ if (list.some(item => item.threshold !== list[0].threshold)) {
76
+ return list
77
+ .map(item => `${getAttrAbbrv(item.attribute)} ${item.threshold}`)
78
+ .join("/");
79
+ }
80
+ else {
81
+ return `${list
82
+ .map(item => getAttrAbbrv(item.attribute))
83
+ .join("/")} ${list[0].threshold}`;
84
+ }
85
+ }
86
+ else {
87
+ return MISSING_VALUE;
88
+ }
89
+ default:
90
+ return assertExhaustive(damageThreshold);
91
+ }
92
+ };
93
+ const renderAttackParryModifier = (attackModifier, parryModifier) => attackModifier === undefined && parryModifier === undefined
94
+ ? "—"
95
+ : `${attackModifier === undefined ? "—" : sign(attackModifier)}/${parryModifier === undefined ? "—" : sign(parryModifier)}`;
96
+ const renderReach = (translateMap, getInstanceById, reach) => reach === undefined
97
+ ? "—"
98
+ : (translateMap(getInstanceById("Reach", reach)?.translations)?.name ??
99
+ MISSING_VALUE);
100
+ const renderLength = (translate, measurements, length) => length === undefined
101
+ ? "—"
102
+ : translate(".input {$value :number} {{{$value} inches}}", {
103
+ value: length * measurements.halffingersMultiplier,
104
+ });
105
+ /**
106
+ * Render combat values of a melee weapon.
107
+ */
108
+ export const renderMeleeWeapon = (translate, translateMap, getInstanceById, measurements, renderDamage, closeCombatTechniqueId, use) => {
109
+ const combatTechnique = getInstanceById("CloseCombatTechnique", closeCombatTechniqueId);
110
+ const fields = combatTechnique?.special ?? {
111
+ can_parry: { kind: "Prohibited" },
112
+ has_damage_threshold: { kind: "Prohibited" },
113
+ has_reach: { kind: "Prohibited" },
114
+ has_length: { kind: "Prohibited" },
115
+ has_shield_size: { kind: "Prohibited" },
116
+ };
117
+ return {
118
+ type: "labeled",
119
+ label: translate("Combat Technique {$name}", {
120
+ name: translateMap(combatTechnique?.translations)?.name ?? MISSING_VALUE,
121
+ }),
122
+ value: {
123
+ type: "definitionList",
124
+ items: [
125
+ {
126
+ label: translate("Damage Points"),
127
+ value: renderDamage(use.damage),
128
+ },
129
+ fields.has_damage_threshold.kind === "Prohibited"
130
+ ? undefined
131
+ : {
132
+ label: translate("Primary Attribute + Damage Threshold"),
133
+ value: renderPrimaryAttributeAndDamageThreshold(translateMap, getInstanceById, combatTechnique, use.damage_threshold),
134
+ },
135
+ {
136
+ label: translate("Attack/Parry Modifier"),
137
+ value: renderAttackParryModifier(use.attackModifier, use.parryModifier),
138
+ },
139
+ fields.has_reach.kind === "Prohibited"
140
+ ? undefined
141
+ : {
142
+ label: translate("Reach"),
143
+ value: renderReach(translateMap, getInstanceById, use.reach),
144
+ },
145
+ fields.has_length.kind === "Prohibited"
146
+ ? undefined
147
+ : {
148
+ label: translate("Length"),
149
+ value: renderLength(translate, measurements, use.length),
150
+ },
151
+ ],
152
+ },
153
+ };
154
+ };
155
+ const renderReloadTime = (translate, reloadTime) => isNotEmpty(reloadTime)
156
+ ? reloadTime.length > 1
157
+ ? translate("{$value} actions", {
158
+ value: reloadTime.map(time => time.value).join("/"),
159
+ })
160
+ : translate(".input {$value :number} {{{$value} actions}}", {
161
+ value: reloadTime[0].value,
162
+ })
163
+ : MISSING_VALUE;
164
+ const renderRangeBrackets = (rangeBrackets) => `${rangeBrackets.close}/${rangeBrackets.medium}/${rangeBrackets.far}`;
165
+ const renderAmmunition = (translateMap, getInstanceById, ammunition) => ammunition === undefined
166
+ ? "—"
167
+ : (translateMap(getInstanceById("Ammunition", ammunition)?.translations)
168
+ ?.name ?? MISSING_VALUE);
169
+ const renderMeleeDamage = (translate) => (damage) => renderDiceAndFlat(translate, damage.dice, damage.flat);
170
+ /**
171
+ * Render combat values of a ranged weapon.
172
+ */
173
+ export const renderRangedWeapon = (translate, translateMap, getInstanceById, measurements, renderDamage, rangedCombatTechniqueId, use) => {
174
+ const combatTechnique = getInstanceById("RangedCombatTechnique", rangedCombatTechniqueId);
175
+ return {
176
+ type: "labeled",
177
+ label: translate("Combat Technique {$name}", {
178
+ name: translateMap(combatTechnique?.translations)?.name ?? MISSING_VALUE,
179
+ }),
180
+ value: {
181
+ type: "definitionList",
182
+ items: [
183
+ {
184
+ label: translate("Damage Points"),
185
+ value: renderDamage(use.damage),
186
+ },
187
+ {
188
+ label: translate("Reload Time"),
189
+ value: renderReloadTime(translate, use.reload_time),
190
+ },
191
+ {
192
+ label: translate("Range Brackets"),
193
+ value: renderRangeBrackets(use.range),
194
+ },
195
+ {
196
+ label: translate("Ammunition"),
197
+ value: renderAmmunition(translateMap, getInstanceById, use.ammunition),
198
+ },
199
+ {
200
+ label: translate("Length"),
201
+ value: renderLength(translate, measurements, use.length),
202
+ },
203
+ ],
204
+ },
205
+ };
206
+ };
207
+ const renderRangedDamage = (translate) => (damage) => {
208
+ switch (damage.kind) {
209
+ case "Default": {
210
+ const renderedDice = renderDice(translate, damage.Default.dice);
211
+ return damage.Default.flat === undefined || damage.Default.flat === 0
212
+ ? renderedDice
213
+ : damage.Default.flat > 0
214
+ ? additionFormatter(renderedDice, damage.Default.flat)
215
+ : subtractionFormatter(renderedDice, damage.Default.flat);
216
+ }
217
+ case "NotApplicable":
218
+ return "—";
219
+ case "Special":
220
+ return translate("Special");
221
+ default:
222
+ return assertExhaustive(damage);
223
+ }
224
+ };
225
+ const renderComplexity = (translate, complexity) => mapNullable(complexity, c => ({
226
+ label: translate("Complexity"),
227
+ value: (() => {
228
+ switch (c.kind) {
229
+ case "Primitive":
230
+ return translate("Primitive");
231
+ case "Simple":
232
+ return translate("Simple");
233
+ case "Complex":
234
+ return `${translate("Complex")} (${translate("{$value} AP", {
235
+ value: c.Complex.ap_value,
236
+ })})`;
237
+ case "Various":
238
+ return translate("Various");
239
+ default:
240
+ return assertExhaustive(c);
241
+ }
242
+ })(),
243
+ }));
244
+ const renderBlessedTraditionRestriction = (translate, translateMap, localeJoin, getInstanceById, name, restriction) => {
245
+ const getName = (traditionId) => translateMap(getInstanceById("BlessedTradition", traditionId)?.translations)
246
+ ?.name;
247
+ if (restriction.isSanctifiedBy) {
248
+ switch (restriction.scope.kind) {
249
+ case "Specific":
250
+ if (!isNotEmpty(restriction.scope.Specific)) {
251
+ return translate("Sanctified ({$tradition}); only Blessed Ones of {$tradition} may purchase weapons sanctified by {$tradition}.", { tradition: MISSING_VALUE });
252
+ }
253
+ else if (restriction.scope.Specific.length > 1) {
254
+ const list = restriction.scope.Specific.map(getName).filter(isNotNullish);
255
+ return translate("Sanctified ({$sanctifiedTraditions}); only Blessed Ones of {$traditions} may purchase weapons sanctified by {$traditions}, respectively.", {
256
+ sanctifiedTraditions: localeJoin(list, "unit"),
257
+ traditions: localeJoin(list, "disjunction"),
258
+ });
259
+ }
260
+ else {
261
+ return translate("Sanctified ({$tradition}); only Blessed Ones of {$tradition} may purchase weapons sanctified by {$tradition}.", {
262
+ tradition: getName(restriction.scope.Specific[0]) ?? MISSING_VALUE,
263
+ });
264
+ }
265
+ case "Church":
266
+ return UNHANDLED_VALUE;
267
+ case "Shamanistic":
268
+ return UNHANDLED_VALUE;
269
+ default:
270
+ return assertExhaustive(restriction.scope);
271
+ }
272
+ }
273
+ else {
274
+ switch (restriction.scope.kind) {
275
+ case "Specific":
276
+ return translate("To buy a {$itemName} during hero creation, the character must have Tradition ({$traditions}).", {
277
+ itemName: name,
278
+ traditions: localeJoin(restriction.scope.Specific.map(traditionId => translateMap(getInstanceById("BlessedTradition", traditionId)
279
+ ?.translations)?.name).filter(isNotNullish), "disjunction"),
280
+ });
281
+ case "Church":
282
+ return UNHANDLED_VALUE;
283
+ case "Shamanistic":
284
+ return translate("To buy a {$itemName} during hero creation, the character must have a shamanistic tradition.", {
285
+ itemName: name,
286
+ });
287
+ default:
288
+ return assertExhaustive(restriction.scope);
289
+ }
290
+ }
291
+ };
292
+ const renderMagicalTraditionRestriction = (translate, translateMap, getInstanceById, localeJoin, name, restriction) => translate("To buy a {$itemName} during hero creation, the character must have Tradition ({$traditions}).", {
293
+ itemName: name,
294
+ traditions: localeJoin(restriction.scope
295
+ .map(traditionId => translateMap(getInstanceById("MagicalTradition", traditionId)?.translations)?.name)
296
+ .filter(isNotNullish), "disjunction"),
297
+ });
298
+ const renderRaceRestriction = (translate, translateMap, getInstanceById, localeJoin, name, restriction) => translate("To buy a {$name} during hero creation, the character must be from a culture common to the race of {$races}.", {
299
+ name,
300
+ races: localeJoin(restriction.scope
301
+ .map(id => translateMap(getInstanceById("Race", id)?.translations)?.name ??
302
+ MISSING_VALUE)
303
+ .filter(isNotNullish), "disjunction"),
304
+ });
305
+ const renderCultureRestriction = (translate, translateMap, getInstanceById, localeJoin, name, restriction) => translate("To buy a {$name} during hero creation, the character must be from the culture of the {$cultures}.", {
306
+ name,
307
+ cultures: localeJoin(restriction.scope
308
+ .map(id => translateMap(getInstanceById("Culture", id)?.translations)
309
+ ?.name ?? MISSING_VALUE)
310
+ .filter(isNotNullish), "disjunction"),
311
+ });
312
+ const renderProfessionRestriction = (_translate, _translateMap, _getInstanceById, _localeJoin, _name, _restriction) => UNHANDLED_VALUE;
313
+ const renderNote = (translate, translateMap, getInstanceById, localeJoin, melee_uses, restrictedTo, name, note) => ensureNonEmpty([
314
+ Object.values(melee_uses ?? {}).some(use => use.is_parrying_weapon)
315
+ ? translate("Parrying weapon (PA bonus +1 for the main weapon)")
316
+ : undefined,
317
+ restrictedTo?.blessedTraditions === undefined
318
+ ? undefined
319
+ : renderBlessedTraditionRestriction(translate, translateMap, localeJoin, getInstanceById, name, restrictedTo.blessedTraditions),
320
+ restrictedTo?.races === undefined
321
+ ? undefined
322
+ : renderRaceRestriction(translate, translateMap, getInstanceById, localeJoin, name, restrictedTo.races),
323
+ restrictedTo?.cultures === undefined
324
+ ? undefined
325
+ : renderCultureRestriction(translate, translateMap, getInstanceById, localeJoin, name, restrictedTo.cultures),
326
+ restrictedTo?.professions === undefined
327
+ ? undefined
328
+ : renderProfessionRestriction(translate, translateMap, getInstanceById, localeJoin, name, restrictedTo.professions),
329
+ restrictedTo?.magicalTraditions === undefined
330
+ ? undefined
331
+ : renderMagicalTraditionRestriction(translate, translateMap, getInstanceById, localeJoin, name, restrictedTo.magicalTraditions),
332
+ note,
333
+ ].filter(isNotNullish))?.join("; ");
334
+ const renderWeight = (translate, measurements, weight) => {
335
+ if (typeof weight === "number") {
336
+ return {
337
+ label: translate("Weight"),
338
+ value: translate(".input {$value :number} {{{$value} pounds}}", {
339
+ value: weight * measurements.stonesMultiplier,
340
+ }),
341
+ };
342
+ }
343
+ else {
344
+ return {
345
+ label: translate("Weight (Bronze/Silver/Gold)"),
346
+ value: translate("{$value} pounds", {
347
+ value: [weight.bronze, weight.silver, weight.gold]
348
+ .map(value => value * measurements.stonesMultiplier)
349
+ .join("/"),
350
+ }),
351
+ };
352
+ }
353
+ };
354
+ const renderCost = (translate, translateMap, cost) => {
355
+ const renderBookCostVariant = (bookCostVariant) => {
356
+ switch (bookCostVariant.kind) {
357
+ case "Definite": {
358
+ const translation = translateMap(bookCostVariant.Definite.translations);
359
+ return (renderCost(translate, translateMap, bookCostVariant.Definite.cost) +
360
+ parensIf(translation?.label));
361
+ }
362
+ case "Indefinite": {
363
+ const translation = translateMap(bookCostVariant.Indefinite.translations);
364
+ return ((translation?.description ?? MISSING_VALUE) +
365
+ parensIf(translation?.label));
366
+ }
367
+ default:
368
+ return assertExhaustive(bookCostVariant);
369
+ }
370
+ };
371
+ if ("bronze" in cost) {
372
+ return {
373
+ label: translate("Cost (Bronze/Silver/Gold)"),
374
+ value: translate("{$value} silverthalers", {
375
+ value: [cost.bronze, cost.silver, cost.gold].join("/"),
376
+ }),
377
+ };
378
+ }
379
+ else {
380
+ return {
381
+ label: translate("Cost"),
382
+ value: (() => {
383
+ switch (cost.kind) {
384
+ case "Free":
385
+ return translate("free");
386
+ case "Various":
387
+ return translate("various");
388
+ case "Invaluable":
389
+ return translate("invaluable");
390
+ case "Fixed": {
391
+ const translation = translateMap(cost.Fixed.translations);
392
+ const main = translate(".input {$value :number} {{{$value} silverthalers}}", {
393
+ value: cost.Fixed.value,
394
+ });
395
+ if (translation?.wrap_in_text === undefined) {
396
+ return main;
397
+ }
398
+ return `${UNHANDLED_VALUE} ${translate(".input {$value :number} {{{$value} silverthalers}}", {
399
+ value: cost.Fixed.value,
400
+ })}`;
401
+ }
402
+ case "Range":
403
+ return translate(".input {$from :number} .input {$to :number} {{{$from}–{$to} silverthalers}}", {
404
+ from: cost.Range.from,
405
+ to: cost.Range.to,
406
+ });
407
+ case "Single":
408
+ return renderBookCostVariant(cost.Single);
409
+ case "Multiple":
410
+ return cost.Multiple.map(renderBookCostVariant).join(", ");
411
+ default:
412
+ return assertExhaustive(cost);
413
+ }
414
+ })(),
415
+ };
416
+ }
417
+ };
418
+ const renderArmorValues = (translate, translateMap, localeCompare, getInstanceById, idMap, values) => [
419
+ { label: translate("Protection"), value: values.protection.toString() },
420
+ { label: translate("Encumbrance"), value: values.encumbrance.toString() },
421
+ {
422
+ label: translate("Additional Penalties"),
423
+ value: values.has_additional_penalties
424
+ ? [
425
+ idMap.DerivedCharacteristic.Movement,
426
+ idMap.DerivedCharacteristic.Initiative,
427
+ ]
428
+ .map(dcId => `${sign(-1)} ${translateMap(getInstanceById("DerivedCharacteristic", dcId)?.translations)?.abbreviation ?? MISSING_VALUE}`)
429
+ .toSorted(localeCompare)
430
+ .join(", ")
431
+ : "—",
432
+ },
433
+ ];
434
+ const normalizeCombatValues = (entity) => {
435
+ switch (entity.entity) {
436
+ case "Weapon":
437
+ return {
438
+ type: "Weapon",
439
+ values: entity.content,
440
+ };
441
+ case "Armor":
442
+ return {
443
+ type: "Armor",
444
+ values: entity.content,
445
+ };
446
+ case "Ammunition":
447
+ case "Animal":
448
+ case "AnimalCare":
449
+ case "BandageOrRemedy":
450
+ case "Book":
451
+ case "CeremonialItem":
452
+ case "Clothes":
453
+ case "ClothingPackage":
454
+ case "Container":
455
+ case "EquipmentOfBlessedOnes":
456
+ case "GemOrPreciousStone":
457
+ case "IlluminationLightSource":
458
+ case "IlluminationRefillOrSupply":
459
+ case "Jewelry":
460
+ case "Laboratory":
461
+ case "Liebesspielzeug":
462
+ case "LuxuryGood":
463
+ case "MagicalArtifact":
464
+ case "MusicalInstrument":
465
+ case "Newspaper":
466
+ case "OrienteeringAid":
467
+ case "RopeOrChain":
468
+ case "Stationery":
469
+ case "ThievesTool":
470
+ case "ToolOfTheTrade":
471
+ case "TravelGearOrTool":
472
+ case "Vehicle":
473
+ case "WeaponAccessory": {
474
+ const baseItem = entity.content;
475
+ if (baseItem.combat_use === undefined) {
476
+ return undefined;
477
+ }
478
+ switch (baseItem.combat_use.kind) {
479
+ case "Weapon":
480
+ return {
481
+ type: "Weapon",
482
+ values: baseItem.combat_use.Weapon,
483
+ };
484
+ case "Armor":
485
+ return {
486
+ type: "Armor",
487
+ values: baseItem.combat_use.Armor,
488
+ };
489
+ default:
490
+ return assertExhaustive(baseItem.combat_use);
491
+ }
492
+ }
493
+ default:
494
+ return assertExhaustive(entity);
495
+ }
496
+ };
497
+ /**
498
+ * Get a JSON representation of the rules text for equipment.
499
+ */
500
+ export const getEquipmentEntityDescription = createEntityDescriptionCreator(({ getInstanceById, idMap }, locale, entry) => {
501
+ const { translate, translateMap } = locale;
502
+ const translation = entry.entity === "ClothingPackage"
503
+ ? undefined
504
+ : translateMap(entry.content.translations);
505
+ if (entry.entity !== "ClothingPackage" && translation === undefined) {
506
+ return undefined;
507
+ }
508
+ const baseItem = entry.content;
509
+ const baseItemTranslation = translation;
510
+ const name = getEquipmentName(translate, translateMap, getInstanceById, entry);
511
+ const combatValues = normalizeCombatValues(entry);
512
+ const combatTranslation = translateMap(combatValues?.values.translations);
513
+ return {
514
+ title: name,
515
+ className: "equipment",
516
+ body: [
517
+ ...(combatValues?.type === "Weapon"
518
+ ? Object.entries(combatValues.values.melee_uses ?? {}).map(([combatTechniqueId, use]) => renderMeleeWeapon(translate, translateMap, getInstanceById, locale.measurementAdjustments, renderMeleeDamage(translate), combatTechniqueId, use))
519
+ : []),
520
+ ...(combatValues?.type === "Weapon"
521
+ ? Object.entries(combatValues.values.ranged_uses ?? {}).map(([combatTechniqueId, use]) => renderRangedWeapon(translate, translateMap, getInstanceById, locale.measurementAdjustments, renderRangedDamage(translate), combatTechniqueId, use))
522
+ : []),
523
+ {
524
+ type: "definitionList",
525
+ items: [
526
+ renderComplexity(translate, baseItem.complexity),
527
+ ...(combatValues?.type === "Armor"
528
+ ? renderArmorValues(translate, translateMap, locale.compare, getInstanceById, idMap, combatValues.values)
529
+ : []),
530
+ mapNullable(baseItem.burning_time, burningTime => ({
531
+ label: translate("Burning Time"),
532
+ value: burningTime.kind === "Unlimited"
533
+ ? translate("unlimited")
534
+ : formatTimeSpan(translate, ResponsiveTextSize.Full, burningTime.Limited.unit, burningTime.Limited.value),
535
+ })),
536
+ mapNullable(baseItemTranslation?.color, color => ({
537
+ label: translate("Color"),
538
+ value: color,
539
+ })),
540
+ baseItemTranslation?.language !== undefined ||
541
+ baseItemTranslation?.script !== undefined
542
+ ? {
543
+ label: translate("Language/Script"),
544
+ value: [
545
+ baseItemTranslation?.language,
546
+ baseItemTranslation?.script,
547
+ ]
548
+ .map(value => value ?? "—")
549
+ .join(" / "),
550
+ }
551
+ : undefined,
552
+ baseItem.structure_points !== undefined &&
553
+ isNotEmpty(baseItem.structure_points)
554
+ ? {
555
+ label: translate("Structure Points"),
556
+ value: baseItem.structure_points.length === 1
557
+ ? translate(".input {$value :number} {{{$value} Structure Points}}", { value: baseItem.structure_points[0].points })
558
+ : translate("{$value} Structure Points", {
559
+ value: baseItem.structure_points
560
+ .map(elem => elem.points)
561
+ .join("/"),
562
+ }),
563
+ }
564
+ : undefined,
565
+ mapNullable(baseItem.weight, weight => renderWeight(translate, locale.measurementAdjustments, weight)),
566
+ mapNullable(baseItem.cost, cost => renderCost(translate, translateMap, cost)),
567
+ mapNullable(renderNote(translate, translateMap, getInstanceById, locale.join, combatValues?.type === "Weapon"
568
+ ? combatValues.values.melee_uses
569
+ : undefined, combatValues?.values.restrictedTo, name, baseItemTranslation?.note), note => ({
570
+ label: translate("Note"),
571
+ value: note,
572
+ })),
573
+ baseItemTranslation?.rules !== undefined
574
+ ? {
575
+ label: translate("Rules"),
576
+ value: baseItemTranslation.rules,
577
+ }
578
+ : undefined,
579
+ combatValues?.type === "Weapon" &&
580
+ combatTranslation?.advantage !== undefined
581
+ ? {
582
+ label: translate("Weapon Advantage"),
583
+ value: combatTranslation.advantage,
584
+ }
585
+ : undefined,
586
+ combatValues?.type === "Weapon" &&
587
+ combatTranslation?.disadvantage !== undefined
588
+ ? {
589
+ label: translate("Weapon Disadvantage"),
590
+ value: combatTranslation.disadvantage,
591
+ }
592
+ : undefined,
593
+ combatValues?.type === "Armor" &&
594
+ combatTranslation?.advantage !== undefined
595
+ ? {
596
+ label: translate("Armor Advantage"),
597
+ value: combatTranslation.advantage,
598
+ }
599
+ : undefined,
600
+ combatValues?.type === "Armor" &&
601
+ combatTranslation?.disadvantage !== undefined
602
+ ? {
603
+ label: translate("Armor Disadvantage"),
604
+ value: combatTranslation.disadvantage,
605
+ }
606
+ : undefined,
607
+ ],
608
+ },
609
+ ],
610
+ errata: baseItemTranslation?.errata,
611
+ references: entry.content.src,
612
+ };
613
+ });
@@ -0,0 +1,8 @@
1
+ import type { EquipmentIdentifier } from "optolith-database-schema/gen";
2
+ import type { GetInstanceById } from "../helpers/getTypes.js";
3
+ /**
4
+ * Get a JSON representation of the rules text for an equipment package.
5
+ */
6
+ export declare const getEquipmentPackageEntityDescription: import("../creator.js").EntityDescriptionCreator<"EquipmentPackage", {
7
+ getInstanceById: GetInstanceById<"Publication" | EquipmentIdentifier["kind"] | "SocialStatus">;
8
+ }, import("../index.js").EntityDescription>;