MENU

Godot 4 でWiz系ダンジョンRPG開発 (セーブ & ロード機能) [015]

今回はセーブ&ロード機能を実装します。前回はメニュー画面の枠組みと設定の実装を行いました。今回のセーブ機能はメニュー画面からの続きです。ロード機能はメニューの設定から行えるものとします。

メニューが作成されていることを前提としております。メニューは前回記事で作成しました。詳しくはそちらを参考にしたうえで、この記事をご確認いただけたらと思います。

今まで作成してきたものは以下の通りです。詳しくは過去記事をご確認ください。

  • ダンジョンの壁と床の設置
  • プレイヤーの現在座標とその表記
  • イベントメッセージの表示
  • 敵とのランダムエンカウント
  • ダンジョンの扉と階段の設置
  • ダンジョンのB1から街に戻る処理
  • ダンジョンのB1からB2に降りる処理
  • ダンジョン画像の置き換え
  • 座標データ管理 (階段)
  • 敵データ作成 (JSON)
  • 敵の複数体ランダム出現処理
  • 敵の名前切り替え処理 (英語 / 日本語)
  • 街の各場所のメニュー表示
  • 街のBGM処理
  • オートマッピング
  • キャラクターの作成
  • 冒険者ギルドの実装
  • メニュー画面 (枠組み)

この記事でお伝えするセーブ&ロード機能は最低限のものです。まだまだ改良する点が多くあります。(プレイ時間など)

目次

セーブ機能

セーブ機能はメニュー画面から行います。逆に言うとメニュー画面以外ではセーブできません。セーブスロットは全部で9つあり、3つずつ表示されます。

ノード構成

  • MenuWindow (Control)
    • MainMenuLayout (PanelContainer)
      • MainHBox (HBoxContainer)
        • LMainMenuLayout (VBoxContainer)
          • DummySpaceL (Control)
          • MessageContainer (PanelContainer)
            • MenuMessageLabel (Label)
          • PartyLayout (VBoxContainer)
            • (省略)
          • ConfigLayout (VBoxContainer)
            • (省略)
          • SaveLayout (VBoxContainer) ※ 新規
            • SaveSlotContainer (VBoxContainer) ※ 新規
              • DummySpaceSaveSlot (Control) ※ 新規
              • SaveLabel (Label) ※ 新規
              • SlotRow1 (HBoxContainer) ※ 新規
                • SlotNoLabel (Label) ※ 新規
                • SaveSlot1 (Button) ※ 新規
                  • SlotHBox (HBoxContainer) ※ 新規
                    • DummySpaceSlot (Control) ※ 新規
                    • SlotLeft (HBoxContainer) ※ 新規
                      • SlotAppearance (TextureRect) ※ 新規
                    • SlotRight (VBoxContainer) ※ 新規
                      • SlotPlayTimeLabel (Label) ※ 新規
                      • SlotCharaInfoLabel (Label) ※ 新規
                      • SlotLvLabel (Label) ※ 新規
                      • SlotClassLabel (Label) ※ 新規
              • SlotRow2 (SaveSlot1 と同じ) ※ 新規
              • SlotRow3 (SaveSlot1 と同じ) ※ 新規
            • SaveBottomHBox (HBoxContainer) ※ 新規
              • NextSlotButton (Button) ※ 新規
              • SaveBackButton (Button) ※ 新規
          • RMainMenuLayout (VBoxContainer)
            • (省略)
        • SaveConfirmDialog (ConfirmationDialog) ※ 新規
        • SaveAlertDialog (AcceptDialog) ※ 新規
SaveLayout (VBoxContainer)セーブ画面用レイアウト。非表示
SaveSlotContainer (VBoxContainer)セーブスロット画面中央部
DummySpaceSaveSlot (Control)スペース調整用。Custom Minimum Size で調整
SaveLabel (Label)セーブ
SlotRow1 (HBoxContainer)セーブスロット1行目
SlotNoLabel (Label)セーブスロット番号。No.1 ~ 9
SaveSlot1 (Button)セーブスロット1。ボタンで実装
SlotHBox (HBoxContainer)セーブスロット内部を左右に分ける
DummySpaceSaveSlot (Control)スペース調整用。Custom Minimum Size で調整
SlotLeft (HBoxContainer)セーブスロット内部左側
SlotAppearance (TextureRect)前衛1 (PartyMember1) の見た目画像。前衛1がいない場合は前衛2, 3, 後衛1, 2, 3 と順番に探し、最初にヒットしたパーティメンバーの見た目画像になる。
SlotRight (VBoxContainer)セーブスロット内部右側
SlotPlayTimeLabel (Label)プレイ時間
SlotNameLabel (Label)前衛1 (PartyMember1) の名前。前衛1がいない場合は前衛2, 3, 後衛1, 2, 3 と順番に探し、最初にヒットしたパーティメンバーの名前になる。
SlotLvLabel (Label)前衛1 (PartyMember1) のレベル。前衛1がいない場合は前衛2, 3, 後衛1, 2, 3 と順番に探し、最初にヒットしたパーティメンバーのレベルになる。
SlotClassLabel (Label)前衛1 (PartyMember1) の職業。前衛1がいない場合は前衛2, 3, 後衛1, 2, 3 と順番に探し、最初にヒットしたパーティメンバーの職業になる。
SlotRow2 SlotRow1 と同じ
SlotRow3SlotRow1 と同じ
SaveBottomHBox (HBoxContainer)セーブスロット画面下部
NextSlotButton (Button)次のセーブスロットを見る
SaveBackButton (Button)戻る
SaveConfirmDialog (ConfirmationDialog)セーブ確認ポップアップ
SaveAlertDialog (AcceptDialog)セーブ完了ポップアップ

上記のようにノード構成を追加します。SaveLayout は非表示に設定してください。レイアウトは好みのものがあると思いますので、上記の DummySpace などを参考に調節をお願いします。

多言語対応用CSVファイルの追記

これまでの言語対応同様に res://data/localization.csv を開き、セーブ用のID,英語表記,日本語表記の項目を用意します。データを記述しCSV形式で保存したあと、インポートしなおすのを忘れないでください。

セーブデータ管理スクリプトの新規作成 (Autoload 設定)

# ==============================================================================
# SaveManager (セーブデータ管理クラス) - res://save_manager.gd
# ==============================================================================
extends Node

const SAVE_PATH_FORMAT = "user://save_slot_%d.save"

## 指定したスロットに現在のゲーム状態を保存する
func save_game(slot_no: int) -> bool:
	# 1. 画面表示(ヘッダー用)の簡易情報の作成
	var header_party = []
	for slot in PartyManager.slots:
		if slot != null:
			header_party.append({
				"name": slot.chara_name,
				"level": slot.level,
				"class": slot.chara_class,
				"face_image_path": slot.face_image_path
			})
			break

	# 2. ギルド名簿の実データをすべて保存
	var serialized_guild_members = []
	for member in GuildManager.registered_members:
		if member != null:
			serialized_guild_members.append(member.serialize())

	# 3. パーティ枠の位置インデックスを保存
	var party_slot_indices = []
	for slot in PartyManager.slots:
		if slot == null:
			party_slot_indices.append(-1)
		else:
			var idx = GuildManager.registered_members.find(slot)
			party_slot_indices.append(idx)

	# 4. MenuWindow から現在の設定インデックス(0 or 1)を取得する
	var saved_lang_index = 0
	var saved_res_index = 0
	
	# パス変更のリスクを避けるため、名前から MenuWindow ノードを安全に検索
	var menu_window = get_tree().root.find_child("MenuWindow", true, false)
	if menu_window:
		if menu_window.language_button:
			saved_lang_index = menu_window.language_button.selected
		if menu_window.resolution_button:
			saved_res_index = menu_window.resolution_button.selected

	# 5. セーブデータ全体の構築
	var save_data = {
		"slot_no": slot_no,
		"play_time": "12:34:56",
		"party": header_party,
		"guild_members": serialized_guild_members,
		"party_slot_indices": party_slot_indices,
		"config_language_index": saved_lang_index, # 【既存の設計に準拠】
		"config_resolution_index": saved_res_index, # 【既存の設計に準拠】
		"system_language": TranslationServer.get_locale() # 既存の互換性のため残す
	}
		
	# ファイルへの書き込み処理
	var path = SAVE_PATH_FORMAT % slot_no
	var file = FileAccess.open(path, FileAccess.WRITE)
	if file == null:
		print("[SaveManager] 失敗: ファイルを開けませんでした -> ", path)
		return false
		
	var json_string = JSON.stringify(save_data)
	file.store_line(json_string)
	file.close()
	print("[SaveManager] 成功: スロットNo.", slot_no, " に保存しました。")
	return true

## 指定したスロットのヘッダー情報を取得する
func get_slot_header(slot_no: int) -> Dictionary:
	var path = SAVE_PATH_FORMAT % slot_no
	if not FileAccess.file_exists(path):
		return {"exists": false, "slot_no": slot_no}
		
	var file = FileAccess.open(path, FileAccess.READ)
	if file == null:
		return {"exists": false, "slot_no": slot_no}
		
	var json_string = file.get_line()
	file.close()
	
	var json = JSON.new()
	if json.parse(json_string) == OK:
		var data = json.get_data()
		data["exists"] = true
		return data
		
	return {"exists": false, "slot_no": slot_no}

まず FileSystem で右クリックをしセーブデータ管理スクリプト (save_manager.gd) を作成します。そのファイルには上記内容を記述してください。これはセーブ機能を実現するものです。

Project メニューから Project Settings を選択してください。

先ほど作成したセーブデータ管理スクリプト (save_manager.gd) を選択し、開きます。

Node Name が SaveManager となることを確認し、Add をクリックします。

