Compare commits

...
13 Commits
Author SHA1 Message Date
Admin 83fbd8fcfc feat: 大境界突破——练气圆满手动花费灵石突破至筑基及以上(D9 完成);失败灵石不返还+修为倒退,配置驱动;修复筑基圆满进度文本越界;文档同步(修炼设计四章/计划表/README) 2026-08-10 00:55:05 +08:00
Admin a9de46f1dd docs: 全局脚本补充文档注释(14 个文件 29 处) 2026-08-09 20:21:47 +08:00
Admin ad72d36522 fix: 导出修复——主场景改路径引用、all_resources 全量打包、cfg 默认值同步;修复主菜单读档槽位点击 2026-08-09 20:18:01 +08:00
Admin b687ab153d feat: 两级存档系统——无限组/槽,建组建槽/自动档/两级浏览/读档还原/删除组与槽/游戏内加载(D8 完成) 2026-08-09 19:17:21 +08:00
Admin 2bb1486df6 docs: D6 验收清单 11/11 全部勾选 2026-08-09 17:37:19 +08:00
Admin 856661a687 docs: 计划表同步 D5/D6/D7 完成 2026-08-09 17:32:25 +08:00
Admin 5bc756d728 feat: 断粮离宗(方案B),D6 全部完成;灵石矿贴图 2026-08-09 17:22:34 +08:00
Admin bfd79c6934 fix: 管理面板关闭时恢复打开前暂停状态(不冲掉手动暂停) 2026-08-09 17:22:26 +08:00
Admin 00c4495bca feat: 口粮消耗与饥饿减半([food] 配置驱动) 2026-08-09 16:19:01 +08:00
Admin b85d7ef991 style: 建筑面板尺寸对齐名录面板;文档同步 D6 步骤3/4/6 完成 2026-08-09 16:16:44 +08:00
Admin 9e1bd055d5 feat: 建筑面板——资源按钮入口+建造升级产出(D6 核心) 2026-08-09 16:13:34 +08:00
Admin 8b31866d8e feat: 开局赠送灵石/灵米(配置驱动);文档同步 D6 步骤1-2 完成 2026-08-09 15:25:53 +08:00
Admin 2317b425fe style: HUD 布局微调——标签行拉伸比例 2026-08-09 15:17:16 +08:00
39 changed files with 1628 additions and 149 deletions
+12 -4
View File
@@ -105,10 +105,12 @@
| 用户设置 | 已完成 | 默认流速持久化 (user://settings.cfg) | | 用户设置 | 已完成 | 默认流速持久化 (user://settings.cfg) |
| 顶部 HUD | 已完成 | 灵石/时间/声望显示,速度按钮组(toggle 高亮) | | 顶部 HUD | 已完成 | 灵石/时间/声望显示,速度按钮组(toggle 高亮) |
| 画面质量 | 已完成 | 1920x1080 视口,MSAA 2D/3D + TAA,文字抗锯齿 | | 画面质量 | 已完成 | 1920x1080 视口,MSAA 2D/3D + TAA,文字抗锯齿 |
| 宗门数据模型 | **进行中** | SectManager:灵石/灵气/声望(D2 | | 宗门数据模型 | 已完成 | SectManager:灵石/声望/灵米 + 增减/设定接口D2 |
| 弟子系统 | 规划中 | 生成/入宗分流/修炼D3-D5 | | 弟子系统 | 已完成 | 生成/入宗分流/天灵根/列表详情D3-D4 |
| 建筑系统 | 规划中 | 建造/产出/升级D6 | | 修炼循环 | 已完成 | 修为逐日增长/门槛升层/瓶颈判定/13 层圆满D5 |
| 存档接入 | 规划中 | SaveSystem 序列化D8 | | 建筑系统 | 已完成 | 灵石矿/灵田建造升级产出,口粮消耗与断粮离宗D6 |
| 存档接入 | 已完成 | 两级存档:无限组/槽,读档全量还原(D8) |
| 大境界突破 | 已完成 | 练气圆满手动花费灵石突破至筑基及以上,失败灵石不返还+修为倒退(D9) |
> 进度跟踪见 `Doc/开发计划表.md`,里程碑:M1(8/12) 核心闭环 → M2(9/6) 丰富玩法 → M3(10/11) 正式版本。 > 进度跟踪见 `Doc/开发计划表.md`,里程碑:M1(8/12) 核心闭环 → M2(9/6) 丰富玩法 → M3(10/11) 正式版本。
@@ -149,6 +151,12 @@
3. "新游戏"直接进入;"读取存档"在存档面板中选择 3. "新游戏"直接进入;"读取存档"在存档面板中选择
4. 游戏内顶部 HUD 可暂停/调速(1x/5x/20x),返回按钮回主菜单 4. 游戏内顶部 HUD 可暂停/调速(1x/5x/20x),返回按钮回主菜单
## 导出
1. 编辑器安装 **4.7.1 导出模板**(编辑器 → 管理导出模板)
2. 项目 → 导出 → Windows Desktop`export_presets.cfg` 已配置 `all_resources`,输出 `aaa.exe` 单文件含嵌入 PCK
3. **约定**`resources/game_config.cfg` 为未知扩展名,**不随导出打包**——导出版使用 `GameConfig.gd` 代码默认值。修改 cfg 参数后,需同步 `GameConfig.gd` 默认值再导出(注释已标注同步项)。
## 命名规范 ## 命名规范
| 类别 | 格式 | 示例 | | 类别 | 格式 | 示例 |
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 MiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bvpgmj08hilgl"
path="res://.godot/imported/灵石矿.png-ecb322a78c1d966bfd5ce47475fa7fb5.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/ui/灵石矿.png"
dest_files=["res://.godot/imported/灵石矿.png-ecb322a78c1d966bfd5ce47475fa7fb5.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+84 -23
View File
@@ -1,8 +1,9 @@
# 修炼系统设计(D5:修炼循环) # 修炼系统设计(D5:修炼循环 + D9:大境界突破
> 对应开发计划表 D5(8/7):① 修为随时间增长(资质×灵气)② 境界门槛 ③ 突破判定。 > 对应开发计划表 D5(8/7):① 修为随时间增长(资质×灵气)② 境界门槛 ③ 突破判定D98/11)追加大境界突破
> 交付标准:弟子自动修炼,可突破。本文档只描述设计,不包含代码 > 交付标准:弟子自动修炼,可突破;练气圆满后玩家手动花费灵石突破大境界
> 状态:①②③ 已实现(每日结算 → 修为达门槛升层 → 瓶颈掷骰判定,失败扣 20%;13 层圆满封顶);修为进度已在管理界面展示 > 本文档只描述设计,不包含代码。
> 状态:D5 部分已实现(每日结算 → 修为达门槛升层 → 瓶颈掷骰判定,失败扣 20%;13 层圆满封顶;修为进度已在管理界面展示);大境界突破已实现(练气圆满 → 手动花费灵石 → 筑基及以上大境界,失败灵石不返还 + 修为倒退;配置驱动,详见第四章)
--- ---
@@ -63,13 +64,58 @@
- **非瓶颈层**:修为满门槛 → 自动升层(无失败) - **非瓶颈层**:修为满门槛 → 自动升层(无失败)
- **瓶颈层**3→4 / 6→7 / 9→10 / 12→13):修为满后进入突破判定 - **瓶颈层**3→4 / 6→7 / 9→10 / 12→13):修为满后进入突破判定
- 13 层圆满:修为封顶,显示"练气圆满";筑基以上大境界需丹药/灵石资源,**本期不做**(后续 P2 接入 - 13 层圆满:修为封顶,显示"练气圆满";**大境界突破见第四章**(玩家手动花费灵石突破至筑基
> 数值均为示例,正式数值在 D9 平衡调整,只改配置不动代码。 > 数值均为示例,正式数值在 D9 平衡调整,只改配置不动代码。
--- ---
## 四、突破判定(瓶颈层 ## 四、大境界突破(D9 追加:手动花费灵石
### 4.1 总览
练气圆满(13 层)后,玩家可在**弟子详情面板手动操作**:花费灵石尝试突破至下一大境界(练气→筑基→金丹→元婴→化神)。
```
练气13层圆满 ── 手动花费灵石 + 成功率判定 ──> 筑基初期 ── 小境自动推进 ── 筑基圆满 ──> 金丹初期 …
```
- 与练气期小突破(自动挂机)不同:**大境界突破是玩家主动行为**,消耗灵石、有失败折损
- 大境界序列沿用 `Disciple.REALM_NAMES`(练气/筑基/金丹/元婴/化神),`realm_index` 表示大境界
### 4.2 触发条件
| 条件 | 说明 |
|------|------|
| 修为满 | 练气期:13 层圆满(修为封顶);筑基及以上:修为 ≥ 当前大境界圆满阈值(见 4.3) |
| 灵石足够 | 当前宗门灵石 ≥ 目标大境界花费(配置 `realm_breakthrough_costs` |
| 玩家操作 | 详情面板点击"大境界突破"按钮(满足条件时可用) |
### 4.3 筑基及以上的小境推进
- 筑基/金丹/元婴/化神各有 4 小境(初期/中期/后期/圆满),显示沿用现有 `get_realm_text()`
- 修为每日继续按同一公式增长(不再以 13 层封顶);满小境门槛**自动推进小境**(挂机,无失败)
- 小境门槛统一读配置 `realm_sub_thresholds`(3 项:初期→中期 / 中期→后期 / 后期→圆满),各大大境界通用
- **圆满**:修为达到圆满小境门槛后封顶,解锁"大境界突破"按钮
### 4.4 突破判定(手动)
1. 玩家点击"大境界突破"→ 扣除灵石(一次性,成功失败都不退还)
2. 掷骰:成功率按**目标大境界**读配置 `realm_breakthrough_chances`(升筑基 0.6 / 升金丹 0.5 / 升元婴 0.4 / 升化神 0.3,越往上越难)
3. **成功**`realm_index+1``sub_realm_index=0`(新大境界初期)、修为清零;控制台与信号广播"X 突破至 筑基初期"
4. **失败****修为倒退**(配置 `realm_breakthrough_fail_drop` 步,默认 3):倒退限于当前大境界内——练气期倒推层数(如 13 层圆满 → 10 层),筑基及以上倒推小境(圆满 → 后期 → 中期 → 初期);倒退后修为清零重新积累;控制台提示"X 突破失败,灵石化为乌有,修为倒退至 练气10层"
5. 失败后修为照常继续增长(无冷却;冷却规则后续版本再考虑)
**设计意图**:为灵石提供经营消耗出口(建筑产出 → 突破消耗形成闭环),并保留"攒灵石 → 点突破"的手动决策感与成败反馈;**失败代价是境界倒退**(练气 13 层 → 10 层意味重新冲 3 个瓶颈层),比单纯扣修为更能制造挫折与经营压力。
### 4.5 灵石不足
- 按钮置灰并显示所需灵石("突破(需 500 灵石)"
- 灵石不足不可点击,不弹窗
---
## 五、突破判定(瓶颈层)
1. 修为 ≥ 瓶颈层门槛时,每日结算后自动尝试突破 1. 修为 ≥ 瓶颈层门槛时,每日结算后自动尝试突破
2. 掷骰:成功概率**按瓶颈层分别配置**(`breakthrough_chances`,与 `bottleneck_levels` 一一对应:升 4 层 0.85 / 升 7 层 0.8 / 升 10 层 0.75 / 升 13 层 0.6,越往上越难) 2. 掷骰:成功概率**按瓶颈层分别配置**(`breakthrough_chances`,与 `bottleneck_levels` 一一对应:升 4 层 0.85 / 升 7 层 0.8 / 升 10 层 0.75 / 升 13 层 0.6,越往上越难)
@@ -81,23 +127,25 @@
--- ---
## 、CultivationManager(新 Autoload ## 、CultivationManager(新 Autoload
存放位置:`src/Character/CultivationManager.gd`(与 DiscipleManager 同域,注册顺序在 DiscipleManager 之后、声明于 project.godot 存放位置:`src/Character/CultivationManager.gd`(与 DiscipleManager 同域,注册顺序在 DiscipleManager 之后、声明于 project.godot
| 接口 | 说明 | | 接口 | 说明 |
|------|------| |------|------|
| 监听 `TimeSystem.day_passed` | 每日结算全体弟子 | | 监听 `TimeSystem.day_passed` | 每日结算全体弟子 |
| 私有 `_gain_exp(d)` | 按公式累计修为 | | 私有 `_gain_exp(d)` | 按公式累计修为(所有大境界通用;练气圆满/大境界圆满封顶) |
| 私有 `_check_breakthrough(d)` | 门槛判定 + 瓶颈掷骰 | | 私有 `_check_breakthrough(d)` | 练气期门槛判定 + 瓶颈掷骰 |
| 私有 `_check_sub_realm(d)` | 筑基及以上小境门槛判定(自动推进,无失败) |
| **公开 `try_realm_breakthrough(d)`** | **大境界突破入口:校验灵石 → 扣费 → 掷骰 → 成功升大境界/失败修为倒退(`realm_breakthrough_fail_drop` 步),返回 bool** |
| 信号 `realm_changed(d)` | 境界变化(详情面板刷新用) | | 信号 `realm_changed(d)` | 境界变化(详情面板刷新用) |
| 信号 `cultivation_log(text)` | 修炼日志文本(控制台打印/后续日志系统) | | 信号 `cultivation_log(text)` | 修炼日志文本(控制台打印/后续日志系统) |
**注意**:只在 `disciples` 名录内结算,候选池不修炼。 **注意**:只在 `disciples` 名录内结算,候选池不修炼;大境界突破必须从 UI 手动调用(挂机不自动花灵石)
--- ---
## 、配置节(game_config.cfg 新增 [cultivation] ## 、配置节(game_config.cfg [cultivation]
```ini ```ini
[cultivation] [cultivation]
@@ -108,35 +156,43 @@ heavenly_bonus=1.5 ; 天灵根额外倍率
breakthrough_chances=[0.85, 0.8, 0.75, 0.6] ; 各瓶颈层突破成功率(与 bottleneck_levels 一一对应) breakthrough_chances=[0.85, 0.8, 0.75, 0.6] ; 各瓶颈层突破成功率(与 bottleneck_levels 一一对应)
level_thresholds=[100, 200, 350, 550, 800, 1100, 1500, 2000, 2600, 3300, 4100, 5000] ; 练气 1→2 到 12→13 各层所需修为 level_thresholds=[100, 200, 350, 550, 800, 1100, 1500, 2000, 2600, 3300, 4100, 5000] ; 练气 1→2 到 12→13 各层所需修为
bottleneck_levels=[4, 7, 10, 13] ; 瓶颈目标层(升到这些层需突破判定) bottleneck_levels=[4, 7, 10, 13] ; 瓶颈目标层(升到这些层需突破判定)
realm_breakthrough_costs=[500, 4000, 12000, 60000] ; 大境界突破灵石花费(索引 0~3 = 升筑基/金丹/元婴/化神)
realm_breakthrough_chances=[0.6, 0.5, 0.4, 0.3] ; 大境界突破成功率(与 costs 一一对应)
realm_breakthrough_fail_drop=3 ; 突破失败倒退步数(练气倒推层数/筑基+倒推小境,默认 3)
realm_sub_thresholds=[6000, 12000, 24000] ; 筑基及以上小境门槛(初期→中期/中期→后期/后期→圆满)
``` ```
- `aptitude_speed`:与资质枚举一一对应,必须 4 项 - `aptitude_speed`:与资质枚举一一对应,必须 4 项
- `level_thresholds`:练气 1→2 到 12→13 共 12 个门槛值 - `level_thresholds`:练气 1→2 到 12→13 共 12 个门槛值
- `bottleneck_levels`:升到这些层时需要突破判定 - `bottleneck_levels`:升到这些层时需要突破判定
- `realm_breakthrough_costs/chances`:索引 0 = 练气圆满→筑基,1 = 筑基圆满→金丹,2 = 金丹圆满→元婴,3 = 元婴圆满→化神;两个数组长度必须一致(4 项)
- `realm_breakthrough_fail_drop`:失败倒退步数,练气期退层数、筑基+退小境,均不跨大境界
- `realm_sub_thresholds`:筑基及以上通用,3 项;修为达到即自动进小境
--- ---
## 、UI 接入(D4 预留区填充) ## 、UI 接入(D4 预留区填充)
- **名录列表**DisciplesPanel):条目追加"修为"列(如 "1234 / 800",当前修为/本层门槛;13 层显示"练气圆满" - **名录列表**DisciplesPanel):条目追加"修为"列(如 "1234 / 800",当前修为/本层门槛;13 层显示"练气圆满";大境界圆满显示"筑基圆满"
- **详情弹窗**:信息区追加"修为:1234 / 2000"(当前/本层门槛;13 层显示 练气圆满) - **详情弹窗**:信息区追加"修为:1234 / 2000"(当前/本层门槛;练气圆满显示"练气圆满"
- 追加"**大境界突破**"按钮:满足触发条件(修为圆满 + 灵石足够)时可点击;按钮文案带花费("大境界突破(500 灵石)");灵石不足置灰
- 点击后直接执行判定(成功/失败均无二次确认),结果通过修炼日志与控制台可见
- 面板打开时暂停时间,进度静态展示;修炼日志:控制台 print(本期不做日志界面) - 面板打开时暂停时间,进度静态展示;修炼日志:控制台 print(本期不做日志界面)
--- ---
## 、实现拆解(小步可验证) ## 、实现拆解(小步可验证)
1. 配置节 [cultivation] + GameConfig 读取 1. 配置节追加 3 项 + GameConfig 读取(默认值与 cfg 同步)
2. Disciple `cultivation_exp: float` 字段 + 门槛查询辅助 2. Disciple 辅助:`is_realm_ready()`(修为圆满判定)、突破花费查询、进度文本支持筑基及以上
3. CultivationManager 骨架:每日结算修为 3. CultivationManager`_gain_exp` 放开到筑基及以上 + 小境自动推进 `_check_sub_realm`
4. 非瓶颈自动升层 4. `try_realm_breakthrough(d)`:校验 → 扣灵石 → 掷骰 → 成功/失败结算
5. 瓶颈突破判定(成功/失败/损耗 5. 详情面板"大境界突破"按钮(可用性 = 条件 + 灵石;文案含花费
6. 详情面板修为显示 + realm_changed 刷新 6. 挂机验证(加速时间观察筑基圆满 → 手动突破金丹)+ 提交
7. 挂机验证(加速时间观察多轮突破)+ 提交
--- ---
## 、验收清单(D5 交付标准) ## 、验收清单(D5 交付标准 + D9 追加
- [x] 挂机时弟子修为逐日增长,速度随资质/天灵根不同(下品慢、天骄快) - [x] 挂机时弟子修为逐日增长,速度随资质/天灵根不同(下品慢、天骄快)
- [x] 非瓶颈层修为满自动升层,无失败 - [x] 非瓶颈层修为满自动升层,无失败
@@ -144,5 +200,10 @@ bottleneck_levels=[4, 7, 10, 13] ; 瓶颈目标层(升到这些层
- [x] 练气 13 层圆满后修为封顶,显示"练气圆满" - [x] 练气 13 层圆满后修为封顶,显示"练气圆满"
- [x] 详情面板显示修为进度,突破后实时刷新 - [x] 详情面板显示修为进度,突破后实时刷新
- [x] 修改配置(速度/概率/门槛)重启生效 - [x] 修改配置(速度/概率/门槛)重启生效
- [x] 详情面板常驻"大境界突破"按钮:修为圆满 + 灵石足够可点击(文案显示所需灵石),否则置灰(未圆满/灵石不足/已至巅峰)
- [x] 点击突破:扣灵石 → 成功升大境界(realm_index+1,显示"筑基初期")或失败**修为倒退**(练气退层/筑基+退小境,配置 `realm_breakthrough_fail_drop` 步),控制台有日志
- [x] 灵石不足时按钮置灰不可点击
- [x] 筑基及以上:修为继续增长,满 `realm_sub_thresholds` 自动推进小境(初期→中期→后期→圆满),圆满后可再大境界突破
- [x] 修改配置(花费/成功率/小境门槛)重启生效
> 注:修为显示("当前/本层门槛")待详情面板接入(拆解第 6 步)。 > 注:修为显示("当前/本层门槛")待详情面板接入(拆解第 6 步)。
+210
View File
@@ -0,0 +1,210 @@
# 存档系统设计(D8:存档接入)
> 对应开发计划表 D88/10):① SaveSystem JSON 序列化 ② 存/读接口 ③ 替换 SavePanel 测试数据。
> 交付标准:读档后数据还原。本文档只描述设计,不包含代码。
> 状态:全部步骤已实现(SaveSystem/序列化/GameState+MainGame 分支/SavePanel 双模式/自动存档/两级浏览/读档还原/删除与容错/游戏内加载进度点/删除存档组),验收 1–9 全部通过
---
## 一、两级存档结构(无限组 / 无限槽)
**第一级:存档组 SaveGroup(一个角色/一次轮回,UI 显示"存档N"****第二级:存档槽 SaveSlot(该组下任意多个进度快照)**
- **每个新游戏 → 创建一个 SaveGroup**(主菜单"新游戏"时自动生成,id 自增)
- **每次点击存档 → 在所属 SaveGroup 下创建一个新 SaveSlot**(不覆盖、不固定槽位)
- **组和槽的数量均无上限**
```
user://saves/
├── profiles.json # 存档组列表 [{id, name, created, last_date}]
├── profile_1/ # 存档组 1
│ ├── auto.json # 自动存档(返回主菜单时写入,特殊槽)
│ ├── slot_1.json # 手动槽(组内序号递增命名)
│ └── slot_2.json
└── profile_2/ ...
```
- 槽位命名:**组内序号递增**`slot_1``slot_2`…),同一组内永不重名、互不覆盖;删除槽位后序号从当前最大 +1 继续(不重用)
- 自动槽 `auto.json` 固定名(续档用)
## 二、可存档数据清单
| 系统 | 数据 | 说明 |
|------|------|------|
| GameState | `year/month/day` | 游戏日期(TimeSystem.set_date 恢复) |
| SectManager | `spirit_stones` / `reputation` / `food` | 三资源 |
| DiscipleManager | `disciples` + `_next_id` | 名录;候选池不存 |
| BuildingManager | `buildings` | 建筑类型/等级 |
> 修为/境界/饥饿天数都在 Disciple 字段内,随名录序列化。
## 三、SaveSystem(新 Autoload
存放位置:`src/Core/SaveSystem.gd`,注册于 project.godot(依赖各管理器,声明在最后)。
| 接口 | 说明 |
|------|------|
| `create_profile() -> int` | 新建存档组,返回 id(名"存档N" |
| `get_profiles() -> Array[Dictionary]` | 存档组列表(id/name/created/last_date |
| `save_game(profile_id) -> String` | **新建一个槽位**并写入当前游戏数据,返回槽文件名;失败返回空串 |
| `save_auto(profile_id) -> bool` | 写自动槽(返回主菜单时调用) |
| `get_slots(profile_id) -> Array[Dictionary]` | 该组全部槽位(槽文件名 + 元数据),按时间升序 |
| `load_game(profile_id, slot_name) -> bool` | 读取槽位 JSON → 还原各系统 |
| `delete_profile(profile_id) -> bool` | 删除存档组(列表移除 + 递归删目录含全部槽位) |
| `delete_slot(profile_id, slot_name) -> bool` | 删除槽位文件 |
| `reset()` | 清空内存游戏数据(各管理器 reset + 资源清零) |
## 四、序列化设计
### 1. 数据模型加 to_dict/from_dict
- `Disciple.to_dict()` / `static Disciple.from_dict(data)`14 字段全量,枚举转 int
- `Building.to_dict()` / `static from_dict(data)`2 字段)
### 2. 槽位 JSON 结构(slot_1754734800.json
```json
{
"version": 1,
"profile_id": 1,
"saved_date": {"year": 3, "month": 5, "day": 12},
"date": {"year": 3, "month": 5, "day": 12},
"resources": {"spirit_stones": 1230, "reputation": 45, "food": 210},
"next_disciple_id": 7,
"disciples": [
{"id": 1, "name": "林清玄", "age": 16, "aptitude": 2, "roots": [0, 1],
"is_heavenly": false, "join_year": 1, "join_month": 1, "join_day": 1,
"realm_index": 0, "sub_realm_index": 4, "cultivation_exp": 512.5, "hunger_days": 0}
],
"buildings": [
{"type": 0, "level": 2},
{"type": 1, "level": 1}
]
}
```
- `saved_date`/`resources` 顶层冗余字段 = 槽位元数据(列表显示用,免解析全档)
- `version` 为格式版本,预留兼容
### 3. profiles.json 结构
```json
{"profiles": [{"id": 1, "name": "存档1", "created": "2026-08-10", "last_date": "3年5月12日"}]}
```
## 五、存档/读档流程
### 存档(游戏内,每次=新建槽)
1. TopBar 新增"存档"按钮 → SavePanel(存档模式,时间暂停)
2. 左侧显示存档组(当前组高亮),右侧显示该组全部槽位
3. 点"新建存档"按钮 → `save_game(profile_id)` 创建新槽 → 列表新增一条
4. 关闭面板恢复时间
### 读档(主菜单,两级浏览)
1. 主菜单"读取存档" → SavePanel(读档模式)
2. **左侧存档组列表**(存档N + 最后日期)→ 点组 → **右侧显示该组槽位列表**(自动+手动,元数据齐全)
3. 点槽位 → `GameState.request_load_game(profile_id, slot_name)` → 进入 MainGame
### MainGame 分支(改造 _ready
```
NEW_GAME → 现有流程:start_new_game + 开局资源 + 招募弹窗(暂停)
LOAD_GAME → SaveSystem.load_game(profile_id, slot_name) → 还原 → 不弹窗、不暂停
```
### 自动存档
TopBar"返回"主菜单时(sign_return_mainmenu 处理前)→ `save_auto(profile_id)` 自动槽——保证"退→再进"能续档。
## 六、UI:SavePanel(复用现有手绘界面)
```
SavePanel (PanelContainer 全屏)
└── VBoxContainer
├── Header: TitleLabel("选择存档") + Spacer + Button_Esc("返回")
├── 存档模式:Button_NewSlot("新建存档") ← 每次点击创建新槽位
└── ScrollContainer
└── HBoxContainer
├── SaveGroup 区(VBoxContainer) ← 左侧:存档组按钮列表(动态生成"存档N"
└── SlotList 区(VBoxContainer) ← 右侧:该组槽位条目列表(动态生成 SaveSlot 实例)
```
**SaveSlot 条目**(复用现有场景,无空槽概念——每个条目都是真实存档):
| 节点 | 填充内容 |
|------|---------|
| SlotIndexLabel | 序号(自动槽=0,手动槽按时间序 1..N) |
| SaveNameLabel | "自动存档" / "存档 N" |
| MetaLine1 | "X年X月X日 弟子N人" |
| MetaLine2 | "灵石 M" |
| DeleteButton | 删除该槽位(删除前确认) |
| ArrowIcon | 读档箭头(点击读档) |
**交互规则**
| 项 | 存档模式(游戏内) | 读档模式(主菜单) |
|----|-------------------|-------------------|
| 左侧组列表 | 当前组高亮(可切换查看,**组行带"删"按钮,当前组禁用**) | 可切换任意组(组行带"删"按钮) |
| 槽位点击 | **回到该进度点**(加载所选槽,未保存进度丢失) | 点击读档进入游戏 |
| 删除按钮 | 可删(确认后) | 可删(确认后) |
| 暂停 | 打开时暂停,关闭恢复 | 不暂停 |
## 七、分步实现规划(小步可验证,每步独立跑通)
### 步骤 1SaveSystem 骨架(新 Autoload)✅
| 项 | 内容 |
|----|------|
| 改动 | 新建 `src/Core/SaveSystem.gd``create_profile()`(建组,名"存档N")、`get_profiles()``save_game(profile_id)`(新建槽,文件名 `slot_{组内序号}.json`,返回槽名)、`save_auto(profile_id)``get_slots(profile_id)`(自动槽最前+手动按序号升序,含元数据)、`load_game(profile_id, slot_name)``delete_slot()``reset()``project.godot` 注册(声明在最后) |
| 验证 | ✅ 建组(存档N 自增)→ 存档 → slot_1/2/3 生成 → `get_slots` 读回;删除后序号不重用 |
### 步骤 2:数据模型序列化 ✅
| 项 | 内容 |
|----|------|
| 改动 | `Disciple.to_dict()/static from_dict()`14 字段,枚举转 int);`Building.to_dict()/static from_dict()``DiscipleManager``get/set_next_disciple_id()``SectManager``set_spirit_stones/set_reputation/set_food()`(带信号,读档后 HUD 自动刷新) |
| 验证 | ✅ 存档文件内容完整(弟子/建筑/资源/日期全量) |
### 步骤 3GameState + MainGame 分支 🟡(GameState 已改,MainGame 分支待接)
| 项 | 内容 |
|----|------|
| 改动 | `GameState``profile_id: int``save_slot: String`(槽文件名);`request_new_game(pid)``request_load_game(pid, slot_name)``MainGame._ready` 分支 NEW_GAME(原流程)/ LOAD_GAME`load_game` 还原,不弹窗不暂停,失败回退新游戏);`sign_return_mainmenu``save_auto(profile_id)` |
| 验证 | 无头:存→清→读闭环,日期/资源/名录(境界修为饥饿)/建筑逐项一致 |
### 步骤 4SavePanel 动态填充(复用现有手绘界面)✅
| 项 | 内容 |
|----|------|
| 改动 | `SavePanel.gd` 双模式:存档模式(游戏内,暂停,当前组高亮 + 槽位列表 + "新建存档"按钮 → `save_game` → 列表新增);读档模式(主菜单,左列组列表 → 点组 → 右列槽位列表,点槽读档)。`SaveSlot.gd` 填充:序号/名称/MetaLine1/MetaLine2/删除(确认后删)/箭头(读档)。TopBar 加"存档"按钮 + `sign_open_save` 信号 |
| 验证 | ✅ 存档模式:打开暂停 → 新建存档 → 槽位生成(内容完整)→ 关闭恢复;读档模式待 F5 手测 |
### 步骤 5:接线闭环 🟡(MainMenu 建组已接,读档模式接线+自动存档待做)
| 项 | 内容 |
|----|------|
| 改动 | `MainMenu`"新游戏"→ `create_profile()` 建组 → 进游戏;"读取存档"→ 面板读档模式;`MainGame.tscn` 挂 SavePanel 实例 |
| 验证 | 完整闭环:新游戏(建组)→ 招弟子 → 挂机 → 存档(建槽)→ 返回(自动槽)→ 主菜单读档 → 数据还原 |
### 步骤 6:整体验证 + 提交
| 项 | 内容 |
|----|------|
| 改动 | 无(仅验证) |
| 验证 | 对照八节验收清单逐项勾选;git 提交 |
> 依赖关系:1→2 顺序执行(序列化被 SaveSystem 消费);3 依赖 1/24 依赖 1/3;5 依赖 4;6 为最终验证。
## 八、验收清单(D8 交付标准)
- [x] 主菜单"新游戏"自动创建新存档组(存档N)
- [x] 游戏内"存档"面板点"新建存档"→ 该组新增一条槽位(文件生成,内容完整)
- [x] 连续多次存档 → 同一组内多个槽位互不覆盖(序号命名,已验证 slot_1/2/3 内容独立)
- [x] 返回主菜单自动写自动槽(auto.json)
- [x] "读取存档"两级浏览:左侧组列表 → 右侧槽位列表(元数据显示)
- [x] 读档后:日期/资源/名录(含境界/修为/饥饿)/建筑全部还原
- [x] 读档进入**不弹招募窗、不暂停**
- [x] 删除槽位后列表刷新;损坏/缺失 JSON 不崩溃(回退提示)
- [x] 新游戏流程不受影响(仍弹开局招募);多组多槽共存
+28 -28
View File
@@ -2,7 +2,7 @@
> 对应开发计划表 D6(8/8):① 灵石矿场景 ② 建造→产出 ③ 升级。交付标准:建筑产资源,升级加速。 > 对应开发计划表 D6(8/8):① 灵石矿场景 ② 建造→产出 ③ 升级。交付标准:建筑产资源,升级加速。
> 本文档只描述设计,不包含代码。 > 本文档只描述设计,不包含代码。
> 状态:设计稿,待审阅通过后实现 > 状态:步骤 1–7 已实现(D6 全部完成:食物/开局/配置/建筑模型管理/面板/口粮饥饿离宗),步骤 7 整体挂机验证待 F5 手测
--- ---
@@ -133,47 +133,47 @@ hunger_threshold_days=30 ; 断粮累计天数上限(达到即离宗)
## 七、分步实现规划(小步可验证,每步独立跑通) ## 七、分步实现规划(小步可验证,每步独立跑通)
### 步骤 1SectManager 加食物资源 + 开局资源发放 ### 步骤 1SectManager 加食物资源 + 开局资源发放
| 项 | 内容 | | 项 | 内容 |
|----|------| |----|------|
| 改动 | `SectManager.gd``food` 资源 + `add_food/remove_food` + `food_changed` 信号;`MainGame.gd` 新游戏时发放开局灵石/食物 | | 改动 | `SectManager.gd``food` 资源 + `add_food/remove_food` + `food_changed` 信号;`GameConfig.gd``[building]` 节开局数量;`MainGame.gd` 新游戏时发放 |
| 验证 | 无头运行后控制台打印 `SectManager.food`,增减接口调用正确 | | 验证 | ✅ HUD 显示 500 灵石 / 100 灵米,增减接口调用正确 |
### 步骤 2HUD 食物显示 ### 步骤 2HUD 食物显示
| 项 | 内容 | | 项 | 内容 |
|----|------| |----|------|
| 改动 | `TopBarHUD.tscn` 加"食物"标签对;`top_bar_hud.gd``food_changed` 信号 + 初始值 | | 改动 | `TopBarHUD.tscn` 加"灵米"标签对;`top_bar_hud.gd``food_changed` 信号 + 初始值 |
| 验证 | 开局 100 食物显示在顶栏;`add_food` 实时刷新 | | 验证 | 开局 100 灵米显示在顶栏;`add_food` 实时刷新 |
### 步骤 3`[building]` / `[food]` 配置节 + GameConfig 读取 ### 步骤 3`[building]` 配置节 + GameConfig 读取 ✅(部分)
| 项 | 内容 | | 项 | 内容 |
|----|------| |----|------|
| 改动 | `game_config.cfg`两节(开局灵石/食物、灵石矿/灵田参数、口粮/饥饿参数);`GameConfig.gd` 加对应字段 + `building_params` 字典 + 解析 | | 改动 | `game_config.cfg` `[building]`/`[building_mine]`/`[building_field]` 节(开局数量、建筑参数、等级上限);`GameConfig.gd` 加对应字段 + `building_params` 字典 + 解析 |
| 验证 | 无头打印各配置值,缺项回退默认值 | | 验证 | 无头打印各配置值正确;`[food]` 口粮/饥饿参数待步骤 5 接入 |
### 步骤 4Building 模型 + BuildingManager ### 步骤 4Building 模型 + BuildingManager
| 项 | 内容 | | 项 | 内容 |
|----|------| |----|------|
| 改动 | `src/Sect/buildings/Building.gd`type/level + 价格产出计算,运行时读配置);`src/Sect/BuildingManager.gd`(建造/升级/每日产出/`buildings_changed` 信号);`project.godot` 注册 Autoload | | 改动 | `src/Sect/buildings/Building.gd`type/level + 价格产出计算,运行时读配置);`src/Sect/BuildingManager.gd`(建造/升级/每日产出/`buildings_changed` 信号);`project.godot` 注册 Autoload |
| 验证 | 模拟:建造扣费入列、每日产出进 SectManager、升级后产出提升 | | 验证 | 建造扣费入列、每日产出进 SectManager、升级后产出提升10→15)、灵石不足拒绝 |
### 步骤 5:口粮消耗 + 饥饿惩罚 + 断粮离宗 ### 步骤 5:口粮消耗 + 饥饿惩罚 + 断粮离宗
| 项 | 内容 | | 项 | 内容 |
|----|------| |----|------|
| 改动 | `Disciple.gd``hunger_days``CultivationManager.gd` 每日先结算口粮(消耗→饥饿→离宗),`_gain_exp` 乘饥饿倍率 | | 改动 | `Disciple.gd``hunger_days``CultivationManager.gd` 每日先结算口粮(消耗→饥饿累计→离宗移除),`_gain_exp` 乘饥饿倍率 |
| 验证 | 模拟 40 天断粮饥饿累计、修减半第 30 天离宗日志可见;恢复食物后清零 | | 验证 | ✅ 充足 30 天精确扣减清零;断粮饥饿累计、修减半第 30 天离宗日志"断粮日久,离宗而去"+名录移除);恢复清零 |
### 步骤 6BuildingsPanel UI + "资源"按钮接线 ### 步骤 6BuildingsPanel UI + "资源"按钮接线
| 项 | 内容 | | 项 | 内容 |
|----|------| |----|------|
| 改动 | `src/UI/Panels/BuildingsPanel/`(面板场景+脚本,每类型一行:名称/等级/日产出+建造或升级按钮);`TopBarHUD.tscn` 连"资源"按钮 → 新信号 → `MainGame.gd` 打开面板 | | 改动 | `src/UI/Panels/BuildingsPanel/`(面板场景+脚本,每类型一行:名称/等级/日产出+建造或升级按钮);`TopBarHUD.tscn` 连"资源"按钮 → 新信号 → `MainGame.gd` 打开面板 |
| 验证 | 无头布局检查 + F5 手测:建造/升级/禁用态/暂停恢复 | | 验证 | ✅ 无头验证:打开暂停/关闭恢复、建造升级扣费、每日产出;面板尺寸与名录面板一致(1340×819 vs 1340×842 |
### 步骤 7:整体挂机验证 + 提交 ### 步骤 7:整体挂机验证 + 提交
@@ -188,17 +188,17 @@ hunger_threshold_days=30 ; 断粮累计天数上限(达到即离宗)
## 八、验收清单(D6 交付标准) ## 八、验收清单(D6 交付标准)
- [ ] 新游戏开局赠送 500 灵石 + 100 食物(配置可调),HUD 可见 - [x] 新游戏开局赠送 500 灵石 + 100 食物(配置可调),HUD 可见
- [ ] 点"资源"按钮打开建筑面板(时间暂停,关闭恢复) - [x] 点"资源"按钮打开建筑面板(时间暂停,关闭恢复)
- [ ] 灵石充足时可建造灵石矿/灵田,扣费入列 - [x] 灵石充足时可建造灵石矿/灵田,扣费入列
- [ ] 每日自动产出灵石/食物,HUD 实时增长 - [x] 每日自动产出灵石/食物,HUD 实时增长
- [ ] 升级建筑后产出提升(升级倍率生效),价格按倍率递增 - [x] 升级建筑后产出提升(升级倍率生效),价格按倍率递增
- [ ] 灵石不足时建造/升级按钮禁用 - [x] 灵石不足时建造/升级按钮禁用
- [ ] 等级达到上限后不可再升级 - [x] 等级达到上限后不可再升级
- [ ] 练气弟子每日消耗食物;断粮后饥饿天数累计、修炼速度减半 - [x] 练气弟子每日消耗食物;断粮后饥饿天数累计、修炼速度减半
- [ ] 断粮累计 30 天弟子离宗(名录移除 + 日志) - [x] 断粮累计 30 天弟子离宗(名录移除 + 日志)
- [ ] 食物恢复充足后饥饿清零 - [x] 食物恢复充足后饥饿清零
- [ ] 建筑/饥饿数据为纯数据(可序列化,D8 存档接入) - [x] 建筑/饥饿数据为纯数据(可序列化,D8 存档接入时加 to_dict/from_dict
--- ---
+7 -9
View File
@@ -1,7 +1,7 @@
# 开发计划表 # 开发计划表
> 开始日期:2026-08-03(周一) 每日有效开发约 2 小时 > 开始日期:2026-08-03(周一) 每日有效开发约 2 小时
> 进度更新:2026-08-08  D1D4 完成;D5(修炼循环)未启动,进度落后 1 天(设计文档已更新:灵根容量公式 1÷根数、无灵根体修预留、修炼数值配置化,待实现);D3 追加增强完成(灵根数量 1~5 权重配置化、天灵根改为仅天骄资质嵌套判定) > 进度更新:2026-08-10  D1D9 全部完成(D8 两级存档系统验收 9/9,D9 大境界突破 + 数值打磨),M1 验收(D10)进行中
## 一、每日作息模板(工作日) ## 一、每日作息模板(工作日)
@@ -23,14 +23,12 @@
| 8/4 二 | D2 | SectManager 数据模型 | ① 单例 ② 灵石/灵气/声望 ③ 增减接口 | 控制台可读写资源 | ✅ 完成(灵石/声望增减+信号,HUD 实时绑定) | | 8/4 二 | D2 | SectManager 数据模型 | ① 单例 ② 灵石/灵气/声望 ③ 增减接口 | 控制台可读写资源 | ✅ 完成(灵石/声望增减+信号,HUD 实时绑定) |
| 8/5 三 | D3 | 弟子生成 + 入宗分流 | ① 随机名字/资质/灵根 ② 按资质分四类 ③ 招募流程 | 开局 5 选 3 必选满,定期弹窗可全拒/全收 | ✅ 完成(弹窗招募:开局5选3+定期3候选,配置驱动含天灵根;新增 GameConfig 配置系统)<br>追加:天灵根改为仅天骄资质再掷(`heavenly_root_chance`),灵根数量 1~5 权重配置化(`root_count_weights`),容量公式 1÷根数 | | 8/5 三 | D3 | 弟子生成 + 入宗分流 | ① 随机名字/资质/灵根 ② 按资质分四类 ③ 招募流程 | 开局 5 选 3 必选满,定期弹窗可全拒/全收 | ✅ 完成(弹窗招募:开局5选3+定期3候选,配置驱动含天灵根;新增 GameConfig 配置系统)<br>追加:天灵根改为仅天骄资质再掷(`heavenly_root_chance`),灵根数量 1~5 权重配置化(`root_count_weights`),容量公式 1÷根数 |
| 8/6 四 | D4 | 弟子列表 UI | ① 列表条目场景 ② 姓名/资质/境界显示 ③ 点击详情 | 列表可浏览弟子 | ✅ 完成(管理按钮入口,名录面板+详情弹窗,天灵根标记,打开面板暂停时间) | | 8/6 四 | D4 | 弟子列表 UI | ① 列表条目场景 ② 姓名/资质/境界显示 ③ 点击详情 | 列表可浏览弟子 | ✅ 完成(管理按钮入口,名录面板+详情弹窗,天灵根标记,打开面板暂停时间) |
| 8/7 五 | D5 | 修炼循环 | ① 修为随时间增长(资质×灵气) ② 境界门槛 ③ 突破判定 | 弟子自动修炼,可突破 | ⬜(设计已就绪:容量=1÷根数、无灵根体修预留、[cultivation] 配置节,待实现 | | 8/7 五 | D5 | 修炼循环 | ① 修为随时间增长(资质×灵气) ② 境界门槛 ③ 突破判定 | 弟子自动修炼,可突破 | ✅ 完成(修为逐日增长/门槛升层/瓶颈按层概率判定/13层圆满封顶/修为进度显示,配置驱动 |
| 8/8 六 | D6 | 建筑系统基础 | ① 灵石矿场景 ② 建造→每时辰产出 ③ 升级 | 建筑产灵石,升级加速 | | | 8/8 六 | D6 | 建筑系统基础 | ① 灵石矿场景 ② 建造→每时辰产出 ③ 升级 | 建筑产灵石,升级加速 | ✅ 完成(步骤1-7:食物/开局赠送/建筑参数配置/Building+BuildingManager/建筑面板+资源按钮/口粮饥饿离宗方案B,验收11/11) |
| 8/9 日 | D7 | 休息 / 补漏 | 修本周 bug,整理代码 | git 干净 | | | 8/9 日 | D7 | 休息 / 补漏 | 修本周 bug,整理代码 | git 干净 | ✅ 完成(修复:管理面板关闭冲掉手动暂停) |
| 8/10 一 | D8 | 存档接入 | ① SaveSystem JSON 序列化 ② 存/读接口 ③ 替换 SavePanel 测试数据 | 读档后数据还原 | | | 8/10 一 | D8 | 存档接入 | ① SaveSystem JSON 序列化 ② 存/读接口 ③ 替换 SavePanel 测试数据 | 读档后数据还原 | ✅ 完成(两级存档:无限组/槽、建组建槽/自动档/两级浏览/读档全量还原/删除组与槽/游戏内加载进度点/损坏容错,验收9/9) |
| 8/11 二 | D9 | 打磨 | 数值平衡初调,玩 10 分钟验证 | 无致命 bug | | | 8/11 二 | D9 | 打磨 | 数值平衡初调,玩 10 分钟验证 | 无致命 bug | ✅ 完成(大境界突破:练气圆满→手动花费灵石→筑基及以上大境界,失败灵石不返还+修为倒退 3 步,花费/成功率/小境门槛配置化;61 项逻辑测试全 PASS) |
| 8/12 三 | D10 | **M1 验收** | 完整跑一遍:新游戏→招弟子→挂机→存→退→读 | 闭环可玩 | ⬜ | | 8/12 三 | D10 | **M1 验收** | 完整跑一遍:新游戏→招弟子→挂机→存→退→读 | 闭环可玩 | ⬜ |## 三、第 35 周(8/179/6):P2 扩展
## 三、第 35 周(8/179/6):P2 扩展
| 日期 | 周 | 任务 | 拆解 | 交付 | | 日期 | 周 | 任务 | 拆解 | 交付 |
| ---- | -- | ---- | ---- | ---- | | ---- | -- | ---- | ---- | ---- |
+1 -1
View File
@@ -8,7 +8,7 @@ name="Windows Desktop"
platform="Windows Desktop" platform="Windows Desktop"
dedicated_server=false dedicated_server=false
custom_features="" custom_features=""
export_filter="scenes" export_filter="all_resources"
export_files=PackedStringArray() export_files=PackedStringArray()
include_filter="" include_filter=""
exclude_filter="" exclude_filter=""
+3 -1
View File
@@ -11,7 +11,7 @@ config_version=5
[application] [application]
config/name="模拟宗门" config/name="模拟宗门"
run/main_scene="uid://d1odycoaqhhyg" run/main_scene="res://src/UI/Panels/MainMenu/MainMenu.tscn"
config/features=PackedStringArray("4.7", "Forward Plus") config/features=PackedStringArray("4.7", "Forward Plus")
[autoload] [autoload]
@@ -23,6 +23,8 @@ SectManager="*res://src/Core/SectManager.gd"
GameConfig="*res://src/Core/GameConfig.gd" GameConfig="*res://src/Core/GameConfig.gd"
DiscipleManager="*res://src/Character/DiscipleManager.gd" DiscipleManager="*res://src/Character/DiscipleManager.gd"
CultivationManager="*res://src/Character/CultivationManager.gd" CultivationManager="*res://src/Character/CultivationManager.gd"
BuildingManager="*res://src/Sect/BuildingManager.gd"
SaveSystem="*res://src/Core/SaveSystem.gd"
[display] [display]
+26
View File
@@ -27,6 +27,32 @@ heavenly_bonus=1.5 ; 天灵根额外修炼速度倍率
breakthrough_chances=[0.85, 0.8, 0.75, 0.6] ; 各瓶颈层突破成功率(与 bottleneck_levels 一一对应:升4层/7层/10层/13层) breakthrough_chances=[0.85, 0.8, 0.75, 0.6] ; 各瓶颈层突破成功率(与 bottleneck_levels 一一对应:升4层/7层/10层/13层)
level_thresholds=[100, 200, 350, 550, 800, 1100, 1500, 2000, 2600, 3300, 4100, 5000] ; 练气 1→2 到 12→13 各层所需修为 level_thresholds=[100, 200, 350, 550, 800, 1100, 1500, 2000, 2600, 3300, 4100, 5000] ; 练气 1→2 到 12→13 各层所需修为
bottleneck_levels=[4, 7, 10, 13] ; 瓶颈目标层(升到这些层需突破判定) bottleneck_levels=[4, 7, 10, 13] ; 瓶颈目标层(升到这些层需突破判定)
realm_breakthrough_costs=[500, 4000, 12000, 60000] ; 大境界突破灵石花费(索引 0~3 = 升筑基/金丹/元婴/化神)
realm_breakthrough_chances=[0.6, 0.5, 0.4, 0.3] ; 大境界突破成功率(与 costs 一一对应)
realm_breakthrough_fail_drop=3 ; 突破失败倒退步数(练气倒推层数/筑基+倒推小境,不跨大境界)
realm_sub_thresholds=[6000, 12000, 24000] ; 筑基及以上小境门槛(初期→中期/中期→后期/后期→圆满)
[ui] ; UI 通用 [ui] ; UI 通用
hud_font_size=24 ; HUD 字体大小 hud_font_size=24 ; HUD 字体大小
[building] ; 建筑系统通用
starting_spirit_stones=500 ; 开局赠送灵石
starting_food=100 ; 开局赠送灵米
max_level=10 ; 建筑等级上限
[building_mine] ; 灵石矿
cost=200 ; 建造价(灵石)
output=10 ; 日产出(灵石)
upgrade_cost_x=2.0 ; 升级价格倍率
upgrade_output_x=1.5 ; 升级产出倍率
[building_field] ; 灵田
cost=100 ; 建造价(灵石)
output=5 ; 日产出(灵米)
upgrade_cost_x=2.0 ; 升级价格倍率
upgrade_output_x=1.5 ; 升级产出倍率
[food] ; 口粮与生存
consumption_per_disciple=1 ; 练气弟子人均日消耗灵米
hunger_speed_multiplier=0.5 ; 饥饿期间修炼速度倍率
hunger_threshold_days=30 ; 断粮累计天数上限(达到即离宗,待接入)
+80 -6
View File
@@ -1,5 +1,5 @@
## 修炼管理器(Autoload 单例)。 ## 修炼管理器(Autoload 单例)。
## 负责弟子修为的每日结算与境界推进(D5:修为逐日增长 + 自动升层 + 瓶颈突破判定)。 ## 负责弟子修为的每日结算与境界推进(D5:修为逐日增长 + 自动升层 + 瓶颈突破判定D9:大境界手动突破)。
extends Node extends Node
## 境界变化信号(突破后发出,供详情面板刷新)。 ## 境界变化信号(突破后发出,供详情面板刷新)。
@@ -8,23 +8,49 @@ signal realm_changed(d: Disciple)
signal cultivation_log(text: String) signal cultivation_log(text: String)
## 挂接每日推进信号。
func _ready() -> void: func _ready() -> void:
# 每日推进时结算全体已入宗弟子的修为与境界 # 每日推进时结算全体已入宗弟子的修为与境界
TimeSystem.day_passed.connect(_on_day_passed) TimeSystem.day_passed.connect(_on_day_passed)
## 每日结算:仅结算已入宗名录(候选池不修炼)。 ## 每日结算:先扣口粮,再结算已入宗弟子的修为与境界(候选池不修炼)。
func _on_day_passed(_year: int, _month: int, _day: int) -> void: func _on_day_passed(_year: int, _month: int, _day: int) -> void:
_settle_food()
for d in DiscipleManager.disciples: for d in DiscipleManager.disciples:
_gain_exp(d) _gain_exp(d)
_check_breakthrough(d) _check_breakthrough(d)
_check_sub_realm(d)
## 每日口粮结算:练气弟子人均消耗灵米(筑基+辟谷豁免)。
## 食物充足 → 扣减并饥饿清零;食物不足 → 归零并全员饥饿天数 +1;
## 饥饿天数 ≥ 阈值(配置 hunger_threshold_days)→ 弟子离宗,从名录移除。
func _settle_food() -> void:
var eaters: Array[Disciple] = []
for d in DiscipleManager.disciples:
if d.realm_index == 0:
eaters.append(d)
var need := eaters.size() * GameConfig.food_consumption_per_disciple
if SectManager.food >= need:
SectManager.remove_food(need)
for d in eaters:
d.hunger_days = 0
return
SectManager.remove_food(SectManager.food)
var leavers: Array[Disciple] = []
for d in eaters:
d.hunger_days += 1
if d.hunger_days >= GameConfig.hunger_threshold_days:
leavers.append(d)
for d in leavers:
_log("%s 断粮日久,离宗而去" % d.name)
DiscipleManager.remove_disciple(d)
## 按公式累计一日修为。 ## 按公式累计一日修为。
## 速度 = 基础速度 × 资质倍率 × 灵根容量 × 天灵根加成 × 灵气浓度 ## 速度 = 基础速度 × 资质倍率 × 灵根容量 × 天灵根加成 × 灵气浓度 × 饥饿倍率
## 练气圆满(13 层)后修为封顶,不再增长。 ## 练气圆满(13 层)或大境界圆满(筑基+ 4 小境满)后修为封顶,不再增长。
## @param d: 已入宗弟子 ## @param d: 已入宗弟子
func _gain_exp(d: Disciple) -> void: func _gain_exp(d: Disciple) -> void:
# 练气圆满(13 层)后修为封顶,不再增长 if d.is_realm_peak():
if d.realm_index != 0 or d.sub_realm_index >= GameConfig.level_thresholds.size():
return return
var speed := GameConfig.cultivation_base_speed var speed := GameConfig.cultivation_base_speed
speed *= GameConfig.aptitude_speed[d.aptitude] speed *= GameConfig.aptitude_speed[d.aptitude]
@@ -32,6 +58,8 @@ func _gain_exp(d: Disciple) -> void:
if d.is_heavenly: if d.is_heavenly:
speed *= GameConfig.heavenly_bonus speed *= GameConfig.heavenly_bonus
speed *= GameConfig.qi_concentration speed *= GameConfig.qi_concentration
if d.hunger_days > 0:
speed *= GameConfig.hunger_speed_multiplier
d.cultivation_exp += speed d.cultivation_exp += speed
## 突破判定:修为满当前层门槛时推进境界。 ## 突破判定:修为满当前层门槛时推进境界。
@@ -58,6 +86,52 @@ func _check_breakthrough(d: Disciple) -> void:
else: else:
_advance_layer(d) _advance_layer(d)
## 筑基及以上小境推进:修为满小境门槛自动升小境(初期→中期→后期→圆满,无失败)。
## 大境界圆满后不再推进,等待玩家手动大境界突破。
## @param d: 已入宗弟子
func _check_sub_realm(d: Disciple) -> void:
if d.realm_index == 0:
return
if d.sub_realm_index >= GameConfig.realm_sub_thresholds.size():
return
if d.cultivation_exp < GameConfig.realm_sub_thresholds[d.sub_realm_index]:
return
var threshold: float = GameConfig.realm_sub_thresholds[d.sub_realm_index]
d.cultivation_exp -= threshold
d.sub_realm_index += 1
realm_changed.emit(d)
_log("%s 突破至 %s" % [d.name, d.get_realm_text()])
## 大境界突破(玩家手动调用,挂机不自动触发)。
## 流程:校验修为圆满与灵石 → 扣灵石(成败不退还)→ 掷骰(成功率按目标大境界配置)。
## 成功:realm_index+1(新大境界初期,修为清零);失败:修为倒退 fail_drop 步(练气退层数/筑基+退小境,不跨大境界)。
## 灵石不足或修为未圆满返回 false(不扣灵石)。
## @param d: 已入宗弟子
func try_realm_breakthrough(d: Disciple) -> bool:
if not d.is_realm_peak():
return false
var next := d.get_next_realm_index()
if next < 0:
return false
var cost := d.get_realm_breakthrough_cost()
if SectManager.spirit_stones < cost:
return false
SectManager.remove_spirit_stones(cost)
var chance: float = GameConfig.realm_breakthrough_chances[next - 1]
if randf() < chance:
d.realm_index = next
d.sub_realm_index = 0
d.cultivation_exp = 0.0
realm_changed.emit(d)
_log("%s 突破至 %s" % [d.name, d.get_realm_text()])
return true
var drop := GameConfig.realm_breakthrough_fail_drop
d.sub_realm_index = maxi(0, d.sub_realm_index - drop)
d.cultivation_exp = 0.0
realm_changed.emit(d)
_log("%s 突破失败,灵石化为乌有,修为倒退至 %s" % [d.name, d.get_realm_text()])
return false
## 推进一层境界:扣除本层门槛修为(余量带入下一层)并发信号。 ## 推进一层境界:扣除本层门槛修为(余量带入下一层)并发信号。
## @param d: 已入宗弟子 ## @param d: 已入宗弟子
func _advance_layer(d: Disciple) -> void: func _advance_layer(d: Disciple) -> void:
+72 -4
View File
@@ -20,7 +20,7 @@ const REALM_NAMES: Array[String] = ["练气", "筑基", "金丹", "元婴", "化
var id: int = 0 # 唯一编号(DiscipleManager 分配) var id: int = 0 # 唯一编号(DiscipleManager 分配)
var name: String = "" var name: String = ""
var age: int = 0 var age: int = 0
var aptitude: Aptitude = Aptitude.ORDINARY var aptitude: Aptitude = Aptitude.ORDINARY # 资质(入宗分流结果)
var roots: Array[SpiritRoot] = [] # 普通弟子 1~5 根;天灵根固定 1 根 var roots: Array[SpiritRoot] = [] # 普通弟子 1~5 根;天灵根固定 1 根
var is_heavenly: bool = false # 天灵根标记(仅天骄资质,概率见配置) var is_heavenly: bool = false # 天灵根标记(仅天骄资质,概率见配置)
var join_year: int = 0 # 入宗年份(0 = 未入宗/候选),accept 时快照 var join_year: int = 0 # 入宗年份(0 = 未入宗/候选),accept 时快照
@@ -29,11 +29,13 @@ var join_day: int = 0 # 入宗日
var realm_index: int = 0 # 大境界索引(0=练气) var realm_index: int = 0 # 大境界索引(0=练气)
var sub_realm_index: int = 0 # 小境界索引(练气层数-1 var sub_realm_index: int = 0 # 小境界索引(练气层数-1
var cultivation_exp: float = 0.0 # 修为(float 可积累小数,D5 每日结算) var cultivation_exp: float = 0.0 # 修为(float 可积累小数,D5 每日结算)
var hunger_days: int = 0 # 断粮累计天数(≥阈值离宗,方案 B)
# —— 辅助函数:把数据翻译成给人看的文本 —— # —— 辅助函数:把数据翻译成给人看的文本 ——
func get_aptitude_name() -> String: func get_aptitude_name() -> String:
return APTITUDE_NAMES[aptitude] return APTITUDE_NAMES[aptitude]
## 灵根显示文本,如 "金、木"。
func get_roots_text() -> String: func get_roots_text() -> String:
var names: Array[String] = [] var names: Array[String] = []
for root in roots: for root in roots:
@@ -48,6 +50,7 @@ func get_root_count_capacity() -> float:
return 0.0 return 0.0
return 1.0 / roots.size() return 1.0 / roots.size()
## 境界显示文本,如 "练气5层"、"筑基初期"。
func get_realm_text() -> String: func get_realm_text() -> String:
if realm_index == 0: if realm_index == 0:
return "%s%d" % [REALM_NAMES[realm_index], sub_realm_index + 1] return "%s%d" % [REALM_NAMES[realm_index], sub_realm_index + 1]
@@ -59,11 +62,76 @@ func get_join_date_text() -> String:
return "" return ""
return "%d%d%d" % [join_year, join_month, join_day] return "%d%d%d" % [join_year, join_month, join_day]
## 序列化为字典(D8 存档用,枚举转 int)。
func to_dict() -> Dictionary:
var roots_data: Array[int] = []
for r in roots:
roots_data.append(int(r))
return {
"id": id,
"name": name,
"age": age,
"aptitude": int(aptitude),
"roots": roots_data,
"is_heavenly": is_heavenly,
"join_year": join_year,
"join_month": join_month,
"join_day": join_day,
"realm_index": realm_index,
"sub_realm_index": sub_realm_index,
"cultivation_exp": cultivation_exp,
"hunger_days": hunger_days,
}
## 从字典还原(D8 读档用)。
## @param data: to_dict 产生的字典
static func from_dict(data: Dictionary) -> Disciple:
var d := Disciple.new()
d.id = int(data["id"])
d.name = str(data["name"])
d.age = int(data["age"])
d.aptitude = int(data["aptitude"]) as Aptitude
d.roots = []
for r in data["roots"]:
d.roots.append(int(r) as SpiritRoot)
d.is_heavenly = bool(data["is_heavenly"])
d.join_year = int(data["join_year"])
d.join_month = int(data["join_month"])
d.join_day = int(data["join_day"])
d.realm_index = int(data["realm_index"])
d.sub_realm_index = int(data["sub_realm_index"])
d.cultivation_exp = float(data["cultivation_exp"])
d.hunger_days = int(data["hunger_days"])
return d
## 本层修炼进度文本,如 "1234 / 2000"(当前修为 / 本层门槛)。 ## 本层修炼进度文本,如 "1234 / 2000"(当前修为 / 本层门槛)。
## 练气圆满(13 层)返回"练气圆满"非练气期返回空串 ## 练气圆满(13 层)返回"练气圆满"筑基及以上返回 "1234 / 6000"(小境门槛),圆满返回"筑基圆满"
func get_cultivation_progress_text() -> String: func get_cultivation_progress_text() -> String:
if realm_index != 0: if realm_index == 0:
return ""
if sub_realm_index >= GameConfig.level_thresholds.size(): if sub_realm_index >= GameConfig.level_thresholds.size():
return "练气圆满" return "练气圆满"
return "%d / %d" % [int(cultivation_exp), int(GameConfig.level_thresholds[sub_realm_index])] return "%d / %d" % [int(cultivation_exp), int(GameConfig.level_thresholds[sub_realm_index])]
if sub_realm_index >= GameConfig.realm_sub_thresholds.size():
return REALM_NAMES[realm_index] + "圆满"
return "%d / %d" % [int(cultivation_exp), int(GameConfig.realm_sub_thresholds[sub_realm_index])]
## 当前大境界是否修为圆满(练气 = 13 层;筑基及以上 = 4 小境修满)。
## 圆满后解锁"大境界突破"(玩家手动花费灵石)。
func is_realm_peak() -> bool:
if realm_index == 0:
return sub_realm_index >= GameConfig.level_thresholds.size()
return sub_realm_index >= GameConfig.realm_sub_thresholds.size() \
and cultivation_exp >= GameConfig.realm_sub_thresholds[GameConfig.realm_sub_thresholds.size() - 1]
## 目标大境界索引(realm_index+1);化神圆满(最高)返回 -1 表示无可突破。
func get_next_realm_index() -> int:
if realm_index + 1 >= REALM_NAMES.size():
return -1
return realm_index + 1
## 大境界突破所需灵石(按目标大境界索引读配置);无可突破返回 -1。
func get_realm_breakthrough_cost() -> int:
var next := get_next_realm_index()
if next < 0:
return -1
return GameConfig.realm_breakthrough_costs[next - 1]
+10
View File
@@ -35,6 +35,7 @@ var _days_until_recruit := 0
var _next_id := 1 var _next_id := 1
## 挂接每日推进信号并初始化招募倒计时。
func _ready() -> void: func _ready() -> void:
# 挂到时间系统上:每日推进时倒计时,满间隔生成候选并暂停时间 # 挂到时间系统上:每日推进时倒计时,满间隔生成候选并暂停时间
TimeSystem.day_passed.connect(_on_day_passed) TimeSystem.day_passed.connect(_on_day_passed)
@@ -110,6 +111,15 @@ func reset() -> void:
_days_until_recruit = _get_interval_days() _days_until_recruit = _get_interval_days()
disciples_changed.emit() disciples_changed.emit()
## 当前下一个弟子 ID(D8 存档用)。
func get_next_disciple_id() -> int:
return _next_id
## 设置下一个弟子 ID(D8 读档用)。
## @param value: 下一个 ID
func set_next_disciple_id(value: int) -> void:
_next_id = value
## 定期招募间隔(游戏天数),由 GameConfig 的"年"换算而来。 ## 定期招募间隔(游戏天数),由 GameConfig 的"年"换算而来。
func _get_interval_days() -> int: func _get_interval_days() -> int:
var days_per_year := TimeSystem.MONTHS_PER_YEAR * TimeSystem.DAYS_PER_MONTH var days_per_year := TimeSystem.MONTHS_PER_YEAR * TimeSystem.DAYS_PER_MONTH
+70 -3
View File
@@ -14,7 +14,8 @@ var opening_applicant_count := 5
## 开局必须选满数。 ## 开局必须选满数。
var opening_pick_count := 3 var opening_pick_count := 3
## 定期招募间隔(游戏年,1 年 = 360 天)。 ## 定期招募间隔(游戏年,1 年 = 360 天)。
var recruit_interval_years := 0.1 ## 注意:cfg 文件不随导出打包(未知扩展名),导出版使用此默认值——与 cfg 保持同步。
var recruit_interval_years := 12.0
## 定期弹窗候选人数。 ## 定期弹窗候选人数。
var recruit_applicant_count := 3 var recruit_applicant_count := 3
## 天灵根概率(仅天骄资质中再掷一次)。 ## 天灵根概率(仅天骄资质中再掷一次)。
@@ -37,10 +38,40 @@ var bottleneck_chances: Array[float] = [0.85, 0.8, 0.75, 0.6]
var level_thresholds: Array[float] = [100.0, 200.0, 350.0, 550.0, 800.0, 1100.0, 1500.0, 2000.0, 2600.0, 3300.0, 4100.0, 5000.0] var level_thresholds: Array[float] = [100.0, 200.0, 350.0, 550.0, 800.0, 1100.0, 1500.0, 2000.0, 2600.0, 3300.0, 4100.0, 5000.0]
## 瓶颈目标层(升到这些层需突破判定)。 ## 瓶颈目标层(升到这些层需突破判定)。
var bottleneck_levels: Array[int] = [4, 7, 10, 13] var bottleneck_levels: Array[int] = [4, 7, 10, 13]
## 大境界突破灵石花费(索引 0~3 = 升筑基/金丹/元婴/化神)。
var realm_breakthrough_costs: Array[int] = [500, 4000, 12000, 60000]
## 大境界突破成功率(与 realm_breakthrough_costs 一一对应,越往上越难)。
var realm_breakthrough_chances: Array[float] = [0.6, 0.5, 0.4, 0.3]
## 大境界突破失败倒退步数(练气退层数/筑基+退小境,不跨大境界)。
var realm_breakthrough_fail_drop := 3
## 筑基及以上小境门槛(初期→中期/中期→后期/后期→圆满,各大大境界通用)。
var realm_sub_thresholds: Array[float] = [6000.0, 12000.0, 24000.0]
# —— UI 通用(对应 cfg 的 [ui] 节)—— # —— UI 通用(对应 cfg 的 [ui] 节)——
## HUD 字体大小。 ## HUD 字体大小(导出版用默认值,与 cfg 保持同步)
var hud_font_size := 32 var hud_font_size := 24
# —— 建筑系统(对应 cfg 的 [building] 节)——
## 开局赠送灵石(新游戏发放)。
var starting_spirit_stones := 500
## 开局赠送灵米(新游戏发放)。
var starting_food := 100
## 建筑等级上限。
var building_max_level := 10
## 各建筑参数表(键 = 建筑类型名,值 = {cost, output, upgrade_cost_x, upgrade_output_x})。
var building_params: Dictionary = {
"mine": {"cost": 200, "output": 10, "upgrade_cost_x": 2.0, "upgrade_output_x": 1.5},
"field": {"cost": 100, "output": 5, "upgrade_cost_x": 2.0, "upgrade_output_x": 1.5},
}
# —— 口粮与生存(对应 cfg 的 [food] 节)——
## 练气弟子人均日消耗灵米。
var food_consumption_per_disciple := 1
## 饥饿期间修炼速度倍率。
var hunger_speed_multiplier := 0.5
## 断粮累计天数上限(达到即离宗)。
var hunger_threshold_days := 30
## 各难度资质权重表(键 = 难度档位,值 = 下品/中品/上品/天骄 权重)。 ## 各难度资质权重表(键 = 难度档位,值 = 下品/中品/上品/天骄 权重)。
var aptitude_weights_by_difficulty: Dictionary = { var aptitude_weights_by_difficulty: Dictionary = {
@@ -49,6 +80,7 @@ var aptitude_weights_by_difficulty: Dictionary = {
} }
## 启动时加载配置文件。
func _ready() -> void: func _ready() -> void:
_load_config() _load_config()
@@ -93,8 +125,43 @@ func _load_config() -> void:
for v in bn_raw: for v in bn_raw:
arr_bn.append(int(v)) arr_bn.append(int(v))
bottleneck_levels = arr_bn bottleneck_levels = arr_bn
var rb_cost_raw: Variant = cfg.get_value("cultivation", "realm_breakthrough_costs", [])
if rb_cost_raw is Array and rb_cost_raw.size() == realm_breakthrough_costs.size():
var arr_rb: Array[int] = []
for v in rb_cost_raw:
arr_rb.append(int(v))
realm_breakthrough_costs = arr_rb
var rb_ch_raw: Variant = cfg.get_value("cultivation", "realm_breakthrough_chances", [])
if rb_ch_raw is Array and rb_ch_raw.size() == realm_breakthrough_chances.size():
var arr_rch: Array[float] = []
for v in rb_ch_raw:
arr_rch.append(float(v))
realm_breakthrough_chances = arr_rch
realm_breakthrough_fail_drop = cfg.get_value("cultivation", "realm_breakthrough_fail_drop", realm_breakthrough_fail_drop)
var sub_raw: Variant = cfg.get_value("cultivation", "realm_sub_thresholds", [])
if sub_raw is Array and sub_raw.size() == realm_sub_thresholds.size():
var arr_sub: Array[float] = []
for v in sub_raw:
arr_sub.append(float(v))
realm_sub_thresholds = arr_sub
# UI 通用 # UI 通用
hud_font_size = cfg.get_value("ui", "hud_font_size", hud_font_size) hud_font_size = cfg.get_value("ui", "hud_font_size", hud_font_size)
# 建筑系统
starting_spirit_stones = cfg.get_value("building", "starting_spirit_stones", starting_spirit_stones)
starting_food = cfg.get_value("building", "starting_food", starting_food)
building_max_level = cfg.get_value("building", "max_level", building_max_level)
for key in building_params.keys():
var p: Dictionary = building_params[key]
building_params[key] = {
"cost": cfg.get_value("building_" + key, "cost", p["cost"]),
"output": cfg.get_value("building_" + key, "output", p["output"]),
"upgrade_cost_x": cfg.get_value("building_" + key, "upgrade_cost_x", p["upgrade_cost_x"]),
"upgrade_output_x": cfg.get_value("building_" + key, "upgrade_output_x", p["upgrade_output_x"]),
}
# 口粮与生存
food_consumption_per_disciple = cfg.get_value("food", "consumption_per_disciple", food_consumption_per_disciple)
hunger_speed_multiplier = cfg.get_value("food", "hunger_speed_multiplier", hunger_speed_multiplier)
hunger_threshold_days = cfg.get_value("food", "hunger_threshold_days", hunger_threshold_days)
var apt_raw: Variant = cfg.get_value("cultivation", "aptitude_speed", []) var apt_raw: Variant = cfg.get_value("cultivation", "aptitude_speed", [])
if apt_raw is Array and apt_raw.size() == aptitude_speed.size(): if apt_raw is Array and apt_raw.size() == aptitude_speed.size():
var arr: Array[float] = [] var arr: Array[float] = []
+17 -11
View File
@@ -7,24 +7,30 @@ enum GameMode { NEW_GAME, LOAD_GAME }
## 当前进入游戏的方式。 ## 当前进入游戏的方式。
var mode: GameMode = GameMode.NEW_GAME var mode: GameMode = GameMode.NEW_GAME
## 要加载的存档 ID仅 LOAD_GAME 时有效)。 ## 当前存档 ID两级存档第一级,SaveSystem 分配)。
var save_id: String = "" var profile_id: int = 0
## 要加载的槽位文件名(仅 LOAD_GAME 时有效,如 "auto"/"slot_1754734800")。
var save_slot: String = ""
# 游戏内时间(12 月 × 30 天 = 360 天/年),实际推进由 TimeSystem 负责 # 游戏内时间(12 月 × 30 天 = 360 天/年),实际推进由 TimeSystem 负责
var year: int = 1 var year: int = 1 # 年
var month: int = 1 var month: int = 1 # 月(1-12
var day: int = 1 var day: int = 1 # 日(1-30
## 请求开始新游戏:重置模式与时间 ## 请求开始新游戏。
func request_new_game() -> void: ## @param pid: 新存档组 ID(主菜单创建)
func request_new_game(pid: int) -> void:
mode = GameMode.NEW_GAME mode = GameMode.NEW_GAME
save_id = "" profile_id = pid
save_slot = ""
year = 1 year = 1
month = 1 month = 1
day = 1 day = 1
## 请求加载指定存档。 ## 请求加载指定存档。
## @param id: 存档 ID ## @param pid: 存档 ID
func request_load_game(id: String) -> void: ## @param slot_name: 槽位文件名(不含 .json
func request_load_game(pid: int, slot_name: String) -> void:
mode = GameMode.LOAD_GAME mode = GameMode.LOAD_GAME
save_id = id profile_id = pid
save_slot = slot_name
+30 -3
View File
@@ -2,18 +2,45 @@
## 进入游戏时根据 GameState 模式初始化,并连接顶部 HUD 信号。 ## 进入游戏时根据 GameState 模式初始化,并连接顶部 HUD 信号。
extends Node extends Node
## 进入游戏世界:注册场景分组,按 GameState 模式初始化(新游戏/读档)。
func _ready() -> void: func _ready() -> void:
# 注册为游戏世界场景(供 TimeSystem 判断是否推进时间) # 注册为游戏世界场景(供 TimeSystem 判断是否推进时间)
add_to_group("game_scene") add_to_group("game_scene")
# 应用用户设置中的默认流速 # 应用用户设置中的默认流速
TimeSystem.apply_default_speed() TimeSystem.apply_default_speed()
# 开局招募:清空旧数据 → 生成 5 名候选 → 暂停时间等待选择(读档流程接入后由存档还原) match GameState.mode:
DiscipleManager.start_new_game() GameState.GameMode.NEW_GAME:
_start_new_game()
GameState.GameMode.LOAD_GAME:
# 读档还原(含日期/资源/名录/建筑);失败回退新游戏
if not SaveSystem.load_game(GameState.profile_id, GameState.save_slot):
push_warning("读档失败,回退新游戏")
_start_new_game()
## 顶部 HUD 的返回主菜单请求 ## 新游戏初始化:清空旧数据 → 开局招募弹窗(暂停)→ 设定开局资源
func _start_new_game() -> void:
# 开局招募:清空旧数据 → 生成 5 名候选 → 暂停时间等待选择
DiscipleManager.start_new_game()
# 开局建筑:清空旧建筑
BuildingManager.reset()
# 开局资源:直接设定(set 而非 add,避免与上次会话残留混淆)
SectManager.set_spirit_stones(GameConfig.starting_spirit_stones)
SectManager.set_food(GameConfig.starting_food)
SectManager.set_reputation(0)
## 顶部 HUD 的返回主菜单请求(先写自动存档)。
func _on_top_bar_hud_sign_return_mainmenu() -> void: func _on_top_bar_hud_sign_return_mainmenu() -> void:
SaveSystem.save_auto(GameState.profile_id)
get_tree().change_scene_to_file("res://src/UI/Panels/MainMenu/MainMenu.tscn") get_tree().change_scene_to_file("res://src/UI/Panels/MainMenu/MainMenu.tscn")
## 顶部 HUD 的"管理"按钮请求:打开弟子名录面板。 ## 顶部 HUD 的"管理"按钮请求:打开弟子名录面板。
func _on_top_bar_hud_sign_open_disciples() -> void: func _on_top_bar_hud_sign_open_disciples() -> void:
$DisciplesPanel.show_panel() $DisciplesPanel.show_panel()
## 顶部 HUD 的"资源"按钮请求:打开建筑面板。
func _on_top_bar_hud_sign_open_buildings() -> void:
$BuildingsPanel.show_panel()
## 顶部 HUD 的"存档"按钮请求:打开存档面板(存档模式)。
func _on_top_bar_hud_sign_open_save() -> void:
$SavePanel.open_save()
+9
View File
@@ -4,6 +4,8 @@
[ext_resource type="PackedScene" uid="uid://cahux61qqblje" path="res://src/UI/Panels/DisciplesPanel/DisciplesPanel.tscn" id="1_d5cpm"] [ext_resource type="PackedScene" uid="uid://cahux61qqblje" path="res://src/UI/Panels/DisciplesPanel/DisciplesPanel.tscn" id="1_d5cpm"]
[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" uid="uid://cahbpjpnmbcfv" path="res://src/UI/Panels/RecruitPanel/RecruitPanel.tscn" id="1_k5nrm"] [ext_resource type="PackedScene" uid="uid://cahbpjpnmbcfv" path="res://src/UI/Panels/RecruitPanel/RecruitPanel.tscn" id="1_k5nrm"]
[ext_resource type="PackedScene" path="res://src/UI/Panels/BuildingsPanel/BuildingsPanel.tscn" id="1_b9dpm"]
[ext_resource type="PackedScene" uid="uid://dj7b5vssydifa" path="res://src/UI/Panels/SavePanel.tscn" id="1_s4vpm"]
[ext_resource type="Texture2D" uid="uid://cweu17mwblxmg" path="res://assets/sprites/ui/宗门后山.png" id="2_2atgf"] [ext_resource type="Texture2D" uid="uid://cweu17mwblxmg" path="res://assets/sprites/ui/宗门后山.png" id="2_2atgf"]
[ext_resource type="Texture2D" uid="uid://dnjuv1btbxm4o" path="res://assets/sprites/ui/宗门全景.png" id="5_58g0x"] [ext_resource type="Texture2D" uid="uid://dnjuv1btbxm4o" path="res://assets/sprites/ui/宗门全景.png" id="5_58g0x"]
@@ -35,5 +37,12 @@ layout_mode = 3
[node name="DisciplesPanel" parent="." unique_id=780000015 instance=ExtResource("1_d5cpm")] [node name="DisciplesPanel" parent="." unique_id=780000015 instance=ExtResource("1_d5cpm")]
[node name="BuildingsPanel" parent="." unique_id=795000012 instance=ExtResource("1_b9dpm")]
[node name="SavePanel" parent="." unique_id=794000020 instance=ExtResource("1_s4vpm")]
visible = false
[connection signal="sign_open_buildings" from="TopBarHud" to="." method="_on_top_bar_hud_sign_open_buildings"]
[connection signal="sign_open_disciples" from="TopBarHud" to="." method="_on_top_bar_hud_sign_open_disciples"] [connection signal="sign_open_disciples" from="TopBarHud" to="." method="_on_top_bar_hud_sign_open_disciples"]
[connection signal="sign_open_save" from="TopBarHud" to="." method="_on_top_bar_hud_sign_open_save"]
[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"]
+286
View File
@@ -0,0 +1,286 @@
## 存档系统(Autoload 单例)。
## 两级存档:存档组 SaveGroup(一个角色/一次轮回)→ 存档槽 SaveSlot(该组下任意多个进度快照)。
## 每个新游戏创建一个组;每次存档在该组下新建一个槽;组与槽数量无上限。
## 文件:user://saves/profile_{组id}/{auto|slot_{时间戳}}.json
extends Node
## 存档根目录。
const SAVE_ROOT := "user://saves"
## 存档组列表文件。
const PROFILES_PATH := "user://saves/profiles.json"
## 自动槽文件名(固定)。
const AUTO_SLOT := "auto"
## 存档格式版本(兼容迁移预留)。
const VERSION := 1
## 新建存档组(主菜单"新游戏"调用),返回组 id(名"存档N")。
func create_profile() -> int:
var profiles := _load_profiles()
var id := 1
for p in profiles:
id = maxi(id, int(p["id"]) + 1)
profiles.append({
"id": id,
"name": "存档%d" % id,
"created": Time.get_date_string_from_system(),
"last_date": "",
})
_save_profiles(profiles)
return id
## 存档组列表(id/name/created/last_date)。
func get_profiles() -> Array[Dictionary]:
return _load_profiles()
## 删除存档组:从列表移除并删除该组目录(含全部槽位)。
## @param profile_id: 存档组 ID
## @return 是否删除成功
func delete_profile(profile_id: int) -> bool:
var profiles := _load_profiles()
var removed := false
for i in profiles.size():
if int(profiles[i]["id"]) == profile_id:
profiles.remove_at(i)
removed = true
break
if not removed:
return false
_save_profiles(profiles)
_remove_dir_recursive("%s/profile_%d" % [SAVE_ROOT, profile_id])
return true
## 保存游戏:在指定组下**新建一个槽位**,返回槽文件名;失败返回空串。
## 槽位命名:组内序号递增(slot_1、slot_2…),删除后从当前最大序号 +1 继续,不重用。
## @param profile_id: 存档组 ID
func save_game(profile_id: int) -> String:
_ensure_profile_dir(profile_id)
var slot_name := "slot_%d" % _next_slot_index(profile_id)
if not _write_slot(profile_id, slot_name):
return ""
_update_profile_last_date(profile_id)
return slot_name
## 保存到自动槽(返回主菜单时调用)。
## @param profile_id: 存档组 ID
func save_auto(profile_id: int) -> bool:
_ensure_profile_dir(profile_id)
return _write_slot(profile_id, AUTO_SLOT)
## 该组全部槽位(含自动槽,自动槽排最前;手动槽按时间升序)。
## 返回 [{name, date_text, disciple_count, spirit_stones}]。
## @param profile_id: 存档组 ID
func get_slots(profile_id: int) -> Array[Dictionary]:
var result: Array[Dictionary] = []
var dir := DirAccess.open("%s/profile_%d" % [SAVE_ROOT, profile_id])
if dir == null:
return result
dir.list_dir_begin()
var file_name := dir.get_next()
while file_name != "":
if not dir.current_is_dir() and file_name.ends_with(".json"):
var meta := get_slot_meta(profile_id, file_name.trim_suffix(".json"))
if not meta.is_empty():
meta["name"] = file_name.trim_suffix(".json")
result.append(meta)
file_name = dir.get_next()
dir.list_dir_end()
result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
var a_auto: bool = a["name"] == AUTO_SLOT
var b_auto: bool = b["name"] == AUTO_SLOT
if a_auto != b_auto:
return a_auto
return str(a["name"]) < str(b["name"])
)
return result
## 加载存档:清空当前数据后还原各系统。
## @param profile_id: 存档组 ID
## @param slot_name: 槽文件名(不含 .json
## @return 是否加载成功
func load_game(profile_id: int, slot_name: String) -> bool:
var f := FileAccess.open(_slot_path(profile_id, slot_name), FileAccess.READ)
if f == null:
push_warning("存档不存在: " + _slot_path(profile_id, slot_name))
return false
var parsed: Variant = JSON.parse_string(f.get_as_text())
if parsed is not Dictionary:
push_warning("存档解析失败: " + _slot_path(profile_id, slot_name))
return false
_restore(parsed)
return true
## 删除槽位文件。
## @param profile_id: 存档组 ID
## @param slot_name: 槽文件名(不含 .json
## @return 是否删除成功
func delete_slot(profile_id: int, slot_name: String) -> bool:
if slot_name == AUTO_SLOT:
return false
var path := _slot_path(profile_id, slot_name)
if not FileAccess.file_exists(path):
return false
return DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) == OK
## 槽位元数据(列表显示用:日期/弟子数/灵石),空档返回空字典。
## @param profile_id: 存档组 ID
## @param slot_name: 槽文件名(不含 .json
func get_slot_meta(profile_id: int, slot_name: String) -> Dictionary:
var f := FileAccess.open(_slot_path(profile_id, slot_name), FileAccess.READ)
if f == null:
return {}
var parsed: Variant = JSON.parse_string(f.get_as_text())
if parsed is not Dictionary:
return {}
var meta: Dictionary = parsed
var d: Dictionary = meta["saved_date"]
return {
"date_text": "%d%d%d" % [d["year"], d["month"], d["day"]],
"disciple_count": int(meta["disciples"].size()),
"spirit_stones": int(meta["resources"]["spirit_stones"]),
}
## 清空内存中所有游戏数据(读档/新游戏前调用)。
func reset() -> void:
DiscipleManager.reset()
BuildingManager.reset()
SectManager.set_spirit_stones(0)
SectManager.set_reputation(0)
SectManager.set_food(0)
## 收集当前游戏数据并写入槽位文件。
## @param profile_id: 存档组 ID
## @param slot_name: 槽文件名(不含 .json
func _write_slot(profile_id: int, slot_name: String) -> bool:
var data := _collect_data(profile_id)
var f := FileAccess.open(_slot_path(profile_id, slot_name), FileAccess.WRITE)
if f == null:
push_warning("存档写入失败: " + _slot_path(profile_id, slot_name))
return false
f.store_string(JSON.stringify(data, "\t"))
return true
## 收集当前游戏数据为字典。
## @param profile_id: 存档组 ID
func _collect_data(profile_id: int) -> Dictionary:
var disciples_data: Array = []
for d in DiscipleManager.disciples:
disciples_data.append(d.to_dict())
var buildings_data: Array = []
for b in BuildingManager.buildings:
buildings_data.append(b.to_dict())
return {
"version": VERSION,
"profile_id": profile_id,
"saved_date": {"year": GameState.year, "month": GameState.month, "day": GameState.day},
"date": {"year": GameState.year, "month": GameState.month, "day": GameState.day},
"resources": {
"spirit_stones": SectManager.spirit_stones,
"reputation": SectManager.reputation,
"food": SectManager.food,
},
"next_disciple_id": DiscipleManager.get_next_disciple_id(),
"disciples": disciples_data,
"buildings": buildings_data,
}
## 将存档数据还原到各系统(setter 带信号,HUD 自动刷新)。
## @param data: 存档字典
func _restore(data: Dictionary) -> void:
reset()
var d: Dictionary = data["date"]
TimeSystem.set_date(int(d["year"]), int(d["month"]), int(d["day"]))
var r: Dictionary = data["resources"]
SectManager.set_spirit_stones(int(r["spirit_stones"]))
SectManager.set_reputation(int(r["reputation"]))
SectManager.set_food(int(r["food"]))
DiscipleManager.set_next_disciple_id(int(data["next_disciple_id"]))
for item in data["disciples"]:
DiscipleManager.disciples.append(Disciple.from_dict(item))
DiscipleManager.disciples_changed.emit()
for item in data["buildings"]:
BuildingManager.buildings.append(Building.from_dict(item))
BuildingManager.buildings_changed.emit()
## 槽位文件路径。
func _slot_path(profile_id: int, slot_name: String) -> String:
return "%s/profile_%d/%s.json" % [SAVE_ROOT, profile_id, slot_name]
## 确保存档组目录存在。
func _ensure_profile_dir(profile_id: int) -> void:
var da := DirAccess.open("user://")
if da == null:
return
da.make_dir_recursive("saves")
da.make_dir_recursive("saves/profile_%d" % profile_id)
## 递归删除目录(Godot 无原生递归删除,手动遍历)。
## @param path: 目录路径(user:// 风格)
func _remove_dir_recursive(path: String) -> void:
var da := DirAccess.open(path)
if da == null:
return
da.list_dir_begin()
var f := da.get_next()
while f != "":
if da.current_is_dir():
_remove_dir_recursive(path + "/" + f)
else:
DirAccess.remove_absolute(ProjectSettings.globalize_path(path + "/" + f))
f = da.get_next()
da.list_dir_end()
da.remove(da.get_current_dir())
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
## 该组下一个槽位序号(现有最大 slot_N 序号 +1,删除后不重用)。
## @param profile_id: 存档组 ID
func _next_slot_index(profile_id: int) -> int:
var max_index := 0
var dir := DirAccess.open("%s/profile_%d" % [SAVE_ROOT, profile_id])
if dir == null:
return 1
dir.list_dir_begin()
var f := dir.get_next()
while f != "":
if f.begins_with("slot_") and f.ends_with(".json"):
var idx := int(f.trim_suffix(".json").trim_prefix("slot_"))
max_index = maxi(max_index, idx)
f = dir.get_next()
dir.list_dir_end()
return max_index + 1
## 读取存档组列表。
func _load_profiles() -> Array[Dictionary]:
if not FileAccess.file_exists(PROFILES_PATH):
return []
var f := FileAccess.open(PROFILES_PATH, FileAccess.READ)
if f == null:
return []
var parsed: Variant = JSON.parse_string(f.get_as_text())
if parsed is not Dictionary or not parsed.has("profiles"):
return []
var result: Array[Dictionary] = []
for p in parsed["profiles"]:
result.append(p)
return result
## 写入存档组列表。
## @param profiles: 存档组列表
func _save_profiles(profiles: Array[Dictionary]) -> void:
var da := DirAccess.open("user://")
if da != null:
da.make_dir_recursive("saves")
var f := FileAccess.open(PROFILES_PATH, FileAccess.WRITE)
if f == null:
return
f.store_string(JSON.stringify({"profiles": profiles}, "\t"))
## 更新存档组的最后存档日期。
## @param profile_id: 存档组 ID
func _update_profile_last_date(profile_id: int) -> void:
var profiles := _load_profiles()
for p in profiles:
if int(p["id"]) == profile_id:
p["last_date"] = "%d%d%d" % [GameState.year, GameState.month, GameState.day]
break
_save_profiles(profiles)
+1
View File
@@ -0,0 +1 @@
uid://bwt83xydsgcir
+33
View File
@@ -10,26 +10,59 @@ var reputation := 0 # 声望
var food := 0 # 灵米(食物资源,HUD 显示"灵米" var food := 0 # 灵米(食物资源,HUD 显示"灵米"
## 增加灵石并广播信号。
## @param amount: 增加量
func add_spirit_stones(amount: int) -> void: func add_spirit_stones(amount: int) -> void:
spirit_stones += amount spirit_stones += amount
emit_signal("spirit_stones_changed", spirit_stones, amount) emit_signal("spirit_stones_changed", spirit_stones, amount)
## 减少灵石并广播信号。
## @param amount: 减少量
func remove_spirit_stones(amount: int) -> void: func remove_spirit_stones(amount: int) -> void:
spirit_stones -= amount spirit_stones -= amount
emit_signal("spirit_stones_changed", spirit_stones, -amount) emit_signal("spirit_stones_changed", spirit_stones, -amount)
## 增加声望并广播信号。
## @param amount: 增加量
func add_reputation(amount: int) -> void: func add_reputation(amount: int) -> void:
reputation += amount reputation += amount
emit_signal("reputation_changed", reputation, amount) emit_signal("reputation_changed", reputation, amount)
## 减少声望并广播信号。
## @param amount: 减少量
func remove_reputation(amount: int) -> void: func remove_reputation(amount: int) -> void:
reputation -= amount reputation -= amount
emit_signal("reputation_changed", reputation, -amount) emit_signal("reputation_changed", reputation, -amount)
## 增加灵米并广播信号。
## @param amount: 增加量
func add_food(amount: int) -> void: func add_food(amount: int) -> void:
food += amount food += amount
emit_signal("food_changed", food, amount) emit_signal("food_changed", food, amount)
## 减少灵米并广播信号。
## @param amount: 减少量
func remove_food(amount: int) -> void: func remove_food(amount: int) -> void:
food -= amount food -= amount
emit_signal("food_changed", food, -amount) emit_signal("food_changed", food, -amount)
## 直接设定灵石(读档/新游戏用,带信号 HUD 自动刷新)。
## @param value: 目标值
func set_spirit_stones(value: int) -> void:
var delta := value - spirit_stones
spirit_stones = value
spirit_stones_changed.emit(spirit_stones, delta)
## 直接设定声望(读档/新游戏用,带信号 HUD 自动刷新)。
## @param value: 目标值
func set_reputation(value: int) -> void:
var delta := value - reputation
reputation = value
reputation_changed.emit(reputation, delta)
## 直接设定灵米(读档/新游戏用,带信号 HUD 自动刷新)。
## @param value: 目标值
func set_food(value: int) -> void:
var delta := value - food
food = value
food_changed.emit(food, delta)
+1
View File
@@ -41,6 +41,7 @@ var paused := false
var _elapsed := 0.0 var _elapsed := 0.0
## 每帧累积时间,满一天推进日期(仅游戏世界场景内)。
func _process(delta: float) -> void: func _process(delta: float) -> void:
if paused or not _is_in_game(): if paused or not _is_in_game():
return return
+64
View File
@@ -0,0 +1,64 @@
## 建筑管理器(Autoload 单例)。
## 负责建筑建造/升级与每日产出结算,产出直接进入 SectManager 资源。
## 本期每种建筑至多一座(面板按类型一行展示);多实例留待后续扩展。
extends Node
## 建筑列表变化时发出(建造/升级/重置)。
signal buildings_changed
## 宗门建筑列表。
var buildings: Array[Building] = []
## 挂接每日推进信号,结算建筑产出。
func _ready() -> void:
# 每日推进时结算各建筑产出
TimeSystem.day_passed.connect(_on_day_passed)
## 建造建筑:检查灵石足够 → 扣费入列。
## 同类型已存在时拒绝(本期一座一型)。
## @param type: 建筑种类
## @return 是否建造成功
func build(type: Building.Type) -> bool:
if _find(type) != null:
return false
var b := Building.new()
b.type = type
if SectManager.spirit_stones < b.get_build_cost():
return false
SectManager.remove_spirit_stones(b.get_build_cost())
buildings.append(b)
buildings_changed.emit()
return true
## 升级建筑:检查灵石足够 → 扣费提升等级。
## @param b: 要升级的建筑
## @return 是否升级成功
func upgrade(b: Building) -> bool:
if b.is_max_level() or SectManager.spirit_stones < b.get_upgrade_cost():
return false
SectManager.remove_spirit_stones(b.get_upgrade_cost())
b.level += 1
buildings_changed.emit()
return true
## 清空建筑列表(新游戏开局调用)。
func reset() -> void:
buildings.clear()
buildings_changed.emit()
## 每日结算:各建筑产出进入 SectManager 对应资源。
func _on_day_passed(_year: int, _month: int, _day: int) -> void:
for b in buildings:
match b.type:
Building.Type.MINE:
SectManager.add_spirit_stones(b.get_output())
Building.Type.FIELD:
SectManager.add_food(b.get_output())
## 查找指定类型的建筑,不存在返回 null。
func _find(type: Building.Type) -> Building:
for b in buildings:
if b.type == type:
return b
return null
+1
View File
@@ -0,0 +1 @@
uid://6hi0wog110up
+66
View File
@@ -0,0 +1,66 @@
## 建筑数据模型(纯数据,不挂在场景树上)。
## 价格与产出不在建筑上存副本,运行时按 type+level 从 GameConfig 计算。
class_name Building
extends Resource
## 建筑种类。
enum Type { MINE, FIELD } # 灵石矿 / 灵田
## 建筑类型。
var type: Type = Type.MINE
## 等级(1 起)。
var level: int = 1
## 建筑类型键(对应 GameConfig.building_params 字典键)。
static func get_type_key(t: Type) -> String:
match t:
Type.MINE:
return "mine"
Type.FIELD:
return "field"
return ""
## 建筑显示名称。
static func get_type_name(t: Type) -> String:
match t:
Type.MINE:
return "灵石矿"
Type.FIELD:
return "灵田"
return ""
## 建造价格(灵石)。
func get_build_cost() -> int:
return get_build_cost_for(type)
## 建造价格(静态查询,无需实例)。
static func get_build_cost_for(t: Type) -> int:
var p: Dictionary = GameConfig.building_params[get_type_key(t)]
return int(p["cost"])
## 日产出量 = 基础产出 × 升级产出倍率^(等级-1)。
func get_output() -> int:
var p: Dictionary = GameConfig.building_params[get_type_key(type)]
return int(p["output"] * pow(p["upgrade_output_x"], level - 1))
## 升级价格 = 建造价 × 升级价格倍率^等级。
func get_upgrade_cost() -> int:
var p: Dictionary = GameConfig.building_params[get_type_key(type)]
return int(p["cost"] * pow(p["upgrade_cost_x"], level))
## 是否已达等级上限。
func is_max_level() -> bool:
return level >= GameConfig.building_max_level
## 序列化为字典(D8 存档用)。
func to_dict() -> Dictionary:
return {"type": int(type), "level": level}
## 从字典还原(D8 读档用)。
## @param data: to_dict 产生的字典
static func from_dict(data: Dictionary) -> Building:
var b := Building.new()
b.type = int(data["type"]) as Type
b.level = int(data["level"])
return b
+1
View File
@@ -0,0 +1 @@
uid://dmfhbyjwngqh3
+18 -5
View File
@@ -91,6 +91,12 @@ size_flags_horizontal = 3
theme = ExtResource("2_ymrsi") theme = ExtResource("2_ymrsi")
text = "资源" text = "资源"
[node name="Button_Save" type="Button" parent="VBoxContainer/HBoxContainer2" unique_id=794000010]
layout_mode = 2
size_flags_horizontal = 3
theme = ExtResource("2_ymrsi")
text = "存档"
[node name="Button_Return" type="Button" parent="VBoxContainer/HBoxContainer2" unique_id=867839141] [node name="Button_Return" type="Button" parent="VBoxContainer/HBoxContainer2" unique_id=867839141]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
@@ -103,7 +109,7 @@ layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HBoxContainer3" unique_id=1608257100] [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HBoxContainer3" unique_id=1608257100]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_stretch_ratio = 25.0 size_flags_stretch_ratio = 12.0
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer3/HBoxContainer" unique_id=1557830593] [node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer3/HBoxContainer" unique_id=1557830593]
layout_mode = 2 layout_mode = 2
@@ -116,7 +122,8 @@ layout_mode = 2
[node name="Label_Time" type="Label" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer/HBoxContainer5" unique_id=671872515] [node name="Label_Time" type="Label" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer/HBoxContainer5" unique_id=671872515]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 8 size_flags_vertical = 1
size_flags_stretch_ratio = 2.0
theme_override_colors/font_color = Color(0.72, 0.85, 0.78, 1) theme_override_colors/font_color = Color(0.72, 0.85, 0.78, 1)
text = "时间" text = "时间"
label_settings = ExtResource("3_4k476") label_settings = ExtResource("3_4k476")
@@ -138,6 +145,7 @@ size_flags_vertical = 3
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 1 size_flags_vertical = 1
size_flags_stretch_ratio = 2.0
theme_override_colors/font_color = Color(0.72, 0.85, 0.78, 1) theme_override_colors/font_color = Color(0.72, 0.85, 0.78, 1)
text = "灵石" text = "灵石"
label_settings = ExtResource("3_4k476") label_settings = ExtResource("3_4k476")
@@ -157,7 +165,8 @@ layout_mode = 2
[node name="Label_Reputation" type="Label" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer/HBoxContainer2" unique_id=985110673] [node name="Label_Reputation" type="Label" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer/HBoxContainer2" unique_id=985110673]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 8 size_flags_vertical = 1
size_flags_stretch_ratio = 2.0
theme_override_colors/font_color = Color(0.72, 0.85, 0.78, 1) theme_override_colors/font_color = Color(0.72, 0.85, 0.78, 1)
text = "声望" text = "声望"
label_settings = ExtResource("3_4k476") label_settings = ExtResource("3_4k476")
@@ -177,7 +186,8 @@ layout_mode = 2
[node name="Label_Food" type="Label" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer/HBoxContainer3" unique_id=791000001] [node name="Label_Food" type="Label" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer/HBoxContainer3" unique_id=791000001]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 8 size_flags_vertical = 1
size_flags_stretch_ratio = 2.0
theme_override_colors/font_color = Color(0.72, 0.85, 0.78, 1) theme_override_colors/font_color = Color(0.72, 0.85, 0.78, 1)
text = "灵米" text = "灵米"
label_settings = ExtResource("3_4k476") label_settings = ExtResource("3_4k476")
@@ -197,7 +207,8 @@ layout_mode = 2
[node name="Label_Qi" type="Label" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer/HBoxContainer4" unique_id=790000001] [node name="Label_Qi" type="Label" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer/HBoxContainer4" unique_id=790000001]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 8 size_flags_vertical = 1
size_flags_stretch_ratio = 2.0
theme_override_colors/font_color = Color(0.72, 0.85, 0.78, 1) theme_override_colors/font_color = Color(0.72, 0.85, 0.78, 1)
text = "灵气" text = "灵气"
label_settings = ExtResource("3_4k476") label_settings = ExtResource("3_4k476")
@@ -219,4 +230,6 @@ size_flags_stretch_ratio = 75.0
[connection signal="button_up" from="VBoxContainer/HBoxContainer2/SpeedGroup/Button_Pause" to="." method="_on_button_pause_button_up"] [connection signal="button_up" from="VBoxContainer/HBoxContainer2/SpeedGroup/Button_Pause" to="." method="_on_button_pause_button_up"]
[connection signal="button_up" from="VBoxContainer/HBoxContainer2/Button_StaffManagement" to="." method="_on_button_staff_management_button_up"] [connection signal="button_up" from="VBoxContainer/HBoxContainer2/Button_StaffManagement" to="." method="_on_button_staff_management_button_up"]
[connection signal="button_up" from="VBoxContainer/HBoxContainer2/Button_ResourcesManagement" to="." method="_on_button_resources_management_button_up"]
[connection signal="button_up" from="VBoxContainer/HBoxContainer2/Button_Save" to="." method="_on_button_save_button_up"]
[connection signal="button_up" from="VBoxContainer/HBoxContainer2/Button_Return" to="." method="_on_button_return_button_up"] [connection signal="button_up" from="VBoxContainer/HBoxContainer2/Button_Return" to="." method="_on_button_return_button_up"]
+13
View File
@@ -6,6 +6,10 @@ extends Control
signal sign_return_mainmenu signal sign_return_mainmenu
## 请求打开弟子名录面板。 ## 请求打开弟子名录面板。
signal sign_open_disciples signal sign_open_disciples
## 请求打开建筑面板。
signal sign_open_buildings
## 请求打开存档面板。
signal sign_open_save
@onready var time_label: Label = %Value_Time @onready var time_label: Label = %Value_Time
@onready var currency_label: Label = %Value_Currency @onready var currency_label: Label = %Value_Currency
@@ -15,6 +19,7 @@ signal sign_open_disciples
@onready var pause_button: Button = %Button_Pause @onready var pause_button: Button = %Button_Pause
@onready var speed_buttons: Array[Button] = [%Button_Speed1, %Button_Speed5, %Button_Speed20] @onready var speed_buttons: Array[Button] = [%Button_Speed1, %Button_Speed5, %Button_Speed20]
## 初始化:连接速度按钮与各信号、设置初始显示、应用配置字号。
func _ready() -> void: func _ready() -> void:
for i in speed_buttons.size(): for i in speed_buttons.size():
var btn := speed_buttons[i] var btn := speed_buttons[i]
@@ -62,6 +67,14 @@ func _on_button_return_button_up() -> void:
func _on_button_staff_management_button_up() -> void: func _on_button_staff_management_button_up() -> void:
sign_open_disciples.emit() sign_open_disciples.emit()
## 点击"资源"按钮:请求打开建筑面板(由 MainGame 处理)。
func _on_button_resources_management_button_up() -> void:
sign_open_buildings.emit()
## 点击"存档"按钮:请求打开存档面板(由 MainGame 处理)。
func _on_button_save_button_up() -> void:
sign_open_save.emit()
## 点击暂停/继续按钮,切换后刷新按钮状态。 ## 点击暂停/继续按钮,切换后刷新按钮状态。
func _on_button_pause_button_up() -> void: func _on_button_pause_button_up() -> void:
TimeSystem.toggle_pause() TimeSystem.toggle_pause()
@@ -0,0 +1,78 @@
[gd_scene format=3 uid="uid://b9k4v8q2x5n3m7p6"]
[ext_resource type="Script" path="res://src/UI/Panels/BuildingsPanel/buildings_panel.gd" id="1_script"]
[ext_resource type="FontFile" uid="uid://4su41gib4l5u" path="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKaiMono-Medium.ttf" id="2_font"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dialog"]
bg_color = Color(0.07, 0.13, 0.12, 0.97)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.4, 0.72, 0.6, 1)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[node name="BuildingsPanel" type="Control" unique_id=795000001]
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=795000002]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.02, 0.05, 0.05, 0.65)
[node name="CenterContainer" type="CenterContainer" parent="." unique_id=795000003]
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=795000004]
theme_override_styles/panel = SubResource("StyleBoxFlat_dialog")
[node name="MarginContainer" type="MarginContainer" parent="CenterContainer/Panel" unique_id=795000005]
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=795000006]
theme_override_constants/separation = 12
[node name="TitleLabel" type="Label" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=795000007]
unique_name_in_owner = true
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 30
theme_override_colors/font_color = Color(0.82, 0.95, 0.88, 1)
text = "宗门建筑"
[node name="ListContainer" type="VBoxContainer" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=795000008]
unique_name_in_owner = true
custom_minimum_size = Vector2(1280, 680)
theme_override_constants/separation = 10
[node name="ButtonRow" type="HBoxContainer" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=795000009]
[node name="Spacer" type="Control" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow" unique_id=795000010]
layout_mode = 2
size_flags_horizontal = 3
[node name="Button_Close" type="Button" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow" unique_id=795000011]
unique_name_in_owner = true
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 22
text = "关闭"
[connection signal="gui_input" from="Backdrop" to="." method="_on_backdrop_gui_input"]
[connection signal="button_up" from="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow/Button_Close" to="." method="hide_panel"]
@@ -0,0 +1,96 @@
## 建筑面板(挂载于 BuildingsPanel.tscn)。
## 展示可建/已建建筑(灵石矿/灵田),提供建造与升级操作。
## 打开面板暂停时间,关闭恢复;监听建筑变化信号实时刷新。
extends Control
@onready var title_label: Label = %TitleLabel
@onready var list_container: VBoxContainer = %ListContainer
## 打开面板前的暂停状态(关闭时恢复,避免冲掉玩家手动暂停)。
var _was_paused := false
## 订阅建筑变化信号,默认隐藏。
func _ready() -> void:
BuildingManager.buildings_changed.connect(_on_buildings_changed)
visible = false
## 打开面板并刷新列表(暂停时间推进)。
func show_panel() -> void:
_was_paused = TimeSystem.paused
_refresh()
TimeSystem.set_paused(true)
visible = true
## 关闭面板(恢复打开前的暂停状态)。
func hide_panel() -> void:
visible = false
TimeSystem.set_paused(_was_paused)
## 点击遮罩关闭面板。
func _on_backdrop_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
hide_panel()
## 建筑列表变化时刷新。
func _on_buildings_changed() -> void:
if visible:
_refresh()
## 重建列表:按建筑类型逐行生成(未建显示建造按钮,已建显示等级/产出/升级按钮)。
func _refresh() -> void:
for child in list_container.get_children():
child.queue_free()
title_label.text = "宗门建筑"
for type in [Building.Type.MINE, Building.Type.FIELD]:
_add_row(type)
## 添加一行建筑条目。
## @param type: 建筑种类
func _add_row(type: Building.Type) -> void:
var b := BuildingManager._find(type)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 12)
var name_label := Label.new()
name_label.text = Building.get_type_name(type)
name_label.custom_minimum_size = Vector2(140, 0)
row.add_child(name_label)
var info_label := Label.new()
info_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
if b == null:
info_label.text = "未建造"
else:
info_label.text = "等级 %d 日产出 %d" % [b.level, b.get_output()]
row.add_child(info_label)
var btn := Button.new()
if b == null:
var cost := Building.get_build_cost_for(type)
btn.text = "建造(%d 灵石)" % cost
btn.disabled = SectManager.spirit_stones < cost
btn.button_up.connect(_on_build_button_up.bind(type))
else:
if b.is_max_level():
btn.text = "已满级"
btn.disabled = true
else:
btn.text = "升级(%d 灵石)" % b.get_upgrade_cost()
btn.disabled = SectManager.spirit_stones < b.get_upgrade_cost()
btn.button_up.connect(_on_upgrade_button_up.bind(b))
row.add_child(btn)
list_container.add_child(row)
## 点击建造按钮。
## @param type: 建筑种类
func _on_build_button_up(type: Building.Type) -> void:
if BuildingManager.build(type):
_refresh()
## 点击升级按钮。
## @param b: 要升级的建筑
func _on_upgrade_button_up(b: Building) -> void:
if BuildingManager.upgrade(b):
_refresh()
@@ -0,0 +1 @@
uid://bhvcvw4ufjhxv
@@ -66,6 +66,12 @@ text = ""
[node name="ButtonRow" type="HBoxContainer" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=781000009] [node name="ButtonRow" type="HBoxContainer" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=781000009]
[node name="Button_RealmBreakthrough" type="Button" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow" unique_id=781000012]
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=781000010] [node name="Spacer" type="Control" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow" unique_id=781000010]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
@@ -77,3 +83,4 @@ theme_override_font_sizes/font_size = 22
text = "关闭" text = "关闭"
[connection signal="button_up" from="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow/Button_Close" to="." method="_on_button_close_button_up"] [connection signal="button_up" from="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow/Button_Close" to="." method="_on_button_close_button_up"]
[connection signal="button_up" from="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow/Button_RealmBreakthrough" to="." method="_on_button_realm_breakthrough_button_up"]
@@ -1,18 +1,23 @@
## 弟子详情弹窗(挂载于 DiscipleDetailPanel.tscn)。 ## 弟子详情弹窗(挂载于 DiscipleDetailPanel.tscn)。
## 静态展示弟子信息(资质/年龄/灵根/境界/编号/入宗时间), ## 静态展示弟子信息(资质/年龄/灵根/境界/编号/入宗时间),
## 修炼进度等动态内容留待 D5 修炼循环填充 ## 修炼进度、大境界突破按钮(手动花费灵石)等动态内容。
extends Control extends Control
@onready var title_label: Label = %TitleLabel @onready var title_label: Label = %TitleLabel
@onready var info_label: Label = %InfoLabel @onready var info_label: Label = %InfoLabel
@onready var breakthrough_button: Button = %Button_RealmBreakthrough
var _current_disciple: Disciple = null
## 默认隐藏。
func _ready() -> void: func _ready() -> void:
visible = false visible = false
## 显示指定弟子的详情。 ## 显示指定弟子的详情。
## @param d: 要展示的弟子 ## @param d: 要展示的弟子
func show_detail(d: Disciple) -> void: func show_detail(d: Disciple) -> void:
_current_disciple = d
title_label.text = d.name + ("(天灵根)" if d.is_heavenly else "") title_label.text = d.name + ("(天灵根)" if d.is_heavenly else "")
info_label.text = "名字:%s\n资质:%s\n年龄:%d\n灵根:%s\n境界:%s\n修为:%s\n入宗时间:%s" % [ info_label.text = "名字:%s\n资质:%s\n年龄:%d\n灵根:%s\n境界:%s\n修为:%s\n入宗时间:%s" % [
d.name, d.name,
@@ -23,8 +28,38 @@ func show_detail(d: Disciple) -> void:
d.get_cultivation_progress_text(), d.get_cultivation_progress_text(),
d.get_join_date_text(), d.get_join_date_text(),
] ]
_refresh_breakthrough_button()
visible = true visible = true
## 刷新大境界突破按钮的文案与可用状态。
## 修为未圆满 / 灵石不足 / 已至巅峰 → 置灰;否则可点击(文案显示所需灵石)。
func _refresh_breakthrough_button() -> void:
if _current_disciple == null:
return
var next := _current_disciple.get_next_realm_index()
if next < 0:
breakthrough_button.text = "已至巅峰"
breakthrough_button.disabled = true
return
var cost := _current_disciple.get_realm_breakthrough_cost()
if not _current_disciple.is_realm_peak():
breakthrough_button.text = "大境界突破(修为未圆满)"
breakthrough_button.disabled = true
return
if SectManager.spirit_stones < cost:
breakthrough_button.text = "大境界突破(需 %d 灵石)" % cost
breakthrough_button.disabled = true
return
breakthrough_button.text = "大境界突破(需 %d 灵石)" % cost
breakthrough_button.disabled = false
## 点击"大境界突破"按钮:执行突破判定(扣灵石 → 成功升大境界/失败修为倒退),随后刷新面板。
func _on_button_realm_breakthrough_button_up() -> void:
if _current_disciple == null:
return
CultivationManager.try_realm_breakthrough(_current_disciple)
show_detail(_current_disciple)
## 点击"关闭"按钮。 ## 点击"关闭"按钮。
func _on_button_close_button_up() -> void: func _on_button_close_button_up() -> void:
visible = false visible = false
@@ -14,7 +14,11 @@ const APTITUDE_COLORS := {
@onready var empty_label: Label = %EmptyLabel @onready var empty_label: Label = %EmptyLabel
@onready var detail_panel: Control = %DiscipleDetailPanel @onready var detail_panel: Control = %DiscipleDetailPanel
## 打开面板前的暂停状态(关闭时恢复,避免冲掉玩家手动暂停)。
var _was_paused := false
## 订阅名录变化信号,默认隐藏。
func _ready() -> void: func _ready() -> void:
# 名录变化时刷新(面板打开期间也同步) # 名录变化时刷新(面板打开期间也同步)
DiscipleManager.disciples_changed.connect(_on_disciples_changed) DiscipleManager.disciples_changed.connect(_on_disciples_changed)
@@ -22,14 +26,15 @@ func _ready() -> void:
## 打开面板并刷新列表(暂停时间推进)。 ## 打开面板并刷新列表(暂停时间推进)。
func show_panel() -> void: func show_panel() -> void:
_was_paused = TimeSystem.paused
_refresh_list() _refresh_list()
TimeSystem.set_paused(true) TimeSystem.set_paused(true)
visible = true visible = true
## 关闭面板(恢复时间推进;子层详情弹窗随父隐藏)。 ## 关闭面板(恢复打开前的暂停状态;子层详情弹窗随父隐藏)。
func hide_panel() -> void: func hide_panel() -> void:
visible = false visible = false
TimeSystem.set_paused(false) TimeSystem.set_paused(_was_paused)
## 点击遮罩关闭面板。 ## 点击遮罩关闭面板。
func _on_backdrop_gui_input(event: InputEvent) -> void: func _on_backdrop_gui_input(event: InputEvent) -> void:
+5 -4
View File
@@ -1,15 +1,16 @@
## 主菜单场景(挂载于 MainMenu.tscn)。 ## 主菜单场景(挂载于 MainMenu.tscn)。
extends Control extends Control
## 点击"新游戏":重置 GameState 后进入游戏世界。 ## 点击"新游戏"创建新存档组 → 重置 GameState 后进入游戏世界。
func MainMenu_NewGame_ButtonUp() -> void: func MainMenu_NewGame_ButtonUp() -> void:
GameState.request_new_game() var pid := SaveSystem.create_profile()
GameState.request_new_game(pid)
get_tree().change_scene_to_file("res://src/Core/MainGame.tscn") get_tree().change_scene_to_file("res://src/Core/MainGame.tscn")
## 点击"读取存档":隐藏主按钮组并显示存档面板。 ## 点击"读取存档":隐藏主按钮组并打开存档面板(读档模式)
func MainMenu_LoadGame_ButtonUp() -> void: func MainMenu_LoadGame_ButtonUp() -> void:
$ButtonGroup.hide() $ButtonGroup.hide()
$SavePanel.show() $SavePanel.open_load()
## 点击"设置"TODO:接入设置面板)。 ## 点击"设置"TODO:接入设置面板)。
func MainMenu_Settings_ButtonUp() -> void: func MainMenu_Settings_ButtonUp() -> void:
@@ -18,6 +18,7 @@ var _check_buttons: Array[CheckButton] = []
var _entry_disciples: Array[Disciple] = [] var _entry_disciples: Array[Disciple] = []
## 订阅候选生成信号,默认隐藏。
func _ready() -> void: func _ready() -> void:
# 订阅候选生成信号:开局与定期招募都会触发 # 订阅候选生成信号:开局与定期招募都会触发
DiscipleManager.applicants_generated.connect(_on_applicants_generated) DiscipleManager.applicants_generated.connect(_on_applicants_generated)
+161 -3
View File
@@ -1,10 +1,168 @@
## 存档选择面板(挂载于 SavePanel.tscn)。 ## 存档面板(挂载于 SavePanel.tscn)。
## 显示存档列表与"新建存档"入口,通过信号通知外部结果 ## 两级浏览:左侧存档组列表 → 右侧该组槽位列表
## 存档模式(游戏内):点"新建存档"每次创建新槽位;读档模式(主菜单):点槽位读档。
extends PanelContainer extends PanelContainer
## 面板模式。
enum Mode { SAVE, LOAD }
## 请求关闭面板(返回上级界面)。 ## 请求关闭面板(返回上级界面)。
signal back_pressed signal back_pressed
## 点击"返回"按钮 ## 槽位条目场景
const SAVE_SLOT_SCENE := preload("res://src/UI/Panels/SaveSlot.tscn")
@onready var title_label: Label = %TitleLabel
@onready var group_container: VBoxContainer = %GroupContainer
@onready var slot_list: VBoxContainer = %SlotList
@onready var new_slot_button: Button = %Button_NewSlot
## 当前模式。
var mode: Mode = Mode.LOAD
## 当前浏览的存档组 ID。
var _current_profile: int = 0
## 打开面板前的暂停状态(存档模式关闭时恢复)。
var _was_paused := false
## 读档模式打开(主菜单,不暂停时间,自动选中第一个存档组)。
func open_load() -> void:
mode = Mode.LOAD
new_slot_button.visible = false
_current_profile = 0
var profiles := SaveSystem.get_profiles()
if profiles.size() > 0:
_current_profile = int(profiles[0]["id"])
_show_profiles()
visible = true
## 存档模式打开(游戏内,暂停时间,当前组固定高亮)。
## 点槽位 = 回到该进度点;点"新建存档" = 保存当前进度为新槽。
func open_save() -> void:
mode = Mode.SAVE
new_slot_button.visible = true
_current_profile = GameState.profile_id
_was_paused = TimeSystem.paused
TimeSystem.set_paused(true)
_show_profiles()
visible = true
## 关闭面板(恢复打开前的暂停状态)。
func close_panel() -> void:
visible = false
TimeSystem.set_paused(_was_paused)
## 点击"返回"。
func _on_button_esc_button_up() -> void: func _on_button_esc_button_up() -> void:
close_panel()
if mode == Mode.LOAD:
back_pressed.emit() back_pressed.emit()
## 点击"新建存档":在所属组下创建新槽位并刷新列表。
func _on_button_new_slot_button_up() -> void:
if SaveSystem.save_game(_current_profile) != "":
_show_slots()
## 左列:存档组列表(当前组高亮;存档模式下当前组固定)。
func _show_profiles() -> void:
title_label.text = "选择存档(存档模式)" if mode == Mode.SAVE else "选择存档"
_clear(group_container)
var profiles := SaveSystem.get_profiles()
if profiles.is_empty():
var label := Label.new()
label.text = "暂无存档组"
group_container.add_child(label)
_show_slots()
return
for p in profiles:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 6)
var btn := Button.new()
btn.text = "%s %s" % [p["name"], p["last_date"]]
btn.toggle_mode = true
var is_current: bool = int(p["id"]) == _current_profile
btn.set_pressed_no_signal(is_current)
btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
if mode == Mode.SAVE and is_current:
btn.disabled = true
else:
btn.button_up.connect(_on_group_pressed.bind(int(p["id"])))
row.add_child(btn)
# 删除按钮:存档模式下当前组禁用(正在使用)
var del_btn := Button.new()
del_btn.text = ""
del_btn.disabled = mode == Mode.SAVE and is_current
del_btn.button_up.connect(_on_profile_delete_requested.bind(int(p["id"])))
row.add_child(del_btn)
group_container.add_child(row)
_show_slots()
## 请求删除存档组:直接删除并刷新(当前浏览组删除后自动切到第一组)。
## @param profile_id: 存档组 ID
func _on_profile_delete_requested(profile_id: int) -> void:
if not SaveSystem.delete_profile(profile_id):
return
if _current_profile == profile_id:
_current_profile = 0
var profiles := SaveSystem.get_profiles()
if profiles.size() > 0:
_current_profile = int(profiles[0]["id"])
_show_profiles()
## 点击存档组:切换浏览。
## @param profile_id: 存档组 ID
func _on_group_pressed(profile_id: int) -> void:
_current_profile = profile_id
_show_profiles()
## 右列:当前组槽位列表。
func _show_slots() -> void:
_clear(slot_list)
if _current_profile == 0:
return
var slots := SaveSystem.get_slots(_current_profile)
if slots.is_empty():
var label := Label.new()
label.text = "(暂无存档,点上方\"新建存档\"" if mode == Mode.SAVE else "(该组暂无存档)"
slot_list.add_child(label)
return
for i in slots.size():
_add_slot_entry(i + 1, slots[i])
## 添加一条槽位条目。
## @param index: 序号(1 起)
## @param slot: 槽位元数据 {name, date_text, disciple_count, spirit_stones}
func _add_slot_entry(index: int, slot: Dictionary) -> void:
var entry: PanelContainer = SAVE_SLOT_SCENE.instantiate()
slot_list.add_child(entry)
var slot_name := str(slot["name"])
var index_text := "自动" if slot_name == "auto" else str(index)
var name_text := "自动存档" if slot_name == "auto" else "存档 %d" % index
entry.setup(slot_name, index_text, name_text,
"%s 弟子%d" % [slot["date_text"], slot["disciple_count"]],
"灵石 %d" % slot["spirit_stones"])
entry.slot_pressed.connect(_on_slot_pressed)
entry.delete_requested.connect(_on_slot_delete_requested)
## 点击槽位:读档模式进入游戏;存档模式回到该进度点(当前未保存进度丢失)。
## @param slot_name: 槽文件名(不含 .json
func _on_slot_pressed(slot_name: String) -> void:
if mode == Mode.LOAD:
GameState.request_load_game(_current_profile, slot_name)
get_tree().change_scene_to_file("res://src/Core/MainGame.tscn")
return
# 存档模式:直接加载所选槽位(回到该进度点)
if SaveSystem.load_game(_current_profile, slot_name):
close_panel()
## 请求删除槽位:直接删除并刷新(确认弹窗后续接入)。
## @param slot_name: 槽文件名(不含 .json
func _on_slot_delete_requested(slot_name: String) -> void:
if SaveSystem.delete_slot(_current_profile, slot_name):
_show_slots()
## 清空容器。
## @param container: 目标容器
func _clear(container: VBoxContainer) -> void:
for child in container.get_children():
child.queue_free()
+19 -11
View File
@@ -1,7 +1,7 @@
[gd_scene format=3 uid="uid://dj7b5vssydifa"] [gd_scene format=3 uid="uid://dj7b5vssydifa"]
[ext_resource type="Script" uid="uid://boao04d7accm" path="res://src/UI/Panels/SavePanel.gd" id="1_oi6jj"] [ext_resource type="Script" uid="uid://boao04d7accm" path="res://src/UI/Panels/SavePanel.gd" id="1_oi6jj"]
[ext_resource type="PackedScene" uid="uid://dk7ggyngkyt18" path="res://src/UI/Panels/SaveSlot.tscn" id="2_uloko"] [ext_resource type="FontFile" uid="uid://4su41gib4l5u" path="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKaiMono-Medium.ttf" id="2_font"]
[node name="SavePanel" type="PanelContainer" unique_id=619488443] [node name="SavePanel" type="PanelContainer" unique_id=619488443]
anchors_preset = 15 anchors_preset = 15
@@ -19,16 +19,27 @@ theme_override_constants/separation = 16
layout_mode = 2 layout_mode = 2
[node name="TitleLabel" type="Label" parent="VBoxContainer/Header" unique_id=784512345] [node name="TitleLabel" type="Label" parent="VBoxContainer/Header" unique_id=784512345]
unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
theme_override_font_sizes/font_size = 24 theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 28
text = "选择存档" text = "选择存档"
[node name="Spacer" type="Control" parent="VBoxContainer/Header" unique_id=784512346] [node name="Spacer" type="Control" parent="VBoxContainer/Header" unique_id=784512346]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
[node name="Button_NewSlot" type="Button" parent="VBoxContainer/Header" unique_id=794000001]
unique_name_in_owner = true
layout_mode = 2
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 22
text = "新建存档"
[node name="Button_Esc" type="Button" parent="VBoxContainer/Header" unique_id=866430190] [node name="Button_Esc" type="Button" parent="VBoxContainer/Header" unique_id=866430190]
layout_mode = 2 layout_mode = 2
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 22
text = "返回" text = "返回"
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer" unique_id=500577526] [node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer" unique_id=500577526]
@@ -41,21 +52,18 @@ layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 3 size_flags_vertical = 3
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer/HBoxContainer" unique_id=844163531] [node name="GroupContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer/HBoxContainer" unique_id=844163531]
unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
theme_override_constants/separation = 8
[node name="SaveGroup" type="Button" parent="VBoxContainer/ScrollContainer/HBoxContainer/VBoxContainer" unique_id=150349711]
layout_mode = 2
text = "存档1"
[node name="SlotList" type="VBoxContainer" parent="VBoxContainer/ScrollContainer/HBoxContainer" unique_id=674823582] [node name="SlotList" type="VBoxContainer" parent="VBoxContainer/ScrollContainer/HBoxContainer" unique_id=674823582]
unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_stretch_ratio = 5.0 size_flags_stretch_ratio = 5.0
theme_override_constants/separation = 8
[node name="SaveSlot" parent="VBoxContainer/ScrollContainer/HBoxContainer/SlotList" unique_id=214547620 instance=ExtResource("2_uloko")] [connection signal="button_up" from="VBoxContainer/Header/Button_NewSlot" to="." method="_on_button_new_slot_button_up"]
layout_mode = 2
size_flags_vertical = 1
[connection signal="button_up" from="VBoxContainer/Header/Button_Esc" to="." method="_on_button_esc_button_up"] [connection signal="button_up" from="VBoxContainer/Header/Button_Esc" to="." method="_on_button_esc_button_up"]
+29 -26
View File
@@ -1,10 +1,13 @@
## 存档槽位条目(挂载于 SaveSlot.tscn)。 ## 存档槽位条目(挂载于 SaveSlot.tscn)。
## 展示单个存档信息;空槽位用于新建存档 ## 展示单个存档槽位信息,支持点击读档与请求删除
extends PanelContainer extends PanelContainer
## 槽位被点击时发出,save_id 为空表示点击了新建槽位 ## 槽位被点击(读档)时发出。
## @param save_id: 存档 ID ## @param slot_name: 槽文件名(不含 .json
signal slot_pressed(save_id: String) signal slot_pressed(slot_name: String)
## 请求删除该槽位时发出。
## @param slot_name: 槽文件名(不含 .json
signal delete_requested(slot_name: String)
@onready var slot_index_label: Label = %SlotIndexLabel @onready var slot_index_label: Label = %SlotIndexLabel
@onready var save_name_label: Label = %SaveNameLabel @onready var save_name_label: Label = %SaveNameLabel
@@ -13,40 +16,40 @@ signal slot_pressed(save_id: String)
@onready var delete_button: Button = %DeleteButton @onready var delete_button: Button = %DeleteButton
@onready var arrow_button: Button = %ArrowIcon @onready var arrow_button: Button = %ArrowIcon
## 本槽位对应的存档 ID ## 本槽位文件名(不含 .json
var save_id: String = "" var slot_name: String = ""
## 是否已有存档数据false 表示新建槽位) ## 是否已有存档数据。
var has_data: bool = false var has_data: bool = false
## 填充为已有存档槽位。 ## 填充为已有存档槽位。
## @param id: 存档 ID ## @param name: 槽文件名(不含 .json
## @param name_text: 存档名称 ## @param index_text: 序号文本(如 "1"、"自动"
## @param name_text: 槽位显示名(如 "存档 1"、"自动存档"
## @param meta1: 第一行元数据 ## @param meta1: 第一行元数据
## @param meta2: 第二行元数据 ## @param meta2: 第二行元数据
func setup(id: String, name_text: String, meta1: String, meta2: String) -> void: func setup(name: String, index_text: String, name_text: String, meta1: String, meta2: String) -> void:
save_id = id slot_name = name
has_data = true has_data = true
slot_index_label.text = id.get_slice("save_", 1) # "save_001" -> "001" slot_index_label.text = index_text
save_name_label.text = name_text save_name_label.text = name_text
meta_line1.text = meta1 meta_line1.text = meta1
meta_line2.text = meta2 meta_line2.text = meta2
delete_button.visible = true delete_button.visible = true
arrow_button.visible = true arrow_button.visible = true
## 填充为"新建存档"空槽位。 ## 槽位本体被点击(读档/回到进度点)
func setup_empty() -> void:
has_data = false
slot_index_label.text = "+"
save_name_label.text = "新建存档"
meta_line1.text = "点击创建新的存档"
meta_line2.text = ""
delete_button.visible = false
arrow_button.visible = false
## 槽位本体被点击。
func _on_pressed() -> void: func _on_pressed() -> void:
slot_pressed.emit(save_id) slot_pressed.emit(slot_name)
## 点击删除按钮(TODO:接入确认弹窗)。 ## 点击箭头按钮(读档)。
func _on_arrow_button_up() -> void:
slot_pressed.emit(slot_name)
## 根节点点击(左键抬起即触发,覆盖整行)。
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
slot_pressed.emit(slot_name)
## 点击删除按钮:发出删除请求(确认由面板处理)。
func _on_delete_button_button_up() -> void: func _on_delete_button_button_up() -> void:
print("删除存档: ", save_id) delete_requested.emit(slot_name)
+1
View File
@@ -55,3 +55,4 @@ size_flags_horizontal = 3
text = "▶" text = "▶"
[connection signal="button_up" from="HBoxContainer/DeleteButton" to="." method="_on_delete_button_button_up"] [connection signal="button_up" from="HBoxContainer/DeleteButton" to="." method="_on_delete_button_button_up"]
[connection signal="button_up" from="HBoxContainer/ArrowIcon" to="." method="_on_arrow_button_up"]