Compare commits

...
4 Commits
Author SHA1 Message Date
Lector 9718f7d341 feat!: derived select options based on property
Refs: #175
Closes #174
2026-06-27 14:55:52 +02:00
Lector 0c8f3518ea feat!: active usage type for combat styles
Refs: #171
Closes #170
Reviewed-by: Lukas Obermann <lukas.obermann@icloud.com>
2026-06-27 14:06:46 +02:00
Lukas Obermann 3b3ae6f3a6 feat: optional additional name for magic styles 2026-06-24 22:19:55 +02:00
Lukas Obermann 028e438909 fix!: remove bannzeichen option native name 2026-06-21 22:53:25 +02:00
13 changed files with 128 additions and 33 deletions
+45 -10
View File
@@ -17,7 +17,9 @@ import type {
ImprovementCost,
Poison,
PrerequisiteForLevel,
Property_ID,
RequirableSelectOptionIdentifier,
Ritual_ID,
SelectOptionCategory,
SelectOptions,
SelectOptionsAdventurePointsValue,
@@ -28,6 +30,7 @@ import type {
SkillSelectOptionCategoryPrerequisite,
SpecificFromSkillSelectOptionCategoryCategory,
SpecificTargetCategory,
Spell_ID,
TargetCategory,
TradeSecretAdventurePointsValue,
} from "../../gen/types.js"
@@ -67,15 +70,17 @@ const wrapPlainApValue = (
): TradeSecretAdventurePointsValue | undefined =>
apValue === undefined ? undefined : Case("Fixed", apValue)
const matchesSpecificSkillishIdList = (
id: string,
config: SpecificFromSkillSelectOptionCategoryCategory<string>,
const matchesSpecificSkillishIdList = <Ref>(
config: SpecificFromSkillSelectOptionCategoryCategory<Ref>,
matches: (required: Ref) => boolean,
): boolean => {
const isInList = config.list.some(matches)
switch (config.operation.kind) {
case "Intersection":
return config.list.includes(id)
return isInList
case "Difference":
return !config.list.includes(id)
return !isInList
default:
return assertExhaustive(config.operation)
}
@@ -233,14 +238,35 @@ const getDefaultSkillishFilter = <E extends SkillishEntityName>(
const { specific } = category
return specific === undefined
? undefined
: ({ id }) => matchesSpecificSkillishIdList(id, specific)
: ({ id }) => matchesSpecificSkillishIdList(specific, required => required === id)
}
const getDerivedSkillishSelectOptions = <E extends SkillishEntityName>(
const getSpellworkFilter = <E extends "Spell" | "Ritual">(
category: GenericSkillsSelectOptionCategoryCategory<
Case<"Single", Spell_ID | Ritual_ID> | Case<"Property", Property_ID>
>,
): ((instance: { id: string; content: Entity<TSONDBTypes, E> }) => boolean) | undefined => {
const { specific } = category
return specific === undefined
? undefined
: ({ id, content: { property } }) =>
matchesSpecificSkillishIdList(specific, required => {
switch (required.kind) {
case "Single":
return required.Single === id
case "Property":
return property === required.Property
default:
return assertExhaustive(required)
}
})
}
const getDerivedSkillishSelectOptions = <E extends SkillishEntityName, ID>(
database: TSONDB<TSONDBTypes>,
entryId: ActivatableIdentifier,
entity: E,
category: GenericSkillsSelectOptionCategoryCategory<string> & {
category: GenericSkillsSelectOptionCategoryCategory<ID> & {
skill_applications?: SkillApplicationOrUse[] | undefined
skill_uses?: SkillApplicationOrUse[] | undefined
},
@@ -248,7 +274,7 @@ const getDerivedSkillishSelectOptions = <E extends SkillishEntityName>(
bindingCost?: SelectOptionsBindingCostValue<SkillishIdentifier | CombatTechniqueIdentifier>
ap_value?: SelectOptionsAdventurePointsValue<SkillishIdentifier | CombatTechniqueIdentifier>
},
filter = getDefaultSkillishFilter<E>(category),
filter: ((instance: { id: string; content: Entity<TSONDBTypes, E> }) => boolean) | undefined,
) => {
const { prerequisites } = category
const { bindingCost, ap_value } = options
@@ -928,7 +954,10 @@ const getDerivedSelectOptions = (
const matchesIdRequirement =
category.Skills.specific === undefined ||
matchesSpecificSkillishIdList(id, category.Skills.specific)
matchesSpecificSkillishIdList(
category.Skills.specific,
required => required === id,
)
return matchesGroupRequirement && matchesIdRequirement
},
@@ -940,6 +969,7 @@ const getDerivedSelectOptions = (
"Spell",
category.Spells,
selectOptionCategory.Skills,
getSpellworkFilter(category.Spells),
)
case "Rituals":
return getDerivedSkillishSelectOptions(
@@ -948,6 +978,7 @@ const getDerivedSelectOptions = (
"Ritual",
category.Rituals,
selectOptionCategory.Skills,
getSpellworkFilter(category.Rituals),
)
case "LiturgicalChants":
return getDerivedSkillishSelectOptions(
@@ -956,6 +987,7 @@ const getDerivedSelectOptions = (
"LiturgicalChant",
category.LiturgicalChants,
selectOptionCategory.Skills,
getDefaultSkillishFilter(category.LiturgicalChants),
)
case "Ceremonies":
return getDerivedSkillishSelectOptions(
@@ -964,6 +996,7 @@ const getDerivedSelectOptions = (
"Ceremony",
category.Ceremonies,
selectOptionCategory.Skills,
getDefaultSkillishFilter(category.Ceremonies),
)
default:
return assertExhaustive(category)
@@ -980,6 +1013,7 @@ const getDerivedSelectOptions = (
"CloseCombatTechnique",
category.CloseCombatTechniques,
selectOptionCategory.CombatTechniques,
getDefaultSkillishFilter(category.CloseCombatTechniques),
)
case "RangedCombatTechniques":
return getDerivedSkillishSelectOptions(
@@ -988,6 +1022,7 @@ const getDerivedSelectOptions = (
"RangedCombatTechnique",
category.RangedCombatTechniques,
selectOptionCategory.CombatTechniques,
getDefaultSkillishFilter(category.RangedCombatTechniques),
)
default:
return assertExhaustive(category)
+1
View File
@@ -1428,6 +1428,7 @@ export const Locale = DB.Entity(import.meta.url, {
"Aspect": null,
"As chosen": null,
"Passive": null,
"Active": null,
"Basic Maneuver": null,
"Special Maneuver": null,
"Volume": null,
+6
View File
@@ -15,6 +15,12 @@ export const name_in_library = DB.Optional({
type: DB.String({ minLength: 1 }),
})
export const additionalName = DB.Optional({
comment:
"A part of the name that is appended in parenthesis. This can be used to specify the group of people that typically uses this entry (e.g. the name of the witch circle for a magic style for witches).\n\nIf `name_in_library` is specified, this will be ignored for places where the `name_in_library` is used.",
type: DB.String({ minLength: 1 }),
})
export const levels = DB.Optional({
comment: "Number of available levels.",
type: DB.Integer({ minimum: 2 }),
+15 -1
View File
@@ -21,11 +21,25 @@ const CombatSpecialAbilityUsageType = DB.Enum(import.meta.url, {
}),
})
export const usage_type = DB.Required({
const CombatStyleUsageType = DB.Enum(import.meta.url, {
name: "CombatSpecialAbilityTypeUsageType",
comment: "The definition of how the combat special ability style can be used in combat.",
values: () => ({
Passive: DB.EnumCase({ type: null }),
Active: DB.EnumCase({ type: null }),
}),
})
export const special_ability_usage_type = DB.Required({
comment: "The definition of how the combat special ability can be used in combat.",
type: DB.IncludeIdentifier(CombatSpecialAbilityUsageType),
})
export const style_usage_type = DB.Required({
comment: "The definition of how the combat style can be used in combat.",
type: DB.IncludeIdentifier(CombatStyleUsageType),
})
const CombatSpecialAbilityType = DB.Enum(import.meta.url, {
name: "CombatSpecialAbilityType",
comment:
+21 -2
View File
@@ -11,6 +11,7 @@ import {
SkillIdentifier,
SpellIdentifier,
TargetCategoryIdentifier,
PropertyIdentifier,
} from "./_Identifier.js"
import {
ActivatableIdentifier,
@@ -211,16 +212,34 @@ const SkillsSelectOptionCategory = DB.TypeAlias(import.meta.url, {
}),
})
const SpellFilter = DB.Enum(import.meta.url, {
name: "SpellFilter",
values: () => ({
Single: DB.EnumCase({ type: SpellIdentifier() }),
Property: DB.EnumCase({ type: PropertyIdentifier() }),
}),
})
const RitualFilter = DB.Enum(import.meta.url, {
name: "RitualFilter",
values: () => ({
Single: DB.EnumCase({ type: RitualIdentifier() }),
Property: DB.EnumCase({ type: PropertyIdentifier() }),
}),
})
const SkillsSelectOptionCategoryCategory = DB.Enum(import.meta.url, {
name: "SkillsSelectOptionCategoryCategory",
values: () => ({
Skills: DB.EnumCase({ type: DB.IncludeIdentifier(SkillSelectOptionCategoryCategory) }),
Spells: DB.EnumCase({
type: DB.GenIncludeIdentifier(GenericSkillsSelectOptionCategoryCategory, [SpellIdentifier()]),
type: DB.GenIncludeIdentifier(GenericSkillsSelectOptionCategoryCategory, [
DB.IncludeIdentifierType(SpellFilter),
]),
}),
Rituals: DB.EnumCase({
type: DB.GenIncludeIdentifier(GenericSkillsSelectOptionCategoryCategory, [
RitualIdentifier(),
DB.IncludeIdentifierType(RitualFilter),
]),
}),
LiturgicalChants: DB.EnumCase({
-4
View File
@@ -238,10 +238,6 @@ export const BannzeichenOption = DB.Entity(import.meta.url, {
The surrounding parenthesis will/should not be included, they will/should be generated.`,
type: DB.String({ minLength: 1 }),
}),
native_name: DB.Required({
comment: "The native name of the Bannzeichen option.",
type: DB.String({ minLength: 1 }),
}),
}),
),
}),
@@ -0,0 +1,18 @@
import * as DB from "tsondb/schema/dsl"
import { PropertyIdentifier } from "../../_Identifier.js"
import { DisplayOption } from "../DisplayOption.js"
export const PropertyPrerequisite = DB.TypeAlias(import.meta.url, {
name: "PropertyPrerequisite",
comment: "Requires a specific property or one of a specific set of properties.",
type: () =>
DB.Object({
id: DB.Required({
comment: "The propertys identifier.",
type: PropertyIdentifier(),
}),
display_option: DB.Optional({
type: DB.IncludeIdentifier(DisplayOption),
}),
}),
})
@@ -1,8 +1,8 @@
import * as DB from "tsondb/schema/dsl"
import { input, levels, maximum, name, name_in_library, rules } from "../_Activatable.js"
import { ap_value, ap_value_append, ap_value_l10n } from "../_ActivatableAdventurePointsValue.js"
import { combat_techniques, penalty, usage_type } from "../_ActivatableCombat.js"
import { activatableDisplayNameCustomizer, nameBuilderRules } from "../_ActivatableNames.ts"
import { combat_techniques, penalty, special_ability_usage_type } from "../_ActivatableCombat.js"
import { activatableDisplayNameCustomizer, nameBuilderRules } from "../_ActivatableNames.js"
import { explicit_select_options, select_options } from "../_ActivatableSelectOptions.js"
import { skill_applications, skill_uses } from "../_ActivatableSkillApplicationsAndUses.js"
import { GeneralPrerequisites } from "../_Prerequisite.js"
@@ -18,7 +18,7 @@ export const AdvancedCombatSpecialAbility = DB.Entity(import.meta.url, {
DB.Object({
levels,
nameBuilderRules,
usage_type,
usage_type: special_ability_usage_type,
select_options,
explicit_select_options,
skill_applications,
@@ -1,8 +1,8 @@
import * as DB from "tsondb/schema/dsl"
import { levels, maximum, name, name_in_library, rules } from "../_Activatable.js"
import { ap_value, ap_value_append, ap_value_l10n } from "../_ActivatableAdventurePointsValue.js"
import { combat_techniques, usage_type } from "../_ActivatableCombat.js"
import { activatableDisplayNameCustomizer, nameBuilderRules } from "../_ActivatableNames.ts"
import { combat_techniques, special_ability_usage_type } from "../_ActivatableCombat.js"
import { activatableDisplayNameCustomizer, nameBuilderRules } from "../_ActivatableNames.js"
import { explicit_select_options, select_options } from "../_ActivatableSelectOptions.js"
import { GeneralPrerequisites } from "../_Prerequisite.js"
import { NestedTranslationMap } from "../Locale.js"
@@ -16,7 +16,7 @@ export const BrawlingSpecialAbility = DB.Entity(import.meta.url, {
DB.Object({
levels,
nameBuilderRules,
usage_type,
usage_type: special_ability_usage_type,
select_options,
explicit_select_options,
maximum,
@@ -1,8 +1,13 @@
import * as DB from "tsondb/schema/dsl"
import { input, levels, maximum, name, name_in_library, rules } from "../_Activatable.js"
import { ap_value, ap_value_append, ap_value_l10n } from "../_ActivatableAdventurePointsValue.js"
import { combat_techniques, penalty, penalty_l10n, usage_type } from "../_ActivatableCombat.js"
import { activatableDisplayNameCustomizer, nameBuilderRules } from "../_ActivatableNames.ts"
import {
combat_techniques,
penalty,
penalty_l10n,
special_ability_usage_type,
} from "../_ActivatableCombat.js"
import { activatableDisplayNameCustomizer, nameBuilderRules } from "../_ActivatableNames.js"
import { explicit_select_options, select_options } from "../_ActivatableSelectOptions.js"
import { skill_applications, skill_uses } from "../_ActivatableSkillApplicationsAndUses.js"
import { GeneralPrerequisites } from "../_Prerequisite.js"
@@ -17,7 +22,7 @@ export const CombatSpecialAbility = DB.Entity(import.meta.url, {
DB.Object({
levels,
nameBuilderRules,
usage_type,
usage_type: special_ability_usage_type,
select_options,
explicit_select_options,
skill_applications,
@@ -2,8 +2,8 @@ import * as DB from "tsondb/schema/dsl"
import { levels, maximum, name, name_in_library, rules } from "../_Activatable.js"
import { advanced } from "../_ActivatableAdvanced.js"
import { ap_value, ap_value_append, ap_value_l10n } from "../_ActivatableAdventurePointsValue.js"
import { combat_techniques, penalty, type, usage_type } from "../_ActivatableCombat.js"
import { activatableDisplayNameCustomizer, nameBuilderRules } from "../_ActivatableNames.ts"
import { combat_techniques, penalty, type, style_usage_type } from "../_ActivatableCombat.js"
import { activatableDisplayNameCustomizer, nameBuilderRules } from "../_ActivatableNames.js"
import { explicit_select_options, select_options } from "../_ActivatableSelectOptions.js"
import { skill_applications, skill_uses } from "../_ActivatableSkillApplicationsAndUses.js"
import { AdvancedCombatSpecialAbilityIdentifier } from "../_Identifier.js"
@@ -19,7 +19,7 @@ export const CombatStyleSpecialAbility = DB.Entity(import.meta.url, {
DB.Object({
levels,
nameBuilderRules,
usage_type,
usage_type: style_usage_type,
type,
select_options,
explicit_select_options,
@@ -1,8 +1,8 @@
import * as DB from "tsondb/schema/dsl"
import { levels, maximum, name, name_in_library, rules } from "../_Activatable.js"
import { ap_value, ap_value_append, ap_value_l10n } from "../_ActivatableAdventurePointsValue.js"
import { combat_techniques, usage_type } from "../_ActivatableCombat.js"
import { activatableDisplayNameCustomizer, nameBuilderRules } from "../_ActivatableNames.ts"
import { combat_techniques, special_ability_usage_type } from "../_ActivatableCombat.js"
import { activatableDisplayNameCustomizer, nameBuilderRules } from "../_ActivatableNames.js"
import { explicit_select_options, select_options } from "../_ActivatableSelectOptions.js"
import { GeneralPrerequisites } from "../_Prerequisite.js"
import { NestedTranslationMap } from "../Locale.js"
@@ -16,7 +16,7 @@ export const CommandSpecialAbility = DB.Entity(import.meta.url, {
DB.Object({
levels,
nameBuilderRules,
usage_type,
usage_type: special_ability_usage_type,
select_options,
explicit_select_options,
maximum,
@@ -1,5 +1,5 @@
import * as DB from "tsondb/schema/dsl"
import { levels, maximum, name, name_in_library, rules } from "../_Activatable.js"
import { additionalName, levels, maximum, name, name_in_library, rules } from "../_Activatable.js"
import { advanced } from "../_ActivatableAdvanced.js"
import { ap_value, ap_value_append, ap_value_l10n } from "../_ActivatableAdventurePointsValue.js"
import { activatableDisplayNameCustomizer, nameBuilderRules } from "../_ActivatableNames.ts"
@@ -34,6 +34,7 @@ export const MagicStyleSpecialAbility = DB.Entity(import.meta.url, {
"MagicStyleSpecialAbility",
DB.Object({
name,
additionalName,
name_in_library,
rules,
ap_value_append,