MENU

Godot 4 でWiz系ダンジョンRPG開発 (オートマッピング) [011]

今回はダンジョンRPGにおけるオートマッピングを実装していきます。どのようなイメージになるかは上記画像をクリックしてください。ダンジョン周りの調整もしています。

前回は街の枠組みを作りました。冒険者ギルドなどに行けるようにし、街のBGMが流れるようになりました。今まで作成してきたのは以下の通りです。詳しくは過去記事をご確認ください。

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

ダンジョンの効果音処理 – 修正

これまではダンジョン処理用のコードに効果音を出すようにしていました。しかし、これを音楽処理用のスクリプトに移し替えます。(今までの設計がマズかったので直すという話です)

効果音用ノード配置変更

今まで効果音用ノードは Camera3D の下にありました。それをドラッグして AudioManager の下に持ってきます。

音楽処理用コード修正

# audio_manager.gd
extends Node

@onready var bgm_player: AudioStreamPlayer = $BGMPlayer
@onready var sfx_player: AudioStreamPlayer = $SfxPlayer

# 再生するBGMのリスト(将来的な管理用)
const BGM_FILES = {
	"town": preload("res://music/town.mp3"),
	# "dungeon": preload("res://music/dungeon_theme.ogg"),
	# "battle": preload("res://music/battle_theme.ogg")
}

# SEの定義
const SFX_FILES = {
	"door_open": preload("res://sound/door_open.mp3"),
	"stairs": preload("res://sound/stairs.mp3")
}

func play_bgm(key: String) -> void:
	if not BGM_FILES.has(key):
		print("BGM key not found: ", key)
		return
	
	# すでにその曲が流れていれば何もしない
	if bgm_player.stream == BGM_FILES[key] and bgm_player.playing:
		return
		
	bgm_player.stream = BGM_FILES[key]
	bgm_player.play()

func stop_bgm() -> void:
	bgm_player.stop()

func play_sfx(key: String) -> void:
	if not SFX_FILES.has(key): return
	sfx_player.stream = SFX_FILES[key]
	sfx_player.play()

音楽処理用のコード (audio_manager.gd) を上記のように修正します。これは効果音用ノードと効果音ファイルの登録、効果音再生のメソッドを追加するものです。

ダンジョン処理用コード修正

# ==============================================================================
# dungeon.gd : 商用・量産対応版(JSONデータ駆動型設計・プランA対応版)
# ==============================================================================
class_name DungeonExplore
extends Camera3D

signal battle_triggered
signal town_entered

enum Direction { NORTH, EAST, SOUTH, WEST }

const MOVE_STEP: float = 2.0
const ROTATE_STEP: float = 90.0

const WALL_ITEM_ID: int = 1
const DOOR_ITEM_ID: int = 2
const STAIRS_DOWN_ITEM_ID: int = 3
const STAIRS_UP_ITEM_ID: int = 4

const MIN_SAFE_STEPS: int = 4
const BASE_ENCOUNTER_CHANCE: float = 0.1

# 💾 JSONから読み込んだマスターデータを格納するメモリバッファ
var dungeon_map_data: Dictionary = {}

var current_grid_pos: Vector2i = Vector2i.ZERO
var current_dir: Direction = Direction.NORTH
var is_showing_message: bool = false
var is_explore_active: bool = false
var triggered_events: Dictionary = {}
var steps_since_last_fight: int = 0
var opened_doors: Dictionary = {}
var current_floor: int = 1

var dungeon_start_position: Vector3
var dungeon_start_rotation: Vector3

@onready var grid_map: GridMap = $"../GridMap"
@onready var status_label: Label = $"../CanvasLayer/Label"
@onready var message_window: PanelContainer = $"../CanvasLayer/PanelContainer"
@onready var battle_text: Label = $"../CanvasLayer/PanelContainer/BattleMenu/BattleText"

@onready var attack_button: Button = $"../CanvasLayer/PanelContainer/BattleMenu/AttackButton"
@onready var auto_button: Button = $"../CanvasLayer/PanelContainer/BattleMenu/AutoButton"
@onready var flee_button: Button = $"../CanvasLayer/PanelContainer/BattleMenu/FleeButton"

func _ready() -> void:
	dungeon_start_position = global_position
	dungeon_start_rotation = global_rotation
	steps_since_last_fight = 0
	
	# 💡 起動時にJSONマスターデータをロード
	_load_map_data_from_json()
	update_player_status()

# 📥 JSONファイルを読み込んで辞書にパースする堅牢なデータロード処理
func _load_map_data_from_json() -> void:
	var file_path: String = "res://data/map_data.json"
	
	if not FileAccess.file_exists(file_path):
		print("【Critical Error】マップデータファイルが見つかりません: ", file_path)
		return
		
	var file = FileAccess.open(file_path, FileAccess.READ)
	var json_string = file.get_as_text()
	file.close()
	
	var json = JSON.new()
	var error = json.parse(json_string)
	
	if error == OK:
		if typeof(json.data) == TYPE_DICTIONARY:
			dungeon_map_data = json.data
			print("【System】JSONマップデータのロードに成功しました。")
		else:
			print("【Error】JSONのルートがオブジェクト(辞書)形式ではありません。")
	else:
		print("【Error】JSONパース失敗。 行: ", json.get_error_line(), " エラー: ", json.get_error_message())

# 指定フロアの敵出現数設定を安全に取得するヘルパー
func get_floor_enemy_limits(floor_num: int) -> Dictionary:
	var floor_key: String = str(floor_num)
	var default_limits: Dictionary = {"min": 1, "max": 4} # フォールバック用初期値
	
	if dungeon_map_data.has(floor_key):
		var floor_data: Dictionary = dungeon_map_data[floor_key]
		if floor_data.has("settings") and typeof(floor_data["settings"]) == TYPE_DICTIONARY:
			var settings: Dictionary = floor_data["settings"]
			return {
				"min": int(settings.get("min_enemies", 1)),
				"max": int(settings.get("max_enemies", 4))
			}
	return default_limits

# 階段移動処理(去る階層の扉を復活させる)
func teleport_by_stairs(current_stair_coord: Vector3i) -> void:
	var floor_key: String = str(current_floor)
	if not dungeon_map_data.has(floor_key): return
	var floor_data: Dictionary = dungeon_map_data[floor_key]
	
	var coord_key: String = "%d,%d,%d" % [current_stair_coord.x, current_stair_coord.y, current_stair_coord.z]
	if not floor_data.has(coord_key): return

	var warp_info: Dictionary = floor_data[coord_key]
	var next_floor: int = int(warp_info["target_floor"])
	var target_pos_array: Array = warp_info["target_pos"]
	var next_pos: Vector3i = Vector3i(target_pos_array[0], target_pos_array[1], target_pos_array[2])

	steps_since_last_fight = 0
	get_node("/root/Main/AudioManager").play_sfx("stairs")

	if next_floor == 0:
		_respawn_doors_on_floor(current_floor)
		town_entered.emit()
	else:
		_respawn_doors_on_floor(current_floor)
		
		var old_floor: int = current_floor
		current_floor = next_floor
		
		var keep_eye_height: float = global_position.y
		
		var safe_global_pos: Vector3 = _get_adjacent_walkable_pos(next_pos)
		global_position = safe_global_pos
		global_position.y = keep_eye_height
		
		force_update_transform()
		
		var final_map_coord = grid_map.local_to_map(grid_map.to_local(global_position))
		current_grid_pos = Vector2i(final_map_coord.x, final_map_coord.z)
		
		if current_floor > old_floor:
			show_explore_message("階段を下りた…… 🕳️\nここは B%d だ。\n\n(Wボタンでメッセージを閉じる)" % current_floor)
		else:
			show_explore_message("階段を上った…… 🧗\nここは B%d だ。\n\n(Wボタンでメッセージを閉じる)" % current_floor)
		update_player_status()

