@lily-cooperative/custom-farm 0.33.6 → 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 +37 -5
- package/assets/sprites/icon_item_pasta.png +0 -0
- package/dist/webui.js +14 -14
- package/icon.png +0 -0
- package/package.json +17 -4
- package/src/ENV_PROMPT.md +4 -3
- package/src/catalog.js +193 -43
package/README.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# 自定义农场 · custom-farm
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
给 Coopanion 桌宠的种田扩展:18 种作物只是起点。**农作物、杂草、装饰物、天气、状态、道具、合成表和贴图都能在面板里完全自定义**——想种什么、长什么样、吃什么补什么,全由你决定。
|
|
4
|
+
|
|
5
|
+
桌宠在**任务栏上沿**种地:任意多棵(不能重叠)、浇水会干、太久不浇会枯萎、
|
|
4
6
|
有的作物能反复收;收下来的东西**掉在地上**,桌宠要走过去捡,捡起来才进背包。
|
|
5
7
|
|
|
6
8
|
## 版本与依赖
|
|
7
9
|
|
|
8
10
|
| 项目 | 要求 |
|
|
9
11
|
|---|---|
|
|
10
|
-
| 当前扩展版本 | `0.
|
|
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` |
|
|
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 #
|
|
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
|
-
| **添加示例** |
|
|
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: '
|
|
1094
|
-
exampleDone: 'Example
|
|
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.
|
|
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
|
|
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,7 +73,7 @@
|
|
|
60
73
|
"experimental"
|
|
61
74
|
],
|
|
62
75
|
"describe": "describe.json",
|
|
63
|
-
"icon": "
|
|
76
|
+
"icon": "icon.png",
|
|
64
77
|
"webUiClient": "dist/webui.js",
|
|
65
78
|
"webUiStyle": "dist/webui.css"
|
|
66
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
|
-
-
|
|
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,
|
|
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', '
|
|
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
|
-
/**
|
|
1615
|
+
/** 一键补齐示例:饱食度 0~100、每天 -5;低于 30 → 饥饿;并给所有食物补默认效果。 */
|
|
1441
1616
|
addExample() {
|
|
1442
|
-
|
|
1443
|
-
|
|
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:
|
|
1621
|
+
return { satiety: ids.satiety, hungry: ids.hungry };
|
|
1472
1622
|
}
|
|
1473
1623
|
|
|
1474
1624
|
groundTile() {
|