Compare commits

..
13 Commits
Author SHA1 Message Date
Admin dded4e0c8f Merge branch 'dev_UI' into dev 2026-09-10 00:28:58 +08:00
Admin ae605c5e48 DiscipleManager 与 TimeSystem 随 MainGame 场景挂载,打通弟子增删信号到 UI
生命周期重构:
- TimeSystem 退出 Autoload,改挂 MainGame.tscn 子节点,补 class_name;
  HUD 对时间系统的引用由全局名改为 %TimeSystem(unique_name_in_owner)
- DiscipleManager 挂 MainGame.tscn(原无效 Autoload 注册已删),
  进游戏随场景创建、返回主菜单随树销毁,杜绝弟子数据跨轮回残留

信号闭环:
- 场景连接 disciple_added / disciple_removed 到 MainGame 回调,
  回调转调 Manager.add_disciple_card / remove_disciple_card(卡按 id 同步增删)
- MainGame._ready 按 NEW_GAME 触发 spawn_initial_roster(),
  父节点 _ready 时序最晚,保证卡片连接先于首批信号就绪
- 恢复误删的 sign_return_mainmenu 场景连接(返回主菜单建组存档链路)

注意:MainGame._process 内含临时删除测试(1000 帧后删 id=2 弟子),
演示移除链路用,后续版本应移除
2026-09-10 00:27:54 +08:00
Admin c1fb348f07 管理面板弟子卡支持 id 存取与按 id 删除
- DiscipleInformation 新增 disciple_id 字段,set_info 时写入对应 Disciple 的 id
- Manager 废弃 test_disciple_table 手动建卡,新增 add_disciple_card:
  Disciple 对象 → 信息字典(含 id/name/realm_name/五行)→ 实例化卡片
- 新增 remove_disciple_card(id):遍历 HFlowContainer 按 disciple_id
  匹配摘除并销毁(供 disciple_removed 信号驱动)
2026-09-10 00:27:44 +08:00
Admin 050c8116ad 修复初始编队生成:对入参建可写副本再补发 id
- Disciple.create 强制要求 data 含 id,且 INITIAL_ROSTER 为 const 常量:
  原实现先 create 后补 id 导致 5 人全部创建失败
- 改为 duplicate() 副本上写入 _master_id 发号,避免污染常量
- INITIAL_ROSTER 补手写 id 占位字段(实际以发号器为准)
2026-09-10 00:27:40 +08:00
Admin f38bcfd210 修复 Disciple 模型:放宽 _init 字典形参、显式化小境界上限类型推断
- _init 形参由 Dictionary[String, Variant] 放宽为 Dictionary:
  duplicate() 产物为普通字典,运行时类型校验导致 create() 失败
