feat!: adjust cache builder for current database

Also adjusts publication ref values and blessed tradition principle
ref values, which might break current database data.
This commit is contained in:
Lukas Obermann
2026-02-09 14:02:34 +01:00
parent 5b50b860e2
commit 9407cb5767
13 changed files with 1206 additions and 1366 deletions
+1
View File
@@ -1,6 +1,7 @@
gen
node_modules
lib
*.tsbuildinfo
# Swift
.DS_Store
+11 -11
View File
@@ -10,7 +10,7 @@
"license": "MPL-2.0",
"dependencies": {
"@elyukai/utils": "^0.2.0",
"tsondb": "^0.19.3",
"tsondb": "^0.19.8",
"yaml": "^2.8.2"
},
"devDependencies": {
@@ -50,9 +50,9 @@
}
},
"node_modules/@elyukai/utils": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@elyukai/utils/-/utils-0.2.0.tgz",
"integrity": "sha512-UWt39Gb9YU8G9fy5F9oUp4D3XfPCBhLXnHbRD8oczU3itQ3iKqSRKQ7k4+8cs7b37HefgVhFkMScMfvXvbXjPQ==",
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@elyukai/utils/-/utils-0.2.1.tgz",
"integrity": "sha512-az6I2MHjZ+QEJNcK7xTe/z7dNucBwyT9mYe3BNGl4iUHQiTc2DuMEbsAYrITLtVNTJT6yO6p48m9bzOiVQJO9w==",
"license": "MPL-2.0"
},
"node_modules/@eslint-community/eslint-utils": {
@@ -290,9 +290,9 @@
"license": "MIT"
},
"node_modules/@preact/signals": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/@preact/signals/-/signals-2.7.0.tgz",
"integrity": "sha512-9rQ030JhUfdBO4xlFB6Iwc/rOBi9IFXaCgu8VrFMSjAJfubGRTTxBPsgSlAF5qmWdY3C91b9kBK09mpUOMh1Rw==",
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@preact/signals/-/signals-2.7.1.tgz",
"integrity": "sha512-mP2+wMYHqDXVKFGzjqkL6CiHj3okB8eVTTJUZBrSVGozi/XfA+zZRCEALKKZYRoSoqLyT4J6qM4lhwT9155s1Q==",
"license": "MIT",
"dependencies": {
"@preact/signals-core": "^1.13.0"
@@ -4165,12 +4165,12 @@
}
},
"node_modules/tsondb": {
"version": "0.19.3",
"resolved": "https://registry.npmjs.org/tsondb/-/tsondb-0.19.3.tgz",
"integrity": "sha512-kfdyzhoIatOUzJfYZCKlIRALPPVl/hGpOpCAmdao+s+dCCdJF1Vw39oE0A555Rm8w8xAxqByvL1qv2Cirelj5g==",
"version": "0.19.8",
"resolved": "https://registry.npmjs.org/tsondb/-/tsondb-0.19.8.tgz",
"integrity": "sha512-1RG6kV+uZvoj9viXTgZqXBnwV3wcizvdqsswai4Ar3aOMczspu73UZAE3p9Xjk/XmQsO5JMoICU8AP+MiRTNqA==",
"license": "ISC",
"dependencies": {
"@elyukai/utils": "^0.2.0",
"@elyukai/utils": "^0.2.1",
"@preact/signals": "^2.7.0",
"debug": "^4.4.3",
"express": "^5.2.1",
+4 -4
View File
@@ -18,10 +18,10 @@
"main": "./lib/main.js",
"exports": {
".": "./lib/main.js",
"./gen": "./gen/types.d.ts",
"./cache/*": "./lib/cache/*.js",
"./cache": "./lib/cache/index.js",
"./types": "./lib/types/index.js",
"./types/*": "./lib/types/*.js"
"./types/*": "./lib/types/*.js",
"./gen": "./gen/types.d.ts"
},
"scripts": {
"build": "tsc",
@@ -38,7 +38,7 @@
"license": "MPL-2.0",
"dependencies": {
"@elyukai/utils": "^0.2.0",
"tsondb": "^0.19.3",
"tsondb": "^0.19.8",
"yaml": "^2.8.2"
},
"devDependencies": {
+856 -980
View File
File diff suppressed because it is too large Load Diff
+23 -28
View File
@@ -1,30 +1,25 @@
import type { CacheConfig } from "../cacheConfig.js"
import type { Advantage_ID } from "../../gen/types.js"
import type { CacheBuilder } from "./internal.ts"
export type AncestorBloodAdvantagesCache = {
ids: number[]
}
export type AncestorBloodAdvantagesCache = Advantage_ID[]
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 buildAncestorBloodAdvantagesCache: CacheBuilder<
AncestorBloodAdvantagesCache
> = database =>
database
.getAllInstanceContainersOfEntity("Advantage")
.filter(({ content }) =>
content.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)
-5
View File
@@ -1,5 +0,0 @@
import { ValidResults } from "../main.js"
export type CacheConfig<T, D extends unknown[] = []> = {
builder: (data: ValidResults, ...deps: D) => T
}
+69
View File
@@ -0,0 +1,69 @@
import type { TSONDB } from "tsondb"
import type { TSONDBTypes } from "../main.ts"
import {
activatableSelectOptionsCacheBuilder,
type ActivatableSelectOptionsCache,
} from "./activatableSelectOptions.ts"
import {
buildAncestorBloodAdvantagesCache,
type AncestorBloodAdvantagesCache,
} from "./ancestorBloodAdvantages.ts"
import {
buildMagicalAndBlessedAdvantagesAndDisadvantagesCache,
type MagicalAndBlessedAdvantagesAndDisadvantagesCache,
} from "./magicalAndBlessedAdvantagesAndDisadvantages.ts"
import {
buildNewApplicationsAndUsesCache,
type NewApplicationsAndUsesCache,
} from "./newApplicationsAndUses.ts"
/**
* Identifiers of specific entries in the database that are used for creating the cache.
*/
export type IdMap = {
Advantage: Record<"Blessed" | "Spellcaster", string>
MagicalSpecialAbility: Record<"PropertyKnowledge", string>
KarmaSpecialAbility: Record<"AspectKnowledge", string>
}
/**
* The cache object containing all precomputed data.
*/
export type Cache = {
activatableSelectOptions: ActivatableSelectOptionsCache
sncestorBloodAdvantages: AncestorBloodAdvantagesCache
magicalAndBlessedAdvantagesAndDisadvantages: MagicalAndBlessedAdvantagesAndDisadvantagesCache
newApplicationsAndUses: NewApplicationsAndUsesCache
}
export type {
ActivatableSelectOptionsCache,
AncestorBloodAdvantagesCache,
MagicalAndBlessedAdvantagesAndDisadvantagesCache,
NewApplicationsAndUsesCache,
}
/**
* Creates the cache by precomputing all necessary data from the database.
* @param db The TSONDB instance containing the database.
* @param idMap An object containing identifiers of specific entries in the database that are used for creating the cache.
* @returns The created cache object.
*/
export const createCache = (db: TSONDB<TSONDBTypes>, idMap: IdMap): Cache => {
const activatableSelectOptions = activatableSelectOptionsCacheBuilder(db, idMap)
const sncestorBloodAdvantages = buildAncestorBloodAdvantagesCache(db, idMap)
const magicalAndBlessedAdvantagesAndDisadvantages =
buildMagicalAndBlessedAdvantagesAndDisadvantagesCache(db, idMap)
const newApplicationsAndUses = buildNewApplicationsAndUsesCache(
db,
idMap,
activatableSelectOptions,
)
return {
activatableSelectOptions,
sncestorBloodAdvantages,
magicalAndBlessedAdvantagesAndDisadvantages,
newApplicationsAndUses,
}
}
+9
View File
@@ -0,0 +1,9 @@
import type { TSONDB } from "tsondb"
import type { TSONDBTypes } from "../main.ts"
import type { IdMap } from "./index.ts"
export type CacheBuilder<T = unknown, Args extends unknown[] = []> = (
db: TSONDB<TSONDBTypes>,
idMap: IdMap,
...args: Args
) => T
+96 -127
View File
@@ -1,41 +1,35 @@
import { assertExhaustive } from "@elyukai/utils/typeSafety"
import type { CacheConfig } from "../cacheConfig.js"
import type { TSONDB } from "tsondb"
import type {
ActivatableIdentifier,
Advantage_ID,
AdvantageDisadvantagePrerequisiteGroup,
AdvantageDisadvantagePrerequisites,
Disadvantage_ID,
RatedIdentifier,
} from "../../gen/types.js"
import type { TSONDBTypes } from "../main.js"
import type { IdMap } from "./index.js"
import type { CacheBuilder } from "./internal.ts"
const BLESSED_ID = 12
const SPELLCASTER_ID = 47
export type MagicalAndBlessedAdvantagesAndDisadvantagesCache = Record<
"Advantage" | "Disadvantage",
Record<"Magical" | "Blessed", string[]>
>
export type MagicalAndBlessedAdvantagesAndDisadvantagesCache = {
advantages: {
magical: {
ids: number[]
}
blessed: {
ids: number[]
}
}
disadvantages: {
magical: {
ids: number[]
}
blessed: {
ids: number[]
}
}
}
const getAdvantageId = (type: "Magical" | "Blessed") => {
const getAdvantageId = (idMap: IdMap, type: "Magical" | "Blessed") => {
switch (type) {
case "Magical":
return SPELLCASTER_ID
return idMap.Advantage.Spellcaster
case "Blessed":
return BLESSED_ID
return idMap.Advantage.Blessed
default:
return assertExhaustive(type)
}
}
const isRatedFor = (type: "Magical" | "Blessed", ratedId: RatedIdentifier) => {
switch (ratedId.tag) {
switch (ratedId.kind) {
case "Spell":
case "Ritual":
return type === "Magical"
@@ -53,28 +47,29 @@ const isRatedFor = (type: "Magical" | "Blessed", ratedId: RatedIdentifier) => {
}
const isPrerequisiteFor = (
idMap: IdMap,
type: "Magical" | "Blessed",
prerequisite: AdvantageDisadvantagePrerequisiteGroup,
getById: (
id: ActivatableIdentifier,
) => { id: number; prerequisites?: AdvantageDisadvantagePrerequisites } | undefined,
traversedIds: number[],
) => { id: string; content: { prerequisites?: AdvantageDisadvantagePrerequisites } } | undefined,
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(idMap, type) &&
prerequisite.Activatable.active
) {
return true
}
const entry = getById(prerequisite.activatable.id)
return entry !== undefined && is(type, entry, getById, traversedIds)
const entry = getById(prerequisite.Activatable.id)
return entry !== undefined && is(idMap, type, entry, getById, traversedIds)
}
case "Rated":
return isRatedFor(type, prerequisite.rated.id)
return isRatedFor(type, prerequisite.Rated.id)
case "CommonSuggestedByRCP":
case "Sex":
case "Race":
@@ -88,7 +83,7 @@ const isPrerequisiteFor = (
case "MagicalTradition":
case "RatedMinimumNumber":
case "RatedSum":
case "ExternalEnhancement":
case "Enhancement":
case "Text":
case "NoOtherAncestorBloodAdvantage":
case "SexualCharacteristic":
@@ -99,32 +94,39 @@ const isPrerequisiteFor = (
}
const is = (
idMap: IdMap,
type: "Magical" | "Blessed",
entry: { id: number; prerequisites?: AdvantageDisadvantagePrerequisites },
entry: { id: string; content: { prerequisites?: AdvantageDisadvantagePrerequisites } },
getById: (
id: ActivatableIdentifier,
) => { id: number; prerequisites?: AdvantageDisadvantagePrerequisites } | undefined,
traversedIds: number[],
) => { id: string; content: { prerequisites?: AdvantageDisadvantagePrerequisites } } | undefined,
traversedIds: string[],
): boolean => {
if (!entry.prerequisites || traversedIds.includes(entry.id)) {
if (!entry.content.prerequisites || traversedIds.includes(entry.id)) {
return false
}
const newTraversedIds = [...traversedIds, entry.id]
return (
entry.prerequisites !== undefined &&
entry.prerequisites.some(prerequisite => {
switch (prerequisite.prerequisite.tag) {
entry.content.prerequisites !== undefined &&
entry.content.prerequisites.some(prerequisite => {
switch (prerequisite.prerequisite.kind) {
case "Single":
return isPrerequisiteFor(type, prerequisite.prerequisite.single, getById, newTraversedIds)
return isPrerequisiteFor(
idMap,
type,
prerequisite.prerequisite.Single,
getById,
newTraversedIds,
)
case "Disjunction":
return prerequisite.prerequisite.disjunction.list.some(p =>
isPrerequisiteFor(type, p, getById, newTraversedIds),
return prerequisite.prerequisite.Disjunction.list.some(p =>
isPrerequisiteFor(idMap, type, p, getById, newTraversedIds),
)
case "Group":
return prerequisite.prerequisite.group.list.some(p =>
isPrerequisiteFor(type, p, getById, newTraversedIds),
return prerequisite.prerequisite.Group.list.some(p =>
isPrerequisiteFor(idMap, type, p, getById, newTraversedIds),
)
default:
return assertExhaustive(prerequisite.prerequisite)
@@ -133,84 +135,51 @@ 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)
}
}
const entityKeyMap = {
Advantage: null,
Disadvantage: null,
} as const
// 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),
},
},
}
},
type EntityKeyMap = typeof entityKeyMap
const typeKeyMap = {
Magical: null,
Blessed: null,
} as const
type TypeKeyMap = typeof typeKeyMap
const collectIds = (
entity: "Advantage" | "Disadvantage",
type: "Magical" | "Blessed",
database: TSONDB<TSONDBTypes>,
idMap: IdMap,
) =>
database
.getAllInstanceContainersOfEntity(entity)
.filter(entry =>
is(idMap, type, entry, database.getInstanceContainerOfEntityById.bind(database), []),
)
.map(({ id }) => id)
export const buildMagicalAndBlessedAdvantagesAndDisadvantagesCache: CacheBuilder<
MagicalAndBlessedAdvantagesAndDisadvantagesCache
> = (database, idMap) => {
return Object.fromEntries(
(Object.keys(entityKeyMap) as (keyof EntityKeyMap)[]).map(
(
entity,
): [keyof EntityKeyMap, Record<keyof TypeKeyMap, (Advantage_ID | Disadvantage_ID)[]>] => [
entity,
Object.fromEntries(
(Object.keys(typeKeyMap) as (keyof TypeKeyMap)[]).map(
(type): [keyof TypeKeyMap, (Advantage_ID | Disadvantage_ID)[]] => [
type,
collectIds(entity, type, database, idMap),
],
),
) as Record<keyof TypeKeyMap, (Advantage_ID | Disadvantage_ID)[]>,
],
),
) as Record<keyof EntityKeyMap, Record<keyof TypeKeyMap, (Advantage_ID | Disadvantage_ID)[]>>
}
+72 -159
View File
@@ -1,171 +1,84 @@
import type { CacheConfig } from "../cacheConfig.js"
import type { SkillApplication, SkillUse } from "../types/_Activatable.js"
import type { ActivatableIdentifier } from "../types/_IdentifierGroup.js"
import { ActivatableSelectOptionsCache } from "./activatableSelectOptions.js"
import type {
ActivatableIdentifier,
NewSkillApplication,
NewSkillApplicationOrUseParentIdentifier,
Skill_ID,
SkillUse,
} from "../../gen/types.js"
import type { ActivatableSelectOptionsCache } from "./activatableSelectOptions.ts"
import type { CacheBuilder } from "./internal.ts"
export type NewApplication = {
source: ActivatableIdentifier
data: SkillApplication
requiredSkillRating?: number
export type ResolvedNewSkillApplication = {
id: string
content: Omit<NewSkillApplication, "parent"> & {
parent: NewSkillApplicationOrUseParentIdentifier | ActivatableIdentifier
}
}
export type Use = {
source: ActivatableIdentifier
data: SkillUse
export type ResolvedSkillUse = {
id: string
content: Omit<SkillUse, "parent"> & {
parent: NewSkillApplicationOrUseParentIdentifier | ActivatableIdentifier
}
}
const groupByAffectedSkill = <
T extends {
content: {
skills: Skill_ID[]
}
},
>(
entries: T[],
prev: Record<Skill_ID, T[]> = {},
): Record<Skill_ID, T[]> =>
entries.reduce<Record<Skill_ID, T[]>>(
(acc, entry) =>
entry.content.skills.reduce(
(acc1, skillId) => ({ ...acc1, [skillId]: [...(acc1[skillId] ?? []), entry] }),
acc,
),
prev,
)
const addGeneratedEntries = (
generatedSelectOptions: ActivatableSelectOptionsCache,
prev: NewApplicationsAndUsesCache,
): NewApplicationsAndUsesCache =>
Object.values(generatedSelectOptions).reduce(
(acc, byInstance) =>
Object.values(byInstance).reduce(
(acc1, resolvedSelectOptions) =>
resolvedSelectOptions.reduce(
(acc2, selectOption) => ({
newApplications: groupByAffectedSkill(
selectOption.newApplications,
acc2.newApplications,
),
uses: groupByAffectedSkill(selectOption.uses, acc2.uses),
}),
acc1,
),
acc,
),
prev,
)
export type NewApplicationsAndUsesCache = {
newApplications: Record<number, NewApplication[]>
uses: Record<number, Use[]>
newApplications: Record<Skill_ID, ResolvedNewSkillApplication[]>
uses: Record<Skill_ID, ResolvedSkillUse[]>
}
export const config: CacheConfig<
export const buildNewApplicationsAndUsesCache: CacheBuilder<
NewApplicationsAndUsesCache,
[generatedSelectOptions: ActivatableSelectOptionsCache]
> = {
builder(database, generatedSelectOptions) {
type WithApplicationsAndUses = {
skill_applications?: SkillApplication[]
skill_uses?: SkillUse[]
}
> = (database, _idMap, generatedSelectOptions) => {
const cache: NewApplicationsAndUsesCache = {
newApplications: groupByAffectedSkill(
database.getAllInstanceContainersOfEntity("NewSkillApplication"),
),
uses: groupByAffectedSkill(database.getAllInstanceContainersOfEntity("SkillUse")),
}
const cache: NewApplicationsAndUsesCache = {
newApplications: {},
uses: {},
}
const addNewApplicationsAndUses = (
activatable: WithApplicationsAndUses,
activatableId: ActivatableIdentifier
) => {
if (activatable.skill_applications) {
for (const newApplication of activatable.skill_applications) {
const pair: NewApplication = { source: activatableId, data: newApplication }
if (newApplication.skill.tag === "Single") {
const previous = (cache.newApplications[newApplication.skill.single.id.skill] ??= [])
previous.push(pair)
} else {
for (const skill of newApplication.skill.multiple.list) {
const previous = (cache.newApplications[skill.id.skill] ??= [])
previous.push(pair)
}
if (newApplication.skill.multiple.required_skill_rating !== undefined) {
pair.requiredSkillRating = newApplication.skill.multiple.required_skill_rating
}
}
}
}
if (activatable.skill_uses) {
for (const use of activatable.skill_uses) {
const pair: Use = { source: activatableId, data: use }
if (use.skill.tag === "Single") {
const previous = (cache.uses[use.skill.single.id.skill] ??= [])
previous.push(pair)
} else {
for (const skill of use.skill.multiple.list) {
const previous = (cache.uses[skill.id.skill] ??= [])
previous.push(pair)
}
}
}
}
}
// prettier-ignore
const entries: [
[id: number, data: WithApplicationsAndUses][],
(numericId: number) => ActivatableIdentifier,
][] = [
[database.advancedCombatSpecialAbilities, n => ({ tag: "AdvancedCombatSpecialAbility", advanced_combat_special_ability: n })],
[database.advancedKarmaSpecialAbilities, n => ({ tag: "AdvancedKarmaSpecialAbility", advanced_karma_special_ability: n })],
[database.advancedMagicalSpecialAbilities, n => ({ tag: "AdvancedMagicalSpecialAbility", advanced_magical_special_ability: n })],
[database.advancedSkillSpecialAbilities, n => ({ tag: "AdvancedSkillSpecialAbility", advanced_skill_special_ability: n })],
[database.advantages, n => ({ tag: "Advantage", advantage: n })],
[database.blessedTraditions, n => ({ tag: "BlessedTradition", blessed_tradition: n })],
[database.ceremonialItemSpecialAbilities, n => ({ tag: "CeremonialItemSpecialAbility", ceremonial_item_special_ability: n })],
[database.combatSpecialAbilities, n => ({ tag: "CombatSpecialAbility", combat_special_ability: n })],
[database.combatStyleSpecialAbilities, n => ({ tag: "CombatStyleSpecialAbility", combat_style_special_ability: n })],
[database.fatePointSpecialAbilities, n => ({ tag: "FatePointSpecialAbility", fate_point_special_ability: n })],
[database.generalSpecialAbilities, n => ({ tag: "GeneralSpecialAbility", general_special_ability: n })],
[database.liturgicalStyleSpecialAbilities, n => ({ tag: "LiturgicalStyleSpecialAbility", liturgical_style_special_ability: n })],
[database.magicStyleSpecialAbilities, n => ({ tag: "MagicStyleSpecialAbility", magic_style_special_ability: n })],
[database.magicalSpecialAbilities, n => ({ tag: "MagicalSpecialAbility", magical_special_ability: n })],
[database.magicalTraditions, n => ({ tag: "MagicalTradition", magical_tradition: n })],
[database.sexSpecialAbilities, n => ({ tag: "SexSpecialAbility", sex_special_ability: n })],
]
entries.forEach(([entityEntries, createId]) => {
entityEntries.forEach(([numericId, activatable]) => {
addNewApplicationsAndUses(activatable, createId(numericId))
})
})
// prettier-ignore
const generatedEntries: [
{ [id: number]: WithApplicationsAndUses[] },
(numericId: number) => ActivatableIdentifier,
][] = [
[generatedSelectOptions.advancedCombatSpecialAbilities, id => ({ tag: "AdvancedCombatSpecialAbility", advanced_combat_special_ability: id })],
[generatedSelectOptions.advancedKarmaSpecialAbilities, id => ({ tag: "AdvancedKarmaSpecialAbility", advanced_karma_special_ability: id })],
[generatedSelectOptions.advancedMagicalSpecialAbilities, id => ({ tag: "AdvancedMagicalSpecialAbility", advanced_magical_special_ability: id })],
[generatedSelectOptions.advancedSkillSpecialAbilities, id => ({ tag: "AdvancedSkillSpecialAbility", advanced_skill_special_ability: id })],
[generatedSelectOptions.advantages, id => ({ tag: "Advantage", advantage: id })],
[generatedSelectOptions.ancestorGlyphs, id => ({ tag: "AncestorGlyph", ancestor_glyph: id })],
[generatedSelectOptions.arcaneOrbEnchantments, id => ({ tag: "ArcaneOrbEnchantment", arcane_orb_enchantment: id })],
[generatedSelectOptions.attireEnchantments, id => ({ tag: "AttireEnchantment", attire_enchantment: id })],
[generatedSelectOptions.blessedTraditions, id => ({ tag: "BlessedTradition", blessed_tradition: id })],
[generatedSelectOptions.bowlEnchantments, id => ({ tag: "BowlEnchantment", bowl_enchantment: id })],
[generatedSelectOptions.brawlingSpecialAbilities, id => ({ tag: "BrawlingSpecialAbility", brawling_special_ability: id })],
[generatedSelectOptions.cauldronEnchantments, id => ({ tag: "CauldronEnchantment", cauldron_enchantment: id })],
[generatedSelectOptions.ceremonialItemSpecialAbilities, id => ({ tag: "CeremonialItemSpecialAbility", ceremonial_item_special_ability: id })],
[generatedSelectOptions.chronicleEnchantments, id => ({ tag: "ChronicleEnchantment", chronicle_enchantment: id })],
[generatedSelectOptions.combatSpecialAbilities, id => ({ tag: "CombatSpecialAbility", combat_special_ability: id })],
[generatedSelectOptions.combatStyleSpecialAbilities, id => ({ tag: "CombatStyleSpecialAbility", combat_style_special_ability: id })],
[generatedSelectOptions.commandSpecialAbilities, id => ({ tag: "CommandSpecialAbility", command_special_ability: id })],
[generatedSelectOptions.daggerRituals, id => ({ tag: "DaggerRitual", dagger_ritual: id })],
[generatedSelectOptions.disadvantages, id => ({ tag: "Disadvantage", disadvantage: id })],
[generatedSelectOptions.familiarSpecialAbilities, id => ({ tag: "FamiliarSpecialAbility", familiar_special_ability: id })],
[generatedSelectOptions.fatePointSexSpecialAbilities, id => ({ tag: "FatePointSexSpecialAbility", fate_point_sex_special_ability: id })],
[generatedSelectOptions.fatePointSpecialAbilities, id => ({ tag: "FatePointSpecialAbility", fate_point_special_ability: id })],
[generatedSelectOptions.foolsHatEnchantments, id => ({ tag: "FoolsHatEnchantment", fools_hat_enchantment: id })],
[generatedSelectOptions.generalSpecialAbilities, id => ({ tag: "GeneralSpecialAbility", general_special_ability: id })],
[generatedSelectOptions.instrumentEnchantments, id => ({ tag: "InstrumentEnchantment", instrument_enchantment: id })],
[generatedSelectOptions.karmaSpecialAbilities, id => ({ tag: "KarmaSpecialAbility", karma_special_ability: id })],
[generatedSelectOptions.krallenkettenzauber, id => ({ tag: "Krallenkettenzauber", krallenkettenzauber: id })],
[generatedSelectOptions.liturgicalStyleSpecialAbilities, id => ({ tag: "LiturgicalStyleSpecialAbility", liturgical_style_special_ability: id })],
[generatedSelectOptions.lycantropicGifts, id => ({ tag: "LycantropicGift", lycantropic_gift: id })],
[generatedSelectOptions.magicalSpecialAbilities, id => ({ tag: "MagicalSpecialAbility", magical_special_ability: id })],
[generatedSelectOptions.magicalTraditions, id => ({ tag: "MagicalTradition", magical_tradition: id })],
[generatedSelectOptions.magicStyleSpecialAbilities, id => ({ tag: "MagicStyleSpecialAbility", magic_style_special_ability: id })],
[generatedSelectOptions.orbEnchantments, id => ({ tag: "OrbEnchantment", orb_enchantment: id })],
[generatedSelectOptions.pactGifts, id => ({ tag: "PactGift", pact_gift: id })],
[generatedSelectOptions.protectiveWardingCircleSpecialAbilities, id => ({ tag: "ProtectiveWardingCircleSpecialAbility", protective_warding_circle_special_ability: id })],
[generatedSelectOptions.ringEnchantments, id => ({ tag: "RingEnchantment", ring_enchantment: id })],
[generatedSelectOptions.sermons, id => ({ tag: "Sermon", sermon: id })],
[generatedSelectOptions.sexSpecialAbilities, id => ({ tag: "SexSpecialAbility", sex_special_ability: id })],
[generatedSelectOptions.sickleRituals, id => ({ tag: "SickleRitual", sickle_ritual: id })],
[generatedSelectOptions.sikaryanDrainSpecialAbilities, id => ({ tag: "SikaryanDrainSpecialAbility", sikaryan_drain_special_ability: id })],
[generatedSelectOptions.staffEnchantments, id => ({ tag: "StaffEnchantment", staff_enchantment: id })],
[generatedSelectOptions.skillStyleSpecialAbilities, id => ({ tag: "SkillStyleSpecialAbility", skill_style_special_ability: id })],
[generatedSelectOptions.spellSwordEnchantments, id => ({ tag: "SpellSwordEnchantment", spell_sword_enchantment: id })],
[generatedSelectOptions.toyEnchantments, id => ({ tag: "ToyEnchantment", toy_enchantment: id })],
[generatedSelectOptions.trinkhornzauber, id => ({ tag: "Trinkhornzauber", trinkhornzauber: id })],
[generatedSelectOptions.vampiricGifts, id => ({ tag: "VampiricGift", vampiric_gift: id })],
[generatedSelectOptions.visions, id => ({ tag: "Vision", vision: id })],
[generatedSelectOptions.wandEnchantments, id => ({ tag: "WandEnchantment", wand_enchantment: id })],
[generatedSelectOptions.weaponEnchantments, id => ({ tag: "WeaponEnchantment", weapon_enchantment: id })],
]
generatedEntries.forEach(([ids, createId]) =>
Object.entries(ids).forEach(([stringId, selectOptions]) => {
const id = createId(Number.parseInt(stringId))
selectOptions.forEach(selectOption => {
addNewApplicationsAndUses(selectOption, id)
})
})
)
return cache
},
return addGeneratedEntries(generatedSelectOptions, cache)
}
+43 -46
View File
@@ -38,20 +38,17 @@ export const PublicationRef = DB.TypeAlias(import.meta.url, {
name: "Occurrence",
namePlural: "Occurrences",
secondaryEntity: Locale,
type: DB.Object(
{
initial: DB.Optional({
comment: "The initial occurrence of the entry.",
type: DB.IncludeIdentifier(InitialOccurrence),
}),
revisions: DB.Optional({
comment:
"Revisions of the entry, resulting in either changed page references or re-addition or removal of an entry.",
type: DB.Array(DB.IncludeIdentifier(Revision), { minItems: 1 }),
}),
},
{ minProperties: 1 },
),
type: DB.Object({
initial: DB.Required({
comment: "The initial occurrence of the entry.",
type: DB.IncludeIdentifier(InitialOccurrence),
}),
revisions: DB.Optional({
comment:
"Revisions of the entry, resulting in either changed page references or re-addition or removal of an entry.",
type: DB.Array(DB.IncludeIdentifier(Revision), { minItems: 1 }),
}),
}),
minProperties: 1,
}),
}),
@@ -97,39 +94,39 @@ const InitialOccurrence = DB.TypeAlias(import.meta.url, {
// },
// })
const _PrintingOccurrence = DB.TypeAlias(import.meta.url, {
name: "PrintingOccurrence",
comment: "The publications printing where the entry has been added, changed, or removed.",
type: () =>
DB.Object({
printing: DB.Required({
comment: "The printing number.",
type: DB.Integer({ minimum: 1 }),
}),
changes: DB.Required({
comment: "The changes made in this printing.",
type: DB.IncludeIdentifier(PrintingChanges),
}),
}),
})
// const _PrintingOccurrence = DB.TypeAlias(import.meta.url, {
// name: "PrintingOccurrence",
// comment: "The publications printing where the entry has been added, changed, or removed.",
// type: () =>
// DB.Object({
// printing: DB.Required({
// comment: "The printing number.",
// type: DB.Integer({ minimum: 1 }),
// }),
// changes: DB.Required({
// comment: "The changes made in this printing.",
// type: DB.IncludeIdentifier(PrintingChanges),
// }),
// }),
// })
const PrintingChanges = DB.Enum(import.meta.url, {
name: "PrintingChanges",
comment:
"A revision of the entry, resulting in either changed page references or re-addition or removal of an entry.",
values: () => ({
Set: DB.EnumCase({
comment:
"The entry has been added, re-added or changed page(s) in this printing. The page references override any previously set ones in case an entry has been moved to a different page in a later printing.",
type: DB.Array(DB.IncludeIdentifier(PageRange), { minItems: 1 }),
}),
Remove: DB.EnumCase({
comment:
"The entry has been removed in this printing.\n\nThis can also be used if the entry hss been removed by the release of this publication (that is, the first printing), which can be useful for books that introduce major revisions of rules into the game.",
type: null,
}),
}),
})
// const PrintingChanges = DB.Enum(import.meta.url, {
// name: "PrintingChanges",
// comment:
// "A revision of the entry, resulting in either changed page references or re-addition or removal of an entry.",
// values: () => ({
// Set: DB.EnumCase({
// comment:
// "The entry has been added, re-added or changed page(s) in this printing. The page references override any previously set ones in case an entry has been moved to a different page in a later printing.",
// type: DB.Array(DB.IncludeIdentifier(PageRange), { minItems: 1 }),
// }),
// Remove: DB.EnumCase({
// comment:
// "The entry has been removed in this printing.\n\nThis can also be used if the entry hss been removed by the release of this publication (that is, the first printing), which can be useful for books that introduce major revisions of rules into the game.",
// type: null,
// }),
// }),
// })
const Revision = DB.Enum(import.meta.url, {
name: "Revision",
+2 -1
View File
@@ -7,6 +7,7 @@ import {
AspectIdentifier,
AttributeIdentifier,
BlessingIdentifier,
GeneralIdentifier,
SkillIdentifier,
} from "../_Identifier.js"
import { CombatTechniqueIdentifier } from "../_IdentifierGroup.js"
@@ -59,7 +60,7 @@ export const BlessedTradition = DB.Entity(import.meta.url, {
associated_principles_id: DB.Optional({
comment:
"The select options identifier of the disadvantage *Principles* that represent this traditions code, if any.",
type: DB.Integer(),
type: GeneralIdentifier(),
}),
prerequisites: DB.Optional({
type: DB.IncludeIdentifier(GeneralPrerequisites),
+20 -5
View File
@@ -1,19 +1,34 @@
{
"compilerOptions": {
"allowJs": true,
"composite": true,
"declaration": true,
"lib": ["ESNext"],
"erasableSyntaxOnly": true,
"forceConsistentCasingInFileNames": true,
"jsx": "react-jsx",
"jsxImportSource": "preact",
"lib": ["esnext"],
"module": "NodeNext",
"noErrorTruncation": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
"noUncheckedSideEffectImports": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "lib",
"rewriteRelativeImportExtensions": true,
"rootDir": "src",
"skipLibCheck": true,
"strict": true,
"target": "ESNext",
"useUnknownInCatchVariables": true,
"verbatimModuleSyntax": true,
},
"include": [
"src/types/**/*",
"src/main.ts",
"src/test.ts",
"src/**/*"
]
}