@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 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 game days per step',
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.2, Math.min(4, Number(w.speed) || 1)),
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.2')),
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 ? '生长条件与天气。天气按持续时间切换,触发概率按权重计算。' : 'Growth conditions and weather. Weather changes by duration and weighted probability.',
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 ? '单次最多补几天' : 'Max offline days', num(d.maxStepDays, (v) => { d.maxStepDays = v; }, '0.1', '0.01')),
2642
- ui.field(zh ? '刷新间隔(毫秒)' : 'Tick interval (ms)', num(d.tickIntervalMs, (v) => { d.tickIntervalMs = v; }, '100', '1000')),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lily-cooperative/custom-farm",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "publishConfig": {
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
- for (const action of this.store.consume ?? []) {
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
- if ((this.store.items ?? []).some((it) => it.id === 'deco_cabin')) return;
527
- const def = this.builtinDecorItems().find((it) => it.id === 'deco_cabin');
528
- if (!def) return;
529
- const item = this.normalizeItem(def, this.store.items.length, this.store.states ?? []);
530
- if (item) { this.store.items.push(item); this.persist(); }
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 total = Object.values(state.inventory).reduce((a, b) => a + b, 0);
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: { ...state.inventory },
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.2, 4, 1),
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', 'pasta']) delete this.#state.inventory?.[key];
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', 'pasta']);
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 是按"游戏分钟"存的,换成按"累计游戏日"之后要清一次,免得永远不枯萎