poi-plugin-leveling-plan 0.0.4 → 0.0.6

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/assets/main.css CHANGED
@@ -178,6 +178,71 @@
178
178
  position: relative;
179
179
  }
180
180
 
181
+ #leveling-plan .map-selector-trigger {
182
+ display: flex;
183
+ align-items: center;
184
+ gap: 4px;
185
+ min-height: 32px;
186
+ padding: 2px 4px;
187
+ border: 1px solid #ccc;
188
+ border-radius: 3px;
189
+ cursor: pointer;
190
+ background: #fff;
191
+ }
192
+
193
+ #leveling-plan .map-selector-trigger:hover {
194
+ border-color: #137cbd;
195
+ }
196
+
197
+ .bp5-dark #leveling-plan .map-selector-trigger {
198
+ background: #30404d;
199
+ border-color: #5f6b7c;
200
+ }
201
+
202
+ .bp5-dark #leveling-plan .map-selector-trigger:hover {
203
+ border-color: #137cbd;
204
+ }
205
+
206
+ #leveling-plan .map-selector-trigger-tags {
207
+ display: flex;
208
+ flex-wrap: wrap;
209
+ gap: 4px;
210
+ flex: 1;
211
+ }
212
+
213
+ #leveling-plan .map-selector-trigger-placeholder {
214
+ flex: 1;
215
+ padding: 0 4px;
216
+ color: #aaa;
217
+ font-size: 13px;
218
+ }
219
+
220
+ #leveling-plan .map-selector-popover-header {
221
+ display: flex;
222
+ align-items: center;
223
+ justify-content: space-between;
224
+ padding: 6px 8px;
225
+ border-bottom: 1px solid #e5e5e5;
226
+ }
227
+
228
+ .bp5-dark #leveling-plan .map-selector-popover-header {
229
+ border-bottom-color: #404854;
230
+ }
231
+
232
+ #leveling-plan .map-selector-popover-title {
233
+ font-size: 13px;
234
+ font-weight: 600;
235
+ color: #333;
236
+ }
237
+
238
+ .bp5-dark #leveling-plan .map-selector-popover-title {
239
+ color: #ddd;
240
+ }
241
+
242
+ #leveling-plan .map-selector-popover-search {
243
+ padding: 4px 8px;
244
+ }
245
+
181
246
  #leveling-plan .map-selector-dropdown {
182
247
  max-height: 200px;
183
248
  overflow-y: auto;
@@ -539,3 +604,10 @@
539
604
  object-fit: contain;
540
605
  vertical-align: middle;
541
606
  }
607
+
608
+ /* 养殖计划行分隔线 */
609
+ #leveling-plan .farming-divider {
610
+ height: 0;
611
+ border-top: 1px solid rgba(128, 128, 128, 0.2);
612
+ margin: 2px 0;
613
+ }
package/i18n/en-US.json CHANGED
@@ -52,5 +52,31 @@
52
52
  "Sample Count": "Sample Count",
53
53
  "Currently Used": "Currently Used",
54
54
  "Samples Insufficient": "Samples Insufficient",
55
- "No personal data": "No personal data"
55
+ "No personal data": "No personal data",
56
+ "Farming": "Farming",
57
+ "Add Farming Plan": "Add Farming Plan",
58
+ "Edit Farming Plan": "Edit Farming Plan",
59
+ "Ship Type": "Ship Type",
60
+ "No farming plans yet": "No farming plans yet",
61
+ "Below target": "Below target",
62
+ "ships": "ships",
63
+ "All ships have reached the target level": "All ships have reached the target level",
64
+ "By Ship Name": "By Ship Name",
65
+ "By Equipment": "By Equipment",
66
+ "Equipment-Ship Data Sync": "Equipment Data Sync",
67
+ "Last sync": "Last sync",
68
+ "Equipment entries": "Equipment entries",
69
+ "Ship entries": "Ship entries",
70
+ "Sync Now": "Sync Now",
71
+ "Syncing...": "Syncing...",
72
+ "Already up to date": "Already up to date",
73
+ "Sync completed": "Sync completed",
74
+ "Sync failed": "Sync failed",
75
+ "No data yet": "No data yet",
76
+ "Apply All": "Apply All",
77
+ "Search equipment...": "Search equipment...",
78
+ "No equipment data. Try syncing in Settings.": "No equipment data. Try syncing in Settings.",
79
+ "No equipment matches filter.": "No equipment matches filter.",
80
+ "Total": "Total",
81
+ "EXP": "EXP"
56
82
  }
