@optolith/entity-descriptions 0.4.6 → 0.4.8
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 +14 -0
- package/lib/entities/activatable.d.ts +8 -3
- package/lib/entities/activatable.js +171 -2
- package/lib/entities/partial/prerequisites/index.d.ts +1 -1
- package/lib/entities/partial/prerequisites/index.js +24 -11
- package/lib/entities/partial/unknown.d.ts +1 -1
- package/lib/entities/partial/unknown.js +1 -1
- package/lib/entities/profession.js +39 -11
- 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,20 @@
|
|
|
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.8](https://github.com/Optolith/entity-descriptions/compare/v0.4.7...v0.4.8) (2026-03-15)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* render style and advanced special ability values ([d7a2a99](https://github.com/Optolith/entity-descriptions/commit/d7a2a99188976ea626e917da0085726cb02f7e26))
|
|
11
|
+
|
|
12
|
+
## [0.4.7](https://github.com/Optolith/entity-descriptions/compare/v0.4.6...v0.4.7) (2026-03-15)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* join profession variants with the same changes ([99ee4f1](https://github.com/Optolith/entity-descriptions/commit/99ee4f1001df9836537bae9640582870e40dff1d))
|
|
18
|
+
|
|
5
19
|
## [0.4.6](https://github.com/Optolith/entity-descriptions/compare/v0.4.5...v0.4.6) (2026-03-14)
|
|
6
20
|
|
|
7
21
|
|
|
@@ -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,24 @@ 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";
|
|
12
16
|
import { additionFormatter } from "./partial/mathOperation.js";
|
|
13
17
|
import { printAdvantageDisadvantagePrerequisites, printGeneralPrerequisites, } from "./partial/prerequisites/index.js";
|
|
18
|
+
import { printActivatableName, } from "./partial/prerequisites/single/activatable.js";
|
|
14
19
|
import { parensIf } from "./partial/rated/activatable/parensIf.js";
|
|
15
20
|
import { getResponsiveText, getResponsiveTextOptional, ResponsiveTextSize, } from "./partial/responsiveText.js";
|
|
16
21
|
import { formatTimeSpan } from "./partial/units/timeSpan.js";
|
|
17
22
|
import { MISSING_VALUE } from "./partial/unknown.js";
|
|
23
|
+
const isStyleSpecialAbilityKind = (value) => value === "SkillStyleSpecialAbility" ||
|
|
24
|
+
value === "CombatStyleSpecialAbility" ||
|
|
25
|
+
value === "MagicStyleSpecialAbility" ||
|
|
26
|
+
value === "LiturgicalStyleSpecialAbility";
|
|
27
|
+
const isAdvancedSpecialAbilityKind = (value) => value === "AdvancedSkillSpecialAbility" ||
|
|
28
|
+
value === "AdvancedCombatSpecialAbility" ||
|
|
29
|
+
value === "AdvancedMagicalSpecialAbility" ||
|
|
30
|
+
value === "AdvancedKarmaSpecialAbility";
|
|
18
31
|
const renderPropertyValue = (getInstanceById, translate, translateMap, propertyDecl) => {
|
|
19
32
|
switch (propertyDecl.kind) {
|
|
20
33
|
case "DependingOnSelection":
|
|
@@ -460,6 +473,155 @@ const renderCost = (translate, translateMap, localeJoin, localeCompare, getAllRe
|
|
|
460
473
|
return assertExhaustive(cost);
|
|
461
474
|
}
|
|
462
475
|
};
|
|
476
|
+
const renderAdvancedLabel = (translate, entityName) => {
|
|
477
|
+
switch (entityName) {
|
|
478
|
+
case "SkillStyleSpecialAbility":
|
|
479
|
+
return translate("Advanced Skill Special Abilities");
|
|
480
|
+
case "CombatStyleSpecialAbility":
|
|
481
|
+
return translate("Advanced Combat Special Abilities");
|
|
482
|
+
case "MagicStyleSpecialAbility":
|
|
483
|
+
return translate("Advanced Magical Special Abilities");
|
|
484
|
+
case "LiturgicalStyleSpecialAbility":
|
|
485
|
+
return translate("Advanced Karma Special Abilities");
|
|
486
|
+
default:
|
|
487
|
+
return assertExhaustive(entityName);
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
const normalizeId = (entityName, id) => {
|
|
491
|
+
if (typeof id === "string") {
|
|
492
|
+
switch (entityName) {
|
|
493
|
+
case "SkillStyleSpecialAbility":
|
|
494
|
+
return Case("AdvancedSkillSpecialAbility", id);
|
|
495
|
+
case "CombatStyleSpecialAbility":
|
|
496
|
+
return Case("AdvancedCombatSpecialAbility", id);
|
|
497
|
+
case "MagicStyleSpecialAbility":
|
|
498
|
+
return Case("AdvancedMagicalSpecialAbility", id);
|
|
499
|
+
case "LiturgicalStyleSpecialAbility":
|
|
500
|
+
return Case("AdvancedKarmaSpecialAbility", id);
|
|
501
|
+
default:
|
|
502
|
+
return assertExhaustive(entityName);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
return id;
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
const renderAdvancedSpecialAbilityName = (translate, translateMap, getInstanceById, getResolvedSelectOptionById, entityName, id, option) => {
|
|
510
|
+
const normalizedId = normalizeId(entityName, id);
|
|
511
|
+
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")`;
|
|
512
|
+
};
|
|
513
|
+
const renderAdvancedValue = (translate, translateMap, localeJoin, getInstanceById, getResolvedSelectOptionById, entityName, advanced) => {
|
|
514
|
+
if (advanced.every(entry => entry.kind === "OneOf") &&
|
|
515
|
+
isNotEmpty(advanced) &&
|
|
516
|
+
advanced.every(entry => entry.OneOf.options.length === 3)) {
|
|
517
|
+
const optionsInAllEntries = advanced[0].OneOf.options.filter(option => advanced.every(otherEntry => otherEntry.OneOf.options.some(otherOption => deepEqual(option, otherOption))));
|
|
518
|
+
if (optionsInAllEntries.length === 2) {
|
|
519
|
+
const uniqueOptions = advanced
|
|
520
|
+
.map(entry => entry.OneOf.options.find(option => optionsInAllEntries.every(commonOption => !deepEqual(option, commonOption))))
|
|
521
|
+
.filter(isNotNullish); // should not do anything, just for type narrowing
|
|
522
|
+
if (uniqueOptions.length === advanced.length) {
|
|
523
|
+
const [first, second] = optionsInAllEntries.map(option => renderAdvancedSpecialAbilityName(translate, translateMap, getInstanceById, getResolvedSelectOptionById, entityName, option));
|
|
524
|
+
return [
|
|
525
|
+
...uniqueOptions.map(option => renderAdvancedSpecialAbilityName(translate, translateMap, getInstanceById, getResolvedSelectOptionById, entityName, option)),
|
|
526
|
+
translate("one or two of these special abilities can alternatively be replaced by advanced special abilities {$first} and/or {$second}", { first, second }),
|
|
527
|
+
].join(", ");
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const derivedFromExternalOptionEntriesToGenerate = count(advanced, entry => entry.kind === "DeriveFromExternalOption" &&
|
|
532
|
+
entry.DeriveFromExternalOption.display_option === undefined);
|
|
533
|
+
return [
|
|
534
|
+
...advanced.map((entry) => {
|
|
535
|
+
switch (entry.kind) {
|
|
536
|
+
case "General":
|
|
537
|
+
return renderAdvancedSpecialAbilityName(translate, translateMap, getInstanceById, getResolvedSelectOptionById, entityName, entry.General);
|
|
538
|
+
case "RestrictOptions":
|
|
539
|
+
return renderAdvancedSpecialAbilityName(translate, translateMap, getInstanceById, getResolvedSelectOptionById, entityName, entry.RestrictOptions.id, entry.RestrictOptions.option);
|
|
540
|
+
case "OneOf": {
|
|
541
|
+
if (entry.OneOf.display_option !== undefined) {
|
|
542
|
+
switch (entry.OneOf.display_option.kind) {
|
|
543
|
+
case "Hide":
|
|
544
|
+
return undefined;
|
|
545
|
+
case "ReplaceWith":
|
|
546
|
+
return (translateMap(entry.OneOf.display_option.ReplaceWith.translations)?.replacement ??
|
|
547
|
+
MISSING_VALUE);
|
|
548
|
+
default:
|
|
549
|
+
return assertExhaustive(entry.OneOf.display_option);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return localeJoin(entry.OneOf.options.map(option => renderAdvancedSpecialAbilityName(translate, translateMap, getInstanceById, getResolvedSelectOptionById, entityName, option)), "disjunction");
|
|
553
|
+
}
|
|
554
|
+
case "DeriveFromExternalOption":
|
|
555
|
+
if (entry.DeriveFromExternalOption.display_option !== undefined) {
|
|
556
|
+
switch (entry.DeriveFromExternalOption.display_option.kind) {
|
|
557
|
+
case "Hide":
|
|
558
|
+
return undefined;
|
|
559
|
+
case "ReplaceWith":
|
|
560
|
+
return (translateMap(entry.DeriveFromExternalOption.display_option.ReplaceWith.translations)?.replacement ?? MISSING_VALUE);
|
|
561
|
+
default:
|
|
562
|
+
return assertExhaustive(entry.DeriveFromExternalOption.display_option);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return undefined;
|
|
566
|
+
default:
|
|
567
|
+
return assertExhaustive(entry);
|
|
568
|
+
}
|
|
569
|
+
}),
|
|
570
|
+
derivedFromExternalOptionEntriesToGenerate === 0
|
|
571
|
+
? undefined
|
|
572
|
+
: translate(".input {$count :number} {{{$count} more by primary patron}}", {
|
|
573
|
+
count: derivedFromExternalOptionEntriesToGenerate,
|
|
574
|
+
}),
|
|
575
|
+
]
|
|
576
|
+
.filter(isNotNullish)
|
|
577
|
+
.join(", ");
|
|
578
|
+
};
|
|
579
|
+
const renderDeriveFromExternalOptionTable = (translate, translateMap, localeCompare, getInstanceById, getResolvedSelectOptionById, entityName, advanced) => {
|
|
580
|
+
if (!isStyleSpecialAbilityKind(entityName)) {
|
|
581
|
+
return undefined;
|
|
582
|
+
}
|
|
583
|
+
const derivedFromExternalOptionEntriesToGenerate = advanced.filter((entry) => entry.kind === "DeriveFromExternalOption" &&
|
|
584
|
+
entry.DeriveFromExternalOption.display_option === undefined);
|
|
585
|
+
if (derivedFromExternalOptionEntriesToGenerate.length === 0) {
|
|
586
|
+
return undefined;
|
|
587
|
+
}
|
|
588
|
+
const rows = Map.groupBy(derivedFromExternalOptionEntriesToGenerate.flatMap(entry => entry.DeriveFromExternalOption.map.map((option) => [
|
|
589
|
+
option.from_option,
|
|
590
|
+
option.to_advanced,
|
|
591
|
+
])), row => row[0])
|
|
592
|
+
.entries()
|
|
593
|
+
.map(([option, list]) => [
|
|
594
|
+
translateMap(getInstanceById("Patron", option)?.translations)?.name ?? MISSING_VALUE,
|
|
595
|
+
list
|
|
596
|
+
.map(([, toAdvanced]) => renderAdvancedSpecialAbilityName(translate, translateMap, getInstanceById, getResolvedSelectOptionById, entityName, toAdvanced))
|
|
597
|
+
.toSorted(localeCompare)
|
|
598
|
+
.join(", "),
|
|
599
|
+
])
|
|
600
|
+
.toArray()
|
|
601
|
+
.toSorted(on(row => row[0], localeCompare));
|
|
602
|
+
return {
|
|
603
|
+
type: "table",
|
|
604
|
+
header: [translate("Patron"), renderAdvancedLabel(translate, entityName)],
|
|
605
|
+
rows,
|
|
606
|
+
};
|
|
607
|
+
};
|
|
608
|
+
const renderTrailingAdvancedPrerequisitesNote = (translate, entityName) => {
|
|
609
|
+
if (isAdvancedSpecialAbilityKind(entityName)) {
|
|
610
|
+
switch (entityName) {
|
|
611
|
+
case "AdvancedCombatSpecialAbility":
|
|
612
|
+
return translate("corresponding combat style special ability");
|
|
613
|
+
case "AdvancedKarmaSpecialAbility":
|
|
614
|
+
return translate("corresponding liturgical style special ability");
|
|
615
|
+
case "AdvancedMagicalSpecialAbility":
|
|
616
|
+
return translate("corresponding magic style special ability");
|
|
617
|
+
case "AdvancedSkillSpecialAbility":
|
|
618
|
+
return translate("corresponding skill style special ability");
|
|
619
|
+
default:
|
|
620
|
+
return assertExhaustive(entityName);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return undefined;
|
|
624
|
+
};
|
|
463
625
|
/**
|
|
464
626
|
* Get a JSON representation of the rules text for a special ability.
|
|
465
627
|
*/
|
|
@@ -526,18 +688,24 @@ export const getActivatableEntityDescription = createEntityDescriptionCreator(({
|
|
|
526
688
|
label: translate("Range"),
|
|
527
689
|
value: range,
|
|
528
690
|
})),
|
|
691
|
+
mapNullable(baseEntry.advanced, advanced => isStyleSpecialAbilityKind(entityName)
|
|
692
|
+
? {
|
|
693
|
+
label: renderAdvancedLabel(translate, entityName),
|
|
694
|
+
value: renderAdvancedValue(translate, translateMap, locale.join, getInstanceById, getResolvedSelectOptionById, entityName, advanced),
|
|
695
|
+
}
|
|
696
|
+
: undefined),
|
|
529
697
|
mapNullable(baseEntry.penalty, penalty => ({
|
|
530
698
|
label: translate("Penalty"),
|
|
531
699
|
value: renderPenaltyValue(getInstanceById, translate, translateMap, translation.name, penalty),
|
|
532
700
|
})),
|
|
533
|
-
mapNullable(entry.prerequisites, prerequisites => ({
|
|
701
|
+
mapNullable(entry.prerequisites ?? (isAdvancedSpecialAbilityKind(entityName) ? [] : undefined), prerequisites => ({
|
|
534
702
|
label: translate("Prerequisites"),
|
|
535
703
|
value: wrappedId.kind === "Advantage" || wrappedId.kind === "Disadvantage"
|
|
536
704
|
? printAdvantageDisadvantagePrerequisites(getInstanceById, getResolvedSelectOptionById, locale, prerequisites, translation.name, wrappedId.kind)
|
|
537
705
|
: printGeneralPrerequisites(getInstanceById, getResolvedSelectOptionById, locale, prerequisites, mapNullable(baseEntry.levels, levels => ({
|
|
538
706
|
id: wrappedId,
|
|
539
707
|
levels,
|
|
540
|
-
}))),
|
|
708
|
+
})), renderTrailingAdvancedPrerequisitesNote(translate, entityName)),
|
|
541
709
|
})),
|
|
542
710
|
mapNullable(baseEntry.combat_techniques, combatTechniques => ({
|
|
543
711
|
label: translate("Combat Techniques"),
|
|
@@ -573,6 +741,7 @@ export const getActivatableEntityDescription = createEntityDescriptionCreator(({
|
|
|
573
741
|
}),
|
|
574
742
|
],
|
|
575
743
|
},
|
|
744
|
+
renderDeriveFromExternalOptionTable(translate, translateMap, locale.compare, getInstanceById, getResolvedSelectOptionById, entityName, baseEntry.advanced ?? []),
|
|
576
745
|
],
|
|
577
746
|
errata: translation.errata,
|
|
578
747
|
references: entry.src,
|
|
@@ -20,7 +20,7 @@ export declare const printPlainGeneralPrerequisites: (getInstanceById: GetInstan
|
|
|
20
20
|
export declare const printGeneralPrerequisites: (getInstanceById: GetInstanceById<"Race" | "Culture" | "PactCategory" | "PactDomain" | "SocialStatus" | "State" | ActivatableIdentifier["kind"] | RatedIdentifier["kind"] | "Property" | "Aspect" | "Enhancement">, getResolvedSelectOptionById: GetResolvedSelectOptionById, locale: LocaleEnvironment, value: GeneralPrerequisites, printPreviousLevelPrerequisites?: {
|
|
21
21
|
id: SpecialAbilityIdentifier;
|
|
22
22
|
levels: number;
|
|
23
|
-
}) => string;
|
|
23
|
+
}, trailingText?: string) => string;
|
|
24
24
|
/**
|
|
25
25
|
* Print profession prerequisites as a string.
|
|
26
26
|
*/
|
|
@@ -78,7 +78,7 @@ const printPrerequisiteForLevel = (printPrerequisite, locale, value) => printPre
|
|
|
78
78
|
/**
|
|
79
79
|
* Print prerequisites for levels as a string.
|
|
80
80
|
*/
|
|
81
|
-
const printPrerequisitesForLevels = (printPrerequisite, locale, value, printPreviousLevelPrerequisites) => {
|
|
81
|
+
const printPrerequisitesForLevels = (printPrerequisite, locale, value, printPreviousLevelPrerequisites, trailingText) => {
|
|
82
82
|
const previousLevelPrerequisites = printPreviousLevelPrerequisites === undefined
|
|
83
83
|
? []
|
|
84
84
|
: Array.from({ length: printPreviousLevelPrerequisites.levels - 1 }, (_, i) => ({
|
|
@@ -89,6 +89,9 @@ const printPrerequisitesForLevels = (printPrerequisite, locale, value, printPrev
|
|
|
89
89
|
},
|
|
90
90
|
}));
|
|
91
91
|
const groupedByLevel = Map.groupBy([...value, ...previousLevelPrerequisites], prerequisite => prerequisite.level);
|
|
92
|
+
if (trailingText !== undefined && !groupedByLevel.has(1)) {
|
|
93
|
+
groupedByLevel.set(1, []);
|
|
94
|
+
}
|
|
92
95
|
const hasBasePrerequisites = groupedByLevel.has(1);
|
|
93
96
|
const sortedByLevel = groupedByLevel
|
|
94
97
|
.entries()
|
|
@@ -104,14 +107,24 @@ const printPrerequisitesForLevels = (printPrerequisite, locale, value, printPrev
|
|
|
104
107
|
})}: ${locale.translate("none")}`,
|
|
105
108
|
]),
|
|
106
109
|
...sortedByLevel.map(([levelNumber, prerequisites]) => {
|
|
107
|
-
const prerequisitesString = joinPrerequisiteParts(locale.translate, locale.translateMap, locale.compare,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
const prerequisitesString = joinPrerequisiteParts(locale.translate, locale.translateMap, locale.compare, [
|
|
111
|
+
...prerequisites
|
|
112
|
+
.map(element => mapNullable(printPrerequisiteForLevel(printPrerequisite, locale, element), part => ({
|
|
113
|
+
type: element.prerequisite.kind === "Single"
|
|
114
|
+
? element.prerequisite.Single.kind
|
|
115
|
+
: element.prerequisite.kind,
|
|
116
|
+
part,
|
|
117
|
+
})))
|
|
118
|
+
.filter(isNotNullish),
|
|
119
|
+
...(levelNumber === 1 && trailingText !== undefined
|
|
120
|
+
? [
|
|
121
|
+
{
|
|
122
|
+
type: "trailing",
|
|
123
|
+
part: { value: trailingText, sentenceType: undefined, isMeta: false },
|
|
124
|
+
},
|
|
125
|
+
]
|
|
126
|
+
: []),
|
|
127
|
+
]);
|
|
115
128
|
return hasOnlyBasePrerequisites
|
|
116
129
|
? prerequisitesString
|
|
117
130
|
: `${locale.translate("Level {$level}", {
|
|
@@ -136,7 +149,7 @@ export const printPlainGeneralPrerequisites = (getInstanceById, getResolvedSelec
|
|
|
136
149
|
/**
|
|
137
150
|
* Print general prerequisites as a string.
|
|
138
151
|
*/
|
|
139
|
-
export const printGeneralPrerequisites = (getInstanceById, getResolvedSelectOptionById, locale, value, printPreviousLevelPrerequisites) => printPrerequisitesForLevels(prerequisite => printGeneralPrerequisiteGroup(getInstanceById, getResolvedSelectOptionById, locale, prerequisite), locale, value, printPreviousLevelPrerequisites === undefined
|
|
152
|
+
export const printGeneralPrerequisites = (getInstanceById, getResolvedSelectOptionById, locale, value, printPreviousLevelPrerequisites, trailingText) => printPrerequisitesForLevels(prerequisite => printGeneralPrerequisiteGroup(getInstanceById, getResolvedSelectOptionById, locale, prerequisite), locale, value, printPreviousLevelPrerequisites === undefined
|
|
140
153
|
? undefined
|
|
141
154
|
: {
|
|
142
155
|
levels: printPreviousLevelPrerequisites.levels,
|
|
@@ -148,7 +161,7 @@ export const printGeneralPrerequisites = (getInstanceById, getResolvedSelectOpti
|
|
|
148
161
|
level,
|
|
149
162
|
},
|
|
150
163
|
}),
|
|
151
|
-
});
|
|
164
|
+
}, trailingText);
|
|
152
165
|
/**
|
|
153
166
|
* Print profession prerequisites as a string.
|
|
154
167
|
*/
|
|
@@ -5,4 +5,4 @@ export declare const MISSING_VALUE = "###MISSING###";
|
|
|
5
5
|
/**
|
|
6
6
|
* String to display when a translation that should be present produces an error.
|
|
7
7
|
*/
|
|
8
|
-
export declare const UNHANDLED_VALUE = "###
|
|
8
|
+
export declare const UNHANDLED_VALUE = "###UNHANDLEDERROR###";
|
|
@@ -3,6 +3,7 @@ import { ensureNonEmpty, isEmpty, isNotEmpty, } from "@elyukai/utils/array/nonEm
|
|
|
3
3
|
import { deepEqual, equal } from "@elyukai/utils/equality";
|
|
4
4
|
import { on } from "@elyukai/utils/function";
|
|
5
5
|
import { isNotNullish, isNullish, mapNullable, nullableToArray, } from "@elyukai/utils/nullable";
|
|
6
|
+
import { omitKeys } from "@elyukai/utils/object";
|
|
6
7
|
import { compareNumber } from "@elyukai/utils/ordering";
|
|
7
8
|
import { Reader } from "@elyukai/utils/reader";
|
|
8
9
|
import { assertExhaustive } from "@elyukai/utils/typeSafety";
|
|
@@ -11,7 +12,7 @@ import { combineNameComponents, getNameComponents, renderActivatableNameComponen
|
|
|
11
12
|
import { renderCommonnessRatedAdvantagesOrDisadvantages, renderValueWithPossibleTranslation, } from "./partial/commonnessRatedAdvantagesAndDisadvantages.js";
|
|
12
13
|
import { printProfessionPrerequisites } from "./partial/prerequisites/index.js";
|
|
13
14
|
import { parensIf } from "./partial/rated/activatable/parensIf.js";
|
|
14
|
-
import { formatR, getChildInstancesForInstanceIdR, getInstanceByIdR, localeCompareR, localeJoinR, localeSortR, nameR, strictNameR, translateR, translationR, } from "./partial/reader.js";
|
|
15
|
+
import { formatR, getChildInstancesForInstanceIdR, getInstanceByIdR, localeCompareR, localeJoinR, localeSortR, nameR, strictNameR, translateMapFnR, translateR, translationR, } from "./partial/reader.js";
|
|
15
16
|
import { MISSING_VALUE, UNHANDLED_VALUE } from "./partial/unknown.js";
|
|
16
17
|
const prepareProfessionPackages = (professionId) => Reader.asks(({ getInstanceById, getChildInstancesForInstanceId }) => getChildInstancesForInstanceId("ProfessionPackage", professionId)
|
|
17
18
|
.map((professionPackage) => ({
|
|
@@ -425,9 +426,11 @@ const renderRatedVariantChanges = (baseList, variantList, renderInstance) => Rea
|
|
|
425
426
|
const renderVariantOption = (base, variant, key, render) => (variant.options?.[key] === undefined
|
|
426
427
|
? Reader.of(undefined)
|
|
427
428
|
: render(base.options?.[key], variant.options[key])).map(text => nullableToArray(text));
|
|
428
|
-
const renderProfessionVariantLabel = (base, variant,
|
|
429
|
+
const renderProfessionVariantLabel = (base, variant, translations) => translateR("{$value} AP", {
|
|
429
430
|
value: base.ap_value + (variant.ap_value ?? 0),
|
|
430
|
-
}).
|
|
431
|
+
}).thenW(apValueText => localeSortR(translations.map(({ id, content }) => `^[${content.name.default}](entity: "ProfessionVariant", instance: "${id}")`))
|
|
432
|
+
.thenW(list => localeJoinR(list, "conjunction"))
|
|
433
|
+
.map(names => names + parensIf(apValueText)));
|
|
431
434
|
const renderProfessionVariantText = (base, variant, translation) => translation.full_text !== undefined
|
|
432
435
|
? Reader.of(translation.full_text)
|
|
433
436
|
: Reader.sequence([
|
|
@@ -468,15 +471,40 @@ const renderProfessionVariantText = (base, variant, translation) => translation.
|
|
|
468
471
|
.filter(isNotEmpty)
|
|
469
472
|
.map(list => list.join(", "))
|
|
470
473
|
.join("; ") + (translation.concluding_text ?? ""));
|
|
471
|
-
const renderProfessionVariant = (base,
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
})))
|
|
474
|
+
const renderProfessionVariant = (base, variants) => translateMapFnR.thenW(translateMap => {
|
|
475
|
+
const translations = variants
|
|
476
|
+
.map(variant => mapNullable(translateMap(variant.content.translations), t => ({
|
|
477
|
+
id: variant.id,
|
|
478
|
+
content: t,
|
|
479
|
+
})))
|
|
480
|
+
.filter(isNotNullish);
|
|
481
|
+
return !isNotEmpty(translations)
|
|
482
|
+
? Reader.of(undefined)
|
|
483
|
+
: renderProfessionVariantLabel(base, variants[0].content, translations).thenW(label => renderProfessionVariantText(base, variants[0].content, translations[0].content).map((value) => ({
|
|
484
|
+
label,
|
|
485
|
+
value,
|
|
486
|
+
})));
|
|
487
|
+
});
|
|
488
|
+
const equalProfessionVariantValues = (translateMap) => on(variant => {
|
|
489
|
+
const translation = translateMap(variant.translations);
|
|
490
|
+
return {
|
|
491
|
+
...variant,
|
|
492
|
+
translations: translation === undefined ? undefined : omitKeys(translation, "name"),
|
|
493
|
+
};
|
|
494
|
+
}, deepEqual);
|
|
495
|
+
const groupProfessionVariantsThatContainTheSameChanges = (variants) => Reader.asks(({ translateMap }) => variants.reduce((acc, variant) => {
|
|
496
|
+
const indexWithSameValuesGranted = acc.findIndex(group => equalProfessionVariantValues(translateMap)(group[0].content, variant.content));
|
|
497
|
+
if (indexWithSameValuesGranted > -1) {
|
|
498
|
+
acc[indexWithSameValuesGranted]?.push(variant);
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
acc.push([variant]);
|
|
502
|
+
}
|
|
503
|
+
return acc;
|
|
504
|
+
}, []));
|
|
477
505
|
const renderProfessionVariants = (professionPackages) => professionPackages.length > 1
|
|
478
506
|
? Reader.of(UNHANDLED_VALUE)
|
|
479
|
-
: getChildInstancesForInstanceIdR("ProfessionVariant", professionPackages[0].id).thenW(variants => Reader.traverse(
|
|
507
|
+
: getChildInstancesForInstanceIdR("ProfessionVariant", professionPackages[0].id).thenW(variants => groupProfessionVariantsThatContainTheSameChanges(variants).thenW(groupedVariants => Reader.traverse(groupedVariants, variant => renderProfessionVariant(professionPackages[0].content, variant))
|
|
480
508
|
.map(list => list.filter(isNotNullish))
|
|
481
509
|
.thenW(list => list.length === 0
|
|
482
510
|
? Reader.of(undefined)
|
|
@@ -486,7 +514,7 @@ const renderProfessionVariants = (professionPackages) => professionPackages.leng
|
|
|
486
514
|
style: "nested",
|
|
487
515
|
items: list.toSorted(on(item => item.label, localeCompare)),
|
|
488
516
|
},
|
|
489
|
-
])));
|
|
517
|
+
]))));
|
|
490
518
|
/**
|
|
491
519
|
* Get a JSON representation of the rules text for a profession version.
|
|
492
520
|
*/
|