@lily-cooperative/custom-farm 0.33.5 → 0.34.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
@@ -1,13 +1,15 @@
1
1
  # 自定义农场 · custom-farm
2
2
 
3
- 桌宠在**任务栏上沿**种地:18 种作物、任意多棵(不能重叠)、浇水会干、太久不浇会枯萎、
3
+ 给 Coopanion 桌宠的种田扩展:18 种作物只是起点。**农作物、杂草、装饰物、天气、状态、道具、合成表和贴图都能在面板里完全自定义**——想种什么、长什么样、吃什么补什么,全由你决定。
4
+
5
+ 桌宠在**任务栏上沿**种地:任意多棵(不能重叠)、浇水会干、太久不浇会枯萎、
4
6
  有的作物能反复收;收下来的东西**掉在地上**,桌宠要走过去捡,捡起来才进背包。
5
7
 
6
8
  ## 版本与依赖
7
9
 
8
10
  | 项目 | 要求 |
9
11
  |---|---|
10
- | 当前扩展版本 | `0.33.5` |
12
+ | 当前扩展版本 | `0.34.0` |
11
13
  | Coopanion | `>= 0.2.13`,World API `6` |
12
14
  | Node.js | `>= 22` |
13
15
  | Electron | `^45.1.0`(overlay 依赖) |
@@ -140,7 +142,7 @@ farm_collect → 捡进背包(图层会播"吸到身上")
140
142
  | `src/index.js` | WorldDefinition + describe |
141
143
  | `src/ENV_PROMPT.md` | 给桌宠的环境提示词(自主行动、三事件、省 token) |
142
144
  | `dist/webui.js` / `.css` | 「农场自定义」面板(纯 ESM,无构建步骤) |
143
- | `tools/selftest.mjs` | 165 项断言(不装 Coopanion 也能跑) |
145
+ | `tools/selftest.mjs` | 166 项断言(不装 Coopanion 也能跑) |
144
146
  | `tools/reset-farm.ps1` | 重开局 |
145
147
 
146
148
  ## 4. 调参(`worlds.farm`)
@@ -162,7 +164,7 @@ farm_collect → 捡进背包(图层会播"吸到身上")
162
164
  ## 5. 验证
163
165
 
164
166
  ```powershell
165
- node tools/selftest.mjs # 165 项
167
+ node tools/selftest.mjs # 166 项
166
168
  node tools/gen-describe.mjs # 改了工具/事件后
167
169
  ```
168
170
 
@@ -240,7 +242,7 @@ node tools/gen-describe.mjs # 改了工具/事件后
240
242
  | **开关状态** | 只有「初始生效」;作为条件的目标(比如「饥饿」) |
241
243
  | **条件** | 当 `[数值状态] [< <= > >= == !=] [阈值]` 成立时,让 `[目标开关]` 生效;多条规则指向同一个开关时按「或」处理 |
242
244
  | **消耗行为** | 触发状态生效(开关生效 / 数值 > 0)且冷却到了,就从选中的道具里吃掉 `count` 个,并把 `变化量` 加到指定数值状态(例如 `饥饿 → 吃番茄×1 → 饱食度 +25`);关掉「自动消耗」就只推事件提醒桌宠 |
243
- | **添加示例** | 一键加「饱食度」0~100、初始 100、每天 -5,「饥饿」,条件 `饱食度 < 30 → 饥饿`,以及消耗行为 `饥饿 → 吃番茄等作物 ×1 → 饱食度 +25`(0.5 天冷却,自动) |
245
+ | **添加示例** | 一键补齐「饱食度」0~100、初始 100、每天 -5,「饥饿」,条件 `饱食度 < 30 → 饥饿`,消耗行为 `饥饿 → 吃作物/米饭/意面 ×1 → 饱食度 +25`(0.5 天冷却,自动),并给所有食物补默认效果。默认配置已经带这些;删掉后想恢复就点它(幂等,不会重复加) |
244
246
 
245
247
  - 定义存在 `custom.json` 的 `states` / `rules` / `consume`;当前值存在存档 `state.states`,冷却进度存在 `state.stateActions`,重启/离线都保留。
246
248
  - 每次 tick 按游戏日漂移、重算条件,然后跑消耗行为;自动消耗成功会推 `farm.state` 事件(「饥饿生效:吃掉了番茄×1,饱食度 +25」)。
@@ -270,12 +272,42 @@ node tools/gen-describe.mjs # 改了工具/事件后
270
272
  | `meat` 鲜肉 | 22 | 6 | 商店食材 |
271
273
  | `egg_fried_rice` 蛋炒饭 | — | 35 | **不能购买**,只能合成 |
272
274
  | `item_15` 番茄炒蛋 | — | 30 | **不能购买**,只能合成(旧道具改图标) |
275
+ | `pasta` 意大利面 | — | 55 | **不能购买**,肉 + 番茄合成(图标 `icon_item_pasta`) |
273
276
 
274
277
  合成表:
275
278
 
276
279
  - 稻穗 ×1 → 大米 ×1
277
280
  - 鸡蛋 ×1 + 大米 ×1 → 蛋炒饭 ×1
278
281
  - 鸡蛋 ×1 + 番茄 ×1 → 番茄炒蛋 ×1
