feat: Introduce PlayerMovementManager to manage player movement, rotation, attack mode push mechanics, and grid-based collision detection.
This commit is contained in:
@@ -116,8 +116,13 @@ func _run_ai_tick():
|
||||
# Don't make new decisions while moving
|
||||
if actor.is_player_moving:
|
||||
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
|
||||
var board_fullness = _get_board_fullness_ratio()
|
||||
@@ -401,7 +406,6 @@ func _try_move() -> bool:
|
||||
else:
|
||||
# PATHFINDING FAILED! (Likely stuck on wall/stand)
|
||||
# Attempt UNSTUCK move to any adjacent valid tile
|
||||
print("[BotController] Pathfinding failed for %s. Attempting UNSTUCK move." % actor.name)
|
||||
return await _try_unstuck_move()
|
||||
|
||||
# Execute SINGLE STEP movement using player manager
|
||||
@@ -409,6 +413,7 @@ func _try_move() -> bool:
|
||||
_is_processing_action = true
|
||||
_current_action = "moving"
|
||||
|
||||
|
||||
# Safety timeout to prevent infinite loop
|
||||
var max_wait_time = 2.0
|
||||
var elapsed = 0.0
|
||||
@@ -427,6 +432,20 @@ func _try_move() -> bool:
|
||||
|
||||
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:
|
||||
"""Randomly move to ANY adjacent valid tile to escape sticky situations."""
|
||||
var neighbors = enhanced_gridmap.get_neighbors(actor.current_position, 0)
|
||||
|
||||
@@ -5,7 +5,7 @@ extends Node
|
||||
signal game_started()
|
||||
signal game_state_changed()
|
||||
|
||||
@export var enable_bots: bool = true
|
||||
@export var enable_bots: bool = false
|
||||
@export var max_players: int = 8
|
||||
|
||||
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)
|
||||
if direction != Vector3.ZERO:
|
||||
target_rotation = atan2(direction.x, direction.z)
|
||||
# Sync rotation to other clients
|
||||
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("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:
|
||||
if is_moving:
|
||||
var direction = grid_position - player.current_position
|
||||
@@ -49,9 +55,11 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
||||
return false
|
||||
|
||||
if not player.is_multiplayer_authority():
|
||||
# print("[Move] Failed: Not authority for ", player.name)
|
||||
return false
|
||||
|
||||
if player.get("is_frozen"):
|
||||
print("[Move] Failed: Player is frozen")
|
||||
return false
|
||||
|
||||
# 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")
|
||||
if manager and manager.has_method("check_movement_violation"):
|
||||
if manager.check_movement_violation(player.name.to_int(), player.current_position, grid_position):
|
||||
print("[Move] Failed: Stop N Go Violation")
|
||||
return false
|
||||
|
||||
var distance: int
|
||||
@@ -68,13 +77,12 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
||||
else:
|
||||
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):
|
||||
# print("[Move] Failed: Position not valid on GridMap %s" % grid_position)
|
||||
return false
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
# PHYSICS CHECK: Ensure no static obstacles (like Stands) are blocking the path
|
||||
if _is_position_blocked_by_physics(grid_position):
|
||||
print("[Move] Failed: Blocked by physics raycast at %s" % grid_position)
|
||||
return false
|
||||
|
||||
if player.is_position_occupied(grid_position):
|
||||
@@ -110,7 +120,7 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
||||
|
||||
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"):
|
||||
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
|
||||
|
||||
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")))
|
||||
|
||||
return true
|
||||
@@ -151,7 +161,7 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
|
||||
|
||||
# 1. Drop Victim's 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
|
||||
|
||||
# 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):
|
||||
# Valid push
|
||||
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.target_position = pushed_to_pos # Logical update
|
||||
|
||||
# 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)
|
||||
|
||||
else:
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
if result:
|
||||
if result.collider != player:
|
||||
# print("Movement Blocked by Physics Body: ", result.collider.name)
|
||||
return true
|
||||
# ONLY block if it's a Static Tekton Stand
|
||||
# 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
|
||||
|
||||
@@ -27,6 +27,7 @@ const TILE_OBSTACLE = 4 # Wall
|
||||
var hud_layer: CanvasLayer
|
||||
var phase_label: Label
|
||||
var mission_label: Label
|
||||
var red_tint_overlay: ColorRect
|
||||
|
||||
func _ready():
|
||||
set_process(false)
|
||||
@@ -37,6 +38,13 @@ func _setup_hud():
|
||||
hud_layer.visible = false
|
||||
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()
|
||||
vbox.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||||
vbox.offset_right = -20
|
||||
@@ -90,6 +98,10 @@ func _update_hud_visuals():
|
||||
if phase_label:
|
||||
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)
|
||||
|
||||
# Toggle Red Screen Tint
|
||||
if red_tint_overlay:
|
||||
red_tint_overlay.visible = (current_phase == Phase.STOP)
|
||||
|
||||
var my_id = multiplayer.get_unique_id()
|
||||
if mission_label and player_missions.has(my_id):
|
||||
@@ -120,7 +132,7 @@ func start_game_mode():
|
||||
|
||||
if multiplayer.is_server():
|
||||
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()
|
||||
_start_phase(Phase.GO)
|
||||
else:
|
||||
@@ -133,11 +145,15 @@ func _start_phase(phase: Phase):
|
||||
phase_timer = GO_DURATION if phase == Phase.GO else STOP_DURATION
|
||||
|
||||
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 multiplayer.get_peers().size() > 0:
|
||||
rpc("sync_phase", phase_name, phase_timer)
|
||||
if can_rpc():
|
||||
rpc("sync_phase", 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")
|
||||
func sync_phase(phase_name: String, duration: float):
|
||||
if not is_active:
|
||||
@@ -152,9 +168,8 @@ func _setup_arena():
|
||||
print("[StopNGo] Setting up 22x10 Arena with Randomized Obstacles...")
|
||||
|
||||
# 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 multiplayer.get_peers().size() > 0:
|
||||
rpc("sync_arena_setup")
|
||||
if can_rpc():
|
||||
rpc("sync_arena_setup")
|
||||
|
||||
# Apply locally for Server (RPC is call_remote)
|
||||
_apply_arena_setup()
|
||||
@@ -169,9 +184,9 @@ func _apply_arena_setup():
|
||||
var gridmap = get_node("/root/Main/EnhancedGridMap")
|
||||
if not gridmap: return
|
||||
|
||||
# Set Size for Stop n Go
|
||||
gridmap.columns = 22
|
||||
gridmap.rows = 10
|
||||
# Set Size for Stop n Go explicitly, bypassing setters that wipe the map
|
||||
gridmap.set("columns", 22)
|
||||
gridmap.set("rows", 10)
|
||||
|
||||
# Clear existing items on all layers
|
||||
gridmap.clear()
|
||||
@@ -232,7 +247,7 @@ func _apply_arena_setup():
|
||||
|
||||
# Sync the WHOLE grid to all clients to ensure size and stripes are correct
|
||||
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
|
||||
var floor0_data = gridmap.get_floor_data(0)
|
||||
var floor1_data = gridmap.get_floor_data(1)
|
||||
@@ -272,9 +287,8 @@ func _assign_missions():
|
||||
}
|
||||
idx += 1
|
||||
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
|
||||
if multiplayer.get_peers().size() > 0:
|
||||
rpc("sync_missions", player_missions)
|
||||
if can_rpc():
|
||||
rpc("sync_missions", player_missions)
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_missions(missions: Dictionary):
|
||||
@@ -306,7 +320,7 @@ func _penalize_player(player_id: int):
|
||||
if player_node:
|
||||
# Don't reset mission progress!
|
||||
# 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")
|
||||
|
||||
# Also send message
|
||||
@@ -331,9 +345,8 @@ func update_mission_progress(player_id: int, tile_id: int):
|
||||
if player_node:
|
||||
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.get_peers().size() > 0:
|
||||
rpc("sync_mission_progress", player_id, mission["current"])
|
||||
if multiplayer.is_server() and can_rpc():
|
||||
rpc("sync_mission_progress", player_id, mission["current"])
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
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)
|
||||
|
||||
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
|
||||
local_player.movement_manager.simple_move_to(target_pos)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user