From 5c4764b082d318346d7d3b6c7d31e99a4441e4e3 Mon Sep 17 00:00:00 2001 From: Yogi Wiguna Date: Thu, 5 Mar 2026 16:41:35 +0800 Subject: [PATCH] feat: Introduce core player movement manager with grid-based movement, player pushing mechanics, and initial bot control and strategic planning. --- scenes/main.gd | 4 +- scenes/player.gd | 17 ++- scripts/bot_controller.gd | 149 +++++++++++++++++--- scripts/bot_strategic_planner.gd | 18 +++ scripts/managers/player_input_manager.gd | 10 +- scripts/managers/player_movement_manager.gd | 12 +- scripts/managers/special_tiles_manager.gd | 6 +- 7 files changed, 169 insertions(+), 47 deletions(-) diff --git a/scenes/main.gd b/scenes/main.gd index 6d36558..044b4ec 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -1642,9 +1642,9 @@ func randomize_game_grid(): var enhanced_gridmap = $EnhancedGridMap 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(): - if randf() <= 0.6: + if randf() <= 0.8: return ScarcityModel.STANDARD_TILES.pick_random() else: return -1 # Empty diff --git a/scenes/player.gd b/scenes/player.gd index 506cfbb..c69a059 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -1271,13 +1271,18 @@ func _find_random_spawn_position() -> Vector2i: var available_positions = [] # Scan the grid for valid walkable floor tiles that are not occupied - for x in range(enhanced_gridmap.columns): - for z in range(enhanced_gridmap.rows): + var cols = enhanced_gridmap.columns if "columns" in enhanced_gridmap else 14 + 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) - # 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)) - 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 if not is_position_occupied(pos): available_positions.append(pos) @@ -1287,7 +1292,9 @@ func _find_random_spawn_position() -> Vector2i: rng.randomize() 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: var rng = RandomNumberGenerator.new() diff --git a/scripts/bot_controller.gd b/scripts/bot_controller.gd index f8835e6..a5015aa 100644 --- a/scripts/bot_controller.gd +++ b/scripts/bot_controller.gd @@ -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 diff --git a/scripts/bot_strategic_planner.gd b/scripts/bot_strategic_planner.gd index 939aa2b..c5c556b 100644 --- a/scripts/bot_strategic_planner.gd +++ b/scripts/bot_strategic_planner.gd @@ -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: 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 # ============================================================================= diff --git a/scripts/managers/player_input_manager.gd b/scripts/managers/player_input_manager.gd index e001017..566baeb 100644 --- a/scripts/managers/player_input_manager.gd +++ b/scripts/managers/player_input_manager.gd @@ -40,15 +40,7 @@ func _process(delta): player.auto_put_item() if move_vec != Vector2i.ZERO: - # Calculate target relative to intent (future position) to prevent zigzagging - 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 + var target_position = player.current_position + move_vec movement_manager.simple_move_to(target_position) diff --git a/scripts/managers/player_movement_manager.gd b/scripts/managers/player_movement_manager.gd index c48465c..757440d 100644 --- a/scripts/managers/player_movement_manager.gd +++ b/scripts/managers/player_movement_manager.gd @@ -50,17 +50,7 @@ func _can_rpc() -> bool: func simple_move_to(grid_position: Vector2i) -> bool: if is_moving: - # Check if we are already moving to this position or if it's already queued - 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 + return false 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()]) diff --git a/scripts/managers/special_tiles_manager.gd b/scripts/managers/special_tiles_manager.gd index b933579..2462cff 100644 --- a/scripts/managers/special_tiles_manager.gd +++ b/scripts/managers/special_tiles_manager.gd @@ -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 item_id = rng.randi_range(7, 10) else: - # Free mode: 60% Chance for Common Tile (7-10), 40% for PowerUp - if rng.randf() < 0.6: + # Free mode: 80% Chance for Common Tile (7-10), 20% for PowerUp + if rng.randf() < 0.8: item_id = rng.randi_range(7, 10) else: - # 40% Chance for PowerUp + # 20% Chance for PowerUp var is_restricted = GameMode.is_restricted(mode) if is_restricted: item_id = [11, 14].pick_random()