@optolith/entity-descriptions 0.4.26 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/lib/entities/activatable.d.ts +3 -3
  3. package/lib/entities/activatable.js +60 -29
  4. package/lib/entities/equipment.d.ts +2 -2
  5. package/lib/entities/equipment.js +7 -7
  6. package/lib/entities/liturgicalChant.js +19 -12
  7. package/lib/entities/partial/enhancements.js +2 -2
  8. package/lib/entities/partial/map.d.ts +25 -36
  9. package/lib/entities/partial/map.js +16 -15
  10. package/lib/entities/partial/prerequisites/prerequisiteGroups.js +9 -2
  11. package/lib/entities/partial/prerequisites/single/enhancement.d.ts +7 -3
  12. package/lib/entities/partial/prerequisites/single/enhancement.js +19 -25
  13. package/lib/entities/partial/rated/activatable/castingTime.d.ts +5 -5
  14. package/lib/entities/partial/rated/activatable/castingTime.js +6 -3
  15. package/lib/entities/partial/rated/activatable/cost.d.ts +20 -14
  16. package/lib/entities/partial/rated/activatable/cost.js +62 -27
  17. package/lib/entities/partial/rated/activatable/duration.d.ts +3 -3
  18. package/lib/entities/partial/rated/activatable/index.d.ts +6 -6
  19. package/lib/entities/partial/rated/activatable/index.js +4 -4
  20. package/lib/entities/partial/reader.d.ts +10 -0
  21. package/lib/entities/partial/reader.js +5 -1
  22. package/lib/entities/partial/units/timeSpan.d.ts +23 -15
  23. package/lib/entities/partial/units/timeSpan.js +17 -3
  24. package/lib/entities/poison.js +9 -9
  25. package/lib/entities/profession.js +17 -15
  26. package/lib/entities/spell.js +34 -18
  27. package/lib/helpers/enums.d.ts +2 -1
  28. package/lib/helpers/enums.js +1 -3
  29. package/lib/index.d.ts +56 -56
  30. package/lib/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +11 -10
