@alpaca-software/40kdc-data 0.3.1 → 0.4.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 +3 -3
- package/dist/bundle-schemas.d.ts.map +1 -1
- package/dist/bundle-schemas.js +17 -0
- package/dist/bundle-schemas.js.map +1 -1
- package/dist/cli.js +5 -0
- package/dist/cli.js.map +1 -1
- package/dist/codegen-data.js +2 -1
- package/dist/codegen-data.js.map +1 -1
- package/dist/commands/populate-base-sizes.d.ts +2 -0
- package/dist/commands/populate-base-sizes.d.ts.map +1 -0
- package/dist/commands/populate-base-sizes.js +158 -0
- package/dist/commands/populate-base-sizes.js.map +1 -0
- package/dist/convert-faction.d.ts +3 -1
- package/dist/convert-faction.d.ts.map +1 -1
- package/dist/convert-faction.js +49 -16
- package/dist/convert-faction.js.map +1 -1
- package/dist/converters/base-size-bridge.d.ts +122 -0
- package/dist/converters/base-size-bridge.d.ts.map +1 -0
- package/dist/converters/base-size-bridge.js +198 -0
- package/dist/converters/base-size-bridge.js.map +1 -0
- package/dist/converters/base-size-guide-extract.d.ts +11 -0
- package/dist/converters/base-size-guide-extract.d.ts.map +1 -0
- package/dist/converters/base-size-guide-extract.js +59 -0
- package/dist/converters/base-size-guide-extract.js.map +1 -0
- package/dist/converters/option-bridge.d.ts +36 -0
- package/dist/converters/option-bridge.d.ts.map +1 -0
- package/dist/converters/option-bridge.js +72 -0
- package/dist/converters/option-bridge.js.map +1 -0
- package/dist/converters/option-parser.d.ts +56 -0
- package/dist/converters/option-parser.d.ts.map +1 -0
- package/dist/converters/option-parser.js +209 -0
- package/dist/converters/option-parser.js.map +1 -0
- package/dist/converters/wargear-options.d.ts +55 -0
- package/dist/converters/wargear-options.d.ts.map +1 -0
- package/dist/converters/wargear-options.js +187 -0
- package/dist/converters/wargear-options.js.map +1 -0
- package/dist/data/bundle.generated.js +1 -1
- package/dist/data/bundle.generated.js.map +1 -1
- package/dist/data/dataset.d.ts +10 -2
- package/dist/data/dataset.d.ts.map +1 -1
- package/dist/data/dataset.js +16 -2
- package/dist/data/dataset.js.map +1 -1
- package/dist/data/entities.d.ts +3 -1
- package/dist/data/entities.d.ts.map +1 -1
- package/dist/data/entities.js +4 -0
- package/dist/data/entities.js.map +1 -1
- package/dist/data/index.d.ts +5 -1
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +5 -1
- package/dist/data/index.js.map +1 -1
- package/dist/data/loadout.d.ts +60 -0
- package/dist/data/loadout.d.ts.map +1 -0
- package/dist/data/loadout.js +135 -0
- package/dist/data/loadout.js.map +1 -0
- package/dist/data/types.d.ts +4 -2
- package/dist/data/types.d.ts.map +1 -1
- package/dist/data/types.js +2 -1
- package/dist/data/types.js.map +1 -1
- package/dist/export/rosterizer.js +1 -1
- package/dist/export/rosterizer.js.map +1 -1
- package/dist/gen-conformance.js +25 -2
- package/dist/gen-conformance.js.map +1 -1
- package/dist/generated.d.ts +112 -55
- package/dist/generated.d.ts.map +1 -1
- package/dist/generated.js.map +1 -1
- package/dist/import/rosterizer.d.ts +1 -1
- package/dist/import/rosterizer.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/runner.d.ts +16 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +55 -1
- package/dist/runner.js.map +1 -1
- package/dist/scoring/index.d.ts +135 -0
- package/dist/scoring/index.d.ts.map +1 -0
- package/dist/scoring/index.js +195 -0
- package/dist/scoring/index.js.map +1 -0
- package/dist/translate/condition.d.ts.map +1 -1
- package/dist/translate/condition.js +4 -0
- package/dist/translate/condition.js.map +1 -1
- package/dist/translate/index.d.ts +1 -1
- package/dist/translate/index.d.ts.map +1 -1
- package/dist/translate/index.js.map +1 -1
- package/dist/translate/scoring.d.ts +6 -0
- package/dist/translate/scoring.d.ts.map +1 -1
- package/dist/translate/scoring.js.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +13 -5
- package/dist/validate.js.map +1 -1
- package/package.json +4 -4
- package/schemas/$defs/common.schema.json +14 -0
- package/schemas/core/secondary-card.schema.json +10 -0
- package/schemas/core/unit-composition.schema.json +5 -1
- package/schemas/core/unit.schema.json +2 -10
- package/schemas/core/wargear-option.schema.json +32 -6
- package/schemas/core/wargear.schema.json +24 -0
- package/schemas/enrichment/ability-dsl/condition.schema.json +3 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"condition.js","sourceRoot":"","sources":["../../src/translate/condition.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Humanize an Ability-DSL / scoring `condition` into plain English.\n *\n * Shared by the ability-text CLI (`commands/translate.ts`) and the scoring-card\n * translator (`scoring.ts`). Output is **ASCII-only** with a fixed clause and\n * parameter order: it is pinned byte-for-byte across the TS and Rust ports by\n * the `conformance/scoring-translation` corpus, so any phrasing change here is a\n * semantic corpus change (bump `conformance/SPEC_VERSION`).\n */\n\n/**\n * Minimal structural view of a condition node. Matches both the ability-dsl\n * condition schema and the `secondary-card` award `when` field (a simple node\n * carries `type` + `parameters` + `negated`; a compound node carries\n * `operator` + `operands`).\n */\nexport interface Condition {\n type?: string;\n operator?: \"and\" | \"or\" | \"not\";\n operands?: Condition[];\n parameters?: Record<string, unknown>;\n negated?: boolean;\n}\n\n/** kebab-case → space-separated words (`enemy-territory` → `enemy territory`). */\nexport function dekebab(s: string): string {\n return s.replace(/-/g, \" \");\n}\n\nfunction str(v: unknown): string {\n return typeof v === \"string\" ? v : String(v);\n}\n\n/** `2` + `objective` → `2+ objectives`. Nouns here are all regular plurals. */\nfunction count(n: unknown, noun: string): string {\n return `${str(n)}+ ${noun}s`;\n}\n\nexport function describeCondition(c: Condition): string {\n // Compound nodes first — join the operands with lowercase connectives so the\n // result reads naturally inside a \"... when X and Y\" clause.\n if (c.operator === \"and\" && c.operands) {\n return c.operands.map(describeCondition).join(\" and \");\n }\n if (c.operator === \"or\" && c.operands) {\n return c.operands.map(describeCondition).join(\" or \");\n }\n if (c.operator === \"not\" && c.operands) {\n return `not (${c.operands.map(describeCondition).join(\", \")})`;\n }\n\n const negate = c.negated ? \"not \" : \"\";\n const p = c.parameters ?? {};\n\n switch (c.type) {\n // ── Ability-DSL conditions (ported from commands/translate.ts) ──────────\n case \"phase-is\":\n return `${negate}during the ${str(p.phase)} phase`;\n case \"timing-is\":\n return `${negate}at ${dekebab(str(p.timing))}`;\n case \"player-turn-is\":\n return `${negate}in ${p.turn === \"your-turn\" ? \"your\" : p.turn === \"opponent-turn\" ? \"the opponent's\" : \"either player's\"} turn`;\n case \"charged-this-turn\":\n return `${negate}the unit charged this turn`;\n case \"advanced-this-turn\":\n return `${negate}the unit advanced this turn`;\n case \"remained-stationary\":\n return `${negate}the unit remained stationary`;\n case \"unit-below-starting-strength\":\n return `${negate}the unit is below starting strength`;\n case \"unit-below-half-strength\":\n return `${negate}the unit is below half strength`;\n case \"unit-has-keyword\":\n return `${negate}the unit has \"${str(p.keyword)}\"`;\n case \"target-has-keyword\":\n return `${negate}the target has \"${str(p.keyword)}\"`;\n case \"model-is-leader\":\n return `${negate}the model is leading a unit`;\n case \"is-attached\":\n return `${negate}attached to a ${p.keyword ? `${str(p.keyword)} ` : \"\"}unit`;\n case \"attack-is-type\":\n return `${negate}for ${str(p.attack_type)} attacks`;\n case \"is-battle-shocked\":\n return `${negate}the unit is battle-shocked`;\n case \"has-lost-wounds\":\n return `${negate}the model has lost wounds`;\n case \"opponent-unit-within-range\":\n return `${negate}an enemy unit is within ${p.range === \"engagement\" ? \"engagement range\" : `${str(p.range)}\"`}`;\n case \"unit-within-range-of\":\n return `${negate}within ${str(p.range)}\" of ${str(p.target_type ?? \"target\")}${p.keyword ? ` (${str(p.keyword)})` : \"\"}`;\n case \"within-range-of-objective\":\n return `${negate}within range of an objective`;\n case \"has-fought-this-phase\":\n return `${negate}has fought this phase`;\n case \"destroyed-by-attack-type\":\n return `${negate}destroyed by a ${str(p.attack_type)} attack`;\n\n // ── Scoring conditions (secondary-card award `when`) ────────────────────\n case \"objective-majority\":\n return `${negate}you hold more objectives than the ${dekebab(str(p.relative_to ?? \"opponent\"))}`;\n case \"controls-objective\": {\n const noun = p.objective_role ? `${dekebab(str(p.objective_role))} objective` : \"objective\";\n let s = `${negate}you control ${count(p.count_min ?? 1, noun)}`;\n if (p.objective != null) s += ` (${dekebab(str(p.objective))})`;\n if (p.scope != null) s += ` in ${dekebab(str(p.scope))}`;\n if (p.exclude != null) s += ` (excluding ${dekebab(str(p.exclude))})`;\n return s;\n }\n case \"units-destroyed\":\n return `${negate}${count(p.count_min ?? 1, `${str(p.side)} unit`)} destroyed ${dekebab(str(p.window))}`;\n case \"units-destroyed-comparison\": {\n const subj = (p.subject ?? {}) as Record<string, unknown>;\n const ref = (p.reference ?? {}) as Record<string, unknown>;\n const cmp = p.comparator === \"greater-or-equal\" ? \"at least as many\" : \"more\";\n const link = p.comparator === \"greater-or-equal\" ? \"as\" : \"than\";\n return `${negate}you destroyed ${cmp} ${str(subj.side)} units ${dekebab(str(subj.window))} ${link} ${str(ref.side)} units ${dekebab(str(ref.window))}`;\n }\n case \"new-objective-controlled\":\n return `${negate}you newly control ${count(p.count_min ?? 1, \"objective\")} this turn`;\n case \"destroyed-while-on-objective\": {\n let s = `${negate}${count(p.count_min ?? 1, \"enemy unit\")} destroyed`;\n if (p.destroyer_on_objective) s += \" by a unit on an objective\";\n if (p.victim_on_objective) s += \" while on an objective\";\n return s;\n }\n case \"action-completed\": {\n let s = `${negate}${count(p.count_min ?? 1, \"action\")} completed`;\n if (p.action_id != null) s += ` (${dekebab(str(p.action_id))})`;\n if (p.target_kind != null) s += ` on ${dekebab(str(p.target_kind))}`;\n const tf = (p.target_filter ?? {}) as Record<string, unknown>;\n if (tf.objective_role != null) s += ` (${dekebab(str(tf.objective_role))})`;\n if (tf.in_enemy_territory) s += \" in enemy territory\";\n if (tf.exclude != null) s += ` (excluding ${dekebab(str(tf.exclude))})`;\n if (p.window != null) s += ` ${dekebab(str(p.window))}`;\n return s;\n }\n case \"objective-has-tag\": {\n let s = `${negate}${count(p.count_min ?? 1, \"objective\")} tagged ${dekebab(str(p.tag))}`;\n if (p.count_max != null) s += ` (at most ${str(p.count_max)})`;\n if (p.objective != null) s += ` (${dekebab(str(p.objective))})`;\n if (p.scope != null) s += ` in ${dekebab(str(p.scope))}`;\n if (p.last_marked) s += \" (most recently marked)\";\n return s;\n }\n case \"unit-has-tag\": {\n let s = `${negate}${count(p.count_min ?? 1, `${str(p.side)} unit`)} tagged ${dekebab(str(p.tag))}`;\n if (p.window != null) s += ` (${dekebab(str(p.window))})`;\n return s;\n }\n case \"terrain-has-tag\": {\n let s = `${negate}terrain tagged ${dekebab(str(p.tag))}`;\n if (p.friendly_units_min != null) s += ` with ${str(p.friendly_units_min)}+ friendly units`;\n if (p.enemy_units_max != null) s += ` and at most ${str(p.enemy_units_max)} enemy units`;\n if (p.last_marked) s += \" (most recently marked)\";\n if (p.in_enemy_dz) s += \" in the enemy deployment zone\";\n return s;\n }\n case \"terrain-area-control\":\n return `${negate}you control a terrain area with ${str(p.min_models ?? 1)}+ models`;\n case \"territory-control\": {\n let s = `${negate}you control ${dekebab(str(p.territory_ref ?? \"your-territory\"))}`;\n if (p.enemy_units_max != null) s += ` with at most ${str(p.enemy_units_max)} enemy units`;\n return s;\n }\n case \"engagement-fronts\":\n return `${negate}you are engaged on ${str(p.count_min ?? 1)}+ fronts`;\n\n default:\n return `${negate}${dekebab(c.type ?? \"unknown\")}`;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"condition.js","sourceRoot":"","sources":["../../src/translate/condition.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Humanize an Ability-DSL / scoring `condition` into plain English.\n *\n * Shared by the ability-text CLI (`commands/translate.ts`) and the scoring-card\n * translator (`scoring.ts`). Output is **ASCII-only** with a fixed clause and\n * parameter order: it is pinned byte-for-byte across the TS and Rust ports by\n * the `conformance/scoring-translation` corpus, so any phrasing change here is a\n * semantic corpus change (bump `conformance/SPEC_VERSION`).\n */\n\n/**\n * Minimal structural view of a condition node. Matches both the ability-dsl\n * condition schema and the `secondary-card` award `when` field (a simple node\n * carries `type` + `parameters` + `negated`; a compound node carries\n * `operator` + `operands`).\n */\nexport interface Condition {\n type?: string;\n operator?: \"and\" | \"or\" | \"not\";\n operands?: Condition[];\n parameters?: Record<string, unknown>;\n negated?: boolean;\n}\n\n/** kebab-case → space-separated words (`enemy-territory` → `enemy territory`). */\nexport function dekebab(s: string): string {\n return s.replace(/-/g, \" \");\n}\n\nfunction str(v: unknown): string {\n return typeof v === \"string\" ? v : String(v);\n}\n\n/** `2` + `objective` → `2+ objectives`. Nouns here are all regular plurals. */\nfunction count(n: unknown, noun: string): string {\n return `${str(n)}+ ${noun}s`;\n}\n\nexport function describeCondition(c: Condition): string {\n // Compound nodes first — join the operands with lowercase connectives so the\n // result reads naturally inside a \"... when X and Y\" clause.\n if (c.operator === \"and\" && c.operands) {\n return c.operands.map(describeCondition).join(\" and \");\n }\n if (c.operator === \"or\" && c.operands) {\n return c.operands.map(describeCondition).join(\" or \");\n }\n if (c.operator === \"not\" && c.operands) {\n return `not (${c.operands.map(describeCondition).join(\", \")})`;\n }\n\n const negate = c.negated ? \"not \" : \"\";\n const p = c.parameters ?? {};\n\n switch (c.type) {\n // ── Ability-DSL conditions (ported from commands/translate.ts) ──────────\n case \"phase-is\":\n return `${negate}during the ${str(p.phase)} phase`;\n case \"timing-is\":\n return `${negate}at ${dekebab(str(p.timing))}`;\n case \"player-turn-is\":\n return `${negate}in ${p.turn === \"your-turn\" ? \"your\" : p.turn === \"opponent-turn\" ? \"the opponent's\" : \"either player's\"} turn`;\n case \"charged-this-turn\":\n return `${negate}the unit charged this turn`;\n case \"advanced-this-turn\":\n return `${negate}the unit advanced this turn`;\n case \"remained-stationary\":\n return `${negate}the unit remained stationary`;\n case \"unit-below-starting-strength\":\n return `${negate}the unit is below starting strength`;\n case \"unit-below-half-strength\":\n return `${negate}the unit is below half strength`;\n case \"unit-has-keyword\":\n return `${negate}the unit has \"${str(p.keyword)}\"`;\n case \"target-has-keyword\":\n return `${negate}the target has \"${str(p.keyword)}\"`;\n case \"model-is-leader\":\n return `${negate}the model is leading a unit`;\n case \"is-attached\":\n return `${negate}attached to a ${p.keyword ? `${str(p.keyword)} ` : \"\"}unit`;\n case \"attack-is-type\":\n return `${negate}for ${str(p.attack_type)} attacks`;\n case \"is-battle-shocked\":\n return `${negate}the unit is battle-shocked`;\n case \"has-lost-wounds\":\n return `${negate}the model has lost wounds`;\n case \"opponent-unit-within-range\":\n return `${negate}an enemy unit is within ${p.range === \"engagement\" ? \"engagement range\" : `${str(p.range)}\"`}`;\n case \"unit-within-range-of\":\n return `${negate}within ${str(p.range)}\" of ${str(p.target_type ?? \"target\")}${p.keyword ? ` (${str(p.keyword)})` : \"\"}`;\n case \"within-range-of-objective\":\n return `${negate}within range of an objective`;\n case \"has-fought-this-phase\":\n return `${negate}has fought this phase`;\n case \"destroyed-by-attack-type\":\n return `${negate}destroyed by a ${str(p.attack_type)} attack`;\n\n // ── Scoring conditions (secondary-card award `when`) ────────────────────\n case \"objective-majority\":\n return `${negate}you hold more objectives than the ${dekebab(str(p.relative_to ?? \"opponent\"))}`;\n case \"controls-objective\": {\n const noun = p.objective_role ? `${dekebab(str(p.objective_role))} objective` : \"objective\";\n let s = `${negate}you control ${count(p.count_min ?? 1, noun)}`;\n if (p.objective != null) s += ` (${dekebab(str(p.objective))})`;\n if (p.scope != null) s += ` in ${dekebab(str(p.scope))}`;\n if (p.exclude != null) s += ` (excluding ${dekebab(str(p.exclude))})`;\n return s;\n }\n case \"units-destroyed\":\n return `${negate}${count(p.count_min ?? 1, `${str(p.side)} unit`)} destroyed ${dekebab(str(p.window))}`;\n case \"units-destroyed-comparison\": {\n const subj = (p.subject ?? {}) as Record<string, unknown>;\n const ref = (p.reference ?? {}) as Record<string, unknown>;\n const cmp = p.comparator === \"greater-or-equal\" ? \"at least as many\" : \"more\";\n const link = p.comparator === \"greater-or-equal\" ? \"as\" : \"than\";\n return `${negate}you destroyed ${cmp} ${str(subj.side)} units ${dekebab(str(subj.window))} ${link} ${str(ref.side)} units ${dekebab(str(ref.window))}`;\n }\n case \"new-objective-controlled\":\n return `${negate}you newly control ${count(p.count_min ?? 1, \"objective\")} this turn`;\n case \"destroyed-while-on-objective\": {\n let s = `${negate}${count(p.count_min ?? 1, \"enemy unit\")} destroyed`;\n if (p.destroyer_on_objective) s += \" by a unit on an objective\";\n if (p.victim_on_objective) s += \" while on an objective\";\n return s;\n }\n case \"destroyed-in-tagged-terrain\": {\n const where = p.at_start_of_turn ? \"that started the turn in\" : \"while in\";\n return `${negate}${count(p.count_min ?? 1, \"enemy unit\")} destroyed ${where} ${dekebab(str(p.tag))} terrain`;\n }\n case \"action-completed\": {\n let s = `${negate}${count(p.count_min ?? 1, \"action\")} completed`;\n if (p.action_id != null) s += ` (${dekebab(str(p.action_id))})`;\n if (p.target_kind != null) s += ` on ${dekebab(str(p.target_kind))}`;\n const tf = (p.target_filter ?? {}) as Record<string, unknown>;\n if (tf.objective_role != null) s += ` (${dekebab(str(tf.objective_role))})`;\n if (tf.in_enemy_territory) s += \" in enemy territory\";\n if (tf.exclude != null) s += ` (excluding ${dekebab(str(tf.exclude))})`;\n if (p.window != null) s += ` ${dekebab(str(p.window))}`;\n return s;\n }\n case \"objective-has-tag\": {\n let s = `${negate}${count(p.count_min ?? 1, \"objective\")} tagged ${dekebab(str(p.tag))}`;\n if (p.count_max != null) s += ` (at most ${str(p.count_max)})`;\n if (p.objective != null) s += ` (${dekebab(str(p.objective))})`;\n if (p.scope != null) s += ` in ${dekebab(str(p.scope))}`;\n if (p.last_marked) s += \" (most recently marked)\";\n return s;\n }\n case \"unit-has-tag\": {\n let s = `${negate}${count(p.count_min ?? 1, `${str(p.side)} unit`)} tagged ${dekebab(str(p.tag))}`;\n if (p.window != null) s += ` (${dekebab(str(p.window))})`;\n return s;\n }\n case \"terrain-has-tag\": {\n let s = `${negate}terrain tagged ${dekebab(str(p.tag))}`;\n if (p.friendly_units_min != null) s += ` with ${str(p.friendly_units_min)}+ friendly units`;\n if (p.enemy_units_max != null) s += ` and at most ${str(p.enemy_units_max)} enemy units`;\n if (p.last_marked) s += \" (most recently marked)\";\n if (p.in_enemy_dz) s += \" in the enemy deployment zone\";\n return s;\n }\n case \"terrain-area-control\":\n return `${negate}you control a terrain area with ${str(p.min_models ?? 1)}+ models`;\n case \"territory-control\": {\n let s = `${negate}you control ${dekebab(str(p.territory_ref ?? \"your-territory\"))}`;\n if (p.enemy_units_max != null) s += ` with at most ${str(p.enemy_units_max)} enemy units`;\n return s;\n }\n case \"engagement-fronts\":\n return `${negate}you are engaged on ${str(p.count_min ?? 1)}+ fronts`;\n\n default:\n return `${negate}${dekebab(c.type ?? \"unknown\")}`;\n }\n}\n"]}
|
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* across language ports by the `conformance/scoring-translation` corpus.
|
|
6
6
|
*/
|
|
7
7
|
export { describeCondition, dekebab, type Condition } from "./condition.js";
|
|
8
|
-
export { describeTrigger, describeAward, describeScoringCard, type ScoringTrigger, type ScoringAward, } from "./scoring.js";
|
|
8
|
+
export { describeTrigger, describeAward, describeScoringCard, type ScoringTrigger, type ScoringAward, type ScoringMode, } from "./scoring.js";
|
|
9
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/translate/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EACL,eAAe,EACf,aAAa,EACb,mBAAmB,EACnB,KAAK,cAAc,EACnB,KAAK,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/translate/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EACL,eAAe,EACf,aAAa,EACb,mBAAmB,EACnB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,WAAW,GACjB,MAAM,cAAc,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/translate/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAkB,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EACL,eAAe,EACf,aAAa,EACb,mBAAmB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/translate/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAkB,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EACL,eAAe,EACf,aAAa,EACb,mBAAmB,GAIpB,MAAM,cAAc,CAAC","sourcesContent":["/**\n * Plain-English translation of structured game data — currently the\n * `secondary-card` scoring `awards` (mission \"how to play\" readouts) and the\n * shared Ability-DSL condition humanizer. Output is ASCII-only and pinned\n * across language ports by the `conformance/scoring-translation` corpus.\n */\nexport { describeCondition, dekebab, type Condition } from \"./condition.js\";\nexport {\n describeTrigger,\n describeAward,\n describeScoringCard,\n type ScoringTrigger,\n type ScoringAward,\n type ScoringMode,\n} from \"./scoring.js\";\n"]}
|
|
@@ -18,6 +18,8 @@ export interface ScoringTrigger {
|
|
|
18
18
|
max?: number;
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
+
/** The scoring approach a card is played under (cards that print both). */
|
|
22
|
+
export type ScoringMode = "fixed" | "tactical";
|
|
21
23
|
/** One VP-award block on a scoring card. */
|
|
22
24
|
export interface ScoringAward {
|
|
23
25
|
trigger?: ScoringTrigger;
|
|
@@ -26,8 +28,12 @@ export interface ScoringAward {
|
|
|
26
28
|
vp_per?: number;
|
|
27
29
|
per?: string;
|
|
28
30
|
per_max?: number;
|
|
31
|
+
/** Per-game VP ceiling for this award (the card's "UP TO N VP"). */
|
|
32
|
+
vp_max?: number;
|
|
29
33
|
cumulative?: boolean;
|
|
30
34
|
exclusive_group?: string;
|
|
35
|
+
/** Which scoring track this award belongs to, on cards that print both. */
|
|
36
|
+
mode?: ScoringMode;
|
|
31
37
|
}
|
|
32
38
|
/** "End of your Command phase (round 2+)" and friends. */
|
|
33
39
|
export declare function describeTrigger(t: ScoringTrigger): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scoring.d.ts","sourceRoot":"","sources":["../../src/translate/scoring.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAA8B,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE5E,sEAAsE;AACtE,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,eAAe,GAAG,aAAa,GAAG,gBAAgB,GAAG,cAAc,GAAG,eAAe,CAAC;IAC/F,KAAK,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAC;IACjE,WAAW,CAAC,EAAE,WAAW,GAAG,eAAe,GAAG,QAAQ,CAAC;IACvD,YAAY,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C;AAED,4CAA4C;AAC5C,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"scoring.d.ts","sourceRoot":"","sources":["../../src/translate/scoring.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAA8B,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE5E,sEAAsE;AACtE,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,eAAe,GAAG,aAAa,GAAG,gBAAgB,GAAG,cAAc,GAAG,eAAe,CAAC;IAC/F,KAAK,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAC;IACjE,WAAW,CAAC,EAAE,WAAW,GAAG,eAAe,GAAG,QAAQ,CAAC;IACvD,YAAY,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C;AAED,2EAA2E;AAC3E,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,UAAU,CAAC;AAE/C,4CAA4C;AAC5C,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAMD,0DAA0D;AAC1D,wBAAgB,eAAe,CAAC,CAAC,EAAE,cAAc,GAAG,MAAM,CAyCzD;AAED,qFAAqF;AACrF,wBAAgB,aAAa,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,CAiBrD;AAED,kFAAkF;AAClF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,EAAE,CAGjE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scoring.js","sourceRoot":"","sources":["../../src/translate/scoring.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAkB,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"scoring.js","sourceRoot":"","sources":["../../src/translate/scoring.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Humanize a `secondary-card` scoring `award` into plain English.\n *\n * Output is **ASCII-only** with a fixed clause order, pinned byte-for-byte\n * across the TS and Rust ports by the `conformance/scoring-translation` corpus.\n * The community `text` summary and the `actions` list are verbatim data, not\n * translation, so they are not produced here — only the structured `awards`.\n */\n\nimport type { SecondaryCard } from \"../generated.js\";\nimport { describeCondition, dekebab, type Condition } from \"./condition.js\";\n\n/** When a VP award is evaluated (the `trigger` block on an award). */\nexport interface ScoringTrigger {\n timing?: \"start-of-turn\" | \"end-of-turn\" | \"start-of-phase\" | \"end-of-phase\" | \"end-of-battle\";\n phase?: \"command\" | \"movement\" | \"shooting\" | \"charge\" | \"fight\";\n player_turn?: \"your-turn\" | \"opponent-turn\" | \"either\";\n battle_round?: { min?: number; max?: number };\n}\n\n/** The scoring approach a card is played under (cards that print both). */\nexport type ScoringMode = \"fixed\" | \"tactical\";\n\n/** One VP-award block on a scoring card. */\nexport interface ScoringAward {\n trigger?: ScoringTrigger;\n when?: Condition;\n vp?: number;\n vp_per?: number;\n per?: string;\n per_max?: number;\n /** Per-game VP ceiling for this award (the card's \"UP TO N VP\"). */\n vp_max?: number;\n cumulative?: boolean;\n exclusive_group?: string;\n /** Which scoring track this award belongs to, on cards that print both. */\n mode?: ScoringMode;\n}\n\nfunction capitalize(s: string): string {\n return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);\n}\n\n/** \"End of your Command phase (round 2+)\" and friends. */\nexport function describeTrigger(t: ScoringTrigger): string {\n const turn =\n t.player_turn === \"opponent-turn\"\n ? \"the opponent's\"\n : t.player_turn === \"either\"\n ? \"any\"\n : \"your\";\n\n let base: string;\n switch (t.timing) {\n case \"start-of-turn\":\n base = `Start of ${turn} turn`;\n break;\n case \"end-of-turn\":\n base = `End of ${turn} turn`;\n break;\n case \"start-of-phase\":\n base = `Start of ${turn} ${capitalize(t.phase ?? \"\")} phase`;\n break;\n case \"end-of-phase\":\n base = `End of ${turn} ${capitalize(t.phase ?? \"\")} phase`;\n break;\n case \"end-of-battle\":\n base = \"End of the battle\";\n break;\n default:\n base = t.phase ? `During ${turn} ${capitalize(t.phase)} phase` : \"Any time\";\n }\n\n const br = t.battle_round;\n if (br) {\n const { min, max } = br;\n if (min != null && max != null) {\n base += min === max ? ` (round ${min})` : ` (rounds ${min}-${max})`;\n } else if (min != null) {\n base += ` (round ${min}+)`;\n } else if (max != null) {\n base += ` (rounds 1-${max})`;\n }\n }\n return base;\n}\n\n/** \"End of your Command phase (round 2+): 3 VP per controlled objective when ...\" */\nexport function describeAward(a: ScoringAward): string {\n const trigger = a.trigger ? describeTrigger(a.trigger) : \"Any time\";\n\n let amount: string;\n if (a.vp != null) {\n amount = `${a.vp} VP`;\n } else if (a.vp_per != null) {\n amount = `${a.vp_per} VP per ${a.per ? dekebab(a.per) : \"instance\"}`;\n if (a.per_max != null) amount += ` (max ${a.per_max})`;\n } else {\n amount = \"no VP\";\n }\n\n const prefix = a.cumulative ? \"+ \" : \"\";\n const when = a.when ? ` when ${describeCondition(a.when)}` : \"\";\n const tier = a.exclusive_group ? \" [highest tier]\" : \"\";\n return `${prefix}${trigger}: ${amount}${when}${tier}`;\n}\n\n/** Humanize every award on a card, in array order (the order is load-bearing). */\nexport function describeScoringCard(card: SecondaryCard): string[] {\n const awards = (card.awards ?? []) as unknown as ScoringAward[];\n return awards.map(describeAward);\n}\n"]}
|
package/dist/validate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAS3B,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAS3B,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAgDD;;;GAGG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,GAAG,EACR,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,gBAAgB,CAAC,CAkF3B"}
|
package/dist/validate.js
CHANGED
|
@@ -17,6 +17,7 @@ const SCHEMA_MAP = {
|
|
|
17
17
|
enhancements: "https://40kdc.dev/schemas/core/enhancement.schema.json",
|
|
18
18
|
stratagems: "https://40kdc.dev/schemas/core/stratagem.schema.json",
|
|
19
19
|
"wargear-options": "https://40kdc.dev/schemas/core/wargear-option.schema.json",
|
|
20
|
+
wargear: "https://40kdc.dev/schemas/core/wargear.schema.json",
|
|
20
21
|
"leader-attachments": "https://40kdc.dev/schemas/core/leader-attachment.schema.json",
|
|
21
22
|
"unit-compositions": "https://40kdc.dev/schemas/core/unit-composition.schema.json",
|
|
22
23
|
"force-dispositions": "https://40kdc.dev/schemas/core/force-disposition.schema.json",
|
|
@@ -34,14 +35,17 @@ const SCHEMA_MAP = {
|
|
|
34
35
|
};
|
|
35
36
|
/**
|
|
36
37
|
* Determine which schema $id to use for a given data file path.
|
|
37
|
-
* Convention: the file's base name
|
|
38
|
+
* Convention: the file's base name starts with a SCHEMA_MAP prefix (real data is
|
|
39
|
+
* `<prefix>.json`; test fixtures are `<prefix>-good.json` / `<prefix>-bad.json`).
|
|
40
|
+
* Prefixes are tried longest-first so `wargear-options.json` resolves to the
|
|
41
|
+
* wargear-option schema rather than the shorter `wargear` key (distinct entities).
|
|
38
42
|
*/
|
|
43
|
+
const SCHEMA_PREFIXES = Object.keys(SCHEMA_MAP).sort((a, b) => b.length - a.length);
|
|
39
44
|
function resolveSchemaId(filePath) {
|
|
40
45
|
const base = basename(filePath);
|
|
41
|
-
for (const
|
|
42
|
-
if (base.startsWith(prefix))
|
|
43
|
-
return
|
|
44
|
-
}
|
|
46
|
+
for (const prefix of SCHEMA_PREFIXES) {
|
|
47
|
+
if (base.startsWith(prefix))
|
|
48
|
+
return SCHEMA_MAP[prefix];
|
|
45
49
|
}
|
|
46
50
|
return null;
|
|
47
51
|
}
|
|
@@ -60,6 +64,10 @@ export async function validateFiles(ajv, pattern, cwd) {
|
|
|
60
64
|
errors: [],
|
|
61
65
|
};
|
|
62
66
|
for (const file of files) {
|
|
67
|
+
// Underscore-prefixed files are scratch/reports (e.g. the converter's
|
|
68
|
+
// `_wargear-options.unparsed.json`), not dataset entities — skip them.
|
|
69
|
+
if (basename(file).startsWith("_"))
|
|
70
|
+
continue;
|
|
63
71
|
const schemaId = resolveSchemaId(file);
|
|
64
72
|
if (!schemaId) {
|
|
65
73
|
result.errors.push({
|
package/dist/validate.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC/D,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;AAgBnD;;GAEG;AACH,MAAM,UAAU,GAA2B;IACzC,QAAQ,EAAE,oDAAoD;IAC9D,KAAK,EAAE,iDAAiD;IACxD,OAAO,EAAE,mDAAmD;IAC5D,iBAAiB,EAAE,2DAA2D;IAC9E,eAAe,EAAE,yDAAyD;IAC1E,WAAW,EAAE,uDAAuD;IACpE,YAAY,EAAE,wDAAwD;IACtE,UAAU,EAAE,sDAAsD;IAClE,iBAAiB,EAAE,2DAA2D;IAC9E,oBAAoB,EAAE,8DAA8D;IACpF,mBAAmB,EAAE,6DAA6D;IAClF,oBAAoB,EAAE,8DAA8D;IACpF,qBAAqB,EAAE,+DAA+D;IACtF,kBAAkB,EAAE,4DAA4D;IAChF,QAAQ,EAAE,oDAAoD;IAC9D,iBAAiB,EAAE,2DAA2D;IAC9E,mBAAmB,EAAE,6DAA6D;IAClF,iBAAiB,EAAE,2DAA2D;IAC9E,gBAAgB,EAAE,gEAAgE;IAClF,cAAc,EAAE,8DAA8D;IAC9E,mBAAmB,EAAE,mEAAmE;IACxF,SAAS,EAAE,sEAAsE;IACjF,gBAAgB,EAAE,gEAAgE;CACnF,CAAC;AAEF
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"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","sourcesContent":["import type Ajv from \"ajv\";\nimport { readFileSync } from \"node:fs\";\nimport { glob } from \"glob\";\nimport { resolve, basename } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = fileURLToPath(new URL(\".\", import.meta.url));\nconst DATA_ROOT = resolve(__dirname, \"../../data\");\n\nexport interface ValidationError {\n file: string;\n index: number;\n errors: Array<{ path: string; message: string }>;\n}\n\nexport interface ValidationResult {\n totalFiles: number;\n totalItems: number;\n passed: number;\n failed: number;\n errors: ValidationError[];\n}\n\n/**\n * Map from data file base-name prefix to schema $id.\n */\nconst SCHEMA_MAP: Record<string, string> = {\n factions: \"https://40kdc.dev/schemas/core/faction.schema.json\",\n units: \"https://40kdc.dev/schemas/core/unit.schema.json\",\n weapons: \"https://40kdc.dev/schemas/core/weapon.schema.json\",\n \"weapon-keywords\": \"https://40kdc.dev/schemas/core/weapon-keyword.schema.json\",\n \"game-versions\": \"https://40kdc.dev/schemas/core/game-version.schema.json\",\n detachments: \"https://40kdc.dev/schemas/core/detachment.schema.json\",\n enhancements: \"https://40kdc.dev/schemas/core/enhancement.schema.json\",\n stratagems: \"https://40kdc.dev/schemas/core/stratagem.schema.json\",\n \"wargear-options\": \"https://40kdc.dev/schemas/core/wargear-option.schema.json\",\n wargear: \"https://40kdc.dev/schemas/core/wargear.schema.json\",\n \"leader-attachments\": \"https://40kdc.dev/schemas/core/leader-attachment.schema.json\",\n \"unit-compositions\": \"https://40kdc.dev/schemas/core/unit-composition.schema.json\",\n \"force-dispositions\": \"https://40kdc.dev/schemas/core/force-disposition.schema.json\",\n \"deployment-patterns\": \"https://40kdc.dev/schemas/core/deployment-pattern.schema.json\",\n \"mission-matchups\": \"https://40kdc.dev/schemas/core/mission-matchup.schema.json\",\n missions: \"https://40kdc.dev/schemas/core/mission.schema.json\",\n \"secondary-cards\": \"https://40kdc.dev/schemas/core/secondary-card.schema.json\",\n \"terrain-templates\": \"https://40kdc.dev/schemas/core/terrain-template.schema.json\",\n \"terrain-layouts\": \"https://40kdc.dev/schemas/core/terrain-layout.schema.json\",\n \"phase-mappings\": \"https://40kdc.dev/schemas/enrichment/phase-mapping.schema.json\",\n \"timing-flags\": \"https://40kdc.dev/schemas/enrichment/timing-flag.schema.json\",\n \"interaction-flags\": \"https://40kdc.dev/schemas/enrichment/interaction-flag.schema.json\",\n abilities: \"https://40kdc.dev/schemas/enrichment/ability-dsl/ability.schema.json\",\n \"resource-pools\": \"https://40kdc.dev/schemas/enrichment/resource-pool.schema.json\",\n};\n\n/**\n * Determine which schema $id to use for a given data file path.\n * Convention: the file's base name starts with a SCHEMA_MAP prefix (real data is\n * `<prefix>.json`; test fixtures are `<prefix>-good.json` / `<prefix>-bad.json`).\n * Prefixes are tried longest-first so `wargear-options.json` resolves to the\n * wargear-option schema rather than the shorter `wargear` key (distinct entities).\n */\nconst SCHEMA_PREFIXES = Object.keys(SCHEMA_MAP).sort((a, b) => b.length - a.length);\nfunction resolveSchemaId(filePath: string): string | null {\n const base = basename(filePath);\n for (const prefix of SCHEMA_PREFIXES) {\n if (base.startsWith(prefix)) return SCHEMA_MAP[prefix];\n }\n return null;\n}\n\n/**\n * Validate all data files matching the given glob pattern.\n * Each data file is expected to be a JSON array; each element is validated individually.\n */\nexport async function validateFiles(\n ajv: Ajv,\n pattern: string,\n cwd?: string,\n): Promise<ValidationResult> {\n const root = cwd ?? DATA_ROOT;\n const files = await glob(pattern, { cwd: root, absolute: true });\n\n const result: ValidationResult = {\n totalFiles: files.length,\n totalItems: 0,\n passed: 0,\n failed: 0,\n errors: [],\n };\n\n for (const file of files) {\n // Underscore-prefixed files are scratch/reports (e.g. the converter's\n // `_wargear-options.unparsed.json`), not dataset entities — skip them.\n if (basename(file).startsWith(\"_\")) continue;\n const schemaId = resolveSchemaId(file);\n if (!schemaId) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: `No schema mapping found for file: ${basename(file)}` }],\n });\n result.failed++;\n continue;\n }\n\n const validate = ajv.getSchema(schemaId);\n if (!validate) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: `Schema not found: ${schemaId}` }],\n });\n result.failed++;\n continue;\n }\n\n let data: unknown;\n try {\n const raw = readFileSync(file, \"utf-8\");\n data = JSON.parse(raw);\n } catch (err) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: `Failed to parse JSON: ${(err as Error).message}` }],\n });\n result.failed++;\n continue;\n }\n\n if (!Array.isArray(data)) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: \"Data file must be a JSON array\" }],\n });\n result.failed++;\n continue;\n }\n\n for (let i = 0; i < data.length; i++) {\n result.totalItems++;\n const valid = validate(data[i]);\n if (valid) {\n result.passed++;\n } else {\n result.failed++;\n result.errors.push({\n file,\n index: i,\n errors: (validate.errors ?? []).map((e) => ({\n path: e.instancePath || \"/\",\n message: e.message ?? \"Unknown validation error\",\n })),\n });\n }\n }\n }\n\n return result;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alpaca-software/40kdc-data",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
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
6
|
"keywords": [
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
"json-schema"
|
|
14
14
|
],
|
|
15
15
|
"license": "MIT",
|
|
16
|
-
"homepage": "https://
|
|
16
|
+
"homepage": "https://40kdc.alpacasoft.dev",
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/
|
|
19
|
+
"url": "git+https://github.com/wn-mitch/40kdc-data.git",
|
|
20
20
|
"directory": "tools"
|
|
21
21
|
},
|
|
22
22
|
"bugs": {
|
|
23
|
-
"url": "https://github.com/
|
|
23
|
+
"url": "https://github.com/wn-mitch/40kdc-data/issues"
|
|
24
24
|
},
|
|
25
25
|
"main": "./dist/index.js",
|
|
26
26
|
"types": "./dist/index.d.ts",
|
|
@@ -124,6 +124,20 @@
|
|
|
124
124
|
"type": "string",
|
|
125
125
|
"enum": ["obscuring", "hidden", "plunging-fire"],
|
|
126
126
|
"description": "An 11e terrain-area keyword. Confirmed launch set; extend as further keywords publish on dataslate."
|
|
127
|
+
},
|
|
128
|
+
"base-size": {
|
|
129
|
+
"type": "object",
|
|
130
|
+
"description": "A model's base. 'round' carries 'diameter'; 'oval' carries 'width'+'length'. 'flying-base' (with 'size': small/large), 'hull', and 'unique' are categories the GW base-size guide gives without standard millimetre dimensions; entries carrying such a category, or any millimetre value not taken from an authoritative source, set 'draft': true to mark them for later hand-authoring.",
|
|
131
|
+
"properties": {
|
|
132
|
+
"shape": { "enum": ["round", "oval", "flying-base", "hull", "unique"] },
|
|
133
|
+
"diameter": { "type": "number", "exclusiveMinimum": 0 },
|
|
134
|
+
"width": { "type": "number", "exclusiveMinimum": 0 },
|
|
135
|
+
"length": { "type": "number", "exclusiveMinimum": 0 },
|
|
136
|
+
"size": { "enum": ["small", "large"], "description": "Flying-base size class, when 'shape' is 'flying-base'." },
|
|
137
|
+
"draft": { "type": "boolean", "default": false, "description": "True when the entry is provisional/guessed (e.g. a category without authoritative dimensions) and should be revisited." }
|
|
138
|
+
},
|
|
139
|
+
"required": ["shape"],
|
|
140
|
+
"additionalProperties": false
|
|
127
141
|
}
|
|
128
142
|
}
|
|
129
143
|
}
|
|
@@ -186,6 +186,16 @@
|
|
|
186
186
|
"minimum": 1,
|
|
187
187
|
"description": "Optional cap on how many instances `vp_per` counts."
|
|
188
188
|
},
|
|
189
|
+
"vp_max": {
|
|
190
|
+
"type": "integer",
|
|
191
|
+
"minimum": 1,
|
|
192
|
+
"description": "Optional cap on the total VP this award can contribute over the game — the card's 'UP TO N VP' ceiling. Distinct from `per_max`, which caps the instance count; use `vp_max` when the printed ceiling is not a multiple of `vp_per` (e.g. Burden of Trust's '2VP per guarded objective, up to 9VP')."
|
|
193
|
+
},
|
|
194
|
+
"mode": {
|
|
195
|
+
"type": "string",
|
|
196
|
+
"enum": ["fixed", "tactical"],
|
|
197
|
+
"description": "Which scoring track this award belongs to on cards that print both. Fixed missions are chosen for the whole game and score the (usually lower) `fixed` values; Tactical missions are drawn each turn and score the `tactical` values. Omitted on cards that score the same regardless of approach. A card may carry parallel `fixed` and `tactical` awards for the same condition; a consumer scores only the awards matching the player's chosen approach."
|
|
198
|
+
},
|
|
189
199
|
"cumulative": {
|
|
190
200
|
"type": "boolean",
|
|
191
201
|
"default": false,
|
|
@@ -24,7 +24,11 @@
|
|
|
24
24
|
"type": "array",
|
|
25
25
|
"items": { "$ref": "../defs/common.schema.json#/$defs/entity-id" }
|
|
26
26
|
},
|
|
27
|
-
"is_leader_model": { "type": "boolean", "default": false }
|
|
27
|
+
"is_leader_model": { "type": "boolean", "default": false },
|
|
28
|
+
"base_size_mm": {
|
|
29
|
+
"description": "This model's base. Absent when no base could be resolved for the model.",
|
|
30
|
+
"$ref": "../defs/common.schema.json#/$defs/base-size"
|
|
31
|
+
}
|
|
28
32
|
},
|
|
29
33
|
"required": ["name", "min", "max"],
|
|
30
34
|
"additionalProperties": false
|
|
@@ -62,17 +62,9 @@
|
|
|
62
62
|
"keywords": { "$ref": "../defs/common.schema.json#/$defs/keyword-list" },
|
|
63
63
|
"faction_keywords": { "$ref": "../defs/common.schema.json#/$defs/keyword-list" },
|
|
64
64
|
"base_size_mm": {
|
|
65
|
+
"description": "The unit's representative base (the most-numerous model's base). Mixed-model units carry the full per-model breakdown in unit-composition; this top-level value is a convenience for consumers that need a single base.",
|
|
65
66
|
"oneOf": [
|
|
66
|
-
{
|
|
67
|
-
"type": "object",
|
|
68
|
-
"properties": {
|
|
69
|
-
"shape": { "enum": ["round", "oval"] },
|
|
70
|
-
"diameter": { "type": "number" },
|
|
71
|
-
"width": { "type": "number" },
|
|
72
|
-
"length": { "type": "number" }
|
|
73
|
-
},
|
|
74
|
-
"required": ["shape"]
|
|
75
|
-
},
|
|
67
|
+
{ "$ref": "../defs/common.schema.json#/$defs/base-size" },
|
|
76
68
|
{ "type": "null" }
|
|
77
69
|
]
|
|
78
70
|
},
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://40kdc.dev/schemas/core/wargear-option.schema.json",
|
|
4
4
|
"title": "Wargear Option",
|
|
5
|
-
"description": "A
|
|
5
|
+
"description": "A wargear option available to models within a unit: a weapon/wargear swap, a pure add-on, or a choice between alternatives. Models start with the unit's base loadout; an option modifies that loadout for the number of models its `model_constraint` permits.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"properties": {
|
|
8
8
|
"id": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
|
|
@@ -11,10 +11,16 @@
|
|
|
11
11
|
"oneOf": [
|
|
12
12
|
{
|
|
13
13
|
"type": "object",
|
|
14
|
+
"description": "Limits how many models may take this option. The eligible-model count is: `any_number` ? model_count : `per_n_models` ? floor(model_count / per_n_models) : (`max_count` ?? 1); then clamped by `max_count` when both are set. `model_name` scopes the option to a specific model profile (e.g. the unit's champion); omit for single-profile units.",
|
|
14
15
|
"properties": {
|
|
15
16
|
"model_name": { "type": "string", "minLength": 1 },
|
|
16
17
|
"per_n_models": { "type": "integer", "minimum": 1 },
|
|
17
|
-
"max_count": { "type": "integer", "minimum": 1 }
|
|
18
|
+
"max_count": { "type": "integer", "minimum": 1 },
|
|
19
|
+
"any_number": {
|
|
20
|
+
"type": "boolean",
|
|
21
|
+
"default": false,
|
|
22
|
+
"description": "When true, every model in the unit may take the option ('Any number of models can each ...'). Mutually exclusive in spirit with `per_n_models`."
|
|
23
|
+
}
|
|
18
24
|
},
|
|
19
25
|
"additionalProperties": false
|
|
20
26
|
},
|
|
@@ -25,13 +31,23 @@
|
|
|
25
31
|
"type": "array",
|
|
26
32
|
"items": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
|
|
27
33
|
"minItems": 1,
|
|
28
|
-
"description": "Weapon IDs
|
|
34
|
+
"description": "Weapon or wargear IDs removed from the model. Omit for a pure add-on (the option only equips new wargear)."
|
|
29
35
|
},
|
|
30
36
|
"replacement": {
|
|
31
37
|
"type": "array",
|
|
32
38
|
"items": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
|
|
33
39
|
"minItems": 1,
|
|
34
|
-
"description": "Weapon IDs
|
|
40
|
+
"description": "Weapon or wargear IDs added to the model — all of them. Exactly one of `replacement` / `replacement_choice` is present."
|
|
41
|
+
},
|
|
42
|
+
"replacement_choice": {
|
|
43
|
+
"type": "array",
|
|
44
|
+
"items": {
|
|
45
|
+
"type": "array",
|
|
46
|
+
"items": { "$ref": "../defs/common.schema.json#/$defs/entity-id" },
|
|
47
|
+
"minItems": 1
|
|
48
|
+
},
|
|
49
|
+
"minItems": 2,
|
|
50
|
+
"description": "A choice of replacements ('one of the following'): pick exactly one inner group; each group's IDs are all added together. Exactly one of `replacement` / `replacement_choice` is present."
|
|
35
51
|
},
|
|
36
52
|
"is_free": { "type": "boolean", "default": true },
|
|
37
53
|
"additional_cost": {
|
|
@@ -42,6 +58,16 @@
|
|
|
42
58
|
},
|
|
43
59
|
"game_version": { "$ref": "../defs/game-version-ref.schema.json" }
|
|
44
60
|
},
|
|
45
|
-
"required": ["id", "unit_id", "
|
|
46
|
-
"additionalProperties": false
|
|
61
|
+
"required": ["id", "unit_id", "game_version"],
|
|
62
|
+
"additionalProperties": false,
|
|
63
|
+
"allOf": [
|
|
64
|
+
{
|
|
65
|
+
"if": { "required": ["replacement"] },
|
|
66
|
+
"then": { "not": { "required": ["replacement_choice"] } }
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"if": { "not": { "required": ["replacement"] } },
|
|
70
|
+
"then": { "required": ["replacement_choice"] }
|
|
71
|
+
}
|
|
72
|
+
]
|
|
47
73
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://40kdc.dev/schemas/core/wargear.schema.json",
|
|
4
|
+
"title": "Wargear",
|
|
5
|
+
"description": "A non-weapon item a model may carry — an icon, attachment, or other piece of equipment with no weapon profile. Weapons live in weapon.schema.json; this entity exists so wargear-option swaps and add-ons can reference equipment that is not a weapon.",
|
|
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
|
+
"oneOf": [
|
|
12
|
+
{
|
|
13
|
+
"type": "string",
|
|
14
|
+
"minLength": 1,
|
|
15
|
+
"description": "Free-form grouping such as 'icon', 'upgrade', or 'equipment'."
|
|
16
|
+
},
|
|
17
|
+
{ "type": "null" }
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"game_version": { "$ref": "../defs/game-version-ref.schema.json" }
|
|
21
|
+
},
|
|
22
|
+
"required": ["id", "name", "game_version"],
|
|
23
|
+
"additionalProperties": false
|
|
24
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"simple-condition": {
|
|
13
13
|
"type": "object",
|
|
14
|
-
"$comment": "Board/meta-state and scoring predicates. `parameters` is intentionally open (additionalProperties: true); each type documents its own param convention. Scoring predicates added for mission cards: `units-destroyed` { side: 'enemy'|'friendly', window: 'this-turn'|'previous-turn', count_min: int } — at least count_min units of `side` were destroyed in `window`. `units-destroyed-comparison` { subject: {side, window}, comparator: 'greater-than'|'greater-or-equal', reference: {side, window} } — compares two destruction tallies (e.g. more enemy units destroyed this turn than friendly last turn). `objective-majority` { relative_to: 'opponent' } — you control more objectives than the named party. `controls-objective` params: { count_min: int, objective_role?: 'central'|'expansion'|'non-home'|'home', exclude?: 'home', objective?: 'opponent-home'|'your-home', scope?: 'enemy-territory'|'your-territory' }. Mission-card extensions (11e primary deck): `action-completed` { action_id?: string, target_kind?: 'objective'|'terrain'|'enemy-unit'|'self', target_filter?: { in_enemy_territory?: bool, objective_role?: 'central'|'non-home', exclude?: 'home' }, count_min: int, window?: 'this-turn'|'previous-turn'|'cumulative' } — at least count_min instances of a named action were completed in the window. `objective-has-tag` { tag: 'baited'|'triangulated'|'consecrated'|'sabotaged'|'marked'|'vanguard'|'spotted', count_min: int, count_max?: int, objective?: 'opponent-home'|'your-home', scope?: 'enemy-territory'|'your-territory' } — at least count_min objectives carry the named transient tag. `unit-has-tag` { tag: 'doomed'|'spotted', side: 'enemy'|'friendly', count_min: int, window?: 'destroyed-this-turn'|'still-on-board' } — at least count_min units of `side` carry the tag (optionally with a destruction filter — Punishment scores when a Doomed unit was destroyed or left the battlefield). `terrain-has-tag` { tag: 'mined'|'marked'|'vanguard', friendly_units_min?: int, enemy_units_max?: int, last_marked?: bool, in_enemy_dz?: bool } — terrain piece state predicate; `last_marked` selects the most-recently-marked piece (Find and Deny / Recover the Relics' Overwhelming Force trigger). `new-objective-controlled` { count_min: int } — at least count_min objectives are controlled this turn that were not controlled in the previous command phase. `engagement-fronts` { count_min: int } — friendly units engage enemies in at least count_min distinct fronts; a 'front' is one of the territory zones from the deployment-pattern's `territories[]`, so this composes with the existing `territory-control` predicate. `destroyed-while-on-objective` { destroyer_on_objective?: bool, victim_on_objective?: bool, count_min: int } — count_min enemy units were destroyed this turn under the named spatial condition (the destroying friendly unit, the destroyed enemy unit, or both were standing on an objective at the moment of the kill).",
|
|
14
|
+
"$comment": "Board/meta-state and scoring predicates. `parameters` is intentionally open (additionalProperties: true); each type documents its own param convention. Scoring predicates added for mission cards: `units-destroyed` { side: 'enemy'|'friendly', window: 'this-turn'|'previous-turn', count_min: int } — at least count_min units of `side` were destroyed in `window`. `units-destroyed-comparison` { subject: {side, window}, comparator: 'greater-than'|'greater-or-equal', reference: {side, window} } — compares two destruction tallies (e.g. more enemy units destroyed this turn than friendly last turn). `objective-majority` { relative_to: 'opponent' } — you control more objectives than the named party. `controls-objective` params: { count_min: int, objective_role?: 'central'|'expansion'|'non-home'|'home', exclude?: 'home', objective?: 'opponent-home'|'your-home', scope?: 'enemy-territory'|'your-territory' }. Mission-card extensions (11e primary deck): `action-completed` { action_id?: string, target_kind?: 'objective'|'terrain'|'enemy-unit'|'self', target_filter?: { in_enemy_territory?: bool, objective_role?: 'central'|'non-home', exclude?: 'home' }, count_min: int, window?: 'this-turn'|'previous-turn'|'cumulative' } — at least count_min instances of a named action were completed in the window. `objective-has-tag` { tag: 'baited'|'triangulated'|'consecrated'|'sabotaged'|'marked'|'vanguard'|'spotted', count_min: int, count_max?: int, objective?: 'opponent-home'|'your-home', scope?: 'enemy-territory'|'your-territory' } — at least count_min objectives carry the named transient tag. `unit-has-tag` { tag: 'doomed'|'spotted', side: 'enemy'|'friendly', count_min: int, window?: 'destroyed-this-turn'|'still-on-board' } — at least count_min units of `side` carry the tag (optionally with a destruction filter — Punishment scores when a Doomed unit was destroyed or left the battlefield). `terrain-has-tag` { tag: 'mined'|'marked'|'vanguard', friendly_units_min?: int, enemy_units_max?: int, last_marked?: bool, in_enemy_dz?: bool } — terrain piece state predicate; `last_marked` selects the most-recently-marked piece (Find and Deny / Recover the Relics' Overwhelming Force trigger). `new-objective-controlled` { count_min: int } — at least count_min objectives are controlled this turn that were not controlled in the previous command phase. `engagement-fronts` { count_min: int } — friendly units engage enemies in at least count_min distinct fronts; a 'front' is one of the territory zones from the deployment-pattern's `territories[]`, so this composes with the existing `territory-control` predicate. `destroyed-while-on-objective` { destroyer_on_objective?: bool, victim_on_objective?: bool, count_min: int } — count_min enemy units were destroyed this turn under the named spatial condition (the destroying friendly unit, the destroyed enemy unit, or both were standing on an objective at the moment of the kill). `destroyed-in-tagged-terrain` { tag: 'mined'|'marked'|'vanguard', at_start_of_turn?: bool, count_min: int } — count_min enemy units were destroyed this turn while in terrain carrying the named tag; with `at_start_of_turn` the victim must have been in that terrain at the start of the turn (Death Trap's Disruption kill bonus), otherwise the spatial test is at the moment of the kill (parallels `destroyed-while-on-objective`).",
|
|
15
15
|
"properties": {
|
|
16
16
|
"type": {
|
|
17
17
|
"type": "string",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"units-destroyed", "units-destroyed-comparison", "objective-majority",
|
|
31
31
|
"action-completed", "objective-has-tag", "unit-has-tag",
|
|
32
32
|
"terrain-has-tag", "new-objective-controlled",
|
|
33
|
-
"engagement-fronts", "destroyed-while-on-objective"
|
|
33
|
+
"engagement-fronts", "destroyed-while-on-objective",
|
|
34
|
+
"destroyed-in-tagged-terrain"
|
|
34
35
|
]
|
|
35
36
|
},
|
|
36
37
|
"parameters": { "type": "object", "additionalProperties": true },
|