diff --git a/addons/enhanced_gridmap/enhanced_gridmap.gd b/addons/enhanced_gridmap/enhanced_gridmap.gd index eb265d6..ca853cf 100644 --- a/addons/enhanced_gridmap/enhanced_gridmap.gd +++ b/addons/enhanced_gridmap/enhanced_gridmap.gd @@ -55,7 +55,8 @@ func _ready(): # Safety check: Don't auto-randomize if game mode manages its own arena if not (ResourceLoader.exists("res://scripts/managers/lobby_manager.gd") \ 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() validate_item_indices() @@ -118,7 +119,7 @@ func generate_grid(floor_index: int = -1): var rng = RandomNumberGenerator.new() rng.randomize() 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: generate_floor(0) @@ -137,7 +138,7 @@ func generate_grid(floor_index: int = -1): var rng = RandomNumberGenerator.new() rng.randomize() var shape = rng.randi() % 4 - generator.generate_map(self, columns, rows, shape) + generator.generate_map(self , columns, rows, shape) else: clear_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): var x = data[i] - var z = data[i+1] - var item = data[i+2] + var z = data[i + 1] + 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) update_grid_data() diff --git a/scenes/main.gd b/scenes/main.gd index cd982cf..30ef440 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -34,10 +34,10 @@ func _ready(): # Setup UI ui_manager.setup_action_buttons(_set_action_state_callback) ui_manager.setup_playerboard_ui() - ui_manager.setup_timer_labels(self) - ui_manager.setup_playerboard_label(self) # NEW - ui_manager.setup_leaderboard_ui(self) - ui_manager.setup_powerup_bar_ui(self) + ui_manager.setup_timer_labels(self ) + ui_manager.setup_playerboard_label(self ) # NEW + ui_manager.setup_leaderboard_ui(self ) + ui_manager.setup_powerup_bar_ui(self ) # GlobalMatchTimer is now static in main.tscn - no setup needed # NetworkPanel is visible during gameplay @@ -71,7 +71,6 @@ func _ready(): stand_spawner.name = "StandSpawner" 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/portal_door.tscn") add_child(stand_spawner) 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.name = "UIManager" add_child(ui_manager) - ui_manager.initialize(self) + ui_manager.initialize(self ) # Goals cycle manager for 60-second timer and scoring goals_cycle_manager = load("res://scripts/managers/goals_cycle_manager.gd").new() goals_cycle_manager.name = "GoalsCycleManager" add_child(goals_cycle_manager) - goals_cycle_manager.initialize(self) + goals_cycle_manager.initialize(self ) # Stop n Go manager for phase-based gameplay 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.name = "PortalModeManager" 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 = load("res://scripts/managers/screen_shake.gd").new() @@ -120,7 +119,7 @@ func _init_managers(): touch_controls.name = "TouchControls" add_child(touch_controls) - touch_controls.initialize(self) + touch_controls.initialize(self ) # NEW: Camera Context Manager for dynamic camera position 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.name = "ObstacleManager" add_child(obstacle_manager) - obstacle_manager.initialize(self, $EnhancedGridMap) + obstacle_manager.initialize(self , $EnhancedGridMap) # Connect signals for UI updates goals_cycle_manager.timer_updated.connect(_on_timer_updated) @@ -567,7 +566,6 @@ func _setup_client_game(): rpc_id(1, "request_full_grid_sync") - func _auto_start_from_lobby(): """Called when main.tscn is loaded from lobby - game is already connected.""" # 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 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 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 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): var enhanced_gridmap = $EnhancedGridMap 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) + # 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) # For simplicity, we trust the grid syncs via normal mechanisms or initial state. func randomize_game_grid(): - if LobbyManager.game_mode == "Stop n Go": - return # Stop n Go manages its own arena setup + if LobbyManager.game_mode == "Stop n Go" or LobbyManager.game_mode == "Tekton Doors": + return # These modes manage their own arena setup and item spawning var enhanced_gridmap = $EnhancedGridMap 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. # 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]) # 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(): rpc_id(sender_id, "sync_full_grid_data", grid_data) 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") func sync_full_grid_data(data: PackedInt32Array): @@ -1620,6 +1633,13 @@ func sync_full_grid_data(data: PackedInt32Array): stop_n_go_manager.name = "StopNGoManager" add_child(stop_n_go_manager) 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 enhanced_gridmap.set_floor_data(1, data) diff --git a/scripts/managers/lobby_manager.gd b/scripts/managers/lobby_manager.gd index 62b797c..7d0d648 100644 --- a/scripts/managers/lobby_manager.gd +++ b/scripts/managers/lobby_manager.gd @@ -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") diff --git a/scripts/managers/playerboard_manager.gd b/scripts/managers/playerboard_manager.gd index 93f7bf9..30c5f9b 100644 --- a/scripts/managers/playerboard_manager.gd +++ b/scripts/managers/playerboard_manager.gd @@ -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 diff --git a/scripts/managers/portal_mode_manager.gd b/scripts/managers/portal_mode_manager.gd index 3fc168d..ce98563 100644 --- a/scripts/managers/portal_mode_manager.gd +++ b/scripts/managers/portal_mode_manager.gd @@ -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 diff --git a/scripts/portal_door.gd b/scripts/portal_door.gd index e9fd475..83edd00 100644 --- a/scripts/portal_door.gd +++ b/scripts/portal_door.gd @@ -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)