package/i18n/ja-JP.json CHANGED
@@ -52,5 +52,31 @@
52
52
  "Sample Count": "サンプル数",
53
53
  "Currently Used": "現在使用中",
54
54
  "Samples Insufficient": "サンプル不足",
55
- "No personal data": "個人データなし"
55
+ "No personal data": "個人データなし",
56
+ "Farming": "養殖",
57
+ "Add Farming Plan": "養殖計画を追加",
58
+ "Edit Farming Plan": "養殖計画を編集",
59
+ "Ship Type": "艦種",
60
+ "No farming plans yet": "養殖計画がありません",
61
+ "Below target": "未達成",
62
+ "ships": "隻",
63
+ "All ships have reached the target level": "全隻目標レベル達成",
64
+ "By Ship Name": "艦娘名で選ぶ",
65
+ "By Equipment": "装備で選ぶ",
66
+ "Equipment-Ship Data Sync": "装備データ同期",
67
+ "Last sync": "最終同期",
68
+ "Equipment entries": "装備エントリ",
69
+ "Ship entries": "艦娘エントリ",
70
+ "Sync Now": "今すぐ同期",
71
+ "Syncing...": "同期中...",
72
+ "Already up to date": "最新です",
73
+ "Sync completed": "同期完了",
74
+ "Sync failed": "同期失敗",
75
+ "No data yet": "データなし",
76
+ "Apply All": "全て適用",
77
+ "Search equipment...": "装備を検索...",
78
+ "No equipment data. Try syncing in Settings.": "装備データがありません。設定で同期してください",
79
+ "No equipment matches filter.": "一致する装備がありません",
80
+ "Total": "合計",
81
+ "EXP": "経験値"
56
82
  }
package/i18n/zh-CN.json CHANGED
@@ -52,5 +52,31 @@
52
52
  "Sample Count": "样本数",
53
53
  "Currently Used": "当前使用",
54
54
  "Samples Insufficient": "样本不足",
55
- "No personal data": "无个人数据"
55
+ "No personal data": "无个人数据",
56
+ "Farming": "养殖",
57
+ "Add Farming Plan": "添加养殖计划",
58
+ "Edit Farming Plan": "编辑养殖计划",
59
+ "Ship Type": "船型",
60
+ "No farming plans yet": "暂无养殖计划",
61
+ "Below target": "未达标",
62
+ "ships": "艘",
63
+ "All ships have reached the target level": "所有船只已达标",
64
+ "By Ship Name": "按舰娘名",
65
+ "By Equipment": "按装备",
66
+ "Equipment-Ship Data Sync": "装备数据同步",
67
+ "Last sync": "上次同步",
68
+ "Equipment entries": "装备条目",
69
+ "Ship entries": "舰娘条目",
70
+ "Sync Now": "立即同步",
71
+ "Syncing...": "同步中...",
72
+ "Already up to date": "已是最新",
73
+ "Sync completed": "同步完成",
74
+ "Sync failed": "同步失败",
75
+ "No data yet": "暂无数据",
76
+ "Apply All": "全部应用",
77
+ "Search equipment...": "搜索装备...",
78
+ "No equipment data. Try syncing in Settings.": "暂无装备数据,请在设置中同步",
79
+ "No equipment matches filter.": "无匹配装备",
80
+ "Total": "总计",
81
+ "EXP": "经验"
56
82
  }
package/i18n/zh-TW.json CHANGED
@@ -52,5 +52,31 @@
52
52
  "Sample Count": "樣本數",
53
53
  "Currently Used": "當前使用",
54
54
  "Samples Insufficient": "樣本不足",
