Files
tekton/scripts/managers/candy_survival_manager.gd
T
2026-07-09 16:27:34 +08:00

806 lines
26 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
# 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 attacker_node and target_node:
if attacker_node.has_method("can_rpc") and attacker_node.can_rpc():
attacker_node.rpc("sync_bump", target_node.current_position)
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])
# Attacker is knocked down briefly (backfire)
if attacker_node:
if attacker_node.has_method("can_rpc") and attacker_node.can_rpc():
attacker_node.rpc("apply_stagger", 2.0)
elif attacker_node.has_method("apply_stagger"):
attacker_node.apply_stagger(2.0)
# 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
# Target is knocked down / staggered on successful knock
if target_node:
if target_node.has_method("can_rpc") and target_node.can_rpc():
target_node.rpc("apply_stagger", 2.0)
elif target_node.has_method("apply_stagger"):
target_node.apply_stagger(2.0)
_add_score(attacker, target_candies * 100)
_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
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]
@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"):
player.rpc_id(peer_id, "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 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)
@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