From a3cbd738ade0fce79151bb40193e74cbe7643426 Mon Sep 17 00:00:00 2001 From: chen Date: Thu, 6 Aug 2026 00:13:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9A=E6=97=B6=E6=8B=9B=E5=8B=9F?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=EF=BC=8C=E5=BC=80=E5=B1=805=E9=80=893+?= =?UTF-8?q?=E5=AE=9A=E6=9C=9F3=E5=80=99=E9=80=89=EF=BC=8C=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E9=A9=B1=E5=8A=A8=EF=BC=88GameConfig/RecruitPanel?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- project.godot | 2 +- resources/game_config.cfg | 9 ++ src/Character/DiscipleManager.gd | 96 ++++++++++++---- src/Core/GameConfig.gd | 32 ++++++ src/Core/GameConfig.gd.uid | 1 + src/Core/MainGame.gd | 4 +- src/Core/MainGame.tscn | 3 + src/UI/Panels/RecruitPanel/RecruitPanel.tscn | 96 ++++++++++++++++ src/UI/Panels/RecruitPanel/recruit_panel.gd | 108 ++++++++++++++++++ .../Panels/RecruitPanel/recruit_panel.gd.uid | 1 + 10 files changed, 324 insertions(+), 28 deletions(-) create mode 100644 resources/game_config.cfg create mode 100644 src/Core/GameConfig.gd create mode 100644 src/Core/GameConfig.gd.uid create mode 100644 src/UI/Panels/RecruitPanel/RecruitPanel.tscn create mode 100644 src/UI/Panels/RecruitPanel/recruit_panel.gd create mode 100644 src/UI/Panels/RecruitPanel/recruit_panel.gd.uid diff --git a/project.godot b/project.godot index 3e06c1b..f67bb02 100644 --- a/project.godot +++ b/project.godot @@ -20,7 +20,7 @@ GameState="*res://src/Core/GameState.gd" TimeSystem="*res://src/Core/TimeSystem.gd" Settings="*res://src/Core/Settings.gd" SectManager="*res://src/Core/SectManager.gd" -DiscipleManager="*res://src/Character/DiscipleManager.gd" +GameConfig="*res://src/Core/GameConfig.gd" DiscipleManager="*res://src/Character/DiscipleManager.gd" [display] diff --git a/resources/game_config.cfg b/resources/game_config.cfg new file mode 100644 index 0000000..f05476d --- /dev/null +++ b/resources/game_config.cfg @@ -0,0 +1,9 @@ +; 模拟宗门 全局配置 +; 所有可调参数集中在此,修改后重启游戏生效 +; 注释以 ; 开头,参数按系统分节 + +[recruitment] +opening_applicant_count=5 +opening_pick_count=3 +recruit_interval_years=0.1 +recruit_applicant_count=3 diff --git a/src/Character/DiscipleManager.gd b/src/Character/DiscipleManager.gd index a5c307c..76f76a8 100644 --- a/src/Character/DiscipleManager.gd +++ b/src/Character/DiscipleManager.gd @@ -1,19 +1,21 @@ ## 弟子管理器(Autoload 单例)。 -## 负责弟子定时招募、入宗分流与宗门名录维护。 +## 负责候选弟子生成、入宗选择与宗门名录维护。 +## 招募流程:开局/定期生成候选 → 暂停时间 → 玩家在弹窗选择 → 结算入名录。 extends Node ## 名录变化时发出(增删/重置)。 signal disciples_changed +## 候选弟子生成时发出(开局/定期,弹窗 UI 监听此信号)。 +## @param is_opening: 是否开局招募(开局必须选满,定期自由选择) +signal applicants_generated(is_opening: bool) -## 宗门弟子名录。 +## 宗门弟子名录(已入宗)。 var disciples: Array[Disciple] = [] - -## 招募间隔(游戏天数,每满间隔自动招募一名弟子)。 -const RECRUIT_INTERVAL_DAYS := 30 +## 候选弟子池(未入宗,等待玩家选择)。 +var applicants: Array[Disciple] = [] ## 入宗分流权重(按资质四类):下品 / 中品 / 上品 / 天骄。 const APTITUDE_WEIGHTS: Array[int] = [45, 35, 15, 5] - ## 天灵根生成概率(3%)。 const HEAVENLY_CHANCE := 0.03 @@ -31,33 +33,48 @@ const GIVEN_CHARS: Array[String] = [ "舒", "歌", "翎", "屿", "珩", "璃", "幽", "衡", ] -## 距下次招募的剩余游戏天数。 -var _days_until_recruit := RECRUIT_INTERVAL_DAYS -## 下一位弟子的 ID(自增,重置归零)。 +## 距下次定期招募的剩余游戏天数。 +var _days_until_recruit := 0 +## 下一位入宗弟子的 ID(自增,重置归零)。 var _next_id := 1 func _ready() -> void: - # 挂到时间系统上:每日推进时倒计时,满间隔自动招募 + # 挂到时间系统上:每日推进时倒计时,满间隔生成候选并暂停时间 TimeSystem.day_passed.connect(_on_day_passed) + _days_until_recruit = _get_interval_days() -## 每日推进:招募倒计时减 1,归零时自动招募一名弟子。 +## 每日推进:定期招募倒计时减 1,归零时生成候选并暂停时间。 func _on_day_passed(_year: int, _month: int, _day: int) -> void: _days_until_recruit -= 1 if _days_until_recruit <= 0: - _days_until_recruit = RECRUIT_INTERVAL_DAYS - recruit() + _days_until_recruit = _get_interval_days() + generate_applicants(GameConfig.recruit_applicant_count) + TimeSystem.set_paused(true) -## 招募一名弟子:随机生成属性、按资质分流后加入名录,返回该弟子。 -func recruit() -> Disciple: - var d := Disciple.new() +## 开局新游戏:清空所有数据,生成开局候选(数量来自配置),暂停时间等待选择。 +func start_new_game() -> void: + reset() + generate_applicants(GameConfig.opening_applicant_count, true) + TimeSystem.set_paused(true) + +## 生成一批候选弟子:清空旧候选后按 count 生成,发信号供弹窗显示。 +## @param count: 候选人数(调用方从 GameConfig 传入) +## @param is_opening: 是否开局招募 +func generate_applicants(count: int, is_opening := false) -> void: + applicants.clear() + for i in count: + applicants.append(_create_applicant()) + applicants_generated.emit(is_opening) + +## 接受一名候选弟子:分配 ID 并移入名录(候选池 → 名录)。 +## @param d: 要接受的候选 +func accept_applicant(d: Disciple) -> void: + if not applicants.has(d): + return + applicants.erase(d) d.id = _next_id _next_id += 1 - d.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() disciples.append(d) disciples_changed.emit() print("弟子入宗:%s %d岁 | %s资质%s | %s灵根 | %s" % [ @@ -65,21 +82,50 @@ func recruit() -> Disciple: "·天灵根" if d.is_heavenly else "", d.get_roots_text(), d.get_realm_text(), ]) - return d -## 移除弟子(逐出/陨落)。 +## 拒绝一名候选弟子:从候选池移除,不进入名录。 +## @param d: 要拒绝的候选 +func reject_applicant(d: Disciple) -> void: + applicants.erase(d) + +## 结算本次招募:selected 内的候选入宗,其余全部拒绝,清空候选池。 +## @param selected: 玩家勾选的候选列表(弹窗"确定"时传入) +func finish_recruitment(selected: Array[Disciple]) -> void: + for d in applicants.duplicate(): + if selected.has(d): + accept_applicant(d) + else: + reject_applicant(d) + +## 移除已入宗弟子(逐出/陨落)。 ## @param d: 要移除的弟子 func remove_disciple(d: Disciple) -> void: disciples.erase(d) disciples_changed.emit() -## 清空名录(新游戏开局调用),招募倒计时复位。 +## 清空名录与候选池(新游戏开局调用),倒计时与 ID 复位。 func reset() -> void: disciples.clear() + applicants.clear() _next_id = 1 - _days_until_recruit = RECRUIT_INTERVAL_DAYS + _days_until_recruit = _get_interval_days() disciples_changed.emit() +## 定期招募间隔(游戏天数),由 GameConfig 的"年"换算而来。 +func _get_interval_days() -> int: + var days_per_year := TimeSystem.MONTHS_PER_YEAR * TimeSystem.DAYS_PER_MONTH + return maxi(1, roundi(GameConfig.recruit_interval_years * days_per_year)) + +## 创建一名候选弟子:随机生成属性(姓名/年龄/灵根/资质), +## 不分配 ID——ID 在入宗时由 accept_applicant 分配,保证连续且无浪费。 +func _create_applicant() -> Disciple: + var d := Disciple.new() + d.name = _generate_name() + d.age = randi_range(12, 18) + d.is_heavenly = randf() < HEAVENLY_CHANCE + d.roots = _generate_roots(d.is_heavenly) + d.aptitude = _roll_aptitude() + return d ## 随机生成姓名:单姓 + 1~2 个名用字。 func _generate_name() -> String: diff --git a/src/Core/GameConfig.gd b/src/Core/GameConfig.gd new file mode 100644 index 0000000..f5389f1 --- /dev/null +++ b/src/Core/GameConfig.gd @@ -0,0 +1,32 @@ +## 游戏全局配置(Autoload 单例)。 +## 启动时从 resources/game_config.cfg 读取所有可调参数, +## 其他系统通过公开变量读取,如 GameConfig.recruit_interval_years。 +extends Node + +## 配置文件路径(res:// 开发期可改,导出后只读)。 +const CONFIG_PATH := "res://resources/game_config.cfg" + +# —— 招募系统(对应 cfg 的 [recruitment] 节)—— +## 开局候选人数。 +var opening_applicant_count := 5 +## 开局必须选满数。 +var opening_pick_count := 3 +## 定期招募间隔(游戏年,1 年 = 360 天)。 +var recruit_interval_years := 0.1 +## 定期弹窗候选人数。 +var recruit_applicant_count := 3 + + +func _ready() -> void: + _load_config() + +## 读取配置文件;缺失的项保持代码默认值,不会报错。 +func _load_config() -> void: + var cfg := ConfigFile.new() + if cfg.load(CONFIG_PATH) != OK: + push_warning("GameConfig: 配置文件不存在,使用默认值") + return + opening_applicant_count = cfg.get_value("recruitment", "opening_applicant_count", opening_applicant_count) + opening_pick_count = cfg.get_value("recruitment", "opening_pick_count", opening_pick_count) + recruit_interval_years = cfg.get_value("recruitment", "recruit_interval_years", recruit_interval_years) + recruit_applicant_count = cfg.get_value("recruitment", "recruit_applicant_count", recruit_applicant_count) diff --git a/src/Core/GameConfig.gd.uid b/src/Core/GameConfig.gd.uid new file mode 100644 index 0000000..811f404 --- /dev/null +++ b/src/Core/GameConfig.gd.uid @@ -0,0 +1 @@ +uid://dlchibfd86oek diff --git a/src/Core/MainGame.gd b/src/Core/MainGame.gd index e096563..ca40c28 100644 --- a/src/Core/MainGame.gd +++ b/src/Core/MainGame.gd @@ -7,8 +7,8 @@ func _ready() -> void: add_to_group("game_scene") # 应用用户设置中的默认流速 TimeSystem.apply_default_speed() - # 新游戏清空弟子名录与招募倒计时(读档流程接入后由存档还原) - DiscipleManager.reset() + # 开局招募:清空旧数据 → 生成 5 名候选 → 暂停时间等待选择(读档流程接入后由存档还原) + DiscipleManager.start_new_game() ## 顶部 HUD 的返回主菜单请求。 func _on_top_bar_hud_sign_return_mainmenu() -> void: diff --git a/src/Core/MainGame.tscn b/src/Core/MainGame.tscn index 020596b..9a3dd1c 100644 --- a/src/Core/MainGame.tscn +++ b/src/Core/MainGame.tscn @@ -2,6 +2,7 @@ [ext_resource type="Script" uid="uid://centrk8l54j7c" path="res://src/Core/MainGame.gd" id="1_8bu23"] [ext_resource type="PackedScene" uid="uid://dl0tb68vsw3c6" path="res://src/UI/HUD/TopBarHUD.tscn" id="1_g6xmk"] +[ext_resource type="PackedScene" path="res://src/UI/Panels/RecruitPanel/RecruitPanel.tscn" id="1_k5nrm"] [node name="MainGame" type="Node" unique_id=2053561265] groups = ["game_scene"] @@ -9,4 +10,6 @@ script = ExtResource("1_8bu23") [node name="TopBarHud" parent="." unique_id=1115394694 instance=ExtResource("1_g6xmk")] +[node name="RecruitPanel" parent="." unique_id=770000016 instance=ExtResource("1_k5nrm")] + [connection signal="sign_return_mainmenu" from="TopBarHud" to="." method="_on_top_bar_hud_sign_return_mainmenu"] diff --git a/src/UI/Panels/RecruitPanel/RecruitPanel.tscn b/src/UI/Panels/RecruitPanel/RecruitPanel.tscn new file mode 100644 index 0000000..7eb28ff --- /dev/null +++ b/src/UI/Panels/RecruitPanel/RecruitPanel.tscn @@ -0,0 +1,96 @@ +[gd_scene format=3 uid="uid://d4y7q3m6k2x5n7w2"] +[ext_resource type="Script" path="res://src/UI/Panels/RecruitPanel/recruit_panel.gd" id="1_script"] +[ext_resource type="FontFile" uid="uid://drt448qncx27p" path="res://assets/fonts/dingliezhuhaifont-20240831GengXinBan)-2.ttf" id="2_font"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dialog"] +bg_color = Color(0.11, 0.08, 0.13, 0.97) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[node name="RecruitPanel" type="Control" unique_id=770000001] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_script") + +[node name="Backdrop" type="ColorRect" parent="." unique_id=770000002] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0, 0, 0, 0.6) + +[node name="CenterContainer" type="CenterContainer" parent="." unique_id=770000003] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Panel" type="PanelContainer" parent="CenterContainer" unique_id=770000004] +theme_override_styles/panel = SubResource("StyleBoxFlat_dialog") + +[node name="MarginContainer" type="MarginContainer" parent="CenterContainer/Panel" unique_id=770000005] +theme_override_constants/margin_left = 28 +theme_override_constants/margin_top = 20 +theme_override_constants/margin_right = 28 +theme_override_constants/margin_bottom = 20 + +[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/Panel/MarginContainer" unique_id=770000006] +theme_override_constants/separation = 12 + +[node name="TitleLabel" type="Label" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=770000007] +unique_name_in_owner = true +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 30 +text = "弟子入宗" + +[node name="HintLabel" type="Label" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=770000008] +unique_name_in_owner = true +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 20 +theme_override_colors/font_color = Color(0.78, 0.75, 0.82, 1) +text = "" + +[node name="ScrollContainer" type="ScrollContainer" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=770000009] +custom_minimum_size = Vector2(640, 300) +size_flags_vertical = 3 + +[node name="ListContainer" type="VBoxContainer" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ScrollContainer" unique_id=770000010] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ButtonRow" type="HBoxContainer" parent="CenterContainer/Panel/MarginContainer/VBoxContainer" unique_id=770000011] +theme_override_constants/separation = 8 + +[node name="Button_AcceptAll" type="Button" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow" unique_id=770000012] +unique_name_in_owner = true +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 22 +text = "全部接受" + +[node name="Button_RejectAll" type="Button" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow" unique_id=770000013] +unique_name_in_owner = true +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 22 +text = "全部拒绝" + +[node name="Spacer" type="Control" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow" unique_id=770000014] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Button_Confirm" type="Button" parent="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow" unique_id=770000015] +unique_name_in_owner = true +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 22 +text = "确定" + +[connection signal="button_up" from="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow/Button_AcceptAll" to="." method="_on_button_accept_all_button_up"] +[connection signal="button_up" from="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow/Button_RejectAll" to="." method="_on_button_reject_all_button_up"] +[connection signal="button_up" from="CenterContainer/Panel/MarginContainer/VBoxContainer/ButtonRow/Button_Confirm" to="." method="_on_button_confirm_button_up"] diff --git a/src/UI/Panels/RecruitPanel/recruit_panel.gd b/src/UI/Panels/RecruitPanel/recruit_panel.gd new file mode 100644 index 0000000..9c81c47 --- /dev/null +++ b/src/UI/Panels/RecruitPanel/recruit_panel.gd @@ -0,0 +1,108 @@ +## 招募弹窗(挂载于 RecruitPanel.tscn)。 +## 监听候选生成信号自动弹出,玩家勾选后点"确定"结算入宗。 +extends Control + +## 招募模式:开局必须选满 / 定期自由选择。 +enum Mode { OPENING, PERIODIC } + +@onready var title_label: Label = %TitleLabel +@onready var hint_label: Label = %HintLabel +@onready var list_container: VBoxContainer = %ListContainer +@onready var confirm_button: Button = %Button_Confirm + +## 当前招募模式。 +var _mode: Mode = Mode.PERIODIC +## 勾选框列表(与 _entry_disciples 按下标一一对应)。 +var _check_buttons: Array[CheckButton] = [] +## 对应勾选框的候选弟子。 +var _entry_disciples: Array[Disciple] = [] + + +func _ready() -> void: + # 订阅候选生成信号:开局与定期招募都会触发 + DiscipleManager.applicants_generated.connect(_on_applicants_generated) + visible = false + +## 候选生成时弹出:按当前候选池重建列表。 +## @param is_opening: 是否开局招募 +func _on_applicants_generated(is_opening: bool) -> void: + _mode = Mode.OPENING if is_opening else Mode.PERIODIC + _clear_entries() + for d in DiscipleManager.applicants: + _add_entry(d) + title_label.text = "弟子入宗(%s)" % ("开局招募" if is_opening else "定期招募") + _update_ui() + visible = true + +## 勾选任一候选后刷新提示与确定按钮可用性。 +func _on_check_toggled(_pressed: bool, _d: Disciple) -> void: + _update_ui() + +## 全部接受:勾选所有候选。 +func _on_button_accept_all_button_up() -> void: + for c in _check_buttons: + c.set_pressed_no_signal(true) + _update_ui() + +## 全部拒绝:取消所有勾选。 +func _on_button_reject_all_button_up() -> void: + for c in _check_buttons: + c.set_pressed_no_signal(false) + _update_ui() + +## 确定:勾选的入宗、未勾选的拒绝,关闭弹窗并恢复时间。 +func _on_button_confirm_button_up() -> void: + var selected: Array[Disciple] = [] + for i in _entry_disciples.size(): + if _check_buttons[i].button_pressed: + selected.append(_entry_disciples[i]) + DiscipleManager.finish_recruitment(selected) + _close() + +## 刷新提示文本与确定按钮状态。 +func _update_ui() -> void: + if _mode == Mode.OPENING: + var picked := 0 + for c in _check_buttons: + if c.button_pressed: + picked += 1 + hint_label.text = "开局招募:请选择 %d 名弟子入宗(已选 %d/%d)" % [ + GameConfig.opening_pick_count, picked, _check_buttons.size(), + ] + confirm_button.disabled = picked < GameConfig.opening_pick_count + else: + hint_label.text = "定期招募:勾选入宗弟子,未勾选的将被拒绝(可全部拒绝)" + confirm_button.disabled = false + +## 关闭弹窗:清空条目并恢复时间推进。 +func _close() -> void: + _clear_entries() + visible = false + TimeSystem.set_paused(false) + +## 清空候选条目。 +func _clear_entries() -> void: + for child in list_container.get_children(): + child.queue_free() + _check_buttons.clear() + _entry_disciples.clear() + +## 添加一名候选的行条目:信息文本 + 入宗勾选框。 +## @param d: 候选弟子 +func _add_entry(d: Disciple) -> void: + var row := HBoxContainer.new() + var info := Label.new() + info.text = "%s %d岁 | %s资质%s | %s灵根 | %s" % [ + d.name, d.age, d.get_aptitude_name(), + "·天灵根" if d.is_heavenly else "", + d.get_roots_text(), d.get_realm_text(), + ] + info.size_flags_horizontal = Control.SIZE_EXPAND_FILL + var check := CheckButton.new() + check.text = "入宗" + check.toggled.connect(_on_check_toggled.bind(d)) + row.add_child(info) + row.add_child(check) + list_container.add_child(row) + _check_buttons.append(check) + _entry_disciples.append(d) diff --git a/src/UI/Panels/RecruitPanel/recruit_panel.gd.uid b/src/UI/Panels/RecruitPanel/recruit_panel.gd.uid new file mode 100644 index 0000000..c01d716 --- /dev/null +++ b/src/UI/Panels/RecruitPanel/recruit_panel.gd.uid @@ -0,0 +1 @@ +uid://byjqc1ssqulc5