poi-plugin-kai-planner 1.0.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.
Files changed (54) hide show
  1. package/README.md +48 -0
  2. package/index.js +35 -0
  3. package/package.json +25 -0
  4. package/src/app/KaiPlannerApp.js +211 -0
  5. package/src/app/tabs/daily/DailyTab.js +439 -0
  6. package/src/app/tabs/debug/DebugTab.js +554 -0
  7. package/src/app/tabs/wishlist/CreatePlanForm.js +185 -0
  8. package/src/app/tabs/wishlist/WishlistTab.js +704 -0
  9. package/src/app/tabs/wishlist/components/MouseComboBox.js +185 -0
  10. package/src/app/tabs/wishlist/components/WishlistExpandedDetail.js +170 -0
  11. package/src/app/tabs/wishlist/components/WishlistTable.js +253 -0
  12. package/src/core/poi/secretaryName.js +64 -0
  13. package/src/data/indexes/buildArrangementIndex.js +103 -0
  14. package/src/data/loaders/loadStaticData.js +182 -0
  15. package/src/data/static/data_manifest.json +15 -0
  16. package/src/data/static/equip_base_cost.json +5000 -0
  17. package/src/data/static/equipment_upgrade_path.json +3555 -0
  18. package/src/data/static/improvement_arrangement.json +15677 -0
  19. package/src/data/static/improvement_consume_item.json +8933 -0
  20. package/src/data/static/improvement_consume_step.json +9284 -0
  21. package/src/data/static/improvement_upgrade_cost.json +4766 -0
  22. package/src/data/static/improvement_upgrade_target.json +2641 -0
  23. package/src/data/static/material.json +90 -0
  24. package/src/services/common/secretaryDisplay.js +42 -0
  25. package/src/services/daily/buildDailyViewModel.js +402 -0
  26. package/src/services/planner/buildUpgradePath.js +79 -0
  27. package/src/services/planner/calcImproveSteps.js +104 -0
  28. package/src/services/planner/calcRemainingPlan.js +169 -0
  29. package/src/services/planner/calcRoutePlan.js +85 -0
  30. package/src/services/planner/calcUpgradeStep.js +85 -0
  31. package/src/services/planner/detectCurrentPosition.js +57 -0
  32. package/src/services/planner/summarizeShortage.js +76 -0
  33. package/src/services/player/countPlayerEquipByMasterId.js +27 -0
  34. package/src/services/player/getEquipOwnerShip.js +66 -0
  35. package/src/services/player/getPlayerData.js +14 -0
  36. package/src/services/player/getPlayerItemCountByUseitemId.js +45 -0
  37. package/src/services/player/getReduxStateFromEnvWindow.js +12 -0
  38. package/src/services/player/resolveMaterialKeyToUseitemId.js +54 -0
  39. package/src/services/static/indexes/buildConsumeIndexes.js +28 -0
  40. package/src/services/static/indexes/buildPathIndexes.js +29 -0
  41. package/src/services/static/indexes/buildUpgradeIndexes.js +57 -0
  42. package/src/services/static/version/dataSourceConfig.js +25 -0
  43. package/src/services/static/version/dataUpdateManager.js +176 -0
  44. package/src/services/static/version/versionStore.js +205 -0
  45. package/src/services/utils/toInt.js +11 -0
  46. package/src/services/utils/tokyoTime.js +58 -0
  47. package/src/services/wishlist/buildWishlistViewModel.js +56 -0
  48. package/src/services/wishlist/dropdownInteraction.js +30 -0
  49. package/src/services/wishlist/wishlistActions.js +485 -0
  50. package/src/storage/userPlans/fileStore.js +106 -0
  51. package/src/storage/userPlans/localStorageStore.js +50 -0
  52. package/src/storage/userPlans/migrate.js +60 -0
  53. package/src/storage/userPlans/planStore.js +107 -0
  54. package/src/storage/userPlans/storeAdapter.js +77 -0
