feat: Introduce PlayerMovementManager to manage player movement, rotation, attack mode push mechanics, and grid-based collision detection.
This commit is contained in:
@@ -555,6 +555,13 @@ func find_path(start: Vector2, end: Vector2, floor_index: int = 0, clear_path_vi
|
|||||||
|
|
||||||
var start_point = start.y * columns + start.x
|
var start_point = start.y * columns + start.x
|
||||||
var end_point = end.y * columns + end.x
|
var end_point = end.y * columns + end.x
|
||||||
|
|
||||||
|
if not is_position_valid(Vector2i(int(start.x), int(start.y))) or not is_position_valid(Vector2i(int(end.x), int(end.y))):
|
||||||
|
return []
|
||||||
|
|
||||||
|
if not astar.has_point(start_point) or not astar.has_point(end_point):
|
||||||
|
return []
|
||||||
|
|
||||||
path = astar.get_point_path(start_point, end_point)
|
path = astar.get_point_path(start_point, end_point)
|
||||||
|
|
||||||
if visualize:
|
if visualize:
|
||||||
|
|||||||
+98
-16
@@ -409,10 +409,11 @@ func _setup_host_game():
|
|||||||
# Ensure Bots are in the tree before assigning positions
|
# Ensure Bots are in the tree before assigning positions
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
|
|
||||||
# NOW assign random spawn positions for EVERYONE (Host, Client, Bots)
|
# INITIALIZE ARENA SIZE for Stop n Go BEFORE spawning players, to prevent out-of-bounds
|
||||||
# This ensures we respect the static tekton reserved zones for all characters
|
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager:
|
||||||
_assign_random_spawn_positions()
|
stop_n_go_manager._setup_arena()
|
||||||
|
|
||||||
|
# Arena is set up, wait for __start_game to assign positions where Socket is open
|
||||||
_start_game()
|
_start_game()
|
||||||
|
|
||||||
func _spawn_lobby_client_sync(peer_id: int):
|
func _spawn_lobby_client_sync(peer_id: int):
|
||||||
@@ -442,6 +443,10 @@ func _setup_client_game():
|
|||||||
var my_id = multiplayer.get_unique_id()
|
var my_id = multiplayer.get_unique_id()
|
||||||
print("Client setup - my peer ID: ", my_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)
|
# 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
|
# This ensures nodes exist to receive RPCs (like 'set_spawn_position') that might arrive before full sync
|
||||||
var lobby_players = LobbyManager.get_players()
|
var lobby_players = LobbyManager.get_players()
|
||||||
@@ -525,15 +530,29 @@ func _auto_start_from_lobby():
|
|||||||
|
|
||||||
func _start_game():
|
func _start_game():
|
||||||
if multiplayer.is_server():
|
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
|
# Allow socket/peer to stabilize before blasting RPCs
|
||||||
await get_tree().create_timer(2.0).timeout
|
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()
|
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:
|
if TurnManager.turn_based_mode:
|
||||||
TurnManager.reset_turn()
|
TurnManager.reset_turn()
|
||||||
var next_player = TurnManager.next_turn(GameStateManager.players)
|
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)
|
# Start the global match timer (this also starts the first cycle)
|
||||||
if LobbyManager.game_mode == "Stop n Go":
|
if LobbyManager.game_mode == "Stop n Go":
|
||||||
@@ -565,6 +584,12 @@ func _assign_random_spawn_positions():
|
|||||||
var spawns_BR = [] # Bottom-Right
|
var spawns_BR = [] # Bottom-Right
|
||||||
var all_spawns = [] # Fallback
|
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_x = enhanced_gridmap.columns / 2
|
||||||
var mid_z = enhanced_gridmap.rows / 2
|
var mid_z = enhanced_gridmap.rows / 2
|
||||||
|
|
||||||
@@ -649,12 +674,49 @@ func _assign_random_spawn_positions():
|
|||||||
# Set position and sync to all clients
|
# Set position and sync to all clients
|
||||||
player.current_position = assigned_pos
|
player.current_position = assigned_pos
|
||||||
player.position = player.grid_to_world(assigned_pos)
|
player.position = player.grid_to_world(assigned_pos)
|
||||||
|
player.is_player_moving = false
|
||||||
player.spawn_point_selected = true
|
player.spawn_point_selected = true
|
||||||
player.rpc("set_spawn_position", assigned_pos)
|
if can_rpc():
|
||||||
|
player.rpc("set_spawn_position", assigned_pos)
|
||||||
else:
|
else:
|
||||||
print("Critical: No spawn point found for player ", player.name)
|
print("Critical: No spawn point found for player ", player.name)
|
||||||
|
|
||||||
spawn_index += 1
|
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
|
# Tekton NPC Management
|
||||||
@@ -706,10 +768,8 @@ func spawn_tekton_npc():
|
|||||||
# Generate a consistent ID/Name for sync (add index to ensure uniqueness)
|
# Generate a consistent ID/Name for sync (add index to ensure uniqueness)
|
||||||
var tekton_id = Time.get_ticks_msec() + spawned_count
|
var tekton_id = Time.get_ticks_msec() + spawned_count
|
||||||
_create_tekton(valid_pos, tekton_id)
|
_create_tekton(valid_pos, tekton_id)
|
||||||
|
if can_rpc():
|
||||||
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
|
rpc("sync_spawn_tekton", valid_pos, tekton_id)
|
||||||
if multiplayer.get_peers().size() > 0:
|
|
||||||
rpc("sync_spawn_tekton", valid_pos, tekton_id)
|
|
||||||
|
|
||||||
spawned_count += 1
|
spawned_count += 1
|
||||||
print("[Main] Spawned Tekton %d at %s" % [spawned_count, valid_pos])
|
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)
|
print("[Main] Grid sync requested by %d" % sender_id)
|
||||||
var enhanced_gridmap = $EnhancedGridMap
|
var enhanced_gridmap = $EnhancedGridMap
|
||||||
if enhanced_gridmap:
|
if enhanced_gridmap:
|
||||||
var grid_data = enhanced_gridmap.get_floor_data(1) # Sync Floor 1 (Items)
|
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager:
|
||||||
print("[Main] Server: Prepared grid data. Size: %d. Sending to %d..." % [grid_data.size(), sender_id])
|
# Resync Full Arena for Stop n Go
|
||||||
|
var floor0_data = enhanced_gridmap.get_floor_data(0)
|
||||||
# Force send (Server is usually always connected)
|
var floor1_data = enhanced_gridmap.get_floor_data(1)
|
||||||
rpc_id(sender_id, "sync_full_grid_data", grid_data)
|
rpc_id(sender_id, "sync_full_grid_data_stop_n_go", floor0_data, floor1_data, enhanced_gridmap.columns, enhanced_gridmap.rows)
|
||||||
print("[Main] Server: Sent rpc_id to %d" % sender_id)
|
|
||||||
|
# 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")
|
@rpc("authority", "call_local", "reliable")
|
||||||
func sync_full_grid_data(data: PackedInt32Array):
|
func sync_full_grid_data(data: PackedInt32Array):
|
||||||
@@ -1939,3 +2013,11 @@ func _on_joystick_toggled(enabled: bool):
|
|||||||
if touch_controls:
|
if touch_controls:
|
||||||
touch_controls.set_joystick_enabled(enabled)
|
touch_controls.set_joystick_enabled(enabled)
|
||||||
touch_controls._save_settings()
|
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
@@ -237,9 +237,9 @@ func _ready():
|
|||||||
set_process_input(false)
|
set_process_input(false)
|
||||||
set_process_unhandled_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 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()
|
current_position = _find_random_spawn_position()
|
||||||
update_player_position(current_position)
|
update_player_position(current_position)
|
||||||
spawn_point_selected = true
|
spawn_point_selected = true
|
||||||
@@ -270,12 +270,12 @@ func _ready():
|
|||||||
enhanced_gridmap.set_diagonal_movement(use_diagonal_movement)
|
enhanced_gridmap.set_diagonal_movement(use_diagonal_movement)
|
||||||
|
|
||||||
# Only set position if not using random spawn (host will assign via RPC)
|
# 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()
|
current_position = find_valid_starting_position()
|
||||||
update_player_position(current_position)
|
update_player_position(current_position)
|
||||||
|
|
||||||
# Ensure proper initial positioning
|
# 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(
|
global_position = Vector3(
|
||||||
current_position.x * 1 + 1 * 0.5,
|
current_position.x * 1 + 1 * 0.5,
|
||||||
1.0,
|
1.0,
|
||||||
@@ -1888,11 +1888,7 @@ func set_spawn_position(pos: Vector2i):
|
|||||||
# Clear any spawn highlights
|
# Clear any spawn highlights
|
||||||
clear_spawn_highlights()
|
clear_spawn_highlights()
|
||||||
# Update visual position
|
# Update visual position
|
||||||
var new_pos = Vector3(
|
var new_pos = grid_to_world(pos)
|
||||||
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
|
|
||||||
|
|
||||||
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])
|
||||||
|
|
||||||
@@ -2068,7 +2064,10 @@ func update_active_player_indicator():
|
|||||||
color = Color.YELLOW
|
color = Color.YELLOW
|
||||||
|
|
||||||
# Apply visual tint to character model across network
|
# 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():
|
func knock_tekton():
|
||||||
# ... legacy or helper function ...
|
# ... legacy or helper function ...
|
||||||
|
|||||||
@@ -116,8 +116,13 @@ func _run_ai_tick():
|
|||||||
# Don't make new decisions while moving
|
# Don't make new decisions while moving
|
||||||
if actor.is_player_moving:
|
if actor.is_player_moving:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# STOP N GO: Red light freezing logic
|
||||||
|
if _should_freeze_for_stop_n_go():
|
||||||
|
# print("[BotController] %s freezes for STOP phase!" % actor.name)
|
||||||
|
return
|
||||||
|
|
||||||
print("[BotController] AI Tick: evaluating priorities...")
|
# print("[BotController] AI Tick: evaluating priorities...")
|
||||||
|
|
||||||
# Evaluate board status
|
# Evaluate board status
|
||||||
var board_fullness = _get_board_fullness_ratio()
|
var board_fullness = _get_board_fullness_ratio()
|
||||||
@@ -401,7 +406,6 @@ func _try_move() -> bool:
|
|||||||
else:
|
else:
|
||||||
# PATHFINDING FAILED! (Likely stuck on wall/stand)
|
# PATHFINDING FAILED! (Likely stuck on wall/stand)
|
||||||
# Attempt UNSTUCK move to any adjacent valid tile
|
# Attempt UNSTUCK move to any adjacent valid tile
|
||||||
print("[BotController] Pathfinding failed for %s. Attempting UNSTUCK move." % actor.name)
|
|
||||||
return await _try_unstuck_move()
|
return await _try_unstuck_move()
|
||||||
|
|
||||||
# Execute SINGLE STEP movement using player manager
|
# Execute SINGLE STEP movement using player manager
|
||||||
@@ -409,6 +413,7 @@ func _try_move() -> bool:
|
|||||||
_is_processing_action = true
|
_is_processing_action = true
|
||||||
_current_action = "moving"
|
_current_action = "moving"
|
||||||
|
|
||||||
|
|
||||||
# Safety timeout to prevent infinite loop
|
# Safety timeout to prevent infinite loop
|
||||||
var max_wait_time = 2.0
|
var max_wait_time = 2.0
|
||||||
var elapsed = 0.0
|
var elapsed = 0.0
|
||||||
@@ -427,6 +432,20 @@ func _try_move() -> bool:
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
func _should_freeze_for_stop_n_go() -> bool:
|
||||||
|
"""Check if the bot should intentionally skip its turn during STOP phase outside of safe zones."""
|
||||||
|
var main = get_tree().root.get_node_or_null("Main")
|
||||||
|
if not main: return false
|
||||||
|
|
||||||
|
var sng_manager = main.get_node_or_null("StopNGoManager")
|
||||||
|
if sng_manager and sng_manager.get("is_active") and sng_manager.get("current_phase") == 1: # Phase.STOP is 1
|
||||||
|
# Check if we are outside the safe zone
|
||||||
|
var tile = enhanced_gridmap.get_cell_item(Vector3i(actor.current_position.x, 0, actor.current_position.y))
|
||||||
|
if tile != sng_manager.TILE_SAFE:
|
||||||
|
return true # Red Light! Freeze!
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
func _try_unstuck_move() -> bool:
|
func _try_unstuck_move() -> bool:
|
||||||
"""Randomly move to ANY adjacent valid tile to escape sticky situations."""
|
"""Randomly move to ANY adjacent valid tile to escape sticky situations."""
|
||||||
var neighbors = enhanced_gridmap.get_neighbors(actor.current_position, 0)
|
var neighbors = enhanced_gridmap.get_neighbors(actor.current_position, 0)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ extends Node
|
|||||||
signal game_started()
|
signal game_started()
|
||||||
signal game_state_changed()
|
signal game_state_changed()
|
||||||
|
|
||||||
@export var enable_bots: bool = true
|
@export var enable_bots: bool = false
|
||||||
@export var max_players: int = 8
|
@export var max_players: int = 8
|
||||||
|
|
||||||
var players: Array = []
|
var players: Array = []
|
||||||
|
|||||||
@@ -37,10 +37,16 @@ func rotate_towards_target(target_pos: Vector2i):
|
|||||||
var direction = Vector3(target_pos.x - player.current_position.x, 0, target_pos.y - player.current_position.y)
|
var direction = Vector3(target_pos.x - player.current_position.x, 0, target_pos.y - player.current_position.y)
|
||||||
if direction != Vector3.ZERO:
|
if direction != Vector3.ZERO:
|
||||||
target_rotation = atan2(direction.x, direction.z)
|
target_rotation = atan2(direction.x, direction.z)
|
||||||
# Sync rotation to other clients
|
if player.is_multiplayer_authority() and _can_rpc():
|
||||||
if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc():
|
|
||||||
player.rpc("sync_rotation", target_rotation)
|
player.rpc("sync_rotation", target_rotation)
|
||||||
|
|
||||||
|
func _can_rpc() -> bool:
|
||||||
|
if not player or not player.is_inside_tree() or not player.multiplayer.has_multiplayer_peer():
|
||||||
|
return false
|
||||||
|
if player.multiplayer.multiplayer_peer.get_connection_status() != MultiplayerPeer.CONNECTION_CONNECTED:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
func simple_move_to(grid_position: Vector2i) -> bool:
|
func simple_move_to(grid_position: Vector2i) -> bool:
|
||||||
if is_moving:
|
if is_moving:
|
||||||
var direction = grid_position - player.current_position
|
var direction = grid_position - player.current_position
|
||||||
@@ -49,9 +55,11 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
if not player.is_multiplayer_authority():
|
if not player.is_multiplayer_authority():
|
||||||
|
# print("[Move] Failed: Not authority for ", player.name)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
if player.get("is_frozen"):
|
if player.get("is_frozen"):
|
||||||
|
print("[Move] Failed: Player is frozen")
|
||||||
return false
|
return false
|
||||||
|
|
||||||
# Stop n Go Mode Violation Check
|
# Stop n Go Mode Violation Check
|
||||||
@@ -60,6 +68,7 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
|||||||
var manager = main.get_node_or_null("StopNGoManager")
|
var manager = main.get_node_or_null("StopNGoManager")
|
||||||
if manager and manager.has_method("check_movement_violation"):
|
if manager and manager.has_method("check_movement_violation"):
|
||||||
if manager.check_movement_violation(player.name.to_int(), player.current_position, grid_position):
|
if manager.check_movement_violation(player.name.to_int(), player.current_position, grid_position):
|
||||||
|
print("[Move] Failed: Stop N Go Violation")
|
||||||
return false
|
return false
|
||||||
|
|
||||||
var distance: int
|
var distance: int
|
||||||
@@ -68,13 +77,12 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
|||||||
else:
|
else:
|
||||||
distance = abs(grid_position.x - player.current_position.x) + abs(grid_position.y - player.current_position.y)
|
distance = abs(grid_position.x - player.current_position.x) + abs(grid_position.y - player.current_position.y)
|
||||||
|
|
||||||
if distance != 1:
|
|
||||||
return false # Only single-step moves allowed
|
|
||||||
|
|
||||||
if not enhanced_gridmap.is_position_valid(grid_position):
|
if not enhanced_gridmap.is_position_valid(grid_position):
|
||||||
|
# print("[Move] Failed: Position not valid on GridMap %s" % grid_position)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
if player.has_method("can_move_to_finish") and not player.can_move_to_finish(grid_position):
|
if player.has_method("can_move_to_finish") and not player.can_move_to_finish(grid_position):
|
||||||
|
print("[Move] Failed: Cannot move to finish yet")
|
||||||
return false
|
return false
|
||||||
|
|
||||||
var cell_item = enhanced_gridmap.get_cell_item(Vector3i(grid_position.x, 0, grid_position.y))
|
var cell_item = enhanced_gridmap.get_cell_item(Vector3i(grid_position.x, 0, grid_position.y))
|
||||||
@@ -83,10 +91,12 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
|||||||
var is_wall_passable = player.get("is_invisible") and cell_item == 4
|
var is_wall_passable = player.get("is_invisible") and cell_item == 4
|
||||||
|
|
||||||
if (cell_item == -1 or cell_item in enhanced_gridmap.non_walkable_items) and not is_wall_passable:
|
if (cell_item == -1 or cell_item in enhanced_gridmap.non_walkable_items) and not is_wall_passable:
|
||||||
|
print("[Move] Failed: Cell Item %d is non-walkable" % cell_item)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
# PHYSICS CHECK: Ensure no static obstacles (like Stands) are blocking the path
|
# PHYSICS CHECK: Ensure no static obstacles (like Stands) are blocking the path
|
||||||
if _is_position_blocked_by_physics(grid_position):
|
if _is_position_blocked_by_physics(grid_position):
|
||||||
|
print("[Move] Failed: Blocked by physics raycast at %s" % grid_position)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
if player.is_position_occupied(grid_position):
|
if player.is_position_occupied(grid_position):
|
||||||
@@ -110,7 +120,7 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
|||||||
|
|
||||||
rotate_towards_target(grid_position)
|
rotate_towards_target(grid_position)
|
||||||
|
|
||||||
if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc():
|
if player.is_multiplayer_authority() and _can_rpc():
|
||||||
if player.has_method("sync_walk_animation"):
|
if player.has_method("sync_walk_animation"):
|
||||||
player.rpc("sync_walk_animation")
|
player.rpc("sync_walk_animation")
|
||||||
|
|
||||||
@@ -119,7 +129,7 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
|||||||
|
|
||||||
current_move_direction = grid_position - player.current_position
|
current_move_direction = grid_position - player.current_position
|
||||||
|
|
||||||
if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc():
|
if player.is_multiplayer_authority() and _can_rpc():
|
||||||
player.rpc("start_movement_along_path", path, not (player.is_bot or player.is_in_group("Bots")))
|
player.rpc("start_movement_along_path", path, not (player.is_bot or player.is_in_group("Bots")))
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -151,7 +161,7 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
|
|||||||
|
|
||||||
# 1. Drop Victim's Tiles
|
# 1. Drop Victim's Tiles
|
||||||
if other_player.has_method("drop_all_tiles"):
|
if other_player.has_method("drop_all_tiles"):
|
||||||
if player.has_method("can_rpc") and player.can_rpc():
|
if _can_rpc():
|
||||||
other_player.rpc("drop_all_tiles") # Sync drop
|
other_player.rpc("drop_all_tiles") # Sync drop
|
||||||
|
|
||||||
# 2. Spawn PowerUps around Victim
|
# 2. Spawn PowerUps around Victim
|
||||||
@@ -169,17 +179,17 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
|
|||||||
not _is_position_blocked_by_physics(pushed_to_pos):
|
not _is_position_blocked_by_physics(pushed_to_pos):
|
||||||
# Valid push
|
# Valid push
|
||||||
var push_path = [Vector2(pushed_to_pos.x, pushed_to_pos.y)]
|
var push_path = [Vector2(pushed_to_pos.x, pushed_to_pos.y)]
|
||||||
if player.has_method("can_rpc") and player.can_rpc():
|
if _can_rpc():
|
||||||
other_player.rpc("start_movement_along_path", push_path, false)
|
other_player.rpc("start_movement_along_path", push_path, false)
|
||||||
other_player.target_position = pushed_to_pos # Logical update
|
other_player.target_position = pushed_to_pos # Logical update
|
||||||
|
|
||||||
# Apply stun/freeze effect as requested (same as wall stagger)
|
# Apply stun/freeze effect as requested (same as wall stagger)
|
||||||
if player.has_method("can_rpc") and player.can_rpc():
|
if _can_rpc():
|
||||||
other_player.rpc("apply_stagger", 1.5)
|
other_player.rpc("apply_stagger", 1.5)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Wall/Blocked -> Stagger in place
|
# Wall/Blocked -> Stagger in place
|
||||||
if player.has_method("can_rpc") and player.can_rpc():
|
if _can_rpc():
|
||||||
other_player.rpc("apply_stagger", 1.5)
|
other_player.rpc("apply_stagger", 1.5)
|
||||||
|
|
||||||
# 4. Consume Boost (Full) - One hit per charge
|
# 4. Consume Boost (Full) - One hit per charge
|
||||||
@@ -453,7 +463,9 @@ func _is_position_blocked_by_physics(target_pos: Vector2i) -> bool:
|
|||||||
var result = space_state.intersect_ray(query)
|
var result = space_state.intersect_ray(query)
|
||||||
if result:
|
if result:
|
||||||
if result.collider != player:
|
if result.collider != player:
|
||||||
# print("Movement Blocked by Physics Body: ", result.collider.name)
|
# ONLY block if it's a Static Tekton Stand
|
||||||
return true
|
# Ignore GridMap floors/walls, which are handled by get_cell_item rules
|
||||||
|
if result.collider.name.find("StaticTektonStand") != -1 or result.collider.is_in_group("StaticTektonStands") or result.collider.has_method("is_stand"):
|
||||||
|
return true
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const TILE_OBSTACLE = 4 # Wall
|
|||||||
var hud_layer: CanvasLayer
|
var hud_layer: CanvasLayer
|
||||||
var phase_label: Label
|
var phase_label: Label
|
||||||
var mission_label: Label
|
var mission_label: Label
|
||||||
|
var red_tint_overlay: ColorRect
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
set_process(false)
|
set_process(false)
|
||||||
@@ -37,6 +38,13 @@ func _setup_hud():
|
|||||||
hud_layer.visible = false
|
hud_layer.visible = false
|
||||||
add_child(hud_layer)
|
add_child(hud_layer)
|
||||||
|
|
||||||
|
# Full-screen red tint overlay (below everything else in this layer, but above game)
|
||||||
|
red_tint_overlay = ColorRect.new()
|
||||||
|
red_tint_overlay.color = Color(1.0, 0.0, 0.0, 0.25) # Transparent red
|
||||||
|
red_tint_overlay.set_anchors_preset(Control.PRESET_FULL_RECT) # Cover whole screen
|
||||||
|
red_tint_overlay.visible = false # Hidden initially
|
||||||
|
hud_layer.add_child(red_tint_overlay)
|
||||||
|
|
||||||
var vbox = VBoxContainer.new()
|
var vbox = VBoxContainer.new()
|
||||||
vbox.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
vbox.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||||||
vbox.offset_right = -20
|
vbox.offset_right = -20
|
||||||
@@ -90,6 +98,10 @@ func _update_hud_visuals():
|
|||||||
if phase_label:
|
if phase_label:
|
||||||
phase_label.text = "PHASE: %s (%.0fs)" % [phase_name, max(0, phase_timer)]
|
phase_label.text = "PHASE: %s (%.0fs)" % [phase_name, max(0, phase_timer)]
|
||||||
phase_label.add_theme_color_override("font_color", Color.GREEN if current_phase == Phase.GO else Color.RED)
|
phase_label.add_theme_color_override("font_color", Color.GREEN if current_phase == Phase.GO else Color.RED)
|
||||||
|
|
||||||
|
# Toggle Red Screen Tint
|
||||||
|
if red_tint_overlay:
|
||||||
|
red_tint_overlay.visible = (current_phase == Phase.STOP)
|
||||||
|
|
||||||
var my_id = multiplayer.get_unique_id()
|
var my_id = multiplayer.get_unique_id()
|
||||||
if mission_label and player_missions.has(my_id):
|
if mission_label and player_missions.has(my_id):
|
||||||
@@ -120,7 +132,7 @@ func start_game_mode():
|
|||||||
|
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
activate_client_side() # Server also needs local processing
|
activate_client_side() # Server also needs local processing
|
||||||
_setup_arena()
|
# _setup_arena() # REMOVED: Already explicitly called in main.gd _setup_host_game to prepare floor before spawns!
|
||||||
_assign_missions()
|
_assign_missions()
|
||||||
_start_phase(Phase.GO)
|
_start_phase(Phase.GO)
|
||||||
else:
|
else:
|
||||||
@@ -133,11 +145,15 @@ func _start_phase(phase: Phase):
|
|||||||
phase_timer = GO_DURATION if phase == Phase.GO else STOP_DURATION
|
phase_timer = GO_DURATION if phase == Phase.GO else STOP_DURATION
|
||||||
|
|
||||||
var phase_name = "GO" if phase == Phase.GO else "STOP"
|
var phase_name = "GO" if phase == Phase.GO else "STOP"
|
||||||
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
|
if can_rpc():
|
||||||
if multiplayer.get_peers().size() > 0:
|
rpc("sync_phase", phase_name, phase_timer)
|
||||||
rpc("sync_phase", phase_name, phase_timer)
|
|
||||||
emit_signal("phase_changed", phase_name, phase_timer)
|
emit_signal("phase_changed", phase_name, phase_timer)
|
||||||
|
|
||||||
|
func can_rpc() -> bool:
|
||||||
|
if not multiplayer.has_multiplayer_peer() or multiplayer.multiplayer_peer.get_connection_status() != MultiplayerPeer.CONNECTION_CONNECTED:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
@rpc("authority", "call_local", "reliable")
|
||||||
func sync_phase(phase_name: String, duration: float):
|
func sync_phase(phase_name: String, duration: float):
|
||||||
if not is_active:
|
if not is_active:
|
||||||
@@ -152,9 +168,8 @@ func _setup_arena():
|
|||||||
print("[StopNGo] Setting up 22x10 Arena with Randomized Obstacles...")
|
print("[StopNGo] Setting up 22x10 Arena with Randomized Obstacles...")
|
||||||
|
|
||||||
# Explicitly sync dimensions and clear grid on all clients
|
# Explicitly sync dimensions and clear grid on all clients
|
||||||
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
|
if can_rpc():
|
||||||
if multiplayer.get_peers().size() > 0:
|
rpc("sync_arena_setup")
|
||||||
rpc("sync_arena_setup")
|
|
||||||
|
|
||||||
# Apply locally for Server (RPC is call_remote)
|
# Apply locally for Server (RPC is call_remote)
|
||||||
_apply_arena_setup()
|
_apply_arena_setup()
|
||||||
@@ -169,9 +184,9 @@ func _apply_arena_setup():
|
|||||||
var gridmap = get_node("/root/Main/EnhancedGridMap")
|
var gridmap = get_node("/root/Main/EnhancedGridMap")
|
||||||
if not gridmap: return
|
if not gridmap: return
|
||||||
|
|
||||||
# Set Size for Stop n Go
|
# Set Size for Stop n Go explicitly, bypassing setters that wipe the map
|
||||||
gridmap.columns = 22
|
gridmap.set("columns", 22)
|
||||||
gridmap.rows = 10
|
gridmap.set("rows", 10)
|
||||||
|
|
||||||
# Clear existing items on all layers
|
# Clear existing items on all layers
|
||||||
gridmap.clear()
|
gridmap.clear()
|
||||||
@@ -232,7 +247,7 @@ func _apply_arena_setup():
|
|||||||
|
|
||||||
# Sync the WHOLE grid to all clients to ensure size and stripes are correct
|
# Sync the WHOLE grid to all clients to ensure size and stripes are correct
|
||||||
var main = get_node("/root/Main")
|
var main = get_node("/root/Main")
|
||||||
if main and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
|
if main and can_rpc():
|
||||||
# Gather all floor 0 and floor 1 data
|
# Gather all floor 0 and floor 1 data
|
||||||
var floor0_data = gridmap.get_floor_data(0)
|
var floor0_data = gridmap.get_floor_data(0)
|
||||||
var floor1_data = gridmap.get_floor_data(1)
|
var floor1_data = gridmap.get_floor_data(1)
|
||||||
@@ -272,9 +287,8 @@ func _assign_missions():
|
|||||||
}
|
}
|
||||||
idx += 1
|
idx += 1
|
||||||
|
|
||||||
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
|
if can_rpc():
|
||||||
if multiplayer.get_peers().size() > 0:
|
rpc("sync_missions", player_missions)
|
||||||
rpc("sync_missions", player_missions)
|
|
||||||
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
@rpc("authority", "call_local", "reliable")
|
||||||
func sync_missions(missions: Dictionary):
|
func sync_missions(missions: Dictionary):
|
||||||
@@ -306,7 +320,7 @@ func _penalize_player(player_id: int):
|
|||||||
if player_node:
|
if player_node:
|
||||||
# Don't reset mission progress!
|
# Don't reset mission progress!
|
||||||
# Just Drop All Tiles (which already exist on player)
|
# Just Drop All Tiles (which already exist on player)
|
||||||
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
|
if can_rpc():
|
||||||
player_node.rpc("drop_all_tiles")
|
player_node.rpc("drop_all_tiles")
|
||||||
|
|
||||||
# Also send message
|
# Also send message
|
||||||
@@ -331,9 +345,8 @@ func update_mission_progress(player_id: int, tile_id: int):
|
|||||||
if player_node:
|
if player_node:
|
||||||
NotificationManager.send_message(player_node, "Mission Complete! Reach the Finish!", NotificationManager.MessageType.GOAL)
|
NotificationManager.send_message(player_node, "Mission Complete! Reach the Finish!", NotificationManager.MessageType.GOAL)
|
||||||
|
|
||||||
if multiplayer.is_server() and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
|
if multiplayer.is_server() and can_rpc():
|
||||||
if multiplayer.get_peers().size() > 0:
|
rpc("sync_mission_progress", player_id, mission["current"])
|
||||||
rpc("sync_mission_progress", player_id, mission["current"])
|
|
||||||
|
|
||||||
@rpc("any_peer", "call_local", "reliable")
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
func sync_mission_progress(player_id: int, current: int):
|
func sync_mission_progress(player_id: int, current: int):
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ func _ensure_shortcut_label(btn: Button, button_name: String):
|
|||||||
btn.add_child(shortcut_lbl)
|
btn.add_child(shortcut_lbl)
|
||||||
|
|
||||||
func _on_joystick_direction(direction: Vector2i):
|
func _on_joystick_direction(direction: Vector2i):
|
||||||
if local_player and local_player.has_method("simple_move_to"):
|
if local_player and local_player.movement_manager:
|
||||||
var target_pos = local_player.current_position + direction
|
var target_pos = local_player.current_position + direction
|
||||||
local_player.movement_manager.simple_move_to(target_pos)
|
local_player.movement_manager.simple_move_to(target_pos)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user