package/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
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.6.0](https://github.com/Optolith/entity-descriptions/compare/v0.5.1...v0.6.0) (2026-09-08)
6
+
7
+ ### ⚠ BREAKING CHANGES
8
+
9
+ * **deps:** upgrade to schema 0.55.0
10
+
11
+ ### build
12
+
13
+ * **deps:** upgrade to schema 0.55.0 ([0dff095](https://github.com/Optolith/entity-descriptions/commit/0dff0950d939beec0e688e792c58d6d0a2ce44d7))
14
+
15
+ ### Bug Fixes
16
+
17
+ * restricted blessings for traditions ([ca26c4c](https://github.com/Optolith/entity-descriptions/commit/ca26c4cadc77198b164967074735707ac6ec8593)), closes [#42](https://github.com/Optolith/entity-descriptions/issues/42)
18
+ ## [0.5.1](https://github.com/Optolith/entity-descriptions/compare/v0.5.0...v0.5.1) (2026-09-08)
19
+ ## [0.5.0](https://github.com/Optolith/entity-descriptions/compare/v0.4.26...v0.5.0) (2026-09-08)
20
+
21
+ ### ⚠ BREAKING CHANGES
22
+
23
+ * **deps:** upgrade to schema 0.54.0 and other newer dependencies
24
+
25
+ ### build
26
+
27
+ * **deps:** upgrade to schema 0.54.0 and other newer dependencies ([5ebe2df](https://github.com/Optolith/entity-descriptions/commit/5ebe2df9e468da66f792dd6e40e414037f395579))
28
+
29
+ ### Features
30
+
31
+ * attributed names for enhancement prerequisites ([0e8600a](https://github.com/Optolith/entity-descriptions/commit/0e8600ac4999c4c7f83b6c719d8955dcc7f87512))
32
+
33
+ ### Bug Fixes
34
+
35
+ * do not display enchantment parent in parent description ([7e3d5da](https://github.com/Optolith/entity-descriptions/commit/7e3d5daf565eb1729b6f6a31229cd7bfa826bf0d)), closes [#26](https://github.com/Optolith/entity-descriptions/issues/26)
36
+ * indentation for multi-paragraph enhancements ([206f13c](https://github.com/Optolith/entity-descriptions/commit/206f13cc5730148e98c5263cc891a53c10391e3f))
37
+ * sort liturgical chant traditions alphabetically ([2255cd4](https://github.com/Optolith/entity-descriptions/commit/2255cd44f8df474ce4b17b253ead84c4f2c10217)), closes [#35](https://github.com/Optolith/entity-descriptions/issues/35)
5
38
  ## [0.4.26](https://github.com/Optolith/entity-descriptions/compare/v0.4.25...v0.4.26) (2026-06-21)
6
39
 
7
40
 
@@ -1,4 +1,4 @@
1
- import type { ActivatableIdentifier, AdvancedSpecialAbility, AdvantageDisadvantagePrerequisites, AdventurePointsValue, ApplicableCombatTechniques, Aspect_ID, CombatSpecialAbilityUsageType, DaggerRitualCost, EnchantmentCost, Errata, GeneralPrerequisites, MagicalSignCost, Penalty, PropertyDeclaration, PublicationRefs, RatedIdentifier, SelectOptions, SpecialRule, Volume } from "@optolith/database-schema/gen";
1
+ import type { ActivatableIdentifier, AdvancedSpecialAbility, AdvantageDisadvantagePrerequisites, AdventurePointsValue, ApplicableCombatTechniques, Aspect_ID, DaggerRitualCost, EnchantmentCost, Errata, GeneralPrerequisites, MagicalSignCost, Penalty, PropertyDeclaration, PublicationRefs, RatedIdentifier, SelectOptions, SpecialRule, Volume } from "@optolith/database-schema/gen";
2
2
  import { Case } from "tsondb/schema/gen";
3
3
  import type { GetAllInstances, GetInstanceById } from "../helpers/getTypes.js";
4
4
  import type { GetAllResolvedNewSkillApplications, GetAllResolvedSelectOptions, GetAllResolvedSkillUses } from "../index.js";
@@ -12,7 +12,7 @@ export type BaseActivatable = {
12
12
  levels?: number;
13
13
  maximum?: number;
14
14
  select_options?: SelectOptions;
15
- usage_type?: CombatSpecialAbilityUsageType;
15
+ usage_type?: Case<"Passive" | "Active" | "BasicManeuver" | "SpecialManeuver">;
16
16
  advanced?: AdvancedSpecialAbility<AdvancedIdentifierSpecialAbility>[];
17
17
  penalty?: Penalty;
18
18
  combat_techniques?: ApplicableCombatTechniques;
@@ -47,7 +47,7 @@ export type BaseActivatableTranslation = {
47
47
  * Get a JSON representation of the rules text for a special ability.
48
48
  */
49
49
  export declare const getActivatableEntityDescription: import("../creator.js").EntityDescriptionCreator<"Advantage" | "Disadvantage" | "AdvancedCombatSpecialAbility" | "AdvancedKarmaSpecialAbility" | "AdvancedMagicalSpecialAbility" | "AdvancedSkillSpecialAbility" | "AncestorGlyph" | "ArcaneOrbEnchantment" | "AttireEnchantment" | "Beutelzauber" | "BlessedTradition" | "BowlEnchantment" | "BrawlingSpecialAbility" | "CauldronEnchantment" | "CeremonialItemSpecialAbility" | "ChronicleEnchantment" | "CombatSpecialAbility" | "CombatStyleSpecialAbility" | "CommandSpecialAbility" | "DaggerRitual" | "FamiliarSpecialAbility" | "FatePointSexSpecialAbility" | "FatePointSpecialAbility" | "FoolsHatEnchantment" | "GeneralSpecialAbility" | "Haubenzauber" | "Hauerkettenzauber" | "InstrumentEnchantment" | "KarmaSpecialAbility" | "Keulenzauber" | "Krallenkettenzauber" | "Kristallkugelzauber" | "LiturgicalStyleSpecialAbility" | "LycantropicGift" | "MagicalSign" | "MagicalSpecialAbility" | "MagicalTradition" | "MagicStyleSpecialAbility" | "OrbEnchantment" | "PactGift" | "ProtectiveWardingCircleSpecialAbility" | "RingEnchantment" | "Schweinetrommelzauber" | "Sermon" | "SexSpecialAbility" | "SickleRitual" | "SikaryanDrainSpecialAbility" | "SkillStyleSpecialAbility" | "SpellSwordEnchantment" | "StaffEnchantment" | "ToyEnchantment" | "Trinkhornzauber" | "VampiricGift" | "Vision" | "WandEnchantment" | "WeaponEnchantment", {
50
- getInstanceById: GetInstanceById<"Publication" | "Subject" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "TradeSecret" | "Aspect" | "Property" | "Race" | "Culture" | "State" | "Enhancement" | "PactCategory" | "PactDomain" | "SocialStatus" | "Weapon" | "Patron" | "PersonalityTrait">;
50
+ getInstanceById: GetInstanceById<"Publication" | "Subject" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "TradeSecret" | "Aspect" | "Property" | "Race" | "Culture" | "State" | "Enhancement" | "PactCategory" | "PactDomain" | "SocialStatus" | "Weapon" | "Patron" | "PersonalityTrait" | "Blessing">;
51
51
  getAllInstances: GetAllInstances<"Script">;
52
52
  getResolvedSelectOptionById: GetResolvedSelectOptionById;
53
53
  getAllResolvedSelectOptions: GetAllResolvedSelectOptions;
@@ -1,7 +1,7 @@
1
1
  import { isNotEmpty } from "@elyukai/utils/array/nonEmpty";
2
2
  import { count } from "@elyukai/utils/array/reductions";
3
3
  import { deepEqual } from "@elyukai/utils/equality";
4
- import { on } from "@elyukai/utils/function";
4
+ import { constant, on } from "@elyukai/utils/function";
5
5
  import { isNotNullish } from "@elyukai/utils/nullable";
6
6
  import { Reader } from "@elyukai/utils/reader";
7
7
  import { numAsc } from "@optolith/helpers/compare";
@@ -18,6 +18,7 @@ import { attributedName } from "./partial/markdown.js";
18
18
  import { additionFormatter } from "./partial/mathOperation.js";
19
19
  import { printAdvantageDisadvantagePrerequisites, printGeneralPrerequisites, } from "./partial/prerequisites/index.js";
20
20
  import { printActivatableName, } from "./partial/prerequisites/single/activatable.js";
21
+ import { renderStandaloneCostMap } from "./partial/rated/activatable/cost.js";
21
22
  import { parensIf } from "./partial/rated/activatable/parensIf.js";
22
23
  import { attributedNameR, localeSortR, translateR, } from "./partial/reader.js";
23
24
  import { getResponsiveText, getResponsiveTextOptional, ResponsiveTextSize, } from "./partial/responsiveText.js";
@@ -274,7 +275,7 @@ const renderVolumeValue = (translate, translateMap, getAllResolvedSelectOptions,
274
275
  levels: volume.ByLevel.list.map((_, index) => romanize(index + 1)).join("/"),
275
276
  });
276
277
  case "Map":
277
- return renderResponsiveMap(volume.Map, option => option.points, values => translate("{$points} points", { points: values })).run({
278
+ return renderResponsiveMap(volume.Map, option => Reader.of(option.points), values => translateR("{$points} points", { points: values })).run({
278
279
  translate,
279
280
  translateMap,
280
281
  responsiveTextSize,
@@ -297,7 +298,7 @@ const renderVolumeValue = (translate, translateMap, getAllResolvedSelectOptions,
297
298
  return assertExhaustive(volume);
298
299
  }
299
300
  };
300
- const renderArcaneEnergyCost = (translate, translateMap, localeJoin, responsiveTextSize, levels, cost) => {
301
+ const renderArcaneEnergyCost = (translate, translateMap, format, localeJoin, responsiveTextSize, levels, cost) => {
301
302
  switch (cost.kind) {
302
303
  case "Fixed": {
303
304
  const translationWrapper = cost.Fixed.is_permanent === true
@@ -310,7 +311,7 @@ const renderArcaneEnergyCost = (translate, translateMap, localeJoin, responsiveT
310
311
  ? str => str
311
312
  : str => translate("{$cost} per {$interval}", {
312
313
  cost: str,
313
- interval: formatTimeSpan(translate, responsiveTextSize, interval.unit, interval.value),
314
+ interval: formatTimeSpan(translate, translateMap, format, responsiveTextSize, interval.unit, interval.value),
314
315
  });
315
316
  const wrapInPerLevel = (() => {
316
317
  if (cost.Fixed.per_level === undefined) {
@@ -352,7 +353,7 @@ const renderArcaneEnergyCost = (translate, translateMap, localeJoin, responsiveT
352
353
  case "Interval":
353
354
  return translate("{$cost} per {$interval}", {
354
355
  cost: translate("{$value} AE", { value: cost.Interval.value }),
355
- interval: formatTimeSpan(translate, responsiveTextSize, cost.Interval.interval.unit, cost.Interval.interval.value),
356
+ interval: formatTimeSpan(translate, translateMap, format, responsiveTextSize, cost.Interval.interval.unit, cost.Interval.interval.value),
356
357
  });
357
358
  case "ActivationAndHalfInterval":
358
359
  return additionFormatter(translate("{$value} AE", {
@@ -361,7 +362,7 @@ const renderArcaneEnergyCost = (translate, translateMap, localeJoin, responsiveT
361
362
  cost: translate("{$value} AE", {
362
363
  value: Math.round(cost.ActivationAndHalfInterval.value / 2),
363
364
  }),
364
- interval: formatTimeSpan(translate, responsiveTextSize, cost.ActivationAndHalfInterval.interval.unit, cost.ActivationAndHalfInterval.interval.value),
365
+ interval: formatTimeSpan(translate, translateMap, format, responsiveTextSize, cost.ActivationAndHalfInterval.interval.unit, cost.ActivationAndHalfInterval.interval.value),
365
366
  }));
366
367
  case "Indefinite":
367
368
  return ((mapNullable(translateMap(cost.Indefinite.translations)?.description, description => getResponsiveText(description, responsiveTextSize)) ?? MISSING_VALUE) +
@@ -372,18 +373,11 @@ const renderArcaneEnergyCost = (translate, translateMap, localeJoin, responsiveT
372
373
  mapNullableDefault(translateMap(option.translations)?.note, note => parensIf(getResponsiveTextOptional(note, responsiveTextSize)), "")), "disjunction"),
373
374
  });
374
375
  case "Map":
375
- return renderResponsiveMap(cost.Map, option => option.value, values => translate("{$value} AE", { value: values }), cost.Map.options.every(option => option.permanent_value !== undefined)
376
- ? {
377
- surround: values => translate(", {$value} of which are permanent", {
378
- value: values,
379
- }),
380
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- is checked beforehand
381
- getAdditionalValue: option => option.permanent_value,
382
- }
383
- : undefined).run({
376
+ return renderStandaloneCostMap(cost.Map).run({
384
377
  translate,
385
378
  translateMap,
386
379
  responsiveTextSize,
380
+ energyUnit: "ArcaneEnergy",
387
381
  });
388
382
  case "Variable":
389
383
  return translate("Variable");
@@ -423,7 +417,7 @@ const renderBindingCost = (translate, translateMap, localeCompare, getAllResolve
423
417
  }),
424
418
  });
425
419
  case "Map":
426
- return renderResponsiveMap(cost.Map, option => option.permanent_value, values => translate("{$value} permanent AE", { value: values })).run({
420
+ return renderResponsiveMap(cost.Map, option => Reader.of(option.permanentValue), values => translateR("{$value} permanent AE", { value: values })).run({
427
421
  translate,
428
422
  translateMap,
429
423
  responsiveTextSize,
@@ -451,7 +445,7 @@ const renderLifePointsCost = (translate, cost) => cost === undefined
451
445
  ? ""
452
446
  : // eslint-disable-next-line no-irregular-whitespace
453
447
  ` (+ ${translate("{$value} LP", { value: cost.Fixed.value })})`;
454
- const renderCost = (translate, translateMap, localeJoin, localeCompare, getAllResolvedSelectOptions, responsiveTextSize, levels, cost) => {
448
+ const renderCost = (translate, translateMap, format, localeJoin, localeCompare, getAllResolvedSelectOptions, responsiveTextSize, levels, cost) => {
455
449
  if (typeof cost === "number") {
456
450
  return {
457
451
  label: translate("AE Cost"),
@@ -466,9 +460,9 @@ const renderCost = (translate, translateMap, localeJoin, localeCompare, getAllRe
466
460
  label: translate("AE Cost"),
467
461
  value: cost.kind === "ArcaneEnergyCost"
468
462
  ? "ae_cost" in cost.ArcaneEnergyCost
469
- ? renderArcaneEnergyCost(translate, translateMap, localeJoin, responsiveTextSize, levels, cost.ArcaneEnergyCost.ae_cost) + renderLifePointsCost(translate, cost.ArcaneEnergyCost.lp_cost)
470
- : renderArcaneEnergyCost(translate, translateMap, localeJoin, responsiveTextSize, levels, cost.ArcaneEnergyCost)
471
- : renderArcaneEnergyCost(translate, translateMap, localeJoin, responsiveTextSize, levels, cost),
463
+ ? renderArcaneEnergyCost(translate, translateMap, format, localeJoin, responsiveTextSize, levels, cost.ArcaneEnergyCost.ae_cost) + renderLifePointsCost(translate, cost.ArcaneEnergyCost.lp_cost)
464
+ : renderArcaneEnergyCost(translate, translateMap, format, localeJoin, responsiveTextSize, levels, cost.ArcaneEnergyCost)
465
+ : renderArcaneEnergyCost(translate, translateMap, format, localeJoin, responsiveTextSize, levels, cost),
472
466
  };
473
467
  case "BindingCost":
474
468
  return {
@@ -657,6 +651,37 @@ const renderFavoredSkillsSelection = (baseList, favoredSkillsSelection) => favor
657
651
  baseList,
658
652
  selection: names.join(", "),
659
653
  }));
654
+ const getRestrictedBlessingsSpecialRuleLabelKey = (restrictedBlessings) => {
655
+ if (restrictedBlessings === "All") {
656
+ return "No Blessings";
657
+ }
658
+ switch (restrictedBlessings.kind) {
659
+ case "Three":
660
+ return "Restricted Blessings";
661
+ case "Six":
662
+ return "Strongly Restricted Blessings";
663
+ default:
664
+ return assertExhaustive(restrictedBlessings);
665
+ }
666
+ };
667
+ const renderRestrictedBlessingsSpecialRule = (restrictedBlessings, nameOfPeopleWithThisTradition) => translateR(getRestrictedBlessingsSpecialRuleLabelKey(restrictedBlessings)).thenW(label => restrictedBlessings === "All"
668
+ ? translateR("{$nameOfBlessedOnes} cannot use blessings.", {
669
+ nameOfBlessedOnes: nameOfPeopleWithThisTradition,
670
+ }).map(text => ({
671
+ label,
672
+ text,
673
+ }))
674
+ : Reader.traverse(restrictedBlessings.kind === "Three"
675
+ ? restrictedBlessings.Three
676
+ : restrictedBlessings.Six, id => attributedNameR("restricted-blessings", "Blessing", id).map(name => name ?? MISSING_VALUE))
677
+ .thenW(localeSortR)
678
+ .thenW(list => translateR("{$nameOfBlessedOnes} cannot use {$blessings :list type=disjunction}.", {
679
+ nameOfBlessedOnes: nameOfPeopleWithThisTradition,
680
+ blessings: list,
681
+ }).map(text => ({
682
+ label,
683
+ text,
684
+ }))));
660
685
  const renderFavoredSkillsSpecialRule = (favoredCombatTechniques, favoredSkills, favoredSkillsSelection) => translateR("Favored Skills").thenW(label => renderFavoredCombatTechniques(favoredCombatTechniques)
661
686
  .thenW(favoredCombatTechniquesText => renderFavoredSkillsBaseList(favoredCombatTechniquesText, favoredSkills))
662
687
  .thenW(list => renderFavoredSkillsSelection(list, favoredSkillsSelection))
@@ -676,7 +701,11 @@ const renderPrimaryAttributeSpecialRule = (traditionName, nameOfPeopleWithThisTr
676
701
  }))).map(description => ({
677
702
  text: description,
678
703
  }));
679
- const renderAdditionalSpecialRules = (traditionName, nameOfPeopleWithThisTradition, favoredCombatTechniques, favoredSkills, favoredSkillsSelection, primaryAttribute) => Reader.sequence([
704
+ const renderAdditionalSpecialRules = (fixedSpecialRules, traditionName, nameOfPeopleWithThisTradition, restrictedBlessings, favoredCombatTechniques, favoredSkills, favoredSkillsSelection, primaryAttribute) => Reader.sequence([
705
+ restrictedBlessings === undefined
706
+ ? Reader.of(undefined)
707
+ : renderRestrictedBlessingsSpecialRule(restrictedBlessings, nameOfPeopleWithThisTradition),
708
+ ...fixedSpecialRules.map(rule => Reader.of(rule)),
680
709
  favoredSkills === undefined
681
710
  ? Reader.of(undefined)
682
711
  : renderFavoredSkillsSpecialRule(favoredCombatTechniques, favoredSkills, favoredSkillsSelection),
@@ -686,7 +715,7 @@ const renderAdditionalSpecialRules = (traditionName, nameOfPeopleWithThisTraditi
686
715
  * Get a JSON representation of the rules text for a special ability.
687
716
  */
688
717
  export const getActivatableEntityDescription = createEntityDescriptionCreator(({ getInstanceById, getAllInstances, getResolvedSelectOptionById, getAllResolvedSelectOptions }, locale, { entity: entityName, content: entry, id }) => {
689
- const { translate, translateMap } = locale;
718
+ const { translate, translateMap, format } = locale;
690
719
  const translation = translateMap(entry.translations);
691
720
  const responsiveTextSize = ResponsiveTextSize.Full;
692
721
  if (translation === undefined) {
@@ -697,14 +726,15 @@ export const getActivatableEntityDescription = createEntityDescriptionCreator(({
697
726
  translateMap,
698
727
  getInstanceById,
699
728
  localeCompare: locale.compare,
729
+ localeJoin: locale.join,
700
730
  };
701
731
  const baseEntry = entry;
702
732
  const wrappedId = Case(entityName, id);
703
- const additionalSpecialRules = entityName === "BlessedTradition"
704
- ? renderAdditionalSpecialRules(translation.name, translation.nameOfBlessedOnes, entry.favored_combat_techniques, entry.favored_skills, entry.favored_skills_selection, entry.primary).run(env)
733
+ const addAdditionalSpecialRules = entityName === "BlessedTradition"
734
+ ? (baseList) => renderAdditionalSpecialRules(baseList, translation.name, translation.nameOfBlessedOnes, entry.type.kind === "Church" ? entry.type.Church.restrictedBlessings : "All", entry.favored_combat_techniques, entry.favored_skills, entry.favored_skills_selection, entry.primary).run(env)
705
735
  : entityName === "MagicalTradition"
706
- ? renderAdditionalSpecialRules(translation.name, translation.nameOfSpellcasters, undefined, undefined, undefined, entry.primary).run(env)
707
- : [];
736
+ ? (baseList) => renderAdditionalSpecialRules(baseList, translation.name, translation.nameOfSpellcasters, undefined, undefined, undefined, undefined, entry.primary).run(env)
737
+ : constant([]);
708
738
  const makeTraditionName = entityName === "BlessedTradition" || entityName === "MagicalTradition"
709
739
  ? name => translate("Tradition ({$tradition})", { tradition: name })
710
740
  : name => name;
@@ -713,6 +743,8 @@ export const getActivatableEntityDescription = createEntityDescriptionCreator(({
713
743
  (baseEntry.levels !== undefined ? ` I–${romanize(baseEntry.levels)}` : ""),
714
744
  subtitle: mapNullable(baseEntry.usage_type, usageType => {
715
745
  switch (usageType.kind) {
746
+ case "Active":
747
+ return translate("Active");
716
748
  case "Passive":
717
749
  return translate("Passive");
718
750
  case "BasicManeuver":
@@ -727,8 +759,7 @@ export const getActivatableEntityDescription = createEntityDescriptionCreator(({
727
759
  body: [
728
760
  mapNullable(translation.special_rules, specialRules => ({
729
761
  type: "plain",
730
- text: specialRules
731
- .concat(additionalSpecialRules)
762
+ text: addAdditionalSpecialRules(specialRules)
732
763
  .map(specialRule => specialRule.label === undefined
733
764
  ? `- ${specialRule.text}`
734
765
  : `- *${specialRule.label}*: ${specialRule.text}`)
@@ -789,7 +820,7 @@ export const getActivatableEntityDescription = createEntityDescriptionCreator(({
789
820
  label: translate("Volume"),
790
821
  value: renderVolumeValue(translate, translateMap, () => getAllResolvedSelectOptions(wrappedId), responsiveTextSize, volume),
791
822
  })),
792
- mapNullable(baseEntry.cost, cost => renderCost(translate, translateMap, locale.join, locale.compare, () => getAllResolvedSelectOptions(wrappedId), responsiveTextSize, baseEntry.levels, cost)),
823
+ mapNullable(baseEntry.cost, cost => renderCost(translate, translateMap, format, locale.join, locale.compare, () => getAllResolvedSelectOptions(wrappedId), responsiveTextSize, baseEntry.levels, cost)),
793
824
  mapNullable(baseEntry.property, property => ({
794
825
  label: translate("Property"),
795
826
  value: renderPropertyValue(getInstanceById, translate, translateMap, property),
@@ -2,7 +2,7 @@ import type { AmmunitionishIdentifier, EquipmentIdentifier, GenMeleeWeapon, GenR
2
2
  import { type TaggedEntity } from "../creator.js";
3
3
  import type { GetInstanceById } from "../helpers/getTypes.js";
4
4
  import type { LocaleJoin } from "../helpers/locale.js";
5
- import type { Translate, TranslateMap } from "../helpers/translate.js";
5
+ import type { Format, Translate, TranslateMap } from "../helpers/translate.js";
6
6
  import type { IdMap, LabeledEntityDescriptionSection, RawEntityDescriptionSectionContent } from "../index.js";
7
7
  /**
8
8
  * Get the name of an equipment item.
@@ -15,7 +15,7 @@ export declare const renderMeleeWeapon: <Damage>(translate: Translate, translate
15
15
  /**
16
16
  * Render combat values of a ranged weapon.
17
17
  */
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
+ export declare const renderRangedWeapon: <Damage>(translate: Translate, translateMap: TranslateMap, format: Format, getInstanceById: GetInstanceById<"RangedCombatTechnique" | AmmunitionishIdentifier["kind"]>, measurements: Required<LocaleMeasurementAdjustments>, renderDamage: (damage: Damage) => string, rangedCombatTechniqueId: string, use: GenRangedWeapon<Damage>) => LabeledEntityDescriptionSection<RawEntityDescriptionSectionContent>;
19
19
  /**
20
20
  * Get a JSON representation of the rules text for equipment.
21
21
  */
@@ -162,12 +162,12 @@ export const renderMeleeWeapon = (translate, translateMap, localeJoin, getInstan
162
162
  },
163
163
  };
164
164
  };
165
- const renderReloadTime = (translate, reloadTime) => isNotEmpty(reloadTime)
165
+ const renderReloadTime = (translate, translateMap, format, reloadTime) => isNotEmpty(reloadTime)
166
166
  ? reloadTime.length > 1
167
167
  ? translate("{$value} actions", {
168
168
  value: reloadTime.map(time => time.value).join("/"),
169
169
  })
170
- : formatTimeSpan(translate, ResponsiveTextSize.Full, "Actions", reloadTime[0].value)
170
+ : formatTimeSpan(translate, translateMap, format, ResponsiveTextSize.Full, "Actions", reloadTime[0].value)
171
171
  : MISSING_VALUE;
172
172
  const renderRangeBrackets = (rangeBrackets) => `${rangeBrackets.close.toFixed()}/${rangeBrackets.medium.toFixed()}/${rangeBrackets.far.toFixed()}`;
173
173
  const renderAmmunition = (translateMap, getInstanceById, ammunition) => ammunition === undefined
@@ -177,7 +177,7 @@ const renderMeleeDamage = (translate) => (damage) => renderDiceAndFlat(translate
177
177
  /**
178
178
  * Render combat values of a ranged weapon.
179
179
  */
180
- export const renderRangedWeapon = (translate, translateMap, getInstanceById, measurements, renderDamage, rangedCombatTechniqueId, use) => {
180
+ export const renderRangedWeapon = (translate, translateMap, format, getInstanceById, measurements, renderDamage, rangedCombatTechniqueId, use) => {
181
181
  const combatTechnique = getInstanceById("RangedCombatTechnique", rangedCombatTechniqueId);
182
182
  return {
183
183
  type: "labeled",
@@ -193,7 +193,7 @@ export const renderRangedWeapon = (translate, translateMap, getInstanceById, mea
193
193
  },
194
194
  {
195
195
  label: translate("Reload Time"),
196
- value: renderReloadTime(translate, use.reload_time),
196
+ value: renderReloadTime(translate, translateMap, format, use.reload_time),
197
197
  },
198
198
  {
199
199
  label: translate("Range Brackets"),
@@ -624,7 +624,7 @@ const renderBookRules = (translate, rules) => {
624
624
  * Get a JSON representation of the rules text for equipment.
625
625
  */
626
626
  export const getEquipmentEntityDescription = createEntityDescriptionCreator(({ getInstanceById, idMap }, locale, entry) => {
627
- const { translate, translateMap } = locale;
627
+ const { translate, translateMap, format } = locale;
628
628
  if (entry.entity === "Book") {
629
629
  const translation = translateMap(entry.content.translations);
630
630
  if (translation === undefined) {
@@ -729,7 +729,7 @@ export const getEquipmentEntityDescription = createEntityDescriptionCreator(({ g
729
729
  ? Object.entries(combatValues.values.melee_uses ?? {}).map(([combatTechniqueId, use]) => renderMeleeWeapon(translate, translateMap, locale.join, getInstanceById, locale.measurementAdjustments, renderMeleeDamage(translate), combatTechniqueId, use))
730
730
  : []),
731
731
  ...(combatValues?.type === "Weapon"
732
- ? Object.entries(combatValues.values.ranged_uses ?? {}).map(([combatTechniqueId, use]) => renderRangedWeapon(translate, translateMap, getInstanceById, locale.measurementAdjustments, renderRangedDamage(translate), combatTechniqueId, use))
732
+ ? Object.entries(combatValues.values.ranged_uses ?? {}).map(([combatTechniqueId, use]) => renderRangedWeapon(translate, translateMap, format, getInstanceById, locale.measurementAdjustments, renderRangedDamage(translate), combatTechniqueId, use))
733
733
  : []),
734
734
  {
735
735
  type: "definitionList",
@@ -742,7 +742,7 @@ export const getEquipmentEntityDescription = createEntityDescriptionCreator(({ g
742
742
  label: translate("Burning Time"),
743
743
  value: burningTime.kind === "Unlimited"
744
744
  ? translate("unlimited")
745
- : formatTimeSpan(translate, ResponsiveTextSize.Full, burningTime.Limited.unit, burningTime.Limited.value),
745
+ : formatTimeSpan(translate, translateMap, format, ResponsiveTextSize.Full, burningTime.Limited.unit, burningTime.Limited.value),
746
746
  })),
747
747
  mapNullable(baseItemTranslation?.color, color => ({
748
748
  label: translate("Color"),
@@ -17,16 +17,19 @@ const getTextForBlessingTraditions = (deps, id) => {
17
17
  const blessedTraditions = deps.getAllInstances("BlessedTradition");
18
18
  const primaryTradition = blessedTraditions.find(trad => trad.content.primaryBlessing === id);
19
19
  const secondaryTraditions = blessedTraditions.filter(trad => {
20
- switch (trad.content.restricted_blessings?.kind) {
21
- case "Six":
22
- return !trad.content.restricted_blessings.Six.includes(id);
23
- case "Three":
24
- return !trad.content.restricted_blessings.Three.includes(id);
25
- case undefined:
26
- return false;
27
- default:
28
- return assertExhaustive(trad.content.restricted_blessings);
20
+ if (trad.content.type.kind === "Church") {
21
+ switch (trad.content.type.Church.restrictedBlessings?.kind) {
22
+ case "Six":
23
+ return !trad.content.type.Church.restrictedBlessings.Six.includes(id);
24
+ case "Three":
25
+ return !trad.content.type.Church.restrictedBlessings.Three.includes(id);
26
+ case undefined:
27
+ return false;
28
+ default:
29
+ return assertExhaustive(trad.content.type.Church.restrictedBlessings);
30
+ }
29
31
  }
32
+ return false;
30
33
  });
31
34
  const translateTradition = (trad) => {
32
35
  const traditionTranslation = deps.translateMap(trad.content.translations);
@@ -84,6 +87,7 @@ const getTextForTraditions = (deps, values) => {
84
87
  }
85
88
  })
86
89
  .filter(isNotNullish)
90
+ .sort(deps.localeCompare)
87
91
  .join(", ");
88
92
  return {
89
93
  label: deps.translate("Traditions"),
@@ -94,7 +98,7 @@ const getTextForTraditions = (deps, values) => {
94
98
  * Get a JSON representation of the rules text for a blessing.
95
99
  */
96
100
  export const getBlessingEntityDescription = createEntityDescriptionCreator(({ getInstanceById, getAllInstances }, locale, { id, content: entry }) => {
97
- const { translate, translateMap } = locale;
101
+ const { translate, translateMap, format } = locale;
98
102
  const translation = translateMap(entry.translations);
99
103
  if (translation === undefined) {
100
104
  return undefined;
@@ -102,6 +106,7 @@ export const getBlessingEntityDescription = createEntityDescriptionCreator(({ ge
102
106
  const env = {
103
107
  translate,
104
108
  translateMap,
109
+ format,
105
110
  localeCompare: locale.compare,
106
111
  getInstanceById,
107
112
  getAllInstances,
@@ -135,7 +140,7 @@ export const getBlessingEntityDescription = createEntityDescriptionCreator(({ ge
135
140
  * Get a JSON representation of the rules text for a liturgical chant.
136
141
  */
137
142
  export const getLiturgicalChantEntityDescription = createEntityDescriptionCreator(({ getInstanceById, getChildInstancesForInstanceId, idMap }, locale, { content: entry, entity, id }, options) => {
138
- const { translate, translateMap, compare: localeCompare, join: localeJoin } = locale;
143
+ const { translate, translateMap, format, compare: localeCompare, join: localeJoin } = locale;
139
144
  const translation = translateMap(entry.translations);
140
145
  if (translation === undefined) {
141
146
  return undefined;
@@ -143,6 +148,7 @@ export const getLiturgicalChantEntityDescription = createEntityDescriptionCreato
143
148
  const env = {
144
149
  translate,
145
150
  translateMap,
151
+ format,
146
152
  getInstanceById,
147
153
  getChildInstancesForInstanceId,
148
154
  localeJoin: locale.join,
@@ -198,7 +204,7 @@ export const getLiturgicalChantEntityDescription = createEntityDescriptionCreato
198
204
  * Get a JSON representation of the rules text for a ceremony.
199
205
  */
200
206
  export const getCeremonyEntityDescription = createEntityDescriptionCreator(({ getInstanceById, getChildInstancesForInstanceId, idMap }, locale, { content: entry, entity, id }, options) => {
201
- const { translate, translateMap, compare: localeCompare, join: localeJoin } = locale;
207
+ const { translate, translateMap, format, compare: localeCompare, join: localeJoin } = locale;
202
208
  const translation = translateMap(entry.translations);
203
209
  if (translation === undefined) {
204
210
  return undefined;
@@ -206,6 +212,7 @@ export const getCeremonyEntityDescription = createEntityDescriptionCreator(({ ge
206
212
  const env = {
207
213
  translate,
208
214
  translateMap,
215
+ format,
209
216
  getInstanceById,
210
217
  getChildInstancesForInstanceId,
211
218
  localeJoin: locale.join,
@@ -20,7 +20,7 @@ const getEnhancementAPValue = (adventurePoints, parentImprovementCost) => {
20
20
  };
21
21
  const renderEnhancement = (enhancement, parentImprovementCost) => translateMapR(enhancement.content.translations).thenW(translation => Reader.ask().map(env => translation === undefined
22
22
  ? undefined
23
- : `- ^[${translation.name}](entity: "Enhancement") (${env.translate("SR {$value}", { value: enhancement.content.skill_rating })}, ${env.translate("{$value} AP", { value: getEnhancementAPValue(enhancement.content.adventurePoints, parentImprovementCost) })}): ${translation.effect}${enhancement.content.prerequisites === undefined ? "" : ` ${env.translate(".input {$hiddenCount :number} {{Prerequisites}}", { hiddenCount: enhancement.content.prerequisites.length })}: ${printEnhancementPrerequisites(env.getInstanceById, { translate: env.translate, translateMap: env.translateMap, compare: env.localeCompare, join: env.localeJoin }, enhancement.content.prerequisites)}`}`));
23
+ : `- ^[${translation.name}](entity: "Enhancement") (${env.translate("SR {$value}", { value: enhancement.content.skill_rating })}, ${env.translate("{$value} AP", { value: getEnhancementAPValue(enhancement.content.adventurePoints, parentImprovementCost) })}): ${translation.effect.split("\n").join("\n ")}${enhancement.content.prerequisites === undefined ? "" : ` ${env.translate(".input {$hiddenCount :number} {{Prerequisites}}", { hiddenCount: enhancement.content.prerequisites.length })}: ${printEnhancementPrerequisites(env.getInstanceById, { translate: env.translate, translateMap: env.translateMap, compare: env.localeCompare, join: env.localeJoin }, enhancement.content.prerequisites)}`}`));
24
24
  /**
25
25
  * Render the enhancements section for an entity description, if applicable.
26
26
  */
@@ -33,7 +33,7 @@ export const renderEnhancements = (parentId, parentImprovementCost) => getChildI
33
33
  }
34
34
  return translateR("Enhancements").map((label) => ({
35
35
  type: "labeled",
36
- label,
36
+ label: `${label}:`,
37
37
  value: {
38
38
  type: "plain",
39
39
  text: nonNullishDescriptions.join("\n"),
@@ -1,51 +1,40 @@
1
- import type { MapStyle, ResponsiveTextOptional } from "@optolith/database-schema/gen";
2
- import type { LocaleMap, Translate, TranslateMap } from "../../helpers/translate.js";
3
- import { type StdReader } from "./reader.js";
1
+ import { Reader } from "@elyukai/utils/reader";
2
+ import type { ParameterMapStyle, ResponsiveTextOptional } from "@optolith/database-schema/gen";
3
+ import type { LocaleMap } from "../../helpers/translate.js";
4
+ import { type StdEnv, type StdReader } from "./reader.js";
4
5
  /**
5
6
  * Renders a map of options to a string.
6
7
  */
7
- export declare const renderMap: <M extends {
8
- options: MO[];
9
- style?: MapStyle;
10
- translations?: LocaleMap<MT>;
11
- }, MOT = M extends {
8
+ export declare const renderMap: <V, T, OT, VEnv, SVEnv>(map: {
12
9
  options: {
13
- translations?: LocaleMap<infer MOT_>;
10
+ value: V;
11
+ translations?: LocaleMap<OT>;
14
12
  }[];
15
- } ? MOT_ : never, MT = M extends {
16
- translations?: LocaleMap<infer MT_>;
17
- } ? MT_ : never, MO extends {
18
- translations?: LocaleMap<MOT>;
19
- } = M extends {
20
- options: (infer MO_ & {
21
- translations?: LocaleMap<MOT>;
22
- })[];
23
- } ? MO_ : never>(translate: Translate, translateMap: TranslateMap, map: M, getValue: (option: MO) => string | number, surroundValues: ((values: string) => string) | undefined, getLabel: (optionTranslation: MOT) => string, getListPrefix: (translation: MT) => string | undefined, getListSuffix: (translation: MT) => string | undefined, getReplacement: (translation: MT) => string | undefined, ...moreToAppend: ({
24
- surround: (values: string) => string;
25
- getAdditionalValue: (option: MO, optionTranslation: MOT) => string | number;
26
- } | undefined)[]) => string;
13
+ style: ParameterMapStyle;
14
+ translations?: LocaleMap<T>;
15
+ }, getValue: (option: V) => Reader<VEnv, string | number>, surroundValues: ((values: string) => Reader<SVEnv, string>) | undefined, getLabel: (optionTranslation: OT) => string, getListPrefix: (translation: T) => string | undefined, getListSuffix: (translation: T) => string | undefined, getReplacement: (translation: T) => string | undefined, ...moreToAppend: ({
16
+ surround: (values: string) => StdReader<string, "t">;
17
+ getAdditionalValue: (option: V, optionTranslation: OT) => string | number;
18
+ } | undefined)[]) => Reader<StdEnv<"t" | "tm"> & VEnv & SVEnv, string>;
27
19
  /**
28
20
  * Renders a map of options to a string.
29
21
  */
30
- export declare const renderResponsiveMap: <M extends {
31
- options: MO[];
32
- style?: MapStyle;
22
+ export declare const renderResponsiveMap: <T, VEnv, SVEnv>(map: {
23
+ options: {
24
+ value: T;
25
+ translations?: LocaleMap<{
26
+ label: ResponsiveTextOptional;
27
+ }>;
28
+ }[];
29
+ style: ParameterMapStyle;
33
30
  translations?: LocaleMap<{
34
- list_prepend?: ResponsiveTextOptional;
35
31
  listPrefix?: ResponsiveTextOptional;
36
- list_append?: ResponsiveTextOptional;
37
32
  listSuffix?: ResponsiveTextOptional;
38
33
  replacement?: ResponsiveTextOptional;
39
34
  }>;
40
- }, MO extends {
41
- translations?: LocaleMap<{
42
- label: ResponsiveTextOptional;
43
- }>;
44
- } = M extends {
45
- options: (infer MO_)[];
46
- } ? MO_ : never>(map: M, getValue: (option: MO) => string | number, surroundValues: ((values: string) => string) | undefined, ...moreToAppend: ({
47
- surround: (values: string) => string;
48
- getAdditionalValue: (option: MO, optionTranslation: {
35
+ }, getValue: (option: T) => Reader<VEnv, string | number>, surroundValues: ((values: string) => Reader<SVEnv, string>) | undefined, ...moreToAppend: ({
36
+ surround: (values: string) => StdReader<string, "t">;
37
+ getAdditionalValue: (option: T, optionTranslation: {
49
38
  label: ResponsiveTextOptional;
50
39
  }) => string | number;
51
- } | undefined)[]) => StdReader<string, "t" | "tm" | "rts">;
40
+ } | undefined)[]) => Reader<StdEnv<"t" | "tm" | "rts"> & VEnv & SVEnv, string>;
@@ -6,32 +6,32 @@ import { MISSING_VALUE } from "./unknown.js";
6
6
  /**
7
7
  * Renders a map of options to a string.
8
8
  */
9
- export const renderMap = (translate, translateMap, map, getValue, surroundValues = values => values, getLabel, getListPrefix, getListSuffix, getReplacement, ...moreToAppend) => {
10
- const translation = translateMap(map.translations);
9
+ export const renderMap = (map, getValue, surroundValues = values => Reader.of(values), getLabel, getListPrefix, getListSuffix, getReplacement, ...moreToAppend) => Reader.asks(env => {
10
+ const translation = env.translateMap(map.translations);
11
11
  const replacement = mapNullable(translation, t => getReplacement(t));
12
12
  if (isNotNullish(replacement)) {
13
13
  return replacement;
14
14
  }
15
- if (map.style?.kind === "Verbose") {
15
+ if (map.style.kind === "Verbose") {
16
16
  return map.options
17
17
  .map(option => {
18
- const value = getValue(option);
19
- const optionTranslation = translateMap(option.translations);
20
- return `${surroundValues(typeof value === "number" ? value.toFixed() : value)} ${translate("for")} ${mapNullableDefault(mapNullable(translation, getListPrefix), listPrefix => `${listPrefix} `, "")}${optionTranslation === undefined ? MISSING_VALUE : getLabel(optionTranslation)}${mapNullableDefault(mapNullable(translation, getListSuffix), listSuffix => ` ${listSuffix}`, "")}${moreToAppend
18
+ const value = getValue(option.value).run(env);
19
+ const optionTranslation = env.translateMap(option.translations);
20
+ return `${surroundValues(typeof value === "number" ? value.toFixed() : value).run(env)} ${env.translate("for")} ${mapNullableDefault(mapNullable(translation, getListPrefix), listPrefix => `${listPrefix} `, "")}${optionTranslation === undefined ? MISSING_VALUE : getLabel(optionTranslation)}${mapNullableDefault(mapNullable(translation, getListSuffix), listSuffix => ` ${listSuffix}`, "")}${moreToAppend
21
21
  .filter(isNotNullish)
22
22
  .map(({ surround, getAdditionalValue }) => {
23
23
  const additionalValue = optionTranslation === undefined
24
24
  ? MISSING_VALUE
25
- : getAdditionalValue(option, optionTranslation);
26
- return surround(typeof additionalValue === "number" ? additionalValue.toFixed() : additionalValue);
25
+ : getAdditionalValue(option.value, optionTranslation);
26
+ return surround(typeof additionalValue === "number" ? additionalValue.toFixed() : additionalValue).run(env);
27
27
  })
28
28
  .join("")}`;
29
29
  })
30
30
  .join(", ");
31
31
  }
32
- return `${surroundValues(map.options.map(option => getValue(option)).join("/"))} ${translate("for")} ${mapNullableDefault(mapNullable(translation, getListPrefix), listPrefix => `${listPrefix} `, "")}${map.options
32
+ return `${surroundValues(map.options.map(option => getValue(option.value).run(env)).join("/")).run(env)} ${env.translate("for")} ${mapNullableDefault(mapNullable(translation, getListPrefix), listPrefix => `${listPrefix} `, "")}${map.options
33
33
  .map(option => {
34
- const optionTranslation = translateMap(option.translations);
34
+ const optionTranslation = env.translateMap(option.translations);
35
35
  if (optionTranslation === undefined) {
36
36
  return MISSING_VALUE;
37
37
  }
@@ -41,16 +41,17 @@ export const renderMap = (translate, translateMap, map, getValue, surroundValues
41
41
  .filter(isNotNullish)
42
42
  .map(({ surround, getAdditionalValue }) => surround(map.options
43
43
  .map(option => {
44
- const optionTranslation = translateMap(option.translations);
44
+ const optionTranslation = env.translateMap(option.translations);
45
45
  if (optionTranslation === undefined) {
46
46
  return MISSING_VALUE;
47
47
  }
48
- return getAdditionalValue(option, optionTranslation);
48
+ return getAdditionalValue(option.value, optionTranslation);
49
49
  })
50
- .join("/")))
50
+ .join("/")).run(env))
51
51
  .join("")}`;
52
- };
52
+ });
53
53
  /**
54
54
  * Renders a map of options to a string.
55
55
  */
56
- export const renderResponsiveMap = (map, getValue, surroundValues, ...moreToAppend) => Reader.asks(({ translate, translateMap, responsiveTextSize }) => renderMap(translate, translateMap, map, getValue, surroundValues, translation => getResponsiveTextOptional(translation.label, responsiveTextSize) ?? translation.label.full, translation => mapNullable(translation.listPrefix ?? translation.list_prepend, text => getResponsiveTextOptional(text, responsiveTextSize)), translation => mapNullable(translation.listSuffix ?? translation.list_append, text => getResponsiveTextOptional(text, responsiveTextSize)), translation => mapNullable(translation.replacement, text => getResponsiveTextOptional(text, responsiveTextSize)), ...moreToAppend));
56
+ export const renderResponsiveMap = (map, getValue, surroundValues, ...moreToAppend) => Reader.asks(env => renderMap(map, getValue, surroundValues, translation => getResponsiveTextOptional(translation.label, env.responsiveTextSize) ??
57
+ translation.label.full, translation => mapNullable(translation.listPrefix, text => getResponsiveTextOptional(text, env.responsiveTextSize)), translation => mapNullable(translation.listSuffix, text => getResponsiveTextOptional(text, env.responsiveTextSize)), translation => mapNullable(translation.replacement, text => getResponsiveTextOptional(text, env.responsiveTextSize)), ...moreToAppend).run(env));