poi-plugin-kai-planner 1.0.18 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,10 +6,10 @@
6
6
 
7
7
  - 在`每日改修`页面查看当天可改修装备、基础消耗、秘书舰要求与可进化状态。
8
8
  - 在`每日改修`页面把常看的装备标记为收藏,收藏项会优先排序并长期保存在本地。
9
- - 在`改修心愿清单`页面创建、编辑、刷新和追踪个人改修计划,查看实时完成进度与资源缺口。
9
+ - 在`改修规划`页面创建、编辑、刷新和追踪个人改修计划,查看实时完成进度与资源缺口。
10
10
  - 在`装备获取规划`页面管理想提前准备的装备,查看它们与未完成改修计划之间的待消耗关联。
11
- - 在`Debug`页面查看插件运行时数据与调试信息。
12
- - 静态数据支持远端版本更新;插件默认每 2 天自动检查一次,也可以手动点击“检查更新”。
11
+ - 按需开启 `poiPlugin.enableDebug` 后,在 `Debug` 页面查看运行时数据;默认隐藏。
12
+ - 静态数据在页面挂载时检查:成功后间隔 2 个 UTC 自然日,失败后间隔 15 分钟;可手动“检查更新”。下载有超时和有限重试,成功后页面自动刷新并保留编辑草稿。
13
13
  - 改修规划的待消耗道具会区分 useitem 与 `api_material` 资源类材料,例如高速建造材、高速修复材、开发资材、改修资材。
14
14
 
15
15
  ## 技术文档
package/index.js CHANGED
@@ -7,6 +7,8 @@ const { WindowEnv } = require("views/components/etc/window-env");
7
7
  const { KaiPlannerApp } = require("./src/app/KaiPlannerApp");
8
8
  const { subscribeMaterialResourceUpdates } = require("./src/services/player/materialResourceCounts");
9
9
 
10
+ const { enableDataUpdates, disposeDataUpdates } = require("./src/services/static/version/dataUpdateManager");
11
+
10
12
  let unsubscribeMaterialResourceUpdates = null;
11
13
 
12
14
  function KaiPlannerAppWithEnv() {
@@ -21,27 +23,26 @@ function KaiPlannerAppWithEnv() {
21
23
  });
22
24
  }
23
25
 
