feat: Introduce PlayerMovementManager to manage player movement, rotation, attack mode push mechanics, and grid-based collision detection.

This commit is contained in:
Yogi Wiguna
2026-02-20 17:54:58 +08:00
parent e90cbfe246
commit 0e4d69f7b9
8 changed files with 193 additions and 61 deletions
+98 -16
View File
@@ -409,10 +409,11 @@ func _setup_host_game():
# Ensure Bots are in the tree before assigning positions
await get_tree().process_frame
# NOW assign random spawn positions for EVERYONE (Host, Client, Bots)
# This ensures we respect the static tekton reserved zones for all characters
_assign_random_spawn_positions()
# INITIALIZE ARENA SIZE for Stop n Go BEFORE spawning players, to prevent out-of-bounds
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager:
stop_n_go_manager._setup_arena()
# Arena is set up, wait for __start_game to assign positions where Socket is open
_start_game()
func _spawn_lobby_client_sync(peer_id: int):
@@ -442,6 +443,10 @@ func _setup_client_game():
var my_id = multiplayer.get_unique_id()
print("Client setup - my peer ID: ", my_id)
# INITIALIZE ARENA SIZE for Stop n Go locally to prevent out-of-bounds before sync arrives
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager:
stop_n_go_manager._apply_arena_setup()
# Pre-spawn ALL players known from LobbyManager (including Host ID 1)
# This ensures nodes exist to receive RPCs (like 'set_spawn_position') that might arrive before full sync
var lobby_players = LobbyManager.get_players()
@@ -525,15 +530,29 @@ func _auto_start_from_lobby():
func _start_game():
if multiplayer.is_server():
# Wait for Nakama websocket to actually be open, up to 5 seconds
var nakama = get_node_or_null("/root/NakamaManager")
if nakama and nakama.has_method("is_connected_to_nakama"):
var wait_time = 0.0
while not nakama.is_connected_to_nakama() and wait_time < 5.0:
await get_tree().create_timer(0.2).timeout
wait_time += 0.2
# Allow socket/peer to stabilize before blasting RPCs
await get_tree().create_timer(2.0).timeout
# NOW assign random spawn positions for EVERYONE (Host, Client, Bots)
# This safely sends RPCs over the completed socket connection
_assign_random_spawn_positions()
GameStateManager.start_game()
rpc("sync_game_start", GameStateManager.players, TurnManager.turn_based_mode)
if can_rpc():
rpc("sync_game_start", GameStateManager.players, TurnManager.turn_based_mode)
if TurnManager.turn_based_mode:
TurnManager.reset_turn()
var next_player = TurnManager.next_turn(GameStateManager.players)
rpc("set_current_turn", next_player)
if can_rpc():
rpc("set_current_turn", next_player)
# Start the global match timer (this also starts the first cycle)
if LobbyManager.game_mode == "Stop n Go":
@@ -565,6 +584,12 @@ func _assign_random_spawn_positions():
var spawns_BR = [] # Bottom-Right
var all_spawns = [] # Fallback
# Stop n Go Custom Spawn Logic
if LobbyManager.game_mode == "Stop n Go":
var all_players = get_tree().get_nodes_in_group("Players")
_assign_stop_n_go_spawn_positions(all_players)
return
var mid_x = enhanced_gridmap.columns / 2
var mid_z = enhanced_gridmap.rows / 2
@@ -649,12 +674,49 @@ func _assign_random_spawn_positions():
# Set position and sync to all clients
player.current_position = assigned_pos
player.position = player.grid_to_world(assigned_pos)
player.is_player_moving = false
player.spawn_point_selected = true
player.rpc("set_spawn_position", assigned_pos)
if can_rpc():
player.rpc("set_spawn_position", assigned_pos)
else:
print("Critical: No spawn point found for player ", player.name)
spawn_index += 1
print("Assigned spawn %s to player %s" % [assigned_pos, player.name])
func _assign_stop_n_go_spawn_positions(all_players: Array):
"""Assigns spawns to the far left columns (Start Line) for Stop N Go mode."""
var enhanced_gridmap = $EnhancedGridMap
var valid_spawns = []
# Find all walkable tiles in the ONLY first column (x = 0)
for x in range(1):
for z in range(enhanced_gridmap.rows):
var ground = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z))
if ground == 0 or ground == 2 or ground == 6: # Walkable, Start Line, or Safe Zone
valid_spawns.append(Vector2i(x, z))
valid_spawns.shuffle()
# Sort players for deterministic assignment
all_players.sort_custom(func(a, b): return a.name.to_int() < b.name.to_int())
var spawn_index = 0
for player in all_players:
var assigned_pos = Vector2i(0, 0) # Fallback
if spawn_index < valid_spawns.size():
assigned_pos = valid_spawns[spawn_index]
# Ensure immediate 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("[StopNGo] Assigned starting block %s to player %s" % [assigned_pos, player.name])
# =============================================================================
# Tekton NPC Management
@@ -706,10 +768,8 @@ func spawn_tekton_npc():
# Generate a consistent ID/Name for sync (add index to ensure uniqueness)
var tekton_id = Time.get_ticks_msec() + spawned_count
_create_tekton(valid_pos, tekton_id)
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
if multiplayer.get_peers().size() > 0:
rpc("sync_spawn_tekton", valid_pos, tekton_id)
if can_rpc():
rpc("sync_spawn_tekton", valid_pos, tekton_id)
spawned_count += 1
print("[Main] Spawned Tekton %d at %s" % [spawned_count, valid_pos])
@@ -1423,12 +1483,26 @@ func request_full_grid_sync():
print("[Main] Grid sync requested by %d" % sender_id)
var enhanced_gridmap = $EnhancedGridMap
if enhanced_gridmap:
var grid_data = enhanced_gridmap.get_floor_data(1) # Sync Floor 1 (Items)
print("[Main] Server: Prepared grid data. Size: %d. Sending to %d..." % [grid_data.size(), sender_id])
# Force send (Server is usually always connected)
rpc_id(sender_id, "sync_full_grid_data", grid_data)
print("[Main] Server: Sent rpc_id to %d" % sender_id)
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager:
# Resync Full Arena for Stop n Go
var floor0_data = enhanced_gridmap.get_floor_data(0)
var floor1_data = enhanced_gridmap.get_floor_data(1)
rpc_id(sender_id, "sync_full_grid_data_stop_n_go", floor0_data, floor1_data, enhanced_gridmap.columns, enhanced_gridmap.rows)
# Resync Phase and Missions since they might have missed the initial broadcast
var phase_name = "GO" if stop_n_go_manager.current_phase == stop_n_go_manager.Phase.GO else "STOP"
stop_n_go_manager.rpc_id(sender_id, "sync_phase", phase_name, stop_n_go_manager.phase_timer)
if stop_n_go_manager.player_missions.has(sender_id):
var mission_dict = {sender_id: stop_n_go_manager.player_missions[sender_id]}
stop_n_go_manager.rpc_id(sender_id, "sync_missions", mission_dict)
else:
var grid_data = enhanced_gridmap.get_floor_data(1) # Sync Floor 1 (Items)
print("[Main] Server: Prepared grid data. Size: %d. Sending to %d..." % [grid_data.size(), sender_id])
# Force send (Server is usually always connected)
rpc_id(sender_id, "sync_full_grid_data", grid_data)
print("[Main] Server: Sent rpc_id to %d" % sender_id)
@rpc("authority", "call_local", "reliable")
func sync_full_grid_data(data: PackedInt32Array):
@@ -1939,3 +2013,11 @@ func _on_joystick_toggled(enabled: bool):
if touch_controls:
touch_controls.set_joystick_enabled(enabled)
touch_controls._save_settings()
func can_rpc() -> bool:
if not multiplayer.has_multiplayer_peer() or multiplayer.multiplayer_peer.get_connection_status() != MultiplayerPeer.CONNECTION_CONNECTED:
return false
var nakama = get_node_or_null("/root/NakamaManager")
if nakama and nakama.has_method("is_connected_to_nakama") and not nakama.is_connected_to_nakama():
return false
return true
+9 -10
View File
@@ -237,9 +237,9 @@ func _ready():
set_process_input(false)
set_process_unhandled_input(false)
# Set initial position for bots
# Set initial position for bots (Only if NOT randomized/Stop N Go by server)
if enhanced_gridmap:
if is_multiplayer_authority():
if is_multiplayer_authority() and not LobbyManager.get_randomize_spawn() and LobbyManager.game_mode != "Stop n Go":
current_position = _find_random_spawn_position()
update_player_position(current_position)
spawn_point_selected = true
@@ -270,12 +270,12 @@ func _ready():
enhanced_gridmap.set_diagonal_movement(use_diagonal_movement)
# Only set position if not using random spawn (host will assign via RPC)
if not LobbyManager.get_randomize_spawn() and not spawn_point_selected:
if not LobbyManager.get_randomize_spawn() and LobbyManager.game_mode != "Stop n Go" and not spawn_point_selected:
current_position = find_valid_starting_position()
update_player_position(current_position)
# Ensure proper initial positioning
if not LobbyManager.get_randomize_spawn() and not spawn_point_selected:
if not LobbyManager.get_randomize_spawn() and LobbyManager.game_mode != "Stop n Go" and not spawn_point_selected:
global_position = Vector3(
current_position.x * 1 + 1 * 0.5,
1.0,
@@ -1888,11 +1888,7 @@ func set_spawn_position(pos: Vector2i):
# Clear any spawn highlights
clear_spawn_highlights()
# Update visual position
var new_pos = Vector3(
current_position.x * cell_size.x + cell_size.x * 0.5,
cell_size.y,
current_position.y * cell_size.z + cell_size.z * 0.5
) + cell_offset
var new_pos = grid_to_world(pos)
print("[Player %s] set_spawn_position: Grid %s -> World %s (CellSize: %s)" % [name, pos, new_pos, cell_size])
@@ -2068,7 +2064,10 @@ func update_active_player_indicator():
color = Color.YELLOW
# Apply visual tint to character model across network
rpc("sync_modulate", color)
if can_rpc():
rpc("sync_modulate", color)
else:
sync_modulate(color) # Apply locally if offline/not ready
func knock_tekton():
# ... legacy or helper function ...