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:
|
||||
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()
|
||||
|
||||
+64
-2
@@ -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
|
||||
|
||||
+14
-5
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user