Compare commits

..
4 Commits
15 changed files with 655 additions and 2 deletions
+193
View File
@@ -0,0 +1,193 @@
# 弟子系统设计(D3:生成 + 入宗分流)
> 对应开发计划表 D3(8/5)。本文档只描述设计,不包含代码。
> 状态:设计稿,待审阅通过后实现
---
## 一、数据模型(Disciple
弟子用一条数据记录表示,不挂在场景树上。
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | int | 唯一编号,由 DiscipleManager 自增分配 |
| `name` | String | 随机生成的姓名 |
| `age` | int | 入宗年龄(12~18 |
| `aptitude` | 枚举 Aptitude | 资质(入宗分流结果) |
| `roots` | 枚举数组 SpiritRoot | 灵根列表(普通弟子 1~3 根,互不重复) |
| `is_heavenly` | bool | 天灵根标记(稀有灵根,见枚举二) |
| `realm_index` | int | 大境界索引(0=练气),D5 修炼系统推进 |
| `sub_realm_index` | int | 小境界索引(练气层数-1),D5 修炼系统推进 |
### 枚举一:资质四类(Aptitude
| 值 | 名称 | 修炼速度倍率 | 说明 |
|----|------|-------------|------|
| 0 | 下品 | 0.6x | 根基平庸 |
| 1 | 中品 | 1.0x | 资质平平 |
| 2 | 上品 | 1.5x | 天赋尚可 |
| 3 | 天骄 | 2.0x | 万中无一 |
> 倍率由 D5 修炼循环使用,此处只定义不消费。
### 枚举二:灵根五行(SpiritRoot
金、木、水、火、土 五种。灵根数量影响修炼速度(根数越少越快),元素种类影响日后生产效率匹配(D6 建筑/职能系统使用),此处只生成不消费。
### 特殊灵根:天灵根
稀有灵根,独立于五行枚举之外,用 `is_heavenly` 标记:
| 项目 | 设定 |
|------|------|
| 生成概率 | 3%(远低于四类资质中"天骄"的 5%,天灵根可叠加于任意资质之上) |
| 灵根构成 | 仅一系灵根(五行之一),纯净单灵根 |
| 修炼速度 | 在资质倍率基础上**额外 +50%**(乘法叠加,D5 修炼循环消费) |
| 地位 | 宗门争抢的种子弟子,日后可挂靠特殊培养事件(W8 预留) |
> 天灵根与资质是两个独立维度:资质定天赋底子,天灵根是稀缺加成,二者随机独立。
### 境界字段说明
新弟子统一为练气一层(realm_index=0, sub_realm_index=0)。境界显示文本规则:
- 练气期:练气 N 层
- 筑基及以上:大境界 + 初/中/后/圆满
境界的成长、突破门槛、寿元计算全部留给 D5 修炼循环,D3 只负责初始状态。
---
## 二、生成规则(随机性)
### 1. 姓名
- 姓氏池:24 个常用修仙风单姓(林、苏、叶、萧、顾、沈、楚、白……)
- 名用字池:24 个(清、玄、墨、尘、风、澜、霜、珏……)
- 规则:随机 1 个姓 + 1~2 个字(50% 概率双字名)
- 不保证唯一(同名弟子可共存,以 id 区分)
### 2. 年龄
12~18 岁均匀随机。寿元消耗、大限判断留给 D5。
### 3. 灵根
- 普通弟子:数量 1~3 根均匀随机,元素从五行中随机抽取、互不重复
- 天灵根:3% 概率判定为天灵根,灵根数量固定 1 根,修炼速度额外 +50%
- 含义:单灵根修炼最快,三灵根最慢(对应修仙世界观)
### 4. 资质(入宗分流)
按权重表掷骰:
| 资质 | 权重 | 概率 |
|------|------|------|
| 下品 | 45 | 45% |
| 中品 | 35 | 35% |
| 上品 | 15 | 15% |
| 天骄 | 5 | 5% |
> 概率说明:开发阶段固定此分布,后期根据游戏难度测试调整权重即可,不改代码结构。
---
## 三、入宗分流
"入宗分流"的含义:**按资质将弟子分为四类**,这是生成时的一次性判定,不是持续运行的机制。四类资质弟子日后可能享受不同的培养路线(D4+ 扩展),本期只完成分类本身。
### 招募机制:定时弹窗招募
招募**不是玩家随时点按钮**,而是"生成候选 → 暂停时间 → 弹窗选择":
- **开局招募**:新游戏开始,生成 **5 名候选**,暂停时间弹窗,**必须选满 3 名**才能确定
- **定期招募**:每满间隔(配置文件 `recruit_interval_years`,默认 0.1 年 = 36 天)生成 **3 名候选**,暂停时间弹窗
- **选择方式**:每个候选独立勾选"入宗",可全部接受、可全部拒绝(定期招募可 0~3 个)
- **结算**:点"确定" → 勾选的入宗、未勾选的拒绝 → 关闭弹窗 → **恢复时间**
- 所有参数(候选数、必选数、间隔)来自 `resources/game_config.cfg`,改配置即改难度
### 招募流程(文字时序)
1. 开局(MainGame 调用 `start_new_game()`)或倒计时归零(`_on_day_passed`
2. 生成候选弟子(先判天灵根 3% → 生成灵根 → 按权重掷资质 → 组名字),进入候选池
3. `applicants_generated(is_opening)` 信号发出 → 弹窗显示候选列表
4. 玩家勾选(开局模式:确定按钮在选满 3 名前禁用)
5. 点"确定" → `finish_recruitment(selected)` 结算 → 恢复时间
6. 控制台打印每位入宗弟子完整信息(姓名/年龄/资质/灵根/境界)—— 本期验收方式
---
## 四、DiscipleManager 接口
| 接口 | 签名 | 说明 |
|------|------|------|
| `start_new_game()` | 无 | 清空全部数据 → 开局候选(配置数量)→ 暂停时间 |
| `generate_applicants(count, is_opening)` | 返回无 | 清空候选池 → 生成 count 名候选 → 发信号 |
| `accept_applicant(d)` | 入参候选 | 分配 ID,候选池 → 名录,发信号,控制台打印 |
| `reject_applicant(d)` | 入参候选 | 从候选池移除 |
| `finish_recruitment(selected)` | 入参选中列表 | 结算:selected 内的入宗,其余全部拒绝 |
| `remove_disciple(d)` | 入参弟子 | 移出名录(逐出/陨落) |
| `reset()` | 无 | 清空名录与候选池、ID 与倒计时复位 |
| 信号 `disciples_changed` | 无参数 | 名录增删/重置时发出 |
| 信号 `applicants_generated(is_opening)` | bool | 候选生成时发出(弹窗监听) |
内部私有方法(不对外):生成姓名、生成灵根(含天灵根判定)、掷资质、创建候选、间隔换算。
内部定时机制:
- 字段 `_days_until_recruit`:距下次定期招募的剩余游戏天数
- 间隔 = `GameConfig.recruit_interval_years` × 360 天(TimeSystem 常量换算)
- 监听 `TimeSystem.day_passed`,每日减 1,归零 → 生成候选 → `TimeSystem.set_paused(true)`(弹窗期间时间冻结)
存放位置:`src/Character/`(与数据模型同目录),注册为 Autoload 单例,供全局访问。
## 四·五、配置系统(GameConfig
所有可调参数集中在 `resources/game_config.cfg`,由 `src/Core/GameConfig.gd`Autoload,声明在 DiscipleManager 之前)启动时读取,缺项回退代码默认值。
```ini
[recruitment]
opening_applicant_count=5 ; 开局候选人数
opening_pick_count=3 ; 开局必须选满数
recruit_interval_years=0.1 ; 定期招募间隔(游戏年)
recruit_applicant_count=3 ; 定期弹窗候选人数
```
> 以后所有可调参数(天赋权重、价格、境界门槛等)统一加到这里。
---
## 五、UI 接入点
- **RecruitPanel**`src/UI/Panels/RecruitPanel/`):弹窗场景,挂在 MainGame 下(默认隐藏)
- 监听 `applicants_generated` 信号自动弹出
- 每个候选一行:姓名/年龄/资质(天灵根标记)/灵根/境界 + "入宗"勾选框
- 底部:全部接受 / 全部拒绝 / 确定
- 开局模式确定按钮在选满 3 名前禁用;确定后关闭弹窗并恢复时间
- 名录的可视化列表属于 D4(弟子列表 UI),届时由 `disciples_changed` 信号驱动
---
## 六、与后续任务衔接
| 后续任务 | 衔接方式 |
|----------|----------|
| D4 弟子列表 UI | 监听 `disciples_changed` 信号 + 遍历 `disciples` 数组 |
| D5 修炼循环 | 消费 `aptitude` 倍率与 `realm_index/sub_realm_index` 字段 |
| D6 建筑/职能系统 | 消费 `roots` 元素与生产效率匹配 |
| D8 存档接入 | Disciple 数据模型为纯数据,可直接序列化 |
---
## 七、验收清单(D3 交付标准)
- [ ] 项目能正常启动,无脚本报错
- [ ] **开局**:进入游戏即暂停,弹出"弟子入宗(开局招募)",显示 5 名候选
- [ ] 开局模式确定按钮在选满 3 名前不可用;选满后可确定
- [ ] 确定后:勾选的 3 名入宗(控制台打印),时间恢复推进
- [ ] **定期**:每 36 游戏日(0.1 年 @1x ≈ 18 秒)再次暂停弹窗,显示 3 名候选
- [ ] 定期模式可勾选 0~3 名,全部拒绝也可确定;时间正常恢复
- [ ] 多次招募后能观察到天灵根弟子(约 3% 概率,弹窗/控制台有标记)
- [ ] 修改 `game_config.cfg``opening_applicant_count` 等参数,重启后开局候选数随之变化
- [ ] 返回主菜单再进入,名录已清空(reset 生效)
+2 -2
View File
@@ -1,7 +1,7 @@
# 开发计划表
> 开始日期:2026-08-03(周一) 每日有效开发约 2 小时
> 进度更新:2026-08-05  D1、D2 完成,D3 进行中
> 进度更新:2026-08-05  D1、D2、D3 完成,D4 进行中
## 一、每日作息模板(工作日)
@@ -21,7 +21,7 @@
| ---- | -- | ---- | ---- | -------- | ---- |
| 8/3 一 | D1 | TimeSystem | ① 日期变量(年月日) ② tick 推进 ③ 暂停/加速 | HUD 显示年月日,可调速度 | ✅ 完成(含月/年信号、Settings 持久化) |
| 8/4 二 | D2 | SectManager 数据模型 | ① 单例 ② 灵石/灵气/声望 ③ 增减接口 | 控制台可读写资源 | ✅ 完成(灵石/声望增减+信号,HUD 实时绑定) |
| 8/5 三 | D3 | 弟子生成 + 入宗分流 | ① 随机名字/资质/灵根 ② 按资质分四类 ③ 招募按钮 | 点按钮生成弟子入列表 | |
| 8/5 三 | D3 | 弟子生成 + 入宗分流 | ① 随机名字/资质/灵根 ② 按资质分四类 ③ 招募按钮 | 点按钮生成弟子入列表 | ✅ 完成(改为每 30 游戏日定时自动招募,含天灵根 3%) |
| 8/6 四 | D4 | 弟子列表 UI | ① 列表条目场景 ② 姓名/资质/境界显示 ③ 点击详情 | 列表可浏览弟子 | ⬜ |
| 8/7 五 | D5 | 修炼循环 | ① 修为随时间增长(资质×灵气) ② 境界门槛 ③ 突破判定 | 弟子自动修炼,可突破 | ⬜ |
| 8/8 六 | D6 | 建筑系统基础 | ① 灵石矿场景 ② 建造→每时辰产出 ③ 升级 | 建筑产灵石,升级加速 | ⬜ |
+2
View File
@@ -20,6 +20,8 @@ GameState="*res://src/Core/GameState.gd"
TimeSystem="*res://src/Core/TimeSystem.gd"
Settings="*res://src/Core/Settings.gd"
SectManager="*res://src/Core/SectManager.gd"
GameConfig="*res://src/Core/GameConfig.gd"
DiscipleManager="*res://src/Character/DiscipleManager.gd"
[display]
+9
View File
@@ -0,0 +1,9 @@
; 模拟宗门 全局配置
; 所有可调参数集中在此,修改后重启游戏生效
; 注释以 ; 开头,参数按系统分节
[recruitment]
opening_applicant_count=5
opening_pick_count=3
recruit_interval_years=0.1
recruit_applicant_count=3
+42
View File
@@ -0,0 +1,42 @@
## 弟子数据模型(纯数据,不挂在场景树上)。
## 生成时决定的属性(姓名/资质/灵根)不可变,成长属性(境界)由 D5 修炼系统推进。
class_name Disciple # 全局类名:全项目可直接用 Disciple.new()
extends Resource # Resource = 轻量纯数据对象,可序列化(D8 存档用)
## 资质四类(入宗分流结果)。
enum Aptitude { ORDINARY, MEDIUM, SUPERIOR, HEAVENLY } # 下品/中品/上品/天骄
## 灵根五行。
enum SpiritRoot { GOLD, WOOD, WATER, FIRE, EARTH }
# —— 常量表:枚举值 → 显示文本 / 倍率(按索引一一对应)——
const APTITUDE_NAMES: Array[String] = ["下品", "中品", "上品", "天骄"]
const APTITUDE_SPEED: Array[float] = [0.6, 1.0, 1.5, 2.0] # D5 用
const ROOT_NAMES: Array[String] = ["", "", "", "", ""]
const REALM_NAMES: Array[String] = ["练气", "筑基", "金丹", "元婴", "化神"] # 按 realm_index 索引
const HEAVENLY_BONUS := 1.5 # 天灵根额外修炼速度倍率(D5 用)
# —— 实例字段 ——
var id: int = 0 # 唯一编号(DiscipleManager 分配)
var name: String = ""
var age: int = 0
var aptitude: Aptitude = Aptitude.ORDINARY
var roots: Array[SpiritRoot] = [] # 普通弟子 1~3 根;天灵根固定 1 根
var is_heavenly: bool = false # 天灵根标记(3% 概率)
var realm_index: int = 0 # 大境界索引(0=练气)
var sub_realm_index: int = 0 # 小境界索引(练气层数-1
# —— 辅助函数:把数据翻译成给人看的文本 ——
func get_aptitude_name() -> String:
return APTITUDE_NAMES[aptitude]
func get_roots_text() -> String:
var names: Array[String] = []
for root in roots:
names.append(ROOT_NAMES[root])
return "".join(names)
func get_realm_text() -> String:
if realm_index == 0:
return "%s%d" % [REALM_NAMES[realm_index], sub_realm_index + 1]
return "%s%s" % [REALM_NAMES[realm_index], ["初期", "中期", "后期", "圆满"][sub_realm_index]]
+1
View File
@@ -0,0 +1 @@
uid://cuybveud5w308
+162
View File
@@ -0,0 +1,162 @@
## 弟子管理器(Autoload 单例)。
## 负责候选弟子生成、入宗选择与宗门名录维护。
## 招募流程:开局/定期生成候选 → 暂停时间 → 玩家在弹窗选择 → 结算入名录。
extends Node
## 名录变化时发出(增删/重置)。
signal disciples_changed
## 候选弟子生成时发出(开局/定期,弹窗 UI 监听此信号)。
## @param is_opening: 是否开局招募(开局必须选满,定期自由选择)
signal applicants_generated(is_opening: bool)
## 宗门弟子名录(已入宗)。
var disciples: Array[Disciple] = []
## 候选弟子池(未入宗,等待玩家选择)。
var applicants: Array[Disciple] = []
## 入宗分流权重(按资质四类):下品 / 中品 / 上品 / 天骄。
const APTITUDE_WEIGHTS: Array[int] = [45, 35, 15, 5]
## 天灵根生成概率(3%)。
const HEAVENLY_CHANCE := 0.03
## 姓氏池。
const SURNAMES: Array[String] = [
"", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "",
]
## 名用字池(修仙风)。
const GIVEN_CHARS: Array[String] = [
"", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "",
"", "", "", "屿", "", "", "", "",
]
## 距下次定期招募的剩余游戏天数。
var _days_until_recruit := 0
## 下一位入宗弟子的 ID(自增,重置归零)。
var _next_id := 1
func _ready() -> void:
# 挂到时间系统上:每日推进时倒计时,满间隔生成候选并暂停时间
TimeSystem.day_passed.connect(_on_day_passed)
_days_until_recruit = _get_interval_days()
## 每日推进:定期招募倒计时减 1,归零时生成候选并暂停时间。
func _on_day_passed(_year: int, _month: int, _day: int) -> void:
_days_until_recruit -= 1
if _days_until_recruit <= 0:
_days_until_recruit = _get_interval_days()
generate_applicants(GameConfig.recruit_applicant_count)
TimeSystem.set_paused(true)
## 开局新游戏:清空所有数据,生成开局候选(数量来自配置),暂停时间等待选择。
func start_new_game() -> void:
reset()
generate_applicants(GameConfig.opening_applicant_count, true)
TimeSystem.set_paused(true)
## 生成一批候选弟子:清空旧候选后按 count 生成,发信号供弹窗显示。
## @param count: 候选人数(调用方从 GameConfig 传入)
## @param is_opening: 是否开局招募
func generate_applicants(count: int, is_opening := false) -> void:
applicants.clear()
for i in count:
applicants.append(_create_applicant())
applicants_generated.emit(is_opening)
## 接受一名候选弟子:分配 ID 并移入名录(候选池 → 名录)。
## @param d: 要接受的候选
func accept_applicant(d: Disciple) -> void:
if not applicants.has(d):
return
applicants.erase(d)
d.id = _next_id
_next_id += 1
disciples.append(d)
disciples_changed.emit()
print("弟子入宗:%s %d岁 | %s资质%s | %s灵根 | %s" % [
d.name, d.age, d.get_aptitude_name(),
"·天灵根" if d.is_heavenly else "",
d.get_roots_text(), d.get_realm_text(),
])
## 拒绝一名候选弟子:从候选池移除,不进入名录。
## @param d: 要拒绝的候选
func reject_applicant(d: Disciple) -> void:
applicants.erase(d)
## 结算本次招募:selected 内的候选入宗,其余全部拒绝,清空候选池。
## @param selected: 玩家勾选的候选列表(弹窗"确定"时传入)
func finish_recruitment(selected: Array[Disciple]) -> void:
for d in applicants.duplicate():
if selected.has(d):
accept_applicant(d)
else:
reject_applicant(d)
## 移除已入宗弟子(逐出/陨落)。
## @param d: 要移除的弟子
func remove_disciple(d: Disciple) -> void:
disciples.erase(d)
disciples_changed.emit()
## 清空名录与候选池(新游戏开局调用),倒计时与 ID 复位。
func reset() -> void:
disciples.clear()
applicants.clear()
_next_id = 1
_days_until_recruit = _get_interval_days()
disciples_changed.emit()
## 定期招募间隔(游戏天数),由 GameConfig 的"年"换算而来。
func _get_interval_days() -> int:
var days_per_year := TimeSystem.MONTHS_PER_YEAR * TimeSystem.DAYS_PER_MONTH
return maxi(1, roundi(GameConfig.recruit_interval_years * days_per_year))
## 创建一名候选弟子:随机生成属性(姓名/年龄/灵根/资质),
## 不分配 ID——ID 在入宗时由 accept_applicant 分配,保证连续且无浪费。
func _create_applicant() -> Disciple:
var d := Disciple.new()
d.name = _generate_name()
d.age = randi_range(12, 18)
d.is_heavenly = randf() < HEAVENLY_CHANCE
d.roots = _generate_roots(d.is_heavenly)
d.aptitude = _roll_aptitude()
return d
## 随机生成姓名:单姓 + 1~2 个名用字。
func _generate_name() -> String:
var surname := SURNAMES[randi() % SURNAMES.size()]
var given := GIVEN_CHARS[randi() % GIVEN_CHARS.size()]
if randf() < 0.5:
given += GIVEN_CHARS[randi() % GIVEN_CHARS.size()]
return surname + given
## 随机生成灵根。
## 天灵根:固定 1 根五行;普通弟子:1~3 根互不重复(根数越少修炼越快)。
## @param heavenly: 是否天灵根
func _generate_roots(heavenly: bool) -> Array[Disciple.SpiritRoot]:
if heavenly:
return [randi() % Disciple.SpiritRoot.size() as Disciple.SpiritRoot]
var count := randi_range(1, 3)
var pool := Disciple.SpiritRoot.values()
pool.shuffle()
var result: Array[Disciple.SpiritRoot] = []
for i in count:
result.append(pool[i])
return result
## 按权重掷资质(入宗分流)。
func _roll_aptitude() -> Disciple.Aptitude:
var total := 0
for w in APTITUDE_WEIGHTS:
total += w
var roll := randi() % total
for i in APTITUDE_WEIGHTS.size():
if roll < APTITUDE_WEIGHTS[i]:
return i as Disciple.Aptitude
roll -= APTITUDE_WEIGHTS[i]
return Disciple.Aptitude.ORDINARY
+1
View File
@@ -0,0 +1 @@
uid://d2aj0gbvb6p4w
+32
View File
@@ -0,0 +1,32 @@
## 游戏全局配置(Autoload 单例)。
## 启动时从 resources/game_config.cfg 读取所有可调参数,
## 其他系统通过公开变量读取,如 GameConfig.recruit_interval_years。
extends Node
## 配置文件路径(res:// 开发期可改,导出后只读)。
const CONFIG_PATH := "res://resources/game_config.cfg"
# —— 招募系统(对应 cfg 的 [recruitment] 节)——
## 开局候选人数。
var opening_applicant_count := 5
## 开局必须选满数。
var opening_pick_count := 3
## 定期招募间隔(游戏年,1 年 = 360 天)。
var recruit_interval_years := 0.1
## 定期弹窗候选人数。
var recruit_applicant_count := 3
func _ready() -> void:
_load_config()
## 读取配置文件;缺失的项保持代码默认值,不会报错。
func _load_config() -> void:
var cfg := ConfigFile.new()
if cfg.load(CONFIG_PATH) != OK:
push_warning("GameConfig: 配置文件不存在,使用默认值")
return
opening_applicant_count = cfg.get_value("recruitment", "opening_applicant_count", opening_applicant_count)
opening_pick_count = cfg.get_value("recruitment", "opening_pick_count", opening_pick_count)
recruit_interval_years = cfg.get_value("recruitment", "recruit_interval_years", recruit_interval_years)
recruit_applicant_count = cfg.get_value("recruitment", "recruit_applicant_count", recruit_applicant_count)
+1
View File
@@ -0,0 +1 @@
uid://dlchibfd86oek
+2
View File
@@ -7,6 +7,8 @@ func _ready() -> void:
add_to_group("game_scene")
# 应用用户设置中的默认流速
TimeSystem.apply_default_speed()
# 开局招募:清空旧数据 → 生成 5 名候选 → 暂停时间等待选择(读档流程接入后由存档还原)
DiscipleManager.start_new_game()
## 顶部 HUD 的返回主菜单请求。
func _on_top_bar_hud_sign_return_mainmenu() -> void:
+3
View File
@@ -2,6 +2,7 @@
[ext_resource type="Script" uid="uid://centrk8l54j7c" path="res://src/Core/MainGame.gd" id="1_8bu23"]
[ext_resource type="PackedScene" uid="uid://dl0tb68vsw3c6" path="res://src/UI/HUD/TopBarHUD.tscn" id="1_g6xmk"]
[ext_resource type="PackedScene" path="res://src/UI/Panels/RecruitPanel/RecruitPanel.tscn" id="1_k5nrm"]
[node name="MainGame" type="Node" unique_id=2053561265]
groups = ["game_scene"]
@@ -9,4 +10,6 @@ script = ExtResource("1_8bu23")
[node name="TopBarHud" parent="." unique_id=1115394694 instance=ExtResource("1_g6xmk")]
[node name="RecruitPanel" parent="." unique_id=770000016 instance=ExtResource("1_k5nrm")]
[connection signal="sign_return_mainmenu" from="TopBarHud" to="." method="_on_top_bar_hud_sign_return_mainmenu"]
@@ -0,0 +1,96 @@
[gd_scene format=3 uid="uid://d4y7q3m6k2x5n7w2"]
[ext_resource type="Script" path="res://src/UI/Panels/RecruitPanel/recruit_panel.gd" id="1_script"]
[ext_resource type="FontFile" uid="uid://drt448qncx27p" path="res://assets/fonts/dingliezhuhaifont-20240831GengXinBan)-2.ttf" id="2_font"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dialog"]
bg_color = Color(0.11, 0.08, 0.13, 0.97)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[node name="RecruitPanel" type="Control" unique_id=770000001]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_script")
[node name="Backdrop" type="ColorRect" parent="." unique_id=770000002]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0, 0, 0.6)
[node name="CenterContainer" type="CenterContainer" parent="." unique_id=770000003]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Panel" type="PanelContainer" parent="CenterContainer" unique_id=770000004]
theme_override_styles/panel = SubResource("StyleBoxFlat_dialog")
[node name="MarginContainer" type="MarginContainer" parent="CenterContainer/Panel" unique_id=770000005]
theme_override_constants/margin_left = 28
theme_override_constants/margin_top = 20
theme_override_constants/margin_right = 28
theme_override_constants/margin_bottom = 20
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/Panel/MarginContainer" unique_id=770000006]
theme_override_constants/separation = 12
[node name="TitleLabel" type="Label" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=770000007]
unique_name_in_owner = true
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 30
text = "弟子入宗"
[node name="HintLabel" type="Label" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=770000008]
unique_name_in_owner = true
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 20
theme_override_colors/font_color = Color(0.78, 0.75, 0.82, 1)
text = ""
[node name="ScrollContainer" type="ScrollContainer" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=770000009]
custom_minimum_size = Vector2(640, 300)
size_flags_vertical = 3
[node name="ListContainer" type="VBoxContainer" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ScrollContainer" unique_id=770000010]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
[node name="ButtonRow" type="HBoxContainer" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=770000011]
theme_override_constants/separation = 8
[node name="Button_AcceptAll" type="Button" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow" unique_id=770000012]
unique_name_in_owner = true
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 22
text = "全部接受"
[node name="Button_RejectAll" type="Button" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow" unique_id=770000013]
unique_name_in_owner = true
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 22
text = "全部拒绝"
[node name="Spacer" type="Control" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow" unique_id=770000014]
layout_mode = 2
size_flags_horizontal = 3
[node name="Button_Confirm" type="Button" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow" unique_id=770000015]
unique_name_in_owner = true
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 22
text = "确定"
[connection signal="button_up" from="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow/Button_AcceptAll" to="." method="_on_button_accept_all_button_up"]
[connection signal="button_up" from="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow/Button_RejectAll" to="." method="_on_button_reject_all_button_up"]
[connection signal="button_up" from="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow/Button_Confirm" to="." method="_on_button_confirm_button_up"]
+108
View File
@@ -0,0 +1,108 @@
## 招募弹窗(挂载于 RecruitPanel.tscn)。
## 监听候选生成信号自动弹出,玩家勾选后点"确定"结算入宗。
extends Control
## 招募模式:开局必须选满 / 定期自由选择。
enum Mode { OPENING, PERIODIC }
@onready var title_label: Label = %TitleLabel
@onready var hint_label: Label = %HintLabel
@onready var list_container: VBoxContainer = %ListContainer
@onready var confirm_button: Button = %Button_Confirm
## 当前招募模式。
var _mode: Mode = Mode.PERIODIC
## 勾选框列表(与 _entry_disciples 按下标一一对应)。
var _check_buttons: Array[CheckButton] = []
## 对应勾选框的候选弟子。
var _entry_disciples: Array[Disciple] = []
func _ready() -> void:
# 订阅候选生成信号:开局与定期招募都会触发
DiscipleManager.applicants_generated.connect(_on_applicants_generated)
visible = false
## 候选生成时弹出:按当前候选池重建列表。
## @param is_opening: 是否开局招募
func _on_applicants_generated(is_opening: bool) -> void:
_mode = Mode.OPENING if is_opening else Mode.PERIODIC
_clear_entries()
for d in DiscipleManager.applicants:
_add_entry(d)
title_label.text = "弟子入宗(%s" % ("开局招募" if is_opening else "定期招募")
_update_ui()
visible = true
## 勾选任一候选后刷新提示与确定按钮可用性。
func _on_check_toggled(_pressed: bool, _d: Disciple) -> void:
_update_ui()
## 全部接受:勾选所有候选。
func _on_button_accept_all_button_up() -> void:
for c in _check_buttons:
c.set_pressed_no_signal(true)
_update_ui()
## 全部拒绝:取消所有勾选。
func _on_button_reject_all_button_up() -> void:
for c in _check_buttons:
c.set_pressed_no_signal(false)
_update_ui()
## 确定:勾选的入宗、未勾选的拒绝,关闭弹窗并恢复时间。
func _on_button_confirm_button_up() -> void:
var selected: Array[Disciple] = []
for i in _entry_disciples.size():
if _check_buttons[i].button_pressed:
selected.append(_entry_disciples[i])
DiscipleManager.finish_recruitment(selected)
_close()
## 刷新提示文本与确定按钮状态。
func _update_ui() -> void:
if _mode == Mode.OPENING:
var picked := 0
for c in _check_buttons:
if c.button_pressed:
picked += 1
hint_label.text = "开局招募:请选择 %d 名弟子入宗(已选 %d/%d" % [
GameConfig.opening_pick_count, picked, _check_buttons.size(),
]
confirm_button.disabled = picked < GameConfig.opening_pick_count
else:
hint_label.text = "定期招募:勾选入宗弟子,未勾选的将被拒绝(可全部拒绝)"
confirm_button.disabled = false
## 关闭弹窗:清空条目并恢复时间推进。
func _close() -> void:
_clear_entries()
visible = false
TimeSystem.set_paused(false)
## 清空候选条目。
func _clear_entries() -> void:
for child in list_container.get_children():
child.queue_free()
_check_buttons.clear()
_entry_disciples.clear()
## 添加一名候选的行条目:信息文本 + 入宗勾选框。
## @param d: 候选弟子
func _add_entry(d: Disciple) -> void:
var row := HBoxContainer.new()
var info := Label.new()
info.text = "%s %d岁 | %s资质%s | %s灵根 | %s" % [
d.name, d.age, d.get_aptitude_name(),
"·天灵根" if d.is_heavenly else "",
d.get_roots_text(), d.get_realm_text(),
]
info.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var check := CheckButton.new()
check.text = "入宗"
check.toggled.connect(_on_check_toggled.bind(d))
row.add_child(info)
row.add_child(check)
list_container.add_child(row)
_check_buttons.append(check)
_entry_disciples.append(d)
@@ -0,0 +1 @@
uid://byjqc1ssqulc5