extends Node class_name CandySurvivalManager # Candy Survival — Sugar Rush Chaos # Speed – Building – Sabotage (blueprints + power-ups) signal sugar_rush_started(pid) signal sugar_rush_ended(pid) signal mekton_face_changed(color_index) # ── Arena ── const ARENA_COLS: int = 18 const ARENA_ROWS: int = 18 const NPC_CENTER: Vector2i = Vector2i(8, 8) const NPC_CELLS: int = 3 # 3x3 Mekton zone # ── Blueprint ── const BLUEPRINT_SIZE: int = 3 const OFF_COLOR_PENALTY: float = 0.5 # ── Candy ── const CANDY_PPS: float = 1.0 # points per second per candy const MULTI_STEP: float = 0.1 # extra multiplier per candy # ── Sugar Rush ── const SR_BASE_TIME: float = 2.0 const SR_PER_CANDY: float = 0.4 # extra seconds per delivered candy const SR_SPEED: float = 2.0 # ── Knock / Ghost ── const START_KNOCK: int = 5 const START_GHOST: int = 5 const GHOST_DURATION: float = 4.0 # ── Mekton ── const COLOR_CYCLE_TIME: float = 5.0 # seconds between face color changes enum CandyColor { HEART, DIAMOND, STAR, COIN } # ── State ── var active: bool = false var main_scene: Node var gridmap: Node var mekton_node: Node var current_face: CandyColor = CandyColor.HEART var face_timer: float = 0.0 var game_elapsed: float = 0.0 # ── HUD ── var hud_layer: CanvasLayer var hud_knock_label: Label var hud_ghost_label: Label var hud_rush_bar: ProgressBar var hud_rush_label: Label var hud_stack_badge: Label var hud_points_label: Label var hud_delivery_indicator: Label var _candy_survival_hud_scene: PackedScene = preload("res://scenes/candy_survival_hud.tscn") # Cache for local client HUD resync (avoids local dictionary/timer desyncs) var _last_badge_count: int = 0 var _last_badge_mult: float = 1.0 var _last_badge_face_match: bool = false func _ready(): set_process(false) _setup_hud() func _exit_tree() -> void: Engine.time_scale = 1.0 func _setup_hud() -> void: var hud_instance = _candy_survival_hud_scene.instantiate() hud_layer = hud_instance hud_layer.visible = false add_child(hud_layer) var bot_box = hud_layer.get_node_or_null("BottomContainer/HBoxContainer") if bot_box: hud_knock_label = bot_box.get_node_or_null("SabotagePanel/KnockChargesLabel") hud_ghost_label = bot_box.get_node_or_null("SabotagePanel/GhostChargesLabel") hud_stack_badge = bot_box.get_node_or_null("ScorePanel/CandyStackBadge") hud_points_label = bot_box.get_node_or_null("ScorePanel/PointsLabel") hud_delivery_indicator = bot_box.get_node_or_null("ScorePanel/DeliveryIndicator") var top_box = hud_layer.get_node_or_null("TopContainer") if top_box: hud_rush_bar = top_box.get_node_or_null("SugarRushBar") if hud_rush_bar: hud_rush_label = hud_rush_bar.get_node_or_null("SugarRushLabel") func activate_client_side() -> void: active = true if hud_layer: hud_layer.visible = true set_process(true) var local = multiplayer.get_unique_id() player_knocks[local] = START_KNOCK player_ghosts[local] = START_GHOST if hud_stack_badge: hud_stack_badge.text = "Stack: 0 (x1.0)\nRed: 0x Blue: 0x Green: 0x Yellow: 0x" if hud_knock_label: hud_knock_label.text = "🥊 Knocks: 5" if hud_ghost_label: hud_ghost_label.text = "👻 Ghosts: 5" # Per-player state var player_candies: Dictionary = {} # pid -> int (stack count) var player_candy_colors: Dictionary = {} # pid -> Array of CandyColor var player_candy_color: Dictionary: get: var d = {} for pid in player_candy_colors: var colors = player_candy_colors[pid] d[pid] = colors.back() if colors.size() > 0 else -1 return d var player_knocks: Dictionary = {} # pid -> int (charges left) var player_ghosts: Dictionary = {} # pid -> int (charges left) var player_ghost_active: Dictionary = {} # pid -> bool var player_ghost_timer: Dictionary = {} # pid -> float var player_score: Dictionary = {} # pid -> int var player_blueprints: Dictionary = {} # pid -> int (completed count) var player_sugar_rush: Dictionary = {} # pid -> float (remaining) var player_last_knocked_by: Dictionary = {} # pid -> int # Sticky wall cells (pre-placed collision only) var sticky_cells: Dictionary = {} # Vector2i -> bool # Tile ID mapping const TILE_IDS: Dictionary = {CandyColor.HEART: 7, CandyColor.DIAMOND: 8, CandyColor.STAR: 9, CandyColor.COIN: 10} # ── Initialization ── func initialize(main, grid) -> void: main_scene = main gridmap = grid _define_sticky_walls() print("[CandySurvival] Initialized") func _define_sticky_walls() -> void: # Arena boundary walls (rows/cols 0 and 17) for x in range(ARENA_COLS): sticky_cells[Vector2i(x, 0)] = true sticky_cells[Vector2i(x, ARENA_ROWS - 1)] = true for y in range(ARENA_ROWS): sticky_cells[Vector2i(0, y)] = true sticky_cells[Vector2i(ARENA_COLS - 1, y)] = true # Mekton NPC zone (3x3 center) for dx in range(NPC_CELLS): for dy in range(NPC_CELLS): sticky_cells[Vector2i(NPC_CENTER.x + dx, NPC_CENTER.y + dy)] = true func start_game_mode() -> void: active = true current_face = CandyColor.HEART face_timer = 0.0 game_elapsed = 0.0 player_candies.clear() player_candy_colors.clear() player_knocks.clear() player_ghosts.clear() player_ghost_active.clear() player_ghost_timer.clear() player_score.clear() player_blueprints.clear() player_sugar_rush.clear() player_last_knocked_by.clear() # Init players from lobby var pids = _get_player_ids() for pid in pids: player_candies[pid] = 0 player_candy_colors[pid] = [] player_knocks[pid] = START_KNOCK player_ghosts[pid] = START_GHOST player_ghost_active[pid] = false player_ghost_timer[pid] = 0.0 player_score[pid] = 0 player_blueprints[pid] = 0 player_sugar_rush[pid] = 0.0 player_last_knocked_by[pid] = 0 _update_candy_badge(pid) _update_special_inventory_for_ghosts(pid) rpc("sync_knock_charge_count", pid, START_KNOCK) rpc("sync_ghost_charge_count", pid, START_GHOST) if multiplayer.is_server(): activate_client_side() print("[CandySurvival] Started with %d players" % pids.size()) func _enter_tree() -> void: set_process(true) func _process(delta: float) -> void: if not active: return if not multiplayer.is_server(): return game_elapsed += delta # Mekton face color cycle face_timer += delta if face_timer >= COLOR_CYCLE_TIME: face_timer = 0.0 current_face = CandyColor.values()[(current_face + 1) % CandyColor.size()] # Sync color to the Mekton NPC if mekton_node and is_instance_valid(mekton_node) and mekton_node.has_method("set_face_color_rpc"): if multiplayer.is_server() and can_rpc(): mekton_node.rpc("set_face_color_rpc", current_face) else: mekton_node.set_face_color_rpc(current_face) # Update delivery indicators for all players based on new face for pid in player_candies: _update_candy_badge(pid) # Sugar Rush timers var any_rush = false for pid in player_sugar_rush.keys(): if player_sugar_rush[pid] > 0: player_sugar_rush[pid] -= delta if player_sugar_rush[pid] <= 0: player_sugar_rush[pid] = 0.0 _restore_time_scale() sugar_rush_ended.emit(pid) if player_sugar_rush[pid] > 0: any_rush = true if any_rush: Engine.time_scale = SR_SPEED # Ghost timers for pid in player_ghost_active.keys(): if player_ghost_active[pid]: player_ghost_timer[pid] -= delta if player_ghost_timer[pid] <= 0: player_ghost_active[pid] = false player_ghost_timer[pid] = 0.0 rpc("sync_ghost_active", pid, false) # Turn off ghost visual/state var all_players = get_tree().get_nodes_in_group("Players") for player in all_players: var curr_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int() if curr_pid == pid: player.set("is_invisible", false) if player.has_method("sync_modulate"): if multiplayer.is_server() and player.has_method("can_rpc") and player.can_rpc(): player.rpc("sync_modulate", Color.WHITE) else: player.sync_modulate(Color.WHITE) break # Candy stack: points per second per candy for pid in player_candies.keys(): var count = player_candies.get(pid, 0) if count > 0: var points = count * CANDY_PPS * delta _add_score(pid, int(points)) # ── Blueprint ── func on_blueprint_completed(pid: int, primary_tile_id: int, off_color: bool) -> void: player_blueprints[pid] = player_blueprints.get(pid, 0) + 1 if player_blueprints[pid] % 2 == 0: if multiplayer.is_server(): add_ghost_charge(pid) var target_tile_id = primary_tile_id var player_node: Node = null if main_scene: player_node = main_scene.get_node_or_null(str(pid)) if player_node and player_node.goals.size() > 0: target_tile_id = player_node.goals[0] var candy_color = CandyColor.HEART for key in TILE_IDS: if TILE_IDS[key] == target_tile_id: candy_color = key break _give_candy(pid, candy_color) _add_score(pid, 100) # Award +100 points when completing goals by collecting tiles # Clear the board now that it's matched and converted to a candy if main_scene and player_node: if player_node.playerboard_manager: # Visual wipe player_node.playerboard.fill(-1) main_scene.rpc("sync_playerboard", pid, player_node.playerboard) # Record goal completion stats in GoalsCycleManager var goals_cycle = main_scene.get_node_or_null("GoalsCycleManager") if goals_cycle: # Increment goal count (but DO NOT trigger the general score points # because in Candy Survival, points are awarded during Mekton deposit) if not goals_cycle.player_goal_counts.has(pid): goals_cycle.player_goal_counts[pid] = 0 goals_cycle.player_goal_counts[pid] += 1 goals_cycle.emit_signal("goal_count_updated", pid, goals_cycle.player_goal_counts[pid]) goals_cycle.rpc("sync_goal_count", pid, goals_cycle.player_goal_counts[pid]) # Generate new goals immediately to keep player active goals_cycle.regenerate_goals_for_player(player_node) # Need to randomize tiles around player to refresh pickups for the new goal goals_cycle._randomize_tiles_around_player(player_node) func can_finish_with_off_color(pid: int, primary_tile_id: int) -> bool: return not _grid_has_color_tiles(primary_tile_id) func _grid_has_color_tiles(target_tile_id: int) -> bool: if not gridmap: return true for x in range(1, ARENA_COLS - 1): for y in range(1, ARENA_ROWS - 1): var v = Vector3i(x, 1, y) if gridmap.get_cell_item(v) == target_tile_id: return true return false # ── Candy Stack ── func _give_candy(pid: int, color: int) -> void: player_candies[pid] = player_candies.get(pid, 0) + 1 if not player_candy_colors.has(pid): player_candy_colors[pid] = [] player_candy_colors[pid].append(color) _update_candy_badge(pid) func _update_candy_badge(pid: int) -> void: var count = player_candies.get(pid, 0) var mult = 1.0 + count * MULTI_STEP var colors = player_candy_colors.get(pid, []) var color = colors.back() if colors.size() > 0 else -1 var face_match = (color != -1) var score = player_score.get(pid, 0) rpc("sync_candy_badge", pid, count, mult, face_match, score, colors) func get_multiplier(pid: int) -> float: var count = player_candies.get(pid, 0) return 1.0 + count * MULTI_STEP # ── Mekton Delivery ── @rpc("any_peer", "call_local", "reliable") func try_deliver(pid: int) -> bool: if not multiplayer.is_server(): return false var colors = player_candy_colors.get(pid, []) var color = colors.back() if colors.size() > 0 else -1 if color == -1 or colors.size() == 0: rpc("sync_delivery_result", pid, false, "No candy") return false var count = colors.size() var mult = get_multiplier(pid) var base_points = 1000 * count _add_score(pid, int(base_points * mult)) player_candies[pid] = 0 player_candy_colors[pid] = [] _update_candy_badge(pid) _trigger_sugar_rush(pid, count, mult) rpc("sync_delivery_result", pid, true, "Delivered!") return true func _trigger_sugar_rush(pid: int, candies: int, mult: float) -> void: var duration = SR_BASE_TIME + candies * SR_PER_CANDY * mult player_sugar_rush[pid] = duration Engine.time_scale = SR_SPEED sugar_rush_started.emit(pid) func _restore_time_scale() -> void: var any_rush = false for pid in player_sugar_rush.keys(): if player_sugar_rush[pid] > 0: any_rush = true break if not any_rush: Engine.time_scale = 1.0 # ── Knock / Ghost ── @rpc("any_peer", "call_local", "reliable") func try_knock(attacker: int, target: int) -> bool: if not multiplayer.is_server(): return false if player_ghost_active.get(target, false): return false if not player_knocks.has(attacker): player_knocks[attacker] = START_KNOCK if player_knocks[attacker] <= 0: return false var target_colors = player_candy_colors.get(target, []) var target_candies = target_colors.size() var attacker_node = _find_player_node(attacker) var target_node = _find_player_node(target) if target_candies == 0: # Backfire: attacker loses a charge & gets staggered player_knocks[attacker] = player_knocks.get(attacker, START_KNOCK) - 1 rpc("sync_knock_charge_count", attacker, player_knocks[attacker]) # Backfire penalty: Attacker's candy stack transfers to Target! var attacker_colors = player_candy_colors.get(attacker, []) var attacker_candies = attacker_colors.size() if attacker_candies > 0: if not player_candy_colors.has(target): player_candy_colors[target] = [] player_candy_colors[target].append_array(attacker_colors) player_candy_colors[attacker] = [] player_candies[target] = player_candy_colors[target].size() player_candies[attacker] = 0 _add_score(target, attacker_candies * 100) _update_candy_badge(attacker) _update_candy_badge(target) rpc("sync_knock_result", attacker, target, 0) return false # Steal all candies (Successful knock) if not player_candy_colors.has(attacker): player_candy_colors[attacker] = [] player_candy_colors[attacker].append_array(target_colors) player_candy_colors[target] = [] player_candies[attacker] = player_candy_colors[attacker].size() player_candies[target] = 0 player_knocks[attacker] = player_knocks.get(attacker, START_KNOCK) - 1 rpc("sync_knock_charge_count", attacker, player_knocks[attacker]) player_last_knocked_by[target] = attacker _add_score(attacker, target_candies * 100 + 100) # Award +100 points for using KNOCK + 100 per candy stolen _update_candy_badge(attacker) _update_candy_badge(target) rpc("sync_knock_result", attacker, target, target_candies) return true @rpc("any_peer", "call_local", "reliable") func try_activate_ghost(pid: int) -> bool: if player_ghost_active.get(pid, false): return false if player_ghosts.get(pid, 0) <= 0: return false player_ghosts[pid] = player_ghosts.get(pid, 0) - 1 player_ghost_active[pid] = true player_ghost_timer[pid] = GHOST_DURATION rpc("sync_ghost_active", pid, true) rpc("sync_ghost_charge_count", pid, player_ghosts[pid]) _update_special_inventory_for_ghosts(pid) # Core powerup integration: set is_invisible on the player node var all_players = get_tree().get_nodes_in_group("Players") for player in all_players: var curr_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int() if curr_pid == pid: player.set("is_invisible", true) if player.has_method("sync_modulate"): if multiplayer.is_server() and player.has_method("can_rpc") and player.can_rpc(): player.rpc("sync_modulate", Color(1.0, 1.0, 1.0, 0.4)) else: player.sync_modulate(Color(1.0, 1.0, 1.0, 0.4)) break _add_score(pid, 100) # Award +100 points for tactically using GHOST return true func is_ghost_active(pid: int) -> bool: return player_ghost_active.get(pid, false) func can_rpc() -> bool: if not multiplayer.has_multiplayer_peer() or multiplayer.multiplayer_peer.get_connection_status() != MultiplayerPeer.CONNECTION_CONNECTED: return false return true # ── Arena ── func _setup_arena() -> void: if not gridmap: return if can_rpc(): rpc("sync_arena_setup") _apply_arena_setup() @rpc("authority", "call_remote", "reliable") func sync_arena_setup() -> void: print("[CandySurvival] Client: Syncing Arena Setup (18x18)...") _apply_arena_setup() func _spawn_mekton_npc(center: Vector2i) -> Node: var path = "res://scenes/candy_survival_npc.tscn" if ResourceLoader.exists(path): var npc = load(path).instantiate() npc.name = "CandySurvivalNpc" npc.position = Vector3(center.x + 1, 0, center.y + 1) add_child(npc, true) if npc.has_method("set_face_color_rpc"): npc.set_face_color_rpc(current_face) return npc return null func _apply_arena_setup() -> void: if not gridmap: return gridmap.set("columns", ARENA_COLS) gridmap.set("rows", ARENA_ROWS) gridmap.clear() # Build floor for x in range(ARENA_COLS): for y in range(ARENA_ROWS): gridmap.set_cell_item(Vector3i(x, 0, y), 0) # Apply sticky walls on layer 2 for pos in sticky_cells.keys(): gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), 14) # Spawn Mekton NPC at center if mekton_node and is_instance_valid(mekton_node): mekton_node.queue_free() mekton_node = _spawn_mekton_npc(NPC_CENTER) print("[CandySurvival] Arena setup applied 18x18") func setup_mission_tiles() -> void: if not gridmap: return randomize() var tile_ids = [7, 8, 9, 10] var count = 0 for x in range(1, ARENA_COLS - 1): for y in range(1, ARENA_ROWS - 1): if sticky_cells.has(Vector2i(x, y)): continue if randf() < 0.6: var tile = tile_ids[randi() % tile_ids.size()] gridmap.set_cell_item(Vector3i(x, 1, y), tile) count += 1 print("[CandySurvival] Spawned %d mission tiles" % count) func get_spawn_points(count: int) -> Array: var points = [] var corners = [ Vector2i(2, 2), Vector2i(ARENA_COLS - 2, 2), Vector2i(2, ARENA_ROWS - 2), Vector2i(ARENA_COLS - 2, ARENA_ROWS - 2) ] for i in range(count): points.append(corners[i % corners.size()]) return points func get_arena_positions() -> Dictionary: return {"columns": ARENA_COLS, "rows": ARENA_ROWS} func get_arena_bounds() -> Dictionary: return {"min_x": -1.0, "max_x": float(ARENA_COLS), "min_z": -1.0, "max_z": float(ARENA_ROWS)} # ── Sticky (pure wall collision) ── func is_sticky_cell(pos: Vector2i) -> bool: return sticky_cells.has(pos) func _is_npc_zone(pos: Vector2i) -> bool: var cx = NPC_CENTER.x var cy = NPC_CENTER.y return pos.x >= cx and pos.x < cx + NPC_CELLS and pos.y >= cy and pos.y < cy + NPC_CELLS # ── Scoring ── func _add_score(pid: int, points: int) -> void: player_score[pid] = player_score.get(pid, 0) + points if main_scene and main_scene.has_method("add_points"): main_scene.add_points(pid, points) # ── Helpers ── func _get_player_ids() -> Array: if main_scene and main_scene.has_method("get_player_ids"): return main_scene.get_player_ids() var pids: Array = [] for p in get_tree().get_nodes_in_group("Players"): if not is_instance_valid(p): continue var pid = p.get("peer_id") if "peer_id" in p else p.name.to_int() if pid != 0 and not pids.has(pid): pids.append(pid) return pids func _find_player_node(pid: int) -> Node: if main_scene: var n = main_scene.get_node_or_null(str(pid)) if n: return n for p in get_tree().get_nodes_in_group("Players"): if not is_instance_valid(p): continue var p_id = p.get("peer_id") if "peer_id" in p else p.name.to_int() if p_id == pid: return p return null func candy_survival_round_duration() -> int: return 180 func get_score(pid: int) -> int: return player_score.get(pid, 0) # ── RPCs (stubs — HUD client handles visuals) ── @rpc("authority", "call_local", "reliable") func sync_candy_badge(pid: int, count: int, mult: float, face_match: bool, score: int, colors: Array) -> void: if not active: activate_client_side() player_candies[pid] = count player_candy_colors[pid] = colors player_score[pid] = score var p_node = _find_player_node(pid) if p_node and p_node.has_method("sync_candy_stack"): p_node.sync_candy_stack(colors) if pid == multiplayer.get_unique_id(): _last_badge_count = count _last_badge_mult = mult _last_badge_face_match = face_match if hud_stack_badge: var red = 0 var blue = 0 var yellow = 0 var green = 0 for col in colors: match int(col): 0: red += 1 1: blue += 1 2: green += 1 3: yellow += 1 hud_stack_badge.text = "Stack: %d (x%.1f)\nRed: %dx Blue: %dx Green: %dx Yellow: %dx" % [count, mult, red, blue, green, yellow] if hud_points_label: hud_points_label.text = "Score: %d" % score if hud_delivery_indicator: if count > 0: if face_match: hud_delivery_indicator.text = "READY TO DELIVER!" hud_delivery_indicator.add_theme_color_override("font_color", Color(0.4, 1.0, 0.4)) else: hud_delivery_indicator.text = "Waiting for match..." hud_delivery_indicator.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4)) else: hud_delivery_indicator.text = "" @rpc("authority", "call_remote", "unreliable") func sync_delivery_result(pid: int, success: bool, msg: String) -> void: if not active: activate_client_side() if pid != multiplayer.get_unique_id(): return if hud_delivery_indicator: if success: hud_delivery_indicator.text = "✓ Delivered! +Sugar Rush!" hud_delivery_indicator.add_theme_color_override("font_color", Color(0.3, 1.0, 0.3)) else: hud_delivery_indicator.text = "✗ " + msg hud_delivery_indicator.add_theme_color_override("font_color", Color(1.0, 0.3, 0.3)) # Flash and auto-clear after 1.5s var tween = create_tween() tween.tween_interval(1.5) tween.tween_callback(_restore_delivery_indicator) func _restore_delivery_indicator() -> void: if not active or not hud_delivery_indicator: return var count = _last_badge_count var face_match = _last_badge_face_match if count > 0: if face_match: hud_delivery_indicator.text = "READY TO DELIVER!" hud_delivery_indicator.add_theme_color_override("font_color", Color(0.4, 1.0, 0.4)) else: hud_delivery_indicator.text = "Waiting for match..." hud_delivery_indicator.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4)) else: hud_delivery_indicator.text = "" @rpc("authority", "call_local", "reliable") func sync_knock_result(attacker: int, target: int, candies: int) -> void: if not active: activate_client_side() var local = multiplayer.get_unique_id() if attacker == local or target == local: if hud_knock_label and player_knocks.has(local): hud_knock_label.text = "🥊 Knocks: %d" % player_knocks[local] var attacker_node = _find_player_node(attacker) var target_node = _find_player_node(target) if not attacker_node or not target_node: return # SFX: Play attack_mode sound like other game modes var sfx = Engine.get_main_loop().root.get_node_or_null("SfxManager") if sfx and sfx.has_method("play"): sfx.play("attack_mode") # Attacker bump animation toward target if attacker_node.has_method("sync_bump"): attacker_node.sync_bump(target_node.current_position, false) # Calculate push direction from attacker to target var diff: Vector2i = target_node.current_position - attacker_node.current_position var push_dir = Vector2i(clamp(diff.x, -1, 1), clamp(diff.y, -1, 1)) if push_dir == Vector2i.ZERO: push_dir = Vector2i(1, 0) if candies > 0: # SUCCESSFUL STEAL: target has candies # Target is knocked back up to 3 tiles & stunned _apply_knock_animation_to_victim(target_node, push_dir, 2.0) else: # BACKFIRE: target had 0 candies # Target is NOT stunned ("whoever doesn't hold candy, will not stunned") # Attacker backfires: knocked back up to 3 tiles away from target & stunned _apply_knock_animation_to_victim(attacker_node, -push_dir, 2.0) func _apply_knock_animation_to_victim(victim_node: Node, push_dir: Vector2i, stagger_duration: float) -> void: if not is_instance_valid(victim_node): return var pushed_to_pos: Vector2i = victim_node.current_position var push_path: Array = [] for i in range(3): var next_back: Vector2i = pushed_to_pos + push_dir var can_push: bool = false if victim_node.get("movement_manager") and victim_node.movement_manager.has_method("_can_push_to"): can_push = victim_node.movement_manager._can_push_to(next_back) if can_push: pushed_to_pos = next_back push_path.append(Vector2(pushed_to_pos.x, pushed_to_pos.y)) if is_sticky_cell(pushed_to_pos): break else: break if push_path.size() > 0: if victim_node.has_method("start_movement_along_path"): victim_node.start_movement_along_path(push_path, false, true) victim_node.target_position = pushed_to_pos if victim_node.has_method("apply_stagger"): victim_node.apply_stagger(stagger_duration) @rpc("authority", "call_remote", "unreliable") func sync_ghost_active(pid: int, active_on: bool) -> void: if not active: activate_client_side() if pid == multiplayer.get_unique_id(): if hud_ghost_label and player_ghosts.has(pid): hud_ghost_label.text = "👻 Ghosts: %d" % player_ghosts[pid] func sync_state_to_player(peer_id: int) -> void: # Ensure the client gets the arena setup before state if can_rpc(): rpc_id(peer_id, "sync_arena_setup") # Sync Mekton's current face color if mekton_node and is_instance_valid(mekton_node) and mekton_node.has_method("set_face_color_rpc"): if can_rpc(): mekton_node.rpc_id(peer_id, "set_face_color_rpc", current_face) # Sync Ghost inventory to make the HUD button visible on player load _update_special_inventory_for_ghosts(peer_id) # Sync Knock and Ghost charges rpc_id(peer_id, "sync_knock_charge_count", peer_id, player_knocks.get(peer_id, START_KNOCK)) rpc_id(peer_id, "sync_ghost_charge_count", peer_id, player_ghosts.get(peer_id, START_GHOST)) # Sync all players' candy stack heights and colors for pid in player_candy_colors.keys(): var count = player_candies.get(pid, 0) var mult = get_multiplier(pid) var colors = player_candy_colors.get(pid, []) var color = colors.back() if colors.size() > 0 else -1 var face_match = (color != -1) var score = player_score.get(pid, 0) # Sync badge rpc_id(peer_id, "sync_candy_badge", pid, count, mult, face_match, score, colors) # Sync 3D stack meshes var all_players = get_tree().get_nodes_in_group("Players") for player in all_players: var curr_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int() if curr_pid == pid: if player.has_method("sync_candy_stack"): if peer_id in multiplayer.get_peers(): player.rpc_id(peer_id, "sync_candy_stack", colors) else: player.sync_candy_stack(colors) break func _update_special_inventory_for_ghosts(pid: int) -> void: if not multiplayer.is_server(): return if main_scene: var player_node = main_scene.get_node_or_null(str(pid)) if player_node: var st_manager = player_node.get_node_or_null("SpecialTilesManager") if st_manager: var has_charges = (player_ghosts.get(pid, 0) > 0) if pid in multiplayer.get_peers(): if has_charges: st_manager.rpc_id(pid, "sync_inventory_add", st_manager.SpecialEffect.INVISIBLE_MODE, 8) else: st_manager.rpc_id(pid, "sync_inventory_remove", st_manager.SpecialEffect.INVISIBLE_MODE) else: if has_charges: st_manager.sync_inventory_add(st_manager.SpecialEffect.INVISIBLE_MODE, 8) else: st_manager.sync_inventory_remove(st_manager.SpecialEffect.INVISIBLE_MODE) @rpc("any_peer", "call_local", "reliable") func add_ghost_charge(pid: int) -> void: if not multiplayer.is_server(): return player_ghosts[pid] = player_ghosts.get(pid, 0) + 1 rpc("sync_ghost_charge_count", pid, player_ghosts[pid]) _update_special_inventory_for_ghosts(pid) @rpc("authority", "call_local", "unreliable") func sync_ghost_charge_count(pid: int, count: int) -> void: if not active: activate_client_side() if player_ghosts.has(pid) or pid == multiplayer.get_unique_id(): player_ghosts[pid] = count if multiplayer.get_unique_id() == pid: if hud_ghost_label: hud_ghost_label.text = "👻 Ghosts: %d" % count @rpc("authority", "call_local", "unreliable") func sync_knock_charge_count(pid: int, count: int) -> void: if not active: activate_client_side() if player_knocks.has(pid) or pid == multiplayer.get_unique_id(): player_knocks[pid] = count if multiplayer.get_unique_id() == pid: if hud_knock_label: hud_knock_label.text = "🥊 Knocks: %d" % count