Files
ImmortalSect/doc/模块联动.md
T

7.1 KiB
Raw Blame History

模块联动规范

本文档定义模块间联动逻辑的处理方案:谁调用谁、变更怎么传播、如何保证一致性。联动场景包括「建筑需要弟子才有产出」「炼丹需输入药材→产出丹药」「消耗药材→库存减少」等跨系统交互。

1. 方案组合结论

选择 理由
主动操作联动(炼丹/交货/种植) 直接调用 + 依赖注入 逻辑显式、可断点调试、单测零成本
回合结算顺序 固定管线(MainGame 依次调用) 消除时间耦合,顺序可预期
状态变更通知(UI/统计/音效) 信号广播(观察者,单向) 不引入依赖,加监听者零改动
全局事件总线 / 规则引擎 不做,数据形状预留 P1~P3 联动类型仅个位数,通用引擎成本大于收益

明确不用的方案

  • 不用事件总线处理权威状态变更(如扣库存):请求-响应型联动需要双向信号,代码量反超直接调用,且结算顺序难追踪。事件广播只用于通知,不用于变更。
  • 不用规则引擎全量执行联动:为 4 类联动(种植收获/炼丹/交货/修炼)写通用执行器是净亏损。P4 功法/术法批量进场(联动类型持续增长)时再评估抽成引擎。

2. 联动场景的本质

场景 本质 参与方
建筑需要弟子才有产出 前置条件检查(人手占用) 生产系统 ↔ 弟子系统
炼丹需输入药材→产出丹药 资源转换(事务) 炼丹系统 ↔ 库存系统
消耗药材→库存减少 库存变更 + 广播 调用方 → 库存 → UI/统计

统一模式:A 向 B 提需求(检查)→ B 执行变更 → 变更通知所有关心者

3. 依赖管理铁律

3.1 依赖图无环(DAG

库存、经济是叶子节点,谁也不依赖;生产类依赖库存/弟子;订单依赖库存+经济。任何 Manager 不得依赖其上层,出现环即设计错误。

OrderManager ──→ InventoryManager(叶子)
            └──→ EconomyManager(叶子)
AlchemyManager ─→ InventoryManager
FieldManager ───→ InventoryManager
DiscipleManager ─→ EconomyManager(修炼扣灵石)
FoodManager ─────→ InventoryManager
              └─→ DiscipleManager

已知隐患P2 启动前必须复查):

  • 忠诚/抽成:弟子抽成 → 经济结算方向仍是 弟子→经济,不构成环;但若经济系统反过来查询弟子(按忠诚发分红)会成环,届时改走信号通知。
  • 收徒拜师礼:一次性收入走 EconomyManager,同向,无环。

3.2 注入点唯一

所有 setup() 集中在 MainGame 一处,Manager 自身零 autoload 引用(TimeSystem/SaveSystem 除外按需)。这是依赖可管理的根源:

# MainGame._ready() 中:
economy_manager.setup()
inventory_manager.setup()
field_manager.setup(inventory_manager)
alchemy_manager.setup(inventory_manager)
food_manager.setup(disciple_manager, inventory_manager)
order_manager.setup(inventory_manager, economy_manager)
disciple_manager.setup(economy_manager)

好处:测试场景只注入 Mock 依赖即可单独运行;依赖关系读一遍 MainGame 全貌可见。

3.3 结算顺序管线化

month_passed 固定按序调用各 Manager 的 settle_month(),消除时间耦合:

灵田(收获入库存) → 炼丹(自动生产) → 弟子(修炼结算)
→ 口粮(凡人弟子消耗) → 订单(过期检查) → 经济(固定收入)

顺序依据:口粮必须在收获后扣(吃的是新粮);经济最后(汇总本月收支)。新增系统只插入管线,不改变既有顺序。

4. 原子事务约定(check-take-give

联动变更必须原子:先检查所有前置条件与材料,全部通过后再执行扣除与产出,不允许扣一半失败

4.1 收敛到库存层

「先检查后扣除」的手工纪律收敛为库存层的两个方法,调用方不可能写错:

# InventoryManager
func try_take(items: Dictionary) -> bool    # 全量检查,够则返回 true
func take(items: Dictionary) -> void        # 实际扣除(try_take 通过后调用)
func add_item(item_id: String, count: int) -> void
func has_items(items: Dictionary) -> bool   # 只查不扣(供 UI 显示可用性)

4.2 调用方模板

# AlchemyManager.refine() —— 主动操作联动标准写法
func refine(recipe_id: String, disciple: Disciple) -> bool:
    var r: Dictionary = ContentDB.get_recipe(recipe_id)
    if disciple.alchemy_skill < r["skill_req"]:
        return false
    if not inventory.try_take(r["inputs"]):
        return false
    inventory.take(r["inputs"])
    inventory.add_item(r["output_id"], r["output_count"])
    pill_refined.emit(r["output_id"])
    return true

5. 单一写入口

每种数据只允许其归属 Manager 变更:

数据 唯一写入口 外部只能
库存物品 InventoryManager.take/add_item 调用这两个方法
灵石余额 EconomyManager.spend/earn 调用这两个方法
弟子属性 DiscipleManager.set_attr 调用该方法
弟子增删 DiscipleManager.add/remove_disciple 调用该方法

违反此规则的直接后果:数据被谁改的不可追踪,信号漏发,UI 不同步。

6. 信号分工

信号类型 用途 方向
Manager 发出的领域信号(inventory_changed/pill_refined…) UI 刷新、统计、音效 后端 → 前端/其他监听者,单向
TimeSystem 的 month_passed 回合结算管线入口 已定,不改
UI 调用 Manager 公开方法 玩家操作 前端 → 后端,直接调用不走信号

原则:信号只通知,不携带状态变更职责。UI 收到信号后从后端读最新值,不依赖信号参数里的数据(参数仅作提示)。

7. 规则引擎的预留(P4 退路)

P1 定义配方数据时,形状直接写成 requires / consumes / produces 三段(逻辑在代码手写):

{
  "id": "refine_peiyuan",
  "requires": [{"type": "disciple_skill", "skill": "alchemy", "min": 3}],
  "consumes": [{"item": "herb_lingzhi", "count": 2}],
  "produces": [{"item": "pill_peiyuan", "count": 1}],
}

将来联动类型膨胀(P4 功法/术法、炼丹失败率、双产出等)时,把各 Manager 手写逻辑抽成通用执行器(检查 requires → 扣 consumes → 给 produces,天然原子),数据文件一行不用改

评估时机:新增联动类型需要改多个 Manager 的代码时,即考虑抽取。当前(P1)不抽取。

8. 违背规范的常见症状

症状 根因 对应规范
库存数量莫名变化 绕过 InventoryManager 直接改 §5 单一写入口
扣了一半材料失败 未先 try_take 全量检查 §4 原子事务
结算结果依赖按钮点击顺序 未走固定管线 §3.3 结算顺序
Manager 互相调用成环 依赖方向失控 §3.1 DAG
换 UI 后数据不刷新 变更未发信号 §6 信号分工