追加された SaveManager の Global Variable の Enable にチェックが付いていることを確認し、ウィンドウを閉じます。

メニュー画面用コード修正

class_name MenuWindow
extends Control

signal menu_opened
signal menu_closed

# メインノードに設定変更をお願いするためのシグナル
signal language_change_requested(index: int)
signal resolution_change_requested(index: int)

# --- 右側メニューボタン参照 ---
@onready var items_button: Button = $MainMenuLayout/MainHBox/RMainMenuLayout/MenuList/ButtonList/Items
@onready var spells_button: Button = $MainMenuLayout/MainHBox/RMainMenuLayout/MenuList/ButtonList/Spells
@onready var skills_button: Button = $MainMenuLayout/MainHBox/RMainMenuLayout/MenuList/ButtonList/Skills
@onready var equipment_button: Button = $MainMenuLayout/MainHBox/RMainMenuLayout/MenuList/ButtonList/Equipment
@onready var formation_button: Button = $MainMenuLayout/MainHBox/RMainMenuLayout/MenuList/ButtonList/Formation
@onready var status_button: Button = $MainMenuLayout/MainHBox/RMainMenuLayout/MenuList/ButtonList/Status
@onready var tactics_button: Button = $MainMenuLayout/MainHBox/RMainMenuLayout/MenuList/ButtonList/Tactics
@onready var config_button: Button = $MainMenuLayout/MainHBox/RMainMenuLayout/MenuList/ButtonList/Config
@onready var save_button: Button = $MainMenuLayout/MainHBox/RMainMenuLayout/MenuList/ButtonList/Save

# --- 左側基本UI参照 ---
@onready var menu_message_label: Label = $MainMenuLayout/MainHBox/LMainMenuLayout/MessageContainer/MenuMessageLabel
@onready var gold_label: Label = $MainMenuLayout/MainHBox/RMainMenuLayout/GoldWindow/GoldHBox/GoldLabel
@onready var party_layout: VBoxContainer = $MainMenuLayout/MainHBox/LMainMenuLayout/PartyLayout
@onready var l_main_menu_layout: Control = $MainMenuLayout/MainHBox/LMainMenuLayout
@onready var r_main_menu_layout: Control = $MainMenuLayout/MainHBox/RMainMenuLayout
@onready var front_row_party_member: HBoxContainer = $MainMenuLayout/MainHBox/LMainMenuLayout/PartyLayout/FrontRowPartyMember
@onready var back_row_party_member: HBoxContainer = $MainMenuLayout/MainHBox/LMainMenuLayout/PartyLayout/BackRowPartyMember

# --- 設定(Config)画面用UI参照 ---
@onready var main_menu_layout: Control = $MainMenuLayout
@onready var config_layout: VBoxContainer = $MainMenuLayout/MainHBox/LMainMenuLayout/ConfigLayout
@onready var language_button: OptionButton = $MainMenuLayout/MainHBox/LMainMenuLayout/ConfigLayout/LanguageHBox/LanguageButton
@onready var resolution_button: OptionButton = $MainMenuLayout/MainHBox/LMainMenuLayout/ConfigLayout/ResolutionHBox/ResolutionButton
@onready var config_back_button: Button = $MainMenuLayout/MainHBox/LMainMenuLayout/ConfigLayout/ConfigBackButton

# 設定画面内のテキストラベル参照
@onready var language_label: Label = $MainMenuLayout/MainHBox/LMainMenuLayout/ConfigLayout/LanguageHBox/LanguageLabel
@onready var resolution_label: Label = $MainMenuLayout/MainHBox/LMainMenuLayout/ConfigLayout/ResolutionHBox/ResolutionLabel

# --- パーティメンバーUI(1〜6)へのルート参照 ---
@onready var member_nodes: Array = [
	$MainMenuLayout/MainHBox/LMainMenuLayout/PartyLayout/FrontRowPartyMember/PartyMember1,
	$MainMenuLayout/MainHBox/LMainMenuLayout/PartyLayout/FrontRowPartyMember/PartyMember2,
	$MainMenuLayout/MainHBox/LMainMenuLayout/PartyLayout/FrontRowPartyMember/PartyMember3,
	$MainMenuLayout/MainHBox/LMainMenuLayout/PartyLayout/BackRowPartyMember/PartyMember4,
	$MainMenuLayout/MainHBox/LMainMenuLayout/PartyLayout/BackRowPartyMember/PartyMember5,
	$MainMenuLayout/MainHBox/LMainMenuLayout/PartyLayout/BackRowPartyMember/PartyMember6
]

# --- セーブ画面用UI参照 ---
@onready var save_layout: VBoxContainer = $MainMenuLayout/MainHBox/LMainMenuLayout/SaveLayout
@onready var next_slot_button: Button = $MainMenuLayout/MainHBox/LMainMenuLayout/SaveLayout/SaveBottomHBox/NextSlotButton
@onready var save_back_button: Button = $MainMenuLayout/MainHBox/LMainMenuLayout/SaveLayout/SaveBottomHBox/SaveBackButton
@onready var save_lavel: Label = $MainMenuLayout/MainHBox/LMainMenuLayout/SaveLayout/SaveSlotContainer/SaveLabel

# セーブスロット(3つ分)のボタン参照配列
@onready var slot_buttons: Array = [
	$MainMenuLayout/MainHBox/LMainMenuLayout/SaveLayout/SaveSlotContainer/SlotRow1/SaveSlot1,
	$MainMenuLayout/MainHBox/LMainMenuLayout/SaveLayout/SaveSlotContainer/SlotRow2/SaveSlot1,
	$MainMenuLayout/MainHBox/LMainMenuLayout/SaveLayout/SaveSlotContainer/SlotRow3/SaveSlot1
]

# ダイアログ類(ポップアップ)の参照
@onready var save_confirm_dialog: ConfirmationDialog = $SaveConfirmDialog
@onready var save_alert_dialog: AcceptDialog = $SaveAlertDialog

# --- セーブ状態管理変数 ---
var _current_page_index: int = 0 # 0=スロット1-3, 1=スロット4-6, 2=スロット7-9
var _selected_slot_no: int = 1
var _last_town_focus_owner: Control = null

func _ready() -> void:
	hide()
	if config_layout: config_layout.hide()
	if save_layout: save_layout.hide()
	
	# 通常メニューボタンのシグナル接続
	if items_button: items_button.pressed.connect(func(): _on_menu_button_pressed("ITEMS"))
	if spells_button: spells_button.pressed.connect(func(): _on_menu_button_pressed("SPELLS"))
	if skills_button: skills_button.pressed.connect(func(): _on_menu_button_pressed("SKILLS"))
	if equipment_button: equipment_button.pressed.connect(func(): _on_menu_button_pressed("EQUIPMENT"))
	if formation_button: formation_button.pressed.connect(func(): _on_menu_button_pressed("FORMATION"))
	if status_button: status_button.pressed.connect(func(): _on_menu_button_pressed("STATUS"))
	if tactics_button: tactics_button.pressed.connect(func(): _on_menu_button_pressed("TACTICS"))
	if config_button: config_button.pressed.connect(func(): _on_menu_button_pressed("CONFIG"))
	if save_button: save_button.pressed.connect(func(): _on_menu_button_pressed("SAVE"))

	# メニューのフォーカスイベント接続
	if items_button: items_button.focus_entered.connect(func(): _update_explanation("MAIN_MENU_ITEMS_EXPLANATION"))
	if spells_button: spells_button.focus_entered.connect(func(): _update_explanation("MAIN_MENU_SPELLS_EXPLANATION"))
	if skills_button: skills_button.focus_entered.connect(func(): _update_explanation("MAIN_MENU_SKILLS_EXPLANATION"))
	if equipment_button: equipment_button.focus_entered.connect(func(): _update_explanation("MAIN_MENU_EQUIPMENT_EXPLANATION"))
	if formation_button: formation_button.focus_entered.connect(func(): _update_explanation("MAIN_MENU_FORMATION_EXPLANATION"))
	if status_button: status_button.focus_entered.connect(func(): _update_explanation("MAIN_MENU_STATUS_EXPLANATION"))
	if tactics_button: tactics_button.focus_entered.connect(func(): _update_explanation("MAIN_MENU_TACTICS_EXPLANATION"))
	if config_button: config_button.focus_entered.connect(func(): _update_explanation("MAIN_MENU_CONFIG_EXPLANATION"))
	if save_button: save_button.focus_entered.connect(func(): _update_explanation("MAIN_MENU_SAVE_EXPLANATION"))
	
	_setup_config_ui()
	_setup_save_ui()
	setup_party_layout_sizes()

func _input(event: InputEvent) -> void:
	if not visible:
		if event.is_action_pressed("ui_select"):
			get_viewport().set_input_as_handled()
			open_menu()
		return
	
	if event.is_action_pressed("ui_cancel"):
		get_viewport().set_input_as_handled()
		if config_layout and config_layout.visible:
			_close_config_screen()
		elif save_layout and save_layout.visible:
			_close_save_screen()
		else:
			close_menu()

func open_menu() -> void:
	update_all_strings() 
	_update_party_display()
	
	_hide_all_sub_layouts()
	if party_layout: party_layout.show()

	_last_town_focus_owner = get_viewport().gui_get_focus_owner()
	if _last_town_focus_owner:
		_last_town_focus_owner.release_focus()
	
	show()
	
	await get_tree().process_frame
	if items_button:
		items_button.grab_focus()
		
	menu_opened.emit()

func _open_config_screen() -> void:
	if not config_layout: return
	_hide_all_sub_layouts()
	config_layout.show()
	await get_tree().process_frame
	language_button.grab_focus()

func _close_config_screen() -> void:
	if not config_layout: return
	_hide_all_sub_layouts()
	if party_layout: party_layout.show()
	await get_tree().process_frame
	if config_button: config_button.grab_focus()

