MENU

Godot 4 でWiz系ダンジョンRPG開発 (冒険者ギルドの実装) [013]

今回は「キャラクターの作成」以外の「冒険者ギルド」の各機能を実装していきます。この記事における冒険者ギルドの実装とは、以下の機能を実装することです。(上記画像をクリックすると、この記事で実現することのイメージが沸くと思います)

  • 冒険者をよびだす
  • 冒険者をあずける
  • 冒険者を作成する 前回記事で作成済み。要調整
  • クラスを変える
  • 冒険者を削除する
  • 名前を変える
  • 見た目を変える
  • 街へ戻る 要調整

この記事の内容を実装することで、パーティメンバーを冒険者ギルドにあずけたりクラスチェンジしたりすることができます。

前回は、冒険者ギルドの機能の1つ「キャラクターの作成」を行いました。今まで作成してきたのは以下の通りです。詳しくは過去記事をご確認ください。

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

冒険者ギルドの実装 “準備”

冒険者ギルドでは、上記画像にある機能を利用することができます。「冒険者を作成する」機能は実装済みです。(詳細は前回記事参照) しかし、他の機能は実装されていません。その実装する前に準備をします。その準備とは以下の通りです。

ギルド管理マネージャーの実装冒険者ギルドを管理し各機能を統括・ハンドリングする。
キャラクター作成のリファクタリング・作成したキャラクターをギルド管理マネージャーに渡す
名前を「カタカナ」でも入力できるようにする
キャラクターデータクラスのローカライゼーション
街処理用スクリプトのリファクタリング今までは上記画像のメニューを街処理用スクリプトが担当していましたが、これをギルド管理マネージャーに移譲する。
パーティ管理マネージャーの実装パーティを管理するクラス。ここではギルドから冒険者を呼び出して受け取ったり、ギルドに冒険者をあずけたりする。(他は省略)

ギルド管理マネージャーの実装

最初に、冒険者ギルドを管理し各機能を統括・ハンドリングするギルド管理マネージャーを作成。そして、このマネージャーをゲーム内のどこからでも呼び出せるようシステムに登録するようにします。

  • ギルド管理用スクリプトファイルの作成
  • Godot Project への登録 (Autoload設定)

ギルド管理用スクリプトファイルの作成

extends Node

# ==============================================================================
# 冒険者ギルド 管理マネージャー (Autoload用)
# ==============================================================================

# 定数定義
const MAX_MEMBERS: int = 24 # 最大登録人数

# データの保持(メモリ管理)
# ギルドに登録されているすべての冒険者(CharaDataインスタンス)を格納する配列
var registered_members: Array[CharaData] = []

# 現在パーティーに加入している(呼び出されている)冒険者のリスト(最大6名などを想定)
var party_members: Array[CharaData] = []


# ==============================================================================
# ギルド機能のコアロジック群 (バックエンド処理)
# ==============================================================================

# 1. 冒険者をよびだす (パーティーに加入させる)
func call_out_adventurer(chara: CharaData) -> bool:
	# すでにパーティーにいる場合は何もしない
	if chara in party_members:
		return false
	
	# パティー人数の上限チェック(例としてWizライクな最大6名と仮定)
	if party_members.size() >= 6:
		print("パーティーが満員です")
		return false
		
	party_members.append(chara)
	print(chara.chara_name, " をパーティーに呼び出しました。")
	return true


# 2. 冒険者をあずける (パーティーから外す)
func leave_adventurer(chara: CharaData) -> bool:
	if chara in party_members:
		party_members.erase(chara)
		print(chara.chara_name, " をギルドに預けました。")
		return true
	return false


# 3. 冒険者を作成する (ギルドへの新規登録)
# キャラクターメイク画面から CharaData のインスタンスを受け取って管理下に置く
func register_new_adventurer(new_chara: CharaData) -> bool:
	# 人数制限チェック (将来の制限復活を見据えて枠だけ実装、現状は常にtrue)
	# if registered_members.size() >= MAX_MEMBERS:
	#     return false
	
	registered_members.append(new_chara)
	print("ギルドに新メンバーを登録しました: ", new_chara.chara_name, " (総員: ", registered_members.size(), "名)")
	return true


# 4. クラスを変える (職業の変更)
func change_character_class(chara: CharaData, new_class_id: String) -> void:
	chara.chara_class = new_class_id
	chara.update_display_parameters() # 攻撃力などの再計算
	print(chara.chara_name, " の職業を ", chara.get_display_class(), " に変更しました。")


# 5. 冒険者を削除する (ギルドからの抹消)
func delete_adventurer(chara: CharaData) -> void:
	# パーティーにいる場合はあらかじめ外す
	if chara in party_members:
		party_members.erase(chara)
		
	if chara in registered_members:
		registered_members.erase(chara)
		print(chara.chara_name, " の登録を削除しました。")


# 6. 名前を変える
func rename_adventurer(chara: CharaData, new_name: String) -> void:
	var old_name = chara.chara_name
	chara.chara_name = new_name
	print("名前を変更しました: ", old_name, " -> ", new_name)


# 7. 見た目を変える
func change_adventurer_appearance(chara: CharaData, new_image_path: String) -> void:
	chara.face_image_path = new_image_path
	print(chara.chara_name, " の見た目を変更しました: ", new_image_path)


# 8. 街へ戻る (必要に応じてギルドの一時データクリアやセーブ処理などをここに挟む)
func prepare_return_to_town() -> void:
	print("街へ戻る準備を行います。")
	# 将来的に自動セーブなどを走らせる場合はここに記述します

Godot Engine の Filesystem に guild_manager.gd スクリプトファイルを作成してください。このスクリプトファイルには上記コードを記述します。

Godot Project への登録 (Autoload設定)

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

Globals タブから Autoload にある Path のフォルダアイコンをクリックします。

先ほど作成した guild_manager.gd を選択し、開いてください。

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

リストに GuildManager が追加され、「有効 (Enable)」にチェックが入っていることを確認して、ウインドウを閉じてください。これで、ゲーム内のどのスクリプトからでも GuildManager.register_new_adventurer(…) や GuildManager.registered_members のように、直接このマネージャーを操作できるようになりました。

キャラクター作成 (リファクタリング)

extends Control

signal creation_canceled

# 子ノードへの参照
@onready var title_label: Label = $NameLayout/TitleLabel
@onready var name_input_field: LineEdit = $NameLayout/NameInputField
@onready var keyboard_grid: GridContainer = $NameLayout/KeyboardGrid
@onready var backspace_button: Button = $NameLayout/BottomMenu/BackspaceButton
@onready var confirm_button: Button = $NameLayout/BottomMenu/ConfirmButton

@onready var race_layout: VBoxContainer = $RaceLayout
@onready var race_title_label: Label = $RaceLayout/RaceTitleLabel
@onready var race_button_container: VBoxContainer = $RaceLayout/RaceButtonContainer

@onready var status_layout: VBoxContainer = $StatusLayout
@onready var status_instruction_label: Label = $StatusLayout/StatusInstructionLabel
@onready var character_summary_label: Label = $StatusLayout/CharacterSummaryLabel
@onready var status_grid_container: GridContainer = $StatusLayout/StatusGridContainer
@onready var status_back_button: Button = $StatusLayout/StatusBottomMenu/StatusBackButton
@onready var status_next_button: Button = $StatusLayout/StatusBottomMenu/StatusNextButton

@onready var class_layout: VBoxContainer = $ClassLayout
@onready var class_title_label: Label = $ClassLayout/ClassTitleLabel
@onready var class_status_preview: GridContainer = $ClassLayout/ClassMainContent/ClassStatusPreview
@onready var basic_class_column: VBoxContainer = $ClassLayout/ClassMainContent/ClassSelectionArea/BasicClassColumn
@onready var advanced_class_column: VBoxContainer = $ClassLayout/ClassMainContent/ClassSelectionArea/AdvancedClassColumn

@onready var appearance_layout: VBoxContainer = $AppearanceLayout
@onready var appearance_title_label: Label = $AppearanceLayout/AppearanceTitleLabel
@onready var appearance_summary_label: Label = $AppearanceLayout/AppearanceSummaryLabel
@onready var appearance_status_preview: GridContainer = $AppearanceLayout/AppearanceMainContent/AppearanceStatusPreview
@onready var appearance_wheel_control: Control = $AppearanceLayout/AppearanceMainContent/AppearanceWheelControl
@onready var appearance_random_button: Button = $AppearanceLayout/AppearanceBottomMenu/AppearanceRandomButton
@onready var appearance_back_button: Button = $AppearanceLayout/AppearanceBottomMenu/AppearanceBackButton
@onready var appearance_next_button: Button = $AppearanceLayout/AppearanceBottomMenu/AppearanceNextButton

@onready var confirmation_layout: VBoxContainer = $ConfirmationLayout
@onready var confirm_title_label: Label = $ConfirmationLayout/ConfirmTitleLabel
@onready var confirm_summary_label: Label = $ConfirmationLayout/ConfirmSummaryLabel
@onready var confirm_status_preview: GridContainer = $ConfirmationLayout/ConfirmMainContent/ConfirmStatusPreview
@onready var confirm_face_preview: TextureRect = $ConfirmationLayout/ConfirmMainContent/ConfirmFacePreview
@onready var confirm_back_button: Button = $ConfirmationLayout/ConfirmBottomMenu/ConfirmBackButton
@onready var confirm_create_button: Button = $ConfirmationLayout/ConfirmBottomMenu/ConfirmCreateButton

@onready var message_layout: VBoxContainer = $MessageLayout
@onready var message_text_label: Label = $MessageLayout/MessageTextLabel

# キーボードの文字データ定義
const KEYS_JA = [
	"あ", "い", "う", "え", "お", "か", "き", "く", "け", "こ",
	"さ", "し", "す", "せ", "そ", "た", "ち", "つ", "て", "と",
	"な", "に", "ぬ", "ね", "の", "は", "ひ", "ふ", "へ", "ほ",
	"ま", "み", "む", "め", "モ", "や", "ゆ", "よ", "わ", "を",
	"ら", "り", "る", "れ", "ろ", "ん", "っ", "ゃ", "ゅ", "ょ",
	"が", "ぎ", "ぐ", "げ", "ご", "ざ", "じ", "ず", "ぜ", "ぞ",
	"だ", "ぢ", "づ", "で", "ど", "ば", "び", "ぶ", "べ", "ぼ",
	"ぱ", "ぴ", "ぷ", "ぺ", "ぽ", "ー", "〜", "、", "。", " ",
	"ア", "イ", "ウ", "エ", "オ", "カ", "キ", "ク", "ケ", "コ",
	"サ", "シ", "ス", "セ", "ソ", "タ", "チ", "ツ", "テ", "ト",
	"ナ", "ニ", "ヌ", "ネ", "ノ", "ハ", "ヒ", "フ", "ヘ", "ホ",
	"マ", "ミ", "ム", "メ", "モ", "ヤ", "ユ", "ヨ", "ワ", "ヲ",
	"ラ", "リ", "ル", "レ", "ロ", "ン", "ッ", "ャ", "ュ", "ョ",
	"ガ", "ギ", "グ", "ゲ", "ゴ", "ザ", "ジ", "ズ", "ゼ", "ゾ",
	"ダ", "ヂ", "ヅ", "デ", "ド", "バ", "ビ", "ブ", "ベ", "ボ",
	"パ", "ピ", "プ", "ペ", "ポ", " ",  " ",  " ",  " ",  " "
]

const KEYS_EN = [
	"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
	"U", "V", "W", "X", "Y", "Z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-", "_", "!", "?",
	"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", 
	"u", "v", "w", "x", "y", "z"
]

var current_max_length: int = 16
var first_button: Button = null 

# --- キャラクターの作成データ保持用 ---
var final_character_name: String = ""
var final_character_race: String = ""  # "RACE_HUMAN", "RACE_ELF" などのIDを入れる運用にします
var final_character_class: String = "" # "CLASS_FIGHTER" などのIDを入れる運用にします
var final_character_image_path: String = "" 

# --- 種族の初期ステータス定義(CSVのマスターIDに準拠) ---
const RACE_BASE_STATS = {
	"RACE_HUMAN": { "LV": 1, "STR": 9, "DEX": 9, "VIT": 9, "AGI": 9, "INT": 9, "MND": 9, "LUC": 9, "HP_MIN": 11, "HP_MAX": 18, "MP_MIN": 9, "MP_MAX": 16 },
	"RACE_ELF":   { "LV": 1, "STR": 6, "DEX": 12, "VIT": 6, "AGI": 12, "INT": 12, "MND": 9, "LUC": 6, "HP_MIN": 7, "HP_MAX": 14, "MP_MIN": 13, "MP_MAX": 20 },
	"RACE_DWARF": { "LV": 1, "STR": 13, "DEX": 10, "VIT": 13, "AGI": 6, "INT": 6, "MND": 9, "LUC": 6, "HP_MIN": 13, "HP_MAX": 20, "MP_MIN": 7, "MP_MAX": 14 }
}

const ALLOCATABLE_STATS = ["STR", "DEX", "VIT", "AGI", "INT", "MND", "LUC"]

# CSVのステータスIDマッピングテーブル
const STAT_ID_MAP = {
	"LV": "STATUS_LEVEL",
	"HP": "STATUS_HP",
	"MP": "STATUS_MP",
	"STR": "STATUS_STR",
	"DEX": "STATUS_DEX",
	"VIT": "STATUS_VIT",
	"AGI": "STATUS_AGI",
	"INT": "STATUS_INT",
	"MND": "STATUS_MND",
	"LUC": "STATUS_LUC"
}

var current_base_stats = {}
var allocated_bonus_stats = { "STR": 0, "DEX": 0, "VIT": 0, "AGI": 0, "INT": 0, "MND": 0, "LUC": 0 }
var initial_bonus_points: int = 0
var remaining_bonus_points: int = 0

var stat_labels_dictionary = {}
var current_focused_stat_button: Button = null

# --- ゲーム進行フラグ ---
var is_advanced_races_unlocked: bool = true 
var is_class_group_1_unlocked: bool = false 
var is_class_group_2_unlocked: bool = false 
var is_class_group_3_unlocked: bool = false 

# --- 見た目(外見)選択用の変数群 ---
var face_image_paths: Array[String] = [] 
var face_sprites: Array[TextureRect] = [] 
var current_image_index: int = 0 
var target_wheel_angle: float = 0.0 
var current_wheel_angle: float = 0.0 
var is_wheel_focused: bool = false 
var wheel_tween: Tween = null

func _ready() -> void:
	if backspace_button and not backspace_button.pressed.is_connected(_on_backspace_button_pressed):
		backspace_button.pressed.connect(_on_backspace_button_pressed)
	if confirm_button and not confirm_button.pressed.is_connected(_on_confirm_button_pressed):
		confirm_button.pressed.connect(_on_confirm_button_pressed)
		
	if status_back_button and not status_back_button.pressed.is_connected(_on_status_back_pressed):
		status_back_button.pressed.connect(_on_status_back_pressed)
	if status_next_button and not status_next_button.pressed.is_connected(_on_status_next_pressed):
		status_next_button.pressed.connect(_on_status_next_pressed)
		
	if appearance_random_button and not appearance_random_button.pressed.is_connected(_on_appearance_random_pressed):
		appearance_random_button.pressed.connect(_on_appearance_random_pressed)
	if appearance_back_button and not appearance_back_button.pressed.is_connected(_on_appearance_back_pressed):
		appearance_back_button.pressed.connect(_on_appearance_back_pressed)
	if appearance_next_button and not appearance_next_button.pressed.is_connected(_on_appearance_next_pressed):
		appearance_next_button.pressed.connect(_on_appearance_next_pressed)
		
	# 確認画面のボタン接続
	if confirm_back_button and not confirm_back_button.pressed.is_connected(_on_confirm_back_pressed):
		confirm_back_button.pressed.connect(_on_confirm_back_pressed)
	if confirm_create_button and not confirm_create_button.pressed.is_connected(_on_confirm_create_pressed):
		confirm_create_button.pressed.connect(_on_confirm_create_pressed)
		
	hide()
	if race_layout: race_layout.hide()
	if status_layout: status_layout.hide()
	if class_layout: class_layout.hide()
	if appearance_layout: appearance_layout.hide()
	if confirmation_layout: confirmation_layout.hide()
	if message_layout: message_layout.hide()

func _input(event: InputEvent) -> void:
	if not is_visible_in_tree():
		return
		
	if event.is_action_pressed("ui_cancel"):
		if $NameLayout.is_visible_in_tree():
			get_viewport().set_input_as_handled()
			if name_input_field.text.length() > 0:
				_on_backspace_button_pressed()
			else:
				_cancel_and_return_to_guild()
				
		elif race_layout.is_visible_in_tree():
			get_viewport().set_input_as_handled()
			_back_to_name_input()
			
		elif status_layout.is_visible_in_tree():
			get_viewport().set_input_as_handled()
			if current_focused_stat_button and current_focused_stat_button.has_meta("stat_key"):
				var stat_key = current_focused_stat_button.get_meta("stat_key")
				if allocated_bonus_stats[stat_key] > 0:
					_modify_stat_point(stat_key, -1)
					return
			
			var total_spent = 0
			for val in allocated_bonus_stats.values():
				total_spent += val
			if total_spent == 0:
				_back_to_race_selection()
				
		elif class_layout.is_visible_in_tree():
			get_viewport().set_input_as_handled()
			_back_to_status_allocation()
			
		elif appearance_layout.is_visible_in_tree():
			get_viewport().set_input_as_handled()
			_back_to_class_selection()
			
		elif confirmation_layout.is_visible_in_tree():
			get_viewport().set_input_as_handled()
			_back_to_appearance_selection()

	if appearance_layout.is_visible_in_tree() and is_wheel_focused and face_image_paths.size() > 0:
		if event.is_action_pressed("ui_right", true) or event.is_action("ui_right"):
			if event.is_pressed():
				get_viewport().set_input_as_handled()
				_rotate_wheel(1)
		elif event.is_action_pressed("ui_left", true) or event.is_action("ui_left"):
			if event.is_pressed():
				get_viewport().set_input_as_handled()
				_rotate_wheel(-1)
		elif event.is_action_pressed("ui_accept"):
			get_viewport().set_input_as_handled()
			if appearance_next_button:
				appearance_next_button.grab_focus()
				
	elif message_layout.is_visible_in_tree():
		if event.is_action_pressed("ui_accept") or event.is_action_pressed("ui_cancel"):
			get_viewport().set_input_as_handled()
			_return_to_guild_menu()

func activate_name_input() -> void:
	name_input_field.text = ""
	show()
	$NameLayout.show()
	if race_layout: race_layout.hide()
	if status_layout: status_layout.hide()
	if class_layout: class_layout.hide()
	if appearance_layout: appearance_layout.hide()
	if confirmation_layout: confirmation_layout.hide()
	if message_layout: message_layout.hide()
	
	var current_locale = TranslationServer.get_locale().left(2)
	
	# システムテキストは本来は localization.csv に追加するのがベストですが、
	# 現状のCSV(ステータスや職業、種族)に合わせた安全な動的切り替えを行います
	if current_locale == "ja":
		current_max_length = 8
		_setup_software_keyboard(KEYS_JA)
		if title_label: title_label.text = "名前を入力してください"
		if backspace_button: backspace_button.text = "一字消す"
		if confirm_button: confirm_button.text = "決定"
	else:
		current_max_length = 16
		_setup_software_keyboard(KEYS_EN)
		if title_label: title_label.text = "Enter your name."
		if backspace_button: backspace_button.text = "BackSpace"
		if confirm_button: confirm_button.text = "Confirm"
		
	name_input_field.max_length = current_max_length
	keyboard_grid.columns = 20
	
	if first_button:
		first_button.grab_focus.call_deferred()

func _setup_software_keyboard(keys_array: Array) -> void:
	first_button = null
	for child in keyboard_grid.get_children():
		child.queue_free()
		
	var is_en = (TranslationServer.get_locale().left(2) == "en")
		
	for i in range(keys_array.size()):
		var key = keys_array[i]
		
		if key == " " and is_en:
			var spacer = Control.new()
			keyboard_grid.add_child(spacer)
			continue
			
		var btn = Button.new()
		btn.text = key
		btn.custom_minimum_size = Vector2(40, 40) 
		btn.focus_mode = Control.FOCUS_ALL 
		
		btn.pressed.connect(_on_key_pressed.bind(key))
		keyboard_grid.add_child(btn)
		
		if first_button == null:
			first_button = btn

func _on_key_pressed(char_text: String) -> void:
	if name_input_field.text.length() < current_max_length:
		name_input_field.text += char_text

func _on_backspace_button_pressed() -> void:
	if name_input_field.text.length() > 0:
		name_input_field.text = name_input_field.text.left(name_input_field.text.length() - 1)

func _on_confirm_button_pressed() -> void:
	var cleaned_name = name_input_field.text.strip_edges()
	if cleaned_name.is_empty():
		return
		
	final_character_name = cleaned_name
	_proceed_to_race_selection()

func _proceed_to_race_selection() -> void:
	$NameLayout.hide()
	if status_layout: status_layout.hide()
	if class_layout: class_layout.hide()
	if appearance_layout: appearance_layout.hide()
	if confirmation_layout: confirmation_layout.hide()
	if message_layout: message_layout.hide()
	race_layout.show()
	
	var current_locale = TranslationServer.get_locale().left(2)
	if current_locale == "ja":
		race_title_label.text = "種族を選択してください"
	else:
		race_title_label.text = "Select a race."
		
	for child in race_button_container.get_children():
		child.queue_free()
		
	# CSVのID(RACE_HUMAN 等)をそのまま管理
	var available_races = ["RACE_HUMAN"]
	if is_advanced_races_unlocked:
		available_races.append("RACE_ELF")
		available_races.append("RACE_DWARF")
		
	var r_buttons = []
	for race_id in available_races:
		var btn = Button.new()
		# tr() を使ってCSVから直接文言を引く("人間"、"Human" などが自動で入る)
		btn.text = tr(race_id)
			
		btn.focus_mode = Control.FOCUS_ALL
		btn.pressed.connect(_on_race_selected.bind(race_id))
		race_button_container.add_child(btn)
		r_buttons.append(btn)
		
	for i in range(r_buttons.size()):
		var prev = r_buttons[(i - 1 + r_buttons.size()) % r_buttons.size()]
		var next = r_buttons[(i + 1) % r_buttons.size()]
		r_buttons[i].focus_neighbor_top = prev.get_path()
		r_buttons[i].focus_neighbor_bottom = next.get_path()
		
	if r_buttons.size() > 0:
		r_buttons[0].grab_focus.call_deferred()

func _on_race_selected(race_id: String) -> void:
	final_character_race = race_id
	_proceed_to_status_allocation()

func _back_to_name_input() -> void:
	race_layout.hide()
	$NameLayout.show()
	if first_button:
		first_button.grab_focus.call_deferred()

func _cancel_and_return_to_guild() -> void:
	hide()
	creation_canceled.emit()

func _proceed_to_status_allocation() -> void:
	race_layout.hide()
	if class_layout: class_layout.hide()
	if appearance_layout: appearance_layout.hide()
	if confirmation_layout: confirmation_layout.hide()
	if message_layout: message_layout.hide()
	status_layout.show()
	
	var current_locale = TranslationServer.get_locale().left(2)
	if current_locale == "ja":
		status_instruction_label.text = "ボーナスポイントを振ってください"
		status_back_button.text = "ひとつ前に戻る"
		status_next_button.text = "次に進む"
	else:
		status_instruction_label.text = "Please allocate your bonus points."
		status_back_button.text = "Back"
		status_next_button.text = "Next"

	var base = RACE_BASE_STATS[final_character_race]
	current_base_stats = {
		"LV": base["LV"],
		"STR": base["STR"],
		"DEX": base["DEX"],
		"VIT": base["VIT"],
		"AGI": base["AGI"],
		"INT": base["INT"],
		"MND": base["MND"],
		"LUC": base["LUC"],
		"HP": randi_range(base["HP_MIN"], base["HP_MAX"]),
		"MP": randi_range(base["MP_MIN"], base["MP_MAX"])
	}
	
	for stat in ALLOCATABLE_STATS:
		allocated_bonus_stats[stat] = 0
		
	initial_bonus_points = randi_range(5, 12)
	remaining_bonus_points = initial_bonus_points
	
	_build_status_grid()
	_update_summary_ui()

func _build_status_grid() -> void:
	for child in status_grid_container.get_children():
		child.queue_free()
		
	stat_labels_dictionary.clear()
	status_grid_container.columns = 4
	
	# 静的ステータスの文言(Lv, HP, MP)もCSVマッピングから取得
	_add_static_stat_row(tr(STAT_ID_MAP["LV"]), current_base_stats["LV"])
	_add_static_stat_row(tr(STAT_ID_MAP["HP"]), current_base_stats["HP"])
	_add_static_stat_row(tr(STAT_ID_MAP["MP"]), current_base_stats["MP"])
	
	var focusable_buttons = []
	for stat_key in ALLOCATABLE_STATS:
		var stat_id = STAT_ID_MAP[stat_key]
		var display_name = tr(stat_id) # CSVから「力」「STR」などを取得
				
		var name_lbl = Label.new()
		name_lbl.text = display_name
		status_grid_container.add_child(name_lbl)
		
		var base_lbl = Label.new()
		base_lbl.text = str(current_base_stats[stat_key])
		status_grid_container.add_child(base_lbl)
		
		var bonus_lbl = Label.new()
		bonus_lbl.text = " +0"
		status_grid_container.add_child(bonus_lbl)
		stat_labels_dictionary[stat_key] = bonus_lbl
		
		var opt_btn = Button.new()
		opt_btn.text = " ◀ ▶ "
		opt_btn.focus_mode = Control.FOCUS_ALL
		opt_btn.set_meta("stat_key", stat_key)
		
		opt_btn.pressed.connect(_on_stat_button_pressed.bind(stat_key))
		opt_btn.focus_entered.connect(func(): current_focused_stat_button = opt_btn)
		
		status_grid_container.add_child(opt_btn)
		focusable_buttons.append(opt_btn)
		
	focusable_buttons.append(status_back_button)
	focusable_buttons.append(status_next_button)
	
	for i in range(focusable_buttons.size()):
		var prev = focusable_buttons[(i - 1 + focusable_buttons.size()) % focusable_buttons.size()]
		var next = focusable_buttons[(i + 1) % focusable_buttons.size()]
		focusable_buttons[i].focus_neighbor_top = prev.get_path()
		focusable_buttons[i].focus_neighbor_bottom = next.get_path()
		
	if focusable_buttons.size() > 0:
		focusable_buttons[0].grab_focus.call_deferred()

func _add_static_stat_row(stat_title: String, val: int) -> void:
	var name_lbl = Label.new()
	name_lbl.text = stat_title
	var val_lbl = Label.new()
	val_lbl.text = str(val)
	status_grid_container.add_child(name_lbl)
	status_grid_container.add_child(val_lbl)
	status_grid_container.add_child(Label.new())
	status_grid_container.add_child(Label.new())

func _update_summary_ui() -> void:
	var current_locale = TranslationServer.get_locale().left(2)
	var display_race = tr(final_character_race) # CSVから取得
			
	if current_locale == "ja":
		character_summary_label.text = "名前: %s   種族: %s   残りボーナスポイント: %d" % [final_character_name, display_race, remaining_bonus_points]
	else:
		character_summary_label.text = "Name: %s   Race: %s   Bonus Points: %d" % [final_character_name, display_race, remaining_bonus_points]

func _on_stat_button_pressed(stat_key: String) -> void:
	if remaining_bonus_points > 0:
		_modify_stat_point(stat_key, 1)

func _modify_stat_point(stat_key: String, amount: int) -> void:
	allocated_bonus_stats[stat_key] += amount
	remaining_bonus_points -= amount
	
	var bonus_label = stat_labels_dictionary[stat_key] as Label
	if allocated_bonus_stats[stat_key] > 0:
		bonus_label.text = " +%d" % allocated_bonus_stats[stat_key]
	else:
		bonus_label.text = " +0"
		
	_update_summary_ui()

func _back_to_race_selection() -> void:
	status_layout.hide()
	_proceed_to_race_selection()

func _on_status_back_pressed() -> void:
	_back_to_race_selection()

func _on_status_next_pressed() -> void:
	_proceed_to_class_selection()

func _proceed_to_class_selection() -> void:
	status_layout.hide()
	if appearance_layout: appearance_layout.hide()
	class_layout.show()
	
	var current_locale = TranslationServer.get_locale().left(2)
	if current_locale == "ja":
		class_title_label.text = "職業を選んでください"
	else:
		class_title_label.text = "Select a class."
		
	_build_class_status_preview()
	_build_class_columns()

func _build_class_status_preview() -> void:
	for child in class_status_preview.get_children():
		child.queue_free()
		
	class_status_preview.columns = 2
	var preview_keys = ["LV", "HP", "MP", "STR", "DEX", "VIT", "AGI", "INT", "MND", "LUC"]
	
	for key in preview_keys:
		var name_lbl = Label.new()
		var total_lbl = Label.new()
		
		# STAT_ID_MAPから翻訳IDを引っ張って tr()
		var stat_id = STAT_ID_MAP[key]
		name_lbl.text = tr(stat_id)
		
		var total_val = current_base_stats[key]
		if key in ALLOCATABLE_STATS:
			total_val += allocated_bonus_stats[key]
			
		total_lbl.text = " %d" % total_val
		
		class_status_preview.add_child(name_lbl)
		class_status_preview.add_child(total_lbl)

func _build_class_columns() -> void:
	for child in basic_class_column.get_children(): child.queue_free()
	for child in advanced_class_column.get_children(): child.queue_free()
		
	# CSVにある接頭辞付きの正規IDで配列を定義
	var basic_ids = [
		"CLASS_FIGHTER", "CLASS_ROGUE", 
		"CLASS_CLERIC", "CLASS_WIZARD"
	]
	var adv_ids = []
	if is_class_group_1_unlocked: adv_ids.append_array(["CLASS_RANGER", "CLASS_KNIGHT"])
	if is_class_group_2_unlocked: adv_ids.append_array(["CLASS_SAMURAI", "CLASS_WARLOCK"])
	if is_class_group_3_unlocked: adv_ids.append_array(["CLASS_NINJA", "CLASS_SAGE"])
	
	var basic_buttons = []
	var adv_buttons = []
	
	for cid in basic_ids:
		var btn = _create_class_button(cid)
		basic_class_column.add_child(btn)
		basic_buttons.append(btn)
		
	for cid in adv_ids:
		var btn = _create_class_button(cid)
		advanced_class_column.add_child(btn)
		adv_buttons.append(btn)
		
	for i in range(basic_buttons.size()):
		var prev = basic_buttons[(i - 1 + basic_buttons.size()) % basic_buttons.size()]
		var next = basic_buttons[(i + 1) % basic_buttons.size()]
		basic_buttons[i].focus_neighbor_top = prev.get_path()
		basic_buttons[i].focus_neighbor_bottom = next.get_path()
		if adv_buttons.size() > 0:
			var target_index = min(i, adv_buttons.size() - 1)
			basic_buttons[i].focus_neighbor_right = adv_buttons[target_index].get_path()
			
	for i in range(adv_buttons.size()):
		var prev = adv_buttons[(i - 1 + adv_buttons.size()) % adv_buttons.size()]
		var next = adv_buttons[(i + 1) % adv_buttons.size()]
		adv_buttons[i].focus_neighbor_top = prev.get_path()
		adv_buttons[i].focus_neighbor_bottom = next.get_path()
		var target_index = min(i, basic_buttons.size() - 1)
		adv_buttons[i].focus_neighbor_left = basic_buttons[target_index].get_path()
		
	if basic_buttons.size() > 0:
		basic_buttons[0].grab_focus.call_deferred()

func _create_class_button(class_id: String) -> Button:
	var btn = Button.new()
	btn.text = tr(class_id) # CSVから直接職業名を取得
		
	btn.focus_mode = Control.FOCUS_ALL
	btn.pressed.connect(_on_class_selected.bind(class_id))
	return btn

func _on_class_selected(class_id: String) -> void:
	final_character_class = class_id
	_proceed_to_appearance_selection()

func _back_to_status_allocation() -> void:
	class_layout.hide()
	_proceed_to_status_allocation()

func _proceed_to_appearance_selection() -> void:
	class_layout.hide()
	if confirmation_layout: confirmation_layout.hide()
	appearance_layout.show()
	
	var current_locale = TranslationServer.get_locale().left(2)
	
	if current_locale == "ja":
		appearance_title_label.text = "見た目を選んでください"
		appearance_random_button.text = "ランダムで決める"
		appearance_back_button.text = "職業選択に戻る"
		appearance_next_button.text = "確認画面に進む"
	else:
		appearance_title_label.text = "Select your appearance."
		appearance_random_button.text = "Choose Randomly"
		appearance_back_button.text = "Back to Class"
		appearance_next_button.text = "Proceed"

	_update_appearance_summary_text(current_locale)
	_build_appearance_status_preview()
	_scan_face_images_folder()
	_build_appearance_wheel()
	_setup_appearance_focus_neighbors()

func _update_appearance_summary_text(current_locale: String) -> void:
	var display_race = tr(final_character_race)
	var display_class = tr(final_character_class)
			
	if current_locale == "ja":
		appearance_summary_label.text = "名前: %s   種族: %s   職業: %s" % [final_character_name, display_race, display_class]
	else:
		appearance_summary_label.text = "Name: %s   Race: %s   Class: %s" % [final_character_name, display_race, display_class]
	
func _build_appearance_status_preview() -> void:
	for child in appearance_status_preview.get_children():
		child.queue_free()
		
	appearance_status_preview.columns = 2
	var preview_keys = ["LV", "HP", "MP", "STR", "DEX", "VIT", "AGI", "INT", "MND", "LUC"]
	
	for key in preview_keys:
		var name_lbl = Label.new()
		var total_lbl = Label.new()
		
		# 前半で定義した STAT_ID_MAP からCSVキーを取得し、tr() で翻訳
		var stat_id = STAT_ID_MAP[key]
		name_lbl.text = tr(stat_id)
		
		var total_val = current_base_stats[key]
		if key in ALLOCATABLE_STATS:
			total_val += allocated_bonus_stats[key]
		total_lbl.text = " %d" % total_val
		
		appearance_status_preview.add_child(name_lbl)
		appearance_status_preview.add_child(total_lbl)

func _scan_face_images_folder() -> void:
	face_image_paths.clear()
	var folder_path = "res://image/face/"
	if DirAccess.dir_exists_absolute(folder_path):
		var dir = DirAccess.open(folder_path)
		if dir:
			dir.list_dir_begin()
			var file_name = dir.get_next()
			while file_name != "":
				if not dir.current_is_dir() and (file_name.ends_with(".png") or file_name.ends_with(".PNG")):
					face_image_paths.append(folder_path + file_name)
				file_name = dir.get_next()
			dir.list_dir_end()
	face_image_paths.sort()

