Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5c7c57799 | ||
|
|
a3cbd738ad | ||
|
|
a00c95af51 | ||
|
|
df52a0b22f |
@@ -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
@@ -1,7 +1,7 @@
|
|||||||
# 开发计划表
|
# 开发计划表
|
||||||
|
|
||||||
> 开始日期:2026-08-03(周一) 每日有效开发约 2 小时
|
> 开始日期: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/3 一 | D1 | TimeSystem | ① 日期变量(年月日) ② tick 推进 ③ 暂停/加速 | HUD 显示年月日,可调速度 | ✅ 完成(含月/年信号、Settings 持久化) |
|
||||||
| 8/4 二 | D2 | SectManager 数据模型 | ① 单例 ② 灵石/灵气/声望 ③ 增减接口 | 控制台可读写资源 | ✅ 完成(灵石/声望增减+信号,HUD 实时绑定) |
|
| 8/4 二 | D2 | SectManager 数据模型 | ① 单例 ② 灵石/灵气/声望 ③ 增减接口 | 控制台可读写资源 | ✅ 完成(灵石/声望增减+信号,HUD 实时绑定) |
|
||||||
| 8/5 三 | D3 | 弟子生成 + 入宗分流 | ① 随机名字/资质/灵根 ② 按资质分四类 ③ 招募按钮 | 点按钮生成弟子入列表 | ⬜ |
|
| 8/5 三 | D3 | 弟子生成 + 入宗分流 | ① 随机名字/资质/灵根 ② 按资质分四类 ③ 招募按钮 | 点按钮生成弟子入列表 | ✅ 完成(改为每 30 游戏日定时自动招募,含天灵根 3%) |
|
||||||
| 8/6 四 | D4 | 弟子列表 UI | ① 列表条目场景 ② 姓名/资质/境界显示 ③ 点击详情 | 列表可浏览弟子 | ⬜ |
|
| 8/6 四 | D4 | 弟子列表 UI | ① 列表条目场景 ② 姓名/资质/境界显示 ③ 点击详情 | 列表可浏览弟子 | ⬜ |
|
||||||
| 8/7 五 | D5 | 修炼循环 | ① 修为随时间增长(资质×灵气) ② 境界门槛 ③ 突破判定 | 弟子自动修炼,可突破 | ⬜ |
|
| 8/7 五 | D5 | 修炼循环 | ① 修为随时间增长(资质×灵气) ② 境界门槛 ③ 突破判定 | 弟子自动修炼,可突破 | ⬜ |
|
||||||
| 8/8 六 | D6 | 建筑系统基础 | ① 灵石矿场景 ② 建造→每时辰产出 ③ 升级 | 建筑产灵石,升级加速 | ⬜ |
|
| 8/8 六 | D6 | 建筑系统基础 | ① 灵石矿场景 ② 建造→每时辰产出 ③ 升级 | 建筑产灵石,升级加速 | ⬜ |
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ GameState="*res://src/Core/GameState.gd"
|
|||||||
TimeSystem="*res://src/Core/TimeSystem.gd"
|
TimeSystem="*res://src/Core/TimeSystem.gd"
|
||||||
Settings="*res://src/Core/Settings.gd"
|
Settings="*res://src/Core/Settings.gd"
|
||||||
SectManager="*res://src/Core/SectManager.gd"
|
SectManager="*res://src/Core/SectManager.gd"
|
||||||
|
GameConfig="*res://src/Core/GameConfig.gd"
|
||||||
|
DiscipleManager="*res://src/Character/DiscipleManager.gd"
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
; 模拟宗门 全局配置
|
||||||
|
; 所有可调参数集中在此,修改后重启游戏生效
|
||||||
|
; 注释以 ; 开头,参数按系统分节
|
||||||
|
|
||||||
|
[recruitment]
|
||||||
|
opening_applicant_count=5
|
||||||
|
opening_pick_count=3
|
||||||
|
recruit_interval_years=0.1
|
||||||
|
recruit_applicant_count=3
|
||||||
@@ -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]]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cuybveud5w308
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d2aj0gbvb6p4w
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dlchibfd86oek
|
||||||
@@ -7,6 +7,8 @@ func _ready() -> void:
|
|||||||
add_to_group("game_scene")
|
add_to_group("game_scene")
|
||||||
# 应用用户设置中的默认流速
|
# 应用用户设置中的默认流速
|
||||||
TimeSystem.apply_default_speed()
|
TimeSystem.apply_default_speed()
|
||||||
|
# 开局招募:清空旧数据 → 生成 5 名候选 → 暂停时间等待选择(读档流程接入后由存档还原)
|
||||||
|
DiscipleManager.start_new_game()
|
||||||
|
|
||||||
## 顶部 HUD 的返回主菜单请求。
|
## 顶部 HUD 的返回主菜单请求。
|
||||||
func _on_top_bar_hud_sign_return_mainmenu() -> void:
|
func _on_top_bar_hud_sign_return_mainmenu() -> void:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
[ext_resource type="Script" uid="uid://centrk8l54j7c" path="res://src/Core/MainGame.gd" id="1_8bu23"]
|
[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" 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]
|
[node name="MainGame" type="Node" unique_id=2053561265]
|
||||||
groups = ["game_scene"]
|
groups = ["game_scene"]
|
||||||
@@ -9,4 +10,6 @@ script = ExtResource("1_8bu23")
|
|||||||
|
|
||||||
[node name="TopBarHud" parent="." unique_id=1115394694 instance=ExtResource("1_g6xmk")]
|
[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"]
|
[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"]
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user