func _close_save_screen() -> void:
	if not save_layout: return
	_hide_all_sub_layouts()
	if party_layout: party_layout.show()
	await get_tree().process_frame
	if save_button:
		save_button.grab_focus()

func _back_to_main_menu_after_save() -> void:
	_close_save_screen()

func close_menu() -> void:
	hide()
	if _last_town_focus_owner and is_instance_valid(_last_town_focus_owner) and _last_town_focus_owner.is_inside_tree():
		_last_town_focus_owner.grab_focus()
	_last_town_focus_owner = null
	menu_closed.emit()

## メインメニューおよび現在の説明文を最新の言語に再翻訳するパブリック関数
func update_all_strings() -> void:
	if items_button: items_button.text = tr("MAIN_MENU_ITEMS")
	if spells_button: spells_button.text = tr("MAIN_MENU_SPELLS")
	if skills_button: skills_button.text = tr("MAIN_MENU_SKILLS")
	if equipment_button: equipment_button.text = tr("MAIN_MENU_EQUIPMENT")
	if formation_button: formation_button.text = tr("MAIN_MENU_FORMATION")
	if status_button: status_button.text = tr("MAIN_MENU_STATUS")
	if tactics_button: tactics_button.text = tr("MAIN_MENU_TACTICS")
	if config_button: config_button.text = tr("MAIN_MENU_CONFIG")
	if save_button: save_button.text = tr("MAIN_MENU_SAVE")
	if gold_label: gold_label.text = tr("MAIN_MENU_GOLD")
	
	if language_label: language_label.text = tr("MENU_SETTINGS_LANGUAGE_LABEL")
	if resolution_label: resolution_label.text = tr("MENU_SETTINGS_RESOLUTION_LABEL")
	if config_back_button: config_back_button.text = tr("MENU_SETTINGS_BACKBUTTON_LABEL")
	
	# セーブ画面のテキスト
	if next_slot_button: next_slot_button.text = tr("MENU_SAVE_NEXT_PAGE")
	if save_back_button: save_back_button.text = tr("MENU_SAVE_BACK")
	if save_lavel: save_lavel.text = tr("MAIN_MENU_SAVE")
	
	# セーブダイアログのウィンドウタイトルのローカライズ
	if save_confirm_dialog:
		save_confirm_dialog.title = tr("MENU_SAVE_CONFIRM_DIALOG_TITLE")
	if save_alert_dialog:
		save_alert_dialog.title = tr("MENU_SAVE_ALERT_DIALOG_TITLE")
	
	if config_layout and config_layout.visible:
		_update_explanation("MAIN_MENU_CONFIG_EXPLANATION")
	elif save_layout and save_layout.visible:
		_update_explanation("MENU_SAVE_MESSAGE_EXPLANATION")
	elif config_button and config_button.has_focus():
		_update_explanation("MAIN_MENU_CONFIG_EXPLANATION")
		
	_update_party_display()
	
	# セーブ画面表示中ならスロット上のテキストも再翻訳して更新
	if save_layout and save_layout.visible:
		_render_save_slots()

func _on_menu_button_pressed(button_id: String) -> void:
	_hide_all_sub_layouts()
	
	match button_id:
		"CONFIG":
			_open_config_screen()
		"SAVE":
			_open_save_screen()
		"ITEMS", "SPELLS", "SKILLS", "EQUIPMENT", "FORMATION", "STATUS", "TACTICS":
			if party_layout: party_layout.show()
			print("[MenuWindow] 選択されたメニュー: ", button_id)
		
func _hide_all_sub_layouts() -> void:
	if party_layout: party_layout.hide()
	if config_layout: config_layout.hide()
	if save_layout: save_layout.hide()

func _update_explanation(loc_id: String) -> void:
	if menu_message_label:
		menu_message_label.text = tr(loc_id)

# ==============================================================================
# メニュー画面全体のフルサイズ固定(全画面化)
# ==============================================================================
func set_menu_full_size() -> void:
	if not main_menu_layout: return
	self.custom_minimum_size = Vector2.ZERO
	main_menu_layout.custom_minimum_size = Vector2.ZERO
	
	var target_screen_size := Vector2(1280, 720)
	self.set_anchors_preset(Control.PRESET_TOP_LEFT)
	self.position = Vector2.ZERO
	main_menu_layout.set_anchors_preset(Control.PRESET_TOP_LEFT)
	main_menu_layout.position = Vector2.ZERO
	self.size = target_screen_size
	main_menu_layout.size = target_screen_size
	self.custom_minimum_size = target_screen_size
	main_menu_layout.custom_minimum_size = target_screen_size

# ==============================================================================
# 設定画面(Config)のコントロールロジック
# ==============================================================================
func _setup_config_ui() -> void:
	if not language_button or not resolution_button or not config_back_button: return
	language_button.clear()
	language_button.add_item("English", 0)
	language_button.add_item("日本語", 1)
	language_button.select(0)
	language_button.item_selected.connect(func(id): language_change_requested.emit(id))
	
	resolution_button.clear()
	resolution_button.add_item("1280x720", 0)
	resolution_button.add_item("1920x1080", 1)
	resolution_button.select(0)
	resolution_button.item_selected.connect(func(id): resolution_change_requested.emit(id))
	config_back_button.pressed.connect(_close_config_screen)

# ==============================================================================
# セーブ画面(Save)のコントロールロジック
# ==============================================================================
func _setup_save_ui() -> void:
	if not next_slot_button or not save_back_button: return
	
	next_slot_button.pressed.connect(_on_next_slot_pressed)
	save_back_button.pressed.connect(_close_save_screen)
	
	for i in range(3):
		var btn = slot_buttons[i]
		if btn:
			btn.pressed.connect(func(): _on_slot_selected(i))
			
	if save_confirm_dialog:
		save_confirm_dialog.confirmed.connect(_execute_save)
		save_confirm_dialog.canceled.connect(func(): slot_buttons[0].grab_focus())
		
	if save_alert_dialog:
		save_alert_dialog.confirmed.connect(_back_to_main_menu_after_save)

func _open_save_screen() -> void:
	if not save_layout or not party_layout: return
	party_layout.hide()
	if config_layout: config_layout.hide()
	save_layout.show()
	
	_current_page_index = 0
	_update_explanation("MENU_SAVE_MESSAGE_EXPLANATION")
	_render_save_slots()
	
	await get_tree().process_frame
	if slot_buttons[0]:
		slot_buttons[0].grab_focus()

## 現在のページインデックス(0~2)に応じたセーブスロットデータを描画する
func _render_save_slots() -> void:
	var start_slot = (_current_page_index * 3) + 1
	
	for i in range(3):
		var slot_no = start_slot + i
		var btn: Button = slot_buttons[i]
		if not btn: continue
		
		var header = SaveManager.get_slot_header(slot_no)
		
		var parent_node = btn.get_parent()
		var lbl_no: Label = null
		if parent_node:
			lbl_no = parent_node.find_child("SlotNoLabel", true, false)
		
		var rect_face: TextureRect = btn.find_child("SlotAppearance", true, false)
		var lbl_time: Label = btn.find_child("SlotPlayTimeLabel", true, false)
		var lbl_name: Label = btn.find_child("SlotNameLabel", true, false)
		var lbl_lv: Label = btn.find_child("SlotLvLabel", true, false)
		var lbl_class: Label = btn.find_child("SlotClassLabel", true, false)
		
		if lbl_no: lbl_no.text = "No.%d" % slot_no
		
		if not header["exists"]:
			# データなし(新規保存用表示)
			if lbl_time: lbl_time.text = "--:--:--"
			# 【ローカライズ適用】
			if lbl_name: lbl_name.text = tr("MENU_SAVE_SLOT_EMPTY")
			if lbl_lv: lbl_lv.text = ""
			if lbl_class: lbl_class.text = ""
			if rect_face: rect_face.texture = null
		else:
			# データあり
			if lbl_time: lbl_time.text = header.get("play_time", "00:00:00")
			
			var party_data = header.get("party", [])
			if party_data.size() > 0:
				var chara = party_data[0]
				
				if lbl_name: lbl_name.text = chara["name"]
				if lbl_lv: lbl_lv.text = "Lv.%d" % chara["level"]
				if lbl_class: lbl_class.text = tr(chara["class"])
				
				if rect_face and chara["face_image_path"] and FileAccess.file_exists(chara["face_image_path"]):
					rect_face.texture = load(chara["face_image_path"])
				elif rect_face:
					rect_face.texture = null
			else:
				# パーティがいない状態でセーブされたデータ
				if lbl_name: lbl_name.text = tr("MENU_SAVE_SLOT_NO_PARTY")
				if lbl_lv: lbl_lv.text = ""
				if lbl_class: lbl_class.text = ""
				if rect_face: rect_face.texture = null

func _on_next_slot_pressed() -> void:
	_current_page_index = (_current_page_index + 1) % 3
	_render_save_slots()
	await get_tree().process_frame
	if slot_buttons[0]:
		slot_buttons[0].grab_focus()

func _on_slot_selected(local_index: int) -> void:
	_selected_slot_no = (_current_page_index * 3) + 1 + local_index
	var header = SaveManager.get_slot_header(_selected_slot_no)
	
	if not save_confirm_dialog: return
	
	# プレースホルダー付き文字列の動的合成
	if not header["exists"]:
		save_confirm_dialog.dialog_text = tr("MENU_SAVE_CONFIRM_DIALOG_NEW_DATA") % _selected_slot_no
	else:
		save_confirm_dialog.dialog_text = tr("MENU_SAVE_CONFIRM_DIALOG_OVERWRITE") % _selected_slot_no
		
	# --- ポップアップを表示する前に、Cancel ボタンへ強制フォーカス ---
	save_confirm_dialog.popup_centered()
	
	# popup された直後のフレームでフォーカスを移さないと効かない場合があるため
	# 安全に 1フレーム待ってから grab_focus() させるか、直接ボタンを奪います
	var cancel_btn = save_confirm_dialog.get_cancel_button()
	if cancel_btn:
		cancel_btn.grab_focus()

