Compare commits

..
3 Commits
16 changed files with 475 additions and 44 deletions
+139
View File
@@ -0,0 +1,139 @@
# 存档系统(SaveSystem)使用说明
Autoload 单例,路径 `res://src/Core/SaveSystem.gd`,已在 `project.godot` 注册。
## 架构
两级存档:
```
存档组 SaveGroup(一次轮回/一个角色) profiles.json 中一条记录
└── 存档槽 SaveSlot(进度快照) user://saves/profile_{组id}/slot_{时间戳}.json
```
- 每个"新游戏"创建一个组,组 id 由 SaveSystem 分配(1、2、3…)
- 每次 `save()` 在组下**新建**一个槽,槽与组数量无上限
- 存档文件为 JSON,含 `version``date``data` 三段,`data` 为各系统数据合并后的字典
## 核心接口
| 接口 | 说明 |
| --- | --- |
| `create_profile() -> int` | 新建存档组,返回组 id。主菜单"新游戏"调用 |
| `get_profiles() -> Array[Dictionary]` | 组列表,每条含 `id / name / created / last_date` |
| `save(profile_id, data = {}) -> String` | 保存游戏,返回槽文件名。`data` 缺省时自动收集各系统数据 |
| `load(profile_id, slot_name) -> Dictionary` | 读取槽位数据,槽不存在或版本高于当前返回 `{}` |
| `list_slots(profile_id) -> Array[Dictionary]` | 组下槽列表(`name / date`),最新在前 |
| `delete_slot(profile_id, slot_name)` | 删除槽位 |
| `register_saver(callable) -> void` | 注册数据提供者(见下) |
| `collect_data() -> Dictionary` | 手动收集所有注册系统的数据 |
## 各系统如何接入存档
新增需要存档的系统只需两步,**无需改动 SaveSystem**
```gdscript
# 例:SectSystem.gd
extends Node
func _ready() -> void:
SaveSystem.register_saver(_to_save_dict)
## 提供存档数据,返回字典,key 用系统名避免冲突
func _to_save_dict() -> Dictionary:
return {
"sect": {
"name": sect_name,
"resources": resources,
"members": members,
},
}
## 读档时恢复(由 MainGame 在 load 后按 key 分发调用)
func load_from_dict(d: Dictionary) -> void:
if not d.has("sect"):
return
var sect: Dictionary = d["sect"]
sect_name = sect.get("name", "")
...
```
约定:
- 每个系统实现 `_to_save_dict()`(提供数据)和 `load_from_dict()`(恢复状态)
- key 用系统名(如 `"sect"`),避免系统间冲突
- 读档用 `d.get(key, {})` / `.get(field, 默认值)` 容错,保证旧档兼容
- 只把**游戏内数据**放进去;会话参数(当前 mode、profile_id 等)由 GameState 管理,不落档
### 示例:GameState 保存游戏时间
GameState 是 Autoload(持久对象),在其 `_ready` 中注册:
```gdscript
# GameState.gd
extends Node
var year: int = 1
var month: int = 1
func _ready() -> void:
SaveSystem.register_saver(_to_save_dict)
## 提供存档数据:key 为 "game_time",值是游戏时间字段
func _to_save_dict() -> Dictionary:
return {
"game_time": {
"year": year,
"month": month,
},
}
## 读档恢复(MainGame 在 load 后调用 GameState.load_from_dict(data)
func load_from_dict(d: Dictionary) -> void:
var t: Dictionary = d.get("game_time", {})
year = int(t.get("year", 1))
month = int(t.get("month", 1))
```
存档后 JSON 中的形态:
```json
"game_time": { "year": 12, "month": 7 }
```
要点:
- `register_saver` 只接收无参 Callable,方法名直接传,**不要**写成 `register_saver(_to_save_dict())`(那是调用而不是注册)
- `year`/`month` 读档用 `get()` 带默认值,旧存档没有该字段时也能启动
## 存档与读档流程
### 存档(回合结算 / 月度结算 / 关窗前)
```gdscript
# TimeSystem 月度结算处
var slot := SaveSystem.save(GameState.profile_id)
print("已保存:%s" % slot)
```
### 读档(主菜单进入游戏)
```gdscript
# MainGame 场景启动时
var data := SaveSystem.load(GameState.profile_id, GameState.save_slot)
if data.is_empty():
push_error("存档读取失败")
return
# 按 key 分发给各系统恢复
SectSystem.load_from_dict(data)
...
# 主菜单:列出组和槽供选择
var profiles := SaveSystem.get_profiles()
var slots := SaveSystem.list_slots(pid) # 选中某组后
GameState.request_load_game(pid, slot["name"])
```
## 存档时机建议(回合制)
- **每月结算时**自动 `save()` 一次,覆盖当前组新建槽
- **关窗前**`NOTIFICATION_WM_CLOSE_REQUEST`)补存一次,避免丢失当月操作
- 不需要手动存档 UI;如担心槽无限增长,可加"保留最近 N 槽"的清理逻辑
-1
View File
@@ -20,7 +20,6 @@ config/icon="res://icon.svg"
GameState="*res://src/Core/GameState.gd" GameState="*res://src/Core/GameState.gd"
TimeSystem="*res://src/Core/TimeSystem.gd" TimeSystem="*res://src/Core/TimeSystem.gd"
Settings="*res://src/Core/Settings.gd" Settings="*res://src/Core/Settings.gd"
GameConfig="*res://src/Core/GameConfig.gd"
SaveSystem="*res://src/Core/SaveSystem.gd" SaveSystem="*res://src/Core/SaveSystem.gd"
[display] [display]
+18
View File
@@ -16,6 +16,24 @@ var save_slot: String = ""
var year: int = 1 # 年 var year: int = 1 # 年
var month: int = 1 # 月(1-12 var month: int = 1 # 月(1-12
func _ready() -> void:
SaveSystem.register_saver(_to_save_dict)
## 提供存档数据:key 为 "game_time",值是游戏时间字段
func _to_save_dict() -> Dictionary:
return {
"game_time": {
"year": year,
"month": month,
},
}
## 读档恢复(MainGame 在 load 后调用 GameState.load_from_dict(data)
func load_from_dict(d: Dictionary) -> void:
var t: Dictionary = d.get("game_time", {})
year = int(t.get("year", 1))
month = int(t.get("month", 1))
## 请求开始新游戏。 ## 请求开始新游戏。
## @param pid: 新存档组 ID(主菜单创建) ## @param pid: 新存档组 ID(主菜单创建)
func request_new_game(pid: int) -> void: func request_new_game(pid: int) -> void:
+4 -1
View File
@@ -2,6 +2,9 @@ extends Node
func _on_top_bar_hud_sign_return_mainmenu() -> void: func _on_top_bar_hud_sign_return_mainmenu() -> void:
# SaveSystem.save_auto(GameState.profile_id) if GameState.mode == GameState.GameMode.NEW_GAME:
if GameState.profile_id == 0:
GameState.profile_id = SaveSystem.create_profile()
SaveSystem.save(GameState.profile_id) # 进游戏立即存首档,防开局就丢
get_tree().change_scene_to_file("res://src/UI/Panels/MainMenu/MainMenu.tscn") get_tree().change_scene_to_file("res://src/UI/Panels/MainMenu/MainMenu.tscn")
pass # Replace with function body. pass # Replace with function body.
-4
View File
@@ -2,7 +2,6 @@
[ext_resource type="Script" uid="uid://cr4by5k8vgkcq" path="res://src/Core/MainGame.gd" id="1_8bu23"] [ext_resource type="Script" uid="uid://cr4by5k8vgkcq" path="res://src/Core/MainGame.gd" id="1_8bu23"]
[ext_resource type="PackedScene" uid="uid://d3njijtx7ws1m" path="res://src/UI/HUD/MainSceneHud.tscn" id="2_8bu23"] [ext_resource type="PackedScene" uid="uid://d3njijtx7ws1m" path="res://src/UI/HUD/MainSceneHud.tscn" id="2_8bu23"]
[ext_resource type="PackedScene" uid="uid://xecv3j1wfxmv" path="res://src/UI/HUD/MainGameButtomHud.tscn" id="3_oyqkd"]
[node name="MainGame" type="Node" unique_id=42551660] [node name="MainGame" type="Node" unique_id=42551660]
script = ExtResource("1_8bu23") script = ExtResource("1_8bu23")
@@ -11,7 +10,4 @@ script = ExtResource("1_8bu23")
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 3 size_flags_vertical = 3
[node name="MainGameButtomHud" parent="." unique_id=12078477 instance=ExtResource("3_oyqkd")]
visible = false
[connection signal="sign_return_mainmenu" from="MainHud" to="." method="_on_top_bar_hud_sign_return_mainmenu"] [connection signal="sign_return_mainmenu" from="MainHud" to="." method="_on_top_bar_hud_sign_return_mainmenu"]
+130 -3
View File
@@ -1,18 +1,34 @@
## 存档系统(Autoload 单例)。 ## 存档系统(Autoload 单例)。
## 两级存档:存档组 SaveGroup(一个角色/一次轮回)→ 存档槽 SaveSlot(该组下任意多个进度快照)。 ## 两级存档:存档组 SaveGroup(一个角色/一次轮回)→ 存档槽 SaveSlot(该组下任意多个进度快照)。
## 每个新游戏创建一个组;每次存档在该组下新建一个槽;组与槽数量无上限。 ## 每个新游戏创建一个组;每次存档在该组下新建一个槽;组与槽数量无上限。
## 文件:user://saves/profile_{组id}/{auto|slot_{时间戳}}.json ## 文件:user://saves/profile_{组id}/slot_{时间戳}.json
## 各系统通过 register_saver() 注册数据提供者,save() 自动收集,无需改动存档核心。
extends Node extends Node
## 存档根目录。 ## 存档根目录。
const SAVE_ROOT := "user://saves" const SAVE_ROOT := "user://saves"
## 存档组列表文件。 ## 存档组列表文件。
const PROFILES_PATH := "user://saves/profiles.json" const PROFILES_PATH := "user://saves/profiles.json"
## 自动槽文件名(固定)。
const AUTO_SLOT := "auto"
## 存档格式版本(兼容迁移预留)。 ## 存档格式版本(兼容迁移预留)。
const VERSION := 1 const VERSION := 1
var _savers: Array[Callable] = []
## 注册存档数据提供者(各系统在 _ready 中调用一次)。
## @param c: 无参 Callable,返回 Dictionarykey 为系统名),如 `_to_save_dict`
func register_saver(c: Callable) -> void:
if not _savers.has(c):
_savers.append(c)
## 遍历收集所有注册系统的数据,合并为一个大 Dictionary。
func collect_data() -> Dictionary:
var data := {}
for c in _savers:
var result: Variant = c.call()
if result is Dictionary:
data.merge(result)
return data
## 新建存档组(主菜单"新游戏"调用),返回组 id(名"存档N")。 ## 新建存档组(主菜单"新游戏"调用),返回组 id(名"存档N")。
func create_profile() -> int: func create_profile() -> int:
var profiles := _load_profiles() var profiles := _load_profiles()
@@ -28,7 +44,118 @@ func create_profile() -> int:
_save_profiles(profiles) _save_profiles(profiles)
return id return id
func get_profiles() -> Array[Dictionary]:
return _load_profiles()
## 保存游戏:在该组下新建一个槽位(文件名 slot_{时间戳}.json),返回槽文件名。
## 不传 data 时自动调用 collect_data() 收集所有注册系统的数据。
## @param profile_id: 存档组 ID
## @param data: 要保存的游戏数据(可选,默认自动收集)
func save(profile_id: int, data: Dictionary = {}) -> String:
if data.is_empty():
data = collect_data()
var slot_name := "slot_%d_%03d" % [Time.get_unix_time_from_system(), Time.get_ticks_msec() % 1000]
_write_slot(profile_id, slot_name, {
"version": VERSION,
"date": Time.get_datetime_string_from_system(),
"data": data,
})
_update_profile_last_date(profile_id)
return slot_name
## 读取指定槽位的游戏数据,槽不存在或损坏返回空 Dictionary。
## 返回的 data 字典由调用方(MainGame)按 key 分发给各系统恢复状态。
## @param profile_id: 存档组 ID
## @param slot_name: 槽文件名(不含 .json,如 "slot_1754734800"
func load(profile_id: int, slot_name: String) -> Dictionary:
var path := "user://saves/profile_%d/%s.json" % [profile_id, slot_name]
if not FileAccess.file_exists(path):
return {}
var f := FileAccess.open(path, FileAccess.READ)
if f == null:
return {}
var parsed: Variant = JSON.parse_string(f.get_as_text())
if parsed is not Dictionary or int(parsed.get("version", 0)) > VERSION:
return {}
return parsed.get("data", {})
## 列出该组下所有槽位信息(文件名 + 存档日期),按时间倒序(最新在前)。
## @param profile_id: 存档组 ID
func list_slots(profile_id: int) -> Array[Dictionary]:
var dir_path := "user://saves/profile_%d" % profile_id
var da := DirAccess.open(dir_path)
if da == null:
return []
var result: Array[Dictionary] = []
da.list_dir_begin()
var fname := da.get_next()
while fname != "":
if fname.ends_with(".json") and not da.current_is_dir():
var path := "%s/%s" % [dir_path, fname]
var f := FileAccess.open(path, FileAccess.READ)
if f != null:
var parsed: Variant = JSON.parse_string(f.get_as_text())
if parsed is Dictionary:
result.append({
"name": fname.trim_suffix(".json"),
"date": str(parsed.get("date", "")),
})
fname = da.get_next()
da.list_dir_end()
result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
return a["name"] > b["name"])
return result
## 删除指定槽位。
func delete_slot(profile_id: int, slot_name: String) -> void:
DirAccess.remove_absolute("user://saves/profile_%d/%s.json" % [profile_id, slot_name])
## 删除整个存档组(含其下所有槽位文件)。
## @param profile_id: 存档组 ID
func delete_profile(profile_id: int) -> void:
var profiles := _load_profiles()
profiles = profiles.filter(func(p: Dictionary) -> bool: return int(p["id"]) != profile_id)
_save_profiles(profiles)
_delete_dir_recursive("user://saves/profile_%d" % profile_id)
## 递归删除目录及其内容。
func _delete_dir_recursive(path: String) -> void:
var da := DirAccess.open(path)
if da == null:
return
da.list_dir_begin()
var fname := da.get_next()
while fname != "":
if fname in [".", ".."]:
fname = da.get_next()
continue
if da.current_is_dir():
_delete_dir_recursive("%s/%s" % [path, fname])
else:
DirAccess.remove_absolute("%s/%s" % [path, fname])
fname = da.get_next()
da.list_dir_end()
da.remove(path)
## 写入槽位文件。
func _write_slot(profile_id: int, slot_name: String, payload: Dictionary) -> void:
var dir_path := "user://saves/profile_%d" % profile_id
var da := DirAccess.open("user://")
if da != null:
da.make_dir_recursive(dir_path)
var f := FileAccess.open("%s/%s.json" % [dir_path, slot_name], FileAccess.WRITE)
if f == null:
return
f.store_string(JSON.stringify(payload, ""))
## 保存后更新该组的最近存档时间。
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"] = Time.get_datetime_string_from_system()
break
_save_profiles(profiles)
## 读取存档组列表。 ## 读取存档组列表。
func _load_profiles() -> Array[Dictionary]: func _load_profiles() -> Array[Dictionary]:
+4
View File
@@ -65,6 +65,10 @@ func advance_month() -> void:
if curr_season != prev_season: if curr_season != prev_season:
season_changed.emit(new_year, curr_season) season_changed.emit(new_year, curr_season)
# 6. 回合结算自动存档
if GameState.profile_id != 0:
SaveSystem.save(GameState.profile_id)
_in_turn = false _in_turn = false
## 日期显示文本,如 "第3年 5月"。 ## 日期显示文本,如 "第3年 5月"。
+1 -1
View File
@@ -136,7 +136,7 @@ size_flags_stretch_ratio = 5.0
[node name="ButtonContinue" type="Button" parent="VBoxContainer/HBoxContainer3" unique_id=472091035] [node name="ButtonContinue" type="Button" parent="VBoxContainer/HBoxContainer3" unique_id=472091035]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
text = "继续" text = "下一回合"
[connection signal="button_up" from="VBoxContainer/HBoxContainer/Button_Esc" to="." method="_on_ButtonEsc_ButtonUp"] [connection signal="button_up" from="VBoxContainer/HBoxContainer/Button_Esc" to="." method="_on_ButtonEsc_ButtonUp"]
[connection signal="button_up" from="VBoxContainer/HBoxContainer3/ButtonContinue" to="." method="_on_button_continue_button_up"] [connection signal="button_up" from="VBoxContainer/HBoxContainer3/ButtonContinue" to="." method="_on_button_continue_button_up"]
+2 -2
View File
@@ -8,8 +8,8 @@ func _draw() -> void:
pass pass
func _on_MainMenu_NewGame_ButtonUp() -> void: func _on_MainMenu_NewGame_ButtonUp() -> void:
#var pid := SaveSystem.create_profile() var pid := SaveSystem.create_profile()
#GameState.request_new_game(pid) GameState.request_new_game(pid)
get_tree().change_scene_to_file("res://src/Core/MainGame.tscn") get_tree().change_scene_to_file("res://src/Core/MainGame.tscn")
pass # Replace with function body. pass # Replace with function body.
+22
View File
@@ -0,0 +1,22 @@
## 存档组条目(挂载于 SaveGroup.tscn,填充左侧组列表)。
extends HBoxContainer
## 请求选中该组。
signal group_selected(profile_id: int)
## 请求删除该组(数据删除与节点销毁由条目自身完成,父面板仅做状态校正)。
signal delete_requested(profile_id: int)
## 该条目对应的存档组 ID。
var group_id: int = 0
func _ready() -> void:
$LabelId.pressed.connect(func(): group_selected.emit(group_id))
func set_data(group: Dictionary) -> void:
group_id = int(group["id"])
$LabelId.text = str(group.get("name", "存档%d" % group_id))
func _on_button_del_button_up() -> void:
delete_requested.emit(group_id)
SaveSystem.delete_profile(group_id)
queue_free()
+1
View File
@@ -0,0 +1 @@
uid://dos00byqj2c7m
+25
View File
@@ -0,0 +1,25 @@
[gd_scene format=3 uid="uid://t1jkqpmxb160"]
[ext_resource type="Script" uid="uid://dos00byqj2c7m" path="res://src/UI/Panels/SaveGroup.gd" id="1_j6osu"]
[ext_resource type="Theme" uid="uid://btnjeju2d47x8" path="res://resources/save_menu_button_theme.tres" id="2_uoetd"]
[node name="SaveGroup" type="HBoxContainer" unique_id=1600113286]
anchors_preset = 10
anchor_right = 1.0
offset_bottom = 29.0
grow_horizontal = 2
script = ExtResource("1_j6osu")
[node name="LabelId" type="Button" parent="." unique_id=253528830]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 3.0
text = "A"
[node name="ButtonDel" type="Button" parent="." unique_id=1248721049]
layout_mode = 2
size_flags_horizontal = 3
theme = ExtResource("2_uoetd")
text = "Del"
[connection signal="button_up" from="ButtonDel" to="." method="_on_button_del_button_up"]
+80 -18
View File
@@ -8,38 +8,100 @@ enum Mode { SAVE, LOAD }
## 请求关闭面板(返回上级界面)。 ## 请求关闭面板(返回上级界面)。
signal save_panel_esc_pressed signal save_panel_esc_pressed
## 请求读档(LOAD 模式点槽位时发出)。
signal load_requested(profile_id: int, slot_name: String)
## 槽位条目场景。 ## 槽位条目场景。
const SAVE_SLOT_SCENE := preload("res://src/UI/Panels/SaveSlot.tscn") const SAVE_SLOT_SCENE := preload("res://src/UI/Panels/SaveSlot.tscn")
## 组条目场景。
const SAVE_GROUP_SCENE := preload("res://src/UI/Panels/SaveGroup.tscn")
#@onready var title_label: Label = %TitleLabel @onready var group_container: VBoxContainer = %GroupContainer
#@onready var group_container: VBoxContainer = %GroupContainer @onready var slot_list: VBoxContainer = %SlotItems
#@onready var slot_list: VBoxContainer = %SlotList
@onready var new_slot_button: Button = %Button_NewSlot @onready var new_slot_button: Button = %Button_NewSlot
## 当前模式。 ## 当前模式。
var mode: Mode = Mode.LOAD var mode: Mode = Mode.LOAD
## 当前浏览的存档组 ID。 ## 当前浏览的存档组 ID0 = 未选中)
#var _current_profile: int = 0 var _current_profile: int = 0
### 打开面板前的暂停状态(存档模式关闭时恢复)。
#var _was_paused := false
func open_load() -> void: func open_load() -> void:
mode = Mode.SAVE
new_slot_button.visible = false
visible = true
pass
func open_save() -> void:
mode = Mode.LOAD mode = Mode.LOAD
new_slot_button.visible = true new_slot_button.visible = false
refresh_groups()
visible = true visible = true
pass
func open_save() -> void:
mode = Mode.SAVE
new_slot_button.visible = true
refresh_groups()
visible = true
## 刷新左侧存档组列表(调用 SaveSystem.get_profiles() 动态创建条目)。
func refresh_groups() -> void:
_clear_children(group_container)
for group in SaveSystem.get_profiles():
var item := SAVE_GROUP_SCENE.instantiate()
item.set_data(group)
item.group_selected.connect(_on_group_selected)
item.delete_requested.connect(_on_group_delete_requested)
group_container.add_child(item)
print(group['id'])
# 校验当前选中组:已被删除则复位并清空右列
if _current_profile == 0 or _current_profile not in _get_profile_ids():
_current_profile = 0
refresh_slots(_current_profile)
## 当前所有组的 id 列表。
func _get_profile_ids() -> Array[int]:
var ids: Array[int] = []
for group in SaveSystem.get_profiles():
ids.append(int(group["id"]))
return ids
## 刷新右侧槽位列表(profile_id 为 0 时清空)。
func refresh_slots(profile_id: int) -> void:
_clear_children(slot_list)
if profile_id == 0:
return
for slot in SaveSystem.list_slots(profile_id):
var item := SAVE_SLOT_SCENE.instantiate()
item.set_data(profile_id, slot["name"], slot["date"])
item.load_requested.connect(_on_slot_load_requested)
item.delete_requested.connect(_on_slot_delete_requested)
slot_list.add_child(item)
print("--->");
print("---------------------->");
func _on_group_selected(profile_id: int) -> void:
_current_profile = profile_id
refresh_slots(profile_id)
func _on_group_delete_requested(profile_id: int) -> void:
# 条目已自行删数据并销毁,这里只校正选中状态
if _current_profile == profile_id:
_current_profile = 0
refresh_slots(_current_profile)
func _on_slot_load_requested(profile_id: int, slot_name: String) -> void:
load_requested.emit(profile_id, slot_name)
func _on_slot_delete_requested(profile_id: int, slot_name: String) -> void:
SaveSystem.delete_slot(profile_id, slot_name)
refresh_slots(profile_id)
## 清空容器的所有子节点。
func _clear_children(container: Node) -> void:
for child in container.get_children():
container.remove_child(child)
child.queue_free()
func _on_SavePanel_Esc_ButtonUp() -> void: func _on_SavePanel_Esc_ButtonUp() -> void:
save_panel_esc_pressed.emit() save_panel_esc_pressed.emit()
pass # Replace with function body.
func _on_SavePanel_NewSlot_ButtonUp() -> void: func _on_SavePanel_NewSlot_ButtonUp() -> void:
pass # Replace with function body. if _current_profile == 0:
return
SaveSystem.save(_current_profile)
refresh_slots(_current_profile)
+16 -2
View File
@@ -59,14 +59,28 @@ size_flags_stretch_ratio = 15.0
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/ScrollContainer" unique_id=1858690088] [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/ScrollContainer" unique_id=1858690088]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="GroupContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer/HBoxContainer" unique_id=1215509679] [node name="GroupScroll" type="ScrollContainer" parent="VBoxContainer/ScrollContainer/HBoxContainer" unique_id=-2142055995]
layout_mode = 2
size_flags_horizontal = 3
[node name="GroupContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer/HBoxContainer/GroupScroll" unique_id=1215509679]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3
[node name="SaveSlotList" type="VBoxContainer" parent="VBoxContainer/ScrollContainer/HBoxContainer" unique_id=1157397953] [node name="SaveSlotList" type="ScrollContainer" parent="VBoxContainer/ScrollContainer/HBoxContainer" unique_id=2071143052]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 7.0
[node name="SlotItems" type="VBoxContainer" parent="VBoxContainer/ScrollContainer/HBoxContainer/SaveSlotList" unique_id=1079543554]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[connection signal="button_up" from="VBoxContainer/HBoxContainer/Button_NewSlot" to="." method="_on_SavePanel_NewSlot_ButtonUp"] [connection signal="button_up" from="VBoxContainer/HBoxContainer/Button_NewSlot" to="." method="_on_SavePanel_NewSlot_ButtonUp"]
[connection signal="button_up" from="VBoxContainer/HBoxContainer/Button_Esc" to="." method="_on_SavePanel_Esc_ButtonUp"] [connection signal="button_up" from="VBoxContainer/HBoxContainer/Button_Esc" to="." method="_on_SavePanel_Esc_ButtonUp"]
+22 -1
View File
@@ -1 +1,22 @@
extends Panel ## 存档槽位条目(挂载于 SaveSlot.tscn,填充右侧槽位列表)。
extends PanelContainer
## 请求读档该槽。
signal load_requested(profile_id: int, slot_name: String)
## 请求删除该槽。
signal delete_requested(profile_id: int, slot_name: String)
var _profile_id: int = 0
var _slot_name: String = ""
func set_data(profile_id: int, slot_name: String, date: String) -> void:
_profile_id = profile_id
_slot_name = slot_name
%SaveNameLabel.text = slot_name
%SaveSlotInfo.text = date
func _on_button_load_button_up() -> void:
load_requested.emit(_profile_id, _slot_name)
func _on_button_delete_button_up() -> void:
delete_requested.emit(_profile_id, _slot_name)
+11 -11
View File
@@ -4,22 +4,17 @@
[ext_resource type="LabelSettings" uid="uid://bn06sljdiloor" path="res://resources/save_menu_label_settings.tres" id="2_lm8et"] [ext_resource type="LabelSettings" uid="uid://bn06sljdiloor" path="res://resources/save_menu_label_settings.tres" id="2_lm8et"]
[ext_resource type="Theme" uid="uid://btnjeju2d47x8" path="res://resources/save_menu_button_theme.tres" id="2_m8lv8"] [ext_resource type="Theme" uid="uid://btnjeju2d47x8" path="res://resources/save_menu_button_theme.tres" id="2_m8lv8"]
[node name="SaveSlot" type="Panel" unique_id=815772812] [node name="SaveSlot" type="PanelContainer" unique_id=270996818]
anchors_preset = 14 anchors_preset = 10
anchor_top = 0.5
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 0.5 offset_bottom = 50.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 custom_minimum_size = Vector2(0, 50)
size_flags_horizontal = 3
script = ExtResource("1_gsy4a") script = ExtResource("1_gsy4a")
[node name="HBoxContainer" type="HBoxContainer" parent="." unique_id=1394744552] [node name="HBoxContainer" type="HBoxContainer" parent="." unique_id=1394744552]
layout_mode = 1 layout_mode = 2
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="SlotIndexLabel" type="Label" parent="HBoxContainer" unique_id=1779842797] [node name="SlotIndexLabel" type="Label" parent="HBoxContainer" unique_id=1779842797]
layout_mode = 2 layout_mode = 2
@@ -35,10 +30,12 @@ size_flags_horizontal = 3
size_flags_stretch_ratio = 15.0 size_flags_stretch_ratio = 15.0
[node name="SaveNameLabel" type="Label" parent="HBoxContainer/VBoxContainer" unique_id=391910928] [node name="SaveNameLabel" type="Label" parent="HBoxContainer/VBoxContainer" unique_id=391910928]
unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_vertical = 3 size_flags_vertical = 3
[node name="SaveSlotInfo" type="Label" parent="HBoxContainer/VBoxContainer" unique_id=125642674] [node name="SaveSlotInfo" type="Label" parent="HBoxContainer/VBoxContainer" unique_id=125642674]
unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_vertical = 3 size_flags_vertical = 3
@@ -53,3 +50,6 @@ layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
theme = ExtResource("2_m8lv8") theme = ExtResource("2_m8lv8")
text = "▶" text = "▶"
[connection signal="button_up" from="HBoxContainer/ButtonDelete" to="." method="_on_button_delete_button_up"]
[connection signal="button_up" from="HBoxContainer/ButtonLoad" to="." method="_on_button_load_button_up"]