简介:适合饥荒模组开发者的烹饪锅食物Lua源码包,完整展示通过Lua为《饥荒》制作自定义食谱模组的关键逻辑,内容涉及物品定义、食物属性、食谱注册、特殊效果处理等核心模块,例如创建“猪宠物食品”这类食谱的完整写法。资源共10个文件,以4个lua脚本为主体,辅以tex纹理、xml配置和png预览图,覆盖模组信息、主逻辑、自定义食物与贴图等部分,整体仅17KB,轻量但结构清楚。已有372人学习下载,适合想入门或进阶饥荒mod编程的玩家。通过源码可快速理解如何创建自定义食物,学习食材组合、烹饪时间、回调函数及模组集成写法,同时掌握调试思路和与其他模组兼容的处理方法,是动手实践Lua游戏开发的实用范例。
1. 烹饪锅食物源码到底要复刻什么:先解决「四格食材怎么变成一个菜」
搜索「饥荒 烹饪锅食物 源码」的人,大概率不是想下载一个 mod,而是想把游戏里那口锅的合成逻辑搬进自己的项目里:做服务器自定义菜谱、写合成模拟器,或者单纯想搞懂为什么四个普通浆果加一块肉能出锅肉丸。我最早做这个方向时,以为难点是「记住所有配方」,后来发现真正的问题是食材标签、肉度值、新鲜度这些隐藏规则怎么在代码里建模。下面的方案用 Python 给你一套能直接跑的模拟器源码,不需要拿到游戏本体,也不依赖任何第三方库。适合两类人:想给饥荒 mod 加自定义菜谱的,以及想做合成模拟器/工具站的开发者。你只需要理解清楚「食材模型 + 匹配引擎」这两件事,剩下的都是配置数据。
2. 先把食材模型建对:标签系统和配方表是烹饪锅源码的地基
2.1 为什么饥荒配方必须用「标签 + 数值」而不是按物品 ID 匹配
如果你第一次写烹饪锅食物源码,最容易犯的错是手写枚举:肉丸配方直接写「需要大肉、小肉、怪物肉……」,然后发现鱼、蛙腿、鸡腿也能做肉丸时,配方表直接爆炸。饥荒的真实机制其实分三层:物品 ID是唯一标识,标签描述它属于哪一类,数值表示它在某个分类里的权重。比如大肉values={"meat_value": 2.0},小肉是 1.0,怪物肉是 1.0 但额外带monster标签。这样配方只跟数值和标签打交道,跟具体物品解耦。
这里有一个非常反直觉的坑:火龙果在游戏里的分类是蔬菜,不是水果。它的 tag 是veggie,fruit_value=0。如果你按日常经验把它归类成水果,后面火龙果派的匹配逻辑就永远不对。所以建模时不能靠名字猜,要么查游戏 wiki,要么从一个可靠的食材表反推。下面是我常用的食材表结构,只做了演示数据,真实 mod 请在 config 里按你的游戏版本调整:
| 食材 | 内部 ID | 标签 | meat_value | veggie_value | fruit_value | egg_value | monster |
|---|---|---|---|---|---|---|---|
| 大肉 | meat | meat | 2.0 | 0 | 0 | 0 | 否 |
| 小肉 | smallmeat | meat | 1.0 | 0 | 0 | 0 | 否 |
| 怪物肉 | monstermeat | meat, monster | 1.0 | 0 | 0 | 0 | 是 |
| 蔬菜 | carrot | veggie | 0 | 1.0 | 0 | 0 | 否 |
| 火龙果 | dragonfruit | veggie | 0 | 1.0 | 0 | 0 | 否 |
| 浆果 | berries | fruit | 0 | 0 | 1.0 | 0 | 否 |
| 鸡蛋 | egg | egg | 0 | 0 | 0 | 1.0 | 否 |
| 鱼 | fish | meat, fish | 0.5 | 0 | 0 | 0 | 否 |
注意鱼是 0.5 的meat_value,肉度计算时它和大肉、小肉不是整倍数关系。如果你把数值声明成 int,后面肉汤要求「肉度≥3.0」时鱼永远凑不够,这种隐蔽 bug 能让你调试一晚上。所以我下面的源码统一用float。
2.2 用 dataclass 建食材与配方表:一份可以直接改的 Python 源码
Python 3.7 以后,推荐直接用dataclass建模。它自动生成__init__和__repr__,调试时能直接看到对象的全部字段。下面是基础类定义:
from dataclasses import dataclass, field from collections import defaultdict from typing import Set, Dict, List, Optional @dataclass class FoodItem: item_id: str # 饥荒内部的物品 ID tags: Set[str] = field(default_factory=set) # 标签,如 {"meat"} values: Dict[str, float] = field(default_factory=dict) # 数值标签,如 {"meat_value": 2.0} freshness: float = 1.0 # 新鲜度,0.0 ~ 1.0 edible: bool = True # 能不能进锅当配方食材 monster: bool = False # 是否为怪物食材,影响特殊配方 @dataclass class Recipe: recipe_id: str # 配方的稳定 ID result_name: str # 产出菜品的显示名 values_required: Dict[str, float] # 各数值标签的最小需求 values_max: Dict[str, float] = field(default_factory=dict) # 各数值标签的上限 special: Dict[str, int] = field(default_factory=dict) # 必须包含某个特定食材 N 个 forbidden_tags: Set[str] = field(default_factory=set) # 只要出现这些标签就跳过 priority: int = 1 # 数值越大越优先 cook_time: int = 10 # 烹饪时间(秒) hunger: float = 0.0 health: float = 0.0 sanity: float = 0.0 character_required: str = "" # 空串表示所有角色可用关键在values_required和values_max:前者是配方门槛,后者是硬性上限。比如「怪物肉千层饼」可以限制monster_value不能超过 1.0,这样两格怪物肉就不会被判成这道菜。special用于火龙果派这种「必须点名要火龙果」的配方,它跟数值无关,只看锅里有几个同名 ID。
然后把食材和配方表初始化出来。食材表我建议集中写在一个函数里,避免在多个地方手写字典导致字段名拼错:
def build_food_table() -> Dict[str, FoodItem]: table = {} table["meat"] = FoodItem("meat", tags={"meat"}, values={"meat_value": 2.0}) table["smallmeat"] = FoodItem("smallmeat", tags={"meat"}, values={"meat_value": 1.0}) table["monstermeat"] = FoodItem( "monstermeat", tags={"meat", "monster"}, values={"meat_value": 1.0, "monster_value": 1.0}, monster=True, ) table["carrot"] = FoodItem("carrot", tags={"veggie"}, values={"veggie_value": 1.0}) table["dragonfruit"] = FoodItem("dragonfruit", tags={"veggie"}, values={"veggie_value": 1.0}) table["berries"] = FoodItem("berries", tags={"fruit"}, values={"fruit_value": 1.0}) table["egg"] = FoodItem("egg", tags={"egg"}, values={"egg_value": 1.0}) table["fish"] = FoodItem("fish", tags={"meat", "fish"}, values={"meat_value": 0.5}) table["twigs"] = FoodItem("twigs", tags={"inedible"}, values={}, edible=False) return table recipes = [ Recipe( recipe_id="meatballs", result_name="肉丸", values_required={"meat_value": 1.0}, priority=10, hunger=62.5, health=3, sanity=5, ), Recipe( recipe_id="meat_stew", result_name="肉汤", values_required={"meat_value": 3.0}, priority=5, hunger=150.0, health=12, sanity=5, ), Recipe( recipe_id="dragonpie", result_name="火龙果派", values_required={"veggie_value": 1.0}, special={"dragonfruit": 1}, priority=30, hunger=75.0, health=40, sanity=15, ), Recipe( recipe_id="fishsticks", result_name="鱼排", values_required={"meat_value": 0.5, "fish_tag": 1.0}, priority=25, hunger=37.5, health=40, sanity=15, ), ]代码逻辑说明:build_food_table统一返回字典,调用方用table["meat"]就能拿对象。values里我额外加了monster_value,这是为了后面values_max能限制「锅里最多允许几个怪物肉」。为什么不用tags计数代替?因为有些食材可能同时带多个标签,标签计数只能判断「有没有」,没法判断「够不够」;而meat_value是连续数值,肉汤要求 3.0 时,两格怪物肉加一块小肉正好满足。参数说明:priority我设成 30 的火龙果派远高于肉丸,因为只要锅里出现火龙果,优先触发专属配方;这是饥荒官方配方的核心逻辑,数值越大越优先,别反着写。
2.3 四个食材槽的数据结构:tuple 还是 list,为什么锁定长度 4
烹饪锅固定四格,这个约束看起来简单,但源码里很容易被忽略。我建议接口上强制校验长度,而不是在匹配函数里用if len < 4补救。数据结构用list比tuple合适,因为你可能要临时替换某一个食材做调试,tuple 不可变反而不方便。先定义一个入口:
COOK_POT_SLOTS = 4 def cook( ingredients: List[FoodItem], recipes: List[Recipe], character: str = "", ) -> "CookResult": if len(ingredients) != COOK_POT_SLOTS: raise ValueError(f"烹饪锅需要 {COOK_POT_SLOTS} 个食材,收到 {len(ingredients)} 个") # 后面接匹配逻辑这里有一个边界:同一种食材放四格,list里是四个对象引用,聚合计算时不会互相覆盖。如果你用dict[item_id]去重,那四块大肉就被算成一块,肉汤直接就做不出来了。正确的做法是聚合时遍历 list,每遇到一个食材就把它的values累加进去,不要按 ID 去重。
3. 用标签计数加优先级排序,写出可跑的匹配引擎
3.1 匹配引擎拆解:先算总和,再逐条过滤,最后排序
饥荒烹饪锅的匹配逻辑不是「找一个最像的配方」,而是「找出所有满足条件的配方,再按优先级挑一个」。这两个思路差别很大:前者是模糊匹配,后者是规则引擎。我选择规则引擎,因为它的行为可预测,而且自定义菜谱时只需要加配置,不动代码。
第一步是把四个食材聚合出三份数据:数值总和、标签计数、物品出现次数。注意树枝这种edible=False的食材不能进聚合,否则树枝会被当成没有任何数值的填充物,影响后续判断。
def aggregate(ingredients: List[FoodItem]): values = defaultdict(float) tags = defaultdict(int) item_counts = defaultdict(int) for ing in ingredients: if not ing.edible: continue for k, v in ing.values.items(): values[k] += v for t in ing.tags: tags[t] += 1 item_counts[ing.item_id] += 1 return values, tags, item_counts这段代码的逻辑是:每个食材的values是一个字典,比如怪物肉是{"meat_value": 1.0, "monster_value": 1.0};聚合时直接遍历这个字典,把同一个 key 的值累加。tags同理,但存的是计数。item_counts记录的是物品 ID 出现次数,给special用。参数说明:如果一个食材有两个meat_value那是建模错误,聚合只会把它加一次;items顺序不影响结果,因为累加满足交换律。
3.2 过滤逻辑写成 Recipe.matches 方法:四个候选为什么被丢掉
有了聚合结果,接下来就是让每个配方自己判断「我满不满足」。把判断逻辑放进Recipe类里,维护起来最舒服。下面是为了适配前一章dataclass补上的方法:
def matches(self, values, tags, item_counts, character="") -> bool: if self.character_required and self.character_required != character: return False for k, need in self.values_required.items(): if values.get(k, 0.0) < need: return False for k, max_v in self.values_max.items(): if values.get(k, 0.0) > max_v: return False for item_id, need_count in self.special.items(): if item_counts.get(item_id, 0) < need_count: return False if self.forbidden_tags & set(tags.keys()): return False return True四个分支分别对应四类约束。values_required是下限,比如肉丸要求meat_value >= 1.0;values_max是上限,比如某些菜要求锅里monster_value <= 1.0,两个怪物肉直接淘汰;special是「必须出现指定物品」,注意它检查的是item_counts不是 tag,所以不会把任何带肉标签的东西误判成火龙果;forbidden_tags是只要 tag 集合里有交叉就直接拒绝,比如「树枝」这种不可食用的标签。
这里有个细节容易被忽略:values_required里如果写{"fish_tag": 1.0},那么食材表里必须真的有fish_tag这个 key,否则永远匹配不上。我在前面鱼排配方里使用了fish_tag,其实更规范的做法是给鱼类食材加fish_value,然后配方写values_required={"fish_value": 1.0}。下面的完整样例会把鱼排改成这种写法,否则新手抄代码时会对着空气报错。
3.3 完整 cook 函数:从聚合到返回菜品和新鲜度
把上面两节拼起来,就是完整的cook函数。我还加了一个CookResult,避免用 tuple 返回四五个值导致调用方很难读:
@dataclass class CookResult: recipe: Optional[Recipe] # None 表示做出来的是湿糊糊 ingredients: List[FoodItem] values: Dict[str, float] freshness: float cook_time: int def cook(ingredients: List[FoodItem], recipes: List[Recipe], character: str = "") -> CookResult: if len(ingredients) != COOK_POT_SLOTS: raise ValueError(f"烹饪锅需要 {COOK_POT_SLOTS} 个食材,收到 {len(ingredients)} 个") values, tags, item_counts = aggregate(ingredients) candidates = [ r for r in recipes if r.matches(values, tags, item_counts, character) ] if not candidates: # 饥荒里失败产物是 Wet Goop,中文常叫湿糊糊 return CookResult(None, ingredients, dict(values), 0.0, 120) candidates.sort(key=lambda r: (-r.priority, r.cook_time)) best = candidates[0] # 新鲜度由最差食材决定;这里保留扩展点给烹饪衰减 base_freshness = min( (i.freshness for i in ingredients if i.edible), default=0.0, ) return CookResult(best, ingredients, dict(values), base_freshness, best.cook_time)排序 key 里我写了(-r.priority, r.cook_time),意思是优先级降序,优先级相同时烹饪时间短的那个先选。为什么还要cook_time兜底?因为同一个优先级下,游戏实际是固定的一个配方;但如果你自定义菜谱时没设好优先级,就靠烹饪时间做二次排序,至少不会随机摇摆。先过滤再排序,候选列表通常很短,不需要刻意优化复杂度。base_freshness的计算逻辑是取所有可食用食材的最小新鲜度,这是我在实际对照游戏时验证过的常见规则,但不同版本有差异,所以放在返回值里而不是写死在属性计算里。
3.4 一个最小命令行入口:把四格食材名称传进 cook.py
源码要落地,必须能直接跑。我一般会给模块加一个极简 CLI,不引argparse,就解析sys.argv[1:]:
import sys def main(): food_table = build_food_table() names = sys.argv[1:] if len(names) != COOK_POT_SLOTS: print(f"用法: python cook.py 食材1 食材2 食材3 食材4") sys.exit(1) try: ings = [food_table[n] for n in names] except KeyError as e: print(f"未知食材: {e}") sys.exit(1) result = cook(ings, recipes) if result.recipe is None: print("湿糊糊") else: print(result.recipe.result_name, "新鲜度:", round(result.freshness, 2), "饱食度:", result.recipe.hunger) if __name__ == "__main__": main()运行效果类似:
python cook.py monstermeat berries berries berries # 肉丸 新鲜度: 1.0 饱食度: 62.5这里把食材名映射到对象是直接查表,没有做别名归一化。如果你输入cookedberries而食材表里只有berries,就会报未知食材。这就是第 4 章要讲的坑之一:生食材和熟食材是两个 ID,必须提前建全。
4. 避坑:烹饪锅食物源码里最容易翻车的五个配方细节
4.1 现象:同样的四格食材,游戏里出肉丸,你的源码却算成肉汤
如果两格大肉加胡萝卜加浆果,游戏里大概率出肉汤;但如果你把肉丸的优先级设成 5,把肉汤设成 1,那四个食材里只要肉度大于 1.0,肉丸就被优先选中,永远做不出肉汤。原因只有一个:优先级的方向反了。饥荒的配方协调原则是越专属的菜越优先,泛用填充菜(比如肉丸)反而最靠后。
解决:给Recipe的priority字段定一条铁律——数值越大越优先。肉丸设 10,肉汤设 5,火龙果派设 30。并且写进单测:当多个配方同时满足时,结果必须是优先级高的那个。我因为这个方向问题吃了大亏,后来直接在字段注释里写死,不让团队里第二个人再踩。
4.2 现象:冰和树枝被当成了正经填充物,永远只出湿糊糊
你往锅里放树枝、树枝、树枝、树枝,游戏不会给你任何正常菜,但在源码里,树枝如果被标记为edible=True,它的values是空字典,聚合后什么约束都不满足,最后掉进湿糊糊分支。看起来结果一样,但问题在于——当你放「树枝 + 三块大肉」时,树枝也被当成填充物参与计算,本应做出肉汤的锅变成了湿糊糊。树枝和冰在饥荒里属于「特殊填充物」,不是所有配方都接受它们。
解决:把这类物品的edible设成False,同时在aggregate里直接跳过不可食用食材。这样它就完全不参与数值计算,但item_counts仍然记录它的 ID;如果你想实现「某个配方点名要一根树枝」这种自定义菜谱,special机制仍然能识别它。注意不要为了省事把edible=False的食材从名单里删掉,否则锅占着四个格子但聚合出 0 个食材,长度校验已经通过,逻辑上会出现空食材锅。
4.3 现象:做出的菜新鲜度永远是默认值,截图对不上
很多初版源码把成品新鲜度写成配方表的固定值,比如肉丸默认 15 天。但游戏里同一道菜,用新鲜大肉做和用快烂的怪物肉做,出锅后保质期差很多。我实测下来的常见规则是:成品新鲜度 = 所有食材里最差的那个新鲜度,再叠加烹饪时间衰减系数。如果你只取平均值,做出菜的保质期会比游戏里长,玩家在仓库里放了一天还没坏,一看就是模拟器。
解决:代码里base_freshness取min(...)而不是sum / len。如果你要更接近原版,再加一个spoilage_factor,在返回结果时用公式freshness = base_freshness * recipe.freshness_factor计算。这个系数因版本而异,建议放到配置里,别写死在逻辑里。我在 2.3 的CookResult里保留了这个扩展点,你可以把freshness_factor加到Recipe类里。
4.4 现象:怪物肉做出来的菜没有负面效果,数值完全按普通肉算
把一块怪物肉和三块填充物丢进锅,游戏会出怪物千层饼或类似菜,饱食度、血量、精神会带上怪物肉的负面惩罚;但如果你在聚合时只统计meat_value,怪物肉的标签没有传播到结果,所有菜都按普通肉做,数值对不上。原因是你把「怪物」当成了配方过滤条件,却没有把它作为影响产出的属性传递下去。
解决:在FoodItem里单独维护monster=True标志,食材聚合时同时算一个monster_count。匹配引擎用values_max={"monster_value": 1.0}过滤掉不允许怪物肉的配方;而对于允许怪物肉的配方,在生成结果时把monster_count > 0传给属性计算函数,让最终菜品的hunger/health/sanity在基础值上叠加惩罚系数。注意不是所有带 monster 的食材都长一个样,cookedmonstermeat的新鲜度会更低,所以建议数值也放进食材表而不是写死在计算函数里。
4.5 现象:物品 ID 和标签不匹配,生熟食材混着加载
最后这个坑最隐蔽。饥荒里berries和cookedberries是两个完全不同的 ID,前者可以进一步烹饪,后者已经熟透不能再进锅;但它们的显示名都是「浆果」。如果你在食材表里只建了一个berries,玩家输入熟浆果时就会报未知食材;反之,如果你把熟浆果建成了可进锅的新食材,又会导致同一物品有两种行为。
解决:在食材表里把生、熟、晒干版本分开建 ID,比如berries、cookedberries、berries_cooked用不同 key。然后写一个normalize_input(name)别名函数,把用户输入的「浆果」「熟浆果」映射到正确 ID。模块启动时再做一次自检,遍历食材表里所有item_id,发现重复或明显冲突就报警。这个自检函数只有十来行,但能救你两次:一次是刚抄完代码时,一次是别人改配置时。
5. 验证这套源码的三种方式:从回归测试到自定义食谱
第一种验证方式是用 pytest 把经典配方写成回归测试。每次改食材表或优先级,跑一遍至少能拦住一半低级错误:
def test_meatballs(): food = build_food_table() result = cook( [food["monstermeat"], food["berries"], food["berries"], food["berries"]], recipes, ) assert result.recipe.recipe_id == "meatballs" assert result.freshness == 1.0 def test_monster_limit(): food = build_food_table() result = cook( [food["monstermeat"], food["monstermeat"], food["carrot"], food["carrot"]], recipes, ) assert result.recipe is None or result.recipe.recipe_id != "meat_stew"第二种验证方式是直接跑 CLI 对照游戏内行为。游戏里四块冰不会出正常菜,那你就在 CLI 里输入四个ice,看程序是不是返回湿糊糊;游戏里两个大肉加一个蛋加一个填充物出培根煎蛋,那你就在 CLI 里把同样组合跑一遍,比对饱食度数值。这不需要自动化,打开游戏放一口锅,点十几次就有足够数据。
第三种是自定义食谱扩展验证。把Recipe变成数据驱动以后,加一道「茄子卷」只需要在配置里加一条 dict 或者 JSON 行,然后在测试里手动构造四个茄子看返回结果。我自己的习惯是每次改优先级就跑一遍全部回归测试,这套流程帮我抓出过至少三次「优先级设反」的问题。顺着前面的排查表对照,多数问题出在食材表没建全或者新鲜度公式不匹配版本。希望你能用这套源码跑通自己的食谱模组,回头加新菜时就不会再被那口锅折磨了。
本文还有配套的精品资源,点击获取