@optolith/entity-descriptions 0.8.0 → 0.9.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,31 @@
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.9.1](https://github.com/Optolith/entity-descriptions/compare/v0.9.0...v0.9.1) (2026-09-11)
6
+
7
+ ### Bug Fixes
8
+
9
+ * missing general poison note ([3d0085d](https://github.com/Optolith/entity-descriptions/commit/3d0085dbc1435bad216be21a421f31ee2ab5be69))
10
+ * missing unit in poison time spans ([942425f](https://github.com/Optolith/entity-descriptions/commit/942425f3ba9586671ab19f3be7b7cc2532746cc6))
11
+ ## [0.9.0](https://github.com/Optolith/entity-descriptions/compare/v0.8.0...v0.9.0) (2026-09-11)
12
+
13
+ ### ⚠ BREAKING CHANGES
14
+
15
+ * **deps:** upgrade to schema 0.58.0
16
+
17
+ ### build
18
+
19
+ * **deps:** upgrade to schema 0.58.0 ([fe93cf7](https://github.com/Optolith/entity-descriptions/commit/fe93cf72768cf397a671bd4e883b1bd18288697c))
20
+
21
+ ### Features
22
+
23
+ * use nameInAPValue for animal shape text generation ([d1c8017](https://github.com/Optolith/entity-descriptions/commit/d1c801791532bdcf1004c67d1e8ae5cb53ae4f07))
24
+
25
+ ### Bug Fixes
26
+
27
+ * render brew for cauldron enchantments ([866703a](https://github.com/Optolith/entity-descriptions/commit/866703a4500a9d6e47a59cf78919aa9ebcbca05a))
28
+ * render translation replacement if available ([6f890fd](https://github.com/Optolith/entity-descriptions/commit/6f890fd9885c070934c32db5dc5b745a86b46ac8))
29
+ * time interval display when value is 1 ([ada5cc7](https://github.com/Optolith/entity-descriptions/commit/ada5cc768286ced1a26bacf18a4c16a3d289aa40))
5
30
  ## [0.8.0](https://github.com/Optolith/entity-descriptions/compare/v0.7.2...v0.8.0) (2026-09-10)
6
31
 
7
32
  ### ⚠ BREAKING CHANGES
@@ -1,4 +1,4 @@
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";
1
+ import type { ActivatableIdentifier, AdvancedSpecialAbility, AdvantageDisadvantagePrerequisites, AdventurePointsValue, ApplicableCombatTechniques, Aspect_ID, Brew_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";
@@ -17,6 +17,7 @@ export type BaseActivatable = {
17
17
  penalty?: Penalty;
18
18
  combat_techniques?: ApplicableCombatTechniques;
19
19
  volume?: Volume;
20
+ brew?: Brew_ID;
20
21
  cost?: EnchantmentCost | DaggerRitualCost | MagicalSignCost | number;
21
22
  property?: PropertyDeclaration;
22
23
  aspect?: Aspect_ID;
@@ -47,7 +48,7 @@ export type BaseActivatableTranslation = {
47
48
  * Get a JSON representation of the rules text for a special ability.
48
49
  */
49
50
  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" | "Blessing">;
51
+ getInstanceById: GetInstanceById<"Publication" | "Subject" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "TradeSecret" | "Aspect" | "Property" | "Race" | "Culture" | "State" | "Enhancement" | "PactCategory" | "PactDomain" | "SocialStatus" | "Weapon" | "Patron" | "PersonalityTrait" | "Blessing" | "Brew">;
51
52
  getAllInstances: GetAllInstances<"Script">;
52
53
  getResolvedSelectOptionById: GetResolvedSelectOptionById;
53
54
  getAllResolvedSelectOptions: GetAllResolvedSelectOptions;
@@ -21,7 +21,7 @@ import { printAdvantageDisadvantagePrerequisites, printGeneralPrerequisites, } f
21
21
  import { printActivatableName, } from "./partial/prerequisites/single/activatable.js";
22
22
  import { renderStandaloneCostMap } from "./partial/rated/activatable/cost.js";
23
23
  import { parensIf } from "./partial/rated/activatable/parensIf.js";
24
- import { attributedNameR, localeSortR, translateR, } from "./partial/reader.js";
24
+ import { attributedNameR, localeSortR, nameR, translateR, } from "./partial/reader.js";
25
25
  import { getResponsiveText, getResponsiveTextOptional, ResponsiveTextSize, } from "./partial/responsiveText.js";
26
26
  import { formatTimeSpan } from "./partial/units/timeSpan.js";
27
27
  import { MISSING_VALUE } from "./partial/unknown.js";
@@ -305,6 +305,7 @@ const renderVolumeValue = (translate, translateMap, getAllResolvedSelectOptions,
305
305
  return assertExhaustive(volume);
306
306
  }
307
307
  };
308
+ const renderBrew = (brew) => nameR("Brew", brew).map(name => name ?? MISSING_VALUE);
308
309
  const renderArcaneEnergyCost = (translate, translateMap, format, localeJoin, responsiveTextSize, levels, cost) => {
309
310
  switch (cost.kind) {
310
311
  case "Fixed": {
@@ -318,7 +319,7 @@ const renderArcaneEnergyCost = (translate, translateMap, format, localeJoin, res
318
319
  ? str => str
319
320
  : str => translate("{$cost} per {$interval}", {
320
321
  cost: str,
321
- interval: formatTimeSpan(translate, translateMap, format, responsiveTextSize, interval.unit, interval.value),
322
+ interval: formatTimeSpan(translate, translateMap, format, responsiveTextSize, interval.unit, interval.value, true),
322
323
  });
323
324
  const wrapInPerLevel = (() => {
324
325
  if (cost.Fixed.per_level === undefined) {
@@ -360,7 +361,7 @@ const renderArcaneEnergyCost = (translate, translateMap, format, localeJoin, res
360
361
  case "Interval":
361
362
  return translate("{$cost} per {$interval}", {
362
363
  cost: translate("{$value} AE", { value: cost.Interval.value }),
363
- interval: formatTimeSpan(translate, translateMap, format, responsiveTextSize, cost.Interval.interval.unit, cost.Interval.interval.value),
364
+ interval: formatTimeSpan(translate, translateMap, format, responsiveTextSize, cost.Interval.interval.unit, cost.Interval.interval.value, true),
364
365
  });
365
366
  case "ActivationAndHalfInterval":
366
367
  return additionFormatter(translate("{$value} AE", {
@@ -369,7 +370,7 @@ const renderArcaneEnergyCost = (translate, translateMap, format, localeJoin, res
369
370
  cost: translate("{$value} AE", {
370
371
  value: Math.round(cost.ActivationAndHalfInterval.value / 2),
371
372
  }),
372
- interval: formatTimeSpan(translate, translateMap, format, responsiveTextSize, cost.ActivationAndHalfInterval.interval.unit, cost.ActivationAndHalfInterval.interval.value),
373
+ interval: formatTimeSpan(translate, translateMap, format, responsiveTextSize, cost.ActivationAndHalfInterval.interval.unit, cost.ActivationAndHalfInterval.interval.value, true),
373
374
  }));
374
375
  case "Indefinite":
375
376
  return ((mapNullable(translateMap(cost.Indefinite.translations)?.description, description => getResponsiveText(description, responsiveTextSize)) ?? MISSING_VALUE) +
@@ -846,6 +847,10 @@ export const getActivatableEntityDescription = createEntityDescriptionCreator(({
846
847
  label: translate("Volume"),
847
848
  value: renderVolumeValue(translate, translateMap, () => getAllResolvedSelectOptions(wrappedId), responsiveTextSize, volume),
848
849
  })),
850
+ mapNullable(baseEntry.brew, brew => ({
851
+ label: translate("Brew"),
852
+ value: renderBrew(brew).run(env),
853
+ })),
849
854
  mapNullable(baseEntry.cost, cost => renderCost(translate, translateMap, format, locale.join, locale.compare, () => getAllResolvedSelectOptions(wrappedId), responsiveTextSize, baseEntry.levels, cost)),
850
855
  mapNullable(baseEntry.property, property => ({
851
856
  label: translate("Property"),
@@ -161,6 +161,8 @@ const getAtomicEquipmentCost = (entry) => {
161
161
  return "Various";
162
162
  case "DependingOnPurchaseOrSale":
163
163
  return entry.content.cost.DependingOnPurchaseOrSale.purchase;
164
+ case "Range":
165
+ return [entry.content.cost.Range.from, entry.content.cost.Range.to];
164
166
  default:
165
167
  return assertExhaustive(entry.content.cost);
166
168
  }
@@ -101,6 +101,10 @@ export const renderAdventurePointsValue = (locale, getNameForId, getNameForSelec
101
101
  }
102
102
  }
103
103
  case "DerivedFromSelection": {
104
+ const derivedTranslation = translateMap(value.DerivedFromSelection.translations);
105
+ if (derivedTranslation?.replacement !== undefined) {
106
+ return derivedTranslation.replacement;
107
+ }
104
108
  const derivedSelectOptions = entry.select_options?.derived;
105
109
  if (derivedSelectOptions === undefined) {
106
110
  return renderFixedSelectOptionsAdventurePointsValue(locale, getAllSelectOptions, getNameForSelectOptionId, item => {
@@ -144,7 +148,7 @@ export const renderAdventurePointsValue = (locale, getNameForId, getNameForSelec
144
148
  return translate("{$values} adventure points for a {$sized} animal shape", {
145
149
  values: sizes.map(size => applyNegative(size.content.ap_value)).join("/"),
146
150
  sized: sizes
147
- .map(size => translateMap(size.content.translations)?.name ?? MISSING_VALUE)
151
+ .map(size => translateMap(size.content.translations)?.nameInAPValue ?? MISSING_VALUE)
148
152
  .join("/"),
149
153
  });
150
154
  }
@@ -4,6 +4,12 @@ import type { Translate } from "../../helpers/translate.js";
4
4
  * Renders a dice expression like "2D6" into a localized string.
5
5
  */
6
6
  export declare const renderDice: (translate: Translate, dice: Dice) => string;
7
+ /**
8
+ * Renders a dice expression like "2D6" into a localized string.
9
+ */
10
+ export declare const renderDiceR: (dice: Dice) => import("@elyukai/utils/reader").Reader<{
11
+ translate: Translate;
12
+ }, string>;
7
13
  /**
8
14
  * Renders a dice expression with an additional flat modifier, like "2D6+3" or "2D6-1".
9
15
  */
@@ -1,8 +1,13 @@
1
1
  import { additionFormatter, subtractionFormatter } from "./mathOperation.js";
2
+ import { translateR } from "./reader.js";
2
3
  /**
3
4
  * Renders a dice expression like "2D6" into a localized string.
4
5
  */
5
6
  export const renderDice = (translate, dice) => translate("{$count}D{$sides}", { count: dice.number, sides: dice.sides });
7
+ /**
8
+ * Renders a dice expression like "2D6" into a localized string.
9
+ */
10
+ export const renderDiceR = (dice) => translateR("{$count}D{$sides}", { count: dice.number, sides: dice.sides });
6
11
  /**
7
12
  * Renders a dice expression with an additional flat modifier, like "2D6+3" or "2D6-1".
8
13
  */
@@ -1,3 +1,4 @@
1
+ import { Reader } from "@elyukai/utils/reader";
1
2
  import type { MathOperation } from "@optolith/database-schema/gen";
2
3
  type UnaryFormatter = (value: string | number) => string;
3
4
  type BinaryFormatter = (left: string | number, right: string | number) => string;
@@ -29,6 +30,10 @@ export declare const groupFormatter: UnaryFormatter;
29
30
  * Render a math operation as a string, using the provided function to render the values.
30
31
  */
31
32
  export declare const renderMathOperation: <T, R extends string | number>(operation: MathOperation<T>, renderValue: (value: T) => R, renderDirectValue?: (value: T) => R) => R | string;
33
+ /**
34
+ * Render a math operation as a string, using the provided function to render the values.
35
+ */
36
+ export declare const renderMathOperationR: <T, R extends string | number, E>(operation: MathOperation<T>, renderValue: (value: T) => Reader<E, R>, renderDirectValue?: (value: T) => Reader<E, R>) => Reader<E, R | string>;
32
37
  /**
33
38
  * Evaluate a math operation, using the provided function to evaluate the values.
34
39
  */
@@ -1,3 +1,4 @@
1
+ import { Reader } from "@elyukai/utils/reader";
1
2
  import { assertExhaustive } from "@optolith/helpers/typeSafety";
2
3
  const printOperand = (operand) => typeof operand === "number" ? operand.toFixed() : operand;
3
4
  /**
@@ -72,6 +73,10 @@ export const renderMathOperation = (operation, renderValue, renderDirectValue) =
72
73
  return assertExhaustive(operation);
73
74
  }
74
75
  };
76
+ /**
77
+ * Render a math operation as a string, using the provided function to render the values.
78
+ */
79
+ export const renderMathOperationR = (operation, renderValue, renderDirectValue) => Reader.asks(env => renderMathOperation(operation, value => renderValue(value).run(env), renderDirectValue ? value => renderDirectValue(value).run(env) : undefined));
75
80
  /**
76
81
  * Evaluate a math operation, using the provided function to evaluate the values.
77
82
  */
@@ -1,15 +1,17 @@
1
1
  import { ensureNonEmpty } from "@elyukai/utils/array/nonEmpty";
2
2
  import { isNotNullish, mapNullable } from "@elyukai/utils/nullable";
3
+ import { Reader } from "@elyukai/utils/reader";
3
4
  import { sign } from "@elyukai/utils/string/number";
4
5
  import { assertExhaustive } from "@elyukai/utils/typeSafety";
5
6
  import { createEntityDescriptionCreator } from "../creator.js";
6
- import { renderDice } from "./partial/dice.js";
7
+ import { renderDice, renderDiceR } from "./partial/dice.js";
7
8
  import { renderAlternativeNames, renderChance, renderLaboratoryLevel, renderResistance, } from "./partial/herbary.js";
8
- import { renderMathOperation } from "./partial/mathOperation.js";
9
+ import { renderMathOperationR } from "./partial/mathOperation.js";
9
10
  import { printPlainGeneralPrerequisites } from "./partial/prerequisites/index.js";
10
11
  import { parensIf } from "./partial/rated/activatable/parensIf.js";
12
+ import { translateMapR, translateR } from "./partial/reader.js";
11
13
  import { ResponsiveTextSize } from "./partial/responsiveText.js";
12
- import { formatTimeSpan } from "./partial/units/timeSpan.js";
14
+ import { formatTimeSpan, formatTimeSpanR } from "./partial/units/timeSpan.js";
13
15
  import { MISSING_VALUE, UNHANDLED_VALUE } from "./partial/unknown.js";
14
16
  const renderApplicationType = (translate, localeJoin, localeCompare, applicationType) => translate("{$types} poison", {
15
17
  types: localeJoin(applicationType
@@ -69,7 +71,7 @@ const renderAddiction = (translate, translateMap, format, getInstanceById, addic
69
71
  default:
70
72
  return assertExhaustive(withdrawalPrevention.interval);
71
73
  }
72
- })()),
74
+ })(), true),
73
75
  })),
74
76
  ]
75
77
  .filter(isNotNullish)
@@ -158,48 +160,48 @@ const renderSourceTypeBasedValues = (translate, translateMap, format, getInstanc
158
160
  return assertExhaustive(sourceType);
159
161
  }
160
162
  };
161
- const renderStart = (translate, translateMap, start) => {
163
+ const renderStart = (start) => {
162
164
  switch (start.kind) {
163
165
  case "Immediate":
164
- return translate("immediate");
166
+ return translateR("immediate");
165
167
  case "ExpressionBased":
166
- return renderMathOperation(start.ExpressionBased.value, value => {
168
+ return renderMathOperationR(start.ExpressionBased.value, (value) => {
167
169
  switch (value.kind) {
168
170
  case "Constant":
169
- return value.Constant.toFixed();
171
+ return Reader.of(value.Constant);
170
172
  case "Dice":
171
- return renderDice(translate, value.Dice);
173
+ return renderDiceR(value.Dice);
172
174
  case "CircleOfDamnation":
173
- return translate("CoD");
175
+ return translateR("CoD");
174
176
  default:
175
177
  return assertExhaustive(value);
176
178
  }
177
- });
179
+ }).thenW(value => formatTimeSpanR(start.ExpressionBased.unit, value));
178
180
  case "Indefinite":
179
- return translateMap(start.Indefinite.translations)?.description ?? MISSING_VALUE;
181
+ return translateMapR(start.Indefinite.translations).map(translation => translation?.description ?? UNHANDLED_VALUE);
180
182
  default:
181
183
  return assertExhaustive(start);
182
184
  }
183
185
  };
184
- const renderDuration = (translate, translateMap, duration) => {
186
+ const renderDuration = (duration) => {
185
187
  switch (duration.kind) {
186
188
  case "Instant":
187
- return translate("instant");
189
+ return translateR("instant");
188
190
  case "ExpressionBased":
189
- return renderMathOperation(duration.ExpressionBased.value, value => {
191
+ return renderMathOperationR(duration.ExpressionBased.value, (value) => {
190
192
  switch (value.kind) {
191
193
  case "Constant":
192
- return value.Constant.toFixed();
194
+ return Reader.of(value.Constant);
193
195
  case "Dice":
194
- return renderDice(translate, value.Dice);
196
+ return renderDiceR(value.Dice);
195
197
  case "CircleOfDamnation":
196
- return translate("CoD");
198
+ return translateR("CoD");
197
199
  default:
198
200
  return assertExhaustive(value);
199
201
  }
200
- });
202
+ }).thenW(value => formatTimeSpanR(duration.ExpressionBased.unit, value));
201
203
  case "Indefinite":
202
- return translateMap(duration.Indefinite.translations)?.description ?? UNHANDLED_VALUE;
204
+ return translateMapR(duration.Indefinite.translations).map(translation => translation?.description ?? UNHANDLED_VALUE);
203
205
  default:
204
206
  return assertExhaustive(duration);
205
207
  }
@@ -222,6 +224,11 @@ const renderCost = (translate, translateMap, cost) => {
222
224
  })} (${translate("sale")})`;
223
225
  case "Indefinite":
224
226
  return translateMap(cost.Indefinite.translations)?.description ?? MISSING_VALUE;
227
+ case "Range":
228
+ return translate(".input {$from :number} .input {$to :number} {{{$from}–{$to} silverthalers}}", {
229
+ from: cost.Range.from,
230
+ to: cost.Range.to,
231
+ });
225
232
  default:
226
233
  return assertExhaustive(cost);
227
234
  }
@@ -246,6 +253,12 @@ export const getPoisonEntityDescription = createEntityDescriptionCreator(({ getI
246
253
  if (translation === undefined) {
247
254
  return undefined;
248
255
  }
256
+ const env = {
257
+ translate,
258
+ translateMap,
259
+ format,
260
+ responsiveTextSize: ResponsiveTextSize.Full,
261
+ };
249
262
  const applicationType = renderApplicationType(translate, localeJoin, localeCompare, entry.application_type);
250
263
  const { level, sourceType, ingestion, sideEffect, overdose, legality, special, addiction, typicalIngredients, priceOfIngedientsPerLevel, laboratory, brewingDifficulty, prerequisitesBrewingProcess, tradeSecret, note, } = renderSourceTypeBasedValues(translate, translateMap, format, getInstanceById, entry.source_type);
251
264
  return {
@@ -293,14 +306,14 @@ export const getPoisonEntityDescription = createEntityDescriptionCreator(({ getI
293
306
  },
294
307
  {
295
308
  label: translate("Start"),
296
- value: renderStart(translate, translateMap, entry.start),
309
+ value: renderStart(entry.start).run(env),
297
310
  },
298
311
  {
299
312
  label: translate("Duration"),
300
- value: renderDuration(translate, translateMap, entry.duration.default) +
313
+ value: renderDuration(entry.duration.default).run(env) +
301
314
  (entry.duration.reduced === undefined
302
315
  ? ""
303
- : ` / ${renderDuration(translate, translateMap, entry.duration.reduced)}`),
316
+ : ` / ${renderDuration(entry.duration.reduced).run(env)}`),
304
317
  },
305
318
  legality === undefined
306
319
  ? undefined
@@ -364,12 +377,17 @@ export const getPoisonEntityDescription = createEntityDescriptionCreator(({ getI
364
377
  label: translate("Quality Levels"),
365
378
  value: translate("The poison levels equals the QL."),
366
379
  },
367
- note === undefined
368
- ? undefined
369
- : {
380
+ translation.notes !== undefined
381
+ ? {
370
382
  label: translate("Note"),
371
- value: note,
372
- },
383
+ value: translation.notes,
384
+ }
385
+ : note === undefined
386
+ ? undefined
387
+ : {
388
+ label: translate("Note"),
389
+ value: note,
390
+ },
373
391
  ],
374
392
  },
375
393
  ],