Compare commits

54 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
Admin 0f1f03f9ef feat: 字体更换为霞鹜文楷(LXGW WenKai,OFL 开源可商用),主题资源同步 2026-08-09 10:14:17 +08:00
Admin c9af0f608e feat: HUD 重构与主题资源——灵米显示、新布局、标签描边阴影、按钮/菜单主题 2026-08-08 23:22:28 +08:00
Admin 543af0a4ff feat: 修炼循环闭环——升层/瓶颈突破(按层概率)+管理界面修为进度+面板放大 2026-08-08 18:16:05 +08:00
Admin 19f8294242 feat: HUD 灵气浓度显示 + 字体大小配置化([ui] 节) 2026-08-08 17:30:18 +08:00
Admin 5ad0272d79 feat: 灵根改为1~5根+容量公式(1÷根数),权重表进配置;文档同步 2026-08-08 17:10:21 +08:00
Admin 8a01806ab0 feat: 游戏场景添加宗门后山背景层 2026-08-08 16:15:46 +08:00
Admin dfd780f3bd feat: 游戏场景添加宗门全景背景图,缩短招募间隔
- MainGame 新增 BG 背景层(宗门全景图,铺满窗口)
- 主菜单背景改为居中缩放模式
- 宗门全景图资源重新压缩优化(12.6MB → 8MB)
2026-08-08 14:54:06 +08:00
Admin b13c20a313 feat: 修炼数值配置化,天灵根规则重构,更新主菜单背景
- GameConfig 新增 [cultivation] 修炼系统配置(基础速度/灵气浓度/资质及灵根倍率/天灵根加成)
- 天灵根改为仅天骄资质再掷(heavenly_root_chance=50%),移入 GameConfig 可配置
- Disciple 修炼倍率常量改为从 GameConfig 读取,新增灵根数量倍率方法
- 主菜单背景更换为"图片大门",新增"宗门全景"资源,删除旧 BG1.png
- 同步更新弟子系统设计文档及新增修炼系统设计文档
2026-08-08 14:32:17 +08:00
Admin e283f6b2b7 style: 详情面板编号改显示名字 2026-08-07 00:00:38 +08:00
Admin 2e447c868b feat: 打开弟子名录面板时暂停时间,关闭恢复 2026-08-06 23:45:24 +08:00
Admin 6b5ec33d85 feat: 弟子名录面板与详情弹窗,完成 D4(管理按钮接线) 2026-08-06 23:42:03 +08:00
Admin 03c765ced8 feat: Disciple 增加入宗时间快照,D4 名录 UI 设计文档 2026-08-06 22:49:29 +08:00
Admin cb9b73dd76 style: 招募面板 UI 调整,TopBar 清理占位控件 2026-08-06 22:17:49 +08:00
Admin cfebb8dded feat: 资质概率按难度配置化,新增普通/困难两档权重
- game_config.cfg 新增 difficulty 档位与 [recruitment_normal]/[recruitment_hard] 权重表
- GameConfig 解析各档权重,get_aptitude_weights() 按当前难度返回
- DiscipleManager 资质判定改读配置权重,删除代码常量
- 设计文档补充配置写法说明(VariantParser 数组字面量陷阱)
- 计划表同步 D3 进度
2026-08-06 00:31:02 +08:00
Admin c5c7c57799 docs: 弟子系统设计更新为弹窗招募机制+配置系统 2026-08-06 00:14:01 +08:00
Admin a3cbd738ad feat: 定时招募弹窗,开局5选3+定期3候选,配置驱动(GameConfig/RecruitPanel) 2026-08-06 00:13:31 +08:00
Admin a00c95af51 docs: 更新开发计划表 D3 完成(定时招募) 2026-08-05 23:50:37 +08:00
Admin df52a0b22f feat: 弟子定时招募+入宗分流,完成 D3(Disciple/DiscipleManager) 2026-08-05 23:50:30 +08:00
Admin cda7a09a09 docs: 添加修仙境界与突破系统设计文档 2026-08-05 22:57:24 +08:00
Admin b1f3d766b3 docs: 更新开发计划表 D2 完成 2026-08-05 22:56:04 +08:00
Admin d907637606 feat: HUD 绑定灵石/声望显示,完成 D2 资源管理 2026-08-05 22:55:57 +08:00
Admin 5e5763028c feat: 新增 SectManager 宗门资源管理单例 2026-08-05 00:09:30 +08:00
Admin 41f50dd070 feat: 主菜单按钮组自适应窗口缩放,增大按钮字体 2026-08-04 00:10:37 +08:00
Admin 123bbf315a style: 顶栏速度按钮改为等宽自适应布局 2026-08-04 00:06:09 +08:00
Admin ee1448a954 docs: 新增核心设计理念设计文档 2026-08-04 00:03:37 +08:00
Admin 559710f765 docs: 更新 README 与开发计划表进度
- README 补充时间系统/用户设置完成状态,更新目录标注
- 开发计划表标记 D1 完成并新增状态列
2026-08-03 22:41:10 +08:00
Admin 1cbfd12574 fix: 场景分组改代码注册,HUD 速度按钮改 toggle 模式
- MainGame 用 add_to_group 注册 game_scene,修复分组未生效导致时间不推进
- 速度按钮改用 toggle_mode + pressed 状态高亮,暂停时禁用
2026-08-03 22:37:55 +08:00
Admin b6c76d83d7 refactor: HUD 节点改用唯一名称引用,场景识别改用分组
- TopBarHUD 用 @onready + % 唯一名称替代硬编码路径
- TimeSystem 用 game_scene 分组替代场景名字符串比较
2026-08-03 22:33:40 +08:00
Admin 476834176c style: HUD 按钮字体统一放大至 32px 2026-08-03 22:31:54 +08:00
Admin 021448a4b0 refactor: TimeSystem 统一速度设置入口
- 新增 apply_default_speed() 封装默认档位应用
- MainGame 改用统一接口,消除双变量手动赋值
2026-08-03 22:29:24 +08:00
Admin 782b1f1701 docs: 按 Godot 规范优化全部 GDScript 注释
- 类/信号/方法添加 ## 文档注释与 @param 标签
- 清理 pass 占位残留
2026-08-03 22:25:43 +08:00
Admin 19f81ceeec feat: TimeSystem 增加月/年信号与日期设置接口
- 新增 month_passed/year_passed 信号,供俸禄与年度结算
- 新增 speed_changed/pause_changed 信号及 set_paused
- 新增 set_date 接口,供读档恢复时间
2026-08-03 22:24:04 +08:00
Admin 0320d98a73 feat: 实现时间流动完整逻辑并同步 HUD 显示
- TimeSystem 支持月/年进位,仅游戏场景推进
- HUD 时间标签订阅 day_passed 实时更新
- 新游戏时重置年月日
2026-08-03 22:20:43 +08:00
Admin 2a39bc6473 feat: HUD 增加时间流速控制,新增 Settings 持久化
- 顶栏改为暂停/1x/5x/20x 按钮组,当前档位高亮
- TimeSystem 支持暂停与档位切换
- 新增 Settings 单例,存档默认流速并开机应用
- GameState 补充年月日,支持 12x30 历法推进
2026-08-03 22:01:35 +08:00
Admin 404480ef00 feat: 新增 TimeSystem 时间系统并注册 Autoload
- project.godot 注册 GameState/TimeSystem 单例
- 存档面板新增快速存档按钮组
- 新增 export_presets.cfg 导出配置
2026-08-03 01:12:16 +08:00
Admin 451f868b83 docs: 新增开发计划表,按作息排期 P1-P3 里程碑 2026-08-02 23:38:04 +08:00
Admin a265e3a5cf docs: 重构 README 为实际工程结构并新增开发计划 2026-08-02 23:26:18 +08:00
Admin d98cbb8266 feat: 实现存档界面与场景切换,优化文字渲染
- 新增 SavePanel/SaveSlot 存档选择界面,支持已有存档与新建存档
- 新增 GameState 单例与 MainGame 入口,按参数区分新游戏/读档
- 主菜单接入中文字体,返回/进入存档面板流程打通
- 开启 2D/3D MSAA、TAA 与屏幕空间 AA,消除放大文字锯齿
- 设置 1920x1080 视口与中文本地化过滤
2026-08-02 23:21:08 +08:00
Admin 72f0a6b8f7 feat: 启动时窗口最大化,UI 跟随缩放 2026-07-30 23:55:13 +08:00
Admin 17994ec4ae refactor: 重构主菜单 UI,将 StartMenu 替换为 MainMenu
- 删除旧 StartMenu.tscn,新建 MainMenu.tscn + MainMenu.gd
- 新增 MainGame.tscn 主游戏场景作为运行入口
- project.godot 配置 run/main_scene 启动场景
- 添加 doc/design/UI/StartMenu.md 主菜单设计文档
- 新增 assets/ 资源目录结构 (audio/fonts/sprites)
- 修复通用资源分析文档中的缩进问题
2026-07-30 23:25:21 +08:00
Admin 226d6992e9 feat: 初始化 UI 框架和资源设计文档
- 新增 TopBarHUD 场景
- 新增开始菜单场景
- 新增通用资源分析设计文档
2026-07-29 23:29:27 +08:00
91 changed files with 5432 additions and 122 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"godotTools.editorPath.godot4": "d:\\game\\Godot\\Godot_v4.7.1-stable_win64.exe\\Godot_v4.7.1-stable_win64.exe"
}
+116 -109
View File
@@ -1,6 +1,8 @@
# 模拟宗门 — 项目目录结构
# 模拟宗门
## 结构总览
基于 Godot 4.7GDScript)的宗门经营模拟游戏。
## 项目结构
```
模拟宗门/
@@ -9,11 +11,13 @@
│ ├── 修仙宗门设计文档.md
│ ├── 宗门行政体系.md
│ ├── 宗门礼仪建筑.md
── 游戏模块总览.md
── 游戏模块总览.md
│ ├── 存档界面UI设计.md
│ └── 开发计划表.md # 开发排期与进度跟踪
├── addons/ # Godot 插件
├── assets/ # 原始资源文件(导入前素材)
├── assets/ # 资源文件
│ ├── sprites/ # 精灵图
│ │ ├── buildings/
│ │ ├── characters/
@@ -22,46 +26,32 @@
│ ├── audio/ # 音频
│ │ ├── bgm/
│ │ └── sfx/
│ └── fonts/ # 字体
│ └── fonts/ # 字体(中文字体已导入)
├── resources/ # Godot 资源文件 (.tres / .res)
│ ├── settings/ # 全局配置
│ ├── buildings/ # 建筑配置表
│ ├── characters/ # 角色模板
│ ├── skills/ # 功法/技能数据
│ ├── items/ # 物品/装备数据
│ └── events/ # 事件配置
├── resources/ # 配置数据 (.tres / .res)
│ ├── settings/
│ ├── buildings/
│ ├── characters/
│ ├── skills/
│ ├── items/
│ └── events/
├── src/ # 源码 + 场景(按功能模块组织
│ │ # .tscn 和 .cs 放在一起)
├── src/ # 源码 + 场景(.gd 与 .tscn 同目录
│ │
│ ├── Core/ # 核心框架
│ │ ├── GameManager.cs # 游戏主循环
│ │ ├── GameManager.tscn
│ │ ├── TimeSystem.cs # 时间系统
│ │ ├── EventBus.cs # 事件总线
│ │ ── SaveSystem.cs # 存档/读档
│ │ └── ConfigLoader.cs # 配置加载
│ │ ├── MainGame.gd # [已实现] 主游戏入口,注册游戏场景分组
│ │ ├── MainGame.tscn
│ │ ├── GameState.gd # [已实现] Autoload,场景参数 + 游戏时间
│ │ ├── TimeSystem.gd # [已实现] Autoload,时间推进/暂停/流速/信号
│ │ ── Settings.gd # [已实现] Autoload,用户设置持久化 (user://)
│ │
│ ├── Data/ # 数据层
│ │ ├── Models/ # 数据模型(DTO)
│ │ └── SO/ # ScriptableObject 定义
│ │ ├── BuildingSO.cs
│ │ ├── SkillSO.cs
│ │ ├── DiscipleSO.cs
│ │ └── EventSO.cs
│ │ ├── Models/
│ │ └── SO/
│ │
│ ├── Sect/ # 宗门管理
│ │ ├── SectManager.cs # 宗门主管理器
│ │ ├── PowerSystem.cs # 权力结构
│ │ ├── FinanceManager.cs # 财政经济
│ │ ├── DiplomacySystem.cs # 外交
│ │ ├── ContributionSystem.cs # 贡献系统
│ │ ├── buildings/ # 建筑系统
│ │ │ ├── BuildingSystem.cs
│ │ │ ├── BuildingBase.cs
│ │ │ ├── BuildingBase.tscn
│ │ │ └── buildings/ # 具体建筑(按分类子目录)
│ ├── Sect/ # 宗门管理(规划)
│ │ ├── buildings/
│ │ │ └── buildings/
│ │ │ ├── admin/ # 行政类
│ │ │ ├── education/ # 教育类
│ │ │ ├── workshop/ # 工坊类
@@ -71,92 +61,109 @@
│ │ │ ├── ritual/ # 礼仪类
│ │ │ └── utility/ # 功能类
│ │ ├── halls/ # 各堂管理
│ │ │ ├── MissionHall.cs # 功德堂/任务堂
│ │ │ ├── TeachHall.cs # 传功堂
│ │ │ ├── LawHall.cs # 执法堂
│ │ │ └── ForeignHall.cs # 外事堂
│ │ └── panels/ # 宗门管理面板场景
│ │ └── panels/
│ │
│ ├── Character/ # 角色系统
│ │ ── Disciple.cs # 弟子类
│ │ ├── Disciple.tscn # 弟子预制场景
│ │ ├── DiscipleGenerator.cs # 弟子生成器
│ │ ├── DisciplePool.cs # 弟子对象池
│ │ ├── Stats.cs # 属性系统
│ │ ├── CultivationSystem.cs # 境界/突破
│ │ ├── SkillSystem.cs # 功法/技能
│ │ ├── EquipmentSystem.cs # 装备系统
│ │ ├── RelationshipSystem.cs # 社交关系
│ │ └── AI/ # 弟子AI
│ │ ├── DiscipleAI.cs
│ │ └── BehaviorNodes.cs
│ ├── Character/ # 角色系统(规划)
│ │ ── AI/
│ │
│ ├── Production/ # 生产制造
│ │ ├── AlchemySystem.cs # 炼丹
│ │ ├── ForgeSystem.cs # 炼器
│ │ ├── TalismanSystem.cs # 符箓
│ │ ├── ArraySystem.cs # 阵法
│ │ ├── PuppetSystem.cs # 傀儡
│ │ ├── FarmingSystem.cs # 种植
│ │ └── BeastSystem.cs # 养殖
│ ├── Production/ # 生产制造(规划)
│ │
│ ├── Combat/ # 战斗系统
│ │ ── BattleManager.cs # 战斗管理器
│ │ ├── BattleField.cs # 战场
│ │ ├── BattleField.tscn
│ │ ├── TurnController.cs # 回合控制
│ │ ├── DamageCalculator.cs # 伤害计算
│ │ ├── SkillEffect.cs # 技能效果
│ │ ├── TeamFormation.cs # 阵型
│ │ └── enemies/ # 敌方配置
│ ├── Combat/ # 战斗系统(规划)
│ │ ── enemies/
│ │
│ ├── World/ # 世界/地图
│ │ ├── MapManager.cs # 地图管理器
│ │ ├── WorldMap.cs # 大地图
│ │ ├── WorldMap.tscn
│ │ ├── SectSite.cs # 宗门驻地
│ │ ├── SectSite.tscn
│ │ ├── ExplorationSystem.cs # 探索系统
│ │ ├── SecretRealm.cs # 秘境
│ │ └── MarketSystem.cs # 市场/交易
│ ├── World/ # 世界/地图(规划)
│ │
│ ├── Events/ # 事件/叙事
│ │ ├── EventManager.cs # 事件管理器
│ │ ├── RandomEvent.cs # 随机事件
│ │ ├── StorySystem.cs # 主线/支线
│ │ ├── TribulationSystem.cs # 天劫系统
│ │ └── SeasonEvent.cs # 赛季活动
│ ├── Events/ # 事件/叙事(规划)
│ │
│ ├── UI/ # 用户界面
│ │ ├── HUD/ # 主界面 HUD
│ │ ├── Panels/ # 功能面板
│ │ │ ── SectPanel.cs # 宗门总览
│ │ │ ├── DisciplePanel.cs # 弟子列表/详情
│ │ ├── BuildingPanel.cs # 建筑详情
│ │ ├── FinancePanel.cs # 财政面板
│ │ ── EventDialog.cs # 事件弹窗
│ │ ├── Controls/ # 自定义控件
│ │ └── Common/ # 通用UI组件
│ │ ├── HUD/ # [已实现] 顶部信息栏(灵石/时间/声望 + 速度控制)
│ │ │ ├── TopBarHUD.tscn
│ │ │ ── top_bar_hud.gd
│ │ └── Panels/ # 功能面板
│ │ ├── MainMenu/ # [已实现] 主菜单(新游戏/读取存档/设置/退出)
│ │ ├── SavePanel.gd # [已实现] 存档选择面板
│ │ ── SavePanel.tscn
│ │ ├── SaveSlot.gd # [已实现] 存档槽位(载入/删除/新建)
│ │ └── SaveSlot.tscn
│ │
│ └── Utils/ # 工具类
│ ├── MathUtils.cs
│ ├── RandomUtils.cs
│ ├── StringUtils.cs
│ └── Extensions.cs
│ └── Utils/ # 工具类(规划)
├── icon.svg
└── project.godot
```
> `[已实现]` = 当前已有代码/场景;未标注目录为规划结构,随开发逐步填充。
## 当前功能进度
| 功能 | 状态 | 说明 |
| ---- | ---- | ---- |
| 主菜单 | 已完成 | 新游戏/读取存档/设置/退出,中文字体 |
| 场景切换 | 已完成 | MainMenu ↔ MainGame,经 GameState 传参 |
| 存档界面 | 已完成 | 槽位动态生成 + 空槽新建,信号驱动 |
| 时间系统 | 已完成 | 12月×30天历法,暂停/1x/5x/20x 流速,日/月/年信号 |
| 用户设置 | 已完成 | 默认流速持久化 (user://settings.cfg) |
| 顶部 HUD | 已完成 | 灵石/时间/声望显示,速度按钮组(toggle 高亮) |
| 画面质量 | 已完成 | 1920x1080 视口,MSAA 2D/3D + TAA,文字抗锯齿 |
| 宗门数据模型 | 已完成 | SectManager:灵石/声望/灵米 + 增减/设定接口(D2) |
| 弟子系统 | 已完成 | 生成/入宗分流/天灵根/列表详情(D3-D4) |
| 修炼循环 | 已完成 | 修为逐日增长/门槛升层/瓶颈判定/13 层圆满(D5) |
| 建筑系统 | 已完成 | 灵石矿/灵田建造升级产出,口粮消耗与断粮离宗(D6) |
| 存档接入 | 已完成 | 两级存档:无限组/槽,读档全量还原(D8) |
| 大境界突破 | 已完成 | 练气圆满手动花费灵石突破至筑基及以上,失败灵石不返还+修为倒退(D9) |
> 进度跟踪见 `Doc/开发计划表.md`,里程碑:M1(8/12) 核心闭环 → M2(9/6) 丰富玩法 → M3(10/11) 正式版本。
## 开发计划
### 下一步:核心循环闭环(P1
目标:跑通"时间推进 → 资源产出 → 弟子修炼"最小循环,让游戏可玩。
| # | 任务 | 说明 | 依赖 |
| - | ---- | ---- | ---- |
| 1 | TimeSystem | 天/月/年推进,挂载 GameState,UI 显示当前日期 | — |
| 2 | 宗门数据模型 | SectManager 单例:灵石/灵气浓度/声望/建筑列表 | — |
| 3 | 建筑系统基础 | 建筑放置、建造/升级耗时、基础产出(灵石/药材) | 1,2 |
| 4 | 弟子系统基础 | 弟子生成(资质/灵根)、入宗分流(亲传/内门/外门/杂役) | 2 |
| 5 | 修炼循环 | 弟子按资质/灵气浓度自动修炼涨修为,境界突破 | 3,4 |
| 6 | 存档接入 | SaveSystem 序列化宗门/弟子/时间数据,替换 SavePanel 测试数据 | 全部 |
### 之后(P2
| 任务 | 说明 |
| ---- | ---- |
| 财政经济 | 收支明细、俸禄发放、市场价格 |
| 任务系统 | 宗门任务发布、贡献值、兑换 |
| 生产制造 | 炼丹/炼器/符箓工坊 |
| 战斗系统 | 弟子组队、外敌入侵事件 |
### 里程碑
- **M1(P1 完成)**:新游戏 → 招弟子 → 挂机产出 → 存档读档,完整闭环
- **M2(P2 完成)**:可体验宗门经营的丰富玩法
- **M3**:战斗/秘境/外交,正式版本
## 运行方式
1. 用 Godot 4.7 打开 `project.godot`
2. F5 运行,主场景为 `MainMenu.tscn`
3. "新游戏"直接进入;"读取存档"在存档面板中选择
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` 默认值再导出(注释已标注同步项)。
## 命名规范
| 类别 | 格式 | 示例 |
| -------- | ------------------ | ----------------------- |
| C# 类 | PascalCase | `SectManager.cs` |
| 场景文件 | PascalCase | `BuildingBase.tscn` |
| -------- | ---------- | ------------------- |
| GDScript | snake_case | `top_bar_hud.gd` |
| 场景文件 | PascalCase | `MainGame.tscn` |
| 信号 | snake_case | `back_pressed` |
| 回调方法 | `_on_` 前缀 | `_on_button_pressed` |
| 资源文件 | snake_case | `iron_sword.tres` |
| 精灵图 | snake_case | `building_office.png` |
| 变量 | camelCase | `currentGold` |
| 私有变量 | _camelCase | `_discipleList` |
> **注意**C# 脚本的 `namespace` 使用 `项目名.模块名`,例如 `SectSim.Sect`、`SectSim.Character`。在 Godot 4 中,脚本路径决定了自动生成的默认命名空间,保持路径与命名空间一致可避免手动配置。
| 精灵图 | snake_case | `bg1.png` |
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://drt448qncx27p"
path="res://.godot/imported/dingliezhuhaifont-20240831GengXinBan)-2.ttf-7b6c32418ce639305b17af2723dddc58.fontdata"
[deps]
source_file="res://assets/fonts/dingliezhuhaifont-20240831GengXinBan)-2.ttf"
dest_files=["res://.godot/imported/dingliezhuhaifont-20240831GengXinBan)-2.ttf-7b6c32418ce639305b17af2723dddc58.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
Binary file not shown.
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://dpw0fmb5wki2h"
path="res://.godot/imported/LXGWWenKai-Light.ttf-ec3765d1a28f599b542e80b43c54944c.fontdata"
[deps]
source_file="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKai-Light.ttf"
dest_files=["res://.godot/imported/LXGWWenKai-Light.ttf-ec3765d1a28f599b542e80b43c54944c.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://cwrccil0e6jgj"
path="res://.godot/imported/LXGWWenKai-Medium.ttf-7874def67b3cdf89ec9f04c454f74b51.fontdata"
[deps]
source_file="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKai-Medium.ttf"
dest_files=["res://.godot/imported/LXGWWenKai-Medium.ttf-7874def67b3cdf89ec9f04c454f74b51.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://ds1gi2of5oydj"
path="res://.godot/imported/LXGWWenKai-Regular.ttf-4b7a37620712eb3c5b0811a13ce812d9.fontdata"
[deps]
source_file="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKai-Regular.ttf"
dest_files=["res://.godot/imported/LXGWWenKai-Regular.ttf-4b7a37620712eb3c5b0811a13ce812d9.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://caf6x1g0netn1"
path="res://.godot/imported/LXGWWenKaiMono-Light.ttf-578c556e63460fe509a3eed7b0e20444.fontdata"
[deps]
source_file="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKaiMono-Light.ttf"
dest_files=["res://.godot/imported/LXGWWenKaiMono-Light.ttf-578c556e63460fe509a3eed7b0e20444.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://4su41gib4l5u"
path="res://.godot/imported/LXGWWenKaiMono-Medium.ttf-9bcb20d0cda8b7e7290f1845c17ae4dc.fontdata"
[deps]
source_file="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKaiMono-Medium.ttf"
dest_files=["res://.godot/imported/LXGWWenKaiMono-Medium.ttf-9bcb20d0cda8b7e7290f1845c17ae4dc.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://ce12uai6ul0hj"
path="res://.godot/imported/LXGWWenKaiMono-Regular.ttf-2931aa1fc087599321bf814a8ef180f6.fontdata"
[deps]
source_file="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKaiMono-Regular.ttf"
dest_files=["res://.godot/imported/LXGWWenKaiMono-Regular.ttf-2931aa1fc087599321bf814a8ef180f6.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
+94
View File
@@ -0,0 +1,94 @@
Copyright 2021-2026 LXGW (https://github.com/lxgw/LxgwWenKai)
Copyright 2020 The Klee Project Authors (https://github.com/fontworks-fonts/Klee)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 MiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c6bthhmao5fyh"
path="res://.godot/imported/图片大门.png-03ea66f8e1eb82ba7952374321f00064.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/ui/图片大门.png"
dest_files=["res://.godot/imported/图片大门.png-03ea66f8e1eb82ba7952374321f00064.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 MiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dnjuv1btbxm4o"
path="res://.godot/imported/宗门全景.png-d969df84bf06c400df6e36c494864bbe.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/ui/宗门全景.png"
dest_files=["res://.godot/imported/宗门全景.png-d969df84bf06c400df6e36c494864bbe.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 MiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cweu17mwblxmg"
path="res://.godot/imported/宗门后山.png-7a9df51075ec2cc8660f9ccd87b9a616.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/ui/宗门后山.png"
dest_files=["res://.godot/imported/宗门后山.png-7a9df51075ec2cc8660f9ccd87b9a616.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
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://b7cfvmuq8m7g3"
path="res://.godot/imported/灵田.png-1999fb337f7e0865f893a16872e15f6b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/ui/灵田.png"
dest_files=["res://.godot/imported/灵田.png-1999fb337f7e0865f893a16872e15f6b.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
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
+124
View File
@@ -0,0 +1,124 @@
# 开始菜单设计
## 一、场景结构
采用**单场景 + 子节点切换**方案,仅一个 `MainMenu.tscn`
```
MainMenu.tscn
├── BG (TextureRect)
│ └── 背景图片
├── Logo (TextureRect)
│ └── 游戏 Logo
├── ButtonGroup (VBoxContainer)
│ ├── 新游戏按钮 → 显示 NewGamePanel
│ ├── 读取存档按钮 → 显示 LoadGamePanel
│ ├── 设置按钮 → 显示 SettingsPanel
│ └── 退出按钮 → get_tree().quit()
├── NewGamePanel (Panel, 默认隐藏)
│ ├── 宗门名称输入框 (LineEdit)
│ ├── 难度选择 (OptionButton)
│ │ ├── 简单
│ │ ├── 普通
│ │ └── 困难
│ ├── 确认按钮 → 进入游戏
│ └── 返回按钮 → 隐藏自身
├── LoadGamePanel (Panel, 默认隐藏)
│ ├── 存档列表 (ItemList)
│ ├── 加载按钮
│ └── 返回按钮
└── SettingsPanel (Panel, 默认隐藏)
├── 音量滑块 (HSlider)
├── 全屏切换 (CheckBox)
└── 返回按钮
```
## 二、场景节点属性说明
### MainMenu(根节点 — Control
| 属性 | 值 | 说明 |
|------|----|------|
| `anchors_preset` | 15Full Rect | 四角锚定父节点边界 |
| `anchor_right` | 1.0 | 右边缘锚定到父节点右边界 |
| `anchor_bottom` | 1.0 | 下边缘锚定到父节点下边界 |
| `grow_horizontal` | 2 | 控件缩小时向右收缩 |
| `grow_vertical` | 2 | 控件缩小时向下收缩 |
| `size_flags_horizontal` | 3 | Fill + Expand,水平填充可用空间 |
| `size_flags_vertical` | 3 | Fill + Expand,垂直填充可用空间 |
### BGTextureRect — 背景)
| 属性 | 值 | 说明 |
|------|----|------|
| `anchors_preset` | 15Full Rect | 铺满父节点 |
| `anchor_right` / `anchor_bottom` | 1.0 | 锚定右下边界 |
| `stretch_mode` | 0(Scale) | 拉伸图片铺满,保持原始比例 |
### LogoTextureRect — 游戏 Logo
| 属性 | 值 | 说明 |
|------|----|------|
| `anchors_preset` | 1Top Center | 顶部居中锚点 |
| `anchor_left` / `anchor_right` | 0.5 | 水平居中 |
| `offset_left` | -200 | 左偏移,使宽度为 400 |
| `offset_right` | 200 | 右偏移 |
| `offset_top` | 60 | 距顶部 60px |
| `offset_bottom` | 260 | 距顶部 260px(高 200 |
| `grow_horizontal` | 2 | 向左收缩 |
### ButtonGroupVBoxContainer — 按钮组)
| 属性 | 值 | 说明 |
|------|----|------|
| `anchors_preset` | 8Center | 居中锚定 |
| `anchor_left / top / right / bottom` | 0.5 | 中心点对齐 |
| `offset_left` | -100 | 向左扩展 100px(总宽 200 |
| `offset_right` | 100 | 向右扩展 100px |
| `offset_top` | -80 | 向上扩展 80px(总高 160 |
| `offset_bottom` | 80 | 向下扩展 80px |
| `grow_horizontal` | 2 | 向左收缩 |
| `grow_vertical` | 2 | 向上收缩 |
| `theme_override_constants/separation` | 16 | 按钮间距 16px |
### Button(子按钮)
| 属性 | 值 | 说明 |
|------|----|------|
| `custom_minimum_size` | Vector2(200, 48) | 按钮最小尺寸,宽 200 高 48 |
| `text` | "新游戏" 等 | 按钮显示文字 |
---
## 三、面板切换逻辑
所有面板放在同一场景内,通过 `visible` 属性切换。
```gdscript
func _on_new_game_pressed():
button_group.hide()
new_game_panel.show()
func _on_back_pressed():
new_game_panel.hide()
button_group.show()
```
## 四、进入游戏流程
新游戏确认后调用:
```gdscript
func _on_confirm_new_game():
var sect_name = $NewGamePanel/NameInput.text
var difficulty = $NewGamePanel/Difficulty.selected
# 初始化游戏数据
GameData.start_new_game(sect_name, difficulty)
get_tree().change_scene_to_file("res://src/main/MainGame.tscn")
```
## 五、注意事项
- 面板切换使用 `show()` / `hide()`,不销毁节点,保留输入状态
- 背景和 Logo 可加淡入动画(AnimationPlayer
- 存档列表数据来源:`ResourceSaver` 序列化的存档文件
+120
View File
@@ -0,0 +1,120 @@
# 修仙资料 — 境界与突破
> 本文档收集、整理修仙体系中与修炼、突破相关的设定资料,供游戏设计参考。
---
## 一、境界总览
| 大境界 | 小境界划分 | 寿元上限 | 修炼参考时长 | 大境界突破需求 |
|--------|-----------|----------|-------------|---------------|
| 凡人 | — | ~80岁 | — | 引气入体即入练气 |
| 练气期 | 一层~十三层 | ~150岁 | 每层 1~3 年 | 筑基丹 + 灵气充沛之地 |
| 筑基期 | 初期 / 中期 / 后期 / 圆满 | ~250岁 | 每小境 10~20 年 | 结丹灵物 + 雷劫(小) |
| 金丹期 | 初期 / 中期 / 后期 / 圆满 | ~600岁 | 每小境 30~50 年 | 元婴丹 / 化婴丹 + 心魔劫 |
| 元婴期 | 初期 / 中期 / 后期 / 圆满 | ~1500岁 | 每小境 50~100 年 | 化神灵物 + 五行劫 |
| 化神期 | 初期 / 中期 / 后期 / 圆满 | ~3000岁 | 每小境 100200 年 | 炼虚感悟 + 天劫 |
| 炼虚期 | 初期 / 中期 / 后期 / 圆满 | ~8000岁 | 每小境 200500 年 | 合体契机 + 道心劫 |
| 合体期 | 初期 / 中期 / 后期 / 圆满 | ~15000岁 | 每小境 5001000 年 | 大乘机缘 |
| 大乘期 | 初期 / 中期 / 后期 / 圆满 | ~30000岁 | 每小境 10003000 年 | 飞升之劫 |
| 渡劫期(真仙劫) | — | 极长 | — | 渡过即飞升 |
---
## 二、大境界突破详情
### 2.1 凡人 → 练气期
- **突破方式**:引天地灵气入体,打通经脉。门槛较低,有灵根者皆可。
- **所需资源**:灵气充裕的环境、基础引气口诀。
- **失败后果**:轻微内伤,修养后可重试。
### 2.2 练气期 → 筑基期
- **突破方式**:压缩灵力、铸就道基,是踏入修仙正途的关键一步。
- **所需资源**
- 筑基丹(必备,提升成功率 ~30% → ~60%)
- 灵石 500~1000(用于布聚灵阵)
- 灵气充沛之地(灵脉节点)
- **突破难度**:高。散修筑基失败率极高。
- **失败后果**:灵力反噬,修为跌落 2~3 层,半年内不可再次尝试。
### 2.3 筑基期 → 金丹期
- **突破方式**:丹田凝聚金丹,需渡过小型雷劫。
- **所需资源**
- 结丹灵物(如千年灵乳、天火液等,依主修方向而定)
- 灵石 30005000
- 渡劫阵旗 / 防御法器(应对雷劫)
- **突破难度**:极高。三成筑基修士止步于此。
- **失败后果**:金丹碎裂,跌落至筑基初期,或身死道消。
### 2.4 金丹期 → 元婴期
- **突破方式**:金丹碎裂、元婴诞生,需对抗心魔劫。
- **所需资源**
- 元婴丹 / 化婴丹(必备)
- 灵石 10000+
- 清心类法宝(抵御心魔)
- 护法弟子或长老
- **突破难度**:极高。心魔劫最为凶险。
- **失败后果**:元婴无法凝聚,修为锐减至金丹初期,或道心崩溃疯癫。
### 2.5 元婴期 → 化神期
- **突破方式**:元婴与天地法则初步融合,渡过五行天劫。
- **所需资源**
- 化神灵物(五行精华各一份)
- 灵石 50000+
- 渡劫大阵
- **失败后果**:天劫之下魂飞魄散,或元婴受创沉睡百年。
### 2.6 化神期及之后
- 化神以上境界所需资源极为稀有,多为天地奇宝,非人力可量产。
- 每层大境界突破均伴随天劫,强度递增。
- 合体期以上几乎只存在于传说中。
---
## 三、小境界修炼与突破
### 3.1 小境界是否需要突破?
| 大境界 | 小境界突破方式 | 说明 |
|--------|---------------|------|
| 练气期 | 积累为主,每三层一个小瓶颈 | 1→2→3 顺畅;3→4 有小瓶颈需冲关;4→5→6 顺畅;6→7 有小瓶颈……依此类推 |
| 筑基期 | 积累 + 小瓶颈突破 | 初期→中期、中期→后期有小瓶颈,需丹药辅助;后期→圆满为积累 |
| 金丹期 | 积累 + 小瓶颈突破 | 同上 |
| 元婴及以上 | 以感悟为主,积累为辅 | 越小境突破依赖法则领悟,资源需求退居次要 |
### 3.2 设计建议
- **练气期**:设为 13 层,部分层数需要"小突破"(消耗少量灵石或丹药),设计为引导玩家熟悉突破机制的教程阶段。
- **筑基及以上**:每个大境界 4 个小境(初期/中期/后期/圆满),均需小突破。小突破资源消耗远低于大境界突破,但失败也会有小幅修为折损。
- **修炼时间**:小境内积累可设为随时间自动增长(类似挂机产出),瓶颈处需要玩家手动"冲关"。
---
## 四、修炼速度影响因素
| 因素 | 影响程度 | 说明 |
|------|---------|------|
| 灵根数量 | ★★★★★ | 灵根越少(如单灵根/天灵根),修炼速度越快 |
| 灵气浓度 | ★★★★ | 灵脉之地 > 普通区域 > 灵气稀薄之地 |
| 丹药辅助 | ★★★★ | 修炼类丹药可大幅加速 |
| 功法品阶 | ★★★★ | 高品功法提升灵力转化效率 |
| 名师指点 | ★★★ | 高阶修士传道可加速感悟 |
| 心境/道心 | ★★★ | 影响瓶颈突破概率 |
| 年龄 | ★★ | 接近寿元上限时修炼效率递减 |
---
## 五、参考资料
- 凡人修仙传(忘语):练气→筑基→金丹→元婴→化神→炼虚→合体→大乘→渡劫
- 遮天(辰东):轮海→道宫→四极→化龙→仙台
- 完美世界(辰东):搬血→洞天→化灵→铭纹→列阵→尊者→神火→真一→圣祭→天神→虚道→斩我→遁一→至尊
- 仙逆(耳根):凝气→筑基→结丹→元婴→化神→婴变→问鼎→阴虚阳实→碎涅→第二步/第三步
> 本作建议采用**凡人修仙传体系**作为基础框架,境界数量适中、认知度高。
+209
View File
@@ -0,0 +1,209 @@
# 修炼系统设计(D5:修炼循环 + D9:大境界突破)
> 对应开发计划表 D5(8/7):① 修为随时间增长(资质×灵气)② 境界门槛 ③ 突破判定;D9(8/11)追加大境界突破。
> 交付标准:弟子自动修炼,可突破;练气圆满后玩家手动花费灵石突破大境界。
> 本文档只描述设计,不包含代码。
> 状态:D5 部分已实现(每日结算 → 修为达门槛升层 → 瓶颈掷骰判定,失败扣 20%;13 层圆满封顶;修为进度已在管理界面展示);大境界突破已实现(练气圆满 → 手动花费灵石 → 筑基及以上大境界,失败灵石不返还 + 修为倒退;配置驱动,详见第四章)
---
## 一、核心循环
```
每日推进 → 修为增长(资质×灵根容量×天灵根×灵气浓度)→ 达到层门槛 → 升层/突破判定 → 境界推进
```
- 纯挂机机制:弟子自动修炼,玩家无需操作
- 修炼只在游戏时间推进时进行(TimeSystem.day_passed),暂停/弹窗期间冻结
---
## 二、修炼速度
**每日修为增量 = 基础速度 × 资质倍率 × 灵根容量倍率 × 天灵根加成 × 灵气浓度**
| 因子 | 来源 | 说明 |
|------|------|------|
| 基础速度 | 配置 `base_speed`(默认 10 | 修为/游戏日 |
| 资质倍率 | 配置 `aptitude_speed`[0.6, 1.0, 1.5, 2.0] | 按下品/中品/上品/天骄索引 |
| 灵根容量倍率 | 固定公式 `1 ÷ 根数`(无配置) | 1根=1.0、2根=0.5、3根=1/3、4根=0.25、5根=0.2 |
| 天灵根加成 | 配置 `heavenly_bonus`1.5 | 仅在 is_heavenly 时乘入 |
| 灵气浓度 | 配置 `qi_concentration`(默认 1.0) | D5 固定值;D6 建筑系统接入后动态化 |
> 所有修炼数值统一从 GameConfig 读取(Disciple 不内置倍率常量),避免双份数值漂移。
### 含义
- **资质定天赋上限**:决定倍率主体(0.6~2.0)
- **灵根数量定灵气容量**:容量 = 1 ÷ 灵根数(硬规则,不提供配置)。单灵根 1 单位灵气即充满(×1.0),双灵根只充 1/2(×0.5),三灵根 1/3,四灵根 1/4,五灵根 1/5,根数越多同一单位灵气分摊越薄
- **天灵根是稀缺加成**:强制单根(满容量)+ 额外 ×1.5,三个维度独立掷骰
- **无灵根(体修路线预留)**:容量为 0,无法练气;暂不生成,待体修系统(炼体)接入后另行结算
组合示例(天骄资质 + 五灵根 = 2.0 × 0.2 = 0.4,慢于 上品资质 + 单灵根 = 1.5 × 1.0 = 1.5
修为存储用 float(允许小数积累),显示时取整。
---
## 三、境界门槛(本期:练气 1–13 层)
| 层 | 需求修为(配置 `level_thresholds` | 是否瓶颈 |
|----|----------------------------------|---------|
| 1→2 | 100 | 否 |
| 2→3 | 200 | 否 |
| 3→4 | 350 | **是** |
| 4→5 | 550 | 否 |
| 5→6 | 800 | 否 |
| 6→7 | 1100 | **是** |
| 7→8 | 1500 | 否 |
| 8→9 | 2000 | 否 |
| 9→10 | 2600 | **是** |
| 10→11 | 3300 | 否 |
| 11→12 | 4100 | 否 |
| 12→13 | 5000 | **是** |
- **非瓶颈层**:修为满门槛 → 自动升层(无失败)
- **瓶颈层**3→4 / 6→7 / 9→10 / 12→13):修为满后进入突破判定
- 13 层圆满:修为封顶,显示"练气圆满";**大境界突破见第四章**(玩家手动花费灵石突破至筑基)
> 数值均为示例,正式数值在 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. 修为 ≥ 瓶颈层门槛时,每日结算后自动尝试突破
2. 掷骰:成功概率**按瓶颈层分别配置**(`breakthrough_chances`,与 `bottleneck_levels` 一一对应:升 4 层 0.85 / 升 7 层 0.8 / 升 10 层 0.75 / 升 13 层 0.6,越往上越难)
3. **成功** → 升层,控制台打印"X 突破至 练气4层"
4. **失败** → 修为扣减 20%(冲关损耗),次日再尝试;控制台打印失败信息
5. 失败不连续重试(每天最多判定一次)
**设计意图**:挂机友好(全自动),但瓶颈制造节奏感与"卡关→等积累"的经营反馈;损耗惩罚避免无脑挂到顶。
---
## 六、CultivationManager(新 Autoload
存放位置:`src/Character/CultivationManager.gd`(与 DiscipleManager 同域,注册顺序在 DiscipleManager 之后、声明于 project.godot
| 接口 | 说明 |
|------|------|
| 监听 `TimeSystem.day_passed` | 每日结算全体弟子 |
| 私有 `_gain_exp(d)` | 按公式累计修为(所有大境界通用;练气圆满/大境界圆满封顶) |
| 私有 `_check_breakthrough(d)` | 练气期门槛判定 + 瓶颈掷骰 |
| 私有 `_check_sub_realm(d)` | 筑基及以上小境门槛判定(自动推进,无失败) |
| **公开 `try_realm_breakthrough(d)`** | **大境界突破入口:校验灵石 → 扣费 → 掷骰 → 成功升大境界/失败修为倒退(`realm_breakthrough_fail_drop` 步),返回 bool** |
| 信号 `realm_changed(d)` | 境界变化(详情面板刷新用) |
| 信号 `cultivation_log(text)` | 修炼日志文本(控制台打印/后续日志系统) |
**注意**:只在 `disciples` 名录内结算,候选池不修炼;大境界突破必须从 UI 手动调用(挂机不自动花灵石)。
---
## 七、配置节(game_config.cfg [cultivation]
```ini
[cultivation]
base_speed=10 ; 基础修为/日
qi_concentration=1.0 ; 宗门灵气浓度(D6 动态化)
aptitude_speed=[0.6, 1.0, 1.5, 2.0] ; 资质倍率(下品/中品/上品/天骄)
heavenly_bonus=1.5 ; 天灵根额外倍率
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 各层所需修为
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 项
- `level_thresholds`:练气 1→2 到 12→13 共 12 个门槛值
- `bottleneck_levels`:升到这些层时需要突破判定
- `realm_breakthrough_costs/chances`:索引 0 = 练气圆满→筑基,1 = 筑基圆满→金丹,2 = 金丹圆满→元婴,3 = 元婴圆满→化神;两个数组长度必须一致(4 项)
- `realm_breakthrough_fail_drop`:失败倒退步数,练气期退层数、筑基+退小境,均不跨大境界
- `realm_sub_thresholds`:筑基及以上通用,3 项;修为达到即自动进小境
---
## 八、UI 接入(D4 预留区填充)
- **名录列表**DisciplesPanel):条目追加"修为"列(如 "1234 / 800",当前修为/本层门槛;13 层显示"练气圆满";大境界圆满显示"筑基圆满")
- **详情弹窗**:信息区追加"修为:1234 / 2000"(当前/本层门槛;练气圆满显示"练气圆满")
- 追加"**大境界突破**"按钮:满足触发条件(修为圆满 + 灵石足够)时可点击;按钮文案带花费("大境界突破(500 灵石)");灵石不足置灰
- 点击后直接执行判定(成功/失败均无二次确认),结果通过修炼日志与控制台可见
- 面板打开时暂停时间,进度静态展示;修炼日志:控制台 print(本期不做日志界面)
---
## 九、实现拆解(小步可验证)
1. 配置节追加 3 项 + GameConfig 读取(默认值与 cfg 同步)
2. Disciple 辅助:`is_realm_ready()`(修为圆满判定)、突破花费查询、进度文本支持筑基及以上
3. CultivationManager`_gain_exp` 放开到筑基及以上 + 小境自动推进 `_check_sub_realm`
4. `try_realm_breakthrough(d)`:校验 → 扣灵石 → 掷骰 → 成功/失败结算
5. 详情面板"大境界突破"按钮(可用性 = 条件 + 灵石;文案含花费)
6. 挂机验证(加速时间观察筑基圆满 → 手动突破金丹)+ 提交
---
## 十、验收清单(D5 交付标准 + D9 追加)
- [x] 挂机时弟子修为逐日增长,速度随资质/天灵根不同(下品慢、天骄快)
- [x] 非瓶颈层修为满自动升层,无失败
- [x] 瓶颈层(4/7/10/13 层)需突破判定,控制台有成败日志,失败扣 20% 修为
- [x] 练气 13 层圆满后修为封顶,显示"练气圆满"
- [x] 详情面板显示修为进度,突破后实时刷新
- [x] 修改配置(速度/概率/门槛)重启生效
- [x] 详情面板常驻"大境界突破"按钮:修为圆满 + 灵石足够可点击(文案显示所需灵石),否则置灰(未圆满/灵石不足/已至巅峰)
- [x] 点击突破:扣灵石 → 成功升大境界(realm_index+1,显示"筑基初期")或失败**修为倒退**(练气退层/筑基+退小境,配置 `realm_breakthrough_fail_drop` 步),控制台有日志
- [x] 灵石不足时按钮置灰不可点击
- [x] 筑基及以上:修为继续增长,满 `realm_sub_thresholds` 自动推进小境(初期→中期→后期→圆满),圆满后可再大境界突破
- [x] 修改配置(花费/成功率/小境门槛)重启生效
> 注:修为显示("当前/本层门槛")待详情面板接入(拆解第 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] 新游戏流程不受影响(仍弹开局招募);多组多槽共存
+212
View File
@@ -0,0 +1,212 @@
# 建筑系统设计(D6:建筑系统基础)
> 对应开发计划表 D6(8/8):① 灵石矿场景 ② 建造→产出 ③ 升级。交付标准:建筑产资源,升级加速。
> 本文档只描述设计,不包含代码。
> 状态:步骤 1–7 已实现(D6 全部完成:食物/开局/配置/建筑模型管理/面板/口粮饥饿离宗),步骤 7 整体挂机验证待 F5 手测
---
## 一、核心循环
```
建造(花灵石)→ 每日产出(灵石/食物)→ 攒资源升级 → 产出加速 → 支撑修炼/招募经济
```
- 纯挂机产出:与修炼/招募同节奏,监听 `TimeSystem.day_passed` 每日结算
- 产出直接进入 SectManager 资源(HUD 实时联动)
---
## 二、数据模型(Building
| 字段 | 类型 | 说明 |
|------|------|------|
| `type` | 枚举 BuildingType | 建筑种类(灵石矿 / 灵田) |
| `level` | int | 等级(1 起) |
价格与产出**不在建筑上存副本**,运行时按 `type + level` 从 GameConfig 计算,杜绝数值漂移(与修炼数值同一原则)。
### 本期建筑种类
| 类型 | 名称 | 产出 | 消费资源 |
|------|------|------|---------|
| SPIRIT_STONE_MINE | 灵石矿 | 灵石/日 | 建造/升级花灵石 |
| SPIRIT_FIELD | 灵田 | 食物/日 | 建造/升级花灵石 |
> 食物为通用资源(顶部栏常驻显示,见通用资源分析文档),需在 SectManager 新增。**HUD 显示名为"灵米"**(内部资源名 `food`)。
>
> **后续建筑规划**:灵药园(产灵草,修炼资源,W5 炼丹材料)——与灵田分工:灵田管口粮(食物),药园管药材(灵草),加新建筑 = 配置加一节 + 枚举加一项 + 产出分发加一行。
> **无地图摆放**:本期建筑统一以面板条目呈现(BuildingsPanel 数据行),灵石矿与灵田同套处理,不建单独场景。地图摆放(建筑实体显示在宗门画面上)属 P2 扩展。
---
## 三、产出机制
- **BuildingManager**(新 Autoload)监听 `day_passed`,遍历已建建筑逐座结算
- 灵石矿 → `SectManager.add_spirit_stones(output)`
- 灵田 → `SectManager.add_food(output)`
- 产出量与等级相关:`产出 = 基础产出 × 升级产出倍率^(等级-1)`
## 三·五、口粮消耗与断粮离宗
灵田产出的食物有真实用途——**练气期弟子每日吃粮**(筑基+辟谷,为将来预留)。
### 断粮后果三方案(选定:方案 B)
按修仙设定,练气期尚未辟谷(引气入体 ≠ 断食),仍需进食;通常筑基期逐渐辟谷、金丹期完全辟谷——"断粮有后果"是成立的。具体后果有三档可选:
| 方案 | 断粮后果 | 特点 |
|------|---------|------|
| **A 饿死** | 断粮累计 N 天(如 30 天)→ 弟子死亡,从名录移除 | 忠于设定,压力最大;但需死亡表现(遗物/坟冢等),范围膨胀 |
| **B 离宗(选定)** | 断粮累计 N 天 → 弟子出走投奔他处,名录移除 | 效果同 A,叙事更软("出走"而非"死亡"),UI/代码最简单,契合宗门经营题材 |
| **C 不致命** | 只修炼效率减半,永远不死 | 最温和,生存压力弱,经济闭环不成立 |
**选定理由**:方案 B 的代码量约等于 A(定时检查 + 名录移除,名录/详情 UI 均为现成),但"弟子出走"比"饿死"更符合宗门经营题材的叙事基调,也避开死亡表现系统的范围膨胀,适合 M1 前落地。
### 机制细则(方案 B
1. **每日消耗**`食物 -= 练气弟子数 × 人均消耗`(配置 `consumption_per_disciple`,默认 1
2. **断粮判定**:食物不足以覆盖全部练气弟子时 → 食物归 0,全体练气弟子**饥饿天数 +1**;食物充足 → 饥饿天数清零
3. **饥饿惩罚**:饥饿期间修炼速度 × `hunger_speed_multiplier`(默认 0.5
4. **离宗**:饥饿天数 ≥ `hunger_threshold_days`(默认 30)→ 弟子**离宗出走**,从名录移除(控制台日志"X 因断粮离宗"
5. 筑基+弟子不消耗口粮(`realm_index != 0` 豁免,为筑基系统预留)
**设计意图**:生存压力驱动经济闭环——断粮 → 修炼放缓 → 弟子出走 → 玩家被迫种灵田/合理经营,而不是让食物纯积累无意义。
---
## 四、建造与升级
| 操作 | 规则 |
|------|------|
| 建造 | 检查灵石 ≥ 建造价 → 扣除 → 建筑入列(等级 1) |
| 升级 | 检查灵石 ≥ 升级价 → 扣除 → 等级 +1 |
| 升级价 | `建造价 × 升级价格倍率^等级`(即升到 2 级花 建造价×倍率) |
| 等级上限 | 配置 `max_level`(默认 10 |
资源不足时:UI 按钮禁用,无其他副作用。
---
## 五、配置节(game_config.cfg 新增)
```ini
[building] ; 建筑系统通用
starting_spirit_stones=500 ; 开局灵石(解决初始资金为 0,新游戏发放)
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 ; 断粮累计天数上限(达到即离宗)
```
> 加新建筑 = 配置加一节 + 枚举加一项 + BuildingManager 产出分发加一行(对照 `[recruitment_*]` 模式)。
---
## 六、UI 建筑面板(BuildingsPanel
- **入口**TopBar"资源"按钮(Button_ResourcesManagement,当前未接线)→ 打开
- 打开面板时**暂停时间**(与弟子名录面板一致,关闭恢复)
- 面板结构(复用青绿弹窗模式):
- 标题"宗门建筑"
- 每种建筑一行:名称 / 等级 / 日产出 + 建造或升级按钮(显示价格,资源不足禁用)
- 底部"关闭"按钮 + 遮罩点击关闭
- 数据驱动:监听 `buildings_changed` 重建列表,打开时主动刷新
---
## 七、分步实现规划(小步可验证,每步独立跑通)
### 步骤 1SectManager 加食物资源 + 开局资源发放 ✅
| 项 | 内容 |
|----|------|
| 改动 | `SectManager.gd``food` 资源 + `add_food/remove_food` + `food_changed` 信号;`GameConfig.gd``[building]` 节开局数量;`MainGame.gd` 新游戏时发放 |
| 验证 | ✅ HUD 显示 500 灵石 / 100 灵米,增减接口调用正确 |
### 步骤 2:HUD 食物显示 ✅
| 项 | 内容 |
|----|------|
| 改动 | `TopBarHUD.tscn` 加"灵米"标签对;`top_bar_hud.gd``food_changed` 信号 + 初始值 |
| 验证 | ✅ 开局 100 灵米显示在顶栏;`add_food` 实时刷新 |
### 步骤 3`[building]` 配置节 + GameConfig 读取 ✅(部分)
| 项 | 内容 |
|----|------|
| 改动 | `game_config.cfg``[building]`/`[building_mine]`/`[building_field]` 节(开局数量、建筑参数、等级上限);`GameConfig.gd` 加对应字段 + `building_params` 字典 + 解析 |
| 验证 | ✅ 无头打印各配置值正确;`[food]` 口粮/饥饿参数待步骤 5 接入 |
### 步骤 4Building 模型 + BuildingManager ✅
| 项 | 内容 |
|----|------|
| 改动 | `src/Sect/buildings/Building.gd`type/level + 价格产出计算,运行时读配置);`src/Sect/BuildingManager.gd`(建造/升级/每日产出/`buildings_changed` 信号);`project.godot` 注册 Autoload |
| 验证 | ✅ 建造扣费入列、每日产出进 SectManager、升级后产出提升(10→15)、灵石不足拒绝 |
### 步骤 5:口粮消耗 + 饥饿惩罚 + 断粮离宗 ✅
| 项 | 内容 |
|----|------|
| 改动 | `Disciple.gd``hunger_days``CultivationManager.gd` 每日先结算口粮(消耗→饥饿累计→离宗移除),`_gain_exp` 乘饥饿倍率 |
| 验证 | ✅ 充足 30 天精确扣减清零;断粮饥饿累计、修为减半;第 30 天离宗(日志"断粮日久,离宗而去"+名录移除);恢复清零 |
### 步骤 6BuildingsPanel UI + "资源"按钮接线 ✅
| 项 | 内容 |
|----|------|
| 改动 | `src/UI/Panels/BuildingsPanel/`(面板场景+脚本,每类型一行:名称/等级/日产出+建造或升级按钮);`TopBarHUD.tscn` 连"资源"按钮 → 新信号 → `MainGame.gd` 打开面板 |
| 验证 | ✅ 无头验证:打开暂停/关闭恢复、建造升级扣费、每日产出;面板尺寸与名录面板一致(1340×819 vs 1340×842 |
### 步骤 7:整体挂机验证 + 提交
| 项 | 内容 |
|----|------|
| 改动 | 无(仅验证) |
| 验证 | 挂机模拟:开局 500 灵石→建灵田→产出覆盖口粮→攒灵石升级→产出加速;断粮场景:不建灵田→30 天弟子离宗;对照八节验收清单逐项勾选 |
> 依赖关系:1→2 顺序执行(HUD 依赖食物资源);3→4 顺序执行(Building 依赖配置);5 依赖 1/3;6 依赖 4;7 为最终验证。
---
## 八、验收清单(D6 交付标准)
- [x] 新游戏开局赠送 500 灵石 + 100 食物(配置可调),HUD 可见
- [x] 点"资源"按钮打开建筑面板(时间暂停,关闭恢复)
- [x] 灵石充足时可建造灵石矿/灵田,扣费入列
- [x] 每日自动产出灵石/食物,HUD 实时增长
- [x] 升级建筑后产出提升(升级倍率生效),价格按倍率递增
- [x] 灵石不足时建造/升级按钮禁用
- [x] 等级达到上限后不可再升级
- [x] 练气弟子每日消耗食物;断粮后饥饿天数累计、修炼速度减半
- [x] 断粮累计 30 天弟子离宗(名录移除 + 日志)
- [x] 食物恢复充足后饥饿清零
- [x] 建筑/饥饿数据为纯数据(可序列化,D8 存档接入时加 to_dict/from_dict
---
## 九、与后续任务衔接
| 后续任务 | 衔接方式 |
|----------|----------|
| D8 存档接入 | Building 纯数据模型直接序列化 |
| W3 财政经济 | 建筑产出进入收支流水;俸禄发放扣灵石 |
| W5 生产制造 | 灵田/灵石矿的弟子匹配度(灵根→效率)接入 |
| 修炼系统 | 后续灵气建筑(灵泉/聚灵阵)动态化 `qi_concentration` |
+293
View File
@@ -0,0 +1,293 @@
# 弟子系统设计(D3:生成 + 入宗分流 / D4:名录 UI)
> 对应开发计划表 D3(8/5)与 D4(8/6)。本文档只描述设计,不包含代码。
> 状态:D3、D4 已实现
---
## 一、数据模型(Disciple
弟子用一条数据记录表示,不挂在场景树上。
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | int | 唯一编号,由 DiscipleManager 自增分配 |
| `name` | String | 随机生成的姓名 |
| `age` | int | 入宗年龄(12~18 |
| `aptitude` | 枚举 Aptitude | 资质(入宗分流结果) |
| `roots` | 枚举数组 SpiritRoot | 灵根列表(普通弟子 1~5 根,互不重复) |
| `is_heavenly` | bool | 天灵根标记(稀有灵根,见枚举二) |
| `join_year` | int | 入宗年份(0 = 未入宗/候选),accept 时快照 GameState |
| `join_month` | int | 入宗月份 |
| `join_day` | int | 入宗日 |
| `realm_index` | int | 大境界索引(0=练气),D5 修炼系统推进 |
| `sub_realm_index` | int | 小境界索引(练气层数-1),D5 修炼系统推进 |
### 枚举一:资质四类(Aptitude
| 值 | 名称 | 修炼速度倍率 | 说明 |
|----|------|-------------|------|
| 0 | 下品 | 0.6x | 根基平庸 |
| 1 | 中品 | 1.0x | 资质平平 |
| 2 | 上品 | 1.5x | 天赋尚可 |
| 3 | 天骄 | 2.0x | 万中无一 |
> 倍率由 D5 修炼循环使用,数值存于 GameConfig(配置 `aptitude_speed`),此处仅为参考展示。
### 枚举二:灵根五行(SpiritRoot
金、木、水、火、土 五种。灵根数量影响灵气容量(容量 = 1 ÷ 根数:单灵根充满、双灵根 1/2、三灵根 1/3……五灵根 1/5,修炼速度按比例折扣),元素种类影响日后生产效率匹配(D6 建筑/职能系统使用),此处只生成不消费。
### 特殊灵根:天灵根
稀有灵根,独立于五行枚举之外,用 `is_heavenly` 标记:
| 项目 | 设定 |
|------|------|
| 出现规则 | **仅天骄资质**Aptitude.HEAVENLY)中再掷一次,非天骄弟子不可能有天灵根 |
| 天骄内概率 | 配置 `heavenly_root_chance`(默认 0.5 = 50%);总体概率 = 天骄概率 × 天骄内概率(普通难度 5% × 50% ≈ 2.5% |
| 灵根构成 | 仅一系灵根(五行之一),纯净单灵根 |
| 修炼速度 | 在资质倍率基础上**额外 +50%**(乘法叠加,数值存于 GameConfig `heavenly_bonus`D5 修炼循环消费) |
| 地位 | 宗门争抢的种子弟子,日后可挂靠特殊培养事件(W8 预留) |
> 天灵根与资质是**嵌套关系**:先掷资质,天骄之后再判天灵根。天灵根 = 天骄 + 单灵根 + 1.5x 加成,是最稀缺组合。
### 境界字段说明
新弟子统一为练气一层(realm_index=0, sub_realm_index=0)。境界显示文本规则:
- 练气期:练气 N 层
- 筑基及以上:大境界 + 初/中/后/圆满
境界的成长、突破门槛、寿元计算全部留给 D5 修炼循环,D3 只负责初始状态。
### 入宗时间字段说明
- `join_year/month/day` 在**入宗瞬间**accept_applicant)快照 GameState 当前日期
- 候选池弟子不设(0),拒绝入宗不产生记录
- 本期只存不消费;入门天数换算(D5 修炼结算)、按入宗排序(列表)、资历事件(W8)后续使用
---
## 二、生成规则(随机性)
### 1. 姓名
- 姓氏池:32 个常用修仙风单姓(林、苏、叶、萧、顾、沈、楚、白……)
- 名用字池:24 个(清、玄、墨、尘、风、澜、霜、珏……)
- 规则:随机 1 个姓 + 1~2 个字(50% 概率双字名)
- 不保证唯一(同名弟子可共存,以 id 区分)
### 2. 年龄
12~18 岁均匀随机。寿元消耗、大限判断留给 D5。
### 3. 灵根
- 普通弟子:数量按配置权重随机(`root_count_weights`,1~5 根),元素从五行中随机抽取、概率均等、互不重复
- 天灵根:仅天骄资质再判(概率 `heavenly_root_chance`),灵根数量固定 1 根,修炼速度额外 +50%
- 含义:单灵根修炼最快,五灵根最慢(对应修仙世界观)
- 无灵根暂不生成(体修路线预留,见修炼系统设计)
### 4. 资质(入宗分流)
按权重表掷骰,权重**随难度档位变化**(配置驱动):
| 难度 | 下品 | 中品 | 上品 | 天骄 |
|------|------|------|------|------|
| 普通(normal | 45 | 35 | 15 | 5 |
| 困难(hard | 60 | 30 | 8 | 2 |
- 当前难度由配置 `[recruitment] difficulty` 指定("normal"/"hard"
- 权重表在 `[recruitment_normal]` / `[recruitment_hard]` 节,INI 数组字面量写法:`aptitude_weights=[45, 35, 15, 5]`
- 改配置即调难度,后续可加更多档位(代码字典加一项 + 配置加一节)
---
## 三、入宗分流
"入宗分流"的含义:**按资质将弟子分为四类**,这是生成时的一次性判定,不是持续运行的机制。四类资质弟子日后可能享受不同的培养路线(D4+ 扩展),本期只完成分类本身。
### 招募机制:定时弹窗招募
招募**不是玩家随时点按钮**,而是"生成候选 → 暂停时间 → 弹窗选择":
- **开局招募**:新游戏开始,生成 **5 名候选**,暂停时间弹窗,**必须选满 3 名**才能确定
- **定期招募**:每满间隔(配置文件 `recruit_interval_years`,当前 0.25 年 = 90 天)生成 **3 名候选**,暂停时间弹窗
- **选择方式**:每个候选独立勾选"入宗",可全部接受、可全部拒绝(定期招募可 0~3 个)
- **结算**:点"确定" → 勾选的入宗、未勾选的拒绝 → 关闭弹窗 → **恢复时间**
- 所有参数(候选数、必选数、间隔)来自 `resources/game_config.cfg`,改配置即改难度
### 招募流程(文字时序)
1. 开局(MainGame 调用 `start_new_game()`)或倒计时归零(`_on_day_passed`
2. 生成候选弟子(按权重掷资质 → 天骄再判天灵根 → 生成灵根 → 组名字),进入候选池
3. `applicants_generated(is_opening)` 信号发出 → 弹窗显示候选列表
4. 玩家勾选(开局模式:确定按钮在选满 3 名前禁用)
5. 点"确定" → `finish_recruitment(selected)` 结算 → 恢复时间
6. 控制台打印每位入宗弟子完整信息(姓名/年龄/资质/灵根/境界)—— 本期验收方式
---
## 四、DiscipleManager 接口
| 接口 | 签名 | 说明 |
|------|------|------|
| `start_new_game()` | 无 | 清空全部数据 → 开局候选(配置数量)→ 暂停时间 |
| `generate_applicants(count, is_opening)` | 返回无 | 清空候选池 → 生成 count 名候选 → 发信号 |
| `accept_applicant(d)` | 入参候选 | 分配 ID,候选池 → 名录,发信号,控制台打印 |
| `reject_applicant(d)` | 入参候选 | 从候选池移除 |
| `finish_recruitment(selected)` | 入参选中列表 | 结算:selected 内的入宗,其余全部拒绝 |
| `remove_disciple(d)` | 入参弟子 | 移出名录(逐出/陨落) |
| `reset()` | 无 | 清空名录与候选池、ID 与倒计时复位 |
| 信号 `disciples_changed` | 无参数 | 名录增删/重置时发出 |
| 信号 `applicants_generated(is_opening)` | bool | 候选生成时发出(弹窗监听) |
内部私有方法(不对外):生成姓名、生成灵根(含天灵根判定)、掷资质、创建候选、间隔换算。
内部定时机制:
- 字段 `_days_until_recruit`:距下次定期招募的剩余游戏天数
- 间隔 = `GameConfig.recruit_interval_years` × 360 天(TimeSystem 常量换算)
- 监听 `TimeSystem.day_passed`,每日减 1,归零 → 生成候选 → `TimeSystem.set_paused(true)`(弹窗期间时间冻结)
存放位置:`src/Character/`(与数据模型同目录),注册为 Autoload 单例,供全局访问。
## 四·五、配置系统(GameConfig
所有可调参数集中在 `resources/game_config.cfg`,由 `src/Core/GameConfig.gd`Autoload,声明在 DiscipleManager 之前)启动时读取,缺项回退代码默认值。
```ini
[recruitment]
difficulty="normal" ; 当前难度档位
opening_applicant_count=5 ; 开局候选人数
opening_pick_count=3 ; 开局必须选满数
recruit_interval_years=0.25 ; 定期招募间隔(游戏年)
recruit_applicant_count=3 ; 定期弹窗候选人数
heavenly_root_chance=0.5 ; 天灵根概率:仅天骄资质中再掷,50%
root_count_weights=[20, 20, 20, 20, 20] ; 灵根数量权重(1根~5根,和=100 即百分比概率)
[recruitment_normal] ; 普通难度资质权重(下品/中品/上品/天骄)
aptitude_weights=[45, 35, 15, 5]
[recruitment_hard] ; 困难难度资质权重
aptitude_weights=[60, 30, 8, 2]
```
> 注意:ConfigFile 用 VariantParser 解析值,**数组必须写 `[45, 35, 15, 5]` 字面量**,字符串值必须加引号(如 `difficulty="normal"`);裸写 `45,35,15,5` 会被截断为 45 并破坏后续节解析。
> 以后所有可调参数(天赋权重、价格、境界门槛等)统一加到这里。
---
## 五、UI 接入点
- **RecruitPanel**`src/UI/Panels/RecruitPanel/`):弹窗场景,挂在 MainGame 下(默认隐藏)
- 监听 `applicants_generated` 信号自动弹出
- 每个候选一行:姓名/年龄/资质(天灵根标记)/灵根/境界 + "入宗"勾选框
- 底部:全部接受 / 全部拒绝 / 确定
- 开局模式确定按钮在选满 3 名前禁用;确定后关闭弹窗并恢复时间
- 名录的可视化列表见**第七节(D4 弟子名录 UI)**
---
## 六、与后续任务衔接
| 后续任务 | 衔接方式 |
|----------|----------|
| D4 弟子列表 UI | 监听 `disciples_changed` 信号 + 遍历 `disciples` 数组 |
| D5 修炼循环 | 消费 `aptitude` 倍率与 `realm_index/sub_realm_index` 字段 |
| D6 建筑/职能系统 | 消费 `roots` 元素与生产效率匹配 |
| D8 存档接入 | Disciple 数据模型为纯数据,可直接序列化 |
---
## 七、弟子名录 UID4
> 对应开发计划表 D4(8/6):① 列表条目场景 ② 姓名/资质/境界显示 ③ 点击详情。交付标准:列表可浏览弟子。
### 1. 入口
- TopBar 顶部栏"**管理**"按钮(Button_StaffManagement)→ 打开弟子名录面板
- 打开面板时**暂停时间**(管理操作不打断挂机节奏,避免浏览时弟子入宗/时间流逝;关闭面板恢复推进)
### 2. 面板结构(两层弹窗,复用青绿仙侠配色)
**第一层:名录列表面板(DisciplesPanel**
| 元素 | 说明 |
|------|------|
| 标题 | "弟子名录(N 人)"N = 当前名录人数 |
| 列表区 | ScrollContainer 动态条目,一个弟子一行,按入宗顺序 |
| 条目内容 | 姓名 / 资质(含天灵根标记)/ 灵根 / 境界 / **修为进度** + "详情"按钮 |
| 空状态 | 名录为空时显示"暂无弟子"提示 |
| 底部 | "关闭"按钮(也可点遮罩关闭) |
**第二层:弟子详情弹窗(DiscipleDetailPanel**
| 元素 | 说明 |
|------|------|
| 标题 | 弟子姓名 + 天灵根标记 |
| 信息区 | 静态展示:名字 / 资质 / 年龄 / 灵根 / 境界 / **修为进度** / **入宗时间** |
| 预留区 | 修炼进度、属性面板等 D5+ 扩展位置(本期留白) |
| 底部 | "关闭"按钮 |
### 3. 数据驱动
- 面板监听 `DiscipleManager.disciples_changed` → 重建列表
- 打开面板时也主动刷新一次(保证与名录同步)
- 条目与弟子数据一一对应:动态生成时把 Disciple 引用绑定到条目上,点"详情"直接取用
### 4. 文件与场景结构
```
src/UI/Panels/DisciplesPanel/
├── DisciplesPanel.tscn # 名录列表弹窗
├── disciples_panel.gd
├── DiscipleDetailPanel.tscn # 弟子详情弹窗
└── disciple_detail_panel.gd
```
- 两个弹窗都挂在 MainGame 下(默认隐藏),详情弹窗作为列表面板的子层(显示时盖在列表之上)
- 条目在代码中动态生成(与 RecruitPanel 同套路),不单独做条目场景(D4 拆解的"列表条目场景"用动态行实现即可)
- TopBar"管理"按钮在 `top_bar_hud.gd` 接线,调 `DisciplesPanel.show_panel()`
### 5. 交互细节
- 详情弹窗从列表面板打开;关闭详情回到列表;关闭列表回到游戏
- 列表浏览期间名录变化(如定期招募入宗)→ 列表实时刷新
- 资质文字可用颜色区分(天骄金色、上品亮青),其余默认——视觉提示,本期可选
---
## 八、验收清单(D4 交付标准)
- [ ] 点顶部"管理"按钮弹出弟子名录面板,显示当前已入宗弟子
- [ ] 列表每行显示:姓名 / 资质(天灵根有标记)/ 灵根 / 境界
- [ ] 名录为空时显示"暂无弟子";招募入宗后列表实时刷新
- [ ] 点条目"详情"弹出详情弹窗,显示名字/资质/年龄/灵根/境界/入宗时间
- [ ] 关闭详情回到列表,关闭列表回到游戏,无报错
- [ ] 打开面板期间时间**暂停**,关闭面板后时间恢复推进
---
## 九、与后续任务衔接(D4 补充)
| 后续任务 | 衔接方式 |
|----------|----------|
| D5 修炼循环 | 详情弹窗预留区填充:修为进度、突破按钮、寿元 |
| D6 建筑/职能系统 | 列表条目可扩展"职位/职能"列 |
| D8 存档接入 | 名录数据来源不变,仅序列化方式接入 |
---
## 十、验收清单(D3 交付标准)
- [ ] 项目能正常启动,无脚本报错
- [ ] **开局**:进入游戏即暂停,弹出"弟子入宗(开局招募)",显示 5 名候选
- [ ] 开局模式确定按钮在选满 3 名前不可用;选满后可确定
- [ ] 确定后:勾选的 3 名入宗(控制台打印),时间恢复推进
- [ ] **定期**:每 90 游戏日(0.25 年 @1x ≈ 45 秒)再次暂停弹窗,显示 3 名候选
- [ ] 定期模式可勾选 0~3 名,全部拒绝也可确定;时间正常恢复
- [ ] 多次招募后能观察到天灵根弟子(仅天骄中再判 50%,总体概率约 2.5%,弹窗/控制台有标记)
- [ ] 修改 `game_config.cfg``opening_applicant_count` 等参数,重启后开局候选数随之变化
- [ ] 返回主菜单再进入,名录已清空(reset 生效)
+222
View File
@@ -0,0 +1,222 @@
# 核心设计理念
## 一、游戏概述
### 1.1 游戏性质
一款独立单机模拟经营游戏,管理修仙宗门从零起步。玩法参考《造化仙门》。
## 二、弟子系统
### 2.1 核心循环
**修炼循环(个人成长)**
资源投入(灵气、丹药、时间) → 修为提升 → 突破瓶颈 → 境界晋升 → 属性/能力提升 → 解锁更高效的资源获取方式。
**宗门循环(组织成长)**
弟子晋升 → 分配至高阶生产建筑 → 产出更高级资源(灵石、丹药、材料) → 资源反哺弟子修炼 → 培养更多高阶弟子。
**关键约束**
境界必须与生产效率强绑定(如筑基期才能炼丹,金丹期才能教导他人),形成无法跳过的正向螺旋。
### 2.2 弟子设计要点
1. **职能**(任选 1 至 2 个):种植(灵米、灵蔬、灵草)、养殖(灵兽、灵禽)、炼丹、阵法、符箓、炼器
2. **主修方向**:炼丹、符箓、阵法、炼器、练剑、炼体、御兽、法术
3. **灵根属性**:影响修习方向和生产效率。例如木灵根适合种植灵草,火灵根适合炼丹。多种灵根会有更多选择(类似《缺氧》中的人物词条)
4. **属性匹配**:弟子属性与职能/建筑的匹配度决定产出品质和效率,这是生产系统的核心深度,替代复杂的多级生产链
5. **灵根数量**:影响修炼速度(灵根越多修炼越慢)
## 三、宗门发展
### 3.1 宗门财政
灵石的收支平衡。
### 3.2 宗门影响力
影响招收弟子的资质和备选弟子的数量。
## 四、经济系统
### 4.1 经济波动系统
- **"活"的市场**:物价随事件、季节、玩家大宗交易而波动
- **安全阀设计**:价格上下限、战略库存、替代资源、自给自足等工具,防止经济崩溃
### 4.2 收支结构
**收入来源**
- 开采灵石矿
- 出售基础药材、丹药等
- 部分事件提供
**支出项目**
- 弟子修炼开销
- 购买修炼资源
- 部分事件消耗
**价格波动**
修炼资源的价格有一定波动,大型事件来临前会有预兆性变化。
## 五、事件与命运设计
### 5.1 事件发生流程
单个事件分四个阶段逐步展开:
1. **传闻阶段**:弟子闲聊、商人对话、日志记录埋下伏笔。玩家闻到风声但不清楚细节
2. **波动阶段**:相关物资价格异常涨跌,作为事件的"脉搏"。玩家可据此预判并提前准备(囤货/抛售)
3. **事件发生**:传闻成真,弹出选择界面,玩家做出决策,世界状态改变,价格进入新平衡
4. **恢复阶段**:事件影响随时间消退,物价回归正常区间,损伤建筑修复,异常状态解除。事件是临时波动而非永久改变
**事件可操作的子系统**
- 宗门灵石增减、声望涨跌
- 建筑耐久损伤或强化
- 弟子状态变更(中毒、受伤、顿悟、突破概率增减)
- 资源物价波动、物资库存增减
- 新任务/新地点解锁
- 后续事件标记(设置延迟触发的连锁事件)
### 5.2 事件链
单个事件的选择不只是"这次的结果",还会影响后续会触发什么事件。
```
事件A
├── 选择1 → 标记事件B1(如"妖兽入侵"
│ ├── B1中选择1 → 事件C1"结盟"
│ └── B1中选择2 → 事件C2"决战"
├── 选择2 → 标记事件B2(如"魔修来访"
│ └── B2中……
└── 选择3 → 无后续事件(该分支终止)
```
**延迟后果的数据结构**
```
事件标记 = {
解锁的事件ID,
触发条件:固定时间(如"10年后")或条件触发(如"声望>500且已有灵石矿"),
来自前序事件的选择ID(用于判断是否该触发此分支),
是否必须触发(主线)还是可错过(支线)
}
```
事件调度器不只按日历时间触发,也检查前置条件是否满足。分支树建议控制在 **2-3 层深度**,避免组合爆炸和制作成本失控。
### 5.3 选择设计
**短期利益 vs 长期代价**
将重大事件设计为"借贷未来"的选择,而非随机奖惩。
**概率与确定的分配**
- **确定的选择**:用于决定"路线与代价"(道途、契约),构建个人传奇
- **概率的波动**:用于日常运营的"效率与节奏"(炼丹品质、挖矿暴击),提供惊喜与心跳
**化解 SL 冲动的设计**
- **后果是自选的**:重大失败源于贪婪或战略误判,而非运气
- **失败是另一种开始**:为失败设计后续事件,使其成为故事转折点
- **沉没成本真实存在**:几十年的经营记忆,让玩家舍不得用读档抹去
- **外部世界的因果**:见证"邻居的兴衰",获得超越个人得失的叙事满足感
### 5.4 示例:天道契约
天道契约非一次性的二元选择,而是横跨数十年的多阶段决策链。
**触发**
某年某日,天道降下契约,宗主面临抉择:接受恩赐或拒绝。
**接受恩赐路径**
自此每隔数年,玩家可向天道索取一次恩赐(修为暴涨、灵石暴增、天材地宝等),但每次索取都会增加隐藏的"代价"值。代价值越高,最终清算越猛烈。玩家也可主动选择"克制不索取"以控制代价积累速度。清算的时间与规模皆由历次索取的总和决定,而非预设的固定死期。反击的准备窗口也由玩家此前的积累(弟子、经济、盟友)决定。
**拒绝恩赐路径**
拒绝后,邻居逐渐崛起成为威胁。每隔数年触发一次互动事件:
- 选择打压 → 削弱邻居,但消耗自身资源
- 选择合作 → 短期互利,但邻居成长更快
- 选择袖手 → 不投入成本,但邻居威胁加大
数十年来,每一次选择的叠加决定邻居的最终命运和玩家为此付出的代价。即使邻居最终崩溃,玩家可能也因长期打压而元气大伤。
**共通原则**
- 不存在"攒足资源一次通关"的完美策略,每次决策都是短期收益与长期代价的权衡
- 最终结局由数十次决策而非一次选择决定
- 每个决策本身都有即时后果(获得/失去),不需等待最终清算才有反馈
## 六、境界体系
*(待补充)*
修为境界的层级划分(练气→筑基→金丹→元婴→化神→……),各境界的突破条件、属性阈值、寿命上限、能力解锁。
## 七、建筑系统
*(待补充)*
宗门建筑的分类(生产类、修炼类、功能类、防御类),建造条件与升级路线。生产链较短(原料 → 成品),深度由弟子属性与建筑的匹配度决定,参考《造化仙门》。
## 八、物品与资源体系
*(待补充)*
资源分类(灵石、丹药、材料、法器、功法等),品质分级。生产链较短,原料直接加工为成品,品质受弟子属性影响。
## 九、功法与技能树
*(待补充)*
功法类型与品阶,弟子修习条件与成长路线,技能解锁机制。
## 十、时间系统
*(待补充)*
时间流速单位(年/月/日),季节变化对生产与事件的影响,建筑生产周期,事件触发频率。
## 十一、玩家角色
*(待补充)*
宗主的修炼路线,专属能力与限制,决策范围与操作边界。
## 十二、人际关系
*(待补充)*
弟子间的羁绊与矛盾(师徒、道侣、同门),好感度机制,社交事件对效率与事件的影响。
## 十三、外部势力
*(待补充)*
其他宗门、散修、妖兽势力、商会的系统化设计,外交、贸易、竞争关系。
## 十四、战斗系统
*(待补充)*
是否包含战斗模块,战斗形式(自动结算 / 战术操作),PVE(妖兽、秘境),无 PVP 内容。
## 十五、秘境与探索
*(待补充)*
弟子外出历练的机制,可探索区域的地图/节点设计,收益与风险。
## 十六、任务系统
*(待补充)*
日常任务、宗门任务、奇遇任务的触发机制与奖励结构。
## 十七、胜利与失败条件
*(待补充)*
游戏终局条件(跻身顶级宗门 / 飞升 / 被灭门 / 无限经营),多结局分支。
+117
View File
@@ -0,0 +1,117 @@
# 通用资源分析
根据展示方式,资源分为两大类:
- **通用资源** → 顶部栏(TopBarHUD)常驻显示
- **稀有资源** → 打开资源面板(ResourcePanel)查看
---
## 一、通用资源(顶部栏常驻显示)
日常使用频繁、数值变化快的核心资源。
| 资源 | 说明 | 用途 |
|------|------|------|
| 灵石 | 修仙界通用货币 | 交易、发俸禄、购买物资 |
| 食物 | 弟子日常消耗 | 农场、种植、采购 |
| 木材 | 基础建材 | 伐木场、探索 |
| 石材 | 基础建材 | 采石场、探索 |
---
## 二、稀有资源(面板查看)
无需常驻显示,打开面板浏览。
| 分类 | 资源 | 说明 |
|------|------|------|
| 修炼 | 灵草、丹药、功法卷轴 | 炼丹、突破、学习 |
| 锻造 | 凡矿、灵矿、兽皮/骨 | 打造法器、制甲制符 |
| 特殊 | 声望、气运、功德 | 外交、事件、渡劫 |
> **贡献度** 不属于全局资源,属于**弟子个人属性**,在弟子详情面板中查看。
---
## 三、资源分类
### 1. 货币类(Currency
| 资源 | 说明 | 用途 |
|------|------|------|
| 灵石 | 修仙界通用货币 | 交易、发俸禄、购买物资 |
### 2. 基础资源(Basic
| 资源 | 说明 | 来源 |
|------|------|------|
| 食物 | 弟子日常消耗 | 农场、种植、采购 |
| 木材 | 基础建材 | 伐木场、探索 |
| 石材 | 基础建材 | 采石场、探索 |
| 水源 | 生活必需 | 灵泉、水渠 |
### 3. 修炼资源(Cultivation
| 资源 | 说明 | 用途 |
|------|------|------|
| 灵气 | 环境灵气浓度 | 修炼速度加成 |
| 灵草 | 低阶草药 | 炼丹材料 |
| 丹药 | 成品丹药 | 突破、恢复、增益 |
| 功法卷轴 | 修炼功法 | 弟子学习 |
### 4. 锻造资源(Crafting
| 资源 | 说明 | 用途 |
|------|------|------|
| 凡矿 | 普通矿石 | 打造基础器具 |
| 灵矿 | 蕴含灵力的矿石 | 炼制法器 |
| 兽皮/骨 | 妖兽材料 | 制甲、制符 |
### 5. 特殊资源(Special
| 资源 | 说明 | 用途 |
|------|------|------|
| 声望 | 宗门知名度 | 收徒吸引力、外交 |
| 气运 | 宗门气运值 | 影响随机事件 |
| 功德 | 天道功德 | 渡劫加成、天材地宝 |
---
## 四、资源属性设计
每个资源实例应包含以下属性:
- `id`: 唯一标识
- `name`: 显示名称
- `icon`: 图标路径
- `max_stack`: 最大堆叠
- `display_type`: 展示方式(`TOPBAR` / `PANEL`
- `category`: 所属分类
- `rarity`: 稀有度(普通/稀有/传说)
```gdscript
enum DisplayType {
TOPBAR, # 顶部栏常驻
PANEL # 面板查看
}
enum ResourceCategory {
CURRENCY, # 货币
BASIC, # 基础
CULTIVATION, # 修炼
CRAFTING, # 锻造
SPECIAL # 特殊
}
```
---
## 五、资源管理系统建议
- 使用 `ResourceData` 资源文件(Scriptable Object 模式)定义每个资源
- 使用 `ResourceManager` 单例统一管理增减
- 支持 emit 信号,UI 自动响应数值变化
---
## 六、后续可扩展资源
- 灵兽蛋 / 灵兽材料
- 阵法材料(阵旗、符石)
- 秘境钥匙 / 地图碎片
- 宗门特产(专属资源)
+70
View File
@@ -0,0 +1,70 @@
# 开发计划表
> 开始日期:2026-08-03(周一) 每日有效开发约 2 小时
> 进度更新:2026-08-10  D1–D9 全部完成(D8 两级存档系统验收 9/9,D9 大境界突破 + 数值打磨),M1 验收(D10)进行中
## 一、每日作息模板(工作日)
| 时间 | 事项 | 备注 |
| ---- | ---- | ---- |
| 20:30 | 下班 | |
| 20:3021:00 | 通勤 / 晚饭 | |
| 21:0021:10 | 热身 | 回顾昨日进度,明确今晚目标 |
| 21:1022:40 | **开发黄金段** | 90 分钟,写代码/搭场景 |
| 22:4022:50 | 休息 | 离开屏幕 |
| 22:5023:20 | 收尾 | 跑通验证、git 提交、写明日 TODO |
| 23:2024:00 | 洗漱放松 | 睡前不刷代码 |
## 二、第 12 周(8/38/12):P1 核心循环
| 日期 | 天 | 任务 | 拆解 | 交付标准 | 状态 |
| ---- | -- | ---- | ---- | -------- | ---- |
| 8/3 一 | D1 | TimeSystem | ① 日期变量(年月日) ② tick 推进 ③ 暂停/加速 | HUD 显示年月日,可调速度 | ✅ 完成(含月/年信号、Settings 持久化) |
| 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/6 四 | D4 | 弟子列表 UI | ① 列表条目场景 ② 姓名/资质/境界显示 ③ 点击详情 | 列表可浏览弟子 | ✅ 完成(管理按钮入口,名录面板+详情弹窗,天灵根标记,打开面板暂停时间) |
| 8/7 五 | D5 | 修炼循环 | ① 修为随时间增长(资质×灵气) ② 境界门槛 ③ 突破判定 | 弟子自动修炼,可突破 | ✅ 完成(修为逐日增长/门槛升层/瓶颈按层概率判定/13层圆满封顶/修为进度显示,配置驱动) |
| 8/8 六 | D6 | 建筑系统基础 | ① 灵石矿场景 ② 建造→每时辰产出 ③ 升级 | 建筑产灵石,升级加速 | ✅ 完成(步骤1-7:食物/开局赠送/建筑参数配置/Building+BuildingManager/建筑面板+资源按钮/口粮饥饿离宗方案B,验收11/11) |
| 8/9 日 | D7 | 休息 / 补漏 | 修本周 bug,整理代码 | git 干净 | ✅ 完成(修复:管理面板关闭冲掉手动暂停) |
| 8/10 一 | D8 | 存档接入 | ① SaveSystem JSON 序列化 ② 存/读接口 ③ 替换 SavePanel 测试数据 | 读档后数据还原 | ✅ 完成(两级存档:无限组/槽、建组建槽/自动档/两级浏览/读档全量还原/删除组与槽/游戏内加载进度点/损坏容错,验收9/9) |
| 8/11 二 | D9 | 打磨 | 数值平衡初调,玩 10 分钟验证 | 无致命 bug | ✅ 完成(大境界突破:练气圆满→手动花费灵石→筑基及以上大境界,失败灵石不返还+修为倒退 3 步,花费/成功率/小境门槛配置化;61 项逻辑测试全 PASS) |
| 8/12 三 | D10 | **M1 验收** | 完整跑一遍:新游戏→招弟子→挂机→存→退→读 | 闭环可玩 | ⬜ |## 三、第 3–5 周(8/179/6):P2 扩展
| 日期 | 周 | 任务 | 拆解 | 交付 |
| ---- | -- | ---- | ---- | ---- |
| 8/178/23 | W3 | 财政经济 | ① 收支流水 ② 俸禄发放 ③ 价格波动 | 财政面板 |
| 8/248/30 | W4 | 任务系统 | ① 任务生成 ② 贡献值 ③ 兑换商店 | 任务面板 |
| 8/319/6 | W5 | 生产制造 + 战斗基础 | ① 炼丹工坊 ② 弟子组队 ③ 外敌入侵事件 | **M2 验收** |
## 四、第 610 周(9/710/11):P3 扩展
| 周 | 任务 | 交付 |
| -- | ---- | ---- |
| W6 | 秘境系统 | 探索/副本 |
| W7 | 外交系统 | 宗门关系/联盟 |
| W8 | 剧情事件 | 主线/随机事件 |
| W9 | 打磨平衡 | 数值/UI 优化 |
| W10 | **M3 验收** | 正式版本 |
## 五、里程碑
```
M1 (8/12) ── 核心循环闭环,可玩
M2 (9/6) ── 经营玩法丰富
M3 (10/11) ── 秘境/外交/剧情,正式版本
```
## 六、每日开工检查单
- [ ] 昨晚 TODO 完成?
- [ ] git status 干净(有改动先提交)
- [ ] F5 能正常运行
- [ ] 今晚只做 1 个主目标(拆解为 30 分钟小步)
## 七、纪律规则
1. 每晚只设 1 个主目标,完成后不贪多
2. 卡壳超过 30 分钟 → 简化方案,不硬磕
3. 每步可运行验证,禁止写一大段不跑通的代码
4. 每天必须 git 提交(哪怕只有一行)
5. 周末至少半天完全休息,防倦怠
+75
View File
@@ -0,0 +1,75 @@
[runnable_presets]
"Windows Desktop"="Windows Desktop"
[preset.0]
name="Windows Desktop"
platform="Windows Desktop"
dedicated_server=false
custom_features=""
export_filter="all_resources"
export_files=PackedStringArray()
include_filter=""
exclude_filter=""
export_path="../aaa.exe"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
patch_delta_min_reduction=0.1
patch_delta_include_filters="*"
patch_delta_exclude_filters=""
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.0.options]
custom_template/debug=""
custom_template/release=""
debug/export_console_wrapper=1
binary_format/embed_pck=true
texture_format/s3tc_bptc=true
texture_format/etc2_astc=false
shader_baker/enabled=true
binary_format/architecture="x86_64"
codesign/enable=false
codesign/timestamp=true
codesign/timestamp_server_url=""
codesign/digest_algorithm=1
codesign/description=""
codesign/custom_options=PackedStringArray()
application/modify_resources=true
application/icon=""
application/console_wrapper_icon=""
application/icon_interpolation=4
application/file_version=""
application/product_version=""
application/company_name=""
application/product_name=""
application/file_description=""
application/copyright=""
application/trademarks=""
application/export_angle=0
application/export_d3d12=0
application/d3d12_agility_sdk_multiarch=true
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
ssh_remote_deploy/port="22"
ssh_remote_deploy/extra_args_ssh=""
ssh_remote_deploy/extra_args_scp=""
ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
$trigger = New-ScheduledTaskTrigger -Once -At 00:00
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
Start-ScheduledTask -TaskName godot_remote_debug
while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
Remove-Item -Recurse -Force '{temp_dir}'"
+28 -3
View File
@@ -11,18 +11,39 @@ config_version=5
[application]
config/name="模拟宗门"
run/main_scene="res://src/UI/Panels/MainMenu/MainMenu.tscn"
config/features=PackedStringArray("4.7", "Forward Plus")
config/icon="res://icon.svg"
[autoload]
GameState="*res://src/Core/GameState.gd"
TimeSystem="*res://src/Core/TimeSystem.gd"
Settings="*res://src/Core/Settings.gd"
SectManager="*res://src/Core/SectManager.gd"
GameConfig="*res://src/Core/GameConfig.gd"
DiscipleManager="*res://src/Character/DiscipleManager.gd"
CultivationManager="*res://src/Character/CultivationManager.gd"
BuildingManager="*res://src/Sect/BuildingManager.gd"
SaveSystem="*res://src/Core/SaveSystem.gd"
[display]
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"
window/size/viewport_width=1920
window/size/viewport_height=1080
window/stretch/mode="viewport"
window/size/window_mode=2
[dotnet]
project/assembly_name="模拟宗门"
[internationalization]
locale/locale_filter_mode=1
locale/script_filter=["Hans"]
locale/country_filter=["CN"]
locale/language_filter=[]
[physics]
3d/physics_engine="Jolt Physics"
@@ -30,3 +51,7 @@ project/assembly_name="模拟宗门"
[rendering]
rendering_device/driver.windows="d3d12"
anti_aliasing/quality/msaa_2d=3
anti_aliasing/quality/msaa_3d=3
anti_aliasing/quality/screen_space_aa=2
anti_aliasing/quality/use_taa=true
+58
View File
@@ -0,0 +1,58 @@
; 模拟宗门 全局配置
; 所有可调参数集中在此,修改后重启游戏生效
; 注释以 ; 开头,参数按系统分节
[recruitment]
difficulty="normal"
opening_applicant_count=5
opening_pick_count=3
recruit_interval_years=12
recruit_applicant_count=3
heavenly_root_chance=0.5 ; 天灵根概率:仅天骄资质中再掷,50%
root_count_weights=[20, 20, 20, 20, 20] ; 灵根数量权重(1根/2根/3根/4根/5根,和=100 即百分比概率)
; 各难度资质权重表(下品/中品/上品/天骄,权重和=100 即百分比概率)
; 当前难度由 [recruitment] difficulty 指定
[recruitment_normal]
aptitude_weights=[45, 35, 15, 5] ; 45% / 35% / 15% / 5%
[recruitment_hard]
aptitude_weights=[60, 30, 8, 2] ; 60% / 30% / 8% / 2%
[cultivation] ; 修炼系统
base_speed=10 ; 基础修为/日
qi_concentration=1.0 ; 宗门灵气浓度
aptitude_speed=[0.6, 1.0, 1.5, 2.0] ; 资质倍率(下品/中品/上品/天骄)
heavenly_bonus=1.5 ; 天灵根额外修炼速度倍率
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 各层所需修为
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 通用
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 ; 断粮累计天数上限(达到即离宗,待接入)
+7
View File
@@ -0,0 +1,7 @@
[gd_resource type="Theme" format=3 uid="uid://05ygdcq8dme4"]
[ext_resource type="FontFile" uid="uid://ce12uai6ul0hj" path="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKaiMono-Regular.ttf" id="1_fwts0"]
[resource]
default_font = ExtResource("1_fwts0")
default_font_size = 24
+12
View File
@@ -0,0 +1,12 @@
[gd_resource type="LabelSettings" format=3 uid="uid://d1v74ab2ldtn7"]
[ext_resource type="FontFile" uid="uid://4su41gib4l5u" path="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKaiMono-Medium.ttf" id="1_fg5xm"]
[resource]
font = ExtResource("1_fg5xm")
font_size = 24
font_color = Color(0.82, 0.95, 0.9, 1)
outline_color = Color(0, 0, 0, 0.8)
shadow_size = 2
shadow_color = Color(0, 0, 0, 0.7)
shadow_offset = Vector2(1, 2)
+7
View File
@@ -0,0 +1,7 @@
[gd_resource type="Theme" format=3 uid="uid://ctuanla5trf4s"]
[ext_resource type="FontFile" uid="uid://ce12uai6ul0hj" path="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKaiMono-Regular.ttf" id="1_uif2x"]
[resource]
default_font = ExtResource("1_uif2x")
default_font_size = 32
+148
View File
@@ -0,0 +1,148 @@
## 修炼管理器(Autoload 单例)。
## 负责弟子修为的每日结算与境界推进(D5:修为逐日增长 + 自动升层 + 瓶颈突破判定;D9:大境界手动突破)。
extends Node
## 境界变化信号(突破后发出,供详情面板刷新)。
signal realm_changed(d: Disciple)
## 修炼日志文本(控制台打印,后续日志系统接入)。
signal cultivation_log(text: String)
## 挂接每日推进信号。
func _ready() -> void:
# 每日推进时结算全体已入宗弟子的修为与境界
TimeSystem.day_passed.connect(_on_day_passed)
## 每日结算:先扣口粮,再结算已入宗弟子的修为与境界(候选池不修炼)。
func _on_day_passed(_year: int, _month: int, _day: int) -> void:
_settle_food()
for d in DiscipleManager.disciples:
_gain_exp(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 层)或大境界圆满(筑基+ 4 小境满)后修为封顶,不再增长。
## @param d: 已入宗弟子
func _gain_exp(d: Disciple) -> void:
if d.is_realm_peak():
return
var speed := GameConfig.cultivation_base_speed
speed *= GameConfig.aptitude_speed[d.aptitude]
speed *= d.get_root_count_capacity()
if d.is_heavenly:
speed *= GameConfig.heavenly_bonus
speed *= GameConfig.qi_concentration
if d.hunger_days > 0:
speed *= GameConfig.hunger_speed_multiplier
d.cultivation_exp += speed
## 突破判定:修为满当前层门槛时推进境界。
## 非瓶颈目标层自动升层;瓶颈目标层掷骰(成功率来自配置),失败扣 20% 修为。
## 每日最多推进一层。
## @param d: 已入宗弟子
func _check_breakthrough(d: Disciple) -> void:
if d.realm_index != 0:
return
var layer := d.sub_realm_index + 1
# 13 层圆满:无更高门槛,不再判定
if layer > GameConfig.level_thresholds.size():
return
if d.cultivation_exp < GameConfig.level_thresholds[layer - 1]:
return
if GameConfig.bottleneck_levels.has(layer + 1):
# 成功率按目标层从配置取(与 bottleneck_levels 一一对应)
var idx := maxi(0, GameConfig.bottleneck_levels.find(layer + 1))
if randf() < GameConfig.bottleneck_chances[idx]:
_advance_layer(d)
else:
d.cultivation_exp *= 0.8
_log("%s 冲关 %s 失败,修为损耗两成" % [d.name, d.get_realm_text()])
else:
_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: 已入宗弟子
func _advance_layer(d: Disciple) -> void:
var threshold := GameConfig.level_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()])
## 输出修炼日志(控制台打印 + 信号广播)。
## @param text: 日志文本
func _log(text: String) -> void:
cultivation_log.emit(text)
print(text)
+1
View File
@@ -0,0 +1 @@
uid://q4pk0n21oawr
+137
View File
@@ -0,0 +1,137 @@
## 弟子数据模型(纯数据,不挂在场景树上)。
## 生成时决定的属性(姓名/资质/灵根)不可变,成长属性(境界)由 D5 修炼系统推进。
class_name Disciple # 全局类名:全项目可直接用 Disciple.new()
extends Resource # Resource = 轻量纯数据对象,可序列化(D8 存档用)
## 资质四类(入宗分流结果)。
enum Aptitude { ORDINARY, MEDIUM, SUPERIOR, HEAVENLY } # 下品/中品/上品/天骄
## 灵根五行。
enum SpiritRoot { GOLD, WOOD, WATER, FIRE, EARTH }
# —— 常量表:枚举值 → 显示文本(按索引一一对应)——
const APTITUDE_NAMES: Array[String] = ["下品", "中品", "上品", "天骄"]
const ROOT_NAMES: Array[String] = ["", "", "", "", ""]
const REALM_NAMES: Array[String] = ["练气", "筑基", "金丹", "元婴", "化神"] # 按 realm_index 索引
# —— 修炼数值一律读 GameConfig(资质倍率/天灵根加成/速度等)——
# —— 实例字段 ——
var id: int = 0 # 唯一编号(DiscipleManager 分配)
var name: String = ""
var age: int = 0
var aptitude: Aptitude = Aptitude.ORDINARY # 资质(入宗分流结果)
var roots: Array[SpiritRoot] = [] # 普通弟子 1~5 根;天灵根固定 1 根
var is_heavenly: bool = false # 天灵根标记(仅天骄资质,概率见配置)
var join_year: int = 0 # 入宗年份(0 = 未入宗/候选),accept 时快照
var join_month: int = 0 # 入宗月份
var join_day: int = 0 # 入宗日
var realm_index: int = 0 # 大境界索引(0=练气)
var sub_realm_index: int = 0 # 小境界索引(练气层数-1
var cultivation_exp: float = 0.0 # 修为(float 可积累小数,D5 每日结算)
var hunger_days: int = 0 # 断粮累计天数(≥阈值离宗,方案 B)
# —— 辅助函数:把数据翻译成给人看的文本 ——
func get_aptitude_name() -> String:
return APTITUDE_NAMES[aptitude]
## 灵根显示文本,如 "金、木"。
func get_roots_text() -> String:
var names: Array[String] = []
for root in roots:
names.append(ROOT_NAMES[root])
return "".join(names)
## 灵根数量灵气容量倍率(根数越少灵气越纯、修炼越快,D5 修炼循环消费)。
## 容量规则:单灵根 1 单位灵气充满,双灵根只充 1/2,三灵根 1/3,以此类推(容量 = 1 ÷ 根数)。
## 无灵根无法练气,容量为 0(体修路线未来另行结算,暂不生成)。
func get_root_count_capacity() -> float:
if roots.is_empty():
return 0.0
return 1.0 / roots.size()
## 境界显示文本,如 "练气5层"、"筑基初期"。
func get_realm_text() -> String:
if realm_index == 0:
return "%s%d" % [REALM_NAMES[realm_index], sub_realm_index + 1]
return "%s%s" % [REALM_NAMES[realm_index], ["初期", "中期", "后期", "圆满"][sub_realm_index]]
## 入宗时间文本,如 "1年2月3日";未入宗返回空串。
func get_join_date_text() -> String:
if join_year <= 0:
return ""
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"(当前修为 / 本层门槛)。
## 练气圆满(13 层)返回"练气圆满";筑基及以上返回 "1234 / 6000"(小境门槛),圆满返回"筑基圆满"。
func get_cultivation_progress_text() -> String:
if realm_index == 0:
if sub_realm_index >= GameConfig.level_thresholds.size():
return "练气圆满"
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]
+1
View File
@@ -0,0 +1 @@
uid://cuybveud5w308
+188
View File
@@ -0,0 +1,188 @@
## 弟子管理器(Autoload 单例)。
## 负责候选弟子生成、入宗选择与宗门名录维护。
## 招募流程:开局/定期生成候选 → 暂停时间 → 玩家在弹窗选择 → 结算入名录。
extends Node
## 名录变化时发出(增删/重置)。
signal disciples_changed
## 候选弟子生成时发出(开局/定期,弹窗 UI 监听此信号)。
## @param is_opening: 是否开局招募(开局必须选满,定期自由选择)
signal applicants_generated(is_opening: bool)
## 宗门弟子名录(已入宗)。
var disciples: Array[Disciple] = []
## 候选弟子池(未入宗,等待玩家选择)。
var applicants: Array[Disciple] = []
## 姓氏池。
const SURNAMES: Array[String] = [
"", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "",
]
## 名用字池(修仙风)。
const GIVEN_CHARS: Array[String] = [
"", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "",
"", "", "", "屿", "", "", "", "",
]
## 距下次定期招募的剩余游戏天数。
var _days_until_recruit := 0
## 下一位入宗弟子的 ID(自增,重置归零)。
var _next_id := 1
## 挂接每日推进信号并初始化招募倒计时。
func _ready() -> void:
# 挂到时间系统上:每日推进时倒计时,满间隔生成候选并暂停时间
TimeSystem.day_passed.connect(_on_day_passed)
_days_until_recruit = _get_interval_days()
## 每日推进:定期招募倒计时减 1,归零时生成候选并暂停时间。
func _on_day_passed(_year: int, _month: int, _day: int) -> void:
_days_until_recruit -= 1
if _days_until_recruit <= 0:
_days_until_recruit = _get_interval_days()
generate_applicants(GameConfig.recruit_applicant_count)
TimeSystem.set_paused(true)
## 开局新游戏:清空所有数据,生成开局候选(数量来自配置),暂停时间等待选择。
func start_new_game() -> void:
reset()
generate_applicants(GameConfig.opening_applicant_count, true)
TimeSystem.set_paused(true)
## 生成一批候选弟子:清空旧候选后按 count 生成,发信号供弹窗显示。
## @param count: 候选人数(调用方从 GameConfig 传入)
## @param is_opening: 是否开局招募
func generate_applicants(count: int, is_opening := false) -> void:
applicants.clear()
for i in count:
applicants.append(_create_applicant())
applicants_generated.emit(is_opening)
## 接受一名候选弟子:分配 ID、快照入宗时间并移入名录(候选池 → 名录)。
## @param d: 要接受的候选
func accept_applicant(d: Disciple) -> void:
if not applicants.has(d):
return
applicants.erase(d)
d.id = _next_id
_next_id += 1
d.join_year = GameState.year
d.join_month = GameState.month
d.join_day = GameState.day
disciples.append(d)
disciples_changed.emit()
print("弟子入宗:%s %d岁 | %s资质%s | %s灵根 | %s" % [
d.name, d.age, d.get_aptitude_name(),
"·天灵根" if d.is_heavenly else "",
d.get_roots_text(), d.get_realm_text(),
])
## 拒绝一名候选弟子:从候选池移除,不进入名录。
## @param d: 要拒绝的候选
func reject_applicant(d: Disciple) -> void:
applicants.erase(d)
## 结算本次招募:selected 内的候选入宗,其余全部拒绝,清空候选池。
## @param selected: 玩家勾选的候选列表(弹窗"确定"时传入)
func finish_recruitment(selected: Array[Disciple]) -> void:
for d in applicants.duplicate():
if selected.has(d):
accept_applicant(d)
else:
reject_applicant(d)
## 移除已入宗弟子(逐出/陨落)。
## @param d: 要移除的弟子
func remove_disciple(d: Disciple) -> void:
disciples.erase(d)
disciples_changed.emit()
## 清空名录与候选池(新游戏开局调用),倒计时与 ID 复位。
func reset() -> void:
disciples.clear()
applicants.clear()
_next_id = 1
_days_until_recruit = _get_interval_days()
disciples_changed.emit()
## 当前下一个弟子 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 的"年"换算而来。
func _get_interval_days() -> int:
var days_per_year := TimeSystem.MONTHS_PER_YEAR * TimeSystem.DAYS_PER_MONTH
return maxi(1, roundi(GameConfig.recruit_interval_years * days_per_year))
## 创建一名候选弟子:随机生成属性(姓名/年龄/资质/天灵根/灵根)。
## 天灵根规则:仅天骄资质中再掷一次(概率来自 GameConfig.heavenly_root_chance)。
## 不分配 ID——ID 在入宗时由 accept_applicant 分配,保证连续且无浪费。
func _create_applicant() -> Disciple:
var d := Disciple.new()
d.name = _generate_name()
d.age = randi_range(12, 18)
d.aptitude = _roll_aptitude()
d.is_heavenly = d.aptitude == Disciple.Aptitude.HEAVENLY \
and randf() < GameConfig.heavenly_root_chance
d.roots = _generate_roots(d.is_heavenly)
return d
## 随机生成姓名:单姓 + 1~2 个名用字。
func _generate_name() -> String:
var surname := SURNAMES[randi() % SURNAMES.size()]
var given := GIVEN_CHARS[randi() % GIVEN_CHARS.size()]
if randf() < 0.5:
given += GIVEN_CHARS[randi() % GIVEN_CHARS.size()]
return surname + given
## 随机生成灵根。
## 天灵根:固定 1 根五行;普通弟子:按配置权重掷 1~5 根,元素从五行中抽取、概率均等。
## 无灵根暂不生成(体修路线预留)。
## @param heavenly: 是否天灵根
func _generate_roots(heavenly: bool) -> Array[Disciple.SpiritRoot]:
if heavenly:
return [randi() % Disciple.SpiritRoot.size() as Disciple.SpiritRoot]
var count := _roll_root_count()
var pool := Disciple.SpiritRoot.values()
pool.shuffle()
var result: Array[Disciple.SpiritRoot] = []
for i in count:
result.append(pool[i])
return result
## 按配置权重掷灵根数量(1~5 根),权重来自 GameConfig.root_count_weights。
func _roll_root_count() -> int:
var weights := GameConfig.root_count_weights
var total := 0
for w in weights:
total += w
var roll := randi() % total
for i in weights.size():
if roll < weights[i]:
return i + 1
roll -= weights[i]
return 1
## 按权重掷资质(入宗分流),权重来自 GameConfig 当前难度配置。
func _roll_aptitude() -> Disciple.Aptitude:
var weights := GameConfig.get_aptitude_weights()
var total := 0
for w in weights:
total += w
var roll := randi() % total
for i in weights.size():
if roll < weights[i]:
return i as Disciple.Aptitude
roll -= weights[i]
return Disciple.Aptitude.ORDINARY
+1
View File
@@ -0,0 +1 @@
uid://d2aj0gbvb6p4w
+186
View File
@@ -0,0 +1,186 @@
## 游戏全局配置(Autoload 单例)。
## 启动时从 resources/game_config.cfg 读取所有可调参数,
## 其他系统通过公开变量读取,如 GameConfig.recruit_interval_years。
extends Node
## 配置文件路径(res:// 开发期可改,导出后只读)。
const CONFIG_PATH := "res://resources/game_config.cfg"
# —— 招募系统(对应 cfg 的 [recruitment] 节)——
## 当前难度档位(对应下方各难度节,如 "normal"/"hard")。
var difficulty := "normal"
## 开局候选人数。
var opening_applicant_count := 5
## 开局必须选满数。
var opening_pick_count := 3
## 定期招募间隔(游戏年,1 年 = 360 天)。
## 注意:cfg 文件不随导出打包(未知扩展名),导出版使用此默认值——与 cfg 保持同步。
var recruit_interval_years := 12.0
## 定期弹窗候选人数。
var recruit_applicant_count := 3
## 天灵根概率(仅天骄资质中再掷一次)。
var heavenly_root_chance := 0.5
## 灵根数量权重(按根数-1 索引:1根~5根,和=100 即百分比概率)。
var root_count_weights: Array[int] = [20, 20, 20, 20, 20]
# —— 修炼系统(对应 cfg 的 [cultivation] 节)——
## 基础修炼速度(修为/游戏日)。
var cultivation_base_speed := 10.0
## 宗门灵气浓度(修炼速度系数,D6 建筑系统接入后动态化)。
var qi_concentration := 1.0
## 资质修炼倍率(按下品/中品/上品/天骄索引)。
var aptitude_speed: Array[float] = [0.6, 1.0, 1.5, 2.0]
## 天灵根额外修炼速度倍率。
var heavenly_bonus := 1.5
## 各瓶颈层突破成功率(与 bottleneck_levels 一一对应:升4层/7层/10层/13层)。
var bottleneck_chances: Array[float] = [0.85, 0.8, 0.75, 0.6]
## 练气期各层升级所需修为(练气 1→2 到 12→13,共 12 项)。
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]
## 大境界突破灵石花费(索引 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] 节)——
## HUD 字体大小(导出版用默认值,与 cfg 保持同步)。
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 = {
"normal": [45, 35, 15, 5],
"hard": [60, 30, 8, 2],
}
## 启动时加载配置文件。
func _ready() -> void:
_load_config()
## 读取配置文件;缺失的项保持代码默认值,不会报错。
func _load_config() -> void:
var cfg := ConfigFile.new()
var err := cfg.load(CONFIG_PATH)
if err != OK:
push_warning("GameConfig: 配置文件不存在,使用默认值")
return
difficulty = cfg.get_value("recruitment", "difficulty", difficulty)
opening_applicant_count = cfg.get_value("recruitment", "opening_applicant_count", opening_applicant_count)
opening_pick_count = cfg.get_value("recruitment", "opening_pick_count", opening_pick_count)
recruit_interval_years = cfg.get_value("recruitment", "recruit_interval_years", recruit_interval_years)
recruit_applicant_count = cfg.get_value("recruitment", "recruit_applicant_count", recruit_applicant_count)
heavenly_root_chance = cfg.get_value("recruitment", "heavenly_root_chance", heavenly_root_chance)
var root_raw: Variant = cfg.get_value("recruitment", "root_count_weights", [])
if root_raw is Array and root_raw.size() == root_count_weights.size():
var arr_roots: Array[int] = []
for w in root_raw:
arr_roots.append(int(w))
root_count_weights = arr_roots
# 修炼系统
cultivation_base_speed = cfg.get_value("cultivation", "base_speed", cultivation_base_speed)
qi_concentration = cfg.get_value("cultivation", "qi_concentration", qi_concentration)
heavenly_bonus = cfg.get_value("cultivation", "heavenly_bonus", heavenly_bonus)
var bc_raw: Variant = cfg.get_value("cultivation", "breakthrough_chances", [])
if bc_raw is Array and bc_raw.size() == bottleneck_chances.size():
var arr_bc: Array[float] = []
for v in bc_raw:
arr_bc.append(float(v))
bottleneck_chances = arr_bc
var th_raw: Variant = cfg.get_value("cultivation", "level_thresholds", [])
if th_raw is Array and th_raw.size() == level_thresholds.size():
var arr_th: Array[float] = []
for v in th_raw:
arr_th.append(float(v))
level_thresholds = arr_th
var bn_raw: Variant = cfg.get_value("cultivation", "bottleneck_levels", [])
if bn_raw is Array and bn_raw.size() == bottleneck_levels.size():
var arr_bn: Array[int] = []
for v in bn_raw:
arr_bn.append(int(v))
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 通用
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", [])
if apt_raw is Array and apt_raw.size() == aptitude_speed.size():
var arr: Array[float] = []
for v in apt_raw:
arr.append(float(v))
aptitude_speed = arr
# 逐档读取难度权重(INI 数组字面量,如 [45, 35, 15, 5]
for key in aptitude_weights_by_difficulty.keys():
var raw: Variant = cfg.get_value("recruitment_" + key, "aptitude_weights", [])
if raw is Array and raw.size() > 0:
var arr: Array[int] = []
for w in raw:
arr.append(int(w))
aptitude_weights_by_difficulty[key] = arr
## 当前难度下的资质权重(下品/中品/上品/天骄)。
func get_aptitude_weights() -> Array[int]:
var raw: Array = aptitude_weights_by_difficulty.get(difficulty, aptitude_weights_by_difficulty["normal"])
var result: Array[int] = []
for w in raw:
result.append(int(w))
return result
+1
View File
@@ -0,0 +1 @@
uid://dlchibfd86oek
+36
View File
@@ -0,0 +1,36 @@
## 全局游戏状态(Autoload 单例)。
## 负责场景间参数传递与会话状态管理,不持有游戏内数据。
extends Node
## 进入游戏的方式。
enum GameMode { NEW_GAME, LOAD_GAME }
## 当前进入游戏的方式。
var mode: GameMode = GameMode.NEW_GAME
## 当前存档组 ID(两级存档第一级,SaveSystem 分配)。
var profile_id: int = 0
## 要加载的槽位文件名(仅 LOAD_GAME 时有效,如 "auto"/"slot_1754734800")。
var save_slot: String = ""
# 游戏内时间(12 月 × 30 天 = 360 天/年),实际推进由 TimeSystem 负责
var year: int = 1 # 年
var month: int = 1 # 月(1-12
var day: int = 1 # 日(1-30
## 请求开始新游戏。
## @param pid: 新存档组 ID(主菜单创建)
func request_new_game(pid: int) -> void:
mode = GameMode.NEW_GAME
profile_id = pid
save_slot = ""
year = 1
month = 1
day = 1
## 请求加载指定存档。
## @param pid: 存档组 ID
## @param slot_name: 槽位文件名(不含 .json
func request_load_game(pid: int, slot_name: String) -> void:
mode = GameMode.LOAD_GAME
profile_id = pid
save_slot = slot_name
+1
View File
@@ -0,0 +1 @@
uid://d1ajv537oir2l
+46
View File
@@ -0,0 +1,46 @@
## 主游戏世界场景入口(挂载于 MainGame.tscn)。
## 进入游戏时根据 GameState 模式初始化,并连接顶部 HUD 信号。
extends Node
## 进入游戏世界:注册场景分组,按 GameState 模式初始化(新游戏/读档)。
func _ready() -> void:
# 注册为游戏世界场景(供 TimeSystem 判断是否推进时间)
add_to_group("game_scene")
# 应用用户设置中的默认流速
TimeSystem.apply_default_speed()
match GameState.mode:
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()
## 新游戏初始化:清空旧数据 → 开局招募弹窗(暂停)→ 设定开局资源。
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:
SaveSystem.save_auto(GameState.profile_id)
get_tree().change_scene_to_file("res://src/UI/Panels/MainMenu/MainMenu.tscn")
## 顶部 HUD 的"管理"按钮请求:打开弟子名录面板。
func _on_top_bar_hud_sign_open_disciples() -> void:
$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()
+1
View File
@@ -0,0 +1 @@
uid://centrk8l54j7c
+48
View File
@@ -0,0 +1,48 @@
[gd_scene format=3 uid="uid://box7tj5iyiopq"]
[ext_resource type="Script" uid="uid://centrk8l54j7c" path="res://src/Core/MainGame.gd" id="1_8bu23"]
[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://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://dnjuv1btbxm4o" path="res://assets/sprites/ui/宗门全景.png" id="5_58g0x"]
[node name="MainGame" type="Node" unique_id=2053561265]
script = ExtResource("1_8bu23")
[node name="BackMounton" type="TextureRect" parent="." unique_id=588798961]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("2_2atgf")
expand_mode = 2
[node name="BG" type="TextureRect" parent="." unique_id=1246873308]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("5_58g0x")
expand_mode = 2
[node name="TopBarHud" parent="." unique_id=1115394694 instance=ExtResource("1_g6xmk")]
[node name="RecruitPanel" parent="." unique_id=770000016 instance=ExtResource("1_k5nrm")]
layout_mode = 3
[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_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"]
+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
+68
View File
@@ -0,0 +1,68 @@
## 宗门资源管理器(Autoload 单例)。
extends Node
signal spirit_stones_changed(new_value: int, delta: int)
signal reputation_changed(new_value: int, delta: int)
signal food_changed(new_value: int, delta: int)
var spirit_stones := 0 # 灵石
var reputation := 0 # 声望
var food := 0 # 灵米(食物资源,HUD 显示"灵米"
## 增加灵石并广播信号。
## @param amount: 增加量
func add_spirit_stones(amount: int) -> void:
spirit_stones += amount
emit_signal("spirit_stones_changed", spirit_stones, amount)
## 减少灵石并广播信号。
## @param amount: 减少量
func remove_spirit_stones(amount: int) -> void:
spirit_stones -= amount
emit_signal("spirit_stones_changed", spirit_stones, -amount)
## 增加声望并广播信号。
## @param amount: 增加量
func add_reputation(amount: int) -> void:
reputation += amount
emit_signal("reputation_changed", reputation, amount)
## 减少声望并广播信号。
## @param amount: 减少量
func remove_reputation(amount: int) -> void:
reputation -= amount
emit_signal("reputation_changed", reputation, -amount)
## 增加灵米并广播信号。
## @param amount: 增加量
func add_food(amount: int) -> void:
food += amount
emit_signal("food_changed", food, amount)
## 减少灵米并广播信号。
## @param amount: 减少量
func remove_food(amount: int) -> void:
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
@@ -0,0 +1 @@
uid://c25eic5uquo2o
+21
View File
@@ -0,0 +1,21 @@
## 用户设置(Autoload 单例)。
## 负责个人偏好设置的持久化,保存于 user:// 目录,与游戏存档分离。
extends Node
## 设置文件的保存路径。
const SAVE_PATH := "user://settings.cfg"
## 新游戏开局时的默认流速档位索引(1 = 5x)。
var default_speed_index := 1
## 从磁盘加载设置(不存在则保持默认值)。
func load_settings() -> void:
var cfg := ConfigFile.new()
if cfg.load(SAVE_PATH) == OK:
default_speed_index = cfg.get_value("time", "speed_index", 1)
## 将当前设置写入磁盘。
func save_settings() -> void:
var cfg := ConfigFile.new()
cfg.set_value("time", "speed_index", default_speed_index)
cfg.save(SAVE_PATH)
+1
View File
@@ -0,0 +1 @@
uid://exwk46qy4x6t
+140
View File
@@ -0,0 +1,140 @@
## 全局时间系统(Autoload 单例)。
## 负责游戏时间的推进与播报,仅在世界场景中运行。
## 历法:12 月 × 30 日 = 360 日/年。
extends Node
## 每当推进一日后发出。
## @param year: 当前年份
## @param month: 当前月份(1-12
## @param day: 当前日(1-30
signal day_passed(year: int, month: int, day: int)
## 每当进入新月份时发出(同一天内 day_passed 之后)。
## @param year: 当前年份
## @param month: 当前月份(1-12
signal month_passed(year: int, month: int)
## 每当进入新年份时发出(同一天内 month_passed 之后)。
## @param year: 当前年份
signal year_passed(year: int)
## 时间流速档位变化时发出。
## @param speed_index: 新档位在 SPEEDS 中的索引
signal speed_changed(speed_index: int)
## 暂停状态变化时发出。
## @param paused: 是否已暂停
signal pause_changed(paused: bool)
## 可选的时间流速倍率档位。
const SPEEDS := [1.0, 5.0, 20.0]
## 每月的天数。
const DAYS_PER_MONTH := 30
## 每年的月数。
const MONTHS_PER_YEAR := 12
## 现实世界每 1 天对应的时长(秒)。
const DAY_LENGTH := 0.5
## 当前流速档位在 SPEEDS 中的索引。
var speed_index := 0
## 当前流速倍率(与 SPEEDS[speed_index] 保持一致)。
var speed := 1.0
## 是否暂停时间推进。
var paused := false
## 已累积的现实时间(秒),未满 1 天。
var _elapsed := 0.0
## 每帧累积时间,满一天推进日期(仅游戏世界场景内)。
func _process(delta: float) -> void:
if paused or not _is_in_game():
return
_elapsed += delta * speed
if _elapsed >= DAY_LENGTH:
_elapsed -= DAY_LENGTH
_advance()
## 判断当前场景是否为游戏世界场景(主菜单/存档界面不推进时间)。
## 通过 "game_scene" 分组识别,而非场景名。
func _is_in_game() -> bool:
var scene := get_tree().current_scene
return scene != null and scene.is_in_group("game_scene")
## 推进一日并广播对应信号,跨月/跨年时补发 month_passed / year_passed。
func _advance() -> void:
var new_month := false
var new_year := false
GameState.day += 1
if GameState.day > DAYS_PER_MONTH:
GameState.day = 1
GameState.month += 1
new_month = true
if GameState.month > MONTHS_PER_YEAR:
GameState.month = 1
GameState.year += 1
new_year = true
day_passed.emit(GameState.year, GameState.month, GameState.day)
if new_month:
month_passed.emit(GameState.year, GameState.month)
if new_year:
year_passed.emit(GameState.year)
## 循环切换下一档流速,返回新倍率。
func cycle_speed() -> float:
speed_index = (speed_index + 1) % SPEEDS.size()
speed = SPEEDS[speed_index]
speed_changed.emit(speed_index)
return speed
## 应用用户设置中的默认流速档位(新游戏开局调用)。
func apply_default_speed() -> void:
set_speed_index(Settings.default_speed_index)
## 直接设置流速档位(越界自动钳制)。
## @param index: 目标档位索引
func set_speed_index(index: int) -> void:
speed_index = clampi(index, 0, SPEEDS.size() - 1)
speed = SPEEDS[speed_index]
speed_changed.emit(speed_index)
## 切换暂停状态,返回切换后的暂停状态。
func toggle_pause() -> bool:
paused = not paused
pause_changed.emit(paused)
return paused
## 显式设置暂停状态(幂等,状态未变化时不发信号)。
## @param value: 目标暂停状态
func set_paused(value: bool) -> void:
if paused == value:
return
paused = value
pause_changed.emit(paused)
## 当前流速的显示文本,如 "x5"。
func get_speed_label() -> String:
return "x%d" % int(speed)
## 当前日期数组 [年, 月, 日]。
func get_current_date() -> Array:
return [GameState.year, GameState.month, GameState.day]
## 当前日期文本,如 "3年 5月 12日"。
func get_date_text() -> String:
return "%d%d%d" % [GameState.year, GameState.month, GameState.day]
## 直接设置日期(供读档/测试使用)。
## @param y: 年
## @param m: 月(1-12
## @param d: 日(1-30
func set_date(y: int, m: int, d: int) -> void:
GameState.year = y
GameState.month = m
GameState.day = d
+1
View File
@@ -0,0 +1 @@
uid://c13ljuiktgrgw
+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
+235
View File
@@ -0,0 +1,235 @@
[gd_scene format=3 uid="uid://dl0tb68vsw3c6"]
[ext_resource type="Script" uid="uid://d3o1ssph7ca58" path="res://src/UI/HUD/top_bar_hud.gd" id="1_vm4eo"]
[ext_resource type="Theme" uid="uid://05ygdcq8dme4" path="res://resources/hud_button_theme.tres" id="2_ymrsi"]
[ext_resource type="LabelSettings" uid="uid://d1v74ab2ldtn7" path="res://resources/hud_label_settings.tres" id="3_4k476"]
[sub_resource type="LabelSettings" id="LabelSettings_bn2o5"]
font_size = 32
shadow_size = 2
shadow_color = Color(0, 0, 0, 0.7)
shadow_offset = Vector2(1, 2)
[node name="TopBarHud" type="Control" unique_id=1115394694]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_vm4eo")
[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=1616038110]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer" unique_id=2089867867]
layout_mode = 2
[node name="SpeedGroup" type="HBoxContainer" parent="VBoxContainer/HBoxContainer2" unique_id=2144893126]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 3.0
theme_override_constants/separation = 4
[node name="Button_Pause" type="Button" parent="VBoxContainer/HBoxContainer2/SpeedGroup" unique_id=2144893127]
unique_name_in_owner = true
custom_minimum_size = Vector2(64, 0)
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 3.0
theme = ExtResource("2_ymrsi")
text = "暂停"
[node name="Button_Speed1" type="Button" parent="VBoxContainer/HBoxContainer2/SpeedGroup" unique_id=2144893128]
unique_name_in_owner = true
custom_minimum_size = Vector2(48, 0)
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 2.0
theme = ExtResource("2_ymrsi")
text = "1x"
[node name="Button_Speed5" type="Button" parent="VBoxContainer/HBoxContainer2/SpeedGroup" unique_id=2144893129]
unique_name_in_owner = true
custom_minimum_size = Vector2(48, 0)
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 2.0
theme = ExtResource("2_ymrsi")
text = "5x"
[node name="Button_Speed20" type="Button" parent="VBoxContainer/HBoxContainer2/SpeedGroup" unique_id=2144893130]
unique_name_in_owner = true
custom_minimum_size = Vector2(48, 0)
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 2.0
theme = ExtResource("2_ymrsi")
text = "20x"
[node name="placeholder_1" type="Label" parent="VBoxContainer/HBoxContainer2" unique_id=1042446504]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
size_flags_stretch_ratio = 8.0
label_settings = SubResource("LabelSettings_bn2o5")
[node name="Button_StaffManagement" type="Button" parent="VBoxContainer/HBoxContainer2" unique_id=568204305]
layout_mode = 2
size_flags_horizontal = 3
theme = ExtResource("2_ymrsi")
text = "管理"
[node name="Button_ResourcesManagement" type="Button" parent="VBoxContainer/HBoxContainer2" unique_id=1424251140]
layout_mode = 2
size_flags_horizontal = 3
theme = ExtResource("2_ymrsi")
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]
layout_mode = 2
size_flags_horizontal = 3
theme = ExtResource("2_ymrsi")
text = "返回"
[node name="HBoxContainer3" type="HBoxContainer" parent="VBoxContainer" unique_id=2058056395]
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HBoxContainer3" unique_id=1608257100]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 12.0
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer3/HBoxContainer" unique_id=1557830593]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 4.0
[node name="HBoxContainer5" type="HBoxContainer" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer" unique_id=967979380]
layout_mode = 2
[node name="Label_Time" type="Label" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer/HBoxContainer5" unique_id=671872515]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
size_flags_stretch_ratio = 2.0
theme_override_colors/font_color = Color(0.72, 0.85, 0.78, 1)
text = "时间"
label_settings = ExtResource("3_4k476")
[node name="Value_Time" type="Label" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer/HBoxContainer5" unique_id=1229577188]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
size_flags_stretch_ratio = 5.0
theme_override_colors/font_color = Color(0.93, 0.96, 0.88, 1)
label_settings = ExtResource("3_4k476")
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer" unique_id=516862989]
layout_mode = 2
size_flags_vertical = 3
[node name="Label_Currency" type="Label" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer/HBoxContainer" unique_id=766096596]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
size_flags_stretch_ratio = 2.0
theme_override_colors/font_color = Color(0.72, 0.85, 0.78, 1)
text = "灵石"
label_settings = ExtResource("3_4k476")
[node name="Value_Currency" type="Label" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer/HBoxContainer" unique_id=1150573727]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
size_flags_stretch_ratio = 5.0
theme_override_colors/font_color = Color(0.93, 0.96, 0.88, 1)
label_settings = ExtResource("3_4k476")
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer" unique_id=2103796675]
layout_mode = 2
[node name="Label_Reputation" type="Label" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer/HBoxContainer2" unique_id=985110673]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
size_flags_stretch_ratio = 2.0
theme_override_colors/font_color = Color(0.72, 0.85, 0.78, 1)
text = "声望"
label_settings = ExtResource("3_4k476")
[node name="Value_Reputation" type="Label" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer/HBoxContainer2" unique_id=2011931119]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
size_flags_stretch_ratio = 5.0
theme_override_colors/font_color = Color(0.93, 0.96, 0.88, 1)
label_settings = ExtResource("3_4k476")
[node name="HBoxContainer3" type="HBoxContainer" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer" unique_id=1647418947]
layout_mode = 2
[node name="Label_Food" type="Label" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer/HBoxContainer3" unique_id=791000001]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
size_flags_stretch_ratio = 2.0
theme_override_colors/font_color = Color(0.72, 0.85, 0.78, 1)
text = "灵米"
label_settings = ExtResource("3_4k476")
[node name="Value_Food" type="Label" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer/HBoxContainer3" unique_id=791000002]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
size_flags_stretch_ratio = 5.0
theme_override_colors/font_color = Color(0.93, 0.96, 0.88, 1)
label_settings = ExtResource("3_4k476")
[node name="HBoxContainer4" type="HBoxContainer" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer" unique_id=758328210]
layout_mode = 2
[node name="Label_Qi" type="Label" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer/HBoxContainer4" unique_id=790000001]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
size_flags_stretch_ratio = 2.0
theme_override_colors/font_color = Color(0.72, 0.85, 0.78, 1)
text = "灵气"
label_settings = ExtResource("3_4k476")
[node name="Value_Qi" type="Label" parent="VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer/HBoxContainer4" unique_id=790000002]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
size_flags_stretch_ratio = 5.0
theme_override_colors/font_color = Color(0.93, 0.96, 0.88, 1)
label_settings = ExtResource("3_4k476")
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer3" unique_id=1867196268]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
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/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"]
+106
View File
@@ -0,0 +1,106 @@
## 顶部信息栏 HUD(挂载于 TopBarHUD.tscn)。
## 显示资源(灵石/声望/灵米/灵气浓度)与时间,提供时间流速控制与返回主菜单入口。
extends Control
## 请求返回主菜单。
signal sign_return_mainmenu
## 请求打开弟子名录面板。
signal sign_open_disciples
## 请求打开建筑面板。
signal sign_open_buildings
## 请求打开存档面板。
signal sign_open_save
@onready var time_label: Label = %Value_Time
@onready var currency_label: Label = %Value_Currency
@onready var reputation_label: Label = %Value_Reputation
@onready var qi_label: Label = %Value_Qi
@onready var food_label: Label = %Value_Food
@onready var pause_button: Button = %Button_Pause
@onready var speed_buttons: Array[Button] = [%Button_Speed1, %Button_Speed5, %Button_Speed20]
## 初始化:连接速度按钮与各信号、设置初始显示、应用配置字号。
func _ready() -> void:
for i in speed_buttons.size():
var btn := speed_buttons[i]
btn.toggle_mode = true
btn.button_up.connect(_on_speed_button_up.bind(i))
# 时间:每日推进时刷新日期
TimeSystem.day_passed.connect(_on_day_passed)
# 资源数值:信号仅在变化时触发,初始值需手动设置一次
SectManager.spirit_stones_changed.connect(_on_spirit_stones_changed)
SectManager.reputation_changed.connect(_on_reputation_changed)
SectManager.food_changed.connect(_on_food_changed)
currency_label.text = str(SectManager.spirit_stones)
reputation_label.text = str(SectManager.reputation)
food_label.text = str(SectManager.food)
# 灵气浓度:当前为配置固定值(D6 建筑系统接入后动态刷新)
qi_label.text = "%d%%" % roundi(GameConfig.qi_concentration * 100)
time_label.text = TimeSystem.get_date_text()
_apply_hud_font()
_update_speed_ui()
## 每日推进时刷新日期显示。
func _on_day_passed(_year: int, _month: int, _day: int) -> void:
time_label.text = TimeSystem.get_date_text()
## 灵石数量变化时刷新显示。
## @param new_value: 变化后的灵石总量
func _on_spirit_stones_changed(new_value: int, _delta: int) -> void:
currency_label.text = str(new_value)
## 声望变化时刷新显示。
## @param new_value: 变化后的声望总量
func _on_reputation_changed(new_value: int, _delta: int) -> void:
reputation_label.text = str(new_value)
## 灵米(食物)变化时刷新显示。
## @param new_value: 变化后的灵米总量
func _on_food_changed(new_value: int, _delta: int) -> void:
food_label.text = str(new_value)
## 点击"返回"按钮。
func _on_button_return_button_up() -> void:
sign_return_mainmenu.emit()
## 点击"管理"按钮:请求打开弟子名录面板(由 MainGame 处理)。
func _on_button_staff_management_button_up() -> void:
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:
TimeSystem.toggle_pause()
_update_speed_ui()
## 点击流速档位按钮。
## @param index: 目标档位索引(由 _ready 中 bind 传入)
func _on_speed_button_up(index: int) -> void:
TimeSystem.set_speed_index(index)
_update_speed_ui()
## 应用配置中的 HUD 字体大小(GameConfig.hud_font_size)。
## 标签共用同一 LabelSettings 资源,改一处全生效;按钮逐个覆盖。
func _apply_hud_font() -> void:
var size := GameConfig.hud_font_size
var settings: LabelSettings = $VBoxContainer/HBoxContainer3/HBoxContainer/VBoxContainer/HBoxContainer/Label_Currency.label_settings
if settings != null:
settings.font_size = size
for btn in $VBoxContainer.find_children("*", "Button", true, false):
btn.add_theme_font_size_override("font_size", size)
## 刷新速度组 UI:暂停按钮文字与各档位按下状态。
## 选中档位用 toggle 的 pressed 状态表达(原生高亮),暂停时禁用档位按钮。
func _update_speed_ui() -> void:
pause_button.text = "继续" if TimeSystem.paused else "暂停"
for i in speed_buttons.size():
var btn := speed_buttons[i]
btn.set_pressed_no_signal(i == TimeSystem.speed_index)
btn.disabled = TimeSystem.paused
+1
View File
@@ -0,0 +1 @@
uid://d3o1ssph7ca58
@@ -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
@@ -0,0 +1,86 @@
[gd_scene format=3 uid="uid://b3i7xibxjq303"]
[ext_resource type="Script" path="res://src/UI/Panels/DisciplesPanel/disciple_detail_panel.gd" id="1_script"]
[ext_resource type="FontFile" uid="uid://drt448qncx27p" path="res://assets/fonts/dingliezhuhaifont-20240831GengXinBan)-2.ttf" id="2_font"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dialog"]
bg_color = Color(0.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="DiscipleDetailPanel" type="Control" unique_id=781000001]
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=781000002]
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=781000003]
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=781000004]
theme_override_styles/panel = SubResource("StyleBoxFlat_dialog")
[node name="MarginContainer" type="MarginContainer" parent="CenterContainer/Panel" unique_id=781000005]
theme_override_constants/margin_left = 32
theme_override_constants/margin_top = 24
theme_override_constants/margin_right = 32
theme_override_constants/margin_bottom = 24
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/Panel/MarginContainer" unique_id=781000006]
theme_override_constants/separation = 16
[node name="TitleLabel" type="Label" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=781000007]
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="InfoLabel" type="Label" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=781000008]
unique_name_in_owner = true
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 22
theme_override_colors/font_color = Color(0.82, 0.95, 0.88, 1)
text = ""
[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]
layout_mode = 2
size_flags_horizontal = 3
[node name="Button_Close" type="Button" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow" unique_id=781000011]
unique_name_in_owner = true
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 22
text = "关闭"
[connection signal="button_up" from="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow/Button_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"]
@@ -0,0 +1,104 @@
[gd_scene format=3 uid="uid://cahux61qqblje"]
[ext_resource type="Script" path="res://src/UI/Panels/DisciplesPanel/disciples_panel.gd" id="1_script"]
[ext_resource type="FontFile" uid="uid://drt448qncx27p" path="res://assets/fonts/dingliezhuhaifont-20240831GengXinBan)-2.ttf" id="2_font"]
[ext_resource type="PackedScene" uid="uid://b3i7xibxjq303" path="res://src/UI/Panels/DisciplesPanel/DiscipleDetailPanel.tscn" id="3_detail"]
[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="DisciplesPanel" type="Control" unique_id=1362665771]
layout_mode = 3
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=1162009235]
layout_mode = 1
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=47958673]
layout_mode = 1
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=1341680090]
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_dialog")
[node name="MarginContainer" type="MarginContainer" parent="CenterContainer/Panel" unique_id=1155028384]
layout_mode = 2
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=523691907]
layout_mode = 2
theme_override_constants/separation = 12
[node name="TitleLabel" type="Label" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=780000007]
unique_name_in_owner = true
layout_mode = 2
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="ScrollContainer" type="ScrollContainer" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=780000008]
custom_minimum_size = Vector2(1280, 680)
layout_mode = 2
size_flags_vertical = 3
[node name="ListContainer" type="VBoxContainer" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ScrollContainer" unique_id=780000009]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
[node name="EmptyLabel" type="Label" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=780000010]
unique_name_in_owner = true
layout_mode = 2
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 22
theme_override_colors/font_color = Color(0.6, 0.78, 0.7, 1)
text = "暂无弟子"
[node name="ButtonRow" type="HBoxContainer" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=780000011]
layout_mode = 2
[node name="Spacer" type="Control" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow" unique_id=780000012]
layout_mode = 2
size_flags_horizontal = 3
[node name="Button_Close" type="Button" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow" unique_id=780000013]
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="DiscipleDetailPanel" parent="." unique_id=780000014 instance=ExtResource("3_detail")]
unique_name_in_owner = true
[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,65 @@
## 弟子详情弹窗(挂载于 DiscipleDetailPanel.tscn)。
## 静态展示弟子信息(资质/年龄/灵根/境界/编号/入宗时间),
## 修炼进度、大境界突破按钮(手动花费灵石)等动态内容。
extends Control
@onready var title_label: Label = %TitleLabel
@onready var info_label: Label = %InfoLabel
@onready var breakthrough_button: Button = %Button_RealmBreakthrough
var _current_disciple: Disciple = null
## 默认隐藏。
func _ready() -> void:
visible = false
## 显示指定弟子的详情。
## @param d: 要展示的弟子
func show_detail(d: Disciple) -> void:
_current_disciple = d
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" % [
d.name,
d.get_aptitude_name(),
d.age,
d.get_roots_text(),
d.get_realm_text(),
d.get_cultivation_progress_text(),
d.get_join_date_text(),
]
_refresh_breakthrough_button()
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:
visible = false
@@ -0,0 +1 @@
uid://da0d24brddyxk
@@ -0,0 +1,101 @@
## 弟子名录列表面板(挂载于 DisciplesPanel.tscn)。
## 展示宗门已入宗弟子名录,点条目"详情"打开详情弹窗。
## 监听名录变化信号实时刷新,打开面板不暂停时间。
extends Control
## 资质颜色提示(天骄金色、上品亮青,其余默认)。
const APTITUDE_COLORS := {
Disciple.Aptitude.HEAVENLY: Color(1.0, 0.84, 0.35),
Disciple.Aptitude.SUPERIOR: Color(0.55, 0.9, 0.8),
}
@onready var title_label: Label = %TitleLabel
@onready var list_container: VBoxContainer = %ListContainer
@onready var empty_label: Label = %EmptyLabel
@onready var detail_panel: Control = %DiscipleDetailPanel
## 打开面板前的暂停状态(关闭时恢复,避免冲掉玩家手动暂停)。
var _was_paused := false
## 订阅名录变化信号,默认隐藏。
func _ready() -> void:
# 名录变化时刷新(面板打开期间也同步)
DiscipleManager.disciples_changed.connect(_on_disciples_changed)
visible = false
## 打开面板并刷新列表(暂停时间推进)。
func show_panel() -> void:
_was_paused = TimeSystem.paused
_refresh_list()
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_disciples_changed() -> void:
if visible:
_refresh_list()
## 重建列表:清空旧条目,按名录顺序逐条生成。
func _refresh_list() -> void:
for child in list_container.get_children():
child.queue_free()
for d in DiscipleManager.disciples:
_add_entry(d)
empty_label.visible = DiscipleManager.disciples.is_empty()
title_label.text = "弟子名录(%d 人)" % DiscipleManager.disciples.size()
## 添加一名弟子的行条目:姓名/资质/灵根/境界 + 详情按钮。
## @param d: 已入宗弟子
func _add_entry(d: Disciple) -> void:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 12)
var name_label := Label.new()
name_label.text = d.name
name_label.custom_minimum_size = Vector2(180, 0)
row.add_child(name_label)
var aptitude_label := Label.new()
aptitude_label.text = d.get_aptitude_name() + ("·天灵根" if d.is_heavenly else "")
if APTITUDE_COLORS.has(d.aptitude):
aptitude_label.add_theme_color_override("font_color", APTITUDE_COLORS[d.aptitude])
aptitude_label.custom_minimum_size = Vector2(260, 0)
row.add_child(aptitude_label)
var roots_label := Label.new()
roots_label.text = d.get_roots_text() + "灵根"
roots_label.custom_minimum_size = Vector2(320, 0)
row.add_child(roots_label)
var realm_label := Label.new()
realm_label.text = d.get_realm_text()
realm_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(realm_label)
var progress_label := Label.new()
progress_label.text = d.get_cultivation_progress_text()
progress_label.custom_minimum_size = Vector2(220, 0)
row.add_child(progress_label)
var detail_button := Button.new()
detail_button.text = "详情"
detail_button.button_up.connect(_on_detail_button_up.bind(d))
row.add_child(detail_button)
list_container.add_child(row)
## 点击条目"详情"按钮:在子层弹窗中展示该弟子信息。
## @param d: 对应弟子
func _on_detail_button_up(d: Disciple) -> void:
detail_panel.show_detail(d)
@@ -0,0 +1 @@
uid://cfqhusp28ku5x
+26
View File
@@ -0,0 +1,26 @@
## 主菜单场景(挂载于 MainMenu.tscn)。
extends Control
## 点击"新游戏":创建新存档组 → 重置 GameState 后进入游戏世界。
func MainMenu_NewGame_ButtonUp() -> void:
var pid := SaveSystem.create_profile()
GameState.request_new_game(pid)
get_tree().change_scene_to_file("res://src/Core/MainGame.tscn")
## 点击"读取存档":隐藏主按钮组并打开存档面板(读档模式)。
func MainMenu_LoadGame_ButtonUp() -> void:
$ButtonGroup.hide()
$SavePanel.open_load()
## 点击"设置"TODO:接入设置面板)。
func MainMenu_Settings_ButtonUp() -> void:
pass
## 点击"退出"。
func MainMenu_Quit_ButtonUp() -> void:
get_tree().quit()
## 存档面板请求返回。
func _on_save_panel_back_pressed() -> void:
$SavePanel.hide()
$ButtonGroup.show()
+1
View File
@@ -0,0 +1 @@
uid://drfyh36my6gj2
+81
View File
@@ -0,0 +1,81 @@
[gd_scene format=3 uid="uid://d1odycoaqhhyg"]
[ext_resource type="Script" uid="uid://drfyh36my6gj2" path="res://src/UI/Panels/MainMenu/MainMenu.gd" id="1_cwvg5"]
[ext_resource type="Texture2D" uid="uid://c6bthhmao5fyh" path="res://assets/sprites/ui/图片大门.png" id="2_q7typ"]
[ext_resource type="Theme" uid="uid://ctuanla5trf4s" path="res://resources/main_menu_theme.tres" id="3_7e5ie"]
[ext_resource type="PackedScene" uid="uid://dj7b5vssydifa" path="res://src/UI/Panels/SavePanel.tscn" id="3_wrf65"]
[node name="MainMenu" type="Control" unique_id=1416317040]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_cwvg5")
[node name="BG" type="TextureRect" parent="." unique_id=2104369666]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("2_q7typ")
expand_mode = 2
stretch_mode = 6
[node name="Logo" type="TextureRect" parent="." unique_id=1485724026]
layout_mode = 1
anchors_preset = 5
anchor_left = 0.5
anchor_right = 0.5
offset_left = -200.0
offset_top = 60.0
offset_right = 200.0
offset_bottom = 260.0
grow_horizontal = 2
[node name="ButtonGroup" type="VBoxContainer" parent="." unique_id=569064410]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/separation = 16
alignment = 1
[node name="NewGame" type="Button" parent="ButtonGroup" unique_id=835084060]
custom_minimum_size = Vector2(200, 48)
layout_mode = 2
theme = ExtResource("3_7e5ie")
text = "新游戏"
[node name="LoadGame" type="Button" parent="ButtonGroup" unique_id=2093610529]
custom_minimum_size = Vector2(200, 48)
layout_mode = 2
theme = ExtResource("3_7e5ie")
text = "读取存档"
[node name="Settings" type="Button" parent="ButtonGroup" unique_id=2045302694]
custom_minimum_size = Vector2(200, 48)
layout_mode = 2
theme = ExtResource("3_7e5ie")
text = "设置"
[node name="Quit" type="Button" parent="ButtonGroup" unique_id=818112870]
custom_minimum_size = Vector2(200, 48)
layout_mode = 2
theme = ExtResource("3_7e5ie")
text = "退出"
[node name="SavePanel" parent="." unique_id=619488443 instance=ExtResource("3_wrf65")]
visible = false
layout_mode = 1
[connection signal="button_up" from="ButtonGroup/NewGame" to="." method="MainMenu_NewGame_ButtonUp"]
[connection signal="button_up" from="ButtonGroup/LoadGame" to="." method="MainMenu_LoadGame_ButtonUp"]
[connection signal="button_up" from="ButtonGroup/Settings" to="." method="MainMenu_Settings_ButtonUp"]
[connection signal="button_up" from="ButtonGroup/Quit" to="." method="MainMenu_Quit_ButtonUp"]
[connection signal="back_pressed" from="SavePanel" to="." method="_on_save_panel_back_pressed"]
@@ -0,0 +1,102 @@
[gd_scene format=3 uid="uid://d4y7q3m6k2x5n7w2"]
[ext_resource type="Script" path="res://src/UI/Panels/RecruitPanel/recruit_panel.gd" id="1_script"]
[ext_resource type="FontFile" uid="uid://drt448qncx27p" path="res://assets/fonts/dingliezhuhaifont-20240831GengXinBan)-2.ttf" id="2_font"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dialog"]
bg_color = Color(0.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="RecruitPanel" type="Control" unique_id=770000001]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_script")
[node name="Backdrop" type="ColorRect" parent="." unique_id=770000002]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.02, 0.05, 0.05, 0.65)
[node name="CenterContainer" type="CenterContainer" parent="." unique_id=770000003]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Panel" type="PanelContainer" parent="CenterContainer" unique_id=770000004]
theme_override_styles/panel = SubResource("StyleBoxFlat_dialog")
[node name="MarginContainer" type="MarginContainer" parent="CenterContainer/Panel" unique_id=770000005]
theme_override_constants/margin_left = 28
theme_override_constants/margin_top = 20
theme_override_constants/margin_right = 28
theme_override_constants/margin_bottom = 20
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/Panel/MarginContainer" unique_id=770000006]
theme_override_constants/separation = 12
[node name="TitleLabel" type="Label" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=770000007]
unique_name_in_owner = true
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 30
theme_override_colors/font_color = Color(0.82, 0.95, 0.88, 1)
text = "弟子入宗"
[node name="HintLabel" type="Label" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=770000008]
unique_name_in_owner = true
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 20
theme_override_colors/font_color = Color(0.6, 0.78, 0.7, 1)
text = ""
[node name="ScrollContainer" type="ScrollContainer" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=770000009]
custom_minimum_size = Vector2(640, 300)
size_flags_vertical = 3
[node name="ListContainer" type="VBoxContainer" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ScrollContainer" unique_id=770000010]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
[node name="ButtonRow" type="HBoxContainer" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=770000011]
theme_override_constants/separation = 8
[node name="Button_AcceptAll" type="Button" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow" unique_id=770000012]
unique_name_in_owner = true
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 22
text = "全部接受"
[node name="Button_RejectAll" type="Button" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow" unique_id=770000013]
unique_name_in_owner = true
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 22
text = "全部拒绝"
[node name="Spacer" type="Control" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow" unique_id=770000014]
layout_mode = 2
size_flags_horizontal = 3
[node name="Button_Confirm" type="Button" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow" unique_id=770000015]
unique_name_in_owner = true
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 22
text = "确定"
[connection signal="button_up" from="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow/Button_AcceptAll" to="." method="_on_button_accept_all_button_up"]
[connection signal="button_up" from="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow/Button_RejectAll" to="." method="_on_button_reject_all_button_up"]
[connection signal="button_up" from="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow/Button_Confirm" to="." method="_on_button_confirm_button_up"]
+109
View File
@@ -0,0 +1,109 @@
## 招募弹窗(挂载于 RecruitPanel.tscn)。
## 监听候选生成信号自动弹出,玩家勾选后点"确定"结算入宗。
extends Control
## 招募模式:开局必须选满 / 定期自由选择。
enum Mode { OPENING, PERIODIC }
@onready var title_label: Label = %TitleLabel
@onready var hint_label: Label = %HintLabel
@onready var list_container: VBoxContainer = %ListContainer
@onready var confirm_button: Button = %Button_Confirm
## 当前招募模式。
var _mode: Mode = Mode.PERIODIC
## 勾选框列表(与 _entry_disciples 按下标一一对应)。
var _check_buttons: Array[CheckButton] = []
## 对应勾选框的候选弟子。
var _entry_disciples: Array[Disciple] = []
## 订阅候选生成信号,默认隐藏。
func _ready() -> void:
# 订阅候选生成信号:开局与定期招募都会触发
DiscipleManager.applicants_generated.connect(_on_applicants_generated)
visible = false
## 候选生成时弹出:按当前候选池重建列表。
## @param is_opening: 是否开局招募
func _on_applicants_generated(is_opening: bool) -> void:
_mode = Mode.OPENING if is_opening else Mode.PERIODIC
_clear_entries()
for d in DiscipleManager.applicants:
_add_entry(d)
title_label.text = "弟子入宗(%s" % ("开局招募" if is_opening else "定期招募")
_update_ui()
visible = true
## 勾选任一候选后刷新提示与确定按钮可用性。
func _on_check_toggled(_pressed: bool, _d: Disciple) -> void:
_update_ui()
## 全部接受:勾选所有候选。
func _on_button_accept_all_button_up() -> void:
for c in _check_buttons:
c.set_pressed_no_signal(true)
_update_ui()
## 全部拒绝:取消所有勾选。
func _on_button_reject_all_button_up() -> void:
for c in _check_buttons:
c.set_pressed_no_signal(false)
_update_ui()
## 确定:勾选的入宗、未勾选的拒绝,关闭弹窗并恢复时间。
func _on_button_confirm_button_up() -> void:
var selected: Array[Disciple] = []
for i in _entry_disciples.size():
if _check_buttons[i].button_pressed:
selected.append(_entry_disciples[i])
DiscipleManager.finish_recruitment(selected)
_close()
## 刷新提示文本与确定按钮状态。
func _update_ui() -> void:
if _mode == Mode.OPENING:
var picked := 0
for c in _check_buttons:
if c.button_pressed:
picked += 1
hint_label.text = "开局招募:请选择 %d 名弟子入宗(已选 %d/%d" % [
GameConfig.opening_pick_count, picked, _check_buttons.size(),
]
confirm_button.disabled = picked < GameConfig.opening_pick_count
else:
hint_label.text = "定期招募:勾选入宗弟子,未勾选的将被拒绝(可全部拒绝)"
confirm_button.disabled = false
## 关闭弹窗:清空条目并恢复时间推进。
func _close() -> void:
_clear_entries()
visible = false
TimeSystem.set_paused(false)
## 清空候选条目。
func _clear_entries() -> void:
for child in list_container.get_children():
child.queue_free()
_check_buttons.clear()
_entry_disciples.clear()
## 添加一名候选的行条目:信息文本 + 入宗勾选框。
## @param d: 候选弟子
func _add_entry(d: Disciple) -> void:
var row := HBoxContainer.new()
var info := Label.new()
info.text = "%s %d岁 | %s资质%s | %s灵根 | %s" % [
d.name, d.age, d.get_aptitude_name(),
"·天灵根" if d.is_heavenly else "",
d.get_roots_text(), d.get_realm_text(),
]
info.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var check := CheckButton.new()
check.text = "入宗"
check.toggled.connect(_on_check_toggled.bind(d))
row.add_child(info)
row.add_child(check)
list_container.add_child(row)
_check_buttons.append(check)
_entry_disciples.append(d)
@@ -0,0 +1 @@
uid://byjqc1ssqulc5
+168
View File
@@ -0,0 +1,168 @@
## 存档面板(挂载于 SavePanel.tscn)。
## 两级浏览:左侧存档组列表 → 右侧该组槽位列表。
## 存档模式(游戏内):点"新建存档"每次创建新槽位;读档模式(主菜单):点槽位读档。
extends PanelContainer
## 面板模式。
enum Mode { SAVE, LOAD }
## 请求关闭面板(返回上级界面)。
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:
close_panel()
if mode == Mode.LOAD:
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()
+1
View File
@@ -0,0 +1 @@
uid://boao04d7accm
+69
View File
@@ -0,0 +1,69 @@
[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="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]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_oi6jj")
[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=1850400883]
layout_mode = 2
theme_override_constants/separation = 16
[node name="Header" type="HBoxContainer" parent="VBoxContainer" unique_id=2020722155]
layout_mode = 2
[node name="TitleLabel" type="Label" parent="VBoxContainer/Header" unique_id=784512345]
unique_name_in_owner = true
layout_mode = 2
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 28
text = "选择存档"
[node name="Spacer" type="Control" parent="VBoxContainer/Header" unique_id=784512346]
layout_mode = 2
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]
layout_mode = 2
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 22
text = "返回"
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer" unique_id=500577526]
layout_mode = 2
size_flags_vertical = 3
size_flags_stretch_ratio = 9.0
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/ScrollContainer" unique_id=596966633]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="GroupContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer/HBoxContainer" unique_id=844163531]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 8
[node name="SlotList" type="VBoxContainer" parent="VBoxContainer/ScrollContainer/HBoxContainer" unique_id=674823582]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 5.0
theme_override_constants/separation = 8
[connection signal="button_up" from="VBoxContainer/Header/Button_NewSlot" to="." method="_on_button_new_slot_button_up"]
[connection signal="button_up" from="VBoxContainer/Header/Button_Esc" to="." method="_on_button_esc_button_up"]
+55
View File
@@ -0,0 +1,55 @@
## 存档槽位条目(挂载于 SaveSlot.tscn)。
## 展示单个存档槽位信息,支持点击读档与请求删除。
extends PanelContainer
## 槽位被点击(读档)时发出。
## @param slot_name: 槽文件名(不含 .json
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 save_name_label: Label = %SaveNameLabel
@onready var meta_line1: Label = %MetaLine1
@onready var meta_line2: Label = %MetaLine2
@onready var delete_button: Button = %DeleteButton
@onready var arrow_button: Button = %ArrowIcon
## 本槽位文件名(不含 .json)。
var slot_name: String = ""
## 是否已有存档数据。
var has_data: bool = false
## 填充为已有存档槽位。
## @param name: 槽文件名(不含 .json
## @param index_text: 序号文本(如 "1"、"自动"
## @param name_text: 槽位显示名(如 "存档 1"、"自动存档"
## @param meta1: 第一行元数据
## @param meta2: 第二行元数据
func setup(name: String, index_text: String, name_text: String, meta1: String, meta2: String) -> void:
slot_name = name
has_data = true
slot_index_label.text = index_text
save_name_label.text = name_text
meta_line1.text = meta1
meta_line2.text = meta2
delete_button.visible = true
arrow_button.visible = true
## 槽位本体被点击(读档/回到进度点)。
func _on_pressed() -> void:
slot_pressed.emit(slot_name)
## 点击箭头按钮(读档)。
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:
delete_requested.emit(slot_name)
+1
View File
@@ -0,0 +1 @@
uid://b0uq6cg05y428
+58
View File
@@ -0,0 +1,58 @@
[gd_scene format=3 uid="uid://dk7ggyngkyt18"]
[ext_resource type="Script" uid="uid://b0uq6cg05y428" path="res://src/UI/Panels/SaveSlot.gd" id="1_ss0t"]
[node name="SaveSlot" type="PanelContainer" unique_id=214547620]
anchors_preset = 14
anchor_top = 0.5
anchor_right = 1.0
anchor_bottom = 0.5
offset_top = -4.0
offset_bottom = 4.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1_ss0t")
[node name="HBoxContainer" type="HBoxContainer" parent="." unique_id=2068970062]
layout_mode = 2
[node name="SlotIndexLabel" type="Label" parent="HBoxContainer" unique_id=945932834]
unique_name_in_owner = true
custom_minimum_size = Vector2(40, 0)
layout_mode = 2
size_flags_horizontal = 3
vertical_alignment = 1
[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer" unique_id=94051654]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 15.0
[node name="SaveNameLabel" type="Label" parent="HBoxContainer/VBoxContainer" unique_id=131729571]
unique_name_in_owner = true
layout_mode = 2
[node name="MetaLine1" type="Label" parent="HBoxContainer/VBoxContainer" unique_id=1955990569]
unique_name_in_owner = true
layout_mode = 2
[node name="MetaLine2" type="Label" parent="HBoxContainer/VBoxContainer" unique_id=1454183959]
unique_name_in_owner = true
layout_mode = 2
[node name="DeleteButton" type="Button" parent="HBoxContainer" unique_id=1697002131]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "删除"
[node name="ArrowIcon" type="Button" parent="HBoxContainer" unique_id=540783971]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "▶"
[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"]