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:
Yogi Wiguna
2026-03-05 16:41:35 +08:00
parent aa26e9f2a4
commit 5c4764b082
7 changed files with 169 additions and 47 deletions
+132 -17
View File
@@ -18,6 +18,10 @@ var _tick_counter: int = 0
var _is_processing_action: bool = false
var _current_action: String = "idle"
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()
# Tile constants
@@ -108,8 +112,29 @@ func _physics_process(delta):
actor.is_player_moving = false
_stuck_timer = 0.0
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:
_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)
var current_tick_rate = tick_rate
@@ -138,6 +163,13 @@ func _run_ai_tick():
if actor.is_player_moving:
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
if LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO) and actor.current_position.x >= 21:
return
@@ -173,29 +205,33 @@ func _run_ai_tick():
print("[BotController] Action Taken: Attack Pursuit")
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)
# Spawning while carrying is high priority; Hunting is medium priority.
if await _try_tekton_action():
print("[BotController] Action Taken: Tekton")
return
# Priority 2: Move toward needed tiles
if await _try_move():
print("[BotController] Action Taken: Move")
return
# Priority 3: Use power-up sabotage if conditions are met
if await _try_use_powerup():
print("[BotController] Action Taken: PowerUp")
return
# Priority 2: Arrange playerboard (Finish what we have FIRST)
# Priority 4: 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
# Priority 5: Grab tiles (goal tiles or holo tiles)
if await _try_grab():
print("[BotController] Action Taken: Grab")
return
# Priority 4: Move toward needed tiles
if await _try_move():
print("[BotController] Action Taken: Move")
return
# Priority 5: Put tiles back on grid (Standard priority)
# Priority 6: Put tiles back on grid (Standard priority)
if not full_board_priority_mode:
if await _try_put():
print("[BotController] Action Taken: Put")
@@ -226,12 +262,94 @@ func _run_ai_tick():
# 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:
"""Check and execute power-up sabotage."""
var powerup_manager = actor.get_node_or_null("PowerUpManager")
if not powerup_manager or not powerup_manager.can_use_special():
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
if actor.get("is_attack_mode"):
return false
@@ -245,9 +363,9 @@ func _try_use_powerup() -> bool:
_is_processing_action = true
_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
if rng.randf() > 0.5:
if rng.randf() > 0.4: # Slightly higher weight to Attack Mode if explicitly sabotaging
# Attack Mode
success = powerup_manager.use_special_effect()
print("[BotController] %s chose ATTACK MODE." % actor.name)
@@ -257,7 +375,6 @@ func _try_use_powerup() -> bool:
success = powerup_manager.spawn_boost_reward()
print("[BotController] %s chose SPAWN ITEM." % actor.name)
else:
# Fallback to attack mode if method missing
success = powerup_manager.use_special_effect()
if success:
@@ -265,8 +382,6 @@ func _try_use_powerup() -> bool:
NotificationManager.send_message(actor, NotificationManager.MESSAGES.USED_SPECIAL_POWER, NotificationManager.MessageType.POWERUP)
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
_current_action = "idle"
return success