@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.
Files changed (155) hide show
  1. package/README.md +78 -0
  2. package/dist/bundle-schemas.d.ts +3 -0
  3. package/dist/bundle-schemas.js +137 -0
  4. package/dist/cli.d.ts +1 -0
  5. package/dist/cli.js +31 -0
  6. package/dist/codegen-data.d.ts +1 -0
  7. package/dist/codegen-data.js +128 -0
  8. package/dist/commands/translate.d.ts +7 -0
  9. package/dist/commands/translate.js +238 -0
  10. package/dist/commands/validate-all.d.ts +3 -0
  11. package/dist/commands/validate-all.js +20 -0
  12. package/dist/commands/validate-core.d.ts +3 -0
  13. package/dist/commands/validate-core.js +12 -0
  14. package/dist/commands/validate-enrichment.d.ts +3 -0
  15. package/dist/commands/validate-enrichment.js +12 -0
  16. package/dist/convert-faction.d.ts +45 -0
  17. package/dist/convert-faction.js +479 -0
  18. package/dist/converters/configs/adepta-sororitas.d.ts +3 -0
  19. package/dist/converters/configs/adepta-sororitas.js +70 -0
  20. package/dist/converters/configs/adeptus-astartes.d.ts +3 -0
  21. package/dist/converters/configs/adeptus-astartes.js +74 -0
  22. package/dist/converters/configs/adeptus-custodes.d.ts +3 -0
  23. package/dist/converters/configs/adeptus-custodes.js +14 -0
  24. package/dist/converters/configs/adeptus-mechanicus.d.ts +3 -0
  25. package/dist/converters/configs/adeptus-mechanicus.js +51 -0
  26. package/dist/converters/configs/aeldari.d.ts +3 -0
  27. package/dist/converters/configs/aeldari.js +79 -0
  28. package/dist/converters/configs/agents-of-the-imperium.d.ts +3 -0
  29. package/dist/converters/configs/agents-of-the-imperium.js +57 -0
  30. package/dist/converters/configs/astra-militarum.d.ts +3 -0
  31. package/dist/converters/configs/astra-militarum.js +80 -0
  32. package/dist/converters/configs/black-templars.d.ts +3 -0
  33. package/dist/converters/configs/black-templars.js +16 -0
  34. package/dist/converters/configs/blood-angels.d.ts +3 -0
  35. package/dist/converters/configs/blood-angels.js +16 -0
  36. package/dist/converters/configs/chaos-daemons.d.ts +3 -0
  37. package/dist/converters/configs/chaos-daemons.js +40 -0
  38. package/dist/converters/configs/chaos-knights.d.ts +3 -0
  39. package/dist/converters/configs/chaos-knights.js +14 -0
  40. package/dist/converters/configs/chaos-space-marines.d.ts +3 -0
  41. package/dist/converters/configs/chaos-space-marines.js +95 -0
  42. package/dist/converters/configs/crimson-fists.d.ts +3 -0
  43. package/dist/converters/configs/crimson-fists.js +16 -0
  44. package/dist/converters/configs/dark-angels.d.ts +3 -0
  45. package/dist/converters/configs/dark-angels.js +16 -0
  46. package/dist/converters/configs/death-guard.d.ts +3 -0
  47. package/dist/converters/configs/death-guard.js +30 -0
  48. package/dist/converters/configs/deathwatch.d.ts +3 -0
  49. package/dist/converters/configs/deathwatch.js +16 -0
  50. package/dist/converters/configs/drukhari.d.ts +3 -0
  51. package/dist/converters/configs/drukhari.js +51 -0
  52. package/dist/converters/configs/emperors-children.d.ts +3 -0
  53. package/dist/converters/configs/emperors-children.js +38 -0
  54. package/dist/converters/configs/genestealer-cults.d.ts +3 -0
  55. package/dist/converters/configs/genestealer-cults.js +36 -0
  56. package/dist/converters/configs/grey-knights.d.ts +3 -0
  57. package/dist/converters/configs/grey-knights.js +39 -0
  58. package/dist/converters/configs/imperial-fists.d.ts +3 -0
  59. package/dist/converters/configs/imperial-fists.js +16 -0
  60. package/dist/converters/configs/imperial-knights.d.ts +3 -0
  61. package/dist/converters/configs/imperial-knights.js +14 -0
  62. package/dist/converters/configs/iron-hands.d.ts +3 -0
  63. package/dist/converters/configs/iron-hands.js +16 -0
  64. package/dist/converters/configs/leagues-of-votann.d.ts +3 -0
  65. package/dist/converters/configs/leagues-of-votann.js +32 -0
  66. package/dist/converters/configs/necrons.d.ts +3 -0
  67. package/dist/converters/configs/necrons.js +19 -0
  68. package/dist/converters/configs/orks.d.ts +3 -0
  69. package/dist/converters/configs/orks.js +71 -0
  70. package/dist/converters/configs/raven-guard.d.ts +3 -0
  71. package/dist/converters/configs/raven-guard.js +16 -0
  72. package/dist/converters/configs/salamanders.d.ts +3 -0
  73. package/dist/converters/configs/salamanders.js +16 -0
  74. package/dist/converters/configs/space-wolves.d.ts +3 -0
  75. package/dist/converters/configs/space-wolves.js +16 -0
  76. package/dist/converters/configs/tau-empire.d.ts +3 -0
  77. package/dist/converters/configs/tau-empire.js +44 -0
  78. package/dist/converters/configs/thousand-sons.d.ts +3 -0
  79. package/dist/converters/configs/thousand-sons.js +30 -0
  80. package/dist/converters/configs/tyranids.d.ts +3 -0
  81. package/dist/converters/configs/tyranids.js +27 -0
  82. package/dist/converters/configs/ultramarines.d.ts +3 -0
  83. package/dist/converters/configs/ultramarines.js +16 -0
  84. package/dist/converters/configs/white-scars.d.ts +3 -0
  85. package/dist/converters/configs/white-scars.js +16 -0
  86. package/dist/converters/configs/world-eaters.d.ts +3 -0
  87. package/dist/converters/configs/world-eaters.js +43 -0
  88. package/dist/converters/faction-config.d.ts +53 -0
  89. package/dist/converters/faction-config.js +22 -0
  90. package/dist/converters/id-generator.d.ts +14 -0
  91. package/dist/converters/id-generator.js +65 -0
  92. package/dist/converters/keyword-filter.d.ts +26 -0
  93. package/dist/converters/keyword-filter.js +78 -0
  94. package/dist/converters/stat-parser.d.ts +22 -0
  95. package/dist/converters/stat-parser.js +84 -0
  96. package/dist/converters/view-selector.d.ts +54 -0
  97. package/dist/converters/view-selector.js +96 -0
  98. package/dist/converters/weapon-dedup.d.ts +60 -0
  99. package/dist/converters/weapon-dedup.js +120 -0
  100. package/dist/data/bundle.generated.d.ts +3 -0
  101. package/dist/data/bundle.generated.js +3 -0
  102. package/dist/data/collection.d.ts +64 -0
  103. package/dist/data/collection.js +118 -0
  104. package/dist/data/dataset.d.ts +50 -0
  105. package/dist/data/dataset.js +134 -0
  106. package/dist/data/entities.d.ts +80 -0
  107. package/dist/data/entities.js +133 -0
  108. package/dist/data/index.d.ts +59 -0
  109. package/dist/data/index.js +57 -0
  110. package/dist/data/normalize.d.ts +29 -0
  111. package/dist/data/normalize.js +37 -0
  112. package/dist/data/types.d.ts +43 -0
  113. package/dist/data/types.js +25 -0
  114. package/dist/generated.d.ts +1084 -0
  115. package/dist/generated.js +2 -0
  116. package/dist/index.d.ts +3 -0
  117. package/dist/index.js +7 -0
  118. package/dist/known-support-10e.d.ts +31 -0
  119. package/dist/known-support-10e.js +113 -0
  120. package/dist/port-10e-faction.d.ts +52 -0
  121. package/dist/port-10e-faction.js +413 -0
  122. package/dist/report.d.ts +3 -0
  123. package/dist/report.js +31 -0
  124. package/dist/schema-loader.d.ts +15 -0
  125. package/dist/schema-loader.js +79 -0
  126. package/dist/validate.d.ts +21 -0
  127. package/dist/validate.js +124 -0
  128. package/package.json +77 -0
  129. package/schemas/$defs/common.schema.json +86 -0
  130. package/schemas/$defs/game-version-ref.schema.json +11 -0
  131. package/schemas/core/deployment-pattern.schema.json +102 -0
  132. package/schemas/core/detachment.schema.json +56 -0
  133. package/schemas/core/enhancement.schema.json +46 -0
  134. package/schemas/core/faction.schema.json +29 -0
  135. package/schemas/core/force-disposition.schema.json +22 -0
  136. package/schemas/core/game-version.schema.json +20 -0
  137. package/schemas/core/leader-attachment.schema.json +18 -0
  138. package/schemas/core/mission-matchup.schema.json +25 -0
  139. package/schemas/core/mission.schema.json +42 -0
  140. package/schemas/core/roster.schema.json +203 -0
  141. package/schemas/core/secondary-card.schema.json +195 -0
  142. package/schemas/core/stratagem.schema.json +58 -0
  143. package/schemas/core/terrain-layout.schema.json +135 -0
  144. package/schemas/core/unit-composition.schema.json +38 -0
  145. package/schemas/core/unit.schema.json +125 -0
  146. package/schemas/core/wargear-option.schema.json +47 -0
  147. package/schemas/core/weapon.schema.json +56 -0
  148. package/schemas/enrichment/ability-dsl/ability.schema.json +60 -0
  149. package/schemas/enrichment/ability-dsl/condition.schema.json +48 -0
  150. package/schemas/enrichment/ability-dsl/effect.schema.json +145 -0
  151. package/schemas/enrichment/ability-dsl/scope.schema.json +12 -0
  152. package/schemas/enrichment/interaction-flag.schema.json +17 -0
  153. package/schemas/enrichment/phase-mapping.schema.json +14 -0
  154. package/schemas/enrichment/resource-pool.schema.json +36 -0
  155. package/schemas/enrichment/timing-flag.schema.json +28 -0
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://40kdc.dev/schemas/core/force-disposition.schema.json",
4
+ "title": "Force Disposition",
5
+ "description": "A 11e strategic-intent tag granted by detachments. Players compare dispositions at game start to determine the shared mission; asymmetric primary objectives result.",
6
+ "type": "object",
7
+ "properties": {
8
+ "id": {
9
+ "$ref": "../defs/common.schema.json#/$defs/force-disposition-id",
10
+ "description": "One of the five confirmed launch Force Dispositions."
11
+ },
12
+ "name": { "type": "string", "minLength": 1, "maxLength": 128 },
13
+ "text": {
14
+ "type": "string",
15
+ "minLength": 1,
16
+ "description": "Community-authored description of the disposition's effect (original prose only — no reproduced rules text)."
17
+ },
18
+ "game_version": { "$ref": "../defs/game-version-ref.schema.json" }
19
+ },
20
+ "required": ["id", "name", "game_version"],
21
+ "additionalProperties": false
22
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://40kdc.dev/schemas/core/game-version.schema.json",
4
+ "title": "Game Version",
5
+ "type": "object",
6
+ "properties": {
7
+ "edition": { "$ref": "../defs/common.schema.json#/$defs/edition" },
8
+ "dataslate": { "$ref": "../defs/common.schema.json#/$defs/dataslate-version" },
9
+ "effective_date": { "type": "string", "format": "date" },
10
+ "label": { "type": "string" },
11
+ "supersedes": {
12
+ "oneOf": [
13
+ { "$ref": "../defs/common.schema.json#/$defs/dataslate-version" },
14
+ { "type": "null" }
15
+ ]
16
+ }
17
+ },
18
+ "required": ["edition", "dataslate", "effective_date"],
19
+ "additionalProperties": false
20
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://40kdc.dev/schemas/core/leader-attachment.schema.json",
4
+ "title": "Leader Attachment",
5
+ "description": "Defines which character units can attach to which bodyguard units.",
6
+ "type": "object",
7
+ "properties": {
8
+ "leader_id": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
9
+ "eligible_bodyguard_ids": {
10
+ "type": "array",
11
+ "items": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
12
+ "minItems": 1
13
+ },
14
+ "game_version": { "$ref": "../defs/game-version-ref.schema.json" }
15
+ },
16
+ "required": ["leader_id", "eligible_bodyguard_ids", "game_version"],
17
+ "additionalProperties": false
18
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://40kdc.dev/schemas/core/mission-matchup.schema.json",
4
+ "title": "Mission Matchup",
5
+ "description": "One cell of the 11e Force Disposition matrix: given the player's own Force Disposition and their opponent's, the mission that player plays. Mirrors a single row on a physical Force Disposition card. The (disposition, opponent_disposition) pair is the conceptual key; compound uniqueness across entries is a data convention, not enforced by this schema.",
6
+ "type": "object",
7
+ "properties": {
8
+ "id": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
9
+ "disposition": {
10
+ "$ref": "../defs/common.schema.json#/$defs/force-disposition-id",
11
+ "description": "The player's own Force Disposition."
12
+ },
13
+ "opponent_disposition": {
14
+ "$ref": "../defs/common.schema.json#/$defs/force-disposition-id",
15
+ "description": "The opponent's Force Disposition."
16
+ },
17
+ "mission_id": {
18
+ "$ref": "../defs/common.schema.json#/$defs/entity-id",
19
+ "description": "Id of the mission entity the player plays for this disposition pairing."
20
+ },
21
+ "game_version": { "$ref": "../defs/game-version-ref.schema.json" }
22
+ },
23
+ "required": ["id", "disposition", "opponent_disposition", "mission_id", "game_version"],
24
+ "additionalProperties": false
25
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://40kdc.dev/schemas/core/mission.schema.json",
4
+ "title": "Mission",
5
+ "description": "An 11e primary mission (the objective a player scores). Which mission a player plays is selected by the Force Disposition matchup matrix (see mission-matchup), keyed on the player's own disposition and their opponent's. Victory points are capped per game and per battle round.",
6
+ "type": "object",
7
+ "properties": {
8
+ "id": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
9
+ "name": { "type": "string", "minLength": 1, "maxLength": 128 },
10
+ "source": {
11
+ "type": "string",
12
+ "minLength": 1,
13
+ "maxLength": 64,
14
+ "description": "Mission pack or source the mission originates from."
15
+ },
16
+ "description": {
17
+ "type": "string",
18
+ "description": "Community-authored mission/objective summary (original prose only — no reproduced rules text)."
19
+ },
20
+ "vp_per_game_cap": {
21
+ "type": "integer",
22
+ "minimum": 0,
23
+ "default": 45,
24
+ "description": "Maximum primary VP scorable across the whole game. 11e default is 45."
25
+ },
26
+ "vp_per_round_cap": {
27
+ "type": "integer",
28
+ "minimum": 0,
29
+ "default": 15,
30
+ "description": "Maximum primary VP scorable in a single battle round. 11e default is 15."
31
+ },
32
+ "deployment_pattern_ids": {
33
+ "type": "array",
34
+ "uniqueItems": true,
35
+ "description": "Ids of the deployment-pattern entities (maps) this mission can be played on. Empty until the per-mission maps are confirmed.",
36
+ "items": { "$ref": "../defs/common.schema.json#/$defs/entity-id" }
37
+ },
38
+ "game_version": { "$ref": "../defs/game-version-ref.schema.json" }
39
+ },
40
+ "required": ["id", "name", "game_version"],
41
+ "additionalProperties": false
42
+ }
@@ -0,0 +1,203 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://40kdc.dev/schemas/core/roster.schema.json",
4
+ "title": "Roster",
5
+ "description": "An army list imported from an external list-builder, resolved onto 40kdc entity IDs. Lenient: unresolved entries are retained with their raw name and candidate suggestions rather than dropped, and a diagnostics block summarises the import. Contains only permitted facts (entity IDs, display names, model counts, points, keywords) — never reproduced rules or ability text.",
6
+ "type": "object",
7
+ "properties": {
8
+ "name": { "type": "string", "minLength": 1, "maxLength": 256 },
9
+ "source": {
10
+ "type": "object",
11
+ "description": "Provenance of the imported list.",
12
+ "properties": {
13
+ "format": { "type": "string", "const": "listforge" },
14
+ "generated_by": {
15
+ "oneOf": [{ "type": "string" }, { "type": "null" }],
16
+ "description": "The `generatedBy` field reported by the source payload (e.g. 'List Forge'), if present."
17
+ }
18
+ },
19
+ "required": ["format", "generated_by"],
20
+ "additionalProperties": false
21
+ },
22
+ "faction_id": {
23
+ "oneOf": [
24
+ { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
25
+ { "type": "null" }
26
+ ],
27
+ "description": "Resolved primary faction; null when the source faction could not be matched."
28
+ },
29
+ "detachment_id": {
30
+ "oneOf": [
31
+ { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
32
+ { "type": "null" }
33
+ ],
34
+ "description": "Resolved detachment; null when absent or unmatched."
35
+ },
36
+ "battle_size": {
37
+ "oneOf": [
38
+ { "$ref": "../defs/common.schema.json#/$defs/battle-size" },
39
+ { "type": "null" }
40
+ ],
41
+ "description": "Resolved battle size; null when the source value could not be mapped."
42
+ },
43
+ "points": {
44
+ "type": "object",
45
+ "description": "Point totals. `total_reported` (from the source payload) and `total_computed` (summed from this roster's units) are kept distinct and never reconciled — a mismatch is surfaced as a diagnostic.",
46
+ "properties": {
47
+ "declared_limit": {
48
+ "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "null" }],
49
+ "description": "Points ceiling parsed from the battle size (e.g. 2000), if known."
50
+ },
51
+ "total_reported": {
52
+ "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "null" }],
53
+ "description": "Total points as reported by the source roster's cost block."
54
+ },
55
+ "total_computed": {
56
+ "type": "integer",
57
+ "minimum": 0,
58
+ "description": "Sum of this roster's per-unit point values."
59
+ }
60
+ },
61
+ "required": ["declared_limit", "total_reported", "total_computed"],
62
+ "additionalProperties": false
63
+ },
64
+ "units": {
65
+ "type": "array",
66
+ "items": { "$ref": "#/$defs/roster-unit" }
67
+ },
68
+ "game_version": { "$ref": "../defs/game-version-ref.schema.json" },
69
+ "diagnostics": { "$ref": "#/$defs/roster-diagnostics" }
70
+ },
71
+ "required": ["name", "source", "faction_id", "detachment_id", "battle_size", "points", "units", "game_version", "diagnostics"],
72
+ "additionalProperties": false,
73
+ "$defs": {
74
+ "roster-resolved-ref": {
75
+ "type": "object",
76
+ "description": "A reference to a 40kdc entity that may or may not have resolved. Retains the source's raw name so the import is lossless even on a miss.",
77
+ "properties": {
78
+ "id": {
79
+ "oneOf": [
80
+ { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
81
+ { "type": "null" }
82
+ ],
83
+ "description": "Resolved entity id, or null when no match was found."
84
+ },
85
+ "raw_name": {
86
+ "type": "string",
87
+ "minLength": 1,
88
+ "description": "The display name exactly as it appeared in the source payload."
89
+ },
90
+ "resolved": {
91
+ "type": "boolean",
92
+ "description": "True iff `id` is non-null."
93
+ },
94
+ "candidates": {
95
+ "type": "array",
96
+ "maxItems": 5,
97
+ "description": "Best-guess alternatives offered when resolution failed (empty when resolved or when no near-matches exist).",
98
+ "items": {
99
+ "type": "object",
100
+ "properties": {
101
+ "id": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
102
+ "name": { "type": "string" }
103
+ },
104
+ "required": ["id", "name"],
105
+ "additionalProperties": false
106
+ }
107
+ }
108
+ },
109
+ "required": ["id", "raw_name", "resolved", "candidates"],
110
+ "additionalProperties": false
111
+ },
112
+ "roster-unit": {
113
+ "type": "object",
114
+ "properties": {
115
+ "ref": { "$ref": "#/$defs/roster-resolved-ref" },
116
+ "model_count": { "type": "integer", "minimum": 1 },
117
+ "points": {
118
+ "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "null" }],
119
+ "description": "Per-unit point value from the source, or null when absent."
120
+ },
121
+ "is_warlord": { "type": "boolean", "default": false },
122
+ "enhancement": {
123
+ "oneOf": [{ "$ref": "#/$defs/roster-resolved-ref" }, { "type": "null" }],
124
+ "description": "Attached enhancement, or null when none."
125
+ },
126
+ "wargear": {
127
+ "type": "array",
128
+ "description": "Weapons/wargear selected on the unit. Always lists the raw name and count; `ref.id` is null when the weapon could not be matched.",
129
+ "items": {
130
+ "type": "object",
131
+ "properties": {
132
+ "ref": { "$ref": "#/$defs/roster-resolved-ref" },
133
+ "count": { "type": "integer", "minimum": 1 }
134
+ },
135
+ "required": ["ref", "count"],
136
+ "additionalProperties": false
137
+ }
138
+ },
139
+ "leader_attachment": {
140
+ "oneOf": [
141
+ {
142
+ "type": "object",
143
+ "description": "Inferred leader→bodyguard attachment. Always provisional: the source format does not encode attachment unambiguously, so this is a heuristic match against leader-attachment data.",
144
+ "properties": {
145
+ "bodyguard_ref": { "$ref": "#/$defs/roster-resolved-ref" },
146
+ "provisional": { "type": "boolean" }
147
+ },
148
+ "required": ["bodyguard_ref", "provisional"],
149
+ "additionalProperties": false
150
+ },
151
+ { "type": "null" }
152
+ ]
153
+ }
154
+ },
155
+ "required": ["ref", "model_count", "points", "is_warlord", "enhancement", "wargear", "leader_attachment"],
156
+ "additionalProperties": false
157
+ },
158
+ "roster-diagnostics": {
159
+ "type": "object",
160
+ "description": "A summary of what resolved and what did not during the import.",
161
+ "properties": {
162
+ "resolved_units": { "type": "integer", "minimum": 0 },
163
+ "unresolved_units": { "type": "integer", "minimum": 0 },
164
+ "resolved_weapons": { "type": "integer", "minimum": 0 },
165
+ "unresolved_weapons": { "type": "integer", "minimum": 0 },
166
+ "warnings": {
167
+ "type": "array",
168
+ "items": {
169
+ "type": "object",
170
+ "properties": {
171
+ "code": {
172
+ "type": "string",
173
+ "enum": [
174
+ "faction-unresolved",
175
+ "unit-unresolved",
176
+ "weapon-unresolved",
177
+ "enhancement-unresolved",
178
+ "detachment-unresolved",
179
+ "battle-size-unmapped",
180
+ "points-mismatch",
181
+ "leader-attachment-inferred",
182
+ "multi-force",
183
+ "unknown-field"
184
+ ]
185
+ },
186
+ "message": {
187
+ "type": "string",
188
+ "description": "Generated, IP-free explanatory text."
189
+ },
190
+ "raw_name": {
191
+ "oneOf": [{ "type": "string" }, { "type": "null" }]
192
+ }
193
+ },
194
+ "required": ["code", "message", "raw_name"],
195
+ "additionalProperties": false
196
+ }
197
+ }
198
+ },
199
+ "required": ["resolved_units", "unresolved_units", "resolved_weapons", "unresolved_weapons", "warnings"],
200
+ "additionalProperties": false
201
+ }
202
+ }
203
+ }
@@ -0,0 +1,195 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://40kdc.dev/schemas/core/secondary-card.schema.json",
4
+ "title": "Secondary Card",
5
+ "description": "An 11e mission card. The deck-level rule (draw 2 per turn, keep unscored cards) is separate and not modelled here. This is the per-card shape: an optional on-draw deck operation, an optional player action, and zero or more VP-award blocks. Primary mission cards reuse this shape via card_type. Mechanic blocks reference the Ability DSL; prose is community-authored (no reproduced rules text).",
6
+ "type": "object",
7
+ "$defs": {
8
+ "scoring-trigger": {
9
+ "type": "object",
10
+ "description": "When a VP award is evaluated. A bare `phase` is the legacy shorthand for 'during this phase'; richer triggers add `timing` (the moment within a phase/turn/game), `player_turn`, and a `battle_round` window. A card's section headers map onto these: 'ANY BATTLE ROUND' omits `battle_round`; 'SECOND BATTLE ROUND ONWARDS' is { min: 2 }; 'END OF THE BATTLE' is timing: end-of-battle.",
11
+ "properties": {
12
+ "phase": {
13
+ "$ref": "../defs/common.schema.json#/$defs/phase",
14
+ "description": "The phase the trigger is relative to. Required when `timing` is start-of-phase or end-of-phase; omitted for turn- or battle-level timings."
15
+ },
16
+ "timing": {
17
+ "type": "string",
18
+ "enum": ["start-of-turn", "end-of-turn", "start-of-phase", "end-of-phase", "end-of-battle"],
19
+ "description": "The moment the award is checked. 'End of your turn' = end-of-turn; 'End of your Command phase' = end-of-phase with phase: command; 'End of the battle' = end-of-battle."
20
+ },
21
+ "player_turn": { "$ref": "../defs/common.schema.json#/$defs/player-turn" },
22
+ "battle_round": {
23
+ "type": "object",
24
+ "description": "Battle-round window in which the trigger is active. Absent means any battle round (1-5). 'Second battle round onwards' is { min: 2 }.",
25
+ "properties": {
26
+ "min": { "type": "integer", "minimum": 1, "maximum": 5 },
27
+ "max": { "type": "integer", "minimum": 1, "maximum": 5 }
28
+ },
29
+ "minProperties": 1,
30
+ "additionalProperties": false
31
+ }
32
+ },
33
+ "minProperties": 1,
34
+ "allOf": [
35
+ {
36
+ "if": {
37
+ "required": ["timing"],
38
+ "properties": { "timing": { "enum": ["start-of-phase", "end-of-phase"] } }
39
+ },
40
+ "then": { "required": ["phase"] }
41
+ }
42
+ ],
43
+ "additionalProperties": false
44
+ },
45
+ "army-composition-predicate": {
46
+ "type": "object",
47
+ "description": "A draw-time predicate over an army list (not runtime board state, so deliberately NOT the Ability DSL condition). Used to gate when_drawn operations such as redraws. Example: a card that is void unless the opponent fields a large unit (10e 'Cull the Horde' redrew when the opponent had no unit of 14+ models) is { subject: 'opponent', quantifier: 'none', unit_filter: { model_count_min: 14 } } with operation 'redraw'.",
48
+ "properties": {
49
+ "subject": {
50
+ "type": "string",
51
+ "enum": ["self", "opponent"],
52
+ "description": "Whose army list the predicate inspects."
53
+ },
54
+ "quantifier": {
55
+ "type": "string",
56
+ "enum": ["any", "none"],
57
+ "description": "Whether the army must contain ('any') or lack ('none') a unit matching unit_filter for the predicate to hold."
58
+ },
59
+ "unit_filter": {
60
+ "type": "object",
61
+ "description": "Criteria a unit in the army must satisfy to match. All present criteria must hold (logical AND).",
62
+ "properties": {
63
+ "model_count_min": { "type": "integer", "minimum": 1 },
64
+ "model_count_max": { "type": "integer", "minimum": 1 },
65
+ "wounds_min": { "type": "integer", "minimum": 1 },
66
+ "keywords": { "$ref": "../defs/common.schema.json#/$defs/keyword-list" }
67
+ },
68
+ "minProperties": 1,
69
+ "additionalProperties": false
70
+ }
71
+ },
72
+ "required": ["subject", "quantifier", "unit_filter"],
73
+ "additionalProperties": false
74
+ }
75
+ },
76
+ "properties": {
77
+ "id": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
78
+ "name": { "type": "string", "minLength": 1, "maxLength": 128 },
79
+ "card_type": {
80
+ "type": "string",
81
+ "enum": ["secondary", "primary"],
82
+ "default": "secondary",
83
+ "description": "Whether this is a secondary card or a primary mission card (which reuses this shape)."
84
+ },
85
+ "subtype": {
86
+ "type": "string",
87
+ "minLength": 1,
88
+ "maxLength": 64,
89
+ "description": "Finer classification within the deck (e.g. a category or tactical/fixed split). Free-form — not enum-locked until 11e categories are confirmed."
90
+ },
91
+ "when_drawn": {
92
+ "type": "object",
93
+ "description": "Optional deck operation performed when this card is drawn (e.g. redraw, swap). Distinct from combat effects — deck operations have no combat target, so they are not modelled via the Ability DSL effect language. If `condition` is present, the operation fires only when the predicate holds.",
94
+ "properties": {
95
+ "operation": {
96
+ "type": "string",
97
+ "enum": ["reshuffle", "replace", "redraw", "draw-extra", "swap"],
98
+ "description": "The deck manipulation this card triggers on draw."
99
+ },
100
+ "card_ids": {
101
+ "type": "array",
102
+ "uniqueItems": true,
103
+ "description": "Other cards this operation references, by id.",
104
+ "items": { "$ref": "../defs/common.schema.json#/$defs/entity-id" }
105
+ },
106
+ "condition": {
107
+ "$ref": "#/$defs/army-composition-predicate",
108
+ "description": "Draw-time army-composition predicate gating the operation (e.g. redraw when the opponent lacks a qualifying unit)."
109
+ }
110
+ },
111
+ "required": ["operation"],
112
+ "additionalProperties": false
113
+ },
114
+ "action": {
115
+ "type": "object",
116
+ "description": "Optional player action the card enables.",
117
+ "properties": {
118
+ "starts": {
119
+ "$ref": "../defs/common.schema.json#/$defs/phase",
120
+ "description": "Phase in which the action can be started."
121
+ },
122
+ "player_turn": { "$ref": "../defs/common.schema.json#/$defs/player-turn" },
123
+ "units": {
124
+ "$ref": "../enrichment/ability-dsl/condition.schema.json",
125
+ "description": "Eligibility predicate for which units may perform the action."
126
+ },
127
+ "use_limit": {
128
+ "type": "integer",
129
+ "minimum": 1,
130
+ "description": "Maximum number of times the action may be performed."
131
+ },
132
+ "completes": {
133
+ "$ref": "../enrichment/ability-dsl/condition.schema.json",
134
+ "description": "Predicate for when the action is considered complete."
135
+ },
136
+ "effect": {
137
+ "$ref": "../enrichment/ability-dsl/effect.schema.json",
138
+ "description": "Effect applied when the action completes (e.g. terrain-area-tag to mark transient state on a terrain piece)."
139
+ }
140
+ },
141
+ "additionalProperties": false
142
+ },
143
+ "awards": {
144
+ "type": "array",
145
+ "minItems": 1,
146
+ "description": "VP-award blocks: each scores when `trigger` fires and the optional `when` condition holds. An award scores either a flat `vp` or a count-scaled `vp_per` (VP per instance of the thing named by `per`). Awards accrue independently and sum; a card's '+ ... CUMULATIVE' rows are modelled as separate awards flagged `cumulative` for faithful round-trip.",
147
+ "items": {
148
+ "type": "object",
149
+ "properties": {
150
+ "trigger": { "$ref": "#/$defs/scoring-trigger" },
151
+ "when": { "$ref": "../enrichment/ability-dsl/condition.schema.json" },
152
+ "vp": {
153
+ "type": "integer",
154
+ "minimum": 0,
155
+ "description": "Flat VP scored when the award fires."
156
+ },
157
+ "vp_per": {
158
+ "type": "integer",
159
+ "minimum": 0,
160
+ "description": "VP scored per instance of the thing named by `per` (e.g. 1 VP per operation marker within range of a controlled objective)."
161
+ },
162
+ "per": {
163
+ "type": "string",
164
+ "minLength": 1,
165
+ "maxLength": 128,
166
+ "description": "What `vp_per` counts, as a kebab-case descriptor (e.g. 'operation-marker-within-range-of-controlled-central-objective'). Required when `vp_per` is present."
167
+ },
168
+ "per_max": {
169
+ "type": "integer",
170
+ "minimum": 1,
171
+ "description": "Optional cap on how many instances `vp_per` counts."
172
+ },
173
+ "cumulative": {
174
+ "type": "boolean",
175
+ "default": false,
176
+ "description": "Marks an award the card shows as an additive '+' bonus to the preceding award in the same trigger block (the card's CUMULATIVE rows). Purely descriptive — all awards accrue independently and are summed."
177
+ }
178
+ },
179
+ "oneOf": [
180
+ { "required": ["vp"] },
181
+ { "required": ["vp_per", "per"] }
182
+ ],
183
+ "required": ["trigger"],
184
+ "additionalProperties": false
185
+ }
186
+ },
187
+ "text": {
188
+ "type": "string",
189
+ "description": "Community-authored card description (original prose only — no reproduced rules text)."
190
+ },
191
+ "game_version": { "$ref": "../defs/game-version-ref.schema.json" }
192
+ },
193
+ "required": ["id", "name", "game_version"],
194
+ "additionalProperties": false
195
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://40kdc.dev/schemas/core/stratagem.schema.json",
4
+ "title": "Stratagem",
5
+ "description": "A CP-costed ability usable during specific game phases.",
6
+ "type": "object",
7
+ "properties": {
8
+ "id": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
9
+ "name": { "type": "string", "minLength": 1, "maxLength": 128 },
10
+ "category": {
11
+ "type": "string",
12
+ "enum": ["core", "detachment"],
13
+ "description": "Whether this is a universal core stratagem or tied to a specific detachment"
14
+ },
15
+ "type": {
16
+ "type": "string",
17
+ "enum": ["battle-tactic", "strategic-ploy", "epic-deed", "wargear"],
18
+ "description": "GW-printed stratagem category from the card"
19
+ },
20
+ "detachment_id": {
21
+ "oneOf": [
22
+ { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
23
+ { "type": "null" }
24
+ ],
25
+ "description": "Null for core stratagems"
26
+ },
27
+ "cp_cost": { "type": "integer", "minimum": 0, "maximum": 4 },
28
+ "phases": { "$ref": "../defs/common.schema.json#/$defs/phase-list" },
29
+ "player_turn": { "$ref": "../defs/common.schema.json#/$defs/player-turn" },
30
+ "timing": {
31
+ "type": "string",
32
+ "enum": ["once-per-phase", "once-per-turn", "once-per-battle", "unlimited"]
33
+ },
34
+ "target_restrictions": {
35
+ "oneOf": [
36
+ {
37
+ "type": "object",
38
+ "properties": {
39
+ "required_keywords": { "$ref": "../defs/common.schema.json#/$defs/keyword-list" },
40
+ "excluded_keywords": { "$ref": "../defs/common.schema.json#/$defs/keyword-list" },
41
+ "notes": { "type": "string" }
42
+ },
43
+ "additionalProperties": false
44
+ },
45
+ { "type": "null" }
46
+ ]
47
+ },
48
+ "ability_id": {
49
+ "oneOf": [
50
+ { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
51
+ { "type": "null" }
52
+ ]
53
+ },
54
+ "game_version": { "$ref": "../defs/game-version-ref.schema.json" }
55
+ },
56
+ "required": ["id", "name", "category", "type", "cp_cost", "phases", "player_turn", "timing", "game_version"],
57
+ "additionalProperties": false
58
+ }