282
+ - 鲜肉 ×1 + 番茄 ×1 → 意大利面 ×1
283
+
284
+ ### 默认饱食度 / 心情
285
+
286
+ 新开局会自动带一套状态(旧存档第一次加载时补上,之后不会覆盖你改过的值):
287
+
288
+ - **饱食度** 0~100,初始 100,每游戏日 -5;**低于 30 → 饥饿**
289
+ - **心情** 0~100,初始 60;`>= 70` 开心,`< 30` 无聊
290
+ - **饥饿**生效时自动吃掉背包里的作物/食物(0.5 游戏日冷却),吃完按道具效果回饱食度
291
+ - 每一种内置作物、米饭和意面都默认同时加饱食度和心情;道具自己的效果优先于「消耗行为」上的 `effectDelta`
292
+
293
+ | 道具 | +饱食度 | +心情 | 道具 | +饱食度 | +心情 |
294
+ |---|---:|---:|---|---:|---:|
295
+ | 土豆 `potato` | 24 | 2 | 胡萝卜 `carrot` | 16 | 2 |
296
+ | 番茄 `tomato` | 14 | 4 | 草莓 `strawberry` | 12 | 7 |
297
+ | 玉米 `corn` | 22 | 3 | 南瓜 `pumpkin` | 28 | 4 |
298
+ | 甜菜根 `beetroot` | 16 | 2 | 蓝莓 `blueberry` | 10 | 7 |
299
+ | 小白菜 `bok_choy` | 14 | 2 | 花椰菜 `cauliflower` | 16 | 2 |
300
+ | 黄瓜 `cucumber` | 12 | 3 | 茄子 `eggplant` | 15 | 2 |
301
+ | 大蒜 `garlic` | 6 | 1 | 菠萝 `pineapple` | 18 | 8 |
302
+ | 小萝卜 `radish` | 10 | 2 | 红辣椒 `red_chili` | 5 | 3 |
303
+ | 红洋葱 `red_onion` | 8 | 1 | 西瓜 `watermelon` | 24 | 7 |
304
+ | 稻穗 `rice_ear` | 8 | 1 | 大米 `rice` | 30 | 3 |
305
+ | 鸡蛋 `egg` | 10 | 2 | 鲜肉 `meat` | 20 | 2 |
306
+ | 蛋炒饭 `egg_fried_rice` | 48 | 10 | 番茄炒蛋 `item_15` | 38 | 8 |
307
+ | 意大利面 `pasta` | 42 | 9 | | | |
308
+
309
+ > 自定义作物默认没有食物效果;在「道具自定义 → 农作物道具」里给它加状态效果即可。
310
+
279
311
  > 删除道具、作物或杂草时,会一并清理:**背包里的该道具、农场里已摆放的该装饰、地上的掉落物、引用它的合成表**,以及这个条目上传的 cust_* 贴图。内置共享贴图(例如所有装饰共用的图标文件)不会被删,避免误删别的条目。
280
312
 
281
313
  - 价格/效果/图标存在 `custom.json` 的 `items`;种子/农作物条目跟作物自动同步,价格和效果不会被覆盖。