func _execute_save() -> void:
	var success = SaveManager.save_game(_selected_slot_no)
	
	if not save_alert_dialog: return
	
	# プレースホルダー付き文字列の動的合成
	if success:
		save_alert_dialog.dialog_text = tr("MENU_SAVE_ALERT_DIALOG_SUCCESS") % _selected_slot_no
	else:
		save_alert_dialog.dialog_text = tr("MENU_SAVE_ALERT_DIALOG_FAILURE") % _selected_slot_no
		
	save_alert_dialog.popup_centered()

# ==============================================================================
# サイズ調整・パーティ表示・その他(変更なし)
# ==============================================================================
func set_config_layout_size(resolution_index: int) -> void:
	if not config_layout: return
	var target_config_size := Vector2(320, 180)
	match resolution_index:
		0: target_config_size = Vector2(320, 180)
		1: target_config_size = Vector2(480, 270)
	config_layout.custom_minimum_size = target_config_size
	config_layout.size = target_config_size

func set_main_layouts_size() -> void:
	if not l_main_menu_layout or not r_main_menu_layout: return
	var left_width := 1120.0
	var right_width := 128.0
	l_main_menu_layout.custom_minimum_size = Vector2(left_width, 0)
	l_main_menu_layout.size.x = left_width
	r_main_menu_layout.custom_minimum_size = Vector2(right_width, 0)
	r_main_menu_layout.size.x = right_width

func _update_party_display() -> void:
	for i in range(6):
		var member_panel = member_nodes[i]
		if not member_panel: continue
		member_panel.show()
		var data = PartyManager.slots[i]
		var label_name: Label = member_panel.find_child("Name", true, false)
		var label_lv: Label = member_panel.find_child("Lv", true, false)
		var rect_appearance: TextureRect = member_panel.find_child("Appearance", true, false)
		var label_class: Label = member_panel.find_child("Class", true, false)
		var label_hp: Label = member_panel.find_child("HPLabel", true, false)
		var bar_hp: ProgressBar = member_panel.find_child("HPBar", true, false)
		var label_mp: Label = member_panel.find_child("MPLabel", true, false)
		var bar_mp: ProgressBar = member_panel.find_child("MPBar", true, false)

		if data == null:
			if label_name: label_name.text = ""
			if label_lv: label_lv.text = ""
			if label_class: label_class.text = ""
			if label_hp: label_hp.text = ""
			if label_mp: label_mp.text = ""
			if rect_appearance: rect_appearance.texture = null
			if bar_hp: bar_hp.hide()
			if bar_mp: bar_mp.hide()
		else:
			if bar_hp: bar_hp.show()
			if bar_mp: bar_mp.show()
			if label_name: label_name.text = data.chara_name
			if label_lv: label_lv.text = "Lv%d" % data.level
			if label_class: label_class.text = data.get_display_class()
			if label_hp: label_hp.text = "HP %d/%d" % [data.current_hp, data.max_hp]
			if label_mp: label_mp.text = "MP %d/%d" % [data.current_mp, data.max_mp]
			if rect_appearance:
				if data.face_image_path and FileAccess.file_exists(data.face_image_path):
					rect_appearance.texture = load(data.face_image_path)
				else:
					rect_appearance.texture = null
				rect_appearance.custom_minimum_size = Vector2(160, 160)
				rect_appearance.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
			if bar_hp:
				bar_hp.min_value = 0
				bar_hp.max_value = data.max_hp
				bar_hp.value = data.current_hp
			if bar_mp:
				bar_mp.min_value = 0
				bar_mp.max_value = data.max_mp
				bar_mp.value = data.current_mp

func setup_party_layout_sizes() -> void:
	if party_layout and front_row_party_member and back_row_party_member:
		party_layout.custom_minimum_size = Vector2(960, 0)
		party_layout.size.x = 960
		front_row_party_member.custom_minimum_size = Vector2(960, 0)
		back_row_party_member.custom_minimum_size = Vector2(960, 0)
		
		for member_panel in member_nodes:
			if not member_panel: continue
			member_panel.custom_minimum_size = Vector2(320, 240)
			member_panel.size = Vector2(320, 240)
			var member_hbox = member_panel.get_node_or_null("MemberHBox1")
			if not member_hbox: member_hbox = member_panel.find_child("MemberHBox*", true, false)
			if member_hbox:
				var l_part = member_hbox.get_node_or_null("LPartyMember1")
				if not l_part: l_part = member_hbox.find_child("LPartyMember*", true, false)
				if l_part:
					l_part.custom_minimum_size = Vector2(160, 0)
					l_part.size.x = 160
				var r_part = member_hbox.get_node_or_null("RPartyMember1")
				if not r_part: r_part = member_hbox.find_child("RPartyMember*", true, false)
				if r_part:
					r_part.custom_minimum_size = Vector2(160, 0)
					r_part.size.x = 160
	
	if save_layout:
		# 大元の SaveLayout の横幅を 720px に固定 (縦幅は自動)
		save_layout.custom_minimum_size = Vector2(420, 0)
		save_layout.size.x = 420
		
		# 3つのセーブスロットボタンの横幅を 720px に強制ロックし、内部パーツのサイズも固定する
		for btn in slot_buttons:
			if btn is Button:
				var current_height = btn.size.y if btn.size.y > 0 else 120.0
				btn.custom_minimum_size = Vector2(420, current_height)
				btn.size = Vector2(420, current_height)
				
				# --- スロット内部の左右コンテナの横幅を固定長にする ---
				var slot_left = btn.find_child("SlotLeft", true, false)
				if slot_left and slot_left is Control:
					# 顔グラフィック領域(左側)の横幅を 160px に固定
					slot_left.custom_minimum_size = Vector2(160, 0)
					slot_left.size.x = 160
				
				var slot_right = btn.find_child("SlotRight", true, false)
				if slot_right and slot_right is Control:
					# テキスト領域(右側)の横幅を残り全体の 520px(720 - 160 - 余白等)に完全固定
					slot_right.custom_minimum_size = Vector2(260, 0)
					slot_right.size.x = 260
				
		print("[MenuWindow] セーブレイアウトおよび内部パーツの横幅を完全固定しました。")

メニュー画面用スクリプト (menu_window.gd) を上記コードに修正します。これでゲームデータのセーブができるようになりました。

const SAVE_PATH_FORMAT = “user://save_slot_%d.save” の場所は Project メニューの Open User Data Folder を選択すると実際にセーブデータが書き出されているフォルダが開きます。

ロード機能

現時点ではタイトル画面は実装されていません。そのため、ロード機能はメニュー画面の設定(Config) に追加します。ここでは、上記画像のように「ゲームデータのロード」ボタンを新設するところから行います。

ノード構成

  • MenuWindow (Control)
    • MainMenuLayout (PanelContainer)
      • MainHBox (HBoxContainer)
        • LMainMenuLayout (VBoxContainer)
          • DummySpaceL (Control)
          • MessageContainer (PanelContainer)
            • MenuMessageLabel (Label)
          • PartyLayout (VBoxContainer)
            • (省略)
          • ConfigLayout (VBoxContainer)
            • DummySpaceConfig (Control)
            • LanguageHBox (HBoxContainer)
              • (省略)
            • ResolutionHBox (HBoxContainer)
              • (省略)
            • LoadButtonHBox (HBoxContainer) ※ 新規
              • LoadButton (Button) ※ 新規
            • ConfigBackButton (HBoxContainer)
          • SaveLayout (VBoxContainer)
            • (省略)
          • LoadLayout (VBoxContainer) ※ 新規
            • LoadSlotContainer (VBoxContainer) ※ 新規
              • DummySpaceLoadSlot (Control) ※ 新規
              • LoadLabel (Label) ※ 新規
              • LoadSlotRow1 (HBoxContainer) ※ 新規
                • LoadSlotNoLabel (Label) ※ 新規
                • LoadSlot1 (Button) ※ 新規
                  • SlotHBox (HBoxContainer) ※ 新規
                    • DummySpaceSlot (Control) ※ 新規
                    • SlotLeft (HBoxContainer) ※ 新規
                      • SlotAppearance (TextureRect) ※ 新規
                    • SlotRight (VBoxContainer) ※ 新規
                      • SlotPlayTimeLabel (Label) ※ 新規
                      • SlotCharaInfoLabel (Label) ※ 新規
                      • SlotLvLabel (Label) ※ 新規
                      • SlotClassLabel (Label) ※ 新規
              • LoadSlotRow2 (SaveSlot1 と同じ) ※ 新規
              • LoadSlotRow3 (SaveSlot1 と同じ) ※ 新規
            • LoadBottomHBox (HBoxContainer) ※ 新規
              • LoadNextSlotButton (Button) ※ 新規
              • LoadBackButton (Button) ※ 新規
          • RMainMenuLayout (VBoxContainer)
            • (省略)
        • SaveConfirmDialog (ConfirmationDialog)
        • SaveAlertDialog (AcceptDialog)
        • LoadConfirmDialog (ConfirmationDialog) ※ 新規
        • LoadAlertDialog (AcceptDialog) ※ 新規
