MultiplayerFighter
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

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()