From 6d41f9dbc62279b5ea78015d6b64338308a69c5a Mon Sep 17 00:00:00 2001 From: Yogi Wiguna Date: Wed, 25 Feb 2026 16:58:59 +0800 Subject: [PATCH] feat: Implement Tekton Doors game mode with portal mechanics, add an in-game message bar, and introduce a pre-game countdown. --- scenes/lobby.gd | 13 + scenes/main.gd | 66 ++++- scenes/player.gd | 19 +- scenes/portal_door.tscn | 72 ++++++ scripts/managers/lobby_manager.gd | 2 +- scripts/managers/portal_mode_manager.gd | 259 ++++++++++++++++++++ scripts/managers/portal_mode_manager.gd.uid | 1 + scripts/portal_door.gd | 88 +++++++ scripts/portal_door.gd.uid | 1 + 9 files changed, 513 insertions(+), 8 deletions(-) create mode 100644 scenes/portal_door.tscn create mode 100644 scripts/managers/portal_mode_manager.gd create mode 100644 scripts/managers/portal_mode_manager.gd.uid create mode 100644 scripts/portal_door.gd create mode 100644 scripts/portal_door.gd.uid diff --git a/scenes/lobby.gd b/scenes/lobby.gd index f52ebd6..4dd9f9c 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -149,6 +149,7 @@ func _ready(): if game_mode_option: game_mode_option.item_selected.connect(_on_game_mode_selected) + _setup_game_modes() # Connect LobbyManager signals LobbyManager.room_list_updated.connect(_on_room_list_updated) @@ -212,6 +213,18 @@ func _on_server_ip_submitted(new_text: String) -> void: if server_option and server_option.selected == 1: NakamaManager.set_server(new_text.strip_edges()) +func _setup_game_modes() -> void: + if not game_mode_option: return + game_mode_option.clear() + for mode in LobbyManager.available_game_modes: + game_mode_option.add_item(mode) + + # Select current mode + for i in range(game_mode_option.item_count): + if game_mode_option.get_item_text(i) == LobbyManager.game_mode: + game_mode_option.selected = i + break + func _setup_player_slots() -> void: """Get references to all player slot nodes.""" player_slots.clear() diff --git a/scenes/main.gd b/scenes/main.gd index d5cb458..cd982cf 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -13,6 +13,7 @@ var camera_context_manager var stop_n_go_manager var stop_n_go_winner_id: int = -1 # Track who finished first in Stop n Go mode var obstacle_manager +var portal_mode_manager # Minimal local state var _connection_check_timer: float = 0.0 @@ -70,6 +71,7 @@ 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): @@ -97,6 +99,13 @@ func _init_managers(): add_child(stop_n_go_manager) # No direct initialize() yet, but we'll call start_game_mode later + # Portal manager for Tekton Doors mode + if LobbyManager.game_mode == "Tekton Doors": + 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) + # Screen shake manager for impact feedback screen_shake_manager = load("res://scripts/managers/screen_shake.gd").new() screen_shake_manager.name = "ScreenShakeManager" @@ -607,8 +616,9 @@ func _start_game(): if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager: stop_n_go_manager.setup_mission_tiles() - # Spawn Static Tektons BEFORE countdown (Free Mode only) - if LobbyManager.game_mode != "Stop n Go": + # Spawn Static Tektons BEFORE countdown (Free Mode Only) + # Exclude for Stop n Go and Tekton Doors + if LobbyManager.game_mode != "Stop n Go" and LobbyManager.game_mode != "Tekton Doors": spawn_static_tektons() await _start_pre_game_countdown() @@ -632,6 +642,13 @@ func _start_game(): if goals_cycle_manager: var match_duration = LobbyManager.get_match_duration() goals_cycle_manager.start_match(float(match_duration), false) # No cycles for Stop n Go + elif LobbyManager.game_mode == "Tekton Doors": + if portal_mode_manager: + portal_mode_manager.start_game_mode() + + if goals_cycle_manager: + var match_duration = LobbyManager.get_match_duration() + goals_cycle_manager.start_match(float(match_duration)) elif goals_cycle_manager: var match_duration = LobbyManager.get_match_duration() goals_cycle_manager.start_match(float(match_duration)) @@ -663,6 +680,12 @@ func _assign_random_spawn_positions(): var all_players = get_tree().get_nodes_in_group("Players") _assign_stop_n_go_spawn_positions(all_players) return + + # Tekton Doors Custom Spawn Logic + if LobbyManager.game_mode == "Tekton Doors": + var all_players = get_tree().get_nodes_in_group("Players") + _assign_portal_mode_spawn_positions(all_players) + return var mid_x = enhanced_gridmap.columns / 2 var mid_z = enhanced_gridmap.rows / 2 @@ -780,6 +803,33 @@ func _assign_stop_n_go_spawn_positions(all_players: Array): spawn_index += 1 print("[StopNGo] Assigned fixed starting block %s to player %s" % [assigned_pos, player.name]) +func _assign_portal_mode_spawn_positions(all_players: Array): + """Assigns spawns to different quadrants for Tekton Doors mode.""" + if not portal_mode_manager: + _assign_random_spawn_positions() # Fallback + return + + # Sort players for deterministic assignment + all_players.sort_custom(func(a, b): return a.name.to_int() < b.name.to_int()) + + var spawn_points = portal_mode_manager.get_spawn_points() + var spawn_index = 0 + + for player in all_players: + var assigned_pos = spawn_points[spawn_index % spawn_points.size()] + + # Sync + player.position = player.grid_to_world(assigned_pos) + player.current_position = assigned_pos + player.is_player_moving = false + player.spawn_point_selected = true + + if can_rpc(): + player.rpc("set_spawn_position", assigned_pos) + + spawn_index += 1 + print("[PortalMode] Assigned Room Quadrant %s to player %s" % [assigned_pos, player.name]) + # ============================================================================= # Tekton NPC Management # ============================================================================= @@ -2118,3 +2168,15 @@ func can_rpc() -> bool: if nakama and nakama.has_method("is_connected_to_nakama") and not nakama.is_connected_to_nakama(): return false return true + +@rpc("authority", "call_local", "reliable") +func display_message(message: String, type: int = 0): + """Broadcasts a message to the local player's UI. This is called via main.rpc from various managers.""" + # Find local player + var all_players = get_tree().get_nodes_in_group("Players") + for player in all_players: + # Check if this player is controlled by THIS client + if player.is_multiplayer_authority(): + if player.has_method("display_message"): + player.display_message(message, type) + break diff --git a/scenes/player.gd b/scenes/player.gd index 5b661d6..e24bced 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -82,6 +82,7 @@ var is_player_moving: bool = false: set(value): if movement_manager: movement_manager.is_moving = value var _verify_timer: float = 0.0 +var _movement_tween: Tween = null var can_finish: bool: get: return race_manager.can_finish if race_manager else false set(value): if race_manager: race_manager.can_finish = value @@ -1307,9 +1308,11 @@ func start_movement_along_path(path: Array, clear_visual: bool = true): if global_position.distance_squared_to(start_world_pos) > 0.001: global_position = start_world_pos - var tween = create_tween() - tween.set_trans(Tween.TRANS_LINEAR) - tween.set_ease(Tween.EASE_IN_OUT) + if _movement_tween: + _movement_tween.kill() + _movement_tween = create_tween() + _movement_tween.set_trans(Tween.TRANS_LINEAR) + _movement_tween.set_ease(Tween.EASE_IN_OUT) var step_duration = 0.25 if movement_manager: @@ -1317,12 +1320,13 @@ func start_movement_along_path(path: Array, clear_visual: bool = true): for point in path: # Use global_position for consistency - tween.tween_property(self , "global_position", grid_to_world(Vector2i(point.x, point.y)), step_duration) + _movement_tween.tween_property(self , "global_position", grid_to_world(Vector2i(point.x, point.y)), step_duration) - tween.tween_callback(func(): + _movement_tween.tween_callback(func(): var old_pos = current_position current_position = Vector2i(path[-1].x, path[-1].y) is_player_moving = false + _movement_tween = null target_position = Vector2i(-1, -1) print("[Player] %s finished move. %s -> %s" % [name, old_pos, current_position]) @@ -1937,6 +1941,11 @@ func set_spawn_position(pos: Vector2i): print("[Player %s] set_spawn_position: Grid %s -> World %s (CellSize: %s)" % [name, pos, new_pos, cell_size]) + if _movement_tween: + _movement_tween.kill() + _movement_tween = null + is_player_moving = false + global_position = new_pos target_visual_position = new_pos diff --git a/scenes/portal_door.tscn b/scenes/portal_door.tscn new file mode 100644 index 0000000..9f2a9b8 --- /dev/null +++ b/scenes/portal_door.tscn @@ -0,0 +1,72 @@ +[gd_scene load_steps=8 format=3 uid="uid://portal_door_001"] + +[ext_resource type="Script" path="res://scripts/portal_door.gd" id="1_script"] + +[sub_resource type="BoxMesh" id="BoxMesh_frame"] +size = Vector3(0.15, 2.2, 0.15) + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_frame"] +albedo_color = Color(0.1, 0.5, 0.8, 1) +metallic = 0.8 +roughness = 0.2 + +[sub_resource type="PlaneMesh" id="PlaneMesh_vortex"] +size = Vector2(1.4, 2.1) + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_vortex"] +transparency = 1 +albedo_color = Color(0.0, 0.6, 1.0, 0.4) +emission_enabled = true +emission = Color(0.0, 0.4, 1.0, 1) +emission_energy_multiplier = 2.0 + +[sub_resource type="BoxShape3D" id="BoxShape3D_trigger"] +size = Vector3(1.4, 2.1, 0.6) + +[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_portal"] +properties/0/path = NodePath(":target_room_id") +properties/0/spawn = true +properties/0/replication_mode = 2 +properties/1/path = NodePath(":target_door_id") +properties/1/spawn = true +properties/1/replication_mode = 2 +properties/2/path = NodePath(":is_active") +properties/2/spawn = true +properties/2/replication_mode = 2 +properties/3/path = NodePath(":portal_color") +properties/3/spawn = true +properties/3/replication_mode = 2 + +[node name="PortalDoor" type="StaticBody3D"] +script = ExtResource("1_script") + +[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."] +replication_config = SubResource("SceneReplicationConfig_portal") + +[node name="Frame_Left" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.75, 1.1, 0) +mesh = SubResource("BoxMesh_frame") +surface_material_override/0 = SubResource("StandardMaterial3D_frame") + +[node name="Frame_Right" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.75, 1.1, 0) +mesh = SubResource("BoxMesh_frame") +surface_material_override/0 = SubResource("StandardMaterial3D_frame") + +[node name="Frame_Top" type="MeshInstance3D" parent="."] +transform = Transform3D(-4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0, 1, 0, 2.2, 0) +mesh = SubResource("BoxMesh_frame") +surface_material_override/0 = SubResource("StandardMaterial3D_frame") + +[node name="Vortex" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 1.1, 0) +mesh = SubResource("PlaneMesh_vortex") +surface_material_override/0 = SubResource("StandardMaterial3D_vortex") + +[node name="Area3D" type="Area3D" parent="."] +collision_layer = 0 +collision_mask = 2 + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Area3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.1, 0) +shape = SubResource("BoxShape3D_trigger") diff --git a/scripts/managers/lobby_manager.gd b/scripts/managers/lobby_manager.gd index 9ec8094..62b797c 100644 --- a/scripts/managers/lobby_manager.gd +++ b/scripts/managers/lobby_manager.gd @@ -41,7 +41,7 @@ signal scarcity_mode_changed(mode: String) # Character and area selection var available_characters: Array[String] = ["Copper", "Dabro", "Gatot", "Pip", "Random"] var available_areas: Array[String] = ["Desert", "Forest", "City", "Factory"] -var available_game_modes: Array[String] = ["Freemode", "Stop n Go"] +var available_game_modes: Array[String] = ["Freemode", "Stop n Go", "Tekton Doors"] var selected_area: String = "Desert" # Host-controlled var game_mode: String = "Freemode" # Host-controlled var local_character_index: int = 0 # Local player's character index diff --git a/scripts/managers/portal_mode_manager.gd b/scripts/managers/portal_mode_manager.gd new file mode 100644 index 0000000..3fc168d --- /dev/null +++ b/scripts/managers/portal_mode_manager.gd @@ -0,0 +1,259 @@ +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 + 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 Size + _setup_arena_size() + + # 2. Setup Room Partitions (visual/physical walls between rooms) + _setup_room_partitions() + + # 3. Spawn Portal Doors + _spawn_portal_doors() + + # 4. Initialize Connections + _randomize_connections() + + # 5. Start Timers + swap_timer.start() + tile_refresh_timer.start() + + # 6. Initial Tile Spawn + _refresh_tiles() + +func _setup_arena_size(): + if not gridmap: return + gridmap.columns = GRID_SIZE + gridmap.rows = GRID_SIZE + gridmap.clear() + # Fill floor + for x in range(GRID_SIZE): + for z in range(GRID_SIZE): + gridmap.set_cell_item(Vector3i(x, 0, z), 0) # Normal floor + +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) + +func _spawn_portal_doors(): + var portal_scene = load("res://scenes/portal_door.tscn") + var stands_container = main.get_node_or_null("Stands") + if not stands_container: return + + 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] + 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) + door.player_entered_portal.connect(handle_portal_interaction) + + gridmap.set_cell_item(Vector3i(cfg["pos"].x, 0, cfg["pos"].y), 0) # Normal floor + +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 + + # 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[i/2 % PORTAL_COLORS.size()] + + doors[a].target_door_id = b + doors[a].portal_color = color + + doors[b].target_door_id = a + doors[b].portal_color = color + + main.rpc("display_message", "PORTALS SWITCHED!") + +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) + gridmap.set_cell_item(Vector3i(center.x, 0, 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(): + # Simple tile fill for each quadrant + 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 + + # Low chance to spawn a tile if empty + 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) + +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) diff --git a/scripts/managers/portal_mode_manager.gd.uid b/scripts/managers/portal_mode_manager.gd.uid new file mode 100644 index 0000000..4389863 --- /dev/null +++ b/scripts/managers/portal_mode_manager.gd.uid @@ -0,0 +1 @@ +uid://cf0a3uwnf23j7 diff --git a/scripts/portal_door.gd b/scripts/portal_door.gd new file mode 100644 index 0000000..e9fd475 --- /dev/null +++ b/scripts/portal_door.gd @@ -0,0 +1,88 @@ +extends StaticBody3D + +# PortalDoor.gd +# Specialized door for "Tekton Doors" mode. +# Teleports players to a target room/door when they step into it. + +signal player_entered_portal(player_node, door_node) + +@export var room_id: int = 0 +@export var door_id: int = 0 # 0: North, 1: South, 2: East, 3: West + +# State synced by PortalModeManager +var target_room_id: int = -1 +var target_door_id: int = -1 +var is_active: bool = true +var portal_color: Color = Color.WHITE: set = set_portal_color + +func set_portal_color(value: Color): + portal_color = value + _update_visuals() + +@onready var detection_area: Area3D = $Area3D + +func _ready(): + add_to_group("PortalDoors") + if detection_area: + detection_area.body_entered.connect(_on_body_entered) + + # Visual feedback: indicate door is active + _update_visuals() + +func _on_body_entered(body: Node3D): + if not is_active: return + + 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) + +var _materials_initialized: bool = false + +func _update_visuals(): + if not is_node_ready() or not is_inside_tree(): 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 + + for part_name in ["Frame_Left", "Frame_Right", "Frame_Top"]: + var frame = get_node_or_null(part_name) + if frame: + var mat = frame.get_surface_override_material(0) + if mat: + mat.albedo_color = portal_color.lerp(Color.BLACK, 0.4) + +func _initialize_unique_materials(): + var vortex = get_node_or_null("Vortex") + if vortex: + var mat = vortex.get_surface_override_material(0) + if not mat: + mat = vortex.mesh.surface_get_material(0) + + if mat: + vortex.set_surface_override_material(0, mat.duplicate()) + + for part_name in ["Frame_Left", "Frame_Right", "Frame_Top"]: + var frame = get_node_or_null(part_name) + if frame: + var mat = frame.get_surface_override_material(0) + if not mat: + mat = frame.mesh.surface_get_material(0) + + if mat: + frame.set_surface_override_material(0, mat.duplicate()) + +func get_teleport_target_position() -> Vector2i: + # This function will be called by the manager to determine WHERE the player spawns + # usually just outside the target door's position. + # For now, let's just return a placeholder that the manager will override. + return Vector2i.ZERO diff --git a/scripts/portal_door.gd.uid b/scripts/portal_door.gd.uid new file mode 100644 index 0000000..ba39d02 --- /dev/null +++ b/scripts/portal_door.gd.uid @@ -0,0 +1 @@ +uid://bitnhtgysi3b7