Compare commits

...
43 Commits
Author SHA1 Message Date
Lukas Obermann 6fc36c131e first changes and cleanup 2025-12-08 15:45:05 +01:00
Lukas Obermann 5cd59ca6ce feat: add and adjust translation strings 2025-12-08 15:35:28 +01:00
Lukas Obermann 075182370c fix: fixed blessing range without values 2025-12-08 15:26:28 +01:00
Lukas Obermann d2b8a4450d fix: double name 2025-12-08 15:25:54 +01:00
Lukas Obermann e42a15c483 build(release): 0.24.0
Signed-off-by: Lukas Obermann <lukas.obermann@icloud.com>
2025-12-05 14:36:38 +01:00
Lukas Obermann 08a7d8d2c8 fix!: profession prerequisites can differ per profession version
BREAKING CHANGE: The prerequisites field is moved from profession
versions to profession packages.
2025-12-05 14:36:31 +01:00
Lukas Obermann d1c9795e83 fix: arcane traditions and elements can be requirable select options 2025-12-05 14:34:18 +01:00
Lukas Obermann 591a36e914 build(release): 0.23.2
Signed-off-by: Lukas Obermann <lukas.obermann@icloud.com>
2025-12-04 21:26:06 +01:00
Lukas Obermann 9392990109 fix: use correct identifiers in restrictions 2025-12-04 21:26:01 +01:00
Lukas Obermann 1961c57a97 build(release): 0.23.1
Signed-off-by: Lukas Obermann <lukas.obermann@icloud.com>
2025-12-04 21:22:03 +01:00
Lukas Obermann d4b8ad3bd8 fix: restore removed magical artifact translation properties
This reverts the previous breaking change of removing these properties.
2025-12-04 21:21:57 +01:00
Lukas Obermann 514653be04 build(release): 0.23.0
Signed-off-by: Lukas Obermann <lukas.obermann@icloud.com>
2025-12-04 21:05:27 +01:00
Lukas Obermann bd6fc3c144 feat: add translatable intoxicant addiction chance
Closes #71
2025-12-04 21:05:00 +01:00
Lukas Obermann 772da37386 fix(gen)!: duplicate translation postfix 2025-12-04 21:01:18 +01:00
Lukas Obermann a7988fc426 feat!: use description, effect and cost for magical artifact
BREAKING CHANGE: This removes previous note and rules fields.

Closes #74
2025-12-04 20:57:11 +01:00
Lukas Obermann 6958dd7f7b feat!: add map and permanent cost options to magical signs
Closes #75
2025-12-04 20:53:01 +01:00
Lukas Obermann d2418f4036 feat!: add item restrictions to equipment of blessed ones
Closes #72
2025-12-04 20:41:11 +01:00
Lukas Obermann 6a14e811fe feat: add item restrictions to armor
Closes #68
2025-12-04 20:40:56 +01:00
Lukas Obermann 5611ba5440 feat!: unify item restrictions and add profession restrictions
Closes #51
2025-12-04 20:39:02 +01:00
Lukas Obermann fcb30ba06b feat: add expression-based poison duration option
Closes #70
2025-12-04 20:10:50 +01:00
Lukas Obermann 90d3adc6f4 feat: optional intoxicant overdose
Closes #69
2025-12-04 20:02:18 +01:00
Lukas Obermann 62818120f9 feat: add structure points option to some item categories
Closes #76
2025-12-04 19:58:00 +01:00
Lukas Obermann 90ca20f52f feat: allow penalty for combat style special abilities
Closes #77
2025-12-04 19:56:10 +01:00
Lukas Obermann 0da2984acf build(gen): infer translation parameters as union 2025-12-04 19:52:00 +01:00
Lukas Obermann b651562db1 feat!: translation keys follow Unicode MessageFormat 2.0 2025-12-04 19:51:27 +01:00
Lukas Obermann 0f1082cdf5 build(deps): upgrade tsondb to 0.12.9 2025-12-04 19:50:43 +01:00
Lukas Obermann ffe04f0859 build(release): 0.22.0
Signed-off-by: Lukas Obermann <lukas.obermann@icloud.com>
2025-12-01 15:41:28 +01:00
Lukas Obermann 80b726f945 feat: ap value may depend on (any) active instance
Closes #49
2025-12-01 15:32:11 +01:00
Lukas Obermann dfd83bd28b feat: support levels for automatic entries 2025-12-01 15:29:22 +01:00
Lukas Obermann a3e35594cc feat: enable automatic entries for advantages 2025-12-01 15:29:02 +01:00
Lukas Obermann 5d5db60ce3 feat: make magical sign property optional
Fixes #52
2025-12-01 14:10:01 +01:00
Lukas Obermann 782ae60ca8 fix: identifier access error
CI reported "Cannot access 'AspectIdentifier' before initialization".
2025-12-01 14:09:19 +01:00
Lukas Obermann f8e53e7dc3 feat: allow magical signs having AE cost
Fixes #53
2025-12-01 14:02:49 +01:00
Lukas Obermann 699fab08d3 refactor: split _Activatable file into multiple 2025-12-01 13:55:37 +01:00
Lukas Obermann 43b86dbf6b feat!: ap value per level based on size category
AP Value per level based on the size category of the character in
addition to values by level.

