Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83fbd8fcfc | ||
|
|
a9de46f1dd | ||
|
|
ad72d36522 | ||
|
|
b687ab153d | ||
|
|
2bb1486df6 | ||
|
|
856661a687 | ||
|
|
5bc756d728 | ||
|
|
bfd79c6934 | ||
|
|
00c4495bca | ||
|
|
b85d7ef991 | ||
|
|
9e1bd055d5 | ||
|
|
8b31866d8e | ||
|
|
2317b425fe | ||
|
|
0f1f03f9ef | ||
|
|
c9af0f608e | ||
|
|
543af0a4ff | ||
|
|
19f8294242 | ||
|
|
5ad0272d79 | ||
|
|
8a01806ab0 | ||
|
|
dfd780f3bd | ||
|
|
b13c20a313 | ||
|
|
e283f6b2b7 | ||
|
|
2e447c868b | ||
|
|
6b5ec33d85 | ||
|
|
03c765ced8 | ||
|
|
cb9b73dd76 | ||
|
|
cfebb8dded | ||
|
|
c5c7c57799 | ||
|
|
a3cbd738ad | ||
|
|
a00c95af51 | ||
|
|
df52a0b22f | ||
|
|
cda7a09a09 | ||
|
|
b1f3d766b3 | ||
|
|
d907637606 | ||
|
|
5e5763028c | ||
|
|
41f50dd070 | ||
|
|
123bbf315a | ||
|
|
ee1448a954 | ||
|
|
559710f765 | ||
|
|
1cbfd12574 | ||
|
|
b6c76d83d7 | ||
|
|
476834176c | ||
|
|
021448a4b0 | ||
|
|
782b1f1701 | ||
|
|
19f81ceeec | ||
|
|
0320d98a73 | ||
|
|
2a39bc6473 | ||
|
|
404480ef00 | ||
|
|
451f868b83 | ||
|
|
a265e3a5cf | ||
|
|
d98cbb8266 | ||
|
|
72f0a6b8f7 | ||
|
|
17994ec4ae | ||
|
|
226d6992e9 |
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"godotTools.editorPath.godot4": "d:\\game\\Godot\\Godot_v4.7.1-stable_win64.exe\\Godot_v4.7.1-stable_win64.exe"
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
# 模拟宗门 — 项目目录结构
|
||||
# 模拟宗门
|
||||
|
||||
## 结构总览
|
||||
基于 Godot 4.7(GDScript)的宗门经营模拟游戏。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
模拟宗门/
|
||||
@@ -9,11 +11,13 @@
|
||||
│ ├── 修仙宗门设计文档.md
|
||||
│ ├── 宗门行政体系.md
|
||||
│ ├── 宗门礼仪建筑.md
|
||||
│ └── 游戏模块总览.md
|
||||
│ ├── 游戏模块总览.md
|
||||
│ ├── 存档界面UI设计.md
|
||||
│ └── 开发计划表.md # 开发排期与进度跟踪
|
||||
│
|
||||
├── addons/ # Godot 插件
|
||||
│
|
||||
├── assets/ # 原始资源文件(导入前素材)
|
||||
├── assets/ # 资源文件
|
||||
│ ├── sprites/ # 精灵图
|
||||
│ │ ├── buildings/
|
||||
│ │ ├── characters/
|
||||
@@ -22,141 +26,144 @@
|
||||
│ ├── 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/ # 具体建筑(按分类子目录)
|
||||
│ │ │ ├── admin/ # 行政类
|
||||
│ │ │ ├── education/ # 教育类
|
||||
│ │ │ ├── workshop/ # 工坊类
|
||||
│ │ │ ├── storage/ # 物资类
|
||||
│ │ │ ├── resource/ # 资源类
|
||||
│ │ │ ├── living/ # 生活类
|
||||
│ │ │ ├── ritual/ # 礼仪类
|
||||
│ │ │ └── utility/ # 功能类
|
||||
│ ├── Sect/ # 宗门管理(规划)
|
||||
│ │ ├── buildings/
|
||||
│ │ │ └── buildings/
|
||||
│ │ │ ├── admin/ # 行政类
|
||||
│ │ │ ├── education/ # 教育类
|
||||
│ │ │ ├── workshop/ # 工坊类
|
||||
│ │ │ ├── storage/ # 物资类
|
||||
│ │ │ ├── resource/ # 资源类
|
||||
│ │ │ ├── living/ # 生活类
|
||||
│ │ │ ├── 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` |
|
||||
| 资源文件 | snake_case | `iron_sword.tres` |
|
||||
| 精灵图 | snake_case | `building_office.png` |
|
||||
| 变量 | camelCase | `currentGold` |
|
||||
| 私有变量 | _camelCase | `_discipleList` |
|
||||
|
||||
> **注意**:C# 脚本的 `namespace` 使用 `项目名.模块名`,例如 `SectSim.Sect`、`SectSim.Character`。在 Godot 4 中,脚本路径决定了自动生成的默认命名空间,保持路径与命名空间一致可避免手动配置。
|
||||
| 类别 | 格式 | 示例 |
|
||||
| -------- | ---------- | ------------------- |
|
||||
| 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 | `bg1.png` |
|
||||
|
||||
Binary file not shown.
@@ -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={}
|
||||
Binary file not shown.
@@ -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={}
|
||||
Binary file not shown.
@@ -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={}
|
||||
Binary file not shown.
@@ -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={}
|
||||
Binary file not shown.
@@ -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={}
|
||||
Binary file not shown.
@@ -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={}
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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
|
||||
@@ -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` | 15(Full 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,垂直填充可用空间 |
|
||||
|
||||
### BG(TextureRect — 背景)
|
||||
|
||||
| 属性 | 值 | 说明 |
|
||||
|------|----|------|
|
||||
| `anchors_preset` | 15(Full Rect) | 铺满父节点 |
|
||||
| `anchor_right` / `anchor_bottom` | 1.0 | 锚定右下边界 |
|
||||
| `stretch_mode` | 0(Scale) | 拉伸图片铺满,保持原始比例 |
|
||||
|
||||
### Logo(TextureRect — 游戏 Logo)
|
||||
|
||||
| 属性 | 值 | 说明 |
|
||||
|------|----|------|
|
||||
| `anchors_preset` | 1(Top 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 | 向左收缩 |
|
||||
|
||||
### ButtonGroup(VBoxContainer — 按钮组)
|
||||
|
||||
| 属性 | 值 | 说明 |
|
||||
|------|----|------|
|
||||
| `anchors_preset` | 8(Center) | 居中锚定 |
|
||||
| `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` 序列化的存档文件
|
||||
@@ -0,0 +1,120 @@
|
||||
# 修仙资料 — 境界与突破
|
||||
|
||||
> 本文档收集、整理修仙体系中与修炼、突破相关的设定资料,供游戏设计参考。
|
||||
|
||||
---
|
||||
|
||||
## 一、境界总览
|
||||
|
||||
| 大境界 | 小境界划分 | 寿元上限 | 修炼参考时长 | 大境界突破需求 |
|
||||
|--------|-----------|----------|-------------|---------------|
|
||||
| 凡人 | — | ~80岁 | — | 引气入体即入练气 |
|
||||
| 练气期 | 一层~十三层 | ~150岁 | 每层 1~3 年 | 筑基丹 + 灵气充沛之地 |
|
||||
| 筑基期 | 初期 / 中期 / 后期 / 圆满 | ~250岁 | 每小境 10~20 年 | 结丹灵物 + 雷劫(小) |
|
||||
| 金丹期 | 初期 / 中期 / 后期 / 圆满 | ~600岁 | 每小境 30~50 年 | 元婴丹 / 化婴丹 + 心魔劫 |
|
||||
| 元婴期 | 初期 / 中期 / 后期 / 圆满 | ~1500岁 | 每小境 50~100 年 | 化神灵物 + 五行劫 |
|
||||
| 化神期 | 初期 / 中期 / 后期 / 圆满 | ~3000岁 | 每小境 100~200 年 | 炼虚感悟 + 天劫 |
|
||||
| 炼虚期 | 初期 / 中期 / 后期 / 圆满 | ~8000岁 | 每小境 200~500 年 | 合体契机 + 道心劫 |
|
||||
| 合体期 | 初期 / 中期 / 后期 / 圆满 | ~15000岁 | 每小境 500~1000 年 | 大乘机缘 |
|
||||
| 大乘期 | 初期 / 中期 / 后期 / 圆满 | ~30000岁 | 每小境 1000~3000 年 | 飞升之劫 |
|
||||
| 渡劫期(真仙劫) | — | 极长 | — | 渡过即飞升 |
|
||||
|
||||
---
|
||||
|
||||
## 二、大境界突破详情
|
||||
|
||||
### 2.1 凡人 → 练气期
|
||||
|
||||
- **突破方式**:引天地灵气入体,打通经脉。门槛较低,有灵根者皆可。
|
||||
- **所需资源**:灵气充裕的环境、基础引气口诀。
|
||||
- **失败后果**:轻微内伤,修养后可重试。
|
||||
|
||||
### 2.2 练气期 → 筑基期
|
||||
|
||||
- **突破方式**:压缩灵力、铸就道基,是踏入修仙正途的关键一步。
|
||||
- **所需资源**:
|
||||
- 筑基丹(必备,提升成功率 ~30% → ~60%)
|
||||
- 灵石 500~1000(用于布聚灵阵)
|
||||
- 灵气充沛之地(灵脉节点)
|
||||
- **突破难度**:高。散修筑基失败率极高。
|
||||
- **失败后果**:灵力反噬,修为跌落 2~3 层,半年内不可再次尝试。
|
||||
|
||||
### 2.3 筑基期 → 金丹期
|
||||
|
||||
- **突破方式**:丹田凝聚金丹,需渡过小型雷劫。
|
||||
- **所需资源**:
|
||||
- 结丹灵物(如千年灵乳、天火液等,依主修方向而定)
|
||||
- 灵石 3000~5000
|
||||
- 渡劫阵旗 / 防御法器(应对雷劫)
|
||||
- **突破难度**:极高。三成筑基修士止步于此。
|
||||
- **失败后果**:金丹碎裂,跌落至筑基初期,或身死道消。
|
||||
|
||||
### 2.4 金丹期 → 元婴期
|
||||
|
||||
- **突破方式**:金丹碎裂、元婴诞生,需对抗心魔劫。
|
||||
- **所需资源**:
|
||||
- 元婴丹 / 化婴丹(必备)
|
||||
- 灵石 10000+
|
||||
- 清心类法宝(抵御心魔)
|
||||
- 护法弟子或长老
|
||||
- **突破难度**:极高。心魔劫最为凶险。
|
||||
- **失败后果**:元婴无法凝聚,修为锐减至金丹初期,或道心崩溃疯癫。
|
||||
|
||||
### 2.5 元婴期 → 化神期
|
||||
|
||||
- **突破方式**:元婴与天地法则初步融合,渡过五行天劫。
|
||||
- **所需资源**:
|
||||
- 化神灵物(五行精华各一份)
|
||||
- 灵石 50000+
|
||||
- 渡劫大阵
|
||||
- **失败后果**:天劫之下魂飞魄散,或元婴受创沉睡百年。
|
||||
|
||||
### 2.6 化神期及之后
|
||||
|
||||
- 化神以上境界所需资源极为稀有,多为天地奇宝,非人力可量产。
|
||||
- 每层大境界突破均伴随天劫,强度递增。
|
||||
- 合体期以上几乎只存在于传说中。
|
||||
|
||||
---
|
||||
|
||||
## 三、小境界修炼与突破
|
||||
|
||||
### 3.1 小境界是否需要突破?
|
||||
|
||||
| 大境界 | 小境界突破方式 | 说明 |
|
||||
|--------|---------------|------|
|
||||
| 练气期 | 积累为主,每三层一个小瓶颈 | 1→2→3 顺畅;3→4 有小瓶颈需冲关;4→5→6 顺畅;6→7 有小瓶颈……依此类推 |
|
||||
| 筑基期 | 积累 + 小瓶颈突破 | 初期→中期、中期→后期有小瓶颈,需丹药辅助;后期→圆满为积累 |
|
||||
| 金丹期 | 积累 + 小瓶颈突破 | 同上 |
|
||||
| 元婴及以上 | 以感悟为主,积累为辅 | 越小境突破依赖法则领悟,资源需求退居次要 |
|
||||
|
||||
### 3.2 设计建议
|
||||
|
||||
- **练气期**:设为 13 层,部分层数需要"小突破"(消耗少量灵石或丹药),设计为引导玩家熟悉突破机制的教程阶段。
|
||||
- **筑基及以上**:每个大境界 4 个小境(初期/中期/后期/圆满),均需小突破。小突破资源消耗远低于大境界突破,但失败也会有小幅修为折损。
|
||||
- **修炼时间**:小境内积累可设为随时间自动增长(类似挂机产出),瓶颈处需要玩家手动"冲关"。
|
||||
|
||||
---
|
||||
|
||||
## 四、修炼速度影响因素
|
||||
|
||||
| 因素 | 影响程度 | 说明 |
|
||||
|------|---------|------|
|
||||
| 灵根数量 | ★★★★★ | 灵根越少(如单灵根/天灵根),修炼速度越快 |
|
||||
| 灵气浓度 | ★★★★ | 灵脉之地 > 普通区域 > 灵气稀薄之地 |
|
||||
| 丹药辅助 | ★★★★ | 修炼类丹药可大幅加速 |
|
||||
| 功法品阶 | ★★★★ | 高品功法提升灵力转化效率 |
|
||||
| 名师指点 | ★★★ | 高阶修士传道可加速感悟 |
|
||||
| 心境/道心 | ★★★ | 影响瓶颈突破概率 |
|
||||
| 年龄 | ★★ | 接近寿元上限时修炼效率递减 |
|
||||
|
||||
---
|
||||
|
||||
## 五、参考资料
|
||||
|
||||
- 凡人修仙传(忘语):练气→筑基→金丹→元婴→化神→炼虚→合体→大乘→渡劫
|
||||
- 遮天(辰东):轮海→道宫→四极→化龙→仙台
|
||||
- 完美世界(辰东):搬血→洞天→化灵→铭纹→列阵→尊者→神火→真一→圣祭→天神→虚道→斩我→遁一→至尊
|
||||
- 仙逆(耳根):凝气→筑基→结丹→元婴→化神→婴变→问鼎→阴虚阳实→碎涅→第二步/第三步
|
||||
|
||||
> 本作建议采用**凡人修仙传体系**作为基础框架,境界数量适中、认知度高。
|
||||
@@ -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 步)。
|
||||
@@ -0,0 +1,210 @@
|
||||
# 存档系统设计(D8:存档接入)
|
||||
|
||||
> 对应开发计划表 D8(8/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 | 读档箭头(点击读档) |
|
||||
|
||||
**交互规则**:
|
||||
|
||||
| 项 | 存档模式(游戏内) | 读档模式(主菜单) |
|
||||
|----|-------------------|-------------------|
|
||||
| 左侧组列表 | 当前组高亮(可切换查看,**组行带"删"按钮,当前组禁用**) | 可切换任意组(组行带"删"按钮) |
|
||||
| 槽位点击 | **回到该进度点**(加载所选槽,未保存进度丢失) | 点击读档进入游戏 |
|
||||
| 删除按钮 | 可删(确认后) | 可删(确认后) |
|
||||
| 暂停 | 打开时暂停,关闭恢复 | 不暂停 |
|
||||
|
||||
## 七、分步实现规划(小步可验证,每步独立跑通)
|
||||
|
||||
### 步骤 1:SaveSystem 骨架(新 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 自动刷新) |
|
||||
| 验证 | ✅ 存档文件内容完整(弟子/建筑/资源/日期全量) |
|
||||
|
||||
### 步骤 3:GameState + 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)` |
|
||||
| 验证 | 无头:存→清→读闭环,日期/资源/名录(境界修为饥饿)/建筑逐项一致 |
|
||||
|
||||
### 步骤 4:SavePanel 动态填充(复用现有手绘界面)✅
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 改动 | `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/2;4 依赖 1/3;5 依赖 4;6 为最终验证。
|
||||
|
||||
## 八、验收清单(D8 交付标准)
|
||||
|
||||
- [x] 主菜单"新游戏"自动创建新存档组(存档N)
|
||||
- [x] 游戏内"存档"面板点"新建存档"→ 该组新增一条槽位(文件生成,内容完整)
|
||||
- [x] 连续多次存档 → 同一组内多个槽位互不覆盖(序号命名,已验证 slot_1/2/3 内容独立)
|
||||
- [x] 返回主菜单自动写自动槽(auto.json)
|
||||
- [x] "读取存档"两级浏览:左侧组列表 → 右侧槽位列表(元数据显示)
|
||||
- [x] 读档后:日期/资源/名录(含境界/修为/饥饿)/建筑全部还原
|
||||
- [x] 读档进入**不弹招募窗、不暂停**
|
||||
- [x] 删除槽位后列表刷新;损坏/缺失 JSON 不崩溃(回退提示)
|
||||
- [x] 新游戏流程不受影响(仍弹开局招募);多组多槽共存
|
||||
@@ -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` 重建列表,打开时主动刷新
|
||||
|
||||
---
|
||||
|
||||
## 七、分步实现规划(小步可验证,每步独立跑通)
|
||||
|
||||
### 步骤 1:SectManager 加食物资源 + 开局资源发放 ✅
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 改动 | `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 接入 |
|
||||
|
||||
### 步骤 4:Building 模型 + 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 天离宗(日志"断粮日久,离宗而去"+名录移除);恢复清零 |
|
||||
|
||||
### 步骤 6:BuildingsPanel 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` |
|
||||
@@ -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 数据模型为纯数据,可直接序列化 |
|
||||
|
||||
---
|
||||
|
||||
## 七、弟子名录 UI(D4)
|
||||
|
||||
> 对应开发计划表 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 生效)
|
||||
@@ -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 内容。
|
||||
|
||||
## 十五、秘境与探索
|
||||
|
||||
*(待补充)*
|
||||
|
||||
弟子外出历练的机制,可探索区域的地图/节点设计,收益与风险。
|
||||
|
||||
## 十六、任务系统
|
||||
|
||||
*(待补充)*
|
||||
|
||||
日常任务、宗门任务、奇遇任务的触发机制与奖励结构。
|
||||
|
||||
## 十七、胜利与失败条件
|
||||
|
||||
*(待补充)*
|
||||
|
||||
游戏终局条件(跻身顶级宗门 / 飞升 / 被灭门 / 无限经营),多结局分支。
|
||||
@@ -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 自动响应数值变化
|
||||
|
||||
---
|
||||
|
||||
## 六、后续可扩展资源
|
||||
|
||||
- 灵兽蛋 / 灵兽材料
|
||||
- 阵法材料(阵旗、符石)
|
||||
- 秘境钥匙 / 地图碎片
|
||||
- 宗门特产(专属资源)
|
||||
@@ -0,0 +1,70 @@
|
||||
# 开发计划表
|
||||
|
||||
> 开始日期:2026-08-03(周一) 每日有效开发约 2 小时
|
||||
> 进度更新:2026-08-10 D1–D9 全部完成(D8 两级存档系统验收 9/9,D9 大境界突破 + 数值打磨),M1 验收(D10)进行中
|
||||
|
||||
## 一、每日作息模板(工作日)
|
||||
|
||||
| 时间 | 事项 | 备注 |
|
||||
| ---- | ---- | ---- |
|
||||
| 20:30 | 下班 | |
|
||||
| 20:30–21:00 | 通勤 / 晚饭 | |
|
||||
| 21:00–21:10 | 热身 | 回顾昨日进度,明确今晚目标 |
|
||||
| 21:10–22:40 | **开发黄金段** | 90 分钟,写代码/搭场景 |
|
||||
| 22:40–22:50 | 休息 | 离开屏幕 |
|
||||
| 22:50–23:20 | 收尾 | 跑通验证、git 提交、写明日 TODO |
|
||||
| 23:20–24:00 | 洗漱放松 | 睡前不刷代码 |
|
||||
|
||||
## 二、第 1–2 周(8/3–8/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/17–9/6):P2 扩展
|
||||
|
||||
| 日期 | 周 | 任务 | 拆解 | 交付 |
|
||||
| ---- | -- | ---- | ---- | ---- |
|
||||
| 8/17–8/23 | W3 | 财政经济 | ① 收支流水 ② 俸禄发放 ③ 价格波动 | 财政面板 |
|
||||
| 8/24–8/30 | W4 | 任务系统 | ① 任务生成 ② 贡献值 ③ 兑换商店 | 任务面板 |
|
||||
| 8/31–9/6 | W5 | 生产制造 + 战斗基础 | ① 炼丹工坊 ② 弟子组队 ③ 外敌入侵事件 | **M2 验收** |
|
||||
|
||||
## 四、第 6–10 周(9/7–10/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. 周末至少半天完全休息,防倦怠
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 ; 断粮累计天数上限(达到即离宗,待接入)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://q4pk0n21oawr
|
||||
@@ -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]
|
||||
@@ -0,0 +1 @@
|
||||
uid://cuybveud5w308
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://d2aj0gbvb6p4w
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://dlchibfd86oek
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://d1ajv537oir2l
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://centrk8l54j7c
|
||||
@@ -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"]
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bwt83xydsgcir
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c25eic5uquo2o
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://exwk46qy4x6t
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://c13ljuiktgrgw
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://6hi0wog110up
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://dmfhbyjwngqh3
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://drfyh36my6gj2
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://boao04d7accm
|
||||
@@ -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"]
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b0uq6cg05y428
|
||||
@@ -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"]
|
||||
Reference in New Issue
Block a user