func set_explore_active(active: bool) -> void:
	is_explore_active = active
	if is_explore_active:
		update_player_status()
	else:
		if status_label:
			status_label.text = ""

func reset_to_dungeon_start() -> void:
	current_floor = 1
	steps_since_last_fight = 0
	
	var town_stair_map_coord: Vector3i = Vector3i.ZERO
	var found: bool = false
	
	if dungeon_map_data.has("1"):
		var b1_data: Dictionary = dungeon_map_data["1"]
		for coord_str in b1_data.keys():
			# "settings" キーは階段座標データではないのでスキップ
			if coord_str == "settings": continue
			
			var warp_info: Dictionary = b1_data[coord_str]
			if int(warp_info["target_floor"]) == 0:
				var parts = coord_str.split(",")
				if parts.size() == 3:
					town_stair_map_coord = Vector3i(int(parts[0]), int(parts[1]), int(parts[2]))
					found = true
					print("【System】JSONから街への階段座標を検出しました: ", town_stair_map_coord)
					break
					
	if not found:
		print("【Critical Error】JSONのB1データ内に、target_floorが0の階段(街へのゲート)が見つかりません。")
		var clean_start_pos: Vector3 = dungeon_start_position
		clean_start_pos.y = 0.0
		town_stair_map_coord = grid_map.local_to_map(grid_map.to_local(clean_start_pos))

	var keep_eye_height: float = dungeon_start_position.y
	var safe_start_pos: Vector3 = _get_adjacent_walkable_pos(town_stair_map_coord)
	
	global_position = safe_start_pos
	global_position.y = keep_eye_height
	global_rotation = dungeon_start_rotation
	
	update_player_status()

func _unhandled_input(event: InputEvent) -> void:
	if not event.is_pressed() or event.is_echo(): return
	if not is_explore_active: return
	if is_showing_message:
		if event.is_action_pressed("move_forward"): close_message_window()
		return
	if event.is_action_pressed("move_forward"): try_move_direction(Vector3(0, 0, -1))
	elif event.is_action_pressed("move_backward"): try_move_direction(Vector3(0, 0, 1))
	elif event.is_action_pressed("move_left"): try_move_direction(Vector3(-1, 0, 0))
	elif event.is_action_pressed("move_right"): try_move_direction(Vector3(1, 0, 0))
	elif event.is_action_pressed("turn_left"): rotate_horizontal(ROTATE_STEP)
	elif event.is_action_pressed("turn_right"): rotate_horizontal(-ROTATE_STEP)

func try_move_direction(local_direction: Vector3) -> void:
	var direction_vector: Vector3 = global_transform.basis * local_direction
	direction_vector.y = 0
	direction_vector = direction_vector.normalized()
	
	var target_global_pos: Vector3 = global_position + (direction_vector * MOVE_STEP)
	var map_coord: Vector3i = grid_map.local_to_map(grid_map.to_local(target_global_pos))
	var cell_item_id: int = grid_map.get_cell_item(map_coord)
	
	if cell_item_id == WALL_ITEM_ID: return
	
	var is_door_opened: bool = false
	if opened_doors.has(current_floor):
		for door_data in opened_doors[current_floor]:
			if door_data["coord"] == map_coord:
				is_door_opened = true
				break
	
	if cell_item_id == DOOR_ITEM_ID and not is_door_opened:
		interact_with_door(map_coord)
	
	var old_map_coord: Vector3i = grid_map.local_to_map(grid_map.to_local(global_position))
	global_position = target_global_pos
	update_player_status()
	
	var current_map_coord: Vector3i = grid_map.local_to_map(grid_map.to_local(global_position))
	var current_cell_id: int = grid_map.get_cell_item(current_map_coord)
	
	if current_map_coord != old_map_coord:
		if current_cell_id == STAIRS_DOWN_ITEM_ID or current_cell_id == STAIRS_UP_ITEM_ID:
			teleport_by_stairs(current_map_coord)
			return

	is_door_opened = false
	if opened_doors.has(current_floor):
		for door_data in opened_doors[current_floor]:
			if door_data["coord"] == current_map_coord:
				is_door_opened = true
				break
				
	if current_cell_id == DOOR_ITEM_ID and not is_door_opened:
		return

	var event_triggered: bool = check_coordinate_event()
	if not event_triggered:
		check_random_encounter()

func interact_with_door(door_coord: Vector3i) -> void:
	if not opened_doors.has(current_floor):
		opened_doors[current_floor] = []
		
	var current_orientation: int = grid_map.get_cell_item_orientation(door_coord)
	var door_data: Dictionary = {
		"coord": door_coord,
		"orientation": current_orientation
	}
	opened_doors[current_floor].append(door_data)
		
	grid_map.set_cell_item(door_coord, GridMap.INVALID_CELL_ITEM)
	
	get_node("/root/Main/AudioManager").play_sfx("door_open")

func _respawn_doors_on_floor(floor_num: int) -> void:
	if not opened_doors.has(floor_num): return
	var closed_count: int = 0
	for door_data in opened_doors[floor_num]:
		var coord: Vector3i = door_data["coord"]
		var orientation: int = door_data["orientation"]
		grid_map.set_cell_item(coord, DOOR_ITEM_ID, orientation)
		closed_count += 1
		
	opened_doors[floor_num].clear()
	print("【System】階層 B%d の扉を正しい向きでリセットしました。復活数: %d" % [floor_num, closed_count])

func _get_adjacent_walkable_pos(stair_map_pos: Vector3i) -> Vector3:
	var directions = [Vector3i(0, 0, -1), Vector3i(1, 0, 0), Vector3i(0, 0, 1), Vector3i(-1, 0, 0)]
	for offset in directions:
		var check_pos = stair_map_pos + offset
		var cell_id = grid_map.get_cell_item(check_pos)
		if cell_id != WALL_ITEM_ID and cell_id != STAIRS_DOWN_ITEM_ID and cell_id != STAIRS_UP_ITEM_ID:
			return grid_map.to_global(grid_map.map_to_local(check_pos))
	return grid_map.to_global(grid_map.map_to_local(stair_map_pos))

func rotate_horizontal(deg: float) -> void:
	rotate_y(deg_to_rad(deg))
	update_player_status()

func update_player_status() -> void:
	if not is_inside_tree() or not is_explore_active: return
	var map_coord: Vector3i = grid_map.local_to_map(grid_map.to_local(global_position))
	current_grid_pos = Vector2i(map_coord.x, map_coord.z)
	var forward_vector: Vector3 = -global_transform.basis.z
	if abs(forward_vector.z) > abs(forward_vector.x):
		current_dir = Direction.NORTH if forward_vector.z < -0.5 else Direction.SOUTH
	else:
		current_dir = Direction.EAST if forward_vector.x > 0.5 else Direction.WEST
	update_ui_display()

