376 lines
12 KiB
GDScript
376 lines
12 KiB
GDScript
extends Node
|
|
|
|
# PortalModeManager - Handles "Tekton Doors" mode logic
|
|
# Manages room partitioning, portal connections, and mode-specific timers.
|
|
|
|
var main: Node
|
|
var gridmap: Node
|
|
|
|
# Room layout config
|
|
const ROOM_COUNT = 4
|
|
const GRID_SIZE = 14
|
|
const ROOM_DIM = 7
|
|
|
|
# State
|
|
var connections = {} # room_id -> {door_id -> {target_room, target_door}}
|
|
var doors = [] # List of PortalDoor nodes
|
|
var swap_timer: Timer
|
|
var tile_refresh_timer: Timer
|
|
var finish_spawned: bool = false
|
|
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)
|
|
swap_timer = Timer.new()
|
|
swap_timer.name = "PortalSwapTimer"
|
|
swap_timer.wait_time = 15.0
|
|
swap_timer.timeout.connect(_on_swap_timer_timeout)
|
|
add_child(swap_timer)
|
|
|
|
# Tile Refresh Timer (25s)
|
|
tile_refresh_timer = Timer.new()
|
|
tile_refresh_timer.name = "TileRefreshTimer"
|
|
tile_refresh_timer.wait_time = 25.0
|
|
tile_refresh_timer.timeout.connect(_on_tile_refresh_timer_timeout)
|
|
add_child(tile_refresh_timer)
|
|
|
|
# Connect to mission tracking
|
|
var gcm = main.get_node_or_null("GoalsCycleManager")
|
|
if gcm:
|
|
gcm.goal_count_updated.connect(_on_goal_count_updated)
|
|
|
|
func start_game_mode():
|
|
if not multiplayer.is_server(): return
|
|
|
|
print("[PortalModeManager] Starting Portal Game Mode...")
|
|
|
|
# 1. Setup Arena (GridMap walls & Doors)
|
|
setup_arena_locally()
|
|
|
|
# 2. Skip individual door spawn as it's now in setup_arena_locally
|
|
# _spawn_portal_doors()
|
|
|
|
# 3. Initialize Connections
|
|
_randomize_connections()
|
|
|
|
# 4. Start Timers
|
|
swap_timer.start()
|
|
tile_refresh_timer.start()
|
|
|
|
# 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()
|
|
# 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)
|
|
|
|
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
|
|
]
|
|
|
|
func _setup_room_partitions():
|
|
for i in range(GRID_SIZE):
|
|
# Vertical wall (middle columns)
|
|
gridmap.set_cell_item(Vector3i(6, 0, i), 4) # Wall item
|
|
gridmap.set_cell_item(Vector3i(7, 0, i), 4)
|
|
|
|
# Horizontal wall (middle rows)
|
|
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:
|
|
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 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 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 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
|
|
]
|
|
|
|
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"]
|
|
door.door_id = i
|
|
door.set_meta("spawn_offset", cfg["offset"]) # Store offset for teleport
|
|
|
|
# Position
|
|
var world_pos = gridmap.map_to_local(Vector3i(cfg["pos"].x, 0, cfg["pos"].y))
|
|
door.transform.origin = world_pos
|
|
door.rotation.y = cfg["rot"]
|
|
|
|
stands_container.add_child(door, true)
|
|
doors.append(door)
|
|
|
|
# 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
|
|
]
|
|
|
|
func _randomize_connections():
|
|
if not multiplayer.is_server(): return
|
|
|
|
print("[PortalModeManager] Swapping portal connections...")
|
|
connections.clear()
|
|
|
|
var door_indices = []
|
|
for i in range(doors.size()):
|
|
door_indices.append(i)
|
|
|
|
# Shuffle and Validate: ensure no pairs are in the same room
|
|
var valid_pairing = false
|
|
var attempts = 0
|
|
while not valid_pairing and attempts < 100:
|
|
attempts += 1
|
|
door_indices.shuffle()
|
|
valid_pairing = true
|
|
for i in range(0, door_indices.size(), 2):
|
|
var a = door_indices[i]
|
|
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]
|
|
connections[a] = b
|
|
connections[b] = a
|
|
|
|
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 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:
|
|
_spawn_finish_room()
|
|
|
|
func _spawn_finish_room():
|
|
print("[PortalModeManager] Missions complete! Spawning Finish Room...")
|
|
finish_spawned = true
|
|
|
|
# Choose a random center room tile (X=3, Z=10 or similar in any room)
|
|
var room_centers = get_spawn_points()
|
|
var center = room_centers[randi() % room_centers.size()]
|
|
|
|
# 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!")
|
|
|
|
func _on_swap_timer_timeout():
|
|
_randomize_connections()
|
|
|
|
func _on_tile_refresh_timer_timeout():
|
|
_refresh_tiles()
|
|
main.rpc("display_message", "TILES REPLENISHED!")
|
|
|
|
func _refresh_tiles():
|
|
# 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):
|
|
# 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
|
|
|
|
# 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)
|
|
# 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
|
|
for w in weights.values(): total_weight += w
|
|
|
|
var r = randi() % total_weight
|
|
var cumulative = 0
|
|
for tile in weights:
|
|
cumulative += weights[tile]
|
|
if r < cumulative:
|
|
return tile
|
|
return 7 # Default Heart
|
|
|
|
func handle_portal_interaction(player, door):
|
|
if not multiplayer.is_server(): return
|
|
|
|
var source_id = door.door_id
|
|
if not connections.has(source_id): return
|
|
|
|
var target_id = connections[source_id]
|
|
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)
|
|
|
|
# Convert world pos back to grid
|
|
var target_world = target_door.global_position
|
|
var target_grid_3d = gridmap.local_to_map(target_world)
|
|
var target_grid = Vector2i(target_grid_3d.x, target_grid_3d.z) + offset
|
|
|
|
print("[Portal] Teleporting %s to Room %d, Pos %s (via Door %d)" % [player.name, target_door.room_id, target_grid, target_id])
|
|
|
|
# Snap player
|
|
if player.has_method("set_spawn_position"):
|
|
player.rpc("set_spawn_position", target_grid)
|