
今回はダンジョン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ファイルのデータを読み込んで表示するようにしたものです。これで、ダンジョン内メッセージの英語/日本語の切り替えが可能になりました。
ダンジョンのオートマッピング実装
現状、ダンジョン内では画面左上に座標だけが表示されています。この画面左上の2次元マップを表示させるようにしていきましょう。
オートマッピングUIの構成
- Main
- CanvasLayer
- MapWindow (PanelContainer)
- VBoxContainer
- MapDisplay (Control)
- StatusLabel (Label)
- VBoxContainer
- MapWindow (PanelContainer)
- CanvasLayer
上記のように構成を変えます。Main と CanvasLayer は既存のものです。StatusLabel は旧Label になります。そのほかは新規に子ノードを作成してください。
| MapWIndow | 画面左上に配置 |
| MapDisplay | マップ描画 |
| StatusLabel | 座標情報を表示 |
オートマッピング処理用スクリプト新規作成

上記、オートマッピング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)
ダンジョン処理用コードを上記のように修正します。これでオートマッピングでドアと階段にも色が付きました。
コメント