@alpaca-software/40kdc-data 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/dist/bundle-schemas.d.ts +3 -0
- package/dist/bundle-schemas.js +137 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +31 -0
- package/dist/codegen-data.d.ts +1 -0
- package/dist/codegen-data.js +128 -0
- package/dist/commands/translate.d.ts +7 -0
- package/dist/commands/translate.js +238 -0
- package/dist/commands/validate-all.d.ts +3 -0
- package/dist/commands/validate-all.js +20 -0
- package/dist/commands/validate-core.d.ts +3 -0
- package/dist/commands/validate-core.js +12 -0
- package/dist/commands/validate-enrichment.d.ts +3 -0
- package/dist/commands/validate-enrichment.js +12 -0
- package/dist/convert-faction.d.ts +45 -0
- package/dist/convert-faction.js +479 -0
- package/dist/converters/configs/adepta-sororitas.d.ts +3 -0
- package/dist/converters/configs/adepta-sororitas.js +70 -0
- package/dist/converters/configs/adeptus-astartes.d.ts +3 -0
- package/dist/converters/configs/adeptus-astartes.js +74 -0
- package/dist/converters/configs/adeptus-custodes.d.ts +3 -0
- package/dist/converters/configs/adeptus-custodes.js +14 -0
- package/dist/converters/configs/adeptus-mechanicus.d.ts +3 -0
- package/dist/converters/configs/adeptus-mechanicus.js +51 -0
- package/dist/converters/configs/aeldari.d.ts +3 -0
- package/dist/converters/configs/aeldari.js +79 -0
- package/dist/converters/configs/agents-of-the-imperium.d.ts +3 -0
- package/dist/converters/configs/agents-of-the-imperium.js +57 -0
- package/dist/converters/configs/astra-militarum.d.ts +3 -0
- package/dist/converters/configs/astra-militarum.js +80 -0
- package/dist/converters/configs/black-templars.d.ts +3 -0
- package/dist/converters/configs/black-templars.js +16 -0
- package/dist/converters/configs/blood-angels.d.ts +3 -0
- package/dist/converters/configs/blood-angels.js +16 -0
- package/dist/converters/configs/chaos-daemons.d.ts +3 -0
- package/dist/converters/configs/chaos-daemons.js +40 -0
- package/dist/converters/configs/chaos-knights.d.ts +3 -0
- package/dist/converters/configs/chaos-knights.js +14 -0
- package/dist/converters/configs/chaos-space-marines.d.ts +3 -0
- package/dist/converters/configs/chaos-space-marines.js +95 -0
- package/dist/converters/configs/crimson-fists.d.ts +3 -0
- package/dist/converters/configs/crimson-fists.js +16 -0
- package/dist/converters/configs/dark-angels.d.ts +3 -0
- package/dist/converters/configs/dark-angels.js +16 -0
- package/dist/converters/configs/death-guard.d.ts +3 -0
- package/dist/converters/configs/death-guard.js +30 -0
- package/dist/converters/configs/deathwatch.d.ts +3 -0
- package/dist/converters/configs/deathwatch.js +16 -0
- package/dist/converters/configs/drukhari.d.ts +3 -0
- package/dist/converters/configs/drukhari.js +51 -0
- package/dist/converters/configs/emperors-children.d.ts +3 -0
- package/dist/converters/configs/emperors-children.js +38 -0
- package/dist/converters/configs/genestealer-cults.d.ts +3 -0
- package/dist/converters/configs/genestealer-cults.js +36 -0
- package/dist/converters/configs/grey-knights.d.ts +3 -0
- package/dist/converters/configs/grey-knights.js +39 -0
- package/dist/converters/configs/imperial-fists.d.ts +3 -0
- package/dist/converters/configs/imperial-fists.js +16 -0
- package/dist/converters/configs/imperial-knights.d.ts +3 -0
- package/dist/converters/configs/imperial-knights.js +14 -0
- package/dist/converters/configs/iron-hands.d.ts +3 -0
- package/dist/converters/configs/iron-hands.js +16 -0
- package/dist/converters/configs/leagues-of-votann.d.ts +3 -0
- package/dist/converters/configs/leagues-of-votann.js +32 -0
- package/dist/converters/configs/necrons.d.ts +3 -0
- package/dist/converters/configs/necrons.js +19 -0
- package/dist/converters/configs/orks.d.ts +3 -0
- package/dist/converters/configs/orks.js +71 -0
- package/dist/converters/configs/raven-guard.d.ts +3 -0
- package/dist/converters/configs/raven-guard.js +16 -0
- package/dist/converters/configs/salamanders.d.ts +3 -0
- package/dist/converters/configs/salamanders.js +16 -0
- package/dist/converters/configs/space-wolves.d.ts +3 -0
- package/dist/converters/configs/space-wolves.js +16 -0
- package/dist/converters/configs/tau-empire.d.ts +3 -0
- package/dist/converters/configs/tau-empire.js +44 -0
- package/dist/converters/configs/thousand-sons.d.ts +3 -0
- package/dist/converters/configs/thousand-sons.js +30 -0
- package/dist/converters/configs/tyranids.d.ts +3 -0
- package/dist/converters/configs/tyranids.js +27 -0
- package/dist/converters/configs/ultramarines.d.ts +3 -0
- package/dist/converters/configs/ultramarines.js +16 -0
- package/dist/converters/configs/white-scars.d.ts +3 -0
- package/dist/converters/configs/white-scars.js +16 -0
- package/dist/converters/configs/world-eaters.d.ts +3 -0
- package/dist/converters/configs/world-eaters.js +43 -0
- package/dist/converters/faction-config.d.ts +53 -0
- package/dist/converters/faction-config.js +22 -0
- package/dist/converters/id-generator.d.ts +14 -0
- package/dist/converters/id-generator.js +65 -0
- package/dist/converters/keyword-filter.d.ts +26 -0
- package/dist/converters/keyword-filter.js +78 -0
- package/dist/converters/stat-parser.d.ts +22 -0
- package/dist/converters/stat-parser.js +84 -0
- package/dist/converters/view-selector.d.ts +54 -0
- package/dist/converters/view-selector.js +96 -0
- package/dist/converters/weapon-dedup.d.ts +60 -0
- package/dist/converters/weapon-dedup.js +120 -0
- package/dist/data/bundle.generated.d.ts +3 -0
- package/dist/data/bundle.generated.js +3 -0
- package/dist/data/collection.d.ts +64 -0
- package/dist/data/collection.js +118 -0
- package/dist/data/dataset.d.ts +50 -0
- package/dist/data/dataset.js +134 -0
- package/dist/data/entities.d.ts +80 -0
- package/dist/data/entities.js +133 -0
- package/dist/data/index.d.ts +59 -0
- package/dist/data/index.js +57 -0
- package/dist/data/normalize.d.ts +29 -0
- package/dist/data/normalize.js +37 -0
- package/dist/data/types.d.ts +43 -0
- package/dist/data/types.js +25 -0
- package/dist/generated.d.ts +1084 -0
- package/dist/generated.js +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/known-support-10e.d.ts +31 -0
- package/dist/known-support-10e.js +113 -0
- package/dist/port-10e-faction.d.ts +52 -0
- package/dist/port-10e-faction.js +413 -0
- package/dist/report.d.ts +3 -0
- package/dist/report.js +31 -0
- package/dist/schema-loader.d.ts +15 -0
- package/dist/schema-loader.js +79 -0
- package/dist/validate.d.ts +21 -0
- package/dist/validate.js +124 -0
- package/package.json +77 -0
- package/schemas/$defs/common.schema.json +86 -0
- package/schemas/$defs/game-version-ref.schema.json +11 -0
- package/schemas/core/deployment-pattern.schema.json +102 -0
- package/schemas/core/detachment.schema.json +56 -0
- package/schemas/core/enhancement.schema.json +46 -0
- package/schemas/core/faction.schema.json +29 -0
- package/schemas/core/force-disposition.schema.json +22 -0
- package/schemas/core/game-version.schema.json +20 -0
- package/schemas/core/leader-attachment.schema.json +18 -0
- package/schemas/core/mission-matchup.schema.json +25 -0
- package/schemas/core/mission.schema.json +42 -0
- package/schemas/core/roster.schema.json +203 -0
- package/schemas/core/secondary-card.schema.json +195 -0
- package/schemas/core/stratagem.schema.json +58 -0
- package/schemas/core/terrain-layout.schema.json +135 -0
- package/schemas/core/unit-composition.schema.json +38 -0
- package/schemas/core/unit.schema.json +125 -0
- package/schemas/core/wargear-option.schema.json +47 -0
- package/schemas/core/weapon.schema.json +56 -0
- package/schemas/enrichment/ability-dsl/ability.schema.json +60 -0
- package/schemas/enrichment/ability-dsl/condition.schema.json +48 -0
- package/schemas/enrichment/ability-dsl/effect.schema.json +145 -0
- package/schemas/enrichment/ability-dsl/scope.schema.json +12 -0
- package/schemas/enrichment/interaction-flag.schema.json +17 -0
- package/schemas/enrichment/phase-mapping.schema.json +14 -0
- package/schemas/enrichment/resource-pool.schema.json +36 -0
- package/schemas/enrichment/timing-flag.schema.json +28 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
export declare const SCHEMAS_ROOT: string;
|
|
3
|
+
/**
|
|
4
|
+
* Recursively find all .schema.json files under a directory.
|
|
5
|
+
*/
|
|
6
|
+
export declare function findSchemaFiles(dir: string): string[];
|
|
7
|
+
/**
|
|
8
|
+
* Create and configure an AJV instance with all project schemas loaded.
|
|
9
|
+
* Schemas are registered by their $id so $ref resolution works across files.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createValidator(): Ajv;
|
|
12
|
+
/**
|
|
13
|
+
* Return the list of all loaded schema $id values.
|
|
14
|
+
*/
|
|
15
|
+
export declare function listSchemaIds(): string[];
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import addFormats from "ajv-formats";
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the schema tree across both layouts:
|
|
9
|
+
* - in-repo / dev: schemas live in the sibling top-level `schemas/` dir
|
|
10
|
+
* (`../../schemas` from `dist/` or `src/`) — always the live source.
|
|
11
|
+
* - packaged: once published, schemas are copied into the package root, so
|
|
12
|
+
* `<pkg>/schemas` sits one level above the compiled file in `dist/`
|
|
13
|
+
* (`../schemas`). When installed, the repo path resolves outside the
|
|
14
|
+
* package and doesn't exist, so this branch is taken.
|
|
15
|
+
* Repo path wins when present so dev runs never read a stale `copy:schemas`
|
|
16
|
+
* artifact; the packaged copy is the fallback for installed consumers.
|
|
17
|
+
*/
|
|
18
|
+
function resolveSchemasRoot() {
|
|
19
|
+
const repo = resolve(__dirname, "../../schemas");
|
|
20
|
+
const packaged = resolve(__dirname, "../schemas");
|
|
21
|
+
return existsSync(repo) ? repo : packaged;
|
|
22
|
+
}
|
|
23
|
+
export const SCHEMAS_ROOT = resolveSchemasRoot();
|
|
24
|
+
/**
|
|
25
|
+
* Recursively find all .schema.json files under a directory.
|
|
26
|
+
*/
|
|
27
|
+
export function findSchemaFiles(dir) {
|
|
28
|
+
const results = [];
|
|
29
|
+
for (const entry of readdirSync(dir)) {
|
|
30
|
+
const full = join(dir, entry);
|
|
31
|
+
if (statSync(full).isDirectory()) {
|
|
32
|
+
results.push(...findSchemaFiles(full));
|
|
33
|
+
}
|
|
34
|
+
else if (entry.endsWith(".schema.json")) {
|
|
35
|
+
results.push(full);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return results;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create and configure an AJV instance with all project schemas loaded.
|
|
42
|
+
* Schemas are registered by their $id so $ref resolution works across files.
|
|
43
|
+
*/
|
|
44
|
+
export function createValidator() {
|
|
45
|
+
const ajv = new Ajv({
|
|
46
|
+
strict: false,
|
|
47
|
+
allErrors: true,
|
|
48
|
+
validateSchema: false,
|
|
49
|
+
});
|
|
50
|
+
addFormats(ajv);
|
|
51
|
+
const schemaFiles = findSchemaFiles(SCHEMAS_ROOT);
|
|
52
|
+
const schemas = schemaFiles.map((file) => {
|
|
53
|
+
const raw = readFileSync(file, "utf-8");
|
|
54
|
+
return JSON.parse(raw);
|
|
55
|
+
});
|
|
56
|
+
// Register all schemas by $id
|
|
57
|
+
for (const schema of schemas) {
|
|
58
|
+
const id = schema["$id"];
|
|
59
|
+
if (id) {
|
|
60
|
+
ajv.addSchema(schema, id);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return ajv;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Return the list of all loaded schema $id values.
|
|
67
|
+
*/
|
|
68
|
+
export function listSchemaIds() {
|
|
69
|
+
const schemaFiles = findSchemaFiles(SCHEMAS_ROOT);
|
|
70
|
+
const ids = [];
|
|
71
|
+
for (const file of schemaFiles) {
|
|
72
|
+
const raw = readFileSync(file, "utf-8");
|
|
73
|
+
const schema = JSON.parse(raw);
|
|
74
|
+
if (schema["$id"]) {
|
|
75
|
+
ids.push(schema["$id"]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return ids;
|
|
79
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type Ajv from "ajv";
|
|
2
|
+
export interface ValidationError {
|
|
3
|
+
file: string;
|
|
4
|
+
index: number;
|
|
5
|
+
errors: Array<{
|
|
6
|
+
path: string;
|
|
7
|
+
message: string;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
export interface ValidationResult {
|
|
11
|
+
totalFiles: number;
|
|
12
|
+
totalItems: number;
|
|
13
|
+
passed: number;
|
|
14
|
+
failed: number;
|
|
15
|
+
errors: ValidationError[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Validate all data files matching the given glob pattern.
|
|
19
|
+
* Each data file is expected to be a JSON array; each element is validated individually.
|
|
20
|
+
*/
|
|
21
|
+
export declare function validateFiles(ajv: Ajv, pattern: string, cwd?: string): Promise<ValidationResult>;
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { glob } from "glob";
|
|
3
|
+
import { resolve, basename } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
6
|
+
const DATA_ROOT = resolve(__dirname, "../../data");
|
|
7
|
+
/**
|
|
8
|
+
* Map from data file base-name prefix to schema $id.
|
|
9
|
+
*/
|
|
10
|
+
const SCHEMA_MAP = {
|
|
11
|
+
factions: "https://40kdc.dev/schemas/core/faction.schema.json",
|
|
12
|
+
units: "https://40kdc.dev/schemas/core/unit.schema.json",
|
|
13
|
+
weapons: "https://40kdc.dev/schemas/core/weapon.schema.json",
|
|
14
|
+
"game-versions": "https://40kdc.dev/schemas/core/game-version.schema.json",
|
|
15
|
+
detachments: "https://40kdc.dev/schemas/core/detachment.schema.json",
|
|
16
|
+
enhancements: "https://40kdc.dev/schemas/core/enhancement.schema.json",
|
|
17
|
+
stratagems: "https://40kdc.dev/schemas/core/stratagem.schema.json",
|
|
18
|
+
"wargear-options": "https://40kdc.dev/schemas/core/wargear-option.schema.json",
|
|
19
|
+
"leader-attachments": "https://40kdc.dev/schemas/core/leader-attachment.schema.json",
|
|
20
|
+
"unit-compositions": "https://40kdc.dev/schemas/core/unit-composition.schema.json",
|
|
21
|
+
"force-dispositions": "https://40kdc.dev/schemas/core/force-disposition.schema.json",
|
|
22
|
+
"deployment-patterns": "https://40kdc.dev/schemas/core/deployment-pattern.schema.json",
|
|
23
|
+
"mission-matchups": "https://40kdc.dev/schemas/core/mission-matchup.schema.json",
|
|
24
|
+
missions: "https://40kdc.dev/schemas/core/mission.schema.json",
|
|
25
|
+
"secondary-cards": "https://40kdc.dev/schemas/core/secondary-card.schema.json",
|
|
26
|
+
"terrain-layouts": "https://40kdc.dev/schemas/core/terrain-layout.schema.json",
|
|
27
|
+
"phase-mappings": "https://40kdc.dev/schemas/enrichment/phase-mapping.schema.json",
|
|
28
|
+
"timing-flags": "https://40kdc.dev/schemas/enrichment/timing-flag.schema.json",
|
|
29
|
+
"interaction-flags": "https://40kdc.dev/schemas/enrichment/interaction-flag.schema.json",
|
|
30
|
+
abilities: "https://40kdc.dev/schemas/enrichment/ability-dsl/ability.schema.json",
|
|
31
|
+
"resource-pools": "https://40kdc.dev/schemas/enrichment/resource-pool.schema.json",
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Determine which schema $id to use for a given data file path.
|
|
35
|
+
* Convention: the file's base name prefix (before the first dot) maps to a schema.
|
|
36
|
+
*/
|
|
37
|
+
function resolveSchemaId(filePath) {
|
|
38
|
+
const base = basename(filePath);
|
|
39
|
+
for (const [prefix, schemaId] of Object.entries(SCHEMA_MAP)) {
|
|
40
|
+
if (base.startsWith(prefix)) {
|
|
41
|
+
return schemaId;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Validate all data files matching the given glob pattern.
|
|
48
|
+
* Each data file is expected to be a JSON array; each element is validated individually.
|
|
49
|
+
*/
|
|
50
|
+
export async function validateFiles(ajv, pattern, cwd) {
|
|
51
|
+
const root = cwd ?? DATA_ROOT;
|
|
52
|
+
const files = await glob(pattern, { cwd: root, absolute: true });
|
|
53
|
+
const result = {
|
|
54
|
+
totalFiles: files.length,
|
|
55
|
+
totalItems: 0,
|
|
56
|
+
passed: 0,
|
|
57
|
+
failed: 0,
|
|
58
|
+
errors: [],
|
|
59
|
+
};
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
const schemaId = resolveSchemaId(file);
|
|
62
|
+
if (!schemaId) {
|
|
63
|
+
result.errors.push({
|
|
64
|
+
file,
|
|
65
|
+
index: -1,
|
|
66
|
+
errors: [{ path: "", message: `No schema mapping found for file: ${basename(file)}` }],
|
|
67
|
+
});
|
|
68
|
+
result.failed++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const validate = ajv.getSchema(schemaId);
|
|
72
|
+
if (!validate) {
|
|
73
|
+
result.errors.push({
|
|
74
|
+
file,
|
|
75
|
+
index: -1,
|
|
76
|
+
errors: [{ path: "", message: `Schema not found: ${schemaId}` }],
|
|
77
|
+
});
|
|
78
|
+
result.failed++;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
let data;
|
|
82
|
+
try {
|
|
83
|
+
const raw = readFileSync(file, "utf-8");
|
|
84
|
+
data = JSON.parse(raw);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
result.errors.push({
|
|
88
|
+
file,
|
|
89
|
+
index: -1,
|
|
90
|
+
errors: [{ path: "", message: `Failed to parse JSON: ${err.message}` }],
|
|
91
|
+
});
|
|
92
|
+
result.failed++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (!Array.isArray(data)) {
|
|
96
|
+
result.errors.push({
|
|
97
|
+
file,
|
|
98
|
+
index: -1,
|
|
99
|
+
errors: [{ path: "", message: "Data file must be a JSON array" }],
|
|
100
|
+
});
|
|
101
|
+
result.failed++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
for (let i = 0; i < data.length; i++) {
|
|
105
|
+
result.totalItems++;
|
|
106
|
+
const valid = validate(data[i]);
|
|
107
|
+
if (valid) {
|
|
108
|
+
result.passed++;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
result.failed++;
|
|
112
|
+
result.errors.push({
|
|
113
|
+
file,
|
|
114
|
+
index: i,
|
|
115
|
+
errors: (validate.errors ?? []).map((e) => ({
|
|
116
|
+
path: e.instancePath || "/",
|
|
117
|
+
message: e.message ?? "Unknown validation error",
|
|
118
|
+
})),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alpaca-software/40kdc-data",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "The 40kdc Warhammer 40K dataset behind a linked, typed API — find units, follow them to their weapons, abilities, phases, and factions. Also validates data against the canonical JSON Schemas.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"warhammer",
|
|
8
|
+
"warhammer-40k",
|
|
9
|
+
"40k",
|
|
10
|
+
"wargaming",
|
|
11
|
+
"dataset",
|
|
12
|
+
"tabletop",
|
|
13
|
+
"json-schema"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"homepage": "https://github.com/Tabletop-Developer-Consortium/40kdc-data/tree/main/tools#readme",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/Tabletop-Developer-Consortium/40kdc-data.git",
|
|
20
|
+
"directory": "tools"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/Tabletop-Developer-Consortium/40kdc-data/issues"
|
|
24
|
+
},
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./schemas/*": "./schemas/*"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"schemas",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"bin": {
|
|
40
|
+
"40kdc-validate": "./dist/cli.js"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public",
|
|
44
|
+
"registry": "https://registry.npmjs.org"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "npm run codegen:data && tsc",
|
|
48
|
+
"codegen:data": "tsx src/codegen-data.ts",
|
|
49
|
+
"docs:api": "typedoc",
|
|
50
|
+
"validate": "tsx src/cli.ts validate-all",
|
|
51
|
+
"validate:core": "tsx src/cli.ts validate-core",
|
|
52
|
+
"validate:enrichment": "tsx src/cli.ts validate-enrichment",
|
|
53
|
+
"translate": "tsx src/cli.ts translate",
|
|
54
|
+
"bundle:schemas": "tsx src/bundle-schemas.ts",
|
|
55
|
+
"codegen:types": "json2ts -i ../crates/wh40kdc/schemas/bundled.schema.json -o src/generated.ts --unreachableDefinitions --bannerComment \"/* Generated from crates/wh40kdc/schemas/bundled.schema.json by 'npm run codegen:types'. DO NOT EDIT BY HAND. */\"",
|
|
56
|
+
"copy:schemas": "rm -rf schemas && cp -R ../schemas schemas",
|
|
57
|
+
"prepack": "npm run build && npm run copy:schemas",
|
|
58
|
+
"pretest": "npm run codegen:data",
|
|
59
|
+
"test": "vitest run"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"ajv": "^8.17.0",
|
|
63
|
+
"ajv-formats": "^3.0.0",
|
|
64
|
+
"chalk": "^5.3.0",
|
|
65
|
+
"commander": "^12.0.0",
|
|
66
|
+
"glob": "^11.0.0"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@types/node": "^22.0.0",
|
|
70
|
+
"json-schema-to-typescript": "^15.0.4",
|
|
71
|
+
"tsx": "^4.0.0",
|
|
72
|
+
"typedoc": "^0.28.0",
|
|
73
|
+
"typedoc-plugin-markdown": "^4.3.0",
|
|
74
|
+
"typescript": "^5.6.0",
|
|
75
|
+
"vitest": "^2.0.0"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://40kdc.dev/schemas/defs/common.schema.json",
|
|
4
|
+
"title": "40kdc Common Definitions",
|
|
5
|
+
"$defs": {
|
|
6
|
+
"entity-id": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$",
|
|
9
|
+
"minLength": 2,
|
|
10
|
+
"maxLength": 128,
|
|
11
|
+
"description": "Kebab-case identifier"
|
|
12
|
+
},
|
|
13
|
+
"edition": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"pattern": "^\\d{1,2}(th)?$",
|
|
16
|
+
"description": "Game edition, e.g. '10th' or '11'"
|
|
17
|
+
},
|
|
18
|
+
"dataslate-version": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"pattern": "^(\\d{4}-q[1-4]|[a-z0-9][a-z0-9-]*[a-z0-9])$",
|
|
21
|
+
"description": "Dataslate version: a quarterly tag (e.g. '2025-q3') or a named kebab-case slug for non-quarterly slates (e.g. 'pre-launch-provisional')"
|
|
22
|
+
},
|
|
23
|
+
"keyword": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"minLength": 1,
|
|
26
|
+
"maxLength": 64
|
|
27
|
+
},
|
|
28
|
+
"keyword-list": {
|
|
29
|
+
"type": "array",
|
|
30
|
+
"items": { "$ref": "#/$defs/keyword" },
|
|
31
|
+
"uniqueItems": true
|
|
32
|
+
},
|
|
33
|
+
"stat-value": {
|
|
34
|
+
"description": "A stat that can be a fixed number or a dice expression",
|
|
35
|
+
"oneOf": [
|
|
36
|
+
{ "type": "integer", "minimum": 0 },
|
|
37
|
+
{ "type": "string", "pattern": "^\\d*[Dd]\\d+(\\+\\d+)?$" }
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"contributor-ref": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "GitHub handle or '40kdc-community'"
|
|
43
|
+
},
|
|
44
|
+
"phase": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"enum": ["command", "movement", "shooting", "charge", "fight"],
|
|
47
|
+
"description": "The five official game phases. Unchanged between 10th and 11th edition — 11e reorders Pile In timing within the Fight phase but adds no top-level phase."
|
|
48
|
+
},
|
|
49
|
+
"phase-list": {
|
|
50
|
+
"type": "array",
|
|
51
|
+
"items": { "$ref": "#/$defs/phase" },
|
|
52
|
+
"uniqueItems": true,
|
|
53
|
+
"minItems": 1
|
|
54
|
+
},
|
|
55
|
+
"source-type": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"enum": ["ability", "stratagem", "enhancement", "detachment-rule", "faction-rule"],
|
|
58
|
+
"description": "Type of game element that is the source of an enrichment entry"
|
|
59
|
+
},
|
|
60
|
+
"player-turn": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"enum": ["your-turn", "opponent-turn", "either"],
|
|
63
|
+
"description": "Which player's turn this applies during"
|
|
64
|
+
},
|
|
65
|
+
"battle-size": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"enum": ["incursion", "strike-force"],
|
|
68
|
+
"description": "11e battle size, which sets the army's points limit and detachment-point budget: 'incursion' = 1000 pts / 2 detachment points; 'strike-force' = 2000 pts / 3 detachment points."
|
|
69
|
+
},
|
|
70
|
+
"force-disposition-id": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"enum": ["take-and-hold", "disruption", "purge-the-foe", "priority-assets", "reconnaissance"],
|
|
73
|
+
"description": "One of the five confirmed 11e launch Force Dispositions. Shared by force-disposition entities and the mission-matchup matrix."
|
|
74
|
+
},
|
|
75
|
+
"vec2": {
|
|
76
|
+
"type": "object",
|
|
77
|
+
"description": "A 2D point in board inches. Origin at a board corner; JSON uses y-down (downstream renderers may flip to y-up).",
|
|
78
|
+
"properties": {
|
|
79
|
+
"x": { "type": "number" },
|
|
80
|
+
"y": { "type": "number" }
|
|
81
|
+
},
|
|
82
|
+
"required": ["x", "y"],
|
|
83
|
+
"additionalProperties": false
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://40kdc.dev/schemas/defs/game-version-ref.schema.json",
|
|
4
|
+
"title": "Game Version Reference",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"edition": { "$ref": "common.schema.json#/$defs/edition" },
|
|
8
|
+
"dataslate": { "$ref": "common.schema.json#/$defs/dataslate-version" }
|
|
9
|
+
},
|
|
10
|
+
"required": ["edition", "dataslate"]
|
|
11
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://40kdc.dev/schemas/core/deployment-pattern.schema.json",
|
|
4
|
+
"title": "Deployment Pattern",
|
|
5
|
+
"description": "A deployment map: per-side deployment zones, objective positions, and (11e) per-side territory polygons. Pattern geometry carries forward unchanged from 10th edition; downstream tooling (e.g. bevy-deploy-helper) consumes this as the canonical encoding.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"$defs": {
|
|
8
|
+
"zone-shape": {
|
|
9
|
+
"description": "A zone footprint, expressed as an axis-aligned rectangle or an explicit polygon. Vertices/extent are relative to the owning element's position.",
|
|
10
|
+
"oneOf": [
|
|
11
|
+
{
|
|
12
|
+
"type": "object",
|
|
13
|
+
"properties": {
|
|
14
|
+
"type": { "const": "rectangle" },
|
|
15
|
+
"width": { "type": "number", "exclusiveMinimum": 0 },
|
|
16
|
+
"height": { "type": "number", "exclusiveMinimum": 0 }
|
|
17
|
+
},
|
|
18
|
+
"required": ["type", "width", "height"],
|
|
19
|
+
"additionalProperties": false
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"type": "object",
|
|
23
|
+
"properties": {
|
|
24
|
+
"type": { "const": "polygon" },
|
|
25
|
+
"points": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"items": { "$ref": "../defs/common.schema.json#/$defs/vec2" },
|
|
28
|
+
"minItems": 3
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"required": ["type", "points"],
|
|
32
|
+
"additionalProperties": false
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"side": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"enum": ["attacker", "defender"],
|
|
39
|
+
"description": "Which player a zone or territory belongs to."
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"properties": {
|
|
43
|
+
"id": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
|
|
44
|
+
"name": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
45
|
+
"source": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"minLength": 1,
|
|
48
|
+
"maxLength": 64,
|
|
49
|
+
"description": "Mission pack or source the pattern originates from (e.g. 'leviathan')."
|
|
50
|
+
},
|
|
51
|
+
"description": { "type": "string" },
|
|
52
|
+
"zones": {
|
|
53
|
+
"type": "array",
|
|
54
|
+
"minItems": 1,
|
|
55
|
+
"description": "Per-side deployment zones.",
|
|
56
|
+
"items": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"properties": {
|
|
59
|
+
"player": { "$ref": "#/$defs/side" },
|
|
60
|
+
"name": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
61
|
+
"shape": { "$ref": "#/$defs/zone-shape" },
|
|
62
|
+
"position": { "$ref": "../defs/common.schema.json#/$defs/vec2" },
|
|
63
|
+
"color": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"pattern": "^#[0-9a-fA-F]{6}$",
|
|
66
|
+
"description": "Hex render color for the zone overlay."
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"required": ["player", "shape", "position"],
|
|
70
|
+
"additionalProperties": false
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"territories": {
|
|
74
|
+
"type": "array",
|
|
75
|
+
"description": "11e per-side territory polygons, mirroring the deployment-zone shape (e.g. the band between a deployment zone and the midline). Empty until authored.",
|
|
76
|
+
"items": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"properties": {
|
|
79
|
+
"player": { "$ref": "#/$defs/side" },
|
|
80
|
+
"shape": { "$ref": "#/$defs/zone-shape" },
|
|
81
|
+
"position": { "$ref": "../defs/common.schema.json#/$defs/vec2" }
|
|
82
|
+
},
|
|
83
|
+
"required": ["player", "shape", "position"],
|
|
84
|
+
"additionalProperties": false
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"objectives": {
|
|
88
|
+
"type": "array",
|
|
89
|
+
"description": "Objective-marker positions on the board.",
|
|
90
|
+
"items": { "$ref": "../defs/common.schema.json#/$defs/vec2" }
|
|
91
|
+
},
|
|
92
|
+
"recommended_terrain_layout_ids": {
|
|
93
|
+
"type": "array",
|
|
94
|
+
"uniqueItems": true,
|
|
95
|
+
"description": "Ids of recommended terrain-layout entities (resolved once terrain-layout data is authored).",
|
|
96
|
+
"items": { "$ref": "../defs/common.schema.json#/$defs/entity-id" }
|
|
97
|
+
},
|
|
98
|
+
"game_version": { "$ref": "../defs/game-version-ref.schema.json" }
|
|
99
|
+
},
|
|
100
|
+
"required": ["id", "name", "zones", "game_version"],
|
|
101
|
+
"additionalProperties": false
|
|
102
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://40kdc.dev/schemas/core/detachment.schema.json",
|
|
4
|
+
"title": "Detachment",
|
|
5
|
+
"description": "A detachment option within a faction, providing a detachment rule, enhancements, and stratagems.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"id": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
|
|
9
|
+
"name": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
10
|
+
"faction_id": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
|
|
11
|
+
"detachment_rule_id": {
|
|
12
|
+
"oneOf": [
|
|
13
|
+
{ "$ref": "../defs/common.schema.json#/$defs/entity-id" },
|
|
14
|
+
{ "type": "null" }
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"detachment_points": {
|
|
18
|
+
"oneOf": [
|
|
19
|
+
{ "type": "integer", "minimum": 1, "maximum": 3 },
|
|
20
|
+
{ "type": "null" }
|
|
21
|
+
],
|
|
22
|
+
"description": "11e: the detachment-point cost (1–3) charged against the army's detachment-point budget. null when not yet assigned."
|
|
23
|
+
},
|
|
24
|
+
"force_dispositions": {
|
|
25
|
+
"type": "array",
|
|
26
|
+
"items": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
|
|
27
|
+
"uniqueItems": true,
|
|
28
|
+
"description": "11e: ids of the Force Disposition entities this detachment grants. Empty until assigned."
|
|
29
|
+
},
|
|
30
|
+
"enhancement_ids": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"items": { "$ref": "../defs/common.schema.json#/$defs/entity-id" }
|
|
33
|
+
},
|
|
34
|
+
"stratagem_ids": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": { "$ref": "../defs/common.schema.json#/$defs/entity-id" }
|
|
37
|
+
},
|
|
38
|
+
"restrictions": {
|
|
39
|
+
"oneOf": [
|
|
40
|
+
{
|
|
41
|
+
"type": "object",
|
|
42
|
+
"properties": {
|
|
43
|
+
"required_keywords": { "$ref": "../defs/common.schema.json#/$defs/keyword-list" },
|
|
44
|
+
"excluded_keywords": { "$ref": "../defs/common.schema.json#/$defs/keyword-list" },
|
|
45
|
+
"notes": { "type": "string" }
|
|
46
|
+
},
|
|
47
|
+
"additionalProperties": false
|
|
48
|
+
},
|
|
49
|
+
{ "type": "null" }
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"game_version": { "$ref": "../defs/game-version-ref.schema.json" }
|
|
53
|
+
},
|
|
54
|
+
"required": ["id", "name", "faction_id", "game_version"],
|
|
55
|
+
"additionalProperties": false
|
|
56
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://40kdc.dev/schemas/core/enhancement.schema.json",
|
|
4
|
+
"title": "Enhancement",
|
|
5
|
+
"description": "A purchasable upgrade for a character unit, provided by a detachment.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"id": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
|
|
9
|
+
"name": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
10
|
+
"detachment_id": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
|
|
11
|
+
"cost": { "type": "integer", "minimum": 0 },
|
|
12
|
+
"points_provisional": {
|
|
13
|
+
"type": "boolean",
|
|
14
|
+
"default": false,
|
|
15
|
+
"description": "True when the cost is carried over provisionally (e.g. seeded from a prior edition during migration) and not yet confirmed against the current dataslate."
|
|
16
|
+
},
|
|
17
|
+
"upgrade_tag": {
|
|
18
|
+
"type": "boolean",
|
|
19
|
+
"default": false,
|
|
20
|
+
"description": "11e: when true, this enhancement applies to up to `max_targets` non-character units while counting as a single Enhancement choice."
|
|
21
|
+
},
|
|
22
|
+
"max_targets": {
|
|
23
|
+
"type": "integer",
|
|
24
|
+
"minimum": 1,
|
|
25
|
+
"default": 1,
|
|
26
|
+
"description": "Number of units this enhancement may be applied to. Only meaningful when `upgrade_tag` is true; defaults to 1."
|
|
27
|
+
},
|
|
28
|
+
"keyword_restrictions": { "$ref": "../defs/common.schema.json#/$defs/keyword-list" },
|
|
29
|
+
"exclusion_keywords": {
|
|
30
|
+
"oneOf": [
|
|
31
|
+
{ "$ref": "../defs/common.schema.json#/$defs/keyword-list" },
|
|
32
|
+
{ "type": "null" }
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"ability_id": {
|
|
36
|
+
"oneOf": [
|
|
37
|
+
{ "$ref": "../defs/common.schema.json#/$defs/entity-id" },
|
|
38
|
+
{ "type": "null" }
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"is_unique": { "type": "boolean", "default": true },
|
|
42
|
+
"game_version": { "$ref": "../defs/game-version-ref.schema.json" }
|
|
43
|
+
},
|
|
44
|
+
"required": ["id", "name", "detachment_id", "cost", "game_version"],
|
|
45
|
+
"additionalProperties": false
|
|
46
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://40kdc.dev/schemas/core/faction.schema.json",
|
|
4
|
+
"title": "Faction",
|
|
5
|
+
"description": "A playable faction or sub-faction.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"id": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
|
|
9
|
+
"name": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
10
|
+
"parent_faction_id": {
|
|
11
|
+
"oneOf": [
|
|
12
|
+
{ "$ref": "../defs/common.schema.json#/$defs/entity-id" },
|
|
13
|
+
{ "type": "null" }
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"game_version": { "$ref": "../defs/game-version-ref.schema.json" },
|
|
17
|
+
"keywords": { "$ref": "../defs/common.schema.json#/$defs/keyword-list" },
|
|
18
|
+
"aliases": { "type": "array", "items": { "type": "string" } },
|
|
19
|
+
"faction_rule_id": {
|
|
20
|
+
"oneOf": [
|
|
21
|
+
{ "$ref": "../defs/common.schema.json#/$defs/entity-id" },
|
|
22
|
+
{ "type": "null" }
|
|
23
|
+
],
|
|
24
|
+
"description": "Reference to the faction-wide ability (e.g., Oath of Moment)"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"required": ["id", "name", "game_version"],
|
|
28
|
+
"additionalProperties": false
|
|
29
|
+
}
|