24
- module.exports = {
25
- reactClass: KaiPlannerAppWithEnv,
26
- windowMode: true,
27
-
28
- pluginDidLoad() {
29
- if (!unsubscribeMaterialResourceUpdates) {
30
- unsubscribeMaterialResourceUpdates = subscribeMaterialResourceUpdates(
31
- typeof window !== "undefined" ? window : null
32
- );
33
- }
34
- if (typeof window !== "undefined" && window.log) {
35
- window.log("[kai-planner] loaded");
36
- }
37
- },
38
- pluginWillUnload() {
39
- if (unsubscribeMaterialResourceUpdates) {
40
- unsubscribeMaterialResourceUpdates();
41
- unsubscribeMaterialResourceUpdates = null;
42
- }
43
- if (typeof window !== "undefined" && window.log) {
44
- window.log("[kai-planner] unloaded");
45
- }
46
- },
26
+ exports.reactClass = KaiPlannerAppWithEnv;
27
+ exports.windowMode = true;
28
+ exports.pluginDidLoad = function pluginDidLoad() {
29
+ enableDataUpdates();
30
+ if (!unsubscribeMaterialResourceUpdates) {
31
+ unsubscribeMaterialResourceUpdates = subscribeMaterialResourceUpdates(
32
+ typeof window !== "undefined" ? window : null
33
+ );
34
+ }
35
+ if (typeof window !== "undefined" && window.log) {
36
+ window.log("[kai-planner] loaded");
37
+ }
38
+ };
39
+ exports.pluginWillUnload = function pluginWillUnload() {
40
+ disposeDataUpdates();
41
+ if (unsubscribeMaterialResourceUpdates) {
42
+ unsubscribeMaterialResourceUpdates();
43
+ unsubscribeMaterialResourceUpdates = null;
44
+ }
45
+ if (typeof window !== "undefined" && window.log) {
46
+ window.log("[kai-planner] unloaded");
47
+ }
47
48
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poi-plugin-kai-planner",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "main": "index.js",
5
5
  "author": "aulu",
6
6
  "contributors": ["aulu"],
@@ -1,7 +1,8 @@
1
1
  const React = require("react");
2
2
  const { DailyTabV2 } = require("./tabs/daily/DailyTabV2");
3
- const { checkAndUpdateData } = require("../services/static/version/dataUpdateManager");
3
+ const { checkAndUpdateData, getUpdateState, subscribeDataUpdates } = require("../services/static/version/dataUpdateManager");
4
4
  const { readMeta } = require("../services/static/version/versionStore");
5
+ const { ensurePluginWindowVisible } = require("../core/poi/ensurePluginWindowVisible");
5
6
  const {
6
7
  DEFAULT_THEME_TOKENS,
7
8
  readPoiTheme,
@@ -66,7 +67,7 @@ function getUpdateFailureTip(result) {
66
67
  data_source_not_configured: "未配置远端数据源",
67
68
  };
68
69
  const label = labels[reason] || "检查失败";
69
- return result && result.error ? `${label}:${result.error}` : label;
70
+ return reason === "update_cancelled" ? "更新已取消。" : `${label},可点击检查更新重试。`;
70
71
  }
71
72
 
72
73
  class KaiPlannerApp extends React.Component {
@@ -77,7 +78,10 @@ class KaiPlannerApp extends React.Component {
77
78
  wishlistLoadError: null,
78
79
  acquisitionLoadError: null,
79
80
  debugLoadError: null,
80
- checkingUpdate: false,
81
+ checkingUpdate: getUpdateState().checking,
82
+ dataRevision: getUpdateState().dataRevision,
83
+ updateDiagnostic: "",
84
+ copyTip: "",
81
85
  lastUpdateAtText: "-",
82
86
  updateTip: "",
83
87
  materialResourceVersion: 0,
@@ -89,12 +93,17 @@ class KaiPlannerApp extends React.Component {
89
93
  this._unsubscribeTheme = null;
90
94
  this._unsubscribeMaterialResources = null;
91
95
  this._themeWarmupTimer = null;
96
+ this._cancelWindowVisibility = null;
92
97
  this.enableDebug = readEnableDebugFlag();
93
98
  this.handleCheckUpdate = this.handleCheckUpdate.bind(this);
94
99
  this.syncTheme = this.syncTheme.bind(this);
95
100
  }
96
101
 
97
102
  componentDidMount() {
103
+ this._mounted = true;
104
+ this._unsubscribeDataUpdates = subscribeDataUpdates(() => this.syncDataUpdateState());
105
+ this.syncDataUpdateState();
106
+ this._cancelWindowVisibility = ensurePluginWindowVisible(this.props.envWindow, this.props.envCtx);
98
107
  this.syncTheme();
99
108
  this._unsubscribeTheme = subscribePoiTheme(this.props.envWindow, (hostTheme) => {
100
109
  this.setState({ themeTokens: buildPlannerThemeTokens(hostTheme) });
@@ -109,6 +118,10 @@ class KaiPlannerApp extends React.Component {
109
118
  }
110
119
 
111
120
  componentWillUnmount() {
121
+ this._mounted = false;
122
+ if (this._unsubscribeDataUpdates) this._unsubscribeDataUpdates();
123
+ if (this._cancelWindowVisibility) this._cancelWindowVisibility();
124
+ this._cancelWindowVisibility = null;
112
125
  if (typeof this._unsubscribeTheme === "function") this._unsubscribeTheme();
113
126
  if (typeof this._unsubscribeMaterialResources === "function") this._unsubscribeMaterialResources();
114
127
  if (this._themeWarmupTimer != null) {
@@ -133,21 +146,44 @@ class KaiPlannerApp extends React.Component {
133
146
  } catch {}
134
147
  }
135
148
 
149
+ syncDataUpdateState() {
150
+ if (!this._mounted) return;
151
+ const update = getUpdateState();
152
+ const meta = readMeta();
153
+ const result = update.result;
154
+ const failure = result && !result.ok ? result : ((!result || result.skipped) && meta.lastCheckOk === false ? {
155
+ reason: meta.lastCheckReason, error: meta.lastCheckError, diagnostic: meta.lastCheckDiagnostic,
156
+ } : null);
157
+ this.setState({
158
+ checkingUpdate: update.checking,
159
+ dataRevision: update.dataRevision,
160
+ updateDiagnostic: failure ? JSON.stringify({ checkedAt: meta.lastCheckAt, reason: failure.reason, diagnostic: failure.diagnostic, error: failure.error }, null, 2) : "",
161
+ });
162
+ this.syncUpdateMeta();
163
+ }
164
+
165
+ async copyUpdateDiagnostic() {
166
+ try {
167
+ await require("views/services/clipboard").writeClipboardText(this.state.updateDiagnostic);
168
+ if (this._mounted) this.setState({ copyTip: "已复制" });
169
+ } catch {
170
+ if (this._mounted) this.setState({ copyTip: "复制失败,请选中下方详情手动复制。" });
171
+ }
172
+ }
173
+
136
174
  async handleCheckUpdate(force) {
137
- if (this.state.checkingUpdate) return;
138
- this.setState({ checkingUpdate: true, updateTip: force ? "正在检查..." : "" });
175
+ if (!this._mounted) return;
176
+ this.setState({ updateTip: force ? "正在检查..." : "", copyTip: "" });
139
177
  try {
140
178
  const result = await checkAndUpdateData({ force: !!force });
141
- this.syncUpdateMeta();
179
+ if (!this._mounted) return;
142
180
  if (force) {
143
- if (result && result.ok && result.updated) this.setState({ updateTip: `已更新到数据版本 ${result.remoteVersion}` });
144
- else if (result && result.ok && !result.updated) this.setState({ updateTip: "当前已是最新数据。" });
181
+ if (result.ok && result.updated) this.setState({ updateTip: `已更新到数据版本 ${result.remoteVersion}` });
182
+ else if (result.ok) this.setState({ updateTip: "当前已是最新数据。" });
145
183
  else this.setState({ updateTip: getUpdateFailureTip(result) });
146
184
  }
147
185
  } catch (error) {
148
- if (force) this.setState({ updateTip: `检查失败:${String(error && (error.stack || error))}` });
149
- } finally {
150
- this.setState({ checkingUpdate: false });
186
+ if (this._mounted) this.setState({ updateTip: "检查失败,可重试。", updateDiagnostic: String(error.stack || error) });
151
187
  }
152
188
  }
153
189
 
@@ -177,6 +213,7 @@ class KaiPlannerApp extends React.Component {
177
213
  content = React.createElement(DailyTabV2, {
178
214
  envWindow: this.props.envWindow,
179
215
  envCtx: this.props.envCtx,
216
+ dataRevision: this.state.dataRevision,
180
217
  themeTokens,
181
218
  });
182
219
  } else if (tab === "wishlist") {
@@ -198,6 +235,7 @@ class KaiPlannerApp extends React.Component {
198
235
  content = React.createElement(this._WishlistTab, {
199
236
  envWindow: this.props.envWindow,
200
237
  envCtx: this.props.envCtx,
238
+ dataRevision: this.state.dataRevision,
201
239
  materialResourceVersion: this.state.materialResourceVersion,
202
240
  themeTokens,
203
241
  });
@@ -223,6 +261,7 @@ class KaiPlannerApp extends React.Component {
223
261
  content = React.createElement(this._AcquisitionTab, {
224
262
  envWindow: this.props.envWindow,
225
263
  envCtx: this.props.envCtx,
264
+ dataRevision: this.state.dataRevision,
226
265
  themeTokens,
227
266
  });
228
267
  } else {
@@ -247,6 +286,7 @@ class KaiPlannerApp extends React.Component {
247
286
  content = React.createElement(this._DebugTab, {
248
287
  envWindow: this.props.envWindow,
249
288
  envCtx: this.props.envCtx,
289
+ dataRevision: this.state.dataRevision,
250
290
  themeTokens,
251
291
  });
252
292
  } else {
@@ -284,6 +324,13 @@ class KaiPlannerApp extends React.Component {
284
324
  this.state.updateTip ? React.createElement("span", { style: { fontSize: 14, color: themeTokens.textMuted } }, this.state.updateTip) : null
285
325
  )
286
326
  ),
327
+ this.state.updateDiagnostic ? React.createElement("details", { style: { marginBottom: 12 } },
328
+ React.createElement("summary", null, "最近一次更新未完成:查看诊断详情"),
329
+ React.createElement("button", { onClick: () => this.copyUpdateDiagnostic() }, "复制诊断详情"),
330
+ React.createElement("span", null, this.state.copyTip),
331
+ React.createElement("textarea", { readOnly: true, value: this.state.updateDiagnostic,
332
+ style: { width: "100%", height: 140 }, onFocus: (event) => event.target.select() })
333
+ ) : null,
287
334
  content
288
335
  );
289
336
  }
@@ -48,11 +48,7 @@ class AcquisitionTab extends React.Component {
48
48
  constructor(props) {
49
49
  super(props);
50
50
 
51
- this.staticData = loadStaticData();
52
- this.consumeIndexes = buildConsumeIndexes(this.staticData);
53
- this.upgradeIndexes = buildUpgradeIndexes(this.staticData);
54
- this.pathByPair = buildPathIndexes(this.staticData).pathByPair;
55
- this.arrangementIndex = buildArrangementIndex(this.staticData.improvementArrangement).index;
51
+ this.reloadStaticData();
56
52
 
57
53
  this.state = {
58
54
  nowText: formatTokyoText(),
@@ -77,11 +73,26 @@ class AcquisitionTab extends React.Component {
77
73
  this.refresh = this.refresh.bind(this);
78
74
  }
79
75
 
76
+ reloadStaticData() {
77
+ this.staticData = loadStaticData();
78
+ this.consumeIndexes = buildConsumeIndexes(this.staticData);
79
+ this.upgradeIndexes = buildUpgradeIndexes(this.staticData);
80
+ this.pathByPair = buildPathIndexes(this.staticData).pathByPair;
81
+ this.arrangementIndex = buildArrangementIndex(this.staticData.improvementArrangement).index;
82
+ }
83
+
80
84
  componentDidMount() {
81
85
  this._timer = setInterval(() => this.setState({ nowText: formatTokyoText() }), 1000);
82
86
  this.refresh();
83
87
  }
84
88
 
89
+ componentDidUpdate(prevProps) {
90
+ if (prevProps.dataRevision !== this.props.dataRevision) {
91
+ this.reloadStaticData();
92
+ this.refresh();
93
+ }
94
+ }
95
+
85
96
  componentWillUnmount() {
86
97
  if (this._timer) clearInterval(this._timer);
87
98
  }
@@ -185,6 +185,10 @@ class DailyTabV2 extends React.Component {
185
185
  }, 1000);
186
186
  }
187
187
 
188
+ componentDidUpdate(prevProps) {
189
+ if (prevProps.dataRevision !== this.props.dataRevision) this.refresh();
190
+ }
191
+
188
192
  componentWillUnmount() {
189
193
  if (this._timer) clearInterval(this._timer);
190
194
  }
@@ -42,6 +42,7 @@ function buildDebugDiagnostics({
42
42
  lastCheckAt: meta.lastCheckAt || "",
43
43
  lastCheckOk: meta.lastCheckOk == null ? null : !!meta.lastCheckOk,
44
44
  lastCheckReason: meta.lastCheckReason || "",
45
+ lastCheckDiagnostic: meta.lastCheckDiagnostic || null,
45
46
  dataDir: staticInfo.dataDir || "",
46
47
  cachePath: staticInfo.cachePath || "",
47
48
  cacheExists: !!staticInfo.cacheExists,
@@ -121,11 +121,7 @@ class WishlistTabV2 extends React.Component {
121
121
  constructor(props) {
122
122
  super(props);
123
123
 
124
- this.staticData = loadStaticData();
125
- this.consumeIndexes = buildConsumeIndexes(this.staticData);
126
- this.upgradeIndexes = buildUpgradeIndexes(this.staticData);
127
- this.pathByPair = buildPathIndexes(this.staticData).pathByPair;
128
- this.arrangementIndex = buildArrangementIndex(this.staticData.improvementArrangement).index;
124
+ this.reloadStaticData();
129
125
  this._ownerShipIndexCache = { shipsRef: null, map: {} };
130
126
  this._startOptionsCache = { targetEquipId: null, equipsRef: null, plansKey: "", ownerMapRef: null, options: [] };
131
127
 
@@ -167,6 +163,14 @@ class WishlistTabV2 extends React.Component {
167
163
  this.refreshPlanById = this.refreshPlanById.bind(this);
168
164
  }
169
165
 
166
+ reloadStaticData() {
167
+ this.staticData = loadStaticData();
168
+ this.consumeIndexes = buildConsumeIndexes(this.staticData);
169
+ this.upgradeIndexes = buildUpgradeIndexes(this.staticData);
170
+ this.pathByPair = buildPathIndexes(this.staticData).pathByPair;
171
+ this.arrangementIndex = buildArrangementIndex(this.staticData.improvementArrangement).index;
172
+ }
173
+
170
174
  componentDidMount() {
171
175
  this._timer = setInterval(() => this.setState({ nowText: formatTokyoText() }), 1000);
172
176
  this.refreshPlans();
@@ -179,6 +183,12 @@ class WishlistTabV2 extends React.Component {
179
183
  }
180
184
 
181
185
  componentDidUpdate(prevProps) {
186
+ if (prevProps.dataRevision !== this.props.dataRevision) {
187
+ this.reloadStaticData();
188
+ this._startOptionsCache = {};
189
+ this.refreshPlans({ preserveInteraction: true });
190
+ return;
191
+ }
182
192
  if (prevProps.materialResourceVersion === this.props.materialResourceVersion) return;
183
193
  const state = this.getReduxState();
184
194
  if (!state || !state.const || !state.info) return;
@@ -374,7 +384,8 @@ class WishlistTabV2 extends React.Component {
374
384
  setFilterNote(value) {
375
385
  this.setState(this.clearSelectionState({ filterNote: value, info: null }));
376
386
  }
377
- refreshPlans() {
387
+ refreshPlans({ preserveInteraction = false } = {}) {
388
+ const interactionPatch = (patch) => preserveInteraction ? patch : this.clearSelectionState(patch);
378
389
  try {
379
390
  const state = this.getReduxState();
380
391
  const plans = loadPlans();
@@ -461,7 +472,7 @@ class WishlistTabV2 extends React.Component {
461
472
  };
462
473
 
463
474
  this.setState(
464
- this.clearSelectionState({
475
+ interactionPatch({
465
476
  ...buildInitialWishlistHydrationState({
466
477
  plans,
467
478
  initialSnapshot,
@@ -474,7 +485,7 @@ class WishlistTabV2 extends React.Component {
474
485
  }
475
486
  );
476
487
  } catch (error) {
477
- this.setState(this.clearSelectionState({ error: String(error && (error.stack || error)), hydrating: false, hydrationQueue: [] }));
488
+ this.setState(interactionPatch({ error: String(error && (error.stack || error)), hydrating: false, hydrationQueue: [] }));
478
489
  }
479
490
  }
480
491
 
@@ -0,0 +1,54 @@
1
+ const PLUGIN_QUERY = "?poi-plugin-kai-planner";
2
+
3
+ // Portal components execute in poi's main renderer. Never use
4
+ // remote.getCurrentWindow(): that would target the main window.
5
+ function ensurePluginWindowVisible(envWindow, envCtx, options = {}) {
6
+ let cancelled = false;
7
+ let timer = null;
8
+ const schedule = options.schedule || setTimeout;
9
+ const clear = options.clear || clearTimeout;
10
+ const warn = options.warn || ((error) => console.warn("[kai-planner] window visibility fallback failed", error));
11
+ const cancel = () => {
12
+ cancelled = true;
13
+ if (timer !== null) clear(timer);
14
+ timer = null;
15
+ };
16
+
17
+ try {
18
+ const mountPoint = envCtx && envCtx.mountPoint;
19
+ if (!envWindow || envWindow.closed || envWindow.isWindowMode !== true || !mountPoint) return cancel;
20
+ const doc = envWindow.document;
21
+ const url = new URL(envWindow.location.href);
22
+ if (url.protocol !== "file:" || !url.pathname.endsWith("/index-plugin.html") || url.search !== PLUGIN_QUERY) return cancel;
23
+ if (mountPoint.ownerDocument !== doc || !mountPoint.isConnected) return cancel;
24
+
25
+ const remote = options.remote || require("@electron/remote");
26
+ const matches = remote.BrowserWindow.getAllWindows().filter(
27
+ (win) => !win.isDestroyed() && win.webContents.getURL() === url.href
28
+ );
29
+ if (matches.length !== 1) return cancel;
30
+ // Capture this instance now, never search again after a close/reopen.
31
+ const target = matches[0];
32
+ if (target.isVisible() || target.isMinimized()) return cancel;
33
+
34
+ // Run after the mount commit; one check only, no polling or focus call.
35
+ timer = schedule(() => {
36
+ timer = null;
37
+ if (cancelled) return;
38
+ try {
39
+ if (envWindow.closed || target.isDestroyed()) return;
40
+ if (envWindow.document !== doc || envWindow.location.href !== url.href) return;
41
+ if (!mountPoint.isConnected || mountPoint.ownerDocument !== doc || !mountPoint.querySelector(".kai-planner-root")) return;
42
+ if (target.webContents.getURL() !== url.href || target.isVisible() || target.isMinimized()) return;
43
+ target.show();
44
+ } catch (error) {
45
+ warn(error);
46
+ }
47
+ }, 0);
48
+ } catch (error) {
49
+ warn(error);
50
+ }
51
+ return cancel;
52
+ }
53
+
54
+ module.exports = { ensurePluginWindowVisible };
@@ -1,8 +1,8 @@
1
1
  {
2
- "data_version": "2026.04.29",
3
- "run_date": "2026-04-29",
4
- "updated_at": "2026-04-29T00:00:00+08:00",
5
- "previous_snapshot": "2026-04-25",
2
+ "data_version": "2026.08.26",
3
+ "run_date": "2026-08-26",
4
+ "updated_at": "2026-08-26T00:00:00+08:00",
5
+ "previous_snapshot": "2026-07-10",
6
6
  "version_complete": true,
7
7
  "source": {
8
8
  "repo": "yukikuri/akashi-list",
@@ -10,39 +10,56 @@
10
10
  "detail_path": "detail/"
11
11
  },
12
12
  "target_equipment_ids": [
13
+ 2,
13
14
  3,
14
- 10,
15
- 13,
16
- 14,
17
- 15,
18
- 24,
19
15
  27,
20
- 45,
21
- 57,
22
- 88,
23
- 89,
24
- 91,
16
+ 35,
17
+ 39,
18
+ 40,
19
+ 48,
20
+ 49,
21
+ 63,
22
+ 76,
25
23
  106,
26
- 125,
24
+ 122,
25
+ 146,
26
+ 171,
27
+ 174,
28
+ 198,
29
+ 205,
30
+ 226,
27
31
  229,
28
- 278,
29
- 284,
30
- 285,
31
- 286,
32
- 287,
33
- 288,
32
+ 256,
33
+ 266,
34
+ 267,
34
35
  294,
35
- 307,
36
- 313,
36
+ 295,
37
37
  315,
38
- 379,
39
- 382,
40
- 397,
41
- 398,
42
- 553,
43
- 572,
44
- 573,
45
- 574
38
+ 317,
39
+ 362,
40
+ 381,
41
+ 385,
42
+ 419,
43
+ 468,
44
+ 470,
45
+ 471,
46
+ 483,
47
+ 503,
48
+ 529,
49
+ 536,
50
+ 537,
51
+ 541,
52
+ 542,
53
+ 577,
54
+ 578,
55
+ 579,
56
+ 580,
57
+ 582,
58
+ 583,
59
+ 584,
60
+ 585,
61
+ 586,
62
+ 587
46
63
  ],
47
64
  "fetch_report_file": "fetch_report.json",
48
65
  "files": [
@@ -1063,6 +1063,13 @@
1063
1063
  "consume_steel": 0,
1064
1064
  "consume_bauxite": 600
1065
1065
  },
1066
+ {
1067
+ "id": 189,
1068
+ "consume_fuel": 200,
1069
+ "consume_ammo": 190,
1070
+ "consume_steel": 20,
1071
+ "consume_bauxite": 490
1072
+ },
1066
1073
  {
1067
1074
  "id": 190,
1068
1075
  "consume_fuel": 24,
@@ -2078,6 +2085,20 @@
2078
2085
  "consume_steel": 420,
2079
2086
  "consume_bauxite": 0
2080
2087
  },
2088
+ {
2089
+ "id": 428,
2090
+ "consume_fuel": 30,
2091
+ "consume_ammo": 260,
2092
+ "consume_steel": 320,
2093
+ "consume_bauxite": 20
2094
+ },
2095
+ {
2096
+ "id": 429,
2097
+ "consume_fuel": 40,
2098
+ "consume_ammo": 320,
2099
+ "consume_steel": 320,
2100
+ "consume_bauxite": 20
2101
+ },
2081
2102
  {
2082
2103
  "id": 431,
2083
2104
  "consume_fuel": 290,
@@ -2574,5 +2595,12 @@
2574
2595
  "consume_ammo": 20,
2575
2596
  "consume_steel": 170,
2576
2597
  "consume_bauxite": 280
2598
+ },
2599
+ {
2600
+ "id": 575,
2601
+ "consume_fuel": 10,
2602
+ "consume_ammo": 40,
2603
+ "consume_steel": 30,
2604
+ "consume_bauxite": 20
2577
2605
  }
2578
2606
  ]