@lily-cooperative/custom-farm 0.33.6 → 0.34.1
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/describe.json +356 -0
- package/dist/webui.js +14 -14
- package/icon.png +0 -0
- package/package.json +18 -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.1` |
|
|
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/describe.json
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tools": [
|
|
3
|
+
{
|
|
4
|
+
"name": "farm_status",
|
|
5
|
+
"description": "查看田地:每棵作物的位置 xPct(屏幕底部百分比)、等级、成熟进度、水分、是否熟了/枯萎了、还能收几次,以及背包里有什么、地上掉了什么、野草有几丛/哪些能收、主人自定义的状态(数值 + 开关)当前是多少。最后一行「最近田里的变化」会列出最近少掉的棵和原因——田空了先看这行,别当成存档被清。想知道情况就查这个,田地不会主动推送生长进度。",
|
|
6
|
+
"parameters": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {}
|
|
9
|
+
},
|
|
10
|
+
"tags": [
|
|
11
|
+
"read",
|
|
12
|
+
"snapshot"
|
|
13
|
+
],
|
|
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)."
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "farm_stand",
|
|
21
|
+
"description": "站到某个位置。at 必填:先用桌宠自己的 pet_move_to 走过去,再把到达的位置(回执里那个百分比除以 100)填进 at。给了 plot 还会顺便校验你有没有真的站到那棵旁边。播种、收割、捡东西都要靠它放行。",
|
|
22
|
+
"parameters": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"properties": {
|
|
25
|
+
"at": {
|
|
26
|
+
"type": "number",
|
|
27
|
+
"minimum": 0,
|
|
28
|
+
"maximum": 1,
|
|
29
|
+
"description": "你现在的位置(0~1),pet_move_to 回执里那个百分比除以 100。必填。"
|
|
30
|
+
},
|
|
31
|
+
"plot": {
|
|
32
|
+
"type": "integer",
|
|
33
|
+
"description": "可选:顺便声明你站在几号地旁边。"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"required": [
|
|
37
|
+
"at"
|
|
38
|
+
],
|
|
39
|
+
"additionalProperties": false
|
|
40
|
+
},
|
|
41
|
+
"tags": [
|
|
42
|
+
"write"
|
|
43
|
+
],
|
|
44
|
+
"summary": {
|
|
45
|
+
"zh": "站到某个位置(at 必填),播种/收割/捡东西都要先站过去。",
|
|
46
|
+
"en": "Stand at a position (at is required); planting, harvesting and collecting all need it."
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "farm_plant",
|
|
51
|
+
"description": "在脚下种一棵。必须先用 pet_move_to 走过去并 farm_stand 报好位置。种一棵要消耗 1 包对应种子(背包里的 seed_tomato 这种);没有种子就自己补 1~2 包(farm_shop 看价、farm_buy 买),不用问主人。crop 可以填作物名(番茄/南瓜/玉米/土豆/草莓/小白菜等),不填就按轮作自动选;x 不填就种在脚下,也可以自己指定 0~1,但必须在当前农田范围内(农田始终以屏幕中心为中线,宽度可调)。默认允许轻微重叠,太挤会被拒绝并告诉你已经种在哪些百分比。",
|
|
52
|
+
"parameters": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"properties": {
|
|
55
|
+
"crop": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "作物名或者 key;不填自动轮作。"
|
|
58
|
+
},
|
|
59
|
+
"x": {
|
|
60
|
+
"type": "number",
|
|
61
|
+
"minimum": 0,
|
|
62
|
+
"maximum": 1,
|
|
63
|
+
"description": "种在屏幕底部哪个位置;不填就种在脚下。"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"additionalProperties": false
|
|
67
|
+
},
|
|
68
|
+
"tags": [
|
|
69
|
+
"write"
|
|
70
|
+
],
|
|
71
|
+
"summary": {
|
|
72
|
+
"zh": "在脚下种一棵:要消耗 1 包对应种子(种子在背包里,没有就自己补 1~2 包),位置可自己挑,两棵不能重叠。",
|
|
73
|
+
"en": "Plant one at your feet; consumes one seed pack from the bag (restock 1–2 packs yourself if empty), position optional, no overlap."
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "farm_water",
|
|
78
|
+
"description": "浇水:走到一棵旁边(先 pet_move_to + farm_stand),一次把够得着的这一片全浇透(含旁边几棵,不用一株一株浇)。给 plot 就以那棵为中心,省略就以你站的位置为中心。已经浇透的会跳过。太久不浇会枯萎,枯了浇不活,要拔掉重种。",
|
|
79
|
+
"parameters": {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"properties": {
|
|
82
|
+
"plot": {
|
|
83
|
+
"type": "integer",
|
|
84
|
+
"description": "可选:以这棵为中心浇它和旁边够得着的;省略则浇你脚下够得着的全部。"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"additionalProperties": false
|
|
88
|
+
},
|
|
89
|
+
"tags": [
|
|
90
|
+
"write"
|
|
91
|
+
],
|
|
92
|
+
"summary": {
|
|
93
|
+
"zh": "浇水:一次浇透脚下够得着的一片(含旁边几棵),已经浇透的会跳过。",
|
|
94
|
+
"en": "Water the whole reachable patch in one call (neighbours included); already-full plants are skipped."
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "farm_harvest",
|
|
99
|
+
"description": "收割脚下这棵熟的作物,或者一丛长成、带收获物的杂草。产物会掉在地上,不会直接进背包——收完再 farm_collect 才拿到手。必须站过去。番茄能收 4 次、草莓 5 次,收完退回中间等级继续长;小麦/土豆/卷心菜/玉米/南瓜只能收 1 次,收完就拔掉(所以收一次地里就少一棵,不是 bug)。杂草要长到最高阶段才收得动;没配收获物的杂草只能靠 farm_plant 铲掉。",
|
|
100
|
+
"parameters": {
|
|
101
|
+
"type": "object",
|
|
102
|
+
"properties": {
|
|
103
|
+
"plot": {
|
|
104
|
+
"type": "integer",
|
|
105
|
+
"description": "收哪一棵;省略则收脚下这棵。"
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"additionalProperties": false
|
|
109
|
+
},
|
|
110
|
+
"tags": [
|
|
111
|
+
"write"
|
|
112
|
+
],
|
|
113
|
+
"summary": {
|
|
114
|
+
"zh": "收脚下这棵熟的,产物掉在地上,要用 farm_collect 捡。",
|
|
115
|
+
"en": "Harvest the ripe plant at your feet; the produce drops on the ground and must be collected."
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "farm_pull",
|
|
120
|
+
"description": "拔掉一棵枯萎的作物(要站在它跟前)。枯萎的作物浇不活,只能拔掉重种;拔的时候会冒一团烟。",
|
|
121
|
+
"parameters": {
|
|
122
|
+
"type": "object",
|
|
123
|
+
"properties": {
|
|
124
|
+
"plot": {
|
|
125
|
+
"type": "integer",
|
|
126
|
+
"description": "拔哪一棵;省略则拔脚下这棵。"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"additionalProperties": false
|
|
130
|
+
},
|
|
131
|
+
"tags": [
|
|
132
|
+
"write"
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"name": "farm_sell",
|
|
137
|
+
"description": "把背包里有出售价的道具卖掉换金币(作物/种子/消耗品/装饰)。没在「道具自定义」里设出售价的不能卖。不给 count 就全卖。",
|
|
138
|
+
"parameters": {
|
|
139
|
+
"type": "object",
|
|
140
|
+
"properties": {
|
|
141
|
+
"item": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"description": "背包里的 item id 或名字,比如 tomato / 番茄 / seed_tomato。"
|
|
144
|
+
},
|
|
145
|
+
"count": {
|
|
146
|
+
"type": "integer",
|
|
147
|
+
"description": "卖几个;省略=全卖。"
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"required": [
|
|
151
|
+
"item"
|
|
152
|
+
],
|
|
153
|
+
"additionalProperties": false
|
|
154
|
+
},
|
|
155
|
+
"tags": [
|
|
156
|
+
"write"
|
|
157
|
+
]
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"name": "farm_shop",
|
|
161
|
+
"description": "看商店:列出所有设了购买价的种子/消耗品/装饰(买价、卖价)。没设购买价的不在商店;没设出售价的不能卖。",
|
|
162
|
+
"parameters": {
|
|
163
|
+
"type": "object",
|
|
164
|
+
"properties": {
|
|
165
|
+
"kind": {
|
|
166
|
+
"type": "string",
|
|
167
|
+
"enum": [
|
|
168
|
+
"seed",
|
|
169
|
+
"consumable",
|
|
170
|
+
"decor",
|
|
171
|
+
"furniture",
|
|
172
|
+
"all"
|
|
173
|
+
],
|
|
174
|
+
"description": "看哪一类;默认 all。"
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
"additionalProperties": false
|
|
178
|
+
},
|
|
179
|
+
"tags": [
|
|
180
|
+
"read"
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"name": "farm_buy",
|
|
185
|
+
"description": "买设了购买价的道具(种子/消耗品/装饰):id 是道具 id(seed_tomato),count 一次买几个;也可以 items:[{id,count}] 一次买多种。买来先进背包;装饰要再用 farm_place 摆。种子不够可以自己补 1~2 包,不用问;买装饰/大件先问主人。",
|
|
186
|
+
"parameters": {
|
|
187
|
+
"type": "object",
|
|
188
|
+
"properties": {
|
|
189
|
+
"id": {
|
|
190
|
+
"type": "string",
|
|
191
|
+
"description": "道具 id(seed_tomato / coffee / scarecrow)"
|
|
192
|
+
},
|
|
193
|
+
"count": {
|
|
194
|
+
"type": "integer",
|
|
195
|
+
"description": "买几个(默认 1,最多 99)"
|
|
196
|
+
},
|
|
197
|
+
"items": {
|
|
198
|
+
"type": "array",
|
|
199
|
+
"description": "一次买多种(推荐,省 token):[{id:\"seed_tomato\",count:1},{id:\"coffee\",count:2}]",
|
|
200
|
+
"items": {
|
|
201
|
+
"type": "object",
|
|
202
|
+
"properties": {
|
|
203
|
+
"id": {
|
|
204
|
+
"type": "string"
|
|
205
|
+
},
|
|
206
|
+
"count": {
|
|
207
|
+
"type": "integer"
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
"required": [
|
|
211
|
+
"id"
|
|
212
|
+
],
|
|
213
|
+
"additionalProperties": false
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
"additionalProperties": false
|
|
218
|
+
},
|
|
219
|
+
"tags": [
|
|
220
|
+
"write"
|
|
221
|
+
]
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"name": "farm_place",
|
|
225
|
+
"description": "把背包里的装饰道具放到农场地面上(消耗 1 个)。「上」层装饰和杂草同层,会清掉落点杂草;「下」层在作物后方、不清草。x 不填就放在你当前站的位置。必须先买/合成到背包里,且道具在「道具自定义」里配了摆放贴图。",
|
|
226
|
+
"parameters": {
|
|
227
|
+
"type": "object",
|
|
228
|
+
"properties": {
|
|
229
|
+
"item": {
|
|
230
|
+
"type": "string",
|
|
231
|
+
"description": "装饰道具 id 或名字(比如 scarecrow / 稻草人)"
|
|
232
|
+
},
|
|
233
|
+
"x": {
|
|
234
|
+
"type": "number",
|
|
235
|
+
"description": "放到屏宽的百分之几(0~1);省略=你站的位置"
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
"required": [
|
|
239
|
+
"item"
|
|
240
|
+
],
|
|
241
|
+
"additionalProperties": false
|
|
242
|
+
},
|
|
243
|
+
"tags": [
|
|
244
|
+
"write"
|
|
245
|
+
]
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"name": "farm_bag",
|
|
249
|
+
"description": "打开(或唤到前面)背包面板。背包里是收下来的作物,可以看每种有多少;面板本身只读,东西还是靠种/收/捡进出。",
|
|
250
|
+
"parameters": {
|
|
251
|
+
"type": "object",
|
|
252
|
+
"properties": {}
|
|
253
|
+
},
|
|
254
|
+
"tags": [
|
|
255
|
+
"write"
|
|
256
|
+
]
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"name": "farm_collect",
|
|
260
|
+
"description": "把脚下够得着的掉落物收进背包。必须站过去(先 farm_stand 报位置)。地上没有就报错并告诉你掉落物在哪些百分比。",
|
|
261
|
+
"parameters": {
|
|
262
|
+
"type": "object",
|
|
263
|
+
"properties": {}
|
|
264
|
+
},
|
|
265
|
+
"tags": [
|
|
266
|
+
"write"
|
|
267
|
+
],
|
|
268
|
+
"summary": {
|
|
269
|
+
"zh": "把脚下的掉落物收进背包。",
|
|
270
|
+
"en": "Pick up the drops around your feet into the bag."
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"name": "farm_consume",
|
|
275
|
+
"description": "吃掉背包里的作物 / 杂草收获物,用来缓解「饥饿」这类状态。需要主人先在「状态自定义」里配好消耗行为(触发状态 + 可消耗道具 + 状态效果)。item 不填就自动挑背包里第一种可吃的;count 默认 1。吃掉后会按配置加减状态值(通常是饱食度)。",
|
|
276
|
+
"parameters": {
|
|
277
|
+
"type": "object",
|
|
278
|
+
"properties": {
|
|
279
|
+
"item": {
|
|
280
|
+
"type": "string",
|
|
281
|
+
"description": "可选:产物 id 或名字(比如 tomato / 番茄)。不填自动挑。"
|
|
282
|
+
},
|
|
283
|
+
"count": {
|
|
284
|
+
"type": "integer",
|
|
285
|
+
"minimum": 1,
|
|
286
|
+
"maximum": 99,
|
|
287
|
+
"description": "吃几个,默认 1。"
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
"additionalProperties": false
|
|
291
|
+
},
|
|
292
|
+
"tags": [
|
|
293
|
+
"write"
|
|
294
|
+
],
|
|
295
|
+
"summary": {
|
|
296
|
+
"zh": "吃掉背包里的作物/杂草收获物,缓解「饥饿」这类状态(要主人在状态自定义里配好消耗行为)。",
|
|
297
|
+
"en": "Eat a crop/weed produce to relieve states like hunger (configured in the status page)."
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
"name": "farm_craft",
|
|
302
|
+
"description": "按「道具合成表」消耗背包里的材料合成道具。recipe 填合成表 id;不填就列出所有合成表和材料。材料不够会报缺什么。",
|
|
303
|
+
"parameters": {
|
|
304
|
+
"type": "object",
|
|
305
|
+
"properties": {
|
|
306
|
+
"recipe": {
|
|
307
|
+
"type": "string",
|
|
308
|
+
"description": "合成表 id(不填=列出所有合成表)"
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
"additionalProperties": false
|
|
312
|
+
},
|
|
313
|
+
"tags": [
|
|
314
|
+
"write"
|
|
315
|
+
],
|
|
316
|
+
"summary": {
|
|
317
|
+
"zh": "按「道具合成表」消耗背包材料合成道具;不填 recipe 就列出所有表。",
|
|
318
|
+
"en": "Craft from the configured recipes; without recipe it lists all recipes."
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
],
|
|
322
|
+
"events": [
|
|
323
|
+
{
|
|
324
|
+
"type": "farm.ripe",
|
|
325
|
+
"description": {
|
|
326
|
+
"zh": "有作物成熟了;正文写明哪几棵、在屏幕底部百分之几,并提示走过去收、收完再捡。",
|
|
327
|
+
"en": "Some plants ripened; the text names them and their position, and says to walk over, harvest, then collect."
|
|
328
|
+
},
|
|
329
|
+
"trigger": "debounce"
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
"type": "farm.dry",
|
|
333
|
+
"description": {
|
|
334
|
+
"zh": "有作物土太干了;正文列出哪几棵、在屏幕底部百分之几,提示走过去浇水。",
|
|
335
|
+
"en": "Some plants are running dry; the text names them and their position and says to water them."
|
|
336
|
+
},
|
|
337
|
+
"trigger": "debounce"
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
"type": "farm.day",
|
|
341
|
+
"description": {
|
|
342
|
+
"zh": "翻到新的一天;正文带上天气、可收数量、地上的掉落物、土干的数量和背包件数。",
|
|
343
|
+
"en": "A new day started: weather, ripe count, drops on the ground, dry plants and bag size."
|
|
344
|
+
},
|
|
345
|
+
"trigger": "debounce"
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
"type": "farm.state",
|
|
349
|
+
"description": {
|
|
350
|
+
"zh": "自定义状态触发消耗行为:比如「饥饿」生效时自动吃掉一个作物(正文写明吃了什么、状态变化),或者提醒桌宠用 farm_consume 去吃;背包里没有可吃道具时也会提醒。",
|
|
351
|
+
"en": "A custom state triggered a consume action: e.g. hunger ate a crop (the text says what and the state change), or asks the pet to use farm_consume; also fires when nothing edible is in the bag."
|
|
352
|
+
},
|
|
353
|
+
"trigger": "debounce"
|
|
354
|
+
}
|
|
355
|
+
]
|
|
356
|
+
}
|
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.1",
|
|
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,8 @@
|
|
|
42
44
|
"overlay-electron",
|
|
43
45
|
"bag",
|
|
44
46
|
"assets",
|
|
47
|
+
"icon.png",
|
|
48
|
+
"describe.json",
|
|
45
49
|
"README.md",
|
|
46
50
|
"LICENSE"
|
|
47
51
|
],
|
|
@@ -53,6 +57,16 @@
|
|
|
53
57
|
"kind": "world",
|
|
54
58
|
"api": 6,
|
|
55
59
|
"displayName": "自定义农场",
|
|
60
|
+
"locales": {
|
|
61
|
+
"zh": {
|
|
62
|
+
"displayName": "自定义农场",
|
|
63
|
+
"description": "18 种作物只是起点:农作物、杂草、装饰物、天气、状态、道具、合成表和贴图都能在面板里完全自定义,想种什么、长什么样、吃什么补什么,全由你决定。种、浇、收、吃、买卖全部本地确定性运行,不唤模型。"
|
|
64
|
+
},
|
|
65
|
+
"en": {
|
|
66
|
+
"displayName": "Custom Farm",
|
|
67
|
+
"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."
|
|
68
|
+
}
|
|
69
|
+
},
|
|
56
70
|
"tags": [
|
|
57
71
|
"game",
|
|
58
72
|
"companion",
|
|
@@ -60,7 +74,7 @@
|
|
|
60
74
|
"experimental"
|
|
61
75
|
],
|
|
62
76
|
"describe": "describe.json",
|
|
63
|
-
"icon": "
|
|
77
|
+
"icon": "icon.png",
|
|
64
78
|
"webUiClient": "dist/webui.js",
|
|
65
79
|
"webUiStyle": "dist/webui.css"
|
|
66
80
|
},
|
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() {
|