- SUB_MAX_BY_REALM 索引两处改用显式 int 声明,消除 Cannot infer 错误
- doc/弟子管理.md 补充修炼经验说明
2026-09-10 00:27:36 +08:00
Admin 2dbbe48f96 Merge branch 'dev' into dev_UI 2026-09-09 21:50:26 +08:00
Admin 0f0dc74e1e Merge branch 'dev' of ssh://git.devquest.cn:2222/chentianya/ImmortalSect into dev 2026-09-09 21:50:19 +08:00
Admin df82e61cf9 Merge branch 'dev' into dev_UI 2026-09-09 21:13:30 +08:00
Admin 8a793fa198 Merge branch 'dev_UI' into dev 2026-09-09 21:13:24 +08:00
Admin 4d1e4d8248 管理界面改为动态创建页签并注册各页面板 2026-09-09 21:08:50 +08:00
Admin 257aeccd2a 新增弟子/建筑/订单/炼丹/库房信息卡脚本与填充接口 2026-09-09 21:08:47 +08:00
cty@comp 190c9dab9e Merge branch 'dev_弟子系统' into dev 2026-09-08 22:52:56 +08:00
cty@comp bea9300d2b 新增Disciple弟子数据类与DiscipleManager管理器及弟子属性设计文档 2026-09-08 22:49:23 +08:00
32 changed files with 1077 additions and 85 deletions
+124
View File
@@ -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 的讨论结论整理(本文件前身为占位草稿)
+39
View File
@@ -83,3 +83,42 @@ 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[分配灵气利用率]
```
## 修炼说明
每个小境界修炼需要的经验 = 基础经验值 * 属性总量
经验获取量 = 基础经验 * 灵气吸收效率
+158
View File
@@ -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 信号分工 |
-1
View File
@@ -18,7 +18,6 @@ config/icon="res://icon.svg"
[autoload]
GameState="*res://src/Core/GameState.gd"
TimeSystem="*res://src/Core/TimeSystem.gd"
Settings="*res://src/Core/Settings.gd"
SaveSystem="*res://src/Core/SaveSystem.gd"
+164
View File
@@ -0,0 +1,164 @@
# 初始版本不提供添加弟子的接口
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 用 int0凡人/1练气,=Disciple.Realm 枚举值)
const INITIAL_ROSTER := [
# 1 玄真(宗主):修士·炼气9层,五行全高,炼丹3级
{
"name": "玄真",
"id" : 1,
"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": "凌霄",
"id" : 2,
"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": "青禾",
"id" : 3,
"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": "阿福",
"id" : 4,
"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": "阿禄",
"id" : 5,
"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 的唯一来源
func _enter_tree() -> void:
pass
# ---- 工厂 ----
# 初始编队(复用 _create_from_data
func spawn_initial_roster() -> void:
for data in INITIAL_ROSTER:
var disciple = _create_from_data(data)
if disciple != null:
add_disciple(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 copy := data.duplicate() # 可写副本(浅拷贝,顶层加键足够)
copy["id"] = _master_id
_master_id += 1
var disciple := Disciple.create(copy)
if disciple == null:
push_error("Failed to create disciple from data: %s" % data)
return null
return disciple
# ---- 仓储 ----
# 公开接口:添加弟子
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
+1
View File
@@ -0,0 +1 @@
uid://bstiea1nx61b8
+22
View File
@@ -1,5 +1,17 @@
extends Node
var cnt := 0;
func _ready() -> void:
%DiscipleManager.spawn_initial_roster()
pass
func _process(delta: float) -> void:
if cnt >= 1000:
var d : Disciple= %DiscipleManager.get_disciple(2);
%DiscipleManager.remove_disciple(d);
else:
cnt = cnt + 1;
pass
func _on_top_bar_hud_sign_return_mainmenu() -> void:
if GameState.mode == GameState.GameMode.NEW_GAME:
@@ -8,3 +20,13 @@ func _on_top_bar_hud_sign_return_mainmenu() -> void:
SaveSystem.save(GameState.profile_id) # 进游戏立即存首档,防开局就丢
get_tree().change_scene_to_file("res://src/UI/Panels/MainMenu/MainMenu.tscn")
pass # Replace with function body.
func _on_disciple_manager_disciple_added(disciple: Disciple) -> void:
$PanelContainer/Manager.add_disciple_card(disciple)
pass # Replace with function body.
func _on_disciple_manager_disciple_removed(disciple: Disciple) -> void:
$PanelContainer/Manager.remove_disciple_card(disciple.id)
pass # Replace with function body.
+16 -2
View File
@@ -1,12 +1,14 @@
[gd_scene format=3 uid="uid://c1exffg84ojqx"]
[ext_resource type="Script" uid="uid://cr4by5k8vgkcq" path="res://src/Core/MainGame.gd" id="1_8bu23"]
[ext_resource type="Script" uid="uid://cr4by5k8vgkcq" path="res://src/Core/MainGame.gd" id="1_2atgf"]
[ext_resource type="Script" uid="uid://bstiea1nx61b8" path="res://src/Character/DiscipleManager.gd" id="1_dl5bp"]
[ext_resource type="PackedScene" uid="uid://d3njijtx7ws1m" path="res://src/UI/HUD/MainSceneHud.tscn" id="2_8bu23"]
[ext_resource type="StyleBox" uid="uid://e86yw57h4v2s" path="res://resources/manager_style_box_flat.tres" id="3_dl5bp"]
[ext_resource type="PackedScene" uid="uid://d1mbwdtarjqof" path="res://src/UI/Panels/Manager/Manager.tscn" id="4_58g0x"]
[ext_resource type="Script" uid="uid://b8f8t2phmhq2e" path="res://src/Core/TimeSystem.gd" id="5_58g0x"]
[node name="MainGame" type="Node" unique_id=42551660]
script = ExtResource("1_8bu23")
script = ExtResource("1_2atgf")
[node name="MainHud" parent="." unique_id=836125968 instance=ExtResource("2_8bu23")]
size_flags_horizontal = 3
@@ -23,4 +25,16 @@ theme_override_styles/panel = ExtResource("3_dl5bp")
[node name="Manager" parent="PanelContainer" unique_id=360636457 instance=ExtResource("4_58g0x")]
layout_mode = 2
[node name="DiscipleManager" type="Node" parent="." unique_id=1485476702]
unique_name_in_owner = true
script = ExtResource("1_dl5bp")
metadata/_custom_type_script = "uid://bstiea1nx61b8"
[node name="TimeSystem" type="Node" parent="." unique_id=1280036358]
unique_name_in_owner = true
script = ExtResource("5_58g0x")
metadata/_custom_type_script = "uid://b8f8t2phmhq2e"
[connection signal="disciple_added" from="DiscipleManager" to="." method="_on_disciple_manager_disciple_added"]
[connection signal="disciple_removed" from="DiscipleManager" to="." method="_on_disciple_manager_disciple_removed"]
[connection signal="sign_return_mainmenu" from="MainHud" to="." method="_on_top_bar_hud_sign_return_mainmenu"]
+1
View File
@@ -1,6 +1,7 @@
## 全局时间系统(Autoload 单例)。
## 负责游戏时间的推进与播报,仅在世界场景中运行。
## 历法:12 月 × 30 日 = 360 日/年。
class_name TimeSystem
extends Node
const MONTHS_PER_YEAR := 12
+253
View File
@@ -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_level0 占位不用):筑基起的 前/中/后/大圆满
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/nameid 应经 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
## 大境界(枚举 RealmMORTAL=凡人 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 = {}) -> 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 : int = 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 : int = 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
+1
View File
@@ -0,0 +1 @@
uid://bttw23ffqifbv
+3 -3
View File
@@ -6,8 +6,8 @@ extends Control
signal sign_return_mainmenu
func _ready() -> void:
time_label.text = TimeSystem.get_date_text()
TimeSystem.month_passed.connect(_on_TimeMonthPassed)
time_label.text = %TimeSystem.get_date_text()
%TimeSystem.month_passed.connect(_on_TimeMonthPassed)
pass
func _on_ButtonEsc_ButtonUp() -> void:
@@ -20,5 +20,5 @@ func _on_TimeMonthPassed(_year: int, _month: int) -> void:
func _on_button_continue_button_up() -> void:
TimeSystem.advance_month()
%TimeSystem.advance_month()
pass # Replace with function body.
@@ -0,0 +1,10 @@
extends Control
func _ready() -> void:
custom_minimum_size = Vector2(180, 240)
custom_maximum_size = Vector2(180, 240)
pass
func set_alchemy_info(info : Dictionary) -> bool:
$PanelContainer/HBoxContainer/HBoxContainer/Name.text = info['name']
return true;
@@ -0,0 +1 @@
uid://bictkemio85fb
@@ -2,6 +2,7 @@
[ext_resource type="StyleBox" uid="uid://kxc0y8146vlk" path="res://resources/manager_info_style_box_flat.tres" id="1_62fqa"]
[ext_resource type="LabelSettings" uid="uid://dur4uye3m0ffd" path="res://resources/manager_info_label_settings.tres" id="1_j02a5"]
[ext_resource type="Script" uid="uid://bictkemio85fb" path="res://src/UI/Panels/Manager/AlchemyPanel/AlchemyPanel.gd" id="1_kuh5g"]
[node name="AlchemyPanel" type="Control" unique_id=729180406]
custom_minimum_size = Vector2(180, 240)
@@ -12,6 +13,7 @@ anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_kuh5g")
[node name="PanelContainer" type="PanelContainer" parent="." unique_id=1524260599]
layout_mode = 1
@@ -0,0 +1,21 @@
extends Control
var disciple_id: int;
func _ready() -> void:
custom_maximum_size = Vector2(180, 320);
custom_minimum_size = Vector2(180, 240);
pass;
func set_info(info: Dictionary):
disciple_id = info['id'];
$VBoxContainer/HBoxContainer/Name.text = info['name'];
$VBoxContainer/HBoxContainer2/Level.text = info['level']
$VBoxContainer/HBoxContainer3/GridContainer/MetalVal.text = str(info['metal'])
$VBoxContainer/HBoxContainer3/GridContainer/WoodVal.text = str(info['wood'])
$VBoxContainer/HBoxContainer3/GridContainer/WaterVal.text = str(info['water'])
$VBoxContainer/HBoxContainer3/GridContainer/FireVal.text = str(info['fire'])
$VBoxContainer/HBoxContainer3/GridContainer/EarthVal.text = str(info['earth'])
#print(info)
pass
@@ -0,0 +1 @@
uid://cc7c76lgm5nrf
@@ -2,18 +2,20 @@
[ext_resource type="LabelSettings" uid="uid://dur4uye3m0ffd" path="res://resources/manager_info_label_settings.tres" id="1_abpqa"]
[ext_resource type="StyleBox" uid="uid://kxc0y8146vlk" path="res://resources/manager_info_style_box_flat.tres" id="1_po3i5"]
[ext_resource type="Script" uid="uid://cc7c76lgm5nrf" path="res://src/UI/Panels/Manager/DisciplePanel/DiscipleInformation.gd" id="2_6fig8"]
[node name="DiscipleInformationPanel" type="PanelContainer" unique_id=2024414593]
custom_minimum_size = Vector2(180, 240)
custom_maximum_size = Vector2(180, 240)
custom_maximum_size = Vector2(180, 320)
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_styles/panel = ExtResource("1_po3i5")
script = ExtResource("2_6fig8")
metadata/discipleId = 0
[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=1066350214]
layout_mode = 2
@@ -25,7 +27,6 @@ size_flags_vertical = 3
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer" unique_id=2078348112]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
text = "姓名"
label_settings = ExtResource("1_abpqa")
@@ -44,7 +45,6 @@ size_flags_vertical = 3
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer2" unique_id=233829191]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
text = "境界"
label_settings = ExtResource("1_abpqa")
@@ -60,100 +60,98 @@ label_settings = ExtResource("1_abpqa")
[node name="HBoxContainer3" type="HBoxContainer" parent="VBoxContainer" unique_id=1358402714]
layout_mode = 2
size_flags_vertical = 3
size_flags_stretch_ratio = 5.0
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer3" unique_id=1781987484]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
text = "属性"
text = "属
性"
label_settings = ExtResource("1_abpqa")
horizontal_alignment = 1
vertical_alignment = 1
[node name="VBoxContainer2" type="VBoxContainer" parent="VBoxContainer/HBoxContainer3" unique_id=251442021]
[node name="GridContainer" type="GridContainer" parent="VBoxContainer/HBoxContainer3" unique_id=431356438]
layout_mode = 2
size_flags_horizontal = 3
columns = 2
[node name="Metal" type="Label" parent="VBoxContainer/HBoxContainer3/VBoxContainer2" unique_id=395220991]
[node name="Metal" type="Label" parent="VBoxContainer/HBoxContainer3/GridContainer" unique_id=395220991]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
text = "金"
label_settings = ExtResource("1_abpqa")
vertical_alignment = 1
[node name="Wood" type="Label" parent="VBoxContainer/HBoxContainer3/VBoxContainer2" unique_id=1852801128]
[node name="MetalVal" type="Label" parent="VBoxContainer/HBoxContainer3/GridContainer" unique_id=334471189]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
text = "1"
label_settings = ExtResource("1_abpqa")
horizontal_alignment = 1
vertical_alignment = 1
[node name="Wood" type="Label" parent="VBoxContainer/HBoxContainer3/GridContainer" unique_id=1852801128]
layout_mode = 2
size_flags_vertical = 3
text = "木"
label_settings = ExtResource("1_abpqa")
vertical_alignment = 1
[node name="Water" type="Label" parent="VBoxContainer/HBoxContainer3/VBoxContainer2" unique_id=1864946709]
[node name="WoodVal" type="Label" parent="VBoxContainer/HBoxContainer3/GridContainer" unique_id=1099683329]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
text = "1"
label_settings = ExtResource("1_abpqa")
horizontal_alignment = 1
vertical_alignment = 1
[node name="Water" type="Label" parent="VBoxContainer/HBoxContainer3/GridContainer" unique_id=1864946709]
layout_mode = 2
size_flags_vertical = 3
text = "水"
label_settings = ExtResource("1_abpqa")
vertical_alignment = 1
[node name="Fire" type="Label" parent="VBoxContainer/HBoxContainer3/VBoxContainer2" unique_id=1916838020]
[node name="WaterVal" type="Label" parent="VBoxContainer/HBoxContainer3/GridContainer" unique_id=1520779703]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
text = "1"
label_settings = ExtResource("1_abpqa")
horizontal_alignment = 1
vertical_alignment = 1
[node name="Fire" type="Label" parent="VBoxContainer/HBoxContainer3/GridContainer" unique_id=1916838020]
layout_mode = 2
size_flags_vertical = 3
text = "火"
label_settings = ExtResource("1_abpqa")
vertical_alignment = 1
[node name="Earth" type="Label" parent="VBoxContainer/HBoxContainer3/VBoxContainer2" unique_id=2106256122]
[node name="FireVal" type="Label" parent="VBoxContainer/HBoxContainer3/GridContainer" unique_id=66098798]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
text = "1"
label_settings = ExtResource("1_abpqa")
horizontal_alignment = 1
vertical_alignment = 1
[node name="Earth" type="Label" parent="VBoxContainer/HBoxContainer3/GridContainer" unique_id=2106256122]
layout_mode = 2
size_flags_vertical = 3
text = "土"
label_settings = ExtResource("1_abpqa")
vertical_alignment = 1
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer3" unique_id=565242402]
layout_mode = 2
size_flags_horizontal = 3
[node name="Metal" type="Label" parent="VBoxContainer/HBoxContainer3/VBoxContainer" unique_id=334471189]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
text = "1"
label_settings = ExtResource("1_abpqa")
vertical_alignment = 1
[node name="Wood" type="Label" parent="VBoxContainer/HBoxContainer3/VBoxContainer" unique_id=1099683329]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
text = "1"
label_settings = ExtResource("1_abpqa")
vertical_alignment = 1
[node name="Water" type="Label" parent="VBoxContainer/HBoxContainer3/VBoxContainer" unique_id=1520779703]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
text = "1"
label_settings = ExtResource("1_abpqa")
vertical_alignment = 1
[node name="Fire" type="Label" parent="VBoxContainer/HBoxContainer3/VBoxContainer" unique_id=66098798]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
text = "1"
label_settings = ExtResource("1_abpqa")
vertical_alignment = 1
[node name="Earth" type="Label" parent="VBoxContainer/HBoxContainer3/VBoxContainer" unique_id=170833691]
[node name="EarthVal" type="Label" parent="VBoxContainer/HBoxContainer3/GridContainer" unique_id=170833691]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
text = "1"
label_settings = ExtResource("1_abpqa")
horizontal_alignment = 1
vertical_alignment = 1
@@ -0,0 +1,10 @@
extends Control
func _ready() -> void:
custom_minimum_size = Vector2(180, 320)
custom_minimum_size = Vector2(180, 240)
pass
func set_field_info(info: Dictionary) -> bool:
$PanelContainer/VBoxContainer/Name.text = info['name']
return true;
@@ -0,0 +1 @@
uid://lw0x0n7r063b
@@ -1,5 +1,6 @@
[gd_scene format=3 uid="uid://bk5w5of88aysu"]
[ext_resource type="Script" uid="uid://lw0x0n7r063b" path="res://src/UI/Panels/Manager/FieldPanel/FieldInformation.gd" id="1_p6bv6"]
[ext_resource type="StyleBox" uid="uid://kxc0y8146vlk" path="res://resources/manager_info_style_box_flat.tres" id="1_rap8l"]
[ext_resource type="LabelSettings" uid="uid://dur4uye3m0ffd" path="res://resources/manager_info_label_settings.tres" id="2_p6bv6"]
@@ -12,6 +13,7 @@ anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_p6bv6")
[node name="PanelContainer" type="PanelContainer" parent="." unique_id=900478219]
layout_mode = 1
@@ -27,14 +29,12 @@ layout_mode = 2
[node name="Name" type="Label" parent="PanelContainer/VBoxContainer" unique_id=909840161]
layout_mode = 2
text = "AAA"
label_settings = ExtResource("2_p6bv6")
horizontal_alignment = 1
vertical_alignment = 1
[node name="Info" type="Label" parent="PanelContainer/VBoxContainer" unique_id=1950109494]
layout_mode = 2
text = "BBB"
label_settings = ExtResource("2_p6bv6")
[node name="GridContainer" type="GridContainer" parent="PanelContainer/VBoxContainer" unique_id=1107543260]
@@ -0,0 +1,39 @@
extends Control
## 测试数据:模拟 sect_data 里的嵌套资源(分类 → 子项)
var test_resources := [
{"name": "粮草", "items": [{"name": "", "quantity": 10}, {"name": "蔬菜", "quantity": 20}]},
{"name": "丹药", "items": [{"name": "聚气丹", "quantity": 5}]},
]
func _ready() -> void:
var tree: Tree = $ScrollContainer/Tree
tree.hide_root = true # 隐藏内部根,顶部就是"粮草/丹药"
tree.columns = 2
tree.item_selected.connect(_on_item_selected)
_refresh(tree)
pass
## 每次数据变化后全量重建(简单可靠)
func _refresh(tree: Tree) -> void:
tree.clear()
for category: Dictionary in test_resources:
var cat_item := tree.create_item() # 无父 = 顶级项(挂在隐藏根下)
cat_item.set_text(0, category["name"])
cat_item.set_metadata(0, {"type": "category"})
for sub: Dictionary in category["items"]:
var item := tree.create_item(cat_item) # 子项
item.set_text(0, sub["name"])
item.set_text(1, str(sub["quantity"]))
item.set_metadata(0, sub) # 存整条数据便于取用
## 动态新增一项后刷新
func _on_add_button() -> void: # 测试:手动调一次
test_resources[0]["items"].append({"name": "灵米", "quantity": 99})
_refresh($ScrollContainer/Tree)
func _on_item_selected() -> void:
var tree: Tree = $ScrollContainer/Tree
var selected := tree.get_selected()
if selected != null:
print(selected.get_text(0), "", selected.get_metadata(0))
@@ -0,0 +1 @@
uid://b55qsl2gyvt3e
@@ -1,5 +1,7 @@
[gd_scene format=3 uid="uid://dxij6vexu5m1q"]
[ext_resource type="Script" uid="uid://b55qsl2gyvt3e" path="res://src/UI/Panels/Manager/InventoryPanel/InventoryPanel.gd" id="1_th4yu"]
[node name="InventoryPanel" type="Control" unique_id=735363118]
layout_mode = 3
anchors_preset = 15
@@ -7,6 +9,7 @@ anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_th4yu")
[node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=887058553]
layout_mode = 1
+131
View File
@@ -1,6 +1,137 @@
extends Control
var test_field_table : Array[Dictionary] = [] # 建筑
var test_order_table : Array[Dictionary] = [] # 订单
var test_alchemy_table : Array[Dictionary] = [] # 炼丹配方
var test_inventory : Array[Dictionary] = [] # 库存
var test_sub_inventory1 : Array[Dictionary] = [] # 库存
var test_sub_inventory2 : Array[Dictionary] = [] # 库存
var test_sub_inventory3 : Array[Dictionary] = [] # 库存
var sect_resources : Dictionary;
var disciple_info_scene := preload("res://src/UI/Panels/Manager/DisciplePanel/DiscipleInformationPanel.tscn")
var field_info_scene := preload("res://src/UI/Panels/Manager/FieldPanel/FieldInformationPanel.tscn")
var order_info_scene := preload("res://src/UI/Panels/Manager/OrderPanel/OrderInformationPanel.tscn")
var alchemy_info_scene := preload("res://src/UI/Panels/Manager/AlchemyPanel/AlchemyPanel.tscn")
func _ready() -> void:
init_test_data();
var manager_panel_scene := load("res://src/UI/Panels/Manager/ManagerPanel.tscn") as PackedScene
var inventory_panel_scene := load("res://src/UI/Panels/Manager/InventoryPanel/InventoryPanel.tscn") as PackedScene
sect_resources["弟子"] = manager_panel_scene.instantiate();
sect_resources["弟子"].name = "弟子";
$TabContainer.add_child(sect_resources["弟子"]);
sect_resources["建筑"] = manager_panel_scene.instantiate();
sect_resources["建筑"].name = "建筑";
$TabContainer.add_child(sect_resources["建筑"]);
sect_resources["订单"] = manager_panel_scene.instantiate();
sect_resources["订单"].name = "订单";
$TabContainer.add_child(sect_resources["订单"]);
sect_resources["炼丹"] = manager_panel_scene.instantiate();
sect_resources["炼丹"].name = "炼丹";
$TabContainer.add_child(sect_resources["炼丹"]);
sect_resources["库房"] = inventory_panel_scene.instantiate();
sect_resources["库房"].name = "库房";
$TabContainer.add_child(sect_resources["库房"]);
for e in test_field_table:
var d := field_info_scene.instantiate()
d.set_field_info(e)
sect_resources["建筑"].add_info_card(d)
pass
for e in test_order_table:
var d := order_info_scene.instantiate()
d.set_order_info(e)
sect_resources['订单'].add_info_card(d)
pass
for e in test_alchemy_table:
var d := alchemy_info_scene.instantiate()
d.set_alchemy_info(e)
sect_resources['炼丹'].add_info_card(d)
pass
pass
func init_test_data() -> void:
test_field_table.append_array([
{"name": "灵田", "弟子": 0},
{"name": "灵田", "弟子": 1},
{"name": "灵药园", "弟子": 2},
{"name": "灵药园", "弟子": 3},
{"name": "灵药园", "弟子": 4},
{"name": "炼丹房", "弟子": 0}
])
test_order_table.append_array([
{"title": "炼制灵丹", "type": "炼丹", "level": 1, "reward": 100, "time": 10, "status": 0},
{"title": "炼制灵丹", "type": "炼丹", "level": 2, "reward": 200, "time": 20, "status": 0},
{"title": "炼制灵丹", "type": "炼丹", "level": 3, "reward": 300, "time": 30, "status": 0},
{"title": "炼制灵丹", "type": "炼丹", "level": 4, "reward": 400, "time": 40, "status": 0},
{"title": "炼制灵丹", "type": "炼丹", "level": 5, "reward": 500, "time": 50, "status": 0}
])
test_alchemy_table.append_array([
{"name": "灵丹", "herb_1": 0, "herb_2": 0, "herb_3": 0, "herb_4": 0, "herb_5": 0, "herb_6": 0, "herb_7": 0, "herb_8": 0, "herb_9": 0},
{"name": "灵丹", "herb_1": 0, "herb_2": 0, "herb_3": 0, "herb_4": 0, "herb_5": 0, "herb_6": 0, "herb_7": 0, "herb_8": 0, "herb_9": 0},
{"name": "灵丹", "herb_1": 0, "herb_2": 0, "herb_3": 0, "herb_4": 0, "herb_5": 0, "herb_6": 0, "herb_7": 0, "herb_8": 0, "herb_9": 0},
{"name": "灵丹", "herb_1": 0, "herb_2": 0, "herb_3": 0, "herb_4": 0, "herb_5": 0, "herb_6": 0, "herb_7": 0, "herb_8": 0, "herb_9": 0},
{"name": "灵丹", "herb_1": 0, "herb_2": 0, "herb_3": 0, "herb_4": 0, "herb_5": 0, "herb_6": 0, "herb_7": 0, "herb_8": 0, "herb_9": 0}
])
test_sub_inventory1.append_array([
{"name": "A", "type": "草药", "quantity": 10},
{"name": "B", "type": "草药", "quantity": 20},
{"name": "C", "type": "草药", "quantity": 30},
{"name": "D", "type": "草药", "quantity": 40},
{"name": "E", "type": "草药", "quantity": 50}
])
test_sub_inventory2.append_array([
{"name": "", "type": "粮食", "quantity": 10},
{"name": "蔬菜", "type": "粮食", "quantity": 20},
])
test_sub_inventory3.append_array([
{"name": "聚气丹", "type": "丹药", "quantity": 10},
{"name": "筑基丹", "type": "丹药", "quantity": 20},
])
test_inventory.append_array([{"草药": test_sub_inventory1}, {"粮食": test_sub_inventory2}, {"丹药": test_sub_inventory3}]);
pass
func add_disciple_card(d : Disciple) -> void:
var ds := disciple_info_scene.instantiate()
var info := {
"id": d.id,
"name": d.name,
"gender" : "x",
"level" : d.realm_name(),
"metal" : int(d.get_attr("metal")),
"wood" : int(d.get_attr("wood")),
"water" : int(d.get_attr("water")),
"fire" : int(d.get_attr("fire")),
"earth" : int(d.get_attr("earth"))
}
ds.set_info(info)
sect_resources["弟子"].add_info_card(ds)
pass
func remove_disciple_card(id: int) -> void:
var panel: Control = sect_resources["弟子"] # ManagerPanel 页
var flow: Node = panel.get_node("ScrollContainer/HFlowContainer")
for child in flow.get_children():
if child.disciple_id == id: # 指定项匹配
flow.remove_child(child) # 1. 先从树摘除
child.queue_free() # 2. 再排队销毁
return # 匹配唯一,删完即走
+1 -27
View File
@@ -1,8 +1,6 @@
[gd_scene format=3 uid="uid://d1mbwdtarjqof"]
[ext_resource type="Script" uid="uid://qjqb0jaqxuu2" path="res://src/UI/Panels/Manager/Manager.gd" id="1_qdh6a"]
[ext_resource type="PackedScene" uid="uid://cbqydw1lerumr" path="res://src/UI/Panels/Manager/ManagerPanel.tscn" id="2_ieenf"]
[ext_resource type="PackedScene" uid="uid://dxij6vexu5m1q" path="res://src/UI/Panels/Manager/InventoryPanel/InventoryPanel.tscn" id="3_k3wlu"]
[node name="Manager" type="Control" unique_id=360636457]
layout_mode = 3
@@ -22,28 +20,4 @@ anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
current_tab = 4
[node name="ManagerPanel" parent="TabContainer" unique_id=1475562434 instance=ExtResource("2_ieenf")]
visible = false
layout_mode = 2
metadata/_tab_index = 0
[node name="ManagerPanel2" parent="TabContainer" unique_id=393118788 instance=ExtResource("2_ieenf")]
visible = false
layout_mode = 2
metadata/_tab_index = 1
[node name="ManagerPanel3" parent="TabContainer" unique_id=609733971 instance=ExtResource("2_ieenf")]
visible = false
layout_mode = 2
metadata/_tab_index = 2
[node name="ManagerPanel4" parent="TabContainer" unique_id=780944038 instance=ExtResource("2_ieenf")]
visible = false
layout_mode = 2
metadata/_tab_index = 3
[node name="InventoryPanel" parent="TabContainer" unique_id=735363118 instance=ExtResource("3_k3wlu")]
layout_mode = 2
metadata/_tab_index = 4
tab_focus_mode = 1
+5
View File
@@ -0,0 +1,5 @@
extends Control
func add_info_card(card : Control) -> bool:
$ScrollContainer/HFlowContainer.add_child(card)
return true;
@@ -0,0 +1 @@
uid://beiiusgimt3s1
+5
View File
@@ -1,5 +1,7 @@
[gd_scene format=3 uid="uid://cbqydw1lerumr"]
[ext_resource type="Script" uid="uid://beiiusgimt3s1" path="res://src/UI/Panels/Manager/ManagerPanel.gd" id="1_1bkxy"]
[node name="ManagerPanel" type="Control" unique_id=1475562434]
layout_mode = 3
anchors_preset = 15
@@ -7,6 +9,7 @@ anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_1bkxy")
[node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=129376527]
layout_mode = 1
@@ -18,4 +21,6 @@ grow_vertical = 2
[node name="HFlowContainer" type="HFlowContainer" parent="ScrollContainer" unique_id=836450693]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
alignment = 1
@@ -0,0 +1,10 @@
extends Control
func _ready() -> void:
custom_minimum_size = Vector2(180, 240)
custom_maximum_size = Vector2(180, 240)
pass
func set_order_info(info : Dictionary) -> bool:
$PanelContainer/VBoxContainer/Title.text = info['title']
return true;
@@ -0,0 +1 @@
uid://cygiqebct7tk7
@@ -1,5 +1,6 @@
[gd_scene format=3 uid="uid://6ui4v8yhobr4"]
[ext_resource type="Script" uid="uid://cygiqebct7tk7" path="res://src/UI/Panels/Manager/OrderPanel/OrderInformation.gd" id="1_0aqu5"]
[ext_resource type="StyleBox" uid="uid://kxc0y8146vlk" path="res://resources/manager_info_style_box_flat.tres" id="1_87463"]
[ext_resource type="LabelSettings" uid="uid://dur4uye3m0ffd" path="res://resources/manager_info_label_settings.tres" id="2_sotk8"]
@@ -12,6 +13,7 @@ anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_0aqu5")
[node name="PanelContainer" type="PanelContainer" parent="." unique_id=1940446491]
layout_mode = 1