MENU

Godot 4 でWiz系ダンジョンRPG開発 (ランダムエンカウントシステムの実装) [004]

今回はダンジョン内でのランダムエンカウントシステムの実装についてお伝えします。この記事までを完成させることで、上記画像のように敵との遭遇が実装可能です。(上記画像をクリックすると動きがわかります)

ここまでのダンジョンRPG制作の流れは以下の別記事をご参照ください。

目次

ランダムエンカウントシステムの実装

ランダムエンカウントシステムを構築するために、以下の順序で作業を行います。

  1. ランダムエンカウント用の定数・変数処理
  2. 戦闘コマンドの表示
  3. 敵の表示

ランダムエンカウント用の定数・変数処理

今回は、「一度戦闘が終わったら、最低でも3歩は絶対に敵が出ない安全歩数を保障し、その後徐々にエンカウント確率が上がっていく」という、実用的なエンカウント制御ロジックを組み込みます。ここでは、敵との遭遇をメッセージボックスで表示という形で表現しています。

コードの修正

extends Camera3D

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 MIN_SAFE_STEPS: int = 3   # 戦闘後、絶対に敵が出ない最小安全歩数
const BASE_ENCOUNTER_CHANCE: float = 0.08 # 安全歩数を超えた後の、1歩あたりの基礎エンカウント確率(8%)

var steps_since_last_fight: int = 0 # 最後の戦闘(または開始)からの歩数カウント

var current_grid_pos: Vector2i = Vector2i.ZERO
var current_dir: Direction = Direction.NORTH
var is_showing_message: bool = false
var triggered_events: Dictionary = {}

@onready var grid_map: GridMap = $"../GridMap"
@onready var status_label: Label = $"../CanvasLayer/Label"
@onready var message_window: PanelContainer = $"../CanvasLayer/PanelContainer"
@onready var message_label: Label = $"../CanvasLayer/PanelContainer/MessageLabel"

func _ready() -> void:
	update_player_status()
	if message_window:
		message_window.hide()
	# ゲーム開始時は歩数カウントをリセットしておく
	steps_since_last_fight = 0

func _unhandled_input(event: InputEvent) -> void:
	if not event.is_pressed() or event.is_echo():
		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
		
	global_position = target_global_pos
	update_player_status()
	
	# 💡 移動成功時、まずは「固定の座標イベント」をチェック
	var event_triggered: bool = check_coordinate_event()
	
	# 💡 固定イベントが「発生しなかった」場合のみ、ランダムエンカウントの判定を行う
	# (メッセージを読んだ瞬間に敵が奇襲してくる多重割り込みバグを防ぐ排他制御)
	if not event_triggered:
		check_random_encounter()

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():
		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):
		if forward_vector.z < -0.5:
			current_dir = Direction.NORTH
		else:
			current_dir = Direction.SOUTH
	else:
		if forward_vector.x > 0.5:
			current_dir = Direction.EAST
		else:
			current_dir = 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 = "現在地: (X: %d, Y: %d) | 向き: %s" % [current_grid_pos.x, current_grid_pos.y, dir_string]

