@alpaca-software/40kdc-data 0.4.16 → 0.4.18
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/dist/author-batch.d.ts.map +1 -1
- package/dist/author-batch.js +3 -2
- package/dist/author-batch.js.map +1 -1
- package/dist/commands/translate.d.ts +3 -2
- package/dist/commands/translate.d.ts.map +1 -1
- package/dist/commands/translate.js +6 -154
- package/dist/commands/translate.js.map +1 -1
- package/dist/data/bundle.generated.js +1 -1
- package/dist/data/bundle.generated.js.map +1 -1
- package/dist/data/entities.d.ts +7 -0
- package/dist/data/entities.d.ts.map +1 -1
- package/dist/data/entities.js +10 -0
- package/dist/data/entities.js.map +1 -1
- package/dist/gen-conformance.js +57 -1
- package/dist/gen-conformance.js.map +1 -1
- package/dist/generated.d.ts +1 -1
- package/dist/generated.d.ts.map +1 -1
- package/dist/generated.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +16 -1
- package/dist/runner.js.map +1 -1
- package/dist/translate/condition.d.ts.map +1 -1
- package/dist/translate/condition.js +11 -0
- package/dist/translate/condition.js.map +1 -1
- package/dist/translate/effect.d.ts +72 -0
- package/dist/translate/effect.d.ts.map +1 -0
- package/dist/translate/effect.js +241 -0
- package/dist/translate/effect.js.map +1 -0
- package/dist/translate/index.d.ts +7 -4
- package/dist/translate/index.d.ts.map +1 -1
- package/dist/translate/index.js +7 -4
- package/dist/translate/index.js.map +1 -1
- package/package.json +2 -2
- package/schemas/enrichment/ability-dsl/condition.schema.json +2 -2
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Humanize an Ability-DSL `effect` tree into plain English — the
|
|
3
|
+
* `ability.print()` of the dataset. Output is an *approximation* generated
|
|
4
|
+
* purely from the structured data (no external rules text), ASCII-only, with
|
|
5
|
+
* a fixed clause order: it is pinned byte-for-byte across the TS and Rust
|
|
6
|
+
* ports by the `conformance/effect-translation` corpus, so any phrasing
|
|
7
|
+
* change here is a semantic corpus change (bump `conformance/SPEC_VERSION`).
|
|
8
|
+
*
|
|
9
|
+
* Container nodes (`sequence`, `conditional`, `choice`, `dice-gated`,
|
|
10
|
+
* `dice-pool-allocation`) render block-style with two-space indentation and
|
|
11
|
+
* an ASCII `-> ` arrow; leaves render as single clauses. Unknown leaf types
|
|
12
|
+
* and unrecognized modifier shapes degrade to a deterministic bracketed form
|
|
13
|
+
* (`[the-type]`) rather than failing — coverage improves as authoring does.
|
|
14
|
+
*/
|
|
15
|
+
import { describeCondition, dekebab } from "./condition.js";
|
|
16
|
+
/** JS-template stringification (numbers print without trailing `.0`). */
|
|
17
|
+
function jstr(v) {
|
|
18
|
+
if (v == null)
|
|
19
|
+
return "?";
|
|
20
|
+
if (Array.isArray(v))
|
|
21
|
+
return v.map(jstr).join(", ");
|
|
22
|
+
return String(v);
|
|
23
|
+
}
|
|
24
|
+
function formatTarget(t) {
|
|
25
|
+
return t ? dekebab(t) : "target";
|
|
26
|
+
}
|
|
27
|
+
function signed(operation, value) {
|
|
28
|
+
const op = operation === "add" || operation === "improve" ? "+" : "-";
|
|
29
|
+
return `${op}${jstr(value)}`;
|
|
30
|
+
}
|
|
31
|
+
function formatComparison(comp, threshold) {
|
|
32
|
+
const th = jstr(threshold);
|
|
33
|
+
switch (comp) {
|
|
34
|
+
case "gte":
|
|
35
|
+
return `${th}+`;
|
|
36
|
+
case "lte":
|
|
37
|
+
return `${th} or less`;
|
|
38
|
+
case "gt":
|
|
39
|
+
return `greater than ${th}`;
|
|
40
|
+
case "lt":
|
|
41
|
+
return `less than ${th}`;
|
|
42
|
+
case "eq":
|
|
43
|
+
return `exactly ${th}`;
|
|
44
|
+
default:
|
|
45
|
+
return `${th}+`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Single-clause translation for leaf effects (and inline container forms). */
|
|
49
|
+
export function describeEffectInline(e) {
|
|
50
|
+
const m = e.modifier ?? {};
|
|
51
|
+
const target = formatTarget(e.target);
|
|
52
|
+
switch (e.type) {
|
|
53
|
+
case "stat-modifier": {
|
|
54
|
+
const scope = m.attack_type ? ` (${jstr(m.attack_type)})` : "";
|
|
55
|
+
if (m.stat == null)
|
|
56
|
+
return `modify stats for ${target}`;
|
|
57
|
+
if (m.operation === "set")
|
|
58
|
+
return `set ${jstr(m.stat)} to ${jstr(m.value)}${scope} for ${target}`;
|
|
59
|
+
return `${signed(m.operation, m.value)} ${jstr(m.stat)}${scope} for ${target}`;
|
|
60
|
+
}
|
|
61
|
+
case "roll-modifier": {
|
|
62
|
+
const ctx = m.context ? ` (${jstr(m.context)})` : "";
|
|
63
|
+
if (m.value == null)
|
|
64
|
+
return `${dekebab(jstr(m.operation))} ${jstr(m.roll)} rolls${ctx} for ${target}`;
|
|
65
|
+
return `${signed(m.operation, m.value)} to ${jstr(m.roll)} rolls${ctx} for ${target}`;
|
|
66
|
+
}
|
|
67
|
+
case "re-roll": {
|
|
68
|
+
const subset = m.subset ? ` (${dekebab(jstr(m.subset))})` : "";
|
|
69
|
+
const atk = m.attack_type ? ` (${jstr(m.attack_type)})` : "";
|
|
70
|
+
return `re-roll ${jstr(m.roll)} rolls${subset}${atk} for ${target}`;
|
|
71
|
+
}
|
|
72
|
+
case "mortal-wounds": {
|
|
73
|
+
const amount = m.count ?? m.amount ?? (m.amount_table ? "variable" : "?");
|
|
74
|
+
const range = m.range ?? m.range_inches;
|
|
75
|
+
const within = range != null ? ` (within ${jstr(range)}")` : "";
|
|
76
|
+
return `deal ${jstr(amount)} mortal wounds to ${target}${within}`;
|
|
77
|
+
}
|
|
78
|
+
case "feel-no-pain":
|
|
79
|
+
return `${target} gains Feel No Pain ${jstr(m.threshold)}+`;
|
|
80
|
+
case "ward":
|
|
81
|
+
return `${target} gains Ward ${jstr(m.threshold ?? m.value)}+`;
|
|
82
|
+
case "invulnerable-save":
|
|
83
|
+
return `${target} gains a ${jstr(m.value)}+ invulnerable save`;
|
|
84
|
+
case "keyword-grant": {
|
|
85
|
+
const kw = Array.isArray(m.keywords) ? m.keywords.map(jstr).join(", ") : jstr(m.keyword ?? "keywords");
|
|
86
|
+
if (m.weapon_name != null)
|
|
87
|
+
return `${target}'s ${jstr(m.weapon_name)} gains ${kw}`;
|
|
88
|
+
if (m.weapon_type != null)
|
|
89
|
+
return `${target}'s ${jstr(m.weapon_type)} weapons gain ${kw}`;
|
|
90
|
+
return `${target}'s weapons gain ${kw}`;
|
|
91
|
+
}
|
|
92
|
+
case "ability-grant": {
|
|
93
|
+
const grant = m.grant_type ?? m.ability_id;
|
|
94
|
+
const cap = m.capacity != null ? ` (${jstr(m.capacity)})` : "";
|
|
95
|
+
return `${target} gains ${grant != null ? dekebab(jstr(grant)) : "an ability"}${cap}`;
|
|
96
|
+
}
|
|
97
|
+
case "movement-modifier": {
|
|
98
|
+
const kind = m.move_type ?? m.type;
|
|
99
|
+
const dist = m.distance ?? m.value;
|
|
100
|
+
const inches = dist != null ? ` ${jstr(dist)}"` : "";
|
|
101
|
+
return `${target} gains ${kind != null ? dekebab(jstr(kind)) : "a movement effect"}${inches}`;
|
|
102
|
+
}
|
|
103
|
+
case "damage-reduction":
|
|
104
|
+
return `reduce incoming damage to ${target} by ${jstr(m.amount ?? m.value)}`;
|
|
105
|
+
case "resurrection":
|
|
106
|
+
return `return ${jstr(m.count ?? 1)} model(s) to ${target} with ${jstr(m.wounds_remaining ?? "full")} wounds`;
|
|
107
|
+
case "model-destruction":
|
|
108
|
+
return `destroy ${jstr(m.count)} non-leader model(s) from ${target}`;
|
|
109
|
+
case "cp-gain":
|
|
110
|
+
return `gain ${jstr(m.amount)} CP`;
|
|
111
|
+
case "cp-refund":
|
|
112
|
+
return `refund ${jstr(m.amount)} CP`;
|
|
113
|
+
case "resource-gain":
|
|
114
|
+
return `gain ${jstr(m.amount)} to ${jstr(m.pool_id)}`;
|
|
115
|
+
case "resource-spend":
|
|
116
|
+
return `spend ${jstr(m.amount)} from ${jstr(m.pool_id)}`;
|
|
117
|
+
case "leadership-modifier": {
|
|
118
|
+
if (m.test != null && m.operation == null)
|
|
119
|
+
return `force a ${dekebab(jstr(m.test))} test on ${target}`;
|
|
120
|
+
if (m.test != null)
|
|
121
|
+
return `${dekebab(jstr(m.operation))} ${dekebab(jstr(m.test))} tests for ${target}`;
|
|
122
|
+
if (m.operation != null)
|
|
123
|
+
return `${signed(m.operation, m.value)} Leadership for ${target}`;
|
|
124
|
+
return `modify Leadership for ${target}`;
|
|
125
|
+
}
|
|
126
|
+
case "fight-first":
|
|
127
|
+
return `${target} fights first`;
|
|
128
|
+
case "fight-last":
|
|
129
|
+
return `${target} fights last`;
|
|
130
|
+
case "fight-on-death":
|
|
131
|
+
return `${target} fights on death`;
|
|
132
|
+
case "shoot-on-death":
|
|
133
|
+
return `${target} shoots on death`;
|
|
134
|
+
case "deep-strike":
|
|
135
|
+
return `${target} can deep strike`;
|
|
136
|
+
case "fallback-and-act":
|
|
137
|
+
return `${target} can fall back and act`;
|
|
138
|
+
case "attack-restriction": {
|
|
139
|
+
const what = m.restriction ?? m.restriction_type;
|
|
140
|
+
const range = m.range != null ? ` (within ${jstr(m.range)}")` : "";
|
|
141
|
+
const max = m.max_models != null ? ` (max ${jstr(m.max_models)} models)` : "";
|
|
142
|
+
return `${target}: ${what != null ? dekebab(jstr(what)) : "attack restriction"}${range}${max}`;
|
|
143
|
+
}
|
|
144
|
+
case "objective-control-modifier": {
|
|
145
|
+
if (m.operation != null)
|
|
146
|
+
return `${signed(m.operation, m.value)} OC for ${target}`;
|
|
147
|
+
return `modify OC of ${target} by ${jstr(m.value)}`;
|
|
148
|
+
}
|
|
149
|
+
case "bs-modifier":
|
|
150
|
+
return `${signed(m.operation, m.value)} BS for ${target}`;
|
|
151
|
+
case "charge-roll-modifier":
|
|
152
|
+
return `${signed(m.operation, m.value)} to charge rolls for ${target}`;
|
|
153
|
+
case "engagement-passthrough":
|
|
154
|
+
return `${target} can move through engagement range`;
|
|
155
|
+
case "terrain-area-tag":
|
|
156
|
+
return `tag the terrain area as ${dekebab(jstr(m.tag))}`;
|
|
157
|
+
case "objective-tag":
|
|
158
|
+
return `tag the objective as ${dekebab(jstr(m.tag))}`;
|
|
159
|
+
case "unit-tag":
|
|
160
|
+
return `tag ${target} as ${dekebab(jstr(m.tag))}`;
|
|
161
|
+
// Container types — inline forms.
|
|
162
|
+
case "conditional":
|
|
163
|
+
return `if ${describeCondition(e.condition ?? {})}: ${describeEffectInline(e.effect ?? {})}`;
|
|
164
|
+
case "sequence":
|
|
165
|
+
return (e.steps ?? []).map(describeEffectInline).join("; ");
|
|
166
|
+
case "choice": {
|
|
167
|
+
const label = e.choice_label ? ` (${e.choice_label})` : "";
|
|
168
|
+
return `choose one${label}: ${(e.options ?? []).map(describeEffectInline).join(" / ")}`;
|
|
169
|
+
}
|
|
170
|
+
case "dice-gated": {
|
|
171
|
+
const comp = formatComparison(e.comparison ?? "gte", e.threshold);
|
|
172
|
+
const success = e.on_success ? describeEffectInline(e.on_success) : "nothing";
|
|
173
|
+
const fail = e.on_fail ? `, otherwise ${describeEffectInline(e.on_fail)}` : "";
|
|
174
|
+
return `roll ${jstr(e.dice)}: on ${comp}, ${success}${fail}`;
|
|
175
|
+
}
|
|
176
|
+
case "dice-pool-allocation": {
|
|
177
|
+
const pool = e.pool ? `${jstr(e.pool.count)}${jstr(e.pool.die)}` : "?";
|
|
178
|
+
const opts = (e.options ?? [])
|
|
179
|
+
.map((o) => `${jstr(o.name)} (${jstr(o.requirement?.min_value)}+): ${describeEffectInline(o.effect ?? {})}`)
|
|
180
|
+
.join(" / ");
|
|
181
|
+
return `roll ${pool}: ${opts}`;
|
|
182
|
+
}
|
|
183
|
+
default:
|
|
184
|
+
return `[${e.type ?? "unknown"}]`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Block translation of an effect tree. Containers expand over multiple lines
|
|
189
|
+
* with two-space indentation; leaves delegate to `describeEffectInline`.
|
|
190
|
+
*/
|
|
191
|
+
export function describeEffect(e, depth = 0) {
|
|
192
|
+
const indent = " ".repeat(depth);
|
|
193
|
+
const arrow = depth > 0 ? "-> " : "";
|
|
194
|
+
switch (e.type) {
|
|
195
|
+
case "conditional":
|
|
196
|
+
return `${indent}If ${describeCondition(e.condition ?? {})}:\n` + describeEffect(e.effect ?? {}, depth + 1);
|
|
197
|
+
case "sequence":
|
|
198
|
+
return (e.steps ?? []).map((s) => describeEffect(s, depth)).join("\n");
|
|
199
|
+
case "choice": {
|
|
200
|
+
const label = e.choice_label ? ` (${e.choice_label})` : "";
|
|
201
|
+
return (`${indent}${arrow}Choose one${label}:\n` +
|
|
202
|
+
(e.options ?? []).map((o, i) => `${indent} ${i + 1}. ${describeEffectInline(o)}`).join("\n"));
|
|
203
|
+
}
|
|
204
|
+
case "dice-gated": {
|
|
205
|
+
const comp = formatComparison(e.comparison ?? "gte", e.threshold);
|
|
206
|
+
const success = e.on_success ? describeEffectInline(e.on_success) : "nothing";
|
|
207
|
+
const fail = e.on_fail ? `, otherwise ${describeEffectInline(e.on_fail)}` : "";
|
|
208
|
+
return `${indent}${arrow}Roll ${jstr(e.dice)}: on ${comp}, ${success}${fail}`;
|
|
209
|
+
}
|
|
210
|
+
case "dice-pool-allocation": {
|
|
211
|
+
const pool = e.pool ? `${jstr(e.pool.count)}${jstr(e.pool.die)}` : "?";
|
|
212
|
+
const lines = [`${indent}${arrow}Roll ${pool} (max ${jstr(e.max_activations)} activations):`];
|
|
213
|
+
for (const opt of e.options ?? []) {
|
|
214
|
+
lines.push(`${indent} - ${jstr(opt.name)}: need ${jstr(opt.requirement?.type)} of ${jstr(opt.requirement?.min_value)}+ -> ${describeEffectInline(opt.effect ?? {})}`);
|
|
215
|
+
}
|
|
216
|
+
return lines.join("\n");
|
|
217
|
+
}
|
|
218
|
+
default:
|
|
219
|
+
return `${indent}${arrow}${describeEffectInline(e)}`;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** `Scope: aura (6"). Duration: phase.` — empty string when absent. */
|
|
223
|
+
export function describeScope(s) {
|
|
224
|
+
if (!s || (!s.range && !s.duration))
|
|
225
|
+
return "";
|
|
226
|
+
const range = dekebab(s.range ?? "");
|
|
227
|
+
const inches = s.range_inches != null ? ` (${jstr(s.range_inches)}")` : "";
|
|
228
|
+
const duration = dekebab(s.duration ?? "");
|
|
229
|
+
return `Scope: ${range}${inches}. Duration: ${duration}.`;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Full generated text for an ability: the effect tree plus a trailing scope
|
|
233
|
+
* line. This is the `ability.print()` consumers render when the dataset
|
|
234
|
+
* carries no rules prose.
|
|
235
|
+
*/
|
|
236
|
+
export function describeAbility(a) {
|
|
237
|
+
const effect = a.effect ? describeEffect(a.effect) : "";
|
|
238
|
+
const scope = describeScope(a.scope);
|
|
239
|
+
return scope ? (effect ? `${effect}\n${scope}` : scope) : effect;
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=effect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effect.js","sourceRoot":"","sources":["../../src/translate/effect.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Humanize an Ability-DSL `effect` tree into plain English — the\n * `ability.print()` of the dataset. Output is an *approximation* generated\n * purely from the structured data (no external rules text), ASCII-only, with\n * a fixed clause order: it is pinned byte-for-byte across the TS and Rust\n * ports by the `conformance/effect-translation` corpus, so any phrasing\n * change here is a semantic corpus change (bump `conformance/SPEC_VERSION`).\n *\n * Container nodes (`sequence`, `conditional`, `choice`, `dice-gated`,\n * `dice-pool-allocation`) render block-style with two-space indentation and\n * an ASCII `-> ` arrow; leaves render as single clauses. Unknown leaf types\n * and unrecognized modifier shapes degrade to a deterministic bracketed form\n * (`[the-type]`) rather than failing — coverage improves as authoring does.\n */\n\nimport { describeCondition, dekebab, type Condition } from \"./condition.js\";\n\n/**\n * Minimal structural view of an effect node. Matches the ability-dsl effect\n * schema: a single effect carries `type` + `target` + `modifier`; containers\n * carry their own shape (`steps`, `options`, `condition`/`effect`, dice\n * fields).\n */\nexport interface Effect {\n type?: string;\n target?: string;\n modifier?: Record<string, unknown>;\n condition?: Condition;\n effect?: Effect;\n steps?: Effect[];\n options?: (Effect & {\n name?: string;\n requirement?: Record<string, unknown>;\n })[];\n choice_label?: string;\n dice?: string;\n threshold?: number | string;\n comparison?: string;\n on_success?: Effect | null;\n on_fail?: Effect | null;\n pool?: { count: number; die: string };\n max_activations?: number;\n}\n\n/** Ability scope, as carried on enrichment ability entries. */\nexport interface AbilityScope {\n range?: string;\n duration?: string;\n range_inches?: number;\n}\n\n/** Minimal ability view for `describeAbility`. */\nexport interface AbilityLike {\n name?: string;\n effect?: Effect;\n scope?: AbilityScope;\n}\n\n/** JS-template stringification (numbers print without trailing `.0`). */\nfunction jstr(v: unknown): string {\n if (v == null) return \"?\";\n if (Array.isArray(v)) return v.map(jstr).join(\", \");\n return String(v);\n}\n\nfunction formatTarget(t?: string): string {\n return t ? dekebab(t) : \"target\";\n}\n\nfunction signed(operation: unknown, value: unknown): string {\n const op = operation === \"add\" || operation === \"improve\" ? \"+\" : \"-\";\n return `${op}${jstr(value)}`;\n}\n\nfunction formatComparison(comp: string, threshold: unknown): string {\n const th = jstr(threshold);\n switch (comp) {\n case \"gte\":\n return `${th}+`;\n case \"lte\":\n return `${th} or less`;\n case \"gt\":\n return `greater than ${th}`;\n case \"lt\":\n return `less than ${th}`;\n case \"eq\":\n return `exactly ${th}`;\n default:\n return `${th}+`;\n }\n}\n\n/** Single-clause translation for leaf effects (and inline container forms). */\nexport function describeEffectInline(e: Effect): string {\n const m = e.modifier ?? {};\n const target = formatTarget(e.target);\n\n switch (e.type) {\n case \"stat-modifier\": {\n const scope = m.attack_type ? ` (${jstr(m.attack_type)})` : \"\";\n if (m.stat == null) return `modify stats for ${target}`;\n if (m.operation === \"set\") return `set ${jstr(m.stat)} to ${jstr(m.value)}${scope} for ${target}`;\n return `${signed(m.operation, m.value)} ${jstr(m.stat)}${scope} for ${target}`;\n }\n case \"roll-modifier\": {\n const ctx = m.context ? ` (${jstr(m.context)})` : \"\";\n if (m.value == null) return `${dekebab(jstr(m.operation))} ${jstr(m.roll)} rolls${ctx} for ${target}`;\n return `${signed(m.operation, m.value)} to ${jstr(m.roll)} rolls${ctx} for ${target}`;\n }\n case \"re-roll\": {\n const subset = m.subset ? ` (${dekebab(jstr(m.subset))})` : \"\";\n const atk = m.attack_type ? ` (${jstr(m.attack_type)})` : \"\";\n return `re-roll ${jstr(m.roll)} rolls${subset}${atk} for ${target}`;\n }\n case \"mortal-wounds\": {\n const amount = m.count ?? m.amount ?? (m.amount_table ? \"variable\" : \"?\");\n const range = m.range ?? m.range_inches;\n const within = range != null ? ` (within ${jstr(range)}\")` : \"\";\n return `deal ${jstr(amount)} mortal wounds to ${target}${within}`;\n }\n case \"feel-no-pain\":\n return `${target} gains Feel No Pain ${jstr(m.threshold)}+`;\n case \"ward\":\n return `${target} gains Ward ${jstr(m.threshold ?? m.value)}+`;\n case \"invulnerable-save\":\n return `${target} gains a ${jstr(m.value)}+ invulnerable save`;\n case \"keyword-grant\": {\n const kw = Array.isArray(m.keywords) ? m.keywords.map(jstr).join(\", \") : jstr(m.keyword ?? \"keywords\");\n if (m.weapon_name != null) return `${target}'s ${jstr(m.weapon_name)} gains ${kw}`;\n if (m.weapon_type != null) return `${target}'s ${jstr(m.weapon_type)} weapons gain ${kw}`;\n return `${target}'s weapons gain ${kw}`;\n }\n case \"ability-grant\": {\n const grant = m.grant_type ?? m.ability_id;\n const cap = m.capacity != null ? ` (${jstr(m.capacity)})` : \"\";\n return `${target} gains ${grant != null ? dekebab(jstr(grant)) : \"an ability\"}${cap}`;\n }\n case \"movement-modifier\": {\n const kind = m.move_type ?? m.type;\n const dist = m.distance ?? m.value;\n const inches = dist != null ? ` ${jstr(dist)}\"` : \"\";\n return `${target} gains ${kind != null ? dekebab(jstr(kind)) : \"a movement effect\"}${inches}`;\n }\n case \"damage-reduction\":\n return `reduce incoming damage to ${target} by ${jstr(m.amount ?? m.value)}`;\n case \"resurrection\":\n return `return ${jstr(m.count ?? 1)} model(s) to ${target} with ${jstr(m.wounds_remaining ?? \"full\")} wounds`;\n case \"model-destruction\":\n return `destroy ${jstr(m.count)} non-leader model(s) from ${target}`;\n case \"cp-gain\":\n return `gain ${jstr(m.amount)} CP`;\n case \"cp-refund\":\n return `refund ${jstr(m.amount)} CP`;\n case \"resource-gain\":\n return `gain ${jstr(m.amount)} to ${jstr(m.pool_id)}`;\n case \"resource-spend\":\n return `spend ${jstr(m.amount)} from ${jstr(m.pool_id)}`;\n case \"leadership-modifier\": {\n if (m.test != null && m.operation == null) return `force a ${dekebab(jstr(m.test))} test on ${target}`;\n if (m.test != null) return `${dekebab(jstr(m.operation))} ${dekebab(jstr(m.test))} tests for ${target}`;\n if (m.operation != null) return `${signed(m.operation, m.value)} Leadership for ${target}`;\n return `modify Leadership for ${target}`;\n }\n case \"fight-first\":\n return `${target} fights first`;\n case \"fight-last\":\n return `${target} fights last`;\n case \"fight-on-death\":\n return `${target} fights on death`;\n case \"shoot-on-death\":\n return `${target} shoots on death`;\n case \"deep-strike\":\n return `${target} can deep strike`;\n case \"fallback-and-act\":\n return `${target} can fall back and act`;\n case \"attack-restriction\": {\n const what = m.restriction ?? m.restriction_type;\n const range = m.range != null ? ` (within ${jstr(m.range)}\")` : \"\";\n const max = m.max_models != null ? ` (max ${jstr(m.max_models)} models)` : \"\";\n return `${target}: ${what != null ? dekebab(jstr(what)) : \"attack restriction\"}${range}${max}`;\n }\n case \"objective-control-modifier\": {\n if (m.operation != null) return `${signed(m.operation, m.value)} OC for ${target}`;\n return `modify OC of ${target} by ${jstr(m.value)}`;\n }\n case \"bs-modifier\":\n return `${signed(m.operation, m.value)} BS for ${target}`;\n case \"charge-roll-modifier\":\n return `${signed(m.operation, m.value)} to charge rolls for ${target}`;\n case \"engagement-passthrough\":\n return `${target} can move through engagement range`;\n case \"terrain-area-tag\":\n return `tag the terrain area as ${dekebab(jstr(m.tag))}`;\n case \"objective-tag\":\n return `tag the objective as ${dekebab(jstr(m.tag))}`;\n case \"unit-tag\":\n return `tag ${target} as ${dekebab(jstr(m.tag))}`;\n\n // Container types — inline forms.\n case \"conditional\":\n return `if ${describeCondition(e.condition ?? {})}: ${describeEffectInline(e.effect ?? {})}`;\n case \"sequence\":\n return (e.steps ?? []).map(describeEffectInline).join(\"; \");\n case \"choice\": {\n const label = e.choice_label ? ` (${e.choice_label})` : \"\";\n return `choose one${label}: ${(e.options ?? []).map(describeEffectInline).join(\" / \")}`;\n }\n case \"dice-gated\": {\n const comp = formatComparison(e.comparison ?? \"gte\", e.threshold);\n const success = e.on_success ? describeEffectInline(e.on_success) : \"nothing\";\n const fail = e.on_fail ? `, otherwise ${describeEffectInline(e.on_fail)}` : \"\";\n return `roll ${jstr(e.dice)}: on ${comp}, ${success}${fail}`;\n }\n case \"dice-pool-allocation\": {\n const pool = e.pool ? `${jstr(e.pool.count)}${jstr(e.pool.die)}` : \"?\";\n const opts = (e.options ?? [])\n .map((o) => `${jstr(o.name)} (${jstr(o.requirement?.min_value)}+): ${describeEffectInline(o.effect ?? {})}`)\n .join(\" / \");\n return `roll ${pool}: ${opts}`;\n }\n\n default:\n return `[${e.type ?? \"unknown\"}]`;\n }\n}\n\n/**\n * Block translation of an effect tree. Containers expand over multiple lines\n * with two-space indentation; leaves delegate to `describeEffectInline`.\n */\nexport function describeEffect(e: Effect, depth: number = 0): string {\n const indent = \" \".repeat(depth);\n const arrow = depth > 0 ? \"-> \" : \"\";\n\n switch (e.type) {\n case \"conditional\":\n return `${indent}If ${describeCondition(e.condition ?? {})}:\\n` + describeEffect(e.effect ?? {}, depth + 1);\n case \"sequence\":\n return (e.steps ?? []).map((s) => describeEffect(s, depth)).join(\"\\n\");\n case \"choice\": {\n const label = e.choice_label ? ` (${e.choice_label})` : \"\";\n return (\n `${indent}${arrow}Choose one${label}:\\n` +\n (e.options ?? []).map((o, i) => `${indent} ${i + 1}. ${describeEffectInline(o)}`).join(\"\\n\")\n );\n }\n case \"dice-gated\": {\n const comp = formatComparison(e.comparison ?? \"gte\", e.threshold);\n const success = e.on_success ? describeEffectInline(e.on_success) : \"nothing\";\n const fail = e.on_fail ? `, otherwise ${describeEffectInline(e.on_fail)}` : \"\";\n return `${indent}${arrow}Roll ${jstr(e.dice)}: on ${comp}, ${success}${fail}`;\n }\n case \"dice-pool-allocation\": {\n const pool = e.pool ? `${jstr(e.pool.count)}${jstr(e.pool.die)}` : \"?\";\n const lines = [`${indent}${arrow}Roll ${pool} (max ${jstr(e.max_activations)} activations):`];\n for (const opt of e.options ?? []) {\n lines.push(\n `${indent} - ${jstr(opt.name)}: need ${jstr(opt.requirement?.type)} of ${jstr(opt.requirement?.min_value)}+ -> ${describeEffectInline(opt.effect ?? {})}`\n );\n }\n return lines.join(\"\\n\");\n }\n default:\n return `${indent}${arrow}${describeEffectInline(e)}`;\n }\n}\n\n/** `Scope: aura (6\"). Duration: phase.` — empty string when absent. */\nexport function describeScope(s?: AbilityScope): string {\n if (!s || (!s.range && !s.duration)) return \"\";\n const range = dekebab(s.range ?? \"\");\n const inches = s.range_inches != null ? ` (${jstr(s.range_inches)}\")` : \"\";\n const duration = dekebab(s.duration ?? \"\");\n return `Scope: ${range}${inches}. Duration: ${duration}.`;\n}\n\n/**\n * Full generated text for an ability: the effect tree plus a trailing scope\n * line. This is the `ability.print()` consumers render when the dataset\n * carries no rules prose.\n */\nexport function describeAbility(a: AbilityLike): string {\n const effect = a.effect ? describeEffect(a.effect) : \"\";\n const scope = describeScope(a.scope);\n return scope ? (effect ? `${effect}\\n${scope}` : scope) : effect;\n}\n"]}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plain-English translation of structured game data —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* across language ports by the
|
|
2
|
+
* Plain-English translation of structured game data — the `secondary-card`
|
|
3
|
+
* scoring `awards` (mission "how to play" readouts), the shared Ability-DSL
|
|
4
|
+
* condition humanizer, and the Ability-DSL effect describer ("ability.print()").
|
|
5
|
+
* Output is ASCII-only and pinned across language ports by the
|
|
6
|
+
* `conformance/scoring-translation` and `conformance/effect-translation`
|
|
7
|
+
* corpora.
|
|
6
8
|
*/
|
|
7
9
|
export { describeCondition, dekebab, type Condition } from "./condition.js";
|
|
8
10
|
export { describeTrigger, describeAward, describeScoringCard, type ScoringTrigger, type ScoringAward, type ScoringMode, } from "./scoring.js";
|
|
11
|
+
export { describeEffect, describeEffectInline, describeAbility, describeScope, type Effect, type AbilityScope, type AbilityLike, } from "./effect.js";
|
|
9
12
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/translate/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/translate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;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;AACtB,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,aAAa,EACb,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,WAAW,GACjB,MAAM,aAAa,CAAC"}
|
package/dist/translate/index.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plain-English translation of structured game data —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* across language ports by the
|
|
2
|
+
* Plain-English translation of structured game data — the `secondary-card`
|
|
3
|
+
* scoring `awards` (mission "how to play" readouts), the shared Ability-DSL
|
|
4
|
+
* condition humanizer, and the Ability-DSL effect describer ("ability.print()").
|
|
5
|
+
* Output is ASCII-only and pinned across language ports by the
|
|
6
|
+
* `conformance/scoring-translation` and `conformance/effect-translation`
|
|
7
|
+
* corpora.
|
|
6
8
|
*/
|
|
7
9
|
export { describeCondition, dekebab } from "./condition.js";
|
|
8
10
|
export { describeTrigger, describeAward, describeScoringCard, } from "./scoring.js";
|
|
11
|
+
export { describeEffect, describeEffectInline, describeAbility, describeScope, } from "./effect.js";
|
|
9
12
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/translate/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/translate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAkB,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EACL,eAAe,EACf,aAAa,EACb,mBAAmB,GAIpB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,aAAa,GAId,MAAM,aAAa,CAAC","sourcesContent":["/**\n * Plain-English translation of structured game data — the `secondary-card`\n * scoring `awards` (mission \"how to play\" readouts), the shared Ability-DSL\n * condition humanizer, and the Ability-DSL effect describer (\"ability.print()\").\n * Output is ASCII-only and pinned across language ports by the\n * `conformance/scoring-translation` and `conformance/effect-translation`\n * corpora.\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\";\nexport {\n describeEffect,\n describeEffectInline,\n describeAbility,\n describeScope,\n type Effect,\n type AbilityScope,\n type AbilityLike,\n} from \"./effect.js\";\n"]}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alpaca-software/40kdc-data",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.18",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "The 40kdc Warhammer 40K dataset behind a linked, typed API
|
|
5
|
+
"description": "The 40kdc Warhammer 40K dataset behind a linked, typed API \u2014 find units, follow them to their weapons, abilities, phases, and factions. Also validates data against the canonical JSON Schemas.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"warhammer",
|
|
8
8
|
"warhammer-40k",
|
|
@@ -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'|'decoyed'|'cleansed'|'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'|'condemned'|'spotted'|'surveilled', side: 'enemy'|'friendly', count_min: int, window?: 'destroyed-this-turn'|'left-battlefield-this-turn'|'this-turn'|'still-on-board' } — at least count_min units of `side` carry the tag (optionally with an event window — Punishment scores when a condemned unit was destroyed or left the battlefield this turn; Surveil the Foe scores on units tagged this turn). `terrain-has-tag` { tag: 'mined'|'trapped'|'marked'|'vanguard'|'plundered', 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 four table quarters (board quadrants about the board's centre — each of the four areas formed by dividing the table along both centre lines). `destroyed-while-on-objective` { destroyer_on_objective?: bool, victim_on_objective?: bool, victim_started_turn_on_objective?: bool, objective_role?: 'central', 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 within range of an objective at the moment of the kill; `victim_started_turn_on_objective` instead tests the victim's position at the start of the turn, and `objective_role` narrows which objectives count — Secure Asset's central-objective kill row). `destroyed-in-tagged-terrain` { tag?: 'mined'|'trapped'|'marked'|'vanguard'|'plundered', 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 kill bonus), otherwise the spatial test is at the moment of the kill (parallels `destroyed-while-on-objective`). With `tag` omitted, any terrain area qualifies (Search and Scour). `operation-markers` { side?: 'friendly'|'opponent', count_min?: int, count_max?: int, within_range_of?: 'opponent-home-objective', friendly_unit_in_same_terrain_area?: bool, no_enemy_in_terrain_area?: bool } — counts operation markers on the battlefield (side omitted counts both sides' markers); count_max: 0 is 'none remain', count_min == count_max == 1 is 'exactly one'; the terrain-area flags add the co-location proviso used by Locate and Deny / Extract Relic ('one of your units is within the same terrain area as that marker, and no enemy units are within it').",
|
|
14
|
+
"$comment": "Board/meta-state and scoring predicates. `parameters` is intentionally open (additionalProperties: true); each type documents its own param convention. Combat-reactive predicate: `was-hit-by-attack` { subject?: 'self'|'target' (default 'self'), attack_type?: 'melee'|'ranged', weapon_name?: string, count_min?: int (default 1) } — the named unit was hit by at least count_min attacks this phase (distinct from `has-lost-wounds`, which fires only when a wound got through — a hit that is saved or shrugged still satisfies this). `subject:'self'` = the bearer was hit (reactive defensive triggers); `subject:'target'` = the unit the bearer attacked was hit (offensive follow-ups, e.g. a debuff applied to a unit the bearer's named weapon hit). Optional `attack_type`/`weapon_name` narrow which attacks count. 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'|'decoyed'|'cleansed'|'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'|'condemned'|'spotted'|'surveilled', side: 'enemy'|'friendly', count_min: int, window?: 'destroyed-this-turn'|'left-battlefield-this-turn'|'this-turn'|'still-on-board' } — at least count_min units of `side` carry the tag (optionally with an event window — Punishment scores when a condemned unit was destroyed or left the battlefield this turn; Surveil the Foe scores on units tagged this turn). `terrain-has-tag` { tag: 'mined'|'trapped'|'marked'|'vanguard'|'plundered', 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 four table quarters (board quadrants about the board's centre — each of the four areas formed by dividing the table along both centre lines). `destroyed-while-on-objective` { destroyer_on_objective?: bool, victim_on_objective?: bool, victim_started_turn_on_objective?: bool, objective_role?: 'central', 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 within range of an objective at the moment of the kill; `victim_started_turn_on_objective` instead tests the victim's position at the start of the turn, and `objective_role` narrows which objectives count — Secure Asset's central-objective kill row). `destroyed-in-tagged-terrain` { tag?: 'mined'|'trapped'|'marked'|'vanguard'|'plundered', 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 kill bonus), otherwise the spatial test is at the moment of the kill (parallels `destroyed-while-on-objective`). With `tag` omitted, any terrain area qualifies (Search and Scour). `operation-markers` { side?: 'friendly'|'opponent', count_min?: int, count_max?: int, within_range_of?: 'opponent-home-objective', friendly_unit_in_same_terrain_area?: bool, no_enemy_in_terrain_area?: bool } — counts operation markers on the battlefield (side omitted counts both sides' markers); count_max: 0 is 'none remain', count_min == count_max == 1 is 'exactly one'; the terrain-area flags add the co-location proviso used by Locate and Deny / Extract Relic ('one of your units is within the same terrain area as that marker, and no enemy units are within it').",
|
|
15
15
|
"properties": {
|
|
16
16
|
"type": {
|
|
17
17
|
"type": "string",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"unit-has-keyword", "unit-within-range-of",
|
|
22
22
|
"model-is-leader", "target-has-keyword",
|
|
23
23
|
"charged-this-turn", "advanced-this-turn", "remained-stationary",
|
|
24
|
-
"is-battle-shocked", "has-lost-wounds",
|
|
24
|
+
"is-battle-shocked", "has-lost-wounds", "was-hit-by-attack",
|
|
25
25
|
"opponent-unit-within-range", "within-range-of-objective",
|
|
26
26
|
"attack-is-type", "has-fought-this-phase",
|
|
27
27
|
"destroyed-by-attack-type", "controls-objective", "is-attached",
|