@lily-cooperative/custom-farm 0.36.0 → 0.37.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.
package/README.md CHANGED
@@ -71,7 +71,9 @@ powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\install.ps1
71
71
  Invoke-RestMethod -Method Post http://127.0.0.1:17788/api/run/restart
72
72
  ```
73
73
 
74
- ## 安装路径
74
+ ## 游戏配置
75
+
76
+ ### 安装路径
75
77
 
76
78
  在 Coopanion 的 **游戏配置 → 安装** 里可以直接看到和修改安装路径:
77
79
 
@@ -80,6 +82,23 @@ Invoke-RestMethod -Method Post http://127.0.0.1:17788/api/run/restart
80
82
  - 存档和自定义配置仍在 `data\home\companion\data\farm\`,不会被搬走
81
83
  - 安装目录里的 `overlay\runtime\` 是运行痕迹,可以清;`assets\sprites\cust_*.png` 是你上传的贴图
82
84
 
85
+ ### 显示器
86
+
87
+ **游戏配置 → 显示器** 可以选择图层放在哪块屏幕:
88
+
89
+ - 「自动」使用主屏
90
+ - 下拉里会列出当前所有显示器,改完约 1 秒内移动到目标屏幕
91
+ - 这个设置作用于 Electron 图层;WPF 回退仍跟随桌宠所在屏幕
92
+
93
+ ### 备份与重置
94
+
95
+ **游戏配置** 页底部有「存档与重置」:
96
+
97
+ - **备份存档**:把 `state.json` 和 `custom.json` 复制到 `data\farm\backups\backup-时间\`
98
+ - **重置一切**:先自动备份,再清空 `state.json` 和 `custom.json`,World 自动重启并生成默认目录;安装路径和已上传的 PNG 文件不会被删除
99
+
100
+ 也可以从「存档与重置」页签操作同样的按钮。
101
+
83
102
  ## 桌面图层
84
103
 
85
104
  - **Windows 10/11**:默认 Electron/HTML 图层;Electron 不可用时回退 WPF。
@@ -151,6 +170,7 @@ Invoke-RestMethod -Method Post http://127.0.0.1:17788/api/run/restart
151
170
  - Coopanion `>= 0.2.13`,World API `6`
152
171
  - Node.js `>= 22`(Coopanion 自带)
153
172
  - Electron `^45.1.0`(桌面图层依赖)
173
+ - Windows 10/11 已实机测试;macOS / Linux 未实机测试
154
174
 
155
175
  ## 许可
156
176
 
package/dist/webui.js CHANGED
@@ -22,14 +22,99 @@ function renameFarmConfigTab() {
22
22
  if (typeof document !== 'undefined') {
23
23
  const start = () => {
24
24
  renameFarmConfigTab();
25
- new MutationObserver(renameFarmConfigTab).observe(document.body, { childList: true, subtree: true, characterData: true });
26
- window.addEventListener('hashchange', renameFarmConfigTab);
27
- setInterval(renameFarmConfigTab, 1000);
25
+ injectFarmConfigActions();
26
+ new MutationObserver(() => { renameFarmConfigTab(); injectFarmConfigActions(); }).observe(document.body, { childList: true, subtree: true, characterData: true });
27
+ window.addEventListener('hashchange', () => { renameFarmConfigTab(); injectFarmConfigActions(); });
28
+ setInterval(() => { renameFarmConfigTab(); injectFarmConfigActions(); }, 1000);
28
29
  };
29
30
  if (document.body) start();
30
31
  else document.addEventListener('DOMContentLoaded', start, { once: true });
31
32
  }
32
33
 
34
+ /** 调 World 的 manage 面板:备份 / 重置。 */
35
+ async function farmManageCall(method) {
36
+ const res = await fetch('/api/webui/providers/' + encodeURIComponent('world:farm') + '/panels/manage/' + method, {
37
+ method: 'POST',
38
+ headers: { 'content-type': 'application/json' },
39
+ body: JSON.stringify({ args: [] }),
40
+ });
41
+ const data = await res.json().catch(() => ({}));
42
+ if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status));
43
+ return data;
44
+ }
45
+
46
+ /** 往宿主的「游戏配置」页里塞「存档与重置」按钮。 */
47
+ function injectFarmConfigActions() {
48
+ try {
49
+ const hash = String(location.hash || '');
50
+ if (!hash.includes('world%3Afarm') && !hash.includes('world:farm')) return;
51
+ const view = document.querySelector('.configview');
52
+ if (!view || view.querySelector('[data-farm-manage]')) return;
53
+
54
+ const section = document.createElement('div');
55
+ section.className = 'tsection';
56
+ section.dataset.farmManage = '1';
57
+ section.textContent = '存档与重置';
58
+
59
+ const desc = document.createElement('div');
60
+ desc.className = 'tsecdesc';
61
+ desc.textContent = '备份会复制 state.json 和 custom.json 到 data/farm/backups;重置会先备份,再清空存档和全部自定义设置,World 随后重启。';
62
+
63
+ const row = document.createElement('div');
64
+ row.className = 'trow';
65
+ const label = document.createElement('span');
66
+ label.className = 'tlabel';
67
+ label.textContent = '操作';
68
+ const field = document.createElement('div');
69
+ field.className = 'tfield';
70
+ field.style.display = 'flex';
71
+ field.style.gap = '8px';
72
+ field.style.flexWrap = 'wrap';
73
+ const status = document.createElement('span');
74
+ status.className = 'tdesc';
75
+ status.setAttribute('role', 'status');
76
+
77
+ const button = (text, primary) => {
78
+ const b = document.createElement('button');
79
+ b.type = 'button';
80
+ b.className = 'btn sm' + (primary ? ' primary' : '');
81
+ b.textContent = text;
82
+ return b;
83
+ };
84
+ const backup = button('备份存档', true);
85
+ const reset = button('重置一切', false);
86
+
87
+ backup.addEventListener('click', async () => {
88
+ backup.disabled = true;
89
+ status.textContent = '正在备份…';
90
+ try {
91
+ const r = await farmManageCall('backup');
92
+ status.textContent = r.text || '已备份';
93
+ } catch (err) {
94
+ status.textContent = '备份失败:' + ((err && err.message) || err);
95
+ } finally { backup.disabled = false; }
96
+ });
97
+
98
+ reset.addEventListener('click', async () => {
99
+ if (!window.confirm('确定重置一切?会清空存档和全部自定义设置(会先自动备份)。')) return;
100
+ reset.disabled = true;
101
+ status.textContent = '正在重置…';
102
+ try {
103
+ const r = await farmManageCall('reset');
104
+ status.textContent = r.text || '已重置,World 正在重启…';
105
+ setTimeout(() => { try { location.reload(); } catch { /* ignore */ } }, 3000);
106
+ } catch (err) {
107
+ status.textContent = '重置失败:' + ((err && err.message) || err);
108
+ reset.disabled = false;
109
+ }
110
+ });
111
+
112
+ field.append(backup, reset, status);
113
+ row.append(label, field);
114
+ view.append(section, desc, row);
115
+ } catch { /* 不是宿主的配置页就算了 */ }
116
+ }
117
+
33
118
  const TEXT = {
34
119
  zh: {
35
120
  title: '农场',
@@ -2700,8 +2785,52 @@ function ecologyPanel(ctx) {
2700
2785
  reload();
2701
2786
  }
2702
2787
 
2788
+ function managePanel(ctx) {
2789
+ const { ui } = ctx;
2790
+ const root = ui.h('div', 'cf-root');
2791
+ ctx.root.appendChild(root);
2792
+ const sheet = ui.sheet({
2793
+ title: '存档与重置',
2794
+ desc: '备份会复制 state.json 和 custom.json 到 data/farm/backups;重置会先备份,再清空存档和全部自定义设置,World 随后重启。',
2795
+ });
2796
+ const bar = ui.rowbar();
2797
+ const status = ui.h('p', 'cf-note', '');
2798
+ const backup = ui.button('备份存档', {
2799
+ variant: 'primary',
2800
+ onClick: async () => {
2801
+ backup.disabled = true;
2802
+ status.textContent = '正在备份…';
2803
+ try {
2804
+ const r = await ctx.invoke('backup');
2805
+ status.textContent = r.text || '已备份';
2806
+ } catch (err) {
2807
+ status.textContent = '备份失败:' + ((err && err.message) || err);
2808
+ } finally { backup.disabled = false; }
2809
+ },
2810
+ });
2811
+ const reset = ui.button('重置一切', {
2812
+ onClick: async () => {
2813
+ if (!window.confirm('确定重置一切?会清空存档和全部自定义设置(会先自动备份)。')) return;
2814
+ reset.disabled = true;
2815
+ status.textContent = '正在重置…';
2816
+ try {
2817
+ const r = await ctx.invoke('reset');
2818
+ status.textContent = r.text || '已重置,World 正在重启…';
2819
+ setTimeout(() => { try { location.reload(); } catch { /* ignore */ } }, 3000);
2820
+ } catch (err) {
2821
+ status.textContent = '重置失败:' + ((err && err.message) || err);
2822
+ reset.disabled = false;
2823
+ }
2824
+ },
2825
+ });
2826
+ bar.append(backup, reset);
2827
+ sheet.body.append(bar, status);
2828
+ root.appendChild(sheet.el);
2829
+ }
2830
+
2703
2831
  export default {
2704
2832
  panels: {
2833
+ manage: { mount: managePanel },
2705
2834
  bag: { mount: bagPanel },
2706
2835
  shop: { mount: shopPanel },
2707
2836
  customize: { mount: panel },
@@ -9,6 +9,7 @@ function arg(name, fallback = '') {
9
9
  }
10
10
 
11
11
  const statePath = path.resolve(arg('state-path', path.join(__dirname, '..', 'overlay', 'runtime', 'state.json')));
12
+ const screensPath = path.join(path.dirname(statePath), 'screens.json');
12
13
  const assetsDir = path.resolve(arg('assets-dir', path.join(__dirname, '..', 'assets', 'sprites')));
13
14
  const logPath = arg('log', '');
14
15
  const pidPath = arg('pid', '');
@@ -46,10 +47,34 @@ function checkLease() {
46
47
  }
47
48
 
48
49
 
50
+ function listScreens() {
51
+ const primary = screen.getPrimaryDisplay();
52
+ return screen.getAllDisplays().map((d, i) => ({
53
+ id: String(d.id),
54
+ label: d.label || ('Display ' + (i + 1)),
55
+ primary: d.id === primary.id,
56
+ width: Math.round(d.bounds.width),
57
+ height: Math.round(d.bounds.height),
58
+ }));
59
+ }
60
+
61
+ function writeScreens() {
62
+ try {
63
+ fs.mkdirSync(path.dirname(screensPath), { recursive: true });
64
+ fs.writeFileSync(screensPath, JSON.stringify(listScreens(), null, 2), 'utf8');
65
+ } catch (e) { log('screens.json failed: ' + e.message); }
66
+ }
67
+
68
+ function displayFor(st) {
69
+ const all = screen.getAllDisplays();
70
+ const id = String(st?.screen ?? '');
71
+ return all.find((d) => String(d.id) === id) || screen.getPrimaryDisplay();
72
+ }
73
+
49
74
  function bounds() {
50
75
  const st = readState() || {};
51
76
  const h = Math.min(800, Math.max(120, Number(st.stripHeight) || 320));
52
- const wa = screen.getPrimaryDisplay().workArea;
77
+ const wa = displayFor(st).workArea;
53
78
  return { x: Math.round(wa.x), y: Math.round(wa.y + wa.height - h), width: Math.round(wa.width), height: h };
54
79
  }
55
80
 
@@ -103,21 +128,22 @@ if (!app.requestSingleInstanceLock()) {
103
128
  app.on('second-instance', () => { if (win) win.showInactive(); });
104
129
  app.whenReady().then(() => {
105
130
  writePid();
131
+ writeScreens();
106
132
  createWindow();
107
- screen.on('display-metrics-changed', () => {
133
+ const fit = () => {
108
134
  if (!win) return;
109
135
  const b = bounds();
110
- win.setBounds(b);
111
- win.webContents.send('resize');
112
- });
136
+ const cur = win.getBounds();
137
+ if (cur.x !== b.x || cur.y !== b.y || cur.width !== b.width || cur.height !== b.height) {
138
+ win.setBounds(b);
139
+ win.webContents.send('resize');
140
+ }
141
+ };
142
+ for (const event of ['display-added', 'display-removed', 'display-metrics-changed']) {
143
+ screen.on(event, fit);
144
+ }
113
145
  setInterval(checkLease, 5000);
114
- setInterval(() => {
115
- if (!win) return;
116
- const st = readState() || {};
117
- const h = Math.min(800, Math.max(120, Number(st.stripHeight) || 320));
118
- const b = bounds();
119
- if (win.getBounds().height !== h) { win.setBounds(b); win.webContents.send('resize'); }
120
- }, 1000);
146
+ setInterval(fit, 1000);
121
147
  });
122
148
  }
123
149
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lily-cooperative/custom-farm",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "publishConfig": {
package/src/farm.js CHANGED
@@ -49,6 +49,8 @@ export const REACH = 0.05;
49
49
  export const DEFAULTS = {
50
50
  /** 安装目录:overlay / overlay-electron / assets 放在这里;空 = <dataDir>/custom-farm */
51
51
  installPath: '',
52
+ /** 选中的显示器 id;空 = 自动(主屏)。 */
53
+ screen: '',
52
54
  /** 最多同时种几棵(屏幕上不重叠地放得下的上限) */
53
55
  maxPlants: 12,
54
56
  /** 屏幕逻辑宽度,用来把"像素宽"换算成 x 间距 */
@@ -1074,6 +1076,7 @@ export function overlayView(state) {
1074
1076
  weatherFx: state.weatherFx ?? weatherFxOf(normalizeWeathers(DEFAULT_WEATHERS)[0]),
1075
1077
  current: state.current ?? null,
1076
1078
  currentAt: Number.isFinite(state.standingAt) ? Number(state.standingAt) : null,
1079
+ screen: state.screen ?? '',
1077
1080
  farm: { min: fb.min, max: fb.max, cells: fb.cells, cropScale: fb.cropScale, cropZoom: Number(state.cropScale) || 1 },
1078
1081
  ground: { tile: groundTile },
1079
1082
  waterFx: live.status ? {
package/src/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { CROPS, ROTATION } from './crops.js';
5
5
  import { DEFAULTS, overlayView, summary } from './farm.js';
6
- import { FARM_TOOL_DECLS, FarmWorld } from './world.js';
6
+ import { FARM_TOOL_DECLS, FarmWorld, screenOptions } from './world.js';
7
7
 
8
8
  const TOOL_SUMMARIES = {
9
9
  farm_status: {
@@ -64,7 +64,9 @@ export const farmDefinition = {
64
64
  botDir: ctx.botDir,
65
65
  packageDir: ctx.packageDir,
66
66
  persist: (patch) => ctx.persist(patch),
67
+ restart: () => ctx.restart(),
67
68
  }),
69
+ configOptions: (kind, language) => (kind === 'farm-screens' ? screenOptions(language) : []),
68
70
  describe: {
69
71
  tools: describeTools(FARM_TOOL_DECLS),
70
72
  events: [
package/src/world.js CHANGED
@@ -18,7 +18,7 @@
18
18
  import { spawn } from 'node:child_process';
19
19
  import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
20
20
  import { execFileSync } from 'node:child_process';
21
- import { mkdir, readFile, readdir, rename, unlink, writeFile } from 'node:fs/promises';
21
+ import { mkdir, copyFile, readFile, readdir, rename, unlink, writeFile } from 'node:fs/promises';
22
22
  import { dirname, join, resolve, sep } from 'node:path';
23
23
  import { fileURLToPath } from 'node:url';
24
24
 
@@ -73,6 +73,17 @@ import {
73
73
  } from './farm.js';
74
74
 
75
75
  const ENV_PROMPT = fileURLToPath(new URL('./ENV_PROMPT.md', import.meta.url));
76
+ /** Electron 图层写出的显示器列表;配置页的「显示器」下拉用它。 */
77
+ let screenChoices = [];
78
+
79
+ export function screenOptions(language = 'zh') {
80
+ const zh = String(language).toLowerCase().startsWith('zh');
81
+ const out = [{ value: '', label: zh ? '自动(主屏)' : 'Auto (primary)' }];
82
+ for (const [i, d] of screenChoices.entries()) {
83
+ out.push({ value: String(d.id), label: String(d.label || (zh ? '显示器 ' : 'Display ') + (i + 1)) + (d.primary ? (zh ? '(主屏)' : ' (primary)') : '') });
84
+ }
85
+ return out;
86
+ }
76
87
  const msg = (err) => (err instanceof Error ? err.message : String(err));
77
88
 
78
89
  const pct = (x) => (x === null || x === undefined ? '?' : Math.round(Number(x) * 100));
@@ -297,6 +308,13 @@ function farmConfigGroups(language = 'zh', defaultHomePath = '') {
297
308
  description: t('影响之后的操作;startCoins 主要影响新开局。', 'Affects subsequent actions; startCoins mainly affects a fresh game.'),
298
309
  properties: {
299
310
  'worlds.farm.startCoins': int(t('新开局初始金币', 'Starting coins'), t('默认 120,只影响新开档。', 'Default 120; only affects a new save.'), 0, 100000),
311
+ 'worlds.farm.screen': {
312
+ type: 'string',
313
+ title: t('显示器', 'Display'),
314
+ description: t('图层放在哪块屏幕;「自动」跟随主屏。', 'Which display hosts the overlay; Auto uses the primary display.'),
315
+ 'x-options': 'farm-screens',
316
+ 'x-hot': true,
317
+ },
300
318
  'worlds.farm.overlayEnabled': bool(t('写桌面图层的 state.json', 'Write the overlay state.json'), t('默认开。关掉后桌面农场停止更新。', 'On by default. Off stops the desktop farm from updating.')),
301
319
  'worlds.farm.hideFarm': bool(t('隐藏农场', 'Hide farm'), t('勾选后停止桌面图层;农场仍在后台生长,取消勾选恢复。', 'Stop the desktop overlay; the farm keeps growing. Uncheck to show it again.')),
302
320
  },
@@ -331,6 +349,7 @@ export class FarmWorld {
331
349
  this.botDir = opts.botDir ?? process.cwd();
332
350
  this.packageDir = opts.packageDir ?? process.cwd();
333
351
  this.persistConfig = typeof opts.persist === 'function' ? opts.persist : null;
352
+ this.restartWorld = typeof opts.restart === 'function' ? opts.restart : null;
334
353
  // 是否由这个 World 管桌面图层的启停(单元测试里传 false,避免碰到真实图层)
335
354
  this.manageLayers = opts.manageLayers !== false;
336
355
  this.language = 'zh';
@@ -372,6 +391,7 @@ export class FarmWorld {
372
391
  const water = this.#catalog?.status?.water ?? null;
373
392
  return {
374
393
  ...DEFAULTS,
394
+ screen: String(this.cfg?.screen ?? ''),
375
395
  maxPlants: this.#num('maxPlants', 1, 24),
376
396
  screenWidth,
377
397
  zoom: this.#num('zoom', 1, 8),
@@ -543,6 +563,67 @@ export class FarmWorld {
543
563
  this.#ensureLayers('安装路径变更');
544
564
  }
545
565
 
566
+ /** 备份存档:把 state.json 和 custom.json 复制到 data/farm/backups/backup-时间/。 */
567
+ async #backupNow() {
568
+ const d = new Date();
569
+ const p2 = (n) => String(n).padStart(2, '0');
570
+ const stamp = d.getFullYear() + p2(d.getMonth() + 1) + p2(d.getDate()) + '-' + p2(d.getHours()) + p2(d.getMinutes()) + p2(d.getSeconds());
571
+ const dir = join(this.dataDir, 'farm', 'backups', 'backup-' + stamp);
572
+ await mkdir(dir, { recursive: true });
573
+ const files = [];
574
+ for (const [name, src] of [
575
+ ['state.json', this.stateFile()],
576
+ ['custom.json', join(this.dataDir, 'farm', 'custom.json')],
577
+ ]) {
578
+ try {
579
+ if (!existsSync(src)) continue;
580
+ await copyFile(src, join(dir, name));
581
+ files.push(name);
582
+ } catch (err) {
583
+ this.#host?.log?.warn?.('自定义农场:备份文件失败', { file: src, err: msg(err) });
584
+ }
585
+ }
586
+ this.#host?.log?.info?.('自定义农场:已备份存档', { dir, files });
587
+ return {
588
+ ok: true,
589
+ path: dir,
590
+ files,
591
+ text: files.length ? '已备份到 ' + dir : '没有可备份的存档文件;已创建空备份目录 ' + dir,
592
+ };
593
+ }
594
+
595
+ /** 重置一切:先备份,再删 state.json / custom.json,最后重启 World 重新生成默认目录。 */
596
+ async #resetAll() {
597
+ const backup = await this.#backupNow().catch(() => null);
598
+ // 先断开内存里的 state,World 重启/停止时就不会把旧状态写回去。
599
+ this.#state = null;
600
+ for (const file of [this.stateFile(), join(this.dataDir, 'farm', 'custom.json')]) {
601
+ try { await unlink(file); } catch { /* 文件本来就不存在 */ }
602
+ }
603
+ this.#host?.log?.warn?.('自定义农场:已重置全部存档与设置', { backup: backup?.path ?? null });
604
+ if (this.restartWorld) this.setTimeoutImpl(() => { void this.restartWorld(); }, 300);
605
+ return {
606
+ ok: true,
607
+ backup: backup?.path ?? null,
608
+ text: '已重置存档和自定义设置' + (backup?.path ? ';备份在 ' + backup.path : '') + '。World 正在重启。',
609
+ };
610
+ }
611
+
612
+ /** 读取 Electron 图层写出的显示器列表,供游戏配置的「显示器」下拉使用。 */
613
+ #refreshScreens() {
614
+ try {
615
+ const file = join(dirname(this.overlayFile()), 'screens.json');
616
+ const list = JSON.parse(readFileSync(file, 'utf8'));
617
+ if (Array.isArray(list) && list.length) {
618
+ screenChoices = list.map((d) => ({
619
+ id: String(d.id ?? ''),
620
+ label: String(d.label ?? ''),
621
+ primary: Boolean(d.primary),
622
+ })).filter((d) => d.id);
623
+ }
624
+ } catch { /* 图层还没起来,保留上次的列表 */ }
625
+ }
626
+
546
627
  /** 自定义目录:custom.json + 安装目录下的 assets\sprites。 */
547
628
  #createCatalog() {
548
629
  const home = this.#homePath();
@@ -1282,7 +1363,9 @@ export class FarmWorld {
1282
1363
  this.#state.dropScale = cfg.dropScale;
1283
1364
  this.#state.groundHeight = cfg.groundHeight;
1284
1365
  this.#state.stripHeight = cfg.stripHeight;
1366
+ this.#state.screen = cfg.screen;
1285
1367
  }
1368
+ this.#refreshScreens();
1286
1369
  await writeJsonAtomic(this.overlayFile(), overlayView(this.#state));
1287
1370
  } catch (err) {
1288
1371
  this.#host?.log?.debug?.('自定义农场:图层状态没写出去', { file: this.overlayFile(), err: msg(err) });
@@ -1482,6 +1565,11 @@ export class FarmWorld {
1482
1565
  { label: '天数', value: started ? '第 ' + s.day + ' 天' : '—', tone: 'plain' },
1483
1566
  ],
1484
1567
  panels: [{
1568
+ id: 'manage',
1569
+ title: '存档与重置',
1570
+ description: '备份存档,或把存档和全部自定义设置重置为默认。',
1571
+ getMethods: ['state'],
1572
+ }, {
1485
1573
  id: 'bag',
1486
1574
  title: '背包',
1487
1575
  description: '背包、农场预览、摆放道具与手动合成;装饰在这里输入坐标放置。',
@@ -1528,6 +1616,12 @@ export class FarmWorld {
1528
1616
  async invoke(panel, method, args = []) {
1529
1617
  return this.#serial(async () => {
1530
1618
  const cfg = this.#cfgAll();
1619
+ if (panel === 'manage') {
1620
+ if (method === 'backup') return this.#backupNow();
1621
+ if (method === 'reset') return this.#resetAll();
1622
+ if (method === 'state') return { ok: true };
1623
+ throw new Error('不认识的存档操作:' + method);
1624
+ }
1531
1625
  if (panel === 'status') return this.#statusInvoke(method, args, cfg);
1532
1626
  if (panel === 'bag') return this.#bagInvoke(method, args);
1533
1627
  if (panel === 'shop') return this.#shopInvoke(method, args);