饥荒烹饪锅食物源码解析:用Python实现配方模拟器
发布时间:2026/10/1 22:33:31来源:尧图网络
简介适合饥荒模组开发者的烹饪锅食物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 是veggiefruit_value0。如果你按日常经验把它归类成水果后面火龙果派的匹配逻辑就永远不对。所以建模时不能靠名字猜要么查游戏 wiki要么从一个可靠的食材表反推。下面是我常用的食材表结构只做了演示数据真实 mod 请在 config 里按你的游戏版本调整食材内部 ID标签meat_valueveggie_valuefruit_valueegg_valuemonster大肉meatmeat2.0000否小肉smallmeatmeat1.0000否怪物肉monstermeatmeat, monster1.0000是蔬菜carrotveggie01.000否火龙果dragonfruitveggie01.000否浆果berriesfruit001.00否鸡蛋eggegg0001.0否鱼fishmeat, fish0.5000否注意鱼是 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_factoryset) # 标签如 {meat} values: Dict[str, float] field(default_factorydict) # 数值标签如 {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_factorydict) # 各数值标签的上限 special: Dict[str, int] field(default_factorydict) # 必须包含某个特定食材 N 个 forbidden_tags: Set[str] field(default_factoryset) # 只要出现这些标签就跳过 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}, monsterTrue, ) 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{}, edibleFalse) return table recipes [ Recipe( recipe_idmeatballs, result_name肉丸, values_required{meat_value: 1.0}, priority10, hunger62.5, health3, sanity5, ), Recipe( recipe_idmeat_stew, result_name肉汤, values_required{meat_value: 3.0}, priority5, hunger150.0, health12, sanity5, ), Recipe( recipe_iddragonpie, result_name火龙果派, values_required{veggie_value: 1.0}, special{dragonfruit: 1}, priority30, hunger75.0, health40, sanity15, ), Recipe( recipe_idfishsticks, result_name鱼排, values_required{meat_value: 0.5, fish_tag: 1.0}, priority25, hunger37.5, health40, sanity15, ), ]代码逻辑说明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 匹配引擎拆解先算总和再逐条过滤最后排序饥荒烹饪锅的匹配逻辑不是「找一个最像的配方」而是「找出所有满足条件的配方再按优先级挑一个」。这两个思路差别很大前者是模糊匹配后者是规则引擎。我选择规则引擎因为它的行为可预测而且自定义菜谱时只需要加配置不动代码。第一步是把四个食材聚合出三份数据数值总和、标签计数、物品出现次数。注意树枝这种edibleFalse的食材不能进聚合否则树枝会被当成没有任何数值的填充物影响后续判断。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.0values_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(keylambda r: (-r.priority, r.cook_time)) best candidates[0] # 新鲜度由最差食材决定这里保留扩展点给烹饪衰减 base_freshness min( (i.freshness for i in ingredients if i.edible), default0.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 现象冰和树枝被当成了正经填充物永远只出湿糊糊你往锅里放树枝、树枝、树枝、树枝游戏不会给你任何正常菜但在源码里树枝如果被标记为edibleTrue它的values是空字典聚合后什么约束都不满足最后掉进湿糊糊分支。看起来结果一样但问题在于——当你放「树枝 三块大肉」时树枝也被当成填充物参与计算本应做出肉汤的锅变成了湿糊糊。树枝和冰在饥荒里属于「特殊填充物」不是所有配方都接受它们。解决把这类物品的edible设成False同时在aggregate里直接跳过不可食用食材。这样它就完全不参与数值计算但item_counts仍然记录它的 ID如果你想实现「某个配方点名要一根树枝」这种自定义菜谱special机制仍然能识别它。注意不要为了省事把edibleFalse的食材从名单里删掉否则锅占着四个格子但聚合出 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里单独维护monsterTrue标志食材聚合时同时算一个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 行然后在测试里手动构造四个茄子看返回结果。我自己的习惯是每次改优先级就跑一遍全部回归测试这套流程帮我抓出过至少三次「优先级设反」的问题。顺着前面的排查表对照多数问题出在食材表没建全或者新鲜度公式不匹配版本。希望你能用这套源码跑通自己的食谱模组回头加新菜时就不会再被那口锅折磨了。本文还有配套的精品资源点击获取
网站建设高端定制企业官网