55
- "No personal data": "無個人數據"
55
+ "No personal data": "無個人數據",
56
+ "Farming": "養殖",
57
+ "Add Farming Plan": "新增養殖計劃",
58
+ "Edit Farming Plan": "編輯養殖計劃",
59
+ "Ship Type": "艦種",
60
+ "No farming plans yet": "暫無養殖計劃",
61
+ "Below target": "未達標",
62
+ "ships": "艘",
63
+ "All ships have reached the target level": "所有船隻已達標",
64
+ "By Ship Name": "按艦娘名",
65
+ "By Equipment": "按裝備",
66
+ "Equipment-Ship Data Sync": "裝備數據同步",
67
+ "Last sync": "上次同步",
68
+ "Equipment entries": "裝備條目",
69
+ "Ship entries": "艦娘條目",
70
+ "Sync Now": "立即同步",
71
+ "Syncing...": "同步中...",
72
+ "Already up to date": "已是最新",
73
+ "Sync completed": "同步完成",
74
+ "Sync failed": "同步失敗",
75
+ "No data yet": "暫無數據",
76
+ "Apply All": "全部應用",
77
+ "Search equipment...": "搜尋裝備...",
78
+ "No equipment data. Try syncing in Settings.": "暫無裝備數據,請在設定中同步",
79
+ "No equipment matches filter.": "無匹配裝備",
80
+ "Total": "總計",
81
+ "EXP": "經驗"
56
82
  }
package/index.js CHANGED
@@ -66,4 +66,31 @@ if (typeof setImmediate === 'function') {
66
66
  console.error('[LevelingPlan] Failed to load battle observer:', error);
67
67
  }
68
68
  }, 0);
