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,58 @@
1
+ /* src/services/utils/tokyoTime.js */
2
+
3
+ const TOKYO_WEEK_KEYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
4
+ const TOKYO_WEEK_LABEL_ZH = {
5
+ monday: "周一",
6
+ tuesday: "周二",
7
+ wednesday: "周三",
8
+ thursday: "周四",
9
+ friday: "周五",
10
+ saturday: "周六",
11
+ sunday: "周日",
12
+ };
13
+
14
+ function pad2(n) {
15
+ return String(n).padStart(2, "0");
16
+ }
17
+
18
+ function getTokyoDate(now = new Date()) {
19
+ const utcMs = now.getTime() + now.getTimezoneOffset() * 60 * 1000;
20
+ return new Date(utcMs + 9 * 60 * 60 * 1000);
21
+ }
22
+
23
+ function getTokyoWeekdayKey(now = new Date()) {
24
+ const t = getTokyoDate(now);
25
+ return TOKYO_WEEK_KEYS[t.getDay()];
26
+ }
27
+
28
+ function getTokyoWeekdayLabelZh(weekdayKey) {
29
+ return TOKYO_WEEK_LABEL_ZH[weekdayKey] || "";
30
+ }
31
+
32
+ function formatTokyoDateTime(now = new Date()) {
33
+ const t = getTokyoDate(now);
34
+ const y = t.getFullYear();
35
+ const m = pad2(t.getMonth() + 1);
36
+ const d = pad2(t.getDate());
37
+ const hh = pad2(t.getHours());
38
+ const mm = pad2(t.getMinutes());
39
+ const ss = pad2(t.getSeconds());
40
+ return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
41
+ }
42
+
43
+ function formatTokyoDateTimeWithWeekday(now = new Date()) {
44
+ const t = getTokyoDate(now);
45
+ const text = formatTokyoDateTime(now);
46
+ const wk = getTokyoWeekdayLabelZh(TOKYO_WEEK_KEYS[t.getDay()]);
47
+ return `${text}(${wk})`;
48
+ }
49
+
50
+ module.exports = {
51
+ TOKYO_WEEK_KEYS,
52
+ TOKYO_WEEK_LABEL_ZH,
53
+ getTokyoDate,
54
+ getTokyoWeekdayKey,
55
+ getTokyoWeekdayLabelZh,
56
+ formatTokyoDateTime,
57
+ formatTokyoDateTimeWithWeekday,
58
+ };
@@ -0,0 +1,56 @@
1
+ /* src/services/wishlist/buildWishlistViewModel.js */
2
+
3
+ function toInt(x, fallback = 0) {
4
+ const n = Number(x);
5
+ return Number.isFinite(n) ? Math.trunc(n) : fallback;
6
+ }
7
+
8
+ function priorityWeight(p) {
9
+ const m = /^P([0-5])$/.exec(String(p || "P0"));
10
+ return m ? toInt(m[1], 0) : 0;
11
+ }
12
+
13
+ function buildWishlistViewModel({ plans, planResults, filterTargetName, getEquipName, getEquipSortno, masterEquipsById }) {
14
+ const list = Array.isArray(plans) ? plans : [];
15
+ const wraps = planResults || {};
16
+ const filterText = String(filterTargetName || "")
17
+ .trim()
18
+ .toLowerCase();
19
+
20
+ const filteredPlans = list
21
+ .filter((p) => {
22
+ if (!filterText) return true;
23
+ const name = getEquipName(masterEquipsById, p.targetEquipId).toLowerCase();
24
+ return name.includes(filterText);
25
+ })
26
+ .sort((a, b) => {
27
+ const pa = priorityWeight(a.priority);
28
+ const pb = priorityWeight(b.priority);
29
+ if (pa !== pb) return pa - pb;
30
+ const sa = getEquipSortno(masterEquipsById, a.targetEquipId);
31
+ const sb = getEquipSortno(masterEquipsById, b.targetEquipId);
32
+ if (sa !== sb) return sa - sb;
33
+ return String(a.id).localeCompare(String(b.id));
34
+ });
35
+
36
+ const rows = filteredPlans.map((p) => {
37
+ const wrap = wraps[p.id] || {};
38
+ const completedCount = wrap.completedCount || 0;
39
+ const totalSteps = Array.isArray(p.snapshotSteps) ? p.snapshotSteps.length : 0;
40
+ return {
41
+ plan: p,
42
+ wrap,
43
+ completedCount,
44
+ totalSteps,
45
+ today: wrap.todaySecretary || "不可",
46
+ targetName: getEquipName(masterEquipsById, p.targetEquipId),
47
+ };
48
+ });
49
+
50
+ return {
51
+ rows,
52
+ filteredPlans,
53
+ };
54
+ }
55
+
56
+ module.exports = { buildWishlistViewModel };
@@ -0,0 +1,30 @@
1
+ /* src/services/wishlist/dropdownInteraction.js */
2
+
3
+ const COMBO_ROOT_ATTR = "data-mouse-combo-root";
4
+ const EVENT_COMBO_OPENED = "kai-planner-combo-opened";
5
+ const EVENT_COMBO_CLOSE_ALL = "kai-planner-combo-close-all";
6
+
7
+ function isInComboRoot(target) {
8
+ return !!(target && typeof target.closest === "function" && target.closest(`[${COMBO_ROOT_ATTR}="1"]`));
9
+ }
10
+
11
+ function shouldCloseOnDocMouseDown({ isOpen, rootRef, target }) {
12
+ if (!isOpen) return false;
13
+ if (!rootRef || typeof rootRef.contains !== "function") return false;
14
+ return !rootRef.contains(target);
15
+ }
16
+
17
+ function shouldCloseOnComboOpened({ isOpen, selfId, openedId }) {
18
+ if (!isOpen) return false;
19
+ if (!openedId) return false;
20
+ return String(selfId) !== String(openedId);
21
+ }
22
+
23
+ module.exports = {
24
+ COMBO_ROOT_ATTR,
25
+ EVENT_COMBO_OPENED,
26
+ EVENT_COMBO_CLOSE_ALL,
27
+ isInComboRoot,
28
+ shouldCloseOnDocMouseDown,
29
+ shouldCloseOnComboOpened,
30
+ };
@@ -0,0 +1,485 @@
1
+ /* src/services/wishlist/wishlistActions.js */
2
+
3
+ const { NULL_ROUTE } = require("../../data/indexes/buildArrangementIndex");
4
+ const { formatSecretaryList } = require("../common/secretaryDisplay");
5
+ const { toInt } = require("../utils/toInt");
6
+
7
+ function toRouteKey(routeKind) {
8
+ if (routeKind == null) return NULL_ROUTE;
9
+ const s = String(routeKind).trim();
10
+ return s ? s : NULL_ROUTE;
11
+ }
12
+
13
+ function getShipIndexes(masterShipsRaw) {
14
+ const shipBySortno = {};
15
+ const shipByApiId = {};
16
+ for (const k of Object.keys(masterShipsRaw || {})) {
17
+ const s = masterShipsRaw[k];
18
+ if (!s) continue;
19
+ if (s.api_sortno != null) shipBySortno[String(s.api_sortno)] = s;
20
+ if (s.api_id != null) shipByApiId[String(s.api_id)] = s;
21
+ }
22
+ return { shipBySortno, shipByApiId };
23
+ }
24
+
25
+ function getSecretaryTodayForStep(step, weekdayKey, arrangementIndex, shipBySortno, shipByApiId) {
26
+ if (!step) return "改修已完成";
27
+ const equipBucket = arrangementIndex[String(step.equipId)] || null;
28
+ if (!equipBucket) return "任意秘书舰均可";
29
+
30
+ const wkBucket = equipBucket[weekdayKey] || null;
31
+ if (!wkBucket) return "不可";
32
+
33
+ if (step.kind === "upgrade") {
34
+ const routeKey = toRouteKey(step.route_kind_required);
35
+ const rows = wkBucket[routeKey] || [];
36
+ if (!rows.length) return "不可";
37
+ return formatSecretaryList({ arrangements: rows, shipBySortno, shipByApiId });
38
+ }
39
+
40
+ const merged = [];
41
+ for (const k of Object.keys(wkBucket)) merged.push(...(wkBucket[k] || []));
42
+ if (!merged.length) return "不可";
43
+ return formatSecretaryList({ arrangements: merged, shipBySortno, shipByApiId });
44
+ }
45
+
46
+ function computeCompletedCount(snapshotSteps, position) {
47
+ const steps = Array.isArray(snapshotSteps) ? snapshotSteps : [];
48
+ if (!position || !position.isValid) return 0;
49
+
50
+ let done = 0;
51
+ for (const s of steps) {
52
+ let completed = false;
53
+ if (s.kind === "improve") {
54
+ if (position.currentEquipId !== s.equipId) completed = true;
55
+ else completed = (position.currentLevel || 0) >= (s.toStar || 0);
56
+ } else if (s.kind === "upgrade") {
57
+ completed = position.currentEquipId !== s.equipId;
58
+ }
59
+ if (!completed) break;
60
+ done += 1;
61
+ }
62
+ return done;
63
+ }
64
+
65
+ function splitStepViews(snapshotSteps, resultSteps, completedCount) {
66
+ const fullSteps = Array.isArray(snapshotSteps) ? snapshotSteps : [];
67
+ const done = Math.max(0, toInt(completedCount, 0));
68
+ const completedSteps = fullSteps.slice(0, Math.min(done, fullSteps.length));
69
+ const remainingSteps =
70
+ Array.isArray(resultSteps) && resultSteps.length
71
+ ? resultSteps
72
+ : fullSteps.slice(Math.min(done, fullSteps.length));
73
+ return { fullSteps, completedSteps, remainingSteps };
74
+ }
75
+
76
+ function getAllowedStartEquipIdsAction({ targetEquipId, pathByPair }) {
77
+ const t = String(toInt(targetEquipId, 0));
78
+ if (!t || t === "0") return new Set();
79
+
80
+ const set = new Set([t]);
81
+ for (const k of Object.keys(pathByPair || {})) {
82
+ const parts = k.split("|");
83
+ if (parts.length !== 2) continue;
84
+ if (String(parts[1]) !== t) continue;
85
+
86
+ const arr = pathByPair[k] || [];
87
+ for (const id of arr) set.add(String(toInt(id, 0)));
88
+ set.add(String(toInt(parts[0], 0)));
89
+ }
90
+ return set;
91
+ }
92
+
93
+ function buildStartOptionsAction({
94
+ state,
95
+ plans,
96
+ targetEquipId,
97
+ equipOwnerShipIndex,
98
+ pathByPair,
99
+ getEquipSortno,
100
+ getEquipName,
101
+ getPathDistance,
102
+ cache,
103
+ getEquipOwnerShipByApiId,
104
+ }) {
105
+ const equips = (state && state.info && state.info.equips) || {};
106
+ const masterEquipsById = (state && state.const && state.const.$equips) || {};
107
+ const allowedSlotitemSet = getAllowedStartEquipIdsAction({ targetEquipId, pathByPair });
108
+ const usedApiIdSet = new Set((plans || []).map((p) => String(p.startApiId)));
109
+ const plansKey = (plans || []).map((p) => String(p.startApiId)).sort().join("|");
110
+ const targetSortno = getEquipSortno(masterEquipsById, targetEquipId);
111
+
112
+ if (
113
+ cache &&
114
+ cache.targetEquipId === String(targetEquipId) &&
115
+ cache.equipsRef === equips &&
116
+ cache.plansKey === plansKey &&
117
+ cache.ownerMapRef === equipOwnerShipIndex
118
+ ) {
119
+ return { options: cache.options || [], cache };
120
+ }
121
+
122
+ const opts = [];
123
+ for (const apiId of Object.keys(equips)) {
124
+ const eq = equips[apiId];
125
+ if (!eq) continue;
126
+ const slotitemId = toInt(eq.api_slotitem_id, 0);
127
+ const level = toInt(eq.api_level, 0);
128
+ const inPath = allowedSlotitemSet.has(String(slotitemId));
129
+ const used = usedApiIdSet.has(String(apiId));
130
+ const owner = getEquipOwnerShipByApiId(equipOwnerShipIndex, apiId);
131
+ const sameSortno = targetSortno > 0 && getEquipSortno(masterEquipsById, slotitemId) === targetSortno;
132
+ const pathDistance = getPathDistance(pathByPair, slotitemId, targetEquipId);
133
+ const isLocked = toInt(eq.api_locked, 0) === 1;
134
+ opts.push({
135
+ apiId: String(apiId),
136
+ slotitemId,
137
+ level,
138
+ name: getEquipName(masterEquipsById, slotitemId),
139
+ ownerShipName: owner && owner.shipName ? owner.shipName : "",
140
+ sameSortno,
141
+ pathDistance,
142
+ isLocked,
143
+ disabled: !inPath || used,
144
+ disabledReason: !inPath ? "不在目标路径中" : used ? "该起点装备已创建计划" : "",
145
+ });
146
+ }
147
+
148
+ opts.sort((a, b) => {
149
+ if (a.disabled !== b.disabled) return a.disabled ? 1 : -1;
150
+ if (a.sameSortno !== b.sameSortno) return a.sameSortno ? -1 : 1;
151
+ if (a.pathDistance !== b.pathDistance) return a.pathDistance - b.pathDistance;
152
+ if (a.isLocked !== b.isLocked) return a.isLocked ? -1 : 1;
153
+ const sa = toInt(a.slotitemId, 0);
154
+ const sb = toInt(b.slotitemId, 0);
155
+ if (sa !== sb) return sa - sb;
156
+ return toInt(a.apiId, 0) - toInt(b.apiId, 0);
157
+ });
158
+
159
+ const nextCache = {
160
+ targetEquipId: String(targetEquipId),
161
+ equipsRef: equips,
162
+ plansKey,
163
+ ownerMapRef: equipOwnerShipIndex,
164
+ options: opts,
165
+ };
166
+ return { options: opts, cache: nextCache };
167
+ }
168
+
169
+ function refreshPlansAction({
170
+ state,
171
+ plans,
172
+ detectCurrentPosition,
173
+ runPlanForInput,
174
+ upsertLastResult,
175
+ arrangementIndex,
176
+ weekdayKey,
177
+ masterShipsRaw,
178
+ planOwnerShipNameByPlanId,
179
+ }) {
180
+ if (!state || !state.const || !state.info) {
181
+ return { plans: plans || [], planResults: {}, error: "Redux state unavailable." };
182
+ }
183
+
184
+ const { shipBySortno, shipByApiId } = getShipIndexes(masterShipsRaw || {});
185
+ const planResults = {};
186
+ for (const plan of plans || []) {
187
+ const position = detectCurrentPosition({ state, plan });
188
+ const snapshotSteps = Array.isArray(plan.snapshotSteps) ? plan.snapshotSteps : [];
189
+ const completedCount = computeCompletedCount(snapshotSteps, position);
190
+ const stepViews = splitStepViews(snapshotSteps, null, completedCount);
191
+ const nextStep = stepViews.remainingSteps.length > 0 ? stepViews.remainingSteps[0] : null;
192
+
193
+ if (!position.isValid) {
194
+ planResults[plan.id] = {
195
+ status: "invalid",
196
+ error: position.reason,
197
+ position,
198
+ result: null,
199
+ completedCount,
200
+ fullSteps: stepViews.fullSteps,
201
+ completedSteps: stepViews.completedSteps,
202
+ remainingSteps: stepViews.remainingSteps,
203
+ nextStep,
204
+ todaySecretary: "不可",
205
+ ownerShipName: "",
206
+ };
207
+ continue;
208
+ }
209
+
210
+ const result = runPlanForInput({
211
+ state,
212
+ apiId: plan.startApiId,
213
+ targetEquipId: plan.targetEquipId,
214
+ targetLevel: plan.targetLevel,
215
+ });
216
+
217
+ const resultSteps = result && result.ok ? result.steps || [] : null;
218
+ const finalStepViews = splitStepViews(snapshotSteps, resultSteps, completedCount);
219
+ const finalNextStep = finalStepViews.remainingSteps.length > 0 ? finalStepViews.remainingSteps[0] : null;
220
+
221
+ if (result && result.ok) upsertLastResult(plan.id, result);
222
+ const todaySecretary =
223
+ finalStepViews.remainingSteps.length === 0
224
+ ? "改修已完成"
225
+ : getSecretaryTodayForStep(finalNextStep, weekdayKey, arrangementIndex, shipBySortno, shipByApiId);
226
+
227
+ planResults[plan.id] = {
228
+ status: result && result.ok ? "ok" : "error",
229
+ error: result && result.ok ? null : result && result.error ? result.error : "compute failed",
230
+ position,
231
+ result: result && result.ok ? result : null,
232
+ completedCount,
233
+ fullSteps: finalStepViews.fullSteps,
234
+ completedSteps: finalStepViews.completedSteps,
235
+ remainingSteps: finalStepViews.remainingSteps,
236
+ nextStep: finalNextStep,
237
+ todaySecretary,
238
+ ownerShipName: planOwnerShipNameByPlanId[String(plan.id)] || "",
239
+ };
240
+ }
241
+ return { plans: plans || [], planResults, error: null };
242
+ }
243
+
244
+ function refreshPlanByIdAction({
245
+ state,
246
+ targetPlan,
247
+ detectCurrentPosition,
248
+ runPlanForInput,
249
+ upsertLastResult,
250
+ arrangementIndex,
251
+ weekdayKey,
252
+ masterShipsRaw,
253
+ planOwnerShipNameByPlanId,
254
+ }) {
255
+ if (!state || !state.const || !state.info || !targetPlan) return null;
256
+
257
+ const { shipBySortno, shipByApiId } = getShipIndexes(masterShipsRaw || {});
258
+ const position = detectCurrentPosition({ state, plan: targetPlan });
259
+ const snapshotSteps = Array.isArray(targetPlan.snapshotSteps) ? targetPlan.snapshotSteps : [];
260
+ const completedCount = computeCompletedCount(snapshotSteps, position);
261
+ const stepViews = splitStepViews(snapshotSteps, null, completedCount);
262
+ const nextStep = stepViews.remainingSteps.length > 0 ? stepViews.remainingSteps[0] : null;
263
+
264
+ if (!position.isValid) {
265
+ return {
266
+ status: "invalid",
267
+ error: position.reason,
268
+ position,
269
+ result: null,
270
+ completedCount,
271
+ fullSteps: stepViews.fullSteps,
272
+ completedSteps: stepViews.completedSteps,
273
+ remainingSteps: stepViews.remainingSteps,
274
+ nextStep,
275
+ todaySecretary: "不可",
276
+ ownerShipName: "",
277
+ };
278
+ }
279
+
280
+ const result = runPlanForInput({
281
+ state,
282
+ apiId: targetPlan.startApiId,
283
+ targetEquipId: targetPlan.targetEquipId,
284
+ targetLevel: targetPlan.targetLevel,
285
+ });
286
+ const resultSteps = result && result.ok ? result.steps || [] : null;
287
+ const finalStepViews = splitStepViews(snapshotSteps, resultSteps, completedCount);
288
+ const finalNextStep = finalStepViews.remainingSteps.length > 0 ? finalStepViews.remainingSteps[0] : null;
289
+ if (result && result.ok) upsertLastResult(targetPlan.id, result);
290
+ return {
291
+ status: result && result.ok ? "ok" : "error",
292
+ error: result && result.ok ? null : result && result.error ? result.error : "compute failed",
293
+ position,
294
+ result: result && result.ok ? result : null,
295
+ completedCount,
296
+ fullSteps: finalStepViews.fullSteps,
297
+ completedSteps: finalStepViews.completedSteps,
298
+ remainingSteps: finalStepViews.remainingSteps,
299
+ nextStep: finalNextStep,
300
+ todaySecretary:
301
+ finalStepViews.remainingSteps.length === 0
302
+ ? "改修已完成"
303
+ : getSecretaryTodayForStep(finalNextStep, weekdayKey, arrangementIndex, shipBySortno, shipByApiId),
304
+ ownerShipName: planOwnerShipNameByPlanId[String(targetPlan.id)] || "",
305
+ };
306
+ }
307
+
308
+ function submitCreateAction({
309
+ state,
310
+ createState,
311
+ priorities,
312
+ isTargetImprovable,
313
+ pathByPair,
314
+ getPlayerEquipByApiId,
315
+ loadPlans,
316
+ runPlanForInput,
317
+ createPlan,
318
+ getEquipName,
319
+ }) {
320
+ if (!state || !state.const || !state.info) {
321
+ return { ok: false, error: "Redux state unavailable." };
322
+ }
323
+
324
+ const c = createState || {};
325
+ const targetEquipId = String(c.targetEquipId || "").trim();
326
+ const startApiId = String(c.startApiId || "").trim();
327
+ const priority = priorities.includes(c.priority) ? c.priority : "P0";
328
+ const improvable = isTargetImprovable(targetEquipId);
329
+ const targetLevel = improvable ? String(c.targetLevel || "0") : "0";
330
+
331
+ if (!targetEquipId || !startApiId) {
332
+ return { ok: false, error: "请完整选择目标装备与起点装备。" };
333
+ }
334
+
335
+ const allowed = getAllowedStartEquipIdsAction({ targetEquipId, pathByPair });
336
+ const startEquip = getPlayerEquipByApiId(state, startApiId);
337
+ if (!startEquip) {
338
+ return { ok: false, error: `找不到起点装备 api_id=${startApiId}` };
339
+ }
340
+ if (!allowed.has(String(toInt(startEquip.api_slotitem_id, 0)))) {
341
+ return { ok: false, error: "起点装备不在目标路径中。" };
342
+ }
343
+
344
+ const plans = loadPlans();
345
+ if (plans.some((p) => String(p.startApiId) === startApiId)) {
346
+ return { ok: false, error: "该起点装备已创建计划,不可重复创建。" };
347
+ }
348
+
349
+ const preview = runPlanForInput({
350
+ state,
351
+ apiId: startApiId,
352
+ targetEquipId,
353
+ targetLevel,
354
+ });
355
+ if (!preview || !preview.ok) {
356
+ return { ok: false, error: preview && preview.error ? preview.error : "创建前计算失败。" };
357
+ }
358
+
359
+ createPlan({
360
+ startApiId,
361
+ targetEquipId,
362
+ targetLevel,
363
+ priority,
364
+ note: String(c.note || ""),
365
+ startSnapshot: {
366
+ equipId: String(toInt(startEquip.api_slotitem_id, 0)),
367
+ level: String(toInt(startEquip.api_level, 0)),
368
+ name: getEquipName(state.const.$equips || {}, toInt(startEquip.api_slotitem_id, 0)),
369
+ },
370
+ snapshotSteps: preview.steps || [],
371
+ });
372
+
373
+ return { ok: true };
374
+ }
375
+
376
+ function updatePlanPriorityAction({
377
+ planId,
378
+ priority,
379
+ priorities,
380
+ loadPlans,
381
+ updatePlanPriority,
382
+ }) {
383
+ const pid = String(planId || "").trim();
384
+ if (!pid) return { ok: false, error: "planId is required." };
385
+ const plans = typeof loadPlans === "function" ? loadPlans() : [];
386
+ const target = (plans || []).find((p) => String(p.id) === pid);
387
+ if (!target) return { ok: false, error: "计划不存在。" };
388
+
389
+ const allowed = Array.isArray(priorities) ? priorities : [];
390
+ const nextPriority = allowed.includes(priority) ? priority : "P0";
391
+ const updated = typeof updatePlanPriority === "function" ? updatePlanPriority(pid, nextPriority) : null;
392
+ if (!updated) return { ok: false, error: "更新优先级失败。" };
393
+ return { ok: true, plan: updated };
394
+ }
395
+
396
+ function updatePlanTargetLevelAction({
397
+ state,
398
+ planId,
399
+ targetLevel,
400
+ isTargetImprovable,
401
+ loadPlans,
402
+ runPlanForInput,
403
+ runPlanForSnapshotInput,
404
+ updatePlanTargetLevel,
405
+ getPlayerEquipByApiId,
406
+ getEquipSortno,
407
+ }) {
408
+ if (!state || !state.const || !state.info) {
409
+ return { ok: false, error: "Redux state unavailable." };
410
+ }
411
+
412
+ const pid = String(planId || "").trim();
413
+ if (!pid) return { ok: false, error: "planId is required." };
414
+
415
+ const plans = typeof loadPlans === "function" ? loadPlans() : [];
416
+ const target = (plans || []).find((p) => String(p.id) === pid);
417
+ if (!target) return { ok: false, error: "计划不存在。" };
418
+
419
+ const improvable = typeof isTargetImprovable === "function" ? isTargetImprovable(target.targetEquipId) : true;
420
+ const lv = Math.max(0, Math.min(10, toInt(targetLevel, 0)));
421
+ const normalizedTargetLevel = improvable ? String(lv) : "0";
422
+
423
+ const playerEquip =
424
+ typeof getPlayerEquipByApiId === "function" ? getPlayerEquipByApiId(state, target.startApiId) : null;
425
+ if (playerEquip && typeof getEquipSortno === "function") {
426
+ const startSortno = getEquipSortno((state.const && state.const.$equips) || {}, playerEquip.api_slotitem_id);
427
+ const targetSortno = getEquipSortno((state.const && state.const.$equips) || {}, target.targetEquipId);
428
+ const currentLevel = Math.max(0, toInt(playerEquip.api_level, 0));
429
+ if (startSortno > 0 && startSortno === targetSortno && toInt(normalizedTargetLevel, 0) < currentLevel) {
430
+ return {
431
+ ok: false,
432
+ error: `目标星级不可低于起点装备当前星级(当前 +${currentLevel})。`,
433
+ };
434
+ }
435
+ }
436
+
437
+ let fullBaselinePreview = null;
438
+ const snap = target.startSnapshot && typeof target.startSnapshot === "object" ? target.startSnapshot : null;
439
+ if (
440
+ snap &&
441
+ runPlanForSnapshotInput &&
442
+ typeof runPlanForSnapshotInput === "function" &&
443
+ toInt(snap.equipId, 0) > 0
444
+ ) {
445
+ fullBaselinePreview = runPlanForSnapshotInput({
446
+ state,
447
+ apiId: target.startApiId,
448
+ startEquipId: String(snap.equipId),
449
+ startLevel: String(snap.level == null ? "0" : snap.level),
450
+ targetEquipId: target.targetEquipId,
451
+ targetLevel: normalizedTargetLevel,
452
+ });
453
+ } else {
454
+ fullBaselinePreview = runPlanForInput({
455
+ state,
456
+ apiId: target.startApiId,
457
+ targetEquipId: target.targetEquipId,
458
+ targetLevel: normalizedTargetLevel,
459
+ });
460
+ }
461
+ if (!fullBaselinePreview || !fullBaselinePreview.ok) {
462
+ return {
463
+ ok: false,
464
+ error:
465
+ fullBaselinePreview && fullBaselinePreview.error ? fullBaselinePreview.error : "更新目标星数前计算失败。",
466
+ };
467
+ }
468
+
469
+ const updated =
470
+ typeof updatePlanTargetLevel === "function"
471
+ ? updatePlanTargetLevel(target.id, normalizedTargetLevel, fullBaselinePreview.steps || [])
472
+ : null;
473
+ if (!updated) return { ok: false, error: "更新目标星数失败。" };
474
+ return { ok: true, plan: updated, preview: fullBaselinePreview };
475
+ }
476
+
477
+ module.exports = {
478
+ getAllowedStartEquipIdsAction,
479
+ buildStartOptionsAction,
480
+ refreshPlansAction,
481
+ refreshPlanByIdAction,
482
+ submitCreateAction,
483
+ updatePlanPriorityAction,
484
+ updatePlanTargetLevelAction,
485
+ };