func update_ui_display() -> void:
	if status_label == null: return
	var dir_string: String = ""
	match current_dir:
		Direction.NORTH: dir_string = "北"
		Direction.EAST:  dir_string = "東"
		Direction.SOUTH: dir_string = "南"
		Direction.WEST:  dir_string = "西"
	status_label.text = "B%d | 現在地: (X: %d, Y: %d) | 向き: %s" % [current_floor, current_grid_pos.x, current_grid_pos.y, dir_string]

func check_coordinate_event() -> bool:
	var events: Dictionary = {
		Vector2i(0, -2): { "message": "「前方に不気味な気配を感じる…」\n\n(Wボタンで閉じる)", "is_repeatable": false },
		Vector2i(2, -4): { "message": "壁に血文字が刻まれている。\n『引き返せ、この先は深淵なり』\n\n(Wボタンで閉じる)", "is_repeatable": false },
		Vector2i(1, -1): { "message": "足元でカチリと音がした!\nピピッ!マスの罠が作動した!\n\n(Wボタンで閉じる)", "is_repeatable": true }
	}
	if not events.has(current_grid_pos): return false
	var event_data: Dictionary = events[current_grid_pos]
	if not event_data["is_repeatable"] and triggered_events.get(current_grid_pos) == true: return false
	show_explore_message(event_data["message"])
	if not event_data["is_repeatable"]: triggered_events[current_grid_pos] = true
	return true

func check_random_encounter() -> void:
	steps_since_last_fight += 1
	if steps_since_last_fight <= MIN_SAFE_STEPS: return
	var current_chance: float = BASE_ENCOUNTER_CHANCE * (steps_since_last_fight - MIN_SAFE_STEPS)
	var roll: float = randf()
	if roll < current_chance:
		steps_since_last_fight = 0
		battle_triggered.emit()

func show_explore_message(text: String) -> void:
	if attack_button: attack_button.hide()
	if auto_button: auto_button.hide()
	if flee_button: flee_button.hide()
	battle_text.text = text
	message_window.show()
	is_showing_message = true

func close_message_window() -> void:
	message_window.hide()
	is_showing_message = false

ダンジョン処理用のコード (dungeon.gd) を上記のように修正します。これは、今までこのスクリプト内で効果音プレイヤーをハンドリングしていたのをやめるようにしたものです。ゲーム上は何も変わっていません。

英語 / 日本語表記切り替え処理 (ローカライズ)

今までダンジョン内で表示されるメッセージなどは日本語でした。その日本語は、スクリプトファイルに直書きされたものです。これを JSONファイルで管理するように修正します。(これもそもそもの設計がマズかったという話です)

JSON ファイル作成 (テキストメッセージ用)

{
  "UI_DIRECTION_NORTH": { "en": "North", "ja": "北" },
  "UI_DIRECTION_EAST":  { "en": "East",  "ja": "東" },
  "UI_DIRECTION_SOUTH": { "en": "South", "ja": "南" },
  "UI_DIRECTION_WEST":  { "en": "West",  "ja": "西" },
  "UI_STATUS_FORMAT":   { "en": "B%d | Location: (X: %d, Y: %d) | Facing: %s", "ja": "B%d | 現在地: (X: %d, Y: %d) | 向き: %s" },
  "MSG_STAIRS_UP": {
    "en": "You climbed the stairs.\nYou are now on floor B%d.\n\n(Press Enter to close)",
    "ja": "階段を上った。\nここは B%d だ。\n\n(決定ボタンでメッセージを閉じる)"
  },
  "MSG_STAIRS_DOWN": {
    "en": "You descended the stairs.\nYou are now on floor B%d.\n\n(Press Enter to close)",
    "ja": "階段を下りた。\nここは B%d だ。\n\n(決定ボタンでメッセージを閉じる)"
  },
  "MSG_EVENT_0_-2": {
    "en": "You feel an eerie presence ahead.\n\n(Press Enter to close)",
    "ja": "前方に不気味な気配を感じる。\n\n(決定ボタンで閉じる)"
  },
  "MSG_EVENT_2_-4": {
    "en": "Blood-red letters are carved into the wall:\n'Turn back, for this path leads to the abyss.'\n\n(Press Enter to close)",
    "ja": "壁に血文字が刻まれている。\n『引き返せ、この先は深淵なり』\n\n(決定ボタンで閉じる)"
  },
  "MSG_EVENT_1_-1": {
    "en": "A sharp click echoes beneath your feet!\nA trap has been triggered!\n\n(Press Enter to close)",
    "ja": "足元でカチリと音がした!\n罠が作動した!\n\n(決定ボタンで閉じる)"
  }
}

res://data/dungeon_text.json を作成し、翻訳キーと各言語のテキストを定義してください。ここではdungeon_text.json は上記の内容とします。記述後は忘れずにインポートしましょう。

ダンジョン処理用コード修正

# ==============================================================================
# dungeon.gd
# ==============================================================================
class_name DungeonExplore
extends Camera3D

signal battle_triggered
signal town_entered

enum Direction { NORTH, EAST, SOUTH, WEST }

const MOVE_STEP: float = 2.0
const ROTATE_STEP: float = 90.0

const WALL_ITEM_ID: int = 1
const DOOR_ITEM_ID: int = 2
const STAIRS_DOWN_ITEM_ID: int = 3
const STAIRS_UP_ITEM_ID: int = 4

const MIN_SAFE_STEPS: int = 4
const BASE_ENCOUNTER_CHANCE: float = 0.1

# JSONから読み込んだマスターデータを格納するメモリバッファ
var dungeon_map_data: Dictionary = {}
var dungeon_text_data: Dictionary = {}

var current_grid_pos: Vector2i = Vector2i.ZERO
var current_dir: Direction = Direction.NORTH
var is_showing_message: bool = false
var is_explore_active: bool = false
var triggered_events: Dictionary = {}
var steps_since_last_fight: int = 0
var opened_doors: Dictionary = {}
var current_floor: int = 1

var dungeon_start_position: Vector3
var dungeon_start_rotation: Vector3

@onready var grid_map: GridMap = $"../GridMap"
@onready var status_label: Label = $"../CanvasLayer/Label"
@onready var message_window: PanelContainer = $"../CanvasLayer/PanelContainer"
@onready var battle_text: Label = $"../CanvasLayer/PanelContainer/BattleMenu/BattleText"

@onready var attack_button: Button = $"../CanvasLayer/PanelContainer/BattleMenu/AttackButton"
@onready var auto_button: Button = $"../CanvasLayer/PanelContainer/BattleMenu/AutoButton"
@onready var flee_button: Button = $"../CanvasLayer/PanelContainer/BattleMenu/FleeButton"

func _ready() -> void:
	dungeon_start_position = global_position
	dungeon_start_rotation = global_rotation
	steps_since_last_fight = 0
	
	# 起動時にJSONマスターデータをロード
	_load_map_data_from_json()
	_load_dungeon_text_from_json()
	update_player_status()

