Compare commits

..
25 Commits
Author SHA1 Message Date
Admin 83fbd8fcfc feat: 大境界突破——练气圆满手动花费灵石突破至筑基及以上(D9 完成);失败灵石不返还+修为倒退,配置驱动;修复筑基圆满进度文本越界;文档同步(修炼设计四章/计划表/README) 2026-08-10 00:55:05 +08:00
Admin a9de46f1dd docs: 全局脚本补充文档注释(14 个文件 29 处) 2026-08-09 20:21:47 +08:00
Admin ad72d36522 fix: 导出修复——主场景改路径引用、all_resources 全量打包、cfg 默认值同步;修复主菜单读档槽位点击 2026-08-09 20:18:01 +08:00
Admin b687ab153d feat: 两级存档系统——无限组/槽,建组建槽/自动档/两级浏览/读档还原/删除组与槽/游戏内加载(D8 完成) 2026-08-09 19:17:21 +08:00
Admin 2bb1486df6 docs: D6 验收清单 11/11 全部勾选 2026-08-09 17:37:19 +08:00
Admin 856661a687 docs: 计划表同步 D5/D6/D7 完成 2026-08-09 17:32:25 +08:00
Admin 5bc756d728 feat: 断粮离宗(方案B),D6 全部完成;灵石矿贴图 2026-08-09 17:22:34 +08:00
Admin bfd79c6934 fix: 管理面板关闭时恢复打开前暂停状态(不冲掉手动暂停) 2026-08-09 17:22:26 +08:00
Admin 00c4495bca feat: 口粮消耗与饥饿减半([food] 配置驱动) 2026-08-09 16:19:01 +08:00
Admin b85d7ef991 style: 建筑面板尺寸对齐名录面板;文档同步 D6 步骤3/4/6 完成 2026-08-09 16:16:44 +08:00
Admin 9e1bd055d5 feat: 建筑面板——资源按钮入口+建造升级产出(D6 核心) 2026-08-09 16:13:34 +08:00
Admin 8b31866d8e feat: 开局赠送灵石/灵米(配置驱动);文档同步 D6 步骤1-2 完成 2026-08-09 15:25:53 +08:00
Admin 2317b425fe style: HUD 布局微调——标签行拉伸比例 2026-08-09 15:17:16 +08:00
Admin 0f1f03f9ef feat: 字体更换为霞鹜文楷(LXGW WenKai,OFL 开源可商用),主题资源同步 2026-08-09 10:14:17 +08:00
Admin c9af0f608e feat: HUD 重构与主题资源——灵米显示、新布局、标签描边阴影、按钮/菜单主题 2026-08-08 23:22:28 +08:00
Admin 543af0a4ff feat: 修炼循环闭环——升层/瓶颈突破(按层概率)+管理界面修为进度+面板放大 2026-08-08 18:16:05 +08:00
Admin 19f8294242 feat: HUD 灵气浓度显示 + 字体大小配置化([ui] 节) 2026-08-08 17:30:18 +08:00
Admin 5ad0272d79 feat: 灵根改为1~5根+容量公式(1÷根数),权重表进配置;文档同步 2026-08-08 17:10:21 +08:00
Admin 8a01806ab0 feat: 游戏场景添加宗门后山背景层 2026-08-08 16:15:46 +08:00
Admin dfd780f3bd feat: 游戏场景添加宗门全景背景图,缩短招募间隔
- MainGame 新增 BG 背景层(宗门全景图,铺满窗口)
- 主菜单背景改为居中缩放模式
- 宗门全景图资源重新压缩优化(12.6MB → 8MB)
2026-08-08 14:54:06 +08:00
Admin b13c20a313 feat: 修炼数值配置化,天灵根规则重构,更新主菜单背景
- GameConfig 新增 [cultivation] 修炼系统配置(基础速度/灵气浓度/资质及灵根倍率/天灵根加成)
- 天灵根改为仅天骄资质再掷(heavenly_root_chance=50%),移入 GameConfig 可配置
- Disciple 修炼倍率常量改为从 GameConfig 读取,新增灵根数量倍率方法
- 主菜单背景更换为"图片大门",新增"宗门全景"资源,删除旧 BG1.png
- 同步更新弟子系统设计文档及新增修炼系统设计文档
2026-08-08 14:32:17 +08:00
Admin e283f6b2b7 style: 详情面板编号改显示名字 2026-08-07 00:00:38 +08:00
Admin 2e447c868b feat: 打开弟子名录面板时暂停时间,关闭恢复 2026-08-06 23:45:24 +08:00
Admin 6b5ec33d85 feat: 弟子名录面板与详情弹窗,完成 D4(管理按钮接线) 2026-08-06 23:42:03 +08:00
Admin 03c765ced8 feat: Disciple 增加入宗时间快照,D4 名录 UI 设计文档 2026-08-06 22:49:29 +08:00
70 changed files with 3241 additions and 227 deletions
+12 -4
View File
@@ -105,10 +105,12 @@
| 用户设置 | 已完成 | 默认流速持久化 (user://settings.cfg) |
| 顶部 HUD | 已完成 | 灵石/时间/声望显示,速度按钮组(toggle 高亮) |
| 画面质量 | 已完成 | 1920x1080 视口,MSAA 2D/3D + TAA,文字抗锯齿 |
| 宗门数据模型 | **进行中** | SectManager:灵石/灵气/声望(D2 |
| 弟子系统 | 规划中 | 生成/入宗分流/修炼D3-D5 |
| 建筑系统 | 规划中 | 建造/产出/升级D6 |
| 存档接入 | 规划中 | SaveSystem 序列化D8 |
| 宗门数据模型 | 已完成 | SectManager:灵石/声望/灵米 + 增减/设定接口D2 |
| 弟子系统 | 已完成 | 生成/入宗分流/天灵根/列表详情D3-D4 |
| 修炼循环 | 已完成 | 修为逐日增长/门槛升层/瓶颈判定/13 层圆满D5 |
| 建筑系统 | 已完成 | 灵石矿/灵田建造升级产出,口粮消耗与断粮离宗D6 |
| 存档接入 | 已完成 | 两级存档:无限组/槽,读档全量还原(D8) |
| 大境界突破 | 已完成 | 练气圆满手动花费灵石突破至筑基及以上,失败灵石不返还+修为倒退(D9) |
> 进度跟踪见 `Doc/开发计划表.md`,里程碑:M1(8/12) 核心闭环 → M2(9/6) 丰富玩法 → M3(10/11) 正式版本。
@@ -149,6 +151,12 @@
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` 默认值再导出(注释已标注同步项)。
## 命名规范
| 类别 | 格式 | 示例 |
Binary file not shown.
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://dpw0fmb5wki2h"
path="res://.godot/imported/LXGWWenKai-Light.ttf-ec3765d1a28f599b542e80b43c54944c.fontdata"
[deps]
source_file="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKai-Light.ttf"
dest_files=["res://.godot/imported/LXGWWenKai-Light.ttf-ec3765d1a28f599b542e80b43c54944c.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://cwrccil0e6jgj"
path="res://.godot/imported/LXGWWenKai-Medium.ttf-7874def67b3cdf89ec9f04c454f74b51.fontdata"
[deps]
source_file="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKai-Medium.ttf"
dest_files=["res://.godot/imported/LXGWWenKai-Medium.ttf-7874def67b3cdf89ec9f04c454f74b51.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://ds1gi2of5oydj"
path="res://.godot/imported/LXGWWenKai-Regular.ttf-4b7a37620712eb3c5b0811a13ce812d9.fontdata"
[deps]
source_file="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKai-Regular.ttf"
dest_files=["res://.godot/imported/LXGWWenKai-Regular.ttf-4b7a37620712eb3c5b0811a13ce812d9.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://caf6x1g0netn1"
path="res://.godot/imported/LXGWWenKaiMono-Light.ttf-578c556e63460fe509a3eed7b0e20444.fontdata"
[deps]
source_file="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKaiMono-Light.ttf"
dest_files=["res://.godot/imported/LXGWWenKaiMono-Light.ttf-578c556e63460fe509a3eed7b0e20444.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://4su41gib4l5u"
path="res://.godot/imported/LXGWWenKaiMono-Medium.ttf-9bcb20d0cda8b7e7290f1845c17ae4dc.fontdata"
[deps]
source_file="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKaiMono-Medium.ttf"
dest_files=["res://.godot/imported/LXGWWenKaiMono-Medium.ttf-9bcb20d0cda8b7e7290f1845c17ae4dc.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://ce12uai6ul0hj"
path="res://.godot/imported/LXGWWenKaiMono-Regular.ttf-2931aa1fc087599321bf814a8ef180f6.fontdata"
[deps]
source_file="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKaiMono-Regular.ttf"
dest_files=["res://.godot/imported/LXGWWenKaiMono-Regular.ttf-2931aa1fc087599321bf814a8ef180f6.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
+94
View File
@@ -0,0 +1,94 @@
Copyright 2021-2026 LXGW (https://github.com/lxgw/LxgwWenKai)
Copyright 2020 The Klee Project Authors (https://github.com/fontworks-fonts/Klee)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 MiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c6bthhmao5fyh"
path="res://.godot/imported/图片大门.png-03ea66f8e1eb82ba7952374321f00064.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/ui/图片大门.png"
dest_files=["res://.godot/imported/图片大门.png-03ea66f8e1eb82ba7952374321f00064.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 MiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dnjuv1btbxm4o"
path="res://.godot/imported/宗门全景.png-d969df84bf06c400df6e36c494864bbe.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/ui/宗门全景.png"
dest_files=["res://.godot/imported/宗门全景.png-d969df84bf06c400df6e36c494864bbe.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 MiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cweu17mwblxmg"
path="res://.godot/imported/宗门后山.png-7a9df51075ec2cc8660f9ccd87b9a616.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/ui/宗门后山.png"
dest_files=["res://.godot/imported/宗门后山.png-7a9df51075ec2cc8660f9ccd87b9a616.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 MiB

@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://caeqoc6wx8toy"
path="res://.godot/imported/BG1.png-6bf4b9c36da6bbdf71bd2c8203f4cb53.ctex"
uid="uid://b7cfvmuq8m7g3"
path="res://.godot/imported/灵田.png-1999fb337f7e0865f893a16872e15f6b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/ui/BG1.png"
dest_files=["res://.godot/imported/BG1.png-6bf4b9c36da6bbdf71bd2c8203f4cb53.ctex"]
source_file="res://assets/sprites/ui/灵田.png"
dest_files=["res://.godot/imported/灵田.png-1999fb337f7e0865f893a16872e15f6b.ctex"]
[params]
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 MiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bvpgmj08hilgl"
path="res://.godot/imported/灵石矿.png-ecb322a78c1d966bfd5ce47475fa7fb5.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/ui/灵石矿.png"
dest_files=["res://.godot/imported/灵石矿.png-ecb322a78c1d966bfd5ce47475fa7fb5.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+209
View File
@@ -0,0 +1,209 @@
# 修炼系统设计(D5:修炼循环 + D9:大境界突破)
> 对应开发计划表 D5(8/7):① 修为随时间增长(资质×灵气)② 境界门槛 ③ 突破判定;D9(8/11)追加大境界突破。
> 交付标准:弟子自动修炼,可突破;练气圆满后玩家手动花费灵石突破大境界。
> 本文档只描述设计,不包含代码。
> 状态:D5 部分已实现(每日结算 → 修为达门槛升层 → 瓶颈掷骰判定,失败扣 20%;13 层圆满封顶;修为进度已在管理界面展示);大境界突破已实现(练气圆满 → 手动花费灵石 → 筑基及以上大境界,失败灵石不返还 + 修为倒退;配置驱动,详见第四章)
---
## 一、核心循环
```
每日推进 → 修为增长(资质×灵根容量×天灵根×灵气浓度)→ 达到层门槛 → 升层/突破判定 → 境界推进
```
- 纯挂机机制:弟子自动修炼,玩家无需操作
- 修炼只在游戏时间推进时进行(TimeSystem.day_passed),暂停/弹窗期间冻结
---
## 二、修炼速度
**每日修为增量 = 基础速度 × 资质倍率 × 灵根容量倍率 × 天灵根加成 × 灵气浓度**
| 因子 | 来源 | 说明 |
|------|------|------|
| 基础速度 | 配置 `base_speed`(默认 10 | 修为/游戏日 |
| 资质倍率 | 配置 `aptitude_speed`[0.6, 1.0, 1.5, 2.0] | 按下品/中品/上品/天骄索引 |
| 灵根容量倍率 | 固定公式 `1 ÷ 根数`(无配置) | 1根=1.0、2根=0.5、3根=1/3、4根=0.25、5根=0.2 |
| 天灵根加成 | 配置 `heavenly_bonus`1.5 | 仅在 is_heavenly 时乘入 |
| 灵气浓度 | 配置 `qi_concentration`(默认 1.0) | D5 固定值;D6 建筑系统接入后动态化 |
> 所有修炼数值统一从 GameConfig 读取(Disciple 不内置倍率常量),避免双份数值漂移。
### 含义
- **资质定天赋上限**:决定倍率主体(0.6~2.0)
- **灵根数量定灵气容量**:容量 = 1 ÷ 灵根数(硬规则,不提供配置)。单灵根 1 单位灵气即充满(×1.0),双灵根只充 1/2(×0.5),三灵根 1/3,四灵根 1/4,五灵根 1/5,根数越多同一单位灵气分摊越薄
- **天灵根是稀缺加成**:强制单根(满容量)+ 额外 ×1.5,三个维度独立掷骰
- **无灵根(体修路线预留)**:容量为 0,无法练气;暂不生成,待体修系统(炼体)接入后另行结算
组合示例(天骄资质 + 五灵根 = 2.0 × 0.2 = 0.4,慢于 上品资质 + 单灵根 = 1.5 × 1.0 = 1.5
修为存储用 float(允许小数积累),显示时取整。
---
## 三、境界门槛(本期:练气 1–13 层)
| 层 | 需求修为(配置 `level_thresholds` | 是否瓶颈 |
|----|----------------------------------|---------|
| 1→2 | 100 | 否 |
| 2→3 | 200 | 否 |
| 3→4 | 350 | **是** |
| 4→5 | 550 | 否 |
| 5→6 | 800 | 否 |
| 6→7 | 1100 | **是** |
| 7→8 | 1500 | 否 |
| 8→9 | 2000 | 否 |
| 9→10 | 2600 | **是** |
| 10→11 | 3300 | 否 |
| 11→12 | 4100 | 否 |
| 12→13 | 5000 | **是** |
- **非瓶颈层**:修为满门槛 → 自动升层(无失败)
- **瓶颈层**3→4 / 6→7 / 9→10 / 12→13):修为满后进入突破判定
- 13 层圆满:修为封顶,显示"练气圆满";**大境界突破见第四章**(玩家手动花费灵石突破至筑基)
> 数值均为示例,正式数值在 D9 平衡调整,只改配置不动代码。
---
## 四、大境界突破(D9 追加:手动花费灵石)
### 4.1 总览
练气圆满(13 层)后,玩家可在**弟子详情面板手动操作**:花费灵石尝试突破至下一大境界(练气→筑基→金丹→元婴→化神)。
```
练气13层圆满 ── 手动花费灵石 + 成功率判定 ──> 筑基初期 ── 小境自动推进 ── 筑基圆满 ──> 金丹初期 …
```
- 与练气期小突破(自动挂机)不同:**大境界突破是玩家主动行为**,消耗灵石、有失败折损
- 大境界序列沿用 `Disciple.REALM_NAMES`(练气/筑基/金丹/元婴/化神),`realm_index` 表示大境界
### 4.2 触发条件
| 条件 | 说明 |
|------|------|
| 修为满 | 练气期:13 层圆满(修为封顶);筑基及以上:修为 ≥ 当前大境界圆满阈值(见 4.3) |
| 灵石足够 | 当前宗门灵石 ≥ 目标大境界花费(配置 `realm_breakthrough_costs` |
| 玩家操作 | 详情面板点击"大境界突破"按钮(满足条件时可用) |
### 4.3 筑基及以上的小境推进
- 筑基/金丹/元婴/化神各有 4 小境(初期/中期/后期/圆满),显示沿用现有 `get_realm_text()`
- 修为每日继续按同一公式增长(不再以 13 层封顶);满小境门槛**自动推进小境**(挂机,无失败)
- 小境门槛统一读配置 `realm_sub_thresholds`(3 项:初期→中期 / 中期→后期 / 后期→圆满),各大大境界通用
- **圆满**:修为达到圆满小境门槛后封顶,解锁"大境界突破"按钮
### 4.4 突破判定(手动)
1. 玩家点击"大境界突破"→ 扣除灵石(一次性,成功失败都不退还)
2. 掷骰:成功率按**目标大境界**读配置 `realm_breakthrough_chances`(升筑基 0.6 / 升金丹 0.5 / 升元婴 0.4 / 升化神 0.3,越往上越难)
3. **成功**`realm_index+1``sub_realm_index=0`(新大境界初期)、修为清零;控制台与信号广播"X 突破至 筑基初期"
4. **失败****修为倒退**(配置 `realm_breakthrough_fail_drop` 步,默认 3):倒退限于当前大境界内——练气期倒推层数(如 13 层圆满 → 10 层),筑基及以上倒推小境(圆满 → 后期 → 中期 → 初期);倒退后修为清零重新积累;控制台提示"X 突破失败,灵石化为乌有,修为倒退至 练气10层"
5. 失败后修为照常继续增长(无冷却;冷却规则后续版本再考虑)
**设计意图**:为灵石提供经营消耗出口(建筑产出 → 突破消耗形成闭环),并保留"攒灵石 → 点突破"的手动决策感与成败反馈;**失败代价是境界倒退**(练气 13 层 → 10 层意味重新冲 3 个瓶颈层),比单纯扣修为更能制造挫折与经营压力。
### 4.5 灵石不足
- 按钮置灰并显示所需灵石("突破(需 500 灵石)"
- 灵石不足不可点击,不弹窗
---
## 五、突破判定(瓶颈层)
1. 修为 ≥ 瓶颈层门槛时,每日结算后自动尝试突破
2. 掷骰:成功概率**按瓶颈层分别配置**(`breakthrough_chances`,与 `bottleneck_levels` 一一对应:升 4 层 0.85 / 升 7 层 0.8 / 升 10 层 0.75 / 升 13 层 0.6,越往上越难)
3. **成功** → 升层,控制台打印"X 突破至 练气4层"
4. **失败** → 修为扣减 20%(冲关损耗),次日再尝试;控制台打印失败信息
5. 失败不连续重试(每天最多判定一次)
**设计意图**:挂机友好(全自动),但瓶颈制造节奏感与"卡关→等积累"的经营反馈;损耗惩罚避免无脑挂到顶。
---
## 六、CultivationManager(新 Autoload
存放位置:`src/Character/CultivationManager.gd`(与 DiscipleManager 同域,注册顺序在 DiscipleManager 之后、声明于 project.godot
| 接口 | 说明 |
|------|------|
| 监听 `TimeSystem.day_passed` | 每日结算全体弟子 |
| 私有 `_gain_exp(d)` | 按公式累计修为(所有大境界通用;练气圆满/大境界圆满封顶) |
| 私有 `_check_breakthrough(d)` | 练气期门槛判定 + 瓶颈掷骰 |
| 私有 `_check_sub_realm(d)` | 筑基及以上小境门槛判定(自动推进,无失败) |
| **公开 `try_realm_breakthrough(d)`** | **大境界突破入口:校验灵石 → 扣费 → 掷骰 → 成功升大境界/失败修为倒退(`realm_breakthrough_fail_drop` 步),返回 bool** |
| 信号 `realm_changed(d)` | 境界变化(详情面板刷新用) |
| 信号 `cultivation_log(text)` | 修炼日志文本(控制台打印/后续日志系统) |
**注意**:只在 `disciples` 名录内结算,候选池不修炼;大境界突破必须从 UI 手动调用(挂机不自动花灵石)。
---
## 七、配置节(game_config.cfg [cultivation]
```ini
[cultivation]
base_speed=10 ; 基础修为/日
qi_concentration=1.0 ; 宗门灵气浓度(D6 动态化)
aptitude_speed=[0.6, 1.0, 1.5, 2.0] ; 资质倍率(下品/中品/上品/天骄)
heavenly_bonus=1.5 ; 天灵根额外倍率
breakthrough_chances=[0.85, 0.8, 0.75, 0.6] ; 各瓶颈层突破成功率(与 bottleneck_levels 一一对应)
level_thresholds=[100, 200, 350, 550, 800, 1100, 1500, 2000, 2600, 3300, 4100, 5000] ; 练气 1→2 到 12→13 各层所需修为
bottleneck_levels=[4, 7, 10, 13] ; 瓶颈目标层(升到这些层需突破判定)
realm_breakthrough_costs=[500, 4000, 12000, 60000] ; 大境界突破灵石花费(索引 0~3 = 升筑基/金丹/元婴/化神)
realm_breakthrough_chances=[0.6, 0.5, 0.4, 0.3] ; 大境界突破成功率(与 costs 一一对应)
realm_breakthrough_fail_drop=3 ; 突破失败倒退步数(练气倒推层数/筑基+倒推小境,默认 3)
realm_sub_thresholds=[6000, 12000, 24000] ; 筑基及以上小境门槛(初期→中期/中期→后期/后期→圆满)
```
- `aptitude_speed`:与资质枚举一一对应,必须 4 项
- `level_thresholds`:练气 1→2 到 12→13 共 12 个门槛值
- `bottleneck_levels`:升到这些层时需要突破判定
- `realm_breakthrough_costs/chances`:索引 0 = 练气圆满→筑基,1 = 筑基圆满→金丹,2 = 金丹圆满→元婴,3 = 元婴圆满→化神;两个数组长度必须一致(4 项)
- `realm_breakthrough_fail_drop`:失败倒退步数,练气期退层数、筑基+退小境,均不跨大境界
- `realm_sub_thresholds`:筑基及以上通用,3 项;修为达到即自动进小境
---
## 八、UI 接入(D4 预留区填充)
- **名录列表**DisciplesPanel):条目追加"修为"列(如 "1234 / 800",当前修为/本层门槛;13 层显示"练气圆满";大境界圆满显示"筑基圆满")
- **详情弹窗**:信息区追加"修为:1234 / 2000"(当前/本层门槛;练气圆满显示"练气圆满")
- 追加"**大境界突破**"按钮:满足触发条件(修为圆满 + 灵石足够)时可点击;按钮文案带花费("大境界突破(500 灵石)");灵石不足置灰
- 点击后直接执行判定(成功/失败均无二次确认),结果通过修炼日志与控制台可见
- 面板打开时暂停时间,进度静态展示;修炼日志:控制台 print(本期不做日志界面)
---
## 九、实现拆解(小步可验证)
1. 配置节追加 3 项 + GameConfig 读取(默认值与 cfg 同步)
2. Disciple 辅助:`is_realm_ready()`(修为圆满判定)、突破花费查询、进度文本支持筑基及以上
3. CultivationManager`_gain_exp` 放开到筑基及以上 + 小境自动推进 `_check_sub_realm`
4. `try_realm_breakthrough(d)`:校验 → 扣灵石 → 掷骰 → 成功/失败结算
5. 详情面板"大境界突破"按钮(可用性 = 条件 + 灵石;文案含花费)
6. 挂机验证(加速时间观察筑基圆满 → 手动突破金丹)+ 提交
---
## 十、验收清单(D5 交付标准 + D9 追加)
- [x] 挂机时弟子修为逐日增长,速度随资质/天灵根不同(下品慢、天骄快)
- [x] 非瓶颈层修为满自动升层,无失败
- [x] 瓶颈层(4/7/10/13 层)需突破判定,控制台有成败日志,失败扣 20% 修为
- [x] 练气 13 层圆满后修为封顶,显示"练气圆满"
- [x] 详情面板显示修为进度,突破后实时刷新
- [x] 修改配置(速度/概率/门槛)重启生效
- [x] 详情面板常驻"大境界突破"按钮:修为圆满 + 灵石足够可点击(文案显示所需灵石),否则置灰(未圆满/灵石不足/已至巅峰)
- [x] 点击突破:扣灵石 → 成功升大境界(realm_index+1,显示"筑基初期")或失败**修为倒退**(练气退层/筑基+退小境,配置 `realm_breakthrough_fail_drop` 步),控制台有日志
- [x] 灵石不足时按钮置灰不可点击
- [x] 筑基及以上:修为继续增长,满 `realm_sub_thresholds` 自动推进小境(初期→中期→后期→圆满),圆满后可再大境界突破
- [x] 修改配置(花费/成功率/小境门槛)重启生效
> 注:修为显示("当前/本层门槛")待详情面板接入(拆解第 6 步)。
+210
View File
@@ -0,0 +1,210 @@
# 存档系统设计(D8:存档接入)
> 对应开发计划表 D88/10):① SaveSystem JSON 序列化 ② 存/读接口 ③ 替换 SavePanel 测试数据。
> 交付标准:读档后数据还原。本文档只描述设计,不包含代码。
> 状态:全部步骤已实现(SaveSystem/序列化/GameState+MainGame 分支/SavePanel 双模式/自动存档/两级浏览/读档还原/删除与容错/游戏内加载进度点/删除存档组),验收 1–9 全部通过
---
## 一、两级存档结构(无限组 / 无限槽)
**第一级:存档组 SaveGroup(一个角色/一次轮回,UI 显示"存档N"****第二级:存档槽 SaveSlot(该组下任意多个进度快照)**
- **每个新游戏 → 创建一个 SaveGroup**(主菜单"新游戏"时自动生成,id 自增)
- **每次点击存档 → 在所属 SaveGroup 下创建一个新 SaveSlot**(不覆盖、不固定槽位)
- **组和槽的数量均无上限**
```
user://saves/
├── profiles.json # 存档组列表 [{id, name, created, last_date}]
├── profile_1/ # 存档组 1
│ ├── auto.json # 自动存档(返回主菜单时写入,特殊槽)
│ ├── slot_1.json # 手动槽(组内序号递增命名)
│ └── slot_2.json
└── profile_2/ ...
```
- 槽位命名:**组内序号递增**`slot_1``slot_2`…),同一组内永不重名、互不覆盖;删除槽位后序号从当前最大 +1 继续(不重用)
- 自动槽 `auto.json` 固定名(续档用)
## 二、可存档数据清单
| 系统 | 数据 | 说明 |
|------|------|------|
| GameState | `year/month/day` | 游戏日期(TimeSystem.set_date 恢复) |
| SectManager | `spirit_stones` / `reputation` / `food` | 三资源 |
| DiscipleManager | `disciples` + `_next_id` | 名录;候选池不存 |
| BuildingManager | `buildings` | 建筑类型/等级 |
> 修为/境界/饥饿天数都在 Disciple 字段内,随名录序列化。
## 三、SaveSystem(新 Autoload
存放位置:`src/Core/SaveSystem.gd`,注册于 project.godot(依赖各管理器,声明在最后)。
| 接口 | 说明 |
|------|------|
| `create_profile() -> int` | 新建存档组,返回 id(名"存档N" |
| `get_profiles() -> Array[Dictionary]` | 存档组列表(id/name/created/last_date |
| `save_game(profile_id) -> String` | **新建一个槽位**并写入当前游戏数据,返回槽文件名;失败返回空串 |
| `save_auto(profile_id) -> bool` | 写自动槽(返回主菜单时调用) |
| `get_slots(profile_id) -> Array[Dictionary]` | 该组全部槽位(槽文件名 + 元数据),按时间升序 |
| `load_game(profile_id, slot_name) -> bool` | 读取槽位 JSON → 还原各系统 |
| `delete_profile(profile_id) -> bool` | 删除存档组(列表移除 + 递归删目录含全部槽位) |
| `delete_slot(profile_id, slot_name) -> bool` | 删除槽位文件 |
| `reset()` | 清空内存游戏数据(各管理器 reset + 资源清零) |
## 四、序列化设计
### 1. 数据模型加 to_dict/from_dict
- `Disciple.to_dict()` / `static Disciple.from_dict(data)`14 字段全量,枚举转 int
- `Building.to_dict()` / `static from_dict(data)`2 字段)
### 2. 槽位 JSON 结构(slot_1754734800.json
```json
{
"version": 1,
"profile_id": 1,
"saved_date": {"year": 3, "month": 5, "day": 12},
"date": {"year": 3, "month": 5, "day": 12},
"resources": {"spirit_stones": 1230, "reputation": 45, "food": 210},
"next_disciple_id": 7,
"disciples": [
{"id": 1, "name": "林清玄", "age": 16, "aptitude": 2, "roots": [0, 1],
"is_heavenly": false, "join_year": 1, "join_month": 1, "join_day": 1,
"realm_index": 0, "sub_realm_index": 4, "cultivation_exp": 512.5, "hunger_days": 0}
],
"buildings": [
{"type": 0, "level": 2},
{"type": 1, "level": 1}
]
}
```
- `saved_date`/`resources` 顶层冗余字段 = 槽位元数据(列表显示用,免解析全档)
- `version` 为格式版本,预留兼容
### 3. profiles.json 结构
```json
{"profiles": [{"id": 1, "name": "存档1", "created": "2026-08-10", "last_date": "3年5月12日"}]}
```
## 五、存档/读档流程
### 存档(游戏内,每次=新建槽)
1. TopBar 新增"存档"按钮 → SavePanel(存档模式,时间暂停)
2. 左侧显示存档组(当前组高亮),右侧显示该组全部槽位
3. 点"新建存档"按钮 → `save_game(profile_id)` 创建新槽 → 列表新增一条
4. 关闭面板恢复时间
### 读档(主菜单,两级浏览)
1. 主菜单"读取存档" → SavePanel(读档模式)
2. **左侧存档组列表**(存档N + 最后日期)→ 点组 → **右侧显示该组槽位列表**(自动+手动,元数据齐全)
3. 点槽位 → `GameState.request_load_game(profile_id, slot_name)` → 进入 MainGame
### MainGame 分支(改造 _ready
```
NEW_GAME → 现有流程:start_new_game + 开局资源 + 招募弹窗(暂停)
LOAD_GAME → SaveSystem.load_game(profile_id, slot_name) → 还原 → 不弹窗、不暂停
```
### 自动存档
TopBar"返回"主菜单时(sign_return_mainmenu 处理前)→ `save_auto(profile_id)` 自动槽——保证"退→再进"能续档。
## 六、UI:SavePanel(复用现有手绘界面)
```
SavePanel (PanelContainer 全屏)
└── VBoxContainer
├── Header: TitleLabel("选择存档") + Spacer + Button_Esc("返回")
├── 存档模式:Button_NewSlot("新建存档") ← 每次点击创建新槽位
└── ScrollContainer
└── HBoxContainer
├── SaveGroup 区(VBoxContainer) ← 左侧:存档组按钮列表(动态生成"存档N"
└── SlotList 区(VBoxContainer) ← 右侧:该组槽位条目列表(动态生成 SaveSlot 实例)
```
**SaveSlot 条目**(复用现有场景,无空槽概念——每个条目都是真实存档):
| 节点 | 填充内容 |
|------|---------|
| SlotIndexLabel | 序号(自动槽=0,手动槽按时间序 1..N) |
| SaveNameLabel | "自动存档" / "存档 N" |
| MetaLine1 | "X年X月X日 弟子N人" |
| MetaLine2 | "灵石 M" |
| DeleteButton | 删除该槽位(删除前确认) |
| ArrowIcon | 读档箭头(点击读档) |
**交互规则**
| 项 | 存档模式(游戏内) | 读档模式(主菜单) |
|----|-------------------|-------------------|
| 左侧组列表 | 当前组高亮(可切换查看,**组行带"删"按钮,当前组禁用**) | 可切换任意组(组行带"删"按钮) |
| 槽位点击 | **回到该进度点**(加载所选槽,未保存进度丢失) | 点击读档进入游戏 |
| 删除按钮 | 可删(确认后) | 可删(确认后) |
| 暂停 | 打开时暂停,关闭恢复 | 不暂停 |
## 七、分步实现规划(小步可验证,每步独立跑通)
### 步骤 1SaveSystem 骨架(新 Autoload)✅
| 项 | 内容 |
|----|------|
| 改动 | 新建 `src/Core/SaveSystem.gd``create_profile()`(建组,名"存档N")、`get_profiles()``save_game(profile_id)`(新建槽,文件名 `slot_{组内序号}.json`,返回槽名)、`save_auto(profile_id)``get_slots(profile_id)`(自动槽最前+手动按序号升序,含元数据)、`load_game(profile_id, slot_name)``delete_slot()``reset()``project.godot` 注册(声明在最后) |
| 验证 | ✅ 建组(存档N 自增)→ 存档 → slot_1/2/3 生成 → `get_slots` 读回;删除后序号不重用 |
### 步骤 2:数据模型序列化 ✅
| 项 | 内容 |
|----|------|
| 改动 | `Disciple.to_dict()/static from_dict()`14 字段,枚举转 int);`Building.to_dict()/static from_dict()``DiscipleManager``get/set_next_disciple_id()``SectManager``set_spirit_stones/set_reputation/set_food()`(带信号,读档后 HUD 自动刷新) |
| 验证 | ✅ 存档文件内容完整(弟子/建筑/资源/日期全量) |
### 步骤 3GameState + MainGame 分支 🟡(GameState 已改,MainGame 分支待接)
| 项 | 内容 |
|----|------|
| 改动 | `GameState``profile_id: int``save_slot: String`(槽文件名);`request_new_game(pid)``request_load_game(pid, slot_name)``MainGame._ready` 分支 NEW_GAME(原流程)/ LOAD_GAME`load_game` 还原,不弹窗不暂停,失败回退新游戏);`sign_return_mainmenu``save_auto(profile_id)` |
| 验证 | 无头:存→清→读闭环,日期/资源/名录(境界修为饥饿)/建筑逐项一致 |
### 步骤 4SavePanel 动态填充(复用现有手绘界面)✅
| 项 | 内容 |
|----|------|
| 改动 | `SavePanel.gd` 双模式:存档模式(游戏内,暂停,当前组高亮 + 槽位列表 + "新建存档"按钮 → `save_game` → 列表新增);读档模式(主菜单,左列组列表 → 点组 → 右列槽位列表,点槽读档)。`SaveSlot.gd` 填充:序号/名称/MetaLine1/MetaLine2/删除(确认后删)/箭头(读档)。TopBar 加"存档"按钮 + `sign_open_save` 信号 |
| 验证 | ✅ 存档模式:打开暂停 → 新建存档 → 槽位生成(内容完整)→ 关闭恢复;读档模式待 F5 手测 |
### 步骤 5:接线闭环 🟡(MainMenu 建组已接,读档模式接线+自动存档待做)
| 项 | 内容 |
|----|------|
| 改动 | `MainMenu`"新游戏"→ `create_profile()` 建组 → 进游戏;"读取存档"→ 面板读档模式;`MainGame.tscn` 挂 SavePanel 实例 |
| 验证 | 完整闭环:新游戏(建组)→ 招弟子 → 挂机 → 存档(建槽)→ 返回(自动槽)→ 主菜单读档 → 数据还原 |
### 步骤 6:整体验证 + 提交
| 项 | 内容 |
|----|------|
| 改动 | 无(仅验证) |
| 验证 | 对照八节验收清单逐项勾选;git 提交 |
> 依赖关系:1→2 顺序执行(序列化被 SaveSystem 消费);3 依赖 1/24 依赖 1/3;5 依赖 4;6 为最终验证。
## 八、验收清单(D8 交付标准)
- [x] 主菜单"新游戏"自动创建新存档组(存档N)
- [x] 游戏内"存档"面板点"新建存档"→ 该组新增一条槽位(文件生成,内容完整)
- [x] 连续多次存档 → 同一组内多个槽位互不覆盖(序号命名,已验证 slot_1/2/3 内容独立)
- [x] 返回主菜单自动写自动槽(auto.json)
- [x] "读取存档"两级浏览:左侧组列表 → 右侧槽位列表(元数据显示)
- [x] 读档后:日期/资源/名录(含境界/修为/饥饿)/建筑全部还原
- [x] 读档进入**不弹招募窗、不暂停**
- [x] 删除槽位后列表刷新;损坏/缺失 JSON 不崩溃(回退提示)
- [x] 新游戏流程不受影响(仍弹开局招募);多组多槽共存
+212
View File
@@ -0,0 +1,212 @@
# 建筑系统设计(D6:建筑系统基础)
> 对应开发计划表 D6(8/8):① 灵石矿场景 ② 建造→产出 ③ 升级。交付标准:建筑产资源,升级加速。
> 本文档只描述设计,不包含代码。
> 状态:步骤 1–7 已实现(D6 全部完成:食物/开局/配置/建筑模型管理/面板/口粮饥饿离宗),步骤 7 整体挂机验证待 F5 手测
---
## 一、核心循环
```
建造(花灵石)→ 每日产出(灵石/食物)→ 攒资源升级 → 产出加速 → 支撑修炼/招募经济
```
- 纯挂机产出:与修炼/招募同节奏,监听 `TimeSystem.day_passed` 每日结算
- 产出直接进入 SectManager 资源(HUD 实时联动)
---
## 二、数据模型(Building
| 字段 | 类型 | 说明 |
|------|------|------|
| `type` | 枚举 BuildingType | 建筑种类(灵石矿 / 灵田) |
| `level` | int | 等级(1 起) |
价格与产出**不在建筑上存副本**,运行时按 `type + level` 从 GameConfig 计算,杜绝数值漂移(与修炼数值同一原则)。
### 本期建筑种类
| 类型 | 名称 | 产出 | 消费资源 |
|------|------|------|---------|
| SPIRIT_STONE_MINE | 灵石矿 | 灵石/日 | 建造/升级花灵石 |
| SPIRIT_FIELD | 灵田 | 食物/日 | 建造/升级花灵石 |
> 食物为通用资源(顶部栏常驻显示,见通用资源分析文档),需在 SectManager 新增。**HUD 显示名为"灵米"**(内部资源名 `food`)。
>
> **后续建筑规划**:灵药园(产灵草,修炼资源,W5 炼丹材料)——与灵田分工:灵田管口粮(食物),药园管药材(灵草),加新建筑 = 配置加一节 + 枚举加一项 + 产出分发加一行。
> **无地图摆放**:本期建筑统一以面板条目呈现(BuildingsPanel 数据行),灵石矿与灵田同套处理,不建单独场景。地图摆放(建筑实体显示在宗门画面上)属 P2 扩展。
---
## 三、产出机制
- **BuildingManager**(新 Autoload)监听 `day_passed`,遍历已建建筑逐座结算
- 灵石矿 → `SectManager.add_spirit_stones(output)`
- 灵田 → `SectManager.add_food(output)`
- 产出量与等级相关:`产出 = 基础产出 × 升级产出倍率^(等级-1)`
## 三·五、口粮消耗与断粮离宗
灵田产出的食物有真实用途——**练气期弟子每日吃粮**(筑基+辟谷,为将来预留)。
### 断粮后果三方案(选定:方案 B)
按修仙设定,练气期尚未辟谷(引气入体 ≠ 断食),仍需进食;通常筑基期逐渐辟谷、金丹期完全辟谷——"断粮有后果"是成立的。具体后果有三档可选:
| 方案 | 断粮后果 | 特点 |
|------|---------|------|
| **A 饿死** | 断粮累计 N 天(如 30 天)→ 弟子死亡,从名录移除 | 忠于设定,压力最大;但需死亡表现(遗物/坟冢等),范围膨胀 |
| **B 离宗(选定)** | 断粮累计 N 天 → 弟子出走投奔他处,名录移除 | 效果同 A,叙事更软("出走"而非"死亡"),UI/代码最简单,契合宗门经营题材 |
| **C 不致命** | 只修炼效率减半,永远不死 | 最温和,生存压力弱,经济闭环不成立 |
**选定理由**:方案 B 的代码量约等于 A(定时检查 + 名录移除,名录/详情 UI 均为现成),但"弟子出走"比"饿死"更符合宗门经营题材的叙事基调,也避开死亡表现系统的范围膨胀,适合 M1 前落地。
### 机制细则(方案 B
1. **每日消耗**`食物 -= 练气弟子数 × 人均消耗`(配置 `consumption_per_disciple`,默认 1
2. **断粮判定**:食物不足以覆盖全部练气弟子时 → 食物归 0,全体练气弟子**饥饿天数 +1**;食物充足 → 饥饿天数清零
3. **饥饿惩罚**:饥饿期间修炼速度 × `hunger_speed_multiplier`(默认 0.5
4. **离宗**:饥饿天数 ≥ `hunger_threshold_days`(默认 30)→ 弟子**离宗出走**,从名录移除(控制台日志"X 因断粮离宗"
5. 筑基+弟子不消耗口粮(`realm_index != 0` 豁免,为筑基系统预留)
**设计意图**:生存压力驱动经济闭环——断粮 → 修炼放缓 → 弟子出走 → 玩家被迫种灵田/合理经营,而不是让食物纯积累无意义。
---
## 四、建造与升级
| 操作 | 规则 |
|------|------|
| 建造 | 检查灵石 ≥ 建造价 → 扣除 → 建筑入列(等级 1) |
| 升级 | 检查灵石 ≥ 升级价 → 扣除 → 等级 +1 |
| 升级价 | `建造价 × 升级价格倍率^等级`(即升到 2 级花 建造价×倍率) |
| 等级上限 | 配置 `max_level`(默认 10 |
资源不足时:UI 按钮禁用,无其他副作用。
---
## 五、配置节(game_config.cfg 新增)
```ini
[building] ; 建筑系统通用
starting_spirit_stones=500 ; 开局灵石(解决初始资金为 0,新游戏发放)
starting_food=100 ; 开局食物(口粮缓冲,新游戏发放)
max_level=10 ; 建筑等级上限
[building_mine] ; 灵石矿
cost=200 ; 建造价(灵石)
output=10 ; 日产出(灵石)
upgrade_cost_x=2.0 ; 升级价格倍率
upgrade_output_x=1.5 ; 升级产出倍率
[building_field] ; 灵田
cost=100 ; 建造价(灵石)
output=5 ; 日产出(食物)
upgrade_cost_x=2.0 ; 升级价格倍率
upgrade_output_x=1.5 ; 升级产出倍率
[food] ; 口粮与生存
consumption_per_disciple=1 ; 练气弟子人均日消耗
hunger_speed_multiplier=0.5 ; 饥饿期间修炼速度倍率
hunger_threshold_days=30 ; 断粮累计天数上限(达到即离宗)
```
> 加新建筑 = 配置加一节 + 枚举加一项 + BuildingManager 产出分发加一行(对照 `[recruitment_*]` 模式)。
---
## 六、UI 建筑面板(BuildingsPanel
- **入口**TopBar"资源"按钮(Button_ResourcesManagement,当前未接线)→ 打开
- 打开面板时**暂停时间**(与弟子名录面板一致,关闭恢复)
- 面板结构(复用青绿弹窗模式):
- 标题"宗门建筑"
- 每种建筑一行:名称 / 等级 / 日产出 + 建造或升级按钮(显示价格,资源不足禁用)
- 底部"关闭"按钮 + 遮罩点击关闭
- 数据驱动:监听 `buildings_changed` 重建列表,打开时主动刷新
---
## 七、分步实现规划(小步可验证,每步独立跑通)
### 步骤 1SectManager 加食物资源 + 开局资源发放 ✅
| 项 | 内容 |
|----|------|
| 改动 | `SectManager.gd``food` 资源 + `add_food/remove_food` + `food_changed` 信号;`GameConfig.gd``[building]` 节开局数量;`MainGame.gd` 新游戏时发放 |
| 验证 | ✅ HUD 显示 500 灵石 / 100 灵米,增减接口调用正确 |
### 步骤 2:HUD 食物显示 ✅
| 项 | 内容 |
|----|------|
| 改动 | `TopBarHUD.tscn` 加"灵米"标签对;`top_bar_hud.gd``food_changed` 信号 + 初始值 |
| 验证 | ✅ 开局 100 灵米显示在顶栏;`add_food` 实时刷新 |
### 步骤 3`[building]` 配置节 + GameConfig 读取 ✅(部分)
| 项 | 内容 |
|----|------|
| 改动 | `game_config.cfg``[building]`/`[building_mine]`/`[building_field]` 节(开局数量、建筑参数、等级上限);`GameConfig.gd` 加对应字段 + `building_params` 字典 + 解析 |
| 验证 | ✅ 无头打印各配置值正确;`[food]` 口粮/饥饿参数待步骤 5 接入 |
### 步骤 4Building 模型 + BuildingManager ✅
| 项 | 内容 |
|----|------|
| 改动 | `src/Sect/buildings/Building.gd`type/level + 价格产出计算,运行时读配置);`src/Sect/BuildingManager.gd`(建造/升级/每日产出/`buildings_changed` 信号);`project.godot` 注册 Autoload |
| 验证 | ✅ 建造扣费入列、每日产出进 SectManager、升级后产出提升(10→15)、灵石不足拒绝 |
### 步骤 5:口粮消耗 + 饥饿惩罚 + 断粮离宗 ✅
| 项 | 内容 |
|----|------|
| 改动 | `Disciple.gd``hunger_days``CultivationManager.gd` 每日先结算口粮(消耗→饥饿累计→离宗移除),`_gain_exp` 乘饥饿倍率 |
| 验证 | ✅ 充足 30 天精确扣减清零;断粮饥饿累计、修为减半;第 30 天离宗(日志"断粮日久,离宗而去"+名录移除);恢复清零 |
### 步骤 6BuildingsPanel UI + "资源"按钮接线 ✅
| 项 | 内容 |
|----|------|
| 改动 | `src/UI/Panels/BuildingsPanel/`(面板场景+脚本,每类型一行:名称/等级/日产出+建造或升级按钮);`TopBarHUD.tscn` 连"资源"按钮 → 新信号 → `MainGame.gd` 打开面板 |
| 验证 | ✅ 无头验证:打开暂停/关闭恢复、建造升级扣费、每日产出;面板尺寸与名录面板一致(1340×819 vs 1340×842 |
### 步骤 7:整体挂机验证 + 提交
| 项 | 内容 |
|----|------|
| 改动 | 无(仅验证) |
| 验证 | 挂机模拟:开局 500 灵石→建灵田→产出覆盖口粮→攒灵石升级→产出加速;断粮场景:不建灵田→30 天弟子离宗;对照八节验收清单逐项勾选 |
> 依赖关系:1→2 顺序执行(HUD 依赖食物资源);3→4 顺序执行(Building 依赖配置);5 依赖 1/3;6 依赖 4;7 为最终验证。
---
## 八、验收清单(D6 交付标准)
- [x] 新游戏开局赠送 500 灵石 + 100 食物(配置可调),HUD 可见
- [x] 点"资源"按钮打开建筑面板(时间暂停,关闭恢复)
- [x] 灵石充足时可建造灵石矿/灵田,扣费入列
- [x] 每日自动产出灵石/食物,HUD 实时增长
- [x] 升级建筑后产出提升(升级倍率生效),价格按倍率递增
- [x] 灵石不足时建造/升级按钮禁用
- [x] 等级达到上限后不可再升级
- [x] 练气弟子每日消耗食物;断粮后饥饿天数累计、修炼速度减半
- [x] 断粮累计 30 天弟子离宗(名录移除 + 日志)
- [x] 食物恢复充足后饥饿清零
- [x] 建筑/饥饿数据为纯数据(可序列化,D8 存档接入时加 to_dict/from_dict
---
## 九、与后续任务衔接
| 后续任务 | 衔接方式 |
|----------|----------|
| D8 存档接入 | Building 纯数据模型直接序列化 |
| W3 财政经济 | 建筑产出进入收支流水;俸禄发放扣灵石 |
| W5 生产制造 | 灵田/灵石矿的弟子匹配度(灵根→效率)接入 |
| 修炼系统 | 后续灵气建筑(灵泉/聚灵阵)动态化 `qi_concentration` |
+112 -20
View File
@@ -1,7 +1,7 @@
# 弟子系统设计(D3:生成 + 入宗分流)
# 弟子系统设计(D3:生成 + 入宗分流 / D4:名录 UI
> 对应开发计划表 D3(8/5)。本文档只描述设计,不包含代码。
> 状态:设计稿,待审阅通过后实现
> 对应开发计划表 D38/5与 D48/6。本文档只描述设计,不包含代码。
> 状态:D3、D4 已实现
---
@@ -15,8 +15,11 @@
| `name` | String | 随机生成的姓名 |
| `age` | int | 入宗年龄(12~18 |
| `aptitude` | 枚举 Aptitude | 资质(入宗分流结果) |
| `roots` | 枚举数组 SpiritRoot | 灵根列表(普通弟子 1~3 根,互不重复) |
| `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 修炼系统推进 |
@@ -29,11 +32,11 @@
| 2 | 上品 | 1.5x | 天赋尚可 |
| 3 | 天骄 | 2.0x | 万中无一 |
> 倍率由 D5 修炼循环使用,此处只定义不消费
> 倍率由 D5 修炼循环使用,数值存于 GameConfig(配置 `aptitude_speed`),此处仅为参考展示
### 枚举二:灵根五行(SpiritRoot
金、木、水、火、土 五种。灵根数量影响修炼速度(根数越少越快),元素种类影响日后生产效率匹配(D6 建筑/职能系统使用),此处只生成不消费。
金、木、水、火、土 五种。灵根数量影响灵气容量(容量 = 1 ÷ 根数:单灵根充满、双灵根 1/2、三灵根 1/3……五灵根 1/5,修炼速度按比例折扣),元素种类影响日后生产效率匹配(D6 建筑/职能系统使用),此处只生成不消费。
### 特殊灵根:天灵根
@@ -41,12 +44,13 @@
| 项目 | 设定 |
|------|------|
| 生成概率 | 3%(远低于四类资质中"天骄"的 5%,天灵根可叠加于任意资质之上) |
| 出现规则 | **仅天骄资质**Aptitude.HEAVENLY)中再掷一次,非天骄弟子不可能有天灵根 |
| 天骄内概率 | 配置 `heavenly_root_chance`(默认 0.5 = 50%);总体概率 = 天骄概率 × 天骄内概率(普通难度 5% × 50% ≈ 2.5% |
| 灵根构成 | 仅一系灵根(五行之一),纯净单灵根 |
| 修炼速度 | 在资质倍率基础上**额外 +50%**(乘法叠加,D5 修炼循环消费) |
| 修炼速度 | 在资质倍率基础上**额外 +50%**(乘法叠加,数值存于 GameConfig `heavenly_bonus`D5 修炼循环消费) |
| 地位 | 宗门争抢的种子弟子,日后可挂靠特殊培养事件(W8 预留) |
> 天灵根与资质是两个独立维度:资质定天赋底子,天灵根是稀缺加成,二者随机独立
> 天灵根与资质是**嵌套关系**:先掷资质,天骄之后再判天灵根。天灵根 = 天骄 + 单灵根 + 1.5x 加成,是最稀缺组合
### 境界字段说明
@@ -57,13 +61,19 @@
境界的成长、突破门槛、寿元计算全部留给 D5 修炼循环,D3 只负责初始状态。
### 入宗时间字段说明
- `join_year/month/day` 在**入宗瞬间**accept_applicant)快照 GameState 当前日期
- 候选池弟子不设(0),拒绝入宗不产生记录
- 本期只存不消费;入门天数换算(D5 修炼结算)、按入宗排序(列表)、资历事件(W8)后续使用
---
## 二、生成规则(随机性)
### 1. 姓名
- 姓氏池:24 个常用修仙风单姓(林、苏、叶、萧、顾、沈、楚、白……)
- 姓氏池:32 个常用修仙风单姓(林、苏、叶、萧、顾、沈、楚、白……)
- 名用字池:24 个(清、玄、墨、尘、风、澜、霜、珏……)
- 规则:随机 1 个姓 + 1~2 个字(50% 概率双字名)
- 不保证唯一(同名弟子可共存,以 id 区分)
@@ -74,9 +84,10 @@
### 3. 灵根
- 普通弟子:数量 1~3均匀随机,元素从五行中随机抽取、互不重复
- 天灵根:3% 概率判定为天灵根,灵根数量固定 1 根,修炼速度额外 +50%
- 含义:单灵根修炼最快,灵根最慢(对应修仙世界观)
- 普通弟子:数量按配置权重随机(`root_count_weights`1~5,元素从五行中随机抽取、概率均等、互不重复
- 天灵根:仅天骄资质再判(概率 `heavenly_root_chance`,灵根数量固定 1 根,修炼速度额外 +50%
- 含义:单灵根修炼最快,灵根最慢(对应修仙世界观)
- 无灵根暂不生成(体修路线预留,见修炼系统设计)
### 4. 资质(入宗分流)
@@ -102,7 +113,7 @@
招募**不是玩家随时点按钮**,而是"生成候选 → 暂停时间 → 弹窗选择":
- **开局招募**:新游戏开始,生成 **5 名候选**,暂停时间弹窗,**必须选满 3 名**才能确定
- **定期招募**:每满间隔(配置文件 `recruit_interval_years`默认 0.1 年 = 36 天)生成 **3 名候选**,暂停时间弹窗
- **定期招募**:每满间隔(配置文件 `recruit_interval_years`当前 0.25 年 = 90 天)生成 **3 名候选**,暂停时间弹窗
- **选择方式**:每个候选独立勾选"入宗",可全部接受、可全部拒绝(定期招募可 0~3 个)
- **结算**:点"确定" → 勾选的入宗、未勾选的拒绝 → 关闭弹窗 → **恢复时间**
- 所有参数(候选数、必选数、间隔)来自 `resources/game_config.cfg`,改配置即改难度
@@ -110,7 +121,7 @@
### 招募流程(文字时序)
1. 开局(MainGame 调用 `start_new_game()`)或倒计时归零(`_on_day_passed`
2. 生成候选弟子(判天灵根 3% → 生成灵根 → 按权重掷资质 → 组名字),进入候选池
2. 生成候选弟子(按权重掷资质 → 天骄再判天灵根 → 生成灵根 → 组名字),进入候选池
3. `applicants_generated(is_opening)` 信号发出 → 弹窗显示候选列表
4. 玩家勾选(开局模式:确定按钮在选满 3 名前禁用)
5. 点"确定" → `finish_recruitment(selected)` 结算 → 恢复时间
@@ -151,8 +162,10 @@
difficulty="normal" ; 当前难度档位
opening_applicant_count=5 ; 开局候选人数
opening_pick_count=3 ; 开局必须选满数
recruit_interval_years=0.1 ; 定期招募间隔(游戏年)
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]
@@ -173,7 +186,7 @@ aptitude_weights=[60, 30, 8, 2]
- 每个候选一行:姓名/年龄/资质(天灵根标记)/灵根/境界 + "入宗"勾选框
- 底部:全部接受 / 全部拒绝 / 确定
- 开局模式确定按钮在选满 3 名前禁用;确定后关闭弹窗并恢复时间
- 名录的可视化列表属于 D4弟子列表 UI,届时由 `disciples_changed` 信号驱动
- 名录的可视化列表见**第七节(D4 弟子名录 UI**
---
@@ -188,14 +201,93 @@ aptitude_weights=[60, 30, 8, 2]
---
## 七、验收清单(D3 交付标准
## 七、弟子名录 UID4
> 对应开发计划表 D4(8/6):① 列表条目场景 ② 姓名/资质/境界显示 ③ 点击详情。交付标准:列表可浏览弟子。
### 1. 入口
- TopBar 顶部栏"**管理**"按钮(Button_StaffManagement)→ 打开弟子名录面板
- 打开面板时**暂停时间**(管理操作不打断挂机节奏,避免浏览时弟子入宗/时间流逝;关闭面板恢复推进)
### 2. 面板结构(两层弹窗,复用青绿仙侠配色)
**第一层:名录列表面板(DisciplesPanel**
| 元素 | 说明 |
|------|------|
| 标题 | "弟子名录(N 人)"N = 当前名录人数 |
| 列表区 | ScrollContainer 动态条目,一个弟子一行,按入宗顺序 |
| 条目内容 | 姓名 / 资质(含天灵根标记)/ 灵根 / 境界 / **修为进度** + "详情"按钮 |
| 空状态 | 名录为空时显示"暂无弟子"提示 |
| 底部 | "关闭"按钮(也可点遮罩关闭) |
**第二层:弟子详情弹窗(DiscipleDetailPanel**
| 元素 | 说明 |
|------|------|
| 标题 | 弟子姓名 + 天灵根标记 |
| 信息区 | 静态展示:名字 / 资质 / 年龄 / 灵根 / 境界 / **修为进度** / **入宗时间** |
| 预留区 | 修炼进度、属性面板等 D5+ 扩展位置(本期留白) |
| 底部 | "关闭"按钮 |
### 3. 数据驱动
- 面板监听 `DiscipleManager.disciples_changed` → 重建列表
- 打开面板时也主动刷新一次(保证与名录同步)
- 条目与弟子数据一一对应:动态生成时把 Disciple 引用绑定到条目上,点"详情"直接取用
### 4. 文件与场景结构
```
src/UI/Panels/DisciplesPanel/
├── DisciplesPanel.tscn # 名录列表弹窗
├── disciples_panel.gd
├── DiscipleDetailPanel.tscn # 弟子详情弹窗
└── disciple_detail_panel.gd
```
- 两个弹窗都挂在 MainGame 下(默认隐藏),详情弹窗作为列表面板的子层(显示时盖在列表之上)
- 条目在代码中动态生成(与 RecruitPanel 同套路),不单独做条目场景(D4 拆解的"列表条目场景"用动态行实现即可)
- TopBar"管理"按钮在 `top_bar_hud.gd` 接线,调 `DisciplesPanel.show_panel()`
### 5. 交互细节
- 详情弹窗从列表面板打开;关闭详情回到列表;关闭列表回到游戏
- 列表浏览期间名录变化(如定期招募入宗)→ 列表实时刷新
- 资质文字可用颜色区分(天骄金色、上品亮青),其余默认——视觉提示,本期可选
---
## 八、验收清单(D4 交付标准)
- [ ] 点顶部"管理"按钮弹出弟子名录面板,显示当前已入宗弟子
- [ ] 列表每行显示:姓名 / 资质(天灵根有标记)/ 灵根 / 境界
- [ ] 名录为空时显示"暂无弟子";招募入宗后列表实时刷新
- [ ] 点条目"详情"弹出详情弹窗,显示名字/资质/年龄/灵根/境界/入宗时间
- [ ] 关闭详情回到列表,关闭列表回到游戏,无报错
- [ ] 打开面板期间时间**暂停**,关闭面板后时间恢复推进
---
## 九、与后续任务衔接(D4 补充)
| 后续任务 | 衔接方式 |
|----------|----------|
| D5 修炼循环 | 详情弹窗预留区填充:修为进度、突破按钮、寿元 |
| D6 建筑/职能系统 | 列表条目可扩展"职位/职能"列 |
| D8 存档接入 | 名录数据来源不变,仅序列化方式接入 |
---
## 十、验收清单(D3 交付标准)
- [ ] 项目能正常启动,无脚本报错
- [ ] **开局**:进入游戏即暂停,弹出"弟子入宗(开局招募)",显示 5 名候选
- [ ] 开局模式确定按钮在选满 3 名前不可用;选满后可确定
- [ ] 确定后:勾选的 3 名入宗(控制台打印),时间恢复推进
- [ ] **定期**:每 36 游戏日(0.1@1x18 秒)再次暂停弹窗,显示 3 名候选
- [ ] **定期**:每 90 游戏日(0.25@1x45 秒)再次暂停弹窗,显示 3 名候选
- [ ] 定期模式可勾选 0~3 名,全部拒绝也可确定;时间正常恢复
- [ ] 多次招募后能观察到天灵根弟子(约 3% 概率,弹窗/控制台有标记)
- [ ] 多次招募后能观察到天灵根弟子(仅天骄中再判 50%,总体概率约 2.5%,弹窗/控制台有标记)
- [ ] 修改 `game_config.cfg``opening_applicant_count` 等参数,重启后开局候选数随之变化
- [ ] 返回主菜单再进入,名录已清空(reset 生效)
+9 -11
View File
@@ -1,7 +1,7 @@
# 开发计划表
> 开始日期:2026-08-03(周一) 每日有效开发约 2 小时
> 进度更新:2026-08-06  D1D3 完成(D3 含弹窗招募与配置系统),D4 进行中
> 进度更新:2026-08-10  D1D9 全部完成(D8 两级存档系统验收 9/9,D9 大境界突破 + 数值打磨),M1 验收(D10)进行中
## 一、每日作息模板(工作日)
@@ -21,16 +21,14 @@
| ---- | -- | ---- | ---- | -------- | ---- |
| 8/3 一 | D1 | TimeSystem | ① 日期变量(年月日) ② tick 推进 ③ 暂停/加速 | HUD 显示年月日,可调速度 | ✅ 完成(含月/年信号、Settings 持久化) |
| 8/4 二 | D2 | SectManager 数据模型 | ① 单例 ② 灵石/灵气/声望 ③ 增减接口 | 控制台可读写资源 | ✅ 完成(灵石/声望增减+信号,HUD 实时绑定) |
| 8/5 三 | D3 | 弟子生成 + 入宗分流 | ① 随机名字/资质/灵根 ② 按资质分四类 ③ 招募流程 | 开局 5 选 3 必选满,定期弹窗可全拒/全收 | ✅ 完成(弹窗招募:开局5选3+定期3候选,配置驱动含天灵根3%;新增 GameConfig 配置系统) |
| 8/6 四 | D4 | 弟子列表 UI | ① 列表条目场景 ② 姓名/资质/境界显示 ③ 点击详情 | 列表可浏览弟子 | |
| 8/7 五 | D5 | 修炼循环 | ① 修为随时间增长(资质×灵气) ② 境界门槛 ③ 突破判定 | 弟子自动修炼,可突破 | |
| 8/8 六 | D6 | 建筑系统基础 | ① 灵石矿场景 ② 建造→每时辰产出 ③ 升级 | 建筑产灵石,升级加速 | |
| 8/9 日 | D7 | 休息 / 补漏 | 修本周 bug,整理代码 | git 干净 | |
| 8/10 一 | D8 | 存档接入 | ① SaveSystem JSON 序列化 ② 存/读接口 ③ 替换 SavePanel 测试数据 | 读档后数据还原 | |
| 8/11 二 | D9 | 打磨 | 数值平衡初调,玩 10 分钟验证 | 无致命 bug | |
| 8/12 三 | D10 | **M1 验收** | 完整跑一遍:新游戏→招弟子→挂机→存→退→读 | 闭环可玩 | ⬜ |
## 三、第 35 周(8/179/6):P2 扩展
| 8/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 验收** | 完整跑一遍:新游戏→招弟子→挂机→存→退→读 | 闭环可玩 | ⬜ |## 三、第 35 周(8/179/6):P2 扩展
| 日期 | 周 | 任务 | 拆解 | 交付 |
| ---- | -- | ---- | ---- | ---- |
+1 -1
View File
@@ -8,7 +8,7 @@ name="Windows Desktop"
platform="Windows Desktop"
dedicated_server=false
custom_features=""
export_filter="scenes"
export_filter="all_resources"
export_files=PackedStringArray()
include_filter=""
exclude_filter=""
+4 -1
View File
@@ -11,7 +11,7 @@ config_version=5
[application]
config/name="模拟宗门"
run/main_scene="uid://d1odycoaqhhyg"
run/main_scene="res://src/UI/Panels/MainMenu/MainMenu.tscn"
config/features=PackedStringArray("4.7", "Forward Plus")
[autoload]
@@ -22,6 +22,9 @@ 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]
+45 -4
View File
@@ -6,12 +6,53 @@
difficulty="normal"
opening_applicant_count=5
opening_pick_count=3
recruit_interval_years=1
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]
aptitude_weights=[45, 35, 15, 5] ; 45% / 35% / 15% / 5%
[recruitment_hard]
aptitude_weights=[60, 30, 8, 2]
aptitude_weights=[60, 30, 8, 2] ; 60% / 30% / 8% / 2%
[cultivation] ; 修炼系统
base_speed=10 ; 基础修为/日
qi_concentration=1.0 ; 宗门灵气浓度
aptitude_speed=[0.6, 1.0, 1.5, 2.0] ; 资质倍率(下品/中品/上品/天骄)
heavenly_bonus=1.5 ; 天灵根额外修炼速度倍率
breakthrough_chances=[0.85, 0.8, 0.75, 0.6] ; 各瓶颈层突破成功率(与 bottleneck_levels 一一对应:升4层/7层/10层/13层)
level_thresholds=[100, 200, 350, 550, 800, 1100, 1500, 2000, 2600, 3300, 4100, 5000] ; 练气 1→2 到 12→13 各层所需修为
bottleneck_levels=[4, 7, 10, 13] ; 瓶颈目标层(升到这些层需突破判定)
realm_breakthrough_costs=[500, 4000, 12000, 60000] ; 大境界突破灵石花费(索引 0~3 = 升筑基/金丹/元婴/化神)
realm_breakthrough_chances=[0.6, 0.5, 0.4, 0.3] ; 大境界突破成功率(与 costs 一一对应)
realm_breakthrough_fail_drop=3 ; 突破失败倒退步数(练气倒推层数/筑基+倒推小境,不跨大境界)
realm_sub_thresholds=[6000, 12000, 24000] ; 筑基及以上小境门槛(初期→中期/中期→后期/后期→圆满)
[ui] ; UI 通用
hud_font_size=24 ; HUD 字体大小
[building] ; 建筑系统通用
starting_spirit_stones=500 ; 开局赠送灵石
starting_food=100 ; 开局赠送灵米
max_level=10 ; 建筑等级上限
[building_mine] ; 灵石矿
cost=200 ; 建造价(灵石)
output=10 ; 日产出(灵石)
upgrade_cost_x=2.0 ; 升级价格倍率
upgrade_output_x=1.5 ; 升级产出倍率
[building_field] ; 灵田
cost=100 ; 建造价(灵石)
output=5 ; 日产出(灵米)
upgrade_cost_x=2.0 ; 升级价格倍率
upgrade_output_x=1.5 ; 升级产出倍率
[food] ; 口粮与生存
consumption_per_disciple=1 ; 练气弟子人均日消耗灵米
hunger_speed_multiplier=0.5 ; 饥饿期间修炼速度倍率
hunger_threshold_days=30 ; 断粮累计天数上限(达到即离宗,待接入)
+7
View File
@@ -0,0 +1,7 @@
[gd_resource type="Theme" format=3 uid="uid://05ygdcq8dme4"]
[ext_resource type="FontFile" uid="uid://ce12uai6ul0hj" path="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKaiMono-Regular.ttf" id="1_fwts0"]
[resource]
default_font = ExtResource("1_fwts0")
default_font_size = 24
+12
View File
@@ -0,0 +1,12 @@
[gd_resource type="LabelSettings" format=3 uid="uid://d1v74ab2ldtn7"]
[ext_resource type="FontFile" uid="uid://4su41gib4l5u" path="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKaiMono-Medium.ttf" id="1_fg5xm"]
[resource]
font = ExtResource("1_fg5xm")
font_size = 24
font_color = Color(0.82, 0.95, 0.9, 1)
outline_color = Color(0, 0, 0, 0.8)
shadow_size = 2
shadow_color = Color(0, 0, 0, 0.7)
shadow_offset = Vector2(1, 2)
+7
View File
@@ -0,0 +1,7 @@
[gd_resource type="Theme" format=3 uid="uid://ctuanla5trf4s"]
[ext_resource type="FontFile" uid="uid://ce12uai6ul0hj" path="res://assets/fonts/lxgw-wenkai-v1.522/LXGWWenKaiMono-Regular.ttf" id="1_uif2x"]
[resource]
default_font = ExtResource("1_uif2x")
default_font_size = 32
+148
View File
@@ -0,0 +1,148 @@
## 修炼管理器(Autoload 单例)。
## 负责弟子修为的每日结算与境界推进(D5:修为逐日增长 + 自动升层 + 瓶颈突破判定;D9:大境界手动突破)。
extends Node
## 境界变化信号(突破后发出,供详情面板刷新)。
signal realm_changed(d: Disciple)
## 修炼日志文本(控制台打印,后续日志系统接入)。
signal cultivation_log(text: String)
## 挂接每日推进信号。
func _ready() -> void:
# 每日推进时结算全体已入宗弟子的修为与境界
TimeSystem.day_passed.connect(_on_day_passed)
## 每日结算:先扣口粮,再结算已入宗弟子的修为与境界(候选池不修炼)。
func _on_day_passed(_year: int, _month: int, _day: int) -> void:
_settle_food()
for d in DiscipleManager.disciples:
_gain_exp(d)
_check_breakthrough(d)
_check_sub_realm(d)
## 每日口粮结算:练气弟子人均消耗灵米(筑基+辟谷豁免)。
## 食物充足 → 扣减并饥饿清零;食物不足 → 归零并全员饥饿天数 +1;
## 饥饿天数 ≥ 阈值(配置 hunger_threshold_days)→ 弟子离宗,从名录移除。
func _settle_food() -> void:
var eaters: Array[Disciple] = []
for d in DiscipleManager.disciples:
if d.realm_index == 0:
eaters.append(d)
var need := eaters.size() * GameConfig.food_consumption_per_disciple
if SectManager.food >= need:
SectManager.remove_food(need)
for d in eaters:
d.hunger_days = 0
return
SectManager.remove_food(SectManager.food)
var leavers: Array[Disciple] = []
for d in eaters:
d.hunger_days += 1
if d.hunger_days >= GameConfig.hunger_threshold_days:
leavers.append(d)
for d in leavers:
_log("%s 断粮日久,离宗而去" % d.name)
DiscipleManager.remove_disciple(d)
## 按公式累计一日修为。
## 速度 = 基础速度 × 资质倍率 × 灵根容量 × 天灵根加成 × 灵气浓度 × 饥饿倍率
## 练气圆满(13 层)或大境界圆满(筑基+ 4 小境满)后修为封顶,不再增长。
## @param d: 已入宗弟子
func _gain_exp(d: Disciple) -> void:
if d.is_realm_peak():
return
var speed := GameConfig.cultivation_base_speed
speed *= GameConfig.aptitude_speed[d.aptitude]
speed *= d.get_root_count_capacity()
if d.is_heavenly:
speed *= GameConfig.heavenly_bonus
speed *= GameConfig.qi_concentration
if d.hunger_days > 0:
speed *= GameConfig.hunger_speed_multiplier
d.cultivation_exp += speed
## 突破判定:修为满当前层门槛时推进境界。
## 非瓶颈目标层自动升层;瓶颈目标层掷骰(成功率来自配置),失败扣 20% 修为。
## 每日最多推进一层。
## @param d: 已入宗弟子
func _check_breakthrough(d: Disciple) -> void:
if d.realm_index != 0:
return
var layer := d.sub_realm_index + 1
# 13 层圆满:无更高门槛,不再判定
if layer > GameConfig.level_thresholds.size():
return
if d.cultivation_exp < GameConfig.level_thresholds[layer - 1]:
return
if GameConfig.bottleneck_levels.has(layer + 1):
# 成功率按目标层从配置取(与 bottleneck_levels 一一对应)
var idx := maxi(0, GameConfig.bottleneck_levels.find(layer + 1))
if randf() < GameConfig.bottleneck_chances[idx]:
_advance_layer(d)
else:
d.cultivation_exp *= 0.8
_log("%s 冲关 %s 失败,修为损耗两成" % [d.name, d.get_realm_text()])
else:
_advance_layer(d)
## 筑基及以上小境推进:修为满小境门槛自动升小境(初期→中期→后期→圆满,无失败)。
## 大境界圆满后不再推进,等待玩家手动大境界突破。
## @param d: 已入宗弟子
func _check_sub_realm(d: Disciple) -> void:
if d.realm_index == 0:
return
if d.sub_realm_index >= GameConfig.realm_sub_thresholds.size():
return
if d.cultivation_exp < GameConfig.realm_sub_thresholds[d.sub_realm_index]:
return
var threshold: float = GameConfig.realm_sub_thresholds[d.sub_realm_index]
d.cultivation_exp -= threshold
d.sub_realm_index += 1
realm_changed.emit(d)
_log("%s 突破至 %s" % [d.name, d.get_realm_text()])
## 大境界突破(玩家手动调用,挂机不自动触发)。
## 流程:校验修为圆满与灵石 → 扣灵石(成败不退还)→ 掷骰(成功率按目标大境界配置)。
## 成功:realm_index+1(新大境界初期,修为清零);失败:修为倒退 fail_drop 步(练气退层数/筑基+退小境,不跨大境界)。
## 灵石不足或修为未圆满返回 false(不扣灵石)。
## @param d: 已入宗弟子
func try_realm_breakthrough(d: Disciple) -> bool:
if not d.is_realm_peak():
return false
var next := d.get_next_realm_index()
if next < 0:
return false
var cost := d.get_realm_breakthrough_cost()
if SectManager.spirit_stones < cost:
return false
SectManager.remove_spirit_stones(cost)
var chance: float = GameConfig.realm_breakthrough_chances[next - 1]
if randf() < chance:
d.realm_index = next
d.sub_realm_index = 0
d.cultivation_exp = 0.0
realm_changed.emit(d)
_log("%s 突破至 %s" % [d.name, d.get_realm_text()])
return true
var drop := GameConfig.realm_breakthrough_fail_drop
d.sub_realm_index = maxi(0, d.sub_realm_index - drop)
d.cultivation_exp = 0.0
realm_changed.emit(d)
_log("%s 突破失败,灵石化为乌有,修为倒退至 %s" % [d.name, d.get_realm_text()])
return false
## 推进一层境界:扣除本层门槛修为(余量带入下一层)并发信号。
## @param d: 已入宗弟子
func _advance_layer(d: Disciple) -> void:
var threshold := GameConfig.level_thresholds[d.sub_realm_index]
d.cultivation_exp -= threshold
d.sub_realm_index += 1
realm_changed.emit(d)
_log("%s 突破至 %s" % [d.name, d.get_realm_text()])
## 输出修炼日志(控制台打印 + 信号广播)。
## @param text: 日志文本
func _log(text: String) -> void:
cultivation_log.emit(text)
print(text)
+1
View File
@@ -0,0 +1 @@
uid://q4pk0n21oawr
+101 -6
View File
@@ -9,34 +9,129 @@ enum Aptitude { ORDINARY, MEDIUM, SUPERIOR, HEAVENLY } # 下品/中品/上品/
## 灵根五行。
enum SpiritRoot { GOLD, WOOD, WATER, FIRE, EARTH }
# —— 常量表:枚举值 → 显示文本 / 倍率(按索引一一对应)——
# —— 常量表:枚举值 → 显示文本(按索引一一对应)——
const APTITUDE_NAMES: Array[String] = ["下品", "中品", "上品", "天骄"]
const APTITUDE_SPEED: Array[float] = [0.6, 1.0, 1.5, 2.0] # D5 用
const ROOT_NAMES: Array[String] = ["", "", "", "", ""]
const REALM_NAMES: Array[String] = ["练气", "筑基", "金丹", "元婴", "化神"] # 按 realm_index 索引
const HEAVENLY_BONUS := 1.5 # 天灵根额外修炼速度倍率(D5 用)
# —— 修炼数值一律读 GameConfig(资质倍率/天灵根加成/速度等)——
# —— 实例字段 ——
var id: int = 0 # 唯一编号(DiscipleManager 分配)
var name: String = ""
var age: int = 0
var aptitude: Aptitude = Aptitude.ORDINARY
var roots: Array[SpiritRoot] = [] # 普通弟子 1~3 根;天灵根固定 1 根
var is_heavenly: bool = false # 天灵根标记(3% 概率
var 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]
+36 -9
View File
@@ -14,14 +14,12 @@ var disciples: Array[Disciple] = []
## 候选弟子池(未入宗,等待玩家选择)。
var applicants: Array[Disciple] = []
## 天灵根生成概率(3%)。
const HEAVENLY_CHANCE := 0.03
## 姓氏池。
const SURNAMES: Array[String] = [
"", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "",
]
## 名用字池(修仙风)。
@@ -37,6 +35,7 @@ var _days_until_recruit := 0
var _next_id := 1
## 挂接每日推进信号并初始化招募倒计时。
func _ready() -> void:
# 挂到时间系统上:每日推进时倒计时,满间隔生成候选并暂停时间
TimeSystem.day_passed.connect(_on_day_passed)
@@ -65,7 +64,7 @@ func generate_applicants(count: int, is_opening := false) -> void:
applicants.append(_create_applicant())
applicants_generated.emit(is_opening)
## 接受一名候选弟子:分配 ID 并移入名录(候选池 → 名录)。
## 接受一名候选弟子:分配 ID、快照入宗时间并移入名录(候选池 → 名录)。
## @param d: 要接受的候选
func accept_applicant(d: Disciple) -> void:
if not applicants.has(d):
@@ -73,6 +72,9 @@ func accept_applicant(d: Disciple) -> void:
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" % [
@@ -109,20 +111,31 @@ func reset() -> void:
_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.is_heavenly = randf() < HEAVENLY_CHANCE
d.roots = _generate_roots(d.is_heavenly)
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 个名用字。
@@ -134,12 +147,13 @@ func _generate_name() -> String:
return surname + given
## 随机生成灵根。
## 天灵根:固定 1 根五行;普通弟子:1~3互不重复(根数越少修炼越快)
## 天灵根:固定 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 := randi_range(1, 3)
var count := _roll_root_count()
var pool := Disciple.SpiritRoot.values()
pool.shuffle()
var result: Array[Disciple.SpiritRoot] = []
@@ -147,6 +161,19 @@ func _generate_roots(heavenly: bool) -> Array[Disciple.SpiritRoot]:
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()
+129 -1
View File
@@ -14,9 +14,64 @@ var opening_applicant_count := 5
## 开局必须选满数。
var opening_pick_count := 3
## 定期招募间隔(游戏年,1 年 = 360 天)。
var recruit_interval_years := 0.1
## 注意: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 = {
@@ -25,6 +80,7 @@ var aptitude_weights_by_difficulty: Dictionary = {
}
## 启动时加载配置文件。
func _ready() -> void:
_load_config()
@@ -40,6 +96,78 @@ func _load_config() -> void:
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", [])
+17 -11
View File
@@ -7,24 +7,30 @@ enum GameMode { NEW_GAME, LOAD_GAME }
## 当前进入游戏的方式。
var mode: GameMode = GameMode.NEW_GAME
## 要加载的存档 ID仅 LOAD_GAME 时有效)。
var save_id: String = ""
## 当前存档 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
var day: int = 1
var year: int = 1 # 年
var month: int = 1 # 月(1-12
var day: int = 1 # 日(1-30
## 请求开始新游戏:重置模式与时间
func request_new_game() -> void:
## 请求开始新游戏。
## @param pid: 新存档组 ID(主菜单创建)
func request_new_game(pid: int) -> void:
mode = GameMode.NEW_GAME
save_id = ""
profile_id = pid
save_slot = ""
year = 1
month = 1
day = 1
## 请求加载指定存档。
## @param id: 存档 ID
func request_load_game(id: String) -> void:
## @param pid: 存档 ID
## @param slot_name: 槽位文件名(不含 .json
func request_load_game(pid: int, slot_name: String) -> void:
mode = GameMode.LOAD_GAME
save_id = id
profile_id = pid
save_slot = slot_name
+34 -3
View File
@@ -2,14 +2,45 @@
## 进入游戏时根据 GameState 模式初始化,并连接顶部 HUD 信号。
extends Node
## 进入游戏世界:注册场景分组,按 GameState 模式初始化(新游戏/读档)。
func _ready() -> void:
# 注册为游戏世界场景(供 TimeSystem 判断是否推进时间)
add_to_group("game_scene")
# 应用用户设置中的默认流速
TimeSystem.apply_default_speed()
# 开局招募:清空旧数据 → 生成 5 名候选 → 暂停时间等待选择(读档流程接入后由存档还原)
DiscipleManager.start_new_game()
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()
## 顶部 HUD 的返回主菜单请求
## 新游戏初始化:清空旧数据 → 开局招募弹窗(暂停)→ 设定开局资源
func _start_new_game() -> void:
# 开局招募:清空旧数据 → 生成 5 名候选 → 暂停时间等待选择
DiscipleManager.start_new_game()
# 开局建筑:清空旧建筑
BuildingManager.reset()
# 开局资源:直接设定(set 而非 add,避免与上次会话残留混淆)
SectManager.set_spirit_stones(GameConfig.starting_spirit_stones)
SectManager.set_food(GameConfig.starting_food)
SectManager.set_reputation(0)
## 顶部 HUD 的返回主菜单请求(先写自动存档)。
func _on_top_bar_hud_sign_return_mainmenu() -> void:
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()
+33
View File
@@ -1,15 +1,48 @@
[gd_scene format=3 uid="uid://box7tj5iyiopq"]
[ext_resource type="Script" uid="uid://centrk8l54j7c" path="res://src/Core/MainGame.gd" id="1_8bu23"]
[ext_resource type="PackedScene" uid="uid://cahux61qqblje" path="res://src/UI/Panels/DisciplesPanel/DisciplesPanel.tscn" id="1_d5cpm"]
[ext_resource type="PackedScene" uid="uid://dl0tb68vsw3c6" path="res://src/UI/HUD/TopBarHUD.tscn" id="1_g6xmk"]
[ext_resource type="PackedScene" uid="uid://cahbpjpnmbcfv" path="res://src/UI/Panels/RecruitPanel/RecruitPanel.tscn" id="1_k5nrm"]
[ext_resource type="PackedScene" path="res://src/UI/Panels/BuildingsPanel/BuildingsPanel.tscn" id="1_b9dpm"]
[ext_resource type="PackedScene" uid="uid://dj7b5vssydifa" path="res://src/UI/Panels/SavePanel.tscn" id="1_s4vpm"]
[ext_resource type="Texture2D" uid="uid://cweu17mwblxmg" path="res://assets/sprites/ui/宗门后山.png" id="2_2atgf"]
[ext_resource type="Texture2D" uid="uid://dnjuv1btbxm4o" path="res://assets/sprites/ui/宗门全景.png" id="5_58g0x"]
[node name="MainGame" type="Node" unique_id=2053561265]
script = ExtResource("1_8bu23")
[node name="BackMounton" type="TextureRect" parent="." unique_id=588798961]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("2_2atgf")
expand_mode = 2
[node name="BG" type="TextureRect" parent="." unique_id=1246873308]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("5_58g0x")
expand_mode = 2
[node name="TopBarHud" parent="." unique_id=1115394694 instance=ExtResource("1_g6xmk")]
[node name="RecruitPanel" parent="." unique_id=770000016 instance=ExtResource("1_k5nrm")]
layout_mode = 3
[node name="DisciplesPanel" parent="." unique_id=780000015 instance=ExtResource("1_d5cpm")]
[node name="BuildingsPanel" parent="." unique_id=795000012 instance=ExtResource("1_b9dpm")]
[node name="SavePanel" parent="." unique_id=794000020 instance=ExtResource("1_s4vpm")]
visible = false
[connection signal="sign_open_buildings" from="TopBarHud" to="." method="_on_top_bar_hud_sign_open_buildings"]
[connection signal="sign_open_disciples" from="TopBarHud" to="." method="_on_top_bar_hud_sign_open_disciples"]
[connection signal="sign_open_save" from="TopBarHud" to="." method="_on_top_bar_hud_sign_open_save"]
[connection signal="sign_return_mainmenu" from="TopBarHud" to="." method="_on_top_bar_hud_sign_return_mainmenu"]
+286
View File
@@ -0,0 +1,286 @@
## 存档系统(Autoload 单例)。
## 两级存档:存档组 SaveGroup(一个角色/一次轮回)→ 存档槽 SaveSlot(该组下任意多个进度快照)。
## 每个新游戏创建一个组;每次存档在该组下新建一个槽;组与槽数量无上限。
## 文件:user://saves/profile_{组id}/{auto|slot_{时间戳}}.json
extends Node
## 存档根目录。
const SAVE_ROOT := "user://saves"
## 存档组列表文件。
const PROFILES_PATH := "user://saves/profiles.json"
## 自动槽文件名(固定)。
const AUTO_SLOT := "auto"
## 存档格式版本(兼容迁移预留)。
const VERSION := 1
## 新建存档组(主菜单"新游戏"调用),返回组 id(名"存档N")。
func create_profile() -> int:
var profiles := _load_profiles()
var id := 1
for p in profiles:
id = maxi(id, int(p["id"]) + 1)
profiles.append({
"id": id,
"name": "存档%d" % id,
"created": Time.get_date_string_from_system(),
"last_date": "",
})
_save_profiles(profiles)
return id
## 存档组列表(id/name/created/last_date)。
func get_profiles() -> Array[Dictionary]:
return _load_profiles()
## 删除存档组:从列表移除并删除该组目录(含全部槽位)。
## @param profile_id: 存档组 ID
## @return 是否删除成功
func delete_profile(profile_id: int) -> bool:
var profiles := _load_profiles()
var removed := false
for i in profiles.size():
if int(profiles[i]["id"]) == profile_id:
profiles.remove_at(i)
removed = true
break
if not removed:
return false
_save_profiles(profiles)
_remove_dir_recursive("%s/profile_%d" % [SAVE_ROOT, profile_id])
return true
## 保存游戏:在指定组下**新建一个槽位**,返回槽文件名;失败返回空串。
## 槽位命名:组内序号递增(slot_1、slot_2…),删除后从当前最大序号 +1 继续,不重用。
## @param profile_id: 存档组 ID
func save_game(profile_id: int) -> String:
_ensure_profile_dir(profile_id)
var slot_name := "slot_%d" % _next_slot_index(profile_id)
if not _write_slot(profile_id, slot_name):
return ""
_update_profile_last_date(profile_id)
return slot_name
## 保存到自动槽(返回主菜单时调用)。
## @param profile_id: 存档组 ID
func save_auto(profile_id: int) -> bool:
_ensure_profile_dir(profile_id)
return _write_slot(profile_id, AUTO_SLOT)
## 该组全部槽位(含自动槽,自动槽排最前;手动槽按时间升序)。
## 返回 [{name, date_text, disciple_count, spirit_stones}]。
## @param profile_id: 存档组 ID
func get_slots(profile_id: int) -> Array[Dictionary]:
var result: Array[Dictionary] = []
var dir := DirAccess.open("%s/profile_%d" % [SAVE_ROOT, profile_id])
if dir == null:
return result
dir.list_dir_begin()
var file_name := dir.get_next()
while file_name != "":
if not dir.current_is_dir() and file_name.ends_with(".json"):
var meta := get_slot_meta(profile_id, file_name.trim_suffix(".json"))
if not meta.is_empty():
meta["name"] = file_name.trim_suffix(".json")
result.append(meta)
file_name = dir.get_next()
dir.list_dir_end()
result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
var a_auto: bool = a["name"] == AUTO_SLOT
var b_auto: bool = b["name"] == AUTO_SLOT
if a_auto != b_auto:
return a_auto
return str(a["name"]) < str(b["name"])
)
return result
## 加载存档:清空当前数据后还原各系统。
## @param profile_id: 存档组 ID
## @param slot_name: 槽文件名(不含 .json
## @return 是否加载成功
func load_game(profile_id: int, slot_name: String) -> bool:
var f := FileAccess.open(_slot_path(profile_id, slot_name), FileAccess.READ)
if f == null:
push_warning("存档不存在: " + _slot_path(profile_id, slot_name))
return false
var parsed: Variant = JSON.parse_string(f.get_as_text())
if parsed is not Dictionary:
push_warning("存档解析失败: " + _slot_path(profile_id, slot_name))
return false
_restore(parsed)
return true
## 删除槽位文件。
## @param profile_id: 存档组 ID
## @param slot_name: 槽文件名(不含 .json
## @return 是否删除成功
func delete_slot(profile_id: int, slot_name: String) -> bool:
if slot_name == AUTO_SLOT:
return false
var path := _slot_path(profile_id, slot_name)
if not FileAccess.file_exists(path):
return false
return DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) == OK
## 槽位元数据(列表显示用:日期/弟子数/灵石),空档返回空字典。
## @param profile_id: 存档组 ID
## @param slot_name: 槽文件名(不含 .json
func get_slot_meta(profile_id: int, slot_name: String) -> Dictionary:
var f := FileAccess.open(_slot_path(profile_id, slot_name), FileAccess.READ)
if f == null:
return {}
var parsed: Variant = JSON.parse_string(f.get_as_text())
if parsed is not Dictionary:
return {}
var meta: Dictionary = parsed
var d: Dictionary = meta["saved_date"]
return {
"date_text": "%d%d%d" % [d["year"], d["month"], d["day"]],
"disciple_count": int(meta["disciples"].size()),
"spirit_stones": int(meta["resources"]["spirit_stones"]),
}
## 清空内存中所有游戏数据(读档/新游戏前调用)。
func reset() -> void:
DiscipleManager.reset()
BuildingManager.reset()
SectManager.set_spirit_stones(0)
SectManager.set_reputation(0)
SectManager.set_food(0)
## 收集当前游戏数据并写入槽位文件。
## @param profile_id: 存档组 ID
## @param slot_name: 槽文件名(不含 .json
func _write_slot(profile_id: int, slot_name: String) -> bool:
var data := _collect_data(profile_id)
var f := FileAccess.open(_slot_path(profile_id, slot_name), FileAccess.WRITE)
if f == null:
push_warning("存档写入失败: " + _slot_path(profile_id, slot_name))
return false
f.store_string(JSON.stringify(data, "\t"))
return true
## 收集当前游戏数据为字典。
## @param profile_id: 存档组 ID
func _collect_data(profile_id: int) -> Dictionary:
var disciples_data: Array = []
for d in DiscipleManager.disciples:
disciples_data.append(d.to_dict())
var buildings_data: Array = []
for b in BuildingManager.buildings:
buildings_data.append(b.to_dict())
return {
"version": VERSION,
"profile_id": profile_id,
"saved_date": {"year": GameState.year, "month": GameState.month, "day": GameState.day},
"date": {"year": GameState.year, "month": GameState.month, "day": GameState.day},
"resources": {
"spirit_stones": SectManager.spirit_stones,
"reputation": SectManager.reputation,
"food": SectManager.food,
},
"next_disciple_id": DiscipleManager.get_next_disciple_id(),
"disciples": disciples_data,
"buildings": buildings_data,
}
## 将存档数据还原到各系统(setter 带信号,HUD 自动刷新)。
## @param data: 存档字典
func _restore(data: Dictionary) -> void:
reset()
var d: Dictionary = data["date"]
TimeSystem.set_date(int(d["year"]), int(d["month"]), int(d["day"]))
var r: Dictionary = data["resources"]
SectManager.set_spirit_stones(int(r["spirit_stones"]))
SectManager.set_reputation(int(r["reputation"]))
SectManager.set_food(int(r["food"]))
DiscipleManager.set_next_disciple_id(int(data["next_disciple_id"]))
for item in data["disciples"]:
DiscipleManager.disciples.append(Disciple.from_dict(item))
DiscipleManager.disciples_changed.emit()
for item in data["buildings"]:
BuildingManager.buildings.append(Building.from_dict(item))
BuildingManager.buildings_changed.emit()
## 槽位文件路径。
func _slot_path(profile_id: int, slot_name: String) -> String:
return "%s/profile_%d/%s.json" % [SAVE_ROOT, profile_id, slot_name]
## 确保存档组目录存在。
func _ensure_profile_dir(profile_id: int) -> void:
var da := DirAccess.open("user://")
if da == null:
return
da.make_dir_recursive("saves")
da.make_dir_recursive("saves/profile_%d" % profile_id)
## 递归删除目录(Godot 无原生递归删除,手动遍历)。
## @param path: 目录路径(user:// 风格)
func _remove_dir_recursive(path: String) -> void:
var da := DirAccess.open(path)
if da == null:
return
da.list_dir_begin()
var f := da.get_next()
while f != "":
if da.current_is_dir():
_remove_dir_recursive(path + "/" + f)
else:
DirAccess.remove_absolute(ProjectSettings.globalize_path(path + "/" + f))
f = da.get_next()
da.list_dir_end()
da.remove(da.get_current_dir())
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
## 该组下一个槽位序号(现有最大 slot_N 序号 +1,删除后不重用)。
## @param profile_id: 存档组 ID
func _next_slot_index(profile_id: int) -> int:
var max_index := 0
var dir := DirAccess.open("%s/profile_%d" % [SAVE_ROOT, profile_id])
if dir == null:
return 1
dir.list_dir_begin()
var f := dir.get_next()
while f != "":
if f.begins_with("slot_") and f.ends_with(".json"):
var idx := int(f.trim_suffix(".json").trim_prefix("slot_"))
max_index = maxi(max_index, idx)
f = dir.get_next()
dir.list_dir_end()
return max_index + 1
## 读取存档组列表。
func _load_profiles() -> Array[Dictionary]:
if not FileAccess.file_exists(PROFILES_PATH):
return []
var f := FileAccess.open(PROFILES_PATH, FileAccess.READ)
if f == null:
return []
var parsed: Variant = JSON.parse_string(f.get_as_text())
if parsed is not Dictionary or not parsed.has("profiles"):
return []
var result: Array[Dictionary] = []
for p in parsed["profiles"]:
result.append(p)
return result
## 写入存档组列表。
## @param profiles: 存档组列表
func _save_profiles(profiles: Array[Dictionary]) -> void:
var da := DirAccess.open("user://")
if da != null:
da.make_dir_recursive("saves")
var f := FileAccess.open(PROFILES_PATH, FileAccess.WRITE)
if f == null:
return
f.store_string(JSON.stringify({"profiles": profiles}, "\t"))
## 更新存档组的最后存档日期。
## @param profile_id: 存档组 ID
func _update_profile_last_date(profile_id: int) -> void:
var profiles := _load_profiles()
for p in profiles:
if int(p["id"]) == profile_id:
p["last_date"] = "%d%d%d" % [GameState.year, GameState.month, GameState.day]
break
_save_profiles(profiles)
+1
View File
@@ -0,0 +1 @@
uid://bwt83xydsgcir
+43
View File
@@ -3,23 +3,66 @@ extends Node
signal spirit_stones_changed(new_value: int, delta: int)
signal reputation_changed(new_value: int, delta: int)
signal food_changed(new_value: int, delta: int)
var spirit_stones := 0 # 灵石
var reputation := 0 # 声望
var food := 0 # 灵米(食物资源,HUD 显示"灵米"
## 增加灵石并广播信号。
## @param amount: 增加量
func add_spirit_stones(amount: int) -> void:
spirit_stones += amount
emit_signal("spirit_stones_changed", spirit_stones, amount)
## 减少灵石并广播信号。
## @param amount: 减少量
func remove_spirit_stones(amount: int) -> void:
spirit_stones -= amount
emit_signal("spirit_stones_changed", spirit_stones, -amount)
## 增加声望并广播信号。
## @param amount: 增加量
func add_reputation(amount: int) -> void:
reputation += amount
emit_signal("reputation_changed", reputation, amount)
## 减少声望并广播信号。
## @param amount: 减少量
func remove_reputation(amount: int) -> void:
reputation -= amount
emit_signal("reputation_changed", reputation, -amount)
## 增加灵米并广播信号。
## @param amount: 增加量
func add_food(amount: int) -> void:
food += amount
emit_signal("food_changed", food, amount)
## 减少灵米并广播信号。
## @param amount: 减少量
func remove_food(amount: int) -> void:
food -= amount
emit_signal("food_changed", food, -amount)
## 直接设定灵石(读档/新游戏用,带信号 HUD 自动刷新)。
## @param value: 目标值
func set_spirit_stones(value: int) -> void:
var delta := value - spirit_stones
spirit_stones = value
spirit_stones_changed.emit(spirit_stones, delta)
## 直接设定声望(读档/新游戏用,带信号 HUD 自动刷新)。
## @param value: 目标值
func set_reputation(value: int) -> void:
var delta := value - reputation
reputation = value
reputation_changed.emit(reputation, delta)
## 直接设定灵米(读档/新游戏用,带信号 HUD 自动刷新)。
## @param value: 目标值
func set_food(value: int) -> void:
var delta := value - food
food = value
food_changed.emit(food, delta)
+1
View File
@@ -41,6 +41,7 @@ var paused := false
var _elapsed := 0.0
## 每帧累积时间,满一天推进日期(仅游戏世界场景内)。
func _process(delta: float) -> void:
if paused or not _is_in_game():
return
+64
View File
@@ -0,0 +1,64 @@
## 建筑管理器(Autoload 单例)。
## 负责建筑建造/升级与每日产出结算,产出直接进入 SectManager 资源。
## 本期每种建筑至多一座(面板按类型一行展示);多实例留待后续扩展。
extends Node
## 建筑列表变化时发出(建造/升级/重置)。
signal buildings_changed
## 宗门建筑列表。
var buildings: Array[Building] = []
## 挂接每日推进信号,结算建筑产出。
func _ready() -> void:
# 每日推进时结算各建筑产出
TimeSystem.day_passed.connect(_on_day_passed)
## 建造建筑:检查灵石足够 → 扣费入列。
## 同类型已存在时拒绝(本期一座一型)。
## @param type: 建筑种类
## @return 是否建造成功
func build(type: Building.Type) -> bool:
if _find(type) != null:
return false
var b := Building.new()
b.type = type
if SectManager.spirit_stones < b.get_build_cost():
return false
SectManager.remove_spirit_stones(b.get_build_cost())
buildings.append(b)
buildings_changed.emit()
return true
## 升级建筑:检查灵石足够 → 扣费提升等级。
## @param b: 要升级的建筑
## @return 是否升级成功
func upgrade(b: Building) -> bool:
if b.is_max_level() or SectManager.spirit_stones < b.get_upgrade_cost():
return false
SectManager.remove_spirit_stones(b.get_upgrade_cost())
b.level += 1
buildings_changed.emit()
return true
## 清空建筑列表(新游戏开局调用)。
func reset() -> void:
buildings.clear()
buildings_changed.emit()
## 每日结算:各建筑产出进入 SectManager 对应资源。
func _on_day_passed(_year: int, _month: int, _day: int) -> void:
for b in buildings:
match b.type:
Building.Type.MINE:
SectManager.add_spirit_stones(b.get_output())
Building.Type.FIELD:
SectManager.add_food(b.get_output())
## 查找指定类型的建筑,不存在返回 null。
func _find(type: Building.Type) -> Building:
for b in buildings:
if b.type == type:
return b
return null
+1
View File
@@ -0,0 +1 @@
uid://6hi0wog110up
+66
View File
@@ -0,0 +1,66 @@
## 建筑数据模型(纯数据,不挂在场景树上)。
## 价格与产出不在建筑上存副本,运行时按 type+level 从 GameConfig 计算。
class_name Building
extends Resource
## 建筑种类。
enum Type { MINE, FIELD } # 灵石矿 / 灵田
## 建筑类型。
var type: Type = Type.MINE
## 等级(1 起)。
var level: int = 1
## 建筑类型键(对应 GameConfig.building_params 字典键)。
static func get_type_key(t: Type) -> String:
match t:
Type.MINE:
return "mine"
Type.FIELD:
return "field"
return ""
## 建筑显示名称。
static func get_type_name(t: Type) -> String:
match t:
Type.MINE:
return "灵石矿"
Type.FIELD:
return "灵田"
return ""
## 建造价格(灵石)。
func get_build_cost() -> int:
return get_build_cost_for(type)
## 建造价格(静态查询,无需实例)。
static func get_build_cost_for(t: Type) -> int:
var p: Dictionary = GameConfig.building_params[get_type_key(t)]
return int(p["cost"])
## 日产出量 = 基础产出 × 升级产出倍率^(等级-1)。
func get_output() -> int:
var p: Dictionary = GameConfig.building_params[get_type_key(type)]
return int(p["output"] * pow(p["upgrade_output_x"], level - 1))
## 升级价格 = 建造价 × 升级价格倍率^等级。
func get_upgrade_cost() -> int:
var p: Dictionary = GameConfig.building_params[get_type_key(type)]
return int(p["cost"] * pow(p["upgrade_cost_x"], level))
## 是否已达等级上限。
func is_max_level() -> bool:
return level >= GameConfig.building_max_level
## 序列化为字典(D8 存档用)。
func to_dict() -> Dictionary:
return {"type": int(type), "level": level}
## 从字典还原(D8 读档用)。
## @param data: to_dict 产生的字典
static func from_dict(data: Dictionary) -> Building:
var b := Building.new()
b.type = int(data["type"]) as Type
b.level = int(data["level"])
return b
+1
View File
@@ -0,0 +1 @@
uid://dmfhbyjwngqh3
+164 -95
View File
@@ -1,11 +1,14 @@
[gd_scene format=3 uid="uid://dl0tb68vsw3c6"]
[ext_resource type="FontFile" uid="uid://drt448qncx27p" path="res://assets/fonts/dingliezhuhaifont-20240831GengXinBan)-2.ttf" id="1_pa2rn"]
[ext_resource type="Script" uid="uid://d3o1ssph7ca58" path="res://src/UI/HUD/top_bar_hud.gd" id="1_vm4eo"]
[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 = ExtResource("1_pa2rn")
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
@@ -16,151 +19,217 @@ grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_vm4eo")
[node name="HBoxContainer" type="HBoxContainer" parent="." unique_id=1608257100]
[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=1616038110]
layout_mode = 1
anchors_preset = 10
anchors_preset = 15
anchor_right = 1.0
offset_bottom = 23.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Label_Currency" type="Label" parent="HBoxContainer" unique_id=766096596]
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer" unique_id=2089867867]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
text = "灵石"
label_settings = SubResource("LabelSettings_bn2o5")
[node name="Value_Currency" type="Label" parent="HBoxContainer" unique_id=1150573727]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
size_flags_stretch_ratio = 2.0
label_settings = SubResource("LabelSettings_bn2o5")
[node name="Label_Time" type="Label" parent="HBoxContainer" unique_id=671872515]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
text = "时间"
label_settings = SubResource("LabelSettings_bn2o5")
[node name="Value_Time" type="Label" parent="HBoxContainer" unique_id=1229577188]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
size_flags_stretch_ratio = 2.0
label_settings = SubResource("LabelSettings_bn2o5")
[node name="Label_Reputation" type="Label" parent="HBoxContainer" unique_id=985110673]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
text = "声望"
label_settings = SubResource("LabelSettings_bn2o5")
[node name="Value_Reputation" type="Label" parent="HBoxContainer" unique_id=2011931119]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
size_flags_stretch_ratio = 2.0
label_settings = SubResource("LabelSettings_bn2o5")
[node name="SpeedGroup" type="HBoxContainer" parent="HBoxContainer" unique_id=2144893126]
[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="HBoxContainer/SpeedGroup" unique_id=2144893127]
[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_override_fonts/font = ExtResource("1_pa2rn")
theme_override_font_sizes/font_size = 32
theme = ExtResource("2_ymrsi")
text = "暂停"
[node name="Button_Speed1" type="Button" parent="HBoxContainer/SpeedGroup" unique_id=2144893128]
[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_override_fonts/font = ExtResource("1_pa2rn")
theme_override_font_sizes/font_size = 32
theme = ExtResource("2_ymrsi")
text = "1x"
[node name="Button_Speed5" type="Button" parent="HBoxContainer/SpeedGroup" unique_id=2144893129]
[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_override_fonts/font = ExtResource("1_pa2rn")
theme_override_font_sizes/font_size = 32
theme = ExtResource("2_ymrsi")
text = "5x"
[node name="Button_Speed20" type="Button" parent="HBoxContainer/SpeedGroup" unique_id=2144893130]
[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_override_fonts/font = ExtResource("1_pa2rn")
theme_override_font_sizes/font_size = 32
theme = ExtResource("2_ymrsi")
text = "20x"
[node name="placeholder_1" type="Label" parent="HBoxContainer" unique_id=1042446504]
[node name="placeholder_1" type="Label" parent="VBoxContainer/HBoxContainer2" unique_id=1042446504]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
size_flags_vertical = 1
size_flags_stretch_ratio = 8.0
label_settings = SubResource("LabelSettings_bn2o5")
[node name="placeholder_2" type="Label" parent="HBoxContainer" unique_id=1526548351]
[node name="Button_StaffManagement" type="Button" parent="VBoxContainer/HBoxContainer2" unique_id=568204305]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
label_settings = SubResource("LabelSettings_bn2o5")
[node name="placeholder_3" type="Label" parent="HBoxContainer" unique_id=1170426663]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
label_settings = SubResource("LabelSettings_bn2o5")
[node name="placeholder_4" type="Label" parent="HBoxContainer" unique_id=1878816574]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
label_settings = SubResource("LabelSettings_bn2o5")
[node name="Button_StaffManagement" type="Button" parent="HBoxContainer" unique_id=568204305]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 4
theme_override_fonts/font = ExtResource("1_pa2rn")
theme_override_font_sizes/font_size = 32
theme = ExtResource("2_ymrsi")
text = "管理"
[node name="Button_ResourcesManagement" type="Button" parent="HBoxContainer" unique_id=1424251140]
[node name="Button_ResourcesManagement" type="Button" parent="VBoxContainer/HBoxContainer2" unique_id=1424251140]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 4
theme_override_fonts/font = ExtResource("1_pa2rn")
theme_override_font_sizes/font_size = 32
theme = ExtResource("2_ymrsi")
text = "资源"
[node name="Button_Return" type="Button" parent="HBoxContainer" unique_id=867839141]
[node name="Button_Save" type="Button" parent="VBoxContainer/HBoxContainer2" unique_id=794000010]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 4
theme_override_fonts/font = ExtResource("1_pa2rn")
theme_override_font_sizes/font_size = 32
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 = "返回"
[connection signal="button_up" from="HBoxContainer/SpeedGroup/Button_Pause" to="." method="_on_button_pause_button_up"]
[connection signal="button_up" from="HBoxContainer/Button_Return" to="." method="_on_button_return_button_up"]
[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"]
+42 -1
View File
@@ -1,16 +1,25 @@
## 顶部信息栏 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]
@@ -21,9 +30,14 @@ func _ready() -> void:
# 资源数值:信号仅在变化时触发,初始值需手动设置一次
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()
## 每日推进时刷新日期显示。
@@ -40,10 +54,27 @@ func _on_spirit_stones_changed(new_value: int, _delta: int) -> void:
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()
@@ -55,6 +86,16 @@ 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:
@@ -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
+5 -4
View File
@@ -1,15 +1,16 @@
## 主菜单场景(挂载于 MainMenu.tscn)。
extends Control
## 点击"新游戏":重置 GameState 后进入游戏世界。
## 点击"新游戏"创建新存档组 → 重置 GameState 后进入游戏世界。
func MainMenu_NewGame_ButtonUp() -> void:
GameState.request_new_game()
var pid := SaveSystem.create_profile()
GameState.request_new_game(pid)
get_tree().change_scene_to_file("res://src/Core/MainGame.tscn")
## 点击"读取存档":隐藏主按钮组并显示存档面板。
## 点击"读取存档":隐藏主按钮组并打开存档面板(读档模式)
func MainMenu_LoadGame_ButtonUp() -> void:
$ButtonGroup.hide()
$SavePanel.show()
$SavePanel.open_load()
## 点击"设置"TODO:接入设置面板)。
func MainMenu_Settings_ButtonUp() -> void:
+9 -11
View File
@@ -1,8 +1,8 @@
[gd_scene format=3 uid="uid://d1odycoaqhhyg"]
[ext_resource type="Script" uid="uid://drfyh36my6gj2" path="res://src/UI/Panels/MainMenu/MainMenu.gd" id="1_cwvg5"]
[ext_resource type="Texture2D" uid="uid://caeqoc6wx8toy" path="res://assets/sprites/ui/BG1.png" id="1_hrti0"]
[ext_resource type="FontFile" uid="uid://drt448qncx27p" path="res://assets/fonts/dingliezhuhaifont-20240831GengXinBan)-2.ttf" id="3_q7typ"]
[ext_resource type="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]
@@ -21,7 +21,9 @@ anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("1_hrti0")
texture = ExtResource("2_q7typ")
expand_mode = 2
stretch_mode = 6
[node name="Logo" type="TextureRect" parent="." unique_id=1485724026]
layout_mode = 1
@@ -47,29 +49,25 @@ alignment = 1
[node name="NewGame" type="Button" parent="ButtonGroup" unique_id=835084060]
custom_minimum_size = Vector2(200, 48)
layout_mode = 2
theme_override_fonts/font = ExtResource("3_q7typ")
theme_override_font_sizes/font_size = 32
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_override_fonts/font = ExtResource("3_q7typ")
theme_override_font_sizes/font_size = 32
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_override_fonts/font = ExtResource("3_q7typ")
theme_override_font_sizes/font_size = 32
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_override_fonts/font = ExtResource("3_q7typ")
theme_override_font_sizes/font_size = 32
theme = ExtResource("3_7e5ie")
text = "退出"
[node name="SavePanel" parent="." unique_id=619488443 instance=ExtResource("3_wrf65")]
@@ -18,6 +18,7 @@ var _check_buttons: Array[CheckButton] = []
var _entry_disciples: Array[Disciple] = []
## 订阅候选生成信号,默认隐藏。
func _ready() -> void:
# 订阅候选生成信号:开局与定期招募都会触发
DiscipleManager.applicants_generated.connect(_on_applicants_generated)
+162 -4
View File
@@ -1,10 +1,168 @@
## 存档选择面板(挂载于 SavePanel.tscn)。
## 显示存档列表与"新建存档"入口,通过信号通知外部结果
## 存档面板(挂载于 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:
back_pressed.emit()
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()
+19 -11
View File
@@ -1,7 +1,7 @@
[gd_scene format=3 uid="uid://dj7b5vssydifa"]
[ext_resource type="Script" uid="uid://boao04d7accm" path="res://src/UI/Panels/SavePanel.gd" id="1_oi6jj"]
[ext_resource type="PackedScene" uid="uid://dk7ggyngkyt18" path="res://src/UI/Panels/SaveSlot.tscn" id="2_uloko"]
[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
@@ -19,16 +19,27 @@ theme_override_constants/separation = 16
layout_mode = 2
[node name="TitleLabel" type="Label" parent="VBoxContainer/Header" unique_id=784512345]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 24
theme_override_fonts/font = ExtResource("2_font")
theme_override_font_sizes/font_size = 28
text = "选择存档"
[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]
@@ -41,21 +52,18 @@ layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer/HBoxContainer" unique_id=844163531]
[node name="GroupContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer/HBoxContainer" unique_id=844163531]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
[node name="SaveGroup" type="Button" parent="VBoxContainer/ScrollContainer/HBoxContainer/VBoxContainer" unique_id=150349711]
layout_mode = 2
text = "存档1"
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
[node name="SaveSlot" parent="VBoxContainer/ScrollContainer/HBoxContainer/SlotList" unique_id=214547620 instance=ExtResource("2_uloko")]
layout_mode = 2
size_flags_vertical = 1
[connection signal="button_up" from="VBoxContainer/Header/Button_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"]
+29 -26
View File
@@ -1,10 +1,13 @@
## 存档槽位条目(挂载于 SaveSlot.tscn)。
## 展示单个存档信息;空槽位用于新建存档
## 展示单个存档槽位信息,支持点击读档与请求删除
extends PanelContainer
## 槽位被点击时发出,save_id 为空表示点击了新建槽位
## @param save_id: 存档 ID
signal slot_pressed(save_id: String)
## 槽位被点击(读档)时发出。
## @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
@@ -13,40 +16,40 @@ signal slot_pressed(save_id: String)
@onready var delete_button: Button = %DeleteButton
@onready var arrow_button: Button = %ArrowIcon
## 本槽位对应的存档 ID
var save_id: String = ""
## 是否已有存档数据false 表示新建槽位)
## 本槽位文件名(不含 .json
var slot_name: String = ""
## 是否已有存档数据。
var has_data: bool = false
## 填充为已有存档槽位。
## @param id: 存档 ID
## @param name_text: 存档名称
## @param name: 槽文件名(不含 .json
## @param index_text: 序号文本(如 "1"、"自动"
## @param name_text: 槽位显示名(如 "存档 1"、"自动存档"
## @param meta1: 第一行元数据
## @param meta2: 第二行元数据
func setup(id: String, name_text: String, meta1: String, meta2: String) -> void:
save_id = id
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 = id.get_slice("save_", 1) # "save_001" -> "001"
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 setup_empty() -> void:
has_data = false
slot_index_label.text = "+"
save_name_label.text = "新建存档"
meta_line1.text = "点击创建新的存档"
meta_line2.text = ""
delete_button.visible = false
arrow_button.visible = false
## 槽位本体被点击。
## 槽位本体被点击(读档/回到进度点)
func _on_pressed() -> void:
slot_pressed.emit(save_id)
slot_pressed.emit(slot_name)
## 点击删除按钮(TODO:接入确认弹窗)。
## 点击箭头按钮(读档)。
func _on_arrow_button_up() -> void:
slot_pressed.emit(slot_name)
## 根节点点击(左键抬起即触发,覆盖整行)。
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
slot_pressed.emit(slot_name)
## 点击删除按钮:发出删除请求(确认由面板处理)。
func _on_delete_button_button_up() -> void:
print("删除存档: ", save_id)
delete_requested.emit(slot_name)
+1
View File
@@ -55,3 +55,4 @@ 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"]