LoadButtonHBox (HBoxContainer)メニューの設定にあるロードボタンのレイアウトを考えて HBoxConatiner で作成。
LoadButton (Button)メニューの設定にあるロードボタン
LoadLayout (VBoxContainer)セーブ画面用レイアウト。非表示
LoadSlotContainer (VBoxContainer)セーブスロット画面中央部
DummySpaceLoadSlot (Control)スペース調整用。Custom Minimum Size で調整
LoadLabel (Label)ロード
LoadSlotRow1 (HBoxContainer)スロット1行目
LoadSlotNoLabel (Label)スロット番号。No.1 ~ 9
LoadSlot1 (Button)スロット1。ボタンで実装
SlotHBox (HBoxContainer)ロードスロット内部を左右に分ける
DummySpaceLoadSlot (Control)スペース調整用。Custom Minimum Size で調整
SlotLeft (HBoxContainer)セーブスロット内部左側
SlotAppearance (TextureRect)前衛1 (PartyMember1) の見た目画像
SlotRight (VBoxContainer)スロット内部右側
SlotPlayTimeLabel (Label)プレイ時間
SlotNameLabel (Label)前衛1 (PartyMember1) の名前
SlotLvLabel (Label)前衛1 (PartyMember1) のレベル
SlotClassLabel (Label)前衛1 (PartyMember1) の職業
LoadSlotRow2SlotRow1 と同じ
LoadSlotRow3SlotRow1 と同じ
LoadBottomHBox (HBoxContainer)スロット画面下部
NextSlotButton (Button)次のスロットを見る
LoadBackButton (Button)戻る
LoadConfirmDialog (ConfirmationDialog)ロード確認ポップアップ
LoadAlertDialog (AcceptDialog)ロード完了ポップアップ

上記のようにノード構成を追加します。LoadLayout は非表示に設定してください。

多言語対応用CSVファイルの追記

これまでの言語対応同様に res://data/localization.csv を開き、ロード用のID,英語表記,日本語表記の項目を用意します。データを記述しCSV形式で保存したあと、インポートしなおすのを忘れないでください。

ロードデータ管理スクリプトの新規作成 (Autoload 設定)

# ==============================================================================
# LoadManager (ロードデータ管理クラス) - res://load_manager.gd
# ==============================================================================
extends Node

const SAVE_PATH_FORMAT = "user://save_slot_%d.save"

signal load_completed

## 指定されたスロットの存在確認を行い、再起動の準備をする
func load_game(slot_no: int) -> bool:
	var path = SAVE_PATH_FORMAT % slot_no
	if not FileAccess.file_exists(path):
		print("[LoadManager] 失敗: スロット ", slot_no, " にファイルが存在しません。")
		return false
		
	print("[LoadManager] スロットNo.", slot_no, " のロードを開始します。シーンをリロードします。")
	
	var main_scene_path = ProjectSettings.get_setting("application/run/main_scene")
	if main_scene_path and ResourceLoader.exists(main_scene_path):
		get_tree().change_scene_to_file(main_scene_path)
	else:
		get_tree().reload_current_scene()
		
	_wait_and_execute_load(slot_no)
	return true

func _wait_and_execute_load(slot_no: int) -> void:
	await get_tree().process_frame
	await get_tree().physics_frame
	
	_execute_actual_load(slot_no)

## 内部的なファイル読み込みと実データ復元ロジック
func _execute_actual_load(slot_no: int) -> void:
	var path = SAVE_PATH_FORMAT % slot_no
	var file = FileAccess.open(path, FileAccess.READ)
	if file == null:
		print("[LoadManager] 実ロード失敗: ファイルが開けません。")
		return
		
	var json_string = file.get_line()
	file.close()
	
	var json = JSON.new()
	if json.parse(json_string) != OK:
		print("[LoadManager] 実ロード失敗: JSONパースエラー。")
		return
		
	var save_data = json.get_data()
	
	# ==========================================================================
	# 1. ギルド名簿の完全総入れ替え
	# ==========================================================================
	var new_registered_members: Array[CharaData] = []
	var guild_members_data = save_data.get("guild_members", [])
	
	for member_data in guild_members_data:
		if member_data != null:
			new_registered_members.append(CharaData.deserialize(member_data))
			
	GuildManager.registered_members = new_registered_members

	# ==========================================================================
	# 2. パーティ配置の復元
	# ==========================================================================
	var party_slot_indices = save_data.get("party_slot_indices", [])
	
	for i in range(PartyManager.slots.size()):
		if i < party_slot_indices.size():
			var member_idx = party_slot_indices[i]
			if member_idx >= 0 and member_idx < GuildManager.registered_members.size():
				PartyManager.slots[i] = GuildManager.registered_members[member_idx]
			else:
				PartyManager.slots[i] = null
		else:
			PartyManager.slots[i] = null

	# ==========================================================================
	# 3. 既存の MenuWindow のシグナル設計を利用した設定の復元
	# ==========================================================================
	var menu_window = get_tree().root.find_child("MenuWindow", true, false)
	if menu_window:
		var lang_idx = save_data.get("config_language_index", 0)
		var res_idx = save_data.get("config_resolution_index", 0)
		
		# UIの選択状態をセーブ時のインデックスに合わせる
		if menu_window.language_button:
			menu_window.language_button.select(lang_idx)
		if menu_window.resolution_button:
			menu_window.resolution_button.select(res_idx)
			
		# MenuWindow が定義しているシグナルをそのままエミットし、
		# メインノード側に本来の言語変更・解像度変更の処理を要求する
		menu_window.language_change_requested.emit(lang_idx)
		menu_window.resolution_change_requested.emit(res_idx)
		
		# メニュー内のテキスト表記を最新言語に更新させる
		if menu_window.has_method("update_all_strings"):
			menu_window.update_all_strings()
			
		print("[LoadManager] MenuWindow の既存シグナルを介して設定を正常に復元しました。")
			
	print("[LoadManager] ロード完了。すべてのデータを復元しました。")
	load_completed.emit()

セーブデータ管理スクリプト同様にロードデータ管理スクリプトも作成します。FileSystem に load_manager.gd を作成、上記内容を記述し、Autoload 設定を行ってください。(Autoload 設定方法については、上記のセーブデータ管理スクリプトをご参照ください)

メニュー画面用コード修正

class_name MenuWindow
extends Control

signal menu_opened
signal menu_closed

# メインノードに設定変更をお願いするためのシグナル
signal language_change_requested(index: int)
signal resolution_change_requested(index: int)

# --- 右側メニューボタン参照 ---
@onready var items_button: Button = $MainMenuLayout/MainHBox/RMainMenuLayout/MenuList/ButtonList/Items
@onready var spells_button: Button = $MainMenuLayout/MainHBox/RMainMenuLayout/MenuList/ButtonList/Spells
@onready var skills_button: Button = $MainMenuLayout/MainHBox/RMainMenuLayout/MenuList/ButtonList/Skills
@onready var equipment_button: Button = $MainMenuLayout/MainHBox/RMainMenuLayout/MenuList/ButtonList/Equipment
@onready var formation_button: Button = $MainMenuLayout/MainHBox/RMainMenuLayout/MenuList/ButtonList/Formation
@onready var status_button: Button = $MainMenuLayout/MainHBox/RMainMenuLayout/MenuList/ButtonList/Status
@onready var tactics_button: Button = $MainMenuLayout/MainHBox/RMainMenuLayout/MenuList/ButtonList/Tactics
@onready var config_button: Button = $MainMenuLayout/MainHBox/RMainMenuLayout/MenuList/ButtonList/Config
@onready var save_button: Button = $MainMenuLayout/MainHBox/RMainMenuLayout/MenuList/ButtonList/Save

# --- 左側基本UI参照 ---
@onready var menu_message_label: Label = $MainMenuLayout/MainHBox/LMainMenuLayout/MessageContainer/MenuMessageLabel
@onready var gold_label: Label = $MainMenuLayout/MainHBox/RMainMenuLayout/GoldWindow/GoldHBox/GoldLabel
@onready var party_layout: VBoxContainer = $MainMenuLayout/MainHBox/LMainMenuLayout/PartyLayout
@onready var l_main_menu_layout: Control = $MainMenuLayout/MainHBox/LMainMenuLayout
@onready var r_main_menu_layout: Control = $MainMenuLayout/MainHBox/RMainMenuLayout
@onready var front_row_party_member: HBoxContainer = $MainMenuLayout/MainHBox/LMainMenuLayout/PartyLayout/FrontRowPartyMember
@onready var back_row_party_member: HBoxContainer = $MainMenuLayout/MainHBox/LMainMenuLayout/PartyLayout/BackRowPartyMember

# --- 設定(Config)画面用UI参照 ---
@onready var main_menu_layout: Control = $MainMenuLayout
@onready var config_layout: VBoxContainer = $MainMenuLayout/MainHBox/LMainMenuLayout/ConfigLayout
@onready var language_button: OptionButton = $MainMenuLayout/MainHBox/LMainMenuLayout/ConfigLayout/LanguageHBox/LanguageButton
@onready var resolution_button: OptionButton = $MainMenuLayout/MainHBox/LMainMenuLayout/ConfigLayout/ResolutionHBox/ResolutionButton
@onready var load_button: Button = $MainMenuLayout/MainHBox/LMainMenuLayout/ConfigLayout/LoadButtonHBox/LoadButton # 【新設】
@onready var config_back_button: Button = $MainMenuLayout/MainHBox/LMainMenuLayout/ConfigLayout/ConfigBackButton

# 設定画面内のテキストラベル参照
@onready var language_label: Label = $MainMenuLayout/MainHBox/LMainMenuLayout/ConfigLayout/LanguageHBox/LanguageLabel
@onready var resolution_label: Label = $MainMenuLayout/MainHBox/LMainMenuLayout/ConfigLayout/ResolutionHBox/ResolutionLabel