# JSONファイルを読み込んで辞書にパースする堅牢なデータロード処理
func _load_map_data_from_json() -> void:
	var file_path: String = "res://data/map_data.json"
	
	if not FileAccess.file_exists(file_path):
		print("【Critical Error】マップデータファイルが見つかりません: ", file_path)
		return
		
	var file = FileAccess.open(file_path, FileAccess.READ)
	var json_string = file.get_as_text()
	file.close()
	
	var json = JSON.new()
	var error = json.parse(json_string)
	
	if error == OK:
		if typeof(json.data) == TYPE_DICTIONARY:
			dungeon_map_data = json.data
			print("【System】JSONマップデータのロードに成功しました。")
		else:
			print("【Error】JSONのルートがオブジェクト(辞書)形式ではありません。")
	else:
		print("【Error】JSONパース失敗。 行: ", json.get_error_line(), " エラー: ", json.get_error_message())

func _load_dungeon_text_from_json() -> void:
	var file_path = "res://data/dungeon_text.json"
	if FileAccess.file_exists(file_path):
		var file = FileAccess.open(file_path, FileAccess.READ)
		var json = JSON.new()
		json.parse(file.get_as_text())
		dungeon_text_data = json.data
		file.close()

# 翻訳テキストを取得する関数
func get_localized_text(key: String, params: Array = []) -> String:
	if not dungeon_text_data.has(key):
		return "Missing key: " + key
	
	var locale = TranslationServer.get_locale().left(2)
	var dict = dungeon_text_data[key]
	var text = dict.get(locale, dict.get("en", "Missing text"))
	
	if params.size() > 0:
		return text % params
	return text

# 指定フロアの敵出現数設定を安全に取得するヘルパー
func get_floor_enemy_limits(floor_num: int) -> Dictionary:
	var floor_key: String = str(floor_num)
	var default_limits: Dictionary = {"min": 1, "max": 4} # フォールバック用初期値
	
	if dungeon_map_data.has(floor_key):
		var floor_data: Dictionary = dungeon_map_data[floor_key]
		if floor_data.has("settings") and typeof(floor_data["settings"]) == TYPE_DICTIONARY:
			var settings: Dictionary = floor_data["settings"]
			return {
				"min": int(settings.get("min_enemies", 1)),
				"max": int(settings.get("max_enemies", 4))
			}
	return default_limits

# 階段移動処理(去る階層の扉を復活させる)
func teleport_by_stairs(current_stair_coord: Vector3i) -> void:
	var floor_key: String = str(current_floor)
	if not dungeon_map_data.has(floor_key): return
	var floor_data: Dictionary = dungeon_map_data[floor_key]
	
	var coord_key: String = "%d,%d,%d" % [current_stair_coord.x, current_stair_coord.y, current_stair_coord.z]
	if not floor_data.has(coord_key): return

	var warp_info: Dictionary = floor_data[coord_key]
	var next_floor: int = int(warp_info["target_floor"])
	var target_pos_array: Array = warp_info["target_pos"]
	var next_pos: Vector3i = Vector3i(target_pos_array[0], target_pos_array[1], target_pos_array[2])

	steps_since_last_fight = 0
	get_node("/root/Main/AudioManager").play_sfx("stairs")

	if next_floor == 0:
		_respawn_doors_on_floor(current_floor)
		town_entered.emit()
	else:
		_respawn_doors_on_floor(current_floor)
		
		var old_floor: int = current_floor
		current_floor = next_floor
		
		var keep_eye_height: float = global_position.y
		
		var safe_global_pos: Vector3 = _get_adjacent_walkable_pos(next_pos)
		global_position = safe_global_pos
		global_position.y = keep_eye_height
		
		force_update_transform()
		
		var final_map_coord = grid_map.local_to_map(grid_map.to_local(global_position))
		current_grid_pos = Vector2i(final_map_coord.x, final_map_coord.z)
		
		if current_floor > old_floor:
			show_explore_message(get_localized_text("MSG_STAIRS_DOWN", [current_floor]))
		else:
			show_explore_message(get_localized_text("MSG_STAIRS_UP", [current_floor]))
		update_player_status()

func set_explore_active(active: bool) -> void:
	is_explore_active = active
	if is_explore_active:
		update_player_status()
	else:
		if status_label:
			status_label.text = ""

func reset_to_dungeon_start() -> void:
	current_floor = 1
	steps_since_last_fight = 0
	
	var town_stair_map_coord: Vector3i = Vector3i.ZERO
	var found: bool = false
	
	if dungeon_map_data.has("1"):
		var b1_data: Dictionary = dungeon_map_data["1"]
		for coord_str in b1_data.keys():
			# "settings" キーは階段座標データではないのでスキップ
			if coord_str == "settings": continue
			
			var warp_info: Dictionary = b1_data[coord_str]
			if int(warp_info["target_floor"]) == 0:
				var parts = coord_str.split(",")
				if parts.size() == 3:
					town_stair_map_coord = Vector3i(int(parts[0]), int(parts[1]), int(parts[2]))
					found = true
					print("【System】JSONから街への階段座標を検出しました: ", town_stair_map_coord)
					break
					
	if not found:
		print("【Critical Error】JSONのB1データ内に、target_floorが0の階段(街へのゲート)が見つかりません。")
		var clean_start_pos: Vector3 = dungeon_start_position
		clean_start_pos.y = 0.0
		town_stair_map_coord = grid_map.local_to_map(grid_map.to_local(clean_start_pos))

	var keep_eye_height: float = dungeon_start_position.y
	var safe_start_pos: Vector3 = _get_adjacent_walkable_pos(town_stair_map_coord)
	
	global_position = safe_start_pos
	global_position.y = keep_eye_height
	global_rotation = dungeon_start_rotation
	
	update_player_status()

func _unhandled_input(event: InputEvent) -> void:
	if not event.is_pressed() or event.is_echo(): return
	if not is_explore_active: return
	if is_showing_message:
		if event.is_action_pressed("ui_accept"): close_message_window()
		return
	if event.is_action_pressed("move_forward"): try_move_direction(Vector3(0, 0, -1))
	elif event.is_action_pressed("move_backward"): try_move_direction(Vector3(0, 0, 1))
	elif event.is_action_pressed("move_left"): try_move_direction(Vector3(-1, 0, 0))
	elif event.is_action_pressed("move_right"): try_move_direction(Vector3(1, 0, 0))
	elif event.is_action_pressed("turn_left"): rotate_horizontal(ROTATE_STEP)
	elif event.is_action_pressed("turn_right"): rotate_horizontal(-ROTATE_STEP)

