@optolith/entity-descriptions 0.5.1 → 0.7.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
4
4
 
5
+ ## [0.7.1](https://github.com/Optolith/entity-descriptions/compare/v0.7.0...v0.7.1) (2026-09-09)
6
+ ## [0.7.0](https://github.com/Optolith/entity-descriptions/compare/v0.6.0...v0.7.0) (2026-09-08)
7
+
8
+ ### ⚠ BREAKING CHANGES
9
+
10
+ * combat type badges
11
+ * **deps:** upgrade to schema 0.56.0
12
+
13
+ ### build
14
+
15
+ * **deps:** upgrade to schema 0.56.0 ([b784991](https://github.com/Optolith/entity-descriptions/commit/b784991d8775bf4315e369891601bf37288b884f))
16
+
17
+ ### Features
18
+
19
+ * combat type badges ([f1824df](https://github.com/Optolith/entity-descriptions/commit/f1824dfd23ae34e6af7334e9e5182c9dbc45603f))
20
+ ## [0.6.0](https://github.com/Optolith/entity-descriptions/compare/v0.5.1...v0.6.0) (2026-09-08)
21
+
22
+ ### ⚠ BREAKING CHANGES
23
+
24
+ * **deps:** upgrade to schema 0.55.0
25
+
26
+ ### build
27
+
28
+ * **deps:** upgrade to schema 0.55.0 ([0dff095](https://github.com/Optolith/entity-descriptions/commit/0dff0950d939beec0e688e792c58d6d0a2ce44d7))
29
+
30
+ ### Bug Fixes
31
+
32
+ * restricted blessings for traditions ([ca26c4c](https://github.com/Optolith/entity-descriptions/commit/ca26c4cadc77198b164967074735707ac6ec8593)), closes [#42](https://github.com/Optolith/entity-descriptions/issues/42)
5
33
  ## [0.5.1](https://github.com/Optolith/entity-descriptions/compare/v0.5.0...v0.5.1) (2026-09-08)
6
34
  ## [0.5.0](https://github.com/Optolith/entity-descriptions/compare/v0.4.26...v0.5.0) (2026-09-08)
7
35
 
@@ -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";
@@ -100,7 +100,7 @@ const renderPenaltyValue = (getInstanceById, translate, translateMap, name, pena
100
100
  case "ByAttack": {
101
101
  const offset = penalty.ByAttack.initial_order ?? 1;
102
102
  return penalty.ByAttack.list
103
- .map((penaltyByAttack, index) => `${penaltyByAttack.value.toFixed()} (${renderPenaltyByAttackLabel(translate, penalty.ByAttack.attack_replacement, index + offset)})`)
103
+ .map((penaltyByAttack, index) => `${sign(penaltyByAttack.value)} (${renderPenaltyByAttackLabel(translate, penalty.ByAttack.attack_replacement, index + offset)})`)
104
104
  .join("; ");
105
105
  }
106
106
  case "DependsOnHitZone":
@@ -651,6 +651,37 @@ const renderFavoredSkillsSelection = (baseList, favoredSkillsSelection) => favor
651
651
  baseList,
652
652
  selection: names.join(", "),
653
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
+ }))));
654
685
  const renderFavoredSkillsSpecialRule = (favoredCombatTechniques, favoredSkills, favoredSkillsSelection) => translateR("Favored Skills").thenW(label => renderFavoredCombatTechniques(favoredCombatTechniques)
655
686
  .thenW(favoredCombatTechniquesText => renderFavoredSkillsBaseList(favoredCombatTechniquesText, favoredSkills))
656
687
  .thenW(list => renderFavoredSkillsSelection(list, favoredSkillsSelection))
@@ -670,7 +701,11 @@ const renderPrimaryAttributeSpecialRule = (traditionName, nameOfPeopleWithThisTr
670
701
  }))).map(description => ({
671
702
  text: description,
672
703
  }));
673
- 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)),
674
709
  favoredSkills === undefined
675
710
  ? Reader.of(undefined)
676
711
  : renderFavoredSkillsSpecialRule(favoredCombatTechniques, favoredSkills, favoredSkillsSelection),