# --- パーティメンバーUI(1〜6)へのルート参照 ---
@onready var member_nodes: Array = [
	$MainMenuLayout/MainHBox/LMainMenuLayout/PartyLayout/FrontRowPartyMember/PartyMember1,
	$MainMenuLayout/MainHBox/LMainMenuLayout/PartyLayout/FrontRowPartyMember/PartyMember2,
	$MainMenuLayout/MainHBox/LMainMenuLayout/PartyLayout/FrontRowPartyMember/PartyMember3,
	$MainMenuLayout/MainHBox/LMainMenuLayout/PartyLayout/BackRowPartyMember/PartyMember4,
	$MainMenuLayout/MainHBox/LMainMenuLayout/PartyLayout/BackRowPartyMember/PartyMember5,
	$MainMenuLayout/MainHBox/LMainMenuLayout/PartyLayout/BackRowPartyMember/PartyMember6
]

# --- セーブ画面用UI参照 ---
@onready var save_layout: VBoxContainer = $MainMenuLayout/MainHBox/LMainMenuLayout/SaveLayout
@onready var next_slot_button: Button = $MainMenuLayout/MainHBox/LMainMenuLayout/SaveLayout/SaveBottomHBox/NextSlotButton
@onready var save_back_button: Button = $MainMenuLayout/MainHBox/LMainMenuLayout/SaveLayout/SaveBottomHBox/SaveBackButton
@onready var save_lavel: Label = $MainMenuLayout/MainHBox/LMainMenuLayout/SaveLayout/SaveSlotContainer/SaveLabel

# セーブスロット(3つ分)のボタン参照配列
@onready var slot_buttons: Array = [
	$MainMenuLayout/MainHBox/LMainMenuLayout/SaveLayout/SaveSlotContainer/SlotRow1/SaveSlot1,
	$MainMenuLayout/MainHBox/LMainMenuLayout/SaveLayout/SaveSlotContainer/SlotRow2/SaveSlot1,
	$MainMenuLayout/MainHBox/LMainMenuLayout/SaveLayout/SaveSlotContainer/SlotRow3/SaveSlot1
]

# --- ロード画面用UI参照 ---
@onready var load_layout: VBoxContainer = $MainMenuLayout/MainHBox/LMainMenuLayout/LoadLayout
@onready var load_next_slot_button: Button = $MainMenuLayout/MainHBox/LMainMenuLayout/LoadLayout/LoadBottomHBox/LoadNextSlotButton
@onready var load_back_button: Button = $MainMenuLayout/MainHBox/LMainMenuLayout/LoadLayout/LoadBottomHBox/LoadBackButton
@onready var load_label: Label = $MainMenuLayout/MainHBox/LMainMenuLayout/LoadLayout/LoadSlotContainer/LoadLabel

# ロードスロット(3つ分)のボタン参照配列
@onready var load_slot_buttons: Array = [
	$MainMenuLayout/MainHBox/LMainMenuLayout/LoadLayout/LoadSlotContainer/LoadSlotRow1/LoadSlot1,
	$MainMenuLayout/MainHBox/LMainMenuLayout/LoadLayout/LoadSlotContainer/LoadSlotRow2/LoadSlot1,
	$MainMenuLayout/MainHBox/LMainMenuLayout/LoadLayout/LoadSlotContainer/LoadSlotRow3/LoadSlot1
]

# ダイアログ類(ポップアップ)の参照
@onready var save_confirm_dialog: ConfirmationDialog = $SaveConfirmDialog
@onready var save_alert_dialog: AcceptDialog = $SaveAlertDialog
@onready var load_confirm_dialog: ConfirmationDialog = $LoadConfirmDialog # 【新設】
@onready var load_alert_dialog: AcceptDialog = $LoadAlertDialog         # 【新設】

# --- 状態管理変数 ---
var _current_page_index: int = 0 # 0=1-3, 1=4-6, 2=7-9 (セーブ用)
var _load_current_page_index: int = 0 # 0=1-3, 1=4-6, 2=7-9 (ロード用) 【新設】
var _selected_slot_no: int = 1
var _selected_load_slot_no: int = 1 # 【新設】
var _last_town_focus_owner: Control = null
var _load_success_flag: bool = false # ロード成功後に再起動を呼ぶためのフラグ 【新設】

func _ready() -> void:
	hide()
	if config_layout: config_layout.hide()
	if save_layout: save_layout.hide()
	if load_layout: load_layout.hide()
	
	# 【追加】LoadManager が実ロードを完了したら、即座にUI表示を最新化する
	LoadManager.load_completed.connect(func():
		_update_party_display()
		# ついでにロード画面を閉じて、メインメニューに戻してあげる
		_hide_all_sub_layouts()
		if party_layout: party_layout.show()
	)
	
	# 通常メニューボタンのシグナル接続
	if items_button: items_button.pressed.connect(func(): _on_menu_button_pressed("ITEMS"))
	if spells_button: spells_button.pressed.connect(func(): _on_menu_button_pressed("SPELLS"))
	if skills_button: skills_button.pressed.connect(func(): _on_menu_button_pressed("SKILLS"))
	if equipment_button: equipment_button.pressed.connect(func(): _on_menu_button_pressed("EQUIPMENT"))
	if formation_button: formation_button.pressed.connect(func(): _on_menu_button_pressed("FORMATION"))
	if status_button: status_button.pressed.connect(func(): _on_menu_button_pressed("STATUS"))
	if tactics_button: tactics_button.pressed.connect(func(): _on_menu_button_pressed("TACTICS"))
	if config_button: config_button.pressed.connect(func(): _on_menu_button_pressed("CONFIG"))
	if save_button: save_button.pressed.connect(func(): _on_menu_button_pressed("SAVE"))

	# メニューのフォーカスイベント接続
	if items_button: items_button.focus_entered.connect(func(): _update_explanation("MAIN_MENU_ITEMS_EXPLANATION"))
	if spells_button: spells_button.focus_entered.connect(func(): _update_explanation("MAIN_MENU_SPELLS_EXPLANATION"))
	if skills_button: skills_button.focus_entered.connect(func(): _update_explanation("MAIN_MENU_SKILLS_EXPLANATION"))
	if equipment_button: equipment_button.focus_entered.connect(func(): _update_explanation("MAIN_MENU_EQUIPMENT_EXPLANATION"))
	if formation_button: formation_button.focus_entered.connect(func(): _update_explanation("MAIN_MENU_FORMATION_EXPLANATION"))
	if status_button: status_button.focus_entered.connect(func(): _update_explanation("MAIN_MENU_STATUS_EXPLANATION"))
	if tactics_button: tactics_button.focus_entered.connect(func(): _update_explanation("MAIN_MENU_TACTICS_EXPLANATION"))
	if config_button: config_button.focus_entered.connect(func(): _update_explanation("MAIN_MENU_CONFIG_EXPLANATION"))
	if save_button: save_button.focus_entered.connect(func(): _update_explanation("MAIN_MENU_SAVE_EXPLANATION"))
	
	_setup_config_ui()
	_setup_save_ui()
	_setup_load_ui() # 【新設】
	setup_party_layout_sizes()

func _input(event: InputEvent) -> void:
	if not visible:
		if event.is_action_pressed("ui_select"):
			get_viewport().set_input_as_handled()
			open_menu()
		return
	
	if event.is_action_pressed("ui_cancel"):
		get_viewport().set_input_as_handled()
		if load_layout and load_layout.visible: # 【新設:ロード画面から設定画面へ戻る】
			_close_load_screen()
		elif config_layout and config_layout.visible:
			_close_config_screen()
		elif save_layout and save_layout.visible:
			_close_save_screen()
		else:
			close_menu()

func open_menu() -> void:
	update_all_strings() 
	_update_party_display()
	
	_hide_all_sub_layouts()
	if party_layout: party_layout.show()

	_last_town_focus_owner = get_viewport().gui_get_focus_owner()
	if _last_town_focus_owner:
		_last_town_focus_owner.release_focus()
	
	show()
	
	await get_tree().process_frame
	if items_button:
		items_button.grab_focus()
		
	menu_opened.emit()

func _open_config_screen() -> void:
	if not config_layout: return
	_hide_all_sub_layouts()
	config_layout.show()
	await get_tree().process_frame
	language_button.grab_focus()

func _close_config_screen() -> void:
	if not config_layout: return
	_hide_all_sub_layouts()
	if party_layout: party_layout.show()
	await get_tree().process_frame
	if config_button: config_button.grab_focus()

func _close_save_screen() -> void:
	if not save_layout: return
	_hide_all_sub_layouts()
	if party_layout: party_layout.show()
	await get_tree().process_frame
	if save_button:
		save_button.grab_focus()

func _back_to_main_menu_after_save() -> void:
	_close_save_screen()

# --- 【新設】ロード画面用コントロールメソッド ---
func _open_load_screen() -> void:
	if not load_layout: return
	_hide_all_sub_layouts()
	load_layout.show()
	
	_load_current_page_index = 0
	_update_explanation("MENU_LOAD_MESSAGE_EXPLANATION") # 「ロードするデータを選択してください」
	_render_load_slots()
	
	await get_tree().process_frame
	if load_slot_buttons[0]:
		load_slot_buttons[0].grab_focus()

func _close_load_screen() -> void:
	if not load_layout: return
	_hide_all_sub_layouts()
	if config_layout: config_layout.show() # 設定画面に戻る仕様
	await get_tree().process_frame
	if load_button: load_button.grab_focus()

func close_menu() -> void:
	hide()
	if _last_town_focus_owner and is_instance_valid(_last_town_focus_owner) and _last_town_focus_owner.is_inside_tree():
		_last_town_focus_owner.grab_focus()
	_last_town_focus_owner = null
	menu_closed.emit()

