@lycoristech/azurapi 0.1.1 → 0.1.3

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/data/version.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "generatedAt": "2026-10-05T03:53:22.456Z",
3
- "shipCount": 752,
2
+ "generatedAt": "2026-10-11T04:44:04.078Z",
3
+ "shipCount": 889,
4
4
  "schemaVersion": "0.0.1"
5
5
  }
@@ -6,12 +6,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.AzurData = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
+ const node_url_1 = require("node:url");
9
10
  const ships_js_1 = require("./ships.js");
10
11
  const fetcher_js_1 = require("./fetcher.js");
12
+ /**
13
+ * Path of this source file as seen in a stack trace: a file:// URL under ESM,
14
+ * a plain path under CJS. This file is compiled to both formats from one
15
+ * source, so `import.meta.url` cannot be written here (it is a syntax error
16
+ * in the CJS build), and `__dirname` only exists in CJS.
17
+ */
18
+ function thisFileFromStack() {
19
+ const stack = new Error().stack ?? "";
20
+ for (const line of stack.split("\n").slice(1)) {
21
+ const m = line.match(/\(?((?:file:\/\/)?\/[^):]+?\.[cm]?[jt]s):\d+:\d+\)?$/);
22
+ if (m?.[1] !== undefined)
23
+ return m[1].startsWith("file://") ? (0, node_url_1.fileURLToPath)(m[1]) : m[1];
24
+ }
25
+ return undefined;
26
+ }
11
27
  function resolveThisDir() {
12
- // CJS: __dirname is injected by Node
13
- // ESM under tsx/ts-node: __dirname is polyfilled
14
- // Pure ESM: falls back to cwd
15
28
  try {
16
29
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
17
30
  // @ts-ignore __dirname exists in CJS but not in ESM type defs
@@ -19,7 +32,10 @@ function resolveThisDir() {
19
32
  return __dirname; // eslint-disable-line no-undef
20
33
  }
21
34
  catch { /* not CJS */ }
22
- return process.cwd();
35
+ const file = thisFileFromStack();
36
+ if (file !== undefined)
37
+ return node_path_1.default.dirname(file);
38
+ return process.cwd(); // last resort; resolveDefaultDataPath walks up from here
23
39
  }
24
40
  class AzurData {
25
41
  ships;
@@ -4,7 +4,44 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Ships = void 0;
7
+ exports.isCloseName = isCloseName;
7
8
  const fuse_js_1 = __importDefault(require("fuse.js"));
9
+ /** Optimal-string-alignment distance (Levenshtein + adjacent transposition). */
10
+ function editDistance(a, b) {
11
+ const rows = a.length + 1, cols = b.length + 1;
12
+ const d = Array.from({ length: rows }, (_, i) => Array.from({ length: cols }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
13
+ for (let i = 1; i < rows; i++) {
14
+ for (let j = 1; j < cols; j++) {
15
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
16
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
17
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
18
+ d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
19
+ }
20
+ }
21
+ }
22
+ return d[rows - 1][cols - 1];
23
+ }
24
+ /**
25
+ * True when `query` is a prefix of one of the ship's names (3+ chars) or within
26
+ * typo distance of one: 1 edit for 4-5 chars, 2 edits for 6+. Shorter queries
27
+ * never fuzzy-match; they must hit an id or a name exactly.
28
+ */
29
+ function isCloseName(query, ship) {
30
+ const q = query.trim().toLowerCase();
31
+ if (q.length < 3)
32
+ return false;
33
+ const maxEdits = q.length >= 6 ? 2 : q.length >= 4 ? 1 : 0;
34
+ for (const raw of Object.values(ship.names)) {
35
+ if (typeof raw !== "string")
36
+ continue;
37
+ const n = raw.toLowerCase();
38
+ if (n.startsWith(q))
39
+ return true;
40
+ if (maxEdits > 0 && Math.abs(n.length - q.length) <= maxEdits && editDistance(q, n) <= maxEdits)
41
+ return true;
42
+ }
43
+ return false;
44
+ }
8
45
  class Ships {
9
46
  data;
10
47
  idMap;
@@ -45,10 +82,13 @@ class Ships {
45
82
  const ciId = ciIdMap[lower];
46
83
  if (ciId && this.data[ciId])
47
84
  return this.data[ciId];
48
- // 4. Fuse.js fuzzy
85
+ // 4. Fuse.js fuzzy, but only for a typo-level edit distance or a name prefix.
86
+ // Fuse scores substrings as perfect matches ("22" -> U-522 at 0.000, "Kuon"
87
+ // -> Kronshtadt at 0.003), which a lookup must not return.
49
88
  const results = this.fuse.search(nameOrId, { limit: 1 });
50
- if (results.length > 0 && results[0].score !== undefined && results[0].score < 0.5) {
51
- return results[0].item;
89
+ const hit = results[0];
90
+ if (hit !== undefined && hit.score !== undefined && hit.score < 0.5 && isCloseName(nameOrId, hit.item)) {
91
+ return hit.item;
52
92
  }
53
93
  return undefined;
54
94
  }
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveThumbnail = resolveThumbnail;
3
4
  exports.normalizeIdentity = normalizeIdentity;
4
5
  const config_js_1 = require("../config.js");
5
6
  const enums_js_1 = require("../translate/enums.js");
@@ -11,11 +12,33 @@ const strings_js_1 = require("../translate/strings.js");
11
12
  function isLikelyChinese(s) {
12
13
  return /[\u4e00-\u9fff]/.test(s);
13
14
  }
15
+ /**
16
+ * Pick the thumbnail filename stem for a ship.
17
+ *
18
+ * Shipyard icons in azurlane-images are named after the texture inside the
19
+ * bundle: historically the numeric group id (20212.png), but newer bundles use
20
+ * the skin's painting name (weikesibao.png, z14.png, 2B.png). Prefer the id so
21
+ * existing URLs stay stable, fall back to the default skin's painting name, and
22
+ * keep the id when nothing is known (no images clone at build time).
23
+ */
24
+ function resolveThumbnail(groupType, defaultPainting, available) {
25
+ if (available === undefined)
26
+ return groupType;
27
+ const byId = available.get(groupType.toLowerCase());
28
+ if (byId !== undefined)
29
+ return byId;
30
+ if (defaultPainting !== undefined) {
31
+ const byPainting = available.get(defaultPainting.toLowerCase());
32
+ if (byPainting !== undefined)
33
+ return byPainting;
34
+ }
35
+ return groupType;
36
+ }
14
37
  // ---------------------------------------------------------------------------
15
38
  // Main
16
39
  // ---------------------------------------------------------------------------
17
40
  function normalizeIdentity(inputs) {
18
- const { groupType, rowIds, en, cn, jp, isEnReleased, isResearch, groupRow, lookups, } = inputs;
41
+ const { groupType, rowIds, en, cn, jp, isEnReleased, isResearch, groupRow, lookups, availableThumbnails, } = inputs;
19
42
  // ------------------------------------------------------------------
20
43
  // Resolve the LB3 row ID — the row whose key ends with "4", or last entry.
21
44
  // ------------------------------------------------------------------
@@ -35,11 +58,12 @@ function normalizeIdentity(inputs) {
35
58
  let nameEn;
36
59
  const rawName = enStats.name;
37
60
  const rawEnglishName = enStats.english_name;
61
+ // Names are trimmed: upstream has trailing spaces on ~40 rows ("Foch ", "Le Malin μ ", ...)
38
62
  if (!isLikelyChinese(rawName) && rawName.trim().length > 0) {
39
- nameEn = rawName;
63
+ nameEn = rawName.trim();
40
64
  }
41
65
  else if (rawEnglishName !== undefined && rawEnglishName.trim().length > 0 && !isLikelyChinese(rawEnglishName)) {
42
- nameEn = rawEnglishName;
66
+ nameEn = rawEnglishName.trim();
43
67
  }
44
68
  // else: both are Chinese/empty → nameEn stays undefined
45
69
  // CN name: from cn.statistics matching row if it looks Chinese; else fall back
@@ -48,22 +72,22 @@ function normalizeIdentity(inputs) {
48
72
  if (cn !== undefined) {
49
73
  const cnStats = cn.statistics[lb3RowId];
50
74
  if (cnStats !== undefined && cnStats.name.trim().length > 0 && isLikelyChinese(cnStats.name)) {
51
- nameCn = cnStats.name;
75
+ nameCn = cnStats.name.trim();
52
76
  }
53
77
  }
54
78
  if (nameCn === undefined && isLikelyChinese(rawName)) {
55
- nameCn = rawName;
79
+ nameCn = rawName.trim();
56
80
  }
57
81
  // JP name: from jp.statistics matching row if provided
58
82
  let nameJp;
59
83
  if (jp !== undefined) {
60
84
  const jpStats = jp.statistics[lb3RowId];
61
85
  if (jpStats !== undefined && jpStats.name.trim().length > 0) {
62
- nameJp = jpStats.name;
86
+ nameJp = jpStats.name.trim();
63
87
  }
64
88
  }
65
89
  // Code: english_name from EN statistics
66
- const nameCode = rawEnglishName !== undefined && rawEnglishName.trim().length > 0 ? rawEnglishName : undefined;
90
+ const nameCode = rawEnglishName !== undefined && rawEnglishName.trim().length > 0 ? rawEnglishName.trim() : undefined;
67
91
  const names = {};
68
92
  if (nameEn !== undefined)
69
93
  names.en = nameEn;
@@ -98,7 +122,10 @@ function normalizeIdentity(inputs) {
98
122
  // ------------------------------------------------------------------
99
123
  // Nationality
100
124
  // ------------------------------------------------------------------
101
- const nationality = (0, enums_js_1.nationalityName)(groupRow.nationality);
125
+ // The per-ship statistics row is authoritative; ship_data_group is the fallback.
126
+ // They disagree for exactly one ship in EN 9.3.384: Musashi-chan (30517), where the
127
+ // group row says Iron Blood and the ship row says Sakura Empire.
128
+ const nationality = (0, enums_js_1.nationalityName)(enStats.nationality ?? groupRow.nationality);
102
129
  // ------------------------------------------------------------------
103
130
  // Wiki URL
104
131
  // ------------------------------------------------------------------
@@ -106,7 +133,8 @@ function normalizeIdentity(inputs) {
106
133
  // ------------------------------------------------------------------
107
134
  // Thumbnail (shipyard icon from azurlane-images repo)
108
135
  // ------------------------------------------------------------------
109
- const thumbnail = `${config_js_1.THUMBNAILS_URL}/${groupType}.png`;
136
+ const defaultPainting = en.skinTemplate?.[`${groupType}0`]?.painting;
137
+ const thumbnail = `${config_js_1.THUMBNAILS_URL}/${resolveThumbnail(groupType, defaultPainting, availableThumbnails)}.png`;
110
138
  // ------------------------------------------------------------------
111
139
  // Build output
112
140
  // ------------------------------------------------------------------
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.isLimitBreakRow = isLimitBreakRow;
36
37
  exports.buildShip = buildShip;
37
38
  exports.buildAllShips = buildAllShips;
38
39
  exports.createBuildContext = createBuildContext;
@@ -45,6 +46,26 @@ const construction_js_1 = require("./construction.js");
45
46
  const retrofit_js_1 = require("./retrofit.js");
46
47
  const misc_js_1 = require("./misc.js");
47
48
  const skins_js_1 = require("./skins.js");
49
+ // ---------------------------------------------------------------------------
50
+ // Limit-break row detection
51
+ // ---------------------------------------------------------------------------
52
+ /**
53
+ * True when `key` is one of a group's real limit-break rows: the group_type
54
+ * followed by a single LB digit (1-4), e.g. 20212 -> 202121..202124.
55
+ *
56
+ * Anything else is an alt/promo form of a ship that also has real LB rows
57
+ * (the 900xxx block: 900004 -> Suffolk, 900465 -> Adventure Galley, ...).
58
+ *
59
+ * Do NOT filter on a "9" prefix instead: Vichya Dominion (9xxxx), Tempesta
60
+ * (960xxx) and META (97xxxx) ships all have their real LB rows at 9-prefixed
61
+ * ids. That heuristic silently dropped all 99 of them (issue #2).
62
+ */
63
+ function isLimitBreakRow(key, groupType) {
64
+ if (key.length !== groupType.length + 1 || !key.startsWith(groupType))
65
+ return false;
66
+ const lb = key[key.length - 1];
67
+ return lb === "1" || lb === "2" || lb === "3" || lb === "4";
68
+ }
48
69
  function buildIndexes(ctx) {
49
70
  // Build groupByGroupType: scan ship_data_group and index by group_type
50
71
  const groupByGroupType = new Map();
@@ -75,8 +96,8 @@ function buildShip(groupType, ctx, indexes) {
75
96
  continue;
76
97
  if (String(row.group_type) !== groupType)
77
98
  continue;
78
- // Skip 9-prefix rows (promo/alt forms)
79
- if (key.startsWith("9"))
99
+ // Keep only the real LB rows; skips the 900xxx alt/promo forms
100
+ if (!isLimitBreakRow(key, groupType))
80
101
  continue;
81
102
  rowIds.push(key);
82
103
  }
@@ -146,7 +167,9 @@ function buildShip(groupType, ctx, indexes) {
146
167
  fleetTechShipTemplate: ctx.en.fleetTechShipTemplate,
147
168
  fleetTechShipClass: ctx.en.fleetTechShipClass,
148
169
  shipDataByType: ctx.en.shipDataByType,
170
+ skinTemplate: ctx.en.skinTemplate,
149
171
  },
172
+ availableThumbnails: ctx.availableThumbnails,
150
173
  cn: ctx.cn !== undefined ? { statistics: ctx.cn.statistics } : undefined,
151
174
  jp: ctx.jp !== undefined ? { statistics: ctx.jp.statistics } : undefined,
152
175
  isEnReleased,
@@ -289,14 +312,16 @@ function buildShip(groupType, ctx, indexes) {
289
312
  function buildAllShips(ctx) {
290
313
  // Pre-build shared indexes once for O(n) total cost
291
314
  const indexes = buildIndexes(ctx);
292
- // Collect all unique group_types from EN template, excluding 9-prefix rows
315
+ // Collect all unique group_types that have real LB rows in the EN template
316
+ // (alt/promo 900xxx rows are ignored; see isLimitBreakRow)
293
317
  const groupTypeSet = new Set();
294
318
  for (const [key, row] of Object.entries(ctx.en.template)) {
295
319
  if (row === undefined)
296
320
  continue;
297
- if (key.startsWith("9"))
321
+ const groupType = String(row.group_type);
322
+ if (!isLimitBreakRow(key, groupType))
298
323
  continue;
299
- groupTypeSet.add(String(row.group_type));
324
+ groupTypeSet.add(groupType);
300
325
  }
301
326
  const ships = [];
302
327
  let failCount = 0;
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildIdMap = buildIdMap;
6
7
  exports.writeIdMap = writeIdMap;
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const path_1 = __importDefault(require("path"));
@@ -10,27 +11,33 @@ const path_1 = __importDefault(require("path"));
10
11
  // writeIdMap
11
12
  // ---------------------------------------------------------------------------
12
13
  /**
13
- * Write a name/alias → canonical id map to disk.
14
+ * Build the name/alias → canonical id map.
14
15
  *
15
16
  * Shape: Record<string, string>
16
17
  *
17
18
  * For each ship: emits names.en → id, names.cn → id, names.jp → id, names.code → id
18
19
  * For each groupRow: emits padded code → group_type string (legacy AzurAPI lookup)
19
20
  *
20
- * Last-writer-wins for duplicate names.
21
+ * Collisions: first-writer-wins, with ships visited in ascending numeric id order.
22
+ * Core factions have 5-digit ids, META 6-digit (97xxxx) and collabs 7-digit, so a
23
+ * shared display name resolves to the core-faction ship: "Enterprise" → USS
24
+ * Enterprise (10706) not HMS Enterprise (20232), "Neptune" → HMS Neptune (29901)
25
+ * not the Neptunia collab (1010001), likewise Fubuki and Kasumi. The other ship
26
+ * stays reachable through its names.code ("HMS Enterprise", "HDN Neptune", ...)
27
+ * or its id. Code names never shadow another ship's display name.
21
28
  */
22
- function writeIdMap(ships, groupRows, outputPath = "data/id-map.json") {
29
+ function buildIdMap(ships, groupRows) {
23
30
  const map = {};
31
+ const ordered = [...ships].sort((a, b) => Number(a.id) - Number(b.id));
24
32
  // Collect all EN names first so we don't let names.code overwrite a real ship name
25
33
  const enNames = new Set();
26
- for (const ship of ships) {
34
+ for (const ship of ordered) {
27
35
  if (ship.names.en !== undefined)
28
36
  enNames.add(ship.names.en);
29
37
  }
30
- // Per-ship name entries (first-writer-wins for code names to avoid collisions)
31
- for (const ship of ships) {
38
+ for (const ship of ordered) {
32
39
  const { id, names } = ship;
33
- if (names.en !== undefined)
40
+ if (names.en !== undefined && !(names.en in map))
34
41
  map[names.en] = id;
35
42
  if (names.cn !== undefined && !(names.cn in map))
36
43
  map[names.cn] = id;
@@ -45,7 +52,11 @@ function writeIdMap(ships, groupRows, outputPath = "data/id-map.json") {
45
52
  const paddedCode = String(row.code).padStart(3, "0");
46
53
  map[paddedCode] = String(row.group_type);
47
54
  }
48
- const json = JSON.stringify(map, null, 2);
55
+ return map;
56
+ }
57
+ /** Write the id map (see buildIdMap) to disk. */
58
+ function writeIdMap(ships, groupRows, outputPath = "data/id-map.json") {
59
+ const json = JSON.stringify(buildIdMap(ships, groupRows), null, 2);
49
60
  const dir = path_1.default.dirname(path_1.default.resolve(outputPath));
50
61
  fs_1.default.mkdirSync(dir, { recursive: true });
51
62
  fs_1.default.writeFileSync(outputPath, json, "utf-8");
@@ -36,7 +36,14 @@ function rarityName(rarity, isResearch) {
36
36
  // ---------------------------------------------------------------------------
37
37
  // Nationality
38
38
  // ---------------------------------------------------------------------------
39
- /** Nationality int → display string. Matches Azur Lane wiki convention. */
39
+ /**
40
+ * Nationality int → display string. Matches Azur Lane wiki convention.
41
+ *
42
+ * Codes verified against upstream ship_data_group.json + ship_data_statistics.json
43
+ * (AzurLaneTools/AzurLaneData 4cca5c24, 2026-05-08) and the game's own faction list in
44
+ * sharecfgdata/gametip.lua (EN 9.3.384); see tests/translate/enums.test.ts.
45
+ * Each collab has its own code from 101 upward, in release order.
46
+ */
40
47
  exports.NATIONALITY_MAP = {
41
48
  0: "Universal",
42
49
  1: "Eagle Union",
@@ -50,21 +57,26 @@ exports.NATIONALITY_MAP = {
50
57
  9: "Vichya Dominion",
51
58
  10: "Iris Orthodoxy",
52
59
  11: "Kingdom of Tulipa",
53
- 96: "Universal",
54
- 97: "Tempesta",
55
- 98: "Universal",
56
- 101: "META",
57
- 102: "Universal",
58
- 103: "Universal",
59
- 104: "Universal",
60
- 105: "Neptunia",
61
- 106: "KizunaAI",
62
- 107: "Hololive",
63
- 108: "Venus Vacation",
64
- 109: "The Idolmaster",
65
- 110: "SSSS",
66
- 111: "Atelier Ryza",
67
- 112: "Senran Kagura",
60
+ 12: "Liga de Pedrería", // Valparaíso (new faction, EN 9.3.384)
61
+ 96: "Tempesta", // MOT Royal Fortune, Golden Hind, ...
62
+ 97: "META", // Hunter.META, Fortune.META, ...
63
+ 98: "Universal", // Bulins
64
+ 101: "Neptunia", // HDN Neptune, Noire, Blanc, ...
65
+ 102: "Bilibili", // 22, 33 (CN only)
66
+ 103: "Utawarerumono", // Kuon, Nekone, Rurutie, ... (CN/JP only)
67
+ 104: "KizunaAI",
68
+ 105: "Hololive", // Shirakami Fubuki, Tokino Sora, ...
69
+ 106: "Venus Vacation", // Marie Rose, Honoka, ...
70
+ 107: "The Idolmaster", // Haruka Amami, ...
71
+ 108: "SSSS", // Rikka Takarada, ...
72
+ 109: "Atelier Ryza", // Reisalin Stout, ...
73
+ 110: "Senran Kagura", // Asuka, Ikaruga, ...
74
+ 111: "To LOVE-Ru", // Lala Satalin Deviluke, ...
75
+ 112: "Black Rock Shooter", // BLACK★ROCK SHOOTER, DEAD MASTER
76
+ 113: "Atelier Yumia", // Yumia Liessfeldt, ...
77
+ 114: "DanMachi", // Hestia, Ryu Lion
78
+ 115: "Date A Live", // Tohka Yatogami, ...
79
+ 117: "NieR:Automata", // A2, 2B (EN 9.3.384)
68
80
  };
69
81
  /** Look up a nationality code; falls back to "Universal". */
70
82
  function nationalityName(code) {
@@ -1 +1 @@
1
- {"version":3,"file":"AzurData.d.ts","sourceRoot":"","sources":["../../src/client/AzurData.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAmB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAGlE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAoB,SAAQ,YAAY;IACvD,8DAA8D;IAC9D,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAcD,qBAAa,QAAQ;IACnB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IAEtB,iEAAiE;gBACrD,OAAO,GAAE,eAAoB;IAWzC;;;;;;;OAOG;WACU,IAAI,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAmBvE,OAAO,CAAC,MAAM,CAAC,sBAAsB;CAUtC"}
1
+ {"version":3,"file":"AzurData.d.ts","sourceRoot":"","sources":["../../src/client/AzurData.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAmB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAGlE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAoB,SAAQ,YAAY;IACvD,8DAA8D;IAC9D,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AA4BD,qBAAa,QAAQ;IACnB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IAEtB,iEAAiE;gBACrD,OAAO,GAAE,eAAoB;IAWzC;;;;;;;OAOG;WACU,IAAI,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAmBvE,OAAO,CAAC,MAAM,CAAC,sBAAsB;CAUtC"}
@@ -1,11 +1,24 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
3
4
  import { Ships } from "./ships.js";
4
5
  import { fetchLatestData } from "./fetcher.js";
6
+ /**
7
+ * Path of this source file as seen in a stack trace: a file:// URL under ESM,
8
+ * a plain path under CJS. This file is compiled to both formats from one
9
+ * source, so `import.meta.url` cannot be written here (it is a syntax error
10
+ * in the CJS build), and `__dirname` only exists in CJS.
11
+ */
12
+ function thisFileFromStack() {
13
+ const stack = new Error().stack ?? "";
14
+ for (const line of stack.split("\n").slice(1)) {
15
+ const m = line.match(/\(?((?:file:\/\/)?\/[^):]+?\.[cm]?[jt]s):\d+:\d+\)?$/);
16
+ if (m?.[1] !== undefined)
17
+ return m[1].startsWith("file://") ? fileURLToPath(m[1]) : m[1];
18
+ }
19
+ return undefined;
20
+ }
5
21
  function resolveThisDir() {
6
- // CJS: __dirname is injected by Node
7
- // ESM under tsx/ts-node: __dirname is polyfilled
8
- // Pure ESM: falls back to cwd
9
22
  try {
10
23
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
11
24
  // @ts-ignore __dirname exists in CJS but not in ESM type defs
@@ -13,7 +26,10 @@ function resolveThisDir() {
13
26
  return __dirname; // eslint-disable-line no-undef
14
27
  }
15
28
  catch { /* not CJS */ }
16
- return process.cwd();
29
+ const file = thisFileFromStack();
30
+ if (file !== undefined)
31
+ return path.dirname(file);
32
+ return process.cwd(); // last resort; resolveDefaultDataPath walks up from here
17
33
  }
18
34
  export class AzurData {
19
35
  ships;
@@ -1 +1 @@
1
- {"version":3,"file":"AzurData.js","sourceRoot":"","sources":["../../src/client/AzurData.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,eAAe,EAAqB,MAAM,cAAc,CAAC;AAYlE,SAAS,cAAc;IACrB,qCAAqC;IACrC,iDAAiD;IACjD,8BAA8B;IAC9B,IAAI,CAAC;QACH,6DAA6D;QAC7D,8DAA8D;QAC9D,IAAI,OAAO,SAAS,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC,CAAC,+BAA+B;IACtF,CAAC;IAAC,MAAM,CAAC,CAAC,aAAa,CAAC,CAAC;IACzB,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC;AACvB,CAAC;AAED,MAAM,OAAO,QAAQ;IACV,KAAK,CAAQ;IAEtB,iEAAiE;IACjE,YAAY,UAA2B,EAAE;QACvC,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,IAAI,QAAQ,CAAC,sBAAsB,EAAE,CAAC;QACtE,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAC1B,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,CAClC,CAAC;QAC1B,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAC1B,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,EAAE,MAAM,CAAC,CACjC,CAAC;QAC5B,IAAI,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IAChE,CAAC;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,UAA+B,EAAE;QACjD,MAAM,EAAE,WAAW,GAAG,IAAI,EAAE,GAAG,SAAS,EAAE,GAAG,OAAO,CAAC;QAErD,IAAI,WAAW,EAAE,CAAC;YAChB,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,SAAS,CAAC,CAAC;YACjD,IAAI,OAAO,EAAE,CAAC;gBACZ,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAa,CAAC;gBAC9D,QAA6B,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC;oBAC/C,IAAI,EAAE,OAAO,CAAC,KAAK;oBACnB,KAAK,EAAE,OAAO,CAAC,KAAK;iBACrB,CAAC,CAAC;gBACH,OAAO,QAAQ,CAAC;YAClB,CAAC;QACH,CAAC;QAED,yBAAyB;QACzB,OAAO,IAAI,QAAQ,EAAE,CAAC;IACxB,CAAC;IAEO,MAAM,CAAC,sBAAsB;QACnC,MAAM,OAAO,GAAG,cAAc,EAAE,CAAC;QACjC,IAAI,SAAS,GAAG,OAAO,CAAC;QACxB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3B,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;YACjD,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;gBAAE,OAAO,QAAQ,CAAC;YACtE,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QAC5C,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,CAAC;IAC7C,CAAC;CACF"}
1
+ {"version":3,"file":"AzurData.js","sourceRoot":"","sources":["../../src/client/AzurData.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,12 @@ export interface ShipsOptions {
4
4
  idMap: Record<string, string>;
5
5
  fuseThreshold?: number;
6
6
  }
7
+ /**
8
+ * True when `query` is a prefix of one of the ship's names (3+ chars) or within
9
+ * typo distance of one: 1 edit for 4-5 chars, 2 edits for 6+. Shorter queries
10
+ * never fuzzy-match; they must hit an id or a name exactly.
11
+ */
12
+ export declare function isCloseName(query: string, ship: Ship): boolean;
7
13
  export declare class Ships {
8
14
  private readonly data;
9
15
  private readonly idMap;
@@ -1 +1 @@
1
- {"version":3,"file":"ships.d.ts","sourceRoot":"","sources":["../../src/client/ships.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAErD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,KAAK;IAChB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAuB;IAC5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;IAC/C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;gBAEtB,OAAO,EAAE,YAAY;IAoBjC,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;IAyBvC,OAAO,CAAC,QAAQ,CAAC,CAAyB;IAC1C,OAAO,CAAC,UAAU;IAUlB,MAAM,IAAI,IAAI,EAAE;IAIhB,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;IAIrC,KAAK,IAAI,MAAM;IAKf,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,IAAI,EAAE;CAI1C"}
1
+ {"version":3,"file":"ships.d.ts","sourceRoot":"","sources":["../../src/client/ships.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAErD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAkBD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAW9D;AAED,qBAAa,KAAK;IAChB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAuB;IAC5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;IAC/C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;gBAEtB,OAAO,EAAE,YAAY;IAoBjC,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;IA4BvC,OAAO,CAAC,QAAQ,CAAC,CAAyB;IAC1C,OAAO,CAAC,UAAU;IAUlB,MAAM,IAAI,IAAI,EAAE;IAIhB,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;IAIrC,KAAK,IAAI,MAAM;IAKf,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,IAAI,EAAE;CAI1C"}
@@ -1,4 +1,40 @@
1
1
  import Fuse from "fuse.js";
2
+ /** Optimal-string-alignment distance (Levenshtein + adjacent transposition). */
3
+ function editDistance(a, b) {
4
+ const rows = a.length + 1, cols = b.length + 1;
5
+ const d = Array.from({ length: rows }, (_, i) => Array.from({ length: cols }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
6
+ for (let i = 1; i < rows; i++) {
7
+ for (let j = 1; j < cols; j++) {
8
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
9
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
10
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
11
+ d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
12
+ }
13
+ }
14
+ }
15
+ return d[rows - 1][cols - 1];
16
+ }
17
+ /**
18
+ * True when `query` is a prefix of one of the ship's names (3+ chars) or within
19
+ * typo distance of one: 1 edit for 4-5 chars, 2 edits for 6+. Shorter queries
20
+ * never fuzzy-match; they must hit an id or a name exactly.
21
+ */
22
+ export function isCloseName(query, ship) {
23
+ const q = query.trim().toLowerCase();
24
+ if (q.length < 3)
25
+ return false;
26
+ const maxEdits = q.length >= 6 ? 2 : q.length >= 4 ? 1 : 0;
27
+ for (const raw of Object.values(ship.names)) {
28
+ if (typeof raw !== "string")
29
+ continue;
30
+ const n = raw.toLowerCase();
31
+ if (n.startsWith(q))
32
+ return true;
33
+ if (maxEdits > 0 && Math.abs(n.length - q.length) <= maxEdits && editDistance(q, n) <= maxEdits)
34
+ return true;
35
+ }
36
+ return false;
37
+ }
2
38
  export class Ships {
3
39
  data;
4
40
  idMap;
@@ -39,10 +75,13 @@ export class Ships {
39
75
  const ciId = ciIdMap[lower];
40
76
  if (ciId && this.data[ciId])
41
77
  return this.data[ciId];
42
- // 4. Fuse.js fuzzy
78
+ // 4. Fuse.js fuzzy, but only for a typo-level edit distance or a name prefix.
79
+ // Fuse scores substrings as perfect matches ("22" -> U-522 at 0.000, "Kuon"
80
+ // -> Kronshtadt at 0.003), which a lookup must not return.
43
81
  const results = this.fuse.search(nameOrId, { limit: 1 });
44
- if (results.length > 0 && results[0].score !== undefined && results[0].score < 0.5) {
45
- return results[0].item;
82
+ const hit = results[0];
83
+ if (hit !== undefined && hit.score !== undefined && hit.score < 0.5 && isCloseName(nameOrId, hit.item)) {
84
+ return hit.item;
46
85
  }
47
86
  return undefined;
48
87
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ships.js","sourceRoot":"","sources":["../../src/client/ships.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ships.js","sourceRoot":"","sources":["../../src/client/ships.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,SAAS,CAAC;AAS3B,gFAAgF;AAChF,SAAS,YAAY,CAAC,CAAS,EAAE,CAAS;IACxC,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;IAC/C,MAAM,CAAC,GAAe,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACtI,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;QAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;YAC9B,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC3C,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC,CAAC,CAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,IAAI,CAAC,CAAC;YACpF,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;gBACrE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,CAAC,CAAC,CAAC;YACxD,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,CAAC,CAAC,IAAI,GAAG,CAAC,CAAE,CAAC,IAAI,GAAG,CAAC,CAAE,CAAC;AACjC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa,EAAE,IAAU;IACnD,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACrC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAC/B,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3D,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5C,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,SAAS;QACtC,MAAM,CAAC,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;QAC5B,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;QACjC,IAAI,QAAQ,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,QAAQ,IAAI,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,QAAQ;YAAE,OAAO,IAAI,CAAC;IAC/G,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,OAAO,KAAK;IACC,IAAI,CAAuB;IAC3B,KAAK,CAAyB;IAC9B,IAAI,CAAa;IAElC,YAAY,OAAqB;QAC/B,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;YAChD,IAAI,EAAE;gBACJ,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,EAAE;gBAC/B,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,EAAE;gBACjC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,EAAE;gBAC/B,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,EAAE;aAChC;YACD,SAAS,EAAE,OAAO,CAAC,aAAa,IAAI,GAAG;YACvC,YAAY,EAAE,IAAI;SACnB,CAAC,CAAC;IACL,CAAC;IAED,2BAA2B;IAC3B,oCAAoC;IACpC,6CAA6C;IAC7C,2CAA2C;IAC3C,8BAA8B;IAC9B,GAAG,CAAC,QAAgB;QAClB,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QAE5E,mBAAmB;QACnB,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAEpD,6CAA6C;QAC7C,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACtC,IAAI,QAAQ,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAEhE,iCAAiC;QACjC,iCAAiC;QACjC,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;QACrC,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;QAClC,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;QAC5B,IAAI,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEpD,8EAA8E;QAC9E,+EAA+E;QAC/E,8DAA8D;QAC9D,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;QACzD,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QACvB,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,KAAK,KAAK,SAAS,IAAI,GAAG,CAAC,KAAK,GAAG,GAAG,IAAI,WAAW,CAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACvG,OAAO,GAAG,CAAC,IAAI,CAAC;QAClB,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IAEO,QAAQ,CAA0B;IAClC,UAAU;QAChB,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;YACnB,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBAChD,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,GAAG,CAAC,CAAC;YACrC,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAED,MAAM;QACJ,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;IAED,OAAO,CAAC,EAAU;QAChB,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACvB,CAAC;IAED,KAAK;QACH,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;IACvC,CAAC;IAED,0BAA0B;IAC1B,MAAM,CAAC,KAAa,EAAE,KAAK,GAAG,EAAE;QAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QACnD,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;CACF"}
@@ -1,5 +1,5 @@
1
1
  import type { Ship } from "../schema/output/ship.js";
2
- import type { ShipDataTemplate, ShipDataStatistics, ShipDataGroup, ShipDataBlueprint, FleetTechShipTemplate, FleetTechShipClass, ShipDataByType, ShipDataGroupValue } from "../schema/raw/index.js";
2
+ import type { ShipDataTemplate, ShipDataStatistics, ShipDataGroup, ShipDataBlueprint, FleetTechShipTemplate, FleetTechShipClass, ShipDataByType, ShipDataGroupValue, ShipSkinTemplate } from "../schema/raw/index.js";
3
3
  import type { Lookups } from "../translate/lookups.js";
4
4
  export interface IdentityInputs {
5
5
  groupType: string;
@@ -13,7 +13,14 @@ export interface IdentityInputs {
13
13
  fleetTechShipTemplate: FleetTechShipTemplate;
14
14
  fleetTechShipClass: FleetTechShipClass;
15
15
  shipDataByType: ShipDataByType;
16
+ /** Needed to resolve the thumbnail by the default skin's painting name. */
17
+ skinTemplate?: ShipSkinTemplate;
16
18
  };
19
+ /**
20
+ * Thumbnail files present in the images repo: lowercase stem -> actual filename stem.
21
+ * When given, the thumbnail URL only points at a file that exists (see resolveThumbnail).
22
+ */
23
+ availableThumbnails?: Map<string, string>;
17
24
  cn?: {
18
25
  statistics: ShipDataStatistics;
19
26
  };
@@ -41,5 +48,15 @@ export interface IdentityOutput {
41
48
  thumbnail: string;
42
49
  isEnReleased?: boolean;
43
50
  }
51
+ /**
52
+ * Pick the thumbnail filename stem for a ship.
53
+ *
54
+ * Shipyard icons in azurlane-images are named after the texture inside the
55
+ * bundle: historically the numeric group id (20212.png), but newer bundles use
56
+ * the skin's painting name (weikesibao.png, z14.png, 2B.png). Prefer the id so
57
+ * existing URLs stay stable, fall back to the default skin's painting name, and
58
+ * keep the id when nothing is known (no images clone at build time).
59
+ */
60
+ export declare function resolveThumbnail(groupType: string, defaultPainting: string | undefined, available: Map<string, string> | undefined): string;
44
61
  export declare function normalizeIdentity(inputs: IdentityInputs): IdentityOutput;
45
62
  //# sourceMappingURL=identity.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/normalize/identity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAErD,OAAO,KAAK,EACV,gBAAgB,EAChB,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,cAAc,EACd,kBAAkB,EACnB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAQvD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,qFAAqF;IACrF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,EAAE,EAAE;QACF,QAAQ,EAAE,gBAAgB,CAAC;QAC3B,UAAU,EAAE,kBAAkB,CAAC;QAC/B,KAAK,EAAE,aAAa,CAAC;QACrB,SAAS,EAAE,iBAAiB,CAAC;QAC7B,qBAAqB,EAAE,qBAAqB,CAAC;QAC7C,kBAAkB,EAAE,kBAAkB,CAAC;QACvC,cAAc,EAAE,cAAc,CAAC;KAChC,CAAC;IACF,EAAE,CAAC,EAAE;QACH,UAAU,EAAE,kBAAkB,CAAC;KAChC,CAAC;IACF,EAAE,CAAC,EAAE;QACH,UAAU,EAAE,kBAAkB,CAAC;KAChC,CAAC;IACF,wDAAwD;IACxD,YAAY,EAAE,OAAO,CAAC;IACtB,oEAAoE;IACpE,UAAU,EAAE,OAAO,CAAC;IACpB,mFAAmF;IACnF,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,2DAA2D;IAC3D,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACrB,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IACvB,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAeD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CAgJxE"}
1
+ {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/normalize/identity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAErD,OAAO,KAAK,EACV,gBAAgB,EAChB,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EACjB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAQvD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,qFAAqF;IACrF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,EAAE,EAAE;QACF,QAAQ,EAAE,gBAAgB,CAAC;QAC3B,UAAU,EAAE,kBAAkB,CAAC;QAC/B,KAAK,EAAE,aAAa,CAAC;QACrB,SAAS,EAAE,iBAAiB,CAAC;QAC7B,qBAAqB,EAAE,qBAAqB,CAAC;QAC7C,kBAAkB,EAAE,kBAAkB,CAAC;QACvC,cAAc,EAAE,cAAc,CAAC;QAC/B,2EAA2E;QAC3E,YAAY,CAAC,EAAE,gBAAgB,CAAC;KACjC,CAAC;IACF;;;OAGG;IACH,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,EAAE,CAAC,EAAE;QACH,UAAU,EAAE,kBAAkB,CAAC;KAChC,CAAC;IACF,EAAE,CAAC,EAAE;QACH,UAAU,EAAE,kBAAkB,CAAC;KAChC,CAAC;IACF,wDAAwD;IACxD,YAAY,EAAE,OAAO,CAAC;IACtB,oEAAoE;IACpE,UAAU,EAAE,OAAO,CAAC;IACpB,mFAAmF;IACnF,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,2DAA2D;IAC3D,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACrB,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IACvB,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAWD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,GACzC,MAAM,CASR;AAMD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CAsJxE"}