@optolith/entity-descriptions 0.4.7 → 0.4.10
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 +22 -0
- package/lib/entities/activatable.d.ts +8 -3
- package/lib/entities/activatable.js +187 -15
- package/lib/entities/curriculum.js +14 -21
- package/lib/entities/derivedCharacteristic.js +5 -7
- package/lib/entities/equipmentPackage.js +2 -1
- package/lib/entities/liturgicalChant.js +7 -3
- package/lib/entities/partial/commonnessRatedAdvantagesAndDisadvantages.js +8 -12
- package/lib/entities/partial/herbary.js +5 -8
- package/lib/entities/partial/markdown.d.ts +55 -0
- package/lib/entities/partial/markdown.js +59 -0
- package/lib/entities/partial/prerequisites/index.d.ts +1 -1
- package/lib/entities/partial/prerequisites/index.js +26 -13
- package/lib/entities/partial/prerequisites/part.js +8 -3
- package/lib/entities/partial/prerequisites/single/culture.js +2 -1
- package/lib/entities/partial/prerequisites/single/personalityTrait.js +2 -1
- package/lib/entities/partial/prerequisites/single/profession.js +4 -1
- package/lib/entities/partial/prerequisites/single/race.js +2 -1
- package/lib/entities/partial/prerequisites/single/rated.js +2 -27
- package/lib/entities/partial/prerequisites/single/ratedMinimumNumber.js +2 -1
- package/lib/entities/partial/prerequisites/single/ratedSum.js +2 -1
- package/lib/entities/partial/prerequisites/single/state.js +2 -1
- package/lib/entities/partial/reader.d.ts +33 -0
- package/lib/entities/partial/reader.js +13 -0
- package/lib/entities/partial/unknown.d.ts +1 -1
- package/lib/entities/partial/unknown.js +1 -1
- package/lib/entities/personalityTrait.js +9 -2
- package/lib/entities/profession.js +25 -25
- package/lib/entities/race.js +4 -6
- package/lib/entities/spell.js +13 -25
- package/lib/index.d.ts +53 -53
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
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.4.10](https://github.com/Optolith/entity-descriptions/compare/v0.4.9...v0.4.10) (2026-03-17)
|
|
6
|
+
|
|
7
|
+
## [0.4.9](https://github.com/Optolith/entity-descriptions/compare/v0.4.8...v0.4.9) (2026-03-17)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* mark generated instances with attributed strings ([85cce91](https://github.com/Optolith/entity-descriptions/commit/85cce917d16ac10833d4dccf7031c2dca4e75d26))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* always append level to activatable name ([10ca82a](https://github.com/Optolith/entity-descriptions/commit/10ca82a1e57afd2d11ba7c32b55a49172e676398))
|
|
18
|
+
* require previous level and fix function typo ([2b12a01](https://github.com/Optolith/entity-descriptions/commit/2b12a01beaaf786843edbbcf42def3d1b1ecb673))
|
|
19
|
+
|
|
20
|
+
## [0.4.8](https://github.com/Optolith/entity-descriptions/compare/v0.4.7...v0.4.8) (2026-03-15)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* render style and advanced special ability values ([d7a2a99](https://github.com/Optolith/entity-descriptions/commit/d7a2a99188976ea626e917da0085726cb02f7e26))
|
|
26
|
+
|
|
5
27
|
## [0.4.7](https://github.com/Optolith/entity-descriptions/compare/v0.4.6...v0.4.7) (2026-03-15)
|
|
6
28
|
|
|
7
29
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import type { ActivatableIdentifier, 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, CombatSpecialAbilityUsageType, DaggerRitualCost, EnchantmentCost, Errata, GeneralPrerequisites, MagicalSignCost, Penalty, PropertyDeclaration, PublicationRefs, RatedIdentifier, SelectOptions, SpecialRule, Volume } from "optolith-database-schema/gen";
|
|
2
|
+
import { Case } from "tsondb/schema/gen";
|
|
2
3
|
import type { GetAllInstances, GetInstanceById } from "../helpers/getTypes.js";
|
|
3
4
|
import type { GetAllResolvedNewSkillApplications, GetAllResolvedSelectOptions, GetAllResolvedSkillUses } from "../index.js";
|
|
4
|
-
import type
|
|
5
|
+
import { type GetResolvedSelectOptionById } from "./partial/prerequisites/single/activatable.js";
|
|
6
|
+
type AdvancedSpecialAbilityKind = "AdvancedSkillSpecialAbility" | "AdvancedCombatSpecialAbility" | "AdvancedMagicalSpecialAbility" | "AdvancedKarmaSpecialAbility";
|
|
7
|
+
type AdvancedIdentifierSpecialAbility = string | Case<AdvancedSpecialAbilityKind, string>;
|
|
5
8
|
/**
|
|
6
9
|
* The base fields for a special ability entity in the database.
|
|
7
10
|
*/
|
|
@@ -10,6 +13,7 @@ export type BaseActivatable = {
|
|
|
10
13
|
maximum?: number;
|
|
11
14
|
select_options?: SelectOptions;
|
|
12
15
|
usage_type?: CombatSpecialAbilityUsageType;
|
|
16
|
+
advanced?: AdvancedSpecialAbility<AdvancedIdentifierSpecialAbility>[];
|
|
13
17
|
penalty?: Penalty;
|
|
14
18
|
combat_techniques?: ApplicableCombatTechniques;
|
|
15
19
|
volume?: Volume;
|
|
@@ -43,10 +47,11 @@ export type BaseActivatableTranslation = {
|
|
|
43
47
|
* Get a JSON representation of the rules text for a special ability.
|
|
44
48
|
*/
|
|
45
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" | "InstrumentEnchantment" | "KarmaSpecialAbility" | "Krallenkettenzauber" | "Kristallkugelzauber" | "LiturgicalStyleSpecialAbility" | "LycantropicGift" | "MagicalSign" | "MagicalSpecialAbility" | "MagicalTradition" | "MagicStyleSpecialAbility" | "OrbEnchantment" | "PactGift" | "ProtectiveWardingCircleSpecialAbility" | "RingEnchantment" | "Sermon" | "SexSpecialAbility" | "SickleRitual" | "SikaryanDrainSpecialAbility" | "SkillStyleSpecialAbility" | "SpellSwordEnchantment" | "StaffEnchantment" | "ToyEnchantment" | "Trinkhornzauber" | "VampiricGift" | "Vision" | "WandEnchantment" | "WeaponEnchantment", {
|
|
46
|
-
getInstanceById: GetInstanceById<"Publication" | "Subject" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "TradeSecret" | "Aspect" | "Property" | "Race" | "Culture" | "State" | "Enhancement" | "PactCategory" | "PactDomain" | "SocialStatus" | "Weapon">;
|
|
50
|
+
getInstanceById: GetInstanceById<"Publication" | "Subject" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "TradeSecret" | "Aspect" | "Property" | "Race" | "Culture" | "State" | "Enhancement" | "PactCategory" | "PactDomain" | "SocialStatus" | "Weapon" | "Patron">;
|
|
47
51
|
getAllInstances: GetAllInstances<"Script">;
|
|
48
52
|
getResolvedSelectOptionById: GetResolvedSelectOptionById;
|
|
49
53
|
getAllResolvedSelectOptions: GetAllResolvedSelectOptions;
|
|
50
54
|
getAllResolvedNewSkillApplications: GetAllResolvedNewSkillApplications;
|
|
51
55
|
getAllResolvedSkillUses: GetAllResolvedSkillUses;
|
|
52
56
|
}, import("../index.js").EntityDescription>;
|
|
57
|
+
export {};
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { isNotEmpty } from "@elyukai/utils/array/nonEmpty";
|
|
2
|
+
import { count } from "@elyukai/utils/array/reductions";
|
|
3
|
+
import { deepEqual } from "@elyukai/utils/equality";
|
|
1
4
|
import { on } from "@elyukai/utils/function";
|
|
2
5
|
import { isNotNullish } from "@elyukai/utils/nullable";
|
|
3
6
|
import { numAsc } from "@optolith/helpers/compare";
|
|
@@ -7,14 +10,25 @@ import { romanize } from "@optolith/helpers/roman";
|
|
|
7
10
|
import { assertExhaustive } from "@optolith/helpers/typeSafety";
|
|
8
11
|
import { Case, fromUniformCase } from "tsondb/schema/gen";
|
|
9
12
|
import { createEntityDescriptionCreator } from "../creator.js";
|
|
13
|
+
import { renderActivatableNameComponents } from "./partial/activatableNameChunks.js";
|
|
10
14
|
import { renderAdventurePointsValue } from "./partial/adventurePointsValue.js";
|
|
11
15
|
import { renderResponsiveMap } from "./partial/map.js";
|
|
16
|
+
import { attributedName } from "./partial/markdown.js";
|
|
12
17
|
import { additionFormatter } from "./partial/mathOperation.js";
|
|
13
18
|
import { printAdvantageDisadvantagePrerequisites, printGeneralPrerequisites, } from "./partial/prerequisites/index.js";
|
|
19
|
+
import { printActivatableName, } from "./partial/prerequisites/single/activatable.js";
|
|
14
20
|
import { parensIf } from "./partial/rated/activatable/parensIf.js";
|
|
15
21
|
import { getResponsiveText, getResponsiveTextOptional, ResponsiveTextSize, } from "./partial/responsiveText.js";
|
|
16
22
|
import { formatTimeSpan } from "./partial/units/timeSpan.js";
|
|
17
23
|
import { MISSING_VALUE } from "./partial/unknown.js";
|
|
24
|
+
const isStyleSpecialAbilityKind = (value) => value === "SkillStyleSpecialAbility" ||
|
|
25
|
+
value === "CombatStyleSpecialAbility" ||
|
|
26
|
+
value === "MagicStyleSpecialAbility" ||
|
|
27
|
+
value === "LiturgicalStyleSpecialAbility";
|
|
28
|
+
const isAdvancedSpecialAbilityKind = (value) => value === "AdvancedSkillSpecialAbility" ||
|
|
29
|
+
value === "AdvancedCombatSpecialAbility" ||
|
|
30
|
+
value === "AdvancedMagicalSpecialAbility" ||
|
|
31
|
+
value === "AdvancedKarmaSpecialAbility";
|
|
18
32
|
const renderPropertyValue = (getInstanceById, translate, translateMap, propertyDecl) => {
|
|
19
33
|
switch (propertyDecl.kind) {
|
|
20
34
|
case "DependingOnSelection":
|
|
@@ -129,7 +143,7 @@ const wrapInParens = (args, append = "") => {
|
|
|
129
143
|
return ` (${filteredArgs.join(", ") + append})`;
|
|
130
144
|
};
|
|
131
145
|
const addSpaceIfNoCommaAtStart = (str) => (str.startsWith(",") ? str : ` ${str}`);
|
|
132
|
-
const renderApplicableCombatTechniquesRestriction = (getInstanceById, locale, main, restriction, translation, weapons,
|
|
146
|
+
const renderApplicableCombatTechniquesRestriction = (getInstanceById, locale, main, restriction, translation, weapons, normalizeExcludedId) => {
|
|
133
147
|
switch (restriction.kind) {
|
|
134
148
|
case "Improvised":
|
|
135
149
|
return main + wrapInParens([locale.translate("only improvised weapons"), weapons]);
|
|
@@ -143,13 +157,12 @@ const renderApplicableCombatTechniquesRestriction = (getInstanceById, locale, ma
|
|
|
143
157
|
return main + wrapInParens([weapons], locale.translate("while mounted"));
|
|
144
158
|
}
|
|
145
159
|
case "Race": {
|
|
146
|
-
const
|
|
147
|
-
const raceName = locale.translateMap(race?.translations)?.name ?? MISSING_VALUE;
|
|
160
|
+
const raceName = attributedName(locale.translateMap, getInstanceById, "applicable-combat-technique", "Race", restriction.Race);
|
|
148
161
|
return (main +
|
|
149
162
|
wrapInParens([
|
|
150
163
|
// originally "while {$racial} weapon", but different to parameterize without inflection support
|
|
151
164
|
locale.translate("while weapon of race {$race}", {
|
|
152
|
-
race: raceName,
|
|
165
|
+
race: raceName ?? MISSING_VALUE,
|
|
153
166
|
}),
|
|
154
167
|
weapons,
|
|
155
168
|
]));
|
|
@@ -159,7 +172,12 @@ const renderApplicableCombatTechniquesRestriction = (getInstanceById, locale, ma
|
|
|
159
172
|
wrapInParens([
|
|
160
173
|
locale.translate("except {$list}", {
|
|
161
174
|
list: locale.join(restriction.ExcludeCombatTechniques.list
|
|
162
|
-
.map(id =>
|
|
175
|
+
.map(id => {
|
|
176
|
+
const normalizedId = normalizeExcludedId?.(id);
|
|
177
|
+
return normalizedId !== undefined
|
|
178
|
+
? (attributedName(locale.translateMap, getInstanceById, "applicable-combat-technique", normalizedId) ?? MISSING_VALUE)
|
|
179
|
+
: MISSING_VALUE;
|
|
180
|
+
})
|
|
163
181
|
.toSorted(locale.compare), "conjunction"),
|
|
164
182
|
}),
|
|
165
183
|
weapons,
|
|
@@ -194,28 +212,27 @@ const renderApplicableCombatTechniquesValue = (getInstanceById, locale, translat
|
|
|
194
212
|
const main = locale.translate("All");
|
|
195
213
|
const mainWithRestriction = applicableCombatTechniques.All.restriction === undefined
|
|
196
214
|
? main
|
|
197
|
-
: renderApplicableCombatTechniquesRestriction(getInstanceById, locale, main, applicableCombatTechniques.All.restriction, translation, undefined, id =>
|
|
215
|
+
: renderApplicableCombatTechniquesRestriction(getInstanceById, locale, main, applicableCombatTechniques.All.restriction, translation, undefined, id => id);
|
|
198
216
|
return mainWithRestriction;
|
|
199
217
|
}
|
|
200
218
|
case "AllClose": {
|
|
201
219
|
const main = locale.translate("All Close Combat Techniques");
|
|
202
220
|
const mainWithRestriction = applicableCombatTechniques.AllClose.restriction === undefined
|
|
203
221
|
? main
|
|
204
|
-
: renderApplicableCombatTechniquesRestriction(getInstanceById, locale, main, applicableCombatTechniques.AllClose.restriction, translation, undefined, id =>
|
|
222
|
+
: renderApplicableCombatTechniquesRestriction(getInstanceById, locale, main, applicableCombatTechniques.AllClose.restriction, translation, undefined, id => Case("CloseCombatTechnique", id));
|
|
205
223
|
return mainWithRestriction;
|
|
206
224
|
}
|
|
207
225
|
case "AllRanged": {
|
|
208
226
|
const main = locale.translate("All Ranged Combat Techniques");
|
|
209
227
|
const mainWithRestriction = applicableCombatTechniques.AllRanged.restriction === undefined
|
|
210
228
|
? main
|
|
211
|
-
: renderApplicableCombatTechniquesRestriction(getInstanceById, locale, main, applicableCombatTechniques.AllRanged.restriction, translation, undefined, id =>
|
|
229
|
+
: renderApplicableCombatTechniquesRestriction(getInstanceById, locale, main, applicableCombatTechniques.AllRanged.restriction, translation, undefined, id => Case("RangedCombatTechnique", id));
|
|
212
230
|
return mainWithRestriction;
|
|
213
231
|
}
|
|
214
232
|
case "Specific": {
|
|
215
233
|
return applicableCombatTechniques.Specific.list
|
|
216
234
|
.map(specific => {
|
|
217
|
-
const
|
|
218
|
-
const main = locale.translateMap(entry?.translations)?.name ?? MISSING_VALUE;
|
|
235
|
+
const main = attributedName(locale.translateMap, getInstanceById, "applicable-combat-technique", specific.id) ?? MISSING_VALUE;
|
|
219
236
|
const mainWithRestriction = specific.restriction === undefined
|
|
220
237
|
? main
|
|
221
238
|
: renderApplicableCombatTechniquesRestriction(getInstanceById, locale, main, specific.restriction, translation, specific.weapons === undefined
|
|
@@ -460,6 +477,155 @@ const renderCost = (translate, translateMap, localeJoin, localeCompare, getAllRe
|
|
|
460
477
|
return assertExhaustive(cost);
|
|
461
478
|
}
|
|
462
479
|
};
|
|
480
|
+
const renderAdvancedLabel = (translate, entityName) => {
|
|
481
|
+
switch (entityName) {
|
|
482
|
+
case "SkillStyleSpecialAbility":
|
|
483
|
+
return translate("Advanced Skill Special Abilities");
|
|
484
|
+
case "CombatStyleSpecialAbility":
|
|
485
|
+
return translate("Advanced Combat Special Abilities");
|
|
486
|
+
case "MagicStyleSpecialAbility":
|
|
487
|
+
return translate("Advanced Magical Special Abilities");
|
|
488
|
+
case "LiturgicalStyleSpecialAbility":
|
|
489
|
+
return translate("Advanced Karma Special Abilities");
|
|
490
|
+
default:
|
|
491
|
+
return assertExhaustive(entityName);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
const normalizeId = (entityName, id) => {
|
|
495
|
+
if (typeof id === "string") {
|
|
496
|
+
switch (entityName) {
|
|
497
|
+
case "SkillStyleSpecialAbility":
|
|
498
|
+
return Case("AdvancedSkillSpecialAbility", id);
|
|
499
|
+
case "CombatStyleSpecialAbility":
|
|
500
|
+
return Case("AdvancedCombatSpecialAbility", id);
|
|
501
|
+
case "MagicStyleSpecialAbility":
|
|
502
|
+
return Case("AdvancedMagicalSpecialAbility", id);
|
|
503
|
+
case "LiturgicalStyleSpecialAbility":
|
|
504
|
+
return Case("AdvancedKarmaSpecialAbility", id);
|
|
505
|
+
default:
|
|
506
|
+
return assertExhaustive(entityName);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
return id;
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
const renderAdvancedSpecialAbilityName = (translate, translateMap, getInstanceById, getResolvedSelectOptionById, entityName, id, option) => {
|
|
514
|
+
const normalizedId = normalizeId(entityName, id);
|
|
515
|
+
return `^[${mapNullable(printActivatableName(getInstanceById, translate, normalizedId, option, undefined, getResolvedSelectOptionById, false), name => renderActivatableNameComponents(translateMap, name, false)) ?? MISSING_VALUE}](entity: "${normalizedId.kind}", instance: "${fromUniformCase(normalizedId)}", style: "normal")`;
|
|
516
|
+
};
|
|
517
|
+
const renderAdvancedValue = (translate, translateMap, localeJoin, getInstanceById, getResolvedSelectOptionById, entityName, advanced) => {
|
|
518
|
+
if (advanced.every(entry => entry.kind === "OneOf") &&
|
|
519
|
+
isNotEmpty(advanced) &&
|
|
520
|
+
advanced.every(entry => entry.OneOf.options.length === 3)) {
|
|
521
|
+
const optionsInAllEntries = advanced[0].OneOf.options.filter(option => advanced.every(otherEntry => otherEntry.OneOf.options.some(otherOption => deepEqual(option, otherOption))));
|
|
522
|
+
if (optionsInAllEntries.length === 2) {
|
|
523
|
+
const uniqueOptions = advanced
|
|
524
|
+
.map(entry => entry.OneOf.options.find(option => optionsInAllEntries.every(commonOption => !deepEqual(option, commonOption))))
|
|
525
|
+
.filter(isNotNullish); // should not do anything, just for type narrowing
|
|
526
|
+
if (uniqueOptions.length === advanced.length) {
|
|
527
|
+
const [first, second] = optionsInAllEntries.map(option => renderAdvancedSpecialAbilityName(translate, translateMap, getInstanceById, getResolvedSelectOptionById, entityName, option));
|
|
528
|
+
return [
|
|
529
|
+
...uniqueOptions.map(option => renderAdvancedSpecialAbilityName(translate, translateMap, getInstanceById, getResolvedSelectOptionById, entityName, option)),
|
|
530
|
+
translate("one or two of these special abilities can alternatively be replaced by advanced special abilities {$first} and/or {$second}", { first, second }),
|
|
531
|
+
].join(", ");
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const derivedFromExternalOptionEntriesToGenerate = count(advanced, entry => entry.kind === "DeriveFromExternalOption" &&
|
|
536
|
+
entry.DeriveFromExternalOption.display_option === undefined);
|
|
537
|
+
return [
|
|
538
|
+
...advanced.map((entry) => {
|
|
539
|
+
switch (entry.kind) {
|
|
540
|
+
case "General":
|
|
541
|
+
return renderAdvancedSpecialAbilityName(translate, translateMap, getInstanceById, getResolvedSelectOptionById, entityName, entry.General);
|
|
542
|
+
case "RestrictOptions":
|
|
543
|
+
return renderAdvancedSpecialAbilityName(translate, translateMap, getInstanceById, getResolvedSelectOptionById, entityName, entry.RestrictOptions.id, entry.RestrictOptions.option);
|
|
544
|
+
case "OneOf": {
|
|
545
|
+
if (entry.OneOf.display_option !== undefined) {
|
|
546
|
+
switch (entry.OneOf.display_option.kind) {
|
|
547
|
+
case "Hide":
|
|
548
|
+
return undefined;
|
|
549
|
+
case "ReplaceWith":
|
|
550
|
+
return (translateMap(entry.OneOf.display_option.ReplaceWith.translations)?.replacement ??
|
|
551
|
+
MISSING_VALUE);
|
|
552
|
+
default:
|
|
553
|
+
return assertExhaustive(entry.OneOf.display_option);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return localeJoin(entry.OneOf.options.map(option => renderAdvancedSpecialAbilityName(translate, translateMap, getInstanceById, getResolvedSelectOptionById, entityName, option)), "disjunction");
|
|
557
|
+
}
|
|
558
|
+
case "DeriveFromExternalOption":
|
|
559
|
+
if (entry.DeriveFromExternalOption.display_option !== undefined) {
|
|
560
|
+
switch (entry.DeriveFromExternalOption.display_option.kind) {
|
|
561
|
+
case "Hide":
|
|
562
|
+
return undefined;
|
|
563
|
+
case "ReplaceWith":
|
|
564
|
+
return (translateMap(entry.DeriveFromExternalOption.display_option.ReplaceWith.translations)?.replacement ?? MISSING_VALUE);
|
|
565
|
+
default:
|
|
566
|
+
return assertExhaustive(entry.DeriveFromExternalOption.display_option);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return undefined;
|
|
570
|
+
default:
|
|
571
|
+
return assertExhaustive(entry);
|
|
572
|
+
}
|
|
573
|
+
}),
|
|
574
|
+
derivedFromExternalOptionEntriesToGenerate === 0
|
|
575
|
+
? undefined
|
|
576
|
+
: translate(".input {$count :number} {{{$count} more by primary patron}}", {
|
|
577
|
+
count: derivedFromExternalOptionEntriesToGenerate,
|
|
578
|
+
}),
|
|
579
|
+
]
|
|
580
|
+
.filter(isNotNullish)
|
|
581
|
+
.join(", ");
|
|
582
|
+
};
|
|
583
|
+
const renderDeriveFromExternalOptionTable = (translate, translateMap, localeCompare, getInstanceById, getResolvedSelectOptionById, entityName, advanced) => {
|
|
584
|
+
if (!isStyleSpecialAbilityKind(entityName)) {
|
|
585
|
+
return undefined;
|
|
586
|
+
}
|
|
587
|
+
const derivedFromExternalOptionEntriesToGenerate = advanced.filter((entry) => entry.kind === "DeriveFromExternalOption" &&
|
|
588
|
+
entry.DeriveFromExternalOption.display_option === undefined);
|
|
589
|
+
if (derivedFromExternalOptionEntriesToGenerate.length === 0) {
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
const rows = Map.groupBy(derivedFromExternalOptionEntriesToGenerate.flatMap(entry => entry.DeriveFromExternalOption.map.map((option) => [
|
|
593
|
+
option.from_option,
|
|
594
|
+
option.to_advanced,
|
|
595
|
+
])), row => row[0])
|
|
596
|
+
.entries()
|
|
597
|
+
.map(([option, list]) => [
|
|
598
|
+
translateMap(getInstanceById("Patron", option)?.translations)?.name ?? MISSING_VALUE,
|
|
599
|
+
list
|
|
600
|
+
.map(([, toAdvanced]) => renderAdvancedSpecialAbilityName(translate, translateMap, getInstanceById, getResolvedSelectOptionById, entityName, toAdvanced))
|
|
601
|
+
.toSorted(localeCompare)
|
|
602
|
+
.join(", "),
|
|
603
|
+
])
|
|
604
|
+
.toArray()
|
|
605
|
+
.toSorted(on(row => row[0], localeCompare));
|
|
606
|
+
return {
|
|
607
|
+
type: "table",
|
|
608
|
+
header: [translate("Patron"), renderAdvancedLabel(translate, entityName)],
|
|
609
|
+
rows,
|
|
610
|
+
};
|
|
611
|
+
};
|
|
612
|
+
const renderTrailingAdvancedPrerequisitesNote = (translate, entityName) => {
|
|
613
|
+
if (isAdvancedSpecialAbilityKind(entityName)) {
|
|
614
|
+
switch (entityName) {
|
|
615
|
+
case "AdvancedCombatSpecialAbility":
|
|
616
|
+
return translate("corresponding combat style special ability");
|
|
617
|
+
case "AdvancedKarmaSpecialAbility":
|
|
618
|
+
return translate("corresponding liturgical style special ability");
|
|
619
|
+
case "AdvancedMagicalSpecialAbility":
|
|
620
|
+
return translate("corresponding magic style special ability");
|
|
621
|
+
case "AdvancedSkillSpecialAbility":
|
|
622
|
+
return translate("corresponding skill style special ability");
|
|
623
|
+
default:
|
|
624
|
+
return assertExhaustive(entityName);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return undefined;
|
|
628
|
+
};
|
|
463
629
|
/**
|
|
464
630
|
* Get a JSON representation of the rules text for a special ability.
|
|
465
631
|
*/
|
|
@@ -473,9 +639,8 @@ export const getActivatableEntityDescription = createEntityDescriptionCreator(({
|
|
|
473
639
|
const baseEntry = entry;
|
|
474
640
|
const wrappedId = Case(entityName, id);
|
|
475
641
|
return {
|
|
476
|
-
title: translation.name_in_library ??
|
|
477
|
-
|
|
478
|
-
(baseEntry.levels !== undefined ? ` I–${romanize(baseEntry.levels)}` : ""),
|
|
642
|
+
title: (translation.name_in_library ?? translation.name) +
|
|
643
|
+
(baseEntry.levels !== undefined ? ` I–${romanize(baseEntry.levels)}` : ""),
|
|
479
644
|
subtitle: mapNullable(baseEntry.usage_type, usageType => {
|
|
480
645
|
switch (usageType.kind) {
|
|
481
646
|
case "Passive":
|
|
@@ -526,18 +691,24 @@ export const getActivatableEntityDescription = createEntityDescriptionCreator(({
|
|
|
526
691
|
label: translate("Range"),
|
|
527
692
|
value: range,
|
|
528
693
|
})),
|
|
694
|
+
mapNullable(baseEntry.advanced, advanced => isStyleSpecialAbilityKind(entityName)
|
|
695
|
+
? {
|
|
696
|
+
label: renderAdvancedLabel(translate, entityName),
|
|
697
|
+
value: renderAdvancedValue(translate, translateMap, locale.join, getInstanceById, getResolvedSelectOptionById, entityName, advanced),
|
|
698
|
+
}
|
|
699
|
+
: undefined),
|
|
529
700
|
mapNullable(baseEntry.penalty, penalty => ({
|
|
530
701
|
label: translate("Penalty"),
|
|
531
702
|
value: renderPenaltyValue(getInstanceById, translate, translateMap, translation.name, penalty),
|
|
532
703
|
})),
|
|
533
|
-
mapNullable(entry.prerequisites, prerequisites => ({
|
|
704
|
+
mapNullable(entry.prerequisites ?? (isAdvancedSpecialAbilityKind(entityName) ? [] : undefined), prerequisites => ({
|
|
534
705
|
label: translate("Prerequisites"),
|
|
535
706
|
value: wrappedId.kind === "Advantage" || wrappedId.kind === "Disadvantage"
|
|
536
707
|
? printAdvantageDisadvantagePrerequisites(getInstanceById, getResolvedSelectOptionById, locale, prerequisites, translation.name, wrappedId.kind)
|
|
537
708
|
: printGeneralPrerequisites(getInstanceById, getResolvedSelectOptionById, locale, prerequisites, mapNullable(baseEntry.levels, levels => ({
|
|
538
709
|
id: wrappedId,
|
|
539
710
|
levels,
|
|
540
|
-
}))),
|
|
711
|
+
})), renderTrailingAdvancedPrerequisitesNote(translate, entityName)),
|
|
541
712
|
})),
|
|
542
713
|
mapNullable(baseEntry.combat_techniques, combatTechniques => ({
|
|
543
714
|
label: translate("Combat Techniques"),
|
|
@@ -573,6 +744,7 @@ export const getActivatableEntityDescription = createEntityDescriptionCreator(({
|
|
|
573
744
|
}),
|
|
574
745
|
],
|
|
575
746
|
},
|
|
747
|
+
renderDeriveFromExternalOptionTable(translate, translateMap, locale.compare, getInstanceById, getResolvedSelectOptionById, entityName, baseEntry.advanced ?? []),
|
|
576
748
|
],
|
|
577
749
|
errata: translation.errata,
|
|
578
750
|
references: entry.src,
|
|
@@ -5,6 +5,7 @@ import { sign } from "@elyukai/utils/string/number";
|
|
|
5
5
|
import { assertExhaustive } from "@elyukai/utils/typeSafety";
|
|
6
6
|
import { isNotNullish, mapNullable } from "@optolith/helpers/nullable";
|
|
7
7
|
import { createEntityDescriptionCreator } from "../creator.js";
|
|
8
|
+
import { attributedName } from "./partial/markdown.js";
|
|
8
9
|
import { getBaseProfessionPackageForCurriculum } from "./partial/professions.js";
|
|
9
10
|
import { parensIf } from "./partial/rated/activatable/parensIf.js";
|
|
10
11
|
import { MISSING_VALUE } from "./partial/unknown.js";
|
|
@@ -14,13 +15,12 @@ const renderElectiveSpellworks = (translate, translateMap, localeCompare, getIns
|
|
|
14
15
|
return translate("All, at the GM’s discretion");
|
|
15
16
|
case "Specific":
|
|
16
17
|
return electiveSpellworks.Specific.list
|
|
17
|
-
.map(item => mapNullable(translateMap
|
|
18
|
+
.map(item => mapNullable(attributedName(translateMap, getInstanceById, "curriculum", item.id), name => {
|
|
18
19
|
if (item.restriction === undefined) {
|
|
19
20
|
return name;
|
|
20
21
|
}
|
|
21
22
|
return (name +
|
|
22
|
-
parensIf(translateMap
|
|
23
|
-
?.name ?? MISSING_VALUE));
|
|
23
|
+
parensIf(attributedName(translateMap, getInstanceById, "curriculum", "Element", item.restriction.Element)));
|
|
24
24
|
}))
|
|
25
25
|
.filter(isNotNullish)
|
|
26
26
|
.toSorted(localeCompare)
|
|
@@ -42,7 +42,7 @@ const renderRestrictedSpellworks = (translate, translateMap, localeCompare, loca
|
|
|
42
42
|
return "";
|
|
43
43
|
}
|
|
44
44
|
const translatedSpellworks = excludedSpellworks
|
|
45
|
-
.map(excludedSpellwork => translateMap
|
|
45
|
+
.map(excludedSpellwork => attributedName(translateMap, getInstanceById, "curriculum", excludedSpellwork))
|
|
46
46
|
.filter(isNotNullish);
|
|
47
47
|
if (translatedSpellworks.length === 0) {
|
|
48
48
|
return "";
|
|
@@ -53,16 +53,14 @@ const renderRestrictedSpellworks = (translate, translateMap, localeCompare, loca
|
|
|
53
53
|
};
|
|
54
54
|
const restrictedProperties = restrictedGroups.get("restrictedProperty")?.map(restriction => translate(".input {$count :number} {{only {$count} additional spellworks with the Property {$property}}}", {
|
|
55
55
|
count: restriction.Property.maximum,
|
|
56
|
-
property: translateMap
|
|
57
|
-
?.name ?? MISSING_VALUE,
|
|
56
|
+
property: attributedName(translateMap, getInstanceById, "curriculum", "Property", restriction.Property.id) ?? MISSING_VALUE,
|
|
58
57
|
}) + addExclusion(restriction.Property.exclude)) ?? [];
|
|
59
58
|
const propertyGroup = restrictedGroups.get("property");
|
|
60
59
|
const translatedProperties = propertyGroup
|
|
61
60
|
?.map(restriction => {
|
|
62
61
|
switch (restriction.kind) {
|
|
63
62
|
case "Property":
|
|
64
|
-
return translateMap
|
|
65
|
-
?.name;
|
|
63
|
+
return attributedName(translateMap, getInstanceById, "curriculum", "Property", restriction.Property.id);
|
|
66
64
|
case "DemonSummoning":
|
|
67
65
|
return translate("Demon Summoning");
|
|
68
66
|
default:
|
|
@@ -80,7 +78,7 @@ const renderRestrictedSpellworks = (translate, translateMap, localeCompare, loca
|
|
|
80
78
|
: [];
|
|
81
79
|
const spellworkGroup = restrictedGroups.get("spellwork");
|
|
82
80
|
const translatedSpellworks = spellworkGroup
|
|
83
|
-
?.map(restriction => translateMap
|
|
81
|
+
?.map(restriction => attributedName(translateMap, getInstanceById, "curriculum", restriction.Spellwork))
|
|
84
82
|
.filter(isNotNullish)
|
|
85
83
|
.toSorted(localeCompare) ?? [];
|
|
86
84
|
const spellworks = translatedSpellworks.length > 0
|
|
@@ -103,24 +101,20 @@ const renderRestrictedSpellworks = (translate, translateMap, localeCompare, loca
|
|
|
103
101
|
}) ?? [];
|
|
104
102
|
return [...restrictedProperties, ...properties, ...others, ...spellworks].join("; ");
|
|
105
103
|
};
|
|
106
|
-
const renderSpellworkAdjustment = (translateMap, getInstanceById, adjustment) => `${translateMap
|
|
104
|
+
const renderSpellworkAdjustment = (translateMap, getInstanceById, adjustment) => `${attributedName(translateMap, getInstanceById, "curriculum", adjustment.id) ?? MISSING_VALUE} ${adjustment.points.toFixed()}${parensIf(adjustment.tradition === undefined
|
|
107
105
|
? undefined
|
|
108
|
-
: translateMap
|
|
106
|
+
: attributedName(translateMap, getInstanceById, "curriculum", "MagicalTradition", adjustment.tradition))}`;
|
|
109
107
|
const renderAbilityAdjustmentName = (translateMap, getInstanceById, abilityAdjustment) => {
|
|
110
108
|
switch (abilityAdjustment.kind) {
|
|
111
109
|
case "Skill":
|
|
112
|
-
return (translateMap
|
|
113
|
-
MISSING_VALUE);
|
|
110
|
+
return (attributedName(translateMap, getInstanceById, "curriculum", "Skill", abilityAdjustment.Skill.id) ?? MISSING_VALUE);
|
|
114
111
|
case "CombatTechnique":
|
|
115
|
-
return (translateMap
|
|
116
|
-
MISSING_VALUE);
|
|
112
|
+
return (attributedName(translateMap, getInstanceById, "curriculum", abilityAdjustment.CombatTechnique.id) ?? MISSING_VALUE);
|
|
117
113
|
case "Spellwork":
|
|
118
|
-
return ((translateMap
|
|
119
|
-
MISSING_VALUE) +
|
|
114
|
+
return ((attributedName(translateMap, getInstanceById, "curriculum", abilityAdjustment.Spellwork.id) ?? MISSING_VALUE) +
|
|
120
115
|
parensIf(abilityAdjustment.Spellwork.tradition === undefined
|
|
121
116
|
? undefined
|
|
122
|
-
: translateMap
|
|
123
|
-
?.translations)?.name));
|
|
117
|
+
: attributedName(translateMap, getInstanceById, "curriculum", "MagicalTradition", abilityAdjustment.Spellwork.tradition)));
|
|
124
118
|
default:
|
|
125
119
|
return assertExhaustive(abilityAdjustment);
|
|
126
120
|
}
|
|
@@ -195,8 +189,7 @@ export const getCurriculumEntityDescription = createEntityDescriptionCreator(({
|
|
|
195
189
|
items: [
|
|
196
190
|
{
|
|
197
191
|
label: translate("Guideline"),
|
|
198
|
-
value: translateMap
|
|
199
|
-
MISSING_VALUE,
|
|
192
|
+
value: attributedName(translateMap, getInstanceById, "curriculum", "Guideline", entry.guideline) ?? MISSING_VALUE,
|
|
200
193
|
},
|
|
201
194
|
{
|
|
202
195
|
label: translate("Elective Spellworks Package"),
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import { assertExhaustive } from "@optolith/helpers/typeSafety";
|
|
2
2
|
import { createEntityDescriptionCreator } from "../creator.js";
|
|
3
|
+
import { attributedCustomName } from "./partial/markdown.js";
|
|
3
4
|
import { renderMathOperation } from "./partial/mathOperation.js";
|
|
4
5
|
import { MISSING_VALUE } from "./partial/unknown.js";
|
|
5
|
-
const
|
|
6
|
-
const attribute = getInstanceById("Attribute", attributeId);
|
|
7
|
-
if (attribute === undefined) {
|
|
8
|
-
return MISSING_VALUE;
|
|
9
|
-
}
|
|
6
|
+
const renderAttributeNameForStyle = (style, translation) => {
|
|
10
7
|
switch (style) {
|
|
11
8
|
case "full":
|
|
12
|
-
return
|
|
9
|
+
return translation.name;
|
|
13
10
|
case "compact":
|
|
14
|
-
return
|
|
11
|
+
return translation.abbreviation;
|
|
15
12
|
default:
|
|
16
13
|
return assertExhaustive(style);
|
|
17
14
|
}
|
|
18
15
|
};
|
|
16
|
+
const getAttribute = (getInstanceById, translateMap, attributeId, style = "full") => attributedCustomName(translateMap, getInstanceById, "derived-characteristic-calculation", translation => renderAttributeNameForStyle(style, translation), "Attribute", attributeId) ?? MISSING_VALUE;
|
|
19
17
|
const getRaceBaseValue = (translate, translateMap, getInstanceById, idMap, raceBaseValue, style = "full") => {
|
|
20
18
|
const instance = getInstanceById("DerivedCharacteristic", idMap.DerivedCharacteristic[raceBaseValue.kind]);
|
|
21
19
|
const translation = translateMap(instance?.translations);
|
|
@@ -2,6 +2,7 @@ import { on } from "@elyukai/utils/function";
|
|
|
2
2
|
import { assertExhaustive } from "@elyukai/utils/typeSafety";
|
|
3
3
|
import { createEntityDescriptionCreator } from "../creator.js";
|
|
4
4
|
import { getEquipmentName } from "./equipment.js";
|
|
5
|
+
import { attributedInstance } from "./partial/markdown.js";
|
|
5
6
|
const sumAtomicEquipmentCost = (acc, current) => {
|
|
6
7
|
if (typeof acc === "string") {
|
|
7
8
|
return acc;
|
|
@@ -280,7 +281,7 @@ export const getEquipmentPackageEntityDescription = createEntityDescriptionCreat
|
|
|
280
281
|
header: [translation.name, translate("Weight"), translate("Cost")],
|
|
281
282
|
rows: actualItems
|
|
282
283
|
.map((item) => [
|
|
283
|
-
getEquipmentName(translate, translateMap, getInstanceById, item.content),
|
|
284
|
+
attributedInstance(getEquipmentName(translate, translateMap, getInstanceById, item.content), item.content.entity, item.content.id, { context: '"equipment-package"' }),
|
|
284
285
|
renderAtomicWeight(translate, locale.measurementAdjustments, getAtomicEquipmentWeight(item.content)),
|
|
285
286
|
renderAtomicCost(translate, getAtomicEquipmentCost(item.content)),
|
|
286
287
|
])
|
|
@@ -3,6 +3,7 @@ import { assertExhaustive } from "@optolith/helpers/typeSafety";
|
|
|
3
3
|
import { Case } from "tsondb/schema/gen";
|
|
4
4
|
import { createEntityDescriptionCreator } from "../creator.js";
|
|
5
5
|
import { renderEnhancements } from "./partial/enhancements.js";
|
|
6
|
+
import { attributedInstance, attributedName } from "./partial/markdown.js";
|
|
6
7
|
import { renderOneTimeDuration } from "./partial/rated/activatable/duration.js";
|
|
7
8
|
import { renderEffect } from "./partial/rated/activatable/effect.js";
|
|
8
9
|
import { combineGeneratedTextWithStaticTranslation, renderFastPerformanceParameters, renderSlowPerformanceParameters, } from "./partial/rated/activatable/index.js";
|
|
@@ -13,7 +14,7 @@ import { renderImprovementCost } from "./partial/rated/improvementCost.js";
|
|
|
13
14
|
import { renderSkillCheckWithPenalty } from "./partial/rated/skillCheck.js";
|
|
14
15
|
import { ResponsiveTextSize } from "./partial/responsiveText.js";
|
|
15
16
|
const getTextForTraditions = (deps, values) => {
|
|
16
|
-
const getAspectName = (aspectId) => deps.translateMap
|
|
17
|
+
const getAspectName = (aspectId) => attributedName(deps.translateMap, deps.getInstanceById, "traditions", "Aspect", aspectId);
|
|
17
18
|
const text = values
|
|
18
19
|
.map(trad => {
|
|
19
20
|
switch (trad.kind) {
|
|
@@ -25,14 +26,17 @@ const getTextForTraditions = (deps, values) => {
|
|
|
25
26
|
if (name === undefined) {
|
|
26
27
|
return undefined;
|
|
27
28
|
}
|
|
29
|
+
const attributedStringName = attributedInstance(name, "BlessedTradition", trad.Tradition.tradition, {
|
|
30
|
+
context: '"traditions"',
|
|
31
|
+
});
|
|
28
32
|
const aspects = trad.Tradition.aspects
|
|
29
33
|
?.map(getAspectName)
|
|
30
34
|
.filter(isNotNullish)
|
|
31
35
|
.sort(deps.localeCompare) ?? [];
|
|
32
36
|
if (aspects.length === 0) {
|
|
33
|
-
return
|
|
37
|
+
return attributedStringName;
|
|
34
38
|
}
|
|
35
|
-
return `${
|
|
39
|
+
return `${attributedStringName} (${aspects.join(" and ")})`;
|
|
36
40
|
}
|
|
37
41
|
default:
|
|
38
42
|
return assertExhaustive(trad);
|
|
@@ -2,18 +2,17 @@ import { isNotEmpty } from "@elyukai/utils/array/nonEmpty";
|
|
|
2
2
|
import { mapNullable } from "@elyukai/utils/nullable";
|
|
3
3
|
import { Reader } from "@elyukai/utils/reader";
|
|
4
4
|
import { ensureNonEmpty } from "@optolith/helpers/array";
|
|
5
|
+
import { attributedCustomName } from "./markdown.js";
|
|
5
6
|
import { parensIf } from "./rated/activatable/parensIf.js";
|
|
6
7
|
import { MISSING_VALUE } from "./unknown.js";
|
|
7
8
|
const renderCommonnessRatedAdvantageOrDisadvantageName = (translateMap, getInstanceById, entity, item) => {
|
|
8
|
-
const instance = getInstanceById(entity, item.id);
|
|
9
|
-
const instanceTranslation = translateMap(instance?.translations);
|
|
10
9
|
const customTranslation = translateMap(item.translations);
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
customTranslation?.options !== undefined &&
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
return (attributedCustomName(translateMap, getInstanceById, entity, translation => {
|
|
11
|
+
const name = translation.name_in_library ?? translation.name;
|
|
12
|
+
return customTranslation?.options !== undefined && name.endsWith(")")
|
|
13
|
+
? `${name.slice(0, -1)}; ${customTranslation.options})`
|
|
14
|
+
: name + parensIf(customTranslation?.options);
|
|
15
|
+
}, entity, item.id) ?? MISSING_VALUE);
|
|
17
16
|
};
|
|
18
17
|
/**
|
|
19
18
|
* Render the names of either commonness-rated advantages or commonness-rated disadvantages.
|
|
@@ -27,10 +26,7 @@ export const renderCommonnessRatedAdvantagesOrDisadvantages = (entity, items) =>
|
|
|
27
26
|
/**
|
|
28
27
|
* Render the names of commonness-rated advantages and disadvantages together.
|
|
29
28
|
*/
|
|
30
|
-
export const renderCommonnessRatedAdvantagesAndDisadvantages = (advantages, disadvantages) => Reader.asks(({ translate, translateMap, getInstanceById, localeCompare }) => mapNullable(ensureNonEmpty([
|
|
31
|
-
["Advantage", advantages],
|
|
32
|
-
["Disadvantage", disadvantages],
|
|
33
|
-
].flatMap(([entity, items]) => items?.map(item => renderCommonnessRatedAdvantageOrDisadvantageName(translateMap, getInstanceById, entity, item)) ?? [])), renderedItems => renderedItems.toSorted(localeCompare).join(", ")) ?? translate("none"));
|
|
29
|
+
export const renderCommonnessRatedAdvantagesAndDisadvantages = (advantages, disadvantages) => Reader.asks(({ translate, translateMap, getInstanceById, localeCompare }) => mapNullable(ensureNonEmpty([["Advantage", advantages], ["Disadvantage", disadvantages]].flatMap(([entity, items]) => items?.map(item => renderCommonnessRatedAdvantageOrDisadvantageName(translateMap, getInstanceById, entity, item)) ?? [])), renderedItems => renderedItems.toSorted(localeCompare).join(", ")) ?? translate("none"));
|
|
34
30
|
/**
|
|
35
31
|
* Render a value with a possible translation, falling back to explicity rendering the value if no translation is available.
|
|
36
32
|
*/
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isNotEmpty } from "@elyukai/utils/array/nonEmpty";
|
|
2
2
|
import { assertExhaustive } from "@elyukai/utils/typeSafety";
|
|
3
3
|
import { renderDice } from "./dice.js";
|
|
4
|
+
import { attributedName } from "./markdown.js";
|
|
4
5
|
import { parensIf } from "./rated/activatable/parensIf.js";
|
|
5
6
|
import { MISSING_VALUE } from "./unknown.js";
|
|
6
7
|
/**
|
|
@@ -24,16 +25,12 @@ export const renderLaboratoryLevel = (translate, level) => {
|
|
|
24
25
|
export const renderResistance = (translate, translateMap, getInstanceById, idMap, resistance) => {
|
|
25
26
|
switch (resistance.kind) {
|
|
26
27
|
case "Spirit":
|
|
27
|
-
return (translateMap
|
|
28
|
-
?.translations)?.name ?? MISSING_VALUE);
|
|
28
|
+
return (attributedName(translateMap, getInstanceById, "resistance", "DerivedCharacteristic", idMap.DerivedCharacteristic.Spirit) ?? MISSING_VALUE);
|
|
29
29
|
case "Toughness":
|
|
30
|
-
return (translateMap
|
|
31
|
-
?.translations)?.name ?? MISSING_VALUE);
|
|
30
|
+
return (attributedName(translateMap, getInstanceById, "resistance", "DerivedCharacteristic", idMap.DerivedCharacteristic.Toughness) ?? MISSING_VALUE);
|
|
32
31
|
case "LowerOfSpiritAndToughness": {
|
|
33
|
-
const spiritTranslation = translateMap
|
|
34
|
-
|
|
35
|
-
const toughnessTranslation = translateMap(getInstanceById("DerivedCharacteristic", idMap.DerivedCharacteristic.Toughness)
|
|
36
|
-
?.translations)?.name ?? MISSING_VALUE;
|
|
32
|
+
const spiritTranslation = attributedName(translateMap, getInstanceById, "resistance", "DerivedCharacteristic", idMap.DerivedCharacteristic.Spirit) ?? MISSING_VALUE;
|
|
33
|
+
const toughnessTranslation = attributedName(translateMap, getInstanceById, "resistance", "DerivedCharacteristic", idMap.DerivedCharacteristic.Toughness) ?? MISSING_VALUE;
|
|
37
34
|
return translate("{$first} or {$second}, depending on which value is lower", {
|
|
38
35
|
first: spiritTranslation,
|
|
39
36
|
second: toughnessTranslation,
|