From bea9300d2ba521886a77e91f33b4393eca605ada Mon Sep 17 00:00:00 2001 From: "cty@comp" Date: Tue, 8 Sep 2026 22:49:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9EDisciple=E5=BC=9F=E5=AD=90?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=B1=BB=E4=B8=8EDiscipleManager=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=99=A8=E5=8F=8A=E5=BC=9F=E5=AD=90=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/弟子属性.md | 124 +++++++++++++++ doc/弟子管理.md | 33 ++++ doc/模块联动.md | 158 +++++++++++++++++++ src/Character/DiscipleManager.gd | 163 ++++++++++++++++++++ src/Data/Models/Disciple.gd | 253 +++++++++++++++++++++++++++++++ src/Data/Models/Disciple.gd.uid | 1 + 6 files changed, 732 insertions(+) create mode 100644 doc/弟子属性.md create mode 100644 doc/模块联动.md create mode 100644 src/Character/DiscipleManager.gd create mode 100644 src/Data/Models/Disciple.gd create mode 100644 src/Data/Models/Disciple.gd.uid diff --git a/doc/弟子属性.md b/doc/弟子属性.md new file mode 100644 index 0000000..3b2d752 --- /dev/null +++ b/doc/弟子属性.md @@ -0,0 +1,124 @@ +# 弟子属性设计 + +本文件定义弟子(Disciple)的属性体系、境界规则与派生逻辑,是 `src/Data/Models/Disciple.gd` 的实现依据。关联:`doc/弟子管理.md`(模块设计)、`doc/模块联动.md`(联动规范)、`doc/Roadmap.md`(P1 核心循环)。 + +## 1. 属性列表(权威定义) + +| 名称 | 范围 | 说明 | +|:---|:---|:---| +| metal | [0, 100] | 金属性值 | +| wood | [0, 100] | 木属性值 | +| water | [0, 100] | 水属性值 | +| fire | [0, 100] | 火属性值 | +| earth | [0, 100] | 土属性值 | +| efficiency | [1, 100] | 灵气吸收效率,决定修炼速度,隐藏属性 | +| realm_level | [0, 5] | 大境界,凡人、练气、筑基、金丹、元婴、化神 | +| sub_realm_level | [1, 13] | 凡人只有一个小境界;练气 13 个小境界,其他各有前、中、后、大圆满四个小境界 | +| cultivation_exp | [0, 2147483647] | 修炼经验 | +| farming_exp | [0, 2147483647] | 耕作技能经验 | +| farming_level | [0, 10] | 耕作技能等级 | +| herb_exp | [0, 2147483647] | 药草耕作技能经验 | +| herb_level | [0, 10] | 药草耕作技能等级 | +| alchemy_exp | [0, 2147483647] | 炼丹技能经验 | +| alchemy_level | [0, 10] | 炼丹技能等级 | +| loyalty | [0, 100] | 忠诚度 | + +> 字段说明:`id` 只读,由管理器分配;`name` 普通字符串。原设计中的 `is_mortal`、`cultivation_skill`、`alchemy_skill` 单值字段由下述规则取代,不单独存字段。 + +## 2. 境界体系 + +大境界 `realm_level` 与显示名、小境界 `sub_realm_level` 数量: + +| realm | 名称 | sub 范围 | 小境界显示 | +| --- | --- | --- | --- | +| 0 | 凡人 | 固定 1 | 凡人 | +| 1 | 练气 | 1~13 | 练气X层(1~13 层) | +| 2 | 筑基 | 1~4 | 筑基前期 / 中期 / 后期 / 大圆满 | +| 3 | 金丹 | 1~4 | 金丹前期 / 中期 / 后期 / 大圆满 | +| 4 | 元婴 | 1~4 | 元婴前期 / 中期 / 后期 / 大圆满 | +| 5 | 化神 | 1~4 | 化神前期 / 中期 / 后期 / 大圆满 | + +## 3. 灵根(五行推导,不存字段) + +有灵根的凡人才能修炼破境。**任一五行属性 ≥ 阈值(占位 20,待调参)即具灵根**: + +- 有灵根 → 可修炼:凡人期积累修炼经验,满后破境入练气 +- 无灵根 → 终生为凡,永不涨修炼经验(且大境界 0) + +派生:`has_spiritual_root()`;凡俗判定 `is_mortal() = (realm_level == 0) 且 无灵根`。 + +## 4. 经验规则 + +### 4.1 修炼经验(cultivation_exp) + +- 每个小境界有经验上限(曲线表配置,见 §5) +- **跨小境界**:经验满自动突破 sub+1,溢出部分**继承**到下一个小境界 +- **跨大境界**:sub 到顶(练气 13 / 其他 4)再满,realm+1 且 sub=1,溢出部分**清零** +- 化神大圆满封顶,不再获得修炼经验 +- 突破自动进行(回合结算时处理),暂不设额外条件 + +### 4.2 技能经验(farming / herb_farming / alchemy 三套相同规则) + +- 每跨一级,溢出的经验**继承**到下一级,直到升满(10 级)为止 +- 满级后继续获得的经验**不再累积**(经验不会提升) +- 每个等级所需经验由配置文件给出(曲线表,见 §5) + +### 4.3 效率的作用 + +`efficiency` 决定修炼速度(隐藏属性),不直接改变经验上限;具体月收益公式由管理器结算实现,数值待调参。 + +## 5. 经验曲线表(放 Disciple.gd 内部 const,占位待调参) + +> 存储决策:曲线表先以常量写在 `Disciple.gd` 内部,自包含易调;静态内容层方案(CSV/Resource)落地后再迁移。 + +### 5.1 修炼曲线 CULTIVATION_EXP_CAPS + +键为 `"{realm}_{sub}"`,共 30 条(凡人 1 + 练气 13 + 筑基/金丹/元婴/化神 各 4)。凡人期条目(`0_1`)供有灵根凡人积累经验破境用;化神大圆满(`5_4`)无上限(哨兵)。 + +``` +{"0_1": 100, "1_1": 100, "1_2": 110, ... 占位递增,待调参} +``` + +### 5.2 技能曲线 SKILL_EXP_CAPS + +10 条,表示 level N → N+1 所需经验(N = 0~9)。三套技能暂共用一张表,将来可拆分。 + +``` +[100, 120, 140, ...] 占位,待调参 +``` + +## 6. 读写与派生接口(数据类 API) + +数值属性读写统一走键名接口(RANGES 为合法键与夹取范围的唯一权威): + +```gdscript +func has_attr(key: String) -> bool # key 是否为登记表中的数值属性 +func get_attr(key: String) -> int # 读数值属性(未知键 push_error 返回 0) +func set_attr(key: String, value: int) -> bool # 写数值属性(夹取 + 境界/小境界联动约束) +``` + +> 说明:`id` 只读语义(由 `create()` 分配),`name` 为字符串不入 RANGES;两者直接字段访问。`total_amount` 为只读属性(实时由五行之和计算)。 + +派生只读方法(境界体系相关,实现时见 §2): + +| 方法 | 逻辑 | +| --- | --- | +| `total_amount`(只读属性) | 五行之和(实时派生) | +| `has_spiritual_root() -> bool` | 任一五行 ≥ 灵根阈值 | +| `can_cultivate() -> bool` | 有灵根 且 未满级 | +| `is_mortal() -> bool` | realm 0 且无灵根 | +| `sub_realm_max() -> int` | 当前大境界的小境界上限(查 SUB_MAX_BY_REALM) | +| `exp_cap() -> int` | 当前 (realm, sub) 修炼经验上限(查曲线);满级返哨兵 | +| `realm_name() -> String` | 完整显示名:凡人 / 练气5层 / 筑基后期 / 化神大圆满 | + +> 状态迁移(突破、升级、经验增减)属领域操作,归 DiscipleManager(见 `doc/弟子管理.md`),数据类只提供纯查询。 + +## 7. 夹取与写入约束 + +- `RANGES` 常量表为 17 个属性的合法范围唯一权威,`create()` 与管理器 `set_attr()` 共用 +- 属性全部普通字段(每属性 1 行声明),不带 setter 样板;夹取收口在两处写入口 +- 直接写字段不夹取,依赖 `doc/弟子管理.md` "修改只走管理器"纪律 + +## 8. 变更记录 + +- 2026-09-07:确立属性列表、境界体系(0~5 + sub)、灵根五行推导、经验曲线表结构;本文档由与 opencode 的讨论结论整理(本文件前身为占位草稿) diff --git a/doc/弟子管理.md b/doc/弟子管理.md index a7d603d..3fa9b2a 100644 --- a/doc/弟子管理.md +++ b/doc/弟子管理.md @@ -83,3 +83,36 @@ func load_from_dict(d) -> void - `settle_month` → 境界提升/灵石消耗正确 - `to_dict / load_from_dict` 往返一致 3. `print` 输出 PASS/FAIL,编辑器直接运行场景验证 + +# 弟子属性 + +## 属性列表 +|名称|范围|说明| +|:---|:---|:---| +|metal|[0, 100]|金属性值| +|wood|[0, 100]|木属性值| +|water|[0, 100]|水属性值| +|fire|[0, 100]|火属性值| +|earth|[0, 100]|土属性值| +|efficiency|[1, 100]|灵气吸收效率,决定修炼速度,隐藏属性| +|realm_level|[0, 5]|大境界,凡人、练气、筑基、金丹、元婴、化神| +|sub_realm_level|[1, 13]|凡人只有一个小境界;练气13个小境界,其他各有前、中、后、大圆满四个小境界| +|cultivation_exp|[0, 2147483647]|修炼经验| +|farming_skill_exp|[0, 2147483647]|耕作技能经验| +|farming_skill_level|[0, 10]|炼丹技能等级| +|herb_farming_skill_exp|[0, 2147483647]|药草耕作技能经验| +|herb_farming_skill_level|[0, 10]|炼丹技能等级| +|alchemy_skill_exp|[0, 2147483647]|炼丹技能经验| +|alchemy_skill_level|[0, 10]|炼丹技能等级| +|loyalty|[0, 100]|忠诚度| + +- **修炼经验说明**:每个小境界都有一个经验上限,跨越小境界,溢出部分会继承到下一个小境界。跨越大境界,溢出部分清零。 + +- **技能经验说明**:耕作技能经验、药草耕作技能经验、炼丹技能经验,每跨越一个等级,溢出的经验会继承到下一个等级,直到升满为止,不会有任何经验提升。每个等级所需的经验为需要在文件中配置。 + + +## 初始分配方式 +```mermaid +graph TD + A[分配属性总量,按正正态分布] --> B[随机分配5种属性,属性之和为1] --> C[分配灵气利用率] +``` \ No newline at end of file diff --git a/doc/模块联动.md b/doc/模块联动.md new file mode 100644 index 0000000..c5163e6 --- /dev/null +++ b/doc/模块联动.md @@ -0,0 +1,158 @@ +# 模块联动规范 + +本文档定义模块间联动逻辑的处理方案:谁调用谁、变更怎么传播、如何保证一致性。联动场景包括「建筑需要弟子才有产出」「炼丹需输入药材→产出丹药」「消耗药材→库存减少」等跨系统交互。 + +## 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 除外按需)。这是依赖可管理的根源: + +```gdscript +# 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 收敛到库存层 + +「先检查后扣除」的手工纪律收敛为库存层的两个方法,调用方不可能写错: + +```gdscript +# 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 调用方模板 + +```gdscript +# 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` 三段(逻辑在代码手写): + +```gdscript +{ + "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 信号分工 | diff --git a/src/Character/DiscipleManager.gd b/src/Character/DiscipleManager.gd new file mode 100644 index 0000000..ac559c5 --- /dev/null +++ b/src/Character/DiscipleManager.gd @@ -0,0 +1,163 @@ +# 初始版本不提供添加弟子的接口 + + +class_name DiscipleManager +extends Node + +signal disciple_added(disciple: Disciple) +signal disciple_removed(disciple: Disciple) +signal disciple_attr_changed(disciple: Disciple, attr_name: String, old_value: Variant, new_value: Variant) + +## 开局随机编队(一次性随机生成的数据,非程序化 RNG;占位,可手调) +## 字段与 Disciple.create() 构造键一致;realm_level 用 int(0凡人/1练气,=Disciple.Realm 枚举值) +const INITIAL_ROSTER := [ + # 1 玄真(宗主):修士·炼气9层,五行全高,炼丹3级 + { + "name": "玄真", + "metal": 45, "wood": 38, "water": 22, "fire": 31, "earth": 19, + "efficiency": 85, + "realm_level": 1, "sub_realm_level": 9, + "farming_level": 1, "herb_level": 2, "alchemy_level": 3, + "loyalty": 90, + }, + # 2 凌霄:修士·炼气4层,金火旺 + { + "name": "凌霄", + "metal": 62, "wood": 12, "water": 15, "fire": 44, "earth": 20, + "efficiency": 76, + "realm_level": 1, "sub_realm_level": 4, + "farming_level": 2, "herb_level": 1, "alchemy_level": 1, + "loyalty": 55, + }, + # 3 青禾:凡人带灵根(木47≥20 可修炼破境),药草天赋 + { + "name": "青禾", + "metal": 8, "wood": 47, "water": 25, "fire": 12, "earth": 10, + "efficiency": 68, + "realm_level": 0, "sub_realm_level": 1, + "farming_level": 2, "herb_level": 3, "alchemy_level": 0, + "loyalty": 60, + }, + # 4 阿福:凡人杂役(无灵根,五行均<20) + { + "name": "阿福", + "metal": 5, "wood": 12, "water": 8, "fire": 9, "earth": 10, + "efficiency": 41, + "realm_level": 0, "sub_realm_level": 1, + "farming_level": 2, "herb_level": 1, "alchemy_level": 0, + "loyalty": 50, + }, + # 5 阿禄:凡人杂役(无灵根) + { + "name": "阿禄", + "metal": 3, "wood": 6, "water": 5, "fire": 8, "earth": 15, + "efficiency": 35, + "realm_level": 0, "sub_realm_level": 1, + "farming_level": 3, "herb_level": 1, "alchemy_level": 0, + "loyalty": 45, + }, +] + +var _disciples: Array[Disciple] = [] # 弟子列表 +var _master_id: int = 0 # 管理器的发号计数,id 的唯一来源 + +# ---- 工厂 ---- + +# 初始编队(复用 _create_from_data) +func spawn_initial_roster() -> Array[Disciple]: + for data in INITIAL_ROSTER: + var disciple = _create_from_data(data) + if disciple != null: + add_disciple(disciple) + # _disciples.append(disciple) + # emit_signal("disciple_added", disciple) + else: + push_error("Failed to spawn disciple from data: %s" % data) + pass + +# 补 id → Disciple.create() +# 从数据字典创建 Disciple 实例,若失败返回 null +func _create_from_data(data: Dictionary) -> Disciple: + var disciple := Disciple.create(data) + if disciple == null: + push_error("Failed to create disciple from data: %s" % data) + return null + disciple.id = _master_id + _master_id += 1 + return disciple + pass + +# ---- 仓储 ---- + +# 公开接口:添加弟子 +func add_disciple(d : Disciple) -> void: + if d == null: + push_error("Cannot add null disciple.") + return + _disciples.append(d) + emit_signal("disciple_added", d) + +# 移除弟子 +func remove_disciple(d : Disciple) -> bool: + if d != null and _disciples.has(d): + _disciples.erase(d) + emit_signal("disciple_removed", d) + return true + return false + +# 公开接口:通过id获取弟子 +func get_disciple(id : int) -> Disciple: + for d in _disciples: + if d.id == id: + return d + return null + +# 公开接口:获取所有弟子(返回副本,避免外部修改原数组) +func get_all() -> Array[Disciple]: + return _disciples.duplicate() # 返回副本,避免外部修改原数组 + +# ---- 修改唯一入口 ---- + +# 调 d.set_attr + 发信号(name 特判) +func set_attr(d :Disciple, attr: String, value: Variant) -> bool: + if _disciples.has(d): + var old_value = d.get_attr(attr) + if d.set_attr(attr, value): + emit_signal("disciple_attr_changed", d, attr, old_value, value) + return true + return false + +# ---- 经验晋级(领域操作)---- + +# 跨小境界继承循环;跨大境界清零;满级不涨;无灵根拒绝 +func add_cultivation_exp(d : Disciple, amount : int) -> void: + if _disciples.has(d): + var old_realm = d.realm_level + var old_sub_realm = d.sub_realm_level + d.add_cultivation_exp(amount) + if d.realm_level != old_realm or d.sub_realm_level != old_sub_realm: + emit_signal("disciple_attr_changed", d, "realm_level", old_realm, d.realm_level) + emit_signal("disciple_attr_changed", d, "sub_realm_level", old_sub_realm, d.sub_realm_level) + +# skill∈{farming,herb,alchemy},查 SKILL_EXP_CAPS,溢出继承至满级 +func add_skill_exp(d : Disciple, skill: String, amount : int) -> void: + if _disciples.has(d): + var old_level = d.get_skill_level(skill) + d.add_skill_exp(skill, amount) + var new_level = d.get_skill_level(skill) + if new_level != old_level: + emit_signal("disciple_attr_changed", d, skill + "_level", old_level, new_level) + +# ---- 月度结算(预留)---- +# 月收益 = f(efficiency),依赖经济扣灵石 —— 取决于下列问题 +func settle_month() -> void: + pass + +# ---- 存档 ---- +func to_dict(): + pass + +func load_from_dict(): + pass + + diff --git a/src/Data/Models/Disciple.gd b/src/Data/Models/Disciple.gd new file mode 100644 index 0000000..50f86ad --- /dev/null +++ b/src/Data/Models/Disciple.gd @@ -0,0 +1,253 @@ +## 弟子数据模型(纯数据,RefCounted,无业务逻辑)。 +## +## 设计依据:doc/弟子属性.md(属性权威表)、doc/弟子管理.md(模块结构)。 +## - 持有弟子全部信息,运行时可变,不依赖场景树,可脱离场景单测 +## - 状态迁移(突破/升级/经验增减)属领域操作,归 DiscipleManager +## - 属性写入一律经 set_attr()/create():RANGES 夹取在此收口,禁止外部裸写字段 +## ("修改只走管理器"纪律,见 doc/弟子管理.md §5) +class_name Disciple +extends RefCounted + +## 大境界枚举(值即 realm_level 的存储值 0~5,与 doc/弟子属性.md §2 对应)。 +## 注意:值为存档数据,严禁改变成员顺序或插入成员;只可在末尾追加。 +enum Realm { + MORTAL, # 0 凡人 + QI_REFINING, # 1 练气 + FOUNDATION, # 2 筑基 + GOLDEN_CORE, # 3 金丹 + NASCENT_SOUL, # 4 元婴 + SPIRIT_TRANSFORMATION # 5 化神 +} + +## 每属性合法范围(唯一夹取权威,create/set_attr 共用)。 +## 与 doc/弟子属性.md §1 权威属性表一一对应;加新属性=加一行。 +## 注意:name 为字符串不在此表(直接字段读写);id 由 create() 分配(只读语义,见 §3)。 +const RANGES := { + # ---- 五行属性 ---- + "metal": [0, 100], # 金 + "wood": [0, 100], # 木 + "water": [0, 100], # 水 + "fire": [0, 100], # 火 + "earth": [0, 100], # 土 + + # ---- 修炼属性 ---- + "efficiency": [1, 100], # 灵气吸收效率(隐藏) + "realm_level": [Realm.MORTAL, Realm.SPIRIT_TRANSFORMATION], # 大境界(枚举 Realm) + "sub_realm_level": [1, 13], # 小境界:凡人1 / 练气1~13 / 筑基+每境1~4 + "cultivation_exp": [0, 2147483647], # 修炼经验 + + # ---- 技能属性(每技能 = 等级 + 经验两字段,词干区分领域)---- + "farming_level": [0, 10], # 耕作等级(凡粮·外田) + "farming_exp": [0, 2147483647], # 耕作经验 + "herb_level": [0, 10], # 药草种植等级(灵药·内田) + "herb_exp": [0, 2147483647], # 药草种植经验 + "alchemy_level": [0, 10], # 炼丹等级 + "alchemy_exp": [0, 2147483647], # 炼丹经验 + + # ---- 其他属性 ---- + "loyalty": [0, 100], # 忠诚度 +} + +# 五行属性字段(数据字典缺哪个就填 0) +const ELEMENT_FIELDS: Array[String] = ["metal", "wood", "water", "fire", "earth"] + +## 各境界的小境界上限(索引 = realm_level):凡人1 / 练气13 / 筑基~化神 各4 +const SUB_MAX_BY_REALM := [1, 13, 4, 4, 4, 4] + +## 大境界显示名(索引 = realm_level) +const REALM_NAMES := ["凡人", "练气", "筑基", "金丹", "元婴", "化神"] +## 小境界显示名(索引 = sub_realm_level,0 占位不用):筑基起的 前/中/后/大圆满 +const SUB_NAMES := ["", "前期", "中期", "后期", "大圆满"] + +## 灵根阈值(占位,待调参):任一五行 ≥ 此值即具灵根,可修炼破境 +const SPIRITUAL_ROOT_THRESHOLD := 20 +## 修炼经验存储上限(属性表定义),满级(化神大圆满)时的经验哨兵 +const EXP_CAP := 2147483647 + +## 修炼经验曲线(占位,待调参):键 "{realm}_{sub}" → 该小境界经验上限。 +## 共 29 条:凡人 1 + 练气 13 + 筑基/金丹/元婴/化神 各 4。 +## 化神大圆满(5_4)无上限不入表,exp_cap() 满级时返回 EXP_CAP。 +const CULTIVATION_EXP_CAPS := { + # 凡人期:有灵根凡人在此积累经验,满则破境入练气 + "0_1": 100, + # 练气 1~13 层 + "1_1": 100, "1_2": 120, "1_3": 140, "1_4": 160, "1_5": 180, + "1_6": 200, "1_7": 220, "1_8": 240, "1_9": 260, "1_10": 280, + "1_11": 300, "1_12": 320, "1_13": 340, + # 筑基 前期/中期/后期/大圆满 + "2_1": 400, "2_2": 460, "2_3": 530, "2_4": 610, + # 金丹 + "3_1": 700, "3_2": 810, "3_3": 930, "3_4": 1070, + # 元婴 + "4_1": 1250, "4_2": 1450, "4_3": 1680, "4_4": 1950, + # 化神(大圆满 5_4 封顶,不入表) + "5_1": 2300, "5_2": 2700, "5_3": 3150, +} + +## 技能经验曲线(占位,待调参):索引 = 当前等级 N(0~9)→ 升到 N+1 所需经验。 +## 耕作/药草种植/炼丹三套技能共用一张表,将来可拆分(doc/弟子属性.md §5.2)。 +const SKILL_EXP_CAPS := [100, 150, 220, 320, 460, 660, 940, 1350, 1900, 2700] + +## ---------- 字段 ---------- +## 各字段默认值即"无数据时的出生值";_init/create 只覆盖 data 中出现的键。 +## 直接读写仅限 id/name(id 应经 create 分配);数值属性读写必须走 get_attr/set_attr。 + +var id: int = 0 # 唯一标识,由 create() 从 data 分配(0 = 未分配) +var name: String = "" # 姓名 + +## 五行:出生后可变,一律经 create()/DiscipleManager.set_attr 写入 +var metal: int = 0 # 金 +var wood: int = 0 # 木 +var water: int = 0 # 水 +var fire: int = 0 # 火 +var earth: int = 0 # 土 + +## 灵气吸收效率 1~100,决定修炼速度(隐藏属性) +var efficiency: int = 1 + +## 大境界(枚举 Realm:MORTAL=凡人 QI_REFINING=练气 FOUNDATION=筑基 GOLDEN_CORE=金丹 NASCENT_SOUL=元婴 SPIRIT_TRANSFORMATION=化神) +var realm_level: int = Realm.MORTAL +## 小境界:凡人固定1,练气1~13,筑基起1~4 +var sub_realm_level: int = 1 +## 修炼经验(每小境界上限查曲线,跨小境界溢出继承,跨大境界清零) +var cultivation_exp: int = 0 + +## 耕作等级 0~10(凡粮·外田) +var farming_level: int = 0 +## 耕作经验 +var farming_exp: int = 0 +## 药草种植等级 0~10(灵药·内田) +var herb_level: int = 0 +## 药草种植经验 +var herb_exp: int = 0 +## 炼丹等级 0~10 +var alchemy_level: int = 0 +## 炼丹经验 +var alchemy_exp: int = 0 + +## 忠诚度 0~100 +var loyalty: int = 50 + +## 属性总量(只读,实时由五行之和计算) +var total_amount: int: + get: + return metal + wood + water + fire + earth + +## ---------- 构造与工厂 ---------- + +## 构造:按 data 覆盖属性,数值属性经 RANGES 夹取;缺键保留字段默认值。 +## @param data: 键名与属性名一致,如 {"id": 1, "name": "玄真", "metal": 60} +func _init(data: Dictionary[String, Variant] = {}) -> void: + if data.has("id"): + id = int(data["id"]) + if data.has("name"): + name = str(data["name"]) + for key in RANGES: + if data.has(key): + set_attr(key, int(data[key])) + +## 工厂:唯一创建入口。id 必填,缺失则报错返回 null(防止未分配 id 的孤儿对象)。 +## @param data: 构造字典(须含 "id") +## @return: 新弟子;id 缺失返回 null +static func create(data: Dictionary) -> Disciple: + if not data.has("id"): + push_error("Disciple.create: 缺少 id,放弃生成 Disciple") + return null + return Disciple.new(data) + +## ---------- 读写接口 ---------- + +## 数值属性是否存在(RANGES 登记表中的键)。 +## @param key: 属性名 +## @return: true=是数值属性 +func has_attr(key: String) -> bool: + return RANGES.has(key) + +## 读数值属性。name/id 请直接字段访问(id 不经此函数,name 为字符串)。 +## 未知键名 push_error 并返回 0(防止拼错键名静默出错)。 +## @param key: RANGES 登记表中的属性名 +## @return: 当前值 +func get_attr(key: String) -> int: + if not RANGES.has(key): + push_error("Disciple.get_attr: 未知数值属性 \"%s\"" % key) + return 0 + return int(get(key)) + +## 写数值属性(唯一数值写入入口):RANGES 夹取 + 境界/小境界联动约束。 +## 未知键名报错返回 false;不直接改 name/id。 +## @param key: RANGES 登记表中的属性名 +## @param value: 新值(越界自动夹取到合法范围) +## @return: true=写入成功 +func set_attr(key: String, value: int) -> bool: + if not RANGES.has(key): + push_error("Disciple.set_attr: 未知数值属性 \"%s\"" % key) + return false + var range_: Array = RANGES[key] + var v := clampi(value, int(range_[0]), int(range_[1])) + if key == "realm_level": + # 大境界变化后,小境界不得超过新境界的上限(如落入凡人则强制 1) + set("realm_level", v) + var max_sub := SUB_MAX_BY_REALM[v] + if int(get("sub_realm_level")) > max_sub: + set("sub_realm_level", max_sub) + elif key == "sub_realm_level": + # 小境界写入时按当前大境界上限夹取(凡人上限 1 / 练气 13 / 其余 4) + var max_sub := SUB_MAX_BY_REALM[int(get("realm_level"))] + set("sub_realm_level", clampi(v, 1, max_sub)) + else: + set(key, v) + return true + +## ---------- 派生只读(不存储,由属性实时计算) ---------- + +## 当前大境界允许的小境界上限(凡人1 / 练气13 / 筑基起4)。 +func sub_realm_max() -> int: + return SUB_MAX_BY_REALM[realm_level] + +## 是否具灵根:任一五行 ≥ SPIRITUAL_ROOT_THRESHOLD。 +## 有灵根是修炼的前提(凡人无灵根则终生无法破境)。 +func has_spiritual_root() -> bool: + for f in ELEMENT_FIELDS: + if int(get(f)) >= SPIRITUAL_ROOT_THRESHOLD: + return true + return false + +## 是否满级(化神大圆满):到达后不再获得修炼经验。 +func is_max_realm() -> bool: + return realm_level == Realm.SPIRIT_TRANSFORMATION \ + and sub_realm_level == SUB_MAX_BY_REALM[Realm.SPIRIT_TRANSFORMATION] + +## 能否修炼:有灵根 且 未满级。 +func can_cultivate() -> bool: + return has_spiritual_root() and not is_max_realm() + +## 是否凡俗:大境界为凡人且无灵根。 +func is_mortal() -> bool: + return realm_level == Realm.MORTAL and not has_spiritual_root() + +## 当前 (realm, sub) 的修炼经验上限;满级返回 EXP_CAP(不再积累)。 +func exp_cap() -> int: + if is_max_realm(): + return EXP_CAP + var key := "%d_%d" % [realm_level, sub_realm_level] + if not CULTIVATION_EXP_CAPS.has(key): + push_error("Disciple.exp_cap: 曲线表缺少条目 %s" % key) + return EXP_CAP + return int(CULTIVATION_EXP_CAPS[key]) + +## 完整境界显示名:凡人 / 练气5层 / 筑基后期 / 化神大圆满。 +func realm_name() -> String: + if realm_level == Realm.MORTAL: + return REALM_NAMES[Realm.MORTAL] + if realm_level == Realm.QI_REFINING: + return "练气%d层" % sub_realm_level + return "%s%s" % [REALM_NAMES[realm_level], SUB_NAMES[sub_realm_level]] + +## ---------- 序列化(存档网关) ---------- + +## 输出全属性字典(键名与构造一致),供管理器 to_dict 汇总存档。 +func to_dict() -> Dictionary: + var result := {"id": id, "name": name} + for key in RANGES: + result[key] = get_attr(key) + return result diff --git a/src/Data/Models/Disciple.gd.uid b/src/Data/Models/Disciple.gd.uid new file mode 100644 index 0000000..78537b2 --- /dev/null +++ b/src/Data/Models/Disciple.gd.uid @@ -0,0 +1 @@ +uid://bttw23ffqifbv