feat: implement Tekton Doors game mode with arena setup, portal management, connection randomization, and game state timers.

This commit is contained in:
2026-02-26 04:25:09 +08:00
parent 551c820d5e
commit ef3d018040
6 changed files with 230 additions and 66 deletions
+8
View File
@@ -443,6 +443,14 @@ func request_room_info(requester_id: int, requester_name: String, requester_char
# Send room data to requester
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
rpc("sync_player_list", players_in_room)
emit_signal("player_list_changed")
+4 -1
View File
@@ -232,6 +232,10 @@ func _check_and_refill_grid_if_needed(server_gridmap: Node):
break
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...")
# Call randomize_floor on floor 1 using ScarcityController
# 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)
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."""
# Only meaningful if we are server
+157 -41
View File
@@ -22,6 +22,14 @@ var missions_required: int = 3
func initialize(p_main: Node, p_gridmap: Node):
main = p_main
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")
# Connection Swap Timer (15s)
@@ -48,42 +56,50 @@ func start_game_mode():
print("[PortalModeManager] Starting Portal Game Mode...")
# 1. Setup Arena Size
_setup_arena_size()
# 1. Setup Arena (GridMap walls & Doors)
setup_arena_locally()
# 2. Setup Room Partitions (visual/physical walls between rooms)
_setup_room_partitions()
# 2. Skip individual door spawn as it's now in setup_arena_locally
# _spawn_portal_doors()
# 3. Spawn Portal Doors
_spawn_portal_doors()
# 4. Initialize Connections
# 3. Initialize Connections
_randomize_connections()
# 5. Start Timers
# 4. Start Timers
swap_timer.start()
tile_refresh_timer.start()
# 6. Initial Tile Spawn
# 5. Initial Tile Spawn
_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():
if not gridmap: return
gridmap.columns = GRID_SIZE
gridmap.rows = GRID_SIZE
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 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]:
# One point per quadrant
return [
Vector2i(3, 3), # Room 0
Vector2i(10, 3), # Room 1
Vector2i(3, 10), # Room 2
Vector2i(10, 10) # Room 3
Vector2i(3, 3), # Room 0
Vector2i(10, 3), # Room 1
Vector2i(3, 10), # Room 2
Vector2i(10, 10) # Room 3
]
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, 7), 4)
var _pending_sync_data = null
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 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 = [
# Room 0
{"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(6, 2), "rot": PI / 2, "offset": Vector2i(-1, 0)}, # East
{"room": 0, "pos": Vector2i(2, 6), "rot": 0, "offset": Vector2i(0, -1)}, # South
# Room 1
{"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(7, 2), "rot": PI / 2, "offset": Vector2i(1, 0)}, # West
{"room": 1, "pos": Vector2i(11, 6), "rot": 0, "offset": Vector2i(0, -1)}, # South
# Room 2
{"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(2, 7), "rot": 0, "offset": Vector2i(0, 1)}, # North
{"room": 2, "pos": Vector2i(6, 11), "rot": PI / 2, "offset": Vector2i(-1, 0)}, # East
# Room 3
{"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(11, 7), "rot": 0, "offset": Vector2i(0, 1)}, # North
{"room": 3, "pos": Vector2i(7, 11), "rot": PI / 2, "offset": Vector2i(1, 0)} # West
]
for i in range(door_configs.size()):
var cfg = door_configs[i]
if not portal_scene:
print("[PortalModeManager] Error: Failed to load portal_door.tscn")
break
var door = portal_scene.instantiate()
door.name = "Portal_%d" % i
door.room_id = cfg["room"]
@@ -131,15 +164,26 @@ func _spawn_portal_doors():
stands_container.add_child(door, true)
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
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 = [
Color(0, 1, 1), # Cyan
Color(1, 0, 1), # Magenta
Color(1, 1, 0), # Yellow
Color(0, 1, 0) # Green
Color(0, 1, 0) # Green
]
func _randomize_connections():
@@ -161,29 +205,89 @@ func _randomize_connections():
valid_pairing = true
for i in range(0, door_indices.size(), 2):
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:
valid_pairing = false
break
# Prepare sync data
var sync_data = [] # [[door_a_id, door_b_id, color], ...]
# Pair them up and assign colors
for i in range(0, door_indices.size(), 2):
var a = door_indices[i]
var b = door_indices[i+1]
var b = door_indices[i + 1]
connections[a] = b
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].portal_color = color
doors[b].target_door_id = a
doors[b].portal_color = color
# Sync to all clients
rpc("sync_portal_data", sync_data)
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 count >= missions_required and not finish_spawned:
@@ -197,8 +301,13 @@ func _spawn_finish_room():
var room_centers = get_spawn_points()
var center = room_centers[randi() % room_centers.size()]
# Place finish tile (ID 3)
gridmap.set_cell_item(Vector3i(center.x, 0, center.y), 3)
# Place finish tile (ID 3) on Floor 1 (Y=1)
# 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.rpc("display_message", "FINISH ROOM REVEALED!")
@@ -210,18 +319,25 @@ func _on_tile_refresh_timer_timeout():
main.rpc("display_message", "TILES REPLENISHED!")
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 z in range(GRID_SIZE):
# Skip walls
if gridmap.get_cell_item(Vector3i(x, 0, z)) == 4: continue
# 1. Check if Floor 0 is a wall or empty (non-walkable)
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:
var weights = ScarcityModel.get_tile_weights()
var tile_id = _pick_weighted_tile(weights)
# 1. Update GridMap
gridmap.set_cell_item(Vector3i(x, 0, z), tile_id)
# Update GridMap Floor 1 via RPC for sync (call_local handles host)
main.rpc("sync_grid_item", x, 1, z, tile_id)
func _pick_weighted_tile(weights: Dictionary) -> int:
var total_weight = 0
@@ -245,7 +361,7 @@ func handle_portal_interaction(player, door):
var target_door = doors[target_id]
# 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
var target_world = target_door.global_position
+12 -4
View File
@@ -34,25 +34,33 @@ func _on_body_entered(body: Node3D):
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])
emit_signal("player_entered_portal", body, self)
emit_signal("player_entered_portal", body, self )
var _materials_initialized: bool = false
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:
_initialize_unique_materials()
_materials_initialized = true
var vortex = get_node_or_null("Vortex")
if vortex:
var mat = vortex.get_surface_override_material(0)
if mat:
mat.albedo_color = portal_color
mat.albedo_color.a = 0.5
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"]:
var frame = get_node_or_null(part_name)