69
+ } // Initialize equip sync in background
70
+
71
+
72
+ if (typeof setImmediate === 'function') {
73
+ setImmediate(() => {
74
+ try {
75
+ const {
76
+ initEquipSync
77
+ } = require('./services/equip-sync-service');
78
+
79
+ initEquipSync();
80
+ } catch (error) {
81
+ console.error('[LevelingPlan] Failed to init equip sync:', error);
82
+ }
83
+ });
84
+ } else {
85
+ setTimeout(() => {
86
+ try {
87
+ const {
88
+ initEquipSync
89
+ } = require('./services/equip-sync-service');
90
+
91
+ initEquipSync();
92
+ } catch (error) {
93
+ console.error('[LevelingPlan] Failed to init equip sync:', error);
94
+ }
95
+ }, 0);
69
96
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poi-plugin-leveling-plan",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Leveling plan plugin for poi",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.initEquipSync = initEquipSync;
5
+ exports.manualSync = manualSync;
6
+ const PAGES_BASE = 'https://yuki.github.io/poi-equip-ships-data';
7
+ const CONFIG_KEY = 'plugin.poi-plugin-leveling-plan';
8
+ const META_KEY = `${CONFIG_KEY}.equipSyncMeta`;
9
+ const DATA_KEY = `${CONFIG_KEY}.equipShipsData`;
10
+
11
+ async function fetchJSON(url) {
12
+ const resp = await fetch(url, {
13
+ cache: 'no-cache',
14
+ headers: {
15
+ Accept: 'application/json'
16
+ }
17
+ });
18
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
19
+ return resp.json();
20
+ }
21
+
22
+ async function initEquipSync() {
23
+ try {
24
+ const meta = await fetchJSON(`${PAGES_BASE}/index.json`);
25
+ if (!meta || !meta.updated_at) return;
26
+ const cachedMeta = window.config.get(META_KEY, {});
27
+
28
+ if (meta.updated_at === cachedMeta.updated_at) {
29
+ return;
30
+ }
31
+
32
+ const data = await fetchJSON(`${PAGES_BASE}/initial_equip_ships.json`);
33
+ if (!data || Object.keys(data).length === 0) return;
34
+ window.config.set(DATA_KEY, data);
35
+ window.config.set(META_KEY, meta);
36
+ } catch (e) {// 静默失败,由 equip-provider 的 loadSyncedData 降级
37
+ }
38
+ }
39
+
40
+ async function manualSync() {
41
+ try {
42
+ const meta = await fetchJSON(`${PAGES_BASE}/index.json`);
43
+
44
+ if (!meta || !meta.updated_at) {
45
+ return {
46
+ success: false,
47
+ error: 'Invalid metadata response'
48
+ };
49
+ }
50
+
51
+ const cachedMeta = window.config.get(META_KEY, {});
52
+
53
+ if (meta.updated_at === cachedMeta.updated_at) {
54
+ return {
55
+ success: true,
56
+ meta,
57
+ unchanged: true
58
+ };
59
+ }
60
+
61
+ const data = await fetchJSON(`${PAGES_BASE}/initial_equip_ships.json`);
62
+
63
+ if (!data || Object.keys(data).length === 0) {
64
+ return {
65
+ success: false,
66
+ error: 'Empty data response'
67
+ };
68
+ }
69
+
70
+ window.config.set(DATA_KEY, data);
71
+ window.config.set(META_KEY, meta);
72
+ return {
73
+ success: true,
74
+ meta,
75
+ unchanged: false
76
+ };
77
+ } catch (e) {
78
+ return {
79
+ success: false,
80
+ error: e.message || 'Network error'
81
+ };
82
+ }
83
+ }
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  exports.__esModule = true;
4
- exports.searchOptions = exports.catMap = exports.expClass = exports.bonusExpScaleNonFlagship = exports.bonusExpScaleFlagship = exports.expPercent = exports.expLevel = exports.shipCat = exports.frequentMaps = exports.EXP_BY_POI_DB = exports.MAX_LEVEL = exports.exp = void 0;
4
+ exports.searchOptions = exports.catMap = exports.expClass = exports.bonusExpScaleNonFlagship = exports.bonusExpScaleFlagship = exports.expPercent = exports.expLevel = exports.shipCat = exports.frequentMaps = exports.WORLD_NAMES = exports.EXP_BY_POI_DB = exports.MAX_LEVEL = exports.exp = void 0;
5
5
  // 等级经验表(1-185级)
6
6
  // 数据来源:poi-plugin-exp-calc
7
7
  const exp = {
@@ -229,10 +229,21 @@ const EXP_BY_POI_DB = {
229
229
  64: 236,
230
230
  65: 242,
231
231
  71: 220,
232
- 72: 213 // 常用练级海图
232
+ 72: 213 // 世界名称映射
233
233
 
234
234
  };
235
235
  exports.EXP_BY_POI_DB = EXP_BY_POI_DB;
236
+ const WORLD_NAMES = {
237
+ 1: '鎮守府海域',
238
+ 2: '南西諸島海域',
239
+ 3: '北方海域',
240
+ 4: '西方海域',
241
+ 5: '南方海域',
242
+ 6: '中部海域',
243
+ 7: '南西海域' // 常用练级海图
244
+
245
+ };
246
+ exports.WORLD_NAMES = WORLD_NAMES;
236
247
  const frequentMaps = [15, 21, 22, 42, 52, 53, 71]; // 舰种分类
237
248
 
238
249
  exports.frequentMaps = frequentMaps;
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.loadSyncedData = loadSyncedData;
5
+ exports.getFarmingMap = getFarmingMap;
6
+ exports.composeEquipmentList = composeEquipmentList;
7
+
8
+ var _initial_equip_ships = _interopRequireDefault(require("../assets/initial_equip_ships.json"));
9
+
10
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
11
+
12
+ let cachedFarmingMap = null;
13
+ let cached$Ships = null;
14
+ let cachedDataVersion = null;
15
+
16
+ const getDataVersion = () => {
17
+ try {
18
+ const meta = window.config.get('plugin.poi-plugin-leveling-plan.equipSyncMeta', {});
19
+ return meta.updated_at || 'bundled';
20
+ } catch (e) {
21
+ return 'bundled';
22
+ }
23
+ };
24
+
25
+ let cachedParentMap = {};
26
+
27
+ const findRoot = (id, parentMap) => {
28
+ let curr = id;
29
+ let steps = 0;
30
+
31
+ while (parentMap[curr] && steps < 20) {
32
+ curr = parentMap[curr];
33
+ steps++;
34
+ }
35
+
36
+ return curr;
37
+ };
38
+
39
+ function loadSyncedData() {
40
+ try {
41
+ const data = window.config.get('plugin.poi-plugin-leveling-plan.equipShipsData', null);
42
+
43
+ if (data && typeof data === 'object' && Object.keys(data).length > 0) {
44
+ return data;
45
+ }
46
+ } catch (e) {// ignore
47
+ }
48
+
49
+ return null;
50
+ }
51
+
52
+ function getFarmingMap($ships) {
53
+ if (!$ships || Object.keys($ships).length === 0) return {};
54
+ if (cachedFarmingMap && cached$Ships === $ships && cachedDataVersion === getDataVersion()) return cachedFarmingMap;
55
+ const parentMap = {};
56
+ const nameToId = {};
57
+ Object.values($ships).forEach(s => {
58
+ const afterId = parseInt(s.api_aftershipid, 10);
59
+
60
+ if (afterId > 0 && !parentMap[afterId]) {
61
+ parentMap[afterId] = s.api_id;
62
+ }
63
+
64
+ if (s.api_name) {
65
+ nameToId[s.api_name] = s.api_id;
66
+ }
67
+ });
68
+ cachedParentMap = parentMap;
69
+
70
+ const activeData = loadSyncedData() || _initial_equip_ships.default;
71
+
72
+ const map = {};
73
+ Object.entries(activeData).forEach(([eqIdStr, providers]) => {
74
+ const equipId = parseInt(eqIdStr, 10);
75
+ if (!Array.isArray(providers)) return;
76
+ providers.forEach(({
77
+ name,
78
+ level
79
+ }) => {
80
+ const shipId = nameToId[name];
81
+ if (!shipId) return;
82
+ const rootId = findRoot(shipId, parentMap);
83
+
84
+ if (!map[rootId]) {
85
+ map[rootId] = {
86
+ baseId: rootId,
87
+ provides: []
88
+ };
89
+ }
90
+
91
+ const exists = map[rootId].provides.some(p => p.equipId === equipId && p.providerId === shipId && p.level === level);
92
+
93
+ if (!exists) {
94
+ map[rootId].provides.push({
95
+ equipId,
96
+ providerId: shipId,
97
+ level
98
+ });
99
+ }
100
+ });
101
+ });
102
+ cachedFarmingMap = map;
103
+ cached$Ships = $ships;
104
+ cachedDataVersion = getDataVersion();
105
+ return map;
106
+ }
107
+
108
+ function composeEquipmentList(farmingMap, $ships, $equipments, $equipTypes) {
109
+ const equipmentMap = {};
110
+ Object.entries(farmingMap).forEach(([baseShipIdStr, info]) => {
111
+ const baseShipId = parseInt(baseShipIdStr, 10);
112
+ const baseShipMaster = $ships[baseShipId] || {};
113
+ const baseShipName = baseShipMaster.api_name || `Ship#${baseShipId}`;
114
+ info.provides.forEach(p => {
115
+ const equipId = p.equipId;
116
+
117
+ if (!equipmentMap[equipId]) {
118
+ const masterEquip = $equipments[equipId] || {};
119
+ const typeId = masterEquip.api_type && masterEquip.api_type[2] || 0;
120
+ equipmentMap[equipId] = {
121
+ id: equipId,
122
+ name: masterEquip.api_name || `Equip#${equipId}`,
123
+ iconId: masterEquip.api_type && masterEquip.api_type[3] || 0,
124
+ typeName: ($equipTypes[typeId] || {}).api_name || 'Unknown',
125
+ typeId,
126
+ ships: []
127
+ };
128
+ }
129
+
130
+ const providerMaster = $ships[p.providerId] || {};
131
+ const providerName = providerMaster.api_name || `Form#${p.providerId}`;
132
+ equipmentMap[equipId].ships.push({
133
+ shipId: baseShipId,
134
+ shipName: baseShipName,
135
+ providerId: p.providerId,
136
+ providerName,
137
+ level: p.level
138
+ });
139
+ });
140
+ });
141
+ const equipmentList = Object.values(equipmentMap);
142
+ equipmentList.forEach(eq => {
143
+ eq.ships.sort((a, b) => a.level - b.level);
144
+ });
145
+ return equipmentList.sort((a, b) => {
146
+ if (a.typeId !== b.typeId) return a.typeId - b.typeId;
147
+ return a.id - b.id;
148
+ });
149
+ }
@@ -28,21 +28,38 @@ exports.generatePlanId = generatePlanId;
28
28
 
29
29
  const validatePlan = (plan, ship = null) => {
30
30
  const errors = [];
31
+ const isFarming = plan.type === 'farming';
31
32
 
32
- if (!plan.shipId) {
33
- errors.push('shipId is required');
34
- }
35
-
36
- if (!plan.shipMasterId) {
37
- errors.push('shipMasterId is required');
38
- }
33
+ if (isFarming && plan.targets) {
34
+ if (!plan.targets.length) {
35
+ errors.push('at least one ship target is required');
36
+ }
39
37
 
40
- if (!plan.targetLevel || plan.targetLevel < 1 || plan.targetLevel > 185) {
41
- errors.push('targetLevel must be between 1 and 185');
42
- }
38
+ plan.targets.forEach((t, i) => {
39
+ if (!t.shipMasterId) {
40
+ errors.push(`targets[${i}]: shipMasterId is required`);
41
+ }
43
42
 
44
- if (ship && plan.targetLevel <= ship.api_lv) {
45
- errors.push('targetLevel must be greater than current level');
43
+ if (!t.targetLevel || t.targetLevel < 1 || t.targetLevel > 185) {
44
+ errors.push(`targets[${i}]: targetLevel must be between 1 and 185`);
45
+ }
46
+ });
47
+ } else {
48
+ if (!isFarming && !plan.shipId) {
49
+ errors.push('shipId is required');
50
+ }
51
+
52
+ if (!plan.shipMasterId) {
53
+ errors.push('shipMasterId is required');
54
+ }
55
+
56
+ if (!plan.targetLevel || plan.targetLevel < 1 || plan.targetLevel > 185) {
57
+ errors.push('targetLevel must be between 1 and 185');
58
+ }
59
+
60
+ if (!isFarming && ship && plan.targetLevel <= ship.api_lv) {
61
+ errors.push('targetLevel must be greater than current level');
62
+ }
46
63
  }
47
64
 
48
65
  if (!plan.maps || !Array.isArray(plan.maps) || plan.maps.length === 0) {
@@ -171,12 +188,7 @@ const shouldAutoComplete = (plan, ship) => {
171
188
  exports.shouldAutoComplete = shouldAutoComplete;
172
189
 
173
190
  const formatMapName = mapId => {
174
- if (!mapId || typeof mapId !== 'string') return ''; // '53' -> '5-3'
175
-
176
- if (mapId.length === 2) {
177
- return `${mapId[0]}-${mapId[1]}`;
178
- } // '11' -> '1-1'
179
-
191
+ if (!mapId || typeof mapId !== 'string') return '';
180
192
 
181
193
  if (mapId.length === 2) {
182
194
  return `${mapId[0]}-${mapId[1]}`;
@@ -197,21 +209,38 @@ const formatMapName = mapId => {
197
209
 
198
210
  exports.formatMapName = formatMapName;
199
211
 
200
- const createPlan = (shipId, shipMasterId, startLevel, targetLevel, maps, notes = '') => {
212
+ const createPlan = (shipId, shipMasterId, startLevel, targetLevel, maps, notes = '', type = 'normal', targets = null) => {
201
213
  const now = Date.now();
202
- return {
214
+ const base = {
203
215
  id: generatePlanId(),
204
- shipId,
205
- shipMasterId,
206
- startLevel,
207
- targetLevel,
208
216
  maps,
209
217
  notes,
210
- completed: false,
211
- completedAt: null,
218
+ type,
212
219
  createdAt: now,
213
220
  updatedAt: now
214
221
  };
222
+
223
+ if (type === 'farming') {
224
+ if (targets && targets.length > 0) {
225
+ return _extends({}, base, {
226
+ targets
227
+ });
228
+ }
229
+
230
+ return _extends({}, base, {
231
+ shipMasterId,
232
+ targetLevel
233
+ });
234
+ }
235
+
236
+ return _extends({}, base, {
237
+ shipId,
238
+ shipMasterId,
239
+ startLevel,
240
+ targetLevel,
241
+ completed: false,
242
+ completedAt: null
243
+ });
215
244
  };
216
245
  /**
217
246
  * 更新计划对象