feat: fix some bug

This commit is contained in:
2026-04-24 22:56:11 +08:00
parent 8c5004d535
commit b76dd2e737
9 changed files with 105 additions and 156 deletions
+39 -52
View File
@@ -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