func try_move_direction(local_direction: Vector3) -> void:
	var direction_vector: Vector3 = global_transform.basis * local_direction
	direction_vector.y = 0
	direction_vector = direction_vector.normalized()
	
	var target_global_pos: Vector3 = global_position + (direction_vector * MOVE_STEP)
	var map_coord: Vector3i = grid_map.local_to_map(grid_map.to_local(target_global_pos))
	var cell_item_id: int = grid_map.get_cell_item(map_coord)
	
	if cell_item_id == WALL_ITEM_ID: return
	
	var is_door_opened: bool = false
	if opened_doors.has(current_floor):
		for door_data in opened_doors[current_floor]:
			if door_data["coord"] == map_coord:
				is_door_opened = true
				break
	
	if cell_item_id == DOOR_ITEM_ID and not is_door_opened:
		interact_with_door(map_coord)
	
	var old_map_coord: Vector3i = grid_map.local_to_map(grid_map.to_local(global_position))
	global_position = target_global_pos
	update_player_status()
	
	var current_map_coord: Vector3i = grid_map.local_to_map(grid_map.to_local(global_position))
	var current_cell_id: int = grid_map.get_cell_item(current_map_coord)
	
	if current_map_coord != old_map_coord:
		if current_cell_id == STAIRS_DOWN_ITEM_ID or current_cell_id == STAIRS_UP_ITEM_ID:
			teleport_by_stairs(current_map_coord)
			return

	is_door_opened = false
	if opened_doors.has(current_floor):
		for door_data in opened_doors[current_floor]:
			if door_data["coord"] == current_map_coord:
				is_door_opened = true
				break
				
	if current_cell_id == DOOR_ITEM_ID and not is_door_opened:
		return

	var event_triggered: bool = check_coordinate_event()
	if not event_triggered:
		check_random_encounter()

func interact_with_door(door_coord: Vector3i) -> void:
	if not opened_doors.has(current_floor):
		opened_doors[current_floor] = []
		
	var current_orientation: int = grid_map.get_cell_item_orientation(door_coord)
	var door_data: Dictionary = {
		"coord": door_coord,
		"orientation": current_orientation
	}
	opened_doors[current_floor].append(door_data)
		
	grid_map.set_cell_item(door_coord, GridMap.INVALID_CELL_ITEM)
	
	get_node("/root/Main/AudioManager").play_sfx("door_open")

func _respawn_doors_on_floor(floor_num: int) -> void:
	if not opened_doors.has(floor_num): return
	var closed_count: int = 0
	for door_data in opened_doors[floor_num]:
		var coord: Vector3i = door_data["coord"]
		var orientation: int = door_data["orientation"]
		grid_map.set_cell_item(coord, DOOR_ITEM_ID, orientation)
		closed_count += 1
		
	opened_doors[floor_num].clear()
	print("【System】階層 B%d の扉を正しい向きでリセットしました。復活数: %d" % [floor_num, closed_count])

func _get_adjacent_walkable_pos(stair_map_pos: Vector3i) -> Vector3:
	var directions = [Vector3i(0, 0, -1), Vector3i(1, 0, 0), Vector3i(0, 0, 1), Vector3i(-1, 0, 0)]
	for offset in directions:
		var check_pos = stair_map_pos + offset
		var cell_id = grid_map.get_cell_item(check_pos)
		if cell_id != WALL_ITEM_ID and cell_id != STAIRS_DOWN_ITEM_ID and cell_id != STAIRS_UP_ITEM_ID:
			return grid_map.to_global(grid_map.map_to_local(check_pos))
	return grid_map.to_global(grid_map.map_to_local(stair_map_pos))

func rotate_horizontal(deg: float) -> void:
	rotate_y(deg_to_rad(deg))
	update_player_status()

func update_player_status() -> void:
	if not is_inside_tree() or not is_explore_active: return
	var map_coord: Vector3i = grid_map.local_to_map(grid_map.to_local(global_position))
	current_grid_pos = Vector2i(map_coord.x, map_coord.z)
	var forward_vector: Vector3 = -global_transform.basis.z
	if abs(forward_vector.z) > abs(forward_vector.x):
		current_dir = Direction.NORTH if forward_vector.z < -0.5 else Direction.SOUTH
	else:
		current_dir = Direction.EAST if forward_vector.x > 0.5 else Direction.WEST
	update_ui_display()

func update_ui_display() -> void:
	if status_label == null: return
	
	# 方位の翻訳キーを取得
	var dir_key = ""
	match current_dir:
		Direction.NORTH: dir_key = "UI_DIRECTION_NORTH"
		Direction.EAST:  dir_key = "UI_DIRECTION_EAST"
		Direction.SOUTH: dir_key = "UI_DIRECTION_SOUTH"
		Direction.WEST:  dir_key = "UI_DIRECTION_WEST"
	
	var dir_string = get_localized_text(dir_key)
	var format_string = get_localized_text("UI_STATUS_FORMAT")
	status_label.text = format_string % [current_floor, current_grid_pos.x, current_grid_pos.y, dir_string]

func check_coordinate_event() -> bool:
	var events: Dictionary = {
		Vector2i(0, -2): { "key": "MSG_EVENT_0_-2", "is_repeatable": false },
		Vector2i(2, -4): { "key": "MSG_EVENT_2_-4", "is_repeatable": false },
		Vector2i(1, -1): { "key": "MSG_EVENT_1_-1", "is_repeatable": true }
	}
	if not events.has(current_grid_pos): return false
	
	var event_data = events[current_grid_pos]
	if not event_data["is_repeatable"] and triggered_events.get(current_grid_pos) == true: return false

	# 翻訳テキストを取得して表示
	show_explore_message(get_localized_text(event_data["key"]))
	if not event_data["is_repeatable"]: triggered_events[current_grid_pos] = true
	return true

func check_random_encounter() -> void:
	steps_since_last_fight += 1
	if steps_since_last_fight <= MIN_SAFE_STEPS: return
	var current_chance: float = BASE_ENCOUNTER_CHANCE * (steps_since_last_fight - MIN_SAFE_STEPS)
	var roll: float = randf()
	if roll < current_chance:
		steps_since_last_fight = 0
		battle_triggered.emit()

func show_explore_message(text: String) -> void:
	if attack_button: attack_button.hide()
	if auto_button: auto_button.hide()
	if flee_button: flee_button.hide()
	battle_text.text = text
	message_window.show()
	is_showing_message = true

func close_message_window() -> void:
	message_window.hide()
	is_showing_message = false

ダンジョン処理用のコード (dungeon.gd) を上記のように修正します。これは、今までこのスクリプト内でメッセージをハンドリングしていたのを、JSONファイルのデータを読み込んで表示するようにしたものです。これで、ダンジョン内メッセージの英語/日本語の切り替えが可能になりました。

この段階でダンジョン内のメッセージボックスを「決定ボタンで閉じる」ようにしました。
(if event.is_action_pressed(“ui_accept“): close_message_window())

ダンジョンのオートマッピング実装

現状、ダンジョン内では画面左上に座標だけが表示されています。この画面左上の2次元マップを表示させるようにしていきましょう。

オートマッピングUIの構成

  • Main
    • CanvasLayer
      • MapWindow (PanelContainer)
        • VBoxContainer
          • MapDisplay (Control)
          • StatusLabel (Label)

上記のように構成を変えます。Main と CanvasLayer は既存のものです。StatusLabel は旧Label になります。そのほかは新規に子ノードを作成してください。

MapWIndow画面左上に配置
MapDisplayマップ描画
StatusLabel座標情報を表示

今までは CanvasLayer の下に Label という子ノードがありました。これが座標情報を表示していましたが、これを StatusLabel と名前変更して VBoxContainer の下に移動させています。

オートマッピング処理用スクリプト新規作成