func _build_appearance_wheel() -> void:
	for child in appearance_wheel_control.get_children():
		child.queue_free()
	face_sprites.clear()
	
	if face_image_paths.is_empty():
		var error_label = Label.new()
		error_label.text = "No PNG images found in res://image/face/"
		appearance_wheel_control.add_child(error_label)
		return
		
	for i in range(face_image_paths.size()):
		var texture = load(face_image_paths[i])
		var tex_rect = TextureRect.new()
		tex_rect.texture = texture
		tex_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
		tex_rect.custom_minimum_size = Vector2(120, 120)
		tex_rect.size = Vector2(120, 120)
		tex_rect.pivot_offset = tex_rect.size / 2
		appearance_wheel_control.add_child(tex_rect)
		face_sprites.append(tex_rect)
		
	current_image_index = 0
	target_wheel_angle = 0.0
	current_wheel_angle = 0.0
	_update_wheel_positions()

func _process(_delta: float) -> void:
	if appearance_layout.is_visible_in_tree() and face_image_paths.size() > 0:
		_update_wheel_positions()

func _update_wheel_positions() -> void:
	var total_count = face_sprites.size()
	if total_count == 0: return
	var radius_x = 160.0
	var radius_y = 60.0 
	var center = appearance_wheel_control.size / 2
	
	for i in range(total_count):
		var angle_offset = (i * 2.0 * PI / total_count)
		var sprite_angle = current_wheel_angle + angle_offset
		var cos_val = cos(sprite_angle + PI/2)
		var sin_val = sin(sprite_angle + PI/2)
		
		var pos_x = center.x + cos_val * radius_x
		var pos_y = center.y + sin_val * radius_y
		var sprite = face_sprites[i]
		sprite.position = Vector2(pos_x - sprite.size.x / 2, pos_y - sprite.size.y / 2)
		
		var depth_ratio = (sin_val + 1.0) / 2.0
		var current_size = lerp(100.0, 180.0, depth_ratio)
		sprite.size = Vector2(current_size, current_size)
		sprite.pivot_offset = sprite.size / 2
		
		var brightness = lerp(0.4, 1.0, depth_ratio)
		sprite.modulate = Color(brightness, brightness, brightness, 1.0)
		sprite.z_index = int(depth_ratio * 100)

func _rotate_wheel(direction: int) -> void:
	var total_count = face_image_paths.size()
	if total_count == 0: return
	
	current_image_index = (current_image_index + direction + total_count) % total_count
	var step_angle = (2.0 * PI / total_count)
	var ideal_target_angle = -current_image_index * step_angle
	
	var angle_diff = ideal_target_angle - target_wheel_angle
	angle_diff = fposmod(angle_diff + PI, 2.0 * PI) - PI
	target_wheel_angle += angle_diff
	
	if wheel_tween: wheel_tween.kill()
	wheel_tween = create_tween()
	wheel_tween.set_ease(Tween.EASE_OUT)
	wheel_tween.set_trans(Tween.TRANS_QUAD)
	wheel_tween.tween_property(self, "current_wheel_angle", target_wheel_angle, 0.2)

func _rotate_to_specific_index(idx: int) -> void:
	if face_image_paths.is_empty(): return
	current_image_index = idx
	var step_angle = (2.0 * PI / face_image_paths.size())
	var ideal_target_angle = -current_image_index * step_angle
	var angle_diff = ideal_target_angle - target_wheel_angle
	angle_diff = fposmod(angle_diff + PI, 2.0 * PI) - PI
	target_wheel_angle += angle_diff
	
	if wheel_tween: wheel_tween.kill()
	wheel_tween = create_tween()
	wheel_tween.set_ease(Tween.EASE_OUT)
	wheel_tween.set_trans(Tween.TRANS_CUBIC)
	wheel_tween.tween_property(self, "current_wheel_angle", target_wheel_angle, 0.4)

func _setup_appearance_focus_neighbors() -> void:
	var wheel_focus_node = appearance_wheel_control
	if appearance_wheel_control.focus_mode == Control.FOCUS_NONE:
		appearance_wheel_control.focus_mode = Control.FOCUS_ALL
		
	if not wheel_focus_node.focus_entered.is_connected(_on_wheel_focus_entered):
		wheel_focus_node.focus_entered.connect(_on_wheel_focus_entered)
	if not wheel_focus_node.focus_exited.is_connected(_on_wheel_focus_exited):
		wheel_focus_node.focus_exited.connect(_on_wheel_focus_exited)
		
	wheel_focus_node.focus_neighbor_left = wheel_focus_node.get_path()
	wheel_focus_node.focus_neighbor_right = wheel_focus_node.get_path()
	wheel_focus_node.focus_neighbor_bottom = appearance_random_button.get_path()
	
	appearance_random_button.focus_neighbor_top = wheel_focus_node.get_path()
	appearance_random_button.focus_neighbor_left = appearance_next_button.get_path()
	appearance_random_button.focus_neighbor_right = appearance_back_button.get_path()
	
	appearance_back_button.focus_neighbor_top = wheel_focus_node.get_path()
	appearance_back_button.focus_neighbor_left = appearance_random_button.get_path()
	appearance_back_button.focus_neighbor_right = appearance_next_button.get_path()
	
	appearance_next_button.focus_neighbor_top = wheel_focus_node.get_path()
	appearance_next_button.focus_neighbor_left = appearance_back_button.get_path()
	appearance_next_button.focus_neighbor_right = appearance_random_button.get_path()
	
	wheel_focus_node.grab_focus.call_deferred()

func _on_wheel_focus_entered() -> void: is_wheel_focused = true
func _on_wheel_focus_exited() -> void: is_wheel_focused = false

func _on_appearance_random_pressed() -> void:
	if face_image_paths.is_empty(): return
	_rotate_to_specific_index(randi() % face_image_paths.size())

func _on_appearance_back_pressed() -> void:
	_back_to_class_selection()

func _back_to_class_selection() -> void:
	if wheel_tween: wheel_tween.kill()
	appearance_layout.hide()
	_proceed_to_class_selection()

func _on_appearance_next_pressed() -> void:
	if face_image_paths.is_empty(): return
	final_character_image_path = face_image_paths[current_image_index]
	_proceed_to_confirmation_screen()

# 確認画面
func _proceed_to_confirmation_screen() -> void:
	appearance_layout.hide()
	confirmation_layout.show()
	
	var current_locale = TranslationServer.get_locale().left(2)
	
	# 1. 見出し・ボタンのローカライズ設定
	if current_locale == "ja":
		confirm_title_label.text = "このキャラクターを作成しますか?"
		confirm_back_button.text = "見た目選択に戻る"
		confirm_create_button.text = "作成する"
	else:
		confirm_title_label.text = "Create this character?"
		confirm_back_button.text = "Back to Appearance"
		confirm_create_button.text = "Create"
		
	# 2. サマリー表示のコピー(名前・種族・職業)
	confirm_summary_label.text = appearance_summary_label.text
	
	# 3. 左側:ステータス合計値表示の構築
	_build_confirm_status_preview()
	
	# 4. 右側:選択した見た目画像を表示(★サイズを 256x256 に統一)
	if FileAccess.file_exists(final_character_image_path):
		confirm_face_preview.texture = load(final_character_image_path)
		confirm_face_preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
		confirm_face_preview.custom_minimum_size = Vector2(256, 256)
		confirm_face_preview.size = Vector2(256, 256)
	
	# 5. フォーカス移動ルート設定
	confirm_back_button.focus_neighbor_left = confirm_create_button.get_path()
	confirm_back_button.focus_neighbor_right = confirm_create_button.get_path()
	confirm_create_button.focus_neighbor_left = confirm_back_button.get_path()
	confirm_create_button.focus_neighbor_right = confirm_back_button.get_path()
	
	# 最初から「作成する」ボタンにフォーカスを当てる
	confirm_create_button.grab_focus.call_deferred()

# 左側:ステータスの表示(合計値)
func _build_confirm_status_preview() -> void:
	for child in confirm_status_preview.get_children():
		child.queue_free()
		
	confirm_status_preview.columns = 2
	var preview_keys = ["LV", "HP", "MP", "STR", "DEX", "VIT", "AGI", "INT", "MND", "LUC"]
	
	for key in preview_keys:
		var name_lbl = Label.new()
		var total_lbl = Label.new()
		
		# STAT_ID_MAP からCSVキーを取得し、tr() で翻訳
		var stat_id = STAT_ID_MAP[key]
		name_lbl.text = tr(stat_id)
		
		var total_val = current_base_stats[key]
		if key in ALLOCATABLE_STATS:
			total_val += allocated_bonus_stats[key]
		total_lbl.text = " %d" % total_val
		
		confirm_status_preview.add_child(name_lbl)
		confirm_status_preview.add_child(total_lbl)

# 「見た目選択に戻る」ボタン処理
func _on_confirm_back_pressed() -> void:
	_back_to_appearance_selection()

func _back_to_appearance_selection() -> void:
	confirmation_layout.hide()
	_proceed_to_appearance_selection()

func _on_confirm_create_pressed() -> void:
	confirmation_layout.hide()
	message_layout.show()
	
	# 1. 最終ステータス(ベース値 + ボーナス値)の合算辞書を作成
	var final_stats: Dictionary = {}
	for key in ALLOCATABLE_STATS:
		final_stats[key] = current_base_stats[key] + allocated_bonus_stats[key]
	
	# 2. 逆変換マッパーを廃止し、CSVのID文字列をそのまま使用します
	var class_id: String = final_character_class
	var race_id: String = final_character_race
	
	# 3. キャラクタークラス(器)のインスタンスを生成してデータを流し込む
	var new_adventurer = CharaData.new(
		final_character_name,
		race_id,
		class_id,
		final_character_image_path,
		final_stats,
		current_base_stats["HP"], 
		current_base_stats["MP"]  
	)
	
	# 4. ギルドマネージャーの管理下にデータを追加する
	GuildManager.register_new_adventurer(new_adventurer)
	
	# デバッグ確認用
	print("--- 新しい冒険者がデータクラスに格納されました ---")
	print("名前: ", new_adventurer.chara_name)
	print("種族(内部ID): ", new_adventurer.race, " (表示名: ", new_adventurer.get_display_race(), ")")
	print("職業(内部ID): ", new_adventurer.chara_class, " (表示名: ", new_adventurer.get_display_class(), ")")
	print("画像パス: ", new_adventurer.face_image_path)
	print("STR: ", new_adventurer.str_val, " / AGI: ", new_adventurer.agi_val, " / LUC: ", new_adventurer.luc_val)
	print("暫定近接攻撃力: ", new_adventurer.get_melee_attack())
	print("--------------------------------------------------")
	
	# 5. 登録完了メッセージの表示
	var current_locale = TranslationServer.get_locale().left(2)
	if current_locale == "ja":
		message_text_label.text = "名前【%s】がギルドに登録されました。" % final_character_name
	else:
		message_text_label.text = "Name [%s] has been registered to the guild." % final_character_name
			
	message_layout.focus_mode = Control.FOCUS_ALL
	message_layout.grab_focus.call_deferred()

# メッセージ確認後、ギルドメニューへ戻る
func _return_to_guild_menu() -> void:
	message_layout.hide()
	hide() 
	creation_canceled.emit() 

まず KeyboardGrid の Columns 数を20にしてください。これで名前入力キーボードが20列になります。そのあと、キャラクター作成用スクリプト (character_creation.gd) の上記のように変更してください。これで、名前がカタカナでも入力することができ、またギルド管理マネージャーにキャラクターが登録されます。

キャラクターデータクラスのローカライゼーション

ローカライゼーション (localization) とは、英語/日本語表記の切り替えなどに対応するものです。冒険者ギルドの機能を実装する上で、キャラクターデータもゲーム内言語の切り替え対応をしていきましょう。

言語表記切り替え用CSVファイル追記

上記のように CSV形式で ID, 英語, 日本語表記を追加してください。このゲームでは localization.csv というCSVファイルに追記しています。追記後はCSVファイルをインポートするのを忘れないでください。

この対応をしないと、冒険者ギルドの機能の表記がうまくいきません。ここから先に進む前にローカライズしておきましょう。

キャラクターデータ用コード修正

class_name CharaData
extends RefCounted

# ==============================================================================
# 1. ステータスデータ (内部管理はID、表示時に自動翻訳)
# ==============================================================================
# ---- プロフィール ----
var chara_name: String = ""
var chara_class: String = "" # 内部ID: "CLASS_FIGHTER", "CLASS_WIZARD" などのCSVキー
var race: String = ""        # 内部ID: "RACE_HUMAN", "RACE_ELF" などのCSVキー
var face_image_path: String = ""

# ---- 基本成長ステータス ----
var level: int = 1
var current_hp: int = 0
var max_hp: int = 0
var current_mp: int = 0
var max_mp: int = 0
var exp_points: int = 0

# ---- 能力値 (レベルアップ等で成長する値) ----
var str_val: int = 0 # 力
var dex_val: int = 0 # 器用さ
var vit_val: int = 0 # 体力
var agi_val: int = 0 # 素早さ
var int_val: int = 0 # 知恵
var mnd_val: int = 0 # 信仰心
var luc_val: int = 0 # 運の良さ

# ---- 戦闘パラメータ ----
var atk: int = 0 
var def: int = 0 
var condition_id: String = "Good" # 内部ID: "Good"(良好), "Poison"(毒), "Paralysis"(マヒ)

# ---- クラスポイント (CP) 保持用辞書 ----
# ※ここは内部計算のキーとして使用するため、念のため従来の英語名と新しいCSVキーの
# どちらが来ても安全なように初期化時に両方カバー、または後続のメソッドで安全に処理します。
var class_points: Dictionary = {
	"CLASS_FIGHTER": 0, "CLASS_ROGUE": 0, "CLASS_CLERIC": 0, "CLASS_WIZARD": 0,
	"CLASS_RANGER": 0, "CLASS_KNIGHT": 0,
	"CLASS_SAMURAI": 0, "CLASS_WARLOCK": 0, 
	"CLASS_NINJA": 0, "CLASS_SAGE": 0,
	"Fighter": 0, "Rogue": 0, "Cleric": 0, "Wizard": 0,
	"Ranger": 0, "Knight": 0,
	"Samurai": 0, "Warlock": 0,
	"Ninja": 0, "Sage": 0
}

# ---- スキル関連 ----
var class_skills: Array[String] = []       
var equipped_skill: String = ""            
var additional_skills: Array[String] = []  

# ---- 装備スロット ----
var equipment: Dictionary = {
	"main_hand": null, "off_hand": null, "head": null, "armor": null,
	"feet": null, "accessory_1": null, "accessory_2": null
}

# ==============================================================================
# 2. 初期化 (コンストラクタ)
# ==============================================================================
func _init(n: String, r: String, c: String, img: String, stats: Dictionary, hp: int, mp: int) -> void:
	chara_name = n
	race = r
	chara_class = c
	face_image_path = img
	
	level = 1
	exp_points = 0
	condition_id = "Good"
	
	str_val = stats.get("STR", 0)
	dex_val = stats.get("DEX", 0)
	vit_val = stats.get("VIT", 0)
	agi_val = stats.get("AGI", 0)
	int_val = stats.get("INT", 0)
	mnd_val = stats.get("MND", 0)
	luc_val = stats.get("LUC", 0)
	
	max_hp = hp
	current_hp = hp
	max_mp = mp
	current_mp = mp
	
	update_display_parameters()

func update_display_parameters() -> void:
	def = get_defense()
	atk = get_melee_attack()

# ==============================================================================
# 日英言語対応(ローカライズ)用表示メソッド群
# ==============================================================================

# 現在のゲーム言語("ja" かそれ以外か)を取得するヘルパー
func _is_japanese() -> bool:
	return TranslationServer.get_locale().left(2) == "ja"

# 職業の表示名を取得。【修正】CSVキーを直接 tr() で翻訳して返します
func get_display_class() -> String:
	return tr(chara_class)

# 種族の表示名を取得。【修正】CSVキーを直接 tr() で翻訳して返します
func get_display_race() -> String:
	return tr(race)

# 状態異常の表示名を取得(ここはテキストデータが残っているため元のロジックを維持)
func get_display_condition() -> String:
	if _is_japanese():
		match condition_id:
			"Good": return "良好"
			"Poison": return "毒"
			"Paralysis": return "マヒ"
			"Dead": return "死亡"
	else:
		match condition_id:
			"Good": return "Good"
			"Poison": return "Poison"
			"Paralysis": return "Paralysis"
			"Dead": return "Dead"
	return condition_id

# ==============================================================================
# 3. 非表示ステータス 計算用メソッド群
# ==============================================================================
func get_melee_attack() -> int:
	var weapon_atk = 0
	return int(str_val / 2) + 1 + weapon_atk

func get_ranged_attack() -> int:
	var weapon_atk = 0
	return int(dex_val / 2) + 1 + weapon_atk

func is_back_row_attack_allowed() -> bool:
	return false

func is_wielding_two_handed() -> bool:
	return false

func get_accuracy() -> int:
	return 100

func get_defense() -> int:
	return 0

func get_evasion() -> int:
	var score = int(dex_val / 2) + (agi_val * 2) + int(luc_val / 2)
	if score <= 100: return 0
	return int(score / 100)

func get_parry_rate() -> int: return 0
func get_healing_power() -> int: return int(mnd_val / 2)
func get_magic_attack() -> int: return int(int_val / 2)

func get_magic_defense() -> int:
	return int(vit_val / 4) + int(int_val / 2) + int(mnd_val / 2)

func get_magic_evasion() -> int: return 0
func get_status_resistance() -> int: return int(ceil(float(luc_val) / 10.0))
func get_critical_rate() -> int: return 0
func get_critical_damage() -> int:return 0
func get_fire_resistance() -> int: return 0
func get_ice_resistance() -> int: return 0
func get_lightning_resistance() -> int: return 0
func get_dark_resistance() -> int: return 0
func get_light_resistance() -> int: return 0

# ==============================================================================
# 4. ユーティリティメソッド
# ==============================================================================
func get_current_class_points() -> int:
	if class_points.has(chara_class): return class_points[chara_class]
	return 0

func add_class_points(class_id: String, amount: int) -> void:
	if class_points.has(class_id):
		class_points[class_id] = clampi(class_points[class_id] + amount, 0, 999)

キャラクター作成のリファクタリングに合わせて、キャラクターデータクラス (chara_data.gd) を上記のように修正してください。見た目上は何も変わりませんが英語 / 日本語対応しています。

街処理用スクリプト (リファクタリング)

今までは街処理用スクリプト (town.gd) が「冒険者ギルド」のメニュー処理を代行していました。これをギルド管理マネージャーに移行します。

街処理用コード修正

# ==============================================================================
# town.gd : 拠点(街)のUI操作と各場所のメニュー管理(ギルド移譲版・エラー修正)
# ==============================================================================
class_name TownSystem
extends RefCounted

signal dungeon_entered

var town_graphic: TextureRect
var town_menu: Control
var dungeon_button: Button
var guild_button: Button
var creation_node: Control
var tavern_button: Button
var shop_button: Button
var temple_button: Button
var library_button: Button

# 他の施設のキーは維持(ギルドのみ削除)
const TAVERN_MENU_KEYS = ["TAVERN_MENU_ACCEPT", "TAVERN_MENU_REPORT", "TAVERN_MENU_TALK", "TAVERN_MENU_BACK"]
const SHOP_MENU_KEYS = ["SHOP_MENU_WEAPON", "SHOP_MENU_ARMOR", "SHOP_MENU_ITEM", "SHOP_MENU_SELL", "SHOP_MENU_BACK"]
const TEMPLE_MENU_KEYS = ["TEMPLE_MENU_BENEDICT", "TEMPLE_MENU_DEPETRIFY", "TEMPLE_MENU_RESURRECT", "TEMPLE_MENU_BACK"]
const LIBRARY_MENU_KEYS = ["LIBRARY_MENU_RECORDS", "LIBRARY_MENU_STORIES", "LIBRARY_MENU_TUTORIAL", "LIBRARY_MENU_BACK"]

const TOWN_IMAGES = {
	"base": "res://image/town/town_background.png",
	"guild": "res://image/town/guild.png",
	"tavern": "res://image/town/tavern.png",
	"shop": "res://image/town/shop.png",
	"temple": "res://image/town/temple.png",
	"library": "res://image/town/library.png"
}

func _init(main_node: Node) -> void:
	town_graphic = main_node.get_node_or_null("CanvasLayer/TownGraphic")
	town_menu = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu")
	
	dungeon_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/DungeonButton")
	guild_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/GuildButton")
	creation_node = main_node.get_node_or_null("CanvasLayer/CharacterCreation")
	
	tavern_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/TavernButton")
	shop_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/ShopButton")
	temple_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/TempleButton")
	library_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/LibraryButton")
	
	var buttons = [guild_button, tavern_button, shop_button, temple_button, library_button, dungeon_button]
	
	for i in range(buttons.size()):
		var prev = buttons[(i - 1 + buttons.size()) % buttons.size()]
		var next = buttons[(i + 1) % buttons.size()]
		if buttons[i]:
			buttons[i].focus_neighbor_top = prev.get_path()
			buttons[i].focus_neighbor_bottom = next.get_path()
	
	if guild_button: guild_button.text = tr("TOWN_MENU_GUILD")
	if tavern_button: tavern_button.text = tr("TOWN_MENU_TAVERN")
	if shop_button: shop_button.text = tr("TOWN_MENU_SHOP")
	if temple_button: temple_button.text = tr("TOWN_MENU_TEMPLE")
	if library_button: library_button.text = tr("TOWN_MENU_LIBRARY")
	if dungeon_button: dungeon_button.text = tr("TOWN_MENU_DUNGEON")
	
	if dungeon_button: dungeon_button.pressed.connect(_on_dungeon_button_pressed)
	
	# ギルドボタンが押されたら、GuildManagerに街の参照を渡して制御を丸投げする
	if guild_button: guild_button.pressed.connect(func(): GuildManager.open_guild(self))
	
	if tavern_button: tavern_button.pressed.connect(func(): _move_to("tavern"))
	if shop_button: shop_button.pressed.connect(func(): _move_to("shop"))
	if temple_button: temple_button.pressed.connect(func(): _move_to("temple"))
	if library_button: library_button.pressed.connect(func(): _move_to("library"))

func start_town_mode() -> void:
	if town_graphic: town_graphic.show()
	reset_to_main_town() # ★修正: アンダースコアを削除

func end_town_mode() -> void:
	if town_graphic: town_graphic.hide()
	cleanup_dynamic_menus() # ★修正: アンダースコアを削除

# GuildManagerからも呼び出せるように、頭のアンダースコアを外したメイン関数
func reset_to_main_town() -> void:
	_update_graphic("base")
	if town_menu:
		town_menu.show()
		dungeon_button.grab_focus()
	cleanup_dynamic_menus() # ★修正: アンダースコアを削除

func _move_to(location: String) -> void:
	_update_graphic(location)
	if town_menu:
		town_menu.hide()
	
	cleanup_dynamic_menus() # ★修正: アンダースコアを削除
	
	match location:
		"tavern": _show_dynamic_menu(TAVERN_MENU_KEYS)
		"shop": _show_dynamic_menu(SHOP_MENU_KEYS)
		"temple": _show_dynamic_menu(TEMPLE_MENU_KEYS)
		"library": _show_dynamic_menu(LIBRARY_MENU_KEYS)
		_: _show_return_button()

func _show_dynamic_menu(menu_keys: Array) -> void:
	var center_container = CenterContainer.new()
	center_container.name = "DynamicMenu"
	center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_graphic.add_child(center_container)
	
	var menu_panel = VBoxContainer.new()
	center_container.add_child(menu_panel)
	
	var buttons = []
	for key in menu_keys:
		var btn = Button.new()
		btn.text = tr(key)
		btn.pressed.connect(func(): _on_menu_selected(key))
		menu_panel.add_child(btn)
		buttons.append(btn)
	
	for i in range(buttons.size()):
		var prev = buttons[(i - 1 + buttons.size()) % buttons.size()]
		var next = buttons[(i + 1) % buttons.size()]
		buttons[i].focus_neighbor_top = prev.get_path()
		buttons[i].focus_neighbor_bottom = next.get_path()
	
	buttons[0].grab_focus()

func _show_return_button() -> void:
	var btn = Button.new()
	btn.name = "DynamicMenu"
	btn.text = "街へ戻る"
	btn.set_anchors_preset(Control.PRESET_CENTER)
	btn.pressed.connect(reset_to_main_town)
	town_graphic.add_child(btn)
	btn.grab_focus()

func _on_menu_selected(key: String) -> void:
	var back_keys = ["TAVERN_MENU_BACK", "SHOP_MENU_BACK", "TEMPLE_MENU_BACK", "LIBRARY_MENU_BACK"]
	if key in back_keys:
		reset_to_main_town()
	else:
		print(tr(key) + " が選択されました")

# GuildManagerからもメニュー消去できるように頭のアンダースコアを外したメイン関数
func cleanup_dynamic_menus() -> void:
	var menu = town_graphic.get_node_or_null("DynamicMenu")
	if menu:
		menu.queue_free()

func _update_graphic(location: String) -> void:
	if town_graphic and TOWN_IMAGES.has(location):
		town_graphic.texture = load(TOWN_IMAGES[location])

func _on_dungeon_button_pressed() -> void:
	dungeon_entered.emit()

街処理用スクリプト (town.gd) を上記のように修正します。これはギルドメニューのキー配列や、ギルド専用の生成・キャンセル処理を削除し、ギルドボタンが押されたときは GuildManager.open_guild(…) を呼び出すようにするものです。

ギルド管理用コードの修正

extends Node

# ==============================================================================
# 冒険者ギルド 管理マネージャー (Autoload用・UIハンドリング機能付き)
# ==============================================================================

const MAX_MEMBERS: int = 24

var registered_members: Array[CharaData] = []
var party_members: Array[CharaData] = []

# 街システム(town.gd)のインスタンスを一時保持する変数
var town_system: TownSystem = null

# ギルドメニューの項目定義(town.gdから移植)
const GUILD_MENU_KEYS = [
	"GUILD_MENU_JOIN", 
	"GUILD_MENU_REMOVE", 
	"GUILD_MENU_CREATE", 
	"GUILD_MENU_CHANGE_CLASS", 
	"GUILD_MENU_DELETE", 
	"GUILD_MENU_CHANGE_NAME", 
	"GUILD_MENU_CHANGE_APPEARANCE", 
	"GUILD_MENU_BACK"
]

# ==============================================================================
# UI制御・画面遷移ハンドリング
# ==============================================================================

# 街からギルドに入ったときに呼び出される関数
func open_guild(town: TownSystem) -> void:
	town_system = town
	
	# 街側UIの制御
	town_system._update_graphic("guild")
	if town_system.town_menu:
		town_system.town_menu.hide()
	
	town_system.cleanup_dynamic_menus()
	
	# キャラクター作成のキャンセルシグナルをギルドマネージャー側で受けるように再接続
	if town_system.creation_node and not town_system.creation_node.is_connected("creation_canceled", _on_creation_canceled):
		town_system.creation_node.creation_canceled.connect(_on_creation_canceled)
		
	# ギルドメニューの生成
	_show_guild_menu()

# ギルド動的メニューの組み立て
func _show_guild_menu() -> void:
	var center_container = CenterContainer.new()
	center_container.name = "DynamicMenu"
	center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(center_container)
	
	var menu_panel = VBoxContainer.new()
	center_container.add_child(menu_panel)
	
	var buttons = []
	for key in GUILD_MENU_KEYS:
		var btn = Button.new()
		btn.text = tr(key)
		btn.pressed.connect(func(): _on_guild_menu_selected(key))
		menu_panel.add_child(btn)
		buttons.append(btn)
	
	for i in range(buttons.size()):
		var prev = buttons[(i - 1 + buttons.size()) % buttons.size()]
		var next = buttons[(i + 1) % buttons.size()]
		buttons[i].focus_neighbor_top = prev.get_path()
		buttons[i].focus_neighbor_bottom = next.get_path()
	
	buttons[0].grab_focus()

# ギルドメニューが選択されたときの分岐処理
func _on_guild_menu_selected(key: String) -> void:
	if key == "GUILD_MENU_BACK":
		prepare_return_to_town()
		return
		
	print(tr(key) + " が選択されました (GuildManagerで処理中)")
	
	match key:
		"GUILD_MENU_JOIN":
			print("TODO: 冒険者をよびだす UIを開く")
		"GUILD_MENU_REMOVE":
			print("TODO: 冒険者をあずける UIを開く")
		"GUILD_MENU_CREATE":
			_on_create_adventurer_selected()
		"GUILD_MENU_CHANGE_CLASS":
			print("TODO: クラスを変える UIを開く")
		"GUILD_MENU_DELETE":
			print("TODO: 冒険者を削除する UIを開く")
		"GUILD_MENU_CHANGE_NAME":
			print("TODO: 名前を変える UIを開く")
		"GUILD_MENU_CHANGE_APPEARANCE":
			print("TODO: 見た目を変える UIを開く")

# 「冒険者を作成する」が選ばれたときの画面遷移
func _on_create_adventurer_selected() -> void:
	if town_system:
		town_system.town_graphic.hide()
		if town_system.creation_node:
			town_system.creation_node.activate_name_input()

# キャラクターメイク画面からキャンセルされたときの戻り処理
func _on_creation_canceled() -> void:
	if town_system:
		town_system.town_graphic.show()
		
		var dynamic_menu = town_system.town_graphic.get_node_or_null("DynamicMenu")
		if dynamic_menu:
			var menu_panel = dynamic_menu.get_child(0)
			if menu_panel and menu_panel.get_child_count() > 0:
				# 「冒険者を作成する」ボタン(上から3番目、インデックス2)にフォーカスを戻すとより親切です
				var create_btn = menu_panel.get_child(2) as Button
				if create_btn:
					create_btn.grab_focus.call_deferred()


# ==============================================================================
# ギルド機能のコアロジック群 (バックエンド処理)
# ==============================================================================
func call_out_adventurer(chara: CharaData) -> bool:
	if chara in party_members: return false
	if party_members.size() >= 6: return false
	party_members.append(chara)
	return true

func leave_adventurer(chara: CharaData) -> bool:
	if chara in party_members:
		party_members.erase(chara)
		return true
	return false

func register_new_adventurer(new_chara: CharaData) -> bool:
	registered_members.append(new_chara)
	return true

func change_character_class(chara: CharaData, new_class_id: String) -> void:
	chara.chara_class = new_class_id
	chara.update_display_parameters()

func delete_adventurer(chara: CharaData) -> void:
	if chara in party_members: party_members.erase(chara)
	if chara in registered_members: registered_members.erase(chara)

func rename_adventurer(chara: CharaData, new_name: String) -> void:
	chara.chara_name = new_name

func change_adventurer_appearance(chara: CharaData, new_image_path: String) -> void:
	chara.face_image_path = new_image_path

# 8. 街へ戻る
func prepare_return_to_town() -> void:
	if town_system:
		town_system.reset_to_main_town()
		town_system = null # 街に戻ったら参照を外す

ギルド管理用コード (guild_manager.gd) スクリプトを上記のように修正します。これは上記で修正した街処理用スクリプト (town.gd) のインスタンスを受け取り、UIの表示・非表示やボタンの生成、各メニュー選択時の実処理をマネージャー側で一手に引き受けるものです。

パーティ管理マネージャーの実装

ギルドから冒険者を呼び出したりギルドに預けたりするには、パーティが必要です。プレイヤーが操作するパーティを管理するマネージャーを作成します。以下の仕様で進めるとします。

  • このゲームのパーティは最大6名
  • 1~6名でダンジョン攻略をする
  • 最大前衛3名、後衛3名 (前衛4名、後衛2名などは不可)
  • 前衛は射程の短い物理攻撃を受けるが、射程の短い武器で攻撃可能
  • 後衛は射程の短い物理攻撃は受けないが、射程の短い武器では攻撃不可
  • 魔法攻撃は前衛後衛共に攻撃対象になる
  • ブレスなどの攻撃も前衛後衛共に攻撃対象になる
  • 前衛が全員死ぬと、後衛が自動的に前衛に移動する

パーティ管理用スクリプトファイルの作成

extends Node

# ==============================================================================
# パーティ管理マネージャー (Autoload用)
# ==============================================================================

# 固定6枠の配列(null または CharaData)
# 0, 1, 2 = 前衛 (Front) / 3, 4, 5 = 後衛 (Back)
var slots: Array = [null, null, null, null, null, null]

# 現在パーティにいる実人数を返す
func get_member_count() -> int:
	var count = 0
	for member in slots:
		if member != null:
			count += 1
	return count

# パーティが満員(6名)かどうか
func is_full() -> bool:
	return get_member_count() >= 6

# 空いている最初のスロット(前衛➔後衛の順)に自動配置する
# 成功したらスロット番号(0-5)、空きがなければ -1 を返す
func auto_add_member(chara: CharaData) -> int:
	for i in range(6):
		if slots[i] == null:
			slots[i] = chara
			print(chara.chara_name, " がパーティスロット ", i, " に入りました(", "前衛" if i < 3 else "後衛", ")")
			return i
	return -1

# 指定したキャラクターをパーティから外す
func remove_member(chara: CharaData) -> bool:
	for i in range(6):
		if slots[i] == chara:
			slots[i] = null
			print(chara.chara_name, " をパーティから外しました。")
			return true
	return false

# ギルド専用:ギルド内にいる冒険者(=登録済みだがパーティに入っていない人)のリストを取得する
func get_members_in_guild() -> Array[CharaData]:
	var in_guild_list: Array[CharaData] = []
	for chara in GuildManager.registered_members:
		if not chara in slots:
			in_guild_list.append(chara)
	return in_guild_list

新しく party_manager.gd というスクリプトを作成し、上記コードを記述します。そのあと GuildManager 同様にプロジェクト設定の「自動読み込み (Autoload)」にノード名 PartyManager として追加してください。

この party_manager.gd には、仕様にある「前衛3名・後衛3名」の合計6枠を管理できるよう固定長の配列としてスロットを用意します (インデックス0〜2が前衛、3〜5が後衛としています)

冒険者ギルドの実装

ここまでで冒険者ギルドの準備が整いました。ここからは以下をひとつずつ実装していきましょう。

  • 冒険者をよびだす
  • 冒険者をあずける
  • 冒険者を作成する 要調整
  • クラスを変える
  • 冒険者を削除する
  • 名前を変える
  • 見た目を変える
  • 街へ戻る 対応済

「冒険者をよびだす」機能

「冒険者ギルド」にある「冒険者をよびだす」機能を実装します。冒険者を呼び出す条件は以下の通りです。

  • 「冒険者をよびだす」とき、パーティの人数が6名のときは呼び出せません。「パーティメンバーは最大6名です。誰かをギルドにあずけてください」のメッセージが出て、ギルドのメニューに戻る。
  • 「冒険者をよびだす」とき、ギルドに1名もいない場合は呼び出せません。「ギルドに冒険者がいません。冒険者を作成してください」のメッセージが出て、ギルドのメニューに戻る。
  • 「冒険者をよびだす」とき、パーティの人数が0~5名、かつギルドにいる冒険者が1名以上である必要があります。

