added blocking doors to paths.

This commit is contained in:
2026-01-10 19:46:55 +01:00
parent 24ea2f3c60
commit 25be2c00bd
33 changed files with 4383 additions and 455 deletions

View File

@@ -22,7 +22,7 @@ var level_exp_collected: float = 0.0
var level_coins_collected: int = 0
# Client ready tracking (server only)
var clients_ready: Dictionary = {} # peer_id -> bool
var clients_ready: Dictionary = {} # peer_id -> bool
func _ready():
# Add to group for easy access
@@ -222,7 +222,7 @@ func _sync_enemy_position(enemy_name: String, enemy_index: int, pos: Vector2, ve
# Clients receive enemy position updates from server
# Find the enemy by name or index
if multiplayer.is_server():
return # Server ignores this (it's the sender)
return # Server ignores this (it's the sender)
var entities_node = get_node_or_null("Entities")
if not entities_node:
@@ -248,7 +248,7 @@ func _sync_enemy_death(enemy_name: String, enemy_index: int):
# Clients receive enemy death sync from server
# Find the enemy by name or index
if multiplayer.is_server():
return # Server ignores this (it's the sender)
return # Server ignores this (it's the sender)
var entities_node = get_node_or_null("Entities")
if not entities_node:
@@ -278,7 +278,7 @@ func _sync_enemy_damage_visual(enemy_name: String, enemy_index: int):
# Clients receive enemy damage visual sync from server
# Find the enemy by name or index
if multiplayer.is_server():
return # Server ignores this (it's the sender)
return # Server ignores this (it's the sender)
var entities_node = get_node_or_null("Entities")
if not entities_node:
@@ -337,7 +337,7 @@ func _request_loot_pickup(loot_id: int, loot_position: Vector2, player_peer_id:
func _sync_show_level_complete(enemies_defeated: int, times_downed: int, exp_collected: float, coins_collected: int):
# Clients receive level complete UI sync from server
if multiplayer.is_server():
return # Server ignores this (it's the sender)
return # Server ignores this (it's the sender)
# Update stats before showing
level_enemies_defeated = enemies_defeated
@@ -352,7 +352,7 @@ func _sync_show_level_complete(enemies_defeated: int, times_downed: int, exp_col
func _sync_hide_level_complete():
# Clients receive hide level complete UI sync from server
if multiplayer.is_server():
return # Server ignores this (it's the sender)
return # Server ignores this (it's the sender)
var level_complete_ui = get_node_or_null("LevelCompleteUI")
if level_complete_ui:
@@ -362,7 +362,7 @@ func _sync_hide_level_complete():
func _sync_show_level_number(level: int):
# Clients receive level number UI sync from server
if multiplayer.is_server():
return # Server ignores this (it's the sender)
return # Server ignores this (it's the sender)
current_level = level
_show_level_number()
@@ -372,7 +372,7 @@ func _sync_loot_remove(loot_id: int, loot_position: Vector2):
# Clients receive loot removal sync from server
# Find the loot by ID or position
if multiplayer.is_server():
return # Server ignores this (it's the sender)
return # Server ignores this (it's the sender)
var entities_node = get_node_or_null("Entities")
if not entities_node:
@@ -469,6 +469,12 @@ func _generate_dungeon():
# Spawn interactable objects
_spawn_interactable_objects()
# Spawn blocking doors
_spawn_blocking_doors()
# Spawn room triggers
_spawn_room_triggers()
# Wait a frame to ensure enemies and objects are properly in scene tree before syncing
await get_tree().process_frame
@@ -483,7 +489,7 @@ func _generate_dungeon():
_move_all_players_to_start_room()
# Update camera immediately to ensure it's looking at the players
await get_tree().process_frame # Wait a frame for players to be fully in scene tree
await get_tree().process_frame # Wait a frame for players to be fully in scene tree
_update_camera()
# Show level number (for initial level generation only - not when called from level completion)
@@ -815,7 +821,7 @@ func _is_safe_spawn_position(world_pos: Vector2) -> bool:
return false
# Check if it's a floor tile
if grid[tile_x][tile_y] == 1: # Floor
if grid[tile_x][tile_y] == 1: # Floor
return true
return false
@@ -824,14 +830,13 @@ func _find_nearby_safe_spawn_position(world_pos: Vector2, max_distance: float =
# Find a nearby safe spawn position (on a floor tile)
# Returns the original position if it's safe, otherwise finds the nearest safe position
# max_distance: Maximum distance to search for a safe position
# First check if the original position is safe
if _is_safe_spawn_position(world_pos):
return world_pos
# Search in expanding circles around the position
var tile_size = 16
var search_radius = 1 # Start with 1 tile radius
var search_radius = 1 # Start with 1 tile radius
var max_radius = int(max_distance / tile_size) + 1
while search_radius <= max_radius:
@@ -898,7 +903,7 @@ func _sync_dungeon(dungeon_data_sync: Dictionary, seed_value: int, level: int, h
dungeon_data = dungeon_data_sync
dungeon_seed = seed_value
current_level = level # Update current_level FIRST before showing level number
current_level = level # Update current_level FIRST before showing level number
print("GameWorld: Client updated current_level to ", current_level, " from sync")
# Clear previous level on client
@@ -906,7 +911,7 @@ func _sync_dungeon(dungeon_data_sync: Dictionary, seed_value: int, level: int, h
# Wait for old entities to be fully freed before spawning new ones
await get_tree().process_frame
await get_tree().process_frame # Wait an extra frame to ensure cleanup is complete
await get_tree().process_frame # Wait an extra frame to ensure cleanup is complete
# Render dungeon on client
_render_dungeon()
@@ -920,9 +925,15 @@ func _sync_dungeon(dungeon_data_sync: Dictionary, seed_value: int, level: int, h
# Spawn interactable objects on client
_spawn_interactable_objects()
# Spawn blocking doors on client
_spawn_blocking_doors()
# Spawn room triggers on client
_spawn_room_triggers()
# Wait a frame to ensure all enemies and objects are properly added to scene tree and initialized
await get_tree().process_frame
await get_tree().process_frame # Wait an extra frame to ensure enemies are fully ready
await get_tree().process_frame # Wait an extra frame to ensure enemies are fully ready
# Update spawn points - use host's room if available, otherwise use start room
if not host_room.is_empty():
@@ -1072,10 +1083,20 @@ func _spawn_enemies():
if "damage" in enemy_data:
enemy.damage = enemy_data.damage
# 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)
# Add to scene tree AFTER setting authority and stats
entities_node.add_child(enemy)
enemy.global_position = enemy_data.position
# CRITICAL: Re-verify collision_mask AFTER adding to scene (in case scene file overrides it)
# This ensures enemies always collide with walls (layer 7 = bit 6 = 64)
if enemy.collision_mask != (1 | 2 | 64):
print("GameWorld: WARNING - Enemy ", enemy.name, " collision_mask was ", enemy.collision_mask, ", expected ", (1 | 2 | 64), "! Correcting...")
enemy.collision_mask = 1 | 2 | 64
# Verify authority is still set after adding to tree
if multiplayer.has_multiplayer_peer():
var auth_after = enemy.get_multiplayer_authority()
@@ -1228,7 +1249,7 @@ func _sync_dungeon_enemy_spawn(enemy_data: Dictionary):
if child.is_in_group("enemy") and child.has_meta("dungeon_spawned"):
# Check if it's a duplicate by position
var child_pos = child.global_position
if child_pos.distance_to(enemy_data.position) < 1.0: # Same position
if child_pos.distance_to(enemy_data.position) < 1.0: # Same position
# Also check if it's dead - if so, remove it first
if "is_dead" in child and child.is_dead:
print("GameWorld: Removing dead duplicate enemy at ", enemy_data.position)
@@ -1275,10 +1296,20 @@ func _sync_dungeon_enemy_spawn(enemy_data: Dictionary):
if "damage" in enemy_data:
enemy.damage = enemy_data.damage
# 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)
# Add to scene tree AFTER setting authority and stats
entities_node.add_child(enemy)
enemy.global_position = enemy_data.position
# CRITICAL: Re-verify collision_mask AFTER adding to scene (in case scene file overrides it)
# This ensures enemies always collide with walls (layer 7 = bit 6 = 64)
if enemy.collision_mask != (1 | 2 | 64):
print("GameWorld: WARNING - Enemy ", enemy.name, " collision_mask was ", enemy.collision_mask, ", expected ", (1 | 2 | 64), "! Correcting...")
enemy.collision_mask = 1 | 2 | 64
# Verify authority is still set
if multiplayer.has_multiplayer_peer():
var auth_after = enemy.get_multiplayer_authority()
@@ -1338,12 +1369,12 @@ func _clear_level():
# Free all entities immediately (not queue_free) to ensure they're gone before spawning new ones
for entity in entities_to_remove:
if is_instance_valid(entity):
entity.free() # Use free() instead of queue_free() for immediate removal
entity.free() # Use free() instead of queue_free() for immediate removal
# Remove stairs area
var stairs_area = get_node_or_null("StairsArea")
if stairs_area:
stairs_area.free() # Use free() for immediate removal
stairs_area.free() # Use free() for immediate removal
# Clear dungeon data (but keep it for now until new one is generated)
# dungeon_data = {} # Don't clear yet, wait for new generation
@@ -1394,7 +1425,7 @@ func _create_stairs_area():
# Set collision layer/mask BEFORE adding to scene
stairs_area.collision_layer = 0
stairs_area.collision_mask = 1 # Detect players (layer 1)
stairs_area.collision_mask = 1 # Detect players (layer 1)
# Add script BEFORE adding to scene (so _ready() is called properly)
var stairs_script = load("res://scripts/stairs.gd")
@@ -1419,7 +1450,7 @@ func _create_stairs_area():
func _on_player_reached_stairs(player: Node):
# Player reached stairs - trigger level complete
if not multiplayer.is_server() and multiplayer.has_multiplayer_peer():
return # Only server handles this
return # Only server handles this
print("GameWorld: Player ", player.name, " reached stairs!")
@@ -1443,7 +1474,7 @@ func _on_player_reached_stairs(player: Node):
_sync_show_level_complete.rpc(level_enemies_defeated, level_times_downed, level_exp_collected, level_coins_collected)
# After delay, hide UI and generate new level
await get_tree().create_timer(5.0).timeout # Show stats for 5 seconds
await get_tree().create_timer(5.0).timeout # Show stats for 5 seconds
# Hide level complete UI (server and clients)
var level_complete_ui = get_node_or_null("LevelCompleteUI")
@@ -1458,7 +1489,7 @@ func _on_player_reached_stairs(player: Node):
# Wait for old entities to be fully freed before generating new level
await get_tree().process_frame
await get_tree().process_frame # Wait an extra frame to ensure cleanup is complete
await get_tree().process_frame # Wait an extra frame to ensure cleanup is complete
# Generate next level
current_level += 1
@@ -1471,7 +1502,7 @@ func _on_player_reached_stairs(player: Node):
# We need to wait for all the async operations in _generate_dungeon() to finish
await get_tree().process_frame
await get_tree().process_frame
await get_tree().process_frame # Extra frame to ensure everything is done
await get_tree().process_frame # Extra frame to ensure everything is done
# Verify current_level is still correct
print("GameWorld: After dungeon generation, current_level = ", current_level)
@@ -1493,7 +1524,7 @@ func _on_player_reached_stairs(player: Node):
# Sync new level to all clients - use start room since all players should be there
# IMPORTANT: Wait multiple frames to ensure dungeon generation and enemy spawning is complete before syncing
await get_tree().process_frame
await get_tree().process_frame # Wait extra frames to ensure enemies are fully initialized
await get_tree().process_frame # Wait extra frames to ensure enemies are fully initialized
if multiplayer.has_multiplayer_peer():
var start_room = dungeon_data.start_room if not dungeon_data.is_empty() and dungeon_data.has("start_room") else {}
@@ -1603,21 +1634,27 @@ func _fade_in_player(player: Node):
for sprite_layer in sprite_layers:
if sprite_layer:
sprite_layer.modulate.a = 0.0 # Start invisible
sprite_layer.modulate.a = 0.0 # Start invisible
fade_tween.tween_property(sprite_layer, "modulate:a", 1.0, 1.0)
func _show_level_complete_ui():
# Create or show level complete UI
var level_complete_ui = get_node_or_null("LevelCompleteUI")
if not level_complete_ui:
# Try to load scene, but fall back to programmatic creation if it doesn't exist
var level_complete_scene = load("res://scenes/level_complete_ui.tscn")
if level_complete_scene:
level_complete_ui = level_complete_scene.instantiate()
level_complete_ui.name = "LevelCompleteUI"
add_child(level_complete_ui)
# Try to load scene if it exists, but fall back to programmatic creation if it doesn't
var scene_path = "res://scenes/level_complete_ui.tscn"
if ResourceLoader.exists(scene_path):
var level_complete_scene = load(scene_path)
if level_complete_scene:
level_complete_ui = level_complete_scene.instantiate()
level_complete_ui.name = "LevelCompleteUI"
add_child(level_complete_ui)
else:
# Scene file exists but failed to load - fall back to programmatic creation
print("GameWorld: Warning - level_complete_ui.tscn exists but failed to load, creating programmatically")
level_complete_ui = _create_level_complete_ui_programmatically()
else:
# Create UI programmatically if scene doesn't exist
# Scene file doesn't exist - create UI programmatically (expected behavior)
level_complete_ui = _create_level_complete_ui_programmatically()
if level_complete_ui:
@@ -1634,14 +1671,20 @@ func _show_level_number():
print("GameWorld: _show_level_number() called with current_level = ", current_level)
var level_text_ui = get_node_or_null("LevelTextUI")
if not level_text_ui:
# Try to load scene, but fall back to programmatic creation if it doesn't exist
var level_text_scene = load("res://scenes/level_text_ui.tscn")
if level_text_scene:
level_text_ui = level_text_scene.instantiate()
level_text_ui.name = "LevelTextUI"
add_child(level_text_ui)
# Try to load scene if it exists, but fall back to programmatic creation if it doesn't
var scene_path = "res://scenes/level_text_ui.tscn"
if ResourceLoader.exists(scene_path):
var level_text_scene = load(scene_path)
if level_text_scene:
level_text_ui = level_text_scene.instantiate()
level_text_ui.name = "LevelTextUI"
add_child(level_text_ui)
else:
# Scene file exists but failed to load - fall back to programmatic creation
print("GameWorld: Warning - level_text_ui.tscn exists but failed to load, creating programmatically")
level_text_ui = _create_level_text_ui_programmatically()
else:
# Create UI programmatically if scene doesn't exist
# Scene file doesn't exist - create UI programmatically (expected behavior)
level_text_ui = _create_level_text_ui_programmatically()
if level_text_ui:
@@ -1664,7 +1707,7 @@ func _create_level_complete_ui_programmatically() -> Node:
var vbox = VBoxContainer.new()
vbox.set_anchors_preset(Control.PRESET_CENTER)
vbox.offset_top = -200 # Position a bit up from center
vbox.offset_top = -200 # Position a bit up from center
canvas_layer.add_child(vbox)
# Title
@@ -1757,3 +1800,649 @@ func _move_players_to_host_room(host_room: Dictionary):
player.position = new_pos
print("GameWorld: Moved player ", player.name, " to ", new_pos)
spawn_index += 1
func _spawn_blocking_doors():
# Spawn blocking doors from dungeon data
if dungeon_data.is_empty() or not dungeon_data.has("blocking_doors"):
return
var blocking_doors = dungeon_data.blocking_doors
if blocking_doors == null or not blocking_doors is Array:
return
var door_scene = preload("res://scenes/door.tscn")
if not door_scene:
push_error("ERROR: Could not load door scene!")
return
var entities_node = get_node_or_null("Entities")
if not entities_node:
push_error("ERROR: Could not find Entities node!")
return
print("GameWorld: Spawning ", blocking_doors.size(), " blocking doors")
for i in range(blocking_doors.size()):
var door_data = blocking_doors[i]
if not door_data is Dictionary:
continue
var door = door_scene.instantiate()
door.name = "BlockingDoor_%d" % i
door.add_to_group("blocking_door")
# Set door properties BEFORE adding to scene (so _ready() has correct values)
door.type = door_data.type if "type" in door_data else "StoneDoor"
door.direction = door_data.direction if "direction" in door_data else "Up"
door.is_closed = door_data.is_closed if "is_closed" in door_data else true
# CRITICAL: Set puzzle requirements based on door_data
if "puzzle_type" in door_data:
if door_data.puzzle_type == "enemy":
door.requires_enemies = true
door.requires_switch = false
print("GameWorld: Door ", door.name, " requires enemies to open (puzzle_type: enemy)")
elif door_data.puzzle_type in ["switch_walk", "switch_pillar"]:
door.requires_enemies = false
door.requires_switch = true
print("GameWorld: Door ", door.name, " requires switch to open (puzzle_type: ", door_data.puzzle_type, ")")
door.blocking_room = door_data.blocking_room if "blocking_room" in door_data else {}
door.switch_room = door_data.switch_room if "switch_room" in door_data else {}
# CRITICAL: Verify door has blocking_room set - StoneDoor/GateDoor MUST be in a puzzle room
if (door_data.type == "StoneDoor" or door_data.type == "GateDoor"):
if not "blocking_room" in door_data or door_data.blocking_room.is_empty():
push_error("GameWorld: ERROR - Blocking door ", door.name, " (", door_data.type, ") has no blocking_room! This door should not exist! Removing it.")
door.queue_free()
continue
# CRITICAL: Verify door has puzzle_type - StoneDoor/GateDoor MUST have a puzzle
if not "puzzle_type" in door_data:
push_error("GameWorld: ERROR - Blocking door ", door.name, " (", door_data.type, ") has no puzzle_type! This door should not exist! Removing it.")
door.queue_free()
continue
print("GameWorld: Creating blocking door ", door.name, " (", door_data.type, ") for puzzle room (", door_data.blocking_room.x, ", ", door_data.blocking_room.y, "), puzzle_type: ", door_data.puzzle_type)
# CRITICAL: Store original door connection info from door_data.door
# For blocking doors: room1 = puzzle room (where door is IN / leads FROM)
# room2 = other room (where door leads TO)
# blocking_room = puzzle room (same as room1, where puzzle is)
if "door" in door_data and door_data.door is Dictionary:
var original_door = door_data.door
if "room1" in original_door and original_door.room1:
door.room1 = original_door.room1
if "room2" in original_door and original_door.room2:
door.room2 = original_door.room2
# CRITICAL: For StoneDoor/GateDoor, verify door.room1 matches blocking_room
# The door should be IN the puzzle room (room1 == blocking_room)
if (door_data.type == "StoneDoor" or door_data.type == "GateDoor") and door.blocking_room and not door.blocking_room.is_empty():
if not door.room1 or door.room1.is_empty():
push_error("GameWorld: ERROR - Blocking door ", door.name, " has no room1! Cannot verify it's in puzzle room! Removing it.")
door.queue_free()
continue
# Verify room1 (where door is) matches blocking_room (puzzle room)
var room1_matches_blocking = (door.room1.x == door.blocking_room.x and \
door.room1.y == door.blocking_room.y and \
door.room1.w == door.blocking_room.w and \
door.room1.h == door.blocking_room.h)
if not room1_matches_blocking:
push_error("GameWorld: ERROR - Blocking door ", door.name, " room1 (", door.room1.x, ",", door.room1.y, ") doesn't match blocking_room (", door.blocking_room.x, ",", door.blocking_room.y, ")! This door is NOT in the puzzle room! Removing it.")
door.queue_free()
continue
print("GameWorld: Blocking door ", door.name, " verified - room1 (", door.room1.x, ",", door.room1.y, ") == blocking_room (", door.blocking_room.x, ",", door.blocking_room.y, ") - door is IN puzzle room")
# Set multiplayer authority BEFORE adding to scene
if multiplayer.has_multiplayer_peer():
door.set_multiplayer_authority(1)
# CRITICAL: Set position BEFORE adding to scene tree (so _ready() can use it)
door.global_position = door_data.position if "position" in door_data else Vector2.ZERO
# Add to scene (this triggers _ready() which will use the position we just set)
entities_node.add_child(door)
# NOTE: Doors are connected to room triggers automatically by room_trigger._find_room_entities()
# No need to manually connect them here
# CRITICAL SAFETY CHECK: Verify door is for a puzzle room (StoneDoor/GateDoor should ONLY exist in puzzle rooms)
if door_data.type == "StoneDoor" or door_data.type == "GateDoor":
if not "blocking_room" in door_data or door_data.blocking_room.is_empty():
push_error("GameWorld: ERROR - Blocking door ", door.name, " (", door_data.type, ") has no blocking_room! This door should not exist!")
door.queue_free()
continue
if not "puzzle_type" in door_data:
push_error("GameWorld: ERROR - Blocking door ", door.name, " (", door_data.type, ") has no puzzle_type! This door should not exist!")
door.queue_free()
continue
# CRITICAL: Verify that this door actually has puzzle elements
# Puzzle elements should already be created in dungeon_generator, but verify they exist
var has_puzzle_element = false
# Spawn floor switch if this door requires one (puzzle_type is "switch_walk" or "switch_pillar")
if "puzzle_type" in door_data and (door_data.puzzle_type == "switch_walk" or door_data.puzzle_type == "switch_pillar"):
if "floor_switch_position" in door_data or ("switch_data" in door_data and door_data.switch_data.has("position")):
var switch_pos = door_data.floor_switch_position if "floor_switch_position" in door_data else door_data.switch_data.position
var switch_tile_x = door_data.switch_tile_x if "switch_tile_x" in door_data else door_data.switch_data.tile_x
var switch_tile_y = door_data.switch_tile_y if "switch_tile_y" in door_data else door_data.switch_data.tile_y
var switch_type = door_data.switch_type if "switch_type" in door_data else ("walk" if door_data.puzzle_type == "switch_walk" else "pillar")
var switch_weight = door_data.switch_required_weight if "switch_required_weight" in door_data else (1.0 if switch_type == "walk" else 5.0)
# CRITICAL: Check if switch already exists for THIS SPECIFIC ROOM (to avoid duplicates)
# Only connect to switches in the SAME blocking_room - never connect across rooms!
var existing_switch = null
var door_blocking_room = door_data.blocking_room if "blocking_room" in door_data else {}
# CRITICAL: Verify door has valid blocking_room before searching for switches
if door_blocking_room.is_empty():
push_error("GameWorld: ERROR - Door ", door.name, " has empty blocking_room! Cannot find switches!")
continue
for existing in get_tree().get_nodes_in_group("floor_switch"):
if not is_instance_valid(existing):
continue
# CRITICAL: Check ROOM FIRST (most important), then position
# Switches MUST have switch_room metadata set when spawned
if not existing.has_meta("switch_room"):
continue # Switch has no room metadata - skip it (can't verify it's in the right room)
var existing_switch_room = existing.get_meta("switch_room")
if existing_switch_room.is_empty():
continue # Invalid room data
# CRITICAL: Verify switch is in the SAME room as door (check room FIRST)
var room_match = (existing_switch_room.x == door_blocking_room.x and \
existing_switch_room.y == door_blocking_room.y and \
existing_switch_room.w == door_blocking_room.w and \
existing_switch_room.h == door_blocking_room.h)
if not room_match:
# Switch is in a different room - DO NOT connect, skip it
continue
# Room matches - now check position (must be exact match)
var pos_match = existing.global_position.distance_to(switch_pos) < 1.0
if pos_match:
# Both room AND position match - this is the correct switch
existing_switch = existing
print("GameWorld: Found existing switch ", existing.name, " in room (", door_blocking_room.x, ",", door_blocking_room.y, ") at position ", existing.global_position, " matching door room and position")
break
if existing_switch:
# CRITICAL: Double-check room match before connecting
var existing_switch_room_final = existing_switch.get_meta("switch_room")
var final_room_match = false
if existing_switch_room_final and not existing_switch_room_final.is_empty() and door_blocking_room and not door_blocking_room.is_empty():
final_room_match = (existing_switch_room_final.x == door_blocking_room.x and \
existing_switch_room_final.y == door_blocking_room.y and \
existing_switch_room_final.w == door_blocking_room.w and \
existing_switch_room_final.h == door_blocking_room.h)
if final_room_match:
# Switch already exists in the SAME room - connect door to existing switch
door.connected_switches.append(existing_switch)
has_puzzle_element = true
print("GameWorld: Connected door ", door.name, " (room: ", door_blocking_room.get("x", "?"), ",", door_blocking_room.get("y", "?"), ") to existing switch ", existing_switch.name, " in SAME room")
else:
push_error("GameWorld: ERROR - Attempted to connect door ", door.name, " to switch ", existing_switch.name, " but rooms don't match! Door room: (", door_blocking_room.get("x", "?"), ",", door_blocking_room.get("y", "?"), "), Switch room: (", existing_switch_room_final.get("x", "?") if existing_switch_room_final else "?", ",", existing_switch_room_final.get("y", "?") if existing_switch_room_final else "?", ")")
# Don't connect - spawn a new switch instead
existing_switch = null
else:
# Spawn new switch - CRITICAL: Only spawn if we have valid room data
if not door_blocking_room or door_blocking_room.is_empty():
push_error("GameWorld: ERROR - Cannot spawn switch for door ", door.name, " - no blocking_room!")
continue
# CRITICAL: Verify switch position matches door_data switch position exactly
# If switch_room in door_data doesn't match blocking_room, it's an error
if "switch_room" in door_data:
var door_switch_room = door_data.switch_room
if door_switch_room and not door_switch_room.is_empty():
var switch_room_matches = (door_switch_room.x == door_blocking_room.x and \
door_switch_room.y == door_blocking_room.y and \
door_switch_room.w == door_blocking_room.w and \
door_switch_room.h == door_blocking_room.h)
if not switch_room_matches:
push_error("GameWorld: ERROR - Door ", door.name, " switch_room (", door_switch_room.x, ",", door_switch_room.y, ") doesn't match blocking_room (", door_blocking_room.x, ",", door_blocking_room.y, ")! This is a bug!")
door.queue_free()
continue
var switch = _spawn_floor_switch(switch_pos, switch_weight, switch_tile_x, switch_tile_y, switch_type, door_blocking_room)
if switch:
# CRITICAL: Verify switch has room metadata set (should be set in _spawn_floor_switch)
if not switch.has_meta("switch_room"):
push_error("GameWorld: ERROR - Switch ", switch.name, " was spawned without switch_room metadata! Setting it now as fallback.")
switch.set_meta("switch_room", door_blocking_room) # Set it now as fallback
# CRITICAL: Verify switch room matches door blocking_room before connecting
# This ensures switches are ONLY connected to doors in the SAME room
var switch_room_check = switch.get_meta("switch_room")
if switch_room_check and not switch_room_check.is_empty() and door_blocking_room and not door_blocking_room.is_empty():
var room_match_before_connect = (switch_room_check.x == door_blocking_room.x and \
switch_room_check.y == door_blocking_room.y and \
switch_room_check.w == door_blocking_room.w and \
switch_room_check.h == door_blocking_room.h)
if room_match_before_connect:
# Connect switch to door ONLY if rooms match exactly
door.connected_switches.append(switch)
has_puzzle_element = true
print("GameWorld: Spawned switch ", switch.name, " in room (", door_blocking_room.x, ",", door_blocking_room.y, ") and connected to door ", door.name, " in SAME room")
# If this is a pillar switch, place a pillar in the same room
if switch_type == "pillar":
_place_pillar_in_room(door_blocking_room, switch_pos)
else:
push_error("GameWorld: ERROR - Switch ", switch.name, " room (", switch_room_check.x, ",", switch_room_check.y, ") doesn't match door blocking_room (", door_blocking_room.x, ",", door_blocking_room.y, ")! NOT connecting! Removing switch.")
switch.queue_free() # Remove the switch since it's in wrong room
has_puzzle_element = false # Don't count this as puzzle element
else:
push_error("GameWorld: ERROR - Switch ", switch.name, " or door ", door.name, " has invalid room data! Switch room: ", switch_room_check, ", Door room: ", door_blocking_room)
switch.queue_free() # Remove invalid switch
has_puzzle_element = false
else:
push_warning("GameWorld: WARNING - Failed to spawn floor switch for door ", door.name, "!")
# Place key in room if this is a KeyDoor
if door_data.type == "KeyDoor" and "key_room" in door_data:
_place_key_in_room(door_data.key_room)
has_puzzle_element = true # KeyDoors are always valid
# Spawn enemy spawners if this door requires enemies (puzzle_type is "enemy")
if "puzzle_type" in door_data and door_data.puzzle_type == "enemy":
print("GameWorld: ===== Door ", door.name, " has puzzle_type 'enemy' - checking for enemy_spawners =====")
if "enemy_spawners" in door_data and door_data.enemy_spawners is Array:
print("GameWorld: Door has enemy_spawners array with ", door_data.enemy_spawners.size(), " spawners")
var spawner_created = false
for spawner_data in door_data.enemy_spawners:
if spawner_data is Dictionary and spawner_data.has("position"):
# Check if spawner already exists for this room (to avoid duplicates)
var existing_spawner = null
for existing in get_tree().get_nodes_in_group("enemy_spawner"):
if existing.global_position.distance_to(spawner_data.position) < 1.0:
existing_spawner = existing
break
if existing_spawner:
# Spawner already exists - just verify it's set up correctly
existing_spawner.set_meta("blocking_room", door_data.blocking_room)
spawner_created = true
print("GameWorld: Found existing spawner ", existing_spawner.name, " for door ", door.name)
else:
# Spawn new spawner
var spawner = _spawn_enemy_spawner(
spawner_data.position,
spawner_data.room if spawner_data.has("room") else door_data.blocking_room,
spawner_data # Pass spawner_data to access spawn_once flag
)
if spawner:
# Store reference to door for spawner (optional - spawner will be found by room trigger)
spawner.set_meta("blocking_room", door_data.blocking_room)
spawner_created = true
print("GameWorld: Spawned enemy spawner ", spawner.name, " for door ", door.name, " at ", spawner_data.position)
if spawner_created:
has_puzzle_element = true
else:
push_warning("GameWorld: WARNING - Failed to spawn enemy spawner for door ", door.name, "!")
if "enemy_spawners" not in door_data:
push_warning("GameWorld: Reason: door_data has no 'enemy_spawners' key!")
elif not door_data.enemy_spawners is Array:
push_warning("GameWorld: Reason: door_data.enemy_spawners is not an Array! Type: ", typeof(door_data.enemy_spawners))
elif door_data.enemy_spawners.size() == 0:
push_warning("GameWorld: Reason: door_data.enemy_spawners array is empty!")
else:
if "puzzle_type" in door_data:
print("GameWorld: Door ", door.name, " has puzzle_type '", door_data.puzzle_type, "' (not 'enemy')")
# CRITICAL: If door has no puzzle elements (neither switch nor spawner), this is an error
# This should never happen if dungeon_generator logic is correct, but add safety check
if door_data.type != "KeyDoor" and not has_puzzle_element:
push_error("GameWorld: ERROR - Blocking door ", door.name, " (type: ", door_data.type, ") has no puzzle elements! This door should not exist!")
print("GameWorld: Door data keys: ", door_data.keys())
print("GameWorld: Door puzzle_type: ", door_data.get("puzzle_type", "MISSING"))
print("GameWorld: Door has requires_switch: ", door_data.get("requires_switch", false))
print("GameWorld: Door has requires_enemies: ", door_data.get("requires_enemies", false))
print("GameWorld: Door has floor_switch_position: ", "floor_switch_position" in door_data)
print("GameWorld: Door has enemy_spawners: ", "enemy_spawners" in door_data)
# Remove the door since it's invalid - it was created without puzzle elements
door.queue_free()
print("GameWorld: Removed invalid blocking door ", door.name, " - it had no puzzle elements!")
continue # Skip to next door
print("GameWorld: Spawned ", blocking_doors.size(), " blocking doors")
func _spawn_floor_switch(i_position: Vector2, required_weight: float, tile_x: int, tile_y: int, switch_type: String = "walk", switch_room: Dictionary = {}) -> Node:
# Spawn a floor switch
# switch_type: "walk" for walk-on switch (weight 1), "pillar" for pillar switch (weight 5)
var switch_script = load("res://scripts/floor_switch.gd")
if not switch_script:
push_error("ERROR: Could not load floor_switch script!")
return null
var switch = Area2D.new()
switch.set_script(switch_script)
switch.name = "FloorSwitch_%d_%d" % [tile_x, tile_y]
switch.add_to_group("floor_switch")
# Set properties
switch.switch_type = switch_type if switch_type == "walk" or switch_type == "pillar" else "walk"
switch.required_weight = required_weight # Will be overridden in _ready() based on switch_type, but set it here too
switch.switch_tile_position = Vector2i(tile_x, tile_y)
# Create collision shape
var collision_shape = CollisionShape2D.new()
var circle_shape = CircleShape2D.new()
circle_shape.radius = 8.0 # 16 pixel diameter
collision_shape.shape = circle_shape
switch.add_child(collision_shape)
# Set multiplayer authority
if multiplayer.has_multiplayer_peer():
switch.set_multiplayer_authority(1)
# CRITICAL: Store switch_room metadata BEFORE adding to scene
# This ensures switches can be matched to doors in the same room
if switch_room and not switch_room.is_empty():
switch.set_meta("switch_room", switch_room)
print("GameWorld: Set switch_room metadata for switch - room (", switch_room.x, ", ", switch_room.y, ")")
else:
push_warning("GameWorld: WARNING - Spawning switch without switch_room metadata! This may cause cross-room connections!")
# Add to scene
var entities_node = get_node_or_null("Entities")
if entities_node:
entities_node.add_child(switch)
switch.global_position = i_position
# Update tilemap to show switch tile (initial inactive state)
if dungeon_tilemap_layer:
var initial_tile: Vector2i
if switch_type == "pillar":
initial_tile = Vector2i(16, 9) # Pillar switch inactive
else:
initial_tile = Vector2i(11, 9) # Walk-on switch inactive
dungeon_tilemap_layer.set_cell(Vector2i(tile_x, tile_y), 0, initial_tile)
print("GameWorld: Spawned ", switch_type, " floor switch at ", i_position, " tile (", tile_x, ", ", tile_y, "), room: (", switch_room.get("x", "?") if switch_room and not switch_room.is_empty() else "?", ", ", switch_room.get("y", "?") if switch_room and not switch_room.is_empty() else "?", ")")
return switch
return null
func _spawn_enemy_spawner(i_position: Vector2, room: Dictionary, spawner_data: Dictionary = {}) -> Node:
# Spawn an enemy spawner for a blocking room
var spawner_script = load("res://scripts/enemy_spawner.gd")
if not spawner_script:
push_error("ERROR: Could not load enemy_spawner script!")
return null
var spawner = Node2D.new()
spawner.set_script(spawner_script)
spawner.name = "EnemySpawner_%d_%d" % [room.x, room.y] if room and not room.is_empty() else "EnemySpawner_%d_%d" % [int(i_position.x), int(i_position.y)]
spawner.add_to_group("enemy_spawner")
# Set spawner properties - IMPORTANT: spawn_on_ready = false so enemies only spawn when player enters room
spawner.spawn_on_ready = false # Don't spawn on ready - wait for room trigger
spawner.respawn_time = 0.0 # Don't respawn - enemies spawn once when entering room
spawner.max_enemies = 1 # One enemy per spawner
# Check if this spawner should be destroyed after spawning once
if spawner_data.has("spawn_once") and spawner_data.spawn_once:
spawner.set_meta("spawn_once", true) # Mark spawner for destruction after spawning
# Set enemy scenes (use default enemy types)
# enemy_scenes is Array[PackedScene], so we need to properly type it
var enemy_scenes: Array[PackedScene] = []
var scene_paths = [
"res://scenes/enemy_rat.tscn",
"res://scenes/enemy_humanoid.tscn",
"res://scenes/enemy_slime.tscn",
"res://scenes/enemy_bat.tscn"
]
# Load scenes and add to typed array
for path in scene_paths:
var scene = load(path) as PackedScene
if scene:
enemy_scenes.append(scene)
spawner.enemy_scenes = enemy_scenes
# Set multiplayer authority
if multiplayer.has_multiplayer_peer():
spawner.set_multiplayer_authority(1)
# Store room reference
if room and not room.is_empty():
spawner.set_meta("room", room)
# Add to scene
var entities_node = get_node_or_null("Entities")
if entities_node:
entities_node.add_child(spawner)
spawner.global_position = i_position
print("GameWorld: ✓✓✓ Successfully spawned enemy spawner '", spawner.name, "' at ", i_position, " for room at (", room.x if room and not room.is_empty() else "unknown", ", ", room.y if room and not room.is_empty() else "unknown", ")")
print("GameWorld: Spawner has room metadata: ", spawner.has_meta("room"))
if spawner.has_meta("room"):
var spawner_room = spawner.get_meta("room")
print("GameWorld: Spawner room metadata: (", spawner_room.x if spawner_room and not spawner_room.is_empty() else "none", ", ", spawner_room.y if spawner_room and not spawner_room.is_empty() else "none", ", ", spawner_room.w if spawner_room and not spawner_room.is_empty() else "none", "x", spawner_room.h if spawner_room and not spawner_room.is_empty() else "none", ")")
print("GameWorld: Spawner in group 'enemy_spawner': ", spawner.is_in_group("enemy_spawner"))
print("GameWorld: Spawner enemy_scenes.size(): ", spawner.enemy_scenes.size() if "enemy_scenes" in spawner else "N/A")
return spawner
return null
func _spawn_room_triggers():
# Spawn room trigger areas for all rooms
if dungeon_data.is_empty() or not dungeon_data.has("rooms"):
return
var rooms = dungeon_data.rooms
if rooms == null or not rooms is Array:
return
var trigger_script = load("res://scripts/room_trigger.gd")
if not trigger_script:
push_error("ERROR: Could not load room_trigger script!")
return
var entities_node = get_node_or_null("Entities")
if not entities_node:
push_error("ERROR: Could not find Entities node!")
return
print("GameWorld: Spawning ", rooms.size(), " room triggers")
for i in range(rooms.size()):
var room = rooms[i]
if not room is Dictionary:
continue
var trigger = Area2D.new()
trigger.set_script(trigger_script)
trigger.name = "RoomTrigger_%d" % i
trigger.add_to_group("room_trigger")
# Set room data
trigger.room = room
# Create collision shape covering ONLY the room interior (no overlap with adjacent rooms)
var collision_shape = CollisionShape2D.new()
var rect_shape = RectangleShape2D.new()
var tile_size = 16
# Room interior is from room.x + 2 to room.x + room.w - 2 (excluding 2-tile walls)
# This ensures the trigger only covers THIS room, not adjacent rooms or doorways
var room_world_x = (room.x + 2) * tile_size
var room_world_y = (room.y + 2) * tile_size
var room_world_w = (room.w - 4) * tile_size # Width excluding 2-tile walls on each side
var room_world_h = (room.h - 4) * tile_size # Height excluding 2-tile walls on each side
rect_shape.size = Vector2(room_world_w, room_world_h)
collision_shape.shape = rect_shape
# Position collision shape at center of room (relative to Area2D)
collision_shape.position = Vector2(room_world_w / 2.0, room_world_h / 2.0)
trigger.add_child(collision_shape)
# Set Area2D global position to the top-left corner of the room interior
# This ensures the trigger ONLY covers this specific room
trigger.global_position = Vector2(room_world_x, room_world_y)
# Set multiplayer authority
if multiplayer.has_multiplayer_peer():
trigger.set_multiplayer_authority(1)
# Add to scene
entities_node.add_child(trigger)
print("GameWorld: Spawned ", rooms.size(), " room triggers")
func _place_key_in_room(room: Dictionary):
# Place a key in the specified room (as loot)
if room.is_empty():
return
var loot_scene = preload("res://scenes/loot.tscn")
if not loot_scene:
return
var entities_node = get_node_or_null("Entities")
if not entities_node:
return
# Find a valid floor position in the room
var tile_size = 16
var valid_positions = []
# Room interior is from room.x + 2 to room.x + room.w - 2
for x in range(room.x + 2, room.x + room.w - 2):
for y in range(room.y + 2, room.y + room.h - 2):
if x >= 0 and x < dungeon_data.map_size.x and y >= 0 and y < dungeon_data.map_size.y:
if dungeon_data.grid[x][y] == 1: # Floor
var world_x = x * tile_size + tile_size / 2.0
var world_y = y * tile_size + tile_size / 2.0
valid_positions.append(Vector2(world_x, world_y))
if valid_positions.size() > 0:
# Pick a random position
var rng = RandomNumberGenerator.new()
rng.randomize()
var key_pos = valid_positions[rng.randi() % valid_positions.size()]
# Spawn key loot
var key_loot = loot_scene.instantiate()
key_loot.name = "KeyLoot_%d_%d" % [int(key_pos.x), int(key_pos.y)]
key_loot.loot_type = key_loot.LootType.KEY
# Set multiplayer authority
if multiplayer.has_multiplayer_peer():
key_loot.set_multiplayer_authority(1)
entities_node.add_child(key_loot)
key_loot.global_position = key_pos
print("GameWorld: Placed key in room at ", key_pos)
func _place_pillar_in_room(room: Dictionary, switch_position: Vector2):
# Place a pillar in the specified room (needed for pillar switches)
if room.is_empty():
return
var interactable_object_scene = preload("res://scenes/interactable_object.tscn")
if not interactable_object_scene:
push_error("ERROR: Could not load interactable_object scene for pillar!")
return
var entities_node = get_node_or_null("Entities")
if not entities_node:
push_error("ERROR: Could not find Entities node for pillar placement!")
return
# Find a valid floor position in the room (away from the switch)
var tile_size = 16
var valid_positions = []
# Room interior is from room.x + 2 to room.x + room.w - 2 (excluding 2-tile walls)
# CRITICAL: Also exclude last row/column of floor tiles to prevent objects from spawning in walls
# Objects are 16x16, so we need at least 1 tile buffer from walls
# Floor tiles are at: room.x+2, room.x+3, ..., room.x+room.w-3 (last floor tile before wall)
# To exclude last tile, we want: room.x+2, room.x+3, ..., room.x+room.w-4
var min_x = room.x + 2
var max_x = room.x + room.w - 4 # Exclude last 2 floor tiles (room.w-3 is last floor, room.w-4 is second-to-last)
var min_y = room.y + 2
var max_y = room.y + room.h - 4 # Exclude last 2 floor tiles (room.h-3 is last floor, room.h-4 is second-to-last)
for x in range(min_x, max_x + 1): # +1 because range is exclusive at end
for y in range(min_y, max_y + 1):
if x >= 0 and x < dungeon_data.map_size.x and y >= 0 and y < dungeon_data.map_size.y:
if dungeon_data.grid[x][y] == 1: # Floor
# CRITICAL: Interactable objects are 16x16 pixels with origin at (0,0) (top-left)
# To center a 16x16 sprite in a 16x16 tile, we need to offset by 8 pixels into the tile
# Tile (x,y) spans from (x*16, y*16) to ((x+1)*16, (y+1)*16)
# Position sprite 8 pixels into the tile from top-left: (x*16 + 8, y*16 + 8)
var world_x = x * tile_size + 8
var world_y = y * tile_size + 8
var world_pos = Vector2(world_x, world_y)
# Ensure pillar is at least 2 tiles away from the switch
var distance_to_switch = world_pos.distance_to(switch_position)
if distance_to_switch >= tile_size * 2: # At least 2 tiles away
valid_positions.append(world_pos)
if valid_positions.size() > 0:
# Pick a random position
var rng = RandomNumberGenerator.new()
rng.randomize()
var pillar_pos = valid_positions[rng.randi() % valid_positions.size()]
# Spawn pillar interactable object
var pillar = interactable_object_scene.instantiate()
pillar.name = "Pillar_%d_%d" % [int(pillar_pos.x), int(pillar_pos.y)]
pillar.set_meta("dungeon_spawned", true)
pillar.set_meta("room", room)
# Set multiplayer authority
if multiplayer.has_multiplayer_peer():
pillar.set_multiplayer_authority(1)
# Add to scene tree
entities_node.add_child(pillar)
pillar.global_position = pillar_pos
# Call setup function to configure as pillar
if pillar.has_method("setup_pillar"):
pillar.call("setup_pillar")
else:
push_error("ERROR: Pillar does not have setup_pillar method!")
# Add to group for easy access
pillar.add_to_group("interactable_object")
print("GameWorld: Placed pillar in room at ", pillar_pos, " (switch at ", switch_position, ")")
else:
push_warning("GameWorld: Could not find valid position for pillar in room! Room might be too small.")
func _connect_door_to_room_trigger(door: Node):
# Connect a door to its room trigger area
# blocking_room is a variable in door.gd, so it should exist
var blocking_room = door.blocking_room
if not blocking_room or blocking_room.is_empty():
return
# Find the room trigger for this room
for trigger in get_tree().get_nodes_in_group("room_trigger"):
if is_instance_valid(trigger):
# room is a variable in room_trigger.gd, compare by values
var trigger_room = trigger.room
if trigger_room and not trigger_room.is_empty() and \
trigger_room.x == blocking_room.x and trigger_room.y == blocking_room.y and \
trigger_room.w == blocking_room.w and trigger_room.h == blocking_room.h:
# Connect door to trigger
door.room_trigger_area = trigger
# Add door to trigger's doors list (doors_in_room is a variable in room_trigger.gd)
trigger.doors_in_room.append(door)
break