@lily-cooperative/custom-farm 0.35.0 → 0.36.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 +4 -4
- package/describe.json +3 -3
- package/dist/webui.js +13 -10
- package/package.json +1 -1
- package/src/catalog.js +36 -14
- package/src/farm.js +15 -2
- package/src/index.js +2 -2
- package/src/weather.js +1 -1
- package/src/world.js +27 -3
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
| **农场自定义 → 杂草** | 增删杂草、阶段与天数、出现权重、上下图层、收获物名字/价值/贴图 |
|
|
26
26
|
| **农场自定义 → 浇水特效 / 枯萎** | 浇水帧序列、水花帧序列、帧间隔、枯萎贴图 |
|
|
27
27
|
| **生态自定义** | 一个游戏日多长、离线最多补几天、刷新间隔、每天是否推事件、浇水蒸发/补水/枯萎天数、天气 |
|
|
28
|
-
| **天气** |
|
|
28
|
+
| **天气** | 增删天气(无风不可删)、持续时间、权重、作物/装饰摇摆方向与幅度、压扁幅度、速度倍率(0~20)、心情变化 |
|
|
29
29
|
| **状态自定义** | 数值状态、开关状态、条件规则、消耗行为(例如饥饿时自动吃背包里的东西) |
|
|
30
30
|
| **道具自定义** | 种子/农作物/消耗品/装饰的买价卖价、能不能吃、吃掉后的状态效果、图标、摆放贴图、图层、缩放 |
|
|
31
31
|
| **合成表** | 1~4 个材料合成 1 个产物,桌宠可以用 `farm_craft` 合成 |
|
|
@@ -91,7 +91,7 @@ Invoke-RestMethod -Method Post http://127.0.0.1:17788/api/run/restart
|
|
|
91
91
|
|
|
92
92
|
| 工具 | 用途 |
|
|
93
93
|
|---|---|
|
|
94
|
-
| `farm_status` |
|
|
94
|
+
| `farm_status` | 看每棵作物的位置、等级、进度、水分、是否可收/枯萎,背包、掉落物、摆放的装饰物位置和最近田里的变化 |
|
|
95
95
|
| `farm_stand` | 站到某个位置,`at` 必填 |
|
|
96
96
|
| `farm_plant` | 在脚下种一棵,消耗 1 包种子 |
|
|
97
97
|
| `farm_water` | 浇脚下一片 |
|
|
@@ -117,8 +117,8 @@ Invoke-RestMethod -Method Post http://127.0.0.1:17788/api/run/restart
|
|
|
117
117
|
| `cropScale` / `dropScale` | 1.5 / 0.6 | 作物 / 掉落物大小 |
|
|
118
118
|
| `groundHeight` / `stripHeight` | 5 / 320 | 地表高度 / 图层纵向覆盖 |
|
|
119
119
|
| `realMinutesPerDay` | 60 | 1 游戏日 = 多少真实分钟 |
|
|
120
|
-
| `maxStepDays` | 1 |
|
|
121
|
-
| `tickIntervalMs` | 5000 |
|
|
120
|
+
| `maxStepDays` | 1 | 离线回来最多补几个游戏日(安全上限,防止关机一夜跳几十天) |
|
|
121
|
+
| `tickIntervalMs` | 5000 | 刷新间隔;只影响状态/事件更新频率,不改变生长速度 |
|
|
122
122
|
| `startCoins` | 120 | 新开局金币 |
|
|
123
123
|
| `moodIntervalMin` | 10 | 心情按真实分钟结算的间隔 |
|
|
124
124
|
| 浇水 | 0.15 / 1 / 2.2 | 每天蒸发 / 一次补水 / 干透后枯萎天数 |
|
package/describe.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"tools": [
|
|
3
3
|
{
|
|
4
4
|
"name": "farm_status",
|
|
5
|
-
"description": "查看田地:每棵作物的位置 xPct
|
|
5
|
+
"description": "查看田地:每棵作物的位置 xPct(屏幕底部百分比)、等级、成熟进度、水分、是否熟了/枯萎了、还能收几次,以及背包里有什么、地上掉了什么、野草有几丛/哪些能收、摆放的装饰物在哪个位置、主人自定义的状态(数值 + 开关)当前是多少。最后一行「最近田里的变化」会列出最近少掉的棵和原因——田空了先看这行,别当成存档被清。想知道情况就查这个,田地不会主动推送生长进度。",
|
|
6
6
|
"parameters": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"properties": {}
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"snapshot"
|
|
13
13
|
],
|
|
14
14
|
"summary": {
|
|
15
|
-
"zh": "
|
|
16
|
-
"en": "Show every plant (position, level, progress, moisture, ripe/wilted) plus the bag, the drops, and recent field changes (which plant is gone and why)."
|
|
15
|
+
"zh": "看田:每棵作物的位置、等级、进度、水分、是否可收/枯萎,外加背包、地上的掉落物、摆放的装饰物位置和最近田里的变化(少了哪棵、为什么)。",
|
|
16
|
+
"en": "Show every plant (position, level, progress, moisture, ripe/wilted) plus the bag, the drops, placed decorations and recent field changes (which plant is gone and why)."
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
{
|
package/dist/webui.js
CHANGED
|
@@ -56,8 +56,8 @@ const TEXT = {
|
|
|
56
56
|
ecologyTime: '时间与生长',
|
|
57
57
|
ecologyWater: '浇水',
|
|
58
58
|
timePerDay: '一个游戏日 = 真实分钟',
|
|
59
|
-
maxStepDays: '
|
|
60
|
-
tickMs: '刷新间隔(ms
|
|
59
|
+
maxStepDays: '离线最多补几天(安全上限)',
|
|
60
|
+
tickMs: '刷新间隔(ms,不影响生长)',
|
|
61
61
|
dailyEvent: '每天推「新的一天」事件',
|
|
62
62
|
saveEcology: '保存生态设置',
|
|
63
63
|
addGround: '添加自定义贴图',
|
|
@@ -160,8 +160,8 @@ const TEXT = {
|
|
|
160
160
|
ecologyTime: 'Time & growth',
|
|
161
161
|
ecologyWater: 'Watering',
|
|
162
162
|
timePerDay: 'Real minutes per game day',
|
|
163
|
-
maxStepDays: 'Max
|
|
164
|
-
tickMs: 'Refresh interval (ms)',
|
|
163
|
+
maxStepDays: 'Max offline days (safety cap)',
|
|
164
|
+
tickMs: 'Refresh interval (ms, does not affect growth)',
|
|
165
165
|
dailyEvent: 'Push a "new day" event daily',
|
|
166
166
|
saveEcology: 'Save ecology settings',
|
|
167
167
|
addGround: 'Add custom tile',
|
|
@@ -2490,7 +2490,7 @@ function ecologyPanel(ctx) {
|
|
|
2490
2490
|
decorAmp: Math.max(0, Math.min(40, Number(w.decorAmp ?? w.swayAmp) || 0)),
|
|
2491
2491
|
squash: Math.max(0, Math.min(0.6, Number(w.squash) || 0)),
|
|
2492
2492
|
mood: Math.max(-100, Math.min(100, Number(w.mood) || 0)),
|
|
2493
|
-
speed: Math.max(0
|
|
2493
|
+
speed: Math.max(0, Math.min(20, Number(w.speed) || 1)),
|
|
2494
2494
|
}));
|
|
2495
2495
|
if (!out.some((w) => w.id === 'calm')) out.unshift({ ...DEFAULT_WEATHERS[0] });
|
|
2496
2496
|
if (!out.some((w) => w.id !== 'calm')) out.push(...DEFAULT_WEATHERS.slice(1).map((w) => ({ ...w })));
|
|
@@ -2519,9 +2519,10 @@ function ecologyPanel(ctx) {
|
|
|
2519
2519
|
if (!draft) draft = cloneSettings(snapshot?.settings);
|
|
2520
2520
|
return draft;
|
|
2521
2521
|
};
|
|
2522
|
-
const num = (value, onChange, step = '1', min = '0') => {
|
|
2522
|
+
const num = (value, onChange, step = '1', min = '0', max = '') => {
|
|
2523
2523
|
const input = ui.input({ type: 'number', value: String(value ?? '') });
|
|
2524
2524
|
input.step = step; input.min = min;
|
|
2525
|
+
if (max !== '') input.max = max;
|
|
2525
2526
|
input.addEventListener('input', () => { const n = Number(input.value); if (Number.isFinite(n)) onChange(n); }, { signal: ctx.signal });
|
|
2526
2527
|
return input;
|
|
2527
2528
|
};
|
|
@@ -2596,7 +2597,7 @@ function ecologyPanel(ctx) {
|
|
|
2596
2597
|
row1.append(
|
|
2597
2598
|
ui.field(zh ? '持续(秒)' : 'Duration (s)', num(w.duration, (v) => { w.duration = v; }, '1', '1')),
|
|
2598
2599
|
ui.field(zh ? '触发概率(权重)' : 'Probability (weight)', num(w.weight, (v) => { w.weight = v; }, '1', '0')),
|
|
2599
|
-
ui.field(zh ? '速度(倍率)' : 'Speed (x)', num(w.speed, (v) => { w.speed = v; }, '0.1', '0
|
|
2600
|
+
ui.field(zh ? '速度(倍率)' : 'Speed (x)', num(w.speed, (v) => { w.speed = v; }, '0.1', '0', '20')),
|
|
2600
2601
|
ui.field(zh ? '心情变化/结算' : 'Mood per settlement', num(w.mood, (v) => { w.mood = v; }, '1', '-100')),
|
|
2601
2602
|
);
|
|
2602
2603
|
const crop = ui.h('div', 'cf-row');
|
|
@@ -2633,13 +2634,15 @@ function ecologyPanel(ctx) {
|
|
|
2633
2634
|
const d = getDraft();
|
|
2634
2635
|
const sheet = ui.sheet({
|
|
2635
2636
|
title: zh ? '生态自定义' : 'Ecology',
|
|
2636
|
-
desc: zh
|
|
2637
|
+
desc: zh
|
|
2638
|
+
? '1 游戏日 = 现实分钟决定生长速度;离线最多补几天是防止关机回来一次跳太多天;刷新间隔只影响状态/事件更新,不影响生长速度。天气按持续时间切换,触发概率按权重计算。'
|
|
2639
|
+
: 'Real minutes per game day sets growth speed; max offline days caps how many game days a long shutdown can catch up; the refresh interval only affects state/event updates, not growth speed. Weather changes by duration and weighted probability.',
|
|
2637
2640
|
});
|
|
2638
2641
|
const time = ui.h('div', 'cf-row');
|
|
2639
2642
|
time.append(
|
|
2640
2643
|
ui.field(zh ? '1 游戏日 = 现实分钟' : 'Real minutes per game day', num(d.realMinutesPerDay, (v) => { d.realMinutesPerDay = v; }, '0.5', '0.5')),
|
|
2641
|
-
ui.field(zh ? '
|
|
2642
|
-
ui.field(zh ? '
|
|
2644
|
+
ui.field(zh ? '离线最多补几天(安全上限)' : 'Max offline days (safety cap)', num(d.maxStepDays, (v) => { d.maxStepDays = v; }, '0.1', '0.01')),
|
|
2645
|
+
ui.field(zh ? '刷新间隔(毫秒,不影响生长)' : 'Refresh interval (ms, does not affect growth)', num(d.tickIntervalMs, (v) => { d.tickIntervalMs = v; }, '100', '1000')),
|
|
2643
2646
|
);
|
|
2644
2647
|
const daily = ui.checkbox(zh ? '每天推送 farm.day' : 'Emit farm.day', {
|
|
2645
2648
|
checked: d.dailyEvent,
|
package/package.json
CHANGED
package/src/catalog.js
CHANGED
|
@@ -387,6 +387,8 @@ export class FarmCatalog {
|
|
|
387
387
|
recipes: [],
|
|
388
388
|
settings: {},
|
|
389
389
|
seededDecor: false,
|
|
390
|
+
seededCabin: false,
|
|
391
|
+
seededDecorSet: false,
|
|
390
392
|
seededFreeCrops: false,
|
|
391
393
|
seededFood: false,
|
|
392
394
|
seededMood: false,
|
|
@@ -472,18 +474,31 @@ export class FarmCatalog {
|
|
|
472
474
|
this.persist();
|
|
473
475
|
}
|
|
474
476
|
|
|
475
|
-
/**
|
|
477
|
+
/** 旧存档切到免费素材作物集:补全新作物,删掉没有替代的小麦/卷心菜/葡萄。只迁移一次,之后保留用户改动。 */
|
|
476
478
|
ensureFreeCrops() {
|
|
477
479
|
if (!this.store) return;
|
|
480
|
+
const removed = new Set(['wheat', 'chinese_cabbage', 'grape']);
|
|
481
|
+
const filterConsume = () => {
|
|
482
|
+
let changed = false;
|
|
483
|
+
for (const action of this.store.consume ?? []) {
|
|
484
|
+
const before = (action.items ?? []).length;
|
|
485
|
+
action.items = (action.items ?? []).filter((id) => !removed.has(id) && !removed.has(String(id).replace(/^seed_/, '')));
|
|
486
|
+
if ((action.items ?? []).length !== before) changed = true;
|
|
487
|
+
}
|
|
488
|
+
return changed;
|
|
489
|
+
};
|
|
490
|
+
if (this.store.seededFreeCrops) {
|
|
491
|
+
const before = this.store.crops.length;
|
|
492
|
+
this.store.crops = this.store.crops.filter((c) => !removed.has(c.key));
|
|
493
|
+
if (filterConsume() || this.store.crops.length !== before) this.persist();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
478
496
|
const defs = new Map(this.builtinCrops().map((c) => [c.key, c]));
|
|
479
|
-
const removed = new Set(["wheat", "chinese_cabbage", "grape"]);
|
|
480
497
|
const order = Object.keys(CROPS);
|
|
481
498
|
// 旧档没有 builtin 标记,但 wheat/chinese_cabbage/grape 一定是旧内置作物;删掉。
|
|
482
499
|
const custom = this.store.crops.filter((c) => !c.builtin && !removed.has(c.key));
|
|
483
500
|
this.store.crops = order.map((key) => defs.get(key)).filter(Boolean).concat(custom);
|
|
484
|
-
|
|
485
|
-
action.items = (action.items ?? []).filter((id) => !removed.has(id) && !removed.has(String(id).replace(/^seed_/, "")));
|
|
486
|
-
}
|
|
501
|
+
filterConsume();
|
|
487
502
|
this.store.seededFreeCrops = true;
|
|
488
503
|
this.persist();
|
|
489
504
|
}
|
|
@@ -519,20 +534,25 @@ export class FarmCatalog {
|
|
|
519
534
|
!(r.inputs ?? []).some((x) => removed.has(x.item)));
|
|
520
535
|
if ((this.store.items?.length ?? 0) !== beforeItems || (this.store.recipes?.length ?? 0) !== beforeRecipes) this.persist();
|
|
521
536
|
}
|
|
522
|
-
/**
|
|
523
|
-
/** 小木屋是后加的内置装饰;旧存档也补上。 */
|
|
537
|
+
/** 小木屋是后加的内置装饰;只在迁移时补一次,用户删掉后不再回来。 */
|
|
524
538
|
ensureCabin() {
|
|
525
|
-
if (!this.store) return;
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
539
|
+
if (!this.store || this.store.seededCabin) return;
|
|
540
|
+
const list = this.store.items ?? (this.store.items = []);
|
|
541
|
+
if (!list.some((it) => it.id === 'deco_cabin')) {
|
|
542
|
+
const def = this.builtinDecorItems().find((it) => it.id === 'deco_cabin');
|
|
543
|
+
if (def) {
|
|
544
|
+
const item = this.normalizeItem(def, list.length, this.store.states ?? []);
|
|
545
|
+
if (item) list.push(item);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
this.store.seededCabin = true;
|
|
549
|
+
this.persist();
|
|
531
550
|
}
|
|
532
551
|
|
|
533
552
|
ensureDecorSet() {
|
|
534
|
-
if (!this.store) return;
|
|
553
|
+
if (!this.store || this.store.seededDecorSet) return;
|
|
535
554
|
this.store.items = (this.store.items ?? []).filter((it) => it.id !== 'deco_haystack');
|
|
555
|
+
this.store.seededDecorSet = true;
|
|
536
556
|
this.persist();
|
|
537
557
|
}
|
|
538
558
|
/** 老存档补一次内置装饰;只补一次,之后用户删了也不会自动回来。 */
|
|
@@ -620,6 +640,8 @@ export class FarmCatalog {
|
|
|
620
640
|
recipes,
|
|
621
641
|
settings: this.normalizeSettings(raw.settings),
|
|
622
642
|
seededDecor: Boolean(raw.seededDecor),
|
|
643
|
+
seededCabin: Boolean(raw.seededCabin),
|
|
644
|
+
seededDecorSet: Boolean(raw.seededDecorSet),
|
|
623
645
|
seededFreeCrops: Boolean(raw.seededFreeCrops),
|
|
624
646
|
seededFood: Boolean(raw.seededFood),
|
|
625
647
|
seededMood: Boolean(raw.seededMood),
|
package/src/farm.js
CHANGED
|
@@ -1162,7 +1162,8 @@ export function overlayView(state) {
|
|
|
1162
1162
|
|
|
1163
1163
|
/** 给工具看的摘要。 */
|
|
1164
1164
|
export function summary(state, cfg = DEFAULTS) {
|
|
1165
|
-
const
|
|
1165
|
+
const inventory = Object.fromEntries(Object.entries(state.inventory ?? {}).filter(([id]) => Boolean(live.items?.[id])));
|
|
1166
|
+
const total = Object.values(inventory).reduce((a, b) => a + b, 0);
|
|
1166
1167
|
const weedList = (state.weeds ?? []).map((w) => {
|
|
1167
1168
|
const t = weedTypeOf(w);
|
|
1168
1169
|
const lv = Number(w.level ?? 0);
|
|
@@ -1180,6 +1181,17 @@ export function summary(state, cfg = DEFAULTS) {
|
|
|
1180
1181
|
};
|
|
1181
1182
|
});
|
|
1182
1183
|
const ripeWeeds = weedList.filter((w) => w.ready);
|
|
1184
|
+
const decorations = (state.furniture ?? [])
|
|
1185
|
+
.filter((f) => f.x !== null && f.x !== undefined)
|
|
1186
|
+
.map((f) => ({
|
|
1187
|
+
uid: f.uid,
|
|
1188
|
+
id: f.id,
|
|
1189
|
+
name: live.names?.[f.id] ?? f.id,
|
|
1190
|
+
x: round2(Number(f.x)),
|
|
1191
|
+
xPct: Math.round(Number(f.x) * 100),
|
|
1192
|
+
layer: live.items?.[f.id]?.layer ?? f.layer ?? 'bottom',
|
|
1193
|
+
}))
|
|
1194
|
+
.sort((a, b) => a.x - b.x);
|
|
1183
1195
|
return {
|
|
1184
1196
|
day: state.day,
|
|
1185
1197
|
clock: clockText(state.minute),
|
|
@@ -1190,7 +1202,7 @@ export function summary(state, cfg = DEFAULTS) {
|
|
|
1190
1202
|
standingAt: Number.isFinite(state.standingAt) ? Number(state.standingAt) : null,
|
|
1191
1203
|
realMinutesPerDay: Number(cfg.realMinutesPerDay),
|
|
1192
1204
|
bagTotal: total,
|
|
1193
|
-
bag:
|
|
1205
|
+
bag: inventory,
|
|
1194
1206
|
drops: state.drops.length,
|
|
1195
1207
|
planted: state.plots.length,
|
|
1196
1208
|
max: cfg.maxPlants,
|
|
@@ -1199,6 +1211,7 @@ export function summary(state, cfg = DEFAULTS) {
|
|
|
1199
1211
|
weeds: weedList.length,
|
|
1200
1212
|
weedRipe: ripeWeeds.length,
|
|
1201
1213
|
weedList: ripeWeeds,
|
|
1214
|
+
decorations,
|
|
1202
1215
|
states: (Array.isArray(cfg.stateDefs) ? cfg.stateDefs : []).map((d) => {
|
|
1203
1216
|
const value = state.states?.[d.id] ?? {};
|
|
1204
1217
|
const flag = d.kind === 'flag';
|
package/src/index.js
CHANGED
|
@@ -7,8 +7,8 @@ import { FARM_TOOL_DECLS, FarmWorld } from './world.js';
|
|
|
7
7
|
|
|
8
8
|
const TOOL_SUMMARIES = {
|
|
9
9
|
farm_status: {
|
|
10
|
-
zh: '
|
|
11
|
-
en: 'Show every plant (position, level, progress, moisture, ripe/wilted) plus the bag, the drops, and recent field changes (which plant is gone and why).',
|
|
10
|
+
zh: '看田:每棵作物的位置、等级、进度、水分、是否可收/枯萎,外加背包、地上的掉落物、摆放的装饰物位置和最近田里的变化(少了哪棵、为什么)。',
|
|
11
|
+
en: 'Show every plant (position, level, progress, moisture, ripe/wilted) plus the bag, the drops, placed decorations and recent field changes (which plant is gone and why).',
|
|
12
12
|
},
|
|
13
13
|
farm_stand: {
|
|
14
14
|
zh: '站到某个位置(at 必填),播种/收割/捡东西都要先站过去。',
|
package/src/weather.js
CHANGED
|
@@ -36,7 +36,7 @@ export function normalizeWeathers(input) {
|
|
|
36
36
|
decorAmp: clamp(row.decorAmp ?? row.swayAmp, 0, 40, 3),
|
|
37
37
|
squash: clamp(row.squash, 0, 0.6, 0),
|
|
38
38
|
mood: clamp(row.mood ?? { calm: 2, breeze: 1, gale: -6, egg: 4 }[id], -100, 100, 0),
|
|
39
|
-
speed: clamp(row.speed, 0
|
|
39
|
+
speed: clamp(row.speed, 0, 20, 1),
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
if (!out.some((w) => w.id === 'calm')) out.unshift({ ...DEFAULT_WEATHERS[0] });
|
package/src/world.js
CHANGED
|
@@ -83,7 +83,7 @@ export const FARM_TOOL_DECLS = [
|
|
|
83
83
|
{
|
|
84
84
|
name: 'farm_status',
|
|
85
85
|
tags: ['read', 'snapshot'],
|
|
86
|
-
description: '查看田地:每棵作物的位置 xPct
|
|
86
|
+
description: '查看田地:每棵作物的位置 xPct(屏幕底部百分比)、等级、成熟进度、水分、是否熟了/枯萎了、还能收几次,以及背包里有什么、地上掉了什么、野草有几丛/哪些能收、摆放的装饰物在哪个位置、主人自定义的状态(数值 + 开关)当前是多少。最后一行「最近田里的变化」会列出最近少掉的棵和原因——田空了先看这行,别当成存档被清。想知道情况就查这个,田地不会主动推送生长进度。',
|
|
87
87
|
parameters: { type: 'object', properties: {} },
|
|
88
88
|
},
|
|
89
89
|
{
|
|
@@ -625,10 +625,30 @@ export class FarmWorld {
|
|
|
625
625
|
delete this.#state.inventory?.['seed_' + key];
|
|
626
626
|
}
|
|
627
627
|
delete this.#state.inventory?.seed_wheat;
|
|
628
|
-
for (const key of ['wheat', 'flour'
|
|
628
|
+
for (const key of ['wheat', 'flour']) delete this.#state.inventory?.[key];
|
|
629
629
|
this.#state.furniture = (this.#state.furniture ?? []).filter((f) => f.id !== 'deco_haystack');
|
|
630
630
|
delete this.#state.inventory?.deco_haystack;
|
|
631
|
-
this.#catalog?.removeRecipesUsing(['chinese_cabbage', 'seed_chinese_cabbage', 'grape', 'seed_grape', 'wheat', 'flour'
|
|
631
|
+
this.#catalog?.removeRecipesUsing(['chinese_cabbage', 'seed_chinese_cabbage', 'grape', 'seed_grape', 'wheat', 'flour']);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** 清掉旧版本留在背包/田里的孤儿 id(比如旧家具 deco_16):当前目录里没有这个道具就删。 */
|
|
635
|
+
#pruneOrphanItems() {
|
|
636
|
+
if (!this.#state) return;
|
|
637
|
+
const known = new Set(Object.keys(live.items ?? {}));
|
|
638
|
+
if (!known.size) return;
|
|
639
|
+
let changed = false;
|
|
640
|
+
for (const id of Object.keys(this.#state.inventory ?? {})) {
|
|
641
|
+
if (known.has(id)) continue;
|
|
642
|
+
delete this.#state.inventory[id];
|
|
643
|
+
changed = true;
|
|
644
|
+
}
|
|
645
|
+
const beforeFurniture = (this.#state.furniture ?? []).length;
|
|
646
|
+
this.#state.furniture = (this.#state.furniture ?? []).filter((f) => known.has(f.id));
|
|
647
|
+
if (this.#state.furniture.length !== beforeFurniture) changed = true;
|
|
648
|
+
const beforeDrops = (this.#state.drops ?? []).length;
|
|
649
|
+
this.#state.drops = (this.#state.drops ?? []).filter((d) => known.has(d.item));
|
|
650
|
+
if (this.#state.drops.length !== beforeDrops) changed = true;
|
|
651
|
+
if (changed) this.#host?.log?.info?.('自定义农场:清掉旧存档里已经不存在的道具');
|
|
632
652
|
}
|
|
633
653
|
#ensureStarted() {
|
|
634
654
|
if (!this.#started || !this.#state) throw new Error('自定义农场 World 还没启动;等它启用后再试。');
|
|
@@ -681,6 +701,9 @@ export class FarmWorld {
|
|
|
681
701
|
lines.push('野草 ' + s.weeds + ' 丛(都还没长成;种东西时会把脚下的铲掉)。');
|
|
682
702
|
}
|
|
683
703
|
}
|
|
704
|
+
if (s.decorations.length) {
|
|
705
|
+
lines.push('摆放的装饰:' + s.decorations.map((d) => d.name + '(' + d.xPct + '%,' + (d.layer === 'top' ? '上层' : '下层') + ')').join('、') + '。');
|
|
706
|
+
}
|
|
684
707
|
if (s.states.length) {
|
|
685
708
|
lines.push('状态:' + s.states.map((st) => (st.kind === 'flag'
|
|
686
709
|
? st.name + (st.active ? '(生效)' : '(未生效)')
|
|
@@ -2490,6 +2513,7 @@ export class FarmWorld {
|
|
|
2490
2513
|
}
|
|
2491
2514
|
this.#state = await this.#loadOrInit();
|
|
2492
2515
|
this.#pruneRemovedCrops();
|
|
2516
|
+
this.#pruneOrphanItems();
|
|
2493
2517
|
this.#state.current = null;
|
|
2494
2518
|
this.#state.standingAt = null;
|
|
2495
2519
|
// 老存档的 drySince 是按"游戏分钟"存的,换成按"累计游戏日"之后要清一次,免得永远不枯萎
|