feat: implement Tekton Doors game mode with arena setup, portal management, connection randomization, and game state timers.
This commit is contained in:
@@ -55,7 +55,8 @@ func _ready():
|
|||||||
# Safety check: Don't auto-randomize if game mode manages its own arena
|
# Safety check: Don't auto-randomize if game mode manages its own arena
|
||||||
if not (ResourceLoader.exists("res://scripts/managers/lobby_manager.gd") \
|
if not (ResourceLoader.exists("res://scripts/managers/lobby_manager.gd") \
|
||||||
and get_node_or_null("/root/LobbyManager") \
|
and get_node_or_null("/root/LobbyManager") \
|
||||||
and get_node("/root/LobbyManager").game_mode == "Stop n Go"):
|
and (get_node("/root/LobbyManager").game_mode == "Stop n Go" \
|
||||||
|
or get_node("/root/LobbyManager").game_mode == "Tekton Doors")):
|
||||||
randomize_grid()
|
randomize_grid()
|
||||||
validate_item_indices()
|
validate_item_indices()
|
||||||
|
|
||||||
@@ -118,7 +119,7 @@ func generate_grid(floor_index: int = -1):
|
|||||||
var rng = RandomNumberGenerator.new()
|
var rng = RandomNumberGenerator.new()
|
||||||
rng.randomize()
|
rng.randomize()
|
||||||
var shape = rng.randi() % 4 # 0-3 (Rect, Islands, Maze, Rooms)
|
var shape = rng.randi() % 4 # 0-3 (Rect, Islands, Maze, Rooms)
|
||||||
generator.generate_map(self, columns, rows, shape)
|
generator.generate_map(self , columns, rows, shape)
|
||||||
else:
|
else:
|
||||||
generate_floor(0)
|
generate_floor(0)
|
||||||
|
|
||||||
@@ -137,7 +138,7 @@ func generate_grid(floor_index: int = -1):
|
|||||||
var rng = RandomNumberGenerator.new()
|
var rng = RandomNumberGenerator.new()
|
||||||
rng.randomize()
|
rng.randomize()
|
||||||
var shape = rng.randi() % 4
|
var shape = rng.randi() % 4
|
||||||
generator.generate_map(self, columns, rows, shape)
|
generator.generate_map(self , columns, rows, shape)
|
||||||
else:
|
else:
|
||||||
clear_floor(floor_index)
|
clear_floor(floor_index)
|
||||||
generate_floor(floor_index)
|
generate_floor(floor_index)
|
||||||
@@ -213,8 +214,16 @@ func set_floor_data(floor_index: int, data: PackedInt32Array):
|
|||||||
|
|
||||||
for i in range(0, count, 3):
|
for i in range(0, count, 3):
|
||||||
var x = data[i]
|
var x = data[i]
|
||||||
var z = data[i+1]
|
var z = data[i + 1]
|
||||||
var item = data[i+2]
|
var item = data[i + 2]
|
||||||
|
|
||||||
|
# WALL-SAFETY CHECK: Skip if item is a tile (7-20) and Floor 0 is a wall
|
||||||
|
if floor_index == 1 and item >= 7 and item <= 20:
|
||||||
|
var f0 = get_cell_item(Vector3i(x, 0, z))
|
||||||
|
if f0 != -1 and f0 in non_walkable_items:
|
||||||
|
# Skip illegal placement from synced data
|
||||||
|
continue
|
||||||
|
|
||||||
set_cell_item(Vector3i(x, floor_index, z), item)
|
set_cell_item(Vector3i(x, floor_index, z), item)
|
||||||
|
|
||||||
update_grid_data()
|
update_grid_data()
|
||||||
|
|||||||
+35
-15
@@ -34,10 +34,10 @@ func _ready():
|
|||||||
# Setup UI
|
# Setup UI
|
||||||
ui_manager.setup_action_buttons(_set_action_state_callback)
|
ui_manager.setup_action_buttons(_set_action_state_callback)
|
||||||
ui_manager.setup_playerboard_ui()
|
ui_manager.setup_playerboard_ui()
|
||||||
ui_manager.setup_timer_labels(self)
|
ui_manager.setup_timer_labels(self )
|
||||||
ui_manager.setup_playerboard_label(self) # NEW
|
ui_manager.setup_playerboard_label(self ) # NEW
|
||||||
ui_manager.setup_leaderboard_ui(self)
|
ui_manager.setup_leaderboard_ui(self )
|
||||||
ui_manager.setup_powerup_bar_ui(self)
|
ui_manager.setup_powerup_bar_ui(self )
|
||||||
# GlobalMatchTimer is now static in main.tscn - no setup needed
|
# GlobalMatchTimer is now static in main.tscn - no setup needed
|
||||||
# NetworkPanel is visible during gameplay
|
# NetworkPanel is visible during gameplay
|
||||||
|
|
||||||
@@ -71,7 +71,6 @@ func _ready():
|
|||||||
stand_spawner.name = "StandSpawner"
|
stand_spawner.name = "StandSpawner"
|
||||||
stand_spawner.spawn_path = NodePath("../Stands") # Relative to Spawner, finding sibling
|
stand_spawner.spawn_path = NodePath("../Stands") # Relative to Spawner, finding sibling
|
||||||
stand_spawner.add_spawnable_scene("res://scenes/static_tekton_stand.tscn")
|
stand_spawner.add_spawnable_scene("res://scenes/static_tekton_stand.tscn")
|
||||||
stand_spawner.add_spawnable_scene("res://scenes/portal_door.tscn")
|
|
||||||
add_child(stand_spawner)
|
add_child(stand_spawner)
|
||||||
|
|
||||||
func _on_goal_count_updated(peer_id: int, count: int):
|
func _on_goal_count_updated(peer_id: int, count: int):
|
||||||
@@ -84,13 +83,13 @@ func _init_managers():
|
|||||||
ui_manager = load("res://scripts/managers/ui_manager.gd").new()
|
ui_manager = load("res://scripts/managers/ui_manager.gd").new()
|
||||||
ui_manager.name = "UIManager"
|
ui_manager.name = "UIManager"
|
||||||
add_child(ui_manager)
|
add_child(ui_manager)
|
||||||
ui_manager.initialize(self)
|
ui_manager.initialize(self )
|
||||||
|
|
||||||
# Goals cycle manager for 60-second timer and scoring
|
# Goals cycle manager for 60-second timer and scoring
|
||||||
goals_cycle_manager = load("res://scripts/managers/goals_cycle_manager.gd").new()
|
goals_cycle_manager = load("res://scripts/managers/goals_cycle_manager.gd").new()
|
||||||
goals_cycle_manager.name = "GoalsCycleManager"
|
goals_cycle_manager.name = "GoalsCycleManager"
|
||||||
add_child(goals_cycle_manager)
|
add_child(goals_cycle_manager)
|
||||||
goals_cycle_manager.initialize(self)
|
goals_cycle_manager.initialize(self )
|
||||||
|
|
||||||
# Stop n Go manager for phase-based gameplay
|
# Stop n Go manager for phase-based gameplay
|
||||||
if LobbyManager.game_mode == "Stop n Go":
|
if LobbyManager.game_mode == "Stop n Go":
|
||||||
@@ -104,7 +103,7 @@ func _init_managers():
|
|||||||
portal_mode_manager = load("res://scripts/managers/portal_mode_manager.gd").new()
|
portal_mode_manager = load("res://scripts/managers/portal_mode_manager.gd").new()
|
||||||
portal_mode_manager.name = "PortalModeManager"
|
portal_mode_manager.name = "PortalModeManager"
|
||||||
add_child(portal_mode_manager)
|
add_child(portal_mode_manager)
|
||||||
portal_mode_manager.initialize(self, $EnhancedGridMap)
|
portal_mode_manager.initialize(self , $EnhancedGridMap)
|
||||||
|
|
||||||
# Screen shake manager for impact feedback
|
# Screen shake manager for impact feedback
|
||||||
screen_shake_manager = load("res://scripts/managers/screen_shake.gd").new()
|
screen_shake_manager = load("res://scripts/managers/screen_shake.gd").new()
|
||||||
@@ -120,7 +119,7 @@ func _init_managers():
|
|||||||
touch_controls.name = "TouchControls"
|
touch_controls.name = "TouchControls"
|
||||||
add_child(touch_controls)
|
add_child(touch_controls)
|
||||||
|
|
||||||
touch_controls.initialize(self)
|
touch_controls.initialize(self )
|
||||||
|
|
||||||
# NEW: Camera Context Manager for dynamic camera position
|
# NEW: Camera Context Manager for dynamic camera position
|
||||||
camera_context_manager = load("res://scripts/managers/camera_context_manager.gd").new()
|
camera_context_manager = load("res://scripts/managers/camera_context_manager.gd").new()
|
||||||
@@ -132,7 +131,7 @@ func _init_managers():
|
|||||||
obstacle_manager = load("res://scripts/managers/obstacle_manager.gd").new()
|
obstacle_manager = load("res://scripts/managers/obstacle_manager.gd").new()
|
||||||
obstacle_manager.name = "ObstacleManager"
|
obstacle_manager.name = "ObstacleManager"
|
||||||
add_child(obstacle_manager)
|
add_child(obstacle_manager)
|
||||||
obstacle_manager.initialize(self, $EnhancedGridMap)
|
obstacle_manager.initialize(self , $EnhancedGridMap)
|
||||||
|
|
||||||
# Connect signals for UI updates
|
# Connect signals for UI updates
|
||||||
goals_cycle_manager.timer_updated.connect(_on_timer_updated)
|
goals_cycle_manager.timer_updated.connect(_on_timer_updated)
|
||||||
@@ -567,7 +566,6 @@ func _setup_client_game():
|
|||||||
rpc_id(1, "request_full_grid_sync")
|
rpc_id(1, "request_full_grid_sync")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func _auto_start_from_lobby():
|
func _auto_start_from_lobby():
|
||||||
"""Called when main.tscn is loaded from lobby - game is already connected."""
|
"""Called when main.tscn is loaded from lobby - game is already connected."""
|
||||||
# Get match ID from LobbyManager
|
# Get match ID from LobbyManager
|
||||||
@@ -1520,7 +1518,7 @@ func randomize_item_at_position(grid_position: Vector2i):
|
|||||||
# If current item exists, replace it (scarcity aware)
|
# If current item exists, replace it (scarcity aware)
|
||||||
# If current item exists OR we are forcing a spawn on valid ground
|
# If current item exists OR we are forcing a spawn on valid ground
|
||||||
var floor_0_item = enhanced_gridmap.get_cell_item(Vector3i(grid_position.x, 0, grid_position.y))
|
var floor_0_item = enhanced_gridmap.get_cell_item(Vector3i(grid_position.x, 0, grid_position.y))
|
||||||
var is_ground = (floor_0_item != -1) # Simple check, or check specific ground items
|
var is_ground = (floor_0_item != -1 and floor_0_item != 4) # Skip walls (4) and empty space (-1)
|
||||||
|
|
||||||
# Prevent stacking on players
|
# Prevent stacking on players
|
||||||
if is_ground:
|
if is_ground:
|
||||||
@@ -1553,14 +1551,25 @@ func request_randomize_item(grid_position: Vector2i):
|
|||||||
func sync_grid_item(x: int, y: int, z: int, item: int):
|
func sync_grid_item(x: int, y: int, z: int, item: int):
|
||||||
var enhanced_gridmap = $EnhancedGridMap
|
var enhanced_gridmap = $EnhancedGridMap
|
||||||
if enhanced_gridmap:
|
if enhanced_gridmap:
|
||||||
|
# WALL-SAFETY CHECK: Block tiles (7-20) from being placed on walls (4)
|
||||||
|
if y == 1 and item >= 7 and item <= 20:
|
||||||
|
var f0 = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z))
|
||||||
|
if f0 == 4:
|
||||||
|
# Log and block illegal placement
|
||||||
|
print("[Main] Blocked illegal tile (%d) placement on wall at (%d, %d)" % [item, x, z])
|
||||||
|
return
|
||||||
|
|
||||||
enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item)
|
enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item)
|
||||||
|
# Force visual update
|
||||||
|
if enhanced_gridmap.has_method("update_grid_data"):
|
||||||
|
enhanced_gridmap.update_grid_data()
|
||||||
|
|
||||||
# Sync grid update (no need to sync whole grid if we do it at start, but if we do it late we might need to sync)
|
# Sync grid update (no need to sync whole grid if we do it at start, but if we do it late we might need to sync)
|
||||||
# For simplicity, we trust the grid syncs via normal mechanisms or initial state.
|
# For simplicity, we trust the grid syncs via normal mechanisms or initial state.
|
||||||
|
|
||||||
func randomize_game_grid():
|
func randomize_game_grid():
|
||||||
if LobbyManager.game_mode == "Stop n Go":
|
if LobbyManager.game_mode == "Stop n Go" or LobbyManager.game_mode == "Tekton Doors":
|
||||||
return # Stop n Go manages its own arena setup
|
return # These modes manage their own arena setup and item spawning
|
||||||
|
|
||||||
var enhanced_gridmap = $EnhancedGridMap
|
var enhanced_gridmap = $EnhancedGridMap
|
||||||
if enhanced_gridmap:
|
if enhanced_gridmap:
|
||||||
@@ -1595,7 +1604,7 @@ func request_full_grid_sync():
|
|||||||
|
|
||||||
# For all modes, only sync Floor 1 (Items) to prevent MTU packet overflow.
|
# For all modes, only sync Floor 1 (Items) to prevent MTU packet overflow.
|
||||||
# Floor 0 logic is deterministic and generated locally on level load.
|
# Floor 0 logic is deterministic and generated locally on level load.
|
||||||
var grid_data = enhanced_gridmap.get_floor_data(1)
|
var grid_data = enhanced_gridmap.get_floor_data(1)
|
||||||
print("[Main] Server: Prepared grid data. Size: %d. Sending to %d..." % [grid_data.size(), sender_id])
|
print("[Main] Server: Prepared grid data. Size: %d. Sending to %d..." % [grid_data.size(), sender_id])
|
||||||
|
|
||||||
# Delay slightly to ensure socket stability after player syncs
|
# Delay slightly to ensure socket stability after player syncs
|
||||||
@@ -1604,6 +1613,10 @@ func request_full_grid_sync():
|
|||||||
if sender_id in multiplayer.get_peers():
|
if sender_id in multiplayer.get_peers():
|
||||||
rpc_id(sender_id, "sync_full_grid_data", grid_data)
|
rpc_id(sender_id, "sync_full_grid_data", grid_data)
|
||||||
print("[Main] Server: Sent grid sync rpc_id to %d" % sender_id)
|
print("[Main] Server: Sent grid sync rpc_id to %d" % sender_id)
|
||||||
|
|
||||||
|
# If Tekton Doors, sync portal connections too
|
||||||
|
if LobbyManager.game_mode == "Tekton Doors" and portal_mode_manager:
|
||||||
|
portal_mode_manager.sync_to_client(sender_id)
|
||||||
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
@rpc("authority", "call_local", "reliable")
|
||||||
func sync_full_grid_data(data: PackedInt32Array):
|
func sync_full_grid_data(data: PackedInt32Array):
|
||||||
@@ -1620,6 +1633,13 @@ func sync_full_grid_data(data: PackedInt32Array):
|
|||||||
stop_n_go_manager.name = "StopNGoManager"
|
stop_n_go_manager.name = "StopNGoManager"
|
||||||
add_child(stop_n_go_manager)
|
add_child(stop_n_go_manager)
|
||||||
stop_n_go_manager._apply_arena_setup()
|
stop_n_go_manager._apply_arena_setup()
|
||||||
|
elif LobbyManager.game_mode == "Tekton Doors":
|
||||||
|
if not portal_mode_manager:
|
||||||
|
portal_mode_manager = load("res://scripts/managers/portal_mode_manager.gd").new()
|
||||||
|
portal_mode_manager.name = "PortalModeManager"
|
||||||
|
add_child(portal_mode_manager)
|
||||||
|
portal_mode_manager.initialize(self , enhanced_gridmap)
|
||||||
|
portal_mode_manager.setup_arena_locally()
|
||||||
|
|
||||||
# Apply the synced data to Floor 1
|
# Apply the synced data to Floor 1
|
||||||
enhanced_gridmap.set_floor_data(1, data)
|
enhanced_gridmap.set_floor_data(1, data)
|
||||||
|
|||||||
@@ -443,6 +443,14 @@ func request_room_info(requester_id: int, requester_name: String, requester_char
|
|||||||
# Send room data to requester
|
# Send room data to requester
|
||||||
rpc_id(requester_id, "receive_room_info", current_room, players_in_room)
|
rpc_id(requester_id, "receive_room_info", current_room, players_in_room)
|
||||||
|
|
||||||
|
# Sync current lobby settings to the joining client
|
||||||
|
rpc_id(requester_id, "sync_match_duration", match_duration)
|
||||||
|
rpc_id(requester_id, "sync_randomize_spawn", randomize_spawn)
|
||||||
|
rpc_id(requester_id, "sync_enable_cycle_timer", enable_cycle_timer)
|
||||||
|
rpc_id(requester_id, "sync_scarcity_mode", scarcity_mode)
|
||||||
|
rpc_id(requester_id, "sync_game_mode", game_mode)
|
||||||
|
rpc_id(requester_id, "sync_area", selected_area)
|
||||||
|
|
||||||
# Also sync updated player list to all other clients
|
# Also sync updated player list to all other clients
|
||||||
rpc("sync_player_list", players_in_room)
|
rpc("sync_player_list", players_in_room)
|
||||||
emit_signal("player_list_changed")
|
emit_signal("player_list_changed")
|
||||||
|
|||||||
@@ -232,6 +232,10 @@ func _check_and_refill_grid_if_needed(server_gridmap: Node):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if not has_items:
|
if not has_items:
|
||||||
|
if LobbyManager.game_mode == "Tekton Doors":
|
||||||
|
# Tekton Doors handles its own wall-aware refill in PortalModeManager
|
||||||
|
return
|
||||||
|
|
||||||
print("[PlayerboardManager] Floor 1 empty! Respawning tiles with Scarcity...")
|
print("[PlayerboardManager] Floor 1 empty! Respawning tiles with Scarcity...")
|
||||||
# Call randomize_floor on floor 1 using ScarcityController
|
# Call randomize_floor on floor 1 using ScarcityController
|
||||||
# ScarcityController is a global class, so we can pass its static function as a Callable
|
# ScarcityController is a global class, so we can pass its static function as a Callable
|
||||||
@@ -250,7 +254,6 @@ func _check_and_refill_grid_if_needed(server_gridmap: Node):
|
|||||||
main.rpc("sync_grid_item", x, 1, z, item)
|
main.rpc("sync_grid_item", x, 1, z, item)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func _force_sync_to_client(cell: Vector3i, server_item: int):
|
func _force_sync_to_client(cell: Vector3i, server_item: int):
|
||||||
"""Force a sync of the specific cell and playerboard to the client who initiated the failed action."""
|
"""Force a sync of the specific cell and playerboard to the client who initiated the failed action."""
|
||||||
# Only meaningful if we are server
|
# Only meaningful if we are server
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ var missions_required: int = 3
|
|||||||
func initialize(p_main: Node, p_gridmap: Node):
|
func initialize(p_main: Node, p_gridmap: Node):
|
||||||
main = p_main
|
main = p_main
|
||||||
gridmap = p_gridmap
|
gridmap = p_gridmap
|
||||||
|
|
||||||
|
if gridmap:
|
||||||
|
# Ensure walls (4) are strictly treated as non-walkable for all internal checks
|
||||||
|
# Use explicit type to avoid Array vs Array[int] mismatch error
|
||||||
|
var non_walkable: Array[int] = [4]
|
||||||
|
gridmap.non_walkable_items = non_walkable
|
||||||
|
|
||||||
|
# Create Stands container if it doesn't exist
|
||||||
print("[PortalModeManager] Initialized")
|
print("[PortalModeManager] Initialized")
|
||||||
|
|
||||||
# Connection Swap Timer (15s)
|
# Connection Swap Timer (15s)
|
||||||
@@ -48,42 +56,50 @@ func start_game_mode():
|
|||||||
|
|
||||||
print("[PortalModeManager] Starting Portal Game Mode...")
|
print("[PortalModeManager] Starting Portal Game Mode...")
|
||||||
|
|
||||||
# 1. Setup Arena Size
|
# 1. Setup Arena (GridMap walls & Doors)
|
||||||
_setup_arena_size()
|
setup_arena_locally()
|
||||||
|
|
||||||
# 2. Setup Room Partitions (visual/physical walls between rooms)
|
# 2. Skip individual door spawn as it's now in setup_arena_locally
|
||||||
_setup_room_partitions()
|
# _spawn_portal_doors()
|
||||||
|
|
||||||
# 3. Spawn Portal Doors
|
# 3. Initialize Connections
|
||||||
_spawn_portal_doors()
|
|
||||||
|
|
||||||
# 4. Initialize Connections
|
|
||||||
_randomize_connections()
|
_randomize_connections()
|
||||||
|
|
||||||
# 5. Start Timers
|
# 4. Start Timers
|
||||||
swap_timer.start()
|
swap_timer.start()
|
||||||
tile_refresh_timer.start()
|
tile_refresh_timer.start()
|
||||||
|
|
||||||
# 6. Initial Tile Spawn
|
# 5. Initial Tile Spawn
|
||||||
_refresh_tiles()
|
_refresh_tiles()
|
||||||
|
|
||||||
|
func setup_arena_locally():
|
||||||
|
"""Sets up GridMap size and walls. Called on host and clients."""
|
||||||
|
print("[PortalModeManager] Setting up arena locally...")
|
||||||
|
_setup_arena_size()
|
||||||
|
_setup_room_partitions()
|
||||||
|
_spawn_portal_doors()
|
||||||
|
|
||||||
func _setup_arena_size():
|
func _setup_arena_size():
|
||||||
if not gridmap: return
|
if not gridmap: return
|
||||||
gridmap.columns = GRID_SIZE
|
gridmap.columns = GRID_SIZE
|
||||||
gridmap.rows = GRID_SIZE
|
gridmap.rows = GRID_SIZE
|
||||||
gridmap.clear()
|
gridmap.clear()
|
||||||
# Fill floor
|
# Explicitly clear Floor 1 to prevent legacy tiles from previous rounds
|
||||||
|
if gridmap.has_method("clear_grid"):
|
||||||
|
gridmap.clear_grid(1)
|
||||||
|
|
||||||
|
# Fill Floor 0 with standard floor (Item ID 0)
|
||||||
for x in range(GRID_SIZE):
|
for x in range(GRID_SIZE):
|
||||||
for z in range(GRID_SIZE):
|
for z in range(GRID_SIZE):
|
||||||
gridmap.set_cell_item(Vector3i(x, 0, z), 0) # Normal floor
|
gridmap.set_cell_item(Vector3i(x, 0, z), 0)
|
||||||
|
|
||||||
func get_spawn_points() -> Array[Vector2i]:
|
func get_spawn_points() -> Array[Vector2i]:
|
||||||
# One point per quadrant
|
# One point per quadrant
|
||||||
return [
|
return [
|
||||||
Vector2i(3, 3), # Room 0
|
Vector2i(3, 3), # Room 0
|
||||||
Vector2i(10, 3), # Room 1
|
Vector2i(10, 3), # Room 1
|
||||||
Vector2i(3, 10), # Room 2
|
Vector2i(3, 10), # Room 2
|
||||||
Vector2i(10, 10) # Room 3
|
Vector2i(10, 10) # Room 3
|
||||||
]
|
]
|
||||||
|
|
||||||
func _setup_room_partitions():
|
func _setup_room_partitions():
|
||||||
@@ -96,28 +112,45 @@ func _setup_room_partitions():
|
|||||||
gridmap.set_cell_item(Vector3i(i, 0, 6), 4)
|
gridmap.set_cell_item(Vector3i(i, 0, 6), 4)
|
||||||
gridmap.set_cell_item(Vector3i(i, 0, 7), 4)
|
gridmap.set_cell_item(Vector3i(i, 0, 7), 4)
|
||||||
|
|
||||||
|
var _pending_sync_data = null
|
||||||
|
|
||||||
func _spawn_portal_doors():
|
func _spawn_portal_doors():
|
||||||
|
# Check if doors already exist to avoid duplicates
|
||||||
|
if not doors.is_empty():
|
||||||
|
print("[PortalModeManager] Doors already exist, skipping spawn. Count: ", doors.size())
|
||||||
|
return
|
||||||
|
|
||||||
|
print("[PortalModeManager] Spawning portal doors. Peer ID: ", multiplayer.get_unique_id())
|
||||||
var portal_scene = load("res://scenes/portal_door.tscn")
|
var portal_scene = load("res://scenes/portal_door.tscn")
|
||||||
var stands_container = main.get_node_or_null("Stands")
|
var stands_container = main.get_node_or_null("Stands")
|
||||||
if not stands_container: return
|
|
||||||
|
if not stands_container:
|
||||||
|
print("[PortalModeManager] Warning: 'Stands' container not found, creating one...")
|
||||||
|
stands_container = Node3D.new()
|
||||||
|
stands_container.name = "Stands"
|
||||||
|
main.add_child(stands_container)
|
||||||
|
|
||||||
var door_configs = [
|
var door_configs = [
|
||||||
# Room 0
|
# Room 0
|
||||||
{"room": 0, "pos": Vector2i(6, 2), "rot": PI/2, "offset": Vector2i(-1, 0)}, # East
|
{"room": 0, "pos": Vector2i(6, 2), "rot": PI / 2, "offset": Vector2i(-1, 0)}, # East
|
||||||
{"room": 0, "pos": Vector2i(2, 6), "rot": 0, "offset": Vector2i(0, -1)}, # South
|
{"room": 0, "pos": Vector2i(2, 6), "rot": 0, "offset": Vector2i(0, -1)}, # South
|
||||||
# Room 1
|
# Room 1
|
||||||
{"room": 1, "pos": Vector2i(7, 2), "rot": PI/2, "offset": Vector2i(1, 0)}, # West
|
{"room": 1, "pos": Vector2i(7, 2), "rot": PI / 2, "offset": Vector2i(1, 0)}, # West
|
||||||
{"room": 1, "pos": Vector2i(11, 6), "rot": 0, "offset": Vector2i(0, -1)}, # South
|
{"room": 1, "pos": Vector2i(11, 6), "rot": 0, "offset": Vector2i(0, -1)}, # South
|
||||||
# Room 2
|
# Room 2
|
||||||
{"room": 2, "pos": Vector2i(2, 7), "rot": 0, "offset": Vector2i(0, 1)}, # North
|
{"room": 2, "pos": Vector2i(2, 7), "rot": 0, "offset": Vector2i(0, 1)}, # North
|
||||||
{"room": 2, "pos": Vector2i(6, 11), "rot": PI/2, "offset": Vector2i(-1, 0)},# East
|
{"room": 2, "pos": Vector2i(6, 11), "rot": PI / 2, "offset": Vector2i(-1, 0)}, # East
|
||||||
# Room 3
|
# Room 3
|
||||||
{"room": 3, "pos": Vector2i(11, 7), "rot": 0, "offset": Vector2i(0, 1)}, # North
|
{"room": 3, "pos": Vector2i(11, 7), "rot": 0, "offset": Vector2i(0, 1)}, # North
|
||||||
{"room": 3, "pos": Vector2i(7, 11), "rot": PI/2, "offset": Vector2i(1, 0)} # West
|
{"room": 3, "pos": Vector2i(7, 11), "rot": PI / 2, "offset": Vector2i(1, 0)} # West
|
||||||
]
|
]
|
||||||
|
|
||||||
for i in range(door_configs.size()):
|
for i in range(door_configs.size()):
|
||||||
var cfg = door_configs[i]
|
var cfg = door_configs[i]
|
||||||
|
if not portal_scene:
|
||||||
|
print("[PortalModeManager] Error: Failed to load portal_door.tscn")
|
||||||
|
break
|
||||||
|
|
||||||
var door = portal_scene.instantiate()
|
var door = portal_scene.instantiate()
|
||||||
door.name = "Portal_%d" % i
|
door.name = "Portal_%d" % i
|
||||||
door.room_id = cfg["room"]
|
door.room_id = cfg["room"]
|
||||||
@@ -131,15 +164,26 @@ func _spawn_portal_doors():
|
|||||||
|
|
||||||
stands_container.add_child(door, true)
|
stands_container.add_child(door, true)
|
||||||
doors.append(door)
|
doors.append(door)
|
||||||
door.player_entered_portal.connect(handle_portal_interaction)
|
|
||||||
|
# Server-only interaction logic
|
||||||
|
if multiplayer.is_server():
|
||||||
|
door.player_entered_portal.connect(handle_portal_interaction)
|
||||||
|
|
||||||
gridmap.set_cell_item(Vector3i(cfg["pos"].x, 0, cfg["pos"].y), 0) # Normal floor
|
gridmap.set_cell_item(Vector3i(cfg["pos"].x, 0, cfg["pos"].y), 0) # Normal floor
|
||||||
|
|
||||||
|
print("[PortalModeManager] Finished spawning %d doors" % doors.size())
|
||||||
|
|
||||||
|
# Apply pending sync if it arrived early
|
||||||
|
if _pending_sync_data:
|
||||||
|
print("[PortalModeManager] Applying pending sync data...")
|
||||||
|
sync_portal_data(_pending_sync_data)
|
||||||
|
_pending_sync_data = null
|
||||||
|
|
||||||
const PORTAL_COLORS = [
|
const PORTAL_COLORS = [
|
||||||
Color(0, 1, 1), # Cyan
|
Color(0, 1, 1), # Cyan
|
||||||
Color(1, 0, 1), # Magenta
|
Color(1, 0, 1), # Magenta
|
||||||
Color(1, 1, 0), # Yellow
|
Color(1, 1, 0), # Yellow
|
||||||
Color(0, 1, 0) # Green
|
Color(0, 1, 0) # Green
|
||||||
]
|
]
|
||||||
|
|
||||||
func _randomize_connections():
|
func _randomize_connections():
|
||||||
@@ -161,29 +205,89 @@ func _randomize_connections():
|
|||||||
valid_pairing = true
|
valid_pairing = true
|
||||||
for i in range(0, door_indices.size(), 2):
|
for i in range(0, door_indices.size(), 2):
|
||||||
var a = door_indices[i]
|
var a = door_indices[i]
|
||||||
var b = door_indices[i+1]
|
var b = door_indices[i + 1]
|
||||||
if doors[a].room_id == doors[b].room_id:
|
if doors[a].room_id == doors[b].room_id:
|
||||||
valid_pairing = false
|
valid_pairing = false
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Prepare sync data
|
||||||
|
var sync_data = [] # [[door_a_id, door_b_id, color], ...]
|
||||||
|
|
||||||
# Pair them up and assign colors
|
# Pair them up and assign colors
|
||||||
for i in range(0, door_indices.size(), 2):
|
for i in range(0, door_indices.size(), 2):
|
||||||
var a = door_indices[i]
|
var a = door_indices[i]
|
||||||
var b = door_indices[i+1]
|
var b = door_indices[i + 1]
|
||||||
connections[a] = b
|
connections[a] = b
|
||||||
connections[b] = a
|
connections[b] = a
|
||||||
|
|
||||||
var color = PORTAL_COLORS[i/2 % PORTAL_COLORS.size()]
|
var color = PORTAL_COLORS[int(i / 2.0) % PORTAL_COLORS.size()]
|
||||||
|
sync_data.append([a, b, color])
|
||||||
|
|
||||||
doors[a].target_door_id = b
|
doors[a].target_door_id = b
|
||||||
doors[a].portal_color = color
|
doors[a].portal_color = color
|
||||||
|
|
||||||
doors[b].target_door_id = a
|
doors[b].target_door_id = a
|
||||||
doors[b].portal_color = color
|
doors[b].portal_color = color
|
||||||
|
|
||||||
|
# Sync to all clients
|
||||||
|
rpc("sync_portal_data", sync_data)
|
||||||
main.rpc("display_message", "PORTALS SWITCHED!")
|
main.rpc("display_message", "PORTALS SWITCHED!")
|
||||||
|
|
||||||
func _on_goal_count_updated(peer_id: int, count: int):
|
func sync_to_client(peer_id: int):
|
||||||
|
"""Syncs current portal connections to a specific client."""
|
||||||
|
var sync_data = []
|
||||||
|
# connections is id -> id
|
||||||
|
# We need to rebuild the pair-based data for the RPC
|
||||||
|
var handled = []
|
||||||
|
for a_id in connections:
|
||||||
|
if a_id in handled: continue
|
||||||
|
var b_id = connections[a_id]
|
||||||
|
var color = doors[a_id].portal_color
|
||||||
|
sync_data.append([a_id, b_id, color])
|
||||||
|
handled.append(a_id)
|
||||||
|
handled.append(b_id)
|
||||||
|
|
||||||
|
rpc_id(peer_id, "sync_portal_data", sync_data)
|
||||||
|
|
||||||
|
@rpc("authority", "call_local", "reliable")
|
||||||
|
func sync_portal_data(data: Array):
|
||||||
|
"""Syncs portal connections and colors to all clients."""
|
||||||
|
print("[PortalModeManager] Received portal sync data. Peed ID: ", multiplayer.get_unique_id())
|
||||||
|
|
||||||
|
# If doors array is empty on client, try to repopulate from Stands group
|
||||||
|
if doors.is_empty():
|
||||||
|
var stands = get_tree().get_nodes_in_group("PortalDoors")
|
||||||
|
# Sort by name to ensure consistent indexing
|
||||||
|
stands.sort_custom(func(a, b): return a.name < b.name)
|
||||||
|
doors = stands
|
||||||
|
|
||||||
|
# If still empty, defer sync until doors are spawned locally
|
||||||
|
if doors.is_empty():
|
||||||
|
print("[PortalModeManager] Doors not yet ready, deferring sync data...")
|
||||||
|
_pending_sync_data = data
|
||||||
|
return
|
||||||
|
|
||||||
|
connections.clear()
|
||||||
|
for pair in data:
|
||||||
|
var a_id = pair[0]
|
||||||
|
var b_id = pair[1]
|
||||||
|
var color = pair[2]
|
||||||
|
|
||||||
|
connections[a_id] = b_id
|
||||||
|
connections[b_id] = a_id
|
||||||
|
|
||||||
|
if a_id < doors.size() and b_id < doors.size():
|
||||||
|
if is_instance_valid(doors[a_id]):
|
||||||
|
doors[a_id].target_door_id = b_id
|
||||||
|
doors[a_id].portal_color = color
|
||||||
|
if is_instance_valid(doors[b_id]):
|
||||||
|
doors[b_id].target_door_id = a_id
|
||||||
|
doors[b_id].portal_color = color
|
||||||
|
else:
|
||||||
|
print("[PortalModeManager] Warning: Door index %d or %d out of range during sync" % [a_id, b_id])
|
||||||
|
|
||||||
|
|
||||||
|
func _on_goal_count_updated(_peer_id: int, count: int):
|
||||||
if not multiplayer.is_server(): return
|
if not multiplayer.is_server(): return
|
||||||
|
|
||||||
if count >= missions_required and not finish_spawned:
|
if count >= missions_required and not finish_spawned:
|
||||||
@@ -197,8 +301,13 @@ func _spawn_finish_room():
|
|||||||
var room_centers = get_spawn_points()
|
var room_centers = get_spawn_points()
|
||||||
var center = room_centers[randi() % room_centers.size()]
|
var center = room_centers[randi() % room_centers.size()]
|
||||||
|
|
||||||
# Place finish tile (ID 3)
|
# Place finish tile (ID 3) on Floor 1 (Y=1)
|
||||||
gridmap.set_cell_item(Vector3i(center.x, 0, center.y), 3)
|
# Check if this center is actually clear (not a wall accidentally)
|
||||||
|
if gridmap.get_cell_item(Vector3i(center.x, 0, center.y)) == 4:
|
||||||
|
# Fallback to any non-wall center if needed, but spawn points are usually safe
|
||||||
|
pass
|
||||||
|
|
||||||
|
main.rpc("sync_grid_item", center.x, 1, center.y, 3)
|
||||||
main.get_node("EnhancedGridMap").update_grid_data()
|
main.get_node("EnhancedGridMap").update_grid_data()
|
||||||
main.rpc("display_message", "FINISH ROOM REVEALED!")
|
main.rpc("display_message", "FINISH ROOM REVEALED!")
|
||||||
|
|
||||||
@@ -210,18 +319,25 @@ func _on_tile_refresh_timer_timeout():
|
|||||||
main.rpc("display_message", "TILES REPLENISHED!")
|
main.rpc("display_message", "TILES REPLENISHED!")
|
||||||
|
|
||||||
func _refresh_tiles():
|
func _refresh_tiles():
|
||||||
# Simple tile fill for each quadrant
|
# GridMap Floor 0 has the walls (ID 4) and floors (ID 0)
|
||||||
|
# GridMap Floor 1 should have the items (Heart, Star, etc)
|
||||||
for x in range(GRID_SIZE):
|
for x in range(GRID_SIZE):
|
||||||
for z in range(GRID_SIZE):
|
for z in range(GRID_SIZE):
|
||||||
# Skip walls
|
# 1. Check if Floor 0 is a wall or empty (non-walkable)
|
||||||
if gridmap.get_cell_item(Vector3i(x, 0, z)) == 4: continue
|
var floor_0_item = gridmap.get_cell_item(Vector3i(x, 0, z))
|
||||||
|
if floor_0_item == 4 or floor_0_item == -1:
|
||||||
|
continue
|
||||||
|
|
||||||
# Low chance to spawn a tile if empty
|
# 2. Check if Floor 1 is already occupied
|
||||||
|
if gridmap.get_cell_item(Vector3i(x, 1, z)) != -1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 3. Low chance to spawn a tile
|
||||||
if randf() < 0.1:
|
if randf() < 0.1:
|
||||||
var weights = ScarcityModel.get_tile_weights()
|
var weights = ScarcityModel.get_tile_weights()
|
||||||
var tile_id = _pick_weighted_tile(weights)
|
var tile_id = _pick_weighted_tile(weights)
|
||||||
# 1. Update GridMap
|
# Update GridMap Floor 1 via RPC for sync (call_local handles host)
|
||||||
gridmap.set_cell_item(Vector3i(x, 0, z), tile_id)
|
main.rpc("sync_grid_item", x, 1, z, tile_id)
|
||||||
|
|
||||||
func _pick_weighted_tile(weights: Dictionary) -> int:
|
func _pick_weighted_tile(weights: Dictionary) -> int:
|
||||||
var total_weight = 0
|
var total_weight = 0
|
||||||
@@ -245,7 +361,7 @@ func handle_portal_interaction(player, door):
|
|||||||
var target_door = doors[target_id]
|
var target_door = doors[target_id]
|
||||||
|
|
||||||
# Use stored offset to avoid infinite loop (spawn inside the target room)
|
# Use stored offset to avoid infinite loop (spawn inside the target room)
|
||||||
var offset = target_door.get_meta("spawn_offset") if target_door.has_meta("spawn_offset") else Vector2i(0,0)
|
var offset = target_door.get_meta("spawn_offset") if target_door.has_meta("spawn_offset") else Vector2i(0, 0)
|
||||||
|
|
||||||
# Convert world pos back to grid
|
# Convert world pos back to grid
|
||||||
var target_world = target_door.global_position
|
var target_world = target_door.global_position
|
||||||
|
|||||||
+12
-4
@@ -34,25 +34,33 @@ func _on_body_entered(body: Node3D):
|
|||||||
|
|
||||||
if body.is_in_group("Players") or body.get("is_bot"):
|
if body.is_in_group("Players") or body.get("is_bot"):
|
||||||
print("[PortalDoor] Player %s entered Door %d in Room %d" % [body.name, door_id, room_id])
|
print("[PortalDoor] Player %s entered Door %d in Room %d" % [body.name, door_id, room_id])
|
||||||
emit_signal("player_entered_portal", body, self)
|
emit_signal("player_entered_portal", body, self )
|
||||||
|
|
||||||
var _materials_initialized: bool = false
|
var _materials_initialized: bool = false
|
||||||
|
|
||||||
func _update_visuals():
|
func _update_visuals():
|
||||||
if not is_node_ready() or not is_inside_tree(): return
|
# Removed is_node_ready() check to allow early setter calls to prepare variables,
|
||||||
|
# but we still need the nodes to exist to apply them.
|
||||||
|
if not is_inside_tree(): return
|
||||||
|
|
||||||
|
var vortex = get_node_or_null("Vortex")
|
||||||
|
var frame_left = get_node_or_null("Frame_Left")
|
||||||
|
|
||||||
|
# If children aren't there yet, we can't update visuals.
|
||||||
|
# This usually happens if called before or during early _ready.
|
||||||
|
if not vortex or not frame_left: return
|
||||||
|
|
||||||
if not _materials_initialized:
|
if not _materials_initialized:
|
||||||
_initialize_unique_materials()
|
_initialize_unique_materials()
|
||||||
_materials_initialized = true
|
_materials_initialized = true
|
||||||
|
|
||||||
var vortex = get_node_or_null("Vortex")
|
|
||||||
if vortex:
|
if vortex:
|
||||||
var mat = vortex.get_surface_override_material(0)
|
var mat = vortex.get_surface_override_material(0)
|
||||||
if mat:
|
if mat:
|
||||||
mat.albedo_color = portal_color
|
mat.albedo_color = portal_color
|
||||||
mat.albedo_color.a = 0.5
|
mat.albedo_color.a = 0.5
|
||||||
if mat.has_method("set_emission"):
|
if mat.has_method("set_emission"):
|
||||||
mat.emission = portal_color
|
mat.set("emission", portal_color)
|
||||||
|
|
||||||
for part_name in ["Frame_Left", "Frame_Right", "Frame_Top"]:
|
for part_name in ["Frame_Left", "Frame_Right", "Frame_Top"]:
|
||||||
var frame = get_node_or_null(part_name)
|
var frame = get_node_or_null(part_name)
|
||||||
|
|||||||
Reference in New Issue
Block a user