ノード構成の作成

  • CanvasLayer (CanvasLayer)
    • (他は省略)
    • CharacterCreation (Control)
    • JoinParty (Control) 新規
      • Background (ColorRect) 新規
      • MainVBox (VBoxContainer) 新規
        • MessageLabel (Label) 新規
        • MemberList (VBoxContainer) 新規
          • Row1 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row2 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row3 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row4 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row5 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row6 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
        • NavigationHBox (HBoxContainer) 新規
          • NextButton (Button) 新規
          • BackButton (Button) 新規
JoinParty (Control)新規作成 非表示
Background (ColorRect)画面全体を覆う黒塗りの背景。必ず MainVBox より上に配置すること。(カラー: 黒、レイアウト:Full Rect)
MainVBox (VBoxContainer)「冒険者をよびだす」の画面全般
MessageLabel (Label)画面上部のメッセージ
「呼び出す冒険者を選択してください」
MemberList (VBoxContainer)画面中央部に冒険者を6名表示する
Row1~6 (HBoxContainer)1~6行目
CursorBtn (Button)カーソル。呼び出す冒険者を選ぶもの
FaceImg (TextureRect)冒険者の見た目画像
InfoLabel(Label)冒険者の名前、レベル、職業、最大HP、最大MP
NavigationHBox (HBoxContainer)画面下部のボタン
NextButton (Button)次の一覧に進むボタン。これを選ぶと、ギルドに6名以上いる場合に次の6名が表示されるようになる。ギルドに6名いない場合は何も起こらない。最後のメンバー一覧のときにこれを押すと、最初のギルドメンバー一覧に戻る
BackButton (Button)ギルドに戻る

このノード構成を作成します。JoinParty (Control) の非表示を忘れずに実施してください。

スクリプトファイルの作成

extends Control

# ==============================================================================
# join_party.gd : 「冒険者をよびだす」画面のUI制御(多言語ローカライズ版)
# ==============================================================================

@onready var message_label: Label = $MainVBox/MessageLabel
@onready var member_list_container: VBoxContainer = $MainVBox/MemberList
@onready var next_button: Button = $MainVBox/NavigationHBox/NextButton
@onready var back_button: Button = $MainVBox/NavigationHBox/BackButton

var rows: Array[HBoxContainer] = []
var _guild_members: Array[CharaData] = []
var _current_page: int = 0
var _is_error_mode: bool = false

# --- ローカライズ用テキスト辞書 ---
const TEXT_DATA = {
	"ja": {
		"title": "呼び出す冒険者を選択してください",
		"btn_next": "次の一覧に進む",
		"btn_back": "ギルドに戻る",
		"btn_close": "閉じる",
		"err_full": "パーティメンバーは最大6名です。誰かをギルドにあずけてください。",
		"err_empty": "ギルドに冒険者がいません。冒険者を作成してください。",
		"msg_join": "名前【%s】がパーティに加わった。"
	},
	"en": {
		"title": "Select an adventurer to call out",
		"btn_next": "Next List",
		"btn_back": "Back to Guild",
		"btn_close": "Close",
		"err_full": "The party is full (max 6). Please leave someone at the guild.",
		"err_empty": "No adventurers in the guild. Please create an adventurer.",
		"msg_join": "【%s】 has joined the party."
	}
}

# 現在の言語設定("en" または "ja")を取得する内部関数(デフォルトは "ja")
func _get_current_lang() -> String:
	var locale = TranslationServer.get_locale().left(2).to_lower()
	if locale == "en":
		return "en"
	return "ja"

func _ready() -> void:
	for i in range(1, 7):
		var row_node = member_list_container.get_node_or_null("Row" + str(i))
		if row_node:
			rows.append(row_node as HBoxContainer)
	
	next_button.pressed.connect(_on_next_button_pressed)
	back_button.pressed.connect(_on_back_button_pressed)
	hide()

# 起動関数
func activate(members: Array[CharaData], is_party_full: bool) -> void:
	_guild_members = members
	_current_page = 0
	_is_error_mode = false
	
	var lang = _get_current_lang()
	
	# 画面表示の際、下部ボタンのテキストを現在の言語に更新
	next_button.text = TEXT_DATA[lang]["btn_next"]
	back_button.text = TEXT_DATA[lang]["btn_back"]
	
	show()
	
	if is_party_full:
		_show_error_message(TEXT_DATA[lang]["err_full"])
		return
		
	if _guild_members.size() == 0:
		_show_error_message(TEXT_DATA[lang]["err_empty"])
		return
		
	member_list_container.show()
	next_button.get_parent().show()
	_update_page_display()

# エラーメッセージ画面
func _show_error_message(text_content: String) -> void:
	_is_error_mode = true
	member_list_container.hide()
	next_button.get_parent().hide()
	
	message_label.text = text_content
	release_focus()

# ページ内容の描画
func _update_page_display() -> void:
	var start_idx = _current_page * 6
	var page_members = _guild_members.slice(start_idx, start_idx + 6)
	var focusable_buttons: Array[Button] = []
	
	var lang = _get_current_lang()
	message_label.text = TEXT_DATA[lang]["title"]
	
	for i in range(6):
		var row = rows[i]
		var cursor_btn = row.get_child(0) as Button
		
		for connection in cursor_btn.pressed.get_connections():
			cursor_btn.pressed.disconnect(connection.callable)
		
		if i < page_members.size():
			var m: CharaData = page_members[i]
			row.show()
			
			var face_img = row.get_child(1) as TextureRect
			var info_label = row.get_child(2) as Label
			
			cursor_btn.text = "▶"
			cursor_btn.disabled = false
			
			var callable = func(target_chara: CharaData): _on_row_selected(target_chara)
			cursor_btn.pressed.connect(callable.bind(m))
			
			if FileAccess.file_exists(m.face_image_path):
				face_img.texture = load(m.face_image_path)
			else:
				face_img.texture = null
			face_img.custom_minimum_size = Vector2(64, 64)
				
			info_label.text = "【%s】 LV:%d %s (HP:%d/%d MP:%d/%d)" % [
				m.chara_name, m.level, m.get_display_class(),
				m.current_hp, m.max_hp, m.current_mp, m.max_mp
			]
			
			focusable_buttons.append(cursor_btn)
		else:
			row.hide()
			cursor_btn.disabled = true
	
	focusable_buttons.append(next_button)
	focusable_buttons.append(back_button)
	
	for i in range(focusable_buttons.size()):
		var prev = focusable_buttons[(i - 1 + focusable_buttons.size()) % focusable_buttons.size()]
		var next = focusable_buttons[(i + 1) % focusable_buttons.size()]
		focusable_buttons[i].focus_neighbor_top = prev.get_path()
		focusable_buttons[i].focus_neighbor_bottom = next.get_path()
	
	if focusable_buttons.size() > 2:
		focusable_buttons[0].grab_focus()
	else:
		next_button.grab_focus()

func _on_row_selected(chara: CharaData) -> void:
	var slot_assigned = PartyManager.auto_add_member(chara)
	if slot_assigned != -1:
		var lang = _get_current_lang()
		var formatted_msg = TEXT_DATA[lang]["msg_join"] % chara.chara_name
		_show_error_message(formatted_msg)

func _on_next_button_pressed() -> void:
	if _guild_members.size() <= 6:
		return
	_current_page += 1
	if (_current_page * 6) >= _guild_members.size():
		_current_page = 0
	_update_page_display()

func _on_back_button_pressed() -> void:
	hide()
	GuildManager.return_to_guild_main_menu()

func _unhandled_input(event: InputEvent) -> void:
	if not is_visible_in_tree():
		return
		
	if _is_error_mode:
		if event.is_action_pressed("ui_accept") or event.is_action_pressed("ui_cancel"):
			get_viewport().set_input_as_handled()
			_is_error_mode = false
			_on_back_button_pressed()
		return

	if event.is_action_pressed("ui_cancel"):
		get_viewport().set_input_as_handled()
		_on_back_button_pressed()

JoinParty ノードを選択した状態で、新規スクリプト join_party.gd を作成し、アタッチしてください。コードは上記の通りです。

ギルド管理用コード修正

extends Node

# ==============================================================================
# 冒険者ギルド 管理マネージャー (UI・ロジック完全版)
# ==============================================================================

const SYSTEM_MAX_SLOTS: int = 25
const MAX_CREATE_LIMIT: int = 24

var registered_members: Array[CharaData] = []
var town_system: TownSystem = null
var join_party_node: Control = null

# ギルドメニューの項目定義
const GUILD_MENU_KEYS = [
	"GUILD_MENU_JOIN", 
	"GUILD_MENU_REMOVE", 
	"GUILD_MENU_CREATE", 
	"GUILD_MENU_CHANGE_CLASS", 
	"GUILD_MENU_DELETE", 
	"GUILD_MENU_CHANGE_NAME", 
	"GUILD_MENU_CHANGE_APPEARANCE", 
	"GUILD_MENU_BACK"
]

# 呼び出し画面用のページング管理変数
var _call_list_cache: Array[CharaData] = []
var _current_page: int = 0
var _selected_row_index: int = 0
var _row_buttons: Array[Button] = []

# ==============================================================================
# UI制御・画面遷移ハンドリング
# ==============================================================================

# 街からギルドに入ったときに呼び出される関数
func open_guild(town: TownSystem) -> void:
	town_system = town
	
	# インスタンス化されている JoinParty ノードをパスから取得
	# (CharacterCreationと同じ CanvasLayer の直下にある前提です)
	if town_system.creation_node:
		join_party_node = town_system.creation_node.get_parent().get_node_or_null("JoinParty")
	
	town_system._update_graphic("guild")
	if town_system.town_menu:
		town_system.town_menu.hide()
	
	town_system.cleanup_dynamic_menus()
	
	if town_system.creation_node and not town_system.creation_node.is_connected("creation_canceled", _on_creation_canceled):
		town_system.creation_node.creation_canceled.connect(_on_creation_canceled)
		
	_show_guild_menu()

# ギルドのメインメニューに戻る(JoinParty画面を閉じたときなどに使用)
func return_to_guild_main_menu() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
		_show_guild_menu()

func _show_guild_menu() -> void:
	var center_container = CenterContainer.new()
	center_container.name = "DynamicMenu"
	center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(center_container)
	
	var menu_panel = VBoxContainer.new()
	center_container.add_child(menu_panel)
	
	var buttons = []
	for key in GUILD_MENU_KEYS:
		var btn = Button.new()
		btn.text = tr(key)
		btn.pressed.connect(func(): _on_guild_menu_selected(key))
		menu_panel.add_child(btn)
		buttons.append(btn)
	
	for i in range(buttons.size()):
		var prev = buttons[(i - 1 + buttons.size()) % buttons.size()]
		var next = buttons[(i + 1) % buttons.size()]
		buttons[i].focus_neighbor_top = prev.get_path()
		buttons[i].focus_neighbor_bottom = next.get_path()
	
	buttons[0].grab_focus()

func _on_guild_menu_selected(key: String) -> void:
	if key == "GUILD_MENU_BACK":
		prepare_return_to_town()
		return
		
	match key:
		"GUILD_MENU_JOIN":
			_on_call_out_selected() # 1. 冒険者をよびだす
		"GUILD_MENU_REMOVE":
			print("TODO: 冒険者をあずける UIを開く")
		"GUILD_MENU_CREATE":
			_on_create_adventurer_selected()
		"GUILD_MENU_CHANGE_CLASS":
			print("TODO: クラスを変える UIを開く")
		"GUILD_MENU_DELETE":
			print("TODO: 冒険者を削除する UIを開く")
		"GUILD_MENU_CHANGE_NAME":
			print("TODO: 名前を変える UIを開く")
		"GUILD_MENU_CHANGE_APPEARANCE":
			print("TODO: 見た目を変える UIを開く")

# ----------------------------------------------------------------              
# 1. 冒険者をよびだす(マネージャー側ロジック)
# ----------------------------------------------------------------              
func _on_call_out_selected() -> void:
	# ギルドメニューUIを一時消去
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	# パーティの人数チェックと、ギルド内のメンバーリスト取得
	var is_party_full = PartyManager.is_full()
	var guild_members = PartyManager.get_members_in_guild()
	
	# 状態をそのまま JoinParty ノードに丸投げする
	if join_party_node:
		join_party_node.activate(guild_members, is_party_full)

# メッセージ単体表示用の簡易関数
func _show_simple_message(msg_text: String) -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	var center_container = CenterContainer.new()
	center_container.name = "DynamicMenu"
	center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(center_container)
	
	var vbox = VBoxContainer.new()
	center_container.add_child(vbox)
	
	var label = Label.new()
	label.text = msg_text
	vbox.add_child(label)
	
	var ok_btn = Button.new()
	ok_btn.text = "閉じる"
	ok_btn.pressed.connect(return_to_guild_main_menu)
	vbox.add_child(ok_btn)
	
	ok_btn.grab_focus()

func _open_call_out_ui() -> void:
	town_system.cleanup_dynamic_menus()
	
	# ベースとなる全画面黒背景のコンテナ
	var bg = ColorRect.new()
	bg.name = "DynamicMenu"
	bg.color = Color.BLACK
	bg.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(bg)
	
	# メインレイアウト(縦並び)
	var main_vbox = VBoxContainer.new()
	main_vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
	main_vbox.offset_left = 20
	main_vbox.offset_top = 20
	main_vbox.offset_right = -20
	main_vbox.offset_bottom = -20
	bg.add_child(main_vbox)
	
	# 画面上部:メッセージ
	var title_label = Label.new()
	title_label.text = "呼び出す冒険者を選択してください"
	title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	main_vbox.add_child(title_label)
	
	# 隙間
	var spacer = Control.new()
	spacer.custom_minimum_size.y = 10
	main_vbox.add_child(spacer)
	
	# 画面中央:メンバー一覧(縦並びのコンテナ)
	var list_vbox = VBoxContainer.new()
	list_vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
	main_vbox.add_child(list_vbox)
	
	# ページ内の表示対象をスライス (6名ずつ)
	var start_idx = _current_page * 6
	var page_members = _call_list_cache.slice(start_idx, start_idx + 6)
	
	_row_buttons.clear()
	
	for i in range(page_members.size()):
		var m: CharaData = page_members[i]
		
		# 横並びの行コンテナ
		var row_hbox = HBoxContainer.new()
		list_vbox.add_child(row_hbox)
		
		# 矢印カーソル代わりの選択ボタン(右向き矢印「▶」)
		var select_btn = Button.new()
		select_btn.text = "▶"
		select_btn.flat = true # ボタンの枠線を消して矢印だけに見せる
		# 決定された時の処理
		select_btn.pressed.connect(func(): _execute_call_out(m))
		row_hbox.add_child(select_btn)
		_row_buttons.append(select_btn)
		
		# 見た目画像
		var img_rect = TextureRect.new()
		if FileAccess.file_exists(m.face_image_path):
			img_rect.texture = load(m.face_image_path)
		img_rect.custom_minimum_size = Vector2(40, 40)
		img_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
		row_hbox.add_child(img_rect)
		
		# ステータス文字列の結合
		# 例: (1) 名前、レベル、職業、最大HP、最大MP
		var info_label = Label.new()
		info_label.text = "【%s】 LV:%d %s (HP:%d/%d MP:%d/%d)" % [
			m.chara_name, m.level, m.get_display_class(), 
			m.current_hp, m.max_hp, m.current_mp, m.max_mp
		]
		row_hbox.add_child(info_label)
	
	# 画面下部:ナビゲーションボタン
	var nav_hbox = HBoxContainer.new()
	nav_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
	main_vbox.add_child(nav_hbox)
	
	var next_btn = Button.new()
	next_btn.text = "次の一覧に進む"
	next_btn.pressed.connect(_on_next_page_pressed)
	nav_hbox.add_child(next_btn)
	
	var back_menu_btn = Button.new()
	back_menu_btn.text = "ギルドに戻る"
	back_menu_btn.pressed.connect(_show_guild_menu)
	nav_hbox.add_child(back_menu_btn)
	
	# フォーカス近隣の動的接続(循環設定)
	var all_focusable = []
	all_focusable.append_array(_row_buttons)
	all_focusable.append(next_btn)
	all_focusable.append(back_menu_btn)
	
	for i in range(all_focusable.size()):
		var prev = all_focusable[(i - 1 + all_focusable.size()) % all_focusable.size()]
		var next = all_focusable[(i + 1) % all_focusable.size()]
		all_focusable[i].focus_neighbor_top = prev.get_path()
		all_focusable[i].focus_neighbor_bottom = next.get_path()
		
	# 最初の矢印にフォーカス
	if _row_buttons.size() > 0:
		_row_buttons[0].grab_focus()
	else:
		next_btn.grab_focus()

# 「次の一覧に進む」が押された時
func _on_next_page_pressed() -> void:
	# ギルドに6名以上いる場合のみページを進める
	if _call_list_cache.size() <= 6:
		return
		
	_current_page += 1
	# 最後のメンバー一覧を超えたら最初のページに戻る
	if (_current_page * 6) >= _call_list_cache.size():
		_current_page = 0
		
	_open_call_out_ui()

# 実際にパーティに加入させる処理
func _execute_call_out(chara: CharaData) -> void:
	# パーティの空き枠(前衛・後衛)に自動追加
	var slot_assigned = PartyManager.auto_add_member(chara)
	if slot_assigned != -1:
		var msg = "名前【%s】がパーティに加わった。" % chara.chara_name
		_show_guild_message(msg)

# ----------------------------------------------------------------              
# 汎用メッセージ画面(1ボタンでギルドメニューに戻る)
# ----------------------------------------------------------------              
func _show_guild_message(msg_text: String) -> void:
	town_system.cleanup_dynamic_menus()
	
	var bg = ColorRect.new()
	bg.name = "DynamicMenu"
	bg.color = Color.BLACK
	bg.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(bg)
	
	var vbox = VBoxContainer.new()
	vbox.set_anchors_preset(Control.PRESET_CENTER)
	bg.add_child(vbox)
	
	var label = Label.new()
	label.text = msg_text
	label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	vbox.add_child(label)
	
	var spacer = Control.new()
	spacer.custom_minimum_size.y = 15
	vbox.add_child(spacer)
	
	var ok_btn = Button.new()
	ok_btn.text = "閉じる"
	ok_btn.pressed.connect(_show_guild_menu)
	vbox.add_child(ok_btn)
	
	ok_btn.grab_focus()

# ----------------------------------------------------------------              
# その他の既存機能
# ----------------------------------------------------------------              
# キャラクター新規作成時の人数制限チェック
func _on_create_adventurer_selected() -> void:
	# プレイヤーが「作成」できるのは累計24名まで
	if registered_members.size() >= MAX_CREATE_LIMIT:
		# 必要に応じて「これ以上作成できません」という黒背景メッセージを出すか、
		# 今回は既存の挙動を維持して作成処理へ進ませないようにガードします。
		_show_simple_message("冒険者は24名までしか作成できません。")
		return

	if town_system:
		town_system.town_graphic.hide()
		if town_system.creation_node:
			town_system.creation_node.activate_name_input()

func _on_creation_canceled() -> void:
	if town_system:
		town_system.town_graphic.show()
		var dynamic_menu = town_system.town_graphic.get_node_or_null("DynamicMenu")
		if dynamic_menu:
			var menu_panel = dynamic_menu.get_child(0)
			if menu_panel and menu_panel.get_child_count() > 0:
				var create_btn = menu_panel.get_child(2) as Button
				if create_btn:
					create_btn.grab_focus.call_deferred()

func register_new_adventurer(new_chara: CharaData) -> bool:
	registered_members.append(new_chara)
	return true

func prepare_return_to_town() -> void:
	if town_system:
		town_system.reset_to_main_town()
		town_system = null

ギルド管理用コード (guild_manager.gd) の上記のように修正してください。メッセージ表示などの調整を行っています。

「冒険者をあずける」機能

冒険者ギルドの「冒険者をあずける」機能を実装します。以下は冒険者ギルドの仕様の一部です。

  • 冒険者ギルドには最大25名分の冒険者枠がある。
  • 冒険者は最大24名作成できる。
  • パーティの人数が6名のとき、ギルドには最大18名存在できるものとします。
  • パーティの人数が5名のとき、ギルドには最大19名存在できるものとします。
  • パーティの人数が4名のとき、ギルドには最大20名存在できるものとします。
  • パーティの人数が3名のとき、ギルドには最大21名存在できるものとします。
  • パーティの人数が2名のとき、ギルドには最大22名存在できるものとします。
  • パーティの人数が1名のとき、ギルドには最大23名存在できるものとします。
  • パーティの人数が0名のとき、ギルドには最大24名存在できるものとします。
  • ギルドの空きスロットは、パーティメンバー入れ替え分。

ノード構成の作成

  • CanvasLayer (CanvasLayer)
    • (他は省略)
    • CharacterCreation (Control)
    • JoinParty (Control)
    • LeaveParty (Control) 新規
      • Background (ColorRect) 新規
      • MainVBox (VBoxContainer) 新規
        • MessageLabel (Label) 新規
        • MemberList (VBoxContainer) 新規
          • Row1 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row2 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row3 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row4 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row5 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row6 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
        • NavigationHBox (HBoxContainer) 新規
          • BackButton (Button) 新規
LeaveParty (Control)新規作成 非表示
Background (ColorRect)画面全体を覆う黒塗りの背景。必ず MainVBox より上に配置すること。(カラー: 黒、レイアウト:Full Rect)
MainVBox (VBoxContainer)「冒険者をあずける」の画面全般
MessageLabel (Label)画面上部のメッセージ
「パーティから外す冒険者を選択してください」
MemberList (VBoxContainer)画面中央部に冒険者を6名表示する
Row1~6 (HBoxContainer)パーティメンバー最大6名
前衛から並ぶようにする
CursorBtn (Button)カーソル。外す冒険者を選ぶもの
FaceImg (TextureRect)冒険者の見た目画像
InfoLabel(Label)冒険者の名前、レベル、職業、最大HP、最大MP
NavigationHBox (HBoxContainer)画面下部のボタン
BackButton (Button)ギルドに戻る

このノード構成を作成します。LeaveParty (Control) の非表示を忘れずに実施してください。

このゲームでは、パーティメンバー0名を許容しています。しかし、パーティメンバー0名だと、ダンジョンには入れないものとします。

スクリプトファイルの作成

extends Control

# ==============================================================================
# leave_party.gd : パーティからメンバーを外す処理
# ==============================================================================

@onready var message_label: Label = $MainVBox/MessageLabel
@onready var member_list_container: VBoxContainer = $MainVBox/MemberList
@onready var back_button: Button = $MainVBox/NavigationHBox/BackButton

var rows: Array[HBoxContainer] = []
var _party_members: Array[CharaData] = []
var _is_error_mode: bool = false

# Localization Dictionary (Default is English)
const TEXT_DATA = {
	"en": {
		"title": "Select an adventurer to leave at the guild",
		"btn_back": "Back to Guild",
		"err_empty": "No adventurers in your party to leave.",
		"err_limit": "The adventurer guild has reached its maximum capacity.",
		"msg_leave": "[%s] has left the party."
	},
	"ja": {
		"title": "パーティから外す冒険者を選択してください",
		"btn_back": "ギルドに戻る",
		"err_empty": "パーティメンバーがいないので、あずけることができません",
		"err_limit": "冒険者ギルドにあずけられる上限に達しました",
		"msg_leave": "名前【%s】がパーティから外れた"
	}
}

func _get_current_lang() -> String:
	var locale = TranslationServer.get_locale().left(2).to_lower()
	if locale == "ja":
		return "ja"
	return "en"

func _ready() -> void:
	# 1から3は前衛, 4から6は後衛
	for i in range(1, 7):
		var row_node = member_list_container.get_node_or_null("Row" + str(i))
		if row_node:
			rows.append(row_node as HBoxContainer)
	
	back_button.pressed.connect(_on_back_button_pressed)
	hide()

# 起動時の関数
func activate() -> void:
	_party_members = PartyManager.get_party_members()
	var guild_count = GuildManager.get_registered_count()
	var party_count = _party_members.size()
	
	_is_error_mode = false
	
	var lang = _get_current_lang()
	back_button.text = TEXT_DATA[lang]["btn_back"]
	
	show()
	
	if party_count == 0:
		_show_error_message(TEXT_DATA[lang]["err_empty"])
		return
		
	var allowed_guild_max = 24 - party_count
	if guild_count > allowed_guild_max:
		_show_error_message(TEXT_DATA[lang]["err_limit"])
		return
		
	member_list_container.show()
	back_button.get_parent().show()
	_update_member_list()

func _show_error_message(text_content: String) -> void:
	_is_error_mode = true
	member_list_container.hide()
	
	# JoinPartyと完全に同じ挙動にします
	# 下部ボタン単体ではなく、その親コンテナ(HBoxContainer)を丸ごと非表示にします
	back_button.get_parent().hide()
	
	message_label.text = text_content
	release_focus()

func _update_member_list() -> void:
	var focusable_buttons: Array[Button] = []
	var lang = _get_current_lang()
	message_label.text = TEXT_DATA[lang]["title"]
	
	for i in range(6):
		var row = rows[i]
		var cursor_btn = row.get_child(0) as Button
		
		for connection in cursor_btn.pressed.get_connections():
			cursor_btn.pressed.disconnect(connection.callable)
		
		if i < _party_members.size():
			var m: CharaData = _party_members[i]
			row.show()
			
			var face_img = row.get_child(1) as TextureRect
			var info_label = row.get_child(2) as Label
			
			cursor_btn.text = "▶"
			cursor_btn.disabled = false
			
			var callable = func(target_chara: CharaData): _on_row_selected(target_chara)
			cursor_btn.pressed.connect(callable.bind(m))
			
			if FileAccess.file_exists(m.face_image_path):
				face_img.texture = load(m.face_image_path)
			else:
				face_img.texture = null
			face_img.custom_minimum_size = Vector2(64, 64)
				
			info_label.text = "[%s] LV:%d %s (MAX HP:%d MAX MP:%d)" % [
				m.chara_name, m.level, m.get_display_class(),
				m.max_hp, m.max_mp
			]
			
			focusable_buttons.append(cursor_btn)
		else:
			row.hide()
			cursor_btn.disabled = true
	
	focusable_buttons.append(back_button)
	
	for i in range(focusable_buttons.size()):
		var prev = focusable_buttons[(i - 1 + focusable_buttons.size()) % focusable_buttons.size()]
		var next = focusable_buttons[(i + 1) % focusable_buttons.size()]
		focusable_buttons[i].focus_neighbor_top = prev.get_path()
		focusable_buttons[i].focus_neighbor_bottom = next.get_path()
	
	if focusable_buttons.size() > 1:
		focusable_buttons[0].grab_focus()
	else:
		back_button.grab_focus()

func _on_row_selected(chara: CharaData) -> void:
	if PartyManager.remove_member(chara):
		var lang = _get_current_lang()
		var formatted_msg = TEXT_DATA[lang]["msg_leave"] % chara.chara_name
		_show_error_message(formatted_msg)

func _on_back_button_pressed() -> void:
	hide()
	GuildManager.return_to_guild_main_menu()

func _unhandled_input(event: InputEvent) -> void:
	if not is_visible_in_tree():
		return
		
	# In error or confirmation message mode, accept or cancel returns to guild menu
	if _is_error_mode:
		if event.is_action_pressed("ui_accept") or event.is_action_pressed("ui_cancel"):
			get_viewport().set_input_as_handled()
			_is_error_mode = false
			_on_back_button_pressed()
		return

	# Cancel button returns directly without message
	if event.is_action_pressed("ui_cancel"):
		get_viewport().set_input_as_handled()
		_on_back_button_pressed()

LeaveParty ノードを選択した状態で、新規スクリプト leave_party.gd を作成し、アタッチしてください。コードは上記の通りです。主に「冒険者をあずける」画面の表示部分を担当しています。

ギルド管理用コード修正

extends Node

# ==============================================================================
# 冒険者ギルド 管理マネージャー (UI・ロジック完全版)
# ==============================================================================

const SYSTEM_MAX_SLOTS: int = 25
const MAX_CREATE_LIMIT: int = 24

var registered_members: Array[CharaData] = []
var town_system: TownSystem = null
var join_party_node: Control = null
var leave_party_node: Control = null

# ギルドメニューの項目定義
const GUILD_MENU_KEYS = [
	"GUILD_MENU_JOIN", 
	"GUILD_MENU_REMOVE", 
	"GUILD_MENU_CREATE", 
	"GUILD_MENU_CHANGE_CLASS", 
	"GUILD_MENU_DELETE", 
	"GUILD_MENU_CHANGE_NAME", 
	"GUILD_MENU_CHANGE_APPEARANCE", 
	"GUILD_MENU_BACK"
]

# 呼び出し画面用のページング管理変数
var _call_list_cache: Array[CharaData] = []
var _current_page: int = 0
var _selected_row_index: int = 0
var _row_buttons: Array[Button] = []

# ==============================================================================
# UI制御・画面遷移ハンドリング
# ==============================================================================

# 街からギルドに入ったときに呼び出される関数
func open_guild(town: TownSystem) -> void:
	town_system = town
	
	if town_system.creation_node:
		join_party_node = town_system.creation_node.get_parent().get_node_or_null("JoinParty")
		leave_party_node = town_system.creation_node.get_parent().get_node_or_null("LeaveParty") # 追加
	
	town_system._update_graphic("guild")
	if town_system.town_menu:
		town_system.town_menu.hide()
	
	town_system.cleanup_dynamic_menus()
	
	if town_system.creation_node and not town_system.creation_node.is_connected("creation_canceled", _on_creation_canceled):
		town_system.creation_node.creation_canceled.connect(_on_creation_canceled)
		
	_show_guild_menu()

# ギルドのメインメニューに戻る(JoinParty画面を閉じたときなどに使用)
func return_to_guild_main_menu() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
		_show_guild_menu()

func _show_guild_menu() -> void:
	var center_container = CenterContainer.new()
	center_container.name = "DynamicMenu"
	center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(center_container)
	
	var menu_panel = VBoxContainer.new()
	center_container.add_child(menu_panel)
	
	var buttons = []
	for key in GUILD_MENU_KEYS:
		var btn = Button.new()
		btn.text = tr(key)
		btn.pressed.connect(func(): _on_guild_menu_selected(key))
		menu_panel.add_child(btn)
		buttons.append(btn)
	
	for i in range(buttons.size()):
		var prev = buttons[(i - 1 + buttons.size()) % buttons.size()]
		var next = buttons[(i + 1) % buttons.size()]
		buttons[i].focus_neighbor_top = prev.get_path()
		buttons[i].focus_neighbor_bottom = next.get_path()
	
	buttons[0].grab_focus()

func _on_guild_menu_selected(key: String) -> void:
	if key == "GUILD_MENU_BACK":
		prepare_return_to_town()
		return
		
	match key:
		"GUILD_MENU_JOIN":
			_on_call_out_selected() # 1. 冒険者をよびだす
		"GUILD_MENU_REMOVE":
			_on_leave_party_selected() # 修正:2. 冒険者をあずける UIを開く
		"GUILD_MENU_CREATE":
			_on_create_adventurer_selected()
		"GUILD_MENU_CHANGE_CLASS":
			print("TODO: クラスを変える UIを開く")
		"GUILD_MENU_DELETE":
			print("TODO: 冒険者を削除する UIを開く")
		"GUILD_MENU_CHANGE_NAME":
			print("TODO: 名前を変える UIを開く")
		"GUILD_MENU_CHANGE_APPEARANCE":
			print("TODO: 見た目を変える UIを開く")

# 追加:2. 冒険者をあずける(マネージャー側ロジック)
func _on_leave_party_selected() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	if leave_party_node:
		leave_party_node.activate()

# 追加:現在ギルド(待機所)にいるメンバーの総数を返すヘルパー関数
func get_registered_count() -> int:
	return PartyManager.get_members_in_guild().size()

# ----------------------------------------------------------------              
# 1. 冒険者をよびだす(マネージャー側ロジック)
# ----------------------------------------------------------------              
func _on_call_out_selected() -> void:
	# ギルドメニューUIを一時消去
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	# パーティの人数チェックと、ギルド内のメンバーリスト取得
	var is_party_full = PartyManager.is_full()
	var guild_members = PartyManager.get_members_in_guild()
	
	# 状態をそのまま JoinParty ノードに丸投げする
	if join_party_node:
		join_party_node.activate(guild_members, is_party_full)

# メッセージ単体表示用の簡易関数
func _show_simple_message(msg_text: String) -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	var center_container = CenterContainer.new()
	center_container.name = "DynamicMenu"
	center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(center_container)
	
	var vbox = VBoxContainer.new()
	center_container.add_child(vbox)
	
	var label = Label.new()
	label.text = msg_text
	vbox.add_child(label)
	
	var ok_btn = Button.new()
	ok_btn.text = "閉じる"
	ok_btn.pressed.connect(return_to_guild_main_menu)
	vbox.add_child(ok_btn)
	
	ok_btn.grab_focus()

func _open_call_out_ui() -> void:
	town_system.cleanup_dynamic_menus()
	
	# ベースとなる全画面黒背景のコンテナ
	var bg = ColorRect.new()
	bg.name = "DynamicMenu"
	bg.color = Color.BLACK
	bg.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(bg)
	
	# メインレイアウト(縦並び)
	var main_vbox = VBoxContainer.new()
	main_vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
	main_vbox.offset_left = 20
	main_vbox.offset_top = 20
	main_vbox.offset_right = -20
	main_vbox.offset_bottom = -20
	bg.add_child(main_vbox)
	
	# 画面上部:メッセージ
	var title_label = Label.new()
	title_label.text = "呼び出す冒険者を選択してください"
	title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	main_vbox.add_child(title_label)
	
	# 隙間
	var spacer = Control.new()
	spacer.custom_minimum_size.y = 10
	main_vbox.add_child(spacer)
	
	# 画面中央:メンバー一覧(縦並びのコンテナ)
	var list_vbox = VBoxContainer.new()
	list_vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
	main_vbox.add_child(list_vbox)
	
	# ページ内の表示対象をスライス (6名ずつ)
	var start_idx = _current_page * 6
	var page_members = _call_list_cache.slice(start_idx, start_idx + 6)
	
	_row_buttons.clear()
	
	for i in range(page_members.size()):
		var m: CharaData = page_members[i]
		
		# 横並びの行コンテナ
		var row_hbox = HBoxContainer.new()
		list_vbox.add_child(row_hbox)
		
		# 矢印カーソル代わりの選択ボタン(右向き矢印「▶」)
		var select_btn = Button.new()
		select_btn.text = "▶"
		select_btn.flat = true # ボタンの枠線を消して矢印だけに見せる
		# 決定された時の処理
		select_btn.pressed.connect(func(): _execute_call_out(m))
		row_hbox.add_child(select_btn)
		_row_buttons.append(select_btn)
		
		# 見た目画像
		var img_rect = TextureRect.new()
		if FileAccess.file_exists(m.face_image_path):
			img_rect.texture = load(m.face_image_path)
		img_rect.custom_minimum_size = Vector2(40, 40)
		img_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
		row_hbox.add_child(img_rect)
		
		# ステータス文字列の結合
		# 例: (1) 名前、レベル、職業、最大HP、最大MP
		var info_label = Label.new()
		info_label.text = "【%s】 LV:%d %s (HP:%d/%d MP:%d/%d)" % [
			m.chara_name, m.level, m.get_display_class(), 
			m.current_hp, m.max_hp, m.current_mp, m.max_mp
		]
		row_hbox.add_child(info_label)
	
	# 画面下部:ナビゲーションボタン
	var nav_hbox = HBoxContainer.new()
	nav_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
	main_vbox.add_child(nav_hbox)
	
	var next_btn = Button.new()
	next_btn.text = "次の一覧に進む"
	next_btn.pressed.connect(_on_next_page_pressed)
	nav_hbox.add_child(next_btn)
	
	var back_menu_btn = Button.new()
	back_menu_btn.text = "ギルドに戻る"
	back_menu_btn.pressed.connect(_show_guild_menu)
	nav_hbox.add_child(back_menu_btn)
	
	# フォーカス近隣の動的接続(循環設定)
	var all_focusable = []
	all_focusable.append_array(_row_buttons)
	all_focusable.append(next_btn)
	all_focusable.append(back_menu_btn)
	
	for i in range(all_focusable.size()):
		var prev = all_focusable[(i - 1 + all_focusable.size()) % all_focusable.size()]
		var next = all_focusable[(i + 1) % all_focusable.size()]
		all_focusable[i].focus_neighbor_top = prev.get_path()
		all_focusable[i].focus_neighbor_bottom = next.get_path()
		
	# 最初の矢印にフォーカス
	if _row_buttons.size() > 0:
		_row_buttons[0].grab_focus()
	else:
		next_btn.grab_focus()