上記、オートマッピングUIの構成にしたあと、MapDisplay の Inspector から Script を作成してください。ここでは map_display.gd とします。これがオートマッピング処理用スクリプトになります。

extends Control

@onready var dungeon = get_node("/root/Main/Camera3D")

func _draw():
	if not dungeon or not dungeon.mapped_cells:
		return
	
	# 40x40マスを収めるため、セルサイズを調整
	var cell_size = 12
	var floor_prefix = str(dungeon.current_floor) + "_"
	
	# 1. 現在の階層の探索済み座標を抽出
	var current_floor_cells = []
	for key in dungeon.mapped_cells.keys():
		if key.begins_with(floor_prefix):
			var parts = key.split("_")
			current_floor_cells.append(Vector2i(int(parts[1]), int(parts[2])))
	
	if current_floor_cells.is_empty():
		return
	
	# 2. 階層ごとの最小座標(左上端)を特定し、そこを基準(0,0)に変換
	var min_x = current_floor_cells[0].x
	var min_y = current_floor_cells[0].y
	for cell in current_floor_cells:
		if cell.x < min_x: min_x = cell.x
		if cell.y < min_y: min_y = cell.y

	# 3. 描画実行
	for cell in current_floor_cells:
		# 全階層共通の最小座標を引くことで、階層を移動しても配置がズレなくなります
		var rel_x = cell.x - min_x
		var rel_y = cell.y - min_y
		
		# 調整用のマージン変数 (画面左上端からの距離)
		var margin = 40 

		# 描画位置の計算に margin を加える
		var pos = Vector2(rel_x * cell_size, rel_y * cell_size) + Vector2(margin, margin)
		
		# 現在地判定
		var is_current = (dungeon.current_grid_pos.x == cell.x and dungeon.current_grid_pos.y == cell.y)
		var color = Color.RED if is_current else Color.WHITE
		
		draw_rect(Rect2(pos, Vector2(cell_size - 1, cell_size - 1)), color)

オートマッピング処理用コード (map_display.gd) を上記のようにしてください。

この記事では、オートマッピングは画面左上に表示されます。マージンやセルのサイズは各々調整してください。

ダンジョン処理用コード修正

# ==============================================================================
# dungeon.gd(戦闘終了・メッセージ終了時のオートマッピング対応版)
# ==============================================================================
class_name DungeonExplore
extends Camera3D

signal battle_triggered
signal town_entered

enum Direction { NORTH, EAST, SOUTH, WEST }

const MOVE_STEP: float = 2.0
const ROTATE_STEP: float = 90.0

const WALL_ITEM_ID: int = 1
const DOOR_ITEM_ID: int = 2
const STAIRS_DOWN_ITEM_ID: int = 3
const STAIRS_UP_ITEM_ID: int = 4

const MIN_SAFE_STEPS: int = 4
const BASE_ENCOUNTER_CHANCE: float = 0.1

var dungeon_map_data: Dictionary = {}
var dungeon_text_data: Dictionary = {}

var current_grid_pos: Vector2i = Vector2i.ZERO
var current_dir: Direction = Direction.NORTH
var is_showing_message: bool = false
var is_explore_active: bool = false
var triggered_events: Dictionary = {}
var steps_since_last_fight: int = 0
var opened_doors: Dictionary = {}
var current_floor: int = 1

var mapped_cells: Dictionary = {}

var dungeon_start_position: Vector3
var dungeon_start_rotation: Vector3

@onready var grid_map: GridMap = $"../GridMap"
@onready var status_label: Label = $"../CanvasLayer/MapWindow/VBoxContainer/StatusLabel"
@onready var message_window: PanelContainer = $"../CanvasLayer/PanelContainer"
@onready var battle_text: Label = $"../CanvasLayer/PanelContainer/BattleMenu/BattleText"

@onready var attack_button: Button = $"../CanvasLayer/PanelContainer/BattleMenu/AttackButton"
@onready var auto_button: Button = $"../CanvasLayer/PanelContainer/BattleMenu/AutoButton"
@onready var flee_button: Button = $"../CanvasLayer/PanelContainer/BattleMenu/FleeButton"

@onready var map_window: PanelContainer = $"../CanvasLayer/MapWindow"

func _ready() -> void:
	dungeon_start_position = global_position
	dungeon_start_rotation = global_rotation
	steps_since_last_fight = 0
	
	_load_map_data_from_json()
	_load_dungeon_text_from_json()
	update_player_status() # 通常マッピング

func _load_map_data_from_json() -> void:
	var file_path: String = "res://data/map_data.json"
	if not FileAccess.file_exists(file_path): return
	var file = FileAccess.open(file_path, FileAccess.READ)
	var json_string = file.get_as_text()
	file.close()
	var json = JSON.new()
	var error = json.parse(json_string)
	if error == OK and typeof(json.data) == TYPE_DICTIONARY:
		dungeon_map_data = json.data

func _load_dungeon_text_from_json() -> void:
	var file_path = "res://data/dungeon_text.json"
	if FileAccess.file_exists(file_path):
		var file = FileAccess.open(file_path, FileAccess.READ)
		var json = JSON.new()
		json.parse(file.get_as_text())
		dungeon_text_data = json.data
		file.close()

func get_localized_text(key: String, params: Array = []) -> String:
	if not dungeon_text_data.has(key): return "Missing key: " + key
	var locale = TranslationServer.get_locale().left(2)
	var dict = dungeon_text_data[key]
	var text = dict.get(locale, dict.get("en", "Missing text"))
	if params.size() > 0: return text % params
	return text

func get_floor_enemy_limits(floor_num: int) -> Dictionary:
	var floor_key: String = str(floor_num)
	if dungeon_map_data.has(floor_key):
		var floor_data: Dictionary = dungeon_map_data[floor_key]
		if floor_data.has("settings") and typeof(floor_data["settings"]) == TYPE_DICTIONARY:
			var settings: Dictionary = floor_data["settings"]
			return {"min": int(settings.get("min_enemies", 1)), "max": int(settings.get("max_enemies", 4))}
	return {"min": 1, "max": 4}

func teleport_by_stairs(current_stair_coord: Vector3i) -> void:
	var floor_key: String = str(current_floor)
	if not dungeon_map_data.has(floor_key): return
	var floor_data: Dictionary = dungeon_map_data[floor_key]
	var coord_key: String = "%d,%d,%d" % [current_stair_coord.x, current_stair_coord.y, current_stair_coord.z]
	if not floor_data.has(coord_key): return
	var warp_info: Dictionary = floor_data[coord_key]
	var next_floor: int = int(warp_info["target_floor"])
	var target_pos_array: Array = warp_info["target_pos"]
	var next_pos: Vector3i = Vector3i(target_pos_array[0], target_pos_array[1], target_pos_array[2])

	steps_since_last_fight = 0
	get_node("/root/Main/AudioManager").play_sfx("stairs")

	if next_floor == 0:
		_respawn_doors_on_floor(current_floor)
		town_entered.emit()
	else:
		_respawn_doors_on_floor(current_floor)
		current_floor = next_floor
		var keep_eye_height: float = global_position.y
		var safe_global_pos: Vector3 = _get_adjacent_walkable_pos(next_pos)
		global_position = safe_global_pos
		global_position.y = keep_eye_height
		
		force_update_transform()
		
		var final_map_coord = grid_map.local_to_map(grid_map.to_local(global_position))
		current_grid_pos = Vector2i(final_map_coord.x, final_map_coord.z)
		
		var map_key = "%d_%d_%d" % [current_floor, current_grid_pos.x, current_grid_pos.y]
		mapped_cells[map_key] = true
		
		var map_display = get_node_or_null("../CanvasLayer/MapWindow/VBoxContainer/MapDisplay")
		if map_display:
			if "current_floor" in map_display:
				map_display.current_floor = current_floor
			map_display.queue_redraw()
		
		update_ui_display()
		
		if map_window:
			map_window.hide()
		
		(func(): map_window.show()).call_deferred()

