@optolith/entity-descriptions 0.13.1 → 0.14.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 (42) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/lib/creator.js +1 -1
  3. package/lib/entities/activatable.d.ts +1 -1
  4. package/lib/entities/activatable.js +28 -18
  5. package/lib/entities/culture.js +3 -3
  6. package/lib/entities/curriculum.js +1 -1
  7. package/lib/entities/elixir.js +1 -1
  8. package/lib/entities/equipment.js +1 -1
  9. package/lib/entities/liturgicalChant.d.ts +2 -2
  10. package/lib/entities/liturgicalChant.js +4 -0
  11. package/lib/entities/partial/adventurePointsValue.js +8 -7
  12. package/lib/entities/partial/dice.js +2 -2
  13. package/lib/entities/partial/enhancements.js +2 -2
  14. package/lib/entities/partial/herbary.js +1 -1
  15. package/lib/entities/partial/prerequisites/index.d.ts +6 -1
  16. package/lib/entities/partial/prerequisites/index.js +24 -1
  17. package/lib/entities/partial/prerequisites/single/personalityTrait.js +1 -1
  18. package/lib/entities/partial/prerequisites/single/ratedSum.js +1 -1
  19. package/lib/entities/partial/profession/options/combatTechniquesForAdventurePoints.d.ts +16 -0
  20. package/lib/entities/partial/profession/options/combatTechniquesForAdventurePoints.js +43 -0
  21. package/lib/entities/partial/profession/options/numberOfSpellworksTotalingAdventurePoints.d.ts +16 -0
  22. package/lib/entities/partial/profession/options/numberOfSpellworksTotalingAdventurePoints.js +39 -0
  23. package/lib/entities/partial/profession/packages.d.ts +67 -0
  24. package/lib/entities/partial/profession/packages.js +84 -0
  25. package/lib/entities/partial/profession/variants.d.ts +21 -0
  26. package/lib/entities/partial/profession/variants.js +31 -0
  27. package/lib/entities/partial/rated/activatable/cost.js +11 -9
  28. package/lib/entities/partial/rated/activatable/effect.js +2 -2
  29. package/lib/entities/partial/reader.d.ts +25 -1
  30. package/lib/entities/partial/reader.js +20 -0
  31. package/lib/entities/partial/units/energy.js +2 -1
  32. package/lib/entities/partial/units/length.js +6 -3
  33. package/lib/entities/partial/units/timeSpan.js +11 -7
  34. package/lib/entities/partial/unknown.d.ts +4 -0
  35. package/lib/entities/partial/unknown.js +4 -0
  36. package/lib/entities/poison.js +1 -1
  37. package/lib/entities/profession.js +44 -62
  38. package/lib/entities/spell.d.ts +2 -2
  39. package/lib/entities/spell.js +3 -1
  40. package/lib/index.d.ts +69 -68
  41. package/lib/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
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.14.0](https://github.com/Optolith/entity-descriptions/compare/v0.13.2...v0.14.0) (2026-09-24)
6
+
7
+ ### ⚠ BREAKING CHANGES
8
+
9
+ * **deps:** upgrade to schema 0.65.0
10
+
11
+ ### build
12
+
13
+ * **deps:** upgrade to schema 0.65.0 ([cb94c96](https://github.com/Optolith/entity-descriptions/commit/cb94c96df8dc3f1d4f5476db729df2ed85ce6b1d))
14
+
15
+ ### Bug Fixes
16
+
17
+ * missing adventure points label for AP value by level ([9aabd43](https://github.com/Optolith/entity-descriptions/commit/9aabd437d587e7b559c18a730249ea3c66bb8387))
18
+ ## [0.13.2](https://github.com/Optolith/entity-descriptions/compare/v0.13.1...v0.13.2) (2026-09-16)
19
+
20
+ ### Features
21
+
22
+ * add focus rule badges to activatable entries, spellworks and liturgical chants and ceremonies ([dd142b4](https://github.com/Optolith/entity-descriptions/commit/dd142b47f3cd29620d29a2561e4a8ae8ad1d2a2d)), closes [#41](https://github.com/Optolith/entity-descriptions/issues/41)
5
23
  ## [0.13.1](https://github.com/Optolith/entity-descriptions/compare/v0.13.0...v0.13.1) (2026-09-16)
6
24
 
7
25
  ### Bug Fixes
package/lib/creator.js CHANGED
@@ -64,7 +64,7 @@ export const createEntityDescriptionCreator = (fn) => (databaseAccessors, locale
64
64
  ...rawEntry,
65
65
  body: rawEntry.body.filter(isNotNullish).map(mapRawSection),
66
66
  errata: rawEntry.errata?.map(({ date, description }) => ({
67
- date: locale.formatDate(date),
67
+ date: locale.formatDate(new Date(date)),
68
68
  description: description.trim(),
69
69
  })),
70
70
  references: rawEntry.references === undefined
@@ -51,7 +51,7 @@ export type BaseActivatableTranslation = {
51
51
  * Get a JSON representation of the rules text for a special ability.
52
52
  */
53
53
  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", {
54
- getInstanceById: GetInstanceById<"Publication" | "Subject" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "TradeSecret" | "Aspect" | "Property" | "Race" | "Culture" | "State" | "Enhancement" | "PactCategory" | "PactDomain" | "SocialStatus" | "Weapon" | "Patron" | "PersonalityTrait" | "Brew" | "Blessing" | "Cantrip">;
54
+ getInstanceById: GetInstanceById<"Publication" | "Subject" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "TradeSecret" | "Aspect" | "Property" | "Race" | "Culture" | "State" | "Enhancement" | "PactCategory" | "PactDomain" | "SocialStatus" | "Weapon" | "Patron" | "PersonalityTrait" | "Brew" | "Blessing" | "Cantrip" | "FocusRule">;
55
55
  getAllInstances: GetAllInstances<"Script">;
56
56
  getResolvedSelectOptionById: GetResolvedSelectOptionById;
57
57
  getAllResolvedSelectOptions: GetAllResolvedSelectOptions;
@@ -18,7 +18,7 @@ import { renderAdventurePointsValue } from "./partial/adventurePointsValue.js";
18
18
  import { renderParameterMap } from "./partial/map.js";
19
19
  import { attributedName } from "./partial/markdown.js";
20
20
  import { additionFormatter } from "./partial/mathOperation.js";
21
- import { printAdvantageDisadvantagePrerequisites, printGeneralPrerequisites, } from "./partial/prerequisites/index.js";
21
+ import { getDerivedFocusRuleBadgeFromPrerequisites, printAdvantageDisadvantagePrerequisites, printGeneralPrerequisites, } from "./partial/prerequisites/index.js";
22
22
  import { printActivatableName, } from "./partial/prerequisites/single/activatable.js";
23
23
  import { renderStandaloneCostMap } from "./partial/rated/activatable/cost.js";
24
24
  import { parensIf } from "./partial/rated/activatable/parensIf.js";
@@ -44,6 +44,10 @@ const renderAdvantageDisadvantageSubtype = (subtype) => {
44
44
  return translateR("Magical Rank");
45
45
  case "MagicalTitle":
46
46
  return translateR("Magical Title");
47
+ case "BlessedRank":
48
+ return translateR("Blessed Title");
49
+ case "BlessedTitle":
50
+ return translateR("Blessed Title");
47
51
  default:
48
52
  return assertExhaustive(subtype);
49
53
  }
@@ -311,7 +315,9 @@ const renderVolumeValue = (translate, translateMap, getAllResolvedSelectOptions,
311
315
  levels: volume.ByLevel.list.map((_, index) => romanize(index + 1)).join("/"),
312
316
  });
313
317
  case "Map":
314
- return renderParameterMap(volume.Map, option => Reader.of(option.points), values => translateR("{$points} points", { points: values })).run({
318
+ return renderParameterMap(volume.Map, option => Reader.of(option.points), values => translateR("{$points} points", {
319
+ points: typeof values === "number" ? values.toFixed() : values,
320
+ })).run({
315
321
  translate,
316
322
  translateMap,
317
323
  responsiveTextSize,
@@ -342,7 +348,7 @@ const renderArcaneEnergyCost = (translate, translateMap, format, localeJoin, res
342
348
  ? value => translate(".input {$value :number} {{{$value} permanent AE}}", {
343
349
  value,
344
350
  })
345
- : value => translate("{$value} AE", { value });
351
+ : value => translate("{$value} AE", { value: value.toFixed() });
346
352
  const { interval } = cost.Fixed;
347
353
  const wrapInInterval = interval === undefined
348
354
  ? str => str
@@ -370,40 +376,40 @@ const renderArcaneEnergyCost = (translate, translateMap, format, localeJoin, res
370
376
  return wrapInPerLevel(value => wrapInInterval(translationWrapper(value))) + noteInParens;
371
377
  }
372
378
  case "Constant":
373
- return (translate("{$value} AE", { value: cost.Constant.value }) +
379
+ return (translate("{$value} AE", { value: cost.Constant.value.toFixed() }) +
374
380
  (cost.Constant.permanent_value === undefined
375
381
  ? ""
376
382
  : translate(", {$value} of which are permanent", {
377
- value: cost.Constant.permanent_value,
383
+ value: cost.Constant.permanent_value.toFixed(),
378
384
  })));
379
385
  case "PerCountable": {
380
386
  const translation = translateMap(cost.PerCountable.translations);
381
387
  return ((cost.PerCountable.base_value === undefined
382
388
  ? ""
383
- : `${translate("{$value} AE", { value: cost.PerCountable.base_value })} + `) +
389
+ : `${translate("{$value} AE", { value: cost.PerCountable.base_value.toFixed() })} + `) +
384
390
  translate("{$cost} per {$countable}", {
385
- cost: translate("{$value} AE", { value: cost.PerCountable.value }),
391
+ cost: translate("{$value} AE", { value: cost.PerCountable.value.toFixed() }),
386
392
  countable: getResponsiveText(translation?.per, responsiveTextSize),
387
393
  }) +
388
394
  parensIf(mapNullable(translation?.note, note => getResponsiveTextOptional(note, responsiveTextSize))));
389
395
  }
390
396
  case "Interval":
391
397
  return translate("{$cost} per {$interval}", {
392
- cost: translate("{$value} AE", { value: cost.Interval.value }),
398
+ cost: translate("{$value} AE", { value: cost.Interval.value.toFixed() }),
393
399
  interval: formatTimeSpan(translate, translateMap, format, responsiveTextSize, cost.Interval.interval.unit, cost.Interval.interval.value, true),
394
400
  });
395
401
  case "ActivationAndHalfInterval":
396
402
  return additionFormatter(translate("{$value} AE", {
397
- value: cost.ActivationAndHalfInterval.value,
403
+ value: cost.ActivationAndHalfInterval.value.toFixed(),
398
404
  }) + parensIf(translate("activation")), translate("{$cost} per {$interval}", {
399
405
  cost: translate("{$value} AE", {
400
- value: Math.round(cost.ActivationAndHalfInterval.value / 2),
406
+ value: Math.round(cost.ActivationAndHalfInterval.value / 2).toFixed(),
401
407
  }),
402
408
  interval: formatTimeSpan(translate, translateMap, format, responsiveTextSize, cost.ActivationAndHalfInterval.interval.unit, cost.ActivationAndHalfInterval.interval.value, true),
403
409
  }));
404
410
  case "Indefinite":
405
411
  return ((mapNullable(translateMap(cost.Indefinite.translations)?.description, description => getResponsiveText(description, responsiveTextSize)) ?? MISSING_VALUE) +
406
- mapNullableDefault(cost.Indefinite.modifier, modifier => ` + ${translate("{$value} AE", { value: modifier.value })}`, ""));
412
+ mapNullableDefault(cost.Indefinite.modifier, modifier => ` + ${translate("{$value} AE", { value: modifier.value.toFixed() })}`, ""));
407
413
  case "Disjunction":
408
414
  return translate("{$value} AE", {
409
415
  value: localeJoin(cost.Disjunction.options.map(option => option.value.toFixed() +
@@ -430,7 +436,7 @@ const renderArcaneEnergyCost = (translate, translateMap, format, localeJoin, res
430
436
  case "Verbose":
431
437
  return cost.ByLevel.levels
432
438
  .map((level, index) => translate("{$cost} for level {$level}", {
433
- cost: translate("{$value} AE", { value: level.value }),
439
+ cost: translate("{$value} AE", { value: level.value.toFixed() }),
434
440
  level: romanize(index + 1),
435
441
  }))
436
442
  .join("; ");
@@ -454,7 +460,9 @@ const renderBindingCost = (translate, translateMap, localeCompare, getAllResolve
454
460
  }),
455
461
  });
456
462
  case "Map":
457
- return renderParameterMap(cost.Map, option => Reader.of(option.permanentValue), values => translateR("{$value} permanent AE", { value: values })).run({
463
+ return renderParameterMap(cost.Map, option => Reader.of(option.permanentValue), values => translateR("{$value} permanent AE", {
464
+ value: typeof values === "number" ? values.toFixed() : values,
465
+ })).run({
458
466
  translate,
459
467
  translateMap,
460
468
  responsiveTextSize,
@@ -481,12 +489,12 @@ const renderBindingCost = (translate, translateMap, localeCompare, getAllResolve
481
489
  const renderLifePointsCost = (translate, cost) => cost === undefined
482
490
  ? ""
483
491
  : // eslint-disable-next-line no-irregular-whitespace
484
- ` (+ ${translate("{$value} LP", { value: cost.Fixed.value })})`;
492
+ ` (+ ${translate("{$value} LP", { value: cost.Fixed.value.toFixed() })})`;
485
493
  const renderCost = (translate, translateMap, format, localeJoin, localeCompare, getAllResolvedSelectOptions, responsiveTextSize, levels, cost) => {
486
494
  if (typeof cost === "number") {
487
495
  return {
488
496
  label: translate("AE Cost"),
489
- value: translate("{$value} AE", { value: cost }),
497
+ value: translate("{$value} AE", { value: cost.toFixed() }),
490
498
  };
491
499
  }
492
500
  switch (cost.kind) {
@@ -826,9 +834,10 @@ export const getActivatableEntityDescription = createEntityDescriptionCreator(({
826
834
  title: addLevels(makeTraditionName(translation.name_in_library ?? translation.name + parensIf(translation.additionalName))),
827
835
  subtitle: mapNullable(baseEntry.subtype, subtype => renderAdvantageDisadvantageSubtype(subtype).run(env)) ??
828
836
  mapNullable(baseEntry.usage_type, usageType => renderCombatSpecialAbilityType(usageType).run(env)),
829
- badge: mapNullable(entityName === "CombatStyleSpecialAbility" || entityName === "AdvancedCombatSpecialAbility"
837
+ badge: mapNullable(entityName === "CombatStyleSpecialAbility" ||
838
+ entityName === "AdvancedCombatSpecialAbility"
830
839
  ? entry.type
831
- : undefined, combatType => {
840
+ : undefined, (combatType) => {
832
841
  switch (combatType.kind) {
833
842
  case "Armed":
834
843
  return { type: "armedCombat", value: translate("AC") };
@@ -837,7 +846,8 @@ export const getActivatableEntityDescription = createEntityDescriptionCreator(({
837
846
  default:
838
847
  return assertExhaustive(combatType);
839
848
  }
840
- }),
849
+ }) ??
850
+ mapNullable(baseEntry.prerequisites, getDerivedFocusRuleBadgeFromPrerequisites)?.run(env),
841
851
  className: "special-ability",
842
852
  body: [
843
853
  mapNullable(translation.special_rules, specialRules => ({
@@ -248,14 +248,14 @@ export const getCultureEntityDescription = createEntityDescriptionCreator(({ get
248
248
  if (allSame(processedScripts, on(([, apValue]) => apValue, equal))) {
249
249
  return (localeJoin(processedScripts.map(([name]) => name).toSorted(localeCompare), "disjunction") +
250
250
  parensIf(translate("{$value} AP", {
251
- value: processedScripts[0][1],
251
+ value: processedScripts[0][1].toFixed(),
252
252
  })));
253
253
  }
254
254
  else {
255
255
  return localeJoin(processedScripts
256
256
  .map(([name, apValue]) => name +
257
257
  parensIf(translate("{$value} AP", {
258
- value: apValue,
258
+ value: apValue.toFixed(),
259
259
  })))
260
260
  .toSorted(localeCompare), "disjunction");
261
261
  }
@@ -306,7 +306,7 @@ export const getCultureEntityDescription = createEntityDescriptionCreator(({ get
306
306
  type: "labeled",
307
307
  label: translate("Cultural Package {$cultureName}", {
308
308
  cultureName: translation.name,
309
- }) + parensIf(translate("{$value} AP", { value: culturalPackageApValue })),
309
+ }) + parensIf(translate("{$value} AP", { value: culturalPackageApValue.toFixed() })),
310
310
  value: {
311
311
  type: "plain",
312
312
  text: culturePackageText,
@@ -183,7 +183,7 @@ const renderAbilityAdjustment = (translate, translateMap, getInstanceById, baseP
183
183
  const printedBaseValue = mapNullable(renderPrintedAbilityAdjustmentBaseValue(abilityAdjustment), value => value + defaultValue);
184
184
  return translate("{$replacement} instead of {$base}", {
185
185
  base: printedBaseValue === basePoints
186
- ? basePoints
186
+ ? basePoints.toFixed()
187
187
  : `<ins>${basePoints.toFixed()}</ins><del>${printedBaseValue?.toFixed() ?? "n/a"}</del>`,
188
188
  replacement: `${renderAbilityAdjustmentName(translateMap, getInstanceById, abilityAdjustment)} ${(basePoints + renderAbilityAdjustmentModifierValue(abilityAdjustment)).toFixed()}`,
189
189
  });
@@ -54,7 +54,7 @@ export const getElixirEntityDescription = createEntityDescriptionCreator(({ getI
54
54
  {
55
55
  label: `${translate("AP Value")} (${translate("Trade Secret")})`,
56
56
  value: translate("{$value} AP", {
57
- value: entry.trade_secret.ap_value,
57
+ value: entry.trade_secret.ap_value.toFixed(),
58
58
  }) +
59
59
  parensIf(mapNullable(entry.trade_secret.prerequisites, prerequisites => `${translate("Prerequisites")}: ${printPlainGeneralPrerequisites(prerequisites).run(env)}`)),
60
60
  },
@@ -239,7 +239,7 @@ const renderComplexity = (translate, complexity) => mapNullable(complexity, c =>
239
239
  return translate("Simple");
240
240
  case "Complex":
241
241
  return `${translate("Complex")} (${translate("{$value} AP", {
242
- value: c.Complex.ap_value,
242
+ value: c.Complex.ap_value.toFixed(),
243
243
  })})`;
244
244
  case "Various":
245
245
  return translate("Various");
@@ -12,7 +12,7 @@ export declare const getBlessingEntityDescription: import("../creator.js").Entit
12
12
  * Get a JSON representation of the rules text for a liturgical chant.
13
13
  */
14
14
  export declare const getLiturgicalChantEntityDescription: import("../creator.js").EntityDescriptionCreator<"LiturgicalChant", {
15
- getInstanceById: GetInstanceById<"Publication" | "Attribute" | "SkillModificationLevel" | "TargetCategory" | "Aspect" | "BlessedTradition" | "DerivedCharacteristic" | RatedIdentifier["kind"] | "Enhancement">;
15
+ getInstanceById: GetInstanceById<"Publication" | "Attribute" | "SkillModificationLevel" | "TargetCategory" | "Aspect" | "BlessedTradition" | "DerivedCharacteristic" | RatedIdentifier["kind"] | "Enhancement" | "FocusRule">;
16
16
  getChildInstancesForInstanceId: GetAllChildInstancesForParent<"Enhancement">;
17
17
  idMap: IdMap;
18
18
  }, import("../index.js").EntityDescription>;
@@ -20,7 +20,7 @@ export declare const getLiturgicalChantEntityDescription: import("../creator.js"
20
20
  * Get a JSON representation of the rules text for a ceremony.
21
21
  */
22
22
  export declare const getCeremonyEntityDescription: import("../creator.js").EntityDescriptionCreator<"Ceremony", {
23
- getInstanceById: GetInstanceById<"Publication" | "Attribute" | "SkillModificationLevel" | "TargetCategory" | "Aspect" | "BlessedTradition" | "DerivedCharacteristic" | RatedIdentifier["kind"] | "Enhancement">;
23
+ getInstanceById: GetInstanceById<"Publication" | "Attribute" | "SkillModificationLevel" | "TargetCategory" | "Aspect" | "BlessedTradition" | "DerivedCharacteristic" | RatedIdentifier["kind"] | "Enhancement" | "FocusRule">;
24
24
  getChildInstancesForInstanceId: GetAllChildInstancesForParent<"Enhancement">;
25
25
  idMap: IdMap;
26
26
  }, import("../index.js").EntityDescription>;
@@ -1,9 +1,11 @@
1
+ import { mapNullable } from "@elyukai/utils/nullable";
1
2
  import { isNotNullish } from "@optolith/helpers/nullable";
2
3
  import { assertExhaustive } from "@optolith/helpers/typeSafety";
3
4
  import { Case } from "tsondb/schema/gen";
4
5
  import { createEntityDescriptionCreator } from "../creator.js";
5
6
  import { renderEnhancements } from "./partial/enhancements.js";
6
7
  import { attributedInstance, attributedName } from "./partial/markdown.js";
8
+ import { getDerivedFocusRuleBadgeFromPrerequisites } from "./partial/prerequisites/index.js";
7
9
  import { renderOneTimeDuration } from "./partial/rated/activatable/duration.js";
8
10
  import { renderEffect } from "./partial/rated/activatable/effect.js";
9
11
  import { combineGeneratedTextWithStaticTranslation, renderFastPerformanceParameters, renderSlowPerformanceParameters, } from "./partial/rated/activatable/index.js";
@@ -173,6 +175,7 @@ export const getLiturgicalChantEntityDescription = createEntityDescriptionCreato
173
175
  return {
174
176
  title: translation.name,
175
177
  className: "liturgical-chant",
178
+ badge: mapNullable(entry.prerequisites, getDerivedFocusRuleBadgeFromPrerequisites)?.run(env),
176
179
  body: [
177
180
  {
178
181
  type: "definitionList",
@@ -237,6 +240,7 @@ export const getCeremonyEntityDescription = createEntityDescriptionCreator(({ ge
237
240
  return {
238
241
  title: translation.name,
239
242
  className: "ceremony",
243
+ badge: mapNullable(entry.prerequisites, getDerivedFocusRuleBadgeFromPrerequisites)?.run(env),
240
244
  body: [
241
245
  {
242
246
  type: "definitionList",
@@ -35,7 +35,7 @@ const renderFixedSelectOptionsAdventurePointsValue = (locale, getAllSelectOption
35
35
  .toSorted(locale.compare), "conjunction"),
36
36
  ])
37
37
  .toSorted(on(item => item[1], locale.compare))
38
- .map(([apValue, itemNames]) => `${itemNames}: ${locale.translate("{$value} Adventure Points", { value: typeof apValue === "number" ? applyNegative(apValue) : apValue })}`)
38
+ .map(([apValue, itemNames]) => `${itemNames}: ${locale.translate("{$value} Adventure Points", { value: typeof apValue === "number" ? applyNegative(apValue).toFixed() : apValue })}`)
39
39
  .join("; ");
40
40
  }
41
41
  };
@@ -88,12 +88,13 @@ export const renderAdventurePointsValue = (locale, getNameForId, getNameForSelec
88
88
  if (entry.levels !== value.ByLevel.list.length) {
89
89
  return MISSING_VALUE;
90
90
  }
91
- const mainValue = `${translate("Level {$level}", {
91
+ const levelsPrefix = translate("Level {$level}", {
92
92
  level: Array.from({ length: entry.levels }, (_, index) => romanize(index + 1)).join("/"),
93
- })}: ${value.ByLevel.list.map(applyNegative).join("/")}`;
93
+ });
94
+ const mainValues = value.ByLevel.list.map(applyNegative).join("/");
94
95
  const { additionalBySizeCategory } = value.ByLevel;
95
96
  if (additionalBySizeCategory === undefined) {
96
- return mainValue;
97
+ return translate("{$value} Adventure Points", { value: mainValues });
97
98
  }
98
99
  else {
99
100
  const sizeCategories = ["tiny", "small", "medium", "large", "huge"];
@@ -101,7 +102,7 @@ export const renderAdventurePointsValue = (locale, getNameForId, getNameForSelec
101
102
  .map(sizeCategory => applyNegative(additionalBySizeCategory[sizeCategory]))
102
103
  .join("/");
103
104
  const labels = sizeCategories.map(sizeCategory => translate(sizeCategory)).join("/");
104
- return `${mainValue} + ${translate("{$values} AP for size category {$labels} (per level)", { values, labels })}`;
105
+ return `${levelsPrefix}: ${mainValues} + ${translate("{$values} AP for size category {$labels} (per level)", { values, labels })}`;
105
106
  }
106
107
  }
107
108
  case "DerivedFromSelection": {
@@ -281,13 +282,13 @@ export const renderAdventurePointsValue = (locale, getNameForId, getNameForSelec
281
282
  const labels = Array.from({ length: entry.maximum ?? 3 }, (_, index) => `${(index + 1).toFixed()}.`).join("/");
282
283
  if (entry.maximum !== undefined) {
283
284
  return translate("{$values} Adventure Points for the {$labels} purchase", {
284
- values,
285
+ values: values.join("/"),
285
286
  labels,
286
287
  });
287
288
  }
288
289
  else {
289
290
  return translate("{$values}/and so on Adventure Points for the {$labels}/and so on purchase", {
290
- values,
291
+ values: values.join("/"),
291
292
  labels,
292
293
  });
293
294
  }
@@ -3,11 +3,11 @@ import { translateR } from "./reader.js";
3
3
  /**
4
4
  * Renders a dice expression like "2D6" into a localized string.
5
5
  */
6
- export const renderDice = (translate, dice) => translate("{$count}D{$sides}", { count: dice.number, sides: dice.sides });
6
+ export const renderDice = (translate, dice) => translate("{$count}D{$sides}", { count: dice.number.toFixed(), sides: dice.sides.toFixed() });
7
7
  /**
8
8
  * Renders a dice expression like "2D6" into a localized string.
9
9
  */
10
- export const renderDiceR = (dice) => translateR("{$count}D{$sides}", { count: dice.number, sides: dice.sides });
10
+ export const renderDiceR = (dice) => translateR("{$count}D{$sides}", { count: dice.number.toFixed(), sides: dice.sides.toFixed() });
11
11
  /**
12
12
  * Renders a dice expression with an additional flat modifier, like "2D6+3" or "2D6-1".
13
13
  */
@@ -22,8 +22,8 @@ const renderEnhancement = (enhancement, parentImprovementCost) => {
22
22
  const { prerequisites } = enhancement.content;
23
23
  return translateMapR(enhancement.content.translations).thenW(translation => translation === undefined
24
24
  ? Reader.of(undefined)
25
- : translateR("SR {$value}", { value: enhancement.content.skill_rating }).thenW(sr => translateR("{$value} AP", {
26
- value: getEnhancementAPValue(enhancement.content.adventurePoints, parentImprovementCost),
25
+ : translateR("SR {$value}", { value: enhancement.content.skill_rating.toFixed() }).thenW(sr => translateR("{$value} AP", {
26
+ value: getEnhancementAPValue(enhancement.content.adventurePoints, parentImprovementCost).toFixed(),
27
27
  }).thenW(ap => (prerequisites === undefined
28
28
  ? Reader.of("")
29
29
  : translateR(".input {$hiddenCount :number} {{Prerequisites}}", {
@@ -47,7 +47,7 @@ export const renderChance = (translate, translateMap, item, includePercentage =
47
47
  (item.chance === undefined
48
48
  ? undefined
49
49
  : translate("{$valueRange} on {$dice}", {
50
- valueRange: item.chance === 5 ? 1 : `1–${(item.chance / 5).toFixed()}`,
50
+ valueRange: item.chance === 5 ? "1" : `1–${(item.chance / 5).toFixed()}`,
51
51
  dice: renderDice(translate, { number: 1, sides: 20 }),
52
52
  }) +
53
53
  (includePercentage
@@ -1,6 +1,7 @@
1
1
  import { Reader } from "@elyukai/utils/reader";
2
2
  import type { ActivatableIdentifier, AdvantageDisadvantagePrerequisites, AnimistPowerPrerequisites, ArcaneTraditionPrerequisites, DerivedCharacteristicPrerequisites, EnhancementPrerequisites, GeneralPrerequisites, GeodeRitualPrerequisites, InfluencePrerequisites, LanguagePrerequisites, LiturgyPrerequisites, PersonalityTraitPrerequisites, PlainGeneralPrerequisites, ProfessionPrerequisites, ProfessionVariantPrerequisites, PublicationPrerequisites, SpellworkPrerequisites } from "@optolith/database-schema/gen";
3
- import type { StdEnv } from "../../../env.js";
3
+ import type { StdEnv, StdReader } from "../../../env.js";
4
+ import type { RawEntityDescriptionBadge } from "../../../index.js";
4
5
  import { printAdvantageDisadvantagePrerequisiteGroup, printAnimistPowerPrerequisiteGroup, printArcaneTraditionPrerequisiteGroup, printDerivedCharacteristicPrerequisiteGroup, printEnhancementPrerequisiteGroup, printGeneralPrerequisiteGroup, printGeodeRitualPrerequisiteGroup, printInfluencePrerequisiteGroup, printLanguagePrerequisiteGroup, printLiturgyPrerequisiteGroup, printPersonalityTraitPrerequisiteGroup, printProfessionPrerequisiteGroup, printPublicationPrerequisiteGroup, printSpellworkPrerequisiteGroup } from "./prerequisiteGroups.js";
5
6
  type DeriveEnvF<T extends (...args: any[]) => any> = ReturnType<T> extends Reader<infer E, unknown> ? E : never;
6
7
  /**
@@ -70,4 +71,8 @@ export declare const printGeodeRitualPrerequisites: (value: GeodeRitualPrerequis
70
71
  * Print enhancement prerequisites as a string.
71
72
  */
72
73
  export declare const printEnhancementPrerequisites: (value: EnhancementPrerequisites) => Reader<DeriveEnvF<typeof printEnhancementPrerequisiteGroup> & StdEnv<"lj" | "lc">, string>;
74
+ /**
75
+ * Get the focus rule badge from prerequisites.
76
+ */
77
+ export declare const getDerivedFocusRuleBadgeFromPrerequisites: (prerequisites: AdvantageDisadvantagePrerequisites | GeneralPrerequisites | PlainGeneralPrerequisites) => StdReader<RawEntityDescriptionBadge | undefined, "ibi", "FocusRule">;
73
78
  export {};
@@ -7,7 +7,7 @@ import { isNotNullish } from "@optolith/helpers/nullable";
7
7
  import { romanize } from "@optolith/helpers/roman";
8
8
  import { assertExhaustive } from "@optolith/helpers/typeSafety";
9
9
  import { renderActivatableNameComponents, renderActivatableNameComponentsCombinedIfPossible, } from "../activatableNameChunks.js";
10
- import { localeJoinR, translateR } from "../reader.js";
10
+ import { getInstanceByIdR, localeJoinR, translateR } from "../reader.js";
11
11
  import { MISSING_VALUE } from "../unknown.js";
12
12
  import { printDisplayOption } from "./displayOption.js";
13
13
  import { hasPartValueObject, joinPrerequisiteParts } from "./part.js";
@@ -230,3 +230,26 @@ export const printGeodeRitualPrerequisites = (value) => printPlainPrerequisites(
230
230
  * Print enhancement prerequisites as a string.
231
231
  */
232
232
  export const printEnhancementPrerequisites = (value) => printPlainPrerequisites(printEnhancementPrerequisiteGroup, value);
233
+ const getDerivedFocusRuleBadgeFromPrerequisite = (prerequisite) => prerequisite.kind === "Single" &&
234
+ prerequisite.Single.kind === "Rule" &&
235
+ prerequisite.Single.Rule.id.kind === "FocusRule"
236
+ ? getInstanceByIdR(prerequisite.Single.Rule.id).map(focusRule => focusRule?.level)
237
+ : Reader.of(undefined);
238
+ /**
239
+ * Get the focus rule badge from prerequisites.
240
+ */
241
+ export const getDerivedFocusRuleBadgeFromPrerequisites = (prerequisites) => Reader.asks(env => {
242
+ for (const element of prerequisites) {
243
+ const actualPrerequisite = "level" in element ? (element.level === 1 ? element.prerequisite : undefined) : element;
244
+ if (actualPrerequisite !== undefined) {
245
+ const badge = getDerivedFocusRuleBadgeFromPrerequisite(actualPrerequisite).run(env);
246
+ if (badge !== undefined) {
247
+ return {
248
+ type: "level",
249
+ value: romanize(badge),
250
+ };
251
+ }
252
+ }
253
+ }
254
+ return undefined;
255
+ });
@@ -12,7 +12,7 @@ export const printPersonalityTraitPrerequisite = (prerequisite) => prerequisite.
12
12
  : attributedNameFromInstanceR(personalityTrait, "prerequisite", "PersonalityTrait", prerequisite.id)
13
13
  .map(name => name ?? MISSING_VALUE)
14
14
  .thenW(name => translateR("Level {$level}", {
15
- level: personalityTrait.level,
15
+ level: personalityTrait.level.toFixed(),
16
16
  }).map(level => `${name} (${level})`))
17
17
  .then(name => prerequisite.active
18
18
  ? translateR("must have {$trait}", {
@@ -13,7 +13,7 @@ export const printRatedSumPrerequisite = (prerequisite) => prerequisite.display_
13
13
  .thenW(skills => localeJoinR(skills, "conjunction"))
14
14
  .thenW(skills => translateR("the SR for {$skill} combined must add up to at least {$minRating}", {
15
15
  skill: skills,
16
- minRating: prerequisite.sum,
16
+ minRating: prerequisite.sum.toFixed(),
17
17
  }))
18
18
  .map(value => ({
19
19
  value,
@@ -0,0 +1,16 @@
1
+ import type { NonEmptyArray } from "@elyukai/utils/array/nonEmpty";
2
+ import { Reader } from "@elyukai/utils/reader";
3
+ import type { CombatTechniquesForAdventurePointsOption, VariantOptionAction } from "@optolith/database-schema/gen";
4
+ import type { StdEnv } from "../../../../env.js";
5
+ import { type PreparedProfessionPackage } from "../packages.js";
6
+ type Option = CombatTechniquesForAdventurePointsOption;
7
+ type OptionEnv = StdEnv<"t" | "tm" | "ibi", "CloseCombatTechnique" | "RangedCombatTechnique">;
8
+ /**
9
+ * Renders the combat techniques for adventure points option for a list of profession packages.
10
+ */
11
+ export declare const renderBaseCombatTechniquesForAdventurePointsOption: (professionPackages: NonEmptyArray<PreparedProfessionPackage>) => Reader<OptionEnv, string | undefined>;
12
+ /**
13
+ * Renders the combat techniques for adventure points option for a variant of a profession package.
14
+ */
15
+ export declare const renderVariantCombatTechniquesForAdventurePointsOption: (baseOptions: Option | undefined, variantOptions: VariantOptionAction<Option>) => Reader<OptionEnv, string>;
16
+ export {};
@@ -0,0 +1,43 @@
1
+ import { deepEqual, equal } from "@elyukai/utils/equality";
2
+ import { Reader } from "@elyukai/utils/reader";
3
+ import { attributedNameR, fixedNumberOrString, translateR } from "../../reader.js";
4
+ import { MISSING_VALUE } from "../../unknown.js";
5
+ import { renderParameterizedOptionForBaseAndVariant, renderParameterizedOptionForMultiplePackages, } from "../packages.js";
6
+ const renderCombatTechniquesForAdventurePointsOptionList = (options) => Reader.traverse(options, id => attributedNameR("profession", id).map(name => name ?? MISSING_VALUE)).map(list => list.join(", "));
7
+ const renderCombatTechniquesForAdventurePointsOptionText = (apValue, list) => translateR("{$apValue} AP to distribute among the following combat techniques: {$list}", {
8
+ apValue: fixedNumberOrString(apValue),
9
+ list,
10
+ });
11
+ const renderStandaloneOption = (option) => renderCombatTechniquesForAdventurePointsOptionList(option.options).thenW(list => renderCombatTechniquesForAdventurePointsOptionText(option.apValue.toFixed(), list));
12
+ const configs = [
13
+ {
14
+ selector: option => option.apValue,
15
+ equality: equal,
16
+ render: apValue => Reader.of(apValue),
17
+ renderAsString: apValue => Reader.of(apValue.toFixed()),
18
+ emptyText: "—",
19
+ joiner: "/",
20
+ },
21
+ {
22
+ selector: option => option.options,
23
+ equality: deepEqual,
24
+ render: renderCombatTechniquesForAdventurePointsOptionList,
25
+ renderAsString: renderCombatTechniquesForAdventurePointsOptionList,
26
+ emptyText: "—",
27
+ joiner: " / ",
28
+ },
29
+ ];
30
+ const render = (apValue, list) => translateR("{$apValue} AP to distribute among the following combat techniques: {$list}", {
31
+ apValue: fixedNumberOrString(apValue),
32
+ list,
33
+ });
34
+ /**
35
+ * Renders the combat techniques for adventure points option for a list of profession packages.
36
+ */
37
+ export const renderBaseCombatTechniquesForAdventurePointsOption = (professionPackages) => renderParameterizedOptionForMultiplePackages(professionPackages, pkg => pkg.content.options?.combatTechniquesForAdventurePoints, { type: "separate", emptyText: "—", joiner: " / " }, render, ...configs);
38
+ /**
39
+ * Renders the combat techniques for adventure points option for a variant of a profession package.
40
+ */
41
+ export const renderVariantCombatTechniquesForAdventurePointsOption = (baseOptions, variantOptions) => renderParameterizedOptionForBaseAndVariant(baseOptions, variantOptions, { type: "separate", emptyText: "—", joiner: " / " }, base => renderCombatTechniquesForAdventurePointsOptionList(base.options).thenW(list => translateR("no AP to distribute among the following combat techniques: {$list}", {
42
+ list,
43
+ })), renderStandaloneOption, render, ...configs);
@@ -0,0 +1,16 @@
1
+ import type { NonEmptyArray } from "@elyukai/utils/array/nonEmpty";
2
+ import { Reader } from "@elyukai/utils/reader";
3
+ import type { SpellworksOptions, VariantOptionAction } from "@optolith/database-schema/gen";
4
+ import type { StdEnv } from "../../../../env.js";
5
+ import { type PreparedProfessionPackage } from "../packages.js";
6
+ type Option = SpellworksOptions;
7
+ type OptionEnv = StdEnv<"t">;
8
+ /**
9
+ * Renders the combat techniques for adventure points option for a list of profession packages.
10
+ */
11
+ export declare const renderBaseNumberOfSpellworksTotalingAdventurePoints: (professionPackages: NonEmptyArray<PreparedProfessionPackage>) => Reader<OptionEnv, string | undefined>;
12
+ /**
13
+ * Renders the combat techniques for adventure points option for a variant of a profession package.
14
+ */
15
+ export declare const renderVariantNumberOfSpellworksTotalingAdventurePoints: (baseOptions: Option | undefined, variantOptions: VariantOptionAction<Option>) => Reader<OptionEnv, string>;
16
+ export {};
@@ -0,0 +1,39 @@
1
+ import { equal } from "@elyukai/utils/equality";
2
+ import { Reader } from "@elyukai/utils/reader";
3
+ import { fixedNumberOrString, translateR } from "../../reader.js";
4
+ import { renderParameterizedOptionForBaseAndVariant, renderParameterizedOptionForMultiplePackages, } from "../packages.js";
5
+ const configs = [
6
+ {
7
+ selector: option => option.number,
8
+ equality: equal,
9
+ render: apValue => Reader.of(apValue),
10
+ renderAsString: apValue => Reader.of(apValue.toFixed()),
11
+ emptyText: "—",
12
+ joiner: "/",
13
+ },
14
+ {
15
+ selector: option => option.apValue,
16
+ equality: equal,
17
+ render: apValue => Reader.of(apValue),
18
+ renderAsString: apValue => Reader.of(apValue.toFixed()),
19
+ emptyText: "—",
20
+ joiner: "/",
21
+ },
22
+ ];
23
+ const render = (count, apValue) => typeof count === "number"
24
+ ? translateR(".input {$count :number} {{{$count} more spellworks totaling {$apValue} AP}}", {
25
+ count,
26
+ apValue: fixedNumberOrString(apValue),
27
+ })
28
+ : translateR("{$count} more spellworks totaling {$apValue} AP", {
29
+ count,
30
+ apValue: fixedNumberOrString(apValue),
31
+ });
32
+ /**
33
+ * Renders the combat techniques for adventure points option for a list of profession packages.
34
+ */
35
+ export const renderBaseNumberOfSpellworksTotalingAdventurePoints = (professionPackages) => renderParameterizedOptionForMultiplePackages(professionPackages, pkg => pkg.content.options?.spellworks, { type: "separate", emptyText: "—", joiner: " / " }, render, ...configs);
36
+ /**
37
+ * Renders the combat techniques for adventure points option for a variant of a profession package.
38
+ */
39
+ export const renderVariantNumberOfSpellworksTotalingAdventurePoints = (baseOptions, variantOptions) => renderParameterizedOptionForBaseAndVariant(baseOptions, variantOptions, { type: "separate", emptyText: "—", joiner: " / " }, base => render(0, base.apValue), base => render(base.number, base.apValue), render, ...configs);