Binary file
package/dist/webui.js CHANGED
@@ -798,7 +798,7 @@ function panel(ctx) {
798
798
  return {
799
799
  farmCells: Number(src.farmCells ?? 48),
800
800
  groundHeight: Number(src.groundHeight ?? 5),
801
- stripHeight: Number(src.stripHeight ?? 320),
801
+ stripHeight: Number(src.stripHeight ?? 320),
802
802
  moodIntervalMin: Number(src.moodIntervalMin ?? 10),
803
803
  maxPlants: Number(src.maxPlants ?? 12),
804
804
  cropGapScale: Number(src.cropGapScale ?? 0.8),
@@ -1023,8 +1023,8 @@ const ST = {
1023
1023
  addNumber: '添加数值状态',
1024
1024
  addFlag: '添加开关状态',
1025
1025
  addRule: '添加条件',
1026
- addExample: '添加示例:饱食度 → 饥饿',
1027
- exampleDone: '示例已添加',
1026
+ addExample: '补齐示例:饱食度 / 饥饿 / 食物效果',
1027
+ exampleDone: '示例已补齐',
1028
1028
  name: '名字',
1029
1029
  kind: '类型',
1030
1030
  number: '数值',
@@ -1090,8 +1090,8 @@ const ST = {
1090
1090
  addNumber: 'Add number state',
1091
1091
  addFlag: 'Add flag state',
1092
1092
  addRule: 'Add rule',
1093
- addExample: 'Add example: satiety → hunger',
1094
- exampleDone: 'Example added',
1093
+ addExample: 'Fill in example: satiety / hunger / food effects',
1094
+ exampleDone: 'Example filled in',
1095
1095
  name: 'Name',
1096
1096
  kind: 'Kind',
1097
1097
  number: 'Number',
@@ -2468,11 +2468,11 @@ function ecologyPanel(ctx) {
2468
2468
  ctx.root.appendChild(root);
2469
2469
  let snapshot = null;
2470
2470
 
2471
- const DEFAULT_WEATHERS = [
2472
- { id: 'calm', name: '无风', builtin: true, deletable: false, duration: 300, weight: 30, cropSway: 'both', cropAmp: 4, decorSway: 'none', decorAmp: 1, squash: 0, mood: 2, speed: 1 },
2473
- { id: 'breeze', name: '微风', builtin: true, deletable: true, duration: 250, weight: 40, cropSway: 'left', cropAmp: 5, decorSway: 'none', decorAmp: 3, squash: 0, mood: 1, speed: 1.2 },
2474
- { id: 'gale', name: '大风', builtin: true, deletable: true, duration: 100, weight: 20, cropSway: 'right', cropAmp: 13, decorSway: 'none', decorAmp: 9, squash: 0, mood: -6, speed: 2.4 },
2475
- { id: 'egg', name: '彩蛋', builtin: true, deletable: true, duration: 300, weight: 10, cropSway: 'none', cropAmp: 0, decorSway: 'none', decorAmp: 0, squash: 0.45, mood: 4, speed: 3 },
2471
+ const DEFAULT_WEATHERS = [
2472
+ { id: 'calm', name: '无风', builtin: true, deletable: false, duration: 300, weight: 30, cropSway: 'both', cropAmp: 4, decorSway: 'none', decorAmp: 1, squash: 0, mood: 2, speed: 1 },
2473
+ { id: 'breeze', name: '微风', builtin: true, deletable: true, duration: 250, weight: 40, cropSway: 'left', cropAmp: 5, decorSway: 'none', decorAmp: 3, squash: 0, mood: 1, speed: 1.2 },
2474
+ { id: 'gale', name: '大风', builtin: true, deletable: true, duration: 100, weight: 20, cropSway: 'right', cropAmp: 13, decorSway: 'none', decorAmp: 9, squash: 0, mood: -6, speed: 2.4 },
2475
+ { id: 'egg', name: '彩蛋', builtin: true, deletable: true, duration: 300, weight: 10, cropSway: 'none', cropAmp: 0, decorSway: 'none', decorAmp: 0, squash: 0.45, mood: 4, speed: 3 },
2476
2476
  ];
2477
2477
 
2478
2478
  function normalizeWeathers(list) {
@@ -2546,7 +2546,7 @@ function ecologyPanel(ctx) {
2546
2546
  tickIntervalMs: d.tickIntervalMs,
2547
2547
  dailyEvent: d.dailyEvent,
2548
2548
  water: { ...d.water },
2549
- moodIntervalMin: d.moodIntervalMin,
2549
+ moodIntervalMin: d.moodIntervalMin,
2550
2550
  weathers: normalizeWeathers(d.weathers),
2551
2551
  }]);
2552
2552
  ui.toast(zh ? '已保存' : 'Saved');
@@ -2596,7 +2596,7 @@ function ecologyPanel(ctx) {
2596
2596
  row1.append(
2597
2597
  ui.field(zh ? '持续(秒)' : 'Duration (s)', num(w.duration, (v) => { w.duration = v; }, '1', '1')),
2598
2598
  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')),
2599
+ ui.field(zh ? '速度(倍率)' : 'Speed (x)', num(w.speed, (v) => { w.speed = v; }, '0.1', '0.2')),
2600
2600
  ui.field(zh ? '心情变化/结算' : 'Mood per settlement', num(w.mood, (v) => { w.mood = v; }, '1', '-100')),
2601
2601
  );
2602
2602
  const crop = ui.h('div', 'cf-row');
@@ -2651,8 +2651,8 @@ function ecologyPanel(ctx) {
2651
2651
  ui.field(zh ? '一次浇水补水' : 'Water gain', num(d.water.waterGain, (v) => { d.water.waterGain = v; }, '0.05', '0')),
2652
2652
  ui.field(zh ? '干透后枯萎天数' : 'Wilt days', num(d.water.wiltDays, (v) => { d.water.wiltDays = v; }, '0.1', '0.01')),
2653
2653
  );
2654
- const moodRow = ui.h('div', 'cf-row');
2655
- moodRow.append(ui.field(zh ? '心情结算间隔(真实分钟)' : 'Mood interval (real min)', num(d.moodIntervalMin, (v) => { d.moodIntervalMin = v; }, '1', '1')));
2654
+ const moodRow = ui.h('div', 'cf-row');
2655
+ moodRow.append(ui.field(zh ? '心情结算间隔(真实分钟)' : 'Mood interval (real min)', num(d.moodIntervalMin, (v) => { d.moodIntervalMin = v; }, '1', '1')));
2656
2656
  const weatherBox = ui.h('div', 'cf-list');
2657
2657
  d.weathers.forEach((w, i) => weatherBox.append(weatherCard(w, i)));
2658
2658
  const add = ui.button(zh ? '添加天气' : 'Add weather', {
package/icon.png ADDED
Binary file
package/package.json CHANGED
@@ -1,17 +1,19 @@
1
1
  {
2
2
  "name": "@lily-cooperative/custom-farm",
3
- "version": "0.33.5",
3
+ "version": "0.34.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },
9
- "description": "自定义农场(Custom Farm)—— 给 Coopanion 桌宠的种田扩展:18 种作物、浇水/生长/收割/卖钱,确定性 tick(纯代码,不唤模型),并把渲染用的 state.json 写给桌面图层。",
9
+ "description": "自定义农场(Custom Farm)—— 给 Coopanion 桌宠的种田扩展:18 种作物只是起点。农作物、杂草、装饰物、天气、状态、道具、合成表和贴图都能在面板里完全自定义;种、浇、收、吃、买卖全部本地确定性运行,不唤模型。",
10
10
  "keywords": [
11
11
  "cortico-world",
12
+ "coopanion",
12
13
  "farm",
13
14
  "种田",
14
- "自定义农场"
15
+ "自定义农场",
16
+ "customizable"
15
17
  ],
16
18
  "license": "MIT",
17
19
  "author": {
@@ -42,6 +44,7 @@
42
44
  "overlay-electron",
43
45
  "bag",
44
46
  "assets",
47
+ "icon.png",
45
48
  "README.md",
46
49
  "LICENSE"
47
50
  ],
@@ -53,6 +56,16 @@
53
56
  "kind": "world",
54
57
  "api": 6,
55
58
  "displayName": "自定义农场",
59
+ "locales": {
60
+ "zh": {
61
+ "displayName": "自定义农场",
62
+ "description": "18 种作物只是起点:农作物、杂草、装饰物、天气、状态、道具、合成表和贴图都能在面板里完全自定义,想种什么、长什么样、吃什么补什么,全由你决定。种、浇、收、吃、买卖全部本地确定性运行,不唤模型。"
63
+ },
64
+ "en": {
65
+ "displayName": "Custom Farm",
66
+ "description": "18 crops are just the beginning: crops, weeds, decorations, weather, states, items, recipes and sprites are all fully customizable in the panel. Plant, water, harvest, eat and trade locally with deterministic ticks — no model calls."
67
+ }
68
+ },
56
69
  "tags": [
57
70
  "game",
58
71
  "companion",
@@ -60,6 +73,7 @@
60
73
  "experimental"
61
74
  ],
62
75
  "describe": "describe.json",
76
+ "icon": "icon.png",
63
77
  "webUiClient": "dist/webui.js",
64
78
  "webUiStyle": "dist/webui.css"
65
79
  },
package/src/ENV_PROMPT.md CHANGED
@@ -62,14 +62,15 @@
62
62
  ## 状态与吃饭
63
63
 
64
64
  - 状态定义、初始值、每天变化、阈值在「状态自定义」;这一页只写你看到状态后怎么做。
65
- - 饥饿/数值偏低时直接吃;没有食物再提醒主人。
66
- - 不要自己改状态数值,也不要吃被设成不可消耗的东西。
65
+ - 默认带饱食度(0~100,初始 100,每天 -5;低于 30 → 饥饿)和心情(0~100,初始 60;>=70 开心,<30 无聊)。
66
+ - 饥饿/数值偏低时直接吃;每一种作物、米饭和意面默认都会加饱食度和心情,优先吃背包里有的。
67
+ - 没有食物再提醒主人;不要自己改状态数值,也不要吃被设成不可消耗的东西。
67
68
 
68
69
  ## 心情
69
70
 
70
71
  - 数值状态 mood 是心情(0~100,初始 60);happy 在 >=70 生效,bored 在 <30 生效。
71
72
  - 天气按「生态自定义」里的结算间隔改变心情:大风扣 6,无风 +2,微风 +1,彩蛋 +4;具体以设置页为准。
72
- - 每吃一个消耗品额外 +8 心情(和道具自身效果叠加)。
73
+ - 吃东西按道具自己的效果加心情(默认作物 +1~8、意面 +9);道具没配心情效果时才退回消耗行为的 +8。
73
74
  - bored 生效时:优先自己吃一个能吃的道具;没有食物就提醒主人,或者主动说一句。
74
75
  - happy 生效时:主动和主人说一句,不要在这时提要求或扫兴。
75
76
  - 收到 farm.state 且 meta 里有 moodState 时,按上面的规则说话/吃东西;不要自己改心情数值。
package/src/catalog.js CHANGED
@@ -30,6 +30,50 @@ const ORIGINAL_PRICES = structuredClone(CROP_PRICES);
30
30
 
31
31
  const RULE_OPS = ['<', '<=', '>', '>=', '==', '!='];
32
32
 
33
+ /** 默认心情/饱食度状态 id;用户改过 id 时由 dietStateIds() 解析实际值。 */
34
+ const SATIETY_ID = 'st_satiety';
35
+ const HUNGRY_ID = 'st_hungry';
36
+ const MOOD_ID = 'mood';
37
+
38
+ /**
39
+ * 每种可吃道具默认加多少饱食度 / 心情。
40
+ * 主食(土豆、玉米、南瓜、米饭、意面)饱食度高;水果心情高;辣椒、大蒜这类只当调味。
41
+ */
42
+ const DEFAULT_FOOD_EFFECTS = {
43
+ potato: { satiety: 24, mood: 2 },
44
+ carrot: { satiety: 16, mood: 2 },
45
+ tomato: { satiety: 14, mood: 4 },
46
+ strawberry: { satiety: 12, mood: 7 },
47
+ corn: { satiety: 22, mood: 3 },
48
+ pumpkin: { satiety: 28, mood: 4 },
49
+ beetroot: { satiety: 16, mood: 2 },
50
+ blueberry: { satiety: 10, mood: 7 },
51
+ bok_choy: { satiety: 14, mood: 2 },
52
+ cauliflower: { satiety: 16, mood: 2 },
53
+ cucumber: { satiety: 12, mood: 3 },
54
+ eggplant: { satiety: 15, mood: 2 },
55
+ garlic: { satiety: 6, mood: 1 },
56
+ pineapple: { satiety: 18, mood: 8 },
57
+ radish: { satiety: 10, mood: 2 },
58
+ red_chili: { satiety: 5, mood: 3 },
59
+ red_onion: { satiety: 8, mood: 1 },
60
+ watermelon: { satiety: 24, mood: 7 },
61
+ rice_ear: { satiety: 8, mood: 1 },
62
+ rice: { satiety: 30, mood: 3 },
63
+ egg: { satiety: 10, mood: 2 },
64
+ meat: { satiety: 20, mood: 2 },
65
+ egg_fried_rice: { satiety: 48, mood: 10 },
66
+ item_15: { satiety: 38, mood: 8 },
67
+ pasta: { satiety: 42, mood: 9 },
68
+ };
69
+
70
+ /** 默认状态定义(只在缺少时补,不会覆盖用户改过的同名状态)。 */
71
+ const DEFAULT_SATIETY_STATE = { id: SATIETY_ID, name: '饱食度', kind: 'number', min: 0, max: 100, initial: 100, driftPerDay: -5, unit: '' };
72
+ const DEFAULT_HUNGRY_STATE = { id: HUNGRY_ID, name: '饥饿', kind: 'flag', initialActive: false };
73
+
74
+ /** 饥饿/无聊时优先吃的道具顺序:作物在前,主食在后补满。 */
75
+ const DEFAULT_EDIBLE_ITEMS = ['tomato', 'potato', 'rice', 'pasta', 'egg_fried_rice', 'item_15', 'carrot', 'strawberry', 'corn', 'pumpkin', 'beetroot', 'bok_choy', 'cauliflower', 'cucumber', 'eggplant', 'garlic', 'pineapple', 'radish', 'red_chili', 'red_onion', 'watermelon'];
76
+
33
77
  /** 内置杂草:游戏里现成的三种野草/花,每个都有逐阶段贴图。 */
34
78
  const BUILTIN_WEEDS = [
35
79
  { key: 'weed', name: '野草', stages: 4, days: [0.45, 0.45, 0.45], weight: 55, prefix: 'sprite_resource_weeds', frames: 4, itemName: '', value: 0 },
@@ -116,12 +160,13 @@ export class FarmCatalog {
116
160
  /** Grok 画的食物/食材图标。菜品 buy=0(只在合成表里出)。 */
117
161
  builtinFoodItems() {
118
162
  return [
119
- { id: 'rice_ear', name: '稻穗', buy: 8, sell: 2, icon: 'icon_item_rice_ear' },
120
- { id: 'rice', name: '大米', buy: 15, sell: 4, icon: 'icon_item_rice' },
121
- { id: 'egg', name: '鸡蛋', buy: 6, sell: 2, icon: 'icon_item_egg' },
122
- { id: 'meat', name: '鲜肉', buy: 22, sell: 6, icon: 'icon_item_meat' },
123
- { id: 'egg_fried_rice', name: '蛋炒饭', buy: 0, sell: 35, icon: 'icon_item_egg_fried_rice' },
124
- { id: 'item_15', name: '番茄炒蛋', buy: 0, sell: 30, icon: 'icon_item_tomato_egg' },
163
+ { id: 'rice_ear', name: '稻穗', buy: 8, sell: 2, icon: 'icon_item_rice_ear', effects: this.defaultItemEffects('rice_ear') },
164
+ { id: 'rice', name: '大米', buy: 15, sell: 4, icon: 'icon_item_rice', effects: this.defaultItemEffects('rice') },
165
+ { id: 'egg', name: '鸡蛋', buy: 6, sell: 2, icon: 'icon_item_egg', effects: this.defaultItemEffects('egg') },
166
+ { id: 'meat', name: '鲜肉', buy: 22, sell: 6, icon: 'icon_item_meat', effects: this.defaultItemEffects('meat') },
167
+ { id: 'egg_fried_rice', name: '蛋炒饭', buy: 0, sell: 35, icon: 'icon_item_egg_fried_rice', effects: this.defaultItemEffects('egg_fried_rice') },
168
+ { id: 'item_15', name: '番茄炒蛋', buy: 0, sell: 30, icon: 'icon_item_tomato_egg', effects: this.defaultItemEffects('item_15') },
169
+ { id: 'pasta', name: '意大利面', buy: 0, sell: 55, icon: 'icon_item_pasta', effects: this.defaultItemEffects('pasta') },
125
170
  ];
126
171
  }
127
172
 
@@ -130,9 +175,130 @@ export class FarmCatalog {
130
175
  { id: 'recipe_rice', inputs: [{ item: 'rice_ear', count: 1 }], output: { item: 'rice', count: 1 } },
131
176
  { id: 'recipe_egg_fried_rice', inputs: [{ item: 'egg', count: 1 }, { item: 'rice', count: 1 }], output: { item: 'egg_fried_rice', count: 1 } },
132
177
  { id: 'recipe_tomato_egg', inputs: [{ item: 'egg', count: 1 }, { item: 'tomato', count: 1 }], output: { item: 'item_15', count: 1 } },
178
+ { id: 'recipe_pasta', inputs: [{ item: 'meat', count: 1 }, { item: 'tomato', count: 1 }], output: { item: 'pasta', count: 1 } },
179
+ ];
180
+ }
181
+
182
+ /** 用当前自定义状态 id 生成某道具的默认效果;状态还没建好时先写内置 id。 */
183
+ defaultItemEffects(id) {
184
+ const spec = DEFAULT_FOOD_EFFECTS[id];
185
+ if (!spec) return [];
186
+ const ids = this.dietStateIds();
187
+ return [
188
+ { state: ids.satietyId ?? SATIETY_ID, delta: spec.satiety },
189
+ { state: ids.moodId ?? MOOD_ID, delta: spec.mood },
133
190
  ];
134
191
  }
135
192
 
193
+ /** 找到当前存档里「饱食度 / 饥饿 / 心情」的实际 id;用户改过 id 或换过名字时也能接上。 */
194
+ dietStateIds() {
195
+ const states = this.store?.states ?? [];
196
+ const satiety = states.find((s) => s.kind === 'number' && (s.id === SATIETY_ID || s.name === '饱食度'));
197
+ const hungry = states.find((s) => s.kind === 'flag' && (s.id === HUNGRY_ID || s.name === '饥饿'));
198
+ const mood = states.find((s) => s.kind === 'number' && (s.id === MOOD_ID || s.name === '心情'));
199
+ return { satietyId: satiety?.id ?? null, hungryId: hungry?.id ?? null, moodId: mood?.id ?? null };
200
+ }
201
+
202
+ /** 默认饱食度/饥饿/条件/消耗行为;已有同名状态时只补缺的部分。 */
203
+ ensureSatiety(force = false) {
204
+ if (!this.store) return { satiety: null, hungry: null };
205
+ if (this.store.seededSatiety && !force) {
206
+ const ids = this.dietStateIds();
207
+ return { satiety: ids.satietyId, hungry: ids.hungryId };
208
+ }
209
+ const states = this.store.states ?? (this.store.states = []);
210
+ let changed = false;
211
+ let satiety = states.find((s) => s.kind === 'number' && (s.id === SATIETY_ID || s.name === '饱食度'));
212
+ if (!satiety) {
213
+ satiety = this.normalizeState(DEFAULT_SATIETY_STATE, states.length);
214
+ if (satiety) { states.push(satiety); changed = true; }
215
+ }
216
+ let hungry = states.find((s) => s.kind === 'flag' && (s.id === HUNGRY_ID || s.name === '饥饿'));
217
+ if (!hungry) {
218
+ hungry = this.normalizeState(DEFAULT_HUNGRY_STATE, states.length);
219
+ if (hungry) { states.push(hungry); changed = true; }
220
+ }
221
+ if (satiety && hungry) {
222
+ const rules = this.store.rules ?? (this.store.rules = []);
223
+ if (!rules.some((r) => r.source === satiety.id && r.target === hungry.id)) {
224
+ const rule = this.normalizeRule({ id: 'r_hungry', source: satiety.id, op: '<', value: 30, target: hungry.id }, rules.length, states);
225
+ if (rule) { rules.push(rule); changed = true; }
226
+ }
227
+ const actions = this.store.consume ?? (this.store.consume = []);
228
+ if (!actions.some((c) => c.trigger === hungry.id && (c.items ?? []).includes('tomato'))) {
229
+ const action = this.normalizeConsume({ id: 'c_hungry', trigger: hungry.id, items: DEFAULT_EDIBLE_ITEMS, count: 1, effectState: satiety.id, effectDelta: 25, cooldownDays: 0.5, auto: true, enabled: true }, actions.length, states);
230
+ if (action) { actions.push(action); changed = true; }
231
+ }
232
+ }
233
+ this.store.seededSatiety = true;
234
+ if (changed) this.persist();
235
+ return { satiety: satiety?.id ?? null, hungry: hungry?.id ?? null };
236
+ }
237
+
238
+ /** 旧档的饥饿消耗行为只列了几种作物;补上米饭/意面等新食物一次。 */
239
+ ensureEdibleItems() {
240
+ if (!this.store || this.store.seededEdibleItems) return;
241
+ const { hungryId } = this.dietStateIds();
242
+ let changed = false;
243
+ if (hungryId) {
244
+ for (const action of this.store.consume ?? []) {
245
+ if (action.trigger !== hungryId || !Array.isArray(action.items)) continue;
246
+ if (action.items.includes('pasta')) continue;
247
+ action.items = [...new Set([...action.items, ...DEFAULT_EDIBLE_ITEMS])].slice(0, 24);
248
+ changed = true;
249
+ }
250
+ }
251
+ this.store.seededEdibleItems = true;
252
+ if (changed) this.persist();
253
+ }
254
+
255
+ /** 给每种作物和食物补默认的 +饱食度 / +心情;只填空效果,不覆盖用户自己配的。 */
256
+ ensureDietEffects(force = false) {
257
+ if (!this.store) return;
258
+ if (this.store.seededDietEffects && !force) return;
259
+ const { satietyId, moodId } = this.dietStateIds();
260
+ if (!satietyId || !moodId) return;
261
+ let changed = false;
262
+ const list = this.store.items ?? (this.store.items = []);
263
+ const builtinFoods = new Map(this.builtinFoodItems().map((it) => [it.id, it]));
264
+ for (const [id, spec] of Object.entries(DEFAULT_FOOD_EFFECTS)) {
265
+ let item = list.find((it) => it.id === id);
266
+ if (!item) {
267
+ const def = builtinFoods.get(id);
268
+ const crop = this.store.crops.find((c) => c.key === id && c.enabled !== false);
269
+ if (def) {
270
+ item = this.normalizeItem({ ...def, kind: 'consumable', consumable: true, effects: [] }, list.length, this.store.states);
271
+ } else if (crop) {
272
+ item = this.normalizeItem({
273
+ id, kind: 'crop', crop: id, name: crop.name,
274
+ icon: crop.itemSprite ?? 'icon_item_soil', buy: 0, sell: crop.value,
275
+ consumable: true, effects: [],
276
+ }, list.length, this.store.states);
277
+ }
278
+ if (!item) continue; // 作物已被用户删掉:不强行加回来
279
+ list.push(item);
280
+ changed = true;
281
+ }
282
+ if (!Array.isArray(item.effects) || item.effects.length === 0) {
283
+ item.effects = [
284
+ { state: satietyId, delta: spec.satiety },
285
+ { state: moodId, delta: spec.mood },
286
+ ];
287
+ changed = true;
288
+ }
289
+ }
290
+ // 旧存档里被清理掉的意面补回来:道具 + 用现有食材(肉 + 番茄)合成。
291
+ const pasta = list.find((it) => it.id === 'pasta');
292
+ if (pasta && !pasta.icon) { pasta.icon = 'icon_item_pasta'; changed = true; }
293
+ const recipes = this.store.recipes ?? (this.store.recipes = []);
294
+ if (!recipes.some((r) => r.id === 'recipe_pasta')) {
295
+ const recipe = this.normalizeRecipe(this.builtinFoodRecipes().find((r) => r.id === 'recipe_pasta'), recipes.length);
296
+ if (recipe) { recipes.push(recipe); changed = true; }
297
+ }
298
+ this.store.seededDietEffects = true;
299
+ if (changed) this.persist();
300
+ }
301
+
136
302
  /** 老存档补一次食物/配方;只补一次。 */
137
303
  ensureFood() {
138
304
  if (!this.store || this.store.seededFood) return;
@@ -140,13 +306,13 @@ export class FarmCatalog {
140
306
  for (const def of this.builtinFoodItems()) {
141
307
  const idx = list.findIndex((it) => it.id === def.id);
142
308
  if (idx < 0) {
143
- const item = this.normalizeItem({ ...def, kind: 'consumable', consumable: true, effects: [] }, list.length, this.store.states ?? []);
309
+ const item = this.normalizeItem({ ...def, kind: 'consumable', consumable: true, effects: def.effects ?? [] }, list.length, this.store.states ?? []);
144
310
  if (item) list.push(item);
145
311
  } else if (def.id === 'item_15') {
146
- // 旧番茄炒蛋道具改成 Grok 图标
312
+ // 旧番茄炒蛋道具改成 Grok 图标;效果不清空,后面 ensureDietEffects 会补默认值。
147
313
  list[idx] = this.normalizeItem({
148
314
  ...list[idx], name: def.name, icon: def.icon, buy: def.buy, sell: def.sell,
149
- kind: 'consumable', consumable: true, effects: [],
315
+ kind: 'consumable', consumable: true,
150
316
  }, idx, this.store.states ?? []) ?? list[idx];
151
317
  }
152
318
  }
@@ -224,6 +390,9 @@ export class FarmCatalog {
224
390
  seededFreeCrops: false,
225
391
  seededFood: false,
226
392
  seededMood: false,
393
+ seededSatiety: false,
394
+ seededDietEffects: false,
395
+ seededEdibleItems: false,
227
396
  };
228
397
  }
229
398
 
@@ -263,10 +432,13 @@ export class FarmCatalog {
263
432
  } catch { raw = null; }
264
433
  this.store = this.normalizeStore(raw);
265
434
  this.ensureFreeCrops();
266
- this.ensureFood();
267
435
  this.ensureMood();
436
+ this.ensureSatiety();
437
+ this.ensureFood();
268
438
  this.ensureRiceEar();
269
439
  this.ensureRemovedFood();
440
+ this.ensureDietEffects();
441
+ this.ensureEdibleItems();
270
442
  this.ensureBuiltinDecor();
271
443
  this.ensureCabin();
272
444
  this.ensureDecorSet();
@@ -292,7 +464,7 @@ export class FarmCatalog {
292
464
  addRule({ id: 'r_bored', source: 'mood', op: '<', value: 30, target: 'bored' });
293
465
  const actions = this.store.consume ?? (this.store.consume = []);
294
466
  if (!actions.some((c) => c.id === 'c_bored_eat')) {
295
- const items = ['tomato', 'egg_fried_rice', 'rice', 'egg', 'carrot', 'potato', 'strawberry', 'corn', 'pumpkin', 'beetroot', 'bok_choy', 'cauliflower', 'cucumber', 'eggplant', 'garlic', 'pineapple', 'radish', 'red_chili', 'red_onion', 'watermelon'];
467
+ const items = ['tomato', 'egg_fried_rice', 'rice', 'pasta', 'egg', 'carrot', 'potato', 'strawberry', 'corn', 'pumpkin', 'beetroot', 'bok_choy', 'cauliflower', 'cucumber', 'eggplant', 'garlic', 'pineapple', 'radish', 'red_chili', 'red_onion', 'watermelon'];
296
468
  const action = this.normalizeConsume({ id: 'c_bored_eat', trigger: 'bored', items, count: 1, effectState: 'mood', effectDelta: 8, cooldownDays: 0.15, auto: true, enabled: true }, actions.length, states);
297
469
  if (action) actions.push(action);
298
470
  }
@@ -316,7 +488,7 @@ export class FarmCatalog {
316
488
  this.persist();
317
489
  }
318
490
 
319
- /** 小麦/面粉/意面没有免费图标,整条链下架;旧存档里的道具和配方一起清掉。 */
491
+ /** 小麦/面粉没有免费图标,整条链下架;意面改用肉 + 番茄的配方保留。 */
320
492
  /** 稻穗容易被误删;它是稻米链的入口,始终保留一个默认道具。 */
321
493
  ensureRiceEar() {
322
494
  if (!this.store) return;
@@ -324,7 +496,7 @@ export class FarmCatalog {
324
496
  if (list.some((it) => it.id === 'rice_ear')) return;
325
497
  const def = this.builtinFoodItems().find((it) => it.id === 'rice_ear');
326
498
  if (!def) return;
327
- const item = this.normalizeItem({ ...def, kind: 'consumable', consumable: true, effects: [] }, list.length, this.store.states ?? []);
499
+ const item = this.normalizeItem({ ...def, kind: 'consumable', consumable: true, effects: def.effects ?? [] }, list.length, this.store.states ?? []);
328
500
  if (item) {
329
501
  list.push(item);
330
502
  if (!(this.store.recipes ?? []).some((r) => r.id === 'recipe_rice')) {
@@ -337,7 +509,7 @@ export class FarmCatalog {
337
509
 
338
510
  ensureRemovedFood() {
339
511
  if (!this.store) return;
340
- const removed = new Set(['wheat', 'flour', 'pasta', 'recipe_flour', 'recipe_pasta']);
512
+ const removed = new Set(['wheat', 'flour', 'recipe_flour']);
341
513
  const beforeItems = this.store.items?.length ?? 0;
342
514
  const beforeRecipes = this.store.recipes?.length ?? 0;
343
515
  this.store.items = (this.store.items ?? []).filter((it) => !removed.has(it.id));
@@ -451,6 +623,9 @@ export class FarmCatalog {
451
623
  seededFreeCrops: Boolean(raw.seededFreeCrops),
452
624
  seededFood: Boolean(raw.seededFood),
453
625
  seededMood: Boolean(raw.seededMood),
626
+ seededSatiety: Boolean(raw.seededSatiety),
627
+ seededDietEffects: Boolean(raw.seededDietEffects),
628
+ seededEdibleItems: Boolean(raw.seededEdibleItems),
454
629
  };
455
630
  }
456
631
 
@@ -1437,38 +1612,13 @@ export class FarmCatalog {
1437
1612
  this.apply();
1438
1613
  }
1439
1614
 
1440
- /** 一键加示例:饱食度 0~100、每天 -5;低于 30 → 饥饿生效。 */
1615
+ /** 一键补齐示例:饱食度 0~100、每天 -5;低于 30 → 饥饿;并给所有食物补默认效果。 */
1441
1616
  addExample() {
1442
- if (this.store.states.some((s) => s.name === '饱食度' || s.name === '饥饿')) {
1443
- throw new Error('已经有「饱食度」或「饥饿」了。');
1444
- }
1445
- let sid = 'st_satiety';
1446
- while (this.store.states.some((s) => s.id === sid)) sid += '_2';
1447
- let hid = 'st_hungry';
1448
- while (this.store.states.some((s) => s.id === hid)) hid += '_2';
1449
- this.store.states.push(
1450
- { id: sid, name: '饱食度', kind: 'number', min: 0, max: 100, initial: 100, driftPerDay: -5, unit: '' },
1451
- { id: hid, name: '饥饿', kind: 'flag', initialActive: false },
1452
- );
1453
- let rid = 'r_hungry';
1454
- while (this.store.rules.some((r) => r.id === rid)) rid += '_2';
1455
- this.store.rules.push({ id: rid, source: sid, op: '<', value: 30, target: hid });
1456
- let cid = 'c_hungry';
1457
- while (this.store.consume.some((c) => c.id === cid)) cid += '_2';
1458
- this.store.consume.push({
1459
- id: cid,
1460
- trigger: hid,
1461
- items: ['tomato', 'potato', 'carrot', 'corn', 'pumpkin', 'strawberry'],
1462
- count: 1,
1463
- effectState: sid,
1464
- effectDelta: 25,
1465
- cooldownDays: 0.5,
1466
- auto: true,
1467
- enabled: true,
1468
- });
1617
+ const ids = this.ensureSatiety(true);
1618
+ this.ensureDietEffects(true);
1469
1619
  this.persist();
1470
1620
  this.apply();
1471
- return { satiety: sid, hungry: hid };
1621
+ return { satiety: ids.satiety, hungry: ids.hungry };
1472
1622
  }
1473
1623
 
1474
1624
  groundTile() {