Compare commits

...
33 Commits
Author SHA1 Message Date
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
53 changed files with 2839 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"
}
+108 -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,101 @@
│ │ │ ├── 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-D5) |
| 建筑系统 | 规划中 | 建造/产出/升级(D6) |
| 存档接入 | 规划中 | SaveSystem 序列化(D8 |
> 进度跟踪见 `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),返回按钮回主菜单
## 命名规范
| 类别 | 格式 | 示例 |
| -------- | ------------------ | ----------------------- |
| 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.

After

Width:  |  Height:  |  Size: 1.1 MiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://caeqoc6wx8toy"
path="res://.godot/imported/BG1.png-6bf4b9c36da6bbdf71bd2c8203f4cb53.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/ui/BG1.png"
dest_files=["res://.godot/imported/BG1.png-6bf4b9c36da6bbdf71bd2c8203f4cb53.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 个小境(初期/中期/后期/圆满),均需小突破。小突破资源消耗远低于大境界突破,但失败也会有小幅修为折损。
- **修炼时间**:小境内积累可设为随时间自动增长(类似挂机产出),瓶颈处需要玩家手动"冲关"。
---
## 四、修炼速度影响因素
| 因素 | 影响程度 | 说明 |
|------|---------|------|
| 灵根数量 | ★★★★★ | 灵根越少(如单灵根/天灵根),修炼速度越快 |
| 灵气浓度 | ★★★★ | 灵脉之地 > 普通区域 > 灵气稀薄之地 |
| 丹药辅助 | ★★★★ | 修炼类丹药可大幅加速 |
| 功法品阶 | ★★★★ | 高品功法提升灵力转化效率 |
| 名师指点 | ★★★ | 高阶修士传道可加速感悟 |
| 心境/道心 | ★★★ | 影响瓶颈突破概率 |
| 年龄 | ★★ | 接近寿元上限时修炼效率递减 |
---
## 五、参考资料
- 凡人修仙传(忘语):练气→筑基→金丹→元婴→化神→炼虚→合体→大乘→渡劫
- 遮天(辰东):轮海→道宫→四极→化龙→仙台
- 完美世界(辰东):搬血→洞天→化灵→铭纹→列阵→尊者→神火→真一→圣祭→天神→虚道→斩我→遁一→至尊
- 仙逆(耳根):凝气→筑基→结丹→元婴→化神→婴变→问鼎→阴虚阳实→碎涅→第二步/第三步
> 本作建议采用**凡人修仙传体系**作为基础框架,境界数量适中、认知度高。
+289
View File
@@ -0,0 +1,289 @@
# 弟子系统设计(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~3 根,互不重复) |
| `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 修炼循环使用,此处只定义不消费。
### 枚举二:灵根五行(SpiritRoot
金、木、水、火、土 五种。灵根数量影响修炼速度(根数越少越快),元素种类影响日后生产效率匹配(D6 建筑/职能系统使用),此处只生成不消费。
### 特殊灵根:天灵根
稀有灵根,独立于五行枚举之外,用 `is_heavenly` 标记:
| 项目 | 设定 |
|------|------|
| 生成概率 | 3%(远低于四类资质中"天骄"的 5%,天灵根可叠加于任意资质之上) |
| 灵根构成 | 仅一系灵根(五行之一),纯净单灵根 |
| 修炼速度 | 在资质倍率基础上**额外 +50%**(乘法叠加,D5 修炼循环消费) |
| 地位 | 宗门争抢的种子弟子,日后可挂靠特殊培养事件(W8 预留) |
> 天灵根与资质是两个独立维度:资质定天赋底子,天灵根是稀缺加成,二者随机独立。
### 境界字段说明
新弟子统一为练气一层(realm_index=0, sub_realm_index=0)。境界显示文本规则:
- 练气期:练气 N 层
- 筑基及以上:大境界 + 初/中/后/圆满
境界的成长、突破门槛、寿元计算全部留给 D5 修炼循环,D3 只负责初始状态。
### 入宗时间字段说明
- `join_year/month/day` 在**入宗瞬间**accept_applicant)快照 GameState 当前日期
- 候选池弟子不设(0),拒绝入宗不产生记录
- 本期只存不消费;入门天数换算(D5 修炼结算)、按入宗排序(列表)、资历事件(W8)后续使用
---
## 二、生成规则(随机性)
### 1. 姓名
- 姓氏池:24 个常用修仙风单姓(林、苏、叶、萧、顾、沈、楚、白……)
- 名用字池:24 个(清、玄、墨、尘、风、澜、霜、珏……)
- 规则:随机 1 个姓 + 1~2 个字(50% 概率双字名)
- 不保证唯一(同名弟子可共存,以 id 区分)
### 2. 年龄
12~18 岁均匀随机。寿元消耗、大限判断留给 D5。
### 3. 灵根
- 普通弟子:数量 1~3 根均匀随机,元素从五行中随机抽取、互不重复
- 天灵根:3% 概率判定为天灵根,灵根数量固定 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.1 年 = 36 天)生成 **3 名候选**,暂停时间弹窗
- **选择方式**:每个候选独立勾选"入宗",可全部接受、可全部拒绝(定期招募可 0~3 个)
- **结算**:点"确定" → 勾选的入宗、未勾选的拒绝 → 关闭弹窗 → **恢复时间**
- 所有参数(候选数、必选数、间隔)来自 `resources/game_config.cfg`,改配置即改难度
### 招募流程(文字时序)
1. 开局(MainGame 调用 `start_new_game()`)或倒计时归零(`_on_day_passed`
2. 生成候选弟子(先判天灵根 3% → 生成灵根 → 按权重掷资质 → 组名字),进入候选池
3. `applicants_generated(is_opening)` 信号发出 → 弹窗显示候选列表
4. 玩家勾选(开局模式:确定按钮在选满 3 名前禁用)
5. 点"确定" → `finish_recruitment(selected)` 结算 → 恢复时间
6. 控制台打印每位入宗弟子完整信息(姓名/年龄/资质/灵根/境界)—— 本期验收方式
---
## 四、DiscipleManager 接口
| 接口 | 签名 | 说明 |
|------|------|------|
| `start_new_game()` | 无 | 清空全部数据 → 开局候选(配置数量)→ 暂停时间 |
| `generate_applicants(count, is_opening)` | 返回无 | 清空候选池 → 生成 count 名候选 → 发信号 |
| `accept_applicant(d)` | 入参候选 | 分配 ID,候选池 → 名录,发信号,控制台打印 |
| `reject_applicant(d)` | 入参候选 | 从候选池移除 |
| `finish_recruitment(selected)` | 入参选中列表 | 结算:selected 内的入宗,其余全部拒绝 |
| `remove_disciple(d)` | 入参弟子 | 移出名录(逐出/陨落) |
| `reset()` | 无 | 清空名录与候选池、ID 与倒计时复位 |
| 信号 `disciples_changed` | 无参数 | 名录增删/重置时发出 |
| 信号 `applicants_generated(is_opening)` | bool | 候选生成时发出(弹窗监听) |
内部私有方法(不对外):生成姓名、生成灵根(含天灵根判定)、掷资质、创建候选、间隔换算。
内部定时机制:
- 字段 `_days_until_recruit`:距下次定期招募的剩余游戏天数
- 间隔 = `GameConfig.recruit_interval_years` × 360 天(TimeSystem 常量换算)
- 监听 `TimeSystem.day_passed`,每日减 1,归零 → 生成候选 → `TimeSystem.set_paused(true)`(弹窗期间时间冻结)
存放位置:`src/Character/`(与数据模型同目录),注册为 Autoload 单例,供全局访问。
## 四·五、配置系统(GameConfig
所有可调参数集中在 `resources/game_config.cfg`,由 `src/Core/GameConfig.gd`Autoload,声明在 DiscipleManager 之前)启动时读取,缺项回退代码默认值。
```ini
[recruitment]
difficulty="normal" ; 当前难度档位
opening_applicant_count=5 ; 开局候选人数
opening_pick_count=3 ; 开局必须选满数
recruit_interval_years=0.1 ; 定期招募间隔(游戏年)
recruit_applicant_count=3 ; 定期弹窗候选人数
[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 名入宗(控制台打印),时间恢复推进
- [ ] **定期**:每 36 游戏日(0.1 年 @1x ≈ 18 秒)再次暂停弹窗,显示 3 名候选
- [ ] 定期模式可勾选 0~3 名,全部拒绝也可确定;时间正常恢复
- [ ] 多次招募后能观察到天灵根弟子(约 3% 概率,弹窗/控制台有标记)
- [ ] 修改 `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 自动响应数值变化
---
## 六、后续可扩展资源
- 灵兽蛋 / 灵兽材料
- 阵法材料(阵旗、符石)
- 秘境钥匙 / 地图碎片
- 宗门特产(专属资源)
+72
View File
@@ -0,0 +1,72 @@
# 开发计划表
> 开始日期:2026-08-03(周一) 每日有效开发约 2 小时
> 进度更新:2026-08-06  D1–D4 完成,D5(修炼循环)明日进行
## 一、每日作息模板(工作日)
| 时间 | 事项 | 备注 |
| ---- | ---- | ---- |
| 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候选,配置驱动含天灵根3%;新增 GameConfig 配置系统) |
| 8/6 四 | D4 | 弟子列表 UI | ① 列表条目场景 ② 姓名/资质/境界显示 ③ 点击详情 | 列表可浏览弟子 | ✅ 完成(管理按钮入口,名录面板+详情弹窗,天灵根标记,打开面板暂停时间) |
| 8/7 五 | D5 | 修炼循环 | ① 修为随时间增长(资质×灵气) ② 境界门槛 ③ 突破判定 | 弟子自动修炼,可突破 | ⬜ |
| 8/8 六 | D6 | 建筑系统基础 | ① 灵石矿场景 ② 建造→每时辰产出 ③ 升级 | 建筑产灵石,升级加速 | ⬜ |
| 8/9 日 | D7 | 休息 / 补漏 | 修本周 bug,整理代码 | git 干净 | ⬜ |
| 8/10 一 | D8 | 存档接入 | ① SaveSystem JSON 序列化 ② 存/读接口 ③ 替换 SavePanel 测试数据 | 读档后数据还原 | ⬜ |
| 8/11 二 | D9 | 打磨 | 数值平衡初调,玩 10 分钟验证 | 无致命 bug | ⬜ |
| 8/12 三 | D10 | **M1 验收** | 完整跑一遍:新游戏→招弟子→挂机→存→退→读 | 闭环可玩 | ⬜ |
## 三、第 35 周(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="scenes"
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}'"
+25 -3
View File
@@ -11,18 +11,36 @@ config_version=5
[application]
config/name="模拟宗门"
run/main_scene="uid://d1odycoaqhhyg"
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"
[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 +48,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
+17
View File
@@ -0,0 +1,17 @@
; 模拟宗门 全局配置
; 所有可调参数集中在此,修改后重启游戏生效
; 注释以 ; 开头,参数按系统分节
[recruitment]
difficulty="normal"
opening_applicant_count=5
opening_pick_count=3
recruit_interval_years=1
recruit_applicant_count=3
; 各难度资质权重(下品/中品/上品/天骄)
[recruitment_normal]
aptitude_weights=[45, 35, 15, 5]
[recruitment_hard]
aptitude_weights=[60, 30, 8, 2]
+51
View File
@@ -0,0 +1,51 @@
## 弟子数据模型(纯数据,不挂在场景树上)。
## 生成时决定的属性(姓名/资质/灵根)不可变,成长属性(境界)由 D5 修炼系统推进。
class_name Disciple # 全局类名:全项目可直接用 Disciple.new()
extends Resource # Resource = 轻量纯数据对象,可序列化(D8 存档用)
## 资质四类(入宗分流结果)。
enum Aptitude { ORDINARY, MEDIUM, SUPERIOR, HEAVENLY } # 下品/中品/上品/天骄
## 灵根五行。
enum SpiritRoot { GOLD, WOOD, WATER, FIRE, EARTH }
# —— 常量表:枚举值 → 显示文本 / 倍率(按索引一一对应)——
const APTITUDE_NAMES: Array[String] = ["下品", "中品", "上品", "天骄"]
const APTITUDE_SPEED: Array[float] = [0.6, 1.0, 1.5, 2.0] # D5 用
const ROOT_NAMES: Array[String] = ["", "", "", "", ""]
const REALM_NAMES: Array[String] = ["练气", "筑基", "金丹", "元婴", "化神"] # 按 realm_index 索引
const HEAVENLY_BONUS := 1.5 # 天灵根额外修炼速度倍率(D5 用)
# —— 实例字段 ——
var id: int = 0 # 唯一编号(DiscipleManager 分配)
var name: String = ""
var age: int = 0
var aptitude: Aptitude = Aptitude.ORDINARY
var roots: Array[SpiritRoot] = [] # 普通弟子 1~3 根;天灵根固定 1 根
var is_heavenly: bool = false # 天灵根标记(3% 概率)
var 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
# —— 辅助函数:把数据翻译成给人看的文本 ——
func get_aptitude_name() -> String:
return APTITUDE_NAMES[aptitude]
func get_roots_text() -> String:
var names: Array[String] = []
for root in roots:
names.append(ROOT_NAMES[root])
return "".join(names)
func get_realm_text() -> String:
if realm_index == 0:
return "%s%d" % [REALM_NAMES[realm_index], sub_realm_index + 1]
return "%s%s" % [REALM_NAMES[realm_index], ["初期", "中期", "后期", "圆满"][sub_realm_index]]
## 入宗时间文本,如 "1年2月3日";未入宗返回空串。
func get_join_date_text() -> String:
if join_year <= 0:
return ""
return "%d%d%d" % [join_year, join_month, join_day]
+1
View File
@@ -0,0 +1 @@
uid://cuybveud5w308
+165
View File
@@ -0,0 +1,165 @@
## 弟子管理器(Autoload 单例)。
## 负责候选弟子生成、入宗选择与宗门名录维护。
## 招募流程:开局/定期生成候选 → 暂停时间 → 玩家在弹窗选择 → 结算入名录。
extends Node
## 名录变化时发出(增删/重置)。
signal disciples_changed
## 候选弟子生成时发出(开局/定期,弹窗 UI 监听此信号)。
## @param is_opening: 是否开局招募(开局必须选满,定期自由选择)
signal applicants_generated(is_opening: bool)
## 宗门弟子名录(已入宗)。
var disciples: Array[Disciple] = []
## 候选弟子池(未入宗,等待玩家选择)。
var applicants: Array[Disciple] = []
## 天灵根生成概率(3%)。
const HEAVENLY_CHANCE := 0.03
## 姓氏池。
const SURNAMES: Array[String] = [
"", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "",
]
## 名用字池(修仙风)。
const GIVEN_CHARS: Array[String] = [
"", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "",
"", "", "", "屿", "", "", "", "",
]
## 距下次定期招募的剩余游戏天数。
var _days_until_recruit := 0
## 下一位入宗弟子的 ID(自增,重置归零)。
var _next_id := 1
func _ready() -> void:
# 挂到时间系统上:每日推进时倒计时,满间隔生成候选并暂停时间
TimeSystem.day_passed.connect(_on_day_passed)
_days_until_recruit = _get_interval_days()
## 每日推进:定期招募倒计时减 1,归零时生成候选并暂停时间。
func _on_day_passed(_year: int, _month: int, _day: int) -> void:
_days_until_recruit -= 1
if _days_until_recruit <= 0:
_days_until_recruit = _get_interval_days()
generate_applicants(GameConfig.recruit_applicant_count)
TimeSystem.set_paused(true)
## 开局新游戏:清空所有数据,生成开局候选(数量来自配置),暂停时间等待选择。
func start_new_game() -> void:
reset()
generate_applicants(GameConfig.opening_applicant_count, true)
TimeSystem.set_paused(true)
## 生成一批候选弟子:清空旧候选后按 count 生成,发信号供弹窗显示。
## @param count: 候选人数(调用方从 GameConfig 传入)
## @param is_opening: 是否开局招募
func generate_applicants(count: int, is_opening := false) -> void:
applicants.clear()
for i in count:
applicants.append(_create_applicant())
applicants_generated.emit(is_opening)
## 接受一名候选弟子:分配 ID、快照入宗时间并移入名录(候选池 → 名录)。
## @param d: 要接受的候选
func accept_applicant(d: Disciple) -> void:
if not applicants.has(d):
return
applicants.erase(d)
d.id = _next_id
_next_id += 1
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()
## 定期招募间隔(游戏天数),由 GameConfig 的"年"换算而来。
func _get_interval_days() -> int:
var days_per_year := TimeSystem.MONTHS_PER_YEAR * TimeSystem.DAYS_PER_MONTH
return maxi(1, roundi(GameConfig.recruit_interval_years * days_per_year))
## 创建一名候选弟子:随机生成属性(姓名/年龄/灵根/资质),
## 不分配 ID——ID 在入宗时由 accept_applicant 分配,保证连续且无浪费。
func _create_applicant() -> Disciple:
var d := Disciple.new()
d.name = _generate_name()
d.age = randi_range(12, 18)
d.is_heavenly = randf() < HEAVENLY_CHANCE
d.roots = _generate_roots(d.is_heavenly)
d.aptitude = _roll_aptitude()
return d
## 随机生成姓名:单姓 + 1~2 个名用字。
func _generate_name() -> String:
var surname := SURNAMES[randi() % SURNAMES.size()]
var given := GIVEN_CHARS[randi() % GIVEN_CHARS.size()]
if randf() < 0.5:
given += GIVEN_CHARS[randi() % GIVEN_CHARS.size()]
return surname + given
## 随机生成灵根。
## 天灵根:固定 1 根五行;普通弟子:1~3 根互不重复(根数越少修炼越快)。
## @param heavenly: 是否天灵根
func _generate_roots(heavenly: bool) -> Array[Disciple.SpiritRoot]:
if heavenly:
return [randi() % Disciple.SpiritRoot.size() as Disciple.SpiritRoot]
var count := randi_range(1, 3)
var pool := Disciple.SpiritRoot.values()
pool.shuffle()
var result: Array[Disciple.SpiritRoot] = []
for i in count:
result.append(pool[i])
return result
## 按权重掷资质(入宗分流),权重来自 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
+58
View File
@@ -0,0 +1,58 @@
## 游戏全局配置(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 天)。
var recruit_interval_years := 0.1
## 定期弹窗候选人数。
var recruit_applicant_count := 3
## 各难度资质权重表(键 = 难度档位,值 = 下品/中品/上品/天骄 权重)。
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)
# 逐档读取难度权重(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
+30
View File
@@ -0,0 +1,30 @@
## 全局游戏状态(Autoload 单例)。
## 负责场景间参数传递与会话状态管理,不持有游戏内数据。
extends Node
## 进入游戏的方式。
enum GameMode { NEW_GAME, LOAD_GAME }
## 当前进入游戏的方式。
var mode: GameMode = GameMode.NEW_GAME
## 要加载的存档 ID(仅 LOAD_GAME 时有效)。
var save_id: String = ""
# 游戏内时间(12 月 × 30 天 = 360 天/年),实际推进由 TimeSystem 负责
var year: int = 1
var month: int = 1
var day: int = 1
## 请求开始新游戏:重置模式与时间。
func request_new_game() -> void:
mode = GameMode.NEW_GAME
save_id = ""
year = 1
month = 1
day = 1
## 请求加载指定存档。
## @param id: 存档 ID
func request_load_game(id: String) -> void:
mode = GameMode.LOAD_GAME
save_id = id
+1
View File
@@ -0,0 +1 @@
uid://d1ajv537oir2l
+19
View File
@@ -0,0 +1,19 @@
## 主游戏世界场景入口(挂载于 MainGame.tscn)。
## 进入游戏时根据 GameState 模式初始化,并连接顶部 HUD 信号。
extends Node
func _ready() -> void:
# 注册为游戏世界场景(供 TimeSystem 判断是否推进时间)
add_to_group("game_scene")
# 应用用户设置中的默认流速
TimeSystem.apply_default_speed()
# 开局招募:清空旧数据 → 生成 5 名候选 → 暂停时间等待选择(读档流程接入后由存档还原)
DiscipleManager.start_new_game()
## 顶部 HUD 的返回主菜单请求。
func _on_top_bar_hud_sign_return_mainmenu() -> void:
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()
+1
View File
@@ -0,0 +1 @@
uid://centrk8l54j7c
+19
View File
@@ -0,0 +1,19 @@
[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://dl0tb68vsw3c6" path="res://src/UI/HUD/TopBarHUD.tscn" id="1_g6xmk"]
[ext_resource type="PackedScene" uid="uid://d4y7q3m6k2x5n7w2" path="res://src/UI/Panels/RecruitPanel/RecruitPanel.tscn" id="1_k5nrm"]
[ext_resource type="PackedScene" path="res://src/UI/Panels/DisciplesPanel/DisciplesPanel.tscn" id="1_d5cpm"]
[node name="MainGame" type="Node" unique_id=2053561265]
groups = ["game_scene"]
script = ExtResource("1_8bu23")
[node name="TopBarHud" parent="." unique_id=1115394694 instance=ExtResource("1_g6xmk")]
[node name="RecruitPanel" parent="." unique_id=770000016 instance=ExtResource("1_k5nrm")]
[node name="DisciplesPanel" parent="." unique_id=780000015 instance=ExtResource("1_d5cpm")]
[connection signal="sign_return_mainmenu" from="TopBarHud" to="." method="_on_top_bar_hud_sign_return_mainmenu"]
[connection signal="sign_open_disciples" from="TopBarHud" to="." method="_on_top_bar_hud_sign_open_disciples"]
+25
View File
@@ -0,0 +1,25 @@
## 宗门资源管理器(Autoload 单例)。
extends Node
signal spirit_stones_changed(new_value: int, delta: int)
signal reputation_changed(new_value: int, delta: int)
var spirit_stones := 0 # 灵石
var reputation := 0 # 声望
func add_spirit_stones(amount: int) -> void:
spirit_stones += amount
emit_signal("spirit_stones_changed", spirit_stones, amount)
func remove_spirit_stones(amount: int) -> void:
spirit_stones -= amount
emit_signal("spirit_stones_changed", spirit_stones, -amount)
func add_reputation(amount: int) -> void:
reputation += amount
emit_signal("reputation_changed", reputation, amount)
func remove_reputation(amount: int) -> void:
reputation -= amount
emit_signal("reputation_changed", reputation, -amount)
+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
+139
View File
@@ -0,0 +1,139 @@
## 全局时间系统(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
+167
View File
@@ -0,0 +1,167 @@
[gd_scene format=3 uid="uid://dl0tb68vsw3c6"]
[ext_resource type="FontFile" uid="uid://drt448qncx27p" path="res://assets/fonts/dingliezhuhaifont-20240831GengXinBan)-2.ttf" id="1_pa2rn"]
[ext_resource type="Script" uid="uid://d3o1ssph7ca58" path="res://src/UI/HUD/top_bar_hud.gd" id="1_vm4eo"]
[sub_resource type="LabelSettings" id="LabelSettings_bn2o5"]
font = ExtResource("1_pa2rn")
font_size = 32
[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="HBoxContainer" type="HBoxContainer" parent="." unique_id=1608257100]
layout_mode = 1
anchors_preset = 10
anchor_right = 1.0
offset_bottom = 23.0
grow_horizontal = 2
[node name="Label_Currency" type="Label" parent="HBoxContainer" unique_id=766096596]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
text = "灵石"
label_settings = SubResource("LabelSettings_bn2o5")
[node name="Value_Currency" type="Label" parent="HBoxContainer" unique_id=1150573727]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
size_flags_stretch_ratio = 2.0
label_settings = SubResource("LabelSettings_bn2o5")
[node name="Label_Time" type="Label" parent="HBoxContainer" unique_id=671872515]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
text = "时间"
label_settings = SubResource("LabelSettings_bn2o5")
[node name="Value_Time" type="Label" parent="HBoxContainer" unique_id=1229577188]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
size_flags_stretch_ratio = 2.0
label_settings = SubResource("LabelSettings_bn2o5")
[node name="Label_Reputation" type="Label" parent="HBoxContainer" unique_id=985110673]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
text = "声望"
label_settings = SubResource("LabelSettings_bn2o5")
[node name="Value_Reputation" type="Label" parent="HBoxContainer" unique_id=2011931119]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
size_flags_stretch_ratio = 2.0
label_settings = SubResource("LabelSettings_bn2o5")
[node name="SpeedGroup" type="HBoxContainer" parent="HBoxContainer" 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="HBoxContainer/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_override_fonts/font = ExtResource("1_pa2rn")
theme_override_font_sizes/font_size = 32
text = "暂停"
[node name="Button_Speed1" type="Button" parent="HBoxContainer/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_override_fonts/font = ExtResource("1_pa2rn")
theme_override_font_sizes/font_size = 32
text = "1x"
[node name="Button_Speed5" type="Button" parent="HBoxContainer/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_override_fonts/font = ExtResource("1_pa2rn")
theme_override_font_sizes/font_size = 32
text = "5x"
[node name="Button_Speed20" type="Button" parent="HBoxContainer/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_override_fonts/font = ExtResource("1_pa2rn")
theme_override_font_sizes/font_size = 32
text = "20x"
[node name="placeholder_1" type="Label" parent="HBoxContainer" unique_id=1042446504]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
label_settings = SubResource("LabelSettings_bn2o5")
[node name="placeholder_2" type="Label" parent="HBoxContainer" unique_id=1526548351]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
label_settings = SubResource("LabelSettings_bn2o5")
[node name="placeholder_3" type="Label" parent="HBoxContainer" unique_id=1170426663]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
label_settings = SubResource("LabelSettings_bn2o5")
[node name="placeholder_4" type="Label" parent="HBoxContainer" unique_id=1878816574]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
label_settings = SubResource("LabelSettings_bn2o5")
[node name="Button_StaffManagement" type="Button" parent="HBoxContainer" unique_id=568204305]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 4
theme_override_fonts/font = ExtResource("1_pa2rn")
theme_override_font_sizes/font_size = 32
text = "管理"
[node name="Button_ResourcesManagement" type="Button" parent="HBoxContainer" unique_id=1424251140]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 4
theme_override_fonts/font = ExtResource("1_pa2rn")
theme_override_font_sizes/font_size = 32
text = "资源"
[node name="Button_Return" type="Button" parent="HBoxContainer" unique_id=867839141]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 4
theme_override_fonts/font = ExtResource("1_pa2rn")
theme_override_font_sizes/font_size = 32
text = "返回"
[connection signal="button_up" from="HBoxContainer/SpeedGroup/Button_Pause" to="." method="_on_button_pause_button_up"]
[connection signal="button_up" from="HBoxContainer/Button_StaffManagement" to="." method="_on_button_staff_management_button_up"]
[connection signal="button_up" from="HBoxContainer/Button_Return" to="." method="_on_button_return_button_up"]
+71
View File
@@ -0,0 +1,71 @@
## 顶部信息栏 HUD(挂载于 TopBarHUD.tscn)。
## 显示灵石/时间/声望,提供时间流速控制与返回主菜单入口。
extends Control
## 请求返回主菜单。
signal sign_return_mainmenu
## 请求打开弟子名录面板。
signal sign_open_disciples
@onready var time_label: Label = %Value_Time
@onready var currency_label: Label = %Value_Currency
@onready var reputation_label: Label = %Value_Reputation
@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)
currency_label.text = str(SectManager.spirit_stones)
reputation_label.text = str(SectManager.reputation)
time_label.text = TimeSystem.get_date_text()
_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)
## 点击"返回"按钮。
func _on_button_return_button_up() -> void:
sign_return_mainmenu.emit()
## 点击"管理"按钮:请求打开弟子名录面板(由 MainGame 处理)。
func _on_button_staff_management_button_up() -> void:
sign_open_disciples.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()
## 刷新速度组 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,79 @@
[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="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"]
@@ -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(640, 340)
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,29 @@
## 弟子详情弹窗(挂载于 DiscipleDetailPanel.tscn)。
## 静态展示弟子信息(资质/年龄/灵根/境界/编号/入宗时间),
## 修炼进度等动态内容留待 D5 修炼循环填充。
extends Control
@onready var title_label: Label = %TitleLabel
@onready var info_label: Label = %InfoLabel
func _ready() -> void:
visible = false
## 显示指定弟子的详情。
## @param d: 要展示的弟子
func show_detail(d: Disciple) -> void:
title_label.text = d.name + ("(天灵根)" if d.is_heavenly else "")
info_label.text = "名字:%s\n资质:%s\n年龄:%d\n灵根:%s\n境界:%s\n入宗时间:%s" % [
d.name,
d.get_aptitude_name(),
d.age,
d.get_roots_text(),
d.get_realm_text(),
d.get_join_date_text(),
]
visible = true
## 点击"关闭"按钮。
func _on_button_close_button_up() -> void:
visible = false
@@ -0,0 +1 @@
uid://da0d24brddyxk
@@ -0,0 +1,91 @@
## 弟子名录列表面板(挂载于 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
func _ready() -> void:
# 名录变化时刷新(面板打开期间也同步)
DiscipleManager.disciples_changed.connect(_on_disciples_changed)
visible = false
## 打开面板并刷新列表(暂停时间推进)。
func show_panel() -> void:
_refresh_list()
TimeSystem.set_paused(true)
visible = true
## 关闭面板(恢复时间推进;子层详情弹窗随父隐藏)。
func hide_panel() -> void:
visible = false
TimeSystem.set_paused(false)
## 点击遮罩关闭面板。
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(90, 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(130, 0)
row.add_child(aptitude_label)
var roots_label := Label.new()
roots_label.text = d.get_roots_text() + "灵根"
roots_label.custom_minimum_size = Vector2(160, 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 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
+25
View File
@@ -0,0 +1,25 @@
## 主菜单场景(挂载于 MainMenu.tscn)。
extends Control
## 点击"新游戏":重置 GameState 后进入游戏世界。
func MainMenu_NewGame_ButtonUp() -> void:
GameState.request_new_game()
get_tree().change_scene_to_file("res://src/Core/MainGame.tscn")
## 点击"读取存档":隐藏主按钮组并显示存档面板。
func MainMenu_LoadGame_ButtonUp() -> void:
$ButtonGroup.hide()
$SavePanel.show()
## 点击"设置"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
+83
View File
@@ -0,0 +1,83 @@
[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://caeqoc6wx8toy" path="res://assets/sprites/ui/BG1.png" id="1_hrti0"]
[ext_resource type="FontFile" uid="uid://drt448qncx27p" path="res://assets/fonts/dingliezhuhaifont-20240831GengXinBan)-2.ttf" id="3_q7typ"]
[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("1_hrti0")
[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_override_fonts/font = ExtResource("3_q7typ")
theme_override_font_sizes/font_size = 32
text = "新游戏"
[node name="LoadGame" type="Button" parent="ButtonGroup" unique_id=2093610529]
custom_minimum_size = Vector2(200, 48)
layout_mode = 2
theme_override_fonts/font = ExtResource("3_q7typ")
theme_override_font_sizes/font_size = 32
text = "读取存档"
[node name="Settings" type="Button" parent="ButtonGroup" unique_id=2045302694]
custom_minimum_size = Vector2(200, 48)
layout_mode = 2
theme_override_fonts/font = ExtResource("3_q7typ")
theme_override_font_sizes/font_size = 32
text = "设置"
[node name="Quit" type="Button" parent="ButtonGroup" unique_id=818112870]
custom_minimum_size = Vector2(200, 48)
layout_mode = 2
theme_override_fonts/font = ExtResource("3_q7typ")
theme_override_font_sizes/font_size = 32
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"]
+108
View File
@@ -0,0 +1,108 @@
## 招募弹窗(挂载于 RecruitPanel.tscn)。
## 监听候选生成信号自动弹出,玩家勾选后点"确定"结算入宗。
extends Control
## 招募模式:开局必须选满 / 定期自由选择。
enum Mode { OPENING, PERIODIC }
@onready var title_label: Label = %TitleLabel
@onready var hint_label: Label = %HintLabel
@onready var list_container: VBoxContainer = %ListContainer
@onready var confirm_button: Button = %Button_Confirm
## 当前招募模式。
var _mode: Mode = Mode.PERIODIC
## 勾选框列表(与 _entry_disciples 按下标一一对应)。
var _check_buttons: Array[CheckButton] = []
## 对应勾选框的候选弟子。
var _entry_disciples: Array[Disciple] = []
func _ready() -> void:
# 订阅候选生成信号:开局与定期招募都会触发
DiscipleManager.applicants_generated.connect(_on_applicants_generated)
visible = false
## 候选生成时弹出:按当前候选池重建列表。
## @param is_opening: 是否开局招募
func _on_applicants_generated(is_opening: bool) -> void:
_mode = Mode.OPENING if is_opening else Mode.PERIODIC
_clear_entries()
for d in DiscipleManager.applicants:
_add_entry(d)
title_label.text = "弟子入宗(%s" % ("开局招募" if is_opening else "定期招募")
_update_ui()
visible = true
## 勾选任一候选后刷新提示与确定按钮可用性。
func _on_check_toggled(_pressed: bool, _d: Disciple) -> void:
_update_ui()
## 全部接受:勾选所有候选。
func _on_button_accept_all_button_up() -> void:
for c in _check_buttons:
c.set_pressed_no_signal(true)
_update_ui()
## 全部拒绝:取消所有勾选。
func _on_button_reject_all_button_up() -> void:
for c in _check_buttons:
c.set_pressed_no_signal(false)
_update_ui()
## 确定:勾选的入宗、未勾选的拒绝,关闭弹窗并恢复时间。
func _on_button_confirm_button_up() -> void:
var selected: Array[Disciple] = []
for i in _entry_disciples.size():
if _check_buttons[i].button_pressed:
selected.append(_entry_disciples[i])
DiscipleManager.finish_recruitment(selected)
_close()
## 刷新提示文本与确定按钮状态。
func _update_ui() -> void:
if _mode == Mode.OPENING:
var picked := 0
for c in _check_buttons:
if c.button_pressed:
picked += 1
hint_label.text = "开局招募:请选择 %d 名弟子入宗(已选 %d/%d" % [
GameConfig.opening_pick_count, picked, _check_buttons.size(),
]
confirm_button.disabled = picked < GameConfig.opening_pick_count
else:
hint_label.text = "定期招募:勾选入宗弟子,未勾选的将被拒绝(可全部拒绝)"
confirm_button.disabled = false
## 关闭弹窗:清空条目并恢复时间推进。
func _close() -> void:
_clear_entries()
visible = false
TimeSystem.set_paused(false)
## 清空候选条目。
func _clear_entries() -> void:
for child in list_container.get_children():
child.queue_free()
_check_buttons.clear()
_entry_disciples.clear()
## 添加一名候选的行条目:信息文本 + 入宗勾选框。
## @param d: 候选弟子
func _add_entry(d: Disciple) -> void:
var row := HBoxContainer.new()
var info := Label.new()
info.text = "%s %d岁 | %s资质%s | %s灵根 | %s" % [
d.name, d.age, d.get_aptitude_name(),
"·天灵根" if d.is_heavenly else "",
d.get_roots_text(), d.get_realm_text(),
]
info.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var check := CheckButton.new()
check.text = "入宗"
check.toggled.connect(_on_check_toggled.bind(d))
row.add_child(info)
row.add_child(check)
list_container.add_child(row)
_check_buttons.append(check)
_entry_disciples.append(d)
@@ -0,0 +1 @@
uid://byjqc1ssqulc5
+10
View File
@@ -0,0 +1,10 @@
## 存档选择面板(挂载于 SavePanel.tscn)。
## 显示存档列表与"新建存档"入口,通过信号通知外部结果。
extends PanelContainer
## 请求关闭面板(返回上级界面)。
signal back_pressed
## 点击"返回"按钮。
func _on_button_esc_button_up() -> void:
back_pressed.emit()
+1
View File
@@ -0,0 +1 @@
uid://boao04d7accm
+61
View File
@@ -0,0 +1,61 @@
[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="PackedScene" uid="uid://dk7ggyngkyt18" path="res://src/UI/Panels/SaveSlot.tscn" id="2_uloko"]
[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]
layout_mode = 2
theme_override_font_sizes/font_size = 24
text = "选择存档"
[node name="Spacer" type="Control" parent="VBoxContainer/Header" unique_id=784512346]
layout_mode = 2
size_flags_horizontal = 3
[node name="Button_Esc" type="Button" parent="VBoxContainer/Header" unique_id=866430190]
layout_mode = 2
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="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer/HBoxContainer" unique_id=844163531]
layout_mode = 2
size_flags_horizontal = 3
[node name="SaveGroup" type="Button" parent="VBoxContainer/ScrollContainer/HBoxContainer/VBoxContainer" unique_id=150349711]
layout_mode = 2
text = "存档1"
[node name="SlotList" type="VBoxContainer" parent="VBoxContainer/ScrollContainer/HBoxContainer" unique_id=674823582]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 5.0
[node name="SaveSlot" parent="VBoxContainer/ScrollContainer/HBoxContainer/SlotList" unique_id=214547620 instance=ExtResource("2_uloko")]
layout_mode = 2
size_flags_vertical = 1
[connection signal="button_up" from="VBoxContainer/Header/Button_Esc" to="." method="_on_button_esc_button_up"]
+52
View File
@@ -0,0 +1,52 @@
## 存档槽位条目(挂载于 SaveSlot.tscn)。
## 展示单个存档信息;空槽位用于新建存档。
extends PanelContainer
## 槽位被点击时发出,save_id 为空表示点击了新建槽位。
## @param save_id: 存档 ID
signal slot_pressed(save_id: 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
## 本槽位对应的存档 ID。
var save_id: String = ""
## 是否已有存档数据(false 表示新建槽位)。
var has_data: bool = false
## 填充为已有存档槽位。
## @param id: 存档 ID
## @param name_text: 存档名称
## @param meta1: 第一行元数据
## @param meta2: 第二行元数据
func setup(id: String, name_text: String, meta1: String, meta2: String) -> void:
save_id = id
has_data = true
slot_index_label.text = id.get_slice("save_", 1) # "save_001" -> "001"
save_name_label.text = name_text
meta_line1.text = meta1
meta_line2.text = meta2
delete_button.visible = true
arrow_button.visible = true
## 填充为"新建存档"空槽位。
func setup_empty() -> void:
has_data = false
slot_index_label.text = "+"
save_name_label.text = "新建存档"
meta_line1.text = "点击创建新的存档"
meta_line2.text = ""
delete_button.visible = false
arrow_button.visible = false
## 槽位本体被点击。
func _on_pressed() -> void:
slot_pressed.emit(save_id)
## 点击删除按钮(TODO:接入确认弹窗)。
func _on_delete_button_button_up() -> void:
print("删除存档: ", save_id)
+1
View File
@@ -0,0 +1 @@
uid://b0uq6cg05y428
+57
View File
@@ -0,0 +1,57 @@
[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"]