feat: Add initial player character, movement, network synchronization, bot AI, and game managers.
This commit is contained in:
+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 = []
|
||||
|
||||
Reference in New Issue
Block a user