@@ -0,0 +1,90 @@
1
+ [
2
+ {
3
+ "key": "ActionReport",
4
+ "name": "戦闘詳報",
5
+ "is_deleted": false,
6
+ "created_at": "2025-12-19 17:06:02.044487+00",
7
+ "updated_at": "2025-12-19 17:06:02.044487+00",
8
+ "deleted_at": null
9
+ },
10
+ {
11
+ "key": "emergency-repair-material",
12
+ "name": "緊急修理資材",
13
+ "is_deleted": false,
14
+ "created_at": "2025-12-21 05:31:47.230177+00",
15
+ "updated_at": "2025-12-21 05:31:47.230177+00",
16
+ "deleted_at": null
17
+ },
18
+ {
19
+ "key": "kaigai-skill",
20
+ "name": "海外艦最新技術",
21
+ "is_deleted": false,
22
+ "created_at": "2025-12-20 09:14:11.251864+00",
23
+ "updated_at": "2025-12-20 09:14:11.251864+00",
24
+ "deleted_at": null
25
+ },
26
+ {
27
+ "key": "MedalL",
28
+ "name": "勲章",
29
+ "is_deleted": false,
30
+ "created_at": "2025-12-20 09:23:41.614305+00",
31
+ "updated_at": "2025-12-20 09:23:41.614305+00",
32
+ "deleted_at": null
33
+ },
34
+ {
35
+ "key": "NeEngine",
36
+ "name": "ネ式エンジン",
37
+ "is_deleted": false,
38
+ "created_at": "2025-12-20 09:27:25.825557+00",
39
+ "updated_at": "2025-12-20 09:27:25.825557+00",
40
+ "deleted_at": null
41
+ },
42
+ {
43
+ "key": "new_gun_material",
44
+ "name": "新型砲熕兵装資材",
45
+ "is_deleted": false,
46
+ "created_at": "2025-12-19 17:06:49.432937+00",
47
+ "updated_at": "2025-12-19 17:06:49.432937+00",
48
+ "deleted_at": null
49
+ },
50
+ {
51
+ "key": "new_model_material",
52
+ "name": "新型兵装資材",
53
+ "is_deleted": false,
54
+ "created_at": "2025-12-19 17:06:52.891009+00",
55
+ "updated_at": "2025-12-19 17:06:52.891009+00",
56
+ "deleted_at": null
57
+ },
58
+ {
59
+ "key": "new_plane_material",
60
+ "name": "新型航空兵装資材",
61
+ "is_deleted": false,
62
+ "created_at": "2025-12-20 08:38:45.799826+00",
63
+ "updated_at": "2025-12-20 08:38:45.799826+00",
64
+ "deleted_at": null
65
+ },
66
+ {
67
+ "key": "new-funsiki-material",
68
+ "name": "新型噴進装備開発資材",
69
+ "is_deleted": false,
70
+ "created_at": "2025-12-20 09:48:09.572883+00",
71
+ "updated_at": "2025-12-20 09:48:09.572883+00",
72
+ "deleted_at": null
73
+ },
74
+ {
75
+ "key": "sensui-hokyu",
76
+ "name": "潜水艦補給物資",
77
+ "is_deleted": false,
78
+ "created_at": "2026-02-07 14:18:49.446289+00",
79
+ "updated_at": "2026-02-07 14:18:49.446289+00",
80
+ "deleted_at": null
81
+ },
82
+ {
83
+ "key": "skilled_crew",
84
+ "name": "熟練搭乗員",
85
+ "is_deleted": false,
86
+ "created_at": "2025-12-20 08:38:46.377125+00",
87
+ "updated_at": "2025-12-20 08:38:46.377125+00",
88
+ "deleted_at": null
89
+ }
90
+ ]
@@ -0,0 +1,42 @@
1
+ /* src/services/common/secretaryDisplay.js */
2
+ /* Format secretary list for UI:
3
+ - name resolved by core/poi/secretaryName (variant=b via aftershipid once)
4
+ - output: "A(备注)/B(备注)" (no note => "A")
5
+ */
6
+
7
+ const { resolveSecretaryName } = require("../../core/poi/secretaryName");
8
+
9
+ function formatOne({ name, note }) {
10
+ const n = (note == null || String(note).trim() === "") ? null : String(note).trim();
11
+ return n ? `${name}(${n})` : name;
12
+ }
13
+
14
+ /**
15
+ * @param {Object} params
16
+ * @param {Array} params.arrangements - rows from improvement_arrangement filtered is_deleted=false
17
+ * @param {Object} params.shipBySortno
18
+ * @param {Object} params.shipByApiId
19
+ * @returns {string} joined display text (or "不可" if empty)
20
+ */
21
+ function formatSecretaryList({ arrangements, shipBySortno, shipByApiId }) {
22
+ const list = (arrangements || [])
23
+ .map((r) => {
24
+ const secretarySortno = Number(r.secretary_id);
25
+ const secretaryVariant = r.secretary_variant == null ? null : String(r.secretary_variant);
26
+ const resolved = resolveSecretaryName({
27
+ secretarySortno,
28
+ secretaryVariant,
29
+ shipBySortno,
30
+ shipByApiId,
31
+ });
32
+ return formatOne({ name: resolved.name, note: r.note });
33
+ })
34
+ .filter((x) => x && String(x).trim() !== "");
35
+
36
+ if (list.length === 0) return "不可";
37
+ return list.join("/");
38
+ }
39
+
40
+ module.exports = {
41
+ formatSecretaryList,
42
+ };
@@ -0,0 +1,402 @@
1
+ /* src/services/daily/buildDailyViewModel.js */
2
+
3
+ const { WEEKDAY_KEYS, NULL_ROUTE } = require("../../data/indexes/buildArrangementIndex");
4
+ const { formatSecretaryList } = require("../common/secretaryDisplay");
5
+
6
+ const ANY_SECRETARY_TEXT = "任意秘书舰均可";
7
+
8
+ function toInt(x) {
9
+ if (x === null || x === undefined) return null;
10
+ const n = Number(x);
11
+ return Number.isFinite(n) ? n : null;
12
+ }
13
+
14
+ function getEquipName(masterEquipsById, equipId) {
15
+ const r = masterEquipsById && masterEquipsById[String(equipId)];
16
+ if (r && r.api_name) return String(r.api_name);
17
+ return `UnknownEquip(${equipId})`;
18
+ }
19
+
20
+ function getEquipSortMeta(masterEquipsById, equipId) {
21
+ const r = masterEquipsById && masterEquipsById[String(equipId)];
22
+ const sortno = r && r.api_sortno != null ? toInt(r.api_sortno) || 0 : 0;
23
+ const typeArr = r && Array.isArray(r.api_type) ? r.api_type.map((x) => toInt(x) || 0) : [];
24
+ return { sortno, typeArr };
25
+ }
26
+
27
+ function buildMaterialMap(staticData) {
28
+ const m = {};
29
+ for (const r of staticData.material || []) {
30
+ if (!r || !r.key) continue;
31
+ m[String(r.key)] = r;
32
+ }
33
+ return m;
34
+ }
35
+
36
+ function buildBaseCostMap(staticData) {
37
+ const m = {};
38
+ for (const r of staticData.equipBaseCost || []) {
39
+ if (!r || r.id == null) continue;
40
+ m[String(r.id)] = r;
41
+ }
42
+ return m;
43
+ }
44
+
45
+ function buildStepMaps(staticData) {
46
+ const stepById = {};
47
+ for (const r of staticData.improvementConsumeStep || []) {
48
+ if (!r || !r.id) continue;
49
+ stepById[String(r.id)] = r;
50
+ }
51
+
52
+ const itemsByStepId = {};
53
+ for (const r of staticData.improvementConsumeItem || []) {
54
+ if (!r || !r.step_id) continue;
55
+ const k = String(r.step_id);
56
+ if (!itemsByStepId[k]) itemsByStepId[k] = [];
57
+ itemsByStepId[k].push(r);
58
+ }
59
+
60
+ return { stepById, itemsByStepId };
61
+ }
62
+
63
+ function buildUpgradeMaps(staticData) {
64
+ const targetsByEquipId = {};
65
+ for (const r of staticData.improvementUpgradeTarget || []) {
66
+ if (!r || r.equipment_id == null) continue;
67
+ const k = String(r.equipment_id);
68
+ if (!targetsByEquipId[k]) targetsByEquipId[k] = [];
69
+ targetsByEquipId[k].push(r);
70
+ }
71
+
72
+ const costsByPair = {};
73
+ for (const r of staticData.improvementUpgradeCost || []) {
74
+ if (!r || r.equipment_id == null || r.upgrade_id == null) continue;
75
+ const k = `${r.equipment_id}|${r.upgrade_id}`;
76
+ if (!costsByPair[k]) costsByPair[k] = [];
77
+ costsByPair[k].push(r);
78
+ }
79
+
80
+ return { targetsByEquipId, costsByPair };
81
+ }
82
+
83
+ function resolveMaterialName(materialByKey, key) {
84
+ if (!key) return null;
85
+ const r = materialByKey && materialByKey[String(key)];
86
+ return r ? String(r.name || key) : String(key);
87
+ }
88
+
89
+ function normalizeItemRow({ row, masterEquipsById, materialByKey }) {
90
+ if (row.item_equipment_id != null) {
91
+ const id = toInt(row.item_equipment_id);
92
+ return {
93
+ kind: "equipment",
94
+ id,
95
+ name: getEquipName(masterEquipsById, id),
96
+ count: toInt(row.count) || 0,
97
+ };
98
+ }
99
+ if (row.item_material_key != null) {
100
+ const key = String(row.item_material_key);
101
+ return {
102
+ kind: "material",
103
+ key,
104
+ name: resolveMaterialName(materialByKey, key),
105
+ count: toInt(row.count) || 0,
106
+ };
107
+ }
108
+ return null;
109
+ }
110
+
111
+ function phaseKey(equipId, phase) {
112
+ return `${equipId}_${phase}`;
113
+ }
114
+
115
+ function routeKindToIndexKey(routeKind) {
116
+ if (routeKind === null || routeKind === undefined || routeKind === "") return NULL_ROUTE;
117
+ return String(routeKind);
118
+ }
119
+
120
+ function buildImprovePhase({ equipId, phase, stepById, itemsByStepId, masterEquipsById, materialByKey }) {
121
+ const sid = phaseKey(equipId, phase);
122
+
123
+ const step = stepById[sid] || null;
124
+ const rows = itemsByStepId[sid] || [];
125
+
126
+ const common = [];
127
+ const extras = {}; // "star_from-star_to" -> items[]
128
+ for (const r of rows) {
129
+ const hasExtra = r.star_from != null && r.star_to != null;
130
+ const item = normalizeItemRow({ row: r, masterEquipsById, materialByKey });
131
+ if (!item) continue;
132
+
133
+ if (!hasExtra) {
134
+ common.push(item);
135
+ } else {
136
+ const k = `${r.star_from}-${r.star_to}`;
137
+ if (!extras[k]) extras[k] = [];
138
+ extras[k].push(item);
139
+ }
140
+ }
141
+
142
+ const extraList = Object.keys(extras).map((k) => {
143
+ const [from, to] = k.split("-").map((x) => toInt(x));
144
+ return { star_from: from, star_to: to, items: extras[k] };
145
+ });
146
+
147
+ return {
148
+ phase,
149
+ step_id: sid,
150
+ dev: {
151
+ min: step ? (toInt(step.consume_development_min) || 0) : 0,
152
+ max: step ? (toInt(step.consume_development_max) || 0) : 0,
153
+ },
154
+ screw: {
155
+ min: step ? (toInt(step.consume_improvement_min) || 0) : 0,
156
+ max: step ? (toInt(step.consume_improvement_max) || 0) : 0,
157
+ },
158
+ items: {
159
+ common,
160
+ extras: extraList,
161
+ },
162
+ };
163
+ }
164
+
165
+ function buildTodayUpgrades({
166
+ equipId,
167
+ wkBucket,
168
+ isAnySecretary,
169
+ targetsByEquipId,
170
+ costsByPair,
171
+ masterEquipsById,
172
+ materialByKey,
173
+ shipBySortno,
174
+ shipByApiId,
175
+ }) {
176
+ const allTargets = (targetsByEquipId[String(equipId)] || []).filter((r) => r && r.is_deleted === false);
177
+ if (allTargets.length === 0) return [];
178
+
179
+ const upgrades = [];
180
+
181
+ for (const t of allTargets) {
182
+ const upgradeId = toInt(t.upgrade_id);
183
+ if (upgradeId == null) continue;
184
+
185
+ let secretaryText = ANY_SECRETARY_TEXT;
186
+
187
+ if (!isAnySecretary) {
188
+ // ✅ 需要命中“今日 + route_kind”的秘书舰
189
+ const routeKey = routeKindToIndexKey(t.route_kind);
190
+ const arrangements = wkBucket && wkBucket[routeKey] ? wkBucket[routeKey] : null;
191
+ if (!arrangements || arrangements.length === 0) continue;
192
+ secretaryText = formatSecretaryList({ arrangements, shipBySortno, shipByApiId });
193
+ }
194
+
195
+ const pairKey = `${equipId}|${upgradeId}`;
196
+ const costRows = (costsByPair[pairKey] || []).filter((r) => r && r.is_deleted === false);
197
+
198
+ const items = [];
199
+ for (const r of costRows) {
200
+ const item = normalizeItemRow({ row: r, masterEquipsById, materialByKey });
201
+ if (item) items.push(item);
202
+ }
203
+
204
+ upgrades.push({
205
+ equipId,
206
+ upgradeId,
207
+ upgradeName: getEquipName(masterEquipsById, upgradeId),
208
+ routeKind: t.route_kind === "" ? null : (t.route_kind == null ? null : String(t.route_kind)),
209
+ secretaryText,
210
+ dev: {
211
+ min: toInt(t.consume_development_min) || 0,
212
+ max: toInt(t.consume_development_max) || 0,
213
+ },
214
+ screw: {
215
+ min: toInt(t.consume_improvement_min) || 0,
216
+ max: toInt(t.consume_improvement_max) || 0,
217
+ },
218
+ items,
219
+ });
220
+ }
221
+
222
+ return upgrades;
223
+ }
224
+
225
+ function buildUpgradeMeta({ equipId, wkBucket, isAnySecretary, targetsByEquipId }) {
226
+ const allTargets = (targetsByEquipId[String(equipId)] || []).filter((r) => r && r.is_deleted === false);
227
+ const hasAnyUpgradeConfig = allTargets.length > 0;
228
+
229
+ if (!hasAnyUpgradeConfig) {
230
+ return {
231
+ hasAnyUpgradeConfig: false,
232
+ canUpgradeToday: false,
233
+ reasonToday: "NO_CONFIG", // 任意日子都不可进化
234
+ };
235
+ }
236
+
237
+ // ✅ “任意秘书舰均可”的装备:每天都可进化(只要有配置)
238
+ if (isAnySecretary) {
239
+ return {
240
+ hasAnyUpgradeConfig: true,
241
+ canUpgradeToday: true,
242
+ reasonToday: "CAN_TODAY",
243
+ };
244
+ }
245
+
246
+ // 有进化配置,但今日是否可进化:需要 wkBucket 命中任意 route_kind
247
+ let canToday = false;
248
+ if (wkBucket) {
249
+ for (const t of allTargets) {
250
+ const rk = routeKindToIndexKey(t.route_kind);
251
+ const arr = wkBucket[rk];
252
+ if (arr && arr.length > 0) {
253
+ canToday = true;
254
+ break;
255
+ }
256
+ }
257
+ }
258
+
259
+ return {
260
+ hasAnyUpgradeConfig: true,
261
+ canUpgradeToday: canToday,
262
+ reasonToday: canToday ? "CAN_TODAY" : "NOT_TODAY",
263
+ };
264
+ }
265
+
266
+ function getSecretaryTextMerged({ wkBucket, shipBySortno, shipByApiId }) {
267
+ const routeKeys = Object.keys(wkBucket || {});
268
+ if (routeKeys.length === 0) return "";
269
+ const merged = [];
270
+ for (const rk of routeKeys) merged.push(...(wkBucket[rk] || []));
271
+ return formatSecretaryList({ arrangements: merged, shipBySortno, shipByApiId });
272
+ }
273
+
274
+ /**
275
+ * @returns {{ weekdayKey: string, rows: Array }}
276
+ */
277
+ function buildDailyViewModel({ staticData, arrangementIndex, poi, weekdayKey }) {
278
+ if (!WEEKDAY_KEYS.includes(weekdayKey)) {
279
+ throw new Error(`invalid weekdayKey: ${weekdayKey}`);
280
+ }
281
+
282
+ const masterEquipsById = poi.masterEquipsById || {};
283
+ const shipBySortno = poi.shipBySortno || {};
284
+ const shipByApiId = poi.shipByApiId || {};
285
+
286
+ const baseCostByEquipId = buildBaseCostMap(staticData);
287
+ const materialByKey = buildMaterialMap(staticData);
288
+ const { stepById, itemsByStepId } = buildStepMaps(staticData);
289
+ const { targetsByEquipId, costsByPair } = buildUpgradeMaps(staticData);
290
+
291
+ // ✅ 关键修复:equipId 来源 = arrangementIndex ∪ equipBaseCost
292
+ const equipIdSet = new Set();
293
+ for (const k of Object.keys(arrangementIndex || {})) {
294
+ const id = toInt(k);
295
+ if (id != null) equipIdSet.add(String(id));
296
+ }
297
+ for (const k of Object.keys(baseCostByEquipId || {})) {
298
+ const id = toInt(k);
299
+ if (id != null) equipIdSet.add(String(id));
300
+ }
301
+
302
+ const equipIds = Array.from(equipIdSet).map((x) => toInt(x)).filter((x) => x != null);
303
+
304
+ const rows = [];
305
+ for (const equipId of equipIds) {
306
+ const equipBucket = arrangementIndex[String(equipId)] || null;
307
+ const wkBucket = equipBucket ? (equipBucket[weekdayKey] || null) : null;
308
+
309
+ // ✅ 真·任意秘书舰:只有当该装备在 improvement_arrangement 完全没有任何记录时才成立
310
+ const isAnySecretary = !equipBucket;
311
+
312
+ // ✅ 有排班但今天没有:秘书舰显示“不可”(不要显示任意)
313
+ const secretaryText = isAnySecretary
314
+ ? ANY_SECRETARY_TEXT
315
+ : (wkBucket ? getSecretaryTextMerged({ wkBucket, shipBySortno, shipByApiId }) : "不可");
316
+
317
+ // ✅ 过滤:有排班记录,但今天没有排班(= 今天不可改修) -> 不展示
318
+ if (!isAnySecretary && !wkBucket) {
319
+ continue;
320
+ }
321
+
322
+ const base = baseCostByEquipId[String(equipId)] || null;
323
+
324
+ const phase0 = buildImprovePhase({
325
+ equipId,
326
+ phase: 0,
327
+ stepById,
328
+ itemsByStepId,
329
+ masterEquipsById,
330
+ materialByKey,
331
+ });
332
+ const phase1 = buildImprovePhase({
333
+ equipId,
334
+ phase: 1,
335
+ stepById,
336
+ itemsByStepId,
337
+ masterEquipsById,
338
+ materialByKey,
339
+ });
340
+
341
+ const upgrades = buildTodayUpgrades({
342
+ equipId,
343
+ wkBucket,
344
+ isAnySecretary,
345
+ targetsByEquipId,
346
+ costsByPair,
347
+ masterEquipsById,
348
+ materialByKey,
349
+ shipBySortno,
350
+ shipByApiId,
351
+ });
352
+
353
+ const upgradeMeta = buildUpgradeMeta({
354
+ equipId,
355
+ wkBucket,
356
+ isAnySecretary,
357
+ targetsByEquipId,
358
+ });
359
+
360
+ rows.push({
361
+ equipId,
362
+ equipName: getEquipName(masterEquipsById, equipId),
363
+ sortMeta: getEquipSortMeta(masterEquipsById, equipId),
364
+ baseCost: base
365
+ ? {
366
+ fuel: toInt(base.consume_fuel) || 0,
367
+ ammo: toInt(base.consume_ammo) || 0,
368
+ steel: toInt(base.consume_steel) || 0,
369
+ bauxite: toInt(base.consume_bauxite) || 0,
370
+ }
371
+ : { fuel: 0, ammo: 0, steel: 0, bauxite: 0, missing: true },
372
+
373
+ secretaryText,
374
+ isAnySecretary,
375
+
376
+ phases: [phase0, phase1],
377
+ upgrades,
378
+ upgradeMeta,
379
+ });
380
+ }
381
+
382
+ rows.sort((a, b) => {
383
+ const at = (a.sortMeta && a.sortMeta.typeArr) || [];
384
+ const bt = (b.sortMeta && b.sortMeta.typeArr) || [];
385
+ const len = Math.max(at.length, bt.length);
386
+ for (let i = 0; i < len; i += 1) {
387
+ const av = at[i] != null ? at[i] : -1;
388
+ const bv = bt[i] != null ? bt[i] : -1;
389
+ if (av !== bv) return av - bv;
390
+ }
391
+ const as = a.sortMeta && a.sortMeta.sortno ? a.sortMeta.sortno : 0;
392
+ const bs = b.sortMeta && b.sortMeta.sortno ? b.sortMeta.sortno : 0;
393
+ if (as !== bs) return as - bs;
394
+ return a.equipId - b.equipId;
395
+ });
396
+
397
+ return { weekdayKey, rows };
398
+ }
399
+
400
+ module.exports = {
401
+ buildDailyViewModel,
402
+ };
@@ -0,0 +1,79 @@
1
+ // src/services/planner/buildUpgradePath.js
2
+ // 生成进化(装备改造/换装)路径:从 equipId -> targetEquipId
3
+ // - 用 BFS 找最短路径;routeKind 有值时优先匹配对应边
4
+
5
+ const { toInt } = require("../utils/toInt");
6
+
7
+ function _pickEdge(edges, routeKind) {
8
+ if (!edges || !edges.length) return null;
9
+ if (!routeKind) return edges[0];
10
+ const hit = edges.find((e) => e && String(e.route_kind || "") === String(routeKind));
11
+ return hit || edges[0];
12
+ }
13
+
14
+ function buildUpgradePath({ equipId, targetEquipId, routeKind, upgradeIndexes }) {
15
+ const fromId = toInt(equipId, 0);
16
+ const toId = toInt(targetEquipId, 0);
17
+ if (!fromId || !toId || fromId === toId) return { steps: [] };
18
+
19
+ const byFrom = upgradeIndexes && upgradeIndexes.targetsByFromEquipId ? upgradeIndexes.targetsByFromEquipId : {};
20
+ const visited = new Set([String(fromId)]);
21
+ const q = [{ id: fromId, path: [] }];
22
+ const MAX_NODES = 2000;
23
+
24
+ while (q.length && visited.size < MAX_NODES) {
25
+ const cur = q.shift();
26
+ const edges = byFrom[String(cur.id)] || [];
27
+ if (!edges.length) continue;
28
+
29
+ const ordered = routeKind
30
+ ? [...edges].sort(
31
+ (a, b) =>
32
+ (String(a.route_kind) === String(routeKind) ? -1 : 1) -
33
+ (String(b.route_kind) === String(routeKind) ? -1 : 1)
34
+ )
35
+ : edges;
36
+
37
+ for (const e of ordered) {
38
+ if (!e) continue;
39
+ const nextId = toInt(e.upgrade_to_equipment_id != null ? e.upgrade_to_equipment_id : e.upgrade_id, 0);
40
+ if (!nextId) continue;
41
+ const key = String(nextId);
42
+ if (visited.has(key)) continue;
43
+
44
+ const nextPath = cur.path.concat([
45
+ {
46
+ equipId: toInt(e.equipment_id, 0),
47
+ toEquipId: nextId,
48
+ route_kind: e.route_kind || null,
49
+ upgrade_id: e.upgrade_id || null,
50
+ },
51
+ ]);
52
+
53
+ if (nextId === toId) return { steps: nextPath };
54
+ visited.add(key);
55
+ q.push({ id: nextId, path: nextPath });
56
+ }
57
+ }
58
+
59
+ // fallback: direct pair
60
+ const pairKey = `${fromId}|${toId}`;
61
+ const direct = upgradeIndexes && upgradeIndexes.targetsByPair ? upgradeIndexes.targetsByPair[pairKey] : null;
62
+ const edge = _pickEdge(direct, routeKind);
63
+ if (edge) {
64
+ return {
65
+ steps: [
66
+ {
67
+ equipId: toInt(edge.equipment_id, 0),
68
+ toEquipId: toInt(edge.upgrade_to_equipment_id != null ? edge.upgrade_to_equipment_id : edge.upgrade_id, 0),
69
+ route_kind: edge.route_kind || null,
70
+ upgrade_id: edge.upgrade_id || null,
71
+ },
72
+ ],
73
+ };
74
+ }
75
+
76
+ return { steps: [] };
77
+ }
78
+
79
+ module.exports = { buildUpgradePath };