2995 lines
123 KiB
GDScript
2995 lines
123 KiB
GDScript
extends Node2D
|
|
|
|
# Game World - Main game scene that manages the gameplay
|
|
|
|
@onready var player_manager = $PlayerManager
|
|
@onready var camera = $Camera2D
|
|
@onready var network_manager = $"/root/NetworkManager"
|
|
|
|
var local_players = []
|
|
|
|
# Dungeon generation
|
|
var dungeon_data: Dictionary = {}
|
|
var dungeon_tilemap_layer: TileMapLayer = null
|
|
var dungeon_tilemap_layer_above: TileMapLayer = null
|
|
var current_level: int = 1
|
|
var dungeon_seed: int = 0
|
|
|
|
# Level stats tracking
|
|
var level_enemies_defeated: int = 0
|
|
var level_times_downed: int = 0
|
|
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
|
|
|
|
# Level complete tracking
|
|
var level_complete_triggered: bool = false # Prevent multiple level complete triggers
|
|
|
|
func _ready():
|
|
# Add to group for easy access
|
|
add_to_group("game_world")
|
|
|
|
# Connect network signals
|
|
if network_manager:
|
|
network_manager.player_connected.connect(_on_player_connected)
|
|
network_manager.player_disconnected.connect(_on_player_disconnected)
|
|
|
|
# Create chat UI
|
|
_create_chat_ui()
|
|
|
|
# Generate dungeon on host
|
|
if multiplayer.is_server() or not multiplayer.has_multiplayer_peer():
|
|
print("GameWorld: _ready() - Will generate dungeon (is_server: ", multiplayer.is_server(), ", has_peer: ", multiplayer.has_multiplayer_peer(), ")")
|
|
call_deferred("_generate_dungeon")
|
|
else:
|
|
print("GameWorld: _ready() - Client, will wait for dungeon sync")
|
|
# Clients spawn players immediately (they'll be moved when dungeon syncs)
|
|
call_deferred("_spawn_all_players")
|
|
|
|
func _spawn_all_players():
|
|
print("GameWorld: Spawning all players. Server: ", multiplayer.is_server())
|
|
print("GameWorld: Players info: ", network_manager.players_info)
|
|
|
|
# Only spawn on server initially - clients will spawn via RPC
|
|
if multiplayer.is_server():
|
|
for peer_id in network_manager.players_info.keys():
|
|
var info = network_manager.players_info[peer_id]
|
|
print("GameWorld: Server spawning ", info.local_player_count, " players for peer ", peer_id)
|
|
player_manager.spawn_players_for_peer(peer_id, info.local_player_count)
|
|
|
|
func _on_player_connected(peer_id: int, player_info: Dictionary):
|
|
print("GameWorld: Player connected signal received for peer ", peer_id, " with info: ", player_info)
|
|
|
|
# Send join message to chat (only on server to avoid duplicates)
|
|
if multiplayer.is_server():
|
|
_send_player_join_message(peer_id, player_info)
|
|
|
|
# Reset ready status for this peer (they need to notify again after spawning)
|
|
if multiplayer.is_server():
|
|
clients_ready[peer_id] = false
|
|
# Reset all_clients_ready flag for all server players when a new client connects
|
|
_reset_server_players_ready_flag()
|
|
|
|
if multiplayer.is_server():
|
|
var host_room = null
|
|
# Sync existing dungeon to the new client (if dungeon has been generated)
|
|
if not dungeon_data.is_empty():
|
|
print("GameWorld: Syncing existing dungeon to client ", peer_id)
|
|
host_room = _get_host_room()
|
|
_sync_dungeon.rpc_id(peer_id, dungeon_data, dungeon_seed, current_level, host_room)
|
|
|
|
# Update spawn points to use host's current room
|
|
host_room = _get_host_room()
|
|
if not host_room.is_empty():
|
|
print("GameWorld: Host is in room at ", host_room.x, ", ", host_room.y)
|
|
_update_spawn_points(host_room)
|
|
else:
|
|
print("GameWorld: Could not find host room, using start room")
|
|
_update_spawn_points() # Use start room as fallback
|
|
|
|
# Server spawns locally
|
|
print("GameWorld: Server spawning players for peer ", peer_id)
|
|
player_manager.spawn_players_for_peer(peer_id, player_info.local_player_count)
|
|
# Sync spawn to all clients
|
|
_sync_spawn_player.rpc(peer_id, player_info.local_player_count)
|
|
|
|
# Sync existing enemies (from spawners) to the new client
|
|
# Wait a bit after dungeon sync to ensure spawners are spawned first
|
|
call_deferred("_sync_existing_enemies_to_client", peer_id)
|
|
|
|
# Sync existing chest open states to the new client
|
|
# Wait a bit after dungeon sync to ensure objects are spawned first
|
|
call_deferred("_sync_existing_chest_states_to_client", peer_id)
|
|
|
|
# Note: Dungeon-spawned enemies are already synced via _sync_dungeon RPC
|
|
# which includes dungeon_data.enemies and calls _spawn_enemies() on the client.
|
|
# So we don't need to sync them again with individual RPCs.
|
|
|
|
# Note: Interactable objects are also synced via _sync_dungeon RPC
|
|
# which includes dungeon_data.interactable_objects and calls _spawn_interactable_objects() on the client.
|
|
# However, chest open states need to be synced separately since they change during gameplay.
|
|
|
|
# Sync existing torches to the new client
|
|
call_deferred("_sync_existing_torches_to_client", peer_id)
|
|
else:
|
|
# Clients spawn directly when they receive this signal
|
|
print("GameWorld: Client spawning players for peer ", peer_id)
|
|
player_manager.spawn_players_for_peer(peer_id, player_info.local_player_count)
|
|
|
|
func _sync_existing_enemies_to_client(client_peer_id: int):
|
|
# Find all enemy spawners and sync their spawned enemies to the new client
|
|
# Spawners are children of the Entities node, not GameWorld directly
|
|
var spawners = []
|
|
var entities_node = get_node_or_null("Entities")
|
|
if entities_node:
|
|
# Find spawners in Entities node
|
|
for child in entities_node.get_children():
|
|
if child.is_in_group("enemy_spawner") or (child.has_method("get_spawned_enemy_positions") and child.has_method("spawn_enemy_at_position")):
|
|
spawners.append(child)
|
|
else:
|
|
# Fallback: search all children (shouldn't happen, but just in case)
|
|
for child in get_children():
|
|
if child.is_in_group("enemy_spawner") or (child.has_method("get_spawned_enemy_positions") and child.has_method("spawn_enemy_at_position")):
|
|
spawners.append(child)
|
|
|
|
print("GameWorld: Syncing existing enemies to client ", client_peer_id, " from ", spawners.size(), " spawners")
|
|
|
|
for spawner in spawners:
|
|
var enemy_data = spawner.get_spawned_enemy_positions()
|
|
for data in enemy_data:
|
|
# Use the stored scene_index and humanoid_type for each enemy
|
|
var pos = data.position
|
|
var scene_index = data.scene_index if "scene_index" in data else -1
|
|
var humanoid_type = data.humanoid_type if "humanoid_type" in data else -1
|
|
_sync_enemy_spawn.rpc_id(client_peer_id, spawner.name, pos, scene_index, humanoid_type)
|
|
print("GameWorld: Sent enemy spawn sync to client ", client_peer_id, ": spawner=", spawner.name, " pos=", pos, " scene_index=", scene_index, " humanoid_type=", humanoid_type)
|
|
|
|
func _on_player_disconnected(peer_id: int, player_info: Dictionary):
|
|
print("GameWorld: Player disconnected - ", peer_id)
|
|
|
|
# Send disconnect message to chat (only on server to avoid duplicates)
|
|
if multiplayer.is_server():
|
|
_send_player_disconnect_message(peer_id, player_info)
|
|
|
|
player_manager.despawn_players_for_peer(peer_id)
|
|
|
|
@rpc("authority", "reliable")
|
|
func _sync_spawn_player(peer_id: int, local_count: int):
|
|
# Only clients process this RPC (server already spawned)
|
|
if not multiplayer.is_server():
|
|
print("GameWorld: Client received RPC to spawn peer ", peer_id)
|
|
player_manager.spawn_players_for_peer(peer_id, local_count)
|
|
# Client will notify server when ready via _notify_client_ready
|
|
|
|
@rpc("any_peer", "reliable")
|
|
func _notify_client_ready(peer_id: int):
|
|
# Client notifies server that it's ready (all players spawned)
|
|
if multiplayer.is_server():
|
|
print("GameWorld: Client ", peer_id, " is ready")
|
|
clients_ready[peer_id] = true
|
|
# Store the time when this client became ready
|
|
var current_time = Time.get_ticks_msec() / 1000.0
|
|
clients_ready[str(peer_id) + "_ready_time"] = current_time
|
|
# Notify all players that a client is ready (so server players can check if all are ready)
|
|
_client_ready_status_changed.rpc(clients_ready.duplicate())
|
|
# Note: We don't reset the flag here - we want server players to check if all are ready now
|
|
|
|
@rpc("authority", "reliable")
|
|
func _client_ready_status_changed(_ready_status: Dictionary):
|
|
# Server broadcasts ready status to all clients
|
|
# This allows server players to know when all clients are ready
|
|
# Currently not used on clients, but kept for future use
|
|
pass
|
|
|
|
func _reset_server_players_ready_flag():
|
|
# Reset all_clients_ready flag for all server players
|
|
# This happens when a new client connects, so server players re-check readiness
|
|
# Called directly on server (not via RPC)
|
|
var entities_node = get_node_or_null("Entities")
|
|
if entities_node:
|
|
for child in entities_node.get_children():
|
|
if child.is_in_group("player"):
|
|
# Only reset for server-controlled players (authority = server peer ID)
|
|
if child.get_multiplayer_authority() == multiplayer.get_unique_id():
|
|
child.all_clients_ready = false
|
|
print("GameWorld: Reset all_clients_ready for server player ", child.name)
|
|
|
|
@rpc("authority", "reliable")
|
|
func _sync_smoke_puffs(_spawner_name: String, puff_positions: Array):
|
|
# Clients spawn smoke puffs when server tells them to
|
|
if not multiplayer.is_server():
|
|
var smoke_puff_scene = preload("res://scenes/smoke_puff.tscn")
|
|
if not smoke_puff_scene:
|
|
return
|
|
|
|
var entities_node = get_node_or_null("Entities")
|
|
if not entities_node:
|
|
return
|
|
|
|
for pos in puff_positions:
|
|
if pos is Vector2:
|
|
var puff = smoke_puff_scene.instantiate()
|
|
if puff:
|
|
puff.global_position = pos
|
|
puff.z_index = 10
|
|
if puff.has_node("Sprite2D"):
|
|
puff.get_node("Sprite2D").z_index = 10
|
|
entities_node.add_child(puff)
|
|
|
|
@rpc("authority", "reliable")
|
|
func _sync_enemy_spawn(spawner_name: String, spawn_position: Vector2, scene_index: int = -1, humanoid_type: int = -1):
|
|
# Clients spawn enemy when server tells them to
|
|
if not multiplayer.is_server():
|
|
print("GameWorld: Client received RPC to spawn enemy at spawner: ", spawner_name, " position: ", spawn_position, " scene_index: ", scene_index, " humanoid_type: ", humanoid_type)
|
|
|
|
# Find the spawner node by name (spawners are children of Entities node, not GameWorld)
|
|
var spawner = null
|
|
var entities_node = get_node_or_null("Entities")
|
|
if entities_node:
|
|
spawner = entities_node.get_node_or_null(spawner_name)
|
|
|
|
# Fallback: try as direct child of GameWorld (for backwards compatibility)
|
|
if not spawner:
|
|
spawner = get_node_or_null(spawner_name)
|
|
|
|
if not spawner:
|
|
push_error("ERROR: Could not find spawner with name: ", spawner_name, " in Entities node or GameWorld")
|
|
return
|
|
|
|
if not spawner.has_method("spawn_enemy_at_position"):
|
|
push_error("ERROR: Spawner does not have spawn_enemy_at_position method!")
|
|
return
|
|
|
|
# Call spawn method on the spawner with scene index and humanoid_type
|
|
spawner.spawn_enemy_at_position(spawn_position, scene_index, humanoid_type)
|
|
|
|
# Loot ID counter (server only)
|
|
var loot_id_counter: int = 0
|
|
|
|
@rpc("authority", "reliable")
|
|
func _sync_loot_spawn(spawn_position: Vector2, loot_type: int, initial_velocity: Vector2, initial_velocity_z: float, loot_id: int = -1):
|
|
# Clients spawn loot when server tells them to
|
|
if not multiplayer.is_server():
|
|
var loot_scene = preload("res://scenes/loot.tscn")
|
|
if not loot_scene:
|
|
return
|
|
|
|
var loot = loot_scene.instantiate()
|
|
var entities_node = get_node_or_null("Entities")
|
|
if entities_node:
|
|
# Set multiplayer authority to server (peer 1) so RPCs work
|
|
if multiplayer.has_multiplayer_peer():
|
|
loot.set_multiplayer_authority(1)
|
|
# Store unique loot ID for identification
|
|
if loot_id >= 0:
|
|
loot.set_meta("loot_id", loot_id)
|
|
entities_node.add_child(loot)
|
|
loot.global_position = spawn_position
|
|
loot.loot_type = loot_type
|
|
# Set initial velocity before _ready() processes
|
|
loot.velocity = initial_velocity
|
|
loot.velocity_z = initial_velocity_z
|
|
loot.velocity_set_by_spawner = true
|
|
loot.is_airborne = true
|
|
print("Client spawned loot: ", loot_type, " at ", spawn_position, " authority: ", loot.get_multiplayer_authority())
|
|
|
|
@rpc("authority", "unreliable")
|
|
func _sync_enemy_position(enemy_name: String, enemy_index: int, pos: Vector2, vel: Vector2, z_pos: float, dir: int, frame: int, anim: String, frame_num: int, state_value: int):
|
|
# 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)
|
|
|
|
var entities_node = get_node_or_null("Entities")
|
|
if not entities_node:
|
|
return
|
|
|
|
# Try to find enemy by name first, then by index
|
|
var enemy = null
|
|
for child in entities_node.get_children():
|
|
if child.is_in_group("enemy"):
|
|
if child.name == enemy_name:
|
|
enemy = child
|
|
break
|
|
elif child.has_meta("enemy_index") and child.get_meta("enemy_index") == enemy_index:
|
|
enemy = child
|
|
break
|
|
|
|
if enemy and enemy.has_method("_sync_position"):
|
|
# Call the enemy's _sync_position method directly (not via RPC)
|
|
enemy._sync_position(pos, vel, z_pos, dir, frame, anim, frame_num, state_value)
|
|
|
|
@rpc("authority", "reliable")
|
|
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)
|
|
|
|
var entities_node = get_node_or_null("Entities")
|
|
if not entities_node:
|
|
return
|
|
|
|
# Try to find enemy by name first, then by index
|
|
var enemy = null
|
|
for child in entities_node.get_children():
|
|
if child.is_in_group("enemy"):
|
|
if child.name == enemy_name:
|
|
enemy = child
|
|
break
|
|
elif child.has_meta("enemy_index") and child.get_meta("enemy_index") == enemy_index:
|
|
enemy = child
|
|
break
|
|
|
|
if enemy and enemy.has_method("_sync_death"):
|
|
# Call the enemy's _sync_death method directly (not via RPC)
|
|
enemy._sync_death()
|
|
else:
|
|
# Enemy not found - might already be freed or never spawned
|
|
# This is okay, just log it
|
|
print("GameWorld: Could not find enemy for death sync: name=", enemy_name, " index=", enemy_index)
|
|
|
|
@rpc("authority", "reliable")
|
|
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)
|
|
|
|
var entities_node = get_node_or_null("Entities")
|
|
if not entities_node:
|
|
return
|
|
|
|
# Try to find enemy by name first, then by index
|
|
var enemy = null
|
|
for child in entities_node.get_children():
|
|
if child.is_in_group("enemy"):
|
|
if child.name == enemy_name:
|
|
enemy = child
|
|
break
|
|
elif child.has_meta("enemy_index") and child.get_meta("enemy_index") == enemy_index:
|
|
enemy = child
|
|
break
|
|
|
|
if enemy and enemy.has_method("_sync_damage_visual"):
|
|
# Call the enemy's _sync_damage_visual method directly (not via RPC)
|
|
enemy._sync_damage_visual()
|
|
else:
|
|
# Enemy not found - might already be freed or never spawned
|
|
# This is okay, just log it
|
|
print("GameWorld: Could not find enemy for damage visual sync: name=", enemy_name, " index=", enemy_index)
|
|
|
|
@rpc("any_peer", "reliable")
|
|
func _request_loot_pickup(loot_id: int, loot_position: Vector2, player_peer_id: int):
|
|
# Server receives loot pickup request from client
|
|
# Route to the correct loot item
|
|
if not multiplayer.is_server():
|
|
return
|
|
|
|
var entities_node = get_node_or_null("Entities")
|
|
if not entities_node:
|
|
return
|
|
|
|
# Find loot by ID or position
|
|
var loot = null
|
|
for child in entities_node.get_children():
|
|
if child.is_in_group("loot") or child.has_method("_request_pickup"):
|
|
# Check by ID first
|
|
if child.has_meta("loot_id") and child.get_meta("loot_id") == loot_id:
|
|
loot = child
|
|
break
|
|
# Fallback: check by position (within 16 pixels tolerance)
|
|
elif child.global_position.distance_to(loot_position) < 16.0:
|
|
loot = child
|
|
break
|
|
|
|
if loot and loot.has_method("_request_pickup"):
|
|
# Call the loot's _request_pickup method directly (it will handle the rest)
|
|
loot._request_pickup(player_peer_id)
|
|
else:
|
|
print("GameWorld: Could not find loot for pickup request: id=", loot_id, " pos=", loot_position)
|
|
|
|
@rpc("authority", "reliable")
|
|
func _sync_player_exit_stairs(player_peer_id: int):
|
|
# Client receives notification that a player reached exit stairs
|
|
if multiplayer.is_server():
|
|
return # Server ignores this (it's the sender)
|
|
|
|
# Find the player by peer ID
|
|
var players = get_tree().get_nodes_in_group("player")
|
|
var target_player = null
|
|
for p in players:
|
|
if p.has_method("get_multiplayer_authority") and p.get_multiplayer_authority() == player_peer_id:
|
|
target_player = p
|
|
break
|
|
|
|
# Only disable controls/collision if this is our local player
|
|
if target_player:
|
|
var my_peer_id = multiplayer.get_unique_id()
|
|
if player_peer_id == my_peer_id:
|
|
# This is our local player - disable controls and collision
|
|
target_player.controls_disabled = true
|
|
target_player.set_collision_layer_value(1, false)
|
|
print("GameWorld: Client disabled controls and collision for local player ", target_player.name)
|
|
# Show black fade overlay for this player
|
|
_show_black_fade_overlay()
|
|
|
|
@rpc("authority", "reliable")
|
|
func _sync_show_level_complete(level_time: float):
|
|
# Clients receive level complete UI sync from server
|
|
if multiplayer.is_server():
|
|
return # Server ignores this (it's the sender)
|
|
|
|
# Stop HUD timer when level completes (on clients too)
|
|
var hud = get_node_or_null("IngameHUD")
|
|
if hud and hud.has_method("stop_timer"):
|
|
hud.stop_timer()
|
|
|
|
# Show level complete UI (each client will show their own local player's stats)
|
|
_show_level_complete_ui(level_time)
|
|
|
|
@rpc("authority", "reliable")
|
|
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)
|
|
|
|
var level_complete_ui = get_node_or_null("LevelCompleteUI")
|
|
if level_complete_ui:
|
|
level_complete_ui.visible = false
|
|
|
|
@rpc("authority", "reliable")
|
|
func _sync_restore_player_controls():
|
|
# Clients receive restore controls/collision sync from server
|
|
if multiplayer.is_server():
|
|
return # Server ignores this (it's the sender)
|
|
|
|
# Restore controls and collision for local player
|
|
var my_peer_id = multiplayer.get_unique_id()
|
|
var players = get_tree().get_nodes_in_group("player")
|
|
for player in players:
|
|
if player.has_method("get_multiplayer_authority") and player.get_multiplayer_authority() == my_peer_id:
|
|
player.controls_disabled = false
|
|
player.set_collision_layer_value(1, true)
|
|
print("GameWorld: Client restored controls and collision for local player ", player.name)
|
|
break
|
|
|
|
@rpc("authority", "reliable")
|
|
func _sync_remove_black_fade():
|
|
# Clients receive remove black fade sync from server
|
|
if multiplayer.is_server():
|
|
return # Server ignores this (it's the sender)
|
|
|
|
_remove_black_fade_overlay()
|
|
|
|
@rpc("authority", "reliable")
|
|
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)
|
|
|
|
current_level = level
|
|
_show_level_number()
|
|
|
|
@rpc("authority", "reliable")
|
|
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)
|
|
|
|
var entities_node = get_node_or_null("Entities")
|
|
if not entities_node:
|
|
return
|
|
|
|
# Try to find loot by ID first, then by position
|
|
var loot = null
|
|
for child in entities_node.get_children():
|
|
if child.is_in_group("loot") or child.has_method("_sync_remove"):
|
|
# Check by ID first
|
|
if child.has_meta("loot_id") and child.get_meta("loot_id") == loot_id:
|
|
loot = child
|
|
break
|
|
# Fallback: check by position (within 16 pixels tolerance)
|
|
elif child.global_position.distance_to(loot_position) < 16.0:
|
|
loot = child
|
|
break
|
|
|
|
if loot and loot.has_method("_sync_remove"):
|
|
# Call the loot's _sync_remove method directly (not via RPC)
|
|
loot._sync_remove()
|
|
else:
|
|
# Loot not found - might already be freed or never spawned
|
|
# This is okay, just log it
|
|
print("GameWorld: Could not find loot for removal sync: id=", loot_id, " pos=", loot_position)
|
|
|
|
func _process(_delta):
|
|
# Update camera to follow local players
|
|
_update_camera()
|
|
|
|
func _update_camera():
|
|
local_players = player_manager.get_local_players()
|
|
|
|
if local_players.is_empty():
|
|
return
|
|
|
|
# Calculate center of all local players
|
|
var center = Vector2.ZERO
|
|
for player in local_players:
|
|
center += player.position
|
|
center /= local_players.size()
|
|
|
|
# Smooth camera movement
|
|
camera.position = camera.position.lerp(center, 0.1)
|
|
|
|
# Adjust zoom based on player spread (for split-screen effect)
|
|
if local_players.size() > 1:
|
|
var max_distance = 0.0
|
|
for player in local_players:
|
|
var distance = center.distance_to(player.position)
|
|
max_distance = max(max_distance, distance)
|
|
|
|
# Adjust zoom to fit all players
|
|
var target_zoom = clamp(800.0 / (max_distance + 400.0), 0.5, 1.5)
|
|
camera.zoom = camera.zoom.lerp(Vector2.ONE * target_zoom, 0.05)
|
|
|
|
func _generate_dungeon():
|
|
print("GameWorld: _generate_dungeon() called - is_server: ", multiplayer.is_server(), ", has_peer: ", multiplayer.has_multiplayer_peer())
|
|
|
|
if not multiplayer.is_server() and multiplayer.has_multiplayer_peer():
|
|
print("GameWorld: Not server, skipping dungeon generation")
|
|
return
|
|
|
|
# Reset level complete flag for new level
|
|
level_complete_triggered = false
|
|
|
|
print("GameWorld: Generating dungeon level ", current_level)
|
|
|
|
# Generate seed (random for all levels)
|
|
if dungeon_seed == 0:
|
|
dungeon_seed = randi()
|
|
|
|
# Create dungeon generator
|
|
var generator = load("res://scripts/dungeon_generator.gd").new()
|
|
var map_size = Vector2i(72, 72) # 72x72 tiles
|
|
|
|
# Generate dungeon (pass current level for scaling)
|
|
dungeon_data = generator.generate_dungeon(map_size, dungeon_seed, current_level)
|
|
|
|
if dungeon_data.is_empty():
|
|
push_error("ERROR: Dungeon generation returned empty data!")
|
|
return
|
|
|
|
print("GameWorld: Dungeon generated with ", dungeon_data.rooms.size(), " rooms")
|
|
print("GameWorld: Start room at ", dungeon_data.start_room.x, ", ", dungeon_data.start_room.y)
|
|
print("GameWorld: Map size: ", dungeon_data.map_size)
|
|
|
|
# Render dungeon
|
|
_render_dungeon()
|
|
|
|
# Spawn torches
|
|
_spawn_torches()
|
|
|
|
# Spawn enemies
|
|
_spawn_enemies()
|
|
|
|
# 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
|
|
|
|
# Update player spawn points based on start room
|
|
_update_spawn_points()
|
|
|
|
# Spawn players for all connected peers (after dungeon is generated and spawn points are set)
|
|
# This ensures players spawn at the correct location
|
|
_spawn_all_players()
|
|
|
|
# Move any already-spawned players to the correct spawn points
|
|
_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
|
|
_update_camera()
|
|
|
|
# Show level number (for initial level generation only - not when called from level completion)
|
|
# Check if this is initial generation by checking if we're in _ready or if level is 1
|
|
# For level completion, the level number is shown after _generate_dungeon() completes
|
|
if current_level == 1:
|
|
_show_level_number()
|
|
# Sync to all clients
|
|
if multiplayer.has_multiplayer_peer():
|
|
_sync_show_level_number.rpc(current_level)
|
|
|
|
# Load HUD after dungeon generation completes (non-blocking)
|
|
call_deferred("_load_hud")
|
|
|
|
# Sync dungeon to all clients
|
|
if multiplayer.has_multiplayer_peer():
|
|
# Get host's current room for spawning new players near host
|
|
var host_room = _get_host_room()
|
|
|
|
# Debug: Check if enemies are in dungeon_data before syncing
|
|
if dungeon_data.has("enemies"):
|
|
print("GameWorld: Server syncing dungeon with ", dungeon_data.enemies.size(), " enemies")
|
|
else:
|
|
print("GameWorld: WARNING: Server dungeon_data has NO 'enemies' key before sync!")
|
|
|
|
_sync_dungeon.rpc(dungeon_data, dungeon_seed, current_level, host_room)
|
|
|
|
print("GameWorld: Dungeon generation completed successfully")
|
|
|
|
func _render_dungeon():
|
|
if dungeon_data.is_empty():
|
|
push_error("ERROR: Cannot render dungeon - no dungeon data!")
|
|
return
|
|
|
|
# Try to use existing TileMapLayer from scene, or create new one
|
|
var environment = get_node_or_null("Environment")
|
|
|
|
if environment:
|
|
dungeon_tilemap_layer = environment.get_node_or_null("DungeonLayer0")
|
|
dungeon_tilemap_layer_above = environment.get_node_or_null("TileMapLayerAbove")
|
|
|
|
if not dungeon_tilemap_layer:
|
|
# Create new TileMapLayer
|
|
print("GameWorld: Creating new TileMapLayer")
|
|
dungeon_tilemap_layer = TileMapLayer.new()
|
|
dungeon_tilemap_layer.name = "DungeonLayer0"
|
|
|
|
# Add to scene
|
|
if environment:
|
|
environment.add_child(dungeon_tilemap_layer)
|
|
else:
|
|
add_child(dungeon_tilemap_layer)
|
|
move_child(dungeon_tilemap_layer, 0)
|
|
|
|
dungeon_tilemap_layer.position = Vector2.ZERO
|
|
print("GameWorld: Created new TileMapLayer and added to scene")
|
|
else:
|
|
print("GameWorld: Using existing TileMapLayer from scene")
|
|
|
|
if not dungeon_tilemap_layer_above:
|
|
# Create new TileMapLayerAbove
|
|
print("GameWorld: Creating new TileMapLayerAbove")
|
|
dungeon_tilemap_layer_above = TileMapLayer.new()
|
|
dungeon_tilemap_layer_above.name = "TileMapLayerAbove"
|
|
|
|
# Add to scene
|
|
if environment:
|
|
environment.add_child(dungeon_tilemap_layer_above)
|
|
else:
|
|
add_child(dungeon_tilemap_layer_above)
|
|
move_child(dungeon_tilemap_layer_above, 0)
|
|
|
|
dungeon_tilemap_layer_above.position = Vector2.ZERO
|
|
print("GameWorld: Created new TileMapLayerAbove and added to scene")
|
|
else:
|
|
print("GameWorld: Using existing TileMapLayerAbove from scene")
|
|
|
|
# TileMapLayer should work standalone - no TileMap needed
|
|
print("GameWorld: TileMapLayer ready for rendering")
|
|
|
|
# Render tiles from dungeon_data
|
|
var tile_grid = dungeon_data.tile_grid
|
|
var grid = dungeon_data.grid
|
|
var map_size = dungeon_data.map_size
|
|
|
|
print("GameWorld: Rendering ", map_size.x, "x", map_size.y, " tiles")
|
|
var tiles_placed = 0
|
|
var above_tiles_placed = 0
|
|
|
|
const BLACK_TILE = Vector2i(2, 2) # Black tile for non-floor/wall/door
|
|
|
|
for x in range(map_size.x):
|
|
for y in range(map_size.y):
|
|
var tile_coords = tile_grid[x][y]
|
|
var grid_value = grid[x][y]
|
|
|
|
# Render main layer - set a tile for EVERY position
|
|
var main_tile: Vector2i
|
|
|
|
# Determine what tile to use based on grid value
|
|
# Only use the tile_coords if it's a valid tile (not 0,0) or if it's a wall
|
|
if grid_value == 0:
|
|
# Wall - use the tile_coords (which should be a wall tile, can be 0,0 for top-left corner)
|
|
main_tile = tile_coords
|
|
elif grid_value == 1:
|
|
# Floor - use the tile_coords (which should be a floor tile)
|
|
main_tile = tile_coords
|
|
elif grid_value == 2:
|
|
# Door - use the tile_coords (which should be a door tile)
|
|
main_tile = tile_coords
|
|
elif grid_value == 3:
|
|
# Corridor - use the tile_coords (which should be a floor tile)
|
|
main_tile = tile_coords
|
|
elif grid_value == 4:
|
|
# Stairs - use the tile_coords (which should be a stairs tile)
|
|
main_tile = tile_coords
|
|
else:
|
|
# Anything else (empty/void) - use black tile (2,2)
|
|
main_tile = BLACK_TILE
|
|
|
|
# If tile_coords is (0,0) and it's not a wall, use black tile instead
|
|
if tile_coords == Vector2i(0, 0) and grid_value != 0:
|
|
main_tile = BLACK_TILE
|
|
|
|
# Always explicitly set a tile for every position (prevents default 0,0)
|
|
dungeon_tilemap_layer.set_cell(Vector2i(x, y), 0, main_tile)
|
|
if main_tile != Vector2i(0, 0): # Count non-default tiles
|
|
tiles_placed += 1
|
|
|
|
# Render above layer
|
|
# Render tile (2,2) for every tile that is NOT floor, room-walls, or doors
|
|
# This includes corridors and any other tiles
|
|
if grid_value == 0:
|
|
# Wall - check if it's actually a wall tile or just empty space
|
|
# Empty spaces have tile_coords == (0,0), actual walls have other tile_coords
|
|
if tile_coords == Vector2i(0, 0):
|
|
# Empty space - render black tile (2,2)
|
|
dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, BLACK_TILE)
|
|
above_tiles_placed += 1
|
|
# else: actual wall - don't render anything in above layer
|
|
elif grid_value == 1:
|
|
# Floor - don't render anything in above layer
|
|
pass
|
|
elif grid_value == 2:
|
|
# Door - render specific door parts
|
|
var current_tile = tile_coords
|
|
|
|
# Check which door type this is based on tile coordinates
|
|
# Door UP: first row (y=0) - tiles (7,0), (8,0), (9,0)
|
|
if current_tile.y == 0 and current_tile.x >= 7 and current_tile.x <= 9:
|
|
dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
|
|
above_tiles_placed += 1
|
|
|
|
# Door RIGHT: second column (x=11) - tiles (11,2), (11,3), (11,4)
|
|
elif current_tile.x == 11 and current_tile.y >= 2 and current_tile.y <= 4:
|
|
dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
|
|
above_tiles_placed += 1
|
|
|
|
# Door LEFT: first column (x=5) - tiles (5,2), (5,3), (5,4)
|
|
elif current_tile.x == 5 and current_tile.y >= 2 and current_tile.y <= 4:
|
|
dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
|
|
above_tiles_placed += 1
|
|
|
|
# Door DOWN: second row (y=6) - tiles (7,6), (8,6), (9,6)
|
|
elif current_tile.y == 6 and current_tile.x >= 7 and current_tile.x <= 9:
|
|
dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
|
|
above_tiles_placed += 1
|
|
elif grid_value == 4: # Stairs - render similar to doors
|
|
var current_tile = tile_coords
|
|
# Render stairs parts similar to doors
|
|
# Stairs use the same structure as doors but with special middle frame tiles
|
|
# Stairs UP: first row (y=0) - tiles (7,0), (10,0), (9,0) - middle tile is (10,0) instead of (8,0)
|
|
if current_tile.y == 0 and current_tile.x >= 7 and current_tile.x <= 9:
|
|
dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
|
|
above_tiles_placed += 1
|
|
# Special case: UP stairs middle tile (10,0)
|
|
elif current_tile == Vector2i(10, 0):
|
|
dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
|
|
above_tiles_placed += 1
|
|
# Stairs RIGHT: columns (x=10 or x=11) - tiles (10,2), (11,1), (10,3), (11,3), (10,4), (11,4)
|
|
# Middle tile is (11,1) instead of (11,3)
|
|
elif (current_tile.x == 10 or current_tile.x == 11) and current_tile.y >= 2 and current_tile.y <= 4:
|
|
dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
|
|
above_tiles_placed += 1
|
|
# Special case: RIGHT stairs middle tile (11,1)
|
|
elif current_tile == Vector2i(11, 1):
|
|
dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
|
|
above_tiles_placed += 1
|
|
# Stairs LEFT: first column (x=5) - tiles (5,2), (5,1), (5,3), (5,4)
|
|
# Middle tile is (5,1) instead of (5,3)
|
|
elif current_tile.x == 5 and current_tile.y >= 1 and current_tile.y <= 4:
|
|
dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
|
|
above_tiles_placed += 1
|
|
# Stairs DOWN: second row (y=5) - tiles (7,5), (6,6), (9,5)
|
|
# Middle tile is (6,6) instead of (8,6)
|
|
elif current_tile.y == 5 and current_tile.x >= 7 and current_tile.x <= 9:
|
|
dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
|
|
above_tiles_placed += 1
|
|
# Special case: DOWN stairs middle tile (6,6)
|
|
elif current_tile == Vector2i(6, 6):
|
|
dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
|
|
above_tiles_placed += 1
|
|
else:
|
|
# Everything else (corridors, empty tiles, etc.) - render black tile (2,2)
|
|
dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, BLACK_TILE)
|
|
above_tiles_placed += 1
|
|
|
|
print("GameWorld: Placed ", tiles_placed, " tiles on main layer")
|
|
print("GameWorld: Placed ", above_tiles_placed, " tiles on above layer")
|
|
print("GameWorld: Dungeon rendered on TileMapLayer")
|
|
|
|
# Create stairs Area2D if stairs data exists
|
|
_create_stairs_area()
|
|
|
|
func _update_spawn_points(target_room: Dictionary = {}, clear_existing: bool = true):
|
|
# Update player manager spawn points based on a room
|
|
# If target_room is empty, use start room (for initial spawn)
|
|
# Otherwise use the provided room (for spawning new players near host)
|
|
# clear_existing: If true, clear existing spawn points first (for respawn/new room)
|
|
if not dungeon_data.has("start_room"):
|
|
return
|
|
|
|
var room = target_room if not target_room.is_empty() else dungeon_data.start_room
|
|
var tile_size = 16 # 16 pixels per tile
|
|
|
|
# Get already assigned spawn positions to exclude them (if not clearing)
|
|
var exclude_positions = []
|
|
if not clear_existing:
|
|
for spawn_point in player_manager.spawn_points:
|
|
exclude_positions.append(spawn_point)
|
|
|
|
# Clear existing spawn points if requested
|
|
if clear_existing:
|
|
player_manager.spawn_points.clear()
|
|
|
|
# Find free floor tiles in the room (excluding already assigned positions)
|
|
var free_tiles = _find_free_floor_tiles_in_room(room, exclude_positions)
|
|
|
|
# Update player manager spawn points
|
|
if free_tiles.size() > 0:
|
|
# Use free floor tiles as spawn points
|
|
for tile_pos in free_tiles:
|
|
var world_x = tile_pos.x * tile_size + tile_size / 2.0 # Center of tile
|
|
var world_y = tile_pos.y * tile_size + tile_size / 2.0 # Center of tile
|
|
player_manager.spawn_points.append(Vector2(world_x, world_y))
|
|
print("GameWorld: Updated spawn points with ", free_tiles.size(), " free floor tiles in room")
|
|
else:
|
|
# Fallback: Create spawn points in a circle around the room center
|
|
var room_center_x = (room.x + room.w / 2.0) * tile_size
|
|
var room_center_y = (room.y + room.h / 2.0) * tile_size
|
|
var num_spawn_points = 8
|
|
for i in range(num_spawn_points):
|
|
var angle = i * PI * 2 / num_spawn_points
|
|
var offset = Vector2(cos(angle), sin(angle)) * 30 # 30 pixel radius
|
|
player_manager.spawn_points.append(Vector2(room_center_x, room_center_y) + offset)
|
|
print("GameWorld: Updated spawn points in circle around room center (no free tiles found)")
|
|
|
|
func _find_room_at_position(world_pos: Vector2) -> Dictionary:
|
|
# Find which room contains the given world position
|
|
if dungeon_data.is_empty() or not dungeon_data.has("rooms"):
|
|
return {}
|
|
|
|
var tile_size = 16 # 16 pixels per tile
|
|
var tile_x = int(world_pos.x / tile_size)
|
|
var tile_y = int(world_pos.y / tile_size)
|
|
|
|
# Check each room to see if the position is inside it
|
|
for room in dungeon_data.rooms:
|
|
# Room interior is from room.x + 2 to room.x + room.w - 2 (excluding 2-tile walls)
|
|
if tile_x >= room.x + 2 and tile_x < room.x + room.w - 2 and \
|
|
tile_y >= room.y + 2 and tile_y < room.y + room.h - 2:
|
|
return room
|
|
|
|
return {}
|
|
|
|
func _find_free_floor_tiles_in_room(room: Dictionary, exclude_positions: Array = []) -> Array:
|
|
# Find all free floor tiles in a room (tiles that are floor and not occupied)
|
|
# exclude_positions: Array of Vector2 world positions to exclude (for players already assigned spawn points)
|
|
if dungeon_data.is_empty() or not dungeon_data.has("grid"):
|
|
return []
|
|
|
|
var free_tiles = []
|
|
var grid = dungeon_data.grid
|
|
var map_size = dungeon_data.map_size
|
|
|
|
# Room interior is from room.x + 2 to room.x + room.w - 2 (excluding 2-tile walls)
|
|
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 < map_size.x and y >= 0 and y < map_size.y:
|
|
# Check if it's a floor tile
|
|
if grid[x][y] == 1: # Floor
|
|
var tile_world_x = x * 16 + 8 # Center of tile
|
|
var tile_world_y = y * 16 + 8 # Center of tile
|
|
var tile_world_pos = Vector2(tile_world_x, tile_world_y)
|
|
|
|
# Check if this position is in the exclude list
|
|
var is_excluded = false
|
|
for excluded_pos in exclude_positions:
|
|
if tile_world_pos.distance_to(excluded_pos) < 32:
|
|
is_excluded = true
|
|
break
|
|
|
|
if is_excluded:
|
|
continue
|
|
|
|
# Check if there's already a player at this position
|
|
var is_free = true
|
|
for player in player_manager.get_all_players():
|
|
if tile_world_pos.distance_to(player.position) < 32:
|
|
is_free = false
|
|
break
|
|
|
|
if is_free:
|
|
free_tiles.append(Vector2i(x, y))
|
|
|
|
# Shuffle free tiles to randomize spawn positions
|
|
free_tiles.shuffle()
|
|
|
|
return free_tiles
|
|
|
|
func _is_safe_spawn_position(world_pos: Vector2) -> bool:
|
|
# Check if a world position is safe for spawning (on a floor tile)
|
|
if dungeon_data.is_empty() or not dungeon_data.has("grid"):
|
|
return false
|
|
|
|
var tile_size = 16
|
|
var tile_x = int(world_pos.x / tile_size)
|
|
var tile_y = int(world_pos.y / tile_size)
|
|
var grid = dungeon_data.grid
|
|
var map_size = dungeon_data.map_size
|
|
|
|
# Check bounds
|
|
if tile_x < 0 or tile_y < 0 or tile_x >= map_size.x or tile_y >= map_size.y:
|
|
return false
|
|
|
|
# Check if it's a floor tile
|
|
if grid[tile_x][tile_y] == 1: # Floor
|
|
return true
|
|
|
|
return false
|
|
|
|
func _find_nearby_safe_spawn_position(world_pos: Vector2, max_distance: float = 64.0) -> Vector2:
|
|
# 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 max_radius = int(max_distance / tile_size) + 1
|
|
|
|
while search_radius <= max_radius:
|
|
# Check all tiles in a square around the position
|
|
var center_tile_x = int(world_pos.x / tile_size)
|
|
var center_tile_y = int(world_pos.y / tile_size)
|
|
|
|
# Check tiles in a square pattern
|
|
for dx in range(-search_radius, search_radius + 1):
|
|
for dy in range(-search_radius, search_radius + 1):
|
|
# Skip if outside the search radius (only check the perimeter)
|
|
if abs(dx) != search_radius and abs(dy) != search_radius:
|
|
continue
|
|
|
|
var check_tile_x = center_tile_x + dx
|
|
var check_tile_y = center_tile_y + dy
|
|
var check_world_pos = Vector2(check_tile_x * tile_size + tile_size / 2.0, check_tile_y * tile_size + tile_size / 2.0)
|
|
|
|
# Check if this position is safe
|
|
if _is_safe_spawn_position(check_world_pos):
|
|
print("GameWorld: Found safe spawn position at ", check_world_pos, " (original was ", world_pos, ")")
|
|
return check_world_pos
|
|
|
|
search_radius += 1
|
|
|
|
# If no safe position found, return original (fallback)
|
|
print("GameWorld: WARNING: Could not find safe spawn position near ", world_pos, ", using original position")
|
|
return world_pos
|
|
|
|
func _get_host_room() -> Dictionary:
|
|
# Get the room where the host (server peer) is currently located
|
|
if not multiplayer.is_server():
|
|
return {}
|
|
|
|
# Find the host's players (server peer is usually peer_id 1)
|
|
var host_peer_id = 1
|
|
var host_players = []
|
|
for player in player_manager.get_all_players():
|
|
if player.peer_id == host_peer_id:
|
|
host_players.append(player)
|
|
|
|
# If no host players found, try to find any server player
|
|
if host_players.is_empty():
|
|
host_players = player_manager.get_all_players()
|
|
|
|
if host_players.is_empty():
|
|
return {}
|
|
|
|
# Use the first host player's position to find the room
|
|
var host_pos = host_players[0].position
|
|
return _find_room_at_position(host_pos)
|
|
|
|
@rpc("authority", "reliable")
|
|
func _sync_dungeon(dungeon_data_sync: Dictionary, seed_value: int, level: int, host_room: Dictionary = {}):
|
|
# Clients receive dungeon data from host
|
|
if not multiplayer.is_server():
|
|
print("GameWorld: Client received dungeon sync for level ", level)
|
|
print("GameWorld: dungeon_data_sync keys: ", dungeon_data_sync.keys())
|
|
if dungeon_data_sync.has("enemies"):
|
|
var enemy_count = dungeon_data_sync.enemies.size() if dungeon_data_sync.enemies is Array else 0
|
|
print("GameWorld: dungeon_data_sync has ", enemy_count, " enemies")
|
|
else:
|
|
print("GameWorld: WARNING: dungeon_data_sync has NO 'enemies' key!")
|
|
|
|
dungeon_data = dungeon_data_sync
|
|
dungeon_seed = seed_value
|
|
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
|
|
_clear_level()
|
|
|
|
# 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
|
|
|
|
# Render dungeon on client
|
|
_render_dungeon()
|
|
|
|
# Spawn torches on client
|
|
_spawn_torches()
|
|
|
|
# Spawn enemies on client
|
|
_spawn_enemies()
|
|
|
|
# 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
|
|
|
|
# Update spawn points - use host's room if available, otherwise use start room
|
|
if not host_room.is_empty():
|
|
print("GameWorld: Using host's room for spawn points")
|
|
_update_spawn_points(host_room)
|
|
# Move any existing players to spawn near host
|
|
_move_players_to_host_room(host_room)
|
|
else:
|
|
print("GameWorld: Host room not available, using start room")
|
|
_update_spawn_points()
|
|
# Move all players to start room
|
|
_move_all_players_to_start_room()
|
|
|
|
# Note: Level number is shown via _sync_show_level_number RPC, not here
|
|
# This prevents duplicate displays and ensures consistent timing
|
|
|
|
# Load HUD on client (same as server does)
|
|
call_deferred("_load_hud")
|
|
|
|
# Sync existing dungeon to newly connected clients
|
|
if multiplayer.is_server():
|
|
# This shouldn't happen, but just in case
|
|
pass
|
|
|
|
func _spawn_torches():
|
|
# Spawn torches from dungeon data
|
|
if dungeon_data.is_empty() or not dungeon_data.has("torches"):
|
|
return
|
|
|
|
var torch_scene = preload("res://scenes/torch_wall.tscn")
|
|
if not torch_scene:
|
|
push_error("ERROR: Could not load torch_wall.tscn!")
|
|
return
|
|
|
|
var entities_node = get_node_or_null("Entities")
|
|
if not entities_node:
|
|
push_error("ERROR: Could not find Entities node!")
|
|
return
|
|
|
|
# Remove existing torches first
|
|
for child in entities_node.get_children():
|
|
if child.name.begins_with("Torch_"):
|
|
child.queue_free()
|
|
|
|
# Spawn torches
|
|
var torches = dungeon_data.torches
|
|
print("GameWorld: Spawning ", torches.size(), " torches")
|
|
|
|
for i in range(torches.size()):
|
|
var torch_data = torches[i]
|
|
var torch = torch_scene.instantiate()
|
|
torch.name = "Torch_%d" % i
|
|
entities_node.add_child(torch)
|
|
torch.global_position = torch_data.position
|
|
torch.rotation_degrees = torch_data.rotation
|
|
|
|
print("GameWorld: Spawned ", torches.size(), " torches")
|
|
|
|
func _spawn_enemies():
|
|
# Spawn enemies from dungeon data
|
|
if dungeon_data.is_empty() or not dungeon_data.has("enemies"):
|
|
return
|
|
|
|
# On server: spawn enemies with full authority
|
|
# On clients: spawn enemies but they'll be authority of server
|
|
var is_server = multiplayer.is_server() or not multiplayer.has_multiplayer_peer()
|
|
|
|
var entities_node = get_node_or_null("Entities")
|
|
if not entities_node:
|
|
push_error("ERROR: Could not find Entities node!")
|
|
return
|
|
|
|
# Remove existing enemies first (except ones spawned by spawners)
|
|
# Also remove dead enemies that might still be in the scene
|
|
# Collect all enemies to remove first, then remove them
|
|
var enemies_to_remove = []
|
|
for child in entities_node.get_children():
|
|
if child.is_in_group("enemy") and child.has_meta("dungeon_spawned"):
|
|
enemies_to_remove.append(child)
|
|
|
|
# Remove all old dungeon enemies
|
|
for enemy in enemies_to_remove:
|
|
print("GameWorld: Removing old dungeon enemy: ", enemy.name)
|
|
if is_instance_valid(enemy):
|
|
enemy.queue_free()
|
|
|
|
# Spawn enemies
|
|
if not dungeon_data.has("enemies"):
|
|
print("GameWorld: WARNING: dungeon_data has no 'enemies' key!")
|
|
return
|
|
|
|
var enemies = dungeon_data.enemies
|
|
if enemies == null or not enemies is Array:
|
|
print("GameWorld: WARNING: dungeon_data.enemies is not an Array! Type: ", typeof(enemies))
|
|
return
|
|
|
|
print("GameWorld: Spawning ", enemies.size(), " enemies (is_server: ", is_server, ")")
|
|
|
|
for i in range(enemies.size()):
|
|
var enemy_data = enemies[i]
|
|
if not enemy_data is Dictionary:
|
|
push_error("ERROR: Enemy data at index ", i, " is not a Dictionary! Type: ", typeof(enemy_data))
|
|
continue
|
|
|
|
if not enemy_data.has("type"):
|
|
push_error("ERROR: Enemy data missing 'type' field: ", enemy_data)
|
|
continue
|
|
|
|
if not enemy_data.has("position"):
|
|
push_error("ERROR: Enemy data missing 'position' field: ", enemy_data)
|
|
continue
|
|
|
|
var enemy_type = enemy_data.type
|
|
if not enemy_type is String:
|
|
push_error("ERROR: Enemy type is not a String! Type: ", typeof(enemy_type), " Value: ", enemy_type)
|
|
continue
|
|
|
|
if not enemy_type.begins_with("res://"):
|
|
# If type is just "enemy_rat", convert to full path
|
|
enemy_type = "res://scenes/" + enemy_type + ".tscn"
|
|
|
|
var enemy_scene = load(enemy_type)
|
|
if not enemy_scene:
|
|
push_error("ERROR: Could not load enemy scene: ", enemy_type)
|
|
continue
|
|
|
|
var enemy = enemy_scene.instantiate()
|
|
# Use consistent naming: Enemy_<index> based on position in enemies array
|
|
# This ensures server and client enemies have the same names
|
|
enemy.name = "Enemy_%d" % i
|
|
enemy.set_meta("dungeon_spawned", true)
|
|
# Store enemy index for identification
|
|
enemy.set_meta("enemy_index", i)
|
|
|
|
# Store room reference for AI (if available)
|
|
if enemy_data.has("room") and enemy_data.room is Dictionary:
|
|
enemy.set_meta("room", enemy_data.room)
|
|
|
|
# Set multiplayer authority BEFORE adding to scene tree (CRITICAL for RPC to work!)
|
|
if multiplayer.has_multiplayer_peer():
|
|
enemy.set_multiplayer_authority(1)
|
|
print("GameWorld: Set enemy authority to 1 BEFORE add_child: ", enemy.name, " authority: ", enemy.get_multiplayer_authority())
|
|
|
|
# Set enemy stats BEFORE adding to scene (so _ready() can use them)
|
|
if "max_health" in enemy_data:
|
|
enemy.max_health = enemy_data.max_health
|
|
enemy.current_health = enemy_data.max_health
|
|
if "move_speed" in enemy_data:
|
|
enemy.move_speed = enemy_data.move_speed
|
|
if "damage" in enemy_data:
|
|
enemy.damage = enemy_data.damage
|
|
|
|
# If it's a humanoid enemy, set the humanoid_type
|
|
if enemy_type.ends_with("enemy_humanoid.tscn") and "humanoid_type" in enemy_data:
|
|
enemy.humanoid_type = enemy_data.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)
|
|
|
|
# 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()
|
|
print("GameWorld: Enemy authority AFTER add_child: ", enemy.name, " authority: ", auth_after, " is_authority: ", enemy.is_multiplayer_authority())
|
|
if auth_after != 1 and is_server:
|
|
push_error("GameWorld: ERROR - Enemy authority lost after add_child! Expected 1, got ", auth_after)
|
|
|
|
# Ensure enemy is fully initialized
|
|
if not enemy.is_inside_tree():
|
|
push_error("GameWorld: ERROR - Enemy not in tree after add_child!")
|
|
|
|
if is_server:
|
|
print("GameWorld: Server spawned enemy: ", enemy.name, " at ", enemy_data.position, " (type: ", enemy_type, ") authority: ", enemy.get_multiplayer_authority(), " in_tree: ", enemy.is_inside_tree(), " is_authority: ", enemy.is_multiplayer_authority(), " index: ", i)
|
|
else:
|
|
print("GameWorld: Client spawned enemy: ", enemy.name, " at ", enemy_data.position, " (type: ", enemy_type, ") authority: ", enemy.get_multiplayer_authority(), " is_authority: ", enemy.is_multiplayer_authority(), " in_tree: ", enemy.is_inside_tree(), " index: ", i)
|
|
|
|
if is_server:
|
|
print("GameWorld: Server spawned ", enemies.size(), " enemies")
|
|
else:
|
|
print("GameWorld: Client spawned ", enemies.size(), " enemies")
|
|
|
|
func _spawn_interactable_objects():
|
|
# Spawn interactable objects from dungeon data
|
|
if dungeon_data.is_empty() or not dungeon_data.has("interactable_objects"):
|
|
return
|
|
|
|
# On server: spawn objects with full authority
|
|
# On clients: spawn objects but they'll be authority of server
|
|
var is_server = multiplayer.is_server() or not multiplayer.has_multiplayer_peer()
|
|
|
|
var entities_node = get_node_or_null("Entities")
|
|
if not entities_node:
|
|
push_error("ERROR: Could not find Entities node!")
|
|
return
|
|
|
|
# Remove existing interactable objects first
|
|
var objects_to_remove = []
|
|
for child in entities_node.get_children():
|
|
if child.is_in_group("interactable_object") and child.has_meta("dungeon_spawned"):
|
|
objects_to_remove.append(child)
|
|
|
|
# Remove all old dungeon objects
|
|
for obj in objects_to_remove:
|
|
print("GameWorld: Removing old dungeon interactable object: ", obj.name)
|
|
if is_instance_valid(obj):
|
|
obj.queue_free()
|
|
|
|
# Spawn objects
|
|
if not dungeon_data.has("interactable_objects"):
|
|
print("GameWorld: WARNING: dungeon_data has no 'interactable_objects' key!")
|
|
return
|
|
|
|
var objects = dungeon_data.interactable_objects
|
|
if objects == null or not objects is Array:
|
|
print("GameWorld: WARNING: dungeon_data.interactable_objects is not an Array! Type: ", typeof(objects))
|
|
return
|
|
|
|
print("GameWorld: Spawning ", objects.size(), " interactable objects (is_server: ", is_server, ")")
|
|
|
|
var interactable_object_scene = load("res://scenes/interactable_object.tscn")
|
|
if not interactable_object_scene:
|
|
push_error("ERROR: Could not load interactable_object scene!")
|
|
return
|
|
|
|
for i in range(objects.size()):
|
|
var object_data = objects[i]
|
|
if not object_data is Dictionary:
|
|
push_error("ERROR: Object data at index ", i, " is not a Dictionary! Type: ", typeof(object_data))
|
|
continue
|
|
|
|
if not object_data.has("type"):
|
|
push_error("ERROR: Object data missing 'type' field: ", object_data)
|
|
continue
|
|
|
|
if not object_data.has("position"):
|
|
push_error("ERROR: Object data missing 'position' field: ", object_data)
|
|
continue
|
|
|
|
if not object_data.has("setup_function"):
|
|
push_error("ERROR: Object data missing 'setup_function' field: ", object_data)
|
|
continue
|
|
|
|
var obj = interactable_object_scene.instantiate()
|
|
# Use consistent naming: InteractableObject_<index>
|
|
obj.name = "InteractableObject_%d" % i
|
|
obj.set_meta("dungeon_spawned", true)
|
|
obj.set_meta("object_index", i)
|
|
|
|
# Store room reference (if available)
|
|
if object_data.has("room") and object_data.room is Dictionary:
|
|
obj.set_meta("room", object_data.room)
|
|
|
|
# Set multiplayer authority BEFORE adding to scene tree
|
|
if multiplayer.has_multiplayer_peer():
|
|
obj.set_multiplayer_authority(1)
|
|
|
|
# Add to scene tree
|
|
entities_node.add_child(obj)
|
|
obj.global_position = object_data.position
|
|
|
|
# Call the setup function to configure the object
|
|
if obj.has_method(object_data.setup_function):
|
|
obj.call(object_data.setup_function)
|
|
else:
|
|
push_error("ERROR: Object does not have method: ", object_data.setup_function)
|
|
|
|
# Add to group for easy access
|
|
obj.add_to_group("interactable_object")
|
|
|
|
print("GameWorld: Spawned ", objects.size(), " interactable objects")
|
|
|
|
func _sync_existing_dungeon_enemies_to_client(client_peer_id: int):
|
|
# Sync existing dungeon-spawned enemies to newly connected client
|
|
# Use dungeon_data.enemies array (like torches) instead of searching scene tree
|
|
if dungeon_data.is_empty() or not dungeon_data.has("enemies"):
|
|
return
|
|
|
|
var enemies = dungeon_data.enemies
|
|
print("GameWorld: Syncing ", enemies.size(), " dungeon-spawned enemies to client ", client_peer_id)
|
|
|
|
# Sync each enemy from dungeon data
|
|
for enemy_data in enemies:
|
|
_sync_dungeon_enemy_spawn.rpc_id(client_peer_id, enemy_data)
|
|
|
|
@rpc("authority", "reliable")
|
|
func _sync_dungeon_enemy_spawn(enemy_data: Dictionary):
|
|
# Client receives dungeon enemy spawn data and spawns it
|
|
print("GameWorld: Client received RPC to spawn dungeon enemy: type=", enemy_data.type, " pos=", enemy_data.position)
|
|
|
|
if not multiplayer.is_server():
|
|
# Convert enemy type to full path if needed (same as _spawn_enemies)
|
|
var enemy_type = enemy_data.type
|
|
if not enemy_type.begins_with("res://"):
|
|
# If type is just "enemy_rat", convert to full path
|
|
enemy_type = "res://scenes/" + enemy_type + ".tscn"
|
|
|
|
var enemy_scene = load(enemy_type)
|
|
if not enemy_scene:
|
|
push_error("ERROR: Could not load enemy scene: ", enemy_type)
|
|
return
|
|
|
|
var entities_node = get_node_or_null("Entities")
|
|
if not entities_node:
|
|
push_error("ERROR: Could not find Entities node!")
|
|
return
|
|
|
|
# Check if enemy already exists (avoid duplicates from _spawn_enemies() + RPC)
|
|
# Also check if enemy is dead or queued for deletion
|
|
for child in entities_node.get_children():
|
|
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
|
|
# 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)
|
|
child.queue_free()
|
|
# Continue to spawn new one
|
|
else:
|
|
print("GameWorld: Enemy already exists at ", enemy_data.position, ", skipping duplicate spawn")
|
|
return
|
|
|
|
# Find the enemy index from the position in the enemies array
|
|
# We need to match the server's enemy index to ensure consistent naming
|
|
var enemy_index = -1
|
|
if dungeon_data.has("enemies") and dungeon_data.enemies is Array:
|
|
for idx in range(dungeon_data.enemies.size()):
|
|
var e_data = dungeon_data.enemies[idx]
|
|
if e_data.has("position") and e_data.position.distance_to(enemy_data.position) < 1.0:
|
|
enemy_index = idx
|
|
break
|
|
|
|
# If we couldn't find the index, use a fallback
|
|
if enemy_index == -1:
|
|
enemy_index = entities_node.get_child_count()
|
|
|
|
var enemy = enemy_scene.instantiate()
|
|
enemy.name = "Enemy_%d" % enemy_index
|
|
enemy.set_meta("dungeon_spawned", true)
|
|
enemy.set_meta("enemy_index", enemy_index)
|
|
|
|
# Store room reference for AI (if available and valid)
|
|
if enemy_data.has("room") and enemy_data.room is Dictionary:
|
|
enemy.set_meta("room", enemy_data.room)
|
|
|
|
# Set multiplayer authority BEFORE adding to scene tree (CRITICAL!)
|
|
if multiplayer.has_multiplayer_peer():
|
|
enemy.set_multiplayer_authority(1)
|
|
print("GameWorld: Set enemy authority to 1 BEFORE add_child (RPC): ", enemy.name)
|
|
|
|
# Set enemy stats BEFORE adding to scene
|
|
if "max_health" in enemy_data:
|
|
enemy.max_health = enemy_data.max_health
|
|
|
|
# If it's a humanoid enemy, set the humanoid_type
|
|
if enemy_type.ends_with("enemy_humanoid.tscn") and "humanoid_type" in enemy_data:
|
|
enemy.humanoid_type = enemy_data.humanoid_type
|
|
if "move_speed" in enemy_data:
|
|
enemy.move_speed = enemy_data.move_speed
|
|
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()
|
|
print("GameWorld: Enemy authority AFTER add_child (RPC): ", enemy.name, " authority: ", auth_after, " is_authority: ", enemy.is_multiplayer_authority())
|
|
if auth_after != 1:
|
|
push_error("GameWorld: ERROR - Enemy authority lost after add_child in RPC! Expected 1, got ", auth_after)
|
|
|
|
print("GameWorld: Client spawned dungeon enemy via RPC: ", enemy.name, " at ", enemy_data.position, " (type: ", enemy_type, ") authority: ", enemy.get_multiplayer_authority())
|
|
|
|
func _sync_existing_chest_states_to_client(client_peer_id: int):
|
|
# Sync chest open states to new client
|
|
var entities_node = get_node_or_null("Entities")
|
|
if not entities_node:
|
|
return
|
|
|
|
var opened_chest_count = 0
|
|
for child in entities_node.get_children():
|
|
if child.is_in_group("interactable_object"):
|
|
if "object_type" in child and child.object_type == "Chest":
|
|
if "is_chest_opened" in child and child.is_chest_opened:
|
|
# Found an opened chest - sync it to the client
|
|
var obj_name = child.name
|
|
_sync_chest_state.rpc_id(client_peer_id, obj_name, true)
|
|
opened_chest_count += 1
|
|
print("GameWorld: Syncing opened chest ", obj_name, " to client ", client_peer_id)
|
|
|
|
print("GameWorld: Synced ", opened_chest_count, " opened chests to client ", client_peer_id)
|
|
|
|
@rpc("authority", "reliable")
|
|
func _sync_chest_state(obj_name: String, is_opened: bool):
|
|
# Client receives chest state sync
|
|
if not multiplayer.is_server():
|
|
var entities_node = get_node_or_null("Entities")
|
|
if entities_node:
|
|
var chest = entities_node.get_node_or_null(obj_name)
|
|
if chest and "is_chest_opened" in chest:
|
|
chest.is_chest_opened = is_opened
|
|
if chest.has_method("_sync_chest_open"):
|
|
# Call the sync function to update visuals (loot type doesn't matter for visual sync)
|
|
chest._sync_chest_open("coin")
|
|
elif "sprite" in chest and "chest_opened_frame" in chest:
|
|
if chest.sprite and chest.chest_opened_frame >= 0:
|
|
chest.sprite.frame = chest.chest_opened_frame
|
|
print("GameWorld: Client received chest state sync for ", obj_name, " - opened: ", is_opened)
|
|
|
|
func _sync_existing_torches_to_client(client_peer_id: int):
|
|
# Sync existing torches to newly connected client
|
|
if dungeon_data.is_empty() or not dungeon_data.has("torches"):
|
|
return
|
|
|
|
var torches = dungeon_data.torches
|
|
print("GameWorld: Syncing ", torches.size(), " torches to client ", client_peer_id)
|
|
|
|
for torch_data in torches:
|
|
_sync_torch_spawn.rpc_id(client_peer_id, torch_data.position, torch_data.rotation)
|
|
|
|
@rpc("authority", "reliable")
|
|
func _sync_torch_spawn(torch_position: Vector2, torch_rotation: float):
|
|
# Clients spawn torch when server tells them to
|
|
if not multiplayer.is_server():
|
|
var torch_scene = preload("res://scenes/torch_wall.tscn")
|
|
if not torch_scene:
|
|
return
|
|
|
|
var entities_node = get_node_or_null("Entities")
|
|
if entities_node:
|
|
var torch = torch_scene.instantiate()
|
|
torch.name = "Torch_%d" % entities_node.get_child_count()
|
|
entities_node.add_child(torch)
|
|
torch.global_position = torch_position
|
|
torch.rotation_degrees = torch_rotation
|
|
print("Client spawned torch at ", torch_position, " with rotation ", torch_rotation)
|
|
|
|
func _clear_level():
|
|
# Clear previous level data
|
|
print("GameWorld: Clearing previous level...")
|
|
|
|
# Clear tilemap layers - ensure we get references from scene if they exist
|
|
var environment = get_node_or_null("Environment")
|
|
if environment:
|
|
var layer0 = environment.get_node_or_null("DungeonLayer0")
|
|
if layer0:
|
|
layer0.clear()
|
|
var layer_above = environment.get_node_or_null("TileMapLayerAbove")
|
|
if layer_above:
|
|
layer_above.clear()
|
|
|
|
# Also clear via stored references if they exist
|
|
if dungeon_tilemap_layer:
|
|
dungeon_tilemap_layer.clear()
|
|
if dungeon_tilemap_layer_above:
|
|
dungeon_tilemap_layer_above.clear()
|
|
|
|
# Remove all entities EXCEPT players (enemies, torches, loot, etc.)
|
|
var entities_node = get_node_or_null("Entities")
|
|
if entities_node:
|
|
var entities_to_remove = []
|
|
for child in entities_node.get_children():
|
|
# Don't free players - they persist across levels
|
|
if not child.is_in_group("player"):
|
|
entities_to_remove.append(child)
|
|
|
|
# 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
|
|
|
|
# Remove stairs area
|
|
var stairs_area = get_node_or_null("StairsArea")
|
|
if stairs_area:
|
|
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
|
|
|
|
print("GameWorld: Previous level cleared")
|
|
|
|
func _move_all_players_to_start_room():
|
|
# Move all players to the start room of the new level
|
|
if dungeon_data.is_empty() or not dungeon_data.has("start_room"):
|
|
return
|
|
|
|
var start_room = dungeon_data.start_room
|
|
_update_spawn_points(start_room)
|
|
|
|
# Move all players to spawn points
|
|
var players = get_tree().get_nodes_in_group("player")
|
|
var spawn_index = 0
|
|
for player in players:
|
|
if spawn_index < player_manager.spawn_points.size():
|
|
var new_pos = player_manager.spawn_points[spawn_index]
|
|
player.global_position = new_pos
|
|
print("GameWorld: Moved player ", player.name, " to start room at ", new_pos)
|
|
spawn_index += 1
|
|
else:
|
|
# Fallback: place in center of start room
|
|
var room_center_x = (start_room.x + start_room.w / 2.0) * 16
|
|
var room_center_y = (start_room.y + start_room.h / 2.0) * 16
|
|
player.global_position = Vector2(room_center_x, room_center_y)
|
|
print("GameWorld: Moved player ", player.name, " to start room center at ", player.global_position)
|
|
|
|
func _create_stairs_area():
|
|
# Remove existing stairs area if any
|
|
var existing_stairs = get_node_or_null("StairsArea")
|
|
if existing_stairs:
|
|
existing_stairs.queue_free()
|
|
|
|
# Check if stairs data exists
|
|
if dungeon_data.is_empty() or not dungeon_data.has("stairs") or dungeon_data.stairs.is_empty():
|
|
return
|
|
|
|
var stairs_data = dungeon_data.stairs
|
|
if not stairs_data.has("world_pos") or not stairs_data.has("world_size"):
|
|
return
|
|
|
|
# Create stairs Area2D programmatically
|
|
var stairs_area = Area2D.new()
|
|
stairs_area.name = "StairsArea"
|
|
|
|
# Set collision layer/mask BEFORE adding to scene
|
|
stairs_area.collision_layer = 0
|
|
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")
|
|
if stairs_script:
|
|
stairs_area.set_script(stairs_script)
|
|
|
|
# Add collision shape
|
|
var collision_shape = CollisionShape2D.new()
|
|
var rect_shape = RectangleShape2D.new()
|
|
rect_shape.size = stairs_data.world_size
|
|
collision_shape.shape = rect_shape
|
|
stairs_area.add_child(collision_shape)
|
|
|
|
# Set position
|
|
stairs_area.global_position = stairs_data.world_pos
|
|
|
|
# Add to scene AFTER everything is set up
|
|
add_child(stairs_area)
|
|
|
|
print("GameWorld: Created stairs Area2D at ", stairs_data.world_pos, " with size ", stairs_data.world_size)
|
|
|
|
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
|
|
|
|
# Prevent multiple triggers - if already triggered, ignore
|
|
if level_complete_triggered:
|
|
print("GameWorld: Level complete already triggered, ignoring duplicate trigger")
|
|
return
|
|
|
|
print("GameWorld: Player ", player.name, " reached stairs!")
|
|
|
|
# Mark as triggered to prevent re-triggering
|
|
level_complete_triggered = true
|
|
|
|
# Disable controls and collision for the player who reached stairs
|
|
var player_peer_id = player.get_multiplayer_authority() if player.has_method("get_multiplayer_authority") else 0
|
|
player.controls_disabled = true
|
|
# Remove collision layer (layer 1 = players)
|
|
player.set_collision_layer_value(1, false)
|
|
print("GameWorld: Disabled controls and collision for player ", player.name)
|
|
|
|
# Show black fade overlay for server's local player if this is the server's player
|
|
var my_peer_id = multiplayer.get_unique_id() if multiplayer.has_multiplayer_peer() else 1
|
|
if player_peer_id == my_peer_id:
|
|
_show_black_fade_overlay()
|
|
|
|
# Sync controls disabled and collision removal to clients
|
|
if multiplayer.has_multiplayer_peer() and player_peer_id > 0:
|
|
_sync_player_exit_stairs.rpc(player_peer_id)
|
|
|
|
# Drop any held objects for all players before level completion
|
|
var entities_node = get_node_or_null("Entities")
|
|
if entities_node:
|
|
for child in entities_node.get_children():
|
|
if child.is_in_group("player") and child.has_method("_force_drop_held_object"):
|
|
child._force_drop_held_object()
|
|
|
|
# Stop HUD timer when level completes
|
|
var hud = get_node_or_null("IngameHUD")
|
|
var level_time: float = 0.0
|
|
if hud and hud.has_method("stop_timer"):
|
|
hud.stop_timer()
|
|
# Get the level time before stopping
|
|
if hud.has_method("get_level_time"):
|
|
level_time = hud.get_level_time()
|
|
|
|
# Fade out player
|
|
_fade_out_player(player)
|
|
|
|
# Show level complete UI (server and clients) with per-player stats
|
|
_show_level_complete_ui(level_time)
|
|
# Sync to all clients (each client will show their own local player's stats)
|
|
if multiplayer.has_multiplayer_peer():
|
|
_sync_show_level_complete.rpc(level_time)
|
|
|
|
# After delay, hide UI and generate new level
|
|
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")
|
|
if level_complete_ui:
|
|
level_complete_ui.visible = false
|
|
# Sync hide to all clients
|
|
if multiplayer.has_multiplayer_peer():
|
|
_sync_hide_level_complete.rpc()
|
|
|
|
# Clear previous level
|
|
_clear_level()
|
|
|
|
# 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
|
|
|
|
# Generate next level
|
|
current_level += 1
|
|
print("GameWorld: Incremented to level ", current_level, " (was level ", current_level - 1, ")")
|
|
|
|
# Generate the new dungeon (this is async but we don't await it - it will complete in background)
|
|
_generate_dungeon()
|
|
|
|
# Wait for dungeon generation to complete (it has await statements inside)
|
|
# 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
|
|
|
|
# Verify current_level is still correct
|
|
print("GameWorld: After dungeon generation, current_level = ", current_level)
|
|
|
|
# Show level number (server and clients) - use the incremented level
|
|
print("GameWorld: Showing level number for level ", current_level)
|
|
_show_level_number()
|
|
# Sync to all clients
|
|
if multiplayer.has_multiplayer_peer():
|
|
print("GameWorld: Syncing level number ", current_level, " to all clients")
|
|
_sync_show_level_number.rpc(current_level)
|
|
|
|
# Restart HUD timer for new level
|
|
hud = get_node_or_null("IngameHUD")
|
|
if hud and hud.has_method("start_timer"):
|
|
hud.start_timer()
|
|
|
|
# Restore controls and collision for all players (server side)
|
|
_restore_player_controls_and_collision()
|
|
|
|
# Sync restore to all clients
|
|
if multiplayer.has_multiplayer_peer():
|
|
_sync_restore_player_controls.rpc()
|
|
|
|
# Remove black fade overlay (server and clients)
|
|
_remove_black_fade_overlay()
|
|
if multiplayer.has_multiplayer_peer():
|
|
_sync_remove_black_fade.rpc()
|
|
|
|
# Move all players to start room (server side)
|
|
_move_all_players_to_start_room()
|
|
|
|
# Fade players back in
|
|
_fade_in_all_players()
|
|
|
|
# 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
|
|
|
|
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 {}
|
|
|
|
# Debug: Verify enemies are in dungeon_data before syncing
|
|
if dungeon_data.has("enemies"):
|
|
print("GameWorld: Server about to sync new level with ", dungeon_data.enemies.size(), " enemies to all clients")
|
|
else:
|
|
print("GameWorld: ERROR: Server dungeon_data has NO 'enemies' key when syncing new level!")
|
|
|
|
_sync_dungeon.rpc(dungeon_data, dungeon_seed, current_level, start_room)
|
|
|
|
func _get_local_player_stats() -> Dictionary:
|
|
# Get stats for the local player (for level complete screen)
|
|
# Returns a dictionary with: enemies_defeated, coins_collected
|
|
var stats = {
|
|
"enemies_defeated": 0,
|
|
"coins_collected": 0
|
|
}
|
|
|
|
# Find local player(s) - in multiplayer, find player with authority matching this client
|
|
# In single-player, just use the first player
|
|
var players = get_tree().get_nodes_in_group("player")
|
|
var local_player = null
|
|
|
|
if multiplayer.has_multiplayer_peer():
|
|
# Multiplayer: find player with matching authority (client's own player)
|
|
var my_peer_id = multiplayer.get_unique_id()
|
|
for player in players:
|
|
if player.has_method("get_multiplayer_authority") and player.get_multiplayer_authority() == my_peer_id:
|
|
local_player = player
|
|
break
|
|
else:
|
|
# Single-player: use first player
|
|
if players.size() > 0:
|
|
local_player = players[0]
|
|
|
|
if local_player and local_player.character_stats:
|
|
# Get enemies defeated (kills)
|
|
if "kills" in local_player.character_stats:
|
|
stats.enemies_defeated = local_player.character_stats.kills
|
|
|
|
# Get coins collected
|
|
if "coin" in local_player.character_stats:
|
|
stats.coins_collected = local_player.character_stats.coin
|
|
|
|
return stats
|
|
|
|
func _fade_out_player(player: Node):
|
|
# Fade out all sprite layers
|
|
var fade_tween = create_tween()
|
|
fade_tween.set_parallel(true)
|
|
|
|
var sprite_layers = []
|
|
if "sprite_body" in player and player.sprite_body:
|
|
sprite_layers.append(player.sprite_body)
|
|
if "sprite_boots" in player and player.sprite_boots:
|
|
sprite_layers.append(player.sprite_boots)
|
|
if "sprite_armour" in player and player.sprite_armour:
|
|
sprite_layers.append(player.sprite_armour)
|
|
if "sprite_facial_hair" in player and player.sprite_facial_hair:
|
|
sprite_layers.append(player.sprite_facial_hair)
|
|
if "sprite_hair" in player and player.sprite_hair:
|
|
sprite_layers.append(player.sprite_hair)
|
|
if "sprite_eyes" in player and player.sprite_eyes:
|
|
sprite_layers.append(player.sprite_eyes)
|
|
if "sprite_eyelashes" in player and player.sprite_eyelashes:
|
|
sprite_layers.append(player.sprite_eyelashes)
|
|
if "sprite_addons" in player and player.sprite_addons:
|
|
sprite_layers.append(player.sprite_addons)
|
|
if "sprite_headgear" in player and player.sprite_headgear:
|
|
sprite_layers.append(player.sprite_headgear)
|
|
if "sprite_weapon" in player and player.sprite_weapon:
|
|
sprite_layers.append(player.sprite_weapon)
|
|
if "shadow" in player and player.shadow:
|
|
sprite_layers.append(player.shadow)
|
|
|
|
for sprite_layer in sprite_layers:
|
|
if sprite_layer:
|
|
fade_tween.tween_property(sprite_layer, "modulate:a", 0.0, 1.0)
|
|
|
|
func _fade_in_all_players():
|
|
# Fade in all players after level transition
|
|
var players = get_tree().get_nodes_in_group("player")
|
|
for player in players:
|
|
_fade_in_player(player)
|
|
|
|
func _fade_in_player(player: Node):
|
|
# Fade in all sprite layers
|
|
var fade_tween = create_tween()
|
|
fade_tween.set_parallel(true)
|
|
|
|
var sprite_layers = []
|
|
if "sprite_body" in player and player.sprite_body:
|
|
sprite_layers.append(player.sprite_body)
|
|
if "sprite_boots" in player and player.sprite_boots:
|
|
sprite_layers.append(player.sprite_boots)
|
|
if "sprite_armour" in player and player.sprite_armour:
|
|
sprite_layers.append(player.sprite_armour)
|
|
if "sprite_facial_hair" in player and player.sprite_facial_hair:
|
|
sprite_layers.append(player.sprite_facial_hair)
|
|
if "sprite_hair" in player and player.sprite_hair:
|
|
sprite_layers.append(player.sprite_hair)
|
|
if "sprite_eyes" in player and player.sprite_eyes:
|
|
sprite_layers.append(player.sprite_eyes)
|
|
if "sprite_eyelashes" in player and player.sprite_eyelashes:
|
|
sprite_layers.append(player.sprite_eyelashes)
|
|
if "sprite_addons" in player and player.sprite_addons:
|
|
sprite_layers.append(player.sprite_addons)
|
|
if "sprite_headgear" in player and player.sprite_headgear:
|
|
sprite_layers.append(player.sprite_headgear)
|
|
if "sprite_weapon" in player and player.sprite_weapon:
|
|
sprite_layers.append(player.sprite_weapon)
|
|
if "shadow" in player and player.shadow:
|
|
sprite_layers.append(player.shadow)
|
|
|
|
for sprite_layer in sprite_layers:
|
|
if sprite_layer:
|
|
sprite_layer.modulate.a = 0.0 # Start invisible
|
|
fade_tween.tween_property(sprite_layer, "modulate:a", 1.0, 1.0)
|
|
|
|
func _show_black_fade_overlay():
|
|
# Create black fade overlay for player who reached exit
|
|
# Remove existing fade if any
|
|
var existing_fade = get_node_or_null("BlackFadeOverlay")
|
|
if existing_fade:
|
|
existing_fade.queue_free()
|
|
|
|
# Create CanvasLayer with z_index 999 (below level complete UI which is 1000)
|
|
var fade_layer = CanvasLayer.new()
|
|
fade_layer.name = "BlackFadeOverlay"
|
|
fade_layer.layer = 999 # Below level complete UI (1000) but above gameplay
|
|
add_child(fade_layer)
|
|
|
|
# Create ColorRect that fills the screen
|
|
var fade_rect = ColorRect.new()
|
|
fade_rect.name = "FadeRect"
|
|
fade_rect.color = Color(0, 0, 0, 1) # Black, fully opaque
|
|
fade_rect.set_anchors_preset(Control.PRESET_FULL_RECT) # Fill entire screen
|
|
fade_layer.add_child(fade_rect)
|
|
|
|
# Fade in from transparent to black
|
|
fade_rect.modulate.a = 0.0 # Start transparent
|
|
var fade_tween = create_tween()
|
|
fade_tween.tween_property(fade_rect, "modulate:a", 1.0, 0.5) # Fade in over 0.5 seconds
|
|
print("GameWorld: Created black fade overlay for player who reached exit")
|
|
|
|
func _remove_black_fade_overlay():
|
|
# Remove black fade overlay when new level starts
|
|
var existing_fade = get_node_or_null("BlackFadeOverlay")
|
|
if existing_fade:
|
|
# Fade out quickly before removing
|
|
var fade_rect = existing_fade.get_node_or_null("FadeRect")
|
|
if fade_rect:
|
|
var fade_tween = create_tween()
|
|
fade_tween.tween_property(fade_rect, "modulate:a", 0.0, 0.2) # Fade out over 0.2 seconds
|
|
await fade_tween.finished
|
|
existing_fade.queue_free()
|
|
print("GameWorld: Removed black fade overlay")
|
|
|
|
func _restore_player_controls_and_collision():
|
|
# Restore controls and collision for all players when new level starts
|
|
var players = get_tree().get_nodes_in_group("player")
|
|
for player in players:
|
|
player.controls_disabled = false
|
|
# Restore collision layer (layer 1 = players)
|
|
player.set_collision_layer_value(1, true)
|
|
print("GameWorld: Restored controls and collision for player ", player.name)
|
|
|
|
func _show_level_complete_ui(level_time: float = 0.0):
|
|
# Create or show level complete UI
|
|
var level_complete_ui = get_node_or_null("LevelCompleteUI")
|
|
if not 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:
|
|
# Scene file doesn't exist - create UI programmatically (expected behavior)
|
|
level_complete_ui = _create_level_complete_ui_programmatically()
|
|
|
|
if level_complete_ui:
|
|
if level_complete_ui.has_method("show_stats"):
|
|
# Get stats for local player
|
|
var local_stats = _get_local_player_stats()
|
|
# Use current_level directly (matches what HUD shows)
|
|
# current_level hasn't been incremented yet when this is called
|
|
level_complete_ui.show_stats(
|
|
local_stats.enemies_defeated,
|
|
0, # times_downed - not shown per user request
|
|
0.0, # exp_collected - not shown per user request
|
|
local_stats.coins_collected,
|
|
level_time,
|
|
current_level
|
|
)
|
|
|
|
func _show_level_number():
|
|
# Show level number text
|
|
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 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:
|
|
# Scene file doesn't exist - create UI programmatically (expected behavior)
|
|
level_text_ui = _create_level_text_ui_programmatically()
|
|
|
|
if level_text_ui:
|
|
if level_text_ui.has_method("show_level"):
|
|
# Store the level number in a local variable to ensure we use the correct value
|
|
var level_to_show = current_level
|
|
print("GameWorld: Calling show_level(", level_to_show, ") on LevelTextUI (current_level = ", current_level, ")")
|
|
# Make sure we pass the current level value explicitly
|
|
level_text_ui.show_level(level_to_show)
|
|
else:
|
|
print("GameWorld: ERROR - LevelTextUI does not have show_level method!")
|
|
else:
|
|
print("GameWorld: ERROR - Could not create or find LevelTextUI!")
|
|
|
|
func _load_hud():
|
|
# Check if HUD already exists - only load it once
|
|
var existing_hud = get_node_or_null("IngameHUD")
|
|
if existing_hud and is_instance_valid(existing_hud):
|
|
print("GameWorld: HUD already exists, skipping load (will just reset timer)")
|
|
# Reset timer for new level if method exists
|
|
if existing_hud.has_method("reset_level_timer"):
|
|
existing_hud.reset_level_timer()
|
|
return
|
|
|
|
# Load HUD dynamically to avoid scene loading errors
|
|
# This is optional - don't crash if HUD scene doesn't exist or fails to load
|
|
# Use a try-catch-like approach by checking for errors
|
|
var hud_scene_path = "res://scenes/ingame_hud.tscn"
|
|
|
|
# Check if scene exists
|
|
if not ResourceLoader.exists(hud_scene_path):
|
|
print("GameWorld: HUD scene not found at ", hud_scene_path, " - HUD disabled")
|
|
return
|
|
|
|
# Try to load the scene
|
|
var hud_scene = load(hud_scene_path)
|
|
if not hud_scene:
|
|
print("GameWorld: Warning - Failed to load HUD scene from ", hud_scene_path)
|
|
return
|
|
|
|
# Try to instantiate
|
|
var hud = null
|
|
if hud_scene.has_method("instantiate"):
|
|
hud = hud_scene.instantiate()
|
|
else:
|
|
print("GameWorld: Warning - HUD scene is not a PackedScene")
|
|
return
|
|
|
|
if not hud:
|
|
print("GameWorld: Warning - Failed to instantiate HUD scene")
|
|
return
|
|
|
|
# Add to scene tree
|
|
hud.name = "IngameHUD"
|
|
|
|
# Ensure HUD is visible and on top layer
|
|
hud.visible = true
|
|
hud.layer = 100 # High layer to ensure HUD is on top of everything
|
|
|
|
add_child(hud)
|
|
|
|
# Reset timer if method exists
|
|
if hud.has_method("reset_level_timer"):
|
|
hud.reset_level_timer()
|
|
|
|
print("GameWorld: HUD loaded successfully and added to scene tree")
|
|
print("GameWorld: HUD visible: ", hud.visible, ", layer: ", hud.layer)
|
|
|
|
func _initialize_hud():
|
|
# Find or get the HUD and reset its level timer
|
|
# This is optional - don't crash if HUD doesn't exist
|
|
var hud = get_node_or_null("IngameHUD")
|
|
if hud and is_instance_valid(hud) and hud.has_method("reset_level_timer"):
|
|
hud.reset_level_timer()
|
|
else:
|
|
print("GameWorld: HUD not found or not ready - this is OK if HUD scene is missing")
|
|
|
|
func _create_chat_ui():
|
|
# Load chat UI scene
|
|
var chat_ui_scene = load("res://scenes/chat_ui.tscn")
|
|
if not chat_ui_scene:
|
|
push_error("GameWorld: Could not load chat_ui.tscn scene!")
|
|
return
|
|
|
|
var chat_ui = chat_ui_scene.instantiate()
|
|
if chat_ui:
|
|
add_child(chat_ui)
|
|
print("GameWorld: Chat UI scene instantiated and added to scene tree")
|
|
else:
|
|
push_error("GameWorld: Failed to instantiate chat_ui.tscn!")
|
|
|
|
func _send_player_join_message(peer_id: int, player_info: Dictionary):
|
|
# Send a chat message when a player joins
|
|
# Only send from server to avoid duplicate messages
|
|
if not multiplayer.is_server():
|
|
return
|
|
|
|
# Get player name (use first player name from the player_info)
|
|
var player_names = player_info.get("player_names", [])
|
|
var player_name = ""
|
|
if player_names.size() > 0:
|
|
player_name = player_names[0]
|
|
else:
|
|
player_name = "Player%d" % peer_id
|
|
|
|
# Get chat UI
|
|
var chat_ui = get_node_or_null("ChatUI")
|
|
if chat_ui and chat_ui.has_method("send_system_message"):
|
|
var message = "%s joined the game" % player_name
|
|
chat_ui.send_system_message(message)
|
|
|
|
func _send_player_disconnect_message(peer_id: int, player_info: Dictionary):
|
|
# Send a chat message when a player disconnects
|
|
# Only send from server to avoid duplicate messages
|
|
if not multiplayer.is_server():
|
|
return
|
|
|
|
# Get player name from player_info
|
|
var player_name = ""
|
|
var player_names = player_info.get("player_names", [])
|
|
if player_names.size() > 0:
|
|
player_name = player_names[0]
|
|
else:
|
|
player_name = "Player%d" % peer_id
|
|
|
|
# Get chat UI
|
|
var chat_ui = get_node_or_null("ChatUI")
|
|
if chat_ui and chat_ui.has_method("send_system_message"):
|
|
var message = "%s left/disconnected" % player_name
|
|
chat_ui.send_system_message(message)
|
|
|
|
func _create_level_complete_ui_programmatically() -> Node:
|
|
# Create level complete UI programmatically
|
|
var canvas_layer = CanvasLayer.new()
|
|
canvas_layer.name = "LevelCompleteUI"
|
|
canvas_layer.layer = 1000 # Very high z_index so it appears above black fade
|
|
add_child(canvas_layer)
|
|
|
|
# Load standard font (as FontFile)
|
|
var standard_font = load("res://assets/fonts/standard_font.png") as FontFile
|
|
if not standard_font:
|
|
print("GameWorld: Warning - Could not load standard_font.png as FontFile")
|
|
|
|
# Create theme with standard font
|
|
var theme = Theme.new()
|
|
if standard_font:
|
|
theme.default_font = standard_font
|
|
theme.default_font_size = 10
|
|
|
|
var vbox = VBoxContainer.new()
|
|
vbox.theme = theme
|
|
canvas_layer.add_child(vbox)
|
|
|
|
# Center the VBoxContainer properly
|
|
# Use PRESET_CENTER to anchor at center, then set offsets to center horizontally
|
|
var screen_size = get_viewport().get_visible_rect().size
|
|
vbox.set_anchors_preset(Control.PRESET_CENTER)
|
|
# Set offsets so container is centered horizontally (equal left/right offsets)
|
|
# and positioned vertically (offset from center)
|
|
vbox.offset_left = - screen_size.x / 2
|
|
vbox.offset_right = screen_size.x / 2
|
|
vbox.offset_top = -200 # Position a bit up from center
|
|
vbox.offset_bottom = screen_size.y / 2 - 200 # Balance to maintain vertical position
|
|
vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
|
|
|
|
# Title - "LEVEL COMPLETE!" in large size
|
|
var title = Label.new()
|
|
title.name = "TitleLabel"
|
|
title.text = "LEVEL COMPLETE!"
|
|
title.theme = theme
|
|
title.add_theme_font_size_override("font_size", 72) # Large size
|
|
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
|
|
vbox.add_child(title)
|
|
|
|
# Stats header - "stats" in smaller size
|
|
var stats_header = Label.new()
|
|
stats_header.name = "StatsHeaderLabel"
|
|
stats_header.text = "stats"
|
|
stats_header.theme = theme
|
|
stats_header.add_theme_font_size_override("font_size", 32) # Smaller than title
|
|
stats_header.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
stats_header.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
|
|
vbox.add_child(stats_header)
|
|
|
|
# Stats container
|
|
var stats_container = VBoxContainer.new()
|
|
stats_container.theme = theme
|
|
stats_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
|
|
vbox.add_child(stats_container)
|
|
|
|
# Stats labels - enemies defeated and level time
|
|
var enemies_label = Label.new()
|
|
enemies_label.name = "EnemiesLabel"
|
|
enemies_label.theme = theme
|
|
enemies_label.add_theme_font_size_override("font_size", 24)
|
|
enemies_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
enemies_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
|
|
stats_container.add_child(enemies_label)
|
|
|
|
var time_label = Label.new()
|
|
time_label.name = "TimeLabel"
|
|
time_label.theme = theme
|
|
time_label.add_theme_font_size_override("font_size", 24)
|
|
time_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
time_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
|
|
stats_container.add_child(time_label)
|
|
|
|
var downed_label = Label.new()
|
|
downed_label.name = "DownedLabel"
|
|
downed_label.theme = theme
|
|
downed_label.add_theme_font_size_override("font_size", 24)
|
|
downed_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
downed_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
|
|
stats_container.add_child(downed_label)
|
|
|
|
var exp_label = Label.new()
|
|
exp_label.name = "ExpLabel"
|
|
exp_label.theme = theme
|
|
exp_label.add_theme_font_size_override("font_size", 24)
|
|
exp_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
exp_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
|
|
stats_container.add_child(exp_label)
|
|
|
|
var coins_label = Label.new()
|
|
coins_label.name = "CoinsLabel"
|
|
coins_label.theme = theme
|
|
coins_label.add_theme_font_size_override("font_size", 24)
|
|
coins_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
coins_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
|
|
stats_container.add_child(coins_label)
|
|
|
|
# Add script
|
|
var script = load("res://scripts/level_complete_ui.gd")
|
|
if script:
|
|
canvas_layer.set_script(script)
|
|
|
|
return canvas_layer
|
|
|
|
func _create_level_text_ui_programmatically() -> Node:
|
|
# Create level text UI programmatically
|
|
var canvas_layer = CanvasLayer.new()
|
|
canvas_layer.name = "LevelTextUI"
|
|
add_child(canvas_layer)
|
|
|
|
var vbox = VBoxContainer.new()
|
|
vbox.set_anchors_preset(Control.PRESET_CENTER)
|
|
|
|
# Center horizontally and position higher up
|
|
var screen_size = get_viewport().get_visible_rect().size
|
|
vbox.offset_left = -screen_size.x / 2
|
|
vbox.offset_right = screen_size.x / 2
|
|
vbox.offset_top = -250 # Position higher up from center
|
|
vbox.offset_bottom = screen_size.y / 2 - 250
|
|
vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill width for proper centering
|
|
|
|
canvas_layer.add_child(vbox)
|
|
|
|
# Level label
|
|
var level_label = Label.new()
|
|
level_label.name = "LevelLabel"
|
|
level_label.text = "LEVEL 1"
|
|
level_label.add_theme_font_size_override("font_size", 64)
|
|
level_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
level_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to center properly
|
|
|
|
# Load standard_font.png as bitmap font
|
|
var standard_font_resource = null
|
|
if ResourceLoader.exists("res://assets/fonts/standard_font.png"):
|
|
standard_font_resource = load("res://assets/fonts/standard_font.png")
|
|
if standard_font_resource:
|
|
level_label.add_theme_font_override("font", standard_font_resource)
|
|
|
|
vbox.add_child(level_label)
|
|
|
|
# Add script
|
|
var script = load("res://scripts/level_text_ui.gd")
|
|
if script:
|
|
canvas_layer.set_script(script)
|
|
|
|
return canvas_layer
|
|
|
|
func _move_players_to_host_room(host_room: Dictionary):
|
|
# Move any existing players to spawn points in the host's room
|
|
if host_room.is_empty() or player_manager.spawn_points.is_empty():
|
|
return
|
|
|
|
# Get all players that belong to this client (local players)
|
|
var my_peer_id = multiplayer.get_unique_id()
|
|
var players_to_move = []
|
|
for player in player_manager.get_all_players():
|
|
# Only move players that belong to this client (local players)
|
|
if player.peer_id == my_peer_id:
|
|
players_to_move.append(player)
|
|
|
|
if players_to_move.is_empty():
|
|
return
|
|
|
|
print("GameWorld: Moving ", players_to_move.size(), " local players to host room")
|
|
|
|
# Move each player to a free spawn point
|
|
var spawn_index = 0
|
|
for player in players_to_move:
|
|
if spawn_index < player_manager.spawn_points.size():
|
|
var new_pos = player_manager.spawn_points[spawn_index]
|
|
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 = load("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")
|
|
|
|
# Track pillar placement per room to avoid duplicates
|
|
var rooms_with_pillars: Dictionary = {} # Key: room string "x,y", Value: true if pillar exists
|
|
|
|
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
|
|
# 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)
|
|
# Use original_room1 and original_room2 from door_data (which may have been swapped)
|
|
if "original_room1" in door_data and door_data.original_room1:
|
|
door.room1 = door_data.original_room1 # This is always the puzzle room
|
|
elif "door" in door_data and door_data.door is Dictionary:
|
|
# Fallback to original door if original_room1 not set
|
|
var original_door = door_data.door
|
|
if "room1" in original_door and original_door.room1:
|
|
door.room1 = original_door.room1
|
|
|
|
if "original_room2" in door_data and door_data.original_room2:
|
|
door.room2 = door_data.original_room2 # This is always the other room
|
|
elif "door" in door_data and door_data.door is Dictionary:
|
|
# Fallback to original door if original_room2 not set
|
|
var original_door = door_data.door
|
|
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")
|
|
|
|
# If this is a pillar switch, ensure a pillar exists in the room
|
|
# Check if switch is a pillar switch (check both door_data and existing switch)
|
|
var is_pillar_switch = (switch_type == "pillar")
|
|
if not is_pillar_switch and "switch_type" in existing_switch:
|
|
is_pillar_switch = (existing_switch.switch_type == "pillar")
|
|
|
|
if is_pillar_switch:
|
|
# Check if we've already verified/placed a pillar for this room
|
|
var room_key = str(door_blocking_room.x) + "," + str(door_blocking_room.y)
|
|
if not rooms_with_pillars.has(room_key):
|
|
# First time checking this room - check if pillar exists
|
|
var pillar_exists_in_room = false
|
|
for obj in get_tree().get_nodes_in_group("interactable_object"):
|
|
if is_instance_valid(obj):
|
|
var obj_type = obj.object_type if "object_type" in obj else ""
|
|
if obj_type == "Pillar":
|
|
if obj.has_meta("room"):
|
|
var obj_room = obj.get_meta("room")
|
|
if obj_room and not obj_room.is_empty():
|
|
if obj_room.x == door_blocking_room.x and obj_room.y == door_blocking_room.y and \
|
|
obj_room.w == door_blocking_room.w and obj_room.h == door_blocking_room.h:
|
|
pillar_exists_in_room = true
|
|
print("GameWorld: Found existing pillar in room (", door_blocking_room.x, ",", door_blocking_room.y, ")")
|
|
break
|
|
|
|
# If no pillar exists, place one
|
|
if not pillar_exists_in_room:
|
|
print("GameWorld: Pillar switch found but no pillar in room (", door_blocking_room.x, ",", door_blocking_room.y, ") - placing pillar now")
|
|
_place_pillar_in_room(door_blocking_room, existing_switch.global_position)
|
|
# Mark room as checked after attempting to place pillar
|
|
# Note: Even if placement fails, mark as checked to avoid repeated attempts
|
|
rooms_with_pillars[room_key] = true
|
|
else:
|
|
# Pillar exists - mark room as checked
|
|
rooms_with_pillars[room_key] = true
|
|
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":
|
|
# Check if we've already placed a pillar for this room
|
|
var room_key = str(door_blocking_room.x) + "," + str(door_blocking_room.y)
|
|
if not rooms_with_pillars.has(room_key):
|
|
print("GameWorld: Placing pillar for new pillar switch in room (", door_blocking_room.x, ",", door_blocking_room.y, ")")
|
|
_place_pillar_in_room(door_blocking_room, switch_pos)
|
|
# Mark room as checked after attempting to place pillar
|
|
# Note: Even if placement fails, mark as checked to avoid repeated attempts
|
|
rooms_with_pillars[room_key] = true
|
|
else:
|
|
print("GameWorld: Pillar already exists/placed for room (", door_blocking_room.x, ",", door_blocking_room.y, ") - skipping placement")
|
|
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 using the scene file
|
|
# switch_type: "walk" for walk-on switch (weight 1), "pillar" for pillar switch (weight 5)
|
|
var switch_scene = load("res://scenes/floor_switch.tscn")
|
|
if not switch_scene:
|
|
push_error("ERROR: Could not load floor_switch scene!")
|
|
return null
|
|
|
|
var switch = switch_scene.instantiate()
|
|
if not switch:
|
|
push_error("ERROR: Could not instantiate floor_switch scene!")
|
|
return null
|
|
|
|
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)
|
|
|
|
# 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")
|
|
|
|
var triggers_spawned = 0
|
|
for i in range(rooms.size()):
|
|
var room = rooms[i]
|
|
if not room is Dictionary:
|
|
print("GameWorld: WARNING - Room at index ", i, " is not a Dictionary, skipping")
|
|
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)
|
|
triggers_spawned += 1
|
|
print("GameWorld: Added room trigger ", trigger.name, " for room (", room.x, ", ", room.y, ") - ", triggers_spawned, "/", rooms.size())
|
|
|
|
print("GameWorld: Spawned ", triggers_spawned, " room triggers (out of ", rooms.size(), " rooms)")
|
|
|
|
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:
|
|
# Use deterministic seed for key placement (ensures same position on host and clients)
|
|
var rng = RandomNumberGenerator.new()
|
|
var key_seed = dungeon_seed + (room.x * 1000) + (room.y * 100) + 5000 # Offset to avoid collisions with other objects
|
|
rng.seed = key_seed
|
|
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 = load("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 floor tiles: from room.x + 2 to room.x + room.w - 3 (excluding 2-tile walls on each side)
|
|
# Floor tiles are at: room.x+2, room.x+3, ..., room.x+room.w-3 (last floor tile before right wall)
|
|
# 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)
|
|
|
|
var interior_width = room.w - 4 # Exclude 2-tile walls on each side
|
|
var interior_height = room.h - 4 # Exclude 2-tile walls on each side
|
|
print("GameWorld: _place_pillar_in_room - Searching for valid positions in room (", room.x, ",", room.y, ",", room.w, "x", room.h, ")")
|
|
print("GameWorld: Interior size: ", interior_width, "x", interior_height, ", Checking tiles: x[", min_x, " to ", max_x, "], y[", min_y, " to ", max_y, "]")
|
|
print("GameWorld: Switch position: ", switch_position)
|
|
|
|
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 1 tile away from the switch
|
|
var distance_to_switch = world_pos.distance_to(switch_position)
|
|
if distance_to_switch >= tile_size * 1: # At least 1 tiles away
|
|
valid_positions.append(world_pos)
|
|
print("GameWorld: Valid position found at (", x, ",", y, ") -> world (", world_x, ",", world_y, "), distance to switch: ", distance_to_switch)
|
|
else:
|
|
print("GameWorld: Position at (", x, ",", y, ") -> world (", world_x, ",", world_y, ") too close to switch (distance: ", distance_to_switch, " < ", tile_size, ")")
|
|
|
|
print("GameWorld: Found ", valid_positions.size(), " valid positions for pillar")
|
|
if valid_positions.size() > 0:
|
|
# Pick a deterministic random position using dungeon seed
|
|
# This ensures server and clients place pillars in the same positions
|
|
var rng = RandomNumberGenerator.new()
|
|
# Use dungeon seed + room position + switch position as seed for deterministic randomness
|
|
var pillar_seed = dungeon_seed + (room.x * 1000) + (room.y * 100) + int(switch_position.x) + int(switch_position.y)
|
|
rng.seed = pillar_seed
|
|
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
|