feat: Introduce core player movement manager with grid-based movement, player pushing mechanics, and initial bot control and strategic planning.
This commit is contained in:
+2
-2
@@ -1642,9 +1642,9 @@ func randomize_game_grid():
|
|||||||
|
|
||||||
var enhanced_gridmap = $EnhancedGridMap
|
var enhanced_gridmap = $EnhancedGridMap
|
||||||
if enhanced_gridmap:
|
if enhanced_gridmap:
|
||||||
# Custom spawn ratio for Free Mode: 60% common tiles, 40% empty tiles (start of game)
|
# Custom spawn ratio for Free Mode: 80% common tiles, 20% empty tiles (start of game)
|
||||||
var density_callable = func():
|
var density_callable = func():
|
||||||
if randf() <= 0.6:
|
if randf() <= 0.8:
|
||||||
return ScarcityModel.STANDARD_TILES.pick_random()
|
return ScarcityModel.STANDARD_TILES.pick_random()
|
||||||
else:
|
else:
|
||||||
return -1 # Empty
|
return -1 # Empty
|
||||||
|
|||||||
+12
-5
@@ -1271,13 +1271,18 @@ func _find_random_spawn_position() -> Vector2i:
|
|||||||
var available_positions = []
|
var available_positions = []
|
||||||
|
|
||||||
# Scan the grid for valid walkable floor tiles that are not occupied
|
# Scan the grid for valid walkable floor tiles that are not occupied
|
||||||
for x in range(enhanced_gridmap.columns):
|
var cols = enhanced_gridmap.columns if "columns" in enhanced_gridmap else 14
|
||||||
for z in range(enhanced_gridmap.rows):
|
var rs = enhanced_gridmap.rows if "rows" in enhanced_gridmap else 14
|
||||||
|
|
||||||
|
for x in range(cols):
|
||||||
|
for z in range(rs):
|
||||||
var pos = Vector2i(x, z)
|
var pos = Vector2i(x, z)
|
||||||
|
|
||||||
# Check Floor 0 for walkable ground (Item 0)
|
# Check Floor 0 for walkable ground (Not a wall/obstacle)
|
||||||
var ground_item = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z))
|
var ground_item = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z))
|
||||||
if ground_item == 0: # Assuming 0 is walkable ground
|
var is_walkable = ground_item != -1 and not ground_item in enhanced_gridmap.non_walkable_items
|
||||||
|
|
||||||
|
if is_walkable:
|
||||||
# Check if position is occupied by another player
|
# Check if position is occupied by another player
|
||||||
if not is_position_occupied(pos):
|
if not is_position_occupied(pos):
|
||||||
available_positions.append(pos)
|
available_positions.append(pos)
|
||||||
@@ -1287,7 +1292,9 @@ func _find_random_spawn_position() -> Vector2i:
|
|||||||
rng.randomize()
|
rng.randomize()
|
||||||
return available_positions[rng.randi() % available_positions.size()]
|
return available_positions[rng.randi() % available_positions.size()]
|
||||||
|
|
||||||
return Vector2i.ZERO
|
# Better fallback center instead of 0,0 which is often a wall/border
|
||||||
|
print("[Player] Warning: No available spawn positions found! Using fallback.")
|
||||||
|
return Vector2i(cols / 2, rs / 2)
|
||||||
|
|
||||||
func find_random_valid_position_in_range() -> Vector2i:
|
func find_random_valid_position_in_range() -> Vector2i:
|
||||||
var rng = RandomNumberGenerator.new()
|
var rng = RandomNumberGenerator.new()
|
||||||
|
|||||||
+138
-23
@@ -18,6 +18,10 @@ var _tick_counter: int = 0
|
|||||||
var _is_processing_action: bool = false
|
var _is_processing_action: bool = false
|
||||||
var _current_action: String = "idle"
|
var _current_action: String = "idle"
|
||||||
var _stuck_timer: float = 0.0
|
var _stuck_timer: float = 0.0
|
||||||
|
var _tekton_spawn_cooldown: float = 0.0 # Prevent spamming spawn actions
|
||||||
|
var _tekton_held_timer: float = 0.0 # Force drop/action after 8s
|
||||||
|
var _idle_stuck_timer: float = 0.0 # Emergency unstuck if sitting still too long
|
||||||
|
var _last_idle_pos: Vector2i = Vector2i(-1, -1)
|
||||||
var rng = RandomNumberGenerator.new()
|
var rng = RandomNumberGenerator.new()
|
||||||
|
|
||||||
# Tile constants
|
# Tile constants
|
||||||
@@ -108,8 +112,29 @@ func _physics_process(delta):
|
|||||||
actor.is_player_moving = false
|
actor.is_player_moving = false
|
||||||
_stuck_timer = 0.0
|
_stuck_timer = 0.0
|
||||||
return
|
return
|
||||||
|
# Update Tekton held timer
|
||||||
|
if actor.get("is_carrying_tekton"):
|
||||||
|
_tekton_held_timer += delta
|
||||||
|
if _tekton_held_timer >= 4.0:
|
||||||
|
print("[BotController] %s held Tekton for 4s! Forcing action." % actor.name)
|
||||||
|
_force_tekton_release()
|
||||||
|
_tekton_held_timer = 0.0
|
||||||
else:
|
else:
|
||||||
_stuck_timer = 0.0
|
_tekton_held_timer = 0.0
|
||||||
|
|
||||||
|
# IDLE STUCK PREVENTION (If sitting in same spot doing NOTHING)
|
||||||
|
if not actor.is_player_moving and not _is_processing_action:
|
||||||
|
if actor.current_position == _last_idle_pos:
|
||||||
|
_idle_stuck_timer += delta
|
||||||
|
if _idle_stuck_timer >= 4.0:
|
||||||
|
print("[BotController] %s stuck at %s for 4s! Forcing unstuck." % [actor.name, actor.current_position])
|
||||||
|
_try_unstuck_move()
|
||||||
|
_idle_stuck_timer = 0.0
|
||||||
|
else:
|
||||||
|
_last_idle_pos = actor.current_position
|
||||||
|
_idle_stuck_timer = 0.0
|
||||||
|
else:
|
||||||
|
_idle_stuck_timer = 0.0
|
||||||
|
|
||||||
# Rate limiting (with difficulty scaling for Stop n Go)
|
# Rate limiting (with difficulty scaling for Stop n Go)
|
||||||
var current_tick_rate = tick_rate
|
var current_tick_rate = tick_rate
|
||||||
@@ -138,6 +163,13 @@ func _run_ai_tick():
|
|||||||
if actor.is_player_moving:
|
if actor.is_player_moving:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if TurnManager.turn_based_mode:
|
||||||
|
_tick_counter = tick_rate # Force immediate evaluation in turn-based
|
||||||
|
|
||||||
|
# Update cooldowns
|
||||||
|
if _tekton_spawn_cooldown > 0:
|
||||||
|
_tekton_spawn_cooldown -= get_process_delta_time()
|
||||||
|
|
||||||
# STOP N GO: Don't process if already at finish line
|
# STOP N GO: Don't process if already at finish line
|
||||||
if LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO) and actor.current_position.x >= 21:
|
if LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO) and actor.current_position.x >= 21:
|
||||||
return
|
return
|
||||||
@@ -173,29 +205,33 @@ func _run_ai_tick():
|
|||||||
print("[BotController] Action Taken: Attack Pursuit")
|
print("[BotController] Action Taken: Attack Pursuit")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Priority 1: Use power-up sabotage if conditions are met (Aggression threshold handled in planner)
|
# Priority 1: Tekton Management (Grab Tekton if full boost, or spawn if carrying)
|
||||||
if await _try_use_powerup():
|
# Spawning while carrying is high priority; Hunting is medium priority.
|
||||||
print("[BotController] Action Taken: PowerUp")
|
if await _try_tekton_action():
|
||||||
|
print("[BotController] Action Taken: Tekton")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Priority 2: Arrange playerboard (Finish what we have FIRST)
|
# Priority 2: Move toward needed tiles
|
||||||
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 4: Move toward needed tiles
|
|
||||||
if await _try_move():
|
if await _try_move():
|
||||||
print("[BotController] Action Taken: Move")
|
print("[BotController] Action Taken: Move")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Priority 5: Put tiles back on grid (Standard priority)
|
# Priority 3: Use power-up sabotage if conditions are met
|
||||||
|
if await _try_use_powerup():
|
||||||
|
print("[BotController] Action Taken: PowerUp")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Priority 4: Arrange playerboard (Finish what we have FIRST)
|
||||||
|
if await _try_arrange():
|
||||||
|
print("[BotController] Action Taken: Arrange")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Priority 5: Grab tiles (goal tiles or holo tiles)
|
||||||
|
if await _try_grab():
|
||||||
|
print("[BotController] Action Taken: Grab")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Priority 6: Put tiles back on grid (Standard priority)
|
||||||
if not full_board_priority_mode:
|
if not full_board_priority_mode:
|
||||||
if await _try_put():
|
if await _try_put():
|
||||||
print("[BotController] Action Taken: Put")
|
print("[BotController] Action Taken: Put")
|
||||||
@@ -226,12 +262,94 @@ func _run_ai_tick():
|
|||||||
# Power-Up / Sabotage
|
# Power-Up / Sabotage
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
func _try_tekton_action() -> bool:
|
||||||
|
"""Handle Tekton-specific actions (Grab or Spawn)."""
|
||||||
|
var powerup_manager = actor.get_node_or_null("PowerUpManager")
|
||||||
|
if not powerup_manager: return false
|
||||||
|
|
||||||
|
# CASE 1: Already carrying a Tekton - SPAWN TILES
|
||||||
|
if actor.get("is_carrying_tekton"):
|
||||||
|
# Only spawn if NOT on cooldown and we actually need tiles
|
||||||
|
if _tekton_spawn_cooldown <= 0 and powerup_manager.can_use_special():
|
||||||
|
var board_fullness = _get_board_fullness_ratio()
|
||||||
|
var needs_tiles = strategic_planner.get_tiles_needed().size() > 0
|
||||||
|
|
||||||
|
# Don't spawn if board is too full (> 80% now) or if we don't need any more tiles
|
||||||
|
if board_fullness < 0.8 and needs_tiles:
|
||||||
|
_is_processing_action = true
|
||||||
|
_current_action = "spawning"
|
||||||
|
print("[BotController] %s is carrying Tekton. Spawning tiles!" % actor.name)
|
||||||
|
if powerup_manager.has_method("spawn_boost_reward"):
|
||||||
|
var success = powerup_manager.spawn_boost_reward()
|
||||||
|
_tekton_spawn_cooldown = 8.0 # Reduced cooldown
|
||||||
|
await _wait_with_variance(action_delay * 2.0)
|
||||||
|
_is_processing_action = false
|
||||||
|
return success
|
||||||
|
return false
|
||||||
|
|
||||||
|
# CASE 2: Not carrying, but full boost - GO GRAB TEKTON (With 60% chance)
|
||||||
|
if powerup_manager.get_bars() >= 2 and rng.randf() < 0.6:
|
||||||
|
var tekton = strategic_planner.find_nearest_roaming_tekton()
|
||||||
|
if tekton:
|
||||||
|
var tekton_pos = tekton.get("current_position")
|
||||||
|
if not tekton_pos: return false
|
||||||
|
|
||||||
|
var dist = abs(tekton_pos.x - actor.current_position.x) + abs(tekton_pos.y - actor.current_position.y)
|
||||||
|
|
||||||
|
if dist <= 1:
|
||||||
|
# Adjacent! Grab it
|
||||||
|
print("[BotController] %s is adjacent to Tekton. Grabbing!" % actor.name)
|
||||||
|
actor.grab_tekton()
|
||||||
|
await _wait_with_variance(action_delay)
|
||||||
|
return true
|
||||||
|
else:
|
||||||
|
# Move towards it
|
||||||
|
var path = enhanced_gridmap.find_path(
|
||||||
|
Vector2(actor.current_position),
|
||||||
|
Vector2(tekton_pos),
|
||||||
|
0, false, false
|
||||||
|
)
|
||||||
|
if path.size() >= 2:
|
||||||
|
var next_step = Vector2i(path[1].x, path[1].y)
|
||||||
|
print("[BotController] %s moving towards Tekton at %s" % [actor.name, next_step])
|
||||||
|
if actor.movement_manager.simple_move_to(next_step):
|
||||||
|
_is_processing_action = true
|
||||||
|
_current_action = "moving_to_tekton"
|
||||||
|
# Wait for move
|
||||||
|
while is_instance_valid(actor) and actor.is_player_moving:
|
||||||
|
await get_tree().process_frame
|
||||||
|
_is_processing_action = false
|
||||||
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
func _force_tekton_release():
|
||||||
|
"""Force the bot to either spawn tiles or drop the Tekton if stuck holding it."""
|
||||||
|
var powerup_manager = actor.get_node_or_null("PowerUpManager")
|
||||||
|
|
||||||
|
# Try to spawn tiles first if possible (even if board is a bit full, we are 'forcing' it)
|
||||||
|
if powerup_manager and powerup_manager.can_use_special():
|
||||||
|
if powerup_manager.has_method("spawn_boost_reward"):
|
||||||
|
print("[BotController] %s forcing spawn instead of drop." % actor.name)
|
||||||
|
powerup_manager.spawn_boost_reward()
|
||||||
|
_tekton_spawn_cooldown = 8.0
|
||||||
|
return
|
||||||
|
|
||||||
|
# If can't spawn (no power), just throw it away (drop)
|
||||||
|
if actor.has_method("throw_tekton"):
|
||||||
|
print("[BotController] %s forcing drop/throw." % actor.name)
|
||||||
|
actor.throw_tekton()
|
||||||
|
|
||||||
func _try_use_powerup() -> bool:
|
func _try_use_powerup() -> bool:
|
||||||
"""Check and execute power-up sabotage."""
|
"""Check and execute power-up sabotage."""
|
||||||
var powerup_manager = actor.get_node_or_null("PowerUpManager")
|
var powerup_manager = actor.get_node_or_null("PowerUpManager")
|
||||||
if not powerup_manager or not powerup_manager.can_use_special():
|
if not powerup_manager or not powerup_manager.can_use_special():
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
# If we are carrying a Tekton, _try_tekton_action handles it
|
||||||
|
if actor.get("is_carrying_tekton"):
|
||||||
|
return false
|
||||||
|
|
||||||
# PREVENT LOOP: If already in Attack Mode, do not try to use it again
|
# PREVENT LOOP: If already in Attack Mode, do not try to use it again
|
||||||
if actor.get("is_attack_mode"):
|
if actor.get("is_attack_mode"):
|
||||||
return false
|
return false
|
||||||
@@ -245,9 +363,9 @@ func _try_use_powerup() -> bool:
|
|||||||
_is_processing_action = true
|
_is_processing_action = true
|
||||||
_current_action = "sabotaging"
|
_current_action = "sabotaging"
|
||||||
|
|
||||||
# 50/50 Chance: Attack Mode vs Spawn Item
|
# 50/50 Chance: Attack Mode vs Spawn Item (If not going for Tekton)
|
||||||
var success = false
|
var success = false
|
||||||
if rng.randf() > 0.5:
|
if rng.randf() > 0.4: # Slightly higher weight to Attack Mode if explicitly sabotaging
|
||||||
# Attack Mode
|
# Attack Mode
|
||||||
success = powerup_manager.use_special_effect()
|
success = powerup_manager.use_special_effect()
|
||||||
print("[BotController] %s chose ATTACK MODE." % actor.name)
|
print("[BotController] %s chose ATTACK MODE." % actor.name)
|
||||||
@@ -257,7 +375,6 @@ func _try_use_powerup() -> bool:
|
|||||||
success = powerup_manager.spawn_boost_reward()
|
success = powerup_manager.spawn_boost_reward()
|
||||||
print("[BotController] %s chose SPAWN ITEM." % actor.name)
|
print("[BotController] %s chose SPAWN ITEM." % actor.name)
|
||||||
else:
|
else:
|
||||||
# Fallback to attack mode if method missing
|
|
||||||
success = powerup_manager.use_special_effect()
|
success = powerup_manager.use_special_effect()
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
@@ -265,8 +382,6 @@ func _try_use_powerup() -> bool:
|
|||||||
NotificationManager.send_message(actor, NotificationManager.MESSAGES.USED_SPECIAL_POWER, NotificationManager.MessageType.POWERUP)
|
NotificationManager.send_message(actor, NotificationManager.MESSAGES.USED_SPECIAL_POWER, NotificationManager.MessageType.POWERUP)
|
||||||
|
|
||||||
await _wait_with_variance(action_delay)
|
await _wait_with_variance(action_delay)
|
||||||
if not is_instance_valid(actor) or not is_instance_valid(self): return true # Early exit if deleted
|
|
||||||
|
|
||||||
_is_processing_action = false
|
_is_processing_action = false
|
||||||
_current_action = "idle"
|
_current_action = "idle"
|
||||||
return success
|
return success
|
||||||
|
|||||||
@@ -304,6 +304,24 @@ func _check_spiral_cell(x: int, z: int, tile_types: Array, result_array: Array):
|
|||||||
if _normalize_tile(item) in tile_types:
|
if _normalize_tile(item) in tile_types:
|
||||||
result_array.append(Vector2i(x, z))
|
result_array.append(Vector2i(x, z))
|
||||||
|
|
||||||
|
func find_nearest_roaming_tekton() -> Node3D:
|
||||||
|
"""Find the nearest Tekton that isn't carried and is on the grid."""
|
||||||
|
var tektons = actor.get_tree().get_nodes_in_group("Tektons")
|
||||||
|
var nearest_tekton = null
|
||||||
|
var min_dist = 999999.0
|
||||||
|
|
||||||
|
for tekton in tektons:
|
||||||
|
if not is_instance_valid(tekton): continue
|
||||||
|
if tekton.get("is_carried") or tekton.get("is_static_turret"): continue
|
||||||
|
if tekton.get("is_recovering"): continue # Cannot target shrinking/recovering Tektons
|
||||||
|
|
||||||
|
var dist = actor.global_position.distance_to(tekton.global_position)
|
||||||
|
if dist < min_dist:
|
||||||
|
min_dist = dist
|
||||||
|
nearest_tekton = tekton
|
||||||
|
|
||||||
|
return nearest_tekton
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Movement Strategy
|
# Movement Strategy
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -40,15 +40,7 @@ func _process(delta):
|
|||||||
player.auto_put_item()
|
player.auto_put_item()
|
||||||
|
|
||||||
if move_vec != Vector2i.ZERO:
|
if move_vec != Vector2i.ZERO:
|
||||||
# Calculate target relative to intent (future position) to prevent zigzagging
|
var target_position = player.current_position + move_vec
|
||||||
var reference_pos = player.current_position
|
|
||||||
if movement_manager.is_moving:
|
|
||||||
if not movement_manager.movement_queue.is_empty():
|
|
||||||
reference_pos = movement_manager.movement_queue[-1]
|
|
||||||
elif player.target_position != Vector2i(-1, -1):
|
|
||||||
reference_pos = player.target_position
|
|
||||||
|
|
||||||
var target_position = reference_pos + move_vec
|
|
||||||
movement_manager.simple_move_to(target_position)
|
movement_manager.simple_move_to(target_position)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -50,17 +50,7 @@ func _can_rpc() -> bool:
|
|||||||
|
|
||||||
func simple_move_to(grid_position: Vector2i) -> bool:
|
func simple_move_to(grid_position: Vector2i) -> bool:
|
||||||
if is_moving:
|
if is_moving:
|
||||||
# Check if we are already moving to this position or if it's already queued
|
return false
|
||||||
var current_target = player.target_position
|
|
||||||
if movement_queue.is_empty():
|
|
||||||
if current_target == grid_position:
|
|
||||||
return false
|
|
||||||
elif movement_queue[-1] == grid_position:
|
|
||||||
return false
|
|
||||||
|
|
||||||
if movement_queue.size() < 1: # Buffer at most 1 future move
|
|
||||||
movement_queue.append(grid_position)
|
|
||||||
return true
|
|
||||||
|
|
||||||
if not player.is_multiplayer_authority():
|
if not player.is_multiplayer_authority():
|
||||||
print("[Move] Failed: Not authority for %s (Authority: %d, My Peer: %d)" % [player.name, player.get_multiplayer_authority(), player.multiplayer.get_unique_id()])
|
print("[Move] Failed: Not authority for %s (Authority: %d, My Peer: %d)" % [player.name, player.get_multiplayer_authority(), player.multiplayer.get_unique_id()])
|
||||||
|
|||||||
@@ -444,11 +444,11 @@ func spawn_powerups_around(center: Vector2i, force_powerups: bool = true, only_c
|
|||||||
# Spawn ONLY common tiles (7-10) for Stop n Go or if forced
|
# Spawn ONLY common tiles (7-10) for Stop n Go or if forced
|
||||||
item_id = rng.randi_range(7, 10)
|
item_id = rng.randi_range(7, 10)
|
||||||
else:
|
else:
|
||||||
# Free mode: 60% Chance for Common Tile (7-10), 40% for PowerUp
|
# Free mode: 80% Chance for Common Tile (7-10), 20% for PowerUp
|
||||||
if rng.randf() < 0.6:
|
if rng.randf() < 0.8:
|
||||||
item_id = rng.randi_range(7, 10)
|
item_id = rng.randi_range(7, 10)
|
||||||
else:
|
else:
|
||||||
# 40% Chance for PowerUp
|
# 20% Chance for PowerUp
|
||||||
var is_restricted = GameMode.is_restricted(mode)
|
var is_restricted = GameMode.is_restricted(mode)
|
||||||
if is_restricted:
|
if is_restricted:
|
||||||
item_id = [11, 14].pick_random()
|
item_id = [11, 14].pick_random()
|
||||||
|
|||||||
Reference in New Issue
Block a user