You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
109 lines
2.7 KiB
109 lines
2.7 KiB
|
7 days ago
|
extends BaseUnit
|
||
|
|
class_name BaseEnemy
|
||
|
|
|
||
|
|
## Base class for all enemies in the game
|
||
|
|
## Provides common enemy functionality like AI, pathfinding, and targeting
|
||
|
|
|
||
|
|
signal target_changed(new_target: Node)
|
||
|
|
|
||
|
|
## Current target (usually a player)
|
||
|
|
var current_target: Node = null
|
||
|
|
## Enemy detection/aggro range
|
||
|
|
@export var detection_range: float = 10.0
|
||
|
|
## Whether this enemy is aggressive (will attack players)
|
||
|
|
@export var is_aggressive: bool = true
|
||
|
|
|
||
|
|
func _ready():
|
||
|
|
super._ready()
|
||
|
|
|
||
|
|
# Enemies should respawn by default
|
||
|
|
can_respawn = true
|
||
|
|
|
||
|
|
# Connect to health signals for AI reactions
|
||
|
|
health_changed.connect(_on_enemy_health_changed)
|
||
|
|
died.connect(_on_enemy_died)
|
||
|
|
respawned.connect(_on_enemy_respawned)
|
||
|
|
|
||
|
|
func _physics_process(delta):
|
||
|
|
# Only server handles enemy AI
|
||
|
|
if not multiplayer.is_server():
|
||
|
|
return
|
||
|
|
|
||
|
|
if is_dead:
|
||
|
|
return
|
||
|
|
|
||
|
|
# Update target if needed
|
||
|
|
_update_target()
|
||
|
|
|
||
|
|
## Find and update current target
|
||
|
|
func _update_target():
|
||
|
|
# Subclasses can override this to implement custom targeting logic
|
||
|
|
pass
|
||
|
|
|
||
|
|
## Get all players in range
|
||
|
|
func get_players_in_range(range_dist: float) -> Array[Node]:
|
||
|
|
var players_in_range: Array[Node] = []
|
||
|
|
|
||
|
|
# Find the players container
|
||
|
|
var level = get_tree().get_current_scene()
|
||
|
|
if not level or not level.has_node("PlayersContainer"):
|
||
|
|
return players_in_range
|
||
|
|
|
||
|
|
var players_container = level.get_node("PlayersContainer")
|
||
|
|
|
||
|
|
for player in players_container.get_children():
|
||
|
|
if player is Character and not player.is_dead:
|
||
|
|
var distance = global_position.distance_to(player.global_position)
|
||
|
|
if distance <= range_dist:
|
||
|
|
players_in_range.append(player)
|
||
|
|
|
||
|
|
return players_in_range
|
||
|
|
|
||
|
|
## Get nearest player
|
||
|
|
func get_nearest_player() -> Node:
|
||
|
|
var players = get_players_in_range(detection_range)
|
||
|
|
|
||
|
|
if players.is_empty():
|
||
|
|
return null
|
||
|
|
|
||
|
|
var nearest_player = null
|
||
|
|
var nearest_distance = INF
|
||
|
|
|
||
|
|
for player in players:
|
||
|
|
var distance = global_position.distance_to(player.global_position)
|
||
|
|
if distance < nearest_distance:
|
||
|
|
nearest_distance = distance
|
||
|
|
nearest_player = player
|
||
|
|
|
||
|
|
return nearest_player
|
||
|
|
|
||
|
|
## Health changed callback
|
||
|
|
func _on_enemy_health_changed(old_health: float, new_health: float):
|
||
|
|
# Subclasses can override to react to damage
|
||
|
|
pass
|
||
|
|
|
||
|
|
## Death callback
|
||
|
|
func _on_enemy_died(killer_id: int):
|
||
|
|
print("[Enemy ", name, "] died. Killer ID: ", killer_id)
|
||
|
|
|
||
|
|
# Subclasses can override for death effects
|
||
|
|
pass
|
||
|
|
|
||
|
|
## Respawn callback
|
||
|
|
func _on_enemy_respawned():
|
||
|
|
print("[Enemy ", name, "] respawned at ", global_position)
|
||
|
|
|
||
|
|
# Clear target on respawn
|
||
|
|
current_target = null
|
||
|
|
target_changed.emit(null)
|
||
|
|
|
||
|
|
# Subclasses can override for respawn effects
|
||
|
|
pass
|
||
|
|
|
||
|
|
## Override hurt animation
|
||
|
|
@rpc("any_peer", "call_local", "reliable")
|
||
|
|
func _play_hurt_animation():
|
||
|
|
# Flash red or play hurt animation
|
||
|
|
# Subclasses should implement this with their specific animations
|
||
|
|
pass
|