1extends Node
2
3# --- State Machine Definition ---
4
5# Define states as constants for clarity and to avoid typos
6const STATE_IDLE = "idle"
7const STATE_PATROLLING = "patrolling"
8const STATE_CHASING = "chasing"
9const STATE_ATTACKING = "attacking"
10const STATE_FLEEING = "fleeing"
11const STATE_SEARCHING = "searching"
12
13var current_state = STATE_IDLE
14var previous_state = null
15
16# State-specific data
17var patrol_points: Array = []
18var current_patrol_index = 0
19var target_node: Node = null
20var player_last_known_position: Vector3 = Vector3.ZERO
21var detection_radius: float = 100.0
22var attack_range: float = 20.0
23var flee_threshold: float = 50.0
24var flee_speed_multiplier: float = 1.5
25var patrol_speed: float = 50.0
26var chase_speed: float = 100.0
27var attack_cooldown: float = 1.0
28var time_since_last_attack: float = 0.0
29var search_timer: float = 5.0
30var time_in_search: float = 0.0
31
32# References to other nodes (e.g., player, navigation agent)
33@onready var navigation_agent = $NavigationAgent3D
34@onready var animation_player = $AnimationPlayer
35@onready var health_component = $HealthComponent # Assuming a HealthComponent exists
36
37# --- Signals ---
38
39signal state_changed(new_state: String, old_state: String)
40
41# --- Initialization ---
42
43func _ready():
44# Initialize patrol points if they are set in the inspector
45if patrol_points.is_empty():
46printerr("Patrol points not set for enemy AI.")
47# Potentially set default patrol points or disable patrolling
48
49# Set initial state
50set_state(STATE_IDLE)
51
52# --- State Management ---
53
54func set_state(new_state: String):
55if current_state == new_state:
56return # Already in this state
57
58previous_state = current_state
59current_state = new_state
60print("Enemy AI state changed from %s to %s" % [previous_state, current_state])
61emit_signal("state_changed", new_state, previous_state)
62
63# Reset state-specific variables on transition
64match new_state:
65STATE_IDLE:
66target_node = null
67player_last_known_position = Vector3.ZERO
68time_in_search = 0.0
69STATE_PATROLLING:
70current_patrol_index = 0
71navigation_agent.set_velocity_computed_callback(null)
72animation_player.play("walk") # Assuming 'walk' animation
73STATE_CHASING:
74# Target node should already be set by the transition logic
75pass
76STATE_ATTACKING:
77animation_player.play("attack") # Assuming 'attack' animation
78# Attack logic will be handled in _process
79STATE_FLEEING:
80animation_player.play("run") # Assuming 'run' animation
81# Fleeing logic will be handled in _process
82STATE_SEARCHING:
83time_in_search = search_timer
84animation_player.play("search") # Assuming 'search' animation
85
86# --- Core Logic (Process) ---
87
88func _process(delta):
89# Handle global cooldowns or timers
90if time_since_last_attack < attack_cooldown:
91time_since_last_attack += delta
92
93# State-specific behavior
94match current_state:
95STATE_IDLE:
96handle_idle(delta)
97STATE_PATROLLING:
98handle_patrolling(delta)
99STATE_CHASING:
100handle_chasing(delta)
101STATE_ATTACKING:
102handle_attacking(delta)
103STATE_FLEEING:
104handle_fleeing(delta)
105STATE_SEARCHING:
106handle_searching(delta)
107
108# Check for global state transitions (e.g., player detection)
109check_global_transitions(delta)
110
111# --- State Handler Functions ---
112
113func handle_idle(delta):
114# In idle, wait for something to happen or transition to patrolling
115# For simplicity, let's say it transitions to patrolling after a short delay
116# Or if a player is detected, it will transition via check_global_transitions
117pass # No specific action in idle, relies on global checks
118
119func handle_patrolling(delta):
120if patrol_points.is_empty():
121set_state(STATE_IDLE)
122return
123
124var current_point = patrol_points[current_patrol_index]
125var distance_to_point = global_position.distance_to(current_point)
126
127# Move towards the current patrol point
128navigation_agent.set_target_position(current_point)
129var next_path_position = navigation_agent.get_next_path_position()
130var direction = (next_path_position - global_position).normalized()
131velocity = direction * patrol_speed
132move_and_slide()
133
134# If close enough to the point, move to the next one
135if distance_to_point < 1.0: # Tolerance for reaching the point
136current_patrol_index = (current_patrol_index + 1) % patrol_points.size()
137# If we've completed a full patrol cycle, consider transitioning
138# For now, just continue patrolling
139
140func handle_chasing(delta):
141if target_node == null or not is_instance_valid(target_node):
142set_state(STATE_SEARCHING)
143return
144
145var distance_to_target = global_position.distance_to(target_node.global_position)
146
147# If within attack range, transition to attacking
148if distance_to_target <= attack_range and time_since_last_attack >= attack_cooldown:
149set_state(STATE_ATTACKING)
150return
151
152# If player is too far or health is low, consider fleeing
153if health_component != null and health_component.current_health < health_component.max_health * 0.3:
154set_state(STATE_FLEEING)
155return
156
157# Move towards the target
158navigation_agent.set_target_position(target_node.global_position)
159var next_path_position = navigation_agent.get_next_path_position()
160var direction = (next_path_position - global_position).normalized()
161velocity = direction * chase_speed
162move_and_slide()
163
164func handle_attacking(delta):
165# Attack logic is primarily triggered by the animation or a timer
166# For this example, assume attack animation plays and we transition back after it finishes
167# Or if the target moves out of range
168if animation_player.is_playing() and animation_player.current_animation == "attack":
169# Keep facing the target
170look_at(target_node.global_position)
171# Attack logic would typically happen here, e.g., dealing damage
172# For simplicity, we'll transition back after a short duration or if target is lost
173pass # Attack animation handles duration
174else:
175# If animation finished or was interrupted
176time_since_last_attack = 0.0 # Reset cooldown
177if target_node != null and is_instance_valid(target_node):
178var distance_to_target = global_position.distance_to(target_node.global_position)
179if distance_to_target <= attack_range:
180set_state(STATE_CHASING) # Re-engage if still in range
181else:
182set_state(STATE_CHASING) # Go back to chasing if out of range
183else:
184set_state(STATE_SEARCHING)
185
186func handle_fleeing(delta):
187if target_node == null or not is_instance_valid(target_node):
188set_state(STATE_SEARCHING)
189return
190
191# Flee away from the player
192var flee_direction = (global_position - target_node.global_position).normalized()
193var flee_target_position = global_position + flee_direction * 50.0 # Flee a set distance
194
195navigation_agent.set_target_position(flee_target_position)
196var next_path_position = navigation_agent.get_next_path_position()
197var direction = (next_path_position - global_position).normalized()
198velocity = direction * chase_speed * flee_speed_multiplier
199move_and_slide()
200
201# If player is no longer a threat (e.g., out of range, or health is high)
202var distance_to_target = global_position.distance_to(target_node.global_position)
203if distance_to_target > flee_threshold * 2.0 or (health_component != null and health_component.current_health > health_component.max_health * 0.7):
204set_state(STATE_SEARCHING)
205
206func handle_searching(delta):
207if time_in_search > 0:
208time_in_search -= delta
209# Look around randomly or in a pattern
210# For simplicity, just stay put and look
211pass
212else:
213# Finished searching, decide next action
214if target_node != null and is_instance_valid(target_node):
215# If player was last seen, maybe go to last known position
216set_state(STATE_CHASING) # Or go back to patrolling
217else:
218set_state(STATE_PATROLLING)
219
220# --- Global Transition Checks ---
221
222func check_global_transitions(delta):
223# Check for player detection
224var player = get_tree().get_first_node_in_group("player") # Assuming player is in 'player' group
225if player != null:
226var distance_to_player = global_position.distance_to(player.global_position)
227
228# Player detection logic
229if distance_to_player <= detection_radius:
230if current_state != STATE_CHASING and current_state != STATE_ATTACKING and current_state != STATE_FLEEING:
231target_node = player
232player_last_known_position = player.global_position
233set_state(STATE_CHASING)
234return # Prioritize chasing if detected
235else:
236# Player out of detection range
237if current_state == STATE_CHASING or current_state == STATE_ATTACKING:
238# If player was just in range and is now out, start searching
239player_last_known_position = player.global_position # Update last known position
240set_state(STATE_SEARCHING)
241return
242
243# Check for low health to flee
244if health_component != null and health_component.current_health < health_component.max_health * 0.3:
245if current_state != STATE_FLEEING:
246set_state(STATE_FLEEING)
247return
248
249# If player is lost and we are not actively chasing/attacking/fleeing
250if target_node == null and current_state != STATE_PATROLLING and current_state != STATE_IDLE:
251set_state(STATE_SEARCHING)
252
253# --- Helper Functions ---
254
255func move_and_slide():
256# Placeholder for actual movement logic, e.g., using CharacterBody3D
257# This would typically involve setting velocity and calling move_and_slide()
258# For this example, we'll assume it's handled elsewhere or is a simplified representation.
259pass
260
261func look_at(target_position: Vector3):
262# Placeholder for looking at a target position
263# This would typically involve rotating the node's basis
264pass
265
266func get_velocity() -> Vector3:
267# Placeholder for getting current velocity
268return velocity
269
270func set_velocity(new_velocity: Vector3):
271# Placeholder for setting current velocity
272velocity = new_velocity