feat: 定时招募弹窗,开局5选3+定期3候选,配置驱动(GameConfig/RecruitPanel)
This commit is contained in:
+1
-1
@@ -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]
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
; 模拟宗门 全局配置
|
||||
; 所有可调参数集中在此,修改后重启游戏生效
|
||||
; 注释以 ; 开头,参数按系统分节
|
||||
|
||||
[recruitment]
|
||||
opening_applicant_count=5
|
||||
opening_pick_count=3
|
||||
recruit_interval_years=0.1
|
||||
recruit_applicant_count=3
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dlchibfd86oek
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,108 @@
|
||||
## 招募弹窗(挂载于 RecruitPanel.tscn)。
|
||||
## 监听候选生成信号自动弹出,玩家勾选后点"确定"结算入宗。
|
||||
extends Control
|
||||
|
||||
## 招募模式:开局必须选满 / 定期自由选择。
|
||||
enum Mode { OPENING, PERIODIC }
|
||||
|
||||
@onready var title_label: Label = %TitleLabel
|
||||
@onready var hint_label: Label = %HintLabel
|
||||
@onready var list_container: VBoxContainer = %ListContainer
|
||||
@onready var confirm_button: Button = %Button_Confirm
|
||||
|
||||
## 当前招募模式。
|
||||
var _mode: Mode = Mode.PERIODIC
|
||||
## 勾选框列表(与 _entry_disciples 按下标一一对应)。
|
||||
var _check_buttons: Array[CheckButton] = []
|
||||
## 对应勾选框的候选弟子。
|
||||
var _entry_disciples: Array[Disciple] = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# 订阅候选生成信号:开局与定期招募都会触发
|
||||
DiscipleManager.applicants_generated.connect(_on_applicants_generated)
|
||||
visible = false
|
||||
|
||||
## 候选生成时弹出:按当前候选池重建列表。
|
||||
## @param is_opening: 是否开局招募
|
||||
func _on_applicants_generated(is_opening: bool) -> void:
|
||||
_mode = Mode.OPENING if is_opening else Mode.PERIODIC
|
||||
_clear_entries()
|
||||
for d in DiscipleManager.applicants:
|
||||
_add_entry(d)
|
||||
title_label.text = "弟子入宗(%s)" % ("开局招募" if is_opening else "定期招募")
|
||||
_update_ui()
|
||||
visible = true
|
||||
|
||||
## 勾选任一候选后刷新提示与确定按钮可用性。
|
||||
func _on_check_toggled(_pressed: bool, _d: Disciple) -> void:
|
||||
_update_ui()
|
||||
|
||||
## 全部接受:勾选所有候选。
|
||||
func _on_button_accept_all_button_up() -> void:
|
||||
for c in _check_buttons:
|
||||
c.set_pressed_no_signal(true)
|
||||
_update_ui()
|
||||
|
||||
## 全部拒绝:取消所有勾选。
|
||||
func _on_button_reject_all_button_up() -> void:
|
||||
for c in _check_buttons:
|
||||
c.set_pressed_no_signal(false)
|
||||
_update_ui()
|
||||
|
||||
## 确定:勾选的入宗、未勾选的拒绝,关闭弹窗并恢复时间。
|
||||
func _on_button_confirm_button_up() -> void:
|
||||
var selected: Array[Disciple] = []
|
||||
for i in _entry_disciples.size():
|
||||
if _check_buttons[i].button_pressed:
|
||||
selected.append(_entry_disciples[i])
|
||||
DiscipleManager.finish_recruitment(selected)
|
||||
_close()
|
||||
|
||||
## 刷新提示文本与确定按钮状态。
|
||||
func _update_ui() -> void:
|
||||
if _mode == Mode.OPENING:
|
||||
var picked := 0
|
||||
for c in _check_buttons:
|
||||
if c.button_pressed:
|
||||
picked += 1
|
||||
hint_label.text = "开局招募:请选择 %d 名弟子入宗(已选 %d/%d)" % [
|
||||
GameConfig.opening_pick_count, picked, _check_buttons.size(),
|
||||
]
|
||||
confirm_button.disabled = picked < GameConfig.opening_pick_count
|
||||
else:
|
||||
hint_label.text = "定期招募:勾选入宗弟子,未勾选的将被拒绝(可全部拒绝)"
|
||||
confirm_button.disabled = false
|
||||
|
||||
## 关闭弹窗:清空条目并恢复时间推进。
|
||||
func _close() -> void:
|
||||
_clear_entries()
|
||||
visible = false
|
||||
TimeSystem.set_paused(false)
|
||||
|
||||
## 清空候选条目。
|
||||
func _clear_entries() -> void:
|
||||
for child in list_container.get_children():
|
||||
child.queue_free()
|
||||
_check_buttons.clear()
|
||||
_entry_disciples.clear()
|
||||
|
||||
## 添加一名候选的行条目:信息文本 + 入宗勾选框。
|
||||
## @param d: 候选弟子
|
||||
func _add_entry(d: Disciple) -> void:
|
||||
var row := HBoxContainer.new()
|
||||
var info := Label.new()
|
||||
info.text = "%s %d岁 | %s资质%s | %s灵根 | %s" % [
|
||||
d.name, d.age, d.get_aptitude_name(),
|
||||
"·天灵根" if d.is_heavenly else "",
|
||||
d.get_roots_text(), d.get_realm_text(),
|
||||
]
|
||||
info.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
var check := CheckButton.new()
|
||||
check.text = "入宗"
|
||||
check.toggled.connect(_on_check_toggled.bind(d))
|
||||
row.add_child(info)
|
||||
row.add_child(check)
|
||||
list_container.add_child(row)
|
||||
_check_buttons.append(check)
|
||||
_entry_disciples.append(d)
|
||||
@@ -0,0 +1 @@
|
||||
uid://byjqc1ssqulc5
|
||||
Reference in New Issue
Block a user