@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,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handles multi-view data for shared units.
|
|
3
|
+
*
|
|
4
|
+
* Shared units (e.g., Chaos Land Raider) appear in multiple factions with the
|
|
5
|
+
* same UUID. Their abilities, models, and weapons tables contain repeated entry
|
|
6
|
+
* groups — one per faction "view". Views are delimited by the line number
|
|
7
|
+
* resetting to 1.
|
|
8
|
+
*
|
|
9
|
+
* For World Eaters, the correct view is identified by having
|
|
10
|
+
* "Blessings of Khorne" as a Faction-type ability.
|
|
11
|
+
*/
|
|
12
|
+
/** Split an array of line-numbered entries into view groups by line-number resets. */
|
|
13
|
+
export function splitIntoViews(entries) {
|
|
14
|
+
const views = [];
|
|
15
|
+
let current = [];
|
|
16
|
+
let lastLine = Infinity;
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const line = parseInt(entry.line, 10);
|
|
19
|
+
if (line <= lastLine && current.length > 0) {
|
|
20
|
+
views.push({ index: views.length, entries: current });
|
|
21
|
+
current = [];
|
|
22
|
+
}
|
|
23
|
+
current.push(entry);
|
|
24
|
+
lastLine = line;
|
|
25
|
+
}
|
|
26
|
+
if (current.length > 0) {
|
|
27
|
+
views.push({ index: views.length, entries: current });
|
|
28
|
+
}
|
|
29
|
+
return views;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Find the view index for a faction's shared unit.
|
|
33
|
+
* Identifies the correct view by matching the faction's primary ability name
|
|
34
|
+
* among the Faction-type abilities in each view.
|
|
35
|
+
* Returns 0 for faction-exclusive units (single view).
|
|
36
|
+
*/
|
|
37
|
+
export function findFactionViewIndex(abilities, factionAbilityName) {
|
|
38
|
+
if (abilities.length === 0)
|
|
39
|
+
return 0;
|
|
40
|
+
const views = splitIntoViews(abilities);
|
|
41
|
+
if (views.length === 1)
|
|
42
|
+
return 0;
|
|
43
|
+
for (const view of views) {
|
|
44
|
+
const hasAbility = view.entries.some((a) => a.type === "Faction" &&
|
|
45
|
+
a.name === factionAbilityName);
|
|
46
|
+
if (hasAbility)
|
|
47
|
+
return view.index;
|
|
48
|
+
}
|
|
49
|
+
// Some shared units (e.g., SM vehicles used by Grey Knights, or units shared
|
|
50
|
+
// with Agents of the Imperium) have views with no Faction-type ability.
|
|
51
|
+
// Use the first such view when no direct match is found.
|
|
52
|
+
const emptyFactionViews = views.filter((v) => !v.entries.some((a) => a.type === "Faction"));
|
|
53
|
+
if (emptyFactionViews.length > 0) {
|
|
54
|
+
return emptyFactionViews[0].index;
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`No "${factionAbilityName}" faction ability found in ${views.length} views ` +
|
|
57
|
+
`for datasheet ${abilities[0]?.datasheet_id}`);
|
|
58
|
+
}
|
|
59
|
+
/** Extract entries for a specific view index from a line-numbered array. */
|
|
60
|
+
export function getViewEntries(entries, viewIndex) {
|
|
61
|
+
if (entries.length === 0)
|
|
62
|
+
return [];
|
|
63
|
+
const views = splitIntoViews(entries);
|
|
64
|
+
if (viewIndex >= views.length) {
|
|
65
|
+
throw new Error(`View index ${viewIndex} out of range (${views.length} views available)`);
|
|
66
|
+
}
|
|
67
|
+
return views[viewIndex].entries;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Split points entries (no line field) into views for a shared unit.
|
|
71
|
+
*
|
|
72
|
+
* Points entries are ordered by view. For simple cases (1 entry per view),
|
|
73
|
+
* indexing by viewIndex works. For multi-squad-size units, views are
|
|
74
|
+
* delimited by the model count resetting (decreasing from prev entry).
|
|
75
|
+
*/
|
|
76
|
+
export function getPointsForView(entries, viewIndex, numViews) {
|
|
77
|
+
if (numViews <= 1)
|
|
78
|
+
return entries;
|
|
79
|
+
// Try splitting by model count resets (decrease signals new view)
|
|
80
|
+
const views = [[]];
|
|
81
|
+
for (let i = 0; i < entries.length; i++) {
|
|
82
|
+
const cur = parseInt(entries[i].models, 10);
|
|
83
|
+
const prev = i > 0 ? parseInt(entries[i - 1].models, 10) : 0;
|
|
84
|
+
if (i > 0 && cur <= prev) {
|
|
85
|
+
views.push([]);
|
|
86
|
+
}
|
|
87
|
+
views[views.length - 1].push(entries[i]);
|
|
88
|
+
}
|
|
89
|
+
if (viewIndex < views.length) {
|
|
90
|
+
return views[viewIndex];
|
|
91
|
+
}
|
|
92
|
+
// Fallback: evenly divide
|
|
93
|
+
const perView = Math.ceil(entries.length / numViews);
|
|
94
|
+
const start = viewIndex * perView;
|
|
95
|
+
return entries.slice(start, start + perView);
|
|
96
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weapon profile merging and cross-unit deduplication.
|
|
3
|
+
*
|
|
4
|
+
* Source data splits multi-profile weapons into separate rows using an
|
|
5
|
+
* en-dash delimiter: "Plasma pistol – Standard" / "Plasma pistol – Supercharge".
|
|
6
|
+
* These become a single weapon entity with a profiles array.
|
|
7
|
+
*
|
|
8
|
+
* Cross-unit dedup: weapons with identical (name, type, all profile stats)
|
|
9
|
+
* across different units are the same entity, referenced by a shared ID.
|
|
10
|
+
*/
|
|
11
|
+
export interface SourceWargear {
|
|
12
|
+
datasheet_id: string;
|
|
13
|
+
line: string;
|
|
14
|
+
line_in_wargear: string;
|
|
15
|
+
dice: string;
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
range: string;
|
|
19
|
+
type: string;
|
|
20
|
+
A: string;
|
|
21
|
+
BS_WS: string;
|
|
22
|
+
S: string;
|
|
23
|
+
AP: string;
|
|
24
|
+
D: string;
|
|
25
|
+
}
|
|
26
|
+
export interface WeaponProfile {
|
|
27
|
+
name: string;
|
|
28
|
+
range: number | "Melee";
|
|
29
|
+
stats: {
|
|
30
|
+
A: number | string;
|
|
31
|
+
BS?: number | null;
|
|
32
|
+
WS?: number | null;
|
|
33
|
+
S: number | string;
|
|
34
|
+
AP: number;
|
|
35
|
+
D: number | string;
|
|
36
|
+
};
|
|
37
|
+
keywords: string[];
|
|
38
|
+
}
|
|
39
|
+
export interface WeaponEntity {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
type: "ranged" | "melee";
|
|
43
|
+
profiles: WeaponProfile[];
|
|
44
|
+
game_version: {
|
|
45
|
+
edition: string;
|
|
46
|
+
dataslate: string;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Merge wargear rows for a single unit into weapon entities,
|
|
51
|
+
* then deduplicate across all units.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildWeaponRegistry(allUnitWargear: Map<string, SourceWargear[]>, gameVersion: {
|
|
54
|
+
edition: string;
|
|
55
|
+
dataslate: string;
|
|
56
|
+
}): {
|
|
57
|
+
weapons: WeaponEntity[];
|
|
58
|
+
/** Maps (datasheetId) → Set of weapon entity IDs for that unit */
|
|
59
|
+
unitWeaponIds: Map<string, Set<string>>;
|
|
60
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weapon profile merging and cross-unit deduplication.
|
|
3
|
+
*
|
|
4
|
+
* Source data splits multi-profile weapons into separate rows using an
|
|
5
|
+
* en-dash delimiter: "Plasma pistol – Standard" / "Plasma pistol – Supercharge".
|
|
6
|
+
* These become a single weapon entity with a profiles array.
|
|
7
|
+
*
|
|
8
|
+
* Cross-unit dedup: weapons with identical (name, type, all profile stats)
|
|
9
|
+
* across different units are the same entity, referenced by a shared ID.
|
|
10
|
+
*/
|
|
11
|
+
import { nameToId } from "./id-generator.js";
|
|
12
|
+
import { parseRange, parseBSWS, parseStatValue, parseAP, parseWeaponKeywords, } from "./stat-parser.js";
|
|
13
|
+
// EN-DASH (U+2013) and regular hyphen used as profile separators
|
|
14
|
+
const PROFILE_SEPARATOR = /\s*[\u2013\u2014-]\s+/;
|
|
15
|
+
/** Split "Plasma pistol – Standard" into { baseName: "Plasma pistol", profileName: "Standard" } */
|
|
16
|
+
function splitProfileName(fullName) {
|
|
17
|
+
const parts = fullName.split(PROFILE_SEPARATOR);
|
|
18
|
+
if (parts.length >= 2) {
|
|
19
|
+
return {
|
|
20
|
+
baseName: parts[0].trim(),
|
|
21
|
+
profileName: parts.slice(1).join(" – ").trim(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return { baseName: fullName.trim(), profileName: fullName.trim() };
|
|
25
|
+
}
|
|
26
|
+
/** Build a single weapon profile from a source wargear row. */
|
|
27
|
+
function buildProfile(row, profileName) {
|
|
28
|
+
const range = parseRange(row.range);
|
|
29
|
+
const isMelee = row.type === "Melee" || range === "Melee";
|
|
30
|
+
const stats = {
|
|
31
|
+
A: parseStatValue(row.A),
|
|
32
|
+
S: parseStatValue(row.S),
|
|
33
|
+
AP: parseAP(row.AP),
|
|
34
|
+
D: parseStatValue(row.D),
|
|
35
|
+
};
|
|
36
|
+
if (isMelee) {
|
|
37
|
+
stats.WS = parseBSWS(row.BS_WS);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
stats.BS = parseBSWS(row.BS_WS);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
name: profileName,
|
|
44
|
+
range,
|
|
45
|
+
stats,
|
|
46
|
+
keywords: parseWeaponKeywords(row.description),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Hash a weapon entity for cross-unit deduplication. */
|
|
50
|
+
function weaponHash(name, type, profiles) {
|
|
51
|
+
const profileParts = profiles
|
|
52
|
+
.map((p) => `${p.name}|${p.range}|${JSON.stringify(p.stats)}|${p.keywords.sort().join(",")}`)
|
|
53
|
+
.sort();
|
|
54
|
+
return `${name.toLowerCase()}|${type}|${profileParts.join(";")}`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Merge wargear rows for a single unit into weapon entities,
|
|
58
|
+
* then deduplicate across all units.
|
|
59
|
+
*/
|
|
60
|
+
export function buildWeaponRegistry(allUnitWargear, gameVersion) {
|
|
61
|
+
// Global dedup registry: hash → WeaponEntity
|
|
62
|
+
const registry = new Map();
|
|
63
|
+
const unitWeaponIds = new Map();
|
|
64
|
+
for (const [datasheetId, rows] of allUnitWargear) {
|
|
65
|
+
const weaponIds = new Set();
|
|
66
|
+
unitWeaponIds.set(datasheetId, weaponIds);
|
|
67
|
+
// Group rows by base weapon name for profile merging
|
|
68
|
+
const groups = new Map();
|
|
69
|
+
for (const row of rows) {
|
|
70
|
+
const { baseName, profileName } = splitProfileName(row.name);
|
|
71
|
+
const key = baseName.toLowerCase();
|
|
72
|
+
if (!groups.has(key)) {
|
|
73
|
+
groups.set(key, { baseName, rows: [], profileNames: [] });
|
|
74
|
+
}
|
|
75
|
+
const group = groups.get(key);
|
|
76
|
+
group.rows.push(row);
|
|
77
|
+
group.profileNames.push(profileName);
|
|
78
|
+
}
|
|
79
|
+
for (const [, group] of groups) {
|
|
80
|
+
const { baseName, rows: groupRows, profileNames } = group;
|
|
81
|
+
const type = groupRows[0].type === "Melee" ? "melee" : "ranged";
|
|
82
|
+
// Skip rows with obviously corrupt source data (shifted columns)
|
|
83
|
+
const validRows = groupRows.filter((row, i) => {
|
|
84
|
+
const s = parseStatValue(row.S);
|
|
85
|
+
if (typeof s === "number" && s < 0)
|
|
86
|
+
return false;
|
|
87
|
+
const d = parseStatValue(row.D);
|
|
88
|
+
if (type === "ranged" && d === 0 && row.D === "")
|
|
89
|
+
return false;
|
|
90
|
+
return true;
|
|
91
|
+
});
|
|
92
|
+
if (validRows.length === 0)
|
|
93
|
+
continue;
|
|
94
|
+
const validProfileNames = groupRows.map((row, i) => ({ row, name: profileNames[i] }))
|
|
95
|
+
.filter(({ row }) => validRows.includes(row))
|
|
96
|
+
.map(({ name }) => name);
|
|
97
|
+
// If there's only one profile and its name matches the base name, use base name as profile name
|
|
98
|
+
const profiles = validRows.map((row, i) => {
|
|
99
|
+
const pName = validRows.length === 1 ? baseName : validProfileNames[i];
|
|
100
|
+
return buildProfile(row, pName);
|
|
101
|
+
});
|
|
102
|
+
const hash = weaponHash(baseName, type, profiles);
|
|
103
|
+
if (!registry.has(hash)) {
|
|
104
|
+
const id = nameToId(baseName);
|
|
105
|
+
registry.set(hash, {
|
|
106
|
+
id,
|
|
107
|
+
name: baseName,
|
|
108
|
+
type: type,
|
|
109
|
+
profiles,
|
|
110
|
+
game_version: gameVersion,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
weaponIds.add(registry.get(hash).id);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
weapons: [...registry.values()],
|
|
118
|
+
unitWeaponIds,
|
|
119
|
+
};
|
|
120
|
+
}
|