@lily-cooperative/custom-farm 0.34.1 → 0.35.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 +157 -373
- package/describe.json +0 -11
- package/dist/webui.css +1 -1
- package/package.json +4 -5
- package/src/farm.js +2 -0
- package/src/index.js +1 -0
- package/src/paths.js +47 -50
- package/src/shop.js +1 -1
- package/src/world.js +180 -48
- package/bag/README.md +0 -60
- package/bag/art/panel.png +0 -0
- package/bag/art/slot.png +0 -0
- package/bag/bag.ps1 +0 -503
- package/bag/shot.png +0 -0
package/src/index.js
CHANGED
package/src/paths.js
CHANGED
|
@@ -1,50 +1,47 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
candidates
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
home,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
assetsDir: join(home, 'assets', 'sprites'),
|
|
49
|
-
};
|
|
50
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 路径解析:home 目录里放 overlay / overlay-electron / assets。
|
|
3
|
+
*
|
|
4
|
+
* 默认 home 是 <dataDir>/custom-farm;扩展从 npm 安装时,包内也自带这三个目录,
|
|
5
|
+
* 首次启动会把它们复制过去。之后在「游戏配置 → 安装」里改路径,World 会把 home
|
|
6
|
+
* 搬到新位置并重启桌面图层。
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { dirname, join, resolve } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const SRC_DIR = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
export const EXTENSION_DIR = resolve(SRC_DIR, '..');
|
|
14
|
+
|
|
15
|
+
export const HOME_NAME = 'custom-farm';
|
|
16
|
+
|
|
17
|
+
/** 一个目录是否已经有可用的桌面图层和素材。 */
|
|
18
|
+
export function looksLikeHome(dir) {
|
|
19
|
+
if (!dir) return false;
|
|
20
|
+
return existsSync(join(dir, 'overlay', 'overlay.ps1'))
|
|
21
|
+
|| (existsSync(join(dir, 'overlay-electron', 'main.js')) && existsSync(join(dir, 'assets', 'sprites')));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 默认 home:Coopanion 数据目录下的 custom-farm。 */
|
|
25
|
+
export function defaultHome(dataDir) {
|
|
26
|
+
return join(dataDir, HOME_NAME);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 扩展包自带或开发目录里的 overlay / assets 源。 */
|
|
30
|
+
export function bundleHome(packageDir) {
|
|
31
|
+
const candidates = [packageDir, resolve(packageDir, '..'), EXTENSION_DIR, resolve(EXTENSION_DIR, '..')];
|
|
32
|
+
for (const dir of candidates) {
|
|
33
|
+
if (looksLikeHome(dir)) return dir;
|
|
34
|
+
}
|
|
35
|
+
return packageDir;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function makePaths(home) {
|
|
39
|
+
return {
|
|
40
|
+
home,
|
|
41
|
+
overlayDir: join(home, 'overlay'),
|
|
42
|
+
overlayScript: join(home, 'overlay', 'overlay.ps1'),
|
|
43
|
+
overlayState: join(home, 'overlay', 'runtime', 'state.json'),
|
|
44
|
+
electronDir: join(home, 'overlay-electron'),
|
|
45
|
+
assetsDir: join(home, 'assets', 'sprites'),
|
|
46
|
+
};
|
|
47
|
+
}
|
package/src/shop.js
CHANGED
|
@@ -101,7 +101,7 @@ export function buyPriceOf(itemId) {
|
|
|
101
101
|
/**
|
|
102
102
|
* 家具总开关:2026-10-27 起暂时下架(摆放方案还没定)。
|
|
103
103
|
* false = 商店不列、farm_buy 不卖家具、farm_place 报错;存档里已经有的家具保留不删。
|
|
104
|
-
* 恢复:改回 true
|
|
104
|
+
* 恢复:改回 true。
|
|
105
105
|
*/
|
|
106
106
|
export const FURNITURE_ENABLED = false;
|
|
107
107
|
|
package/src/world.js
CHANGED
|
@@ -16,17 +16,17 @@
|
|
|
16
16
|
* 「新的一天」要 dailyEvent=true 才推,绝不因刷新本身唤醒模型。
|
|
17
17
|
*/
|
|
18
18
|
import { spawn } from 'node:child_process';
|
|
19
|
-
import { existsSync, readFileSync, unlinkSync } from 'node:fs';
|
|
19
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import { execFileSync } from 'node:child_process';
|
|
21
21
|
import { mkdir, readFile, readdir, rename, unlink, writeFile } from 'node:fs/promises';
|
|
22
|
-
import { dirname, join } from 'node:path';
|
|
22
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
23
23
|
import { fileURLToPath } from 'node:url';
|
|
24
24
|
|
|
25
25
|
import { CROPS, ROTATION } from './crops.js';
|
|
26
26
|
import { FarmCatalog } from './catalog.js';
|
|
27
27
|
import { live } from './catalog-state.js';
|
|
28
28
|
import { buildZip, readZip } from './zip.js';
|
|
29
|
-
import {
|
|
29
|
+
import { bundleHome, defaultHome, makePaths } from './paths.js';
|
|
30
30
|
import { CROP_PRICES, FURNITURE, FURNITURE_ENABLED, SEED_BY_CROP, SEED_BY_ID, SEEDS, sellPriceOf } from './shop.js';
|
|
31
31
|
import { normalizeWeathers, pickWeather, weatherById, weatherFxOf } from './weather.js';
|
|
32
32
|
import {
|
|
@@ -213,12 +213,6 @@ export const FARM_TOOL_DECLS = [
|
|
|
213
213
|
additionalProperties: false,
|
|
214
214
|
},
|
|
215
215
|
},
|
|
216
|
-
{
|
|
217
|
-
name: 'farm_bag',
|
|
218
|
-
tags: ['write'],
|
|
219
|
-
description: '打开(或唤到前面)背包面板。背包里是收下来的作物,可以看每种有多少;面板本身只读,东西还是靠种/收/捡进出。',
|
|
220
|
-
parameters: { type: 'object', properties: {} },
|
|
221
|
-
},
|
|
222
216
|
{
|
|
223
217
|
name: 'farm_collect',
|
|
224
218
|
tags: ['write'],
|
|
@@ -258,7 +252,7 @@ export const FARM_TOOL_DECLS = [
|
|
|
258
252
|
* 键是 `worlds.farm.*`,必须与 `DEFAULTS` 里的键一一对应(dry-mount 会检查)。
|
|
259
253
|
* 都是 hot:写回 config.json 后,下一次 tick 就按新值跑,不需要重启。
|
|
260
254
|
*/
|
|
261
|
-
function farmConfigGroups(language = 'zh') {
|
|
255
|
+
function farmConfigGroups(language = 'zh', defaultHomePath = '') {
|
|
262
256
|
const zh = String(language).toLowerCase().startsWith('zh');
|
|
263
257
|
const t = (a, b) => (zh ? a : b);
|
|
264
258
|
const num = (title, description, minimum, maximum, suffix) => ({
|
|
@@ -270,8 +264,30 @@ function farmConfigGroups(language = 'zh') {
|
|
|
270
264
|
...(suffix ? { 'x-suffix': suffix } : {}),
|
|
271
265
|
});
|
|
272
266
|
const bool = (title, description) => ({ type: 'boolean', title, description, 'x-hot': true });
|
|
267
|
+
const dir = (title, description) => ({
|
|
268
|
+
type: 'string',
|
|
269
|
+
title,
|
|
270
|
+
description,
|
|
271
|
+
'x-hot': true,
|
|
272
|
+
'x-path': { kind: 'directory', ...(defaultHomePath ? { recommendedDir: defaultHomePath } : {}) },
|
|
273
|
+
});
|
|
273
274
|
|
|
274
275
|
return [
|
|
276
|
+
{
|
|
277
|
+
id: 'world:farm/install',
|
|
278
|
+
owner: 'world:farm',
|
|
279
|
+
schema: {
|
|
280
|
+
type: 'object',
|
|
281
|
+
title: t('安装', 'Installation'),
|
|
282
|
+
description: t('桌面图层、素材和运行文件放在这里。修改路径会自动停图层、搬过去再重启。', 'Desktop layers, sprites and runtime files live here. Changing the path stops the layers, moves the folder and restarts them.'),
|
|
283
|
+
properties: {
|
|
284
|
+
'worlds.farm.installPath': dir(
|
|
285
|
+
t('安装路径', 'Install path'),
|
|
286
|
+
t('默认 <Coopanion 数据目录>\\custom-farm;改完自动搬过去。', 'Defaults to <Coopanion data>\\custom-farm; the folder is moved automatically when changed.'),
|
|
287
|
+
),
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
275
291
|
{
|
|
276
292
|
id: 'world:farm/field',
|
|
277
293
|
owner: 'world:farm',
|
|
@@ -296,6 +312,8 @@ export class FarmWorld {
|
|
|
296
312
|
#timer = null;
|
|
297
313
|
#queue = Promise.resolve();
|
|
298
314
|
#paths = null;
|
|
315
|
+
#homeChanged = false;
|
|
316
|
+
#previousHome = null;
|
|
299
317
|
#cmdTimer = null;
|
|
300
318
|
#layerChild = null;
|
|
301
319
|
#layerAt = 0;
|
|
@@ -312,6 +330,7 @@ export class FarmWorld {
|
|
|
312
330
|
this.dataDir = opts.dataDir ?? process.cwd();
|
|
313
331
|
this.botDir = opts.botDir ?? process.cwd();
|
|
314
332
|
this.packageDir = opts.packageDir ?? process.cwd();
|
|
333
|
+
this.persistConfig = typeof opts.persist === 'function' ? opts.persist : null;
|
|
315
334
|
// 是否由这个 World 管桌面图层的启停(单元测试里传 false,避免碰到真实图层)
|
|
316
335
|
this.manageLayers = opts.manageLayers !== false;
|
|
317
336
|
this.language = 'zh';
|
|
@@ -390,15 +409,146 @@ export class FarmWorld {
|
|
|
390
409
|
return Math.round(this.#num('tickIntervalMs', 1000, 3600000));
|
|
391
410
|
}
|
|
392
411
|
|
|
393
|
-
|
|
412
|
+
// ------------------------------------------------------------------ 安装目录
|
|
413
|
+
#installMarker() {
|
|
414
|
+
return join(this.dataDir, 'farm', 'install-home.txt');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
#readInstallMarker() {
|
|
418
|
+
try {
|
|
419
|
+
const value = readFileSync(this.#installMarker(), 'utf8').trim();
|
|
420
|
+
return value || null;
|
|
421
|
+
} catch { return null; }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
#writeInstallMarker(home) {
|
|
425
|
+
try {
|
|
426
|
+
mkdirSync(join(this.dataDir, 'farm'), { recursive: true });
|
|
427
|
+
writeFileSync(this.#installMarker(), home, 'utf8');
|
|
428
|
+
} catch { /* 只是个提示文件,写不了不影响 */ }
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** 把扩展包自带的 overlay / overlay-electron / assets 同步到 home。 */
|
|
432
|
+
#copyBundleHome(target) {
|
|
433
|
+
const source = bundleHome(this.packageDir);
|
|
434
|
+
if (!source || resolve(source) === resolve(target)) return;
|
|
435
|
+
for (const name of ['overlay', 'overlay-electron', 'assets']) {
|
|
436
|
+
const src = join(source, name);
|
|
437
|
+
const dst = join(target, name);
|
|
438
|
+
if (!existsSync(src)) continue;
|
|
439
|
+
try {
|
|
440
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
441
|
+
// 图层代码每次覆盖(升级扩展后能拿到新代码);素材只补缺失文件,保留用户上传的 cust_*。
|
|
442
|
+
cpSync(src, dst, {
|
|
443
|
+
recursive: true,
|
|
444
|
+
force: name !== 'assets',
|
|
445
|
+
filter: (path) => {
|
|
446
|
+
const normalized = path.replace(/\\/g, '/');
|
|
447
|
+
if (normalized.includes('/overlay/runtime')) return false;
|
|
448
|
+
if (normalized.includes('/overlay-electron/node_modules')) return false;
|
|
449
|
+
return !/\.(log|pid)$/i.test(normalized);
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
} catch (err) {
|
|
453
|
+
this.#host?.log?.warn?.('自定义农场:复制安装文件失败', { from: src, to: dst, err: msg(err) });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// Electron 的 node_modules 很大,不复制;用 junction 指回扩展包里的那一份。
|
|
457
|
+
const modules = join(source, 'overlay-electron', 'node_modules');
|
|
458
|
+
const targetModules = join(target, 'overlay-electron', 'node_modules');
|
|
459
|
+
if (existsSync(modules) && !existsSync(targetModules)) {
|
|
460
|
+
try {
|
|
461
|
+
symlinkSync(modules, targetModules, process.platform === 'win32' ? 'junction' : 'dir');
|
|
462
|
+
} catch { /* 没有 junction 时 launch.js 会从包的 node_modules 找 */ }
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** 旧 home 搬到新 home;只搬三个目录,不搬存档和 custom.json。 */
|
|
467
|
+
#moveHome(oldHome, target) {
|
|
468
|
+
if (!oldHome || resolve(oldHome) === resolve(target) || !existsSync(oldHome)) return;
|
|
469
|
+
const source = bundleHome(this.packageDir);
|
|
470
|
+
if (resolve(oldHome) === resolve(source)) return; // 扩展包本身不能搬
|
|
471
|
+
const oldResolved = resolve(oldHome);
|
|
472
|
+
const targetResolved = resolve(target);
|
|
473
|
+
if (targetResolved.startsWith(oldResolved + sep) || oldResolved.startsWith(targetResolved + sep)) return;
|
|
474
|
+
mkdirSync(target, { recursive: true });
|
|
475
|
+
for (const name of ['overlay', 'overlay-electron', 'assets']) {
|
|
476
|
+
const src = join(oldHome, name);
|
|
477
|
+
const dst = join(target, name);
|
|
478
|
+
if (!existsSync(src) || existsSync(dst)) continue;
|
|
479
|
+
try {
|
|
480
|
+
renameSync(src, dst);
|
|
481
|
+
} catch {
|
|
482
|
+
try {
|
|
483
|
+
cpSync(src, dst, { recursive: true });
|
|
484
|
+
rmSync(src, { recursive: true, force: true });
|
|
485
|
+
} catch (err) {
|
|
486
|
+
this.#host?.log?.warn?.('自定义农场:搬运安装目录失败', { from: src, to: dst, err: msg(err) });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** 解析配置里的安装路径:留空用 <dataDir>/custom-farm,并写回配置方便在游戏配置里看到。 */
|
|
493
|
+
#prepareInstallHome() {
|
|
494
|
+
const configured = String(this.cfg?.installPath ?? '').trim();
|
|
495
|
+
// 测试/嵌入式使用时没有 persistConfig:直接用扩展包旁边的 home,不复制、不写配置。
|
|
496
|
+
if (!configured && !this.persistConfig) {
|
|
497
|
+
this.#paths = makePaths(bundleHome(this.packageDir));
|
|
498
|
+
return this.#paths;
|
|
499
|
+
}
|
|
500
|
+
const fallback = defaultHome(this.dataDir);
|
|
501
|
+
const target = resolve(configured || fallback);
|
|
502
|
+
if (!configured && this.persistConfig) {
|
|
503
|
+
try { this.persistConfig({ installPath: target }); } catch { /* 配置写不了就先用默认路径 */ }
|
|
504
|
+
}
|
|
505
|
+
const marker = this.#readInstallMarker();
|
|
506
|
+
if (marker && resolve(marker) !== target) {
|
|
507
|
+
this.#previousHome = resolve(marker);
|
|
508
|
+
this.#homeChanged = true;
|
|
509
|
+
this.#moveHome(marker, target);
|
|
510
|
+
} else if (!marker && !existsSync(target)) {
|
|
511
|
+
this.#homeChanged = true;
|
|
512
|
+
const source = bundleHome(this.packageDir);
|
|
513
|
+
if (source && resolve(source) !== target) this.#previousHome = resolve(source);
|
|
514
|
+
}
|
|
515
|
+
this.#copyBundleHome(target);
|
|
516
|
+
this.#writeInstallMarker(target);
|
|
517
|
+
this.#paths = makePaths(target);
|
|
518
|
+
return this.#paths;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
#homePath() {
|
|
522
|
+
return this.#paths ?? this.#prepareInstallHome();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** 游戏配置里改了安装路径:停图层 → 搬运 → 重建目录 → 重启图层。 */
|
|
526
|
+
async #syncInstallHome() {
|
|
527
|
+
const configured = String(this.cfg?.installPath ?? '').trim();
|
|
528
|
+
const target = resolve(configured || defaultHome(this.dataDir));
|
|
529
|
+
if (!this.#paths) { this.#prepareInstallHome(); return; }
|
|
530
|
+
if (resolve(this.#paths.home) === target) return;
|
|
531
|
+
await this.#stopLayers('安装路径变更');
|
|
532
|
+
this.#moveHome(this.#paths.home, target);
|
|
533
|
+
this.#copyBundleHome(target);
|
|
534
|
+
this.#writeInstallMarker(target);
|
|
535
|
+
this.#paths = makePaths(target);
|
|
536
|
+
if (this.#catalog) {
|
|
537
|
+
this.#catalog.spritesDir = this.#paths.assetsDir;
|
|
538
|
+
try { this.#catalog.load(); } catch (err) {
|
|
539
|
+
this.#host?.log?.warn?.('自定义农场:新安装目录的素材读不出来', { err: msg(err) });
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
this.#host?.log?.info?.('自定义农场:安装路径已切换', { home: this.#paths.home });
|
|
543
|
+
this.#ensureLayers('安装路径变更');
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** 自定义目录:custom.json + 安装目录下的 assets\sprites。 */
|
|
394
547
|
#createCatalog() {
|
|
395
|
-
const
|
|
396
|
-
const spritesDir = petHome
|
|
397
|
-
? join(petHome, 'assets', 'sprites')
|
|
398
|
-
: join(this.packageDir, '..', 'assets', 'sprites');
|
|
548
|
+
const home = this.#homePath();
|
|
399
549
|
return new FarmCatalog({
|
|
400
550
|
storePath: join(this.dataDir, 'farm', 'custom.json'),
|
|
401
|
-
spritesDir,
|
|
551
|
+
spritesDir: home.assetsDir,
|
|
402
552
|
pointsPerDay: this.#num('pointsPerDay', 1, 100000),
|
|
403
553
|
waterDefaults: {
|
|
404
554
|
dryPerDay: this.#num('dryPerDay', 0, 20),
|
|
@@ -417,17 +567,11 @@ export class FarmWorld {
|
|
|
417
567
|
return join(this.dataDir, 'farm', 'state.json');
|
|
418
568
|
}
|
|
419
569
|
|
|
420
|
-
/**
|
|
570
|
+
/** 图层状态文件。配置里给了就用配置,否则用安装目录。 */
|
|
421
571
|
overlayFile() {
|
|
422
572
|
const p = this.cfg?.overlayStatePath;
|
|
423
573
|
if (typeof p === 'string' && p.trim()) return p.trim();
|
|
424
|
-
this.#
|
|
425
|
-
return this.#paths.overlayState;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
bagScript() {
|
|
429
|
-
this.#paths ??= makePaths(resolvePetHome(this.cfg?.petRoot));
|
|
430
|
-
return this.#paths.bagScript;
|
|
574
|
+
return this.#homePath().overlayState;
|
|
431
575
|
}
|
|
432
576
|
|
|
433
577
|
envPromptVars() {
|
|
@@ -460,7 +604,6 @@ export class FarmWorld {
|
|
|
460
604
|
case 'farm_collect': return await this.#collectTool();
|
|
461
605
|
case 'farm_consume': return await this.#consumeTool(args);
|
|
462
606
|
case 'farm_craft': return await this.#craftTool(args);
|
|
463
|
-
case 'farm_bag': return await this.#bagTool();
|
|
464
607
|
case 'farm_pull': return await this.#pullTool(args);
|
|
465
608
|
case 'farm_sell': return await this.#sellTool(args);
|
|
466
609
|
case 'farm_shop': return this.#shopTool(args);
|
|
@@ -825,23 +968,6 @@ export class FarmWorld {
|
|
|
825
968
|
(r.cleared ? ',清掉了 ' + r.cleared + ' 丛杂草' : '') + '。' };
|
|
826
969
|
}
|
|
827
970
|
|
|
828
|
-
async #bagTool() {
|
|
829
|
-
if (process.platform !== 'win32') {
|
|
830
|
-
return { text: '背包窗口是 Windows WPF;当前平台请在 Coopanion WebUI 的「背包」页查看。' };
|
|
831
|
-
}
|
|
832
|
-
const script = this.bagScript();
|
|
833
|
-
if (!existsSync(script)) {
|
|
834
|
-
throw new Error('找不到背包脚本:' + script + '(整个 Pet 文件夹要一起拷贝)');
|
|
835
|
-
}
|
|
836
|
-
// 注意:Windows 上不能加 detached——那会用 DETACHED_PROCESS 起进程,
|
|
837
|
-
// PowerShell 5.1 在那种情况下起不来(表现为窗口一直没出现、日志也没有)。不加 detached 也一样能活过父进程。
|
|
838
|
-
const child = spawn('powershell.exe',
|
|
839
|
-
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-File', script],
|
|
840
|
-
{ stdio: 'ignore', windowsHide: true });
|
|
841
|
-
child.unref();
|
|
842
|
-
return { text: '背包面板叫出来了(' + script + ')。它只读,里面有多少就是背包里有多少。' };
|
|
843
|
-
}
|
|
844
|
-
|
|
845
971
|
#requireHere() {
|
|
846
972
|
const at = this.#state.standingAt;
|
|
847
973
|
if (!Number.isFinite(Number(at))) {
|
|
@@ -1203,6 +1329,7 @@ export class FarmWorld {
|
|
|
1203
1329
|
|
|
1204
1330
|
async #tick() {
|
|
1205
1331
|
if (!this.#started || !this.#state) return;
|
|
1332
|
+
await this.#syncInstallHome();
|
|
1206
1333
|
const cfg = this.#cfgAll();
|
|
1207
1334
|
this.#syncLayerVisibility();
|
|
1208
1335
|
const now = this.now();
|
|
@@ -1364,7 +1491,7 @@ export class FarmWorld {
|
|
|
1364
1491
|
}],
|
|
1365
1492
|
// 状态用 lamp + badges 展示,玩法参数放到下面的 config,走框架自带的「配置」页签。
|
|
1366
1493
|
invoke: (panel, method, args) => this.invoke(panel, method, args),
|
|
1367
|
-
config: farmConfigGroups(language),
|
|
1494
|
+
config: farmConfigGroups(language, defaultHome(this.dataDir)),
|
|
1368
1495
|
promptDocs: [{
|
|
1369
1496
|
key: 'worlds.' + this.id + '.env',
|
|
1370
1497
|
title: '环境提示词',
|
|
@@ -2129,7 +2256,7 @@ export class FarmWorld {
|
|
|
2129
2256
|
else this.#ensureLayers('显示农场');
|
|
2130
2257
|
}
|
|
2131
2258
|
|
|
2132
|
-
#stopLayers(reason) {
|
|
2259
|
+
#stopLayers(reason, home = null) {
|
|
2133
2260
|
if (!this.manageLayers) { return Promise.resolve(); }
|
|
2134
2261
|
try {
|
|
2135
2262
|
const pidFile = join(this.dataDir, 'farm', 'electron.pid');
|
|
@@ -2154,7 +2281,7 @@ export class FarmWorld {
|
|
|
2154
2281
|
let done = false;
|
|
2155
2282
|
const finish = () => { if (!done) { done = true; resolve(); } };
|
|
2156
2283
|
try {
|
|
2157
|
-
const paths =
|
|
2284
|
+
const paths = home ? makePaths(home) : this.#homePath();
|
|
2158
2285
|
const script = join(paths.overlayDir, 'restart-layers.ps1');
|
|
2159
2286
|
if (!existsSync(script)) { finish(); return; }
|
|
2160
2287
|
const child = spawn('powershell.exe',
|
|
@@ -2217,7 +2344,7 @@ export class FarmWorld {
|
|
|
2217
2344
|
|
|
2218
2345
|
#ensureLayers(reason) {
|
|
2219
2346
|
if (!this.manageLayers || this.#flag('hideFarm')) { return; }
|
|
2220
|
-
const paths0 = this.#
|
|
2347
|
+
const paths0 = this.#homePath();
|
|
2221
2348
|
if (this.#electronReady(paths0)) {
|
|
2222
2349
|
if (this.#electronPidAlive()) return;
|
|
2223
2350
|
if (process.platform === 'win32') {
|
|
@@ -2236,7 +2363,7 @@ export class FarmWorld {
|
|
|
2236
2363
|
}
|
|
2237
2364
|
if (process.platform !== 'win32') {
|
|
2238
2365
|
try {
|
|
2239
|
-
const paths = this.#
|
|
2366
|
+
const paths = this.#homePath();
|
|
2240
2367
|
const launcher = join(paths.electronDir, 'launch.js');
|
|
2241
2368
|
const localElectron = join(paths.electronDir, 'node_modules', 'electron');
|
|
2242
2369
|
const packageElectron = join(paths.home, 'node_modules', 'electron');
|
|
@@ -2252,7 +2379,7 @@ export class FarmWorld {
|
|
|
2252
2379
|
return;
|
|
2253
2380
|
}
|
|
2254
2381
|
try {
|
|
2255
|
-
const paths = this.#
|
|
2382
|
+
const paths = this.#homePath();
|
|
2256
2383
|
const script = join(paths.overlayDir, 'restart-layers.ps1');
|
|
2257
2384
|
if (!existsSync(script)) return;
|
|
2258
2385
|
const child = spawn('powershell.exe',
|
|
@@ -2350,6 +2477,11 @@ export class FarmWorld {
|
|
|
2350
2477
|
if (this.#started) return;
|
|
2351
2478
|
this.#host = host;
|
|
2352
2479
|
try {
|
|
2480
|
+
this.#prepareInstallHome();
|
|
2481
|
+
if (this.#homeChanged) {
|
|
2482
|
+
if (this.#previousHome) await this.#stopLayers('安装路径迁移', this.#previousHome);
|
|
2483
|
+
await this.#stopLayers('安装路径迁移');
|
|
2484
|
+
}
|
|
2353
2485
|
this.#catalog = this.#createCatalog();
|
|
2354
2486
|
try {
|
|
2355
2487
|
this.#catalog.load();
|
package/bag/README.md
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# 背包窗口(bag)
|
|
2
|
-
|
|
3
|
-
桌宠右键菜单里的「背包」打开的就是它。
|
|
4
|
-
|
|
5
|
-

|
|
6
|
-
|
|
7
|
-
## 长什么样
|
|
8
|
-
|
|
9
|
-
照游戏里的 UI 做法:**圆角面板 + 斜面边框 + 物品格栅**,标题栏带农场图标,格子里的数量压在右下角,
|
|
10
|
-
空槽也画出来(固定 16 格)。
|
|
11
|
-
|
|
12
|
-
## 美术怎么来的
|
|
13
|
-
|
|
14
|
-
游戏里的 UI 形状是「白色蒙版 + 运行时染色」,所以底板和格子也是用游戏自己的形状拼的:
|
|
15
|
-
|
|
16
|
-
| 素材 | 用途 |
|
|
17
|
-
|---|---|
|
|
18
|
-
| `assets\sprites\ui_background_default24_border.png` | 面板外框的圆角形状 |
|
|
19
|
-
| `assets\sprites\ui_background_item.png` | 物品格的形状 |
|
|
20
|
-
| `assets\sprites\ui_menuicon_farm.png` | 标题栏图标 |
|
|
21
|
-
| `assets\sprites\icon_item_*.png` | 格子里的物品图标 |
|
|
22
|
-
|
|
23
|
-
`bag\art\panel.png`(360×400)、`bag\art\slot.png`(64×64)已生成好;旧生成脚本已删除。
|
|
24
|
-
都是 2 倍最近邻放大,跟图层同一个像素刻度。
|
|
25
|
-
|
|
26
|
-
**想换风格**:直接替换 `bag\art\panel.png` / `slot.png`,
|
|
27
|
-
或者直接替换 `art\panel.png` 和 `art\slot.png` 这两张图,脚本一行都不用动。
|
|
28
|
-
|
|
29
|
-
## 行为
|
|
30
|
-
|
|
31
|
-
- 只读 `Pet\overlay\runtime\state.json`(farm World 每 5 秒写一次),每秒刷新
|
|
32
|
-
- 拖动面板移动,右上角 × 或 Esc 关闭
|
|
33
|
-
- 单实例:已经开着再点菜单不会再开一个
|
|
34
|
-
|
|
35
|
-
## 单独跑
|
|
36
|
-
|
|
37
|
-
```powershell
|
|
38
|
-
Set-ExecutionPolicy -Scope Process Bypass -Force
|
|
39
|
-
.\bag.ps1 # × 或 Esc 关闭
|
|
40
|
-
.\bag.ps1 -Seconds 5 # 5 秒后自动关(自检用)
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## 它怎么被打开
|
|
44
|
-
|
|
45
|
-
```
|
|
46
|
-
右键菜单「背包」 tile
|
|
47
|
-
→ pet-app.js send({ t: 'control', action: 'bag' })
|
|
48
|
-
→ src/world.ts onControl('bag') → spawn powershell bag.ps1
|
|
49
|
-
→ 本窗口
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
前两步是**本地补丁**,打在 Coopanion 自己的桌宠扩展包里:
|
|
53
|
-
|
|
54
|
-
| 文件 | 改了什么 |
|
|
55
|
-
|---|---|
|
|
56
|
-
| `node_modules\cortico-world-desktop-pet\web\ui.js` | `ICONS.bag` 图标 |
|
|
57
|
-
| `...\web\pet-app.js` | 菜单的 `ACTIONS` 表加一项 `bag` |
|
|
58
|
-
| `...\src\world.ts` | `onControl` 加 `action === 'bag'` → spawn 本脚本 |
|
|
59
|
-
|
|
60
|
-
**Coopanion 升级会覆盖这三个文件**,重跑 `..\scripts\repatch-pet-menu.ps1` 即可(幂等);`-Restore` 可还原。
|
package/bag/art/panel.png
DELETED
|
Binary file
|
package/bag/art/slot.png
DELETED
|
Binary file
|