@alpaca-software/40kdc-data 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/dist/bundle-schemas.d.ts +3 -0
- package/dist/bundle-schemas.js +137 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +31 -0
- package/dist/codegen-data.d.ts +1 -0
- package/dist/codegen-data.js +128 -0
- package/dist/commands/translate.d.ts +7 -0
- package/dist/commands/translate.js +238 -0
- package/dist/commands/validate-all.d.ts +3 -0
- package/dist/commands/validate-all.js +20 -0
- package/dist/commands/validate-core.d.ts +3 -0
- package/dist/commands/validate-core.js +12 -0
- package/dist/commands/validate-enrichment.d.ts +3 -0
- package/dist/commands/validate-enrichment.js +12 -0
- package/dist/convert-faction.d.ts +45 -0
- package/dist/convert-faction.js +479 -0
- package/dist/converters/configs/adepta-sororitas.d.ts +3 -0
- package/dist/converters/configs/adepta-sororitas.js +70 -0
- package/dist/converters/configs/adeptus-astartes.d.ts +3 -0
- package/dist/converters/configs/adeptus-astartes.js +74 -0
- package/dist/converters/configs/adeptus-custodes.d.ts +3 -0
- package/dist/converters/configs/adeptus-custodes.js +14 -0
- package/dist/converters/configs/adeptus-mechanicus.d.ts +3 -0
- package/dist/converters/configs/adeptus-mechanicus.js +51 -0
- package/dist/converters/configs/aeldari.d.ts +3 -0
- package/dist/converters/configs/aeldari.js +79 -0
- package/dist/converters/configs/agents-of-the-imperium.d.ts +3 -0
- package/dist/converters/configs/agents-of-the-imperium.js +57 -0
- package/dist/converters/configs/astra-militarum.d.ts +3 -0
- package/dist/converters/configs/astra-militarum.js +80 -0
- package/dist/converters/configs/black-templars.d.ts +3 -0
- package/dist/converters/configs/black-templars.js +16 -0
- package/dist/converters/configs/blood-angels.d.ts +3 -0
- package/dist/converters/configs/blood-angels.js +16 -0
- package/dist/converters/configs/chaos-daemons.d.ts +3 -0
- package/dist/converters/configs/chaos-daemons.js +40 -0
- package/dist/converters/configs/chaos-knights.d.ts +3 -0
- package/dist/converters/configs/chaos-knights.js +14 -0
- package/dist/converters/configs/chaos-space-marines.d.ts +3 -0
- package/dist/converters/configs/chaos-space-marines.js +95 -0
- package/dist/converters/configs/crimson-fists.d.ts +3 -0
- package/dist/converters/configs/crimson-fists.js +16 -0
- package/dist/converters/configs/dark-angels.d.ts +3 -0
- package/dist/converters/configs/dark-angels.js +16 -0
- package/dist/converters/configs/death-guard.d.ts +3 -0
- package/dist/converters/configs/death-guard.js +30 -0
- package/dist/converters/configs/deathwatch.d.ts +3 -0
- package/dist/converters/configs/deathwatch.js +16 -0
- package/dist/converters/configs/drukhari.d.ts +3 -0
- package/dist/converters/configs/drukhari.js +51 -0
- package/dist/converters/configs/emperors-children.d.ts +3 -0
- package/dist/converters/configs/emperors-children.js +38 -0
- package/dist/converters/configs/genestealer-cults.d.ts +3 -0
- package/dist/converters/configs/genestealer-cults.js +36 -0
- package/dist/converters/configs/grey-knights.d.ts +3 -0
- package/dist/converters/configs/grey-knights.js +39 -0
- package/dist/converters/configs/imperial-fists.d.ts +3 -0
- package/dist/converters/configs/imperial-fists.js +16 -0
- package/dist/converters/configs/imperial-knights.d.ts +3 -0
- package/dist/converters/configs/imperial-knights.js +14 -0
- package/dist/converters/configs/iron-hands.d.ts +3 -0
- package/dist/converters/configs/iron-hands.js +16 -0
- package/dist/converters/configs/leagues-of-votann.d.ts +3 -0
- package/dist/converters/configs/leagues-of-votann.js +32 -0
- package/dist/converters/configs/necrons.d.ts +3 -0
- package/dist/converters/configs/necrons.js +19 -0
- package/dist/converters/configs/orks.d.ts +3 -0
- package/dist/converters/configs/orks.js +71 -0
- package/dist/converters/configs/raven-guard.d.ts +3 -0
- package/dist/converters/configs/raven-guard.js +16 -0
- package/dist/converters/configs/salamanders.d.ts +3 -0
- package/dist/converters/configs/salamanders.js +16 -0
- package/dist/converters/configs/space-wolves.d.ts +3 -0
- package/dist/converters/configs/space-wolves.js +16 -0
- package/dist/converters/configs/tau-empire.d.ts +3 -0
- package/dist/converters/configs/tau-empire.js +44 -0
- package/dist/converters/configs/thousand-sons.d.ts +3 -0
- package/dist/converters/configs/thousand-sons.js +30 -0
- package/dist/converters/configs/tyranids.d.ts +3 -0
- package/dist/converters/configs/tyranids.js +27 -0
- package/dist/converters/configs/ultramarines.d.ts +3 -0
- package/dist/converters/configs/ultramarines.js +16 -0
- package/dist/converters/configs/white-scars.d.ts +3 -0
- package/dist/converters/configs/white-scars.js +16 -0
- package/dist/converters/configs/world-eaters.d.ts +3 -0
- package/dist/converters/configs/world-eaters.js +43 -0
- package/dist/converters/faction-config.d.ts +53 -0
- package/dist/converters/faction-config.js +22 -0
- package/dist/converters/id-generator.d.ts +14 -0
- package/dist/converters/id-generator.js +65 -0
- package/dist/converters/keyword-filter.d.ts +26 -0
- package/dist/converters/keyword-filter.js +78 -0
- package/dist/converters/stat-parser.d.ts +22 -0
- package/dist/converters/stat-parser.js +84 -0
- package/dist/converters/view-selector.d.ts +54 -0
- package/dist/converters/view-selector.js +96 -0
- package/dist/converters/weapon-dedup.d.ts +60 -0
- package/dist/converters/weapon-dedup.js +120 -0
- package/dist/data/bundle.generated.d.ts +3 -0
- package/dist/data/bundle.generated.js +3 -0
- package/dist/data/collection.d.ts +64 -0
- package/dist/data/collection.js +118 -0
- package/dist/data/dataset.d.ts +50 -0
- package/dist/data/dataset.js +134 -0
- package/dist/data/entities.d.ts +80 -0
- package/dist/data/entities.js +133 -0
- package/dist/data/index.d.ts +59 -0
- package/dist/data/index.js +57 -0
- package/dist/data/normalize.d.ts +29 -0
- package/dist/data/normalize.js +37 -0
- package/dist/data/types.d.ts +43 -0
- package/dist/data/types.js +25 -0
- package/dist/generated.d.ts +1084 -0
- package/dist/generated.js +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/known-support-10e.d.ts +31 -0
- package/dist/known-support-10e.js +113 -0
- package/dist/port-10e-faction.d.ts +52 -0
- package/dist/port-10e-faction.js +413 -0
- package/dist/report.d.ts +3 -0
- package/dist/report.js +31 -0
- package/dist/schema-loader.d.ts +15 -0
- package/dist/schema-loader.js +79 -0
- package/dist/validate.d.ts +21 -0
- package/dist/validate.js +124 -0
- package/package.json +77 -0
- package/schemas/$defs/common.schema.json +86 -0
- package/schemas/$defs/game-version-ref.schema.json +11 -0
- package/schemas/core/deployment-pattern.schema.json +102 -0
- package/schemas/core/detachment.schema.json +56 -0
- package/schemas/core/enhancement.schema.json +46 -0
- package/schemas/core/faction.schema.json +29 -0
- package/schemas/core/force-disposition.schema.json +22 -0
- package/schemas/core/game-version.schema.json +20 -0
- package/schemas/core/leader-attachment.schema.json +18 -0
- package/schemas/core/mission-matchup.schema.json +25 -0
- package/schemas/core/mission.schema.json +42 -0
- package/schemas/core/roster.schema.json +203 -0
- package/schemas/core/secondary-card.schema.json +195 -0
- package/schemas/core/stratagem.schema.json +58 -0
- package/schemas/core/terrain-layout.schema.json +135 -0
- package/schemas/core/unit-composition.schema.json +38 -0
- package/schemas/core/unit.schema.json +125 -0
- package/schemas/core/wargear-option.schema.json +47 -0
- package/schemas/core/weapon.schema.json +56 -0
- package/schemas/enrichment/ability-dsl/ability.schema.json +60 -0
- package/schemas/enrichment/ability-dsl/condition.schema.json +48 -0
- package/schemas/enrichment/ability-dsl/effect.schema.json +145 -0
- package/schemas/enrichment/ability-dsl/scope.schema.json +12 -0
- package/schemas/enrichment/interaction-flag.schema.json +17 -0
- package/schemas/enrichment/phase-mapping.schema.json +14 -0
- package/schemas/enrichment/resource-pool.schema.json +36 -0
- package/schemas/enrichment/timing-flag.schema.json +28 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** How a {@link Collection} reads keys and builds views from raw records. */
|
|
2
|
+
export interface CollectionConfig<T, V> {
|
|
3
|
+
items: T[];
|
|
4
|
+
/** Primary id of a record (e.g. `u => u.id`, `a => a.ability_id`). */
|
|
5
|
+
idOf: (item: T) => string;
|
|
6
|
+
/**
|
|
7
|
+
* Uniqueness key used for deduplication. Defaults to {@link idOf}. Set to a
|
|
8
|
+
* composite (e.g. `(faction_id, id)`) for records that share an id across
|
|
9
|
+
* factions, so distinct copies are preserved rather than collapsed.
|
|
10
|
+
*/
|
|
11
|
+
dedupeKeyOf?: (item: T) => string;
|
|
12
|
+
/** Display name, if the record has one — drives {@link Collection.find}. */
|
|
13
|
+
nameOf?: (item: T) => string | undefined;
|
|
14
|
+
/** Owning faction id, if applicable — drives {@link Collection.byFaction}. */
|
|
15
|
+
factionOf?: (item: T) => string | null | undefined;
|
|
16
|
+
/** Wrap a raw record in its linked view. */
|
|
17
|
+
wrap: (item: T) => V;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A collection of one entity type, exposing id/name/faction lookups.
|
|
21
|
+
*
|
|
22
|
+
* Iterable: `for (const unit of units) { … }`.
|
|
23
|
+
*
|
|
24
|
+
* @typeParam T - the raw (generated) record type
|
|
25
|
+
* @typeParam V - the linked view type returned to callers
|
|
26
|
+
*/
|
|
27
|
+
export declare class Collection<T, V> implements Iterable<V> {
|
|
28
|
+
private readonly items;
|
|
29
|
+
private readonly byId;
|
|
30
|
+
private readonly byNorm;
|
|
31
|
+
private readonly byFactionId;
|
|
32
|
+
private readonly nameOf?;
|
|
33
|
+
private readonly wrapFn;
|
|
34
|
+
constructor(cfg: CollectionConfig<T, V>);
|
|
35
|
+
/** Every record, deduplicated by id, in first-seen order. */
|
|
36
|
+
get all(): V[];
|
|
37
|
+
/** Number of distinct records. */
|
|
38
|
+
get size(): number;
|
|
39
|
+
/** Look up by exact id. */
|
|
40
|
+
get(id: string): V | undefined;
|
|
41
|
+
/** Whether a record with this exact id exists. */
|
|
42
|
+
has(id: string): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Find one record by id or name. Name matching is diacritic- and
|
|
45
|
+
* punctuation-insensitive (see {@link normalizeName}), trying, in order:
|
|
46
|
+
* exact id → exact normalized name → normalized-name substring. Returns the
|
|
47
|
+
* first match; names can repeat across factions, so use {@link findAll} or
|
|
48
|
+
* {@link byFaction} when a query may be ambiguous.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* units.find("Kharn"); // resolves "Khârn the Betrayer"
|
|
52
|
+
*/
|
|
53
|
+
find(query: string): V | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* All records matching a query, by the same rules as {@link find}. An exact id
|
|
56
|
+
* match returns just that record; otherwise every normalized-name-exact match
|
|
57
|
+
* is returned, falling back to every normalized-name-substring match. Useful
|
|
58
|
+
* to surface (rather than silently collapse) names shared across factions.
|
|
59
|
+
*/
|
|
60
|
+
findAll(query: string): V[];
|
|
61
|
+
/** All records belonging to a faction id (empty if the type has no faction). */
|
|
62
|
+
byFaction(factionId: string): V[];
|
|
63
|
+
[Symbol.iterator](): Iterator<V>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A queryable, iterable view over one entity collection.
|
|
3
|
+
*
|
|
4
|
+
* Indexes (by id, by normalized name, by faction) are built once at construction.
|
|
5
|
+
* Records are deduplicated by {@link CollectionConfig.dedupeKeyOf} (default: id,
|
|
6
|
+
* first occurrence wins). Some records are intentionally shared: the same unit
|
|
7
|
+
* id (e.g. `ministorum-priest`) appears under several factions, so units dedupe
|
|
8
|
+
* on `(faction_id, id)` to keep each faction's copy; identical core abilities
|
|
9
|
+
* (e.g. `leader`) copied into many faction files dedupe away on `ability_id`.
|
|
10
|
+
*
|
|
11
|
+
* `get(id)`/`find` return the first match when an id is shared across factions;
|
|
12
|
+
* use {@link Collection.byFaction} or {@link Collection.findAll} to disambiguate.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
import { normalizeName } from "./normalize.js";
|
|
17
|
+
/**
|
|
18
|
+
* A collection of one entity type, exposing id/name/faction lookups.
|
|
19
|
+
*
|
|
20
|
+
* Iterable: `for (const unit of units) { … }`.
|
|
21
|
+
*
|
|
22
|
+
* @typeParam T - the raw (generated) record type
|
|
23
|
+
* @typeParam V - the linked view type returned to callers
|
|
24
|
+
*/
|
|
25
|
+
export class Collection {
|
|
26
|
+
items = [];
|
|
27
|
+
byId = new Map();
|
|
28
|
+
byNorm = new Map();
|
|
29
|
+
byFactionId = new Map();
|
|
30
|
+
nameOf;
|
|
31
|
+
wrapFn;
|
|
32
|
+
constructor(cfg) {
|
|
33
|
+
this.nameOf = cfg.nameOf;
|
|
34
|
+
this.wrapFn = cfg.wrap;
|
|
35
|
+
const dedupeKeyOf = cfg.dedupeKeyOf ?? cfg.idOf;
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
for (const item of cfg.items) {
|
|
38
|
+
const dedupeKey = dedupeKeyOf(item);
|
|
39
|
+
if (seen.has(dedupeKey))
|
|
40
|
+
continue; // first-wins dedup
|
|
41
|
+
seen.add(dedupeKey);
|
|
42
|
+
this.items.push(item);
|
|
43
|
+
const id = cfg.idOf(item);
|
|
44
|
+
if (!this.byId.has(id))
|
|
45
|
+
this.byId.set(id, item); // first-wins for shared ids
|
|
46
|
+
const name = cfg.nameOf?.(item);
|
|
47
|
+
if (name)
|
|
48
|
+
push(this.byNorm, normalizeName(name), item);
|
|
49
|
+
const faction = cfg.factionOf?.(item);
|
|
50
|
+
if (faction)
|
|
51
|
+
push(this.byFactionId, faction, item);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Every record, deduplicated by id, in first-seen order. */
|
|
55
|
+
get all() {
|
|
56
|
+
return this.items.map((item) => this.wrapFn(item));
|
|
57
|
+
}
|
|
58
|
+
/** Number of distinct records. */
|
|
59
|
+
get size() {
|
|
60
|
+
return this.items.length;
|
|
61
|
+
}
|
|
62
|
+
/** Look up by exact id. */
|
|
63
|
+
get(id) {
|
|
64
|
+
const item = this.byId.get(id);
|
|
65
|
+
return item ? this.wrapFn(item) : undefined;
|
|
66
|
+
}
|
|
67
|
+
/** Whether a record with this exact id exists. */
|
|
68
|
+
has(id) {
|
|
69
|
+
return this.byId.has(id);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Find one record by id or name. Name matching is diacritic- and
|
|
73
|
+
* punctuation-insensitive (see {@link normalizeName}), trying, in order:
|
|
74
|
+
* exact id → exact normalized name → normalized-name substring. Returns the
|
|
75
|
+
* first match; names can repeat across factions, so use {@link findAll} or
|
|
76
|
+
* {@link byFaction} when a query may be ambiguous.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* units.find("Kharn"); // resolves "Khârn the Betrayer"
|
|
80
|
+
*/
|
|
81
|
+
find(query) {
|
|
82
|
+
return this.findAll(query)[0];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* All records matching a query, by the same rules as {@link find}. An exact id
|
|
86
|
+
* match returns just that record; otherwise every normalized-name-exact match
|
|
87
|
+
* is returned, falling back to every normalized-name-substring match. Useful
|
|
88
|
+
* to surface (rather than silently collapse) names shared across factions.
|
|
89
|
+
*/
|
|
90
|
+
findAll(query) {
|
|
91
|
+
const byId = this.byId.get(query);
|
|
92
|
+
if (byId)
|
|
93
|
+
return [this.wrapFn(byId)];
|
|
94
|
+
const key = normalizeName(query);
|
|
95
|
+
const exact = this.byNorm.get(key);
|
|
96
|
+
if (exact && exact.length > 0)
|
|
97
|
+
return exact.map((i) => this.wrapFn(i));
|
|
98
|
+
if (!this.nameOf || key === "")
|
|
99
|
+
return [];
|
|
100
|
+
return this.items
|
|
101
|
+
.filter((item) => normalizeName(this.nameOf(item) ?? "").includes(key))
|
|
102
|
+
.map((item) => this.wrapFn(item));
|
|
103
|
+
}
|
|
104
|
+
/** All records belonging to a faction id (empty if the type has no faction). */
|
|
105
|
+
byFaction(factionId) {
|
|
106
|
+
return (this.byFactionId.get(factionId) ?? []).map((i) => this.wrapFn(i));
|
|
107
|
+
}
|
|
108
|
+
[Symbol.iterator]() {
|
|
109
|
+
return this.items.map((item) => this.wrapFn(item))[Symbol.iterator]();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function push(map, key, value) {
|
|
113
|
+
const existing = map.get(key);
|
|
114
|
+
if (existing)
|
|
115
|
+
existing.push(value);
|
|
116
|
+
else
|
|
117
|
+
map.set(key, [value]);
|
|
118
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {@link Dataset} ties the embedded records together: it owns every
|
|
3
|
+
* {@link Collection}, builds the cross-entity indexes once, and is the `this`
|
|
4
|
+
* the linked views resolve against.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
import type { DeploymentPattern, Detachment, Enhancement, ForceDisposition, GameVersion, InteractionFlag, LeaderAttachment, Mission, MissionMatchup, Phase, ResourcePool, SecondaryCard, Stratagem, TimingFlag, Unit, UnitComposition, WargearOption } from "../generated.js";
|
|
9
|
+
import { Collection } from "./collection.js";
|
|
10
|
+
import { AbilityView, FactionView, UnitView, WeaponView } from "./entities.js";
|
|
11
|
+
import { type RawData } from "./types.js";
|
|
12
|
+
/** The whole dataset, with linked accessors over every entity collection. */
|
|
13
|
+
export declare class Dataset {
|
|
14
|
+
readonly units: Collection<Unit, UnitView>;
|
|
15
|
+
readonly weapons: Collection<RawData["weapons"][number], WeaponView>;
|
|
16
|
+
readonly factions: Collection<RawData["factions"][number], FactionView>;
|
|
17
|
+
readonly abilities: Collection<RawData["abilities"][number], AbilityView>;
|
|
18
|
+
readonly detachments: Collection<Detachment, Detachment>;
|
|
19
|
+
readonly enhancements: Collection<Enhancement, Enhancement>;
|
|
20
|
+
readonly stratagems: Collection<Stratagem, Stratagem>;
|
|
21
|
+
readonly wargearOptions: Collection<WargearOption, WargearOption>;
|
|
22
|
+
readonly missions: Collection<Mission, Mission>;
|
|
23
|
+
readonly missionMatchups: Collection<MissionMatchup, MissionMatchup>;
|
|
24
|
+
readonly secondaryCards: Collection<SecondaryCard, SecondaryCard>;
|
|
25
|
+
readonly deploymentPatterns: Collection<DeploymentPattern, DeploymentPattern>;
|
|
26
|
+
readonly forceDispositions: Collection<ForceDisposition, ForceDisposition>;
|
|
27
|
+
readonly resourcePools: Collection<ResourcePool, ResourcePool>;
|
|
28
|
+
readonly leaderAttachments: readonly LeaderAttachment[];
|
|
29
|
+
readonly unitCompositions: readonly UnitComposition[];
|
|
30
|
+
readonly gameVersions: readonly GameVersion[];
|
|
31
|
+
readonly timingFlags: readonly TimingFlag[];
|
|
32
|
+
readonly interactionFlags: readonly InteractionFlag[];
|
|
33
|
+
readonly phaseMappings: readonly RawData["phaseMappings"][number][];
|
|
34
|
+
/** `source_type:source_id` → unioned phases. */
|
|
35
|
+
private readonly phaseIndex;
|
|
36
|
+
/** ability id → units that list it. */
|
|
37
|
+
private readonly unitsByAbility;
|
|
38
|
+
/** weapon id → units that list it. */
|
|
39
|
+
private readonly unitsByWeapon;
|
|
40
|
+
constructor(raw?: RawData);
|
|
41
|
+
/** The dataset built from the package's embedded data. */
|
|
42
|
+
static embedded(): Dataset;
|
|
43
|
+
/** Phases a source acts in, unioned across its phase-mappings. */
|
|
44
|
+
phasesFor(sourceType: string, sourceId: string): Phase[];
|
|
45
|
+
/** Units that list the given ability id. */
|
|
46
|
+
unitsWithAbility(abilityId: string): UnitView[];
|
|
47
|
+
/** Units that list the given weapon id. */
|
|
48
|
+
unitsWithWeapon(weaponId: string): UnitView[];
|
|
49
|
+
private buildIndexes;
|
|
50
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Collection } from "./collection.js";
|
|
2
|
+
import { AbilityView, FactionView, UnitView, WeaponView } from "./entities.js";
|
|
3
|
+
import { emptyRawData } from "./types.js";
|
|
4
|
+
import { RAW_DATA } from "./bundle.generated.js";
|
|
5
|
+
/** The whole dataset, with linked accessors over every entity collection. */
|
|
6
|
+
export class Dataset {
|
|
7
|
+
// Richly-linked collections.
|
|
8
|
+
units;
|
|
9
|
+
weapons;
|
|
10
|
+
factions;
|
|
11
|
+
abilities;
|
|
12
|
+
// Id-bearing collections without bespoke views (records returned as-is).
|
|
13
|
+
detachments;
|
|
14
|
+
enhancements;
|
|
15
|
+
stratagems;
|
|
16
|
+
wargearOptions;
|
|
17
|
+
missions;
|
|
18
|
+
missionMatchups;
|
|
19
|
+
secondaryCards;
|
|
20
|
+
deploymentPatterns;
|
|
21
|
+
forceDispositions;
|
|
22
|
+
resourcePools;
|
|
23
|
+
// Id-less collections, exposed as plain arrays.
|
|
24
|
+
leaderAttachments;
|
|
25
|
+
unitCompositions;
|
|
26
|
+
gameVersions;
|
|
27
|
+
timingFlags;
|
|
28
|
+
interactionFlags;
|
|
29
|
+
phaseMappings;
|
|
30
|
+
/** `source_type:source_id` → unioned phases. */
|
|
31
|
+
phaseIndex = new Map();
|
|
32
|
+
/** ability id → units that list it. */
|
|
33
|
+
unitsByAbility = new Map();
|
|
34
|
+
/** weapon id → units that list it. */
|
|
35
|
+
unitsByWeapon = new Map();
|
|
36
|
+
constructor(raw = emptyRawData()) {
|
|
37
|
+
this.units = new Collection({
|
|
38
|
+
items: raw.units,
|
|
39
|
+
idOf: (u) => u.id,
|
|
40
|
+
// The same unit id is shared across factions (e.g. ministorum-priest);
|
|
41
|
+
// keep each faction's copy, collapse only true within-faction duplicates.
|
|
42
|
+
dedupeKeyOf: (u) => `${u.faction_id}::${u.id}`,
|
|
43
|
+
nameOf: (u) => u.name,
|
|
44
|
+
factionOf: (u) => u.faction_id,
|
|
45
|
+
wrap: (u) => new UnitView(u, this),
|
|
46
|
+
});
|
|
47
|
+
this.weapons = new Collection({
|
|
48
|
+
items: raw.weapons,
|
|
49
|
+
idOf: (w) => w.id,
|
|
50
|
+
nameOf: (w) => w.name,
|
|
51
|
+
wrap: (w) => new WeaponView(w, this),
|
|
52
|
+
});
|
|
53
|
+
this.factions = new Collection({
|
|
54
|
+
items: raw.factions,
|
|
55
|
+
idOf: (f) => f.id,
|
|
56
|
+
nameOf: (f) => f.name,
|
|
57
|
+
wrap: (f) => new FactionView(f, this),
|
|
58
|
+
});
|
|
59
|
+
this.abilities = new Collection({
|
|
60
|
+
items: raw.abilities,
|
|
61
|
+
idOf: (a) => a.ability_id,
|
|
62
|
+
nameOf: (a) => a.name,
|
|
63
|
+
factionOf: (a) => a.faction_id,
|
|
64
|
+
wrap: (a) => new AbilityView(a, this),
|
|
65
|
+
});
|
|
66
|
+
this.detachments = idCollection(raw.detachments, (d) => d.faction_id);
|
|
67
|
+
this.enhancements = idCollection(raw.enhancements);
|
|
68
|
+
this.stratagems = idCollection(raw.stratagems);
|
|
69
|
+
this.wargearOptions = idCollection(raw.wargearOptions);
|
|
70
|
+
this.missions = idCollection(raw.missions);
|
|
71
|
+
this.missionMatchups = idCollection(raw.missionMatchups);
|
|
72
|
+
this.secondaryCards = idCollection(raw.secondaryCards);
|
|
73
|
+
this.deploymentPatterns = idCollection(raw.deploymentPatterns);
|
|
74
|
+
this.forceDispositions = idCollection(raw.forceDispositions);
|
|
75
|
+
this.resourcePools = idCollection(raw.resourcePools);
|
|
76
|
+
this.leaderAttachments = raw.leaderAttachments;
|
|
77
|
+
this.unitCompositions = raw.unitCompositions;
|
|
78
|
+
this.gameVersions = raw.gameVersions;
|
|
79
|
+
this.timingFlags = raw.timingFlags;
|
|
80
|
+
this.interactionFlags = raw.interactionFlags;
|
|
81
|
+
this.phaseMappings = raw.phaseMappings;
|
|
82
|
+
this.buildIndexes(raw);
|
|
83
|
+
}
|
|
84
|
+
/** The dataset built from the package's embedded data. */
|
|
85
|
+
static embedded() {
|
|
86
|
+
return new Dataset(RAW_DATA);
|
|
87
|
+
}
|
|
88
|
+
/** Phases a source acts in, unioned across its phase-mappings. */
|
|
89
|
+
phasesFor(sourceType, sourceId) {
|
|
90
|
+
return this.phaseIndex.get(`${sourceType}:${sourceId}`) ?? [];
|
|
91
|
+
}
|
|
92
|
+
/** Units that list the given ability id. */
|
|
93
|
+
unitsWithAbility(abilityId) {
|
|
94
|
+
return (this.unitsByAbility.get(abilityId) ?? []).map((u) => new UnitView(u, this));
|
|
95
|
+
}
|
|
96
|
+
/** Units that list the given weapon id. */
|
|
97
|
+
unitsWithWeapon(weaponId) {
|
|
98
|
+
return (this.unitsByWeapon.get(weaponId) ?? []).map((u) => new UnitView(u, this));
|
|
99
|
+
}
|
|
100
|
+
buildIndexes(raw) {
|
|
101
|
+
for (const pm of raw.phaseMappings) {
|
|
102
|
+
const key = `${pm.source_type}:${pm.source_id}`;
|
|
103
|
+
const existing = this.phaseIndex.get(key) ?? [];
|
|
104
|
+
for (const phase of pm.phases) {
|
|
105
|
+
if (!existing.includes(phase))
|
|
106
|
+
existing.push(phase);
|
|
107
|
+
}
|
|
108
|
+
this.phaseIndex.set(key, existing);
|
|
109
|
+
}
|
|
110
|
+
for (const unit of raw.units) {
|
|
111
|
+
for (const abilityId of unit.ability_ids ?? [])
|
|
112
|
+
push(this.unitsByAbility, abilityId, unit);
|
|
113
|
+
for (const weaponId of unit.weapon_ids ?? [])
|
|
114
|
+
push(this.unitsByWeapon, weaponId, unit);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Build a passthrough collection for an id-bearing record type. */
|
|
119
|
+
function idCollection(items, factionOf) {
|
|
120
|
+
return new Collection({
|
|
121
|
+
items,
|
|
122
|
+
idOf: (i) => i.id,
|
|
123
|
+
nameOf: (i) => i.name,
|
|
124
|
+
factionOf,
|
|
125
|
+
wrap: (i) => i,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function push(map, key, value) {
|
|
129
|
+
const existing = map.get(key);
|
|
130
|
+
if (existing)
|
|
131
|
+
existing.push(value);
|
|
132
|
+
else
|
|
133
|
+
map.set(key, [value]);
|
|
134
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linked views over the four richly-connected entity types. Each wraps a raw
|
|
3
|
+
* generated record and resolves its relationships lazily against the owning
|
|
4
|
+
* {@link Dataset}; the full underlying record is always available via `.raw`.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
import type { AbilityDSLEntry, Faction, Phase, Unit, Weapon } from "../generated.js";
|
|
9
|
+
import type { Dataset } from "./dataset.js";
|
|
10
|
+
/** A unit, linked to its faction, weapons, and abilities. */
|
|
11
|
+
export declare class UnitView {
|
|
12
|
+
/** The full generated `Unit` record. */
|
|
13
|
+
readonly raw: Unit;
|
|
14
|
+
private readonly ds;
|
|
15
|
+
constructor(
|
|
16
|
+
/** The full generated `Unit` record. */
|
|
17
|
+
raw: Unit, ds: Dataset);
|
|
18
|
+
get id(): string;
|
|
19
|
+
get name(): string;
|
|
20
|
+
/** The unit's faction, or `undefined` if its `faction_id` is unknown. */
|
|
21
|
+
get faction(): FactionView | undefined;
|
|
22
|
+
/** Weapons referenced by `weapon_ids`; unresolved ids are skipped. */
|
|
23
|
+
get weapons(): WeaponView[];
|
|
24
|
+
/** Abilities referenced by `ability_ids`; unresolved ids are skipped. */
|
|
25
|
+
get abilities(): AbilityView[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* An ability, linked to the phases it acts in and the units that have it.
|
|
29
|
+
*
|
|
30
|
+
* Phases are not stored on the ability — they live in `phase-mappings` records.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* units.find("Kharn")!.abilities
|
|
34
|
+
* .filter(a => a.phases.includes("shooting"));
|
|
35
|
+
*/
|
|
36
|
+
export declare class AbilityView {
|
|
37
|
+
/** The full generated ability record. */
|
|
38
|
+
readonly raw: AbilityDSLEntry;
|
|
39
|
+
private readonly ds;
|
|
40
|
+
constructor(
|
|
41
|
+
/** The full generated ability record. */
|
|
42
|
+
raw: AbilityDSLEntry, ds: Dataset);
|
|
43
|
+
/** The ability's id (`ability_id` in the raw record). */
|
|
44
|
+
get id(): string;
|
|
45
|
+
get name(): string;
|
|
46
|
+
/** Game phases this ability acts in, unioned across its phase-mappings. */
|
|
47
|
+
get phases(): Phase[];
|
|
48
|
+
/** Units that list this ability in their `ability_ids`. */
|
|
49
|
+
get units(): UnitView[];
|
|
50
|
+
}
|
|
51
|
+
/** A weapon, linked to the units that carry it. */
|
|
52
|
+
export declare class WeaponView {
|
|
53
|
+
/** The full generated `Weapon` record. */
|
|
54
|
+
readonly raw: Weapon;
|
|
55
|
+
private readonly ds;
|
|
56
|
+
constructor(
|
|
57
|
+
/** The full generated `Weapon` record. */
|
|
58
|
+
raw: Weapon, ds: Dataset);
|
|
59
|
+
get id(): string;
|
|
60
|
+
get name(): string;
|
|
61
|
+
/** Units that list this weapon in their `weapon_ids`. */
|
|
62
|
+
get units(): UnitView[];
|
|
63
|
+
}
|
|
64
|
+
/** A faction, linked to its units and the records scoped to it. */
|
|
65
|
+
export declare class FactionView {
|
|
66
|
+
/** The full generated `Faction` record. */
|
|
67
|
+
readonly raw: Faction;
|
|
68
|
+
private readonly ds;
|
|
69
|
+
constructor(
|
|
70
|
+
/** The full generated `Faction` record. */
|
|
71
|
+
raw: Faction, ds: Dataset);
|
|
72
|
+
get id(): string;
|
|
73
|
+
get name(): string;
|
|
74
|
+
/** Units whose `faction_id` is this faction (may be empty for successors). */
|
|
75
|
+
get units(): UnitView[];
|
|
76
|
+
/** Faction-scoped abilities (abilities whose `faction_id` is this faction). */
|
|
77
|
+
get abilities(): AbilityView[];
|
|
78
|
+
/** Distinct weapons carried by this faction's units. */
|
|
79
|
+
get weapons(): WeaponView[];
|
|
80
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/** A unit, linked to its faction, weapons, and abilities. */
|
|
2
|
+
export class UnitView {
|
|
3
|
+
raw;
|
|
4
|
+
ds;
|
|
5
|
+
constructor(
|
|
6
|
+
/** The full generated `Unit` record. */
|
|
7
|
+
raw, ds) {
|
|
8
|
+
this.raw = raw;
|
|
9
|
+
this.ds = ds;
|
|
10
|
+
}
|
|
11
|
+
get id() {
|
|
12
|
+
return this.raw.id;
|
|
13
|
+
}
|
|
14
|
+
get name() {
|
|
15
|
+
return this.raw.name;
|
|
16
|
+
}
|
|
17
|
+
/** The unit's faction, or `undefined` if its `faction_id` is unknown. */
|
|
18
|
+
get faction() {
|
|
19
|
+
return this.ds.factions.get(this.raw.faction_id);
|
|
20
|
+
}
|
|
21
|
+
/** Weapons referenced by `weapon_ids`; unresolved ids are skipped. */
|
|
22
|
+
get weapons() {
|
|
23
|
+
return resolveAll(this.raw.weapon_ids, (id) => this.ds.weapons.get(id));
|
|
24
|
+
}
|
|
25
|
+
/** Abilities referenced by `ability_ids`; unresolved ids are skipped. */
|
|
26
|
+
get abilities() {
|
|
27
|
+
return resolveAll(this.raw.ability_ids, (id) => this.ds.abilities.get(id));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* An ability, linked to the phases it acts in and the units that have it.
|
|
32
|
+
*
|
|
33
|
+
* Phases are not stored on the ability — they live in `phase-mappings` records.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* units.find("Kharn")!.abilities
|
|
37
|
+
* .filter(a => a.phases.includes("shooting"));
|
|
38
|
+
*/
|
|
39
|
+
export class AbilityView {
|
|
40
|
+
raw;
|
|
41
|
+
ds;
|
|
42
|
+
constructor(
|
|
43
|
+
/** The full generated ability record. */
|
|
44
|
+
raw, ds) {
|
|
45
|
+
this.raw = raw;
|
|
46
|
+
this.ds = ds;
|
|
47
|
+
}
|
|
48
|
+
/** The ability's id (`ability_id` in the raw record). */
|
|
49
|
+
get id() {
|
|
50
|
+
return this.raw.ability_id;
|
|
51
|
+
}
|
|
52
|
+
get name() {
|
|
53
|
+
return this.raw.name;
|
|
54
|
+
}
|
|
55
|
+
/** Game phases this ability acts in, unioned across its phase-mappings. */
|
|
56
|
+
get phases() {
|
|
57
|
+
return this.ds.phasesFor("ability", this.raw.ability_id);
|
|
58
|
+
}
|
|
59
|
+
/** Units that list this ability in their `ability_ids`. */
|
|
60
|
+
get units() {
|
|
61
|
+
return this.ds.unitsWithAbility(this.raw.ability_id);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** A weapon, linked to the units that carry it. */
|
|
65
|
+
export class WeaponView {
|
|
66
|
+
raw;
|
|
67
|
+
ds;
|
|
68
|
+
constructor(
|
|
69
|
+
/** The full generated `Weapon` record. */
|
|
70
|
+
raw, ds) {
|
|
71
|
+
this.raw = raw;
|
|
72
|
+
this.ds = ds;
|
|
73
|
+
}
|
|
74
|
+
get id() {
|
|
75
|
+
return this.raw.id;
|
|
76
|
+
}
|
|
77
|
+
get name() {
|
|
78
|
+
return this.raw.name;
|
|
79
|
+
}
|
|
80
|
+
/** Units that list this weapon in their `weapon_ids`. */
|
|
81
|
+
get units() {
|
|
82
|
+
return this.ds.unitsWithWeapon(this.raw.id);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** A faction, linked to its units and the records scoped to it. */
|
|
86
|
+
export class FactionView {
|
|
87
|
+
raw;
|
|
88
|
+
ds;
|
|
89
|
+
constructor(
|
|
90
|
+
/** The full generated `Faction` record. */
|
|
91
|
+
raw, ds) {
|
|
92
|
+
this.raw = raw;
|
|
93
|
+
this.ds = ds;
|
|
94
|
+
}
|
|
95
|
+
get id() {
|
|
96
|
+
return this.raw.id;
|
|
97
|
+
}
|
|
98
|
+
get name() {
|
|
99
|
+
return this.raw.name;
|
|
100
|
+
}
|
|
101
|
+
/** Units whose `faction_id` is this faction (may be empty for successors). */
|
|
102
|
+
get units() {
|
|
103
|
+
return this.ds.units.byFaction(this.raw.id);
|
|
104
|
+
}
|
|
105
|
+
/** Faction-scoped abilities (abilities whose `faction_id` is this faction). */
|
|
106
|
+
get abilities() {
|
|
107
|
+
return this.ds.abilities.byFaction(this.raw.id);
|
|
108
|
+
}
|
|
109
|
+
/** Distinct weapons carried by this faction's units. */
|
|
110
|
+
get weapons() {
|
|
111
|
+
const seen = new Set();
|
|
112
|
+
const out = [];
|
|
113
|
+
for (const unit of this.units) {
|
|
114
|
+
for (const weapon of unit.weapons) {
|
|
115
|
+
if (seen.has(weapon.id))
|
|
116
|
+
continue;
|
|
117
|
+
seen.add(weapon.id);
|
|
118
|
+
out.push(weapon);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Resolve a list of ids, dropping any that don't resolve. */
|
|
125
|
+
function resolveAll(ids, get) {
|
|
126
|
+
const out = [];
|
|
127
|
+
for (const id of ids ?? []) {
|
|
128
|
+
const v = get(id);
|
|
129
|
+
if (v !== undefined)
|
|
130
|
+
out.push(v);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The linked, typed 40kdc dataset.
|
|
3
|
+
*
|
|
4
|
+
* The default {@link dataset} is built once from the data embedded in this
|
|
5
|
+
* package; the top-level collections below are its accessors, re-exported for
|
|
6
|
+
* the ergonomic one-liner form.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import { units } from "@alpaca-software/40kdc-data";
|
|
12
|
+
*
|
|
13
|
+
* units.find("Kharn")!.abilities
|
|
14
|
+
* .filter(a => a.phases.includes("shooting"))
|
|
15
|
+
* .map(a => a.id); // ["berzerker-frenzy"]
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* import { factions } from "@alpaca-software/40kdc-data";
|
|
19
|
+
*
|
|
20
|
+
* factions.find("World Eaters")!.units.length;
|
|
21
|
+
*/
|
|
22
|
+
export { Dataset } from "./dataset.js";
|
|
23
|
+
export { Collection } from "./collection.js";
|
|
24
|
+
export type { CollectionConfig } from "./collection.js";
|
|
25
|
+
export { UnitView, AbilityView, WeaponView, FactionView } from "./entities.js";
|
|
26
|
+
export { normalizeName } from "./normalize.js";
|
|
27
|
+
export { emptyRawData } from "./types.js";
|
|
28
|
+
export type { RawData } from "./types.js";
|
|
29
|
+
import { Dataset } from "./dataset.js";
|
|
30
|
+
/** The dataset built from this package's embedded data. */
|
|
31
|
+
export declare const dataset: Dataset;
|
|
32
|
+
/** All units, linked to their faction, weapons, and abilities. */
|
|
33
|
+
export declare const units: import("./collection.js").Collection<import("../generated.js").Unit, import("./entities.js").UnitView>;
|
|
34
|
+
/** All weapons, linked to the units that carry them. */
|
|
35
|
+
export declare const weapons: import("./collection.js").Collection<import("../generated.js").Weapon, import("./entities.js").WeaponView>;
|
|
36
|
+
/** All factions, linked to their units, abilities, and weapons. */
|
|
37
|
+
export declare const factions: import("./collection.js").Collection<import("../generated.js").Faction, import("./entities.js").FactionView>;
|
|
38
|
+
/** All abilities, linked to their phases and the units that have them. */
|
|
39
|
+
export declare const abilities: import("./collection.js").Collection<import("../generated.js").AbilityDSLEntry, import("./entities.js").AbilityView>;
|
|
40
|
+
/** All detachments. */
|
|
41
|
+
export declare const detachments: import("./collection.js").Collection<import("../generated.js").Detachment, import("../generated.js").Detachment>;
|
|
42
|
+
/** All enhancements. */
|
|
43
|
+
export declare const enhancements: import("./collection.js").Collection<import("../generated.js").Enhancement, import("../generated.js").Enhancement>;
|
|
44
|
+
/** All stratagems. */
|
|
45
|
+
export declare const stratagems: import("./collection.js").Collection<import("../generated.js").Stratagem, import("../generated.js").Stratagem>;
|
|
46
|
+
/** All wargear options. */
|
|
47
|
+
export declare const wargearOptions: import("./collection.js").Collection<import("../generated.js").WargearOption, import("../generated.js").WargearOption>;
|
|
48
|
+
/** All missions. */
|
|
49
|
+
export declare const missions: import("./collection.js").Collection<import("../generated.js").Mission, import("../generated.js").Mission>;
|
|
50
|
+
/** All mission matchups. */
|
|
51
|
+
export declare const missionMatchups: import("./collection.js").Collection<import("../generated.js").MissionMatchup, import("../generated.js").MissionMatchup>;
|
|
52
|
+
/** All secondary mission cards. */
|
|
53
|
+
export declare const secondaryCards: import("./collection.js").Collection<import("../generated.js").SecondaryCard, import("../generated.js").SecondaryCard>;
|
|
54
|
+
/** All deployment patterns. */
|
|
55
|
+
export declare const deploymentPatterns: import("./collection.js").Collection<import("../generated.js").DeploymentPattern, import("../generated.js").DeploymentPattern>;
|
|
56
|
+
/** All force dispositions. */
|
|
57
|
+
export declare const forceDispositions: import("./collection.js").Collection<import("../generated.js").ForceDisposition, import("../generated.js").ForceDisposition>;
|
|
58
|
+
/** All resource pools. */
|
|
59
|
+
export declare const resourcePools: import("./collection.js").Collection<import("../generated.js").ResourcePool, import("../generated.js").ResourcePool>;
|