first changes and cleanup

This commit is contained in:
Lukas Obermann
2025-12-08 15:45:05 +01:00
parent 5cd59ca6ce
commit 6fc36c131e
17 changed files with 224 additions and 517 deletions
+29 -11
View File
@@ -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
View File
@@ -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
View File
@@ -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
+20 -26
View File
@@ -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),
})
+7 -4
View File
@@ -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
View File
@@ -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),
},
},
})
-13
View File
@@ -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"
-30
View File
@@ -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)
},
})
-63
View File
@@ -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
View File
@@ -1 +0,0 @@
export const collator = Intl.Collator(undefined, { numeric: true })
-60
View File
@@ -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(".")
-14
View File
@@ -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("/")
-27
View File
@@ -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)]
-41
View File
@@ -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]
+34
View File
@@ -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
View File
@@ -12,8 +12,6 @@
"target": "ESNext",
},
"include": [
"src/types/**/*",
"src/main.ts",
"src/test.ts",
"src",
]
}
+4 -1
View File
@@ -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",
},