feat: Add initial player character, movement, network synchronization, bot AI, and game managers.
This commit is contained in:
@@ -1283,6 +1283,7 @@ func move_player_to_clicked_position(grid_position: Vector2i):
|
||||
|
||||
@rpc("any_peer", "call_local")
|
||||
func start_movement_along_path(path: Array, clear_visual: bool = true):
|
||||
print("[Player] %s starting move along path: %s" % [name, path])
|
||||
# SERVER-SIDE VIOLATION CHECK (for Stop n Go)
|
||||
if multiplayer.is_server() and LobbyManager.game_mode == "Stop n Go":
|
||||
var main = get_tree().root.get_node_or_null("Main")
|
||||
@@ -1317,10 +1318,13 @@ func start_movement_along_path(path: Array, clear_visual: bool = true):
|
||||
tween.tween_property(self , "global_position", grid_to_world(Vector2i(point.x, point.y)), step_duration)
|
||||
|
||||
tween.tween_callback(func():
|
||||
var old_pos = current_position
|
||||
current_position = Vector2i(path[-1].x, path[-1].y)
|
||||
is_player_moving = false
|
||||
target_position = Vector2i(-1, -1)
|
||||
|
||||
print("[Player] %s finished move. %s -> %s" % [name, old_pos, current_position])
|
||||
|
||||
if is_carrying_tekton and is_instance_valid(carried_tekton):
|
||||
carried_tekton.current_position = current_position
|
||||
|
||||
|
||||
+106
-51
@@ -24,6 +24,13 @@ var rng = RandomNumberGenerator.new()
|
||||
const GOAL_TILES = [7, 8, 9, 10]
|
||||
const HOLO_TILES = [11, 12, 13, 14]
|
||||
|
||||
func _normalize_tile(tile: int) -> int:
|
||||
"""Normal tiles 7-10 are goals. 11-14 are powerups and not goals."""
|
||||
# Treat holo tiles as their normal counterparts for goal matching
|
||||
if tile >= 11 and tile <= 14:
|
||||
return tile - 4 # 11->7, 12->8, etc.
|
||||
return tile
|
||||
|
||||
func _ready():
|
||||
# print("[BotController] _ready called for parent: ", get_parent().name)
|
||||
if Engine.is_editor_hint():
|
||||
@@ -98,9 +105,13 @@ func _physics_process(delta):
|
||||
else:
|
||||
_stuck_timer = 0.0
|
||||
|
||||
# Rate limiting
|
||||
# Rate limiting (with difficulty scaling for Stop n Go)
|
||||
var current_tick_rate = tick_rate
|
||||
if LobbyManager.game_mode == "Stop n Go" and actor.current_position.x > 10:
|
||||
current_tick_rate = int(tick_rate * 0.6) # 40% faster updates after column 10
|
||||
|
||||
_tick_counter += 1
|
||||
if _tick_counter < tick_rate:
|
||||
if _tick_counter < current_tick_rate:
|
||||
return
|
||||
_tick_counter = 0
|
||||
|
||||
@@ -121,9 +132,13 @@ func _run_ai_tick():
|
||||
if actor.is_player_moving:
|
||||
return
|
||||
|
||||
# STOP N GO: Don't process if already at finish line
|
||||
if LobbyManager.game_mode == "Stop n Go" and actor.current_position.x >= 21:
|
||||
return
|
||||
|
||||
# STOP N GO: Red light freezing logic
|
||||
if _should_freeze_for_stop_n_go():
|
||||
# print("[BotController] %s freezes for STOP phase!" % actor.name)
|
||||
if _should_freeze_for_stop_n_go() or actor.get("is_frozen") or actor.get("is_stop_frozen"):
|
||||
# print("[BotController] %s is frozen! Skipping tick." % actor.name)
|
||||
return
|
||||
|
||||
# print("[BotController] AI Tick: evaluating priorities...")
|
||||
@@ -132,6 +147,12 @@ func _run_ai_tick():
|
||||
var board_fullness = _get_board_fullness_ratio()
|
||||
var full_board_priority_mode = board_fullness > 0.6
|
||||
|
||||
print("[BotController] %s AI Tick. Goals: %s, Fullness: %.2f" % [actor.name, actor.goals, board_fullness])
|
||||
|
||||
# 0. BOT AGGRESSION THRESHOLD (Stop n Go)
|
||||
var is_sng = LobbyManager.game_mode == "Stop n Go"
|
||||
var can_be_aggressive = not is_sng or actor.current_position.x > 10
|
||||
|
||||
# PRIORITY OVERRIDE: If board is getting full, prioritize clearing space!
|
||||
if full_board_priority_mode:
|
||||
print("[BotController] Board fullness %.2f > 0.6! Prioritizing PUT." % board_fullness)
|
||||
@@ -140,41 +161,51 @@ func _run_ai_tick():
|
||||
return
|
||||
|
||||
# Priority 0: Attack Mode (Aggressive Chase)
|
||||
if actor.get("is_attack_mode"):
|
||||
if can_be_aggressive and actor.get("is_attack_mode"):
|
||||
print("[BotController] Attack Mode Active! Hunting targets...")
|
||||
if await _try_attack_chase():
|
||||
print("[BotController] Action Taken: Attack Pursuit")
|
||||
return
|
||||
|
||||
# Priority 1: Use power-up sabotage if conditions are met
|
||||
# Priority 1: Use power-up sabotage if conditions are met (Aggression threshold handled in planner)
|
||||
if await _try_use_powerup():
|
||||
print("[BotController] Action Taken: PowerUp")
|
||||
return
|
||||
|
||||
# Priority 2: Grab tiles (goal tiles or holo tiles)
|
||||
# Priority 2: Arrange playerboard (Finish what we have FIRST)
|
||||
if await _try_arrange():
|
||||
print("[BotController] Action Taken: Arrange")
|
||||
return
|
||||
|
||||
# Priority 3: Grab tiles (goal tiles or holo tiles)
|
||||
# ONLY if not in critical full mode (unless it's a critical needed tile?)
|
||||
# Refined: If > 80% full, disable grabbing completely unless strict conditions met inner function
|
||||
if await _try_grab():
|
||||
print("[BotController] Action Taken: Grab")
|
||||
return
|
||||
|
||||
# Priority 3: Move toward needed tiles
|
||||
|
||||
# Priority 4: Move toward needed tiles
|
||||
if await _try_move():
|
||||
print("[BotController] Action Taken: Move")
|
||||
return
|
||||
|
||||
# Priority 4: Put tiles back on grid (Standard priority)
|
||||
|
||||
# Priority 5: Put tiles back on grid (Standard priority)
|
||||
if not full_board_priority_mode:
|
||||
if await _try_put():
|
||||
print("[BotController] Action Taken: Put")
|
||||
return
|
||||
|
||||
# Priority 5: Arrange playerboard
|
||||
if await _try_arrange():
|
||||
print("[BotController] Action Taken: Arrange")
|
||||
return
|
||||
|
||||
print("[BotController] %s - No action taken (Idle). AP: %d, GoalsAchieved: %s" % [actor.name, actor.action_points, _is_goals_achieved()])
|
||||
var goals_achv = _is_goals_achieved()
|
||||
|
||||
if actor.action_points > 1: # Only print if they have multi-AP
|
||||
print("[BotController] %s AI Tick. AP: %d, GoalsAchieved: %s, Board: %s" % [actor.name, actor.action_points, str(goals_achv), str(board_fullness)])
|
||||
|
||||
# Only stop completely if objectives are met AND we are at the finish line (if applicable)
|
||||
if goals_achv:
|
||||
if is_sng and actor.current_position.x >= 21:
|
||||
return
|
||||
elif not is_sng:
|
||||
return # In standard mode, goal achievement = game over usually
|
||||
|
||||
# STALL PREVENTION: If we have AP but couldn't do anything, we are stuck.
|
||||
# Skip turn to prevent game freeze in turn-based mode.
|
||||
@@ -254,6 +285,17 @@ func _try_attack_chase() -> bool:
|
||||
if path.size() >= 2:
|
||||
var next_step = Vector2i(path[1].x, path[1].y)
|
||||
|
||||
# STOP N GO BOUNDARY PROTECTION
|
||||
if LobbyManager.game_mode == "Stop n Go" and next_step.x >= 21:
|
||||
var main = get_tree().root.get_node_or_null("Main")
|
||||
var gc_manager = main.get_node_or_null("GoalsCycleManager") if main else null
|
||||
var time_remaining = gc_manager.get_global_time_remaining() if gc_manager else 999.0
|
||||
var is_late = (gc_manager.is_match_running() and time_remaining > 0.0 and time_remaining <= 30.0) if gc_manager else false
|
||||
|
||||
if not is_late:
|
||||
# print("[BotController] %s attack blocked by boundary (Not late game yet)." % actor.name)
|
||||
return false
|
||||
|
||||
# Move to next step (If occupied by victim, movement_manager will trigger PUSH)
|
||||
if actor.movement_manager.simple_move_to(next_step):
|
||||
_is_processing_action = true
|
||||
@@ -293,13 +335,6 @@ func _try_grab() -> bool:
|
||||
if TurnManager.turn_based_mode and actor.action_points <= 0:
|
||||
return false
|
||||
|
||||
# PANIC MODE CHECK
|
||||
var main = get_tree().get_root().get_node_or_null("Main")
|
||||
if main:
|
||||
var goals_cycle_manager = main.get_node_or_null("GoalsCycleManager")
|
||||
if goals_cycle_manager and goals_cycle_manager.get_time_remaining() < 5.0:
|
||||
return false
|
||||
|
||||
if _is_playerboard_full():
|
||||
return false
|
||||
|
||||
@@ -309,7 +344,7 @@ func _try_grab() -> bool:
|
||||
pass
|
||||
|
||||
# Check if goals already achieved
|
||||
if _is_goals_achieved():
|
||||
if _is_goals_achieved() and LobbyManager.game_mode != "Stop n Go":
|
||||
return false
|
||||
|
||||
# Get tiles we need
|
||||
@@ -339,28 +374,36 @@ func _try_grab() -> bool:
|
||||
|
||||
func _find_tile_to_grab(tiles_needed: Array) -> Dictionary:
|
||||
"""Find best tile to grab from current or adjacent positions."""
|
||||
var result = {"position": null, "type": - 1}
|
||||
var result = {"position": null, "type": -1}
|
||||
|
||||
# Check current position first
|
||||
var current_cell = Vector3i(actor.current_position.x, 1, actor.current_position.y)
|
||||
var item = enhanced_gridmap.get_cell_item(current_cell)
|
||||
|
||||
print("[BotController] %s checking current pos %s. Item on Layer 1: %d" % [actor.name, actor.current_position, item])
|
||||
|
||||
# Priority: needed tiles > holo tiles > any goal tile
|
||||
if item in tiles_needed:
|
||||
var norm_item = _normalize_tile(item)
|
||||
if norm_item in tiles_needed:
|
||||
print("[BotController] %s found NEEDED tile %d (normalized %d) at current pos!" % [actor.name, item, norm_item])
|
||||
return {"position": actor.current_position, "type": item}
|
||||
if item in HOLO_TILES:
|
||||
print("[BotController] %s found HOLO tile %d at current pos." % [actor.name, item])
|
||||
result = {"position": actor.current_position, "type": item}
|
||||
|
||||
# Check adjacent cells
|
||||
var neighbors = enhanced_gridmap.get_neighbors(actor.current_position, 0)
|
||||
# print("[BotController] %s has %d walkable neighbors" % [actor.name, neighbors.size()])
|
||||
|
||||
for neighbor in neighbors:
|
||||
if not neighbor.is_walkable:
|
||||
continue
|
||||
var cell = Vector3i(neighbor.position.x, 1, neighbor.position.y)
|
||||
item = enhanced_gridmap.get_cell_item(cell)
|
||||
norm_item = _normalize_tile(item)
|
||||
|
||||
if item in tiles_needed:
|
||||
# print("[BotController] %s checking neighbor %s. Item: %d" % [actor.name, neighbor.position, item])
|
||||
|
||||
if norm_item in tiles_needed:
|
||||
print("[BotController] %s found NEEDED tile %d (normalized %d) at neighbor %s!" % [actor.name, item, norm_item, neighbor.position])
|
||||
return {"position": neighbor.position, "type": item}
|
||||
|
||||
if item in HOLO_TILES and not result.position:
|
||||
@@ -377,7 +420,7 @@ func _try_move() -> bool:
|
||||
if TurnManager.turn_based_mode and actor.action_points <= 0:
|
||||
return false
|
||||
|
||||
if _is_goals_achieved():
|
||||
if _is_goals_achieved() and LobbyManager.game_mode != "Stop n Go":
|
||||
return false
|
||||
|
||||
# Find optimal movement target
|
||||
@@ -402,21 +445,22 @@ func _try_move() -> bool:
|
||||
# Extract immediate next step from path
|
||||
next_step = Vector2i(path[1].x, path[1].y)
|
||||
else:
|
||||
# Fallback: Pathfinding failed or target is too close?
|
||||
# Check if target is adjacent and we can move directly
|
||||
# Fallback: Check if target is adjacent and we can move directly
|
||||
var dist = abs(final_target.x - actor.current_position.x) + abs(final_target.y - actor.current_position.y)
|
||||
if dist == 1:
|
||||
print("[BotController] %s performing direct move to adjacent target %s" % [actor.name, final_target])
|
||||
next_step = final_target
|
||||
else:
|
||||
# PATHFINDING FAILED! (Likely stuck on wall/stand)
|
||||
# Attempt UNSTUCK move to any adjacent valid tile
|
||||
return await _try_unstuck_move()
|
||||
print("[BotController] %s A* PATH FAILED from %s to %s. Target likely walled off or unreachable." % [actor.name, actor.current_position, final_target])
|
||||
var unstuck = await _try_unstuck_move()
|
||||
return unstuck
|
||||
|
||||
# Execute SINGLE STEP movement using player manager
|
||||
if actor.movement_manager.simple_move_to(next_step):
|
||||
_is_processing_action = true
|
||||
_current_action = "moving"
|
||||
|
||||
print("[BotController] %s started move to %s. Current Pos: %s" % [actor.name, next_step, actor.current_position])
|
||||
|
||||
# Safety timeout to prevent infinite loop
|
||||
var max_wait_time = 2.0
|
||||
@@ -426,13 +470,17 @@ func _try_move() -> bool:
|
||||
await get_tree().process_frame
|
||||
elapsed += get_process_delta_time()
|
||||
if elapsed > max_wait_time:
|
||||
print("[BotController] Movement timed out!")
|
||||
print("[BotController] %s movement TIMEOUT after %.1fs" % [actor.name, elapsed])
|
||||
break
|
||||
|
||||
if not is_instance_valid(self): return true
|
||||
_is_processing_action = false
|
||||
_current_action = "idle"
|
||||
print("[BotController] %s move finished. New Pos: %s" % [actor.name, actor.current_position])
|
||||
return true
|
||||
else:
|
||||
print("[BotController] %s simple_move_to BLOCKED (others). Trying unstuck move." % actor.name)
|
||||
return await _try_unstuck_move()
|
||||
|
||||
return false
|
||||
|
||||
@@ -451,34 +499,33 @@ func _should_freeze_for_stop_n_go() -> bool:
|
||||
return false
|
||||
|
||||
func _try_unstuck_move() -> bool:
|
||||
"""Randomly move to ANY adjacent valid tile to escape sticky situations."""
|
||||
"""Move to ANY valid neighbor to escape clumping."""
|
||||
var neighbors = enhanced_gridmap.get_neighbors(actor.current_position, 0)
|
||||
neighbors.shuffle() # Randomize to avoid oscillating
|
||||
neighbors.shuffle()
|
||||
|
||||
for n in neighbors:
|
||||
if not n.is_walkable: continue
|
||||
|
||||
var cell = Vector3i(n.position.x, 0, n.position.y) # Check Floor 0
|
||||
var item = enhanced_gridmap.get_cell_item(cell)
|
||||
|
||||
# Ensure we don't walk into a wall (Item 4) or Void (-1)
|
||||
# Obstacles should be checked by is_walkable but let's be sure
|
||||
if item == 4 or item == -1: continue
|
||||
|
||||
# Attempt move
|
||||
if actor.movement_manager.simple_move_to(n.position):
|
||||
_is_processing_action = true
|
||||
_current_action = "moving_unstuck"
|
||||
print("[BotController] Unstuck move to %s" % n.position)
|
||||
print("[BotController] %s Unstuck move initiated to %s" % [actor.name, n.position])
|
||||
|
||||
# Wait for move
|
||||
await _wait_with_variance(action_delay)
|
||||
# Proper wait for movement completion
|
||||
var max_wait = 1.5
|
||||
var elapsed = 0.0
|
||||
while actor.is_player_moving and is_instance_valid(self):
|
||||
await get_tree().process_frame
|
||||
elapsed += get_process_delta_time()
|
||||
if elapsed > max_wait: break
|
||||
|
||||
if not is_instance_valid(self): return true
|
||||
_is_processing_action = false
|
||||
_current_action = "idle"
|
||||
print("[BotController] %s Unstuck move finished at %s" % [actor.name, actor.current_position])
|
||||
return true
|
||||
|
||||
print("[BotController] %s is TRULY stuck! No valid neighbors." % actor.name)
|
||||
return false
|
||||
|
||||
# =============================================================================
|
||||
@@ -629,7 +676,15 @@ func _get_board_fullness_ratio() -> float:
|
||||
return float(occupied) / float(actor.playerboard.size())
|
||||
|
||||
func _is_goals_achieved() -> bool:
|
||||
"""Check if goal pattern is complete in any 3x3 region of playerboard."""
|
||||
"""Check if goal pattern is complete (Standard) or mission complete (Stop n Go)."""
|
||||
if LobbyManager.game_mode == "Stop n Go":
|
||||
var main = get_tree().root.get_node_or_null("Main")
|
||||
if main:
|
||||
var sng_manager = main.get_node_or_null("StopNGoManager")
|
||||
if sng_manager:
|
||||
return sng_manager.is_mission_complete(actor.name.to_int())
|
||||
|
||||
# STANDARD MODE PATTERN CHECK
|
||||
var goals_2d = []
|
||||
for i in range(3):
|
||||
var row = []
|
||||
|
||||
@@ -15,6 +15,13 @@ func _init(p_actor: Node3D, p_gridmap: Node):
|
||||
actor = p_actor
|
||||
enhanced_gridmap = p_gridmap
|
||||
|
||||
func _normalize_tile(tile: int) -> int:
|
||||
"""Normal tiles 7-10 are goals. 11-14 are powerups and not goals."""
|
||||
# If it's a holo tile, treat it as its normal counterpart for goal matching
|
||||
if tile >= 11 and tile <= 14:
|
||||
return tile - 4 # 11->7, 12->8, etc.
|
||||
return tile
|
||||
|
||||
# =============================================================================
|
||||
# Goal Analysis
|
||||
# =============================================================================
|
||||
@@ -40,7 +47,7 @@ func calculate_goal_progress() -> float:
|
||||
|
||||
total_required += 1
|
||||
var board_idx = (i + 1) * 5 + (j + 1) # Center 3x3 in 5x5 board
|
||||
if board_idx < actor.playerboard.size() and actor.playerboard[board_idx] == goal_value:
|
||||
if board_idx < actor.playerboard.size() and _normalize_tile(actor.playerboard[board_idx]) == goal_value:
|
||||
matches += 1
|
||||
|
||||
if total_required == 0:
|
||||
@@ -53,6 +60,7 @@ func get_tiles_needed() -> Array:
|
||||
var needed = []
|
||||
|
||||
if not actor or actor.goals.size() == 0:
|
||||
# print("[BotStrategicPlanner] %s - No goals assigned yet." % actor.name)
|
||||
return needed
|
||||
|
||||
for i in range(3):
|
||||
@@ -66,10 +74,11 @@ func get_tiles_needed() -> Array:
|
||||
continue
|
||||
|
||||
var board_idx = (i + 1) * 5 + (j + 1)
|
||||
if board_idx >= actor.playerboard.size() or actor.playerboard[board_idx] != goal_value:
|
||||
if board_idx >= actor.playerboard.size() or _normalize_tile(actor.playerboard[board_idx]) != goal_value:
|
||||
if not goal_value in needed:
|
||||
needed.append(goal_value)
|
||||
|
||||
# print("[BotStrategicPlanner] %s goals: %s. Needed: %s" % [actor.name, actor.goals, needed])
|
||||
return needed
|
||||
|
||||
func find_best_slot_for_tile(tile_type: int) -> int:
|
||||
@@ -238,20 +247,17 @@ func find_nearest_tile_of_type(tile_types: Array) -> Vector2i:
|
||||
|
||||
# Check center first
|
||||
var center_cell = Vector3i(current_pos.x, 1, current_pos.y)
|
||||
if enhanced_gridmap.get_cell_item(center_cell) in tile_types:
|
||||
var center_item = enhanced_gridmap.get_cell_item(center_cell)
|
||||
if _normalize_tile(center_item) in tile_types:
|
||||
return current_pos
|
||||
|
||||
for r in range(1, max_radius + 1):
|
||||
# Spiral perimeter:
|
||||
# Top row: (x-r, y-r) to (x+r, y-r)
|
||||
# Bottom row: (x-r, y+r) to (x+r, y+r)
|
||||
# Left col: (x-r, y-r+1) to (x-r, y+r-1)
|
||||
# Right col: (x+r, y-r+1) to (x+r, y+r-1)
|
||||
var found_in_layer = []
|
||||
|
||||
# We'll check the ring. Note: Manhattan distance might be better metric for "nearest"
|
||||
# but layer-by-layer is efficient for finding "close enough" quickly.
|
||||
# In Stop n Go, prefer tiles "ahead" (higher X)
|
||||
var is_stop_n_go = LobbyManager.game_mode == "Stop n Go"
|
||||
|
||||
# Check the ring
|
||||
for x_off in range(-r, r + 1):
|
||||
_check_spiral_cell(current_pos.x + x_off, current_pos.y - r, tile_types, found_in_layer) # Top
|
||||
_check_spiral_cell(current_pos.x + x_off, current_pos.y + r, tile_types, found_in_layer) # Bottom
|
||||
@@ -266,6 +272,22 @@ func find_nearest_tile_of_type(tile_types: Array) -> Vector2i:
|
||||
var min_dist = 999999
|
||||
for pos in found_in_layer:
|
||||
var dist = abs(pos.x - current_pos.x) + abs(pos.y - current_pos.y)
|
||||
|
||||
# DIRECTIONAL BIAS: In Stop n Go, penalize tiles that are "behind" us
|
||||
if is_stop_n_go:
|
||||
if current_pos.x <= 10:
|
||||
# EARLY GAME: Extremely focused on moving right
|
||||
if pos.x < current_pos.x:
|
||||
dist += 10 # Heavier penalty for backtracking
|
||||
elif pos.x > current_pos.x:
|
||||
dist -= 4 # Heavier bonus for moving forward
|
||||
else:
|
||||
# LATE GAME: Normal bias
|
||||
if pos.x < current_pos.x:
|
||||
dist += 5
|
||||
elif pos.x > current_pos.x:
|
||||
dist -= 2
|
||||
|
||||
if dist < min_dist:
|
||||
min_dist = dist
|
||||
nearest_in_layer = pos
|
||||
@@ -279,7 +301,7 @@ func _check_spiral_cell(x: int, z: int, tile_types: Array, result_array: Array):
|
||||
|
||||
var cell = Vector3i(x, 1, z)
|
||||
var item = enhanced_gridmap.get_cell_item(cell)
|
||||
if item in tile_types:
|
||||
if _normalize_tile(item) in tile_types:
|
||||
result_array.append(Vector2i(x, z))
|
||||
|
||||
# =============================================================================
|
||||
@@ -288,62 +310,184 @@ func _check_spiral_cell(x: int, z: int, tile_types: Array, result_array: Array):
|
||||
|
||||
func find_optimal_move_target() -> Vector2i:
|
||||
"""Calculate the best position to move towards."""
|
||||
var main = actor.get_tree().get_root().get_node_or_null("Main")
|
||||
var is_sng = LobbyManager.game_mode == "Stop n Go"
|
||||
var gc_manager = main.get_node_or_null("GoalsCycleManager") if main else null
|
||||
var time_left = gc_manager.get_global_time_remaining() if gc_manager else 999.0
|
||||
var is_match_running = gc_manager.is_match_running() if gc_manager else false
|
||||
var is_late_game = is_sng and is_match_running and time_left > 0.0 and time_left <= 30.0
|
||||
|
||||
# 1. STOP N GO: Reach the finish line if goals are complete
|
||||
if is_sng and main:
|
||||
var sng_manager = main.get_node_or_null("StopNGoManager")
|
||||
if sng_manager and sng_manager.is_mission_complete(actor.name.to_int()):
|
||||
# MISSION COMPLETE: We CAN finish, but should we?
|
||||
|
||||
if not is_late_game:
|
||||
# CHAOS MODE: Allow falling through to target holo tiles, but we'll limit the target X later
|
||||
# print("[BotStrategicPlanner] %s mission complete (Chaos Phase %.1fs). Roaming field." % [actor.name, time_left])
|
||||
pass
|
||||
else:
|
||||
# Late game: go to finish
|
||||
var finish_target = Vector2i(21, actor.current_position.y)
|
||||
|
||||
# Ensure finish_target is walkable
|
||||
if not _is_valid_move_target(finish_target):
|
||||
for dy in [1, -1, 2, -2]:
|
||||
var alt = Vector2i(21, actor.current_position.y + dy)
|
||||
if _is_valid_move_target(alt):
|
||||
finish_target = alt
|
||||
break
|
||||
|
||||
print("[BotStrategicPlanner] %s mission complete (Late Game %.1fs)! Heading to finish: %s" % [actor.name, time_left, finish_target])
|
||||
return finish_target
|
||||
|
||||
var sng_manager = main.get_node_or_null("StopNGoManager") if main else null
|
||||
var is_mission_complete = sng_manager.is_mission_complete(actor.name.to_int()) if sng_manager else false
|
||||
|
||||
var needed_tiles = get_tiles_needed()
|
||||
|
||||
# First: move toward tiles we need
|
||||
if needed_tiles.size() > 0:
|
||||
var target = find_nearest_tile_of_type(needed_tiles)
|
||||
if target != Vector2i(-1, -1):
|
||||
return _get_adjacent_position(target)
|
||||
# Priority targets: needed tiles > holo tiles > any goal tile
|
||||
var targets_to_try = []
|
||||
|
||||
# Second: move toward holo tiles if we need power-ups
|
||||
var powerup_manager = actor.get_node_or_null("PowerUpManager")
|
||||
if powerup_manager and powerup_manager.current_points < powerup_manager.MAX_POINTS:
|
||||
var target = find_nearest_tile_of_type(HOLO_TILES)
|
||||
if target != Vector2i(-1, -1):
|
||||
return _get_adjacent_position(target)
|
||||
# If mission is complete, we don't need goal tiles or specific board tiles
|
||||
if not is_mission_complete:
|
||||
if needed_tiles.size() > 0:
|
||||
targets_to_try.append(needed_tiles)
|
||||
|
||||
# Third: move toward any goal tile that might be useful
|
||||
var target = find_nearest_tile_of_type(GOAL_TILES)
|
||||
if target != Vector2i(-1, -1):
|
||||
return _get_adjacent_position(target)
|
||||
var pu_manager = actor.get_node_or_null("PowerUpManager")
|
||||
if pu_manager and pu_manager.current_points < pu_manager.MAX_POINTS:
|
||||
targets_to_try.append(HOLO_TILES)
|
||||
|
||||
if not is_mission_complete:
|
||||
targets_to_try.append(GOAL_TILES)
|
||||
|
||||
# CONSTRAINT: In Stop n Go, NEVER target X >= 21 unless it's late game (last 30s)
|
||||
var max_x = 22 # No limit by default
|
||||
if is_sng and not is_late_game:
|
||||
max_x = 20
|
||||
|
||||
for tile_set in targets_to_try:
|
||||
var target = find_nearest_tile_of_type(tile_set)
|
||||
if target != Vector2i(-1, -1) and target.x <= max_x:
|
||||
# Just return the target directly if it's a valid tile position.
|
||||
# The BotController will use A* to find the path.
|
||||
# We only need _get_adjacent_position if the target itself is an obstacle (e.g. Tekton Stand).
|
||||
if _is_valid_move_target(target, true):
|
||||
return target
|
||||
else:
|
||||
# If we can't stand ON it (e.g. it's on a stand), find a spot NEXT to it.
|
||||
var final = _get_adjacent_position(target)
|
||||
if final != actor.current_position and final.x <= max_x:
|
||||
return final
|
||||
|
||||
# Fallback: move Right in Stop n Go mode even if idle
|
||||
if is_sng:
|
||||
# Only force forward if we haven't finished our mission OR time is almost up
|
||||
# DRIFT PREVENTION: Only step right if incomplete AND not already late in the track
|
||||
# If they reach column 16 without goals, they should stay there and wait for items.
|
||||
if (not is_mission_complete and actor.current_position.x < 16) or is_late_game:
|
||||
var right_step = actor.current_position + Vector2i(1, 0)
|
||||
if _is_valid_move_target(right_step):
|
||||
return right_step
|
||||
|
||||
# Fallback: random valid position
|
||||
return _get_random_valid_position()
|
||||
var rnd = _get_random_valid_position()
|
||||
# Apply X constraint to random move
|
||||
if is_sng and rnd.x > max_x:
|
||||
rnd.x = max_x # Clamp to safe zone
|
||||
if not _is_valid_move_target(rnd):
|
||||
# Try to find any other valid y at this x
|
||||
for dy in [1, -1, 2, -2]:
|
||||
var alt = Vector2i(rnd.x, rnd.y + dy)
|
||||
if _is_valid_move_target(alt):
|
||||
rnd = alt
|
||||
break
|
||||
if rnd.x > max_x: return actor.current_position # Last resort
|
||||
|
||||
return rnd
|
||||
|
||||
func _get_adjacent_position(target: Vector2i) -> Vector2i:
|
||||
"""Get a valid position adjacent to or at the target."""
|
||||
var current_pos = actor.current_position
|
||||
|
||||
# If we can reach the target directly, return it
|
||||
if _is_within_movement_range(target):
|
||||
# If we are already at the target, stay there
|
||||
if current_pos == target:
|
||||
return target
|
||||
|
||||
# If the target is walkable and within range, return it
|
||||
if _is_valid_move_target(target) and _is_within_movement_range(target):
|
||||
return target
|
||||
|
||||
# Otherwise, move one step closer
|
||||
# 1. ORTHOGONAL NEIGHBORS (Normal priority)
|
||||
var neighbors = [
|
||||
target + Vector2i(1, 0), target + Vector2i(-1, 0),
|
||||
target + Vector2i(0, 1), target + Vector2i(0, -1)
|
||||
]
|
||||
|
||||
# Priority: Pick neighbors that are NOT our current position first
|
||||
var candidates = []
|
||||
for n_pos in neighbors:
|
||||
# Use ignore_players=true here because we want to see ALL potentially walkable paths
|
||||
# The movement manager will handle actual collisions/pushes
|
||||
if _is_valid_move_target(n_pos, true) and _is_within_movement_range(n_pos):
|
||||
candidates.append(n_pos)
|
||||
|
||||
if candidates.size() > 0:
|
||||
# If we have candidates that aren't where we are, pick the closest one to target
|
||||
var non_current = candidates.filter(func(p): return p != current_pos)
|
||||
if non_current.size() > 0:
|
||||
non_current.sort_custom(func(a, b):
|
||||
return (a - current_pos).length_squared() < (b - current_pos).length_squared()
|
||||
)
|
||||
return non_current[0]
|
||||
else:
|
||||
# If only option is current pos, we are "at" the target neighbor
|
||||
return current_pos
|
||||
|
||||
# 2. STEP CLOSER FALLBACK
|
||||
var dx = sign(target.x - current_pos.x)
|
||||
var dz = sign(target.y - current_pos.y)
|
||||
|
||||
var positions_to_try = [
|
||||
var steps = [
|
||||
Vector2i(current_pos.x + dx, current_pos.y + dz),
|
||||
Vector2i(current_pos.x + dx, current_pos.y),
|
||||
Vector2i(current_pos.x, current_pos.y + dz)
|
||||
]
|
||||
|
||||
for pos in positions_to_try:
|
||||
if _is_valid_move_target(pos):
|
||||
return pos
|
||||
for step in steps:
|
||||
if _is_valid_move_target(step, true): # Ignore players for step planning
|
||||
return step
|
||||
|
||||
return Vector2i(-1, -1)
|
||||
|
||||
func _is_within_movement_range(pos: Vector2i) -> bool:
|
||||
var current_pos = actor.current_position
|
||||
var dist = max(abs(pos.x - current_pos.x), abs(pos.y - current_pos.y))
|
||||
return dist <= actor.movement_range
|
||||
if actor.get("use_diagonal_movement"):
|
||||
return max(abs(pos.x - current_pos.x), abs(pos.y - current_pos.y)) <= actor.movement_range
|
||||
else:
|
||||
return (abs(pos.x - current_pos.x) + abs(pos.y - current_pos.y)) <= actor.movement_range
|
||||
|
||||
func _is_valid_move_target(pos: Vector2i) -> bool:
|
||||
func _is_valid_move_target(pos: Vector2i, ignore_players: bool = false) -> bool:
|
||||
if not enhanced_gridmap or not enhanced_gridmap.is_position_valid(pos):
|
||||
return false
|
||||
if actor.is_position_occupied(pos):
|
||||
|
||||
# Check Floor 0 (Ground/Walls)
|
||||
var floor_item = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y))
|
||||
if floor_item == -1 or floor_item in enhanced_gridmap.non_walkable_items:
|
||||
return false
|
||||
|
||||
# Check Floor 1 (Items/Obstacles)
|
||||
var item = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 1, pos.y))
|
||||
if item != -1 and item in enhanced_gridmap.non_walkable_items:
|
||||
return false
|
||||
|
||||
# Check Physics (Stands/Static Objects)
|
||||
if actor.movement_manager and actor.movement_manager.has_method("_is_position_blocked_by_physics"):
|
||||
if actor.movement_manager._is_position_blocked_by_physics(pos):
|
||||
return false
|
||||
|
||||
if not ignore_players and actor.is_position_occupied(pos):
|
||||
return false
|
||||
return true
|
||||
|
||||
@@ -376,6 +520,10 @@ func evaluate_sabotage_opportunity() -> Dictionary:
|
||||
if not powerup_manager or not powerup_manager.can_use_special():
|
||||
return result
|
||||
|
||||
# 0. STOP N GO THRESHOLD: No sabotage until passing column 10
|
||||
if LobbyManager.game_mode == "Stop n Go" and actor.current_position.x <= 10:
|
||||
return result
|
||||
|
||||
# Get opponents
|
||||
var opponents = _get_opponents()
|
||||
if opponents.size() == 0:
|
||||
@@ -391,9 +539,13 @@ func evaluate_sabotage_opportunity() -> Dictionary:
|
||||
return result
|
||||
|
||||
# Condition 2: Opponent is close to completing their goal
|
||||
var progress_threshold = 0.7
|
||||
if LobbyManager.game_mode == "Stop n Go" and actor.current_position.x > 10:
|
||||
progress_threshold = 0.4 # More aggressive in late game!
|
||||
|
||||
for opponent in opponents:
|
||||
var opponent_progress = _estimate_opponent_progress(opponent)
|
||||
if opponent_progress >= 0.7: # 70% complete
|
||||
if opponent_progress >= progress_threshold:
|
||||
result.should_sabotage = true
|
||||
result.reason = "opponent_close_to_winning"
|
||||
result.target = opponent
|
||||
@@ -409,6 +561,14 @@ func evaluate_sabotage_opportunity() -> Dictionary:
|
||||
result.reason = "behind_in_score"
|
||||
result.target = opponents[0] if opponents.size() > 0 else null
|
||||
return result
|
||||
|
||||
# Condition 4: Random Aggression (Stop n Go Late Game)
|
||||
if LobbyManager.game_mode == "Stop n Go" and actor.current_position.x > 12:
|
||||
if randf() < 0.3: # 30% chance each tick to just be mean
|
||||
result.should_sabotage = true
|
||||
result.reason = "random_aggression"
|
||||
result.target = opponents[randi() % opponents.size()]
|
||||
return result
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
||||
return false
|
||||
|
||||
if not player.is_multiplayer_authority():
|
||||
# print("[Move] Failed: Not authority for ", player.name)
|
||||
print("[Move] Failed: Not authority for %s (Authority: %d, My Peer: %d)" % [player.name, player.get_multiplayer_authority(), player.multiplayer.get_unique_id()])
|
||||
return false
|
||||
|
||||
if player.get("is_frozen") or player.get("is_stop_frozen"):
|
||||
@@ -144,7 +144,7 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
|
||||
# === NEW LOGIC: Only allow push if in ATTACK MODE ===
|
||||
if not player.get("is_attack_mode"):
|
||||
# Standard bumping effect or nothing?
|
||||
# User said "Remove standard push", so we just do nothing or small shake
|
||||
print("[Move] Push blocked: Not in attack mode (%s trying to push)" % player.name)
|
||||
return false
|
||||
|
||||
# === SUPER PUSH (Attack Mode) ===
|
||||
@@ -196,19 +196,23 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
|
||||
# Consume all available boost to force a full recharge cycle
|
||||
player.powerup_manager.consume_boost(100.0)
|
||||
|
||||
# SCORING: 200 Points for successful attack
|
||||
# SCORING: 200 Points for successful attack (ONLY in Free Mode)
|
||||
if player.is_multiplayer_authority():
|
||||
var main = player.get_tree().get_root().get_node_or_null("Main")
|
||||
if main:
|
||||
var gcm = main.get_node_or_null("GoalsCycleManager")
|
||||
if gcm:
|
||||
if multiplayer.is_server():
|
||||
# Server/Bot: Directly add score to specific player ID
|
||||
gcm.add_score(player.name.to_int(), 200)
|
||||
else:
|
||||
# Client: Request score add (sender ID used)
|
||||
gcm.rpc("request_add_score", 200)
|
||||
NotificationManager.send_message(player, "Successful Attack! +200 Pts", NotificationManager.MessageType.GOAL)
|
||||
var is_sng = LobbyManager.game_mode == "Stop n Go"
|
||||
if not is_sng:
|
||||
var main = player.get_tree().get_root().get_node_or_null("Main")
|
||||
if main:
|
||||
var gcm = main.get_node_or_null("GoalsCycleManager")
|
||||
if gcm:
|
||||
if multiplayer.is_server():
|
||||
# Server/Bot: Directly add score to specific player ID
|
||||
gcm.add_score(player.name.to_int(), 200)
|
||||
else:
|
||||
# Client: Request score add (sender ID used)
|
||||
gcm.rpc("request_add_score", 200)
|
||||
NotificationManager.send_message(player, "Successful Attack! +200 Pts", NotificationManager.MessageType.GOAL)
|
||||
else:
|
||||
NotificationManager.send_message(player, "Successful Attack!", NotificationManager.MessageType.GOAL)
|
||||
|
||||
# 5. Attack Mode Persistence
|
||||
# logic moved to consume_boost: checks if <= 0 then disables.
|
||||
|
||||
@@ -31,10 +31,15 @@ func _normalize_tile(tile: int) -> int:
|
||||
func grab_item(grid_position: Vector2i) -> bool:
|
||||
var has_ap = player.action_points > 0 if TurnManager.turn_based_mode else true
|
||||
|
||||
if not enhanced_gridmap or not has_ap:
|
||||
if not enhanced_gridmap:
|
||||
print("[Grab] Failed for %s: enhanced_gridmap is null" % player.name)
|
||||
return false
|
||||
if not has_ap:
|
||||
print("[Grab] Failed for %s: no AP (%d)" % [player.name, player.action_points])
|
||||
return false
|
||||
|
||||
if player.get("is_frozen"):
|
||||
print("[Grab] Failed for %s: player is frozen" % player.name)
|
||||
return false
|
||||
|
||||
var cell = Vector3i(grid_position.x, 1, grid_position.y)
|
||||
@@ -49,9 +54,11 @@ func grab_item(grid_position: Vector2i) -> bool:
|
||||
is_adjacent = true
|
||||
break
|
||||
if not is_adjacent:
|
||||
print("[Grab] Failed for %s: %s is not adjacent to current %s" % [player.name, grid_position, player.current_position])
|
||||
return false
|
||||
|
||||
if item == -1:
|
||||
print("[Grab] Failed for %s: no item at %s Layer 1" % [player.name, grid_position])
|
||||
return false
|
||||
|
||||
# === AUTO-ARRANGE LOGIC (Client-side pre-check) ===
|
||||
@@ -62,12 +69,15 @@ func grab_item(grid_position: Vector2i) -> bool:
|
||||
if not is_powerup:
|
||||
target_slot = find_best_goal_slot_for_item(item)
|
||||
if target_slot == -1:
|
||||
print("Player: No valid slot found for item.")
|
||||
print("[Grab] Failed for %s: No valid slot found for item %d." % [player.name, item])
|
||||
return false # no space
|
||||
|
||||
if not player.is_multiplayer_authority():
|
||||
print("[Grab] Failed for %s: not authority" % player.name)
|
||||
return false
|
||||
|
||||
print("[Grab] %s SUCCESS! Grabbing item %d at %s into slot %d" % [player.name, item, grid_position, target_slot])
|
||||
|
||||
# Play pickup animation (synced across network)
|
||||
if player.is_multiplayer_authority() and player.has_method("sync_pickup_animation"):
|
||||
player.rpc("sync_pickup_animation")
|
||||
|
||||
@@ -315,14 +315,18 @@ func _execute_area_freeze(center_pos: Vector2i = Vector2i.ZERO):
|
||||
hit_count += 1
|
||||
|
||||
if hit_count > 0 and player.is_multiplayer_authority():
|
||||
var points = hit_count * 50
|
||||
var main = player.get_tree().get_root().get_node_or_null("Main")
|
||||
if main:
|
||||
var gcm = main.get_node_or_null("GoalsCycleManager")
|
||||
if gcm:
|
||||
gcm.rpc("request_add_score", points)
|
||||
|
||||
NotificationManager.send_message(player, "Hit %d Players! +%d Pts" % [hit_count, points], NotificationManager.MessageType.GOAL)
|
||||
var is_sng = LobbyManager.game_mode == "Stop n Go"
|
||||
if not is_sng:
|
||||
var points = hit_count * 50
|
||||
var main = player.get_tree().get_root().get_node_or_null("Main")
|
||||
if main:
|
||||
var gcm = main.get_node_or_null("GoalsCycleManager")
|
||||
if gcm:
|
||||
gcm.rpc("request_add_score", points)
|
||||
|
||||
NotificationManager.send_message(player, "Hit %d Players! +%d Pts" % [hit_count, points], NotificationManager.MessageType.GOAL)
|
||||
else:
|
||||
NotificationManager.send_message(player, "Hit %d Players!" % hit_count, NotificationManager.MessageType.GOAL)
|
||||
|
||||
# Visual Feedback (Turn Floor Blue - Item 12 on Layer 0)
|
||||
if player.is_multiplayer_authority():
|
||||
|
||||
@@ -11,7 +11,7 @@ enum Phase {GO, STOP}
|
||||
|
||||
const GO_DURATION: float = 8.0
|
||||
const STOP_DURATION: float = 4.0
|
||||
const REQUIRED_GOALS: int = 2
|
||||
const REQUIRED_GOALS: int = 5
|
||||
|
||||
var current_phase: Phase = Phase.GO
|
||||
var phase_timer: float = GO_DURATION
|
||||
@@ -416,12 +416,7 @@ func sync_mission_progress(_player_id: int, _mission_index: int, _current: int):
|
||||
# Deprecated
|
||||
pass
|
||||
|
||||
func check_win_condition(player_id: int, position: Vector2i) -> bool:
|
||||
# 1. Must reach the finish line (Column 21)
|
||||
if position.x < finish_line_x:
|
||||
return false
|
||||
|
||||
# 2. Must have enough Goal Completions (tracked by GoalsCycleManager)
|
||||
func is_mission_complete(player_id: int) -> bool:
|
||||
var main = get_node_or_null("/root/Main")
|
||||
if not main: return false
|
||||
|
||||
@@ -429,15 +424,23 @@ func check_win_condition(player_id: int, position: Vector2i) -> bool:
|
||||
if not goals_cycle_manager: return false
|
||||
|
||||
var completed_count = goals_cycle_manager.player_goal_counts.get(player_id, 0)
|
||||
|
||||
if completed_count >= REQUIRED_GOALS:
|
||||
print("[StopNGo] Player %d REACHED FINISH with %d goals complete!" % [player_id, completed_count])
|
||||
return completed_count >= REQUIRED_GOALS
|
||||
|
||||
func check_win_condition(player_id: int, position: Vector2i) -> bool:
|
||||
# 1. Must reach the finish line (Column 21)
|
||||
if position.x < finish_line_x:
|
||||
return false
|
||||
|
||||
# 2. Must have enough Goal Completions
|
||||
if is_mission_complete(player_id):
|
||||
print("[StopNGo] Player %d REACHED FINISH with goals complete!" % player_id)
|
||||
return true
|
||||
else:
|
||||
# Inform the player locally if they reach the end without goals
|
||||
var player_node = main.get_node_or_null(str(player_id))
|
||||
var main = get_node_or_null("/root/Main")
|
||||
var player_node = main.get_node_or_null(str(player_id)) if main else null
|
||||
if player_node:
|
||||
NotificationManager.send_message(player_node, "Incomplete! Achieve %d goals (x%d) to win!" % [REQUIRED_GOALS, REQUIRED_GOALS], NotificationManager.MessageType.WARNING)
|
||||
NotificationManager.send_message(player_node, "Incomplete! Achieve %d goals to win!" % REQUIRED_GOALS, NotificationManager.MessageType.WARNING)
|
||||
|
||||
print("[StopNGo] Player %d reached finish but goal count too low: %d/%d" % [player_id, completed_count, REQUIRED_GOALS])
|
||||
print("[StopNGo] Player %d reached finish but goals incomplete." % player_id)
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user