# 「次の一覧に進む」が押された時
func _on_next_page_pressed() -> void:
	# ギルドに6名以上いる場合のみページを進める
	if _call_list_cache.size() <= 6:
		return
		
	_current_page += 1
	# 最後のメンバー一覧を超えたら最初のページに戻る
	if (_current_page * 6) >= _call_list_cache.size():
		_current_page = 0
		
	_open_call_out_ui()

# 実際にパーティに加入させる処理
func _execute_call_out(chara: CharaData) -> void:
	# パーティの空き枠(前衛・後衛)に自動追加
	var slot_assigned = PartyManager.auto_add_member(chara)
	if slot_assigned != -1:
		var msg = "名前【%s】がパーティに加わった。" % chara.chara_name
		_show_guild_message(msg)

# ----------------------------------------------------------------              
# 汎用メッセージ画面(1ボタンでギルドメニューに戻る)
# ----------------------------------------------------------------              
func _show_guild_message(msg_text: String) -> void:
	town_system.cleanup_dynamic_menus()
	
	var bg = ColorRect.new()
	bg.name = "DynamicMenu"
	bg.color = Color.BLACK
	bg.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(bg)
	
	var vbox = VBoxContainer.new()
	vbox.set_anchors_preset(Control.PRESET_CENTER)
	bg.add_child(vbox)
	
	var label = Label.new()
	label.text = msg_text
	label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	vbox.add_child(label)
	
	var spacer = Control.new()
	spacer.custom_minimum_size.y = 15
	vbox.add_child(spacer)
	
	var ok_btn = Button.new()
	ok_btn.text = "閉じる"
	ok_btn.pressed.connect(_show_guild_menu)
	vbox.add_child(ok_btn)
	
	ok_btn.grab_focus()

# ----------------------------------------------------------------              
# その他の既存機能
# ----------------------------------------------------------------              
# キャラクター新規作成時の人数制限チェック
func _on_create_adventurer_selected() -> void:
	# プレイヤーが「作成」できるのは累計24名まで
	if registered_members.size() >= MAX_CREATE_LIMIT:
		# 必要に応じて「これ以上作成できません」という黒背景メッセージを出すか、
		# 今回は既存の挙動を維持して作成処理へ進ませないようにガードします。
		_show_simple_message("冒険者は24名までしか作成できません。")
		return

	if town_system:
		town_system.town_graphic.hide()
		if town_system.creation_node:
			town_system.creation_node.activate_name_input()

func _on_creation_canceled() -> void:
	if town_system:
		town_system.town_graphic.show()
		var dynamic_menu = town_system.town_graphic.get_node_or_null("DynamicMenu")
		if dynamic_menu:
			var menu_panel = dynamic_menu.get_child(0)
			if menu_panel and menu_panel.get_child_count() > 0:
				var create_btn = menu_panel.get_child(2) as Button
				if create_btn:
					create_btn.grab_focus.call_deferred()

func register_new_adventurer(new_chara: CharaData) -> bool:
	registered_members.append(new_chara)
	return true

func prepare_return_to_town() -> void:
	if town_system:
		town_system.reset_to_main_town()
		town_system = null

ギルド管理用コード (guild_manager.gd) の上記のように修正してください。メッセージ表示などの調整を行っています。

パーティ管理用コード修正

extends Node

# ==============================================================================
# パーティ管理マネージャー (Autoload用)
# ==============================================================================

# 固定6枠の配列(null または CharaData)
# 0, 1, 2 = 前衛 (Front) / 3, 4, 5 = 後衛 (Back)
var slots: Array = [null, null, null, null, null, null]

# 現在パーティにいる実人数を返す
func get_member_count() -> int:
	var count = 0
	for member in slots:
		if member != null:
			count += 1
	return count

# パーティが満員(6名)かどうか
func is_full() -> bool:
	return get_member_count() >= 6

# 空いている最初のスロット(前衛➔後衛の順)に自動配置する
# 成功したらスロット番号(0-5)、空きがなければ -1 を返す
func auto_add_member(chara: CharaData) -> int:
	for i in range(6):
		if slots[i] == null:
			slots[i] = chara
			print(chara.chara_name, " がパーティスロット ", i, " に入りました(", "前衛" if i < 3 else "後衛", ")")
			return i
	return -1

# 指定したキャラクターをパーティから外す
func remove_member(chara: CharaData) -> bool:
	for i in range(6):
		if slots[i] == chara:
			slots[i] = null
			print(chara.chara_name, " をパーティから外しました。")
			return true
	return false

# ギルド専用:ギルド内にいる冒険者を取得する
func get_members_in_guild() -> Array[CharaData]:
	var in_guild_list: Array[CharaData] = []
	for chara in GuildManager.registered_members:
		if not chara in slots:
			in_guild_list.append(chara)
	return in_guild_list

# 「冒険者をあずける」画面用に、現在パーティにいるメンバーを順に取得する
func get_party_members() -> Array[CharaData]:
	var party_list: Array[CharaData] = []
	for member in slots:
		if member != null:
			party_list.append(member)
	return party_list
	

パーティ管理用コード (party_manager.gd) の上記のように修正してください。冒険者をあずけたときに一人減るようにしています。

冒険者作成上限対応処理

冒険者は最大24名作成できるという仕様です。そのため、25名作成しようとするとエラーメッセージが出るようにします。

ギルド管理用コード修正

extends Node

# ==============================================================================
# 冒険者ギルド 管理マネージャー (UI・ロジック完全版)
# ==============================================================================

const SYSTEM_MAX_SLOTS: int = 25
const MAX_CREATE_LIMIT: int = 24

var registered_members: Array[CharaData] = []
var town_system: TownSystem = null
var join_party_node: Control = null
var leave_party_node: Control = null

# ギルドメニューの項目定義
const GUILD_MENU_KEYS = [
	"GUILD_MENU_JOIN", 
	"GUILD_MENU_REMOVE", 
	"GUILD_MENU_CREATE", 
	"GUILD_MENU_CHANGE_CLASS", 
	"GUILD_MENU_DELETE", 
	"GUILD_MENU_CHANGE_NAME", 
	"GUILD_MENU_CHANGE_APPEARANCE", 
	"GUILD_MENU_BACK"
]

# 呼び出し画面用のページング管理変数
var _call_list_cache: Array[CharaData] = []
var _current_page: int = 0
var _selected_row_index: int = 0
var _row_buttons: Array[Button] = []

# ==============================================================================
# UI制御・画面遷移ハンドリング
# ==============================================================================

# 街からギルドに入ったときに呼び出される関数
func open_guild(town: TownSystem) -> void:
	town_system = town
	
	if town_system.creation_node:
		join_party_node = town_system.creation_node.get_parent().get_node_or_null("JoinParty")
		leave_party_node = town_system.creation_node.get_parent().get_node_or_null("LeaveParty") # 追加
	
	town_system._update_graphic("guild")
	if town_system.town_menu:
		town_system.town_menu.hide()
	
	town_system.cleanup_dynamic_menus()
	
	if town_system.creation_node and not town_system.creation_node.is_connected("creation_canceled", _on_creation_canceled):
		town_system.creation_node.creation_canceled.connect(_on_creation_canceled)
		
	_show_guild_menu()

# ギルドのメインメニューに戻る(JoinParty画面を閉じたときなどに使用)
func return_to_guild_main_menu() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
		_show_guild_menu()

func _show_guild_menu() -> void:
	var center_container = CenterContainer.new()
	center_container.name = "DynamicMenu"
	center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(center_container)
	
	var menu_panel = VBoxContainer.new()
	center_container.add_child(menu_panel)
	
	var buttons = []
	for key in GUILD_MENU_KEYS:
		var btn = Button.new()
		btn.text = tr(key)
		btn.pressed.connect(func(): _on_guild_menu_selected(key))
		menu_panel.add_child(btn)
		buttons.append(btn)
	
	for i in range(buttons.size()):
		var prev = buttons[(i - 1 + buttons.size()) % buttons.size()]
		var next = buttons[(i + 1) % buttons.size()]
		buttons[i].focus_neighbor_top = prev.get_path()
		buttons[i].focus_neighbor_bottom = next.get_path()
	
	buttons[0].grab_focus()

func _on_guild_menu_selected(key: String) -> void:
	if key == "GUILD_MENU_BACK":
		prepare_return_to_town()
		return
		
	match key:
		"GUILD_MENU_JOIN":
			_on_call_out_selected() # 1. 冒険者をよびだす
		"GUILD_MENU_REMOVE":
			_on_leave_party_selected() # 修正:2. 冒険者をあずける UIを開く
		"GUILD_MENU_CREATE":
			_on_create_adventurer_selected()
		"GUILD_MENU_CHANGE_CLASS":
			print("TODO: クラスを変える UIを開く")
		"GUILD_MENU_DELETE":
			print("TODO: 冒険者を削除する UIを開く")
		"GUILD_MENU_CHANGE_NAME":
			print("TODO: 名前を変える UIを開く")
		"GUILD_MENU_CHANGE_APPEARANCE":
			print("TODO: 見た目を変える UIを開く")

# 追加:2. 冒険者をあずける(マネージャー側ロジック)
func _on_leave_party_selected() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	if leave_party_node:
		leave_party_node.activate()

# 追加:現在ギルド(待機所)にいるメンバーの総数を返すヘルパー関数
func get_registered_count() -> int:
	return PartyManager.get_members_in_guild().size()

# ----------------------------------------------------------------              
# 1. 冒険者をよびだす(マネージャー側ロジック)
# ----------------------------------------------------------------              
func _on_call_out_selected() -> void:
	# ギルドメニューUIを一時消去
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	# パーティの人数チェックと、ギルド内のメンバーリスト取得
	var is_party_full = PartyManager.is_full()
	var guild_members = PartyManager.get_members_in_guild()
	
	# 状態をそのまま JoinParty ノードに丸投げする
	if join_party_node:
		join_party_node.activate(guild_members, is_party_full)

# メッセージ単体表示用の簡易関数
func _show_simple_message(msg_text: String) -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	var center_container = CenterContainer.new()
	center_container.name = "DynamicMenu"
	center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(center_container)
	
	var vbox = VBoxContainer.new()
	center_container.add_child(vbox)
	
	var label = Label.new()
	label.text = msg_text
	vbox.add_child(label)
	
	var ok_btn = Button.new()
	ok_btn.text = "閉じる"
	ok_btn.pressed.connect(return_to_guild_main_menu)
	vbox.add_child(ok_btn)
	
	ok_btn.grab_focus()

func _open_call_out_ui() -> void:
	town_system.cleanup_dynamic_menus()
	
	# ベースとなる全画面黒背景のコンテナ
	var bg = ColorRect.new()
	bg.name = "DynamicMenu"
	bg.color = Color.BLACK
	bg.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(bg)
	
	# メインレイアウト(縦並び)
	var main_vbox = VBoxContainer.new()
	main_vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
	main_vbox.offset_left = 20
	main_vbox.offset_top = 20
	main_vbox.offset_right = -20
	main_vbox.offset_bottom = -20
	bg.add_child(main_vbox)
	
	# 画面上部:メッセージ
	var title_label = Label.new()
	title_label.text = "呼び出す冒険者を選択してください"
	title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	main_vbox.add_child(title_label)
	
	# 隙間
	var spacer = Control.new()
	spacer.custom_minimum_size.y = 10
	main_vbox.add_child(spacer)
	
	# 画面中央:メンバー一覧(縦並びのコンテナ)
	var list_vbox = VBoxContainer.new()
	list_vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
	main_vbox.add_child(list_vbox)
	
	# ページ内の表示対象をスライス (6名ずつ)
	var start_idx = _current_page * 6
	var page_members = _call_list_cache.slice(start_idx, start_idx + 6)
	
	_row_buttons.clear()
	
	for i in range(page_members.size()):
		var m: CharaData = page_members[i]
		
		# 横並びの行コンテナ
		var row_hbox = HBoxContainer.new()
		list_vbox.add_child(row_hbox)
		
		# 矢印カーソル代わりの選択ボタン(右向き矢印「▶」)
		var select_btn = Button.new()
		select_btn.text = "▶"
		select_btn.flat = true # ボタンの枠線を消して矢印だけに見せる
		# 決定された時の処理
		select_btn.pressed.connect(func(): _execute_call_out(m))
		row_hbox.add_child(select_btn)
		_row_buttons.append(select_btn)
		
		# 見た目画像
		var img_rect = TextureRect.new()
		if FileAccess.file_exists(m.face_image_path):
			img_rect.texture = load(m.face_image_path)
		img_rect.custom_minimum_size = Vector2(40, 40)
		img_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
		row_hbox.add_child(img_rect)
		
		# ステータス文字列の結合
		# 例: (1) 名前、レベル、職業、最大HP、最大MP
		var info_label = Label.new()
		info_label.text = "【%s】 LV:%d %s (HP:%d/%d MP:%d/%d)" % [
			m.chara_name, m.level, m.get_display_class(), 
			m.current_hp, m.max_hp, m.current_mp, m.max_mp
		]
		row_hbox.add_child(info_label)
	
	# 画面下部:ナビゲーションボタン
	var nav_hbox = HBoxContainer.new()
	nav_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
	main_vbox.add_child(nav_hbox)
	
	var next_btn = Button.new()
	next_btn.text = "次の一覧に進む"
	next_btn.pressed.connect(_on_next_page_pressed)
	nav_hbox.add_child(next_btn)
	
	var back_menu_btn = Button.new()
	back_menu_btn.text = "ギルドに戻る"
	back_menu_btn.pressed.connect(_show_guild_menu)
	nav_hbox.add_child(back_menu_btn)
	
	# フォーカス近隣の動的接続(循環設定)
	var all_focusable = []
	all_focusable.append_array(_row_buttons)
	all_focusable.append(next_btn)
	all_focusable.append(back_menu_btn)
	
	for i in range(all_focusable.size()):
		var prev = all_focusable[(i - 1 + all_focusable.size()) % all_focusable.size()]
		var next = all_focusable[(i + 1) % all_focusable.size()]
		all_focusable[i].focus_neighbor_top = prev.get_path()
		all_focusable[i].focus_neighbor_bottom = next.get_path()
		
	# 最初の矢印にフォーカス
	if _row_buttons.size() > 0:
		_row_buttons[0].grab_focus()
	else:
		next_btn.grab_focus()

# 「次の一覧に進む」が押された時
func _on_next_page_pressed() -> void:
	# ギルドに6名以上いる場合のみページを進める
	if _call_list_cache.size() <= 6:
		return
		
	_current_page += 1
	# 最後のメンバー一覧を超えたら最初のページに戻る
	if (_current_page * 6) >= _call_list_cache.size():
		_current_page = 0
		
	_open_call_out_ui()

# 実際にパーティに加入させる処理
func _execute_call_out(chara: CharaData) -> void:
	# パーティの空き枠(前衛・後衛)に自動追加
	var slot_assigned = PartyManager.auto_add_member(chara)
	if slot_assigned != -1:
		var msg = "名前【%s】がパーティに加わった。" % chara.chara_name
		_show_guild_message(msg)

# ----------------------------------------------------------------              
# 汎用メッセージ画面(1ボタンでギルドメニューに戻る)
# ----------------------------------------------------------------              
func _show_guild_message(msg_text: String) -> void:
	town_system.cleanup_dynamic_menus()
	
	var bg = ColorRect.new()
	bg.name = "DynamicMenu"
	bg.color = Color.BLACK
	bg.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(bg)
	
	var vbox = VBoxContainer.new()
	vbox.set_anchors_preset(Control.PRESET_CENTER)
	bg.add_child(vbox)
	
	var label = Label.new()
	label.text = msg_text
	label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	vbox.add_child(label)
	
	var spacer = Control.new()
	spacer.custom_minimum_size.y = 15
	vbox.add_child(spacer)
	
	var ok_btn = Button.new()
	ok_btn.text = "閉じる"
	ok_btn.pressed.connect(_show_guild_menu)
	vbox.add_child(ok_btn)
	
	ok_btn.grab_focus()

# ----------------------------------------------------------------              
# その他の既存機能
# ----------------------------------------------------------------              
# キャラクター新規作成時の人数制限チェック
func _on_create_adventurer_selected() -> void:
	# プレイヤーが「作成」できるのは累計24名まで
	if registered_members.size() >= MAX_CREATE_LIMIT:
		if town_system and town_system.creation_node:
			# 既存の街グラフィックを隠し、キャラクター作成画面をアクティブにする
			town_system.town_graphic.hide()
			
			# CharacterCreationノードにある上限エラー表示用の独自関数を呼び出す
			town_system.creation_node.activate_limit_error()
		return

	if town_system:
		town_system.town_graphic.hide()
		if town_system.creation_node:
			town_system.creation_node.activate_name_input()

func _on_creation_canceled() -> void:
	if town_system:
		town_system.town_graphic.show()
		var dynamic_menu = town_system.town_graphic.get_node_or_null("DynamicMenu")
		if dynamic_menu:
			var menu_panel = dynamic_menu.get_child(0)
			if menu_panel and menu_panel.get_child_count() > 0:
				var create_btn = menu_panel.get_child(2) as Button
				if create_btn:
					create_btn.grab_focus.call_deferred()

func register_new_adventurer(new_chara: CharaData) -> bool:
	registered_members.append(new_chara)
	return true

func prepare_return_to_town() -> void:
	if town_system:
		town_system.reset_to_main_town()
		town_system = null

ギルド管理用コード (guild_manager.gd) の上記のように修正してください。25人目を登録しようとすると、CharacterCreation をメッセージラベルを使用してエラーメッセージを出すようにします。

キャラクター作成用コード修正

extends Control

signal creation_canceled

# 子ノードへの参照
@onready var title_label: Label = $NameLayout/TitleLabel
@onready var name_input_field: LineEdit = $NameLayout/NameInputField
@onready var keyboard_grid: GridContainer = $NameLayout/KeyboardGrid
@onready var backspace_button: Button = $NameLayout/BottomMenu/BackspaceButton
@onready var confirm_button: Button = $NameLayout/BottomMenu/ConfirmButton

@onready var race_layout: VBoxContainer = $RaceLayout
@onready var race_title_label: Label = $RaceLayout/RaceTitleLabel
@onready var race_button_container: VBoxContainer = $RaceLayout/RaceButtonContainer

@onready var status_layout: VBoxContainer = $StatusLayout
@onready var status_instruction_label: Label = $StatusLayout/StatusInstructionLabel
@onready var character_summary_label: Label = $StatusLayout/CharacterSummaryLabel
@onready var status_grid_container: GridContainer = $StatusLayout/StatusGridContainer
@onready var status_back_button: Button = $StatusLayout/StatusBottomMenu/StatusBackButton
@onready var status_next_button: Button = $StatusLayout/StatusBottomMenu/StatusNextButton

@onready var class_layout: VBoxContainer = $ClassLayout
@onready var class_title_label: Label = $ClassLayout/ClassTitleLabel
@onready var class_status_preview: GridContainer = $ClassLayout/ClassMainContent/ClassStatusPreview
@onready var basic_class_column: VBoxContainer = $ClassLayout/ClassMainContent/ClassSelectionArea/BasicClassColumn
@onready var advanced_class_column: VBoxContainer = $ClassLayout/ClassMainContent/ClassSelectionArea/AdvancedClassColumn

@onready var appearance_layout: VBoxContainer = $AppearanceLayout
@onready var appearance_title_label: Label = $AppearanceLayout/AppearanceTitleLabel
@onready var appearance_summary_label: Label = $AppearanceLayout/AppearanceSummaryLabel
@onready var appearance_status_preview: GridContainer = $AppearanceLayout/AppearanceMainContent/AppearanceStatusPreview
@onready var appearance_wheel_control: Control = $AppearanceLayout/AppearanceMainContent/AppearanceWheelControl
@onready var appearance_random_button: Button = $AppearanceLayout/AppearanceBottomMenu/AppearanceRandomButton
@onready var appearance_back_button: Button = $AppearanceLayout/AppearanceBottomMenu/AppearanceBackButton
@onready var appearance_next_button: Button = $AppearanceLayout/AppearanceBottomMenu/AppearanceNextButton

@onready var confirmation_layout: VBoxContainer = $ConfirmationLayout
@onready var confirm_title_label: Label = $ConfirmationLayout/ConfirmTitleLabel
@onready var confirm_summary_label: Label = $ConfirmationLayout/ConfirmSummaryLabel
@onready var confirm_status_preview: GridContainer = $ConfirmationLayout/ConfirmMainContent/ConfirmStatusPreview
@onready var confirm_face_preview: TextureRect = $ConfirmationLayout/ConfirmMainContent/ConfirmFacePreview
@onready var confirm_back_button: Button = $ConfirmationLayout/ConfirmBottomMenu/ConfirmBackButton
@onready var confirm_create_button: Button = $ConfirmationLayout/ConfirmBottomMenu/ConfirmCreateButton

@onready var message_layout: VBoxContainer = $MessageLayout
@onready var message_text_label: Label = $MessageLayout/MessageTextLabel

# キーボードの文字データ定義
const KEYS_JA = [
	"あ", "い", "う", "え", "お", "か", "き", "く", "け", "こ",
	"さ", "し", "す", "せ", "そ", "た", "ち", "つ", "て", "と",
	"な", "に", "ぬ", "ね", "の", "は", "ひ", "ふ", "へ", "ほ",
	"ま", "み", "む", "め", "モ", "や", "ゆ", "よ", "わ", "を",
	"ら", "り", "る", "れ", "ろ", "ん", "っ", "ゃ", "ゅ", "ょ",
	"が", "ぎ", "ぐ", "げ", "ご", "ざ", "じ", "ず", "ぜ", "ぞ",
	"だ", "ぢ", "づ", "で", "ど", "ば", "び", "ぶ", "べ", "ぼ",
	"ぱ", "ぴ", "ぷ", "ぺ", "ぽ", "ー", "〜", "、", "。", " ",
	"ア", "イ", "ウ", "エ", "オ", "カ", "キ", "ク", "ケ", "コ",
	"サ", "シ", "ス", "セ", "ソ", "タ", "チ", "ツ", "テ", "ト",
	"ナ", "ニ", "ヌ", "ネ", "ノ", "ハ", "ヒ", "フ", "ヘ", "ホ",
	"マ", "ミ", "ム", "メ", "モ", "ヤ", "ユ", "ヨ", "ワ", "ヲ",
	"ラ", "リ", "ル", "レ", "ろ", "ン", "ッ", "ャ", "ュ", "ョ",
	"ガ", "ギ", "ぐ", "ゲ", "ゴ", "ザ", "ジ", "ズ", "ゼ", "ゾ",
	"ダ", "ヂ", "ヅ", "デ", "ド", "バ", "ビ", "ブ", "ベ", "ボ",
	"パ", "ピ", "プ", "ペ", "ポ", " ",  " ",  " ",  " ",  " "
]

const KEYS_EN = [
	"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
	"U", "V", "W", "X", "Y", "Z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-", "_", "!", "?",
	"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", 
	"u", "v", "w", "x", "y", "z"
]

var current_max_length: int = 16
var first_button: Button = null 

# --- キャラクターの作成データ保持用 ---
var final_character_name: String = ""
var final_character_race: String = ""  # "RACE_HUMAN", "RACE_ELF" などのIDを入れる運用にします
var final_character_class: String = "" # "CLASS_FIGHTER" などのIDを入れる運用にします
var final_character_image_path: String = "" 

# --- 種族の初期ステータス定義(CSVのマスターIDに準拠) ---
const RACE_BASE_STATS = {
	"RACE_HUMAN": { "LV": 1, "STR": 9, "DEX": 9, "VIT": 9, "AGI": 9, "INT": 9, "MND": 9, "LUC": 9, "HP_MIN": 11, "HP_MAX": 18, "MP_MIN": 9, "MP_MAX": 16 },
	"RACE_ELF":   { "LV": 1, "STR": 6, "DEX": 12, "VIT": 6, "AGI": 12, "INT": 12, "MND": 9, "LUC": 6, "HP_MIN": 7, "HP_MAX": 14, "MP_MIN": 13, "MP_MAX": 20 },
	"RACE_DWARF": { "LV": 1, "STR": 13, "DEX": 10, "VIT": 13, "AGI": 6, "INT": 6, "MND": 9, "LUC": 6, "HP_MIN": 13, "HP_MAX": 20, "MP_MIN": 7, "MP_MAX": 14 }
}

const ALLOCATABLE_STATS = ["STR", "DEX", "VIT", "AGI", "INT", "MND", "LUC"]

# CSVのステータスIDマッピングテーブル
const STAT_ID_MAP = {
	"LV": "STATUS_LEVEL",
	"HP": "STATUS_HP",
	"MP": "STATUS_MP",
	"STR": "STATUS_STR",
	"DEX": "STATUS_DEX",
	"VIT": "STATUS_VIT",
	"AGI": "STATUS_AGI",
	"INT": "STATUS_INT",
	"MND": "STATUS_MND",
	"LUC": "STATUS_LUC"
}

var current_base_stats = {}
var allocated_bonus_stats = { "STR": 0, "DEX": 0, "VIT": 0, "AGI": 0, "INT": 0, "MND": 0, "LUC": 0 }
var initial_bonus_points: int = 0
var remaining_bonus_points: int = 0

var stat_labels_dictionary = {}
var current_focused_stat_button: Button = null

# --- ゲーム進行フラグ ---
var is_advanced_races_unlocked: bool = true 
var is_class_group_1_unlocked: bool = false 
var is_class_group_2_unlocked: bool = false 
var is_class_group_3_unlocked: bool = false 

# --- 見た目(外見)選択用の変数群 ---
var face_image_paths: Array[String] = [] 
var face_sprites: Array[TextureRect] = [] 
var current_image_index: int = 0 
var target_wheel_angle: float = 0.0 
var current_wheel_angle: float = 0.0 
var is_wheel_focused: bool = false 
var wheel_tween: Tween = null

func _ready() -> void:
	if backspace_button and not backspace_button.pressed.is_connected(_on_backspace_button_pressed):
		backspace_button.pressed.connect(_on_backspace_button_pressed)
	if confirm_button and not confirm_button.pressed.is_connected(_on_confirm_button_pressed):
		confirm_button.pressed.connect(_on_confirm_button_pressed)
		
	if status_back_button and not status_back_button.pressed.is_connected(_on_status_back_pressed):
		status_back_button.pressed.connect(_on_status_back_pressed)
	if status_next_button and not status_next_button.pressed.is_connected(_on_status_next_pressed):
		status_next_button.pressed.connect(_on_status_next_pressed)
		
	if appearance_random_button and not appearance_random_button.pressed.is_connected(_on_appearance_random_pressed):
		appearance_random_button.pressed.connect(_on_appearance_random_pressed)
	if appearance_back_button and not appearance_back_button.pressed.is_connected(_on_appearance_back_pressed):
		appearance_back_button.pressed.connect(_on_appearance_back_pressed)
	if appearance_next_button and not appearance_next_button.pressed.is_connected(_on_appearance_next_pressed):
		appearance_next_button.pressed.connect(_on_appearance_next_pressed)
		
	# 確認画面のボタン接続
	if confirm_back_button and not confirm_back_button.pressed.is_connected(_on_confirm_back_pressed):
		confirm_back_button.pressed.connect(_on_confirm_back_pressed)
	if confirm_create_button and not confirm_create_button.pressed.is_connected(_on_confirm_create_pressed):
		confirm_create_button.pressed.connect(_on_confirm_create_pressed)
		
	hide()
	if race_layout: race_layout.hide()
	if status_layout: status_layout.hide()
	if class_layout: class_layout.hide()
	if appearance_layout: appearance_layout.hide()
	if confirmation_layout: confirmation_layout.hide()
	if message_layout: message_layout.hide()

func _input(event: InputEvent) -> void:
	if not is_visible_in_tree():
		return
		
	if event.is_action_pressed("ui_cancel"):
		if $NameLayout.is_visible_in_tree():
			get_viewport().set_input_as_handled()
			if name_input_field.text.length() > 0:
				_on_backspace_button_pressed()
			else:
				_cancel_and_return_to_guild()
				
		elif race_layout.is_visible_in_tree():
			get_viewport().set_input_as_handled()
			_back_to_name_input()
			
		elif status_layout.is_visible_in_tree():
			get_viewport().set_input_as_handled()
			if current_focused_stat_button and current_focused_stat_button.has_meta("stat_key"):
				var stat_key = current_focused_stat_button.get_meta("stat_key")
				if allocated_bonus_stats[stat_key] > 0:
					_modify_stat_point(stat_key, -1)
					return
			
			var total_spent = 0
			for val in allocated_bonus_stats.values():
				total_spent += val
			if total_spent == 0:
				_back_to_race_selection()
				
		elif class_layout.is_visible_in_tree():
			get_viewport().set_input_as_handled()
			_back_to_status_allocation()
			
		elif appearance_layout.is_visible_in_tree():
			get_viewport().set_input_as_handled()
			_back_to_class_selection()
			
		elif confirmation_layout.is_visible_in_tree():
			get_viewport().set_input_as_handled()
			_back_to_appearance_selection()

	if appearance_layout.is_visible_in_tree() and is_wheel_focused and face_image_paths.size() > 0:
		if event.is_action_pressed("ui_right", true) or event.is_action("ui_right"):
			if event.is_pressed():
				get_viewport().set_input_as_handled()
				_rotate_wheel(1)
		elif event.is_action_pressed("ui_left", true) or event.is_action("ui_left"):
			if event.is_pressed():
				get_viewport().set_input_as_handled()
				_rotate_wheel(-1)
		elif event.is_action_pressed("ui_accept"):
			get_viewport().set_input_as_handled()
			if appearance_next_button:
				appearance_next_button.grab_focus()
				
	elif message_layout.is_visible_in_tree():
		if event.is_action_pressed("ui_accept") or event.is_action_pressed("ui_cancel"):
			get_viewport().set_input_as_handled()
			_return_to_guild_menu()

func activate_name_input() -> void:
	name_input_field.text = ""
	show()
	$NameLayout.show()
	if race_layout: race_layout.hide()
	if status_layout: status_layout.hide()
	if class_layout: class_layout.hide()
	if appearance_layout: appearance_layout.hide()
	if confirmation_layout: confirmation_layout.hide()
	if message_layout: message_layout.hide()
	
	var current_locale = TranslationServer.get_locale().left(2)
	
	# システムテキストは本来は localization.csv に追加するのがベストですが、
	# 現状のCSV(ステータスや職業、種族)に合わせた安全な動的切り替えを行います
	if current_locale == "ja":
		current_max_length = 8
		_setup_software_keyboard(KEYS_JA)
		if title_label: title_label.text = "名前を入力してください"
		if backspace_button: backspace_button.text = "一字消す"
		if confirm_button: confirm_button.text = "決定"
	else:
		current_max_length = 16
		_setup_software_keyboard(KEYS_EN)
		if title_label: title_label.text = "Enter your name."
		if backspace_button: backspace_button.text = "BackSpace"
		if confirm_button: confirm_button.text = "Confirm"
		
	name_input_field.max_length = current_max_length
	keyboard_grid.columns = 20
	
	if first_button:
		first_button.grab_focus.call_deferred()

func _setup_software_keyboard(keys_array: Array) -> void:
	first_button = null
	for child in keyboard_grid.get_children():
		child.queue_free()
		
	var is_en = (TranslationServer.get_locale().left(2) == "en")
		
	for i in range(keys_array.size()):
		var key = keys_array[i]
		
		if key == " " and is_en:
			var spacer = Control.new()
			keyboard_grid.add_child(spacer)
			continue
			
		var btn = Button.new()
		btn.text = key
		btn.custom_minimum_size = Vector2(40, 40) 
		btn.focus_mode = Control.FOCUS_ALL 
		
		btn.pressed.connect(_on_key_pressed.bind(key))
		keyboard_grid.add_child(btn)
		
		if first_button == null:
			first_button = btn

func _on_key_pressed(char_text: String) -> void:
	if name_input_field.text.length() < current_max_length:
		name_input_field.text += char_text

func _on_backspace_button_pressed() -> void:
	if name_input_field.text.length() > 0:
		name_input_field.text = name_input_field.text.left(name_input_field.text.length() - 1)

func _on_confirm_button_pressed() -> void:
	var cleaned_name = name_input_field.text.strip_edges()
	if cleaned_name.is_empty():
		return
		
	final_character_name = cleaned_name
	_proceed_to_race_selection()

func _proceed_to_race_selection() -> void:
	$NameLayout.hide()
	if status_layout: status_layout.hide()
	if class_layout: class_layout.hide()
	if appearance_layout: appearance_layout.hide()
	if confirmation_layout: confirmation_layout.hide()
	if message_layout: message_layout.hide()
	race_layout.show()
	
	var current_locale = TranslationServer.get_locale().left(2)
	if current_locale == "ja":
		race_title_label.text = "種族を選択してください"
	else:
		race_title_label.text = "Select a race."
		
	for child in race_button_container.get_children():
		child.queue_free()
		
	# CSVのID(RACE_HUMAN 等)をそのまま管理
	var available_races = ["RACE_HUMAN"]
	if is_advanced_races_unlocked:
		available_races.append("RACE_ELF")
		available_races.append("RACE_DWARF")
		
	var r_buttons = []
	for race_id in available_races:
		var btn = Button.new()
		# tr() を使ってCSVから直接文言を引く("人間"、"Human" などが自動で入る)
		btn.text = tr(race_id)
			
		btn.focus_mode = Control.FOCUS_ALL
		btn.pressed.connect(_on_race_selected.bind(race_id))
		race_button_container.add_child(btn)
		r_buttons.append(btn)
		
	for i in range(r_buttons.size()):
		var prev = r_buttons[(i - 1 + r_buttons.size()) % r_buttons.size()]
		var next = r_buttons[(i + 1) % r_buttons.size()]
		r_buttons[i].focus_neighbor_top = prev.get_path()
		r_buttons[i].focus_neighbor_bottom = next.get_path()
		
	if r_buttons.size() > 0:
		r_buttons[0].grab_focus.call_deferred()

