first changes and cleanup
This commit is contained in:
@@ -12,13 +12,22 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Use Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm test
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build TypeScript (1st pass)
|
||||
run: npm run build
|
||||
continue-on-error: true
|
||||
- name: Generate database typings
|
||||
run: npm run generate
|
||||
- name: Build TypeScript (2nd pass)
|
||||
run: npm run build
|
||||
|
||||
publish-npm:
|
||||
needs: build
|
||||
@@ -27,12 +36,21 @@ jobs:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Use Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run generate
|
||||
- run: npm publish --provenance --access public
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build TypeScript (1st pass)
|
||||
run: npm run build
|
||||
continue-on-error: true
|
||||
- name: Generate database typings
|
||||
run: npm run generate
|
||||
- name: Build TypeScript (2nd pass)
|
||||
run: npm run build
|
||||
- name: Publish package to npm
|
||||
run: npm publish --provenance --access public
|
||||
|
||||
+1
-2
@@ -19,10 +19,9 @@
|
||||
"exports": {
|
||||
".": "./lib/main.js",
|
||||
"./gen": "./gen/types.d.ts",
|
||||
"./errors": "./lib/errors.js",
|
||||
"./utils": "./lib/utils.js",
|
||||
"./cache/*": "./lib/cache/*.js",
|
||||
"./config/*": "./lib/config/*.js",
|
||||
"./rendering/*": "./lib/rendering/*.js",
|
||||
"./types": "./lib/types/index.js",
|
||||
"./types/*": "./lib/types/*.js"
|
||||
},
|
||||
|
||||
+61
-107
@@ -1,60 +1,14 @@
|
||||
import { isNotNullish } from "@optolith/helpers/nullable"
|
||||
import { mapObject } from "@optolith/helpers/object"
|
||||
import { assertExhaustive } from "@optolith/helpers/typeSafety"
|
||||
import type { CacheConfig } from "../cacheConfig.js"
|
||||
import { TypeMap } from "../config/types.js"
|
||||
import { ValidResults } from "../main.js"
|
||||
import { Aspect } from "../types/Aspect.js"
|
||||
import { Element } from "../types/Element.js"
|
||||
import { Property } from "../types/Property.js"
|
||||
import { TargetCategory } from "../types/TargetCategory.js"
|
||||
import {
|
||||
ExplicitSelectOption,
|
||||
SelectOptions,
|
||||
SkillApplication,
|
||||
SkillApplications,
|
||||
SkillUse,
|
||||
SkillUses,
|
||||
} from "../types/_Activatable.js"
|
||||
import {
|
||||
SelectOptionCategory,
|
||||
SelectOptionsAdventurePointsValue,
|
||||
SkillApplicationOrUse,
|
||||
SkillSelectOptionCategoryPrerequisite,
|
||||
SpecificFromSkillSelectOptionCategoryCategory,
|
||||
SpecificTargetCategory,
|
||||
} from "../types/_ActivatableSelectOptionCategory.js"
|
||||
import {
|
||||
CeremonyIdentifier,
|
||||
CloseCombatTechniqueIdentifier,
|
||||
LiturgicalChantIdentifier,
|
||||
RangedCombatTechniqueIdentifier,
|
||||
RitualIdentifier,
|
||||
SkillIdentifier,
|
||||
SpellIdentifier,
|
||||
} from "../types/_Identifier.js"
|
||||
import {
|
||||
ActivatableIdentifier,
|
||||
CombatTechniqueIdentifier,
|
||||
SelectOptionIdentifier,
|
||||
SkillishIdentifier as SkillIdentifierGroup,
|
||||
} from "../types/_IdentifierGroup.js"
|
||||
import { ImprovementCost } from "../types/_ImprovementCost.js"
|
||||
import { LocaleMap } from "../types/_LocaleMap.js"
|
||||
import { GeneralPrerequisites, PrerequisiteForLevel } from "../types/_Prerequisite.js"
|
||||
import { Poison } from "../types/equipment/item/Poison.js"
|
||||
import { GeneralPrerequisiteGroup } from "../types/prerequisites/PrerequisiteGroups.js"
|
||||
import { Errata } from "../types/source/_Erratum.js"
|
||||
import { PublicationRefs } from "../types/source/_PublicationRef.js"
|
||||
import { BlessedTradition } from "../types/specialAbility/BlessedTradition.js"
|
||||
import { Language } from "../types/specialAbility/sub/Language.js"
|
||||
import * as Database from "../../gen/types.js"
|
||||
|
||||
const PRINCIPLES_ID = 31
|
||||
const PROPERTY_KNOWLEDGE_ID = 3
|
||||
const ASPECT_KNOWLEDGE_ID = 1
|
||||
|
||||
export type ResolvedSelectOption = {
|
||||
id: SelectOptionIdentifier
|
||||
id: Database.RequirableSelectOptionIdentifier
|
||||
|
||||
/**
|
||||
* Sometimes, professions use specific text selections that are not
|
||||
@@ -99,7 +53,7 @@ export type ResolvedSelectOption = {
|
||||
*/
|
||||
ap_value?: number
|
||||
|
||||
src?: PublicationRefs
|
||||
src?: Database.PublicationRefs
|
||||
|
||||
/**
|
||||
* All translations for the entry, identified by IETF language tag (BCP47).
|
||||
@@ -129,10 +83,10 @@ export type ResolvedSelectOptionTranslation = {
|
||||
}
|
||||
const matchesSpecificSkillishIdList = <T>(
|
||||
id: T,
|
||||
config: SpecificFromSkillSelectOptionCategoryCategory<{ id: T }>,
|
||||
equalsId: (a: T, b: T) => boolean
|
||||
config: Database.SpecificFromSkillSelectOptionCategoryCategory<{ id: T }>,
|
||||
equalsId: (a: T, b: T) => boolean,
|
||||
): boolean => {
|
||||
switch (config.operation) {
|
||||
switch (config.operation.kind) {
|
||||
case "Intersection":
|
||||
return config.list.some(ref => equalsId(ref.id, id))
|
||||
case "Difference":
|
||||
@@ -143,8 +97,8 @@ const matchesSpecificSkillishIdList = <T>(
|
||||
}
|
||||
|
||||
const getSkillishPrerequisites = (
|
||||
ps: SkillSelectOptionCategoryPrerequisite[] | undefined,
|
||||
id: SkillIdentifierGroup | CombatTechniqueIdentifier
|
||||
ps: Database.SkillSelectOptionCategoryPrerequisite[] | undefined,
|
||||
id: SkillishIdentifier | Database.CombatTechniqueIdentifier,
|
||||
): GeneralPrerequisites | undefined => {
|
||||
if (ps === undefined) {
|
||||
return undefined
|
||||
@@ -190,7 +144,7 @@ const getSkillishPrerequisites = (
|
||||
|
||||
const equalsSkillishIdGroup = (
|
||||
a: SkillIdentifierGroup | CombatTechniqueIdentifier,
|
||||
b: SkillIdentifierGroup | CombatTechniqueIdentifier
|
||||
b: SkillIdentifierGroup | CombatTechniqueIdentifier,
|
||||
): boolean => {
|
||||
switch (a.tag) {
|
||||
case "Skill":
|
||||
@@ -221,7 +175,7 @@ const getApValueForSkillish = (
|
||||
| SelectOptionsAdventurePointsValue<SkillIdentifierGroup | CombatTechniqueIdentifier>
|
||||
| undefined,
|
||||
id: SkillIdentifierGroup | CombatTechniqueIdentifier,
|
||||
ic: ImprovementCost
|
||||
ic: ImprovementCost,
|
||||
): number | undefined => {
|
||||
if (config === undefined) {
|
||||
return undefined
|
||||
@@ -259,7 +213,7 @@ const getApValueForSkillish = (
|
||||
|
||||
const convertSkillApplicationOrUse = (
|
||||
id: SkillIdentifier,
|
||||
applicationOrUse: SkillApplicationOrUse
|
||||
applicationOrUse: SkillApplicationOrUse,
|
||||
): SkillApplication | SkillUse => ({
|
||||
id: applicationOrUse.id,
|
||||
skill: { tag: "Single", single: { id } },
|
||||
@@ -269,7 +223,7 @@ const convertSkillApplicationOrUse = (
|
||||
const getDerivedSelectOptions = (
|
||||
selectOptionCategory: SelectOptionCategory,
|
||||
entryId: ActivatableIdentifier,
|
||||
database: ValidResults
|
||||
database: ValidResults,
|
||||
): ResolvedSelectOption[] => {
|
||||
switch (selectOptionCategory.tag) {
|
||||
case "Blessings":
|
||||
@@ -317,14 +271,14 @@ const getDerivedSelectOptions = (
|
||||
.toSorted(([_1, a], [_2, b]) => a.size.id - b.size.id)
|
||||
.map(([id]) => id),
|
||||
}),
|
||||
{}
|
||||
{},
|
||||
)
|
||||
|
||||
return database.animalShapes.map(([_, animalShape]) => {
|
||||
const path = database.animalShapePaths.find(([id]) => id === animalShape.path.id)?.[1]
|
||||
const size = database.animalShapeSizes.find(([id]) => id === animalShape.size.id)?.[1]
|
||||
const pathIndex =
|
||||
path !== undefined ? pathsWithOrderedIds[path.id]?.indexOf(animalShape.id) ?? -1 : -1
|
||||
path !== undefined ? (pathsWithOrderedIds[path.id]?.indexOf(animalShape.id) ?? -1) : -1
|
||||
return {
|
||||
id: { tag: "AnimalShape", animal_shape: animalShape.id },
|
||||
prerequisites:
|
||||
@@ -333,7 +287,7 @@ const getDerivedSelectOptions = (
|
||||
? database.animalShapePaths
|
||||
.filter(
|
||||
([id]) =>
|
||||
id !== animalShape.path.id && pathsWithOrderedIds[id]?.[0] !== undefined
|
||||
id !== animalShape.path.id && pathsWithOrderedIds[id]?.[0] !== undefined,
|
||||
)
|
||||
.map(([id]) => ({
|
||||
level: 1,
|
||||
@@ -445,7 +399,7 @@ const getDerivedSelectOptions = (
|
||||
translations: mapObject(race.translations, t10n => ({
|
||||
name: t10n.name,
|
||||
})),
|
||||
})
|
||||
}),
|
||||
),
|
||||
...database.cultures.map(
|
||||
([_, culture]): ResolvedSelectOption => ({
|
||||
@@ -454,13 +408,13 @@ const getDerivedSelectOptions = (
|
||||
translations: mapObject(culture.translations, t10n => ({
|
||||
name: t10n.name,
|
||||
})),
|
||||
})
|
||||
}),
|
||||
),
|
||||
]
|
||||
|
||||
case "BlessedTraditions": {
|
||||
const getPrerequisites = (
|
||||
blessedTradition: BlessedTradition
|
||||
blessedTradition: BlessedTradition,
|
||||
): GeneralPrerequisites | undefined => {
|
||||
if (
|
||||
selectOptionCategory.blessed_traditions.require_principles &&
|
||||
@@ -500,7 +454,7 @@ const getDerivedSelectOptions = (
|
||||
case "Elements": {
|
||||
const mapToResolvedSelectOption = ([_, element]: [
|
||||
number,
|
||||
Element
|
||||
Element,
|
||||
]): ResolvedSelectOption => ({
|
||||
id: { tag: "Element", element: element.id },
|
||||
translations: mapObject(element.translations, t10n => ({
|
||||
@@ -511,7 +465,7 @@ const getDerivedSelectOptions = (
|
||||
if (selectOptionCategory.elements.specific) {
|
||||
return database.elements
|
||||
.filter(([id]) =>
|
||||
selectOptionCategory.elements.specific!.some(ref => ref.id.element === id)
|
||||
selectOptionCategory.elements.specific!.some(ref => ref.id.element === id),
|
||||
)
|
||||
.map(mapToResolvedSelectOption)
|
||||
}
|
||||
@@ -650,9 +604,9 @@ const getDerivedSelectOptions = (
|
||||
? undefined
|
||||
: {
|
||||
name: t10n.master_of_aspect_suffix,
|
||||
}
|
||||
},
|
||||
),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.filter(value => Object.keys(value.translations).length > 0)
|
||||
}
|
||||
@@ -759,7 +713,7 @@ const getDerivedSelectOptions = (
|
||||
const matchesGroupRequirement =
|
||||
category.skills.groups === undefined ||
|
||||
category.skills.groups.some(
|
||||
ref => ref.id.skill_group === skill.group.id.skill_group
|
||||
ref => ref.id.skill_group === skill.group.id.skill_group,
|
||||
)
|
||||
|
||||
const matchesIdRequirement =
|
||||
@@ -767,7 +721,7 @@ const getDerivedSelectOptions = (
|
||||
matchesSpecificSkillishIdList<SkillIdentifier>(
|
||||
{ tag: "Skill", skill: skill.id },
|
||||
category.skills.specific,
|
||||
equalsSkillishIdGroup
|
||||
equalsSkillishIdGroup,
|
||||
)
|
||||
|
||||
return matchesGroupRequirement && matchesIdRequirement
|
||||
@@ -777,16 +731,16 @@ const getDerivedSelectOptions = (
|
||||
return {
|
||||
id,
|
||||
skill_uses: category.skills.skill_uses?.map(use =>
|
||||
convertSkillApplicationOrUse(id, use)
|
||||
convertSkillApplicationOrUse(id, use),
|
||||
),
|
||||
skill_applications: category.skills.skill_applications?.map(use =>
|
||||
convertSkillApplicationOrUse(id, use)
|
||||
convertSkillApplicationOrUse(id, use),
|
||||
),
|
||||
prerequisites: getSkillishPrerequisites(category.skills.prerequisites, id),
|
||||
ap_value: getApValueForSkillish(
|
||||
category.skills.ap_value ?? apValueGen,
|
||||
id,
|
||||
skill.improvement_cost
|
||||
skill.improvement_cost,
|
||||
),
|
||||
src: skill.src,
|
||||
translations: mapObject(skill.translations, t10n => ({
|
||||
@@ -802,8 +756,8 @@ const getDerivedSelectOptions = (
|
||||
matchesSpecificSkillishIdList<SpellIdentifier>(
|
||||
{ tag: "Spell", spell: spell.id },
|
||||
category.spells.specific,
|
||||
equalsSkillishIdGroup
|
||||
)
|
||||
equalsSkillishIdGroup,
|
||||
),
|
||||
)
|
||||
.map(([_, spell]): ResolvedSelectOption => {
|
||||
const id: SpellIdentifier = { tag: "Spell", spell: spell.id }
|
||||
@@ -825,8 +779,8 @@ const getDerivedSelectOptions = (
|
||||
matchesSpecificSkillishIdList<RitualIdentifier>(
|
||||
{ tag: "Ritual", ritual: ritual.id },
|
||||
category.rituals.specific,
|
||||
equalsSkillishIdGroup
|
||||
)
|
||||
equalsSkillishIdGroup,
|
||||
),
|
||||
)
|
||||
.map(([_, ritual]): ResolvedSelectOption => {
|
||||
const id: RitualIdentifier = { tag: "Ritual", ritual: ritual.id }
|
||||
@@ -848,8 +802,8 @@ const getDerivedSelectOptions = (
|
||||
matchesSpecificSkillishIdList<LiturgicalChantIdentifier>(
|
||||
{ tag: "LiturgicalChant", liturgical_chant: liturgicalChant.id },
|
||||
category.liturgical_chants.specific,
|
||||
equalsSkillishIdGroup
|
||||
)
|
||||
equalsSkillishIdGroup,
|
||||
),
|
||||
)
|
||||
.map(([_, liturgicalChant]): ResolvedSelectOption => {
|
||||
const id: LiturgicalChantIdentifier = {
|
||||
@@ -860,7 +814,7 @@ const getDerivedSelectOptions = (
|
||||
id,
|
||||
prerequisites: getSkillishPrerequisites(
|
||||
category.liturgical_chants.prerequisites,
|
||||
id
|
||||
id,
|
||||
),
|
||||
ap_value: getApValueForSkillish(apValueGen, id, liturgicalChant.improvement_cost),
|
||||
src: liturgicalChant.src,
|
||||
@@ -877,8 +831,8 @@ const getDerivedSelectOptions = (
|
||||
matchesSpecificSkillishIdList<CeremonyIdentifier>(
|
||||
{ tag: "Ceremony", ceremony: ceremony.id },
|
||||
category.ceremonies.specific,
|
||||
equalsSkillishIdGroup
|
||||
)
|
||||
equalsSkillishIdGroup,
|
||||
),
|
||||
)
|
||||
.map(([_, ceremony]): ResolvedSelectOption => {
|
||||
const id: CeremonyIdentifier = { tag: "Ceremony", ceremony: ceremony.id }
|
||||
@@ -914,8 +868,8 @@ const getDerivedSelectOptions = (
|
||||
close_combat_technique: closeCombatTechnique.id,
|
||||
},
|
||||
category.close_combat_techniques.specific,
|
||||
equalsSkillishIdGroup
|
||||
)
|
||||
equalsSkillishIdGroup,
|
||||
),
|
||||
)
|
||||
.map(([_, closeCombatTechnique]): ResolvedSelectOption => {
|
||||
const id: CloseCombatTechniqueIdentifier = {
|
||||
@@ -926,12 +880,12 @@ const getDerivedSelectOptions = (
|
||||
id,
|
||||
prerequisites: getSkillishPrerequisites(
|
||||
category.close_combat_techniques.prerequisites,
|
||||
id
|
||||
id,
|
||||
),
|
||||
ap_value: getApValueForSkillish(
|
||||
apValueGen,
|
||||
id,
|
||||
closeCombatTechnique.improvement_cost
|
||||
closeCombatTechnique.improvement_cost,
|
||||
),
|
||||
src: closeCombatTechnique.src,
|
||||
translations: mapObject(closeCombatTechnique.translations, t10n => ({
|
||||
@@ -950,8 +904,8 @@ const getDerivedSelectOptions = (
|
||||
ranged_combat_technique: rangedCombatTechnique.id,
|
||||
},
|
||||
category.ranged_combat_techniques.specific,
|
||||
equalsSkillishIdGroup
|
||||
)
|
||||
equalsSkillishIdGroup,
|
||||
),
|
||||
)
|
||||
.map(([_, rangedCombatTechnique]): ResolvedSelectOption => {
|
||||
const id: RangedCombatTechniqueIdentifier = {
|
||||
@@ -962,12 +916,12 @@ const getDerivedSelectOptions = (
|
||||
id,
|
||||
prerequisites: getSkillishPrerequisites(
|
||||
category.ranged_combat_techniques.prerequisites,
|
||||
id
|
||||
id,
|
||||
),
|
||||
ap_value: getApValueForSkillish(
|
||||
apValueGen,
|
||||
id,
|
||||
rangedCombatTechnique.improvement_cost
|
||||
rangedCombatTechnique.improvement_cost,
|
||||
),
|
||||
src: rangedCombatTechnique.src,
|
||||
translations: mapObject(rangedCombatTechnique.translations, t10n => ({
|
||||
@@ -984,7 +938,7 @@ const getDerivedSelectOptions = (
|
||||
case "TargetCategories": {
|
||||
const mapToResolvedSelectOption = (
|
||||
targetCategory: TargetCategory,
|
||||
specificTargetCategory?: SpecificTargetCategory
|
||||
specificTargetCategory?: SpecificTargetCategory,
|
||||
): ResolvedSelectOption => ({
|
||||
id: { tag: "TargetCategory", target_category: targetCategory.id },
|
||||
volume: specificTargetCategory?.volume,
|
||||
@@ -996,15 +950,15 @@ const getDerivedSelectOptions = (
|
||||
if (selectOptionCategory.target_categories.list) {
|
||||
return database.targetCategories
|
||||
.filter(([id]) =>
|
||||
selectOptionCategory.target_categories.list!.some(ref => ref.id.target_category === id)
|
||||
selectOptionCategory.target_categories.list!.some(ref => ref.id.target_category === id),
|
||||
)
|
||||
.map(([id, targetCategory]) =>
|
||||
mapToResolvedSelectOption(
|
||||
targetCategory,
|
||||
selectOptionCategory.target_categories.list!.find(
|
||||
ref => ref.id.target_category === id
|
||||
)
|
||||
)
|
||||
ref => ref.id.target_category === id,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1019,7 +973,7 @@ const getDerivedSelectOptions = (
|
||||
const joinLocaleMaps = <T, U, V>(
|
||||
a: LocaleMap<T>,
|
||||
b: LocaleMap<U>,
|
||||
join: (a?: T, b?: U) => V
|
||||
join: (a?: T, b?: U) => V,
|
||||
): LocaleMap<NonNullable<V>> => {
|
||||
const combinedLocaleMap: LocaleMap<NonNullable<V>> = {}
|
||||
for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) {
|
||||
@@ -1033,7 +987,7 @@ const joinLocaleMaps = <T, U, V>(
|
||||
|
||||
const getExplicitSelectOptions = (
|
||||
explicitSelectOptions: ExplicitSelectOption[],
|
||||
database: ValidResults
|
||||
database: ValidResults,
|
||||
): ResolvedSelectOption[] =>
|
||||
explicitSelectOptions
|
||||
.map((explicitSelectOption): ResolvedSelectOption | undefined => {
|
||||
@@ -1053,16 +1007,16 @@ const getExplicitSelectOptions = (
|
||||
...explicitSelectOption.skill,
|
||||
id,
|
||||
skill_applications: explicitSelectOption.skill.skill_applications?.map(use =>
|
||||
convertSkillApplicationOrUse(id, use)
|
||||
convertSkillApplicationOrUse(id, use),
|
||||
),
|
||||
skill_uses: explicitSelectOption.skill.skill_uses?.map(use =>
|
||||
convertSkillApplicationOrUse(id, use)
|
||||
convertSkillApplicationOrUse(id, use),
|
||||
),
|
||||
translations: joinLocaleMaps(
|
||||
explicitSelectOption.skill.translations ?? {},
|
||||
skill.translations,
|
||||
(explicit, base) =>
|
||||
base === undefined ? undefined : { ...explicit, name: base.name }
|
||||
base === undefined ? undefined : { ...explicit, name: base.name },
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -1071,7 +1025,7 @@ const getExplicitSelectOptions = (
|
||||
case "CloseCombatTechnique": {
|
||||
const id = explicitSelectOption.combat_technique.id.close_combat_technique
|
||||
const closeCombatTechnique = database.closeCombatTechniques.find(
|
||||
p => p[0] === id
|
||||
p => p[0] === id,
|
||||
)?.[1]
|
||||
if (closeCombatTechnique === undefined) {
|
||||
return undefined
|
||||
@@ -1083,14 +1037,14 @@ const getExplicitSelectOptions = (
|
||||
explicitSelectOption.combat_technique.translations ?? {},
|
||||
closeCombatTechnique.translations,
|
||||
(explicit, base) =>
|
||||
base === undefined ? undefined : { ...explicit, name: base.name }
|
||||
base === undefined ? undefined : { ...explicit, name: base.name },
|
||||
),
|
||||
}
|
||||
}
|
||||
case "RangedCombatTechnique": {
|
||||
const id = explicitSelectOption.combat_technique.id.ranged_combat_technique
|
||||
const rangedCombatTechnique = database.rangedCombatTechniques.find(
|
||||
p => p[0] === id
|
||||
p => p[0] === id,
|
||||
)?.[1]
|
||||
if (rangedCombatTechnique === undefined) {
|
||||
return undefined
|
||||
@@ -1102,7 +1056,7 @@ const getExplicitSelectOptions = (
|
||||
explicitSelectOption.combat_technique.translations ?? {},
|
||||
rangedCombatTechnique.translations,
|
||||
(explicit, base) =>
|
||||
base === undefined ? undefined : { ...explicit, name: base.name }
|
||||
base === undefined ? undefined : { ...explicit, name: base.name },
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -1118,7 +1072,7 @@ const getExplicitSelectOptions = (
|
||||
const getSelectOptions = (
|
||||
selectOptions: SelectOptions,
|
||||
id: ActivatableIdentifier,
|
||||
database: ValidResults
|
||||
database: ValidResults,
|
||||
): ResolvedSelectOption[] => [
|
||||
...(selectOptions.derived === undefined
|
||||
? []
|
||||
@@ -1131,7 +1085,7 @@ const getSelectOptions = (
|
||||
const getSelectOptionsForResults = (
|
||||
database: ValidResults,
|
||||
idTag: ActivatableIdentifier["tag"],
|
||||
results: [id: number, data: { select_options?: SelectOptions }][]
|
||||
results: [id: number, data: { select_options?: SelectOptions }][],
|
||||
) =>
|
||||
results.reduce<{
|
||||
[id: number]: ResolvedSelectOption[]
|
||||
@@ -1247,7 +1201,7 @@ const getSelectOptionsForResults = (
|
||||
return assertExhaustive(idTag)
|
||||
}
|
||||
})(),
|
||||
database
|
||||
database,
|
||||
)
|
||||
if (options.length > 0) {
|
||||
acc[id] = options
|
||||
|
||||
Vendored
+20
-26
@@ -1,30 +1,24 @@
|
||||
import type { CacheConfig } from "../cacheConfig.js"
|
||||
import type { CacheBuilder } from "./cacheConfig.js"
|
||||
|
||||
export type AncestorBloodAdvantagesCache = {
|
||||
ids: number[]
|
||||
ids: string[]
|
||||
}
|
||||
|
||||
export const config: CacheConfig<AncestorBloodAdvantagesCache> = {
|
||||
builder(database) {
|
||||
return {
|
||||
ids: database.advantages
|
||||
.filter(([id, advantage]) =>
|
||||
advantage.prerequisites?.some(p => {
|
||||
switch (p.prerequisite.tag) {
|
||||
case "Single":
|
||||
return p.prerequisite.single.tag === "NoOtherAncestorBloodAdvantage"
|
||||
case "Disjunction":
|
||||
return p.prerequisite.disjunction.list.some(
|
||||
d => d.tag === "NoOtherAncestorBloodAdvantage"
|
||||
)
|
||||
case "Group":
|
||||
return p.prerequisite.group.list.some(
|
||||
g => g.tag === "NoOtherAncestorBloodAdvantage"
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
.map(([id]) => id),
|
||||
}
|
||||
},
|
||||
}
|
||||
export const build: CacheBuilder<AncestorBloodAdvantagesCache> = ({ getAllInstances }) => ({
|
||||
ids: getAllInstances("Advantage")
|
||||
.filter(([id, advantage]) =>
|
||||
advantage.prerequisites?.some(p => {
|
||||
switch (p.prerequisite.kind) {
|
||||
case "Single":
|
||||
return p.prerequisite.Single.kind === "NoOtherAncestorBloodAdvantage"
|
||||
case "Disjunction":
|
||||
return p.prerequisite.Disjunction.list.some(
|
||||
d => d.kind === "NoOtherAncestorBloodAdvantage",
|
||||
)
|
||||
case "Group":
|
||||
return p.prerequisite.Group.list.some(g => g.kind === "NoOtherAncestorBloodAdvantage")
|
||||
}
|
||||
}),
|
||||
)
|
||||
.map(([id]) => id),
|
||||
})
|
||||
|
||||
Vendored
+7
-4
@@ -1,5 +1,8 @@
|
||||
import { ValidResults } from "../main.js"
|
||||
import type { ChildEntityMap, EntityMap } from "../../gen/types.js"
|
||||
import type { GetAllChildInstancesForParent, GetAllInstances, GetInstanceById } from "../utils.js"
|
||||
|
||||
export type CacheConfig<T, D extends unknown[] = []> = {
|
||||
builder: (data: ValidResults, ...deps: D) => T
|
||||
}
|
||||
export type CacheBuilder<T> = (database: {
|
||||
getInstanceById: GetInstanceById<keyof EntityMap>
|
||||
getAllInstances: GetAllInstances<keyof EntityMap>
|
||||
getAllChildInstancesForParent: GetAllChildInstancesForParent<keyof ChildEntityMap>
|
||||
}) => T
|
||||
|
||||
+67
-114
@@ -1,8 +1,12 @@
|
||||
import { assertExhaustive } from "@optolith/helpers/typeSafety"
|
||||
import type { CacheConfig } from "../cacheConfig.js"
|
||||
import { ActivatableIdentifier, RatedIdentifier } from "../types/_IdentifierGroup.js"
|
||||
import { AdvantageDisadvantagePrerequisites } from "../types/_Prerequisite.js"
|
||||
import { AdvantageDisadvantagePrerequisiteGroup } from "../types/prerequisites/PrerequisiteGroups.js"
|
||||
import type {
|
||||
AdvantageDisadvantagePrerequisiteGroup,
|
||||
AdvantageDisadvantagePrerequisites,
|
||||
EntityMap,
|
||||
RatedIdentifier,
|
||||
} from "../../gen/types.js"
|
||||
import { stringFromEnumIdentifier, type GetInstanceById } from "../utils.js"
|
||||
import type { CacheBuilder } from "./cacheConfig.js"
|
||||
|
||||
const BLESSED_ID = 12
|
||||
const SPELLCASTER_ID = 47
|
||||
@@ -10,18 +14,18 @@ const SPELLCASTER_ID = 47
|
||||
export type MagicalAndBlessedAdvantagesAndDisadvantagesCache = {
|
||||
advantages: {
|
||||
magical: {
|
||||
ids: number[]
|
||||
ids: string[]
|
||||
}
|
||||
blessed: {
|
||||
ids: number[]
|
||||
ids: string[]
|
||||
}
|
||||
}
|
||||
disadvantages: {
|
||||
magical: {
|
||||
ids: number[]
|
||||
ids: string[]
|
||||
}
|
||||
blessed: {
|
||||
ids: number[]
|
||||
ids: string[]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +42,7 @@ const getAdvantageId = (type: "Magical" | "Blessed") => {
|
||||
}
|
||||
|
||||
const isRatedFor = (type: "Magical" | "Blessed", ratedId: RatedIdentifier) => {
|
||||
switch (ratedId.tag) {
|
||||
switch (ratedId.kind) {
|
||||
case "Spell":
|
||||
case "Ritual":
|
||||
return type === "Magical"
|
||||
@@ -58,26 +62,33 @@ const isRatedFor = (type: "Magical" | "Blessed", ratedId: RatedIdentifier) => {
|
||||
const isPrerequisiteFor = (
|
||||
type: "Magical" | "Blessed",
|
||||
prerequisite: AdvantageDisadvantagePrerequisiteGroup,
|
||||
getById: (
|
||||
id: ActivatableIdentifier
|
||||
) => { id: number; prerequisites?: AdvantageDisadvantagePrerequisites } | undefined,
|
||||
traversedIds: number[]
|
||||
getInstanceById: GetInstanceById<keyof EntityMap>,
|
||||
traversedIds: string[],
|
||||
): boolean => {
|
||||
switch (prerequisite.tag) {
|
||||
switch (prerequisite.kind) {
|
||||
case "Activatable": {
|
||||
if (
|
||||
prerequisite.activatable.id.tag === "Advantage" &&
|
||||
prerequisite.activatable.id.advantage === getAdvantageId(type) &&
|
||||
prerequisite.activatable.active
|
||||
prerequisite.Activatable.id.kind === "Advantage" &&
|
||||
prerequisite.Activatable.id.Advantage === getAdvantageId(type) &&
|
||||
prerequisite.Activatable.active
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
const entry = getById(prerequisite.activatable.id)
|
||||
return entry !== undefined && is(type, entry, getById, traversedIds)
|
||||
const entry = getInstanceById(prerequisite.Activatable.id)
|
||||
return (
|
||||
entry !== undefined &&
|
||||
is(
|
||||
type,
|
||||
stringFromEnumIdentifier(prerequisite.Activatable.id),
|
||||
entry,
|
||||
getInstanceById,
|
||||
traversedIds,
|
||||
)
|
||||
)
|
||||
}
|
||||
case "Rated":
|
||||
return isRatedFor(type, prerequisite.rated.id)
|
||||
return isRatedFor(type, prerequisite.Rated.id)
|
||||
case "CommonSuggestedByRCP":
|
||||
case "Sex":
|
||||
case "Race":
|
||||
@@ -91,7 +102,7 @@ const isPrerequisiteFor = (
|
||||
case "MagicalTradition":
|
||||
case "RatedMinimumNumber":
|
||||
case "RatedSum":
|
||||
case "ExternalEnhancement":
|
||||
case "Enhancement":
|
||||
case "Text":
|
||||
case "NoOtherAncestorBloodAdvantage":
|
||||
case "SexualCharacteristic":
|
||||
@@ -103,31 +114,35 @@ const isPrerequisiteFor = (
|
||||
|
||||
const is = (
|
||||
type: "Magical" | "Blessed",
|
||||
entry: { id: number; prerequisites?: AdvantageDisadvantagePrerequisites },
|
||||
getById: (
|
||||
id: ActivatableIdentifier
|
||||
) => { id: number; prerequisites?: AdvantageDisadvantagePrerequisites } | undefined,
|
||||
traversedIds: number[]
|
||||
entryId: string,
|
||||
entry: { prerequisites?: AdvantageDisadvantagePrerequisites },
|
||||
getInstanceById: GetInstanceById<keyof EntityMap>,
|
||||
traversedIds: string[],
|
||||
): boolean => {
|
||||
if (!entry.prerequisites || traversedIds.includes(entry.id)) {
|
||||
if (!entry.prerequisites || traversedIds.includes(entryId)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const newTraversedIds = [...traversedIds, entry.id]
|
||||
const newTraversedIds = [...traversedIds, entryId]
|
||||
|
||||
return (
|
||||
entry.prerequisites !== undefined &&
|
||||
entry.prerequisites.some(prerequisite => {
|
||||
switch (prerequisite.prerequisite.tag) {
|
||||
switch (prerequisite.prerequisite.kind) {
|
||||
case "Single":
|
||||
return isPrerequisiteFor(type, prerequisite.prerequisite.single, getById, newTraversedIds)
|
||||
return isPrerequisiteFor(
|
||||
type,
|
||||
prerequisite.prerequisite.Single,
|
||||
getInstanceById,
|
||||
newTraversedIds,
|
||||
)
|
||||
case "Disjunction":
|
||||
return prerequisite.prerequisite.disjunction.list.some(p =>
|
||||
isPrerequisiteFor(type, p, getById, newTraversedIds)
|
||||
return prerequisite.prerequisite.Disjunction.list.some(p =>
|
||||
isPrerequisiteFor(type, p, getInstanceById, newTraversedIds),
|
||||
)
|
||||
case "Group":
|
||||
return prerequisite.prerequisite.group.list.some(p =>
|
||||
isPrerequisiteFor(type, p, getById, newTraversedIds)
|
||||
return prerequisite.prerequisite.Group.list.some(p =>
|
||||
isPrerequisiteFor(type, p, getInstanceById, newTraversedIds),
|
||||
)
|
||||
default:
|
||||
return assertExhaustive(prerequisite.prerequisite)
|
||||
@@ -136,84 +151,22 @@ const is = (
|
||||
)
|
||||
}
|
||||
|
||||
export const config: CacheConfig<MagicalAndBlessedAdvantagesAndDisadvantagesCache> = {
|
||||
builder(database) {
|
||||
const getActivatableById = (id: ActivatableIdentifier) => {
|
||||
// prettier-ignore
|
||||
switch (id.tag) {
|
||||
case "AdvancedCombatSpecialAbility": return database.advancedCombatSpecialAbilities.find(([entryId]) => entryId === id.advanced_combat_special_ability)?.[1]
|
||||
case "AdvancedKarmaSpecialAbility": return database.advancedKarmaSpecialAbilities.find(([entryId]) => entryId === id.advanced_karma_special_ability)?.[1]
|
||||
case "AdvancedMagicalSpecialAbility": return database.advancedMagicalSpecialAbilities.find(([entryId]) => entryId === id.advanced_magical_special_ability)?.[1]
|
||||
case "AdvancedSkillSpecialAbility": return database.advancedSkillSpecialAbilities.find(([entryId]) => entryId === id.advanced_skill_special_ability)?.[1]
|
||||
case "Advantage": return database.advantages.find(([entryId]) => entryId === id.advantage)?.[1]
|
||||
case "AncestorGlyph": return database.ancestorGlyphs.find(([entryId]) => entryId === id.ancestor_glyph)?.[1]
|
||||
case "ArcaneOrbEnchantment": return database.arcaneOrbEnchantments.find(([entryId]) => entryId === id.arcane_orb_enchantment)?.[1]
|
||||
case "AttireEnchantment": return database.attireEnchantments.find(([entryId]) => entryId === id.attire_enchantment)?.[1]
|
||||
case "BlessedTradition": return database.blessedTraditions.find(([entryId]) => entryId === id.blessed_tradition)?.[1]
|
||||
case "BowlEnchantment": return database.bowlEnchantments.find(([entryId]) => entryId === id.bowl_enchantment)?.[1]
|
||||
case "BrawlingSpecialAbility": return database.brawlingSpecialAbilities.find(([entryId]) => entryId === id.brawling_special_ability)?.[1]
|
||||
case "CauldronEnchantment": return database.cauldronEnchantments.find(([entryId]) => entryId === id.cauldron_enchantment)?.[1]
|
||||
case "CeremonialItemSpecialAbility": return database.ceremonialItemSpecialAbilities.find(([entryId]) => entryId === id.ceremonial_item_special_ability)?.[1]
|
||||
case "ChronicleEnchantment": return database.chronicleEnchantments.find(([entryId]) => entryId === id.chronicle_enchantment)?.[1]
|
||||
case "CombatSpecialAbility": return database.combatSpecialAbilities.find(([entryId]) => entryId === id.combat_special_ability)?.[1]
|
||||
case "CombatStyleSpecialAbility": return database.combatStyleSpecialAbilities.find(([entryId]) => entryId === id.combat_style_special_ability)?.[1]
|
||||
case "CommandSpecialAbility": return database.commandSpecialAbilities.find(([entryId]) => entryId === id.command_special_ability)?.[1]
|
||||
case "DaggerRitual": return database.daggerRituals.find(([entryId]) => entryId === id.dagger_ritual)?.[1]
|
||||
case "Disadvantage": return database.disadvantages.find(([entryId]) => entryId === id.disadvantage)?.[1]
|
||||
case "FamiliarSpecialAbility": return database.familiarSpecialAbilities.find(([entryId]) => entryId === id.familiar_special_ability)?.[1]
|
||||
case "FatePointSexSpecialAbility": return database.fatePointSexSpecialAbilities.find(([entryId]) => entryId === id.fate_point_sex_special_ability)?.[1]
|
||||
case "FatePointSpecialAbility": return database.fatePointSpecialAbilities.find(([entryId]) => entryId === id.fate_point_special_ability)?.[1]
|
||||
case "FoolsHatEnchantment": return database.foolsHatEnchantments.find(([entryId]) => entryId === id.fools_hat_enchantment)?.[1]
|
||||
case "GeneralSpecialAbility": return database.generalSpecialAbilities.find(([entryId]) => entryId === id.general_special_ability)?.[1]
|
||||
case "InstrumentEnchantment": return database.instrumentEnchantments.find(([entryId]) => entryId === id.instrument_enchantment)?.[1]
|
||||
case "KarmaSpecialAbility": return database.karmaSpecialAbilities.find(([entryId]) => entryId === id.karma_special_ability)?.[1]
|
||||
case "Krallenkettenzauber": return database.krallenkettenzauber.find(([entryId]) => entryId === id.krallenkettenzauber)?.[1]
|
||||
case "LiturgicalStyleSpecialAbility": return database.liturgicalStyleSpecialAbilities.find(([entryId]) => entryId === id.liturgical_style_special_ability)?.[1]
|
||||
case "LycantropicGift": return database.lycantropicGifts.find(([entryId]) => entryId === id.lycantropic_gift)?.[1]
|
||||
case "MagicalSign": return database.magicalSigns.find(([entryId]) => entryId === id.magical_sign)?.[1]
|
||||
case "MagicalSpecialAbility": return database.magicalSpecialAbilities.find(([entryId]) => entryId === id.magical_special_ability)?.[1]
|
||||
case "MagicalTradition": return database.magicalTraditions.find(([entryId]) => entryId === id.magical_tradition)?.[1]
|
||||
case "MagicStyleSpecialAbility": return database.magicStyleSpecialAbilities.find(([entryId]) => entryId === id.magic_style_special_ability)?.[1]
|
||||
case "OrbEnchantment": return database.orbEnchantments.find(([entryId]) => entryId === id.orb_enchantment)?.[1]
|
||||
case "PactGift": return database.pactGifts.find(([entryId]) => entryId === id.pact_gift)?.[1]
|
||||
case "ProtectiveWardingCircleSpecialAbility": return database.protectiveWardingCircleSpecialAbilities.find(([entryId]) => entryId === id.protective_warding_circle_special_ability)?.[1]
|
||||
case "RingEnchantment": return database.ringEnchantments.find(([entryId]) => entryId === id.ring_enchantment)?.[1]
|
||||
case "Sermon": return database.sermons.find(([entryId]) => entryId === id.sermon)?.[1]
|
||||
case "SexSpecialAbility": return database.sexSpecialAbilities.find(([entryId]) => entryId === id.sex_special_ability)?.[1]
|
||||
case "SickleRitual": return database.sickleRituals.find(([entryId]) => entryId === id.sickle_ritual)?.[1]
|
||||
case "SikaryanDrainSpecialAbility": return database.sikaryanDrainSpecialAbilities.find(([entryId]) => entryId === id.sikaryan_drain_special_ability)?.[1]
|
||||
case "SkillStyleSpecialAbility": return database.skillStyleSpecialAbilities.find(([entryId]) => entryId === id.skill_style_special_ability)?.[1]
|
||||
case "SpellSwordEnchantment": return database.spellSwordEnchantments.find(([entryId]) => entryId === id.spell_sword_enchantment)?.[1]
|
||||
case "StaffEnchantment": return database.staffEnchantments.find(([entryId]) => entryId === id.staff_enchantment)?.[1]
|
||||
case "ToyEnchantment": return database.toyEnchantments.find(([entryId]) => entryId === id.toy_enchantment)?.[1]
|
||||
case "Trinkhornzauber": return database.trinkhornzauber.find(([entryId]) => entryId === id.trinkhornzauber)?.[1]
|
||||
case "VampiricGift": return database.vampiricGifts.find(([entryId]) => entryId === id.vampiric_gift)?.[1]
|
||||
case "Vision": return database.visions.find(([entryId]) => entryId === id.vision)?.[1]
|
||||
case "WandEnchantment": return database.wandEnchantments.find(([entryId]) => entryId === id.wand_enchantment)?.[1]
|
||||
case "WeaponEnchantment": return database.weaponEnchantments.find(([entryId]) => entryId === id.weapon_enchantment)?.[1]
|
||||
default:
|
||||
return assertExhaustive(id)
|
||||
}
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
return {
|
||||
advantages: {
|
||||
magical: {
|
||||
ids: database.advantages.filter(([_, entry]) => is("Magical", entry, getActivatableById, [])).map(([id]) => id),
|
||||
},
|
||||
blessed: {
|
||||
ids: database.advantages.filter(([_, entry]) => is("Blessed", entry, getActivatableById, [])).map(([id]) => id),
|
||||
},
|
||||
},
|
||||
disadvantages: {
|
||||
magical: {
|
||||
ids: database.disadvantages.filter(([_, entry]) => is("Magical", entry, getActivatableById, [])).map(([id]) => id),
|
||||
},
|
||||
blessed: {
|
||||
ids: database.disadvantages.filter(([_, entry]) => is("Blessed", entry, getActivatableById, [])).map(([id]) => id),
|
||||
},
|
||||
},
|
||||
}
|
||||
// prettier-ignore
|
||||
export const config: CacheBuilder<MagicalAndBlessedAdvantagesAndDisadvantagesCache> = ({ getAllInstances, getInstanceById }) => ({
|
||||
advantages: {
|
||||
magical: {
|
||||
ids: getAllInstances("Advantage").filter(([id, entry]) => is("Magical", id, entry, getInstanceById, [])).map(([id]) => id),
|
||||
},
|
||||
blessed: {
|
||||
ids: getAllInstances("Advantage").filter(([id, entry]) => is("Blessed", id, entry, getInstanceById, [])).map(([id]) => id),
|
||||
},
|
||||
},
|
||||
}
|
||||
disadvantages: {
|
||||
magical: {
|
||||
ids: getAllInstances("Disadvantage").filter(([id, entry]) => is("Magical", id, entry, getInstanceById, [])).map(([id]) => id),
|
||||
},
|
||||
blessed: {
|
||||
ids: getAllInstances("Disadvantage").filter(([id, entry]) => is("Blessed", id, entry, getInstanceById, [])).map(([id]) => id),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { dirname, join } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { JsonSchemaSpec } from "optolith-tsjsonschemamd/renderers/jsonSchema"
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..", "..")
|
||||
|
||||
export const sourceDir = join(root, "src", "types")
|
||||
export const libDir = join(root, "lib", "types")
|
||||
export const jsonSchemaDir = join(root, "schema")
|
||||
export const markdownDir = join(root, "docs", "reference")
|
||||
export const swiftDir = join(root, "Sources", "OptolithDatabaseSchema", "GeneratedTypes")
|
||||
|
||||
export const jsonSchemaSpec: JsonSchemaSpec = "Draft_2019_09"
|
||||
@@ -1,30 +0,0 @@
|
||||
declare global {
|
||||
interface Array<T> {
|
||||
/**
|
||||
* Returns a `Promise` that resolves when all `Promise`s in the array
|
||||
* resolve and rejects when any of the `Promise`s in the array rejects.
|
||||
*/
|
||||
promiseAll<T>(this: Promise<T>[]): Promise<T[]>
|
||||
|
||||
/**
|
||||
* Returns an object from an array of key-value pairs.
|
||||
*/
|
||||
objectFromEntries<K extends string, T>(this: [key: K, value: T][]): Record<K, T>
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(Array.prototype, "promiseAll", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: function promiseAll<T>(this: Promise<T>[]): Promise<T[]> {
|
||||
return Promise.all(this)
|
||||
},
|
||||
})
|
||||
|
||||
Object.defineProperty(Array.prototype, "objectFromEntries", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: function objectFromEntries<T>(this: [string, T][]): { [k: string]: T } {
|
||||
return Object.fromEntries(this)
|
||||
},
|
||||
})
|
||||
@@ -1,63 +0,0 @@
|
||||
import { filterNonNullable } from "@optolith/helpers/array"
|
||||
import { TypeValidationError } from "../main.js"
|
||||
import { collator } from "./i18n.js"
|
||||
import { IntegrityError } from "./validation/builders/integrity.js"
|
||||
import { FileNameError } from "./validation/builders/naming.js"
|
||||
|
||||
/**
|
||||
* Options for pretty-printing errors.
|
||||
*/
|
||||
export type PrintOptions = {
|
||||
/**
|
||||
* Whether to print each error message in a file or just print that a file has
|
||||
* errors.
|
||||
* @default false
|
||||
*/
|
||||
verbose?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Pretty-prints an error map.
|
||||
* @param errorsByFile A map from file paths to the errors that occurred in
|
||||
* them.
|
||||
* @param printOptions Configuration options for the printer.
|
||||
* @returns The pretty-printed error map.
|
||||
*/
|
||||
export const printErrors = (
|
||||
errorsByFile: Record<string, TypeValidationError[]>,
|
||||
printOptions: PrintOptions = {},
|
||||
): string => {
|
||||
const { verbose = false } = printOptions
|
||||
|
||||
return Object.entries(errorsByFile)
|
||||
.sort(([filePathA], [filePathB]) => collator.compare(filePathA, filePathB))
|
||||
.flatMap(
|
||||
verbose
|
||||
? ([filePath, errors]) => filterNonNullable(errors.map(printVerboseError(filePath)))
|
||||
: ([filePath, errors]) => [
|
||||
...errors
|
||||
.filter(
|
||||
(error): error is IntegrityError | FileNameError =>
|
||||
error.keyword === "integrity" || error.keyword === "filename",
|
||||
)
|
||||
.map(printVerboseError(filePath)),
|
||||
...(errors.some(error => error.keyword !== "integrity" && error.keyword !== "filename")
|
||||
? [errorMessageBlock([filePath], "has schema errors")]
|
||||
: []),
|
||||
],
|
||||
)
|
||||
.join("\n\n")
|
||||
}
|
||||
|
||||
const printVerboseError =
|
||||
(filePath: string) =>
|
||||
(error: TypeValidationError): string => {
|
||||
const pathSegments = [filePath, ...error.instancePath.split("/").slice(1)]
|
||||
return errorMessageBlock(pathSegments, error.message ?? "")
|
||||
}
|
||||
|
||||
const errorMessageBlock = (path: string[], message: string): string =>
|
||||
[
|
||||
...path.map((segment, i) => `${" ".repeat(i * 2)}in "${segment}":`),
|
||||
`${" ".repeat(path.length * 2)}${message}`,
|
||||
].join("\n")
|
||||
@@ -1 +0,0 @@
|
||||
export const collator = Intl.Collator(undefined, { numeric: true })
|
||||
@@ -1,60 +0,0 @@
|
||||
import { readFile, readdir } from "node:fs/promises"
|
||||
import { basename, dirname, extname, join } from "node:path"
|
||||
import YAML from "yaml"
|
||||
|
||||
/**
|
||||
* Recursively reads all files in a directory and its subdirectories.
|
||||
* @param dirPath The path to the directory to read.
|
||||
*/
|
||||
export async function* readDirectoryRec(
|
||||
dirPath: string,
|
||||
filterName: (entryName: string) => boolean = () => true
|
||||
): AsyncGenerator<string> {
|
||||
const dirEntries = await readdir(dirPath, { withFileTypes: true })
|
||||
|
||||
for (const dirEntry of dirEntries) {
|
||||
if (filterName(dirEntry.name)) {
|
||||
const dirEntryPath = join(dirPath, dirEntry.name)
|
||||
if (dirEntry.isDirectory()) {
|
||||
yield* readDirectoryRec(dirEntryPath)
|
||||
} else {
|
||||
yield dirEntryPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a JSON file and parses its content.
|
||||
* @param filePath The path to the JSON file.
|
||||
* @returns The parsed JSON content.
|
||||
*/
|
||||
export const readJsonFile = async (filePath: string): Promise<unknown> => {
|
||||
const fileContent = await readFile(filePath, "utf-8")
|
||||
return JSON.parse(fileContent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a YAML file and parses its content.
|
||||
* @param filePath The path to the YAML file.
|
||||
* @returns The parsed YAML content.
|
||||
*/
|
||||
export const readYamlFile = async (filePath: string): Promise<unknown> => {
|
||||
const fileContent = await readFile(filePath, "utf-8")
|
||||
return YAML.parse(fileContent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the extension of a file path.
|
||||
* @param path The path to the file.
|
||||
* @param ext The new extension to use.
|
||||
* @returns
|
||||
*/
|
||||
export const changeFileExtension = (path: string, ext: string) =>
|
||||
join(dirname(path), basename(path, extname(path)) + ext)
|
||||
|
||||
/**
|
||||
* Checks if a file name represents a hidden file.
|
||||
* @param name The file name to check.
|
||||
*/
|
||||
export const isHiddenFileName = (name: string) => !name.startsWith(".")
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* A path segment descriptor is either a key or an index.
|
||||
*/
|
||||
export type PathSegmentDescriptor = { k: string } | { i: number }
|
||||
|
||||
/**
|
||||
* A path descriptor is a list of path segment descriptors.
|
||||
*/
|
||||
export type PathDescriptor = PathSegmentDescriptor[]
|
||||
|
||||
export const printPathDescriptor = (pathDescriptor: PathDescriptor): string =>
|
||||
pathDescriptor
|
||||
.map(segment => "k" in segment ? segment.k : segment.i)
|
||||
.join("/")
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* Map the second element of a pair using a function that receives both
|
||||
* elements.
|
||||
*/
|
||||
export const mapSecond = <A, B, C>(f: (second: B, first: A) => C) =>
|
||||
(pair: [A, B]): [A, C] => [pair[0], f(pair[1], pair[0])]
|
||||
|
||||
/**
|
||||
* Map the second element of a pair using a function that receives both
|
||||
* elements and returns a `Promise`.
|
||||
*/
|
||||
export const mapSecondAsync = <A, B, C>(f: (second: B, first: A) => Promise<C>) =>
|
||||
async (pair: [A, B]): Promise<[A, C]> => [pair[0], await f(pair[1], pair[0])]
|
||||
|
||||
/**
|
||||
* Map a value using a function into the second value of a pair with the
|
||||
* original value.
|
||||
*/
|
||||
export const mapToSecond = <A, B>(f: (value: A) => B) =>
|
||||
(value: A): [A, B] => [value, f(value)]
|
||||
|
||||
/**
|
||||
* Map a value using a function that returns a `Promise` into the second value
|
||||
* of a pair with the original value.
|
||||
*/
|
||||
export const mapToSecondAsync = <A, B>(f: (value: A) => Promise<B>) =>
|
||||
async (value: A): Promise<[A, B]> => [value, await f(value)]
|
||||
@@ -1,41 +0,0 @@
|
||||
import { PluralizationCategories } from "../types/_I18n.js"
|
||||
import { UI } from "../types/UI.js"
|
||||
|
||||
const pluralType: {
|
||||
[K in keyof UI as UI[K] extends PluralizationCategories ? K : never]: Intl.PluralRuleType
|
||||
} = {
|
||||
"{0} Adventure Points": "cardinal",
|
||||
"You are missing {0} Adventure Points to do this.": "cardinal",
|
||||
"since the {0}. printing": "ordinal",
|
||||
"removed in {0}. printing": "ordinal",
|
||||
"{0} actions": "cardinal",
|
||||
"{0} hours": "cardinal",
|
||||
"{0} minutes": "cardinal",
|
||||
"{0} rounds": "cardinal",
|
||||
"{0} seduction actions": "cardinal",
|
||||
", {0} of which are permanent": "cardinal",
|
||||
"{0} centuries": "cardinal",
|
||||
"{0} combat rounds": "cardinal",
|
||||
"{0} days": "cardinal",
|
||||
"{0} months": "cardinal",
|
||||
"{0} mos.": "cardinal",
|
||||
"{0} seconds": "cardinal",
|
||||
"{0} weeks": "cardinal",
|
||||
"{0} wks.": "cardinal",
|
||||
"{0} years": "cardinal",
|
||||
"{0} yrs.": "cardinal",
|
||||
"{0} miles": "cardinal",
|
||||
"{0} yards": "cardinal",
|
||||
}
|
||||
|
||||
/**
|
||||
* The keys of the UI strings that support pluralization categories.
|
||||
*/
|
||||
export type KeySupportingPluralizationCategories = keyof typeof pluralType
|
||||
|
||||
/**
|
||||
* Returns the plural rule type for the given key.
|
||||
*/
|
||||
export const getPluralRuleTypeForKey = (
|
||||
key: KeySupportingPluralizationCategories
|
||||
): Intl.PluralRuleType => pluralType[key]
|
||||
@@ -0,0 +1,34 @@
|
||||
import type * as Database from "../gen/types.js"
|
||||
|
||||
export type GetInstanceById<T extends keyof Database.EntityMap> = {
|
||||
<U extends T>(enumCase: IdentifierEnum<U>): Database.EntityMap[U] | undefined
|
||||
<U extends T>(entity: U, id: string): Database.EntityMap[U] | undefined
|
||||
}
|
||||
|
||||
export type GetAllInstances<T extends keyof Database.EntityMap> = <U extends T>(
|
||||
entity: U,
|
||||
) => [id: string, content: Database.EntityMap[U]][]
|
||||
|
||||
export type GetAllChildInstancesForParent<T extends keyof Database.ChildEntityMap> = <U extends T>(
|
||||
entity: U,
|
||||
parentId: Database.ChildEntityMap[U][2],
|
||||
) => [id: string, content: Database.ChildEntityMap[U][0]][]
|
||||
|
||||
export type Case<K extends string, T = undefined> = T extends NonNullable<unknown> | null
|
||||
? { kind: K } & { [Key in K]: Extract<T, NonNullable<unknown> | null> }
|
||||
: { kind: K }
|
||||
|
||||
/**
|
||||
* Creates an enum case object.
|
||||
*/
|
||||
export const Case = (<K extends string, T>(kind: K, value: T): Case<K, T> =>
|
||||
(value === undefined ? { kind } : { kind, [kind]: value }) as Case<K, T>) as {
|
||||
<K extends string>(kind: K): Case<K>
|
||||
<K extends string, T extends NonNullable<unknown> | null>(kind: K, value: T): Case<K, T>
|
||||
}
|
||||
|
||||
export type IdentifierEnum<K extends keyof Database.EntityMap> = { [K1 in K]: Case<K1, string> }[K]
|
||||
|
||||
export const stringFromEnumIdentifier = (
|
||||
enumCase: IdentifierEnum<keyof Database.EntityMap>,
|
||||
): string => enumCase[enumCase.kind as keyof typeof enumCase]
|
||||
+1
-3
@@ -12,8 +12,6 @@
|
||||
"target": "ESNext",
|
||||
},
|
||||
"include": [
|
||||
"src/types/**/*",
|
||||
"src/main.ts",
|
||||
"src/test.ts",
|
||||
"src",
|
||||
]
|
||||
}
|
||||
|
||||
+4
-1
@@ -9,7 +9,10 @@ const config: GenerationConfig = {
|
||||
TypeScriptOutput({
|
||||
targetPath: join(import.meta.dirname, "gen", "types.d.ts"),
|
||||
rendererOptions: {
|
||||
generateEntityMapType: true,
|
||||
generateHelpers: {
|
||||
entityMap: true,
|
||||
childEntityMap: true,
|
||||
},
|
||||
inferTranslationParameters: {
|
||||
format: "mf2",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user