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.
251 lines
7.3 KiB
251 lines
7.3 KiB
extends Node3D |
|
class_name EnemySpawner |
|
|
|
## Spawns enemies in waves around the arena edge |
|
## Server-authoritative spawning with multiplayer sync |
|
|
|
signal wave_started(wave_number: int) |
|
signal wave_completed(wave_number: int) |
|
signal enemy_spawned(enemy: Node) |
|
signal all_enemies_defeated() |
|
|
|
## Spawn configuration |
|
@export_category("Spawn Settings") |
|
@export var spawn_radius: float = 20.0 ## Distance from spawner center to spawn enemies |
|
@export var spawn_height: float = 0.5 ## Height above ground to spawn |
|
@export var enemies_per_wave: int = 3 ## How many enemies spawn per wave |
|
@export var auto_start_next_wave: bool = false ## Auto-start next wave when all defeated |
|
@export var wave_delay: float = 5.0 ## Delay before next wave auto-starts |
|
|
|
@export_category("Enemy Pool") |
|
@export var enemy_scenes: Array[PackedScene] = [] ## List of enemy scenes to spawn from |
|
|
|
## Wave tracking |
|
var current_wave: int = 0 |
|
var active_enemies: Array[Node] = [] |
|
var _wave_delay_timer: float = 0.0 |
|
var _waiting_for_next_wave: bool = false |
|
var _enemy_id_counter: int = 0 |
|
|
|
## Debug visualization |
|
@export var show_spawn_radius: bool = false ## Show spawn radius circle in game |
|
var _debug_circle: MeshInstance3D = null |
|
|
|
func _ready(): |
|
print("[EnemySpawner] Ready. Server: ", multiplayer.is_server()) |
|
_create_debug_circle() |
|
|
|
## Start a new wave |
|
func start_wave(): |
|
if not multiplayer.is_server(): |
|
return |
|
|
|
if not is_inside_tree(): |
|
push_error("[EnemySpawner] Cannot start wave - spawner not in scene tree!") |
|
return |
|
|
|
if enemy_scenes.is_empty(): |
|
push_error("[EnemySpawner] No enemy scenes configured!") |
|
return |
|
|
|
current_wave += 1 |
|
print("[EnemySpawner] Starting wave ", current_wave) |
|
wave_started.emit(current_wave) |
|
|
|
# Spawn enemies |
|
for i in range(enemies_per_wave): |
|
_spawn_enemy(i, enemies_per_wave) |
|
|
|
## Spawn a single enemy at a position around the circle |
|
func _spawn_enemy(index: int, total: int): |
|
if not multiplayer.is_server(): |
|
return |
|
|
|
if not is_inside_tree(): |
|
push_error("[EnemySpawner] Cannot spawn enemy - spawner not in scene tree!") |
|
return |
|
|
|
if enemy_scenes.is_empty(): |
|
return |
|
|
|
# Pick a random enemy scene from the pool |
|
var enemy_scene = enemy_scenes[randi() % enemy_scenes.size()] |
|
|
|
# Calculate spawn position in a circle |
|
var angle = (TAU / total) * index # Evenly distribute around circle |
|
var offset = Vector3( |
|
cos(angle) * spawn_radius, |
|
spawn_height, |
|
sin(angle) * spawn_radius |
|
) |
|
var spawn_pos = global_position + offset |
|
|
|
# Generate unique name for multiplayer sync |
|
_enemy_id_counter += 1 |
|
var enemy_name = "Enemy_" + str(current_wave) + "_" + str(_enemy_id_counter) |
|
|
|
# Spawn on all clients via RPC (call_local will spawn on server too) |
|
var enemy_scene_path = enemy_scene.resource_path |
|
rpc("_spawn_enemy_on_client", enemy_name, spawn_pos, enemy_scene_path) |
|
|
|
## RPC to spawn enemy on all clients (including server via call_local) |
|
@rpc("any_peer", "call_local", "reliable") |
|
func _spawn_enemy_on_client(enemy_name: String, spawn_pos: Vector3, scene_path: String): |
|
# Load the enemy scene |
|
var enemy_scene = load(scene_path) |
|
if not enemy_scene: |
|
push_error("[EnemySpawner] Failed to load enemy scene: ", scene_path) |
|
return |
|
|
|
# Instantiate enemy |
|
var enemy = enemy_scene.instantiate() |
|
enemy.name = enemy_name |
|
enemy.position = spawn_pos |
|
|
|
# Find enemies container |
|
var level = get_tree().get_current_scene() |
|
var enemies_container = null |
|
if level and level.has_node("EnemiesContainer"): |
|
enemies_container = level.get_node("EnemiesContainer") |
|
else: |
|
push_error("[EnemySpawner] EnemiesContainer not found!") |
|
return |
|
|
|
# Add to scene |
|
enemies_container.add_child(enemy, true) |
|
|
|
# Only server tracks active enemies and connects signals |
|
if multiplayer.is_server(): |
|
active_enemies.append(enemy) |
|
|
|
# Connect death signal if enemy is BaseEnemy |
|
if enemy is BaseEnemy: |
|
enemy.died.connect(_on_enemy_died.bind(enemy)) |
|
|
|
enemy_spawned.emit(enemy) |
|
|
|
print("[EnemySpawner] Spawned ", enemy.name, " at ", enemy.global_position, " on peer ", multiplayer.get_unique_id()) |
|
|
|
## Called when an enemy dies |
|
func _on_enemy_died(killer_id: int, enemy: Node): |
|
if not multiplayer.is_server(): |
|
return |
|
|
|
print("[EnemySpawner] Enemy ", enemy.name, " defeated by ", killer_id) |
|
|
|
# Will be cleaned up in _update_active_enemies |
|
|
|
## Update list of active enemies and check if wave complete |
|
func _update_active_enemies(): |
|
if not multiplayer.is_server(): |
|
return |
|
|
|
# Remove dead/invalid enemies from active list |
|
var alive_count = 0 |
|
for enemy in active_enemies: |
|
if is_instance_valid(enemy) and enemy is BaseEnemy and not enemy.is_dead: |
|
alive_count += 1 |
|
|
|
# Check if wave is complete |
|
if active_enemies.size() > 0 and alive_count == 0: |
|
_on_wave_completed() |
|
|
|
## Called when all enemies in wave are defeated |
|
func _on_wave_completed(): |
|
if not multiplayer.is_server(): |
|
return |
|
|
|
print("[EnemySpawner] Wave ", current_wave, " completed!") |
|
wave_completed.emit(current_wave) |
|
|
|
# Clean up dead enemies after a delay |
|
await get_tree().create_timer(2.0).timeout |
|
_cleanup_dead_enemies() |
|
|
|
all_enemies_defeated.emit() |
|
|
|
# Auto-start next wave if enabled |
|
if auto_start_next_wave: |
|
_waiting_for_next_wave = true |
|
_wave_delay_timer = wave_delay |
|
print("[EnemySpawner] Next wave starts in ", wave_delay, " seconds") |
|
|
|
## Remove dead enemies from the scene |
|
func _cleanup_dead_enemies(): |
|
var cleaned = 0 |
|
for enemy in active_enemies: |
|
if is_instance_valid(enemy) and enemy is BaseEnemy and enemy.is_dead: |
|
enemy.queue_free() |
|
cleaned += 1 |
|
|
|
active_enemies.clear() |
|
print("[EnemySpawner] Cleaned up ", cleaned, " defeated enemies") |
|
|
|
## Manual wave control |
|
func start_next_wave(): |
|
if not multiplayer.is_server(): |
|
return |
|
start_wave() |
|
|
|
func stop_waves(): |
|
_waiting_for_next_wave = false |
|
|
|
## Get current wave info |
|
func get_alive_enemy_count() -> int: |
|
var count = 0 |
|
for enemy in active_enemies: |
|
if is_instance_valid(enemy) and enemy is BaseEnemy and not enemy.is_dead: |
|
count += 1 |
|
return count |
|
|
|
func is_wave_active() -> bool: |
|
return get_alive_enemy_count() > 0 |
|
|
|
## Create debug visualization circle |
|
func _create_debug_circle(): |
|
# Create a circle mesh to show spawn radius |
|
_debug_circle = MeshInstance3D.new() |
|
_debug_circle.name = "DebugSpawnCircle" |
|
|
|
# Create circle mesh using ImmediateMesh |
|
var immediate_mesh = ImmediateMesh.new() |
|
var material = StandardMaterial3D.new() |
|
material.albedo_color = Color(1, 0.5, 0, 0.6) # Orange |
|
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA |
|
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED |
|
material.cull_mode = BaseMaterial3D.CULL_DISABLED |
|
|
|
# Draw circle |
|
immediate_mesh.surface_begin(Mesh.PRIMITIVE_LINE_STRIP) |
|
var segments = 64 |
|
for i in range(segments + 1): |
|
var angle = (TAU / segments) * i |
|
var x = cos(angle) * spawn_radius |
|
var z = sin(angle) * spawn_radius |
|
immediate_mesh.surface_add_vertex(Vector3(x, spawn_height, z)) |
|
immediate_mesh.surface_end() |
|
|
|
_debug_circle.mesh = immediate_mesh |
|
_debug_circle.material_override = material |
|
_debug_circle.visible = show_spawn_radius |
|
add_child(_debug_circle) |
|
|
|
## Update debug visualization (useful when changing spawn_radius in editor) |
|
func _process(_delta): |
|
# Update circle visibility |
|
if _debug_circle: |
|
_debug_circle.visible = show_spawn_radius |
|
|
|
# Server spawning logic |
|
if not multiplayer.is_server(): |
|
return |
|
|
|
# Handle wave delay timer |
|
if _waiting_for_next_wave: |
|
_wave_delay_timer -= _delta |
|
if _wave_delay_timer <= 0: |
|
_waiting_for_next_wave = false |
|
start_wave() |
|
|
|
# Check if all enemies defeated |
|
_update_active_enemies()
|
|
|