# 💡 戻り値(bool)を返すように変更。イベントが発生したら true を返す
func check_coordinate_event() -> bool:
	var events: Dictionary = {
		Vector2i(0, -2): {
			"message": "「前方に不気味な気配を感じる…」",
			"is_repeatable": false
		},
		Vector2i(2, -4): {
			"message": "壁に血文字が刻まれている。\n『引き返せ、この先は深淵なり』",
			"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"]:
		if triggered_events.has(current_grid_pos) and triggered_events[current_grid_pos] == true:
			return false
	
	show_message_window(event_data["message"])
	
	if not event_data["is_repeatable"]:
		triggered_events[current_grid_pos] = true
		
	return true # イベントが発生したことを通知

# 💡 【新規追加】ランダムエンカウントの判定関数
func check_random_encounter() -> void:
	# 1. 歩数カウントを進める
	steps_since_last_fight += 1
	
	# 2. 最低安全歩数に達していないなら、絶対にエンカウントしない
	if steps_since_last_fight <= MIN_SAFE_STEPS:
		return
		
	# 3. 確率計算(安全歩数を超えたら、歩くほど徐々に確率が増していく設計)
	# 例: 4歩目は 8%, 5歩目は 16%, 6歩目は 24% ... と確率が累積する
	var current_chance: float = BASE_ENCOUNTER_CHANCE * (steps_since_last_fight - MIN_SAFE_STEPS)
	
	# 0.0 ~ 1.0 までの乱数を取得
	var roll: float = randf()
	
	# デバッグ用に出力(現在の確率とサイコロの目をコンソールに流す)
	print("歩数: %d | 遭遇確率: %d%% | 出目: %d%%" % [steps_since_last_fight, int(current_chance * 100), int(roll * 100)])
	
	# 4. 乱数が計算確率を下回ったらエンカウント成立!
	if roll < current_chance:
		trigger_battle_encounter()

# 💡 【新規追加】戦闘突入時の処理
func trigger_battle_encounter() -> void:
	# 歩数カウントをリセット(次の戦闘に備える)
	steps_since_last_fight = 0
	
	# メッセージウィンドウを使い、敵が出現したメッセージを表示
	# (移動がロックされます)
	show_message_window("モンスターが現れた!\n\n(Wボタンで戦闘画面(仮)を閉じる)")
	
# スクリプトの最末尾に追記してください

# メッセージウィンドウを開く関数
func show_message_window(text: String) -> void:
	message_label.text = text
	message_window.show()     # 画面中央の PanelContainer を表示
	is_showing_message = true # 移動入力をブロックするフラグをON

# メッセージウィンドウを閉じる関数
func close_message_window() -> void:
	message_window.hide()      # 画面中央の PanelContainer を非表示
	is_showing_message = false # 移動入力をブロックするフラグをOFF	

現在のスクリプト (camera_3d.gd) に、エンカウント用の変数と確率計算ロジックを追加します。

二重発生の防止
(優先順位の確率)
try_move_direction 内で、先に座標イベントを評価しています。 check_coordinate_event() が true を返した場合は、その直後の check_random_encounter() がスキップされます。これにより、「固定イベントのメッセージウィンドウ」と「ランダム戦闘のメッセージウィンドウ」が同じマスで同時に開いてフリーズするような競合バグを回避しています。
エンカウント率の線形増加ロジック BASE_ENCOUNTER_CHANCE * (歩数 – 安全歩数) この数式により、戦闘直後の3歩はエンカウント率 0% ですが、4歩目からは 8% ➔ 16% ➔ 24% ➔ 32% …と、歩けば歩くほど確実に敵に出会いやすくなります。「どれだけ歩いても全く敵が出ない」あるいは「1歩目で即出る」というゲームテンポの悪さを排除した確率制御です。

上記コードはあくまで一例です。該当箇所をご自身のお好みの値に変えてください。

戦闘コマンドの表示

上記のコードだとテキストメッセージで「モンスターが現れた!」の表記が出るだけです。まずここから戦闘コマンドを表示させるようにしていきます。

  1. UIノードの構築
  2. インプットマップに「UI決定」を登録
  3. コードの修正

UIノードの構築

モンスターが現れたとき「たたかう・オート・にげる」の選択肢を画面中央にポップアップさせるようにします。すでに中央に配置してある PanelContainer (メッセージウィンドウ) の中に、選択肢用のボタンを組み込みます。

今は「たたかう」「オート」は未実装「にげる」が100%成功します。これらは後の工程で修正します。

Main シーンの PanelContainer の中にある MessageLabel を右クリックし Change Type を選択します。

VBoxContainer にノードタイプを変更します。名前を MessageLabel から BattleMenu に変えてください。

変更した BattleMenu (VBoxContainer) の下に、子ノードとして以下の4つのノードを上から順に追加します。

Label 名前を BattleText に変更。ここに「モンスターが現れた!」などの文字を表示します
Button 名前を AttackButton に変更。インスペクターの Text に「たたかう」と入力
Button 名前を AutoButton に変更。インスペクターの Text に「オート」と入力
Button 名前を FleeButton に変更。インスペクターの Text に「にげる」と入力

インプットマップに「UI決定」を登録

ゲームパッドの決定ボタンを追加バインドして、「たたかう」「オート」「にげる」を選択したときに反応するようにします。

メニューの Project から Project Settings を選択してください。開いたウィンドウで Input Map タブをクリックします。検索欄に ui_accept を入力し、その ui_accept の右側の + ボタンを押してください。

ポップアップが出たら、ゲームパッドの 「決定として使いたいボタン (PlayStationなら〇や✕、XboxならAボタンなど) を実際にポチッと押します。(上記画像は×ボタン) 表示されたら、OKを押してウィンドウを閉じてください。

コードの修正

extends Camera3D

enum Direction { NORTH, EAST, SOUTH, WEST }
# 💡 【新規】ゲームの状態を管理するステート定義
enum GameState { EXPLORE, BATTLE }

const MOVE_STEP: float = 2.0
const ROTATE_STEP: float = 90.0
const WALL_ITEM_ID: int = 1
const MIN_SAFE_STEPS: int = 3
const BASE_ENCOUNTER_CHANCE: float = 0.08

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

# 💡 【新規】現在のゲーム状態(初期値はダンジョン探索中)
var current_state: GameState = GameState.EXPLORE

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

# 💡 【新規】戦闘UI内の各ノードへの参照
@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:
	update_player_status()
	if message_window:
		message_window.hide()
	steps_since_last_fight = 0
	
	# 💡 【新規】ボタンが押されたときのイベント(シグナル)をコードから接続する
	attack_button.pressed.connect(_on_attack_button_pressed)
	auto_button.pressed.connect(_on_auto_button_pressed)
	flee_button.pressed.connect(_on_flee_button_pressed)

func _unhandled_input(event: InputEvent) -> void:
	if not event.is_pressed() or event.is_echo():
		return

	# 💡 【排他制御の強化】戦闘状態(BATTLE)の時は、キーボードやパッドによる移動入力を一切遮断する
	if current_state == GameState.BATTLE:
		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
		
	global_position = target_global_pos
	update_player_status()
	
	var event_triggered: bool = check_coordinate_event()
	if not event_triggered:
		check_random_encounter()

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():
		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):
		if forward_vector.z < -0.5:
			current_dir = Direction.NORTH
		else:
			current_dir = Direction.SOUTH
	else:
		if forward_vector.x > 0.5:
			current_dir = Direction.EAST
		else:
			current_dir = 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 = "現在地: (X: %d, Y: %d) | 向き: %s" % [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"]:
		if triggered_events.has(current_grid_pos) and triggered_events[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()
	
	print("歩数: %d | 遭遇確率: %d%% | 出目: %d%%" % [steps_since_last_fight, int(current_chance * 100), int(roll * 100)])
	
	if roll < current_chance:
		trigger_battle_encounter()

# 💡 固定イベント用のメッセージ表示(ボタンを隠す)
func show_explore_message(text: String) -> void:
	battle_text.text = text
	attack_button.hide()
	auto_button.hide()
	flee_button.hide()
	message_window.show()
	is_showing_message = true

# 💡 【設計変更】戦闘突入時の処理(ステートをBATTLEにし、戦闘コマンドを表示)
func trigger_battle_encounter() -> void:
	current_state = GameState.BATTLE # 状態を戦闘中へ変更
	steps_since_last_fight = 0
	
	# 戦闘用の画面レイアウトを設定
	battle_text.text = "モンスターが現れた!\nコマンドを選択してください。"
	attack_button.show()
	auto_button.show()
	flee_button.show()
	
	message_window.show()
	
	# 💡 UIを開いた瞬間、ゲームパッドやキーボードでボタンを選択できるようにフォーカスを当てる
	attack_button.grab_focus()

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

# 💡 【新規】「たたかう」ボタンが押されたときの処理
func _on_attack_button_pressed() -> void:
	if current_state != GameState.BATTLE: return
	battle_text.text = "パーティメンバーのコマンドへ (未実装)"
	print("たたかうが選択されました")

# 💡 【新規】「オート」ボタンが押されたときの処理
func _on_auto_button_pressed() -> void:
	if current_state != GameState.BATTLE: return
	battle_text.text = "オートバトル (未実装)"
	print("オートが選択されました")

# 💡 【新規】「にげる」ボタンが押されたときの処理(100%成功してダンジョンに戻る)
func _on_flee_button_pressed() -> void:
	if current_state != GameState.BATTLE: return
	
	# 戦闘状態を解除し、通常の探索状態に戻す
	current_state = GameState.EXPLORE
	message_window.hide()
	print("無事に逃げ切った!探索に戻ります。")

上記の通りにコード (camera_3d.gd) を修正してください。これは、ゲームの状態 (探索中 / 戦闘中) を管理するフラグを追加し、「にげる」ボタンが押されたら戦闘を終了してダンジョンに戻るロジックです。

敵の表示

ここでは、ゴブリン1匹だけを表示させるようにします。最初にゴブリンの画像ファイルを用意してください。このゴブリンの画像ファイル名をここでは goblin.png とします。サイズは 512 x 512 です。

Godot Editor の左下にある Filesystem タブの中に、この goblin.png をドラッグ&ドロップして、プロジェクトのリソースとして登録します。

UIノードの構築

戦闘メニューの奥にゴブリンの画像を出すために、Godotの画像表示専用ノードである TextureRect を配置します。

CanvasLayer を右クリックし Add Child Node を選択します。TextureRect ノードを追加してください。

CanvasLayer の子ノードの順番を上から Label, EnemyGraphic, PanelContainer の順にします。

EnemyGraphic の Inspector でゴブリン画像の調整を行います。

Textureファイルシステムから goblin.png をここにドラッグ&ドロップして割り当てます
Control > Layout > Anchors PresetCenter
Control > Layout > Transform > Position敵の位置 (お好みの値にしてください)
Expand ModeIgnore Size
Size敵のサイズ (お好みの値にしてください)
CanvasItem > Visibility通常時は隠しておきたいため Visible のチェックを外して 非表示 にしておきます

また、コードを見てわかった人もいると思いますが、この camera_3d.gd のコードに「敵の処理」などカメラ以外の処理も含まれています。これはプログラミング的にはよくない構造です。そのため、後の記事でこれを修正します。

コードの修正

extends Camera3D

enum Direction { NORTH, EAST, SOUTH, WEST }
enum GameState { EXPLORE, BATTLE }

const MOVE_STEP: float = 2.0
const ROTATE_STEP: float = 90.0
const WALL_ITEM_ID: int = 1
const MIN_SAFE_STEPS: int = 3
const BASE_ENCOUNTER_CHANCE: float = 0.08

var current_grid_pos: Vector2i = Vector2i.ZERO
var current_dir: Direction = Direction.NORTH
var is_showing_message: bool = false
var triggered_events: Dictionary = {}
var steps_since_last_fight: int = 0
var current_state: GameState = GameState.EXPLORE

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

# 💡 【新規】敵グラフィックノードへの参照
@onready var enemy_graphic: TextureRect = $"../CanvasLayer/EnemyGraphic"

@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:
	update_player_status()
	if message_window:
		message_window.hide()
	# 💡 初期状態では敵グラフィックを確実に隠しておく
	if enemy_graphic:
		enemy_graphic.hide()
	steps_since_last_fight = 0
	
	attack_button.pressed.connect(_on_attack_button_pressed)
	auto_button.pressed.connect(_on_auto_button_pressed)
	flee_button.pressed.connect(_on_flee_button_pressed)

func _unhandled_input(event: InputEvent) -> void:
	if not event.is_pressed() or event.is_echo():
		return

	if current_state == GameState.BATTLE:
		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
		
	global_position = target_global_pos
	update_player_status()
	
	var event_triggered: bool = check_coordinate_event()
	if not event_triggered:
		check_random_encounter()

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():
		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):
		if forward_vector.z < -0.5:
			current_dir = Direction.NORTH
		else:
			current_dir = Direction.SOUTH
	else:
		if forward_vector.x > 0.5:
			current_dir = Direction.EAST
		else:
			current_dir = 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 = "現在地: (X: %d, Y: %d) | 向き: %s" % [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"]:
		if triggered_events.has(current_grid_pos) and triggered_events[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()
	
	print("歩数: %d | 遭遇確率: %d%% | 出目: %d%%" % [steps_since_last_fight, int(current_chance * 100), int(roll * 100)])
	
	if roll < current_chance:
		trigger_battle_encounter()

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

# 💡 戦闘突入時の処理(敵グラフィックを表示する)
func trigger_battle_encounter() -> void:
	current_state = GameState.BATTLE
	steps_since_last_fight = 0
	
	battle_text.text = "ゴブリンが現れた!\nコマンドを選択してください。"
	attack_button.show()
	auto_button.show()
	flee_button.show()
	
	message_window.show()
	
	# 💡 【新規】戦闘開始時に敵グラフィックを表示
	if enemy_graphic:
		enemy_graphic.show()
		
	attack_button.grab_focus()

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

func _on_attack_button_pressed() -> void:
	if current_state != GameState.BATTLE: return
	battle_text.text = "パーティメンバーのコマンドへ (未実装)"

func _on_auto_button_pressed() -> void:
	if current_state != GameState.BATTLE: return
	battle_text.text = "オートバトル (未実装)"

# 💡 「にげる」ボタンが押されたときの処理(敵グラフィックを隠す)
func _on_flee_button_pressed() -> void:
	if current_state != GameState.BATTLE: return
	
	current_state = GameState.EXPLORE
	message_window.hide()
	
	# 💡 【新規】戦闘終了時に敵グラフィックを隠す
	if enemy_graphic:
		enemy_graphic.hide()
		
	print("無事に逃げ切った!探索に戻ります。")

コード (camera_3d.gd) を上記のものに差し替えてください。これは、戦闘開始時にゴブリンの画像を show() させ、逃げ切ったときに hide() させるコードを追加したものになります。

敵の複数体表示、敵出現時のエフェクト、「たたかう・オート・にげる」コマンドの本格的な実装は後の工程で行います。

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

この記事を書いた人

コメント

コメントする

CAPTCHA


目次