func _on_race_selected(race_id: String) -> void:
	final_character_race = race_id
	_proceed_to_status_allocation()

func _back_to_name_input() -> void:
	race_layout.hide()
	$NameLayout.show()
	if first_button:
		first_button.grab_focus.call_deferred()

func _cancel_and_return_to_guild() -> void:
	hide()
	creation_canceled.emit()

func _proceed_to_status_allocation() -> void:
	race_layout.hide()
	if class_layout: class_layout.hide()
	if appearance_layout: appearance_layout.hide()
	if confirmation_layout: confirmation_layout.hide()
	if message_layout: message_layout.hide()
	status_layout.show()
	
	var current_locale = TranslationServer.get_locale().left(2)
	if current_locale == "ja":
		status_instruction_label.text = "ボーナスポイントを振ってください"
		status_back_button.text = "ひとつ前に戻る"
		status_next_button.text = "次に進む"
	else:
		status_instruction_label.text = "Please allocate your bonus points."
		status_back_button.text = "Back"
		status_next_button.text = "Next"

	var base = RACE_BASE_STATS[final_character_race]
	current_base_stats = {
		"LV": base["LV"],
		"STR": base["STR"],
		"DEX": base["DEX"],
		"VIT": base["VIT"],
		"AGI": base["AGI"],
		"INT": base["INT"],
		"MND": base["MND"],
		"LUC": base["LUC"],
		"HP": randi_range(base["HP_MIN"], base["HP_MAX"]),
		"MP": randi_range(base["MP_MIN"], base["MP_MAX"])
	}
	
	for stat in ALLOCATABLE_STATS:
		allocated_bonus_stats[stat] = 0
		
	initial_bonus_points = randi_range(5, 12)
	remaining_bonus_points = initial_bonus_points
	
	_build_status_grid()
	_update_summary_ui()

func _build_status_grid() -> void:
	for child in status_grid_container.get_children():
		child.queue_free()
		
	stat_labels_dictionary.clear()
	status_grid_container.columns = 4
	
	# 静的ステータスの文言(Lv, HP, MP)もCSVマッピングから取得
	_add_static_stat_row(tr(STAT_ID_MAP["LV"]), current_base_stats["LV"])
	_add_static_stat_row(tr(STAT_ID_MAP["HP"]), current_base_stats["HP"])
	_add_static_stat_row(tr(STAT_ID_MAP["MP"]), current_base_stats["MP"])
	
	var focusable_buttons = []
	for stat_key in ALLOCATABLE_STATS:
		var stat_id = STAT_ID_MAP[stat_key]
		var display_name = tr(stat_id) # CSVから「力」「STR」などを取得
				
		var name_lbl = Label.new()
		name_lbl.text = display_name
		status_grid_container.add_child(name_lbl)
		
		var base_lbl = Label.new()
		base_lbl.text = str(current_base_stats[stat_key])
		status_grid_container.add_child(base_lbl)
		
		var bonus_lbl = Label.new()
		bonus_lbl.text = " +0"
		status_grid_container.add_child(bonus_lbl)
		stat_labels_dictionary[stat_key] = bonus_lbl
		
		var opt_btn = Button.new()
		opt_btn.text = " ◀ ▶ "
		opt_btn.focus_mode = Control.FOCUS_ALL
		opt_btn.set_meta("stat_key", stat_key)
		
		opt_btn.pressed.connect(_on_stat_button_pressed.bind(stat_key))
		opt_btn.focus_entered.connect(func(): current_focused_stat_button = opt_btn)
		
		status_grid_container.add_child(opt_btn)
		focusable_buttons.append(opt_btn)
		
	focusable_buttons.append(status_back_button)
	focusable_buttons.append(status_next_button)
	
	for i in range(focusable_buttons.size()):
		var prev = focusable_buttons[(i - 1 + focusable_buttons.size()) % focusable_buttons.size()]
		var next = focusable_buttons[(i + 1) % focusable_buttons.size()]
		focusable_buttons[i].focus_neighbor_top = prev.get_path()
		focusable_buttons[i].focus_neighbor_bottom = next.get_path()
		
	if focusable_buttons.size() > 0:
		focusable_buttons[0].grab_focus.call_deferred()

func _add_static_stat_row(stat_title: String, val: int) -> void:
	var name_lbl = Label.new()
	name_lbl.text = stat_title
	var val_lbl = Label.new()
	val_lbl.text = str(val)
	status_grid_container.add_child(name_lbl)
	status_grid_container.add_child(val_lbl)
	status_grid_container.add_child(Label.new())
	status_grid_container.add_child(Label.new())

func _update_summary_ui() -> void:
	var current_locale = TranslationServer.get_locale().left(2)
	var display_race = tr(final_character_race) # CSVから取得
			
	if current_locale == "ja":
		character_summary_label.text = "名前: %s   種族: %s   残りボーナスポイント: %d" % [final_character_name, display_race, remaining_bonus_points]
	else:
		character_summary_label.text = "Name: %s   Race: %s   Bonus Points: %d" % [final_character_name, display_race, remaining_bonus_points]

func _on_stat_button_pressed(stat_key: String) -> void:
	if remaining_bonus_points > 0:
		_modify_stat_point(stat_key, 1)

func _modify_stat_point(stat_key: String, amount: int) -> void:
	allocated_bonus_stats[stat_key] += amount
	remaining_bonus_points -= amount
	
	var bonus_label = stat_labels_dictionary[stat_key] as Label
	if allocated_bonus_stats[stat_key] > 0:
		bonus_label.text = " +%d" % allocated_bonus_stats[stat_key]
	else:
		bonus_label.text = " +0"
		
	_update_summary_ui()

func _back_to_race_selection() -> void:
	status_layout.hide()
	_proceed_to_race_selection()

func _on_status_back_pressed() -> void:
	_back_to_race_selection()

func _on_status_next_pressed() -> void:
	_proceed_to_class_selection()

func _proceed_to_class_selection() -> void:
	status_layout.hide()
	if appearance_layout: appearance_layout.hide()
	class_layout.show()
	
	var current_locale = TranslationServer.get_locale().left(2)
	if current_locale == "ja":
		class_title_label.text = "職業を選んでください"
	else:
		class_title_label.text = "Select a class."
		
	_build_class_status_preview()
	_build_class_columns()

func _build_class_status_preview() -> void:
	for child in class_status_preview.get_children():
		child.queue_free()
		
	class_status_preview.columns = 2
	var preview_keys = ["LV", "HP", "MP", "STR", "DEX", "VIT", "AGI", "INT", "MND", "LUC"]
	
	for key in preview_keys:
		var name_lbl = Label.new()
		var total_lbl = Label.new()
		
		# STAT_ID_MAPから翻訳IDを引っ張って tr()
		var stat_id = STAT_ID_MAP[key]
		name_lbl.text = tr(stat_id)
		
		var total_val = current_base_stats[key]
		if key in ALLOCATABLE_STATS:
			total_val += allocated_bonus_stats[key]
			
		total_lbl.text = " %d" % total_val
		
		class_status_preview.add_child(name_lbl)
		class_status_preview.add_child(total_lbl)

func _build_class_columns() -> void:
	for child in basic_class_column.get_children(): child.queue_free()
	for child in advanced_class_column.get_children(): child.queue_free()
		
	# CSVにある接頭辞付きの正規IDで配列を定義
	var basic_ids = [
		"CLASS_FIGHTER", "CLASS_ROGUE", 
		"CLASS_CLERIC", "CLASS_WIZARD"
	]
	var adv_ids = []
	if is_class_group_1_unlocked: adv_ids.append_array(["CLASS_RANGER", "CLASS_KNIGHT"])
	if is_class_group_2_unlocked: adv_ids.append_array(["CLASS_SAMURAI", "CLASS_WARLOCK"])
	if is_class_group_3_unlocked: adv_ids.append_array(["CLASS_NINJA", "CLASS_SAGE"])
	
	var basic_buttons = []
	var adv_buttons = []
	
	for cid in basic_ids:
		var btn = _create_class_button(cid)
		basic_class_column.add_child(btn)
		basic_buttons.append(btn)
		
	for cid in adv_ids:
		var btn = _create_class_button(cid)
		advanced_class_column.add_child(btn)
		adv_buttons.append(btn)
		
	for i in range(basic_buttons.size()):
		var prev = basic_buttons[(i - 1 + basic_buttons.size()) % basic_buttons.size()]
		var next = basic_buttons[(i + 1) % basic_buttons.size()]
		basic_buttons[i].focus_neighbor_top = prev.get_path()
		basic_buttons[i].focus_neighbor_bottom = next.get_path()
		if adv_buttons.size() > 0:
			var target_index = min(i, adv_buttons.size() - 1)
			basic_buttons[i].focus_neighbor_right = adv_buttons[target_index].get_path()
			
	for i in range(adv_buttons.size()):
		var prev = adv_buttons[(i - 1 + adv_buttons.size()) % adv_buttons.size()]
		var next = adv_buttons[(i + 1) % adv_buttons.size()]
		adv_buttons[i].focus_neighbor_top = prev.get_path()
		adv_buttons[i].focus_neighbor_bottom = next.get_path()
		var target_index = min(i, basic_buttons.size() - 1)
		adv_buttons[i].focus_neighbor_left = basic_buttons[target_index].get_path()
		
	if basic_buttons.size() > 0:
		basic_buttons[0].grab_focus.call_deferred()

func _create_class_button(class_id: String) -> Button:
	var btn = Button.new()
	btn.text = tr(class_id) # CSVから直接職業名を取得
		
	btn.focus_mode = Control.FOCUS_ALL
	btn.pressed.connect(_on_class_selected.bind(class_id))
	return btn

func _on_class_selected(class_id: String) -> void:
	final_character_class = class_id
	_proceed_to_appearance_selection()

func _back_to_status_allocation() -> void:
	class_layout.hide()
	_proceed_to_status_allocation()

func _proceed_to_appearance_selection() -> void:
	class_layout.hide()
	if confirmation_layout: confirmation_layout.hide()
	appearance_layout.show()
	
	var current_locale = TranslationServer.get_locale().left(2)
	
	if current_locale == "ja":
		appearance_title_label.text = "見た目を選んでください"
		appearance_random_button.text = "ランダムで決める"
		appearance_back_button.text = "職業選択に戻る"
		appearance_next_button.text = "確認画面に進む"
	else:
		appearance_title_label.text = "Select your appearance."
		appearance_random_button.text = "Choose Randomly"
		appearance_back_button.text = "Back to Class"
		appearance_next_button.text = "Proceed"

	_update_appearance_summary_text(current_locale)
	_build_appearance_status_preview()
	_scan_face_images_folder()
	_build_appearance_wheel()
	_setup_appearance_focus_neighbors()

func _update_appearance_summary_text(current_locale: String) -> void:
	var display_race = tr(final_character_race)
	var display_class = tr(final_character_class)
			
	if current_locale == "ja":
		appearance_summary_label.text = "名前: %s   種族: %s   職業: %s" % [final_character_name, display_race, display_class]
	else:
		appearance_summary_label.text = "Name: %s   Race: %s   Class: %s" % [final_character_name, display_race, display_class]
	
func _build_appearance_status_preview() -> void:
	for child in appearance_status_preview.get_children():
		child.queue_free()
		
	appearance_status_preview.columns = 2
	var preview_keys = ["LV", "HP", "MP", "STR", "DEX", "VIT", "AGI", "INT", "MND", "LUC"]
	
	for key in preview_keys:
		var name_lbl = Label.new()
		var total_lbl = Label.new()
		
		# 前半で定義した STAT_ID_MAP からCSVキーを取得し、tr() で翻訳
		var stat_id = STAT_ID_MAP[key]
		name_lbl.text = tr(stat_id)
		
		var total_val = current_base_stats[key]
		if key in ALLOCATABLE_STATS:
			total_val += allocated_bonus_stats[key]
		total_lbl.text = " %d" % total_val
		
		appearance_status_preview.add_child(name_lbl)
		appearance_status_preview.add_child(total_lbl)

func _scan_face_images_folder() -> void:
	face_image_paths.clear()
	var folder_path = "res://image/face/"
	if DirAccess.dir_exists_absolute(folder_path):
		var dir = DirAccess.open(folder_path)
		if dir:
			dir.list_dir_begin()
			var file_name = dir.get_next()
			while file_name != "":
				if not dir.current_is_dir() and (file_name.ends_with(".png") or file_name.ends_with(".PNG")):
					face_image_paths.append(folder_path + file_name)
				file_name = dir.get_next()
			dir.list_dir_end()
	face_image_paths.sort()

func _build_appearance_wheel() -> void:
	for child in appearance_wheel_control.get_children():
		child.queue_free()
	face_sprites.clear()
	
	if face_image_paths.is_empty():
		var error_label = Label.new()
		error_label.text = "No PNG images found in res://image/face/"
		appearance_wheel_control.add_child(error_label)
		return
		
	for i in range(face_image_paths.size()):
		var texture = load(face_image_paths[i])
		var tex_rect = TextureRect.new()
		tex_rect.texture = texture
		tex_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
		tex_rect.custom_minimum_size = Vector2(120, 120)
		tex_rect.size = Vector2(120, 120)
		tex_rect.pivot_offset = tex_rect.size / 2
		appearance_wheel_control.add_child(tex_rect)
		face_sprites.append(tex_rect)
		
	current_image_index = 0
	target_wheel_angle = 0.0
	current_wheel_angle = 0.0
	_update_wheel_positions()

func _process(_delta: float) -> void:
	if appearance_layout.is_visible_in_tree() and face_image_paths.size() > 0:
		_update_wheel_positions()

func _update_wheel_positions() -> void:
	var total_count = face_sprites.size()
	if total_count == 0: return
	var radius_x = 160.0
	var radius_y = 60.0 
	var center = appearance_wheel_control.size / 2
	
	for i in range(total_count):
		var angle_offset = (i * 2.0 * PI / total_count)
		var sprite_angle = current_wheel_angle + angle_offset
		var cos_val = cos(sprite_angle + PI/2)
		var sin_val = sin(sprite_angle + PI/2)
		
		var pos_x = center.x + cos_val * radius_x
		var pos_y = center.y + sin_val * radius_y
		var sprite = face_sprites[i]
		sprite.position = Vector2(pos_x - sprite.size.x / 2, pos_y - sprite.size.y / 2)
		
		var depth_ratio = (sin_val + 1.0) / 2.0
		var current_size = lerp(100.0, 180.0, depth_ratio)
		sprite.size = Vector2(current_size, current_size)
		sprite.pivot_offset = sprite.size / 2
		
		var brightness = lerp(0.4, 1.0, depth_ratio)
		sprite.modulate = Color(brightness, brightness, brightness, 1.0)
		sprite.z_index = int(depth_ratio * 100)

func _rotate_wheel(direction: int) -> void:
	var total_count = face_image_paths.size()
	if total_count == 0: return
	
	current_image_index = (current_image_index + direction + total_count) % total_count
	var step_angle = (2.0 * PI / total_count)
	var ideal_target_angle = -current_image_index * step_angle
	
	var angle_diff = ideal_target_angle - target_wheel_angle
	angle_diff = fposmod(angle_diff + PI, 2.0 * PI) - PI
	target_wheel_angle += angle_diff
	
	if wheel_tween: wheel_tween.kill()
	wheel_tween = create_tween()
	wheel_tween.set_ease(Tween.EASE_OUT)
	wheel_tween.set_trans(Tween.TRANS_QUAD)
	wheel_tween.tween_property(self, "current_wheel_angle", target_wheel_angle, 0.2)

func _rotate_to_specific_index(idx: int) -> void:
	if face_image_paths.is_empty(): return
	current_image_index = idx
	var step_angle = (2.0 * PI / face_image_paths.size())
	var ideal_target_angle = -current_image_index * step_angle
	var angle_diff = ideal_target_angle - target_wheel_angle
	angle_diff = fposmod(angle_diff + PI, 2.0 * PI) - PI
	target_wheel_angle += angle_diff
	
	if wheel_tween: wheel_tween.kill()
	wheel_tween = create_tween()
	wheel_tween.set_ease(Tween.EASE_OUT)
	wheel_tween.set_trans(Tween.TRANS_CUBIC)
	wheel_tween.tween_property(self, "current_wheel_angle", target_wheel_angle, 0.4)

func _setup_appearance_focus_neighbors() -> void:
	var wheel_focus_node = appearance_wheel_control
	if appearance_wheel_control.focus_mode == Control.FOCUS_NONE:
		appearance_wheel_control.focus_mode = Control.FOCUS_ALL
		
	if not wheel_focus_node.focus_entered.is_connected(_on_wheel_focus_entered):
		wheel_focus_node.focus_entered.connect(_on_wheel_focus_entered)
	if not wheel_focus_node.focus_exited.is_connected(_on_wheel_focus_exited):
		wheel_focus_node.focus_exited.connect(_on_wheel_focus_exited)
		
	wheel_focus_node.focus_neighbor_left = wheel_focus_node.get_path()
	wheel_focus_node.focus_neighbor_right = wheel_focus_node.get_path()
	wheel_focus_node.focus_neighbor_bottom = appearance_random_button.get_path()
	
	appearance_random_button.focus_neighbor_top = wheel_focus_node.get_path()
	appearance_random_button.focus_neighbor_left = appearance_next_button.get_path()
	appearance_random_button.focus_neighbor_right = appearance_back_button.get_path()
	
	appearance_back_button.focus_neighbor_top = wheel_focus_node.get_path()
	appearance_back_button.focus_neighbor_left = appearance_random_button.get_path()
	appearance_back_button.focus_neighbor_right = appearance_next_button.get_path()
	
	appearance_next_button.focus_neighbor_top = wheel_focus_node.get_path()
	appearance_next_button.focus_neighbor_left = appearance_back_button.get_path()
	appearance_next_button.focus_neighbor_right = appearance_random_button.get_path()
	
	wheel_focus_node.grab_focus.call_deferred()

func _on_wheel_focus_entered() -> void: is_wheel_focused = true
func _on_wheel_focus_exited() -> void: is_wheel_focused = false

func _on_appearance_random_pressed() -> void:
	if face_image_paths.is_empty(): return
	_rotate_to_specific_index(randi() % face_image_paths.size())

func _on_appearance_back_pressed() -> void:
	_back_to_class_selection()

func _back_to_class_selection() -> void:
	if wheel_tween: wheel_tween.kill()
	appearance_layout.hide()
	_proceed_to_class_selection()

func _on_appearance_next_pressed() -> void:
	if face_image_paths.is_empty(): return
	final_character_image_path = face_image_paths[current_image_index]
	_proceed_to_confirmation_screen()

# 確認画面
func _proceed_to_confirmation_screen() -> void:
	appearance_layout.hide()
	confirmation_layout.show()
	
	var current_locale = TranslationServer.get_locale().left(2)
	
	# 1. 見出し・ボタンのローカライズ設定
	if current_locale == "ja":
		confirm_title_label.text = "このキャラクターを作成しますか?"
		confirm_back_button.text = "見た目選択に戻る"
		confirm_create_button.text = "作成する"
	else:
		confirm_title_label.text = "Create this character?"
		confirm_back_button.text = "Back to Appearance"
		confirm_create_button.text = "Create"
		
	# 2. サマリー表示のコピー(名前・種族・職業)
	confirm_summary_label.text = appearance_summary_label.text
	
	# 3. 左側:ステータス合計値表示の構築
	_build_confirm_status_preview()
	
	# 4. 右側:選択した見た目画像を表示(★サイズを 256x256 に統一)
	if FileAccess.file_exists(final_character_image_path):
		confirm_face_preview.texture = load(final_character_image_path)
		confirm_face_preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
		confirm_face_preview.custom_minimum_size = Vector2(256, 256)
		confirm_face_preview.size = Vector2(256, 256)
	
	# 5. フォーカス移動ルート設定
	confirm_back_button.focus_neighbor_left = confirm_create_button.get_path()
	confirm_back_button.focus_neighbor_right = confirm_create_button.get_path()
	confirm_create_button.focus_neighbor_left = confirm_back_button.get_path()
	confirm_create_button.focus_neighbor_right = confirm_back_button.get_path()
	
	# 最初から「作成する」ボタンにフォーカスを当てる
	confirm_create_button.grab_focus.call_deferred()

# 左側:ステータスの表示(合計値)
func _build_confirm_status_preview() -> void:
	for child in confirm_status_preview.get_children():
		child.queue_free()
		
	confirm_status_preview.columns = 2
	var preview_keys = ["LV", "HP", "MP", "STR", "DEX", "VIT", "AGI", "INT", "MND", "LUC"]
	
	for key in preview_keys:
		var name_lbl = Label.new()
		var total_lbl = Label.new()
		
		# STAT_ID_MAP からCSVキーを取得し、tr() で翻訳
		var stat_id = STAT_ID_MAP[key]
		name_lbl.text = tr(stat_id)
		
		var total_val = current_base_stats[key]
		if key in ALLOCATABLE_STATS:
			total_val += allocated_bonus_stats[key]
		total_lbl.text = " %d" % total_val
		
		confirm_status_preview.add_child(name_lbl)
		confirm_status_preview.add_child(total_lbl)

# 「見た目選択に戻る」ボタン処理
func _on_confirm_back_pressed() -> void:
	_back_to_appearance_selection()

func _back_to_appearance_selection() -> void:
	confirmation_layout.hide()
	_proceed_to_appearance_selection()

# 「作成する」ボタン処理
func _on_confirm_create_pressed() -> void:
	confirmation_layout.hide()
	message_layout.show()
	
	# 1. 最終ステータス(ベース値 + ボーナス値)の合算辞書を作成
	var final_stats: Dictionary = {}
	for key in ALLOCATABLE_STATS:
		final_stats[key] = current_base_stats[key] + allocated_bonus_stats[key]
	
	# 2. 逆変換マッパーを廃止し、CSVのID文字列をそのまま使用します
	var class_id: String = final_character_class
	var race_id: String = final_character_race
	
	# 3. キャラクタークラス(器)のインスタンスを生成してデータを流し込む
	var new_adventurer = CharaData.new(
		final_character_name,
		race_id,
		class_id,
		final_character_image_path,
		final_stats,
		current_base_stats["HP"], 
		current_base_stats["MP"]  
	)
	
	# 4. ギルドマネージャーの管理下にデータを追加する
	GuildManager.register_new_adventurer(new_adventurer)
	
	# デバッグ確認用
	print("--- 新しい冒険者がデータクラスに格納されました ---")
	print("名前: ", new_adventurer.chara_name)
	print("種族(内部ID): ", new_adventurer.race, " (表示名: ", new_adventurer.get_display_race(), ")")
	print("職業(内部ID): ", new_adventurer.chara_class, " (表示名: ", new_adventurer.get_display_class(), ")")
	print("画像パス: ", new_adventurer.face_image_path)
	print("STR: ", new_adventurer.str_val, " / AGI: ", new_adventurer.agi_val, " / LUC: ", new_adventurer.luc_val)
	print("暫定近接攻撃力: ", new_adventurer.get_melee_attack())
	print("--------------------------------------------------")
	
	# 5. 登録完了メッセージの表示
	var current_locale = TranslationServer.get_locale().left(2)
	if current_locale == "ja":
		message_text_label.text = "名前【%s】がギルドに登録されました。" % final_character_name
	else:
		message_text_label.text = "Name [%s] has been registered to the guild." % final_character_name
			
	message_layout.focus_mode = Control.FOCUS_ALL
	message_layout.grab_focus.call_deferred()

# メッセージ確認後、ギルドメニューへ戻る
func _return_to_guild_menu() -> void:
	message_layout.hide()
	hide() 
	creation_canceled.emit() 
	
# 外部(GuildManager)から上限制限時に呼び出されるエラー表示関数
func activate_limit_error() -> void:
	show()
	$NameLayout.hide()
	if race_layout: race_layout.hide()
	if status_layout: status_layout.hide()
	if class_layout: class_layout.hide()
	if appearance_layout: appearance_layout.hide()
	if confirmation_layout: confirmation_layout.hide()
	
	if message_layout:
		message_layout.show()
		
		var current_locale = TranslationServer.get_locale().left(2).to_lower()
		if current_locale == "ja":
			message_text_label.text = "これ以上は冒険者を作成できません"
		else:
			message_text_label.text = "You cannot create any more adventurers."
			
		message_layout.focus_mode = Control.FOCUS_ALL
		message_layout.grab_focus.call_deferred()

## 自身のすべての内部ステータスをシリアライズ(辞書化)して返す
func serialize() -> Dictionary:
	return {
		"chara_name": chara_name,
		"chara_class": chara_class,
		"race": race,
		"face_image_path": face_image_path,
		"level": level,
		"current_hp": current_hp,
		"max_hp": max_hp,
		"current_mp": current_mp,
		"max_mp": max_mp,
		"exp_points": exp_points,
		"str_val": str_val,
		"dex_val": dex_val,
		"vit_val": vit_val,
		"agi_val": agi_val,
		"int_val": int_val,
		"mnd_val": mnd_val,
		"luc_val": luc_val,
		"condition_id": condition_id,
		"class_points": class_points.duplicate(), # 参照ではなく複製して保存
		"class_skills": class_skills.duplicate(),
		"equipped_skill": equipped_skill,
		"additional_skills": additional_skills.duplicate()
		# ※ equipment は現状 null のため必要に応じて拡張
	}

## 辞書データからインスタンスを作成して返す静的関数 (ファクトリメソッド)
static func deserialize(data: Dictionary) -> CharaData:
	# コンストラクタをダミーで通すための最小限の枠(直後にデータで全上書きするため何でもよい)
	var empty_stats = {"STR": 0, "DEX": 0, "VIT": 0, "AGI": 0, "INT": 0, "MND": 0, "LUC": 0}
	var chara = CharaData.new("", "", "", "", empty_stats, 0, 0)
	
	# 保存された値をそのまま完全上書き
	chara.chara_name = data.get("chara_name", "")
	chara.chara_class = data.get("chara_class", "")
	chara.race = data.get("race", "")
	chara.face_image_path = data.get("face_image_path", "")
	chara.level = int(data.get("level", 1))
	chara.current_hp = int(data.get("current_hp", 0))
	chara.max_hp = int(data.get("max_hp", 0))
	chara.current_mp = int(data.get("current_mp", 0))
	chara.max_mp = int(data.get("max_mp", 0))
	chara.exp_points = int(data.get("exp_points", 0))
	chara.str_val = int(data.get("str_val", 0))
	chara.dex_val = int(data.get("dex_val", 0))
	chara.vit_val = int(data.get("vit_val", 0))
	chara.agi_val = int(data.get("agi_val", 0))
	chara.int_val = int(data.get("int_val", 0))
	chara.mnd_val = int(data.get("mnd_val", 0))
	chara.luc_val = int(data.get("luc_val", 0))
	chara.condition_id = data.get("condition_id", "Good")
	chara.class_points = data.get("class_points", {}).duplicate()
	
	# 型安全のために Array[String] に明示的変換して代入
	chara.class_skills.clear()
	for s in data.get("class_skills", []): chara.class_skills.append(str(s))
	chara.equipped_skill = data.get("equipped_skill", "")
	chara.additional_skills.clear()
	for s in data.get("additional_skills", []): chara.additional_skills.append(str(s))
	
	chara.update_display_parameters()
	return chara

キャラクター作成用スクリプト (character_creation.gd) の上記のように変更してください。これで、25人目を登録しようとするとエラーが出るようになります。

「クラスを変える」機能

クラス(職業)を変える機能の実装です。クラスは冒険者ギルドでしか変更できません。このゲームでは、ステータスが何であれ選択可能な職業に就くことができます。

ノード構成の作成

  • CanvasLayer (CanvasLayer)
    • (他は省略)
    • CharacterCreation (Control)
    • JoinParty (Control)
    • LeaveParty (Control)
    • ChangeClass (Control) 新規
      • Background (ColorRect) 新規
      • MainVBox (VBoxContainer) 新規
        • MessageLabel (Label) 新規
        • MemberList (VBoxContainer) 新規
          • Row1 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row2 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row3 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row4 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row5 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row6 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
        • ClassLayoutContent (HBoxContainer) 新規
          • ClassStatus (GridContainer) 新規
          • ClassSelectionArea (HBoxContainer) 新規
            • BasicClassColumn (VBoxContainer) 新規
            • AdvancdClassColumn (VBoxContainer) 新規
        • NavigationHBox (HBoxContainer) 新規
          • BackButton (Button) 新規
ChangeClass (Control)新規作成 非表示
Background (ColorRect)画面全体を覆う黒塗りの背景。必ず MainVBox より上に配置すること。(カラー: 黒、レイアウト:Full Rect)
MainVBox (VBoxContainer)「冒険者をあずける」の画面全般
MessageLabel (Label)画面上部のメッセージ
「パーティから外す冒険者を選択してください」
MemberList (VBoxContainer)画面中央部に冒険者を6名表示する
Row1~6 (HBoxContainer)パーティメンバー最大6名
前衛から並ぶようにする
CursorBtn (Button)カーソル。外す冒険者を選ぶもの
FaceImg (TextureRect)冒険者の見た目画像
InfoLabel(Label)冒険者の名前、レベル、職業、最大HP、最大MP
ClassLayoutContent (HBoxContainer)職業選択画面用
ClassStatus (GridContainer)選択された冒険者のステータス
ClassSelectionArea (HBoxContainer)職業一覧
BasicClassColumn (VBoxContainer)基本職用表示枠
AdvancdClassColumn (VBoxContainer)追加職用表示枠
NavigationHBox (HBoxContainer)画面下部のボタン
BackButton (Button)ギルドに戻る

このノード構成を作成します。ChangeClass (Control) の非表示を忘れずに実施してください。

スクリプトファイルの作成

extends Control

# ==============================================================================
# change_class.gd : パーティメンバーのクラス(職業)を変更する画面のUI制御
# ==============================================================================

@onready var message_label: Label = $MainVBox/MessageLabel
@onready var member_list_container: VBoxContainer = $MainVBox/MemberList

# CharacterCreationの構造を完全踏襲
@onready var class_layout_content: HBoxContainer = $MainVBox/ClassLayoutContent
@onready var class_status: GridContainer = $MainVBox/ClassLayoutContent/ClassStatus
@onready var class_selection_area: HBoxContainer = $MainVBox/ClassLayoutContent/ClassSelectionArea
@onready var basic_class_column: VBoxContainer = $MainVBox/ClassLayoutContent/ClassSelectionArea/BasicClassColumn
@onready var advanced_class_column: VBoxContainer = $MainVBox/ClassLayoutContent/ClassSelectionArea/AdvancedClassColumn

@onready var navigation_hbox: HBoxContainer = $MainVBox/NavigationHBox
@onready var back_button: Button = $MainVBox/NavigationHBox/BackButton

# システムのアンロックフラグ
var is_class_group_1_unlocked: bool = false
var is_class_group_2_unlocked: bool = false
var is_class_group_3_unlocked: bool = false

var rows: Array[HBoxContainer] = []
var _party_characters: Array[CharaData] = []
var _target_character: CharaData = null

enum Phase { MEMBER_SELECT, CLASS_SELECT, MESSAGE_OUT }
var _current_phase: Phase = Phase.MEMBER_SELECT

# 正確な全14職・全10ステータスのローカライズデータ
const TEXT_DATA = {
	"en": {
		"title_member": "Select a party member to change class",
		"title_class": "Select a class",
		"btn_back": "Back to Guild",
		"err_no_party": "There are no members in the party.",
		"confirm_msg": "Name [%s]'s class has been changed to [%s]."
	},
	"ja": {
		"title_member": "クラスを変えるパーティメンバーを選択してください",
		"title_class": "クラスを選択してください",
		"btn_back": "ギルドに戻る",
		"err_no_party": "パーティメンバーがいません",
		"confirm_msg": "名前【%s】の職業が【%s】に変更されました"
	}
}

# CSVのid列に定義されたステータスキーへのマッピング
const STAT_ID_MAP = {
	"LV": "STATUS_LEVEL",
	"HP": "STATUS_HP",
	"MP": "STATUS_MP",
	"STR": "STATUS_STR",
	"DEX": "STATUS_DEX",
	"VIT": "STATUS_VIT",
	"AGI": "STATUS_AGI",
	"INT": "STATUS_INT",
	"MND": "STATUS_MND",
	"LUC": "STATUS_LUC"
}

func _get_current_lang() -> String:
	var locale = TranslationServer.get_locale().left(2).to_lower()
	if locale == "ja":
		return "ja"
	return "en"

func _ready() -> void:
	for i in range(1, 7):
		var row_node = member_list_container.get_node_or_null("Row" + str(i))
		if row_node:
			rows.append(row_node as HBoxContainer)
			
	back_button.pressed.connect(_on_back_button_pressed)
	hide()

func activate() -> void:
	_target_character = null
	var lang = _get_current_lang()
	back_button.text = TEXT_DATA[lang]["btn_back"]
	
	show()
	
	# ガードルック:パーティメンバーが0名の場合
	_party_characters = PartyManager.get_party_members()
	if _party_characters.size() == 0:
		_show_no_party_error()
		return
		
	_switch_to_member_select_phase()

func _show_no_party_error() -> void:
	_current_phase = Phase.MESSAGE_OUT
	member_list_container.hide()
	class_layout_content.hide()
	navigation_hbox.hide()
	
	var lang = _get_current_lang()
	message_label.text = TEXT_DATA[lang]["err_no_party"]
	release_focus()

func _switch_to_member_select_phase() -> void:
	_current_phase = Phase.MEMBER_SELECT
	class_layout_content.hide()
	
	var lang = _get_current_lang()
	message_label.text = TEXT_DATA[lang]["title_member"]
	
	member_list_container.show()
	navigation_hbox.show()
	back_button.show()
	
	_update_member_list()