@@ -691,14 +726,15 @@ export const getActivatableEntityDescription = createEntityDescriptionCreator(({
691
726
  translateMap,
692
727
  getInstanceById,
693
728
  localeCompare: locale.compare,
729
+ localeJoin: locale.join,
694
730
  };
695
731
  const baseEntry = entry;
696
732
  const wrappedId = Case(entityName, id);
697
- const additionalSpecialRules = entityName === "BlessedTradition"
698
- ? 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)
699
735
  : entityName === "MagicalTradition"
700
- ? renderAdditionalSpecialRules(translation.name, translation.nameOfSpellcasters, undefined, undefined, undefined, entry.primary).run(env)
701
- : [];
736
+ ? (baseList) => renderAdditionalSpecialRules(baseList, translation.name, translation.nameOfSpellcasters, undefined, undefined, undefined, undefined, entry.primary).run(env)
737
+ : constant([]);
702
738
  const makeTraditionName = entityName === "BlessedTradition" || entityName === "MagicalTradition"
703
739
  ? name => translate("Tradition ({$tradition})", { tradition: name })
704
740
  : name => name;
@@ -719,12 +755,23 @@ export const getActivatableEntityDescription = createEntityDescriptionCreator(({
719
755
  return assertExhaustive(usageType);
720
756
  }
721
757
  }),
758
+ badge: mapNullable(entityName === "CombatStyleSpecialAbility" || entityName === "AdvancedCombatSpecialAbility"
759
+ ? entry.type
760
+ : undefined, combatType => {
761
+ switch (combatType.kind) {
762
+ case "Armed":
763
+ return { type: "armedCombat", value: translate("AC") };
764
+ case "Unarmed":
765
+ return { type: "unarmedCombat", value: translate("UC") };
766
+ default:
767
+ return assertExhaustive(combatType);
768
+ }
769
+ }),
722
770
  className: "special-ability",
723
771
  body: [
724
772
  mapNullable(translation.special_rules, specialRules => ({
725
773
  type: "plain",
726
- text: specialRules
727
- .concat(additionalSpecialRules)
774
+ text: addAdditionalSpecialRules(specialRules)
728
775
  .map(specialRule => specialRule.label === undefined
729
776
  ? `- ${specialRule.text}`
730
777
  : `- *${specialRule.label}*: ${specialRule.text}`)
@@ -11,7 +11,7 @@ export const getFocusRuleEntityDescription = createEntityDescriptionCreator(({ g
11
11
  }
12
12
  return {
13
13
  title: translation.name,
14
- badge: entry.level,
14
+ badge: { type: "level", value: romanize(entry.level) },
15
15
  className: "focus-rule",
16
16
  body: [
17
17
  {
@@ -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);
@@ -471,25 +471,27 @@ const renderRestrictedBlessings = (restrictedBlessings) => {
471
471
  return assertExhaustive(restrictedBlessings);
472
472
  }
473
473
  };
474
- const renderSingleBlessings = (traditions) => Reader.traverse(traditions, traditionId => getInstanceByIdR("BlessedTradition", traditionId).map(tradition => tradition
475
- ? { restrictedBlessings: tradition.restricted_blessings, type: tradition.type }
476
- : undefined)).thenW(restrictedBlessingsList => !isNotEmpty(restrictedBlessingsList)
474
+ const renderSingleBlessings = (traditions) => Reader.traverse(traditions, traditionId => getInstanceByIdR("BlessedTradition", traditionId).map(tradition => tradition?.type))
475
+ .map(traditionTypes => traditionTypes.filter(isNotNullish))
476
+ .thenW(traditionTypes => !isNotEmpty(traditionTypes)
477
477
  ? Reader.of(undefined)
478
- : restrictedBlessingsList.length === 1
478
+ : traditionTypes.length === 1
479
479
  ? (() => {
480
- const [first] = restrictedBlessingsList;
481
- const { restrictedBlessings, type } = first ?? {};
482
- return type?.kind === "Shamanistic"
483
- ? Reader.of(undefined)
484
- : restrictedBlessings === undefined
485
- ? translateR("The Twelve Blessings")
486
- : translateR("The Twelve Blessings").thenW(base => renderRestrictedBlessings(restrictedBlessings).thenW(list => translateR("except for {$list :list type=conjunction}", {
487
- list,
488
- }).map(note => base + parensIf(note))));
480
+ const [first] = traditionTypes;
481
+ if (first.kind === "Shamanistic") {
482
+ return Reader.of(undefined);
483
+ }
484
+ const { restrictedBlessings } = first.Church;
485
+ return restrictedBlessings === undefined
486
+ ? translateR("The Twelve Blessings")
487
+ : translateR("The Twelve Blessings").thenW(base => renderRestrictedBlessings(restrictedBlessings).thenW(list => translateR("except for {$list :list type=conjunction}", {
488
+ list,
489
+ }).map(note => base + parensIf(note))));
489
490
  })()
490
- : restrictedBlessingsList.every(trad => trad?.type.kind === "Shamanistic")
491
+ : traditionTypes.every(trad => trad.kind === "Shamanistic")
491
492
  ? Reader.of(undefined)
492
- : restrictedBlessingsList.some(trad => trad?.restrictedBlessings !== undefined || trad?.type.kind === "Shamanistic")
493
+ : traditionTypes.some(trad => (trad.kind === "Church" && trad.Church.restrictedBlessings !== undefined) ||
494
+ trad.kind === "Shamanistic")
493
495
  ? translateR("The Twelve Blessings").map2(translateR("depends on selected tradition"), (base, note) => base + parensIf(note))
494
496
  : translateR("The Twelve Blessings"));
495
497
  const renderBlessings = (traditions) => {