## メインメニューおよび現在の説明文を最新の言語に再翻訳するパブリック関数
func update_all_strings() -> void:
	if items_button: items_button.text = tr("MAIN_MENU_ITEMS")
	if spells_button: spells_button.text = tr("MAIN_MENU_SPELLS")
	if skills_button: skills_button.text = tr("MAIN_MENU_SKILLS")
	if equipment_button: equipment_button.text = tr("MAIN_MENU_EQUIPMENT")
	if formation_button: formation_button.text = tr("MAIN_MENU_FORMATION")
	if status_button: status_button.text = tr("MAIN_MENU_STATUS")
	if tactics_button: tactics_button.text = tr("MAIN_MENU_TACTICS")
	if config_button: config_button.text = tr("MAIN_MENU_CONFIG")
	if save_button: save_button.text = tr("MAIN_MENU_SAVE")
	if gold_label: gold_label.text = tr("MAIN_MENU_GOLD")
	
	if language_label: language_label.text = tr("MENU_SETTINGS_LANGUAGE_LABEL")
	if resolution_label: resolution_label.text = tr("MENU_SETTINGS_RESOLUTION_LABEL")
	if load_button: load_button.text = tr("MENU_SETTINGS_LOADBUTTON_LABEL") # 【新設:ゲームデータのロード】
	if config_back_button: config_back_button.text = tr("MENU_SETTINGS_BACKBUTTON_LABEL")
	
	# セーブ画面のテキスト
	if next_slot_button: next_slot_button.text = tr("MENU_SAVE_NEXT_PAGE")
	if save_back_button: save_back_button.text = tr("MENU_SAVE_BACK")
	if save_lavel: save_lavel.text = tr("MAIN_MENU_SAVE")
	
	# ロード画面のテキスト 【新設】
	if load_next_slot_button: load_next_slot_button.text = tr("MENU_SAVE_NEXT_PAGE") # 「次のスロットを見る」を共通流用
	if load_back_button: load_back_button.text = tr("MENU_SETTINGS_BACKBUTTON_LABEL") # 「戻る」を流用
	if load_label: load_label.text = tr("MENU_LOAD") # 「ロード」タイトル
	
	# ダイアログのウィンドウタイトルのローカライズ
	if save_confirm_dialog:
		save_confirm_dialog.title = tr("MENU_SAVE_CONFIRM_DIALOG_TITLE")
	if save_alert_dialog:
		save_alert_dialog.title = tr("MENU_SAVE_ALERT_DIALOG_TITLE")
	if load_confirm_dialog: # 【新設】
		load_confirm_dialog.title = tr("MENU_LOAD_CONFIRM_DIALOG_TITLE")
	if load_alert_dialog: # 【新設】
		load_alert_dialog.title = tr("MENU_LOAD_ALERT_DIALOG_TITLE")
	
	if config_layout and config_layout.visible:
		_update_explanation("MAIN_MENU_CONFIG_EXPLANATION")
	elif save_layout and save_layout.visible:
		_update_explanation("MENU_SAVE_MESSAGE_EXPLANATION")
	elif load_layout and load_layout.visible: # 【新設】
		_update_explanation("MENU_LOAD_MESSAGE_EXPLANATION")
	elif config_button and config_button.has_focus():
		_update_explanation("MAIN_MENU_CONFIG_EXPLANATION")
		
	_update_party_display()
	
	# セーブ画面・ロード画面表示中ならスロット上のテキストも再翻訳して更新
	if save_layout and save_layout.visible:
		_render_save_slots()
	elif load_layout and load_layout.visible: # 【新設】
		_render_load_slots()

func _on_menu_button_pressed(button_id: String) -> void:
	_hide_all_sub_layouts()
	
	match button_id:
		"CONFIG":
			_open_config_screen()
		"SAVE":
			_open_save_screen()
		"ITEMS", "SPELLS", "SKILLS", "EQUIPMENT", "FORMATION", "STATUS", "TACTICS":
			if party_layout: party_layout.show()
			print("[MenuWindow] 選択されたメニュー: ", button_id)
		
func _hide_all_sub_layouts() -> void:
	if party_layout: party_layout.hide()
	if config_layout: config_layout.hide()
	if save_layout: save_layout.hide()
	if load_layout: load_layout.hide() # 【新設】

func _update_explanation(loc_id: String) -> void:
	if menu_message_label:
		menu_message_label.text = tr(loc_id)

# ==============================================================================
# メニュー画面全体のフルサイズ固定(全画面化)
# ==============================================================================
func set_menu_full_size() -> void:
	if not main_menu_layout: return
	self.custom_minimum_size = Vector2.ZERO
	main_menu_layout.custom_minimum_size = Vector2.ZERO
	
	var target_screen_size := Vector2(1280, 720)
	self.set_anchors_preset(Control.PRESET_TOP_LEFT)
	self.position = Vector2.ZERO
	main_menu_layout.set_anchors_preset(Control.PRESET_TOP_LEFT)
	main_menu_layout.position = Vector2.ZERO
	self.size = target_screen_size
	main_menu_layout.size = target_screen_size
	self.custom_minimum_size = target_screen_size
	main_menu_layout.custom_minimum_size = target_screen_size

# ==============================================================================
# 設定画面(Config)のコントロールロジック
# ==============================================================================
func _setup_config_ui() -> void:
	if not language_button or not resolution_button or not config_back_button: return
	language_button.clear()
	language_button.add_item("English", 0)
	language_button.add_item("日本語", 1)
	language_button.select(0)
	language_button.item_selected.connect(func(id): language_change_requested.emit(id))
	
	resolution_button.clear()
	resolution_button.add_item("1280x720", 0)
	resolution_button.add_item("1920x1080", 1)
	resolution_button.select(0)
	resolution_button.item_selected.connect(func(id): resolution_change_requested.emit(id))
	
	# ロード画面へ遷移するボタンの接続 【新設】
	if load_button:
		load_button.pressed.connect(_open_load_screen)
		
	config_back_button.pressed.connect(_close_config_screen)

# ==============================================================================
# セーブ画面(Save)のコントロールロジック
# ==============================================================================
func _setup_save_ui() -> void:
	if not next_slot_button or not save_back_button: return
	
	next_slot_button.pressed.connect(_on_next_slot_pressed)
	save_back_button.pressed.connect(_close_save_screen)
	
	for i in range(3):
		var btn = slot_buttons[i]
		if btn:
			btn.pressed.connect(func(): _on_slot_selected(i))
			
	if save_confirm_dialog:
		save_confirm_dialog.confirmed.connect(_execute_save)
		save_confirm_dialog.canceled.connect(func(): slot_buttons[0].grab_focus())
		
	if save_alert_dialog:
		save_alert_dialog.confirmed.connect(_back_to_main_menu_after_save)

func _open_save_screen() -> void:
	if not save_layout or not party_layout: return
	party_layout.hide()
	if config_layout: config_layout.hide()
	save_layout.show()
	
	_current_page_index = 0
	_update_explanation("MENU_SAVE_MESSAGE_EXPLANATION")
	_render_save_slots()
	
	await get_tree().process_frame
	if slot_buttons[0]:
		slot_buttons[0].grab_focus()

func _render_save_slots() -> void:
	var start_slot = (_current_page_index * 3) + 1
	_render_generic_slots(start_slot, slot_buttons)

func _on_next_slot_pressed() -> void:
	_current_page_index = (_current_page_index + 1) % 3
	_render_save_slots()
	await get_tree().process_frame
	if slot_buttons[0]:
		slot_buttons[0].grab_focus()

func _on_slot_selected(local_index: int) -> void:
	_selected_slot_no = (_current_page_index * 3) + 1 + local_index
	var header = SaveManager.get_slot_header(_selected_slot_no)
	
	if not save_confirm_dialog: return
	
	if not header["exists"]:
		save_confirm_dialog.dialog_text = tr("MENU_SAVE_CONFIRM_DIALOG_NEW_DATA") % _selected_slot_no
	else:
		save_confirm_dialog.dialog_text = tr("MENU_SAVE_CONFIRM_DIALOG_OVERWRITE") % _selected_slot_no
		
	save_confirm_dialog.popup_centered()
	var cancel_btn = save_confirm_dialog.get_cancel_button()
	if cancel_btn:
		cancel_btn.grab_focus()

func _execute_save() -> void:
	var success = SaveManager.save_game(_selected_slot_no)
	if not save_alert_dialog: return
	if success:
		save_alert_dialog.dialog_text = tr("MENU_SAVE_ALERT_DIALOG_SUCCESS") % _selected_slot_no
	else:
		save_alert_dialog.dialog_text = tr("MENU_SAVE_ALERT_DIALOG_FAILURE") % _selected_slot_no
	save_alert_dialog.popup_centered()

# ==============================================================================
# 【新設】ロード画面(Load)のコントロールロジック
# ==============================================================================
func _setup_load_ui() -> void:
	if not load_next_slot_button or not load_back_button: return
	
	load_next_slot_button.pressed.connect(_on_load_next_slot_pressed)
	load_back_button.pressed.connect(_close_load_screen)
	
	for i in range(3):
		var btn = load_slot_buttons[i]
		if btn:
			btn.pressed.connect(func(): _on_load_slot_selected(i))
			
	if load_confirm_dialog:
		load_confirm_dialog.confirmed.connect(_execute_load)
		load_confirm_dialog.canceled.connect(func(): load_slot_buttons[0].grab_focus()) # キャンセル時はスロットにフォーカス
		
	if load_alert_dialog:
		load_alert_dialog.confirmed.connect(_on_load_alert_confirmed)

func _render_load_slots() -> void:
	var start_slot = (_load_current_page_index * 3) + 1
	_render_generic_slots(start_slot, load_slot_buttons)

func _on_load_next_slot_pressed() -> void:
	_load_current_page_index = (_load_current_page_index + 1) % 3
	_render_load_slots()
	await get_tree().process_frame
	if load_slot_buttons[0]:
		load_slot_buttons[0].grab_focus()

