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
@@ -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
View File
@@ -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
View File
@@ -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 ...
+21 -2
View File
@@ -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)
+1 -1
View File
@@ -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 = []
+25 -13
View File
@@ -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
+31 -18
View File
@@ -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):
+1 -1
View File
@@ -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)