feat: fix some bug
This commit is contained in:
@@ -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
|
||||
|
||||
+10
-6
@@ -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")
|
||||
|
||||
+17
-8
@@ -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
|
||||
@@ -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()
|
||||
|
||||
|
||||
+5
-9
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
# 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.
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user