func set_explore_active(active: bool) -> void:
	is_explore_active = active
	if is_explore_active:
		if map_window:
			map_window.show()
		# 戦闘復帰時:引数なし。通常通りマッピング(記録・描画)を行います
		update_player_status()
	else:
		if status_label:
			status_label.text = ""

func reset_to_dungeon_start() -> void:
	current_floor = 1
	steps_since_last_fight = 0
	var town_stair_map_coord: Vector3i = Vector3i.ZERO
	var found: bool = false
	if dungeon_map_data.has("1"):
		var b1_data: Dictionary = dungeon_map_data["1"]
		for coord_str in b1_data.keys():
			if coord_str == "settings": continue
			var warp_info: Dictionary = b1_data[coord_str]
			if int(warp_info["target_floor"]) == 0:
				var parts = coord_str.split(",")
				if parts.size() == 3:
					town_stair_map_coord = Vector3i(int(parts[0]), int(parts[1]), int(parts[2]))
					found = true
					break
	if not found:
		var clean_start_pos: Vector3 = dungeon_start_position
		clean_start_pos.y = 0.0
		town_stair_map_coord = grid_map.local_to_map(grid_map.to_local(clean_start_pos))
	var keep_eye_height: float = dungeon_start_position.y
	var safe_start_pos: Vector3 = _get_adjacent_walkable_pos(town_stair_map_coord)
	global_position = safe_start_pos
	global_position.y = keep_eye_height
	global_rotation = dungeon_start_rotation
	update_player_status() # 通常マッピング

func _unhandled_input(event: InputEvent) -> void:
	if not event.is_pressed() or event.is_echo(): return
	if not is_explore_active: return
	if is_showing_message:
		if event.is_action_pressed("ui_accept"): close_message_window()
		return
	
	# 上下LRキー(前進・後退・平行移動):移動処理(中で通常マッピングが走る)
	if event.is_action_pressed("move_forward"):
		try_move_direction(Vector3(0, 0, -1))
	elif event.is_action_pressed("move_backward"):
		try_move_direction(Vector3(0, 0, 1))
	elif event.is_action_pressed("move_left"):
		try_move_direction(Vector3(-1, 0, 0))
	elif event.is_action_pressed("move_right"):
		try_move_direction(Vector3(1, 0, 0))
	
	# 左右キー(旋回):マッピングを明示的にスキップする関数を呼び出す
	elif event.is_action_pressed("turn_left"):
		rotate_horizontal_skip_map(ROTATE_STEP)
	elif event.is_action_pressed("turn_right"):
		rotate_horizontal_skip_map(-ROTATE_STEP)

func try_move_direction(local_direction: Vector3) -> void:
	var direction_vector: Vector3 = global_transform.basis * local_direction
	direction_vector.y = 0
	direction_vector = direction_vector.normalized()
	var target_global_pos: Vector3 = global_position + (direction_vector * MOVE_STEP)
	var map_coord: Vector3i = grid_map.local_to_map(grid_map.to_local(target_global_pos))
	var cell_item_id: int = grid_map.get_cell_item(map_coord)
	if cell_item_id == WALL_ITEM_ID: return
	var is_door_opened: bool = false
	if opened_doors.has(current_floor):
		for door_data in opened_doors[current_floor]:
			if door_data["coord"] == map_coord:
				is_door_opened = true
				break
	if cell_item_id == DOOR_ITEM_ID and not is_door_opened:
		interact_with_door(map_coord)
	var old_map_coord: Vector3i = grid_map.local_to_map(grid_map.to_local(global_position))
	global_position = target_global_pos
	
	# 移動時は通常マッピング(記録・描画)を行います
	update_player_status()
	
	var current_map_coord: Vector3i = grid_map.local_to_map(grid_map.to_local(global_position))
	var current_cell_id: int = grid_map.get_cell_item(current_map_coord)
	if current_map_coord != old_map_coord:
		if current_cell_id == STAIRS_DOWN_ITEM_ID or current_cell_id == STAIRS_UP_ITEM_ID:
			teleport_by_stairs(current_map_coord)
			return
	is_door_opened = false
	if opened_doors.has(current_floor):
		for door_data in opened_doors[current_floor]:
			if door_data["coord"] == current_map_coord:
				is_door_opened = true
				break
	if current_cell_id == DOOR_ITEM_ID and not is_door_opened: return
	var event_triggered: bool = check_coordinate_event()
	if not event_triggered: check_random_encounter()

func interact_with_door(door_coord: Vector3i) -> void:
	if not opened_doors.has(current_floor): opened_doors[current_floor] = []
	var current_orientation: int = grid_map.get_cell_item_orientation(door_coord)
	var door_data: Dictionary = {"coord": door_coord, "orientation": current_orientation}
	opened_doors[current_floor].append(door_data)
	grid_map.set_cell_item(door_coord, GridMap.INVALID_CELL_ITEM)
	get_node("/root/Main/AudioManager").play_sfx("door_open")

func _respawn_doors_on_floor(floor_num: int) -> void:
	if not opened_doors.has(floor_num): return
	for door_data in opened_doors[floor_num]:
		var coord: Vector3i = door_data["coord"]
		var orientation: int = door_data["orientation"]
		grid_map.set_cell_item(coord, DOOR_ITEM_ID, orientation)
	opened_doors[floor_num].clear()

func _get_adjacent_walkable_pos(stair_map_pos: Vector3i) -> Vector3:
	var directions = [Vector3i(0, 0, -1), Vector3i(1, 0, 0), Vector3i(0, 0, 1), Vector3i(-1, 0, 0)]
	for offset in directions:
		var check_pos = stair_map_pos + offset
		var cell_id = grid_map.get_cell_item(check_pos)
		if cell_id != WALL_ITEM_ID and cell_id != STAIRS_DOWN_ITEM_ID and cell_id != STAIRS_UP_ITEM_ID:
			return grid_map.to_global(grid_map.map_to_local(check_pos))
	return grid_map.to_global(grid_map.map_to_local(stair_map_pos))

# 移動用とは別に、左右キー旋回時のみ通る関数を用意し、マッピングスキップを明示します
func rotate_horizontal_skip_map(deg: float) -> void:
	rotate_y(deg_to_rad(deg))
	update_player_status(true) # マッピングをスキップ

