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
+17 -25
View File
@@ -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."""
+3 -23
View File
@@ -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():
+1 -1
View File
@@ -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
+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