func _update_member_list() -> void:
	var focusable_buttons: Array[Button] = []
	
	for i in range(6):
		var row = rows[i]
		var cursor_btn = row.get_child(0) as Button
		
		for connection in cursor_btn.pressed.get_connections():
			cursor_btn.pressed.disconnect(connection.callable)
			
		if i < _party_characters.size():
			var c: CharaData = _party_characters[i]
			row.show()
			
			var face_img = row.get_child(1) as TextureRect
			var info_label = row.get_child(2) as Label
			
			cursor_btn.text = "▶"
			cursor_btn.disabled = false
			
			var callable = func(target: CharaData): _on_member_selected(target)
			cursor_btn.pressed.connect(callable.bind(c))
			
			if FileAccess.file_exists(c.face_image_path):
				face_img.texture = load(c.face_image_path)
			else:
				face_img.texture = null
			face_img.custom_minimum_size = Vector2(64, 64)
			
			info_label.text = "[%s] LV:%d %s (HP:%d MP:%d)" % [
				c.chara_name, c.level, c.get_display_class(),
				c.max_hp, c.max_mp
			]
			
			focusable_buttons.append(cursor_btn)
		else:
			row.hide()
			cursor_btn.disabled = true
			
	focusable_buttons.append(back_button)
	
	for i in range(focusable_buttons.size()):
		var prev = focusable_buttons[(i - 1 + focusable_buttons.size()) % focusable_buttons.size()]
		var next = focusable_buttons[(i + 1) % focusable_buttons.size()]
		focusable_buttons[i].focus_neighbor_top = prev.get_path()
		focusable_buttons[i].focus_neighbor_bottom = next.get_path()
		
	if focusable_buttons.size() > 1:
		focusable_buttons[0].grab_focus()
	else:
		back_button.grab_focus()

func _on_member_selected(chara: CharaData) -> void:
	_target_character = chara
	_switch_to_class_select_phase()

func _switch_to_class_select_phase() -> void:
	_current_phase = Phase.CLASS_SELECT
	
	member_list_container.hide()
	back_button.hide()
	navigation_hbox.hide()
	
	var lang = _get_current_lang()
	message_label.text = TEXT_DATA[lang]["title_class"]
	
	class_layout_content.show()
	class_status.show()
	class_selection_area.show()
	basic_class_column.show()
	advanced_class_column.show()
	
	_update_status_grid()
	_build_class_columns_for_change()

func _update_status_grid() -> void:
	if not _target_character or not class_status:
		return
		
	for child in class_status.get_children():
		child.queue_free()
		
	class_status.columns = 2
	var preview_keys = ["LV", "HP", "MP", "STR", "DEX", "VIT", "AGI", "INT", "MND", "LUC"]
	
	for key in preview_keys:
		var name_lbl = Label.new()
		var total_lbl = Label.new()
		
		var stat_csv_id = STAT_ID_MAP[key]
		name_lbl.text = tr(stat_csv_id)
		
		var total_val = 0
		match key:
			"LV": total_val = _target_character.level
			"HP": total_val = _target_character.max_hp
			"MP": total_val = _target_character.max_mp
			"STR": total_val = _target_character.str_val
			"DEX": total_val = _target_character.dex_val
			"VIT": total_val = _target_character.vit_val
			"AGI": total_val = _target_character.agi_val
			"INT": total_val = _target_character.int_val
			"MND": total_val = _target_character.mnd_val
			"LUC": total_val = _target_character.luc_val
			
		total_lbl.text = " %d" % total_val
		
		class_status.add_child(name_lbl)
		class_status.add_child(total_lbl)

func _build_class_columns_for_change() -> void:
	for child in basic_class_column.get_children(): child.queue_free()
	for child in advanced_class_column.get_children(): child.queue_free()
	
	var basic_ids = [
		"CLASS_FIGHTER", "CLASS_ROGUE", 
		"CLASS_CLERIC", "CLASS_WIZARD"
	]
	var adv_ids = []
	
	if is_class_group_1_unlocked: adv_ids.append_array(["CLASS_RANGER", "CLASS_KNIGHT"])
	if is_class_group_2_unlocked: adv_ids.append_array(["CLASS_SAMURAI", "CLASS_WARLOCK"])
	if is_class_group_3_unlocked: adv_ids.append_array(["CLASS_NINJA", "CLASS_SAGE"])
	
	var basic_buttons = []
	var adv_buttons = []
	
	for cid in basic_ids:
		var btn = _create_class_button_for_change(cid)
		basic_class_column.add_child(btn)
		basic_buttons.append(btn)
		
	for cid in adv_ids:
		var btn = _create_class_button_for_change(cid)
		advanced_class_column.add_child(btn)
		adv_buttons.append(btn)
		
	for i in range(basic_buttons.size()):
		var prev = basic_buttons[(i - 1 + basic_buttons.size()) % basic_buttons.size()]
		var next = basic_buttons[(i + 1) % basic_buttons.size()]
		basic_buttons[i].focus_neighbor_top = prev.get_path()
		basic_buttons[i].focus_neighbor_bottom = next.get_path()
		if adv_buttons.size() > 0:
			var target_index = min(i, adv_buttons.size() - 1)
			basic_buttons[i].focus_neighbor_right = adv_buttons[target_index].get_path()
			
	for i in range(adv_buttons.size()):
		var prev = adv_buttons[(i - 1 + adv_buttons.size()) % adv_buttons.size()]
		var next = adv_buttons[(i + 1) % adv_buttons.size()]
		adv_buttons[i].focus_neighbor_top = prev.get_path()
		adv_buttons[i].focus_neighbor_bottom = next.get_path()
		var target_index = min(i, basic_buttons.size() - 1)
		adv_buttons[i].focus_neighbor_left = basic_buttons[target_index].get_path()
		
	if basic_buttons.size() > 0:
		basic_buttons[0].grab_focus.call_deferred()

func _create_class_button_for_change(class_id: String) -> Button:
	var btn = Button.new()
	btn.text = tr(class_id)
	btn.focus_mode = Control.FOCUS_ALL
	btn.pressed.connect(_on_class_button_pressed.bind(class_id))
	return btn

func _setup_class_selection_focus() -> void:
	var basic_btns: Array[Button] = []
	var adv_btns: Array[Button] = []
	
	for child in basic_class_column.get_children():
		if child is Button and child.is_visible_in_tree():
			basic_btns.append(child)
			
	for child in advanced_class_column.get_children():
		if child is Button and child.is_visible_in_tree():
			adv_btns.append(child)
			
	for i in range(basic_btns.size()):
		var prev = basic_btns[(i - 1 + basic_btns.size()) % basic_btns.size()]
		var next = basic_btns[(i + 1) % basic_btns.size()]
		basic_btns[i].focus_neighbor_top = prev.get_path()
		basic_btns[i].focus_neighbor_bottom = next.get_path()
		
		if adv_btns.size() > 0:
			var target_idx = clampi(i, 0, adv_btns.size() - 1)
			basic_btns[i].focus_neighbor_right = adv_btns[target_idx].get_path()
		else:
			basic_btns[i].focus_neighbor_right = basic_btns[i].get_path()
			
	for i in range(adv_btns.size()):
		var prev = adv_btns[(i - 1 + adv_btns.size()) % adv_btns.size()]
		var next = adv_btns[(i + 1) % adv_btns.size()]
		adv_btns[i].focus_neighbor_top = prev.get_path()
		adv_btns[i].focus_neighbor_bottom = next.get_path()
		
		if basic_btns.size() > 0:
			var target_idx = clampi(i, 0, basic_btns.size() - 1)
			adv_btns[i].focus_neighbor_left = basic_btns[target_idx].get_path()
			
	if basic_btns.size() > 0:
		basic_btns[0].grab_focus()

# 引数 node_name には "CLASS_FIGHTER" などのCSVキー文字列が渡されます
func _on_class_button_pressed(node_name: String) -> void:
	if not _target_character:
		return
		
	# CharaDataのchara_classにCSVのキー文字列(CLASS_XXXX)をそのまま保存
	_target_character.chara_class = node_name
	
	if _target_character.has_method("update_display_parameters"):
		_target_character.update_display_parameters()
		
	_show_completion_message(node_name)

func _show_completion_message(class_id_key: String) -> void:
	_current_phase = Phase.MESSAGE_OUT
	member_list_container.hide()
	class_layout_content.hide()
	navigation_hbox.hide()
	
	var lang = _get_current_lang()
	
	# 渡されたCSVキー(CLASS_XXXX)を直接 tr() で翻訳して表示名にする
	var class_display_name = tr(class_id_key)
		
	# 確定したローカライズ表示名を使ってメッセージをフォーマット
	var formatted_msg = TEXT_DATA[lang]["confirm_msg"] % [_target_character.chara_name, class_display_name]
	
	message_label.text = formatted_msg
	release_focus()

func _on_back_button_pressed() -> void:
	hide()
	GuildManager.return_to_guild_main_menu()

func _unhandled_input(event: InputEvent) -> void:
	if not is_visible_in_tree():
		return
		
	if _current_phase == Phase.MESSAGE_OUT:
		if event.is_action_pressed("ui_accept") or event.is_action_pressed("ui_cancel"):
			get_viewport().set_input_as_handled()
			_on_back_button_pressed()
		return
		
	if _current_phase == Phase.CLASS_SELECT:
		if event.is_action_pressed("ui_cancel"):
			get_viewport().set_input_as_handled()
			_switch_to_member_select_phase()
		return

	if _current_phase == Phase.MEMBER_SELECT:
		if event.is_action_pressed("ui_cancel"):
			get_viewport().set_input_as_handled()
			_on_back_button_pressed()
		return

ChangeClass ノードを選択した状態で、新規スクリプト change_class.gd を作成し、アタッチしてください。コードは上記の通りです。主に「冒険者を削除する」画面の表示部分を担当しています。

ギルド管理用コード修正

extends Node

# ==============================================================================
# 冒険者ギルド 管理マネージャー (UI・ロジック完全版)
# ==============================================================================

const SYSTEM_MAX_SLOTS: int = 25
const MAX_CREATE_LIMIT: int = 24

var registered_members: Array[CharaData] = []
var town_system: TownSystem = null
var join_party_node: Control = null
var leave_party_node: Control = null
var delete_character_node: Control = null

# ギルドメニューの項目定義
const GUILD_MENU_KEYS = [
	"GUILD_MENU_JOIN", 
	"GUILD_MENU_REMOVE", 
	"GUILD_MENU_CREATE", 
	"GUILD_MENU_CHANGE_CLASS", 
	"GUILD_MENU_DELETE", 
	"GUILD_MENU_CHANGE_NAME", 
	"GUILD_MENU_CHANGE_APPEARANCE", 
	"GUILD_MENU_BACK"
]

# 呼び出し画面用のページング管理変数
var _call_list_cache: Array[CharaData] = []
var _current_page: int = 0
var _selected_row_index: int = 0
var _row_buttons: Array[Button] = []

# ==============================================================================
# UI制御・画面遷移ハンドリング
# ==============================================================================

# 街からギルドに入ったときに呼び出される関数
func open_guild(town: TownSystem) -> void:
	town_system = town
	
	if town_system.creation_node:
		join_party_node = town_system.creation_node.get_parent().get_node_or_null("JoinParty")
		leave_party_node = town_system.creation_node.get_parent().get_node_or_null("LeaveParty")
		change_class_node = town_system.creation_node.get_parent().get_node_or_null("ChangeClass")
		delete_character_node = town_system.creation_node.get_parent().get_node_or_null("DeleteCharacter")
	
	town_system._update_graphic("guild")
	if town_system.town_menu:
		town_system.town_menu.hide()
	
	town_system.cleanup_dynamic_menus()
	
	if town_system.creation_node and not town_system.creation_node.is_connected("creation_canceled", _on_creation_canceled):
		town_system.creation_node.creation_canceled.connect(_on_creation_canceled)
		
	_show_guild_menu()

# ギルドのメインメニューに戻る(JoinParty画面を閉じたときなどに使用)
func return_to_guild_main_menu() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
		_show_guild_menu()

func _show_guild_menu() -> void:
	var center_container = CenterContainer.new()
	center_container.name = "DynamicMenu"
	center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(center_container)
	
	var menu_panel = VBoxContainer.new()
	center_container.add_child(menu_panel)
	
	var buttons = []
	for key in GUILD_MENU_KEYS:
		var btn = Button.new()
		btn.text = tr(key)
		btn.pressed.connect(func(): _on_guild_menu_selected(key))
		menu_panel.add_child(btn)
		buttons.append(btn)
	
	for i in range(buttons.size()):
		var prev = buttons[(i - 1 + buttons.size()) % buttons.size()]
		var next = buttons[(i + 1) % buttons.size()]
		buttons[i].focus_neighbor_top = prev.get_path()
		buttons[i].focus_neighbor_bottom = next.get_path()
	
	buttons[0].grab_focus()

func _on_guild_menu_selected(key: String) -> void:
	if key == "GUILD_MENU_BACK":
		prepare_return_to_town()
		return
		
	match key:
		"GUILD_MENU_JOIN":
			_on_call_out_selected()
		"GUILD_MENU_REMOVE":
			_on_leave_party_selected()
		"GUILD_MENU_CREATE":
			_on_create_adventurer_selected()
		"GUILD_MENU_CHANGE_CLASS":
			_on_change_class_selected()
		"GUILD_MENU_DELETE":
			print("TODO: 冒険者を削除する UIを開く")
		"GUILD_MENU_CHANGE_NAME":
			print("TODO: 名前を変える UIを開く")
		"GUILD_MENU_CHANGE_APPEARANCE":
			print("TODO: 見た目を変える UIを開く")

# 冒険者をあずける
func _on_leave_party_selected() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	if leave_party_node:
		leave_party_node.activate()

# 現在ギルド(待機所)にいるメンバーの総数を返すヘルパー関数
func get_registered_count() -> int:
	return PartyManager.get_members_in_guild().size()

# 冒険者をよびだす(マネージャー側ロジック)            
func _on_call_out_selected() -> void:
	# ギルドメニューUIを一時消去
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	# パーティの人数チェックと、ギルド内のメンバーリスト取得
	var is_party_full = PartyManager.is_full()
	var guild_members = PartyManager.get_members_in_guild()
	
	# 状態をそのまま JoinParty ノードに丸投げする
	if join_party_node:
		join_party_node.activate(guild_members, is_party_full)

# メッセージ単体表示用の簡易関数
func _show_simple_message(msg_text: String) -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	var center_container = CenterContainer.new()
	center_container.name = "DynamicMenu"
	center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(center_container)
	
	var vbox = VBoxContainer.new()
	center_container.add_child(vbox)
	
	var label = Label.new()
	label.text = msg_text
	vbox.add_child(label)
	
	var ok_btn = Button.new()
	ok_btn.text = "閉じる"
	ok_btn.pressed.connect(return_to_guild_main_menu)
	vbox.add_child(ok_btn)
	
	ok_btn.grab_focus()

func _open_call_out_ui() -> void:
	town_system.cleanup_dynamic_menus()
	
	# ベースとなる全画面黒背景のコンテナ
	var bg = ColorRect.new()
	bg.name = "DynamicMenu"
	bg.color = Color.BLACK
	bg.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(bg)
	
	# メインレイアウト(縦並び)
	var main_vbox = VBoxContainer.new()
	main_vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
	main_vbox.offset_left = 20
	main_vbox.offset_top = 20
	main_vbox.offset_right = -20
	main_vbox.offset_bottom = -20
	bg.add_child(main_vbox)
	
	# 画面上部:メッセージ
	var title_label = Label.new()
	title_label.text = "呼び出す冒険者を選択してください"
	title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	main_vbox.add_child(title_label)
	
	# 隙間
	var spacer = Control.new()
	spacer.custom_minimum_size.y = 10
	main_vbox.add_child(spacer)
	
	# 画面中央:メンバー一覧(縦並びのコンテナ)
	var list_vbox = VBoxContainer.new()
	list_vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
	main_vbox.add_child(list_vbox)
	
	# ページ内の表示対象をスライス (6名ずつ)
	var start_idx = _current_page * 6
	var page_members = _call_list_cache.slice(start_idx, start_idx + 6)
	
	_row_buttons.clear()
	
	for i in range(page_members.size()):
		var m: CharaData = page_members[i]
		
		# 横並びの行コンテナ
		var row_hbox = HBoxContainer.new()
		list_vbox.add_child(row_hbox)
		
		# 矢印カーソル代わりの選択ボタン(右向き矢印「▶」)
		var select_btn = Button.new()
		select_btn.text = "▶"
		select_btn.flat = true # ボタンの枠線を消して矢印だけに見せる
		# 決定された時の処理
		select_btn.pressed.connect(func(): _execute_call_out(m))
		row_hbox.add_child(select_btn)
		_row_buttons.append(select_btn)
		
		# 見た目画像
		var img_rect = TextureRect.new()
		if FileAccess.file_exists(m.face_image_path):
			img_rect.texture = load(m.face_image_path)
		img_rect.custom_minimum_size = Vector2(40, 40)
		img_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
		row_hbox.add_child(img_rect)
		
		# ステータス文字列の結合
		# 例: (1) 名前、レベル、職業、最大HP、最大MP
		var info_label = Label.new()
		info_label.text = "【%s】 LV:%d %s (HP:%d/%d MP:%d/%d)" % [
			m.chara_name, m.level, m.get_display_class(), 
			m.current_hp, m.max_hp, m.current_mp, m.max_mp
		]
		row_hbox.add_child(info_label)
	
	# 画面下部:ナビゲーションボタン
	var nav_hbox = HBoxContainer.new()
	nav_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
	main_vbox.add_child(nav_hbox)
	
	var next_btn = Button.new()
	next_btn.text = "次の一覧に進む"
	next_btn.pressed.connect(_on_next_page_pressed)
	nav_hbox.add_child(next_btn)
	
	var back_menu_btn = Button.new()
	back_menu_btn.text = "ギルドに戻る"
	back_menu_btn.pressed.connect(_show_guild_menu)
	nav_hbox.add_child(back_menu_btn)
	
	# フォーカス近隣の動的接続(循環設定)
	var all_focusable = []
	all_focusable.append_array(_row_buttons)
	all_focusable.append(next_btn)
	all_focusable.append(back_menu_btn)
	
	for i in range(all_focusable.size()):
		var prev = all_focusable[(i - 1 + all_focusable.size()) % all_focusable.size()]
		var next = all_focusable[(i + 1) % all_focusable.size()]
		all_focusable[i].focus_neighbor_top = prev.get_path()
		all_focusable[i].focus_neighbor_bottom = next.get_path()
		
	# 最初の矢印にフォーカス
	if _row_buttons.size() > 0:
		_row_buttons[0].grab_focus()
	else:
		next_btn.grab_focus()

# 「次の一覧に進む」が押された時
func _on_next_page_pressed() -> void:
	# ギルドに6名以上いる場合のみページを進める
	if _call_list_cache.size() <= 6:
		return
		
	_current_page += 1
	# 最後のメンバー一覧を超えたら最初のページに戻る
	if (_current_page * 6) >= _call_list_cache.size():
		_current_page = 0
		
	_open_call_out_ui()

# 実際にパーティに加入させる処理
func _execute_call_out(chara: CharaData) -> void:
	# パーティの空き枠(前衛・後衛)に自動追加
	var slot_assigned = PartyManager.auto_add_member(chara)
	if slot_assigned != -1:
		var msg = "名前【%s】がパーティに加わった。" % chara.chara_name
		_show_guild_message(msg)

# クラスを変える
func _on_change_class_selected() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	if change_class_node:
		change_class_node.activate()

# 汎用メッセージ画面(1ボタンでギルドメニューに戻る)           
func _show_guild_message(msg_text: String) -> void:
	town_system.cleanup_dynamic_menus()
	
	var bg = ColorRect.new()
	bg.name = "DynamicMenu"
	bg.color = Color.BLACK
	bg.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(bg)
	
	var vbox = VBoxContainer.new()
	vbox.set_anchors_preset(Control.PRESET_CENTER)
	bg.add_child(vbox)
	
	var label = Label.new()
	label.text = msg_text
	label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	vbox.add_child(label)
	
	var spacer = Control.new()
	spacer.custom_minimum_size.y = 15
	vbox.add_child(spacer)
	
	var ok_btn = Button.new()
	ok_btn.text = "閉じる"
	ok_btn.pressed.connect(_show_guild_menu)
	vbox.add_child(ok_btn)
	
	ok_btn.grab_focus()
	
# ----------------------------------------------------------------              
# その他の既存機能
# ----------------------------------------------------------------              
# キャラクター新規作成時の人数制限チェック
func _on_create_adventurer_selected() -> void:
	# プレイヤーが「作成」できるのは累計24名まで
	if registered_members.size() >= MAX_CREATE_LIMIT:
		if town_system and town_system.creation_node:
			# 既存の街グラフィックを隠し、キャラクター作成画面をアクティブにする
			town_system.town_graphic.hide()
			
			# CharacterCreationノードにある上限エラー表示用の独自関数を呼び出す
			town_system.creation_node.activate_limit_error()
		return

	if town_system:
		town_system.town_graphic.hide()
		if town_system.creation_node:
			town_system.creation_node.activate_name_input()

func _on_creation_canceled() -> void:
	if town_system:
		town_system.town_graphic.show()
		var dynamic_menu = town_system.town_graphic.get_node_or_null("DynamicMenu")
		if dynamic_menu:
			var menu_panel = dynamic_menu.get_child(0)
			if menu_panel and menu_panel.get_child_count() > 0:
				var create_btn = menu_panel.get_child(2) as Button
				if create_btn:
					create_btn.grab_focus.call_deferred()

func register_new_adventurer(new_chara: CharaData) -> bool:
	registered_members.append(new_chara)
	return true

func prepare_return_to_town() -> void:
	if town_system:
		town_system.reset_to_main_town()
		town_system = null

ギルド管理用コード (guild_manager.gd) の上記のように修正してください。メッセージ表示などの調整を行っています。

「キャラクターを削除する」機能

「冒険者を削除する」は、冒険者ギルドにいる冒険者が対象であり、パーティメンバーは対象外とします。「冒険者を削除する」とき、ギルドに1名もいない場合は削除できません。「ギルドに冒険者がいません」のメッセージが出て、ギルドのメニューに戻るように実装します。また、削除対象の冒険者を選んだら、確認画面を出すようにします。

ノード構成の作成

  • CanvasLayer (CanvasLayer)
    • (他は省略)
    • CharacterCreation (Control)
    • JoinParty (Control)
    • LeaveParty (Control)
    • ChangeClass (Control)
    • DeleteCharacter (Control) 新規
      • Background (ColorRect) 新規
      • MainVBox (VBoxContainer) 新規
        • MessageLabel (Label) 新規
        • MemberList (VBoxContainer) 新規
          • Row1 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row2 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row3 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row4 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row5 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
          • Row6 (HBoxContainer) 新規
            • CursorBtn (Button) 新規
            • FaceImg (TextureRect) 新規
            • InfoLabel (Label) 新規
        • ConfirmBox (VBoxContainer) 新規
          • ChoiceHBox (HBoxContainer) 新規
            • NoButton (Button) 新規
            • YesButton (Button) 新規
        • NavigationHBox (HBoxContainer) 新規
          • NextButton (Button) 新規
          • BackButton (Button) 新規
DeleteCharacter (Control)新規作成 非表示
Background (ColorRect)画面全体を覆う黒塗りの背景。必ず MainVBox より上に配置すること。(カラー: 黒、レイアウト:Full Rect)
MainVBox (VBoxContainer)「冒険者を削除する」の画面全般
MessageLabel (Label)画面上部のメッセージ
「削除する冒険者を選択してください」
MemberList (VBoxContainer)画面中央部に冒険者を6名表示する
Row1~6 (HBoxContainer)1~6行目
CursorBtn (Button)カーソル。呼び出す冒険者を選ぶもの
FaceImg (TextureRect)冒険者の見た目画像
InfoLabel(Label)冒険者の名前、レベル、職業、最大HP、最大MP
NavigationHBox (HBoxContainer)画面下部のボタン
ConfirmBox (VBoxContainer)確認画面用
ChoiceHBox (HBoxContainer)選択肢
NoButton (Button)いいえ。デフォルトでフォーカスが当たっている
YesButton (Button)はい。
NextButton (Button)次の一覧に進むボタン。これを選ぶと、ギルドに6名以上いる場合に次の6名が表示されるようになる。ギルドに6名いない場合は何も起こらない。最後のメンバー一覧のときにこれを押すと、最初のギルドメンバー一覧に戻る
BackButton (Button)ギルドに戻る

このノード構成を作成します。DeleteCharacter (Control) の非表示を忘れないようにしてください。

スクリプトファイルの作成

extends Control

# ==============================================================================
# delete_character.gd : ギルドメンバー(キャラクター)を削除する画面のUI制御
# ==============================================================================

@onready var message_label: Label = $MainVBox/MessageLabel
@onready var member_list_container: VBoxContainer = $MainVBox/MemberList
@onready var confirm_box: VBoxContainer = $MainVBox/ConfirmBox
@onready var no_button: Button = $MainVBox/ConfirmBox/ChoiceHBox/NoButton
@onready var yes_button: Button = $MainVBox/ConfirmBox/ChoiceHBox/YesButton
@onready var navigation_hbox: HBoxContainer = $MainVBox/NavigationHBox
@onready var next_button: Button = $MainVBox/NavigationHBox/NextButton
@onready var back_button: Button = $MainVBox/NavigationHBox/BackButton

var rows: Array[HBoxContainer] = []
var _guild_characters: Array[CharaData] = []
var _current_page: int = 0
var _is_error_mode: bool = false
var _is_confirm_mode: bool = false
var _target_character: CharaData = null

const TEXT_DATA = {
	"en": {
		"title": "Select an adventurer to delete",
		"btn_next": "Next List",
		"btn_back": "Back to Guild",
		"err_empty": "There are no adventurers in the guild.",
		"confirm": "Deleted characters cannot be used again.\nTruly delete Name [%s]?",
		"yes": "Yes",
		"no": "No",
		"msg_deleted": "Name [%s] has been deleted."
	},
	"ja": {
		"title": "削除する冒険者を選択してください",
		"btn_next": "次の一覧に進む",
		"btn_back": "ギルドに戻る",
		"err_empty": "ギルドに冒険者がいません",
		"confirm": "削除されたキャラクターは永久に使えなくなります。\n本当に名前【%s】を削除しますか?",
		"yes": "はい",
		"no": "いいえ",
		"msg_deleted": "名前【%s】が削除されました"
	}
}

func _get_current_lang() -> String:
	var locale = TranslationServer.get_locale().left(2).to_lower()
	if locale == "ja":
		return "ja"
	return "en"

func _ready() -> void:
	for i in range(1, 7):
		var row_node = member_list_container.get_node_or_null("Row" + str(i))
		if row_node:
			rows.append(row_node as HBoxContainer)
			
	next_button.pressed.connect(_on_next_button_pressed)
	back_button.pressed.connect(_on_back_button_pressed)
	no_button.pressed.connect(_on_no_button_pressed)
	yes_button.pressed.connect(_on_yes_button_pressed)
	
	hide()

# 外部(GuildManager)から呼び出される起動関数
func activate() -> void:
	_guild_characters = PartyManager.get_members_in_guild()
	_current_page = 0
	_is_error_mode = false
	_is_confirm_mode = false
	_target_character = null
	
	var lang = _get_current_lang()
	next_button.text = TEXT_DATA[lang]["btn_next"]
	back_button.text = TEXT_DATA[lang]["btn_back"]
	yes_button.text = TEXT_DATA[lang]["yes"]
	no_button.text = TEXT_DATA[lang]["no"]
	
	show()
	confirm_box.hide()
	
	if _guild_characters.size() == 0:
		_show_error_message(TEXT_DATA[lang]["err_empty"])
		return
		
	member_list_container.show()
	navigation_hbox.show()
	_update_member_list()

# エラー・完了メッセージ表示
func _show_error_message(text_content: String) -> void:
	_is_error_mode = true
	_is_confirm_mode = false
	member_list_container.hide()
	confirm_box.hide()
	navigation_hbox.hide()
	
	message_label.text = text_content
	release_focus()

func _update_member_list() -> void:
	var focusable_buttons: Array[Button] = []
	var lang = _get_current_lang()
	message_label.text = TEXT_DATA[lang]["title"]
	
	var start_idx = _current_page * 6
	var page_characters = _guild_characters.slice(start_idx, start_idx + 6)
	
	for i in range(6):
		var row = rows[i]
		var cursor_btn = row.get_child(0) as Button
		
		for connection in cursor_btn.pressed.get_connections():
			cursor_btn.pressed.disconnect(connection.callable)
			
		if i < page_characters.size():
			var c: CharaData = page_characters[i]
			row.show()
			
			var face_img = row.get_child(1) as TextureRect
			var info_label = row.get_child(2) as Label
			
			cursor_btn.text = "▶"
			cursor_btn.disabled = false
			
			var callable = func(target: CharaData): _on_row_selected(target)
			cursor_btn.pressed.connect(callable.bind(c))
			
			if FileAccess.file_exists(c.face_image_path):
				face_img.texture = load(c.face_image_path)
			else:
				face_img.texture = null
			face_img.custom_minimum_size = Vector2(64, 64)
			
			info_label.text = "[%s] LV:%d %s (MAX HP:%d MAX MP:%d)" % [
				c.chara_name, c.level, c.get_display_class(),
				c.max_hp, c.max_mp
			]
			
			focusable_buttons.append(cursor_btn)
		else:
			row.hide()
			cursor_btn.disabled = true
			
	focusable_buttons.append(next_button)
	focusable_buttons.append(back_button)
	
	for i in range(focusable_buttons.size()):
		var prev = focusable_buttons[(i - 1 + focusable_buttons.size()) % focusable_buttons.size()]
		var next = focusable_buttons[(i + 1) % focusable_buttons.size()]
		focusable_buttons[i].focus_neighbor_top = prev.get_path()
		focusable_buttons[i].focus_neighbor_bottom = next.get_path()
		
	if focusable_buttons.size() > 2:
		focusable_buttons[0].grab_focus()
	else:
		back_button.grab_focus()

func _on_row_selected(chara: CharaData) -> void:
	_is_confirm_mode = true
	_target_character = chara
	
	member_list_container.hide()
	navigation_hbox.hide()
	
	var lang = _get_current_lang()
	message_label.text = TEXT_DATA[lang]["confirm"] % chara.chara_name
	confirm_box.show()
	
	no_button.focus_neighbor_left = yes_button.get_path()
	no_button.focus_neighbor_right = yes_button.get_path()
	yes_button.focus_neighbor_left = no_button.get_path()
	yes_button.focus_neighbor_right = no_button.get_path()
	
	# 仕様通り初期フォーカスは「いいえ」
	no_button.grab_focus()

func _on_no_button_pressed() -> void:
	_is_confirm_mode = false
	_target_character = null
	confirm_box.hide()
	member_list_container.show()
	navigation_hbox.show()
	_update_member_list()

func _on_yes_button_pressed() -> void:
	if _target_character:
		var lang = _get_current_lang()
		var formatted_msg = TEXT_DATA[lang]["msg_deleted"] % _target_character.chara_name
		
		# ギルドマネージャー経由で永続配列から削除
		GuildManager.delete_character_from_system(_target_character)
		
		_show_error_message(formatted_msg)

func _on_next_button_pressed() -> void:
	if _guild_characters.size() <= 6:
		return
		
	_current_page += 1
	if (_current_page * 6) >= _guild_characters.size():
		_current_page = 0
		
	_update_member_list()

func _on_back_button_pressed() -> void:
	hide()
	GuildManager.return_to_guild_main_menu()

func _unhandled_input(event: InputEvent) -> void:
	if not is_visible_in_tree():
		return
		
	if _is_error_mode:
		if event.is_action_pressed("ui_accept") or event.is_action_pressed("ui_cancel"):
			get_viewport().set_input_as_handled()
			_is_error_mode = false
			_on_back_button_pressed()
		return
		
	if _is_confirm_mode:
		if event.is_action_pressed("ui_cancel"):
			get_viewport().set_input_as_handled()
			_on_no_button_pressed()
		return

	if event.is_action_pressed("ui_cancel"):
		get_viewport().set_input_as_handled()
		_on_back_button_pressed()

DeleteCharacter ノードを選択した状態で、新規スクリプト delete_character.gd を作成し、アタッチしてください。コードは上記の通りです。主に「冒険者を削除する」画面の表示部分を担当しています。

ギルド管理用コード修正

extends Node

# ==============================================================================
# 冒険者ギルド 管理マネージャー (UI・ロジック完全版)
# ==============================================================================

const SYSTEM_MAX_SLOTS: int = 25
const MAX_CREATE_LIMIT: int = 24

var registered_members: Array[CharaData] = []
var town_system: TownSystem = null
var join_party_node: Control = null
var leave_party_node: Control = null
var delete_character_node: Control = null
var change_class_node: Control = null

# ギルドメニューの項目定義
const GUILD_MENU_KEYS = [
	"GUILD_MENU_JOIN", 
	"GUILD_MENU_REMOVE", 
	"GUILD_MENU_CREATE", 
	"GUILD_MENU_CHANGE_CLASS", 
	"GUILD_MENU_DELETE", 
	"GUILD_MENU_CHANGE_NAME", 
	"GUILD_MENU_CHANGE_APPEARANCE", 
	"GUILD_MENU_BACK"
]

# 呼び出し画面用のページング管理変数
var _call_list_cache: Array[CharaData] = []
var _current_page: int = 0
var _selected_row_index: int = 0
var _row_buttons: Array[Button] = []

# ==============================================================================
# UI制御・画面遷移ハンドリング
# ==============================================================================

# 街からギルドに入ったときに呼び出される関数
func open_guild(town: TownSystem) -> void:
	town_system = town
	
	if town_system.creation_node:
		join_party_node = town_system.creation_node.get_parent().get_node_or_null("JoinParty")
		leave_party_node = town_system.creation_node.get_parent().get_node_or_null("LeaveParty")
		change_class_node = town_system.creation_node.get_parent().get_node_or_null("ChangeClass")
		delete_character_node = town_system.creation_node.get_parent().get_node_or_null("DeleteCharacter")
	
	town_system._update_graphic("guild")
	if town_system.town_menu:
		town_system.town_menu.hide()
	
	town_system.cleanup_dynamic_menus()
	
	if town_system.creation_node and not town_system.creation_node.is_connected("creation_canceled", _on_creation_canceled):
		town_system.creation_node.creation_canceled.connect(_on_creation_canceled)
		
	_show_guild_menu()

# ギルドのメインメニューに戻る(JoinParty画面を閉じたときなどに使用)
func return_to_guild_main_menu() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
		_show_guild_menu()

func _show_guild_menu() -> void:
	var center_container = CenterContainer.new()
	center_container.name = "DynamicMenu"
	center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(center_container)
	
	var menu_panel = VBoxContainer.new()
	center_container.add_child(menu_panel)
	
	var buttons = []
	for key in GUILD_MENU_KEYS:
		var btn = Button.new()
		btn.text = tr(key)
		btn.pressed.connect(func(): _on_guild_menu_selected(key))
		menu_panel.add_child(btn)
		buttons.append(btn)
	
	for i in range(buttons.size()):
		var prev = buttons[(i - 1 + buttons.size()) % buttons.size()]
		var next = buttons[(i + 1) % buttons.size()]
		buttons[i].focus_neighbor_top = prev.get_path()
		buttons[i].focus_neighbor_bottom = next.get_path()
	
	buttons[0].grab_focus()

