feat: Implement Tekton Doors game mode with portal mechanics, add an in-game message bar, and introduce a pre-game countdown.
This commit is contained in:
@@ -149,6 +149,7 @@ func _ready():
|
|||||||
|
|
||||||
if game_mode_option:
|
if game_mode_option:
|
||||||
game_mode_option.item_selected.connect(_on_game_mode_selected)
|
game_mode_option.item_selected.connect(_on_game_mode_selected)
|
||||||
|
_setup_game_modes()
|
||||||
|
|
||||||
# Connect LobbyManager signals
|
# Connect LobbyManager signals
|
||||||
LobbyManager.room_list_updated.connect(_on_room_list_updated)
|
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:
|
if server_option and server_option.selected == 1:
|
||||||
NakamaManager.set_server(new_text.strip_edges())
|
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:
|
func _setup_player_slots() -> void:
|
||||||
"""Get references to all player slot nodes."""
|
"""Get references to all player slot nodes."""
|
||||||
player_slots.clear()
|
player_slots.clear()
|
||||||
|
|||||||
+64
-2
@@ -13,6 +13,7 @@ var camera_context_manager
|
|||||||
var stop_n_go_manager
|
var stop_n_go_manager
|
||||||
var stop_n_go_winner_id: int = -1 # Track who finished first in Stop n Go mode
|
var stop_n_go_winner_id: int = -1 # Track who finished first in Stop n Go mode
|
||||||
var obstacle_manager
|
var obstacle_manager
|
||||||
|
var portal_mode_manager
|
||||||
|
|
||||||
# Minimal local state
|
# Minimal local state
|
||||||
var _connection_check_timer: float = 0.0
|
var _connection_check_timer: float = 0.0
|
||||||
@@ -70,6 +71,7 @@ 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):
|
||||||
@@ -97,6 +99,13 @@ func _init_managers():
|
|||||||
add_child(stop_n_go_manager)
|
add_child(stop_n_go_manager)
|
||||||
# No direct initialize() yet, but we'll call start_game_mode later
|
# 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 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()
|
||||||
screen_shake_manager.name = "ScreenShakeManager"
|
screen_shake_manager.name = "ScreenShakeManager"
|
||||||
@@ -607,8 +616,9 @@ func _start_game():
|
|||||||
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager:
|
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager:
|
||||||
stop_n_go_manager.setup_mission_tiles()
|
stop_n_go_manager.setup_mission_tiles()
|
||||||
|
|
||||||
# Spawn Static Tektons BEFORE countdown (Free Mode only)
|
# Spawn Static Tektons BEFORE countdown (Free Mode Only)
|
||||||
if LobbyManager.game_mode != "Stop n Go":
|
# Exclude for Stop n Go and Tekton Doors
|
||||||
|
if LobbyManager.game_mode != "Stop n Go" and LobbyManager.game_mode != "Tekton Doors":
|
||||||
spawn_static_tektons()
|
spawn_static_tektons()
|
||||||
|
|
||||||
await _start_pre_game_countdown()
|
await _start_pre_game_countdown()
|
||||||
@@ -632,6 +642,13 @@ func _start_game():
|
|||||||
if goals_cycle_manager:
|
if goals_cycle_manager:
|
||||||
var match_duration = LobbyManager.get_match_duration()
|
var match_duration = LobbyManager.get_match_duration()
|
||||||
goals_cycle_manager.start_match(float(match_duration), false) # No cycles for Stop n Go
|
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:
|
elif goals_cycle_manager:
|
||||||
var match_duration = LobbyManager.get_match_duration()
|
var match_duration = LobbyManager.get_match_duration()
|
||||||
goals_cycle_manager.start_match(float(match_duration))
|
goals_cycle_manager.start_match(float(match_duration))
|
||||||
@@ -664,6 +681,12 @@ func _assign_random_spawn_positions():
|
|||||||
_assign_stop_n_go_spawn_positions(all_players)
|
_assign_stop_n_go_spawn_positions(all_players)
|
||||||
return
|
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_x = enhanced_gridmap.columns / 2
|
||||||
var mid_z = enhanced_gridmap.rows / 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
|
spawn_index += 1
|
||||||
print("[StopNGo] Assigned fixed starting block %s to player %s" % [assigned_pos, player.name])
|
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
|
# 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():
|
if nakama and nakama.has_method("is_connected_to_nakama") and not nakama.is_connected_to_nakama():
|
||||||
return false
|
return false
|
||||||
return true
|
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
|
||||||
|
|||||||
+14
-5
@@ -82,6 +82,7 @@ var is_player_moving: bool = false:
|
|||||||
set(value): if movement_manager: movement_manager.is_moving = value
|
set(value): if movement_manager: movement_manager.is_moving = value
|
||||||
|
|
||||||
var _verify_timer: float = 0.0
|
var _verify_timer: float = 0.0
|
||||||
|
var _movement_tween: Tween = null
|
||||||
var can_finish: bool:
|
var can_finish: bool:
|
||||||
get: return race_manager.can_finish if race_manager else false
|
get: return race_manager.can_finish if race_manager else false
|
||||||
set(value): if race_manager: race_manager.can_finish = value
|
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:
|
if global_position.distance_squared_to(start_world_pos) > 0.001:
|
||||||
global_position = start_world_pos
|
global_position = start_world_pos
|
||||||
|
|
||||||
var tween = create_tween()
|
if _movement_tween:
|
||||||
tween.set_trans(Tween.TRANS_LINEAR)
|
_movement_tween.kill()
|
||||||
tween.set_ease(Tween.EASE_IN_OUT)
|
_movement_tween = create_tween()
|
||||||
|
_movement_tween.set_trans(Tween.TRANS_LINEAR)
|
||||||
|
_movement_tween.set_ease(Tween.EASE_IN_OUT)
|
||||||
|
|
||||||
var step_duration = 0.25
|
var step_duration = 0.25
|
||||||
if movement_manager:
|
if movement_manager:
|
||||||
@@ -1317,12 +1320,13 @@ func start_movement_along_path(path: Array, clear_visual: bool = true):
|
|||||||
|
|
||||||
for point in path:
|
for point in path:
|
||||||
# Use global_position for consistency
|
# 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
|
var old_pos = current_position
|
||||||
current_position = Vector2i(path[-1].x, path[-1].y)
|
current_position = Vector2i(path[-1].x, path[-1].y)
|
||||||
is_player_moving = false
|
is_player_moving = false
|
||||||
|
_movement_tween = null
|
||||||
target_position = Vector2i(-1, -1)
|
target_position = Vector2i(-1, -1)
|
||||||
|
|
||||||
print("[Player] %s finished move. %s -> %s" % [name, old_pos, current_position])
|
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])
|
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
|
global_position = new_pos
|
||||||
target_visual_position = new_pos
|
target_visual_position = new_pos
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -41,7 +41,7 @@ signal scarcity_mode_changed(mode: String)
|
|||||||
# Character and area selection
|
# Character and area selection
|
||||||
var available_characters: Array[String] = ["Copper", "Dabro", "Gatot", "Pip", "Random"]
|
var available_characters: Array[String] = ["Copper", "Dabro", "Gatot", "Pip", "Random"]
|
||||||
var available_areas: Array[String] = ["Desert", "Forest", "City", "Factory"]
|
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 selected_area: String = "Desert" # Host-controlled
|
||||||
var game_mode: String = "Freemode" # Host-controlled
|
var game_mode: String = "Freemode" # Host-controlled
|
||||||
var local_character_index: int = 0 # Local player's character index
|
var local_character_index: int = 0 # Local player's character index
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cf0a3uwnf23j7
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bitnhtgysi3b7
|
||||||
Reference in New Issue
Block a user