func _on_load_slot_selected(local_index: int) -> void:
	_selected_load_slot_no = (_load_current_page_index * 3) + 1 + local_index
	
	if not load_confirm_dialog: return
	
	# 仕様:「データNo.%dをロードしますか?」の確認文を表示
	load_confirm_dialog.dialog_text = tr("MENU_LOAD_CONFIRM_DIALOG_QUESTION") % _selected_load_slot_no
	load_confirm_dialog.popup_centered()
	
	# 仕様:デフォルトフォーカスを「いいえ (Cancel)」にする
	var cancel_btn = load_confirm_dialog.get_cancel_button()
	if cancel_btn:
		cancel_btn.grab_focus()

func _execute_load() -> void:
	# ロード処理を実行(この中で change_scene が走る)
	_load_success_flag = LoadManager.load_game(_selected_load_slot_no)
	
	if _load_success_flag:
		# 【修正】ロード成功時はシーンが切り替わるため、この瞬間にダイアログを出してはいけない。
		# 何もせず、LoadManager 側のシーン切り替え待機に処理を委ねる。
		print("[MenuWindow] ロード成功。シーン切り替えを待機します。")
	else:
		# ロード失敗時のみ、シーンは切り替わらないので安全にエラーダイアログを出せる
		if load_alert_dialog:
			load_alert_dialog.dialog_text = tr("MENU_LOAD_ALERT_DIALOG_FAILURE") % _selected_load_slot_no
			load_alert_dialog.popup_centered()

func _on_load_alert_confirmed() -> void:
	# 成功時はここを通らなくなったため、ここは「失敗してアラートを閉じたとき」の処理のみにする
	await get_tree().process_frame
	if load_slot_buttons[0]:
		load_slot_buttons[0].grab_focus()

# ==============================================================================
# セーブ/ロード共通のスロットレンダリング共通関数
# ==============================================================================
func _render_generic_slots(start_slot: int, target_buttons: Array) -> void:
	for i in range(3):
		var slot_no = start_slot + i
		var btn: Button = target_buttons[i]
		if not btn: continue
		
		var header = SaveManager.get_slot_header(slot_no)
		var parent_node = btn.get_parent()
		var lbl_no: Label = null
		if parent_node:
			# セーブとロードでノード名が異なるため部分一致検索
			lbl_no = parent_node.find_child("*SlotNoLabel", true, false)
		
		var rect_face: TextureRect = btn.find_child("SlotAppearance", true, false)
		var lbl_time: Label = btn.find_child("SlotPlayTimeLabel", true, false)
		var lbl_name: Label = btn.find_child("SlotNameLabel", true, false)
		var lbl_lv: Label = btn.find_child("SlotLvLabel", true, false)
		var lbl_class: Label = btn.find_child("SlotClassLabel", true, false)
		
		if lbl_no: lbl_no.text = "No.%d" % slot_no
		
		if not header["exists"]:
			if lbl_time: lbl_time.text = "--:--:--"
			if lbl_name: lbl_name.text = tr("MENU_SAVE_SLOT_EMPTY")
			if lbl_lv: lbl_lv.text = ""
			if lbl_class: lbl_class.text = ""
			if rect_face: rect_face.texture = null
		else:
			if lbl_time: lbl_time.text = header.get("play_time", "00:00:00")
			var party_data = header.get("party", [])
			if party_data.size() > 0:
				var chara = party_data[0]
				if lbl_name: lbl_name.text = chara["name"]
				if lbl_lv: lbl_lv.text = "Lv.%d" % chara["level"]
				if lbl_class: lbl_class.text = tr(chara["class"])
				
				if rect_face and chara["face_image_path"] and FileAccess.file_exists(chara["face_image_path"]):
					rect_face.texture = load(chara["face_image_path"])
				elif rect_face:
					rect_face.texture = null
			else:
				if lbl_name: lbl_name.text = tr("MENU_SAVE_SLOT_NO_PARTY")
				if lbl_lv: lbl_lv.text = ""
				if lbl_class: lbl_class.text = ""
				if rect_face: rect_face.texture = null

# ==============================================================================
# サイズ調整・パーティ表示・その他(修正・追加版)
# ==============================================================================
func set_config_layout_size(resolution_index: int) -> void:
	if not config_layout: return
	var target_config_size := Vector2(320, 180)
	match resolution_index:
		0: target_config_size = Vector2(320, 180)
		1: target_config_size = Vector2(480, 270)
	config_layout.custom_minimum_size = target_config_size
	config_layout.size = target_config_size

func set_main_layouts_size() -> void:
	if not l_main_menu_layout or not r_main_menu_layout: return
	var left_width := 1120.0
	var right_width := 128.0
	l_main_menu_layout.custom_minimum_size = Vector2(left_width, 0)
	l_main_menu_layout.size.x = left_width
	r_main_menu_layout.custom_minimum_size = Vector2(right_width, 0)
	r_main_menu_layout.size.x = right_width

func _update_party_display() -> void:
	for i in range(6):
		var member_panel = member_nodes[i]
		if not member_panel: continue
		member_panel.show()
		var data = PartyManager.slots[i]
		var label_name: Label = member_panel.find_child("Name", true, false)
		var label_lv: Label = member_panel.find_child("Lv", true, false)
		var rect_appearance: TextureRect = member_panel.find_child("Appearance", true, false)
		var label_class: Label = member_panel.find_child("Class", true, false)
		var label_hp: Label = member_panel.find_child("HPLabel", true, false)
		var bar_hp: ProgressBar = member_panel.find_child("HPBar", true, false)
		var label_mp: Label = member_panel.find_child("MPLabel", true, false)
		var bar_mp: ProgressBar = member_panel.find_child("MPBar", true, false)

		if data == null:
			if label_name: label_name.text = ""
			if label_lv: label_lv.text = ""
			if label_class: label_class.text = ""
			if label_hp: label_hp.text = ""
			if label_mp: label_mp.text = ""
			if rect_appearance: rect_appearance.texture = null
			if bar_hp: bar_hp.hide()
			if bar_mp: bar_mp.hide()
		else:
			if bar_hp: bar_hp.show()
			if bar_mp: bar_mp.show()
			if label_name: label_name.text = data.chara_name
			if label_lv: label_lv.text = "Lv%d" % data.level
			if label_class: label_class.text = data.get_display_class()
			if label_hp: label_hp.text = "HP %d/%d" % [data.current_hp, data.max_hp]
			if label_mp: label_mp.text = "MP %d/%d" % [data.current_mp, data.max_mp]
			if rect_appearance:
				if data.face_image_path and FileAccess.file_exists(data.face_image_path):
					rect_appearance.texture = load(data.face_image_path)
				else:
					rect_appearance.texture = null
				rect_appearance.custom_minimum_size = Vector2(160, 160)
				rect_appearance.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
			if bar_hp:
				bar_hp.min_value = 0
				bar_hp.max_value = data.max_hp
				bar_hp.value = data.current_hp
			if bar_mp:
				bar_mp.min_value = 0
				bar_mp.max_value = data.max_mp
				bar_mp.value = data.current_mp

func setup_party_layout_sizes() -> void:
	if party_layout and front_row_party_member and back_row_party_member:
		party_layout.custom_minimum_size = Vector2(960, 0)
		party_layout.size.x = 960
		front_row_party_member.custom_minimum_size = Vector2(960, 0)
		back_row_party_member.custom_minimum_size = Vector2(960, 0)
		
		for member_panel in member_nodes:
			if not member_panel: continue
			member_panel.custom_minimum_size = Vector2(320, 240)
			member_panel.size = Vector2(320, 240)
			var member_hbox = member_panel.get_node_or_null("MemberHBox1")
			if not member_hbox: member_hbox = member_panel.find_child("MemberHBox*", true, false)
			if member_hbox:
				var l_part = member_hbox.get_node_or_null("LPartyMember1")
				if not l_part: l_part = member_hbox.find_child("LPartyMember*", true, false)
				if l_part:
					l_part.custom_minimum_size = Vector2(160, 0)
					l_part.size.x = 160
				var r_part = member_hbox.get_node_or_null("RPartyMember1")
				if not r_part: r_part = member_hbox.find_child("RPartyMember*", true, false)
				if r_part:
					r_part.custom_minimum_size = Vector2(160, 0)
					r_part.size.x = 160
	
	# セーブレイアウト側の固定
	if save_layout:
		_apply_layout_fixing_sizes(save_layout, slot_buttons)
		
	# ロードレイアウト側の固定
	if load_layout:
		_apply_layout_fixing_sizes(load_layout, load_slot_buttons)

## 前回のレイアウト崩れ・名前の長さによるズレ防止を共通適用するプライベート関数
func _apply_layout_fixing_sizes(target_layout: Control, target_buttons: Array) -> void:
	target_layout.custom_minimum_size = Vector2(420, 0)
	target_layout.size.x = 420
	
	for btn in target_buttons:
		if btn is Button:
			var current_height = btn.size.y if btn.size.y > 0 else 120.0
			btn.custom_minimum_size = Vector2(420, current_height)
			btn.size = Vector2(420, current_height)
			
			var slot_left = btn.find_child("SlotLeft", true, false)
			if slot_left and slot_left is Control:
				slot_left.custom_minimum_size = Vector2(160, 0)
				slot_left.size.x = 160
			
			var slot_right = btn.find_child("SlotRight", true, false)
			if slot_right and slot_right is Control:
				slot_right.custom_minimum_size = Vector2(260, 0)
				slot_right.size.x = 260
				
	print("[MenuWindow] レイアウトの横幅を固定長にロックしました: ", target_layout.name)

メニュー画面用スクリプト (menu_window.gd) を上記コードに修正します。これでゲームデータのロードができるようになりました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

CAPTCHA


目次