func _on_guild_menu_selected(key: String) -> void:
	if key == "GUILD_MENU_BACK":
		prepare_return_to_town()
		return
		
	match key:
		"GUILD_MENU_JOIN":
			_on_call_out_selected()
		"GUILD_MENU_REMOVE":
			_on_leave_party_selected()
		"GUILD_MENU_CREATE":
			_on_create_adventurer_selected()
		"GUILD_MENU_CHANGE_CLASS":
			_on_change_class_selected()
		"GUILD_MENU_DELETE":
			_on_delete_character_selected()
		"GUILD_MENU_CHANGE_NAME":
			print("TODO: 名前を変える UIを開く")
		"GUILD_MENU_CHANGE_APPEARANCE":
			print("TODO: 見た目を変える UIを開く")

# 冒険者をよびだす         
func _on_call_out_selected() -> void:
	# ギルドメニューUIを一時消去
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	# パーティの人数チェックと、ギルド内のメンバーリスト取得
	var is_party_full = PartyManager.is_full()
	var guild_members = PartyManager.get_members_in_guild()
	
	# 状態をそのまま JoinParty ノードに丸投げする
	if join_party_node:
		join_party_node.activate(guild_members, is_party_full)

# メッセージ単体表示用の簡易関数
func _show_simple_message(msg_text: String) -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	var center_container = CenterContainer.new()
	center_container.name = "DynamicMenu"
	center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(center_container)
	
	var vbox = VBoxContainer.new()
	center_container.add_child(vbox)
	
	var label = Label.new()
	label.text = msg_text
	vbox.add_child(label)
	
	var ok_btn = Button.new()
	ok_btn.text = "閉じる"
	ok_btn.pressed.connect(return_to_guild_main_menu)
	vbox.add_child(ok_btn)
	
	ok_btn.grab_focus()

# 冒険者をあずける
func _on_leave_party_selected() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	if leave_party_node:
		leave_party_node.activate()

# 現在ギルド(待機所)にいるメンバーの総数を返すヘルパー関数
func get_registered_count() -> int:
	return PartyManager.get_members_in_guild().size()

func _open_call_out_ui() -> void:
	town_system.cleanup_dynamic_menus()
	
	# ベースとなる全画面黒背景のコンテナ
	var bg = ColorRect.new()
	bg.name = "DynamicMenu"
	bg.color = Color.BLACK
	bg.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(bg)
	
	# メインレイアウト(縦並び)
	var main_vbox = VBoxContainer.new()
	main_vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
	main_vbox.offset_left = 20
	main_vbox.offset_top = 20
	main_vbox.offset_right = -20
	main_vbox.offset_bottom = -20
	bg.add_child(main_vbox)
	
	# 画面上部:メッセージ
	var title_label = Label.new()
	title_label.text = "呼び出す冒険者を選択してください"
	title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	main_vbox.add_child(title_label)
	
	# 隙間
	var spacer = Control.new()
	spacer.custom_minimum_size.y = 10
	main_vbox.add_child(spacer)
	
	# 画面中央:メンバー一覧(縦並びのコンテナ)
	var list_vbox = VBoxContainer.new()
	list_vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
	main_vbox.add_child(list_vbox)
	
	# ページ内の表示対象をスライス (6名ずつ)
	var start_idx = _current_page * 6
	var page_members = _call_list_cache.slice(start_idx, start_idx + 6)
	
	_row_buttons.clear()
	
	for i in range(page_members.size()):
		var m: CharaData = page_members[i]
		
		# 横並びの行コンテナ
		var row_hbox = HBoxContainer.new()
		list_vbox.add_child(row_hbox)
		
		# 矢印カーソル代わりの選択ボタン(右向き矢印「▶」)
		var select_btn = Button.new()
		select_btn.text = "▶"
		select_btn.flat = true # ボタンの枠線を消して矢印だけに見せる
		# 決定された時の処理
		select_btn.pressed.connect(func(): _execute_call_out(m))
		row_hbox.add_child(select_btn)
		_row_buttons.append(select_btn)
		
		# 見た目画像
		var img_rect = TextureRect.new()
		if FileAccess.file_exists(m.face_image_path):
			img_rect.texture = load(m.face_image_path)
		img_rect.custom_minimum_size = Vector2(40, 40)
		img_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
		row_hbox.add_child(img_rect)
		
		# ステータス文字列の結合
		# 例: (1) 名前、レベル、職業、最大HP、最大MP
		var info_label = Label.new()
		info_label.text = "【%s】 LV:%d %s (HP:%d/%d MP:%d/%d)" % [
			m.chara_name, m.level, m.get_display_class(), 
			m.current_hp, m.max_hp, m.current_mp, m.max_mp
		]
		row_hbox.add_child(info_label)
	
	# 画面下部:ナビゲーションボタン
	var nav_hbox = HBoxContainer.new()
	nav_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
	main_vbox.add_child(nav_hbox)
	
	var next_btn = Button.new()
	next_btn.text = "次の一覧に進む"
	next_btn.pressed.connect(_on_next_page_pressed)
	nav_hbox.add_child(next_btn)
	
	var back_menu_btn = Button.new()
	back_menu_btn.text = "ギルドに戻る"
	back_menu_btn.pressed.connect(_show_guild_menu)
	nav_hbox.add_child(back_menu_btn)
	
	# フォーカス近隣の動的接続(循環設定)
	var all_focusable = []
	all_focusable.append_array(_row_buttons)
	all_focusable.append(next_btn)
	all_focusable.append(back_menu_btn)
	
	for i in range(all_focusable.size()):
		var prev = all_focusable[(i - 1 + all_focusable.size()) % all_focusable.size()]
		var next = all_focusable[(i + 1) % all_focusable.size()]
		all_focusable[i].focus_neighbor_top = prev.get_path()
		all_focusable[i].focus_neighbor_bottom = next.get_path()
		
	# 最初の矢印にフォーカス
	if _row_buttons.size() > 0:
		_row_buttons[0].grab_focus()
	else:
		next_btn.grab_focus()

# 「次の一覧に進む」が押された時
func _on_next_page_pressed() -> void:
	# ギルドに6名以上いる場合のみページを進める
	if _call_list_cache.size() <= 6:
		return
		
	_current_page += 1
	# 最後のメンバー一覧を超えたら最初のページに戻る
	if (_current_page * 6) >= _call_list_cache.size():
		_current_page = 0
		
	_open_call_out_ui()

# 実際にパーティに加入させる処理
func _execute_call_out(chara: CharaData) -> void:
	# パーティの空き枠(前衛・後衛)に自動追加
	var slot_assigned = PartyManager.auto_add_member(chara)
	if slot_assigned != -1:
		var msg = "名前【%s】がパーティに加わった。" % chara.chara_name
		_show_guild_message(msg)

# クラスを変える
func _on_change_class_selected() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	if change_class_node:
		change_class_node.activate()

# キャラクター削除
func _on_delete_character_selected() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	if delete_character_node:
		delete_character_node.activate()

func delete_character_from_system(chara: CharaData) -> void:
	if chara in registered_members:
		registered_members.erase(chara)
		print(chara.chara_name, " がシステム(registered_members)から完全消去されました。")

# ----------------------------------------------------------------              
# 汎用メッセージ画面(1ボタンでギルドメニューに戻る)
# ----------------------------------------------------------------              
func _show_guild_message(msg_text: String) -> void:
	town_system.cleanup_dynamic_menus()
	
	var bg = ColorRect.new()
	bg.name = "DynamicMenu"
	bg.color = Color.BLACK
	bg.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(bg)
	
	var vbox = VBoxContainer.new()
	vbox.set_anchors_preset(Control.PRESET_CENTER)
	bg.add_child(vbox)
	
	var label = Label.new()
	label.text = msg_text
	label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	vbox.add_child(label)
	
	var spacer = Control.new()
	spacer.custom_minimum_size.y = 15
	vbox.add_child(spacer)
	
	var ok_btn = Button.new()
	ok_btn.text = "閉じる"
	ok_btn.pressed.connect(_show_guild_menu)
	vbox.add_child(ok_btn)
	
	ok_btn.grab_focus()
	
# ----------------------------------------------------------------              
# その他の既存機能
# ----------------------------------------------------------------              
# キャラクター新規作成時の人数制限チェック
func _on_create_adventurer_selected() -> void:
	# プレイヤーが「作成」できるのは累計24名まで
	if registered_members.size() >= MAX_CREATE_LIMIT:
		if town_system and town_system.creation_node:
			# 既存の街グラフィックを隠し、キャラクター作成画面をアクティブにする
			town_system.town_graphic.hide()
			
			# CharacterCreationノードにある上限エラー表示用の独自関数を呼び出す
			town_system.creation_node.activate_limit_error()
		return

	if town_system:
		town_system.town_graphic.hide()
		if town_system.creation_node:
			town_system.creation_node.activate_name_input()

func _on_creation_canceled() -> void:
	if town_system:
		town_system.town_graphic.show()
		var dynamic_menu = town_system.town_graphic.get_node_or_null("DynamicMenu")
		if dynamic_menu:
			var menu_panel = dynamic_menu.get_child(0)
			if menu_panel and menu_panel.get_child_count() > 0:
				var create_btn = menu_panel.get_child(2) as Button
				if create_btn:
					create_btn.grab_focus.call_deferred()

func register_new_adventurer(new_chara: CharaData) -> bool:
	registered_members.append(new_chara)
	return true

func prepare_return_to_town() -> void:
	if town_system:
		town_system.reset_to_main_town()
		town_system = null

ギルド管理用コード (guild_manager.gd) の上記のように修正してください。メッセージ表示などの調整を行っています。

「名前を変える」機能

名前を変える機能の実装です。名前は冒険者ギルドでしか変更できません。「名前を変える」は、パーティメンバーが対象であり、冒険者ギルドにいる冒険者は対象外です。

ノード構成の作成

  • CanvasLayer (CanvasLayer)
    • (他は省略)
    • CharacterCreation (Control)
    • JoinParty (Control)
    • LeaveParty (Control)
    • ChangeClass (Control)
    • DeleteCharacter (Control)
    • RenameCharacter (Control)  新規
      • Background (ColorRect)  新規
      • MainVBox (VBoxContainer)  新規
        • MessageLabel (Label)  新規
        • MemberList (VBoxContainer)  新規
          • Row1 (HBoxContainer)  新規
            • CursorBtn (Button)  新規
            • FaceImg (TextureRect)  新規
            • InfoLabel (Label)  新規
          • Row2 (HBoxContainer)  新規
            • CursorBtn (Button)  新規
            • FaceImg (TextureRect)  新規
            • InfoLabel (Label)  新規
          • Row3 (HBoxContainer)  新規
            • CursorBtn (Button)  新規
            • FaceImg (TextureRect)  新規
            • InfoLabel (Label)  新規
          • Row4 (HBoxContainer)  新規
            • CursorBtn (Button)  新規
            • FaceImg (TextureRect)  新規
            • InfoLabel (Label)  新規
          • Row5 (HBoxContainer)  新規
            • CursorBtn (Button)  新規
            • FaceImg (TextureRect)  新規
            • InfoLabel (Label)  新規
          • Row6 (HBoxContainer)  新規
            • CursorBtn (Button)  新規
            • FaceImg (TextureRect)  新規
            • InfoLabel (Label)  新規
        • NameLayout (VBoxContainer)  新規
          • TitleLabel (Label)  新規
          • NameInputField (LineEdit)  新規
          • KeyboardGrid (GridContainer)  新規
          • BottomMenu (HBoxContainer)  新規
            • BackToMemberListButton (Button)  新規
            • ConfirmButton (Button)  新規
        • NavigationHBox (HBoxContainer)  新規
          • BackButton (Button)  新規
RenameCharacter (Control)新規作成  非表示
Background (ColorRect)画面全体を覆う黒塗りの背景。必ず MainVBox より上に配置すること。(カラー: 黒、レイアウト:Full Rect)
MainVBox (VBoxContainer)「冒険者を削除する」の画面全般
MessageLabel (Label)画面上部のメッセージ
「削除する冒険者を選択してください」
MemberList (VBoxContainer)画面中央部に冒険者を6名表示する
Row1~6 (HBoxContainer)1~6行目
CursorBtn (Button)カーソル。呼び出す冒険者を選ぶもの
FaceImg (TextureRect)冒険者の見た目画像
InfoLabel(Label)冒険者の名前、レベル、職業、最大HP、最大MP
NameLayout (VBoxContainer)名前変更用レイアウト
TitleLabel (Label)「新しい名前を入力してください」
NameInputField (LineEdit)名前欄
KeyboardGrid (GridContainer)文字入力キーボード部分
BottomMenu (HBoxContainer)画面下部のボタン
BackToMemberListButton (Button)パーティメンバーを選択する画面に戻る
ConfirmButton (Button)決定
NavigationHBox (HBoxContainer)画面下部のボタン
BackButton (Button)ギルドに戻る

このノード構成を作成します。RenameCharacter (Control) の非表示を忘れないようにしてください。

スクリプトファイルの作成

class_name RenameCharacter
extends Control

signal rename_finished

enum Phase { MEMBER_SELECT, NAME_INPUT, MESSAGE_OUT }
var _current_phase: Phase = Phase.MEMBER_SELECT

# 多言語対応テキスト
const TEXT_DATA = {
	"ja": {
		"title_select": "名前を変えるパーティメンバーを選択してください",
		"title_input": "新しい名前を入力してください",
		"btn_back_guild": "ギルドに戻る",
		"btn_back_select": "メンバー選択に戻る",
		"btn_confirm": "決定",
		"success_msg": "名前が【%s】に変更されました",
		"no_member_err": "パーティメンバーがいません。"
	},
	"en": {
		"title_select": "Select a party member to rename.",
		"title_input": "Enter a new name.",
		"btn_back_guild": "Back to Guild",
		"btn_back_select": "Back to Selection",
		"btn_confirm": "Confirm",
		"success_msg": "Name has been changed to [%s].",
		"no_member_err": "There are no party members."
	}
}

const KEYS_JA = [
	"あ", "い", "う", "え", "お", "か", "き", "く", "け", "こ",
	"さ", "し", "す", "せ", "そ", "た", "ち", "つ", "て", "と",
	"な", "に", "ぬ", "ね", "の", "は", "ひ", "ふ", "へ", "ほ",
	"ま", "み", "む", "め", "モ", "や", "ゆ", "よ", "わ", "を",
	"ら", "り", "る", "れ", "ろ", "ん", "っ", "ゃ", "ゅ", "ょ",
	"が", "ぎ", "ぐ", "げ", "ご", "ざ", "じ", "ず", "ぜ", "ぞ",
	"だ", "ぢ", "づ", "で", "ど", "ば", "び", "ぶ", "べ", "ぼ",
	"ぱ", "ぴ", "ぷ", "ぺ", "ぽ", "ー", "〜", "、", "。", " ",
	"ア", "イ", "ウ", "エ", "オ", "カ", "キ", "ク", "ケ", "コ",
	"サ", "シ", "ス", "セ", "ソ", "タ", "チ", "ツ", "テ", "ト",
	"ナ", "ニ", "ヌ", "ネ", "ノ", "ハ", "ヒ", "フ", "ヘ", "ホ",
	"マ", "ミ", "ム", "メ", "モ", "ヤ", "ユ", "ヨ", "ワ", "ヲ",
	"ラ", "リ", "ル", "レ", "ロ", "ン", "ッ", "ャ", "ュ", "ョ",
	"ガ", "ギ", "グ", "ゲ", "ゴ", "ザ", "ジ", "ズ", "ゼ", "ゾ",
	"ダ", "ヂ", "ヅ", "デ", "ド", "バ", "ビ", "ブ", "ベ", "ボ",
	"パ", "ピ", "プ", "ペ", "ポ", " ",  " ",  " ",  " ",  " "
]

const KEYS_EN = [
	"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
	"U", "V", "W", "X", "Y", "Z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-", "_", "!", "?",
	"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", 
	"u", "v", "w", "x", "y", "z"
]

# ==============================================================================
# 指定されたノード構成への完全一致参照
# ==============================================================================
@onready var message_label: Label = $MainVBox/MessageLabel
@onready var member_list: VBoxContainer = $MainVBox/MemberList
@onready var navigation_hbox: HBoxContainer = $MainVBox/NavigationHBox
@onready var back_button: Button = $MainVBox/NavigationHBox/BackButton

# NameLayout 内
@onready var name_layout: VBoxContainer = $MainVBox/NameLayout
@onready var title_label: Label = $MainVBox/NameLayout/TitleLabel
@onready var name_input_field: LineEdit = $MainVBox/NameLayout/NameInputField
@onready var keyboard_grid: GridContainer = $MainVBox/NameLayout/KeyboardGrid
@onready var back_to_member_list_button: Button = $MainVBox/NameLayout/BottomMenu/BackToMemberListButton
@onready var confirm_button: Button = $MainVBox/NameLayout/BottomMenu/ConfirmButton

# 内部保持
var _party_members: Array = []
var _target_character: CharaData = null
var _first_key_button: Button = null
var _current_max_length: int = 8
var _row_nodes: Array[Control] = []

func _ready() -> void:
	# Row1 〜 Row6 のノードを配列にまとめて管理
	_row_nodes = [
		$MainVBox/MemberList/Row1,
		$MainVBox/MemberList/Row2,
		$MainVBox/MemberList/Row3,
		$MainVBox/MemberList/Row4,
		$MainVBox/MemberList/Row5,
		$MainVBox/MemberList/Row6
	]
	
	# シグナル接続
	if back_button and not back_button.pressed.is_connected(_on_back_button_pressed):
		back_button.pressed.connect(_on_back_button_pressed)
	if back_to_member_list_button and not back_to_member_list_button.pressed.is_connected(_on_back_to_member_list_pressed):
		back_to_member_list_button.pressed.connect(_on_back_to_member_list_pressed)
	if confirm_button and not confirm_button.pressed.is_connected(_on_confirm_pressed):
		confirm_button.pressed.connect(_on_confirm_pressed)
		
	hide()

# 外部(Guild)からの開始処理
func start_rename_flow(party_list: Array) -> void:
	_party_members = party_list
	_target_character = null
	show()
	
	# 有効なメンバーがいるかチェック
	var valid_members = _party_members.filter(func(member): return member != null)
	if valid_members.is_empty():
		# メンバーがいない場合は専用のエラーメッセージフェーズへ
		_show_no_member_error()
		return
		
	_switch_to_member_select_phase()

func _show_no_member_error() -> void:
	_current_phase = Phase.MESSAGE_OUT
	member_list.hide()
	navigation_hbox.hide()
	name_layout.hide()
	
	var lang = _get_current_lang()
	message_label.text = TEXT_DATA[lang]["no_member_err"]
	release_focus()

func _input(event: InputEvent) -> void:
	if not is_visible_in_tree(): return
	
	if event.is_action_pressed("ui_cancel"):
		get_viewport().set_input_as_handled()
		match _current_phase:
			Phase.MEMBER_SELECT:
				_return_to_guild()
			Phase.NAME_INPUT:
				_pop_last_character()
			Phase.MESSAGE_OUT:
				_return_to_guild()

	elif _current_phase == Phase.MESSAGE_OUT:
		if event.is_action_pressed("ui_accept") or event.is_action_pressed("ui_cancel"):
			get_viewport().set_input_as_handled()
			_return_to_guild()

# 名前の最後の文字を1文字消去するヘルパー関数
func _pop_last_character() -> void:
	var current_text = name_input_field.text
	if not current_text.is_empty():
		# 末尾の1文字を削った文字列を再代入
		name_input_field.text = current_text.left(current_text.length() - 1)

# ==============================================================================
# フェーズ管理
# ==============================================================================
func _switch_to_member_select_phase() -> void:
	_current_phase = Phase.MEMBER_SELECT
	name_layout.hide()
	message_label.show()
	
	member_list.show()
	navigation_hbox.show()
	
	var lang = _get_current_lang()
	message_label.text = TEXT_DATA[lang]["title_select"]
	back_button.text = TEXT_DATA[lang]["btn_back_guild"]
	
	_update_member_list_display()

func _switch_to_name_input_phase() -> void:
	_current_phase = Phase.NAME_INPUT
	member_list.hide()
	navigation_hbox.hide()
	
	# 画面上部の共通メッセージラベルを非表示にして重複を防ぐ
	message_label.hide()
	
	name_layout.show()
	
	var lang = _get_current_lang()
	title_label.text = TEXT_DATA[lang]["title_input"]
	back_to_member_list_button.text = TEXT_DATA[lang]["btn_back_select"]
	confirm_button.text = TEXT_DATA[lang]["btn_confirm"]
	
	name_input_field.text = _target_character.chara_name
	
	if lang == "ja":
		_current_max_length = 8
		_setup_software_keyboard(KEYS_JA)
	else:
		_current_max_length = 16
		_setup_software_keyboard(KEYS_EN)
		
	name_input_field.max_length = _current_max_length
	keyboard_grid.columns = 20
	
	if _first_key_button:
		_first_key_button.grab_focus.call_deferred()

func _show_success_message() -> void:
	_current_phase = Phase.MESSAGE_OUT
	member_list.hide()
	navigation_hbox.hide()
	name_layout.hide()
	message_label.show()
	
	var lang = _get_current_lang()
	message_label.text = TEXT_DATA[lang]["success_msg"] % _target_character.chara_name
	release_focus()

# ==============================================================================
# 表示・キーボード更新(完全固定ノード制御)
# ==============================================================================
func _update_member_list_display() -> void:
	var focus_buttons = []
	
	# 既存の全Rowを巡回し、パーティ人数に応じて出し分ける
	for i in range(6):
		var row_node = _row_nodes[i]
		
		# パーティメンバーがその枠に存在するかチェック
		if i < _party_members.size() and _party_members[i] != null:
			var member = _party_members[i]
			row_node.show()
			
			# 子ノードへのアクセス
			var cursor_btn = row_node.get_node("CursorBtn") as Button
			var face_img = row_node.get_node("FaceImg") as TextureRect
			var label = row_node.get_node("Label") as Label
			
			# カーソルボタン設定
			cursor_btn.text = "▶"
			cursor_btn.focus_mode = Control.FOCUS_ALL
			
			if cursor_btn.pressed.is_connected(_on_member_row_selected):
				cursor_btn.pressed.disconnect(_on_member_row_selected)
			cursor_btn.pressed.connect(_on_member_row_selected.bind(member))
			
			focus_buttons.append(cursor_btn)
			
			# グラフィックの適用
			if FileAccess.file_exists(member.face_image_path):
				face_img.texture = load(member.face_image_path)
			else:
				face_img.texture = null
				
			# CharaData側に実装した get_display_class() を呼び出して、
			# 自動的に現在の言語に応じた翻訳テキスト(「戦士」や「Fighter」)を取得します。
			var display_class_name = member.get_display_class()
			
			# テキスト情報の適用
			label.text = "[%s] LV:%d %s (HP:%d MP:%d)" % [
				member.chara_name, member.level, display_class_name,
				member.max_hp, member.max_mp
			]
			
		else:
			row_node.hide()
			
	# 下部の「ギルドに戻る」をフォーカス循環に加える
	focus_buttons.append(back_button)
	
	for i in range(focus_buttons.size()):
		var prev = focus_buttons[(i - 1 + focus_buttons.size()) % focus_buttons.size()]
		var next = focus_buttons[(i + 1) % focus_buttons.size()]
		focus_buttons[i].focus_neighbor_top = prev.get_path()
		focus_buttons[i].focus_neighbor_bottom = next.get_path()
		
	if focus_buttons.size() > 0:
		focus_buttons[0].grab_focus.call_deferred()

func _setup_software_keyboard(keys_array: Array) -> void:
	_first_key_button = null
	for child in keyboard_grid.get_children():
		child.queue_free()
		
	var is_en = (TranslationServer.get_locale().left(2) == "en")
	
	for i in range(keys_array.size()):
		var key = keys_array[i]
		if key == " " and is_en:
			var spacer = Control.new()
			keyboard_grid.add_child(spacer)
			continue
			
		var btn = Button.new()
		btn.text = key
		btn.custom_minimum_size = Vector2(32, 32)
		btn.focus_mode = Control.FOCUS_ALL
		btn.pressed.connect(func():
			if name_input_field.text.length() < _current_max_length:
				name_input_field.text += key
		)
		keyboard_grid.add_child(btn)
		
		if _first_key_button == null:
			_first_key_button = btn

# ==============================================================================
# シグナルハンドラ
# ==============================================================================
func _on_member_row_selected(member: CharaData) -> void:
	_target_character = member
	_switch_to_name_input_phase()

func _on_back_button_pressed() -> void:
	_return_to_guild()

func _on_back_to_member_list_pressed() -> void:
	_switch_to_member_select_phase()

func _on_confirm_pressed() -> void:
	var cleaned_name = name_input_field.text.strip_edges()
	if cleaned_name.is_empty():
		return
		
	_target_character.chara_name = cleaned_name
	_show_success_message()

func _return_to_guild() -> void:
	hide()
	rename_finished.emit()

func _get_current_lang() -> String:
	return "ja" if TranslationServer.get_locale().left(2) == "ja" else "en"

RenameCharacter ノードを選択した状態で、新規スクリプト rename_character.gd を作成し、アタッチしてください。コードは上記の通りです。主に「名前を変える」画面の表示部分を担当しています。

ギルド管理用コード修正

extends Node

# ==============================================================================
# 冒険者ギルド 管理マネージャー (UI・ロジック完全版)
# ==============================================================================

const SYSTEM_MAX_SLOTS: int = 25
const MAX_CREATE_LIMIT: int = 24

var registered_members: Array[CharaData] = []
var town_system: TownSystem = null
var join_party_node: Control = null
var leave_party_node: Control = null
var delete_character_node: Control = null
var change_class_node: Control = null
var rename_character_node: RenameCharacter = null

# ギルドメニューの項目定義
const GUILD_MENU_KEYS = [
	"GUILD_MENU_JOIN", 
	"GUILD_MENU_REMOVE", 
	"GUILD_MENU_CREATE", 
	"GUILD_MENU_CHANGE_CLASS", 
	"GUILD_MENU_DELETE", 
	"GUILD_MENU_CHANGE_NAME", 
	"GUILD_MENU_CHANGE_APPEARANCE", 
	"GUILD_MENU_BACK"
]

# 呼び出し画面用のページング管理変数
var _call_list_cache: Array[CharaData] = []
var _current_page: int = 0
var _selected_row_index: int = 0
var _row_buttons: Array[Button] = []

# ==============================================================================
# UI制御・画面遷移ハンドリング
# ==============================================================================

# 街からギルドに入ったときに呼び出される関数
func open_guild(town: TownSystem) -> void:
	town_system = town
	
	if town_system.creation_node:
		var parent = town_system.creation_node.get_parent()
		join_party_node = parent.get_node_or_null("JoinParty")
		leave_party_node = parent.get_node_or_null("LeaveParty")
		change_class_node = parent.get_node_or_null("ChangeClass")
		delete_character_node = parent.get_node_or_null("DeleteCharacter")
		rename_character_node = parent.get_node_or_null("RenameCharacter")
	
	town_system._update_graphic("guild")
	if town_system.town_menu:
		town_system.town_menu.hide()
	
	town_system.cleanup_dynamic_menus()
	
	if town_system.creation_node and not town_system.creation_node.is_connected("creation_canceled", _on_creation_canceled):
		town_system.creation_node.creation_canceled.connect(_on_creation_canceled)
		
	if rename_character_node and not rename_character_node.rename_finished.is_connected(return_to_guild_main_menu):
		rename_character_node.rename_finished.connect(return_to_guild_main_menu)
		
	_show_guild_menu()

# ギルドのメインメニューに戻る
func return_to_guild_main_menu() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
		_show_guild_menu()

func _show_guild_menu() -> void:
	var center_container = CenterContainer.new()
	center_container.name = "DynamicMenu"
	center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(center_container)
	
	var menu_panel = VBoxContainer.new()
	center_container.add_child(menu_panel)
	
	var buttons = []
	for key in GUILD_MENU_KEYS:
		var btn = Button.new()
		btn.text = tr(key)
		btn.pressed.connect(func(): _on_guild_menu_selected(key))
		menu_panel.add_child(btn)
		buttons.append(btn)
	
	for i in range(buttons.size()):
		var prev = buttons[(i - 1 + buttons.size()) % buttons.size()]
		var next = buttons[(i + 1) % buttons.size()]
		buttons[i].focus_neighbor_top = prev.get_path()
		buttons[i].focus_neighbor_bottom = next.get_path()
	
	buttons[0].grab_focus()

func _on_guild_menu_selected(key: String) -> void:
	if key == "GUILD_MENU_BACK":
		prepare_return_to_town()
		return
		
	match key:
		"GUILD_MENU_JOIN":
			_on_call_out_selected()
		"GUILD_MENU_REMOVE":
			_on_leave_party_selected()
		"GUILD_MENU_CREATE":
			_on_create_adventurer_selected()
		"GUILD_MENU_CHANGE_CLASS":
			_on_change_class_selected()
		"GUILD_MENU_DELETE":
			_on_delete_character_selected()
		"GUILD_MENU_CHANGE_NAME":
			_on_change_name_selected()
		"GUILD_MENU_CHANGE_APPEARANCE":
			print("TODO: 見た目を変える UIを開く")

# 冒険者をあずける
func _on_leave_party_selected() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	if leave_party_node:
		leave_party_node.activate()

# 現在ギルド(待機所)にいるメンバーの総数を返すヘルパー関数
func get_registered_count() -> int:
	return PartyManager.get_members_in_guild().size()

# 冒険者をよびだす             
func _on_call_out_selected() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	var is_party_full = PartyManager.is_full()
	var guild_members = PartyManager.get_members_in_guild()
	
	if join_party_node:
		join_party_node.activate(guild_members, is_party_full)

# メッセージ単体表示用の簡易関数
func _show_simple_message(msg_text: String) -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	var center_container = CenterContainer.new()
	center_container.name = "DynamicMenu"
	center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(center_container)
	
	var vbox = VBoxContainer.new()
	center_container.add_child(vbox)
	
	var label = Label.new()
	label.text = msg_text
	vbox.add_child(label)
	
	var ok_btn = Button.new()
	ok_btn.text = "閉じる"
	ok_btn.pressed.connect(return_to_guild_main_menu)
	vbox.add_child(ok_btn)
	
	ok_btn.grab_focus()

func _open_call_out_ui() -> void:
	town_system.cleanup_dynamic_menus()
	
	var bg = ColorRect.new()
	bg.name = "DynamicMenu"
	bg.color = Color.BLACK
	bg.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(bg)
	
	var main_vbox = VBoxContainer.new()
	main_vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
	main_vbox.offset_left = 20
	main_vbox.offset_top = 20
	main_vbox.offset_right = -20
	main_vbox.offset_bottom = -20
	bg.add_child(main_vbox)
	
	var title_label = Label.new()
	title_label.text = "呼び出す冒険者を選択してください"
	title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	main_vbox.add_child(title_label)
	
	var spacer = Control.new()
	spacer.custom_minimum_size.y = 10
	main_vbox.add_child(spacer)
	
	var list_vbox = VBoxContainer.new()
	list_vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
	main_vbox.add_child(list_vbox)
	
	var start_idx = _current_page * 6
	var page_members = _call_list_cache.slice(start_idx, start_idx + 6)
	
	_row_buttons.clear()
	
	for i in range(page_members.size()):
		var m: CharaData = page_members[i]
		
		var row_hbox = HBoxContainer.new()
		list_vbox.add_child(row_hbox)
		
		var select_btn = Button.new()
		select_btn.text = "▶"
		select_btn.flat = true 
		select_btn.pressed.connect(func(): _execute_call_out(m))
		row_hbox.add_child(select_btn)
		_row_buttons.append(select_btn)
		
		var img_rect = TextureRect.new()
		if FileAccess.file_exists(m.face_image_path):
			img_rect.texture = load(m.face_image_path)
		img_rect.custom_minimum_size = Vector2(40, 40)
		img_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
		row_hbox.add_child(img_rect)
		
		var info_label = Label.new()
		info_label.text = "【%s】 LV:%d %s (HP:%d/%d MP:%d/%d)" % [
			m.chara_name, m.level, m.get_display_class(), 
			m.current_hp, m.max_hp, m.current_mp, m.max_mp
		]
		row_hbox.add_child(info_label)
	
	var nav_hbox = HBoxContainer.new()
	nav_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
	main_vbox.add_child(nav_hbox)
	
	var next_btn = Button.new()
	next_btn.text = "次の一覧に進む"
	next_btn.pressed.connect(_on_next_page_pressed)
	nav_hbox.add_child(next_btn)
	
	var back_menu_btn = Button.new()
	back_menu_btn.text = "ギルドに戻る"
	back_menu_btn.pressed.connect(_show_guild_menu)
	nav_hbox.add_child(back_menu_btn)
	
	var all_focusable = []
	all_focusable.append_array(_row_buttons)
	all_focusable.append(next_btn)
	all_focusable.append(back_menu_btn)
	
	for i in range(all_focusable.size()):
		var prev = all_focusable[(i - 1 + all_focusable.size()) % all_focusable.size()]
		var next = all_focusable[(i + 1) % all_focusable.size()]
		all_focusable[i].focus_neighbor_top = prev.get_path()
		all_focusable[i].focus_neighbor_bottom = next.get_path()
		
	if _row_buttons.size() > 0:
		_row_buttons[0].grab_focus()
	else:
		next_btn.grab_focus()

func _on_next_page_pressed() -> void:
	if _call_list_cache.size() <= 6:
		return
		
	_current_page += 1
	if (_current_page * 6) >= _call_list_cache.size():
		_current_page = 0
		
	_open_call_out_ui()

func _execute_call_out(chara: CharaData) -> void:
	var slot_assigned = PartyManager.auto_add_member(chara)
	if slot_assigned != -1:
		var msg = "名前【%s】がパーティに加わった。" % chara.chara_name
		_show_guild_message(msg)

# クラスを変える
func _on_change_class_selected() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	if change_class_node:
		change_class_node.activate()

# キャラクター削除
func _on_delete_character_selected() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	if delete_character_node:
		delete_character_node.activate()

func delete_character_from_system(chara: CharaData) -> void:
	if chara in registered_members:
		registered_members.erase(chara)
		print(chara.chara_name, " がシステム(registered_members)から完全消去されました。")

# 名前を変える
func _on_change_name_selected() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	if rename_character_node:
		# パーティメンバー一覧の配列をそのまま渡して改名フローを開始
		var party_list = PartyManager.get_party_members()
		rename_character_node.start_rename_flow(party_list)
		  
# 汎用メッセージ画面(1ボタンでギルドメニューに戻る)           
func _show_guild_message(msg_text: String) -> void:
	town_system.cleanup_dynamic_menus()
	
	var bg = ColorRect.new()
	bg.name = "DynamicMenu"
	bg.color = Color.BLACK
	bg.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(bg)
	
	var vbox = VBoxContainer.new()
	vbox.set_anchors_preset(Control.PRESET_CENTER)
	bg.add_child(vbox)
	
	var label = Label.new()
	label.text = msg_text
	label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	vbox.add_child(label)
	
	var spacer = Control.new()
	spacer.custom_minimum_size.y = 15
	vbox.add_child(spacer)
	
	var ok_btn = Button.new()
	ok_btn.text = "閉じる"
	ok_btn.pressed.connect(_show_guild_menu)
	vbox.add_child(ok_btn)
	
	ok_btn.grab_focus()
				 