# デフォルトでは常にオートマッピング(記録+描画)を行います。
# skip_mapping が true(左右キーでの旋回時)のとき「だけ」、記録処理をスキップします。
func update_player_status(skip_mapping: bool = false) -> void:
	if not is_inside_tree() or not is_explore_active: return

	var map_coord: Vector3i = grid_map.local_to_map(grid_map.to_local(global_position))
	current_grid_pos = Vector2i(map_coord.x, map_coord.z)
	var forward_vector: Vector3 = -global_transform.basis.z
	if abs(forward_vector.z) > abs(forward_vector.x):
		current_dir = Direction.NORTH if forward_vector.z < -0.5 else Direction.SOUTH
	else:
		current_dir = Direction.EAST if forward_vector.x > 0.5 else Direction.WEST
	
	update_ui_display()

	# 左右キー(旋回)以外から呼ばれた場合は、確実にオートマッピング(マップ記録)を行う
	if not skip_mapping:
		var map_key = "%d_%d_%d" % [current_floor, current_grid_pos.x, current_grid_pos.y]
		mapped_cells[map_key] = true
	
	# マップの描画更新は毎回必ず実行する
	var map_display = get_node_or_null("../CanvasLayer/MapWindow/VBoxContainer/MapDisplay")
	if map_display:
		if "current_floor" in map_display:
			map_display.current_floor = current_floor
		map_display.queue_redraw()

func update_ui_display() -> void:
	if status_label == null: return
	
	var dir_key = ""
	match current_dir:
		Direction.NORTH: dir_key = "UI_DIRECTION_NORTH"
		Direction.EAST:  dir_key = "UI_DIRECTION_EAST"
		Direction.SOUTH: dir_key = "UI_DIRECTION_SOUTH"
		Direction.WEST:  dir_key = "UI_DIRECTION_WEST"
	
	var dir_string = get_localized_text(dir_key)
	status_label.text = "B%d  %s%s  %s" % [current_floor, "", "", dir_string]

func check_coordinate_event() -> bool:
	var events: Dictionary = {
		Vector2i(0, -2): { "key": "MSG_EVENT_0_-2", "is_repeatable": false },
		Vector2i(2, -4): { "key": "MSG_EVENT_2_-4", "is_repeatable": false },
		Vector2i(1, -1): { "key": "MSG_EVENT_1_-1", "is_repeatable": true }
	}
	if not events.has(current_grid_pos): return false
	var event_data = events[current_grid_pos]
	if not event_data["is_repeatable"] and triggered_events.get(current_grid_pos) == true: return false
	show_explore_message(get_localized_text(event_data["key"]))
	if not event_data["is_repeatable"]: triggered_events[current_grid_pos] = true
	return true

func check_random_encounter() -> void:
	steps_since_last_fight += 1
	if steps_since_last_fight <= MIN_SAFE_STEPS: return
	var current_chance: float = BASE_ENCOUNTER_CHANCE * (steps_since_last_fight - MIN_SAFE_STEPS)
	var roll: float = randf()
	if roll < current_chance:
		steps_since_last_fight = 0
		
		if map_window:
			map_window.hide()
			
		battle_triggered.emit()

func show_explore_message(text: String) -> void:
	if attack_button: attack_button.hide()
	if auto_button: auto_button.hide()
	if flee_button: flee_button.hide()
	battle_text.text = text
	message_window.show()
	is_showing_message = true

func close_message_window() -> void:
	message_window.hide()
	is_showing_message = false
	update_player_status()

ダンジョン処理用のコード (dungeon.gd) を上記のように修正します。これは、ダンジョン内で移動したときにオートマッピングするようにしたものです。

オートマッピング (配色)

上記のコードでは、オートマッピング上のプレイヤーの位置は赤・踏破した箇所は白になっていました。これでは地図を見ても、階段や扉の位置がわかりません。これを修正していきます。

通常の床
プレイヤーの位置
ドア
下り階段
上り階段

オートマッピング処理用コード修正

extends Control

@onready var dungeon = get_node("/root/Main/Camera3D")

func _draw():
	if not dungeon or not dungeon.mapped_cells or not dungeon.grid_map:
		return
	
	# 40x40マスを収めるため、セルサイズを調整
	var cell_size = 12
	var floor_prefix = str(dungeon.current_floor) + "_"
	
	# 1. 現在の階層の探索済み座標を抽出
	var current_floor_cells = []
	for key in dungeon.mapped_cells.keys():
		if key.begins_with(floor_prefix):
			var parts = key.split("_")
			current_floor_cells.append(Vector2i(int(parts[1]), int(parts[2])))
	
	if current_floor_cells.is_empty():
		return
	
	# 2. 階層ごとの最小座標(左上端)を特定し、そこを基準(0,0)に変換
	var min_x = current_floor_cells[0].x
	var min_y = current_floor_cells[0].y
	for cell in current_floor_cells:
		if cell.x < min_x: min_x = cell.x
		if cell.y < min_y: min_y = cell.y

	# 3. 描画実行
	for cell in current_floor_cells:
		var rel_x = cell.x - min_x
		var rel_y = cell.y - min_y
		
		# 調整用のマージン変数 (画面左上端からの距離)
		var margin = 40 

		# 描画位置の計算に margin を加える
		var pos = Vector2(rel_x * cell_size, rel_y * cell_size) + Vector2(margin, margin)
		
		# --- 色の決定ロジック ---
		var color = Color.WHITE # デフォルトは通常の床(白)
		
		# 1. GridMap上のアイテムIDから基本色を判定(y座標は通常0として計算)
		var map_coord_3d = Vector3i(cell.x, 0, cell.y)
		var cell_item_id = dungeon.grid_map.get_cell_item(map_coord_3d)
		
		if cell_item_id == dungeon.DOOR_ITEM_ID:
			color = Color.html("#8B4513") # ドア (茶色)
		elif cell_item_id == dungeon.STAIRS_DOWN_ITEM_ID:
			color = Color.GREEN # 下り階段(緑)
		elif cell_item_id == dungeon.STAIRS_UP_ITEM_ID:
			color = Color.BLUE # 上り階段(青)
		
		# 2. 開いた後のドアの判定 (GridMap上は消えているが、開通済みリストにある場合)
		if dungeon.opened_doors.has(dungeon.current_floor):
			for door_data in dungeon.opened_doors[dungeon.current_floor]:
				if door_data["coord"].x == cell.x and door_data["coord"].z == cell.y:
					color = Color.html("#8B4513") # 茶色(開いた後も維持)
					break
		
		# 3. ワナ(イベント座標)の判定
		# ※現状 dungeon.gd 内の check_coordinate_event にあるイベント座標をチェックします
		var trap_coordinates = [Vector2i(0, -2), Vector2i(2, -4), Vector2i(1, -1)]
		if cell in trap_coordinates:
			color = Color.YELLOW # ワナ(黄)
			
		# 4. 最優先:プレイヤーの現在地判定
		var is_current = (dungeon.current_grid_pos.x == cell.x and dungeon.current_grid_pos.y == cell.y)
		if is_current:
			color = Color.RED # プレイヤー位置(赤)
		
		# セルの描画
		draw_rect(Rect2(pos, Vector2(cell_size - 1, cell_size - 1)), color)

ダンジョン処理用コードを上記のように修正します。これでオートマッピングでドアと階段にも色が付きました。

これを応用すれば、ワナやイベントなどでも配色できます。

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

この記事を書いた人

コメント

コメントする

CAPTCHA


目次