From b76dd2e73771bc8914f85c7bcd7a759cd33d17c4 Mon Sep 17 00:00:00 2001 From: adtpdn Date: Fri, 24 Apr 2026 22:56:11 +0800 Subject: [PATCH] feat: fix some bug --- CHANGELOG_DRAFT.md | 9 ++ scenes/lobby.gd | 16 ++-- scenes/main.gd | 29 ++++--- scenes/player.gd | 14 ++-- scripts/bot_controller.gd | 32 +------- scripts/managers/goals_cycle_manager.gd | 42 ++++------ scripts/managers/player_action_manager.gd | 26 +----- scripts/managers/player_movement_manager.gd | 2 +- scripts/managers/playerboard_manager.gd | 91 +++++++++------------ 9 files changed, 105 insertions(+), 156 deletions(-) diff --git a/CHANGELOG_DRAFT.md b/CHANGELOG_DRAFT.md index a27f45a..d29c6a4 100644 --- a/CHANGELOG_DRAFT.md +++ b/CHANGELOG_DRAFT.md @@ -1,5 +1,14 @@ ## [NEXT] +## [2.1.8] — 2026-04-24 +- Optimized network synchronization with deterministic tile slots and granular board sync +- Removed lag-sensitive server adjacency checks to fix "disappearing tiles" on high-latency connections +- Fixed "No multiplayer peer assigned" crash during host disconnection and cleanup +- Finalized AP system removal, transitioning to a fully real-time authoritative model +- Restored bot mission completion logic +- Stabilized Nakama socket cleanup during match-to-lobby transitions + + ## [2.1.7] — 2026-04-24 - Upgraded Gacha interface with dynamic CSGO-style sequential reveal animations - Added an animation skip button for faster 10x multi-pulls diff --git a/scenes/lobby.gd b/scenes/lobby.gd index fbc3410..bfe615e 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -109,6 +109,7 @@ var admin_panel_instance: Control var current_match_id: String = "" var leaderboard_panel_instance: Control +var shop_panel_instance: Control # Bot name tracking keyed by slot index to avoid re-generating on each update var _bot_names: Dictionary = {} @@ -787,13 +788,16 @@ func _on_shop_pressed() -> void: connection_status.text = "Must be logged in" return - var shop_scene = load("res://scenes/ui/shop_panel.tscn") - if shop_scene: - var shop = shop_scene.instantiate() - add_child(shop) + if not shop_panel_instance: + var shop_scene = load("res://scenes/ui/shop_panel.tscn") + if shop_scene: + shop_panel_instance = shop_scene.instantiate() + add_child(shop_panel_instance) + shop_panel_instance.closed.connect(func(): if main_menu_panel: main_menu_panel.show()) + + if shop_panel_instance: if main_menu_panel: main_menu_panel.hide() - shop.closed.connect(func(): if main_menu_panel: main_menu_panel.show()) - shop.show_panel() + shop_panel_instance.show_panel() func _on_banner1_pressed() -> void: var gacha_scene = load("res://scenes/ui/gacha_panel.tscn") diff --git a/scenes/main.gd b/scenes/main.gd index 8590a77..dcdf01a 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -985,10 +985,10 @@ func _assign_stop_n_go_spawn_positions(all_players: Array): # Collect valid walkable spawn positions from the leftmost columns var valid_spawns: Array[Vector2i] = [] - for col in range(0, min(5, enhanced_gridmap.columns)): # Check first 5 columns + for col in range(0, min(5, enhanced_gridmap.columns)): # Check first 5 columns for row in range(enhanced_gridmap.rows): var tile = enhanced_gridmap.get_cell_item(Vector3i(col, 0, row)) - if tile == 0 or tile == 3: # Walkable or Start + if tile == 0 or tile == 3: # Walkable or Start valid_spawns.append(Vector2i(col, row)) if valid_spawns.size() >= all_players.size(): break @@ -1036,7 +1036,7 @@ func _assign_portal_mode_spawn_positions(all_players: Array): for dx in range(-radius, radius + 1): for dz in range(-radius, radius + 1): # Only check the "ring" at the current radius - if abs(dx) != radius and abs(dz) != radius and radius > 0: + if abs(dx) != radius and abs(dz) != radius and radius > 0: continue var test_pos = center_pos + Vector2i(dx, dz) @@ -1056,7 +1056,7 @@ func _assign_portal_mode_spawn_positions(all_players: Array): if abs(test_pos.x - reserved.x) <= 2 and abs(test_pos.y - reserved.y) <= 2: is_reserved = true break - if is_reserved: + if is_reserved: continue # 4. Check if occupied by another already-assigned player @@ -1408,6 +1408,7 @@ func add_newly_connected_player_character(new_peer_id: int): func _on_peer_disconnected(peer_id: int): if not is_inside_tree(): return + if not multiplayer.has_multiplayer_peer(): return if multiplayer.is_server(): print("[Main] Peer %d disconnected. Checking for bot replacement..." % peer_id) @@ -1506,7 +1507,6 @@ func set_current_turn(player_id: int): player.is_my_turn = is_current_turn if is_current_turn and not (player.is_bot or player.is_in_group("Bots")): - player.action_points = 2 player.has_moved_this_turn = false player.has_performed_action = false player.start_turn() @@ -1585,6 +1585,20 @@ func sync_playerboard(player_id: int, new_playerboard: Array): if player_id == multiplayer.get_unique_id() and GameStateManager.local_player_character: ui_manager.update_playerboard_ui() +@rpc("any_peer", "call_local", "reliable") +func sync_playerboard_slot(player_id: int, slot_index: int, item_id: int): + """Patch a single playerboard slot without touching other slots. + Used by _execute_grab on grab confirmation to avoid overwriting concurrent + in-flight optimistic grab updates on high-latency clients.""" + var player = get_node_or_null(str(player_id)) + if player and slot_index >= 0 and slot_index < player.playerboard.size(): + player.playerboard[slot_index] = item_id + + # Update UI for local player only + if player_id == multiplayer.get_unique_id() and GameStateManager.local_player_character: + ui_manager.update_playerboard_ui() + + # ============================================================================= @@ -2076,11 +2090,6 @@ func _on_match_ended(): is_match_ended = true print("[Main] Match ended! Showing game over screen...") - # Disable player controls - var local_player = GameStateManager.local_player_character - if local_player: - local_player.action_points = 0 - # Signal Global Game End (Stops Bot ticks and logic) GameStateManager.end_game() diff --git a/scenes/player.gd b/scenes/player.gd index 76cfbc2..27347e8 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -123,7 +123,7 @@ var _is_processing_action: bool = false var selected_gridmap_position = Vector2i(-1, -1) var selected_playerboard_slot = -1 var targeted_playerboard_slot = -1 -var action_points: int = 2 +#var has_performed_action: bool = false # Modifier for player models var rotation_speed: float = 10.0 @@ -1784,7 +1784,6 @@ func grid_to_world(grid_position: Vector2i) -> Vector3: return world_position func start_turn(): - action_points = 2 has_moved_this_turn = false has_performed_action = false is_my_turn = true @@ -1996,7 +1995,7 @@ func _execute_grab(grid_pos: Vector2i, cell: Vector3i, item_id: int): # This function runs on the server when requested by a client # ----------------------------------------------------------------- @rpc("any_peer", "reliable") -func request_server_grab(grid_pos: Vector2i, x: int, y: int, z: int, item_id: int): +func request_server_grab(grid_pos: Vector2i, x: int, y: int, z: int, item_id: int, expected_slot: int = -1): # 1. Only the server (peer 1) should process this if not multiplayer.is_server(): return @@ -2006,8 +2005,9 @@ func request_server_grab(grid_pos: Vector2i, x: int, y: int, z: int, item_id: in push_error("Security: Non-authority tried to grab item!") return - # 3. Call the execution logic - playerboard_manager._execute_grab(grid_pos, Vector3i(x, y, z), item_id) + # 3. Call the execution logic (pass expected_slot for deterministic placement) + playerboard_manager._execute_grab(grid_pos, Vector3i(x, y, z), item_id, expected_slot) + # ----------------------------------------------------------------- # Auto-put: no manual selection needed @@ -2074,7 +2074,6 @@ func request_server_put(grid_position: Vector2i, slot_index: int, x: int, y: int # Update player state has_performed_action = true - action_points -= 1 selected_playerboard_slot = -1 # Notify about action completion @@ -2161,9 +2160,6 @@ func highlight_adjacent_cells(): func highlight_empty_adjacent_cells(): action_manager.highlight_empty_adjacent_cells() -@rpc("any_peer", "call_local") -func sync_action_points(points: int): - action_manager.sync_action_points(points) func highlight_random_valid_cells(): action_manager.highlight_random_valid_cells() diff --git a/scripts/bot_controller.gd b/scripts/bot_controller.gd index 0440829..8ce6726 100644 --- a/scripts/bot_controller.gd +++ b/scripts/bot_controller.gd @@ -247,21 +247,12 @@ func _run_ai_tick(): 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) + # Only stop completely if objectives are met and at finish (or standard mode) 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. - if TurnManager.turn_based_mode and actor.action_points > 0: - print("[BotController] %s is STUCK with AP %d! Skipping turn to proceed flow." % [actor.name, actor.action_points]) - actor.consume_action_points(actor.action_points) + return # ============================================================================= # Power-Up / Sabotage @@ -501,9 +492,6 @@ func _find_nearest_victim() -> Node3D: func _try_grab() -> bool: """Try to grab a tile from the grid using direct player control.""" - # Check AP only if turn-based - if TurnManager.turn_based_mode and actor.action_points <= 0: - return false if _is_playerboard_full(): return false @@ -572,8 +560,6 @@ func _find_tile_to_grab(tiles_needed: Array) -> Dictionary: func _try_move() -> bool: """Try to move toward needed tiles taking single steps like a player.""" - if TurnManager.turn_based_mode and actor.action_points <= 0: - return false if _is_goals_achieved() and LobbyManager.game_mode != "Stop n Go": return false @@ -700,9 +686,6 @@ func _try_unstuck_move() -> bool: func _try_put(high_priority: bool = false) -> bool: """Try to put a tile from playerboard onto grid.""" - if actor.action_points <= 0: - return false - var put_slot = -1 # Check for Panic Mode @@ -718,21 +701,17 @@ func _try_put(high_priority: bool = false) -> bool: is_panic = true if is_panic: - # Aggressive dumping put_slot = strategic_planner.get_unneeded_tile_slot_panic() else: - # Normal smart dumping put_slot = strategic_planner.get_unneeded_tile_slot() if put_slot == -1: return false - # Find empty adjacent cell var put_position = _find_empty_adjacent_cell() if not put_position: return false - # Execute put _is_processing_action = true _current_action = "putting" @@ -742,7 +721,6 @@ func _try_put(high_priority: bool = false) -> bool: actor.playerboard[put_slot] = -1 actor.rpc("sync_grid_item", cell.x, cell.y, cell.z, item) actor.rpc("sync_playerboard", actor.playerboard) - actor.action_points -= 1 print("[BotController] %s put unneeded tile %d at %s (Panic: %s)" % [actor.name, item, put_position, is_panic]) await _wait_with_variance(action_delay) @@ -773,18 +751,13 @@ func _find_empty_adjacent_cell() -> Vector2i: func _try_arrange() -> bool: """Try to rearrange tiles in playerboard.""" - if actor.action_points < 2: - return false - if _is_goals_achieved(): return false - # Find misplaced item and better position var arrangement = _find_best_arrangement() if arrangement.is_empty(): return false - # Execute arrangement _is_processing_action = true _current_action = "arranging" @@ -793,7 +766,6 @@ func _try_arrange() -> bool: actor.playerboard[arrangement.target_slot] = item actor.playerboard[arrangement.source_slot] = -1 actor.rpc("sync_playerboard", actor.playerboard) - actor.action_points -= 2 print("[BotController] %s arranged slot %d -> %d" % [actor.name, arrangement.source_slot, arrangement.target_slot]) await _wait_with_variance(action_delay) diff --git a/scripts/managers/goals_cycle_manager.gd b/scripts/managers/goals_cycle_manager.gd index 574e3f3..854c020 100644 --- a/scripts/managers/goals_cycle_manager.gd +++ b/scripts/managers/goals_cycle_manager.gd @@ -215,7 +215,9 @@ func sync_cycle_end(): func on_goal_completed(player: Node, time_remaining: float): """Called when a player completes their goal pattern.""" - # 1. LOCAL OPTIMISTIC UPDATE (for smoothness) + # CLIENT PATH: clear board immediately for visual responsiveness, + # then let server send back the single authoritative new goals. + # Do NOT generate goals locally — that caused rollback/blinking. if player.is_multiplayer_authority() and not multiplayer.is_server(): _handle_local_goal_completion(player, time_remaining) return @@ -223,48 +225,38 @@ func on_goal_completed(player: Node, time_remaining: float): if not multiplayer.is_server(): return - # SERVER LOGIC continues... + # SERVER LOGIC _process_goal_completion(player, time_remaining) func _handle_local_goal_completion(player: Node, time_remaining: float): - print("[GoalsCycle] Client: Handling goal completion locally for smoothness.") + print("[GoalsCycle] Client: Goal completed — clearing board and requesting server sync.") - # Clear playerboard locally + # Clear playerboard locally for immediate visual feedback (empty board) player.playerboard.fill(-1) - # Generate new goals locally (optimistic) - var new_goals = GoalManager.initialize_random_goals(9, 7, 10, 1.0) - var int_goals: Array[int] = [] - for g in new_goals: - int_goals.append(g) - player.goals = int_goals - - # Update UI immediately + # Update UI immediately so the board shows empty rather than stale if main_scene and main_scene.ui_manager: main_scene.ui_manager.update_playerboard_ui() - # Notify server to sync score and broadcast to others - rpc_id(1, "request_server_goal_completion", time_remaining, int_goals) - - # Visual/Sfx + # Play sound locally — server will also trigger it, but client plays first for responsiveness SfxManager.play("complete_mission") + + # Notify server — server will validate, award score, and broadcast authoritative new goals + rpc_id(1, "request_server_goal_completion", time_remaining) @rpc("any_peer") -func request_server_goal_completion(time_remaining: float, client_generated_goals: Array): - if not multiplayer.is_server(): return +func request_server_goal_completion(time_remaining: float): + """Client notifies server of goal completion. Server validates, awards score, + and broadcasts authoritative new goals back to all peers.""" + if not multiplayer.is_server(): + return var sender_id = multiplayer.get_remote_sender_id() - # Bots call it locally, so sender_id might be 1 or 0 if sender_id == 0: sender_id = 1 var player_node = main_scene.get_node_or_null(str(sender_id)) if player_node: - # Use provided goals from client to ensure sync - var int_goals: Array[int] = [] - for g in client_generated_goals: int_goals.append(g) - - # Process completion with provided goals - _process_goal_completion(player_node, time_remaining, int_goals) + _process_goal_completion(player_node, time_remaining) func _process_goal_completion(player: Node, time_remaining: float, provided_goals: Array = []): """Internal server-side logic for completion rewards and broadcasting.""" diff --git a/scripts/managers/player_action_manager.gd b/scripts/managers/player_action_manager.gd index b0d3f60..d4f2bd4 100644 --- a/scripts/managers/player_action_manager.gd +++ b/scripts/managers/player_action_manager.gd @@ -11,29 +11,9 @@ func initialize(p_player: Node3D, p_gridmap: Node): player = p_player enhanced_gridmap = p_gridmap -# ============================================================================= -# Action Point Management -# ============================================================================= - -func consume_action_points(points: int): - if not is_instance_valid(player) or not player.is_multiplayer_authority(): - return - - var main = player.get_tree().get_root().get_node_or_null("Main") - if not main: - return - - # Non-turn-based mode: unlimited action points for real-time fast-paced gameplay - if not TurnManager.turn_based_mode: - after_action_completed() - return - - # Turn-based mode: consume action points normally - player.action_points -= points - - if player.action_points <= 0: - main.request_next_turn() - +func consume_action_points(_points: int): + # Action points are no longer used (real-time mode only). + # Kept as a no-op stub so call sites don't need mass refactoring. after_action_completed() func after_action_completed(): diff --git a/scripts/managers/player_movement_manager.gd b/scripts/managers/player_movement_manager.gd index 4744fce..e97f03c 100644 --- a/scripts/managers/player_movement_manager.gd +++ b/scripts/managers/player_movement_manager.gd @@ -306,7 +306,7 @@ func _on_movement_finished(): emit_signal("movement_finished") func move_to_clicked_position(grid_position: Vector2i) -> bool: - if not player.is_multiplayer_authority() or is_moving or player.action_points <= 0: + if not player.is_multiplayer_authority() or is_moving: return false # Check if player is frozen diff --git a/scripts/managers/playerboard_manager.gd b/scripts/managers/playerboard_manager.gd index 5024198..57ba0e5 100644 --- a/scripts/managers/playerboard_manager.gd +++ b/scripts/managers/playerboard_manager.gd @@ -31,14 +31,9 @@ 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: 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) @@ -109,19 +104,15 @@ func grab_item(grid_position: Vector2i) -> bool: if sng_manager and sng_manager.has_method("update_mission_progress"): sng_manager.update_mission_progress(player.name.to_int(), item) - # Update UI immediately for responsiveness - # Update UI immediately for responsiveness var main = player.get_tree().get_root().get_node_or_null("Main") if main and main.ui_manager and not (player.is_bot or player.is_in_group("Bots")): main.ui_manager.update_playerboard_ui() - # Check if goal is completed after grabbing - _check_goal_completion() - # === Server Sync === if multiplayer.is_server(): - # HOST/SERVER: Broadcast to all clients + # HOST/SERVER: Check goal, then broadcast authoritative state to all clients + _check_goal_completion() main.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1) var peer_id = player.name.to_int() main.rpc("sync_playerboard", peer_id, player.playerboard) @@ -129,17 +120,21 @@ func grab_item(grid_position: Vector2i) -> bool: player.consume_action_points(1) player.rpc("force_action_state_none") else: - # CLIENT: Optimistic Local Update for lag compensation + # CLIENT: Optimistic local visual only — do NOT check goal completion here. + # The server checks authoritatively in _execute_grab and broadcasts results. + # This prevents double-award and the goal-blink caused by optimistic rollback. player.has_performed_action = true - player.consume_action_points(1) + # Do NOT consume action points here — server does it and syncs back via + # sync_action_points (or force_sync_to_client on failure), preventing double-decrement. player.force_action_state_none() - # Send RPC request to server for validation - player.rpc_id(1, "request_server_grab", grid_position, cell.x, cell.y, cell.z, item) + # Send RPC to server — include the slot we used locally so server places in same slot, + # preventing the slot mismatch that caused board overwrites on rapid grabs. + player.rpc_id(1, "request_server_grab", grid_position, cell.x, cell.y, cell.z, item, target_slot) - return true # Action applied locally + return true # Action applied locally (grid visual only) -func _execute_grab(grid_pos: Vector2i, cell: Vector3i, item_id: int): +func _execute_grab(grid_pos: Vector2i, cell: Vector3i, item_id: int, expected_slot: int = -1): var main = player.get_tree().get_root().get_node_or_null("Main") if not main: push_error("Server: Main node not found.") @@ -153,31 +148,32 @@ func _execute_grab(grid_pos: Vector2i, cell: Vector3i, item_id: int): # 1. Server-side Validation var server_item = server_gridmap.get_cell_item(cell) - # Check if item is still there + # Check if item is still there (critical — prevents grabbing already-taken tiles) if server_item != item_id: print("Server: Item mismatch or already taken. Server has ", server_item) _force_sync_to_client(cell, server_item) return false - # Check action points - if player.action_points <= 0: - print("Server: Player has no action points.") - _force_sync_to_client(cell, server_item) - return false - - # Check adjacency - if grid_pos != player.current_position: - var neighbors = server_gridmap.get_neighbors(player.current_position, 0) - if not neighbors.any(func(n): return n.position == grid_pos): - print("Server: Player is not adjacent to item.") - _force_sync_to_client(cell, server_item) - return false + # NOTE: Adjacency check intentionally removed. + # The client validates adjacency locally before sending the RPC. + # With high-latency connections the server's player.current_position lags, + # causing false rejections that make tiles "disappear" from the floor without + # registering on the board. Item-existence + slot-availability checks are sufficient. - # 2. Server-side Auto-Arrange + # 2. Server-side slot resolution var is_powerup = (item_id >= 11 and item_id <= 14) var target_slot = -1 if not is_powerup: - target_slot = find_best_goal_slot_for_item(item_id) + # Honour the client's expected slot if it is still empty on the server. + # This ensures client and server agree on placement during rapid multi-grabs, + # so sync_playerboard_slot (slot-only update) is safe and unambiguous. + if expected_slot >= 0 and expected_slot < player.playerboard.size() \ + and player.playerboard[expected_slot] == -1 \ + and not (expected_slot in HIDDEN_SLOTS): + target_slot = expected_slot + else: + # Fallback: find the best available slot (e.g. client sent stale slot) + target_slot = find_best_goal_slot_for_item(item_id) if target_slot == -1: print("Server: Player has no valid slot for item.") _force_sync_to_client(cell, server_item) @@ -193,7 +189,7 @@ func _execute_grab(grid_pos: Vector2i, cell: Vector3i, item_id: int): var special_tiles_manager = player.get_node_or_null("SpecialTilesManager") if special_tiles_manager: special_tiles_manager.add_powerup_from_item(item_id) - # Do not add to playerboard + # Powerup consumed instantly — no board slot update needed else: player.playerboard[target_slot] = item_id @@ -202,24 +198,21 @@ func _execute_grab(grid_pos: Vector2i, cell: Vector3i, item_id: int): if sng_manager and sng_manager.has_method("update_mission_progress"): sng_manager.update_mission_progress(player.name.to_int(), item_id) - # 3c. Broadcast the new playerboard state to all clients + # 3c. Broadcast ONLY THE CHANGED SLOT to all clients. + # Using sync_playerboard_slot instead of full sync_playerboard prevents + # a rapid second grab (still in-flight on client) from being overwritten. var peer_id = player.name.to_int() - main.rpc("sync_playerboard", peer_id, player.playerboard) + if not is_powerup: + main.rpc("sync_playerboard_slot", peer_id, target_slot, item_id) - # 3d. Check if goal is completed (SERVER-SIDE - this triggers goal regeneration for clients!) - # Logic only runs if board changed, but theoretically powerup pickup shouldn't trigger goal + # 3d. Check if goal is completed (SERVER-SIDE) if not is_powerup: _check_goal_completion() - # 3e. Consume action points - player.has_performed_action = true - player.consume_action_points(1) - - # 3f. Reset the UI for the player who acted + # Reset the UI for the player who acted player.rpc("force_action_state_none") # 4. Check if we need to respawn tiles (Scarcity Logic) - # "during no more tiles" -> Refill if floor is empty _check_and_refill_grid_if_needed(server_gridmap) return true @@ -281,14 +274,10 @@ func _force_sync_to_client(cell: Vector3i, server_item: int): # Sync their Playerboard (which they thought they updated) main.rpc_id(peer_id, "sync_playerboard", peer_id, player.playerboard) - # Restore Action Points locally for the client (sync action points back) - # player is the caller peer since this is the server instance of the player's manager - player.rpc_id(peer_id, "sync_action_points", player.action_points) - print("Server: Forced sync to client %d due to action failure." % peer_id) func bot_try_grab_item() -> bool: - if not enhanced_gridmap or player.action_points <= 0: + if not enhanced_gridmap: return false # First check current position @@ -326,7 +315,6 @@ func bot_try_grab_item() -> bool: main.rpc("sync_grid_item", current_cell.x, current_cell.y, current_cell.z, -1) main.rpc("sync_playerboard", player.name.to_int(), player.playerboard) player.has_performed_action = true - player.action_points -= 1 _check_goal_completion() return true @@ -357,7 +345,6 @@ func bot_try_grab_item() -> bool: main.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1) main.rpc("sync_playerboard", player.name.to_int(), player.playerboard) player.has_performed_action = true - player.action_points -= 1 return true return false @@ -368,7 +355,7 @@ func bot_try_grab_item() -> bool: func auto_put_item() -> bool: # Check AP only if in turn-based mode - var has_ap = player.action_points > 0 if TurnManager.turn_based_mode else true + var has_ap = true # Action points removed; put always allowed if not enhanced_gridmap or not has_ap or player.is_bot or player.is_in_group("Bots") or player.get("is_frozen"): return false