Closes #43
BREAKING CHANGE: Previous lists of values by level need to be wrapped in
an object.
2025-12-01 13:13:40 +01:00
Lukas Obermann 92d9269a92 feat: add two-handed option to combat technique restriction
Closes #67
2025-12-01 10:54:34 +01:00
Lukas Obermann 3858cec872 refactor: extract combat-related types
Extract types for combat-related special abilities into their own file.
2025-12-01 10:52:43 +01:00
Lukas Obermann 6d3e560394 build(release): 0.21.2
Signed-off-by: Lukas Obermann <lukas.obermann@icloud.com>
2025-11-30 21:40:57 +01:00
Lukas Obermann a2fdcdb06e build: add linting and formatting
Add configuration and CI checks as well as fix any linting and
formatting issues reported.
2025-11-30 21:27:30 +01:00
Lukas Obermann 9d2dc029c2 feat: allow three fraction digits for item weight 2025-11-30 16:51:17 +01:00
Lukas Obermann fbde6692ed build(deps): upgrade tsondb to 0.12.6 2025-11-30 16:51:00 +01:00
Lukas Obermann bb4a906cdf ci: remove NODE_AUTH_TOKEN in favor of OIDC 2025-11-27 19:54:06 +01:00
Lukas Obermann c4cb19947c ci: add test workflow to check schema integrity 2025-11-27 19:51:59 +01:00
179 changed files with 4737 additions and 3538 deletions
+29 -13
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,14 +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
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
- 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
+33
View File
@@ -0,0 +1,33 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Node.js CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [23, 24]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build --if-present
- run: npm run lint
- run: npm run format:check
- run: npm test
+88
View File
@@ -2,6 +2,94 @@
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [0.24.0](https://github.com/elyukai/optolith-database-schema/compare/v0.23.2...v0.24.0) (2025-12-05)
### ⚠ BREAKING CHANGES
* The prerequisites field is moved from profession
versions to profession packages.
### Bug Fixes
* arcane traditions and elements can be requirable select options ([d1c9795](https://github.com/elyukai/optolith-database-schema/commit/d1c9795e8378b8f7da69e47491c48df6514661a8))
* profession prerequisites can differ per profession version ([08a7d8d](https://github.com/elyukai/optolith-database-schema/commit/08a7d8d2c88beff69ea7b4d68995b1eb6d2528a6))
## [0.23.2](https://github.com/elyukai/optolith-database-schema/compare/v0.23.1...v0.23.2) (2025-12-04)
### Bug Fixes
* use correct identifiers in restrictions ([9392990](https://github.com/elyukai/optolith-database-schema/commit/9392990109296bc4cfbc5116c8fa5360efd4a02d))
## [0.23.1](https://github.com/elyukai/optolith-database-schema/compare/v0.23.0...v0.23.1) (2025-12-04)
### Bug Fixes
* restore removed magical artifact translation properties ([d4b8ad3](https://github.com/elyukai/optolith-database-schema/commit/d4b8ad3bd854b32d2a6b05d1e79837c210910d4b))
## [0.23.0](https://github.com/elyukai/optolith-database-schema/compare/v0.22.0...v0.23.0) (2025-12-04)
### ⚠ BREAKING CHANGES
* **gen:** duplicate translation postfix
* This removes previous note and rules fields.
* add map and permanent cost options to magical signs
* add item restrictions to equipment of blessed ones
* unify item restrictions and add profession restrictions
* translation keys follow Unicode MessageFormat 2.0
### Features
* add expression-based poison duration option ([fcb30ba](https://github.com/elyukai/optolith-database-schema/commit/fcb30ba06b93924ce9399ea5481b98bd2689b229)), closes [#70](https://github.com/elyukai/optolith-database-schema/issues/70)
* add item restrictions to armor ([6a14e81](https://github.com/elyukai/optolith-database-schema/commit/6a14e811fe71e2f6ade006572d4ba4740cc7b4cd)), closes [#68](https://github.com/elyukai/optolith-database-schema/issues/68)
* add item restrictions to equipment of blessed ones ([d2418f4](https://github.com/elyukai/optolith-database-schema/commit/d2418f4036c52d5cf972208c67ecf1229e883c85)), closes [#72](https://github.com/elyukai/optolith-database-schema/issues/72)
* add map and permanent cost options to magical signs ([6958dd7](https://github.com/elyukai/optolith-database-schema/commit/6958dd7f7bda88027d38ba2a3de75a0c645ff7f7)), closes [#75](https://github.com/elyukai/optolith-database-schema/issues/75)
* add structure points option to some item categories ([6281812](https://github.com/elyukai/optolith-database-schema/commit/62818120f9e849ba7e95f3a68a3f00758fb8a514)), closes [#76](https://github.com/elyukai/optolith-database-schema/issues/76)
* add translatable intoxicant addiction chance ([bd6fc3c](https://github.com/elyukai/optolith-database-schema/commit/bd6fc3c1448c4b6a221603f33197d8b44a743509)), closes [#71](https://github.com/elyukai/optolith-database-schema/issues/71)
* allow penalty for combat style special abilities ([90ca20f](https://github.com/elyukai/optolith-database-schema/commit/90ca20f52f806824325a916b8a89f46070db941a)), closes [#77](https://github.com/elyukai/optolith-database-schema/issues/77)
* optional intoxicant overdose ([90d3adc](https://github.com/elyukai/optolith-database-schema/commit/90d3adc6f4a214f3a4178fb402becc446984005d)), closes [#69](https://github.com/elyukai/optolith-database-schema/issues/69)
* translation keys follow Unicode MessageFormat 2.0 ([b651562](https://github.com/elyukai/optolith-database-schema/commit/b651562db13d1531d160f8d3288485a04f9bfa3e))
* unify item restrictions and add profession restrictions ([5611ba5](https://github.com/elyukai/optolith-database-schema/commit/5611ba54402ce6ac401d40160513b3db70327d8d)), closes [#51](https://github.com/elyukai/optolith-database-schema/issues/51)
* use description, effect and cost for magical artifact ([a7988fc](https://github.com/elyukai/optolith-database-schema/commit/a7988fc426ad76f4b876071b4cde05b11d523212)), closes [#74](https://github.com/elyukai/optolith-database-schema/issues/74)
### Bug Fixes
* **gen:** duplicate translation postfix ([772da37](https://github.com/elyukai/optolith-database-schema/commit/772da37386622cff0a1ab4954a392eeaca1ac72a))
## [0.22.0](https://github.com/elyukai/optolith-database-schema/compare/v0.21.2...v0.22.0) (2025-12-01)
### ⚠ BREAKING CHANGES
* Previous lists of values by level need to be wrapped in
an object.
### Features
* add two-handed option to combat technique restriction ([92d9269](https://github.com/elyukai/optolith-database-schema/commit/92d9269a923ccaa9a99617c34de612947ddbcb88)), closes [#67](https://github.com/elyukai/optolith-database-schema/issues/67)
* allow magical signs having AE cost ([f8e53e7](https://github.com/elyukai/optolith-database-schema/commit/f8e53e7dc3645e19a24cc60f1257ccd33f8579d6)), closes [#53](https://github.com/elyukai/optolith-database-schema/issues/53)
* ap value may depend on (any) active instance ([80b726f](https://github.com/elyukai/optolith-database-schema/commit/80b726f945cd6fe4609dfc0b11195ee3801d6433)), closes [#49](https://github.com/elyukai/optolith-database-schema/issues/49)
* ap value per level based on size category ([43b86db](https://github.com/elyukai/optolith-database-schema/commit/43b86dbf6b6940b6352a69d2a5e9c2f2430e2516)), closes [#43](https://github.com/elyukai/optolith-database-schema/issues/43)
* enable automatic entries for advantages ([a3e3559](https://github.com/elyukai/optolith-database-schema/commit/a3e35594ccaf85a7d10db4425234bdf8adc7bca0))
* make magical sign property optional ([5d5db60](https://github.com/elyukai/optolith-database-schema/commit/5d5db60ce3dba9330ed1b6f63b8ceb9e8b4c393b)), closes [#52](https://github.com/elyukai/optolith-database-schema/issues/52)
* support levels for automatic entries ([dfd83bd](https://github.com/elyukai/optolith-database-schema/commit/dfd83bd28b68bf73df6936d566765f526b1879c4))
### Bug Fixes
* identifier access error ([782ae60](https://github.com/elyukai/optolith-database-schema/commit/782ae60ca8e20f9118448922946735313852fed8))
## [0.21.2](https://github.com/elyukai/optolith-database-schema/compare/v0.21.1...v0.21.2) (2025-11-30)
### Features
* allow three fraction digits for item weight ([9d2dc02](https://github.com/elyukai/optolith-database-schema/commit/9d2dc029c2a07ec3180b4d340ff5ea8afe67fcba))
## [0.21.1](https://github.com/elyukai/optolith-database-schema/compare/v0.21.0...v0.21.1) (2025-11-27)
+86
View File
@@ -0,0 +1,86 @@
import js from "@eslint/js"
import { defineConfig, globalIgnores } from "eslint/config"
import globals from "globals"
import tseslint from "typescript-eslint"
export default defineConfig([
globalIgnores([
"**/gen/",
"**/lib/",
"src/{cache,config,helpers,rendering}/**/*",
"tsondb.config.{js,mjs,cjs,ts,mts,cts}",
]),
{
files: [
"src/**/*.{js,mjs,cjs,ts,mts,cts}"
],
plugins: {
js,
},
extends: ["js/recommended"],
},
{
files: [
"src/**/*.{js,mjs,cjs,ts,mts,cts}"
],
languageOptions: {
globals: {
...globals.node,
},
},
},
{
ignores: [
"eslint.config.js",
],
extends: tseslint.configs.strictTypeChecked,
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
"@typescript-eslint/no-unused-vars": [
"error",
{
args: "all",
argsIgnorePattern: "^_",
caughtErrors: "all",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
"@typescript-eslint/consistent-type-exports": "error",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-empty-object-type": [
"error",
{
allowInterfaces: "with-single-extends",
},
],
"@typescript-eslint/no-namespace": [
"error",
{
allowDeclarations: true,
},
],
"@typescript-eslint/prefer-promise-reject-errors": [
"error",
{
allowThrowingUnknown: true,
}
],
"@typescript-eslint/no-base-to-string": "off",
"@typescript-eslint/no-array-constructor": "off",
},
},
// {
// files: ["test/**/*.{js,mjs,cjs,ts,mts,cts}"],
// rules: {
// "@typescript-eslint/no-floating-promises": "off",
// }
// },
])
+1355 -53
View File
File diff suppressed because it is too large Load Diff
+13 -6
View File
@@ -1,6 +1,6 @@
{
"name": "optolith-database-schema",
"version": "0.21.1",
"version": "0.24.0",
"description": "Definitions and utilities for the flat-file database of Optolith, a character creation tool for the Pen and Paper RPG “The Dark Eye 5”, and its external integrations into other software.",
"keywords": [
"tde",
@@ -19,18 +19,20 @@
"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"
},
"scripts": {
"build": "tsc",
"generate": "tsondb generate",
"watch": "tsc -w",
"generate": "tsondb generate",
"test": "node ./lib/test.js",
"lint": "eslint",
"format": "prettier \"src/{types/**/*.{ts,tsx},*.{ts,tsx}}\" --write",
"format:check": "prettier \"src/{types/**/*.{ts,tsx},*.{ts,tsx}}\" --check",
"release": "commit-and-tag-version",
"release:sign": "commit-and-tag-version --sign --signoff"
},
@@ -38,13 +40,18 @@
"license": "MPL-2.0",
"dependencies": {
"@optolith/helpers": "^0.2.2",
"tsondb": "^0.12.5",
"tsondb": "^0.12.9",
"yaml": "^2.8.1"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"commit-and-tag-version": "^12.6.0",
"typescript": "^5.9.3"
"eslint": "^9.39.1",
"globals": "^16.5.0",
"prettier": "^3.7.3",
"typescript": "^5.9.3",
"typescript-eslint": "^8.48.0"
},
"repository": "github:elyukai/optolith-database-schema",
"bugs": {
+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),
})
+8
View File
@@ -0,0 +1,8 @@
import type { ChildEntityMap, EntityMap } from "../../gen/types.js"
import type { GetAllChildInstancesForParent, GetAllInstances, GetInstanceById } from "../utils.js"
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),
},
},
})
-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
}
-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"
-63
View File
@@ -1,63 +0,0 @@
import { filterNonNullable } from "@optolith/helpers/array"
import { collator } from "./helpers/i18n.js"
import { TypeValidationError } from "./main.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")
-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)
},
})
-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)]
+1 -1
View File
@@ -158,7 +158,7 @@ export const schema = Schema(
Types.Weapon,
Types.ZibiljaRitual,
],
Types.Locale
Types.Locale,
)
// /**
-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]
+7 -17
View File
@@ -1,20 +1,9 @@
import { Boolean, Entity, IncludeIdentifier, Object, Optional, Required } from "tsondb/schema/def"
import {
ap_value,
ap_value_append,
ap_value_l10n,
explicit_select_options,
input,
levels,
maximum,
name,
name_in_library,
range_l10n,
rules,
select_options,
skill_applications,
skill_uses,
} from "./_Activatable.js"
import { input, levels, maximum, name, name_in_library, range_l10n, rules } from "./_Activatable.js"
import { ap_value, ap_value_append, ap_value_l10n } from "./_ActivatableAdventurePointsValue.js"
import { automatic_entries } from "./_ActivatableAutomatic.js"
import { explicit_select_options, select_options } from "./_ActivatableSelectOptions.js"
import { skill_applications, skill_uses } from "./_ActivatableSkillApplicationsAndUses.js"
import { AdvantageDisadvantagePrerequisites } from "./_Prerequisite.js"
import { NestedTranslationMap } from "./Locale.js"
import { Errata } from "./source/_Erratum.js"
@@ -31,6 +20,7 @@ export const Advantage = Entity(import.meta.url, {
skill_applications,
skill_uses,
maximum,
automatic_entries,
prerequisites: Optional({
type: IncludeIdentifier(AdvantageDisadvantagePrerequisites),
}),
@@ -59,7 +49,7 @@ export const Advantage = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
+1 -1
View File
@@ -14,7 +14,7 @@ export const AnimalType = Entity(import.meta.url, {
comment: "The animal types name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
+1 -1
View File
@@ -21,7 +21,7 @@ export const Aspect = Entity(import.meta.url, {
minLength: 1,
}),
}),
})
}),
),
}),
displayName: {},
+1 -1
View File
@@ -23,7 +23,7 @@ export const Attribute = Entity(import.meta.url, {
comment: "The description of the attribute.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
+4 -3
View File
@@ -11,6 +11,7 @@ import {
TypeAlias,
} from "tsondb/schema/def"
import { DurationUnit } from "./_ActivatableSkillDuration.js"
import { FixedRange } from "./_ActivatableSkillRange.js"
import { AffectedTargetCategories } from "./_ActivatableSkillTargetCategory.js"
import { NestedTranslationMap } from "./Locale.js"
import { Errata } from "./source/_Erratum.js"
@@ -57,7 +58,7 @@ export const Blessing = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
@@ -78,7 +79,7 @@ const BlessingRange = Enum(import.meta.url, {
values: () => ({
Self: EnumCase({ type: null }),
Touch: EnumCase({ type: null }),
Fixed: EnumCase({ type: null }),
Fixed: EnumCase({ type: IncludeIdentifier(FixedRange) }),
}),
})
@@ -118,7 +119,7 @@ const IndefiniteBlessingDuration = TypeAlias(import.meta.url, {
comment: "A description of the duration.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
})
+3 -3
View File
@@ -76,7 +76,7 @@ export const Cantrip = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
@@ -138,7 +138,7 @@ const CommonCantripTraditionNote = TypeAlias(import.meta.url, {
comment: "A note, appended to the generated string in parenthesis.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
})
@@ -203,7 +203,7 @@ const IndefiniteCantripDuration = TypeAlias(import.meta.url, {
comment: "A description of the duration.",
type: IncludeIdentifier(ResponsiveText),
}),
})
}),
),
}),
})
+1 -1
View File
@@ -96,7 +96,7 @@ export const Ceremony = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
enhancements: Required({
type: ChildEntities(Enhancement),
+2 -2
View File
@@ -54,7 +54,7 @@ export const CloseCombatTechnique = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
@@ -114,7 +114,7 @@ export const RangedCombatTechnique = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
+1 -1
View File
@@ -39,7 +39,7 @@ export const Condition = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
+1 -1
View File
@@ -16,7 +16,7 @@ export const Continent = Entity(import.meta.url, {
comment: "The continent name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
+9 -9
View File
@@ -66,28 +66,28 @@ export const Culture = Entity(import.meta.url, {
comment: "A list of common advantages.",
type: Array(
GenIncludeIdentifier(CommonnessRatedAdvantageDisadvantage, [AdvantageIdentifier()]),
{ minItems: 1 }
{ minItems: 1 },
),
}),
common_disadvantages: Optional({
comment: "A list of common disadvantages.",
type: Array(
GenIncludeIdentifier(CommonnessRatedAdvantageDisadvantage, [DisadvantageIdentifier()]),
{ minItems: 1 }
{ minItems: 1 },
),
}),
uncommon_advantages: Optional({
comment: "A list of uncommon advantages.",
type: Array(
GenIncludeIdentifier(CommonnessRatedAdvantageDisadvantage, [AdvantageIdentifier()]),
{ minItems: 1 }
{ minItems: 1 },
),
}),
uncommon_disadvantages: Optional({
comment: "A list of uncommon disadvantages.",
type: Array(
GenIncludeIdentifier(CommonnessRatedAdvantageDisadvantage, [DisadvantageIdentifier()]),
{ minItems: 1 }
{ minItems: 1 },
),
}),
common_skills: Required({
@@ -138,7 +138,7 @@ export const Culture = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
@@ -333,7 +333,7 @@ const GroupedCommonProfessions = TypeAlias(import.meta.url, {
]),
}),
},
{ minProperties: 1 }
{ minProperties: 1 },
),
})
@@ -420,7 +420,7 @@ const CommonNames = TypeAlias(import.meta.url, {
type: String({ minLength: 1 }),
}),
},
{ minProperties: 1 }
{ minProperties: 1 },
),
})
@@ -442,7 +442,7 @@ const CommonNameGroup = TypeAlias(import.meta.url, {
type: Array(IncludeIdentifier(CommonName), { minItems: 1 }),
}),
},
{ minProperties: 1 }
{ minProperties: 1 },
),
})
@@ -459,6 +459,6 @@ const CommonName = TypeAlias(import.meta.url, {
type: String({ minLength: 1 }),
}),
},
{ minProperties: 1 }
{ minProperties: 1 },
),
})
+1 -1
View File
@@ -36,7 +36,7 @@ export const DerivedCharacteristic = Entity(import.meta.url, {
comment: "Possible calculation strings for the final value.",
type: IncludeIdentifier(CalculationTranslation),
}),
})
}),
),
}),
displayName: {},
+5 -16
View File
@@ -7,21 +7,10 @@ import {
Required,
String,
} from "tsondb/schema/def"
import {
ap_value,
ap_value_append,
ap_value_l10n,
explicit_select_options,
input,
levels,
maximum,
name,
name_in_library,
rules,
select_options,
skill_applications,
skill_uses,
} from "./_Activatable.js"
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 { explicit_select_options, select_options } from "./_ActivatableSelectOptions.js"
import { skill_applications, skill_uses } from "./_ActivatableSkillApplicationsAndUses.js"
import { AdvantageDisadvantagePrerequisites } from "./_Prerequisite.js"
import { NestedTranslationMap } from "./Locale.js"
import { Errata } from "./source/_Erratum.js"
@@ -69,7 +58,7 @@ export const Disadvantage = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
+1 -1
View File
@@ -14,7 +14,7 @@ export const Element = Entity(import.meta.url, {
comment: "The elements name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
+1 -1
View File
@@ -43,7 +43,7 @@ export const ExperienceLevel = Entity(import.meta.url, {
comment: "The experience levels name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
+1 -1
View File
@@ -14,7 +14,7 @@ export const EyeColor = Entity(import.meta.url, {
comment: "The eye colors name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
+4 -4
View File
@@ -72,7 +72,7 @@ If no animal types are given, the animal disease applies to all animal types.`,
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
@@ -98,7 +98,7 @@ const IndefiniteFamiliarsTrickProperty = TypeAlias(import.meta.url, {
comment: "A description of the property.",
type: IncludeIdentifier(ResponsiveText),
}),
})
}),
),
}),
})
@@ -164,8 +164,8 @@ const FamiliarsTrickFixedOneTimeCost = TypeAlias(import.meta.url, {
type: IncludeIdentifier(ResponsiveTextOptional),
}),
},
{ minProperties: 1 }
)
{ minProperties: 1 },
),
),
}),
})
+1 -1
View File
@@ -14,7 +14,7 @@ export const HairColor = Entity(import.meta.url, {
comment: "The hair colors name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
+4 -4
View File
@@ -42,7 +42,7 @@ export const Guideline = Entity(import.meta.url, {
comment: "The guidelines name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
@@ -83,7 +83,7 @@ export const Curriculum = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
@@ -200,10 +200,10 @@ export const LessonPackage = Entity(import.meta.url, {
"The spell values difference of the lesson package. Use this field to specify a text that is displayed instead of the generated `spellwork_changes` list. The field is displayed even if no list is present.",
type: String({ minLength: 1 }),
}),
})
}),
),
},
{ minProperties: 3 }
{ minProperties: 3 },
),
parentReferenceKey: "curriculum",
displayName: {},
+1 -1
View File
@@ -96,7 +96,7 @@ export const LiturgicalChant = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
enhancements: Required({
type: ChildEntities(Enhancement),
+178 -140
View File
@@ -1,15 +1,15 @@
import {
Boolean,
Entity,
MemberDecl,
type MemberDecl,
NestedEntityMap,
Object,
ObjectType,
type ObjectType,
Optional,
Required,
String,
TranslationObject,
Type,
type Type,
} from "tsondb/schema/def"
export const Locale = Entity(import.meta.url, {
@@ -38,14 +38,14 @@ export const Locale = Entity(import.meta.url, {
// prettier-ignore
type: TranslationObject({
// Menu
"About {0}": null,
"About {$app}": null,
"Preferences …": null,
"Services": null,
"Hide {0}": null,
"Hide {$app}": null,
"Hide Others": null,
"Show All": null,
"Quit {0}": null,
"File": null,
"Quit {$app}": null,
".input {$os :string} {{File}}": null,
"Close": null,
"Quit": null,
"Edit": null,
@@ -85,7 +85,7 @@ export const Locale = Entity(import.meta.url, {
/**
* - `0`: Version number
*/
"Version {0} is available! Do you wish to download and install?": null,
"Version {$version} is available! Do you wish to download and install?": null,
"Download": null,
"Download Later": null,
"Downloading update …": null,
@@ -131,7 +131,7 @@ export const Locale = Entity(import.meta.url, {
/**
* - `0`: AP left
*/
"{0} AP Remaining": null,
"{$value} AP Remaining": null,
"Save": null,
"Show Settings": null,
"Toggle DevTools": null,
@@ -142,93 +142,93 @@ export const Locale = Entity(import.meta.url, {
/**
* - `0`: AP value
*/
"{0} Adventure Points": null,
".input {$value :number} {{{$value} Adventure Points}}": null,
/**
* - `0`: AP value
*/
"{0} AP": null,
"{$value} AP": null,
/**
* - `0`: AP Total
*/
"{0} Total AP": null,
"{$value} Total AP": null,
/**
* - `0`: AP Spent
*/
"{0} AP Spent": null,
"{$value} AP Spent": null,
/**
* - `0`: Current AP spent on advantages
* - `1`: Maximum possible AP spent on advantages
*/
"{0}/{1} AP spent on advantages": null,
"{$value}/{$max} AP spent on advantages": null,
/**
* - `0`: Current AP spent on magic advantages
* - `1`: Maximum possible AP spent on magic advantages
*/
"Thereof {0}/{1} on magic advantages": null,
"Thereof {$value}/{$max} on magic advantages": null,
/**
* - `0`: Current AP spent on blessed advantages
* - `1`: Maximum possible AP spent on blessed advantages
*/
"Thereof {0}/{1} on blessed advantages": null,
"Thereof {$value}/{$max} on blessed advantages": null,
/**
* - `0`: Current AP spent on disadvantages
* - `1`: Maximum possible AP spent on disadvantages
*/
"{0}/{1} AP received from disadvantages": null,
"{$value}/{$max} AP received from disadvantages": null,
/**
* - `0`: Current AP spent on magic disadvantages
* - `1`: Maximum possible AP spent on magic disadvantages
*/
"Thereof {0}/{1} from magic disadvantages": null,
"Thereof {$value}/{$max} from magic disadvantages": null,
/**
* - `0`: Current AP spent on blessed disadvantages
* - `1`: Maximum possible AP spent on blessed disadvantages
*/
"Thereof {0}/{1} from blessed disadvantages": null,
"Thereof {$value}/{$max} from blessed disadvantages": null,
/**
* - `0`: AP spent on race
*/
"{0} AP spent on race": null,
"{$value} AP spent on race": null,
/**
* - `0`: AP spent on profession
*/
"{0} AP spent on profession": null,
"{$value} AP spent on profession": null,
/**
* - `0`: AP spent on attributes
*/
"{0} AP spent on attributes": null,
"{$value} AP spent on attributes": null,
/**
* - `0`: AP spent on skills
*/
"{0} AP spent on skills": null,
"{$value} AP spent on skills": null,
/**
* - `0`: AP spent on combat techniques
*/
"{0} AP spent on combat techniques": null,
"{$value} AP spent on combat techniques": null,
/**
* - `0`: AP spent on spells
*/
"{0} AP spent on spells": null,
"{$value} AP spent on spells": null,
/**
* - `0`: AP spent on cantrips
*/
"{0} AP spent on cantrips": null,
"{$value} AP spent on cantrips": null,
/**
* - `0`: AP spent on liturgical chants
*/
"{0} AP spent on liturgical chants": null,
"{$value} AP spent on liturgical chants": null,
/**
* - `0`: AP spent on blessings
*/
"{0} AP spent on blessings": null,
"{$value} AP spent on blessings": null,
/**
* - `0`: AP spent on special abilities
*/
"{0} AP spent on special abilities": null,
"{$value} AP spent on special abilities": null,
/**
* - `0`: AP spent on energies (LP/AE/KP)
*/
"{0} AP spent on improving/buying back LP/AE/KP": null,
"{$value} AP spent on improving/buying back LP/AE/KP": null,
"header.dialogs.herosaved": null,
"header.dialogs.allsaved": null,
@@ -281,19 +281,19 @@ export const Locale = Entity(import.meta.url, {
"Apply": null,
"Add": null,
"Not enough AP": null,
"You are missing {0} Adventure Points to do this.": null,
".input {$value :number} {{You are missing {$value} Adventure Points to do this.}}": null,
"Exceeding Adventure Points limit for advantages": null,
"You cannot spend more than {0} AP on advantages. You would exceed this limit by {1} AP.": null,
"You cannot spend more than {$max} AP on advantages. You would exceed this limit by {$over} AP.": null,
"Exceeding Adventure Points limit for magical advantages": null,
"You cannot spend more than {0} AP on magical advantages. You would exceed this limit by {1} AP.": null,
"You cannot spend more than {$max} AP on magical advantages. You would exceed this limit by {$over} AP.": null,
"Exceeding Adventure Points limit for blessed advantages": null,
"You cannot spend more than {0} AP on blessed advantages. You would exceed this limit by {1} AP.": null,
"You cannot spend more than {$max} AP on blessed advantages. You would exceed this limit by {$over} AP.": null,
"Exceeding Adventure Points limit for disadvantages": null,
"You cannot receive more than {0} AP from disadvantages. You would exceed this limit by {1} AP.": null,
"You cannot receive more than {$max} AP from disadvantages. You would exceed this limit by {$over} AP.": null,
"Exceeding Adventure Points limit for magical disadvantages": null,
"You cannot receive more than {0} AP from magical disadvantages. You would exceed this limit by {1} AP.": null,
"You cannot receive more than {$max} AP from magical disadvantages. You would exceed this limit by {$over} AP.": null,
"Exceeding Adventure Points limit for blessed disadvantages": null,
"You cannot receive more than {0} AP from blessed disadvantages. You would exceed this limit by {1} AP.": null,
"You cannot receive more than {$max} AP from blessed disadvantages. You would exceed this limit by {$over} AP.": null,
"heroes.filters.origin.allheroes": null,
"heroes.filters.origin.ownheroes": null,
@@ -828,7 +828,7 @@ export const Locale = Entity(import.meta.url, {
/**
* - `0`: Entry name
*/
"AP Cost for {0}": null,
"AP Cost for {$category}": null,
"Custom option": null,
"specialabilities.nativetonguelevel": null,
@@ -965,9 +965,9 @@ export const Locale = Entity(import.meta.url, {
"spells.traditions.general": null,
"magicalactions.animistforces.tribes.general": null,
" (modified by {0})": null,
" ( {0})": null,
"{0} or {1}, depending on which value is higher": null,
" (modified by {$modifier})": null,
" ({$modifier})": null,
"{$first} or {$second}, depending on which value is higher": null,
"Invocation Difficulty": null,
"ID": null,
"Creation Difficulty": null,
@@ -996,86 +996,104 @@ export const Locale = Entity(import.meta.url, {
" (you cannot use a modification on this ceremonys cost)": null,
" (you cannot use a modification on this ceremonys range)": null,
" (cannot modify)": null,
"{0} act": null,
"{$value} act": null,
"act": null,
"{0} actions": null,
".input {$value :number} {{{$value} actions}}": null,
"{$value} actions": null,
"actions": null,
"{0} s": null,
"{$value} s": null,
"s": null,
"{0} seconds": null,
".input {$value :number} {{{$value} seconds}}": null,
"{$value} seconds": null,
"seconds": null,
"{0} min": null,
"{$value} min": null,
"min": null,
"{0} minutes": null,
".input {$value :number} {{{$value} minutes}}": null,
"{$value} minutes": null,
"minutes": null,
"{0} h": null,
"{$value} h": null,
"h": null,
"{0} hours": null,
".input {$value :number} {{{$value} hours}}": null,
"{$value} hours": null,
"hours": null,
"{0} d": null,
"{$value} d": null,
"d": null,
"{0} days": null,
".input {$value :number} {{{$value} days}}": null,
"{$value} days": null,
"days": null,
"{0} wks.": null,
".input {$value :number} {{{$value} wks.}}": null,
"{$value} wks.": null,
"wks.": null,
"{0} weeks": null,
".input {$value :number} {{{$value} weeks}}": null,
"{$value} weeks": null,
"weeks": null,
"{0} mos.": null,
".input {$value :number} {{{$value} mos.}}": null,
"{$value} mos.": null,
"mos.": null,
"{0} months": null,
".input {$value :number} {{{$value} months}}": null,
"{$value} months": null,
"months": null,
"{0} yrs.": null,
".input {$value :number} {{{$value} yrs.}}": null,
"{$value} yrs.": null,
"yrs.": null,
"{0} years": null,
".input {$value :number} {{{$value} years}}": null,
"{$value} years": null,
"years": null,
"{0} cent.": null,
"{$value} cent.": null,
"cent.": null,
"{0} centuries": null,
".input {$value :number} {{{$value} centuries}}": null,
"{$value} centuries": null,
"centuries": null,
"{0} SA": null,
"{0} seduction actions": null,
"{0} rnds": null,
"{0} rounds": null,
"{0} CR": null,
"{$value} SA": null,
".input {$value :number} {{{$value} seduction actions}}": null,
"{$value} seduction actions": null,
"{$value} rnds": null,
".input {$value :number} {{{$value} rounds}}": null,
"{$value} rounds": null,
"{$value} CR": null,
"CR": null,
"{0} combat rounds": null,
".input {$value :number} {{{$value} combat rounds}}": null,
"{$value} combat rounds": null,
"combat rounds": null,
"{0} AE": null,
"{0} KP": null,
"{$value} AE": null,
"{$value} KP": null,
"min. ": null,
"at least ": null,
"min. {0}": null,
"at least {0}": null,
"/{0}": null,
" per {0}": null,
", minimum of {0}": null,
" ({0} perm.)": null,
", {0} of which are permanent": null,
"min. {$value}": null,
"at least {$value}": null,
"/{$value}": null,
" per {$value}": null,
", minimum of {$value}": null,
" ({$value} perm.)": null,
".input {$value :number} {{, {$value} of which are permanent}}": null,
", {$value} of which are permanent": null,
"half of the activation cost": null,
" and ": null,
" + ": null,
" or ": null,
" / ": null,
" for ": null,
" (no more than {0})": null,
" (max. {0})": null,
"no more than {0}": null,
"max. {0}": null,
" (no more than {$value})": null,
" (max. {$value})": null,
"no more than {$value}": null,
"max. {$value}": null,
"Immediate": null,
"Permanent": null,
"no more than ": null,
"max. ": null,
"Quality Levels": null,
"QL": null,
"QL {0}": null,
"QL {$value}": null,
"Skill Points": null,
"SP": null,
"Sustained": null,
"(S)": null,
"{0} yards": null,
"{0} yd": null,
"{0} miles": null,
"{0} mi.": null,
".input {$value :number} {{{$value} yards}}": null,
"{$value} yards": null,
"{$value} yd": null,
".input {$value :number} {{{$value} miles}}": null,
"{$value} miles": null,
"{$value} mi.": null,
"Sight": null,
"Self": null,
"Global": null,
@@ -1090,58 +1108,22 @@ export const Locale = Entity(import.meta.url, {
"none": null,
"State": null,
"Social Status {0} or higher": null,
"Person with {0}": null,
"Social Status {$minStatus} or higher": null,
"Person with {$sexualCharacteristic}": null,
"Penis": null,
"Vagina": null,
"the SR for {0} combined must add up to at least {1}": null,
"{0} on at least SR {1}: {2}": null,
"{0} arcane works with the property {1} at SR {2} or higher": null,
"{0} liturgical chants and ceremonies with the aspect {1} at SR {2} or higher": null,
"one of the following skills": null,
"two of the following skills": null,
"three of the following skills": null,
"four of the following skills": null,
"five of the following skills": null,
"six of the following skills": null,
"seven of the following skills": null,
"eight of the following skills": null,
"nine of the following skills": null,
"{0} of the following skills": null,
"one combat technique": null,
"two combat techniques": null,
"three combat techniques": null,
"four combat techniques": null,
"five combat techniques": null,
"six combat techniques": null,
"seven combat techniques": null,
"eight combat techniques": null,
"nine combat techniques": null,
"{0} combat techniques": null,
"one close combat technique": null,
"two close combat techniques": null,
"three close combat techniques": null,
"four close combat techniques": null,
"five close combat techniques": null,
"six close combat techniques": null,
"seven close combat techniques": null,
"eight close combat techniques": null,
"nine close combat techniques": null,
"{0} close combat techniques": null,
"one ranged combat technique": null,
"two ranged combat techniques": null,
"three ranged combat techniques": null,
"four ranged combat techniques": null,
"five ranged combat techniques": null,
"six ranged combat techniques": null,
"seven ranged combat techniques": null,
"eight ranged combat techniques": null,
"nine ranged combat techniques": null,
"{0} ranged combat techniques": null,
"the SR for {$skill} combined must add up to at least {$minRating}": null,
".input {$minRating :number} {{{$count} on at least SR {$minRating}: {$list}}}": null,
".input {$count :number} .input {$minRating :number} {{{$count} arcane works with the property {$property} at SR {$minRating} or higher}}": null,
".input {$count :number} .input {$minRating :number} {{{$count} liturgical chants and ceremonies with the aspect {$aspect} at SR {$minRating} or higher}}": null,
".input {$count :number} {{{$count} of the following skills}}": null,
".input {$count :number} {{{$count} combat techniques}}": null,
".input {$count :number} {{{$count} close combat techniques}}": null,
".input {$count :number} {{{$count} ranged combat techniques}}": null,
"special ability": null,
"no special ability": null,
"Tradition": null,
"Tradition ({0})": null,
"Tradition ({$tradition})": null,
"Church": null,
"Shaman": null,
"Tradition must be able to use rituals": null,
@@ -1150,12 +1132,12 @@ export const Locale = Entity(import.meta.url, {
"liturgical enhancement": null,
"for": null,
"no other ancestor blood advantage": null,
"Race, culture, or profession must have {0} as an automatic or suggested {1}": null,
"Race, culture, or profession must have {$entry} as an automatic or suggested {$itemOfCategory}": null,
"advantage": null,
"disadvantage": null,
"domain {0}": null,
"{0} level {1}": null,
"Level {0}:": null,
"domain {$domain}": null,
"{$pact} level {$pactLevel}": null,
"Level {$level}:": null,
"inlinewiki.castingtime": null,
"inlinewiki.ritualtime": null,
@@ -1371,17 +1353,17 @@ export const Locale = Entity(import.meta.url, {
"Front Cover Inside": null,
"Back Cover Inside": null,
"since the {0}. printing": null,
"removed in {0}. printing": null,
}),
".input {$printing :number} {{since the {$printing}. printing}}": null,
".input {$printing :number} {{removed in {$printing}. printing}}": null,
}, { allKeysAreRequired: true }),
}),
}),
})
export const NestedTranslationMap = <
Name extends string,
T extends Record<string, MemberDecl<Type, boolean>>,
R extends boolean
T extends Record<string, MemberDecl>,
R extends boolean,
>(
MemberDeclCreator: <T extends Type>(options: {
comment?: string
@@ -1389,7 +1371,7 @@ export const NestedTranslationMap = <
type: T
}) => MemberDecl<T, R>,
entityName: Name,
type: ObjectType<T>
type: ObjectType<T>,
) =>
MemberDeclCreator({
comment: "All translations for the entry, identified by IETF language tag (BCP47).",
@@ -1400,3 +1382,59 @@ export const NestedTranslationMap = <
type,
}),
})
// /**
// * Any type that can be converted to a string via a `toString` method.
// */
// interface Stringable {
// toString(): string
// }
// type Whitespace = " " | "\n" | "\t" | "\r"
// /**
// * Removes leading and trailing whitespace from a string literal type.
// */
// type Trim<L extends string> = L extends `${Whitespace}${infer Rest}`
// ? Trim<Rest>
// : L extends `${infer Rest}${Whitespace}`
// ? Trim<Rest>
// : L
// /**
// * Maps the type function string to the actual TypeScript type.
// */
// type ActualType<F extends string> = F extends "number" | "integer"
// ? number
// : F extends "string"
// ? string
// : F extends "datetime" | "date" | "time"
// ? Date
// : Stringable
// /**
// * Makes the parameter types readable by removing any TypeScript-specific constructs.
// */
// type ReadableParams<P extends object> = {
// [K in keyof P]: P[K]
// }
// /**
// * Extracts parameter names and their types from a simple message string. It also works for complex messages, but is not designed for that.
// */
// type ParamsFromSimpleMessage<L extends string> = L extends `${infer Start}}${infer Rest}`
// ? Start extends `${string}{$${infer Name} :${infer TypeFunction}`
// ? { [K in Name]: ActualType<TypeFunction> } & Omit<ParamsFromSimpleMessage<Rest>, Name>
// : Start extends `${string}{$${infer Name}`
// ? ParamsFromSimpleMessage<Rest> extends { [K in Name]: Stringable }
// ? ParamsFromSimpleMessage<Rest>
// : { [K in Name]: Stringable } & ParamsFromSimpleMessage<Rest>
// : ParamsFromSimpleMessage<Rest>
// : object
// /**
// * Extracts parameter names and their types from an Unicode MessageFormat 2.0 (MF2) string.
// */
// export type ExtractParamsFromMessage<Msg extends string> = ReadableParams<
// ParamsFromSimpleMessage<Msg>
// >
+1 -1
View File
@@ -41,7 +41,7 @@ export const MetaCondition = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
+3 -3
View File
@@ -37,7 +37,7 @@ export const PactCategory = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
@@ -60,7 +60,7 @@ export const PactType = Entity(import.meta.url, {
comment: "The types name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
parentReferenceKey: "parent",
@@ -84,7 +84,7 @@ export const PactDomain = Entity(import.meta.url, {
comment: "The domains name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
parentReferenceKey: "parent",
+1 -1
View File
@@ -83,7 +83,7 @@ export const Patron = Entity(import.meta.url, {
comment: "The patrons name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
+1 -1
View File
@@ -20,7 +20,7 @@ export const PatronCategory = Entity(import.meta.url, {
comment: "The patron categorys name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
+1 -1
View File
@@ -50,7 +50,7 @@ export const PersonalityTrait = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
+24 -26
View File
@@ -70,7 +70,7 @@ export const Profession = Entity(import.meta.url, {
const [firstProfessionVersion, ...otherProfessionVersions] = getChildInstancesForInstanceId(
Profession.name,
instanceId,
ProfessionVersion.name
ProfessionVersion.name,
)
if (!firstProfessionVersion) {
@@ -87,7 +87,7 @@ export const Profession = Entity(import.meta.url, {
return {
...displayName,
name: `${displayName.name} (+${otherProfessionVersions.length} version${
name: `${displayName.name} (+${otherProfessionVersions.length.toString()} version${
otherProfessionVersions.length > 1 ? "s" : ""
})`,
}
@@ -132,11 +132,6 @@ export const ProfessionVersion = Entity(import.meta.url, {
comment: "The associated profession.",
type: ProfessionIdentifier(),
}),
prerequisites: Optional({
comment:
"Which prerequisites must be met to buy the stat block? For example, a character might need the advantage Spellcaster or Blessed. Note: the AP cost for a profession package does not include these prerequisites.",
type: IncludeIdentifier(ProfessionPrerequisites),
}),
package: Required({
comment:
"A list of available race variants where one has to be selected. If no variants are to be selected, a single variant with no name has to be provided which will be used as the missing values for the base race.",
@@ -146,14 +141,14 @@ export const ProfessionVersion = Entity(import.meta.url, {
comment: "A list of typical advantages.",
type: Array(
GenIncludeIdentifier(CommonnessRatedAdvantageDisadvantage, [AdvantageIdentifier()]),
{ minItems: 1 }
{ minItems: 1 },
),
}),
suggested_disadvantages: Optional({
comment: "A list of typical disadvantages.",
type: Array(
GenIncludeIdentifier(CommonnessRatedAdvantageDisadvantage, [DisadvantageIdentifier()]),
{ minItems: 1 }
{ minItems: 1 },
),
}),
unsuitable_advantages: Optional({
@@ -161,7 +156,7 @@ export const ProfessionVersion = Entity(import.meta.url, {
"A list of advantages that do not fit well with this profession; to be checked with the GM before taking any of them.",
type: Array(
GenIncludeIdentifier(CommonnessRatedAdvantageDisadvantage, [AdvantageIdentifier()]),
{ minItems: 1 }
{ minItems: 1 },
),
}),
unsuitable_disadvantages: Optional({
@@ -169,7 +164,7 @@ export const ProfessionVersion = Entity(import.meta.url, {
"A list of disadvantages that do not fit well with this profession; to be checked with the GM before taking any of them.",
type: Array(
GenIncludeIdentifier(CommonnessRatedAdvantageDisadvantage, [DisadvantageIdentifier()]),
{ minItems: 1 }
{ minItems: 1 },
),
}),
src,
@@ -207,7 +202,7 @@ export const ProfessionVersion = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
parentReferenceKey: "profession",
@@ -216,17 +211,15 @@ export const ProfessionVersion = Entity(import.meta.url, {
pathInLocaleMap: "name.default",
},
displayNameCustomizer: ({ instance, instanceId, locales }) => {
if (typeof instance.translations === "object" && instance.translations !== null) {
const translations = Object.entries(instance.translations)
for (const locale of locales) {
const translation = translations.find(([key]) => key === locale)
if (translation) {
const [, value] = translation
return {
name:
value.name.default + (value.specification ? ` (${value.specification.default})` : ""),
localeId: locale,
}
const translations = Object.entries(instance.translations)
for (const locale of locales) {
const translation = translations.find(([key]) => key === locale)
if (translation) {
const [, value] = translation
return {
name:
value.name.default + (value.specification ? ` (${value.specification.default})` : ""),
localeId: locale,
}
}
}
@@ -255,6 +248,11 @@ export const ProfessionPackage = Entity(import.meta.url, {
comment: "What does the professional package cost in adventure points?",
type: Integer({ minimum: 0 }),
}),
prerequisites: Optional({
comment:
"Which prerequisites must be met to buy the stat block? For example, a character might need the advantage Spellcaster or Blessed. Note: the AP cost for a profession package does not include these prerequisites.",
type: IncludeIdentifier(ProfessionPrerequisites),
}),
options: Optional({
comment:
"In some areas, the profession package grants a loose set of stats where the player must choose between different options for the profession package.",
@@ -377,7 +375,7 @@ export const ProfessionVariant = Entity(import.meta.url, {
comment: `A text that is appended to the generated text for the profession variant.`,
type: String({ minLength: 1 }),
}),
})
}),
),
}),
parentReferenceKey: "profession_package",
@@ -581,7 +579,7 @@ const ProfessionPackageOptions = TypeAlias(import.meta.url, {
type: IncludeIdentifier(SkillsOptions),
}),
},
{ minProperties: 1 }
{ minProperties: 1 },
),
})
@@ -622,7 +620,7 @@ const ProfessionVariantPackageOptions = TypeAlias(import.meta.url, {
type: GenIncludeIdentifier(VariantOptionAction, [IncludeIdentifier(SkillsOptions)]),
}),
},
{ minProperties: 1 }
{ minProperties: 1 },
),
})
+1 -1
View File
@@ -20,7 +20,7 @@ export const Property = Entity(import.meta.url, {
comment: "The propertys name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
+9 -9
View File
@@ -57,14 +57,14 @@ export const Race = Entity(import.meta.url, {
comment: "A list of strongly recommended advantages.",
type: Array(
GenIncludeIdentifier(CommonnessRatedAdvantageDisadvantage, [AdvantageIdentifier()]),
{ minItems: 1 }
{ minItems: 1 },
),
}),
strongly_recommended_disadvantages: Optional({
comment: "A list of strongly recommended disadvantages.",
type: Array(
GenIncludeIdentifier(CommonnessRatedAdvantageDisadvantage, [DisadvantageIdentifier()]),
{ minItems: 1 }
{ minItems: 1 },
),
}),
weight: Required({
@@ -125,7 +125,7 @@ export const Race = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
@@ -174,7 +174,7 @@ The array only permits a single entry because no race specified more than one se
}),
}),
},
{ minProperties: 1 }
{ minProperties: 1 },
),
})
@@ -291,28 +291,28 @@ export const RaceVariant = Entity(import.meta.url, {
comment: "A list of common advantages.",
type: Array(
GenIncludeIdentifier(CommonnessRatedAdvantageDisadvantage, [AdvantageIdentifier()]),
{ minItems: 1 }
{ minItems: 1 },
),
}),
common_disadvantages: Optional({
comment: "A list of common disadvantages.",
type: Array(
GenIncludeIdentifier(CommonnessRatedAdvantageDisadvantage, [DisadvantageIdentifier()]),
{ minItems: 1 }
{ minItems: 1 },
),
}),
uncommon_advantages: Optional({
comment: "A list of uncommon advantages.",
type: Array(
GenIncludeIdentifier(CommonnessRatedAdvantageDisadvantage, [AdvantageIdentifier()]),
{ minItems: 1 }
{ minItems: 1 },
),
}),
uncommon_disadvantages: Optional({
comment: "A list of uncommon disadvantages.",
type: Array(
GenIncludeIdentifier(CommonnessRatedAdvantageDisadvantage, [DisadvantageIdentifier()]),
{ minItems: 1 }
{ minItems: 1 },
),
}),
hair_color: Optional({
@@ -354,7 +354,7 @@ export const RaceVariant = Entity(import.meta.url, {
comment: "The respective uncommon disadvantages text from the source book.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
parentReferenceKey: "race",
+1 -1
View File
@@ -14,7 +14,7 @@ export const Region = Entity(import.meta.url, {
comment: "The regions name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
+1 -1
View File
@@ -95,7 +95,7 @@ export const Ritual = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
enhancements: Required({
type: ChildEntities(Enhancement),
+1 -1
View File
@@ -41,7 +41,7 @@ export const Service = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
+1 -1
View File
@@ -33,7 +33,7 @@ export const SexPractice = Entity(import.meta.url, {
comment: "Effects of a failed *Seduction* check.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
+2 -2
View File
@@ -89,7 +89,7 @@ export const Skill = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
@@ -138,7 +138,7 @@ export const SkillApplication = Entity(import.meta.url, {
comment: "The skill applications name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
parentReferenceKey: "parent",
+1 -1
View File
@@ -23,7 +23,7 @@ export const SkillGroup = Entity(import.meta.url, {
comment: "The skill groups long name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
+7 -9
View File
@@ -45,19 +45,17 @@ export const SkillModificationLevel = Entity(import.meta.url, {
type: IncludeIdentifier(LevelTypeConfigTranslation),
}),
},
{ minProperties: 1 }
)
{ minProperties: 1 },
),
),
}),
displayName: null,
displayNameCustomizer: ({ instance, locales }) => ({
name: `${(instance.fast as any).casting_time} Act. / ${
(instance.slow as any).casting_time.value
} ${(instance.slow as any).casting_time.unit.kind} (fast/slow) — ${
(instance.fast as any).range === (instance.slow as any).range
? (instance.fast as any).range
: (instance.fast as any).range + "/" + (instance.slow as any).range + " (fast/slow)"
} m — ${(instance.fast as any).cost}/${(instance.slow as any).cost} (fast/slow) AE or KP`,
name: `${instance.fast.casting_time.toString()} Act. / ${instance.slow.casting_time.value.toString()} ${instance.slow.casting_time.unit.kind} (fast/slow) — ${
instance.fast.range === instance.slow.range
? instance.fast.range.toString()
: `${instance.fast.range.toString()}/${instance.slow.range.toString()} (fast/slow)`
} m — ${instance.fast.cost.toString()}/${instance.slow.cost.toString()} (fast/slow) AE or KP`,
localeId: locales[0],
}),
})
+1 -1
View File
@@ -19,7 +19,7 @@ export const SocialStatus = Entity(import.meta.url, {
comment: "The social status name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
+1 -1
View File
@@ -95,7 +95,7 @@ export const Spell = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
enhancements: Required({
type: ChildEntities(Enhancement),
+1 -1
View File
@@ -24,7 +24,7 @@ export const State = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
+1 -1
View File
@@ -53,7 +53,7 @@ export const Talisman = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
+1 -1
View File
@@ -19,7 +19,7 @@ export const TargetCategory = Entity(import.meta.url, {
comment: "The target categorys name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
+1 -1751
View File
File diff suppressed because it is too large Load Diff
+128
View File
@@ -0,0 +1,128 @@
import {
Array,
Boolean,
EnumCase,
GenEnum,
GenIncludeIdentifier,
GenTypeAlias,
IncludeIdentifier,
Object,
Optional,
Param,
Required,
type Type,
TypeArgument,
} from "tsondb/schema/def"
import { MagicalTraditionIdentifier, PatronIdentifier } from "./_Identifier.js"
import { AdvancedSpecialAbilityRestrictedOptionIdentifier } from "./_IdentifierGroup.js"
import { DisplayOption } from "./prerequisites/DisplayOption.js"
const AdvancedSpecialAbility = GenEnum(import.meta.url, {
name: "AdvancedSpecialAbility",
comment: "A reference to an advanced special ability.",
parameters: [Param("Identifier")],
values: Identifier => ({
General: EnumCase({ type: TypeArgument(Identifier) }),
RestrictOptions: EnumCase({
type: GenIncludeIdentifier(RestrictAdvancedSpecialAbilityOptions, [TypeArgument(Identifier)]),
}),
OneOf: EnumCase({
type: GenIncludeIdentifier(OneOfAdvancedSpecialAbilityOptions, [TypeArgument(Identifier)]),
}),
DeriveFromExternalOption: EnumCase({
type: GenIncludeIdentifier(AdvancedSpecialAbilityDerivedFromExternalOption, [
TypeArgument(Identifier),
]),
}),
}),
})
const RestrictAdvancedSpecialAbilityOptions = GenTypeAlias(import.meta.url, {
name: "RestrictAdvancedSpecialAbilityOptions",
parameters: [Param("Identifier")],
type: Identifier =>
Object({
id: Required({
comment: "The advanced special abilitys identifier.",
type: TypeArgument(Identifier),
}),
option: Required({
comment:
"Specify the select option(s) that only are allowed for the referenced advanced special ability; others are disallowed.",
type: Array(IncludeIdentifier(AdvancedSpecialAbilityRestrictedOptionIdentifier), {
minItems: 1,
}),
}),
}),
})
const OneOfAdvancedSpecialAbilityOptions = GenTypeAlias(import.meta.url, {
name: "OneOfAdvancedSpecialAbilityOptions",
parameters: [Param("Identifier")],
type: Identifier =>
Object({
options: Required({
comment: "The possible advanced special abilities.",
type: Array(TypeArgument(Identifier), { minItems: 2 }),
}),
is_selection_required_on_purchase: Required({
comment:
"Do have to choose the advanced special ability when buying the style special ability? Otherwise the decision can be made later.",
type: Boolean(),
}),
display_option: Optional({
type: IncludeIdentifier(DisplayOption),
}),
}),
})
const AdvancedSpecialAbilityDerivedFromExternalOption = GenTypeAlias(import.meta.url, {
name: "AdvancedSpecialAbilityDerivedFromExternalOption",
parameters: [Param("Identifier")],
type: Identifier =>
Object({
external_entry: Required({
comment: "The possible advanced special abilities.",
type: MagicalTraditionIdentifier(),
}),
map: Required({
comment: "Map options from the external entry to allowed advanced special abilities.",
type: Array(
GenIncludeIdentifier(AdvancedSpecialAbilityDerivedFromExternalOptionMapping, [
TypeArgument(Identifier),
]),
{ minItems: 1 },
),
}),
display_option: Optional({
type: IncludeIdentifier(DisplayOption),
}),
}),
})
const AdvancedSpecialAbilityDerivedFromExternalOptionMapping = GenTypeAlias(import.meta.url, {
name: "AdvancedSpecialAbilityDerivedFromExternalOptionMapping",
parameters: [Param("Identifier")],
comment: "Mapping from an option of the external entry to an allowed advanced special ability.",
type: Identifier =>
Object({
from_option: Required({
comment: "The select options identifier.",
type: PatronIdentifier(),
}),
to_advanced: Required({
comment: "The advanced special abilitys identifier.",
type: TypeArgument(Identifier),
}),
}),
})
export const advanced = (identifier: Type) =>
Required({
comment:
"The Advanced Special Abilities for the respective Style Special Ability. Sometimes, only a specific select option or a set of select options of an entry is allowed, which can be modelled by the option property. It can also be that you can choose from a set of special abilities, but then you cant specify an option.",
type: Array(GenIncludeIdentifier(AdvancedSpecialAbility, [identifier]), {
minItems: 3,
maxItems: 3,
}),
})
@@ -0,0 +1,186 @@
import {
ArrayType as Array,
EnumDecl as Enum,
EnumCaseDecl as EnumCase,
GenIncludeIdentifierType as GenIncludeIdentifier,
IncludeIdentifier,
IntegerType as Integer,
ObjectType as Object,
Optional,
Required,
String,
TypeAlias,
} from "tsondb/schema/def"
import { ActivatableIdentifier } from "./_IdentifierGroup.js"
import { MathOperation } from "./_MathExpression.js"
import { BySizeCategory } from "./_SizeCategory.js"
import { NestedTranslationMap } from "./Locale.js"
const AdventurePointsValue = Enum(import.meta.url, {
name: "AdventurePointsValue",
values: () => ({
Fixed: EnumCase({ type: IncludeIdentifier(FixedAdventurePointsValue) }),
ByLevel: EnumCase({ type: IncludeIdentifier(AdventurePointsValueByLevel) }),
DerivedFromSelection: EnumCase({
type: IncludeIdentifier(AdventurePointsDerivedFromSelection),
}),
DependingOnActive: EnumCase({
type: IncludeIdentifier(AdventurePointsDependingOnActive),
}),
DependingOnActiveInstances: EnumCase({
type: IncludeIdentifier(AdventurePointsDependingOnActiveInstances),
}),
Indefinite: EnumCase({ type: null }),
}),
})
export const ap_value = Required({
comment: "The adventure points value.",
type: IncludeIdentifier(AdventurePointsValue),
})
export const ap_value_l10n = Optional({
comment: "The adventure points value.",
isDeprecated: true,
type: String({ minLength: 1 }),
})
export const ap_value_append = Optional({
comment: "The string to append to the generated AP value text.",
isDeprecated: true,
type: String({ minLength: 1 }),
})
export const FixedAdventurePointsValue = TypeAlias(import.meta.url, {
name: "FixedAdventurePointsValue",
comment:
"A fixed adventure points value. If the entry has levels, this is the cost per level as well.",
type: () => IncludeIdentifier(AdventurePointsSingleValue),
})
const AdventurePointsValueByLevel = TypeAlias(import.meta.url, {
name: "AdventurePointsValueByLevel",
comment: "An entry with levels may have different costs for each level.",
type: () =>
Object({
list: Required({
comment:
"The list of adventure point values, where the position in the list corresponds to the level (i.e. the first value is for level I, the second value is for level II, and so on). The length of the list must match the amount of levels the special ability has.",
type: Array(IncludeIdentifier(AdventurePointsSingleValue), { minItems: 2 }),
}),
additionalBySizeCategory: Optional({
comment:
"Values may be added to each level based on the size category of the character. Negative values mean that the AP value is lowered.",
type: GenIncludeIdentifier(BySizeCategory, [Integer()]),
}),
}),
})
const AdventurePointsDerivedFromSelection = TypeAlias(import.meta.url, {
name: "AdventurePointsDerivedFromSelection",
comment:
"The adventure points value is derived from the selection of the special ability. Its display value may be able to be derived from the given information for the select options. If that is not the case or the generated text would not match the original one, a replacement text can be provided.",
type: () =>
Object({
translations: NestedTranslationMap(
Optional,
"AdventurePointsDerivedFromSelection",
Object(
{
replacement: Optional({
comment:
"A replacement for the generated text if it would not match the original one.",
type: String({ minLength: 1, isMarkdown: true }),
}),
},
{ minProperties: 1 },
),
),
}),
})
const AdventurePointsSingleValue = TypeAlias(import.meta.url, {
name: "AdventurePointsSingleValue",
comment: "A single adventure points value.",
type: () => Integer({ minimum: 0 }),
})
export const AdventurePointsDependingOnActive = TypeAlias(import.meta.url, {
name: "AdventurePointsDependingOnActive",
comment: "The adventure points cost depends on if an instance of an entry is active.",
type: () =>
Object({
id: Required({
comment: "The identifier of the entry to that decides which AP value to pick.",
type: IncludeIdentifier(ActivatableIdentifier),
}),
active: Required({
comment: "The adventure points value if an instance of the referenced entry is active.",
type: IncludeIdentifier(AdventurePointsSingleValue),
}),
inactive: Required({
comment: "The adventure points value if no instance of the referenced entry is active.",
type: IncludeIdentifier(AdventurePointsSingleValue),
}),
}),
})
export const AdventurePointsDependingOnActiveInstances = Enum(import.meta.url, {
name: "AdventurePointsDependingOnActiveInstances",
comment:
"The adventure points cost depends on how many instances of the entry are already active.",
values: () => ({
Threshold: EnumCase({
type: IncludeIdentifier(AdventurePointsDependingOnActiveInstancesThreshold),
}),
Expression: EnumCase({
type: IncludeIdentifier(AdventurePointsDependingOnActiveInstancesExpression),
}),
}),
})
const AdventurePointsDependingOnActiveInstancesThreshold = TypeAlias(import.meta.url, {
name: "AdventurePointsDependingOnActiveInstancesThreshold",
comment:
"The adventure points cost depends on how many instances of the entry are already active.",
type: () =>
Object({
normal: Required({
comment: "The normal adventure points value.",
type: Integer({ minimum: 0 }),
}),
threshold: Required({
comment: "The number of active instances **after** which the alternative cost applies.",
type: Integer({ minimum: 1 }),
}),
alternative: Required({
comment: "The alternative adventure points value.",
type: Integer({ minimum: 0 }),
}),
}),
})
const AdventurePointsDependingOnActiveInstancesExpression = TypeAlias(import.meta.url, {
name: "AdventurePointsDependingOnActiveInstancesExpression",
comment:
"The adventure points cost depends on how many instances of the entry are already active.",
type: () =>
GenIncludeIdentifier(MathOperation, [
IncludeIdentifier(AdventurePointsDependingOnActiveInstancesExpressionValue),
]),
})
const AdventurePointsDependingOnActiveInstancesExpressionValue = Enum(import.meta.url, {
name: "AdventurePointsDependingOnActiveInstancesExpressionValue",
comment: `The type of value allowed in the expression to calculate the AP value.`,
values: () => ({
Constant: EnumCase({
comment: "A constant integer value.",
type: Integer(),
}),
Active: EnumCase({
comment: "The number of active instances.",
type: null,
}),
}),
})
+89
View File
@@ -0,0 +1,89 @@
import {
Array,
Boolean,
Enum,
EnumCase,
IncludeIdentifier,
Integer,
Object,
Optional,
Required,
TypeAlias,
} from "tsondb/schema/def"
import { ActivatableIdentifier } from "./_IdentifierGroup.js"
const AutomaticEntry = TypeAlias(import.meta.url, {
name: "AutomaticEntry",
type: () =>
Object({
action: Required({
comment: "What type of action is applied to the target entry?",
type: IncludeIdentifier(AutomaticEntryAction),
}),
apply_ap_value: Required({
comment:
"If an entry is added or removed, does is cost or grant adventure points or is it free of charge?",
type: Boolean(),
}),
target: Required({
comment:
"The entry that is to be added or removed. It can be a fixed entry or a selection where the player must choose one entry.",
type: IncludeIdentifier(AutomaticEntryTarget),
}),
}),
})
export const automatic_entries = Optional({
comment:
"This entry has a direct influence on the existence of other entries. It may add or remove entries.",
type: Array(IncludeIdentifier(AutomaticEntry), { minItems: 1 }),
})
const AutomaticEntryAction = Enum(import.meta.url, {
name: "AutomaticEntryAction",
values: () => ({
Add: EnumCase({ type: null }),
Remove: EnumCase({ type: null }),
}),
})
const AutomaticEntryTarget = Enum(import.meta.url, {
name: "AutomaticEntryTarget",
values: () => ({
Selection: EnumCase({ type: IncludeIdentifier(AutomaticEntryTargetSelection) }),
Fixed: EnumCase({ type: IncludeIdentifier(FixedAutomaticEntryTarget) }),
}),
})
const AutomaticEntryTargetSelection = TypeAlias(import.meta.url, {
name: "AutomaticEntryTargetSelection",
type: () =>
Object({
list: Required({
type: IncludeIdentifier(AutomaticEntryTargetSelectionList),
}),
}),
})
const AutomaticEntryTargetSelectionList = Enum(import.meta.url, {
name: "AutomaticEntryTargetSelectionList",
values: () => ({
MagicalTraditions: EnumCase({ type: null }),
MagicalDilettanteTraditions: EnumCase({ type: null }),
}),
})
const FixedAutomaticEntryTarget = TypeAlias(import.meta.url, {
name: "FixedAutomaticEntryTarget",
type: () =>
Object({
id: Required({
type: IncludeIdentifier(ActivatableIdentifier),
}),
level: Optional({
comment:
"The level of the target entry to be added or removed. If this is omitted for a multi-level entry, level 1 is assumed.",
type: Integer({ minimum: 1 }),
}),
}),
})
+450
View File
@@ -0,0 +1,450 @@
import {
Array,
Boolean,
Enum,
EnumCase,
GenIncludeIdentifier,
GenTypeAlias,
IncludeIdentifier,
Integer,
Object,
Optional,
Param,
Required,
String,
TypeAlias,
TypeArgument,
} from "tsondb/schema/def"
import {
CloseCombatTechniqueIdentifier,
RaceIdentifier,
RangedCombatTechniqueIdentifier,
WeaponIdentifier,
} from "./_Identifier.js"
import {
CombatRelatedSpecialAbilityIdentifier,
CombatTechniqueIdentifier,
} from "./_IdentifierGroup.js"
const CombatSpecialAbilityUsageType = Enum(import.meta.url, {
name: "CombatSpecialAbilityUsageType",
comment: "The definition of how the combat special ability can be used in combat.",
values: () => ({
Passive: EnumCase({ type: null }),
BasicManeuver: EnumCase({ type: null }),
SpecialManeuver: EnumCase({ type: null }),
}),
})
export const usage_type = Required({
comment: "The definition of how the combat special ability can be used in combat.",
type: IncludeIdentifier(CombatSpecialAbilityUsageType),
})
const CombatSpecialAbilityType = Enum(import.meta.url, {
name: "CombatSpecialAbilityType",
comment:
"The definition of if the combat special ability can be used when armed or when unarmed.",
values: () => ({
Armed: EnumCase({ type: null }),
Unarmed: EnumCase({ type: null }),
}),
})
export const type = Required({
comment:
"The definition of if the combat special ability can be used when armed or when unarmed.",
type: IncludeIdentifier(CombatSpecialAbilityType),
})
const Penalty = Enum(import.meta.url, {
name: "Penalty",
comment: "The penalty the special ability gives when used.",
values: () => ({
Single: EnumCase({ type: IncludeIdentifier(SinglePenalty) }),
ByHandedness: EnumCase({ type: IncludeIdentifier(PenaltyByHandedness) }),
ByActivation: EnumCase({ type: IncludeIdentifier(PenaltyByActivation) }),
Selection: EnumCase({ type: IncludeIdentifier(PenaltySelection) }),
ByLevel: EnumCase({ type: IncludeIdentifier(PenaltyByLevel) }),
ByAttack: EnumCase({ type: IncludeIdentifier(PenaltyByAttack) }),
DependsOnHitZone: EnumCase({ type: null }),
}),
})
export const penalty = Optional({
comment: "The penalty the special ability gives when used.",
type: IncludeIdentifier(Penalty),
})
export const penalty_l10n = Optional({
comment: "The penalty the special ability gives when used.",
isDeprecated: true,
type: String({ minLength: 1 }),
})
const SinglePenalty = TypeAlias(import.meta.url, {
name: "SinglePenalty",
type: () =>
Object({
value: Required({
comment: "The penalty value.",
type: Integer(),
}),
applies_to_parry: Optional({
comment: "Set to `true` if the penalty applies to the parry instead of the attack.",
type: Boolean(),
}),
}),
})
const PenaltyByHandedness = TypeAlias(import.meta.url, {
name: "PenaltyByHandedness",
type: () =>
Object({
one_handed: Required({
comment: "The penalty value for one-handed weapons.",
type: Integer(),
}),
two_handed: Required({
comment: "The penalty value for two-handed weapons.",
type: Integer(),
}),
applies_to_parry: Optional({
comment: "Set to `true` if the penalty applies to the parry instead of the attack.",
type: Boolean(),
}),
}),
})
const PenaltyByActivation = TypeAlias(import.meta.url, {
name: "PenaltyByActivation",
type: () =>
Object({
active: Required({
comment: "The penalty value if the entry has been bought by the character.",
type: Integer(),
}),
inactive: Required({
comment: "The penalty value if the entry has not been bought by the character.",
type: Integer(),
}),
applies_to_parry: Optional({
comment: "Set to `true` if the penalty applies to the parry instead of the attack.",
type: Boolean(),
}),
}),
})
const PenaltySelection = TypeAlias(import.meta.url, {
name: "PenaltySelection",
type: () =>
Object({
options: Required({
type: IncludeIdentifier(PenaltySelectionOptions),
}),
}),
})
const PenaltySelectionOptions = Enum(import.meta.url, {
name: "PenaltySelectionOptions",
values: () => ({
Specific: EnumCase({ type: IncludeIdentifier(SpecificPenaltySelectionOptions) }),
Range: EnumCase({ type: IncludeIdentifier(PenaltySelectionOptionsRange) }),
}),
})
const SpecificPenaltySelectionOptions = TypeAlias(import.meta.url, {
name: "SpecificPenaltySelectionOptions",
type: () =>
Object({
list: Required({
comment: "The list of specific penalty options.",
type: Array(IncludeIdentifier(SpecificPenaltySelectionOption), { minItems: 2 }),
}),
}),
})
const SpecificPenaltySelectionOption = TypeAlias(import.meta.url, {
name: "SpecificPenaltySelectionOption",
type: () =>
Object({
value: Required({
comment: "The penalty value.",
type: Integer(),
}),
}),
})
const PenaltySelectionOptionsRange = TypeAlias(import.meta.url, {
name: "PenaltySelectionOptionsRange",
type: () =>
Object({
minimum: Required({
comment: "The minimum penalty value.",
type: Integer(),
}),
maximum: Required({
comment: "The maximum penalty value.",
type: Integer(),
}),
}),
})
const PenaltyByLevel = TypeAlias(import.meta.url, {
name: "PenaltyByLevel",
type: () =>
Object({
levels: Required({
comment:
"A continuous range of penalties for each level. The first element is the penalty for the first level, the second element is the penalty for the second level, and so on.",
type: Array(IncludeIdentifier(PenaltyByLevelLevel), { minItems: 2 }),
}),
external: Optional({
comment:
"The combat-related special ability of which the level defines the penalty instead.",
type: IncludeIdentifier(PenaltyByExternalLevel),
}),
}),
})
const PenaltyByLevelLevel = TypeAlias(import.meta.url, {
name: "PenaltyByLevelLevel",
type: () =>
Object({
value: Required({
comment: "The penalty value for this level.",
type: Integer(),
}),
}),
})
const PenaltyByExternalLevel = TypeAlias(import.meta.url, {
name: "PenaltyByExternalLevel",
comment: "The combat-related special ability of which the level defines the penalty instead.",
type: () =>
Object({
id: Required({
comment:
"The identifier of the combat-related special ability of which the level defines the penalty instead.",
type: IncludeIdentifier(CombatRelatedSpecialAbilityIdentifier),
}),
}),
})
const PenaltyByAttack = TypeAlias(import.meta.url, {
name: "PenaltyByAttack",
type: () =>
Object({
list: Required({
comment:
"A list of penalties for subsequent attacks. The first element is the penalty for the first attack, the second element is the penalty for the second attack, and so on. The order of the first element may be changed using `initial_order`, so that e.g. if set to `2`, the first element is the penalty for the second attack, the second element is the penalty for the third attack, and so on.",
type: Array(IncludeIdentifier(PenaltyByAttackOrderItem), { minItems: 1 }),
}),
initial_order: Optional({
comment: "The order of the first element in the `list` of penalties.",
type: Integer(),
}),
attack_replacement: Optional({
comment:
"Set if a predefined different word should be used instead of the word `attack` for display purposes.",
type: IncludeIdentifier(PenaltyByAttackReplacement),
}),
}),
})
const PenaltyByAttackOrderItem = TypeAlias(import.meta.url, {
name: "PenaltyByAttackOrderItem",
type: () =>
Object({
value: Required({
comment: "The penalty value for this order.",
type: Integer(),
}),
}),
})
const PenaltyByAttackReplacement = Enum(import.meta.url, {
name: "PenaltyByAttackReplacement",
comment:
"Set if a predefined different word should be used instead of the word `attack` for display purposes.",
values: () => ({
Throw: EnumCase({ type: null }),
}),
})
const ApplicableCombatTechniques = Enum(import.meta.url, {
name: "ApplicableCombatTechniques",
values: () => ({
None: EnumCase({ type: null }),
DependingOnCombatStyle: EnumCase({ type: null }),
All: EnumCase({ type: IncludeIdentifier(AllApplicableCombatTechniques) }),
AllClose: EnumCase({ type: IncludeIdentifier(AllApplicableCloseCombatTechniques) }),
AllRanged: EnumCase({ type: IncludeIdentifier(AllApplicableRangedCombatTechniques) }),
Specific: EnumCase({ type: IncludeIdentifier(SpecificApplicableCombatTechniques) }),
}),
})
export const combat_techniques = Required({
comment: "The combat techniques the special ability is applicable to.",
type: IncludeIdentifier(ApplicableCombatTechniques),
})
const AllApplicableCombatTechniques = TypeAlias(import.meta.url, {
name: "AllApplicableCombatTechniques",
type: () =>
Object({
restrictions: Optional({
type: Array(IncludeIdentifier(ApplicableAllCombatTechniquesRestriction), { minItems: 1 }),
}),
}),
})
const AllApplicableCloseCombatTechniques = TypeAlias(import.meta.url, {
name: "AllApplicableCloseCombatTechniques",
type: () =>
Object({
restrictions: Optional({
type: Array(IncludeIdentifier(ApplicableCloseCombatTechniquesRestriction), { minItems: 1 }),
}),
}),
})
const AllApplicableRangedCombatTechniques = TypeAlias(import.meta.url, {
name: "AllApplicableRangedCombatTechniques",
type: () =>
Object({
restrictions: Optional({
type: Array(IncludeIdentifier(ApplicableRangedCombatTechniquesRestriction), {
minItems: 1,
}),
}),
}),
})
const SpecificApplicableCombatTechniques = TypeAlias(import.meta.url, {
name: "SpecificApplicableCombatTechniques",
type: () =>
Object({
list: Required({
type: Array(IncludeIdentifier(SpecificApplicableCombatTechnique), { minItems: 1 }),
}),
}),
})
const SpecificApplicableCombatTechnique = TypeAlias(import.meta.url, {
name: "SpecificApplicableCombatTechnique",
type: () =>
Object({
id: Required({
type: IncludeIdentifier(CombatTechniqueIdentifier),
}),
restrictions: Optional({
type: Array(IncludeIdentifier(ApplicableSpecificCombatTechniquesRestriction), {
minItems: 1,
}),
}),
}),
})
const ApplicableAllCombatTechniquesRestriction = Enum(import.meta.url, {
name: "ApplicableAllCombatTechniquesRestriction",
values: () => ({
Improvised: EnumCase({ type: null }),
PointedBlade: EnumCase({ type: null }),
Mount: EnumCase({ type: null }),
Race: EnumCase({ type: IncludeIdentifier(ApplicableCombatTechniquesRaceRestriction) }),
ExcludeCombatTechniques: EnumCase({
type: GenIncludeIdentifier(ApplicableCombatTechniquesNegativeCombatTechniquesRestriction, [
IncludeIdentifier(CombatTechniqueIdentifier),
]),
}),
}),
})
const ApplicableCloseCombatTechniquesRestriction = Enum(import.meta.url, {
name: "ApplicableCloseCombatTechniquesRestriction",
values: () => ({
Improvised: EnumCase({ type: null }),
PointedBlade: EnumCase({ type: null }),
Mount: EnumCase({ type: null }),
HasParry: EnumCase({ type: null }),
OneHanded: EnumCase({ type: null }),
TwoHanded: EnumCase({ type: null }),
ParryingWeapon: EnumCase({ type: null }),
Race: EnumCase({ type: IncludeIdentifier(ApplicableCombatTechniquesRaceRestriction) }),
ExcludeCombatTechniques: EnumCase({
type: GenIncludeIdentifier(ApplicableCombatTechniquesNegativeCombatTechniquesRestriction, [
CloseCombatTechniqueIdentifier(),
]),
}),
}),
})
const ApplicableRangedCombatTechniquesRestriction = Enum(import.meta.url, {
name: "ApplicableRangedCombatTechniquesRestriction",
values: () => ({
Improvised: EnumCase({ type: null }),
PointedBlade: EnumCase({ type: null }),
Mount: EnumCase({ type: null }),
Race: EnumCase({ type: IncludeIdentifier(ApplicableCombatTechniquesRaceRestriction) }),
ExcludeCombatTechniques: EnumCase({
type: GenIncludeIdentifier(ApplicableCombatTechniquesNegativeCombatTechniquesRestriction, [
RangedCombatTechniqueIdentifier(),
]),
}),
}),
})
const ApplicableSpecificCombatTechniquesRestriction = Enum(import.meta.url, {
name: "ApplicableSpecificCombatTechniquesRestriction",
values: () => ({
Improvised: EnumCase({ type: null }),
PointedBlade: EnumCase({ type: null }),
Mount: EnumCase({ type: null }),
Race: EnumCase({ type: IncludeIdentifier(ApplicableCombatTechniquesRaceRestriction) }),
Level: EnumCase({ type: IncludeIdentifier(ApplicableCombatTechniquesLevelRestriction) }),
Weapons: EnumCase({ type: IncludeIdentifier(ApplicableCombatTechniquesWeaponRestriction) }),
}),
})
const ApplicableCombatTechniquesNegativeCombatTechniquesRestriction = GenTypeAlias(
import.meta.url,
{
name: "ApplicableCombatTechniquesNegativeCombatTechniquesRestriction",
parameters: [Param("Ref")],
type: Ref =>
Object({
list: Required({
comment: "The combat techniques this combat special ability is **not** applicable to.",
type: Array(TypeArgument(Ref), { minItems: 1 }),
}),
}),
},
)
const ApplicableCombatTechniquesRaceRestriction = TypeAlias(import.meta.url, {
name: "ApplicableCombatTechniquesRaceRestriction",
type: () => RaceIdentifier(),
})
const ApplicableCombatTechniquesLevelRestriction = TypeAlias(import.meta.url, {
name: "ApplicableCombatTechniquesLevelRestriction",
type: () =>
Object({
level: Required({
comment: "The combat special ability is only applicable on a certain level.",
type: Integer({ minimum: 1 }),
}),
}),
})
const ApplicableCombatTechniquesWeaponRestriction = TypeAlias(import.meta.url, {
name: "ApplicableCombatTechniquesWeaponRestriction",
type: () =>
Object({
list: Required({
comment: "The specific weapons this combat special ability is only applicable to.",
type: Array(WeaponIdentifier(), { minItems: 1 }),
}),
}),
})
+628
View File
@@ -0,0 +1,628 @@
import {
Array,
Boolean,
Enum,
EnumCase,
IncludeIdentifier,
Integer,
Object,
Optional,
Required,
String,
TypeAlias,
} from "tsondb/schema/def"
import { NestedTranslationMap } from "./Locale.js"
import { DurationUnitValue } from "./_ActivatableSkillDuration.js"
import { AspectIdentifier, PropertyIdentifier } from "./_Identifier.js"
import { VolumePointsOptionReferenceIdentifier } from "./_IdentifierGroup.js"
import { ResponsiveText, ResponsiveTextOptional } from "./_ResponsiveText.js"
const EnchantmentCost = Enum(import.meta.url, {
name: "EnchantmentCost",
values: () => ({
ArcaneEnergyCost: EnumCase({ type: IncludeIdentifier(ArcaneEnergyCost) }),
BindingCost: EnumCase({ type: IncludeIdentifier(BindingCost) }),
}),
})
export const cost = Optional({
comment: "The cost when casting the entry.",
type: IncludeIdentifier(EnchantmentCost),
})
export const ArcaneEnergyCost = Enum(import.meta.url, {
name: "ArcaneEnergyCost",
comment: "The AE cost.",
values: () => ({
Fixed: EnumCase({ type: IncludeIdentifier(FixedArcaneEnergyCost) }),
PerCountable: EnumCase({ type: IncludeIdentifier(ArcaneEnergyCostPerCountable) }),
Interval: EnumCase({ type: IncludeIdentifier(IntervalArcaneEnergyCost) }),
ActivationAndHalfInterval: EnumCase({
type: IncludeIdentifier(ActivationAndHalfIntervalArcaneEnergyCost),
}),
Indefinite: EnumCase({ type: IncludeIdentifier(IndefiniteArcaneEnergyCost) }),
Disjunction: EnumCase({ type: IncludeIdentifier(ArcaneEnergyCostDisjunction) }),
Variable: EnumCase({ type: null }),
}),
})
export const aeCost = Optional({
comment: "The AE cost.",
isDeprecated: true,
type: String({ minLength: 1 }),
})
const FixedArcaneEnergyCost = TypeAlias(import.meta.url, {
name: "FixedArcaneEnergyCost",
type: () =>
Object({
value: Required({
comment: "The AE cost value.",
type: Integer({ minimum: 1 }),
}),
is_permanent: Optional({
comment: "Set to `true` if the AE costs are permanent.",
type: Boolean(),
}),
interval: Optional({
comment: "Specified if the AE cost `value` has to be paid for each time interval.",
type: IncludeIdentifier(DurationUnitValue),
}),
per_level: Optional({
comment:
"The AE cost are per level of the enchantment. It may either be displayed in a compressed way (e.g. `1 AE per level`) or in a verbose way (e.g. `1 AE for level I; 2 AE for level II`).",
type: IncludeIdentifier(FixedArcaneEnergyCostPerLevel),
}),
translations: NestedTranslationMap(
Optional,
"FixedArcaneEnergyCost",
Object(
{
note: Optional({
comment: "A note, appended to the generated option string in parenthesis.",
type: IncludeIdentifier(ResponsiveTextOptional),
}),
},
{ minProperties: 1 },
),
),
}),
})
const FixedArcaneEnergyCostPerLevel = Enum(import.meta.url, {
name: "FixedArcaneEnergyCostPerLevel",
comment:
"The AE cost are per level of the enchantment. It may either be displayed in a compressed way (e.g. `1 AE per level`) or in a verbose way (e.g. `1 AE for level I; 2 AE for level II`).",
values: () => ({
Compressed: EnumCase({ type: null }),
Verbose: EnumCase({ type: null }),
}),
})
const ArcaneEnergyCostPerCountable = TypeAlias(import.meta.url, {
name: "ArcaneEnergyCostPerCountable",
type: () =>
Object({
value: Required({
comment: "The AE cost value that has to be per a specific countable entity.",
type: Integer({ minimum: 1 }),
}),
base_value: Optional({
comment:
"If defined, in addition to the cost per entity you have to pay a flat amount, regardless of the entity count.",
type: Integer({ minimum: 1 }),
}),
translations: NestedTranslationMap(
Required,
"ArcaneEnergyCostPerCountable",
Object({
per: Required({
comment: "The cost have to be per a specific countable entity, e.g. `8 AE per person`.",
type: IncludeIdentifier(ResponsiveText),
}),
note: Optional({
comment: "A note, appended to the generated string in parenthesis.",
type: IncludeIdentifier(ResponsiveTextOptional),
}),
}),
),
}),
})
const IntervalArcaneEnergyCost = TypeAlias(import.meta.url, {
name: "IntervalArcaneEnergyCost",
type: () =>
Object({
value: Required({
comment: "The AE cost value that has to be payed each interval.",
type: Integer({ minimum: 1 }),
}),
interval: Required({
comment: "The time interval for which the AE cost `value` has to be paid.",
type: IncludeIdentifier(DurationUnitValue),
}),
}),
})
const ActivationAndHalfIntervalArcaneEnergyCost = TypeAlias(import.meta.url, {
name: "ActivationAndHalfIntervalArcaneEnergyCost",
type: () =>
Object({
value: Required({
comment:
"The AE cost value that has to be payed for activation. Half of this value has to be payed each interval.",
type: Integer({ minimum: 2, multipleOf: 2 }),
}),
interval: Required({
comment: "The time interval for which the AE cost `value` has to be paid.",
type: IncludeIdentifier(DurationUnitValue),
}),
}),
})
const IndefiniteArcaneEnergyCost = TypeAlias(import.meta.url, {
name: "IndefiniteArcaneEnergyCost",
type: () =>
Object({
modifier: Optional({
comment:
"Specified if the indefinite AP cost description needs to be modified by a certain value.",
type: IncludeIdentifier(IndefiniteArcaneEnergyCostModifier),
}),
translations: NestedTranslationMap(
Required,
"IndefiniteArcaneEnergyCost",
Object({
description: Required({
comment: "A description of where the cost come from.",
type: IncludeIdentifier(ResponsiveText),
}),
}),
),
}),
})
const IndefiniteArcaneEnergyCostModifier = TypeAlias(import.meta.url, {
name: "IndefiniteArcaneEnergyCostModifier",
type: () =>
Object({
arithmetic: Required({
comment: "The arithmetic how to apply the `value` to the `base`.",
type: IncludeIdentifier(IndefiniteArcaneEnergyCostModifierArithmetic),
}),
value: Required({
comment: "The value that is applied to the `base` using the defined `arithmetic`.",
type: Integer({ minimum: 1 }),
}),
}),
})
const IndefiniteArcaneEnergyCostModifierArithmetic = Enum(import.meta.url, {
name: "IndefiniteArcaneEnergyCostModifierArithmetic",
values: () => ({
Add: EnumCase({ type: null }),
}),
})
const ArcaneEnergyCostDisjunction = TypeAlias(import.meta.url, {
name: "ArcaneEnergyCostDisjunction",
type: () =>
Object({
interval: Optional({
comment: "Specified if the selected AE cost option has to be paid for each time interval.",
type: IncludeIdentifier(ArcaneEnergyCostDisjunctionInterval),
}),
options: Required({
comment: "The possible AE cost values.",
type: Array(IncludeIdentifier(ArcaneEnergyCostDisjunctionOption), { minItems: 1 }),
}),
}),
})
const ArcaneEnergyCostDisjunctionInterval = TypeAlias(import.meta.url, {
name: "ArcaneEnergyCostDisjunctionInterval",
type: () =>
Object({
value: Required({
comment: "The interval itself.",
type: IncludeIdentifier(DurationUnitValue),
}),
activation_value: Required({
comment: "The AE cost value for activation.",
type: Integer({ minimum: 1 }),
}),
after_activation: Required({
comment: `Set to \`true\` if the action where the enchantment is casted does not as a part of the first interval that has to be payed, so that the first interval payment needs to be done after the activation.
This works different than other sustained spells, since for them the end of the cast usually already counts as part of the first interval.`,
type: Boolean(),
}),
}),
})
const ArcaneEnergyCostDisjunctionOption = TypeAlias(import.meta.url, {
name: "ArcaneEnergyCostDisjunctionOption",
type: () =>
Object({
value: Required({
comment: "A possible AE cost value.",
type: Integer({ minimum: 1 }),
}),
translations: NestedTranslationMap(
Optional,
"ArcaneEnergyCostDisjunctionOption",
Object(
{
note: Optional({
comment: "A note, appended to the generated option string in parenthesis.",
type: IncludeIdentifier(ResponsiveTextOptional),
}),
},
{ minProperties: 1 },
),
),
}),
})
const Volume = Enum(import.meta.url, {
name: "Volume",
comment: "The volume points the enchantment needs.",
values: () => ({
Fixed: EnumCase({ type: IncludeIdentifier(FixedVolume) }),
PerLevel: EnumCase({ type: IncludeIdentifier(VolumePerLevel) }),
ByLevel: EnumCase({ type: IncludeIdentifier(VolumeByLevel) }),
Map: EnumCase({ type: IncludeIdentifier(VolumeMap) }),
DerivedFromSelection: EnumCase({ type: IncludeIdentifier(VolumeDerivedFromSelection) }),
}),
})
export const volume = Required({
comment: "The volume points the enchantment needs.",
type: IncludeIdentifier(Volume),
})
export const volume_l10n = Optional({
comment: "The volume points the enchantment needs.",
isDeprecated: true,
type: String({ minLength: 1 }),
})
const FixedVolume = TypeAlias(import.meta.url, {
name: "FixedVolume",
type: () =>
Object({
points: Required({
comment: "The volume points.",
type: Integer({ minimum: 0 }),
}),
}),
})
const VolumePerLevel = TypeAlias(import.meta.url, {
name: "VolumePerLevel",
type: () =>
Object({
points: Required({
comment: "The volume points per level.",
type: Integer({ minimum: 1 }),
}),
}),
})
const VolumeByLevel = TypeAlias(import.meta.url, {
name: "VolumeByLevel",
type: () =>
Object({
list: Required({
comment:
"The volume points for each level. The first element is the volume points for the first level, the second element is the volume points for the second level, and so on.",
type: Array(IncludeIdentifier(VolumeByLevelItem), { minItems: 2 }),
}),
}),
})
const VolumeByLevelItem = TypeAlias(import.meta.url, {
name: "VolumeByLevelItem",
type: () =>
Object({
points: Required({
comment: "The volume points for this level.",
type: Integer({ minimum: 0 }),
}),
}),
})
const VolumeMap = TypeAlias(import.meta.url, {
name: "VolumeMap",
comment: `A content that is \`3/4/5 Points for Chimera, Daimonid, Golems, Undead / Fairies, Ghosts / Demons, Elementals\` may be respresented as the following map:
\`\`\`yaml
options:
- points: 3
associated_options:
- id:
tag: General
value: # ...
# ...
translations:
en-US:
label: "Chimera, Daimonid, Golems, Undead"
label_standalone: "Chimera/Daimonid/Golems/Undead"
- points: 4
associated_options:
- id:
tag: General
value: # ...
# ...
translations:
en-US:
label: "Fairies, Ghosts"
label_standalone: "Fairies/Ghosts"
- points: 5
associated_options:
- id:
tag: General
value: # ...
# ...
translations:
en-US:
label: "Demons, Elementals"
label_standalone: "Demons/Elementals"
\`\`\`
This will generate the exact same string as seen above. The associated options are not present in the example, but they link to the options the volume specification is meant for.`,
type: () =>
Object({
options: Required({
comment: "The possible costs and associated labels.",
type: Array(IncludeIdentifier(VolumeMapOption), { minItems: 2 }),
}),
translations: NestedTranslationMap(
Optional,
"VolumeMap",
Object(
{
list_prepend: Optional({
comment: "Place a string between the `for` and the grouped map option labels.",
type: IncludeIdentifier(ResponsiveTextOptional),
}),
list_append: Optional({
comment: "Place a string after the grouped map option labels.",
type: IncludeIdentifier(ResponsiveTextOptional),
}),
replacement: Optional({
comment:
"If the string from the book cannot be generated using the default generation technique, use this string. All options still need to be inserted propertly, since it may be used by in-game tools to provide a selection to players.",
type: IncludeIdentifier(ResponsiveTextOptional),
}),
},
{ minProperties: 1 },
),
),
}),
})
const VolumeMapOption = TypeAlias(import.meta.url, {
name: "VolumeMapOption",
type: () =>
Object({
points: Required({
comment: "The full permanent AE cost value for this option.",
type: Integer({ minimum: 1 }),
}),
associated_options: Required({
comment: "Links to the options this volume specification is meant for.",
type: Array(IncludeIdentifier(VolumeMapOptionAssociatedOption), { minItems: 1 }),
}),
translations: NestedTranslationMap(
Optional,
"VolumeMapOption",
Object(
{
label: Required({
comment: "The description of the option for cost string generation.",
type: IncludeIdentifier(ResponsiveTextOptional),
}),
label_standalone: Optional({
comment:
"The description of the option if used standalone. Only used if different from `label`.",
type: IncludeIdentifier(ResponsiveTextOptional),
}),
},
{ minProperties: 1 },
),
),
}),
})
const VolumeMapOptionAssociatedOption = TypeAlias(import.meta.url, {
name: "VolumeMapOptionAssociatedOption",
type: () =>
Object({
id: Required({
comment: "The options identifier.",
type: IncludeIdentifier(VolumePointsOptionReferenceIdentifier),
}),
}),
})
const VolumeDerivedFromSelection = TypeAlias(import.meta.url, {
name: "VolumeDerivedFromSelection",
type: () =>
Object({
fallback: Required({
comment: "The volume for the selection if it does not define a specific one.",
type: Integer({ minimum: 0 }),
}),
}),
})
export const BindingCost = Enum(import.meta.url, {
name: "BindingCost",
comment: "The binding cost for an enchantment.",
values: () => ({
Fixed: EnumCase({ type: IncludeIdentifier(FixedBindingCost) }),
PerLevel: EnumCase({ type: IncludeIdentifier(BindingCostPerLevel) }),
Map: EnumCase({ type: IncludeIdentifier(BindingCostMap) }),
DerivedFromSelection: EnumCase({ type: IncludeIdentifier(BindingCostDerivedFromSelection) }),
}),
})
export const bindingCost = Optional({
comment: "The binding cost for an enchantment.",
isDeprecated: true,
type: String({ minLength: 1 }),
})
const FixedBindingCost = TypeAlias(import.meta.url, {
name: "FixedBindingCost",
type: () =>
Object({
permanent_value: Required({
comment: `The permanent AE cost.
If the enchantment has multiple levels, it is only applied for the first level.`,
type: Integer({ minimum: 1 }),
}),
}),
})
const BindingCostPerLevel = TypeAlias(import.meta.url, {
name: "BindingCostPerLevel",
type: () =>
Object({
permanent_value: Required({
comment: "The permanent AE cost per level.",
type: Integer({ minimum: 1 }),
}),
}),
})
const BindingCostMap = TypeAlias(import.meta.url, {
name: "BindingCostMap",
comment: `A content that is \`2/4/8 permanent AE for spell-swords with the combat technique Daggers, Swords, or Two-Handed Swords\` may be respresented as the following map:
\`\`\`yaml
options:
- permanent_value: 2
translations:
en-US:
label: "Daggers"
label_standalone: "Dagger"
- permanent_value: 4
translations:
en-US:
label: "Swords"
label_standalone: "Sword"
- permanent_value: 8
translations:
en-US:
label: "Two-Handed Swords"
label_standalone: "Two-Handed Sword"
list_prepend: "spell-swords with the combat technique"
\`\`\`
This will generate the exact same string as seen above.`,
type: () =>
Object({
options: Required({
comment: "The possible costs and associated labels.",
type: Array(IncludeIdentifier(BindingCostMapOption), { minItems: 2 }),
}),
translations: NestedTranslationMap(
Optional,
"BindingCostMap",
Object(
{
list_prepend: Optional({
comment: "Place a string between the `for` and the grouped map option labels.",
type: IncludeIdentifier(ResponsiveTextOptional),
}),
list_append: Optional({
comment: "Place a string after the grouped map option labels.",
type: IncludeIdentifier(ResponsiveTextOptional),
}),
replacement: Optional({
comment:
"If the string from the book cannot be generated using the default generation technique, use this string. All options still need to be inserted propertly, since it may be used by in-game tools to provide a selection to players.",
type: IncludeIdentifier(ResponsiveTextOptional),
}),
},
{ minProperties: 1 },
),
),
}),
})
const BindingCostMapOption = TypeAlias(import.meta.url, {
name: "BindingCostMapOption",
type: () =>
Object({
permanent_value: Required({
comment: "The full permanent AE cost value for this option.",
type: Integer({ minimum: 1 }),
}),
translations: NestedTranslationMap(
Optional,
"BindingCostMapOption",
Object({
label: Required({
comment: "The description of the option for cost string generation.",
type: IncludeIdentifier(ResponsiveTextOptional),
}),
label_standalone: Optional({
comment:
"The description of the option if used standalone. Only used if different from `label`.",
type: IncludeIdentifier(ResponsiveTextOptional),
}),
}),
),
}),
})
const BindingCostDerivedFromSelection = TypeAlias(import.meta.url, {
name: "BindingCostDerivedFromSelection",
type: () =>
Object({
fallback: Required({
comment: "The binding cost for the selection if it does not define a specific one.",
type: Integer({ minimum: 0 }),
}),
}),
})
const PropertyDeclaration = Enum(import.meta.url, {
name: "PropertyDeclaration",
comment:
"The magic propertys identifier. `DependingOnProperty` can only be used if the special ability has an option to select a property.",
values: () => ({
DependingOnSelection: EnumCase({ type: null }),
Fixed: EnumCase({ type: PropertyIdentifier() }),
}),
})
export const property = () =>
Required({
comment:
"The magic propertys identifier. `DependingOnProperty` can only be used if the special ability has an option to select a property.",
type: IncludeIdentifier(PropertyDeclaration),
})
export const propertyOptional = () =>
Optional({
comment:
"The magic propertys identifier. `DependingOnProperty` can only be used if the special ability has an option to select a property.",
type: IncludeIdentifier(PropertyDeclaration),
})
export const aspect = () =>
Required({
comment: "The blessed aspect.",
type: AspectIdentifier(),
})
export const aspectOptional = () =>
Optional({
comment: "The blessed aspect.",
type: AspectIdentifier(),
})
@@ -294,7 +294,7 @@ const SkillApplicationOrUse = TypeAlias(import.meta.url, {
"The name of the application or use if different from the activatable entrys name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
})
@@ -424,7 +424,7 @@ const SelectOptionsFixedAdventurePointsValue = GenTypeAlias(import.meta.url, {
type: Array(
GenIncludeIdentifier(SelectOptionsFixedAdventurePointsValueMapping, [
TypeArgument(Identifier),
])
]),
),
}),
default: Required({
+333
View File
@@ -0,0 +1,333 @@
import {
Boolean,
ChildEntities,
Entity,
Enum,
EnumCase,
getAnyEnumCaseValue,
IncludeIdentifier,
Integer,
Object,
Optional,
Required,
String,
TypeAlias,
} from "tsondb/schema/def"
import { NestedTranslationMap } from "./Locale.js"
import { SelectOptionCategory } from "./_ActivatableSelectOptionCategory.js"
import { NewSkillApplication, SkillUse } from "./_ActivatableSkillApplicationsAndUses.js"
import { GeneralIdentifier, SkillIdentifier } from "./_Identifier.js"
import { CombatTechniqueIdentifier, SelectOptionParentIdentifier } from "./_IdentifierGroup.js"
import { GeneralPrerequisites } from "./_Prerequisite.js"
import { Errata } from "./source/_Erratum.js"
import { optionalSrc } from "./source/_PublicationRef.js"
const SelectOptions = TypeAlias(import.meta.url, {
name: "SelectOptions",
comment: `Definitions for possible options for the activatable entry. They can either be derived from entry categories or be defined explicitly. Both can happen as well, but if there is an explicitly defined select option and a derived select option has the same identifier (which may only happen if skill or combat technique identifiers are used for explicit select options), the explicit definition overwrites the derived option.
Note that this is only a full definition of options for simple logic that can be made explicit using the more detailed configuration for both derived categories and explicit options. There are quite a few entries whose option logic cannot be fully represented here, so that it needs to be implemented manually.`,
type: () =>
Object(
{
derived: Optional({
comment: `An entry category with optional further configuration. All available entries from the specified categories will be included as separate select options. You can also specify a set of groups that should only be included. Groups not mentioned will be excluded then.`,
type: IncludeIdentifier(SelectOptionCategory),
}),
},
{ minProperties: 1 },
),
})
export const select_options = Optional({
comment: `Definitions for possible options for the activatable entry, derived from entry categories.
Note that this is only a full definition of options for simple logic that can be made explicit using the more detailed configuration for both derived categories and explicit options. There are quite a few entries whose option logic cannot be fully represented here, so that it needs to be implemented manually.`,
type: IncludeIdentifier(SelectOptions),
})
export const ExplicitSelectOption = Enum(import.meta.url, {
name: "ExplicitSelectOption",
values: () => ({
General: EnumCase({ type: GeneralIdentifier() }),
Skill: EnumCase({ type: IncludeIdentifier(ExplicitSkillSelectOption) }),
CombatTechnique: EnumCase({ type: IncludeIdentifier(ExplicitCombatTechniqueSelectOption) }),
}),
})
export const GeneralSelectOption = Entity(import.meta.url, {
name: "GeneralSelectOption",
namePlural: "GeneralSelectOptions",
type: () =>
Object({
parent: Required({
comment: "The entry that contains select option.",
type: IncludeIdentifier(SelectOptionParentIdentifier),
}),
profession_only: Optional({
comment:
"Sometimes, professions use specific text selections that are not contained in described lists. This ensures you can use them for professions only. They are not going to be displayed as options to the user.",
type: Boolean(),
}),
skill_applications: Required({
comment:
"Registers new applications, which get enabled once this entry is activated with its respective select option. It specifies an entry-unique identifier and the skill it belongs to. A translation can be left out if its name equals the name of the origin select option.",
type: ChildEntities(NewSkillApplication),
}),
skill_uses: Required({
comment:
"Registers uses, which get enabled once this entry is activated with its respective select option. It specifies an entry-unique identifier and the skill it belongs to. A translation can be left out if its name equals the name of the origin select option.",
type: ChildEntities(SkillUse),
}),
prerequisites: Optional({
comment: "Prerequisites for the select option.",
type: IncludeIdentifier(GeneralPrerequisites),
}),
binding_cost: Optional({
comment:
"Specific binding cost for the select option. Only has an effect if the associated entry supports binding costs.",
type: Integer({ minimum: 0 }),
}),
ap_value: Optional({
comment: "Specific AP cost for the select option.",
type: Integer({ minimum: 1 }),
}),
src: optionalSrc,
translations: NestedTranslationMap(
Required,
"GeneralSelectOption",
Object({
name: Required({
comment: "The name of the select option.",
type: String({ minLength: 1 }),
}),
name_in_profession: Optional({
comment: "The name of the select option when displayed in a generated profession text.",
type: String({ minLength: 1 }),
}),
description: Optional({
comment:
"The description of the select option. Useful for Bad Habits, Trade Secrets and other entries where a description is available.",
type: String({ minLength: 1, isMarkdown: true }),
}),
errata: Optional({
type: IncludeIdentifier(Errata),
}),
}),
),
}),
parentReferenceKey: "parent",
displayName: {},
displayNameCustomizer: ({
instance,
instanceDisplayName,
instanceDisplayNameLocaleId,
getDisplayNameForInstanceId,
}) => ({
name: `${
instanceDisplayName.length > 0
? instanceDisplayName
: (getDisplayNameForInstanceId(getAnyEnumCaseValue(instance.parent))?.name ?? "")
}${instanceDisplayName}`,
localeId: instanceDisplayNameLocaleId,
}),
})
export const explicit_select_options = Required({
comment: `Explicit definitions for possible options for the activatable entry.
Note that this is only a full definition of options for simple logic that can be made explicit using the more detailed configuration for both derived categories and explicit options. There are quite a few entries whose option logic cannot be fully represented here, so that it needs to be implemented manually.`,
type: ChildEntities(GeneralSelectOption),
})
const ExplicitSkillSelectOption = TypeAlias(import.meta.url, {
name: "ExplicitSkillSelectOption",
type: () =>
Object({
id: Required({
comment: "The skills identifier.",
type: SkillIdentifier(),
}),
prerequisites: Optional({
comment: "Prerequisites for the select option.",
type: IncludeIdentifier(GeneralPrerequisites),
}),
binding_cost: Optional({
comment:
"Specific binding cost for the select option. Only has an effect if the associated entry supports binding costs.",
type: Integer({ minimum: 0 }),
}),
ap_value: Optional({
comment: "Specific AP cost for the select option.",
type: Integer({ minimum: 1 }),
}),
src: optionalSrc,
translations: NestedTranslationMap(
Optional,
"ExplicitSkillSelectOption",
Object(
{
errata: Optional({
type: IncludeIdentifier(Errata),
}),
},
{ minProperties: 1 },
),
),
}),
})
const ExplicitCombatTechniqueSelectOption = TypeAlias(import.meta.url, {
name: "ExplicitCombatTechniqueSelectOption",
type: () =>
Object({
id: Required({
comment: "The combat techniques identifier.",
type: IncludeIdentifier(CombatTechniqueIdentifier),
}),
prerequisites: Optional({
comment: "Prerequisites for the select option.",
type: IncludeIdentifier(GeneralPrerequisites),
}),
binding_cost: Optional({
comment:
"Specific binding cost for the select option. Only has an effect if the associated entry supports binding costs.",
type: Integer({ minimum: 0 }),
}),
ap_value: Optional({
comment: "Specific AP cost for the select option.",
type: Integer({ minimum: 1 }),
}),
src: optionalSrc,
translations: NestedTranslationMap(
Optional,
"ExplicitCombatTechniqueSelectOption",
Object(
{
errata: Optional({
type: IncludeIdentifier(Errata),
}),
},
{ minProperties: 1 },
),
),
}),
})
// "Options": {
// "$comment": "Unfinished generalized options handling.",
// "type": "array",
// "minItems": 1,
// "items": {
// "oneOf": [
// {
// "type": "object",
// "properties": {
// "type": { "const": "List" },
// "value": {
// "type": "object",
// "properties": {
// "list": {
// "oneOf": [
// {
// "type": "object",
// "properties": {
// "type": { "const": "Generate" },
// "value": { "$ref": "#/definitions/SelectOptionCategories" }
// },
// "required": ["type", "value"],
// "additionalProperties": false
// },
// {
// "type": "object",
// "properties": {
// "type": { "const": "Explicit" },
// "value": { "$ref": "#/definitions/SelectOptions" }
// },
// "required": ["type", "value"],
// "additionalProperties": false
// }
// ]
// },
// "multiple": {
// "description": "Sometimes, multiple options from a single list have to or can be chosen. Its possible to define a number of options that have to be selected or a range of numbers of options that can be selected.",
// "oneOf": [
// {
// "type": "object",
// "properties": {
// "type": { "const": "Fixed" },
// "value": {
// "description": "The number of options that have to be selected.",
// "type": "integer",
// "minimum": 2
// }
// },
// "required": ["type", "value"],
// "additionalProperties": false
// },
// {
// "type": "object",
// "properties": {
// "type": { "const": "Range" },
// "value": {
// "type": "object",
// "properties": {
// "min": {
// "description": "The minimum number of options that need to be selected. If left empty it defaults to 1.",
// "type": "integer",
// "minimum": 2
// },
// "max": {
// "description": "The maximum number of options that need to be selected.",
// "type": "integer",
// "minimum": 2
// }
// },
// "required": ["max"],
// "additionalProperties": false
// }
// },
// "required": ["type", "value"],
// "additionalProperties": false
// }
// ]
// }
// },
// "required": ["list"],
// "additionalProperties": false
// }
// },
// "required": ["type", "value"],
// "additionalProperties": false
// },
// {
// "type": "object",
// "properties": {
// "type": { "const": "TextInput" },
// "value": {
// "type": "object",
// "properties": {
// "label": {
// "type": "object",
// "description": "All translations for the entry, identified by IETF language tag (BCP47).",
// "patternProperties": {
// "^[a-z]{2}-[A-Z]{2}$": {
// "description": "The text input label.",
// "type": "string",
// "minLength": 1
// }
// },
// "minProperties": 1,
// "additionalProperties": false
// }
// },
// "required": ["label"],
// "additionalProperties": false
// }
// },
// "required": ["type", "value"],
// "additionalProperties": false
// }
// ]
// }
// },
@@ -0,0 +1,121 @@
import {
Array,
ChildEntities,
Entity,
getAnyEnumCaseValue,
IncludeIdentifier,
Integer,
Object,
Optional,
Required,
String,
} from "tsondb/schema/def"
import { NestedTranslationMap } from "./Locale.js"
import { SkillIdentifier } from "./_Identifier.js"
import { NewSkillApplicationOrUseParentIdentifier } from "./_IdentifierGroup.js"
export const NewSkillApplication = Entity(import.meta.url, {
name: "NewSkillApplication",
namePlural: "NewSkillApplications",
comment:
"Registers new skill applications, which get enabled once the referenced entry is activated. It specifies the skill(s) it belongs to. A translation can be left out if its name equals the name of the origin activatable entry.",
type: () =>
Object({
parent: Required({
comment: "The entry that enables the new skill application.",
type: IncludeIdentifier(NewSkillApplicationOrUseParentIdentifier),
}),
skills: Required({
comment: "The skill(s) this application belongs to.",
type: Array(SkillIdentifier(), { minItems: 1 }),
}),
required_skill_rating: Optional({
comment:
"If an application applies to multiple skills, it may need to ensure the respective skill is on a certain skill rating if the activatable entry cannot ensure this prerequisite.",
type: Integer({ minimum: 1 }),
}),
translations: NestedTranslationMap(
Optional,
"NewSkillApplication",
Object({
name: Required({
comment: "The name of the application if different from the activatable entrys name.",
type: String({ minLength: 1 }),
}),
}),
),
}),
parentReferenceKey: "parent",
displayName: {},
displayNameCustomizer: ({
instance,
instanceDisplayName,
instanceDisplayNameLocaleId,
getDisplayNameForInstanceId,
}) => ({
name:
instanceDisplayName.length > 0
? instanceDisplayName
: (getDisplayNameForInstanceId(getAnyEnumCaseValue(instance.parent))?.name ?? ""),
localeId: instanceDisplayNameLocaleId,
}),
})
export const skill_applications = Required({
comment:
"Registers new skill applications, which get enabled once this entry is activated. It specifies an entry-unique identifier and the skill it belongs to. A translation can be left out if its name equals the name of the origin activatable entry.",
type: ChildEntities(NewSkillApplication),
})
export const SkillUse = Entity(import.meta.url, {
name: "SkillUse",
namePlural: "SkillUses",
comment:
"Registers uses, which get enabled once this entry is activated. It specifies the skill(s) it belongs to. A translation can be left out if its name equals the name of the origin activatable entry.",
type: () =>
Object({
parent: Required({
comment: "The entry that enables the use.",
type: IncludeIdentifier(NewSkillApplicationOrUseParentIdentifier),
}),
skills: Required({
comment: "The skill(s) this use belongs to.",
type: Array(SkillIdentifier(), { minItems: 1 }),
}),
required_skill_rating: Optional({
comment:
"If a use applies to multiple skills, it may need to ensure the respective skill is on a certain skill rating if the activatable entry cannot ensure this prerequisite.",
type: Integer({ minimum: 1 }),
}),
translations: NestedTranslationMap(
Optional,
"SkillUse",
Object({
name: Required({
comment: "The name of the use if different from the activatable entrys name.",
type: String({ minLength: 1 }),
}),
}),
),
}),
parentReferenceKey: "parent",
displayName: {},
displayNameCustomizer: ({
instance,
instanceDisplayName,
instanceDisplayNameLocaleId,
getDisplayNameForInstanceId,
}) => ({
name:
instanceDisplayName.length > 0
? instanceDisplayName
: (getDisplayNameForInstanceId(getAnyEnumCaseValue(instance.parent))?.name ?? ""),
localeId: instanceDisplayNameLocaleId,
}),
})
export const skill_uses = Required({
comment:
"Registers uses, which get enabled once this entry is activated. It specifies an entry-unique identifier and the skill it belongs to. A translation can be left out if its name equals the name of the origin activatable entry.",
type: ChildEntities(SkillUse),
})
+1 -1
View File
@@ -55,7 +55,7 @@ There must always be at least one casting time value.`,
type: IncludeIdentifier(CastingTimeDuringLovemaking),
}),
},
{ minProperties: 1 }
{ minProperties: 1 },
),
})
+8 -8
View File
@@ -62,7 +62,7 @@ const ModifiableOneTimeCost = TypeAlias(import.meta.url, {
comment: "A replacement string.",
type: IncludeIdentifier(ResponsiveTextReplace),
}),
})
}),
),
}),
})
@@ -95,7 +95,7 @@ const NonModifiableOneTimeCost = TypeAlias(import.meta.url, {
comment: "A note, appended to the generated string in parenthesis.",
type: IncludeIdentifier(ResponsiveTextOptional),
}),
})
}),
),
}),
})
@@ -116,7 +116,7 @@ const NonModifiableOneTimeCostPerCountable = TypeAlias(import.meta.url, {
comment: "The countable entity name.",
type: IncludeIdentifier(ResponsiveText),
}),
})
}),
),
}),
})
@@ -133,7 +133,7 @@ export const IndefiniteOneTimeCost = TypeAlias(import.meta.url, {
comment: "A description of where the cost come from.",
type: IncludeIdentifier(ResponsiveText),
}),
})
}),
),
}),
})
@@ -183,8 +183,8 @@ This will generate the exact same string as seen above given it is set for a
type: IncludeIdentifier(ResponsiveTextOptional),
}),
},
{ minProperties: 1 }
)
{ minProperties: 1 },
),
),
}),
})
@@ -214,7 +214,7 @@ const CostMapOption = TypeAlias(import.meta.url, {
"The description of the option if used standalone. Only used if different from `label`.",
type: IncludeIdentifier(ResponsiveTextOptional),
}),
})
}),
),
}),
})
@@ -285,7 +285,7 @@ const NonModifiableSustainedCostPerCountable = TypeAlias(import.meta.url, {
comment: "The countable entity name.",
type: IncludeIdentifier(ResponsiveText),
}),
})
}),
),
}),
})
+6 -6
View File
@@ -40,7 +40,7 @@ const Immediate = TypeAlias(import.meta.url, {
comment: "A replacement string.",
type: IncludeIdentifier(ResponsiveTextReplace),
}),
})
}),
),
}),
})
@@ -57,7 +57,7 @@ const PermanentDuration = TypeAlias(import.meta.url, {
comment: "A replacement string.",
type: IncludeIdentifier(ResponsiveTextReplace),
}),
})
}),
),
}),
})
@@ -88,8 +88,8 @@ export const FixedDuration = TypeAlias(import.meta.url, {
type: IncludeIdentifier(ResponsiveTextReplace),
}),
},
{ minProperties: 1 }
)
{ minProperties: 1 },
),
),
}),
})
@@ -122,7 +122,7 @@ export const CheckResultBasedDuration = TypeAlias(import.meta.url, {
comment: "A replacement string.",
type: IncludeIdentifier(ResponsiveTextReplace),
}),
})
}),
),
}),
})
@@ -139,7 +139,7 @@ export const IndefiniteDuration = TypeAlias(import.meta.url, {
comment: "A description of the duration.",
type: IncludeIdentifier(ResponsiveText),
}),
})
}),
),
}),
})
+2 -2
View File
@@ -38,8 +38,8 @@ export const Range = TypeAlias(import.meta.url, {
type: IncludeIdentifier(ResponsiveTextReplace),
}),
},
{ minProperties: 1 }
)
{ minProperties: 1 },
),
),
}),
})
+1 -1
View File
@@ -30,7 +30,7 @@ const SpecificAffectedTargetCategory = TypeAlias(import.meta.url, {
"SpecificAffectedTargetCategory",
Object({
note: Required({ type: String({ minLength: 1 }) }),
})
}),
),
}),
})
+1 -1
View File
@@ -17,6 +17,6 @@ export const ArcaneTraditionType =
comment: "The arcane traditions name.",
type: String({ minLength: 1 }),
}),
})
}),
),
})
+1 -1
View File
@@ -72,7 +72,7 @@ export const Cause = TypeAlias(import.meta.url, {
comment: "An additional note about this cause.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
})
+3 -2
View File
@@ -5,6 +5,7 @@
import {
ChildEntities,
Entity,
getAnyEnumCaseValue,
IncludeIdentifier,
Integer,
Object,
@@ -76,7 +77,7 @@ export const Enhancement = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
parentReferenceKey: "parent",
@@ -88,7 +89,7 @@ export const Enhancement = Entity(import.meta.url, {
getDisplayNameForInstanceId,
}) => ({
name: `${
getDisplayNameForInstanceId((instance as any).parent[(instance as any).parent.kind])?.name
getDisplayNameForInstanceId(getAnyEnumCaseValue(instance.parent))?.name ?? ""
}${instanceDisplayName}`,
localeId: instanceDisplayNameLocaleId,
}),
+2 -1
View File
@@ -1,5 +1,6 @@
import { ReferenceIdentifier as R } from "tsondb/schema/def"
import { GeneralSelectOption, NewSkillApplication, SkillUse } from "./_Activatable.js"
import { GeneralSelectOption } from "./_ActivatableSelectOptions.js"
import { NewSkillApplication, SkillUse } from "./_ActivatableSkillApplicationsAndUses.js"
import { Enhancement } from "./_Enhancements.js"
import { Influence } from "./_Influence.js"
import { Advantage } from "./Advantage.js"
+3
View File
@@ -493,6 +493,9 @@ export const RequirableSelectOptionIdentifier = Enum(import.meta.url, {
Spell: EnumCase({ type: SpellIdentifier() }),
Ritual: EnumCase({ type: RitualIdentifier() }),
TradeSecret: EnumCase({ type: TradeSecretIdentifier() }),
ArcaneBardTradition: EnumCase({ type: ArcaneBardTraditionIdentifier() }),
ArcaneDancerTradition: EnumCase({ type: ArcaneDancerTraditionIdentifier() }),
Element: EnumCase({ type: ElementIdentifier() }),
}),
})
+1 -1
View File
@@ -38,7 +38,7 @@ export const Influence = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
+2 -2
View File
@@ -26,7 +26,7 @@ type BinaryMathOperation = {
export type MathOperation = Enum<
"MathOperation",
{
Value: EnumCase<TypeArgument<TypeParameter>>
Value: EnumCase<TypeArgument>
} & BinaryMathOperation,
[Value: TypeParameter]
>
@@ -39,7 +39,7 @@ export const MathOperation: MathOperation = GenEnum(import.meta.url, {
GenIncludeIdentifier<MathOperation, [Value: TypeParameter]>(MathOperation, [
TypeArgument(Value),
]),
{ minItems: 2, maxItems: 2 }
{ minItems: 2, maxItems: 2 },
)
return {
+1 -1
View File
@@ -56,7 +56,7 @@ const PrerequisiteGroup = GenTypeAlias(import.meta.url, {
text: Required({
type: String({ minLength: 1 }),
}),
})
}),
),
}),
})
+35
View File
@@ -0,0 +1,35 @@
import {
Enum,
EnumCase,
GenTypeAlias,
Object,
Param,
Required,
TypeArgument,
} from "tsondb/schema/def"
export const SizeCategory = Enum(import.meta.url, {
name: "SizeCategory",
comment: "Each creature fits into one of these size categories.",
values: () => ({
Tiny: EnumCase({ type: null }),
Small: EnumCase({ type: null }),
Medium: EnumCase({ type: null }),
Large: EnumCase({ type: null }),
Huge: EnumCase({ type: null }),
}),
})
export const BySizeCategory = GenTypeAlias(import.meta.url, {
name: "BySizeCategory",
comment: "An object that holds values for the different size categories",
parameters: [Param("T")],
type: T =>
Object({
tiny: Required({ type: TypeArgument(T) }),
small: Required({ type: TypeArgument(T) }),
medium: Required({ type: TypeArgument(T) }),
large: Required({ type: TypeArgument(T) }),
huge: Required({ type: TypeArgument(T) }),
}),
})
+1 -1
View File
@@ -32,7 +32,7 @@ export const EquipmentPackage = Entity(import.meta.url, {
comment: "The equipment packages name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
+1 -1
View File
@@ -37,7 +37,7 @@ export const Ammunition = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
+14 -4
View File
@@ -16,7 +16,7 @@ import { ArmorIdentifier, ArmorTypeIdentifier } from "../../_Identifier.js"
import { NestedTranslationMap } from "../../Locale.js"
import { Errata } from "../../source/_Erratum.js"
import { src } from "../../source/_PublicationRef.js"
import { ComplexComplexity, Cost, Weight } from "./_Item.js"
import { ComplexComplexity, Cost, RestrictedTo, Weight } from "./_Item.js"
export const Armor = Entity(import.meta.url, {
name: "Armor",
@@ -55,6 +55,11 @@ export const Armor = Entity(import.meta.url, {
comment: "Specify if armor is only available for a specific hit zone.",
type: IncludeIdentifier(HitZone),
}),
restrictedTo: Optional({
comment:
"Define if during character creation this weapon can only be bought by a specific subset of characters.",
type: IncludeIdentifier(RestrictedTo),
}),
src,
translations: NestedTranslationMap(
Required,
@@ -87,7 +92,7 @@ export const Armor = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
@@ -117,6 +122,11 @@ export const SecondaryArmor = TypeAlias(import.meta.url, {
comment: "Specify if armor is only available for a specific hit zone.",
type: IncludeIdentifier(HitZone),
}),
restrictedTo: Optional({
comment:
"Define if during character creation this armor can only be bought by a specific subset of characters.",
type: IncludeIdentifier(RestrictedTo),
}),
translations: NestedTranslationMap(
Optional,
"SecondaryArmor",
@@ -131,8 +141,8 @@ export const SecondaryArmor = TypeAlias(import.meta.url, {
type: String({ minLength: 1, isMarkdown: true }),
}),
},
{ minProperties: 1 }
)
{ minProperties: 1 },
),
),
}),
})
+4 -4
View File
@@ -74,7 +74,7 @@ export const Book = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
@@ -185,8 +185,8 @@ const DefiniteBookCostVariant = TypeAlias(import.meta.url, {
type: String({ minLength: 1, isMarkdown: true }),
}),
},
{ minProperties: 1 }
)
{ minProperties: 1 },
),
),
}),
})
@@ -209,7 +209,7 @@ const IndefiniteBookCostVariant = TypeAlias(import.meta.url, {
comment: "The description of the cost variant, e.g. “less”.",
type: String({ minLength: 1, isMarkdown: true }),
}),
})
}),
),
}),
})
+1 -1
View File
@@ -76,7 +76,7 @@ export const Elixir = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
@@ -1,7 +1,6 @@
import { Array, Entity, IncludeIdentifier, Object, Optional, Required } from "tsondb/schema/def"
import { BlessedTraditionIdentifier } from "../../_Identifier.js"
import { Entity, IncludeIdentifier, Object, Optional, Required } from "tsondb/schema/def"
import { src } from "../../source/_PublicationRef.js"
import { CombatUse, Cost, DefaultItemTranslations, StructurePoints } from "./_Item.js"
import { CombatUse, Cost, DefaultItemTranslations, RestrictedTo, StructurePoints } from "./_Item.js"
export const EquipmentOfBlessedOnes = Entity(import.meta.url, {
name: "EquipmentOfBlessedOnes",
@@ -17,9 +16,10 @@ export const EquipmentOfBlessedOnes = Entity(import.meta.url, {
"The structure points of the item. Use an array if the item consists of multiple components that have individual structure points.",
type: IncludeIdentifier(StructurePoints),
}),
associated_tradition: Required({
comment: "The deity/deities associated with the equipment item.",
type: Array(BlessedTraditionIdentifier(), { minItems: 1, uniqueItems: true }),
restrictedTo: Optional({
comment:
"Define if during character creation this item can only be bought by a specific subset of characters.",
type: IncludeIdentifier(RestrictedTo),
}),
combat_use: Optional({
comment:
@@ -41,7 +41,7 @@ export const GemOrPreciousStone = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
+46 -3
View File
@@ -1,6 +1,8 @@
import { Entity, IncludeIdentifier, Object, Optional, Required } from "tsondb/schema/def"
import { Entity, IncludeIdentifier, Object, Optional, Required, String } from "tsondb/schema/def"
import { NestedTranslationMap } from "../../Locale.js"
import { Errata } from "../../source/_Erratum.js"
import { src } from "../../source/_PublicationRef.js"
import { CombatUse, Complexity, Cost, DefaultItemTranslations, Weight } from "./_Item.js"
import { CombatUse, Complexity, Cost, StructurePoints, Weight } from "./_Item.js"
export const MagicalArtifact = Entity(import.meta.url, {
name: "MagicalArtifact",
@@ -19,13 +21,54 @@ export const MagicalArtifact = Entity(import.meta.url, {
comment: "The complexity of crafting the item.",
type: IncludeIdentifier(Complexity),
}),
structure_points: Optional({
comment:
"The structure points of the item. Use an array if the item consists of multiple components that have individual structure points.",
type: IncludeIdentifier(StructurePoints),
}),
combat_use: Optional({
comment:
"The item can also be used either as an improvised weapon or as an armor, although this is not the primary use case of the item.",
type: IncludeIdentifier(CombatUse),
}),
src,
translations: DefaultItemTranslations("MagicalArtifact"),
translations: NestedTranslationMap(
Required,
`MagicalArtifact`,
Object({
name: Required({
comment: "The items name.",
type: String({ minLength: 1 }),
}),
secondary_name: Optional({
comment: "An auxiliary name or label of the item, if available.",
type: String({ minLength: 1 }),
}),
description: Optional({
comment: "Description text.",
type: String({ minLength: 1, isMarkdown: true }),
}),
note: Optional({
comment: "A note.",
type: String({ minLength: 1, isMarkdown: true }),
}),
rules: Optional({
comment: "Rules text.",
type: String({ minLength: 1, isMarkdown: true }),
}),
effect: Optional({
comment: "Effect text.",
type: String({ minLength: 1, isMarkdown: true }),
}),
cost: Optional({
comment: "Cost text.",
type: String({ minLength: 1, isMarkdown: true }),
}),
errata: Optional({
type: IncludeIdentifier(Errata),
}),
}),
),
}),
displayName: {},
})
+13 -1
View File
@@ -1,6 +1,13 @@
import { Entity, IncludeIdentifier, Object, Optional, Required } from "tsondb/schema/def"
import { src } from "../../source/_PublicationRef.js"
import { CombatUse, Complexity, Cost, DefaultItemTranslations, Weight } from "./_Item.js"
import {
CombatUse,
Complexity,
Cost,
DefaultItemTranslations,
StructurePoints,
Weight,
} from "./_Item.js"
export const MusicalInstrument = Entity(import.meta.url, {
name: "MusicalInstrument",
@@ -19,6 +26,11 @@ export const MusicalInstrument = Entity(import.meta.url, {
comment: "The complexity of crafting the item.",
type: IncludeIdentifier(Complexity),
}),
structure_points: Optional({
comment:
"The structure points of the item. Use an array if the item consists of multiple components that have individual structure points.",
type: IncludeIdentifier(StructurePoints),
}),
combat_use: Optional({
comment:
"The item can also be used either as an improvised weapon or as an armor, although this is not the primary use case of the item.",
+1 -1
View File
@@ -29,7 +29,7 @@ export const Newspaper = Entity(import.meta.url, {
comment: "The place the newspaper is published.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
+54 -12
View File
@@ -19,6 +19,7 @@ import { NestedTranslationMap } from "../../Locale.js"
import { AlternativeName } from "../../_AlternativeNames.js"
import { Dice } from "../../_Dice.js"
import { Reduceable, Resistance } from "../../_DiseasePoison.js"
import { MathOperation } from "../../_MathExpression.js"
import { Errata } from "../../source/_Erratum.js"
import { src } from "../../source/_PublicationRef.js"
import { EffectType, LaboratoryLevel, RecipeTradeSecret } from "./_Herbary.js"
@@ -80,7 +81,7 @@ export const Poison = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
@@ -112,6 +113,7 @@ const PoisonDuration = Enum(import.meta.url, {
Instant: EnumCase({ type: null }),
Constant: EnumCase({ type: IncludeIdentifier(ConstantPoisonTime) }),
DiceBased: EnumCase({ type: IncludeIdentifier(DiceBasedPoisonTime) }),
ExpressionBased: EnumCase({ type: IncludeIdentifier(ExpressionBasedPoisonTime) }),
Indefinite: EnumCase({ type: IncludeIdentifier(IndefinitePoisonTime) }),
}),
})
@@ -138,7 +140,7 @@ export const NoPoisonCost = TypeAlias(import.meta.url, {
comment: "A description of the cost.",
type: String({ minLength: 1, isMarkdown: true }),
}),
})
}),
),
}),
})
@@ -155,7 +157,7 @@ export const IndefinitePoisonCost = TypeAlias(import.meta.url, {
comment: "A description of the cost.",
type: String({ minLength: 1, isMarkdown: true }),
}),
})
}),
),
}),
})
@@ -190,6 +192,35 @@ export const DiceBasedPoisonTime = TypeAlias(import.meta.url, {
}),
})
export const ExpressionBasedPoisonTime = TypeAlias(import.meta.url, {
name: "ExpressionBasedPoisonTime",
type: () =>
Object({
value: Required({
type: IncludeIdentifier(ExpressionBasedPoisonTimeValue),
}),
unit: Required({
type: IncludeIdentifier(PoisonTimeUnit),
}),
}),
})
export const ExpressionBasedPoisonTimeValue = TypeAlias(import.meta.url, {
name: "ExpressionBasedPoisonTimeValue",
type: () =>
GenIncludeIdentifier(MathOperation, [
IncludeIdentifier(ExpressionBasedPoisonTimeExpressionValue),
]),
})
export const ExpressionBasedPoisonTimeExpressionValue = Enum(import.meta.url, {
name: "ExpressionBasedPoisonTimeExpressionValue",
values: () => ({
Constant: EnumCase({ type: Integer({ minimum: 1 }) }),
Dice: EnumCase({ type: IncludeIdentifier(Dice) }),
}),
})
const PoisonTimeUnit = Enum(import.meta.url, {
name: "PoisonTimeUnit",
values: () => ({
@@ -214,8 +245,8 @@ export const IndefinitePoisonTime = TypeAlias(import.meta.url, {
type: String({ minLength: 1, isMarkdown: true }),
}),
},
{ minProperties: 1 }
)
{ minProperties: 1 },
),
),
}),
})
@@ -277,10 +308,10 @@ const AnimalVenomLevelBySubType = TypeAlias(import.meta.url, {
"The subtype name. If there are multiple subtypes with the same level, specify them separately.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
{ minItems: 1 }
{ minItems: 1 },
),
})
@@ -324,7 +355,7 @@ export const AlchemicalPoison = TypeAlias(import.meta.url, {
comment: "Prerequsites for the brewing process, if any.",
type: String({ minLength: 1, isMarkdown: true }),
}),
})
}),
),
}),
})
@@ -377,8 +408,8 @@ export const DemonicPoison = TypeAlias(import.meta.url, {
type: String({ minLength: 1, isMarkdown: true }),
}),
},
{ minProperties: 1 }
)
{ minProperties: 1 },
),
),
}),
})
@@ -446,7 +477,7 @@ export const Intoxicant = TypeAlias(import.meta.url, {
"The intoxicants side effects that always happen, no matter whether the intoxicant has the default or the reduced effect.",
type: String({ minLength: 1, isMarkdown: true }),
}),
overdose: Required({
overdose: Optional({
comment:
"What happens if the intoxicant has been overdosed, that is, it has been ingested another time within the duration.",
type: String({ minLength: 1, isMarkdown: true }),
@@ -455,7 +486,7 @@ export const Intoxicant = TypeAlias(import.meta.url, {
comment: "Special information about the intoxicant.",
type: String({ minLength: 1, isMarkdown: true }),
}),
})
}),
),
}),
})
@@ -488,6 +519,17 @@ export const IntoxicantAddiction = TypeAlias(import.meta.url, {
"The maximum interval at which it, while addicted, must be ingested to not suffer from withdrawal symptoms.",
type: IncludeIdentifier(IntoxicantAddictionInterval),
}),
translations: NestedTranslationMap(
Optional,
"IntoxicantAddiction",
Object({
chance: Required({
comment:
"The chance of getting addicted after an ingestion. Use if and only if the plain constant percent is not sufficient.",
type: String({ minLength: 1, isMarkdown: true }),
}),
}),
),
}),
})
+10 -63
View File
@@ -1,8 +1,5 @@
import {
Array,
Entity,
Enum,
EnumCase,
IncludeIdentifier,
NestedEntityMap,
Object,
@@ -11,17 +8,11 @@ import {
String,
TypeAlias,
} from "tsondb/schema/def"
import {
BlessedTraditionIdentifier,
CultureIdentifier,
MagicalTraditionIdentifier,
RaceIdentifier,
} from "../../_Identifier.js"
import { CloseCombatTechnique, RangedCombatTechnique } from "../../CombatTechnique.js"
import { NestedTranslationMap } from "../../Locale.js"
import { Errata } from "../../source/_Erratum.js"
import { src } from "../../source/_PublicationRef.js"
import { Complexity, Cost, StructurePoints, Weight } from "./_Item.js"
import { Complexity, Cost, RestrictedTo, StructurePoints, Weight } from "./_Item.js"
import { MeleeWeapon } from "./_MeleeWeapon.js"
import { RangedWeapon } from "./_RangedWeapon.js"
@@ -67,19 +58,10 @@ export const Weapon = Entity(import.meta.url, {
type: IncludeIdentifier(RangedWeapon),
}),
}),
sanctified_by: Optional({
comment: "If the weapon is sanctified by a god and thus restricted to it's Blessed Ones.",
type: IncludeIdentifier(SanctifiedBy),
}),
restricted_to_cultures: Optional({
restrictedTo: Optional({
comment:
"Define if during character creation this weapon can only be bought by characters of a specific race or culture.",
type: IncludeIdentifier(RestrictedToCultures),
}),
restricted_to_traditions: Optional({
comment:
"Define if during character creation this weapon can only be bought by characters of specific magical or blessed traditions.",
type: Array(MagicalTraditionIdentifier(), { minItems: 1 }),
"Define if during character creation this weapon can only be bought by a specific subset of characters.",
type: IncludeIdentifier(RestrictedTo),
}),
src,
translations: NestedTranslationMap(
@@ -113,7 +95,7 @@ export const Weapon = Entity(import.meta.url, {
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
@@ -143,19 +125,10 @@ export const ImprovisedWeapon = TypeAlias(import.meta.url, {
type: IncludeIdentifier(RangedWeapon),
}),
}),
sanctified_by: Optional({
comment: "If the weapon is sanctified by a god and thus restricted to it's Blessed Ones.",
type: IncludeIdentifier(SanctifiedBy),
}),
restricted_to_cultures: Optional({
restrictedTo: Optional({
comment:
"Define if during character creation this weapon can only be bought by characters of a specific race or culture.",
type: IncludeIdentifier(RestrictedToCultures),
}),
restricted_to_traditions: Optional({
comment:
"Define if during character creation this weapon can only be bought by characters of specific magical or blessed traditions.",
type: Array(MagicalTraditionIdentifier(), { minItems: 1 }),
"Define if during character creation this weapon can only be bought by a specific subset of characters.",
type: IncludeIdentifier(RestrictedTo),
}),
translations: NestedTranslationMap(
Optional,
@@ -171,34 +144,8 @@ export const ImprovisedWeapon = TypeAlias(import.meta.url, {
type: String({ minLength: 1, isMarkdown: true }),
}),
},
{ minProperties: 1 }
)
{ minProperties: 1 },
),
),
}),
})
export const SanctifiedBy = TypeAlias(import.meta.url, {
name: "SanctifiedBy",
comment: "If the weapon is sanctified by a god and thus restricted to it's Blessed Ones.",
type: () => Array(BlessedTraditionIdentifier(), { minItems: 1 }),
})
const RestrictedToCultures = Enum(import.meta.url, {
name: "RestrictedToCultures",
comment:
"Define if during character creation this weapon can only be bought by characters of a specific race or culture.",
values: () => ({
CulturesOfRace: EnumCase({ type: RaceIdentifier() }),
Cultures: EnumCase({ type: IncludeIdentifier(RestrictedToSpecificCultures) }),
}),
})
const RestrictedToSpecificCultures = TypeAlias(import.meta.url, {
name: "RestrictedToSpecificCultures",
type: () =>
Object({
list: Required({
type: Array(CultureIdentifier(), { minItems: 1 }),
}),
}),
})
+137 -5
View File
@@ -1,5 +1,6 @@
import {
Array,
Boolean,
Entity,
Enum,
EnumCase,
@@ -12,6 +13,13 @@ import {
String,
TypeAlias,
} from "tsondb/schema/def"
import {
BlessedTraditionIdentifier,
CultureIdentifier,
MagicalTraditionIdentifier,
ProfessionIdentifier,
RaceIdentifier,
} from "../../_Identifier.js"
import { NestedTranslationMap } from "../../Locale.js"
import { Errata } from "../../source/_Erratum.js"
import { src } from "../../source/_PublicationRef.js"
@@ -55,7 +63,7 @@ export const DefaultItem = (sourceUrl: string, singularName: string, pluralName?
export const DefaultItemTranslations = (singularName: string) =>
NestedTranslationMap(
Required,
`${singularName}Translation`,
singularName,
Object({
name: Required({
comment: "The items name.",
@@ -76,7 +84,7 @@ export const DefaultItemTranslations = (singularName: string) =>
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
)
export const CombatUse = Enum(import.meta.url, {
@@ -137,7 +145,7 @@ export const FixedCost = TypeAlias(import.meta.url, {
"FixedCost",
Object({
wrap_in_text,
})
}),
),
}),
})
@@ -159,7 +167,7 @@ const CostRange = TypeAlias(import.meta.url, {
"CostRange",
Object({
wrap_in_text,
})
}),
),
}),
})
@@ -167,7 +175,7 @@ const CostRange = TypeAlias(import.meta.url, {
export const Weight = TypeAlias(import.meta.url, {
name: "Weight",
comment: "The weight in kg.",
type: () => Float({ minimum: { value: 0, isExclusive: true } }),
type: () => Float({ minimum: { value: 0, isExclusive: true }, fractionDigits: 3 }),
})
export const Complexity = Enum(import.meta.url, {
@@ -190,3 +198,127 @@ export const ComplexComplexity = TypeAlias(import.meta.url, {
}),
}),
})
export const RestrictedTo = TypeAlias(import.meta.url, {
name: "RestrictedTo",
comment:
"If the item is in any case restricted to a subset of characters. The differenciating aspect may be tradition, race, culture, or a profession. Each element must be applicable to a character. ",
type: () =>
Object(
{
races: Optional({
comment:
"The item is restricted to one of a list of races. If only one race is allowed, the list may only have a single element.",
type: IncludeIdentifier(RestrictedToRaces),
}),
cultures: Optional({
comment:
"The item is restricted to one of a list of cultures. If only one culture is allowed, the list may only have a single element.",
type: IncludeIdentifier(RestrictedToCultures),
}),
professions: Optional({
comment:
"The item is restricted to one of a list of professions. If only one profession is allowed, the list may only have a single element.",
type: IncludeIdentifier(RestrictedToProfessions),
}),
magicalTraditions: Optional({
comment:
"The item is restricted to one of a list of magical traditions. If only one magical tradition is allowed, the list may only have a single element.",
type: IncludeIdentifier(RestrictedToMagicalTraditions),
}),
blessedTraditions: Optional({
comment:
"The item is restricted to one of a list of blessed traditions. If only one blessed tradition is allowed, the list may only have a single element.",
type: IncludeIdentifier(RestrictedToBlessedTraditions),
}),
},
{ minProperties: 1 },
),
})
export const RestrictedToRaces = TypeAlias(import.meta.url, {
name: "RestrictedToRaces",
comment:
"The item is restricted to one of a list of races. If only one race is allowed, the list may only have a single element.",
type: () =>
Object({
scope: Required({
comment:
"The item is restricted to one of a list of races. If only one race is allowed, the list may only have a single element.",
type: Array(RaceIdentifier(), { minItems: 1, uniqueItems: true }),
}),
}),
})
export const RestrictedToCultures = TypeAlias(import.meta.url, {
name: "RestrictedToCultures",
comment:
"The item is restricted to one of a list of cultures. If only one culture is allowed, the list may only have a single element.",
type: () =>
Object({
scope: Required({
comment:
"The item is restricted to one of a list of cultures. If only one culture is allowed, the list may only have a single element.",
type: Array(CultureIdentifier(), { minItems: 1, uniqueItems: true }),
}),
}),
})
export const RestrictedToProfessions = TypeAlias(import.meta.url, {
name: "RestrictedToProfessions",
comment:
"The item is restricted to one of a list of professions. If only one profession is allowed, the list may only have a single element.",
type: () =>
Object({
scope: Required({
comment:
"The item is restricted to one of a list of professions. If only one profession is allowed, the list may only have a single element.",
type: Array(ProfessionIdentifier(), { minItems: 1, uniqueItems: true }),
}),
}),
})
export const RestrictedToMagicalTraditions = TypeAlias(import.meta.url, {
name: "RestrictedToMagicalTraditions",
comment:
"The item is restricted to one of a list of magical traditions. If only one magical tradition is allowed, the list may only have a single element.",
type: () =>
Object({
scope: Required({
comment:
"The item is restricted to one of a list of magical traditions. If only one magical tradition is allowed, the list may only have a single element.",
type: Array(MagicalTraditionIdentifier(), { minItems: 1, uniqueItems: true }),
}),
}),
})
export const RestrictedToBlessedTraditions = TypeAlias(import.meta.url, {
name: "RestrictedToBlessedTraditions",
comment:
"The item is restricted to one of a list of blessed traditions. If only one blessed tradition is allowed, the list may only have a single element. The item may also be declared as sanctified.",
type: () =>
Object({
scope: Required({
comment:
"The item is restricted to one of a list of blessed traditions. If only one blessed tradition is allowed, the list may only have a single element.",
type: IncludeIdentifier(RestrictedToBlessedTraditionsScope),
}),
isSanctifiedBy: Required({
comment: "The item is sanctified by the applicable listed tradition.",
type: Boolean(),
}),
}),
})
export const RestrictedToBlessedTraditionsScope = Enum(import.meta.url, {
name: "RestrictedToBlessedTraditionsScope",
comment:
"The item is restricted to one of a list of blessed traditions. If only one blessed tradition is allowed, the list may only have a single element. Instead of providing an specific set of traditions, the restriction may also cover a category of blessed traditions.",
values: () => ({
Specific: EnumCase({
type: Array(BlessedTraditionIdentifier(), { minItems: 1, uniqueItems: true }),
}),
Church: EnumCase({ type: null }),
Shamanistic: EnumCase({ type: null }),
}),
})
+1 -1
View File
@@ -19,7 +19,7 @@ export const ArmorType = Entity(import.meta.url, {
comment: "The armor types name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
+1 -1
View File
@@ -14,7 +14,7 @@ export const Reach = Entity(import.meta.url, {
comment: "The reachs name.",
type: String({ minLength: 1 }),
}),
})
}),
),
}),
displayName: {},
+3 -3
View File
@@ -90,7 +90,7 @@ If no tribe tradition is given, the animist power is generally available to all
errata: Optional({
type: IncludeIdentifier(Errata),
}),
})
}),
),
}),
displayName: {},
@@ -116,7 +116,7 @@ const AnimistPowerLevel = TypeAlias(import.meta.url, {
comment: "An additional effect description for this level.",
type: String({ minLength: 1, isMarkdown: true }),
}),
})
}),
),
}),
})
@@ -185,7 +185,7 @@ const AnimistPowerCostByPrimaryPatron = TypeAlias(import.meta.url, {
comment: "A note, appended to the generated string in parenthesis.",
type: IncludeIdentifier(ResponsiveTextOptional),
}),
})
}),
),
}),
})

Some files were not shown because too many files have changed in this diff Show More