extends Node2D # Enemy Spawner - Spawns enemies at this position @export var enemy_scenes: Array[PackedScene] = [] # List of enemy scenes to randomly choose from @export var spawn_on_ready: bool = true @export var respawn_time: float = 10.0 # Time to respawn after enemy dies @export var max_enemies: int = 1 # Maximum number of enemies this spawner can have alive var spawned_enemies: Array = [] var respawn_timer: float = 0.0 var smoke_puff_scene = preload("res://scenes/smoke_puff.tscn") var has_ever_spawned: bool = false # Track if this spawner has ever spawned an enemy func _ready(): print("========== EnemySpawner READY ==========") print(" Position: ", global_position) print(" Is server: ", multiplayer.is_server()) print(" Has multiplayer peer: ", multiplayer.has_multiplayer_peer()) print(" Is authority: ", str(is_multiplayer_authority()) if multiplayer.has_multiplayer_peer() else "N/A") print(" spawn_on_ready: ", spawn_on_ready) print(" max_enemies: ", max_enemies) print(" enemy_scenes.size(): ", enemy_scenes.size()) print(" Parent: ", get_parent()) # Verify enemy_scenes is set if enemy_scenes.size() == 0: push_error("EnemySpawner: ERROR - enemy_scenes array is EMPTY! Spawner will not be able to spawn enemies!") # Spawn on server, or in single player (no multiplayer peer) var should_spawn = spawn_on_ready and (multiplayer.is_server() or not multiplayer.has_multiplayer_peer()) print(" Should spawn? ", should_spawn) if should_spawn: print(" Calling spawn_enemy()...") call_deferred("spawn_enemy") # Use call_deferred to ensure scene is ready else: print(" NOT spawning - conditions not met (spawn_on_ready=", spawn_on_ready, ", will spawn when player enters room)") print("========================================") func _process(delta): # Only server spawns, or single player if multiplayer.has_multiplayer_peer() and not multiplayer.is_server(): return # Clean up dead enemies from list spawned_enemies = spawned_enemies.filter(func(e): return is_instance_valid(e) and not e.is_dead) # Check if we need to respawn (only if respawn_time > 0) # Puzzle spawners have respawn_time = 0.0, so they won't respawn if respawn_time > 0.0 and spawned_enemies.size() < max_enemies: respawn_timer += delta if respawn_timer >= respawn_time: spawn_enemy() respawn_timer = 0.0 func spawn_enemy(): print(">>> spawn_enemy() CALLED <<<") print(" Spawner: ", name, " at ", global_position) print(" enemy_scenes.size(): ", enemy_scenes.size()) print(" spawned_enemies.size(): ", spawned_enemies.size(), " / max_enemies: ", max_enemies) print(" spawn_on_ready: ", spawn_on_ready) # CRITICAL: Check if we can spawn (don't spawn if already at max) # Clean up dead enemies first spawned_enemies = spawned_enemies.filter(func(e): return is_instance_valid(e) and not e.is_dead) if spawned_enemies.size() >= max_enemies: print(" ERROR: Cannot spawn - already at max enemies (", spawned_enemies.size(), " >= ", max_enemies, ")") return # Only spawn on server (authority) if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority(): print(" ERROR: Cannot spawn - not multiplayer authority!") return # Choose enemy scene to spawn var scene_to_spawn: PackedScene = null if enemy_scenes.size() > 0: # Use random scene from list scene_to_spawn = enemy_scenes[randi() % enemy_scenes.size()] print(" Selected enemy scene from list: ", scene_to_spawn) else: push_error("ERROR: enemy_scenes array is EMPTY! Spawner has no enemy scenes to spawn!") return if not scene_to_spawn: push_error("ERROR: Failed to select enemy scene!") return print(" Spawning enemy at ", global_position) # CRITICAL: Spawn 3-4 smoke puffs first, wait for them to finish, THEN spawn enemy var num_puffs = randi_range(3, 4) # 3 or 4 smoke puffs print(" Spawning ", num_puffs, " smoke puffs before enemy...") # Spawn multiple smoke puffs at slightly different positions var smoke_puffs = [] var puff_spawn_radius = 8.0 # Pixels - spawn puffs in a small area around spawner var puff_positions = [] # Store positions for syncing to clients # Calculate puff positions first for i in range(num_puffs): var puff_offset = Vector2( randf_range(-puff_spawn_radius, puff_spawn_radius), randf_range(-puff_spawn_radius, puff_spawn_radius) ) puff_positions.append(global_position + puff_offset) # Spawn smoke puffs on server for puff_pos in puff_positions: var puff = _spawn_smoke_puff_at_position(puff_pos) if puff: smoke_puffs.append(puff) # Sync smoke puffs to all clients if multiplayer.has_multiplayer_peer() and multiplayer.is_server(): var game_world = get_tree().get_first_node_in_group("game_world") if not game_world: # Fallback: traverse up the tree to find GameWorld var node = get_parent() while node: if node.has_method("_sync_smoke_puffs"): game_world = node break node = node.get_parent() if game_world and game_world.has_method("_sync_smoke_puffs"): game_world._rpc_to_ready_peers("_sync_smoke_puffs", [name, puff_positions]) # Wait for smoke puffs to finish animating before spawning enemy # Reduced duration for faster spawning: 4 frames * (1/10.0) seconds per frame = 0.4s, plus move_duration 1.0s, plus fade_duration 0.3s = ~1.7s total var smoke_animation_duration = (4.0 / 10.0) + 1.0 + 0.3 # Reduced from 2.4s to 1.7s await get_tree().create_timer(smoke_animation_duration).timeout print(" Smoke puffs finished - now spawning enemy...") print(" Instantiating enemy scene...") var enemy = scene_to_spawn.instantiate() if not enemy: push_error("ERROR: Failed to instantiate enemy!") return print(" Enemy instantiated: ", enemy) enemy.global_position = global_position # Set spawn position for deterministic appearance seed (before adding to scene) if "spawn_position" in enemy: enemy.spawn_position = global_position print(" Set enemy position to: ", global_position) # If it's a humanoid enemy, randomize the humanoid_type var humanoid_type = null if scene_to_spawn.resource_path.ends_with("enemy_humanoid.tscn"): # Random humanoid type: 0=CYCLOPS, 1=DEMON, 2=HUMANOID, 3=NIGHTELF, 4=GOBLIN, 5=ORC, 6=SKELETON # Weight towards common types (goblins, humans, orcs) - 40% goblin, 30% humanoid, 20% orc, 10% other var rand_val = randf() var type_value = 2 # Default to HUMANOID if rand_val < 0.4: type_value = 4 # GOBLIN (40%) elif rand_val < 0.7: type_value = 2 # HUMANOID (30%) elif rand_val < 0.9: type_value = 5 # ORC (20%) else: # 10% for other types (distributed evenly) var other_types = [0, 1, 3, 6] # CYCLOPS, DEMON, NIGHTELF, SKELETON type_value = other_types[randi() % other_types.size()] enemy.humanoid_type = type_value humanoid_type = type_value print(" Randomized humanoid_type: ", type_value) # CRITICAL: Mark this enemy as spawned from a spawner (for door puzzle tracking) enemy.set_meta("spawned_from_spawner", true) enemy.set_meta("spawner_name", name) # CRITICAL: Set collision mask BEFORE adding to scene to ensure enemies collide with walls (layer 7 = bit 6 = 64) # This overrides any collision_mask set in the scene file enemy.collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64) # Set multiplayer authority BEFORE adding to scene tree (CRITICAL for RPC to work!) if multiplayer.has_multiplayer_peer(): enemy.set_multiplayer_authority(1) # Add to YSort node for automatic Y-sorting var ysort = get_parent().get_node_or_null("Entities") var parent = ysort if ysort else get_parent() print(" Parent node: ", parent) if not parent: push_error("ERROR: No parent node!") return print(" Adding enemy as child...") parent.add_child(enemy) # CRITICAL: Re-verify collision_mask AFTER adding to scene (in case _ready() or scene file overrides it) # Use call_deferred to ensure _ready() has completed first, then set the entire mask call_deferred("_verify_enemy_collision_mask", enemy) # Determine which scene index was used (for syncing to clients) var scene_index = -1 if enemy_scenes.size() > 0: # Find which scene was used for i in range(enemy_scenes.size()): if enemy_scenes[i] == scene_to_spawn: scene_index = i break # Store scene_index as metadata on the enemy (for syncing existing enemies to new clients) enemy.set_meta("spawn_scene_index", scene_index) spawned_enemies.append(enemy) has_ever_spawned = true # Mark that this spawner has spawned at least once print(" ✓ Successfully spawned enemy: ", enemy.name, " at ", global_position, " scene_index: ", scene_index) print(" Total spawned enemies: ", spawned_enemies.size()) # If this spawner is marked for one-time spawn, destroy it after spawning if has_meta("spawn_once") and get_meta("spawn_once"): print(" Spawner marked for one-time spawn - destroying after spawn") call_deferred("queue_free") # Destroy spawner after spawning once # Sync spawn to all clients via GameWorld if multiplayer.has_multiplayer_peer() and multiplayer.is_server(): # Get GameWorld by traversing up the tree (spawner is child of Entities, which is child of GameWorld) var game_world = get_tree().get_first_node_in_group("game_world") if not game_world: # Fallback: traverse up the tree to find GameWorld var node = get_parent() while node: if node.has_method("_sync_enemy_spawn"): game_world = node break node = node.get_parent() if game_world and game_world.has_method("_sync_enemy_spawn"): # Use spawner name for identification # Pass humanoid_type if it's a humanoid enemy (for syncing to clients) var sync_humanoid_type = humanoid_type if humanoid_type != null else -1 print(" DEBUG: Calling _sync_enemy_spawn.rpc with name=", name, " pos=", global_position, " scene_index=", scene_index, " humanoid_type=", sync_humanoid_type) game_world._rpc_to_ready_peers("_sync_enemy_spawn", [name, global_position, scene_index, sync_humanoid_type]) print(" Sent RPC to sync enemy spawn to clients: spawner=", name, " pos=", global_position, " scene_index=", scene_index, " humanoid_type=", sync_humanoid_type) else: var has_method_str = str(game_world.has_method("_sync_enemy_spawn")) if game_world else "N/A" push_error("ERROR: Could not find GameWorld or _sync_enemy_spawn method! game_world=", game_world, " has_method=", has_method_str) func spawn_enemy_at_position(spawn_pos: Vector2, scene_index: int = -1, humanoid_type: int = -1): # This method is called by GameWorld RPC to spawn enemies on clients # scene_index tells us which scene from enemy_scenes array was used on the server # humanoid_type tells us the humanoid type if it's a humanoid enemy (for syncing from server) var scene_to_spawn: PackedScene = null if scene_index >= 0 and scene_index < enemy_scenes.size(): # Use the scene index that was synced from server scene_to_spawn = enemy_scenes[scene_index] print("Client: Using enemy scene at index ", scene_index, ": ", scene_to_spawn) elif enemy_scenes.size() > 0: # Fallback: use first scene if index is invalid scene_to_spawn = enemy_scenes[0] print("Client: Invalid scene_index, using first enemy scene: ", scene_to_spawn) if not scene_to_spawn: push_error("ERROR: Spawner has no enemy scenes set! Add scenes to enemy_scenes array.") return print("Client: spawn_enemy_at_position called at ", spawn_pos, " humanoid_type: ", humanoid_type) # NOTE: Smoke puffs are synced via RPC (_sync_smoke_puffs) from server # so we don't spawn them here - they're already spawned by the RPC handler # Instantiate and add enemy var enemy = scene_to_spawn.instantiate() if not enemy: push_error("ERROR: Failed to instantiate enemy on client!") return enemy.global_position = spawn_pos # Set spawn position for deterministic appearance seed (before adding to scene) if "spawn_position" in enemy: enemy.spawn_position = spawn_pos # If it's a humanoid enemy, set the humanoid_type from server if humanoid_type >= 0 and scene_to_spawn.resource_path.ends_with("enemy_humanoid.tscn"): enemy.humanoid_type = humanoid_type print("Client: Set humanoid_type to ", humanoid_type) # CRITICAL: Set collision mask BEFORE adding to scene to ensure enemies collide with walls (layer 7 = bit 6 = 64) # This overrides any collision_mask set in the scene file enemy.collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64) # Set multiplayer authority BEFORE adding to scene tree (CRITICAL!) if multiplayer.has_multiplayer_peer(): enemy.set_multiplayer_authority(1) # Add to YSort node for automatic Y-sorting var ysort = get_parent().get_node_or_null("Entities") var parent = ysort if ysort else get_parent() if not parent: push_error("ERROR: No parent node on client!") return parent.add_child(enemy) # CRITICAL: Re-verify collision_mask AFTER adding to scene (in case _ready() or scene file overrides it) # Use call_deferred to ensure _ready() has completed first, then set the entire mask call_deferred("_verify_enemy_collision_mask", enemy) print(" ✓ Client spawned enemy: ", enemy.name, " at ", spawn_pos) func get_spawned_enemy_positions() -> Array: # Return array of dictionaries with position, scene_index, and humanoid_type for all currently spawned enemies var enemy_data = [] for enemy in spawned_enemies: if is_instance_valid(enemy) and not enemy.is_dead: var scene_index = -1 if enemy.has_meta("spawn_scene_index"): scene_index = enemy.get_meta("spawn_scene_index") var data = {"position": enemy.global_position, "scene_index": scene_index} # Include humanoid_type if it's a humanoid enemy if "humanoid_type" in enemy: data["humanoid_type"] = enemy.humanoid_type else: data["humanoid_type"] = -1 enemy_data.append(data) return enemy_data func _verify_enemy_collision_mask(enemy: Node): # Verify and correct enemy collision_mask after _ready() has completed # This ensures enemies always collide with walls (layer 7 = bit 6 = 64), not layer 3 or 4 if not is_instance_valid(enemy): return var expected_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64) if enemy.collision_mask != expected_mask: print("EnemySpawner: WARNING - Enemy ", enemy.name, " collision_mask was ", enemy.collision_mask, ", expected ", expected_mask, "! Correcting...") enemy.collision_mask = expected_mask # Double-check by setting individual layers to be absolutely sure enemy.set_collision_mask_value(1, true) # Players enemy.set_collision_mask_value(2, true) # Objects enemy.set_collision_mask_value(7, true) # Walls (layer 7) enemy.set_collision_mask_value(3, false) # Ensure layer 3 is NOT set enemy.set_collision_mask_value(4, false) # Ensure layer 4 is NOT set print("EnemySpawner: Corrected enemy ", enemy.name, " collision_mask to ", enemy.collision_mask) func _spawn_smoke_puff(): # Legacy function - use _spawn_smoke_puff_at_position instead _spawn_smoke_puff_at_position(global_position) func _spawn_smoke_puff_at_position(puff_position: Vector2) -> Node: print(" _spawn_smoke_puff_at_position() called at ", puff_position) print(" smoke_puff_scene: ", smoke_puff_scene) if smoke_puff_scene: print(" Instantiating smoke puff...") var puff = smoke_puff_scene.instantiate() if puff: puff.global_position = puff_position # Ensure smoke puff is visible - set high z_index so it appears above ground puff.z_index = 10 # High z-index to ensure visibility if puff.has_node("Sprite2D"): puff.get_node("Sprite2D").z_index = 10 # Add to Entities node (same as enemies) for proper layering var entities_node = get_parent().get_node_or_null("Entities") var parent = entities_node if entities_node else get_parent() if parent: parent.add_child(puff) print(" ✓ Smoke puff spawned at ", puff_position, " z_index: ", puff.z_index) return puff else: print(" ERROR: No parent node for smoke puff!") puff.queue_free() return null else: print(" ERROR: Failed to instantiate smoke puff") return null else: print(" WARNING: No smoke puff scene loaded") return null