# その他の既存機能           
func _on_create_adventurer_selected() -> void:
	if registered_members.size() >= MAX_CREATE_LIMIT:
		if town_system and town_system.creation_node:
			town_system.town_graphic.hide()
			town_system.creation_node.activate_limit_error()
		return

	if town_system:
		town_system.town_graphic.hide()
		if town_system.creation_node:
			town_system.creation_node.activate_name_input()

func _on_creation_canceled() -> void:
	if town_system:
		town_system.town_graphic.show()
		var dynamic_menu = town_system.town_graphic.get_node_or_null("DynamicMenu")
		if dynamic_menu:
			var menu_panel = dynamic_menu.get_child(0)
			if menu_panel and menu_panel.get_child_count() > 0:
				var create_btn = menu_panel.get_child(2) as Button
				if create_btn:
					create_btn.grab_focus.call_deferred()

func register_new_adventurer(new_chara: CharaData) -> bool:
	registered_members.append(new_chara)
	return true

func prepare_return_to_town() -> void:
	if town_system:
		town_system.reset_to_main_town()
		town_system = null

ギルド管理用コード (guild_manager.gd) の上記のように修正してください。メッセージ表示などの調整を行っています。

「見た目を変える」機能

見た目を変える機能の実装です。見た目は冒険者ギルドでしか変更できません。「見た目を変える」は、パーティメンバーが対象であり、冒険者ギルドにいる冒険者は対象外です。

ノード構成の作成

  • CanvasLayer (CanvasLayer)
    • (他は省略)
    • CharacterCreation (Control)
    • JoinParty (Control)
    • LeaveParty (Control)
    • ChangeClass (Control)
    • DeleteCharacter (Control)
    • RenameCharacter (Control)
    • ChangeAppearance(Control)  新規
      • Background (ColorRect)  新規
      • MainVBox (VBoxContainer)  新規
        • MessageLabel (Label)  新規
        • MemberList (VBoxContainer)  新規
          • Row1 (HBoxContainer)  新規
            • CursorBtn (Button)  新規
            • FaceImg (TextureRect)  新規
            • InfoLabel (Label)  新規
          • Row2 (HBoxContainer)  新規
            • CursorBtn (Button)  新規
            • FaceImg (TextureRect)  新規
            • InfoLabel (Label)  新規
          • Row3 (HBoxContainer)  新規
            • CursorBtn (Button)  新規
            • FaceImg (TextureRect)  新規
            • InfoLabel (Label)  新規
          • Row4 (HBoxContainer)  新規
            • CursorBtn (Button)  新規
            • FaceImg (TextureRect)  新規
            • InfoLabel (Label)  新規
          • Row5 (HBoxContainer)  新規
            • CursorBtn (Button)  新規
            • FaceImg (TextureRect)  新規
            • InfoLabel (Label)  新規
          • Row6 (HBoxContainer)  新規
            • CursorBtn (Button)  新規
            • FaceImg (TextureRect)  新規
            • InfoLabel (Label)  新規
        • AppearanceLayout (VBoxContainer)  新規
          • AppearanceTitleLabel (Label)  新規
          • AppearanceWheelControl (Control)  新規
          • AppearanceBottomMenu (HBoxContainer)  新規
            • AppearanceRandomButton (Button)  新規
            • BackToMemberListButton (Button)  新規
            • ConfirmButton (Button)  新規
        • NavigationHBox (HBoxContainer)  新規
          • BackButton (Button)  新規
RenameCharacter (Control)新規作成 ※ 非表示
Background (ColorRect)画面全体を覆う黒塗りの背景。必ず MainVBox より上に配置すること。(カラー: 黒、レイアウト:Full Rect)
MainVBox (VBoxContainer)「冒険者を削除する」の画面全般
MessageLabel (Label)画面上部のメッセージ
「削除する冒険者を選択してください」
MemberList (VBoxContainer)画面中央部に冒険者を6名表示する
Row1~6 (HBoxContainer)1~6行目
CursorBtn (Button)カーソル。呼び出す冒険者を選ぶもの
FaceImg (TextureRect)冒険者の見た目画像
InfoLabel(Label)冒険者の名前、レベル、職業、最大HP、最大MP
AppearanceLayout (VBoxContainer)見た目変更用レイアウト
AppearanceTitleLabel (Label)「新しい見た目を選択してください」
AppearanceWheelControl (Control)画像表示
AppearanceBottomMenu (HBoxContainer)画面下部のボタン
AppearanceRandomButton (HBoxContainer)ランダムで選択
BackToMemberListButton (Button)パーティメンバーを選択する画面に戻る
ConfirmButton (Button)決定
NavigationHBox (HBoxContainer)画面下部のボタン
BackButton (Button)ギルドに戻る

このノード構成を作成します。ChangeAppearance (Control) の非表示を忘れずに実施してください。

スクリプトファイルの作成

class_name ChangeAppearance
extends Control

signal appearance_finished

enum Phase { MEMBER_SELECT, APPEARANCE_SELECT, MESSAGE_OUT }
var _current_phase: Phase = Phase.MEMBER_SELECT

# 多言語対応テキスト(仕様完全準拠)
const TEXT_DATA = {
	"ja": {
		"title_select": "見た目を変えるパーティメンバーを選択してください",
		"title_input": "新しい見た目を選択してください",
		"btn_back_guild": "ギルドに戻る",
		"btn_back_select": "パーティメンバー選択画面に戻る",
		"btn_confirm": "決定",
		"btn_random": "ランダムで選ぶ",
		"success_msg": "見た目が変更されました",
		"no_member_err": "パーティメンバーがいません。"
	},
	"en": {
		"title_select": "Select a party member to change appearance.",
		"title_input": "Select your new appearance.",
		"btn_back_guild": "Back to Guild",
		"btn_back_select": "Back to Selection",
		"btn_confirm": "Confirm",
		"btn_random": "Choose Randomly",
		"success_msg": "Appearance has been changed.",
		"no_member_err": "There are no party members."
	}
}

# 指定されたノード構成への完全一致参照
@onready var message_label: Label = $MainVBox/MessageLabel
@onready var member_list: VBoxContainer = $MainVBox/MemberList
@onready var navigation_hbox: HBoxContainer = $MainVBox/NavigationHBox
@onready var back_button: Button = $MainVBox/NavigationHBox/BackButton

# AppearanceLayout 内
@onready var appearance_layout: VBoxContainer = $MainVBox/AppearanceLayout
@onready var appearance_title_label: Label = $MainVBox/AppearanceLayout/AppearanceTitleLabel
@onready var appearance_wheel_control: Control = $MainVBox/AppearanceLayout/AppearanceWheelControl
@onready var appearance_bottom_menu: HBoxContainer = $MainVBox/AppearanceLayout/AppearanceBottomMenu
@onready var appearance_random_button: Button = $MainVBox/AppearanceLayout/AppearanceBottomMenu/AppearanceRandomButton
@onready var back_to_member_list_button: Button = $MainVBox/AppearanceLayout/AppearanceBottomMenu/BackToMemberListButton
@onready var confirm_button: Button = $MainVBox/AppearanceLayout/AppearanceBottomMenu/ConfirmButton

# ホイール制御用変数
var face_image_paths: Array[String] = []
var face_sprites: Array[TextureRect] = []
var current_image_index: int = 0
var target_wheel_angle: float = 0.0
var current_wheel_angle: float = 0.0
var wheel_tween: Tween = null
var is_wheel_focused: bool = false

# 内部保持
var _party_members: Array = []
var _target_character: CharaData = null
var _row_nodes: Array[Control] = []

func _ready() -> void:
	# Row1 〜 Row6 のノードを配列管理
	_row_nodes = [
		$MainVBox/MemberList/Row1,
		$MainVBox/MemberList/Row2,
		$MainVBox/MemberList/Row3,
		$MainVBox/MemberList/Row4,
		$MainVBox/MemberList/Row5,
		$MainVBox/MemberList/Row6
	]
	
	# シグナル接続
	if back_button and not back_button.pressed.is_connected(_on_back_button_pressed):
		back_button.pressed.connect(_on_back_button_pressed)
	if back_to_member_list_button and not back_to_member_list_button.pressed.is_connected(_on_back_to_member_list_pressed):
		back_to_member_list_button.pressed.connect(_on_back_to_member_list_pressed)
	if confirm_button and not confirm_button.pressed.is_connected(_on_confirm_pressed):
		confirm_button.pressed.connect(_on_confirm_pressed)
	if appearance_random_button and not appearance_random_button.pressed.is_connected(_on_appearance_random_pressed):
		appearance_random_button.pressed.connect(_on_appearance_random_pressed)
		
	# ホイール自体のフォーカス監視接続
	if appearance_wheel_control.focus_mode == Control.FOCUS_NONE:
		appearance_wheel_control.focus_mode = Control.FOCUS_ALL
	appearance_wheel_control.focus_entered.connect(func(): is_wheel_focused = true)
	appearance_wheel_control.focus_exited.connect(func(): is_wheel_focused = false)
		
	hide()

# 外部(GuildManager)からの開始処理
func start_appearance_flow(party_list: Array) -> void:
	_party_members = party_list
	_target_character = null
	show()
	
	# ガード処理: 有効なメンバーがいるかチェック
	var valid_members = _party_members.filter(func(member): return member != null)
	if valid_members.is_empty():
		_show_no_member_error()
		return
		
	_switch_to_member_select_phase()

func _process(_delta: float) -> void:
	if appearance_layout.is_visible_in_tree() and face_image_paths.size() > 0:
		_update_wheel_positions()

func _input(event: InputEvent) -> void:
	if not is_visible_in_tree(): return
	
	if event.is_action_pressed("ui_cancel"):
		get_viewport().set_input_as_handled()
		match _current_phase:
			Phase.MEMBER_SELECT:
				_return_to_guild()
			Phase.APPEARANCE_SELECT:
				_switch_to_member_select_phase()
			Phase.MESSAGE_OUT:
				_return_to_guild()

	elif _current_phase == Phase.MESSAGE_OUT:
		if event.is_action_pressed("ui_accept") or event.is_action_pressed("ui_cancel"):
			get_viewport().set_input_as_handled()
			_return_to_guild()
			
	elif _current_phase == Phase.APPEARANCE_SELECT and is_wheel_focused:
		if event.is_action_pressed("ui_left"):
			get_viewport().set_input_as_handled()
			_rotate_wheel(-1)
		elif event.is_action_pressed("ui_right"):
			get_viewport().set_input_as_handled()
			_rotate_wheel(1)
		# 追加:見た目ホイールフォーカス中に ui_accept が押されたら、決定ボタンへフォーカスを移す
		elif event.is_action_pressed("ui_accept"):
			get_viewport().set_input_as_handled()
			if confirm_button:
				confirm_button.grab_focus()

# ==============================================================================
# フェーズ管理
# ==============================================================================

func _switch_to_member_select_phase() -> void:
	_current_phase = Phase.MEMBER_SELECT
	appearance_layout.hide()
	
	if wheel_tween: wheel_tween.kill()
	message_label.show()
	member_list.show()
	navigation_hbox.show()
	
	var lang = _get_current_lang()
	message_label.text = TEXT_DATA[lang]["title_select"]
	back_button.text = TEXT_DATA[lang]["btn_back_guild"]
	
	_update_member_list_display()

func _switch_to_name_input_phase() -> void: # 便宜上名称は残していますが「見た目選択」への遷移です
	_current_phase = Phase.APPEARANCE_SELECT
	member_list.hide()
	navigation_hbox.hide()
	message_label.hide()
	appearance_layout.show()
	
	var lang = _get_current_lang()
	appearance_title_label.text = TEXT_DATA[lang]["title_input"]
	appearance_random_button.text = TEXT_DATA[lang]["btn_random"]
	back_to_member_list_button.text = TEXT_DATA[lang]["btn_back_select"]
	confirm_button.text = TEXT_DATA[lang]["btn_confirm"]
	
	_scan_face_images_folder()
	_build_appearance_wheel()
	_setup_appearance_focus_neighbors()

func _show_success_message() -> void:
	_current_phase = Phase.MESSAGE_OUT
	member_list.hide()
	navigation_hbox.hide()
	appearance_layout.hide()
	
	message_label.show()
	var lang = _get_current_lang()
	message_label.text = TEXT_DATA[lang]["success_msg"]
	release_focus()

func _show_no_member_error() -> void:
	_current_phase = Phase.MESSAGE_OUT
	member_list.hide()
	navigation_hbox.hide()
	appearance_layout.hide()
	
	message_label.show()
	var lang = _get_current_lang()
	message_label.text = TEXT_DATA[lang]["no_member_err"]
	release_focus()

# ==============================================================================
# 表示更新・ホイールロジック
# ==============================================================================

func _update_member_list_display() -> void:
	var focus_buttons = []
	var lang = _get_current_lang()
	
	for i in range(6):
		var row_node = _row_nodes[i]
		
		if i < _party_members.size() and _party_members[i] != null:
			var member = _party_members[i]
			row_node.show()
			
			var cursor_btn = row_node.get_node("CursorBtn") as Button
			var face_img = row_node.get_node("FaceImg") as TextureRect
			var label = row_node.get_node("Label") as Label
			
			cursor_btn.text = "▶"
			cursor_btn.focus_mode = Control.FOCUS_ALL
			
			if cursor_btn.pressed.is_connected(_on_member_row_selected):
				cursor_btn.pressed.disconnect(_on_member_row_selected)
			cursor_btn.pressed.connect(_on_member_row_selected.bind(member))
			
			focus_buttons.append(cursor_btn)
			
			if FileAccess.file_exists(member.face_image_path):
				face_img.texture = load(member.face_image_path)
			else:
				face_img.texture = null
				
			# 14職業の日本語ダイレクト置換判定
			var raw_class = member.chara_class
			var display_class_name = raw_class
			if lang == "ja":
				match raw_class:
					"Fighter": display_class_name = "戦士"
					"Rogue": display_class_name = "盗賊"
					"Cleric": display_class_name = "僧侶"
					"Wizard": display_class_name = "魔法使い"
					"Ranger": display_class_name = "狩人"
					"Knight": display_class_name = "ナイト"
					"Samurai": display_class_name = "侍"
					"Warlock": display_class_name = "ワーロック"
					"Ninja": display_class_name = "忍者"
					"Sage": display_class_name = "賢者"
			
			label.text = "名前: %-10s  Lv: %2d  職業: %-10s  HP: %d/%d  MP: %d/%d" % [
				member.chara_name, member.level, display_class_name,
				member.current_hp, member.max_hp, member.current_mp, member.max_mp
			]
		else:
			row_node.hide()
			
	focus_buttons.append(back_button)
	
	for i in range(focus_buttons.size()):
		var prev = focus_buttons[(i - 1 + focus_buttons.size()) % focus_buttons.size()]
		var next = focus_buttons[(i + 1) % focus_buttons.size()]
		focus_buttons[i].focus_neighbor_top = prev.get_path()
		focus_buttons[i].focus_neighbor_bottom = next.get_path()
		
	if focus_buttons.size() > 0:
		focus_buttons[0].grab_focus.call_deferred()

func _scan_face_images_folder() -> void:
	face_image_paths.clear()
	var folder_path = "res://image/face/"
	if DirAccess.dir_exists_absolute(folder_path):
		var dir = DirAccess.open(folder_path)
		if dir:
			dir.list_dir_begin()
			var file_name = dir.get_next()
			while file_name != "":
				if not dir.current_is_dir() and (file_name.ends_with(".png") or file_name.ends_with(".PNG")):
					face_image_paths.append(folder_path + file_name)
				file_name = dir.get_next()
			dir.list_dir_end()
	face_image_paths.sort()

func _build_appearance_wheel() -> void:
	for child in appearance_wheel_control.get_children():
		child.queue_free()
	face_sprites.clear()
	
	if face_image_paths.is_empty():
		var error_label = Label.new()
		error_label.text = "No PNG images found in res://image/face/"
		appearance_wheel_control.add_child(error_label)
		return
		
	for i in range(face_image_paths.size()):
		var texture = load(face_image_paths[i])
		var tex_rect = TextureRect.new()
		tex_rect.texture = texture
		tex_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
		tex_rect.custom_minimum_size = Vector2(120, 120)
		tex_rect.size = Vector2(120, 120)
		tex_rect.pivot_offset = tex_rect.size / 2
		appearance_wheel_control.add_child(tex_rect)
		face_sprites.append(tex_rect)
		
	# 初期選択を現在のキャラクターの顔グラに合わせる(あれば)
	current_image_index = 0
	if _target_character and _target_character.face_image_path in face_image_paths:
		current_image_index = face_image_paths.find(_target_character.face_image_path)
		
	var step_angle = (2.0 * PI / face_image_paths.size()) if not face_image_paths.is_empty() else 1.0
	target_wheel_angle = -current_image_index * step_angle
	current_wheel_angle = target_wheel_angle
	_update_wheel_positions()

func _update_wheel_positions() -> void:
	var total_count = face_sprites.size()
	if total_count == 0: return
	var radius_x = 160.0
	var radius_y = 60.0 
	var center = appearance_wheel_control.size / 2
	
	for i in range(total_count):
		var angle_offset = (i * 2.0 * PI / total_count)
		var sprite_angle = current_wheel_angle + angle_offset
		var cos_val = cos(sprite_angle + PI/2)
		var sin_val = sin(sprite_angle + PI/2)
		
		var pos_x = center.x + cos_val * radius_x
		var pos_y = center.y + sin_val * radius_y
		var sprite = face_sprites[i]
		sprite.position = Vector2(pos_x - sprite.size.x / 2, pos_y - sprite.size.y / 2)
		
		var depth_ratio = (sin_val + 1.0) / 2.0
		var current_size = lerp(100.0, 180.0, depth_ratio)
		sprite.size = Vector2(current_size, current_size)
		sprite.pivot_offset = sprite.size / 2
		
		var brightness = lerp(0.4, 1.0, depth_ratio)
		sprite.modulate = Color(brightness, brightness, brightness, 1.0)
		sprite.z_index = int(depth_ratio * 100)

func _rotate_wheel(direction: int) -> void:
	var total_count = face_image_paths.size()
	if total_count == 0: return
	
	current_image_index = (current_image_index + direction + total_count) % total_count
	var step_angle = (2.0 * PI / total_count)
	var ideal_target_angle = -current_image_index * step_angle
	
	var angle_diff = ideal_target_angle - target_wheel_angle
	angle_diff = fposmod(angle_diff + PI, 2.0 * PI) - PI
	target_wheel_angle += angle_diff
	
	if wheel_tween: wheel_tween.kill()
	wheel_tween = create_tween()
	wheel_tween.set_ease(Tween.EASE_OUT)
	wheel_tween.set_trans(Tween.TRANS_QUAD)
	wheel_tween.tween_property(self, "current_wheel_angle", target_wheel_angle, 0.2)

func _rotate_to_specific_index(idx: int) -> void:
	if face_image_paths.is_empty(): return
	current_image_index = idx
	var step_angle = (2.0 * PI / face_image_paths.size())
	var ideal_target_angle = -current_image_index * step_angle
	var angle_diff = ideal_target_angle - target_wheel_angle
	angle_diff = fposmod(angle_diff + PI, 2.0 * PI) - PI
	target_wheel_angle += angle_diff
	
	if wheel_tween: wheel_tween.kill()
	wheel_tween = create_tween()
	wheel_tween.set_ease(Tween.EASE_OUT)
	wheel_tween.set_trans(Tween.TRANS_CUBIC)
	wheel_tween.tween_property(self, "current_wheel_angle", target_wheel_angle, 0.4)

func _setup_appearance_focus_neighbors() -> void:
	var wheel_focus_node = appearance_wheel_control
	
	wheel_focus_node.focus_neighbor_left = wheel_focus_node.get_path()
	wheel_focus_node.focus_neighbor_right = wheel_focus_node.get_path()
	wheel_focus_node.focus_neighbor_bottom = appearance_random_button.get_path()
	
	appearance_random_button.focus_neighbor_top = wheel_focus_node.get_path()
	appearance_random_button.focus_neighbor_left = confirm_button.get_path()
	appearance_random_button.focus_neighbor_right = back_to_member_list_button.get_path()
	
	back_to_member_list_button.focus_neighbor_top = wheel_focus_node.get_path()
	back_to_member_list_button.focus_neighbor_left = appearance_random_button.get_path()
	back_to_member_list_button.focus_neighbor_right = confirm_button.get_path()
	
	confirm_button.focus_neighbor_top = wheel_focus_node.get_path()
	confirm_button.focus_neighbor_left = back_to_member_list_button.get_path()
	confirm_button.focus_neighbor_right = appearance_random_button.get_path()
	
	wheel_focus_node.grab_focus.call_deferred()

# ==============================================================================
# シグナルハンドラ
# ==============================================================================

func _on_member_row_selected(member: CharaData) -> void:
	_target_character = member
	_switch_to_name_input_phase()

func _on_back_button_pressed() -> void:
	_return_to_guild()

func _on_back_to_member_list_pressed() -> void:
	_switch_to_member_select_phase()

func _on_appearance_random_pressed() -> void:
	if face_image_paths.is_empty(): return
	_rotate_to_specific_index(randi() % face_image_paths.size())

func _on_confirm_pressed() -> void:
	if face_image_paths.is_empty() or _target_character == null: return
	
	# 選択された見た目画像を確定反映
	_target_character.face_image_path = face_image_paths[current_image_index]
	_show_success_message()

func _return_to_guild() -> void:
	if wheel_tween: wheel_tween.kill()
	hide()
	appearance_finished.emit()

func _get_current_lang() -> String:
	return "ja" if TranslationServer.get_locale().left(2) == "ja" else "en"

ChangeAppearance ノードを選択した状態で、新規スクリプト change_appearance.gd を作成し、アタッチしてください。コードは上記の通りです。主に「見た目を変える」画面の表示部分を担当しています。

ギルド管理用コード修正

extends Node

# ==============================================================================
# 冒険者ギルド 管理マネージャー (UI・ロジック完全版)
# ==============================================================================

const SYSTEM_MAX_SLOTS: int = 25
const MAX_CREATE_LIMIT: int = 24

var registered_members: Array[CharaData] = []
var _last_selected_menu_idx: int = 0
var town_system: TownSystem = null
var join_party_node: Control = null
var leave_party_node: Control = null
var delete_character_node: Control = null
var change_class_node: Control = null
var rename_character_node: RenameCharacter = null
var change_appearance_node: ChangeAppearance = null

# ギルドメニューの項目定義
const GUILD_MENU_KEYS = [
	"GUILD_MENU_JOIN", 
	"GUILD_MENU_REMOVE", 
	"GUILD_MENU_CREATE", 
	"GUILD_MENU_CHANGE_CLASS", 
	"GUILD_MENU_DELETE", 
	"GUILD_MENU_CHANGE_NAME", 
	"GUILD_MENU_CHANGE_APPEARANCE", 
	"GUILD_MENU_BACK"
]

# 呼び出し画面用のページング管理変数
var _call_list_cache: Array[CharaData] = []
var _current_page: int = 0
var _selected_row_index: int = 0
var _row_buttons: Array[Button] = []

# ==============================================================================
# UI制御・画面遷移ハンドリング
# ==============================================================================

# 街からギルドに入ったときに呼び出される関数
func open_guild(town: TownSystem) -> void:
	town_system = town
	
	if town_system.creation_node:
		var parent = town_system.creation_node.get_parent()
		join_party_node = parent.get_node_or_null("JoinParty")
		leave_party_node = parent.get_node_or_null("LeaveParty")
		change_class_node = parent.get_node_or_null("ChangeClass")
		delete_character_node = parent.get_node_or_null("DeleteCharacter")
		rename_character_node = parent.get_node_or_null("RenameCharacter")
		change_appearance_node = parent.get_node_or_null("ChangeAppearance")
	
	town_system._update_graphic("guild")
	if town_system.town_menu:
		town_system.town_menu.hide()
	
	town_system.cleanup_dynamic_menus()
	
	if town_system.creation_node and not town_system.creation_node.is_connected("creation_canceled", _on_creation_canceled):
		town_system.creation_node.creation_canceled.connect(_on_creation_canceled)
		
	if rename_character_node and not rename_character_node.rename_finished.is_connected(return_to_guild_main_menu):
		rename_character_node.rename_finished.connect(return_to_guild_main_menu)
		
	if change_appearance_node and not change_appearance_node.appearance_finished.is_connected(return_to_guild_main_menu):
		change_appearance_node.appearance_finished.connect(return_to_guild_main_menu)
		
	_show_guild_menu()

# ギルドのメインメニューに戻る
func return_to_guild_main_menu() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
		_show_guild_menu()

func _show_guild_menu() -> void:
	var center_container = CenterContainer.new()
	center_container.name = "DynamicMenu"
	center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(center_container)
	
	var menu_panel = VBoxContainer.new()
	center_container.add_child(menu_panel)
	
	var buttons = []
	for i in range(GUILD_MENU_KEYS.size()):
		var key = GUILD_MENU_KEYS[i]
		var btn = Button.new()
		btn.text = tr(key)
		
		# ボタンが押されたとき、またはフォーカスが当たったときに、そのインデックスを記憶する
		btn.pressed.connect(func(): 
			_last_selected_menu_idx = i
			_on_guild_menu_selected(key)
		)
		btn.focus_entered.connect(func():
			_last_selected_menu_idx = i
		)
		
		menu_panel.add_child(btn)
		buttons.append(btn)
		
		# ボタンの上でキャンセルボタンが押されたら街へ戻る
		btn.gui_input.connect(func(event: InputEvent):
			if event.is_action_pressed("ui_cancel"):
				btn.get_viewport().set_input_as_handled()
				prepare_return_to_town()
		)
	
	for i in range(buttons.size()):
		var prev = buttons[(i - 1 + buttons.size()) % buttons.size()]
		var next = buttons[(i + 1) % buttons.size()]
		buttons[i].focus_neighbor_top = prev.get_path()
		buttons[i].focus_neighbor_bottom = next.get_path()
	
	if _last_selected_menu_idx >= 0 and _last_selected_menu_idx < buttons.size():
		buttons[_last_selected_menu_idx].grab_focus()
	else:
		buttons[0].grab_focus()

func _on_guild_menu_selected(key: String) -> void:
	if key == "GUILD_MENU_BACK":
		prepare_return_to_town()
		return
		
	match key:
		"GUILD_MENU_JOIN":
			_on_call_out_selected()
		"GUILD_MENU_REMOVE":
			_on_leave_party_selected()
		"GUILD_MENU_CREATE":
			_on_create_adventurer_selected()
		"GUILD_MENU_CHANGE_CLASS":
			_on_change_class_selected()
		"GUILD_MENU_DELETE":
			_on_delete_character_selected()
		"GUILD_MENU_CHANGE_NAME":
			_on_change_name_selected()
		"GUILD_MENU_CHANGE_APPEARANCE":
			_on_change_appearance_selected()

# 冒険者をあずける
func _on_leave_party_selected() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	if leave_party_node:
		leave_party_node.activate()

# 現在ギルド(待機所)にいるメンバーの総数を返すヘルパー関数
func get_registered_count() -> int:
	return PartyManager.get_members_in_guild().size()

# 冒険者をよびだす             
func _on_call_out_selected() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	var is_party_full = PartyManager.is_full()
	var guild_members = PartyManager.get_members_in_guild()
	
	if join_party_node:
		join_party_node.activate(guild_members, is_party_full)

# メッセージ単体表示用の簡易関数
func _show_simple_message(msg_text: String) -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	var center_container = CenterContainer.new()
	center_container.name = "DynamicMenu"
	center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(center_container)
	
	var vbox = VBoxContainer.new()
	center_container.add_child(vbox)
	
	var label = Label.new()
	label.text = msg_text
	vbox.add_child(label)
	
	var ok_btn = Button.new()
	ok_btn.text = "閉じる"
	ok_btn.pressed.connect(return_to_guild_main_menu)
	vbox.add_child(ok_btn)
	
	ok_btn.grab_focus()

func _open_call_out_ui() -> void:
	town_system.cleanup_dynamic_menus()
	
	var bg = ColorRect.new()
	bg.name = "DynamicMenu"
	bg.color = Color.BLACK
	bg.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(bg)
	
	var main_vbox = VBoxContainer.new()
	main_vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
	main_vbox.offset_left = 20
	main_vbox.offset_top = 20
	main_vbox.offset_right = -20
	main_vbox.offset_bottom = -20
	bg.add_child(main_vbox)
	
	var title_label = Label.new()
	title_label.text = "呼び出す冒険者を選択してください"
	title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	main_vbox.add_child(title_label)
	
	var spacer = Control.new()
	spacer.custom_minimum_size.y = 10
	main_vbox.add_child(spacer)
	
	var list_vbox = VBoxContainer.new()
	list_vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
	main_vbox.add_child(list_vbox)
	
	var start_idx = _current_page * 6
	var page_members = _call_list_cache.slice(start_idx, start_idx + 6)
	
	_row_buttons.clear()
	
	for i in range(page_members.size()):
		var m: CharaData = page_members[i]
		
		var row_hbox = HBoxContainer.new()
		list_vbox.add_child(row_hbox)
		
		var select_btn = Button.new()
		select_btn.text = "▶"
		select_btn.flat = true 
		select_btn.pressed.connect(func(): _execute_call_out(m))
		row_hbox.add_child(select_btn)
		_row_buttons.append(select_btn)
		
		var img_rect = TextureRect.new()
		if FileAccess.file_exists(m.face_image_path):
			img_rect.texture = load(m.face_image_path)
		img_rect.custom_minimum_size = Vector2(40, 40)
		img_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
		row_hbox.add_child(img_rect)
		
		var info_label = Label.new()
		info_label.text = "【%s】 LV:%d %s (HP:%d/%d MP:%d/%d)" % [
			m.chara_name, m.level, m.get_display_class(), 
			m.current_hp, m.max_hp, m.current_mp, m.max_mp
		]
		row_hbox.add_child(info_label)
	
	var nav_hbox = HBoxContainer.new()
	nav_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
	main_vbox.add_child(nav_hbox)
	
	var next_btn = Button.new()
	next_btn.text = "次の一覧に進む"
	next_btn.pressed.connect(_on_next_page_pressed)
	nav_hbox.add_child(next_btn)
	
	var back_menu_btn = Button.new()
	back_menu_btn.text = "ギルドに戻る"
	back_menu_btn.pressed.connect(_show_guild_menu)
	nav_hbox.add_child(back_menu_btn)
	
	var all_focusable = []
	all_focusable.append_array(_row_buttons)
	all_focusable.append(next_btn)
	all_focusable.append(back_menu_btn)
	
	for i in range(all_focusable.size()):
		var prev = all_focusable[(i - 1 + all_focusable.size()) % all_focusable.size()]
		var next = all_focusable[(i + 1) % all_focusable.size()]
		all_focusable[i].focus_neighbor_top = prev.get_path()
		all_focusable[i].focus_neighbor_bottom = next.get_path()
		
	if _row_buttons.size() > 0:
		_row_buttons[0].grab_focus()
	else:
		next_btn.grab_focus()

func _on_next_page_pressed() -> void:
	if _call_list_cache.size() <= 6:
		return
		
	_current_page += 1
	if (_current_page * 6) >= _call_list_cache.size():
		_current_page = 0
		
	_open_call_out_ui()

func _execute_call_out(chara: CharaData) -> void:
	var slot_assigned = PartyManager.auto_add_member(chara)
	if slot_assigned != -1:
		var msg = "名前【%s】がパーティに加わった。" % chara.chara_name
		_show_guild_message(msg)

# クラスを変える
func _on_change_class_selected() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	if change_class_node:
		change_class_node.activate()

# キャラクター削除
func _on_delete_character_selected() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	if delete_character_node:
		delete_character_node.activate()

func delete_character_from_system(chara: CharaData) -> void:
	if chara in registered_members:
		registered_members.erase(chara)
		print(chara.chara_name, " がシステム(registered_members)から完全消去されました。")

# 名前を変える
func _on_change_name_selected() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
	
	if rename_character_node:
		# パーティメンバー一覧の配列をそのまま渡して改名フローを開始
		var party_list = PartyManager.get_party_members()
		rename_character_node.start_rename_flow(party_list)

# 見た目を変える
func _on_change_appearance_selected() -> void:
	if town_system:
		town_system.cleanup_dynamic_menus()
		
	if change_appearance_node:
		# パーティメンバー一覧を取得してそのまま見た目変更フローを開始
		var party_list = PartyManager.get_party_members()
		change_appearance_node.start_appearance_flow(party_list)
		  
# 汎用メッセージ画面(1ボタンでギルドメニューに戻る)           
func _show_guild_message(msg_text: String) -> void:
	town_system.cleanup_dynamic_menus()
	
	var bg = ColorRect.new()
	bg.name = "DynamicMenu"
	bg.color = Color.BLACK
	bg.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_system.town_graphic.add_child(bg)
	
	var vbox = VBoxContainer.new()
	vbox.set_anchors_preset(Control.PRESET_CENTER)
	bg.add_child(vbox)
	
	var label = Label.new()
	label.text = msg_text
	label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	vbox.add_child(label)
	
	var spacer = Control.new()
	spacer.custom_minimum_size.y = 15
	vbox.add_child(spacer)
	
	var ok_btn = Button.new()
	ok_btn.text = "閉じる"
	ok_btn.pressed.connect(_show_guild_menu)
	vbox.add_child(ok_btn)
	
	ok_btn.grab_focus()
				 
# その他の既存機能           
func _on_create_adventurer_selected() -> void:
	if registered_members.size() >= MAX_CREATE_LIMIT:
		if town_system and town_system.creation_node:
			town_system.town_graphic.hide()
			town_system.creation_node.activate_limit_error()
		return

	if town_system:
		town_system.town_graphic.hide()
		if town_system.creation_node:
			town_system.creation_node.activate_name_input()

func _on_creation_canceled() -> void:
	if town_system:
		town_system.town_graphic.show()
		var dynamic_menu = town_system.town_graphic.get_node_or_null("DynamicMenu")
		if dynamic_menu:
			var menu_panel = dynamic_menu.get_child(0)
			if menu_panel and menu_panel.get_child_count() > 0:
				var create_btn = menu_panel.get_child(2) as Button
				if create_btn:
					create_btn.grab_focus.call_deferred()

func register_new_adventurer(new_chara: CharaData) -> bool:
	registered_members.append(new_chara)
	return true

# 【追加・修正】街へ戻る前のクリーンアップ処理にインデックスのリセットを追加
func prepare_return_to_town() -> void:
	if town_system:
		town_system.reset_to_main_town()
		town_system = null
		_last_selected_menu_idx = 0

ギルド管理用コード (guild_manager.gd) の上記のように修正してください。メッセージ表示などの調整を行っています。

お伝えしてきている通り、設計があまかったりコードに首をかしげたくなったりするかもしれません。AI生成のコードやノード構成であることもあり、ご容赦いただけると幸いです。

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

この記事を書いた人

コメント

コメントする

CAPTCHA


目次