extends Node # UIManager - Handles all UI setup and updates const item_tex = [ preload("res://assets/textures/player_board_and_blue_print/tiles_slot.png"), preload("res://assets/textures/player_board_and_blue_print/tile_heart.tres"), preload("res://assets/textures/player_board_and_blue_print/tile_diamond.tres"), preload("res://assets/textures/player_board_and_blue_print/tile_star.tres"), preload("res://assets/textures/player_board_and_blue_print/tile_coin.tres") ] # Node references - will be set by Main # var victory_ui_scene = preload("res://scenes/ui/victory_ui.tscn") var victory_ui_scene = null var powerup_inventory_ui_script = preload("res://scripts/ui/powerup_inventory_ui.gd") var main_menu_instance var victory_ui_instance var playerboard_ui var action_menu_instance var powerup_inventory_ui var timer_label: Label var playerboard_label: Label # Shows (xN) goal completions var player_name_label: Label # Shows player name on main UI var local_player_character var _previous_playerboard_state: Array = [] func initialize(player_node): # Get PowerUp Inventory UI from scene powerup_inventory_ui = player_node.get_node_or_null("TouchLayer/TouchControls/PowerUpInventoryUI") # Get node references from main scene playerboard_ui = player_node.get_node_or_null("PlayerBoardUI/PlayerboardUI") if not playerboard_ui: playerboard_ui = player_node.get_node_or_null("PlayerboardUI") # Connect PlayerName label — now lives under PlayerBoardUI player_name_label = player_node.get_node_or_null("PlayerBoardUI/PlayerName") if not player_name_label: player_name_label = player_node.get_node_or_null("PlayerName") func set_local_player(player): local_player_character = player if powerup_inventory_ui: powerup_inventory_ui.setup(player) # Connect to powerup signals with deferred call (manager needs time to initialize) _connect_powerup_manager_deferred(player) # Update Player Name Label if player_name_label: player_name_label.text = player.display_name print("[UIManager] Updated PlayerName label to: ", player.display_name) func setup_playerboard_ui(): if not playerboard_ui: push_warning("playerboard_ui node is missing!") return for child in playerboard_ui.get_children(): child.queue_free() playerboard_ui.columns = 5 for i in range(25): var slot = TextureRect.new() var highlight_rect = TextureRect.new() var hr_tex = load("res://assets/models/pboard/HighlightRect.tres") var select_rect = TextureRect.new() var sr_tex = load("res://assets/models/pboard/SelectRect.tres") var adjacent_rect = TextureRect.new() var ar_tex = load("res://assets/models/pboard/AdjacentRect.tres") slot.custom_minimum_size = Vector2(36, 36) slot.texture = item_tex[0] # 0-based indices corresponding to User's 1-based request: 1,5,6,10,11,15,16,20,21,22,23,24,25 var hidden_slots = [0, 4, 5, 9, 10, 14, 15, 19, 20, 21, 22, 23, 24] if i in hidden_slots: slot.modulate = Color(1, 1, 1, 0) slot.mouse_filter = Control.MOUSE_FILTER_IGNORE else: slot.modulate = Color.WHITE slot.mouse_filter = Control.MOUSE_FILTER_PASS playerboard_ui.add_child(slot, true) highlight_rect.texture = hr_tex highlight_rect.size = Vector2(36, 36) select_rect.texture = sr_tex select_rect.size = Vector2(36, 36) adjacent_rect.texture = ar_tex adjacent_rect.size = Vector2(36, 36) slot.add_child(highlight_rect) slot.add_child(select_rect) slot.add_child(adjacent_rect) slot.get_child(0).hide() slot.get_child(1).hide() slot.get_child(2).hide() func update_playerboard_ui(): if not local_player_character or not playerboard_ui: return # Center 3x3 slot indices in a 5x5 grid (0-indexed) # Row 1: 6, 7, 8 # Row 2: 11, 12, 13 # Center 3x3 slot indices in a 5x5 grid (0-indexed) # Row 1: 6, 7, 8 (Now Storage - but kept in index map for goals[0-2]) # Row 2: 11, 12, 13 (Goals[3-5]) # Row 3: 16, 17, 18 (Goals[6-8]) var center_slots = [6, 7, 8, 11, 12, 13, 16, 17, 18] var goals = local_player_character.goals if local_player_character.goals else [] for i in range(25): var slot = playerboard_ui.get_child(i) # Safety check: Ensure playerboard has enough items if i >= local_player_character.playerboard.size(): continue var item = local_player_character.playerboard[i] # 0-based indices corresponding to User's 1-based request: 1,5,6,10,11,15,16,20,21,22,23,24,25 var hidden_slots = [0, 4, 5, 9, 10, 14, 15, 19, 20, 21, 22, 23, 24] # Default texture (empty) slot.texture = item_tex[0] if i in hidden_slots: slot.modulate = Color(1, 1, 1, 0) slot.mouse_filter = Control.MOUSE_FILTER_IGNORE else: slot.modulate = Color.WHITE slot.mouse_filter = Control.MOUSE_FILTER_PASS # Check if this is a center slot that should show a goal # BUT only show ghost goals for rows 2 & 3 (indices 11+) var center_index = center_slots.find(i) if center_index != -1 and center_index < goals.size() and i > 8: var goal_value = goals[center_index] if item != -1: # Player has a tile in this slot - show it at full brightness match item: 7: slot.texture = item_tex[1] 8: slot.texture = item_tex[2] 9: slot.texture = item_tex[3] 10: slot.texture = item_tex[4] slot.modulate = Color.WHITE else: # Show goal tile dimmed (not collected yet) match goal_value: 7: slot.texture = item_tex[1] 8: slot.texture = item_tex[2] 9: slot.texture = item_tex[3] 10: slot.texture = item_tex[4] _: slot.texture = item_tex[0] # Dim uncollected goals with black overlay slot.modulate = Color(0.3, 0.3, 0.3, 1.0) else: # Non-center slot - just show playerboard item normally match item: 7: slot.texture = item_tex[1] 8: slot.texture = item_tex[2] 9: slot.texture = item_tex[3] 10: slot.texture = item_tex[4] # Non-center slots always full brightness (UNLESS HIDDEN) if not (i in hidden_slots): slot.modulate = Color.WHITE # Check for new special tile placement to trigger effect if i < _previous_playerboard_state.size(): var prev_item = _previous_playerboard_state[i] # If slot was empty or different, and now has a special tile (7-10) if item != prev_item and item >= 7 and item <= 10: _pulse_slot_effect(slot) # Update cache _previous_playerboard_state = local_player_character.playerboard.duplicate() func _pulse_slot_effect(slot: Control): """Visual feedback when a special tile is placed.""" var tween = create_tween() # Reset scale first to be safe slot.scale = Vector2.ONE slot.pivot_offset = slot.size / 2 # Center pivot # Pop effect tween.tween_property(slot, "scale", Vector2(1.4, 1.4), 0.15).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) tween.tween_property(slot, "scale", Vector2(1.0, 1.0), 0.2).set_trans(Tween.TRANS_BOUNCE).set_ease(Tween.EASE_OUT) # Flash effect var original_modulate = slot.modulate slot.modulate = Color(1.5, 1.5, 1.5) # Overbright tween.parallel().tween_property(slot, "modulate", original_modulate, 0.3) func _connect_powerup_manager_deferred(player): """Wait for PowerUpManager to be initialized before connecting.""" # player._ready waits 0.5s before creating managers, so wait longer await player.get_tree().create_timer(0.8).timeout var powerup_manager = player.get_node_or_null("PowerUpManager") if powerup_manager: if not powerup_manager.points_changed.is_connected(_on_powerup_points_changed): powerup_manager.points_changed.connect(_on_powerup_points_changed) # Initialize bar with current values update_powerup_bar(powerup_manager.get_points(), powerup_manager.get_max_points()) # Force initial label update _on_powerup_points_changed(powerup_manager.get_points(), powerup_manager.get_max_points()) else: push_warning("[UIManager] PowerUpManager not found on player after 0.8s wait") # ============================================================================= # Power-Up Bar UI (Battery Style) # ============================================================================= var powerup_bar: PanelContainer var powerup_ready_rect: TextureRect var powerup_segments: Array = [] func setup_powerup_bar_ui(main_node): """Get reference to existing PowerUpBar in scene and cache segment references.""" powerup_bar = main_node.get_node_or_null("PowerBar/PowerUpBar") if not powerup_bar: powerup_bar = main_node.get_node_or_null("PowerUpBar") if not powerup_bar: push_warning("PowerUpBar node not found in scene") return powerup_ready_rect = main_node.get_node_or_null("PowerBar/PowerUpReady") # Get segment references from scene powerup_segments.clear() var hbox = powerup_bar.get_node_or_null("HBox") if hbox: for i in range(3): var segment = hbox.get_node_or_null("Segment" + str(i)) if segment: # Apply initial empty style var style = StyleBoxTexture.new() var tex_path = "res://assets/graphics/gui/gauge/Segment%d_empty.png" % i if ResourceLoader.exists(tex_path): style.texture = load(tex_path) segment.add_theme_stylebox_override("panel", style) powerup_segments.append(segment) func update_powerup_bar(current_points: int, _max_points: int): """Update battery segments based on current power-up points.""" # 3 Segments total. Max Boost is 100. So each segment represents 33.33 points. var points_per_segment = _max_points / 3.0 var bars_filled = int(current_points / points_per_segment) for i in range(powerup_segments.size()): var segment = powerup_segments[i] var style = StyleBoxTexture.new() var tex_path = "" if i < bars_filled: # Filled segment tex_path = "res://assets/graphics/gui/gauge/Segment%d_filled.png" % i else: # Empty segment tex_path = "res://assets/graphics/gui/gauge/Segment%d_empty.png" % i if ResourceLoader.exists(tex_path): style.texture = load(tex_path) segment.add_theme_stylebox_override("panel", style) if powerup_ready_rect: if current_points >= _max_points and _max_points > 0: powerup_ready_rect.visible = true else: powerup_ready_rect.visible = false var _previous_bars: int = 0 func _on_powerup_points_changed(current: int, max_points: int): if current % 10 == 0: print("[UIManager] Points changed: ", current) # Calculate based on max points (100) / 3 segments = 33.33 points per segment var new_bars = int(current / (max_points / 3.0)) # Detect if a new bar was filled if new_bars > _previous_bars and powerup_bar: # Pulse effect on newly filled segment var segment_index = new_bars - 1 if segment_index >= 0 and segment_index < powerup_segments.size(): var segment = powerup_segments[segment_index] _pulse_segment(segment) _previous_bars = new_bars update_powerup_bar(current, max_points) # Update Safety: Check if timer_label is valid if timer_label and local_player_character and local_player_character.powerup_manager: var time_left = local_player_character.powerup_manager.get_time_until_full() if time_left <= 0: timer_label.text = " READY " timer_label.add_theme_color_override("font_color", Color(0.3, 0.9, 0.3)) # Green else: # User request: "Do it on int not float" timer_label.text = str(int(ceil(time_left))) + "s" timer_label.add_theme_color_override("font_color", Color(1.0, 0.85, 0.2)) # Gold func _pulse_segment(segment: Panel): """Create a visual pulse effect on a powerup segment.""" var original_scale = segment.scale var tween = segment.create_tween() tween.set_loops(2) tween.tween_property(segment, "scale", Vector2(1.3, 1.3), 0.1).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) tween.tween_property(segment, "scale", original_scale, 0.15).set_trans(Tween.TRANS_SINE) # ============================================================================= # Leaderboard UI # ============================================================================= var leaderboard_panel: PanelContainer func setup_leaderboard_ui(main_node): """Get reference to existing LeaderboardPanel in scene.""" leaderboard_panel = main_node.get_node_or_null("LeaderboardPanel") if not leaderboard_panel: push_warning("LeaderboardPanel node not found in scene") return func _get_rank_text(rank: int) -> String: match rank: 1: return "1st" 2: return "2nd" 3: return "3rd" 4: return "4th" _: return str(rank) + "th" # ============================================================================= # Timer Labels for Goal Panels # ============================================================================= func setup_timer_labels(main_node): """Apply styling to standalone GoalsTimer in scene.""" var goals_timer = main_node.get_node_or_null("GoalsTimer") if not goals_timer: push_warning("GoalsTimer node not found in scene") return # Apply dark background style var style = StyleBoxFlat.new() style.bg_color = Color(0.1, 0.1, 0.15, 0.9) style.border_color = Color(1.0, 0.85, 0.2, 1.0) # Gold border style.set_border_width_all(2) style.corner_radius_top_left = 8 style.corner_radius_top_right = 8 style.corner_radius_bottom_left = 8 style.corner_radius_bottom_right = 8 goals_timer.add_theme_stylebox_override("panel", style) # Style the timer label var t_label = goals_timer.get_node_or_null("TimerLabel") if t_label: timer_label = t_label # Store reference t_label.add_theme_color_override("font_color", Color(1.0, 0.85, 0.2)) else: print("[UIManager] ERROR: GoalsTimer found but TimerLabel NOT found at node/TimerLabel") var suffix_label = goals_timer.get_node_or_null("SuffixLabel") if suffix_label: suffix_label.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7)) func setup_playerboard_label(main_node): # PlayerBoardLabel now lives under PlayerBoardUI var lbl = main_node.get_node_or_null("PlayerBoardUI/PlayerBoardLabel") if not lbl: lbl = main_node.get_node_or_null("PlayerBoardLabel") if lbl: playerboard_label = lbl playerboard_label.text = "x0" print("[UIManager] Found PlayerBoardLabel") else: print("[UIManager] PlayerBoardLabel not found") func update_goal_count_label(count: int): if playerboard_label: playerboard_label.text = "x%d" % count # Pop effect only for progress if count > 0: var tween = playerboard_label.create_tween() playerboard_label.scale = Vector2(1.5, 1.5) tween.tween_property(playerboard_label, "scale", Vector2(1.0, 1.0), 0.3).set_trans(Tween.TRANS_BOUNCE) # Method to update leaderboard with all players in match func initialize_leaderboard_with_players(players: Array): """Initialize leaderboard showing all players with score 0.""" if not leaderboard_panel: return var vbox = leaderboard_panel.get_node_or_null("MarginContainer/VBox") if not vbox: vbox = leaderboard_panel.get_node_or_null("VBox") if not vbox: return for i in range(4): var entry_root = vbox.get_node_or_null("Entry" + str(i + 1)) if not entry_root: continue var entry = entry_root.get_node_or_null("HBox") if not entry: entry = entry_root if i < players.size(): var player = players[i] var name_label = entry.get_node_or_null("SplitterContainer/SectionA/NameLabel") var score_label = entry.get_node_or_null("SplitterContainer/SectionB/ScoreLabel") var portrait_rect = entry.get_node_or_null("PortraitRect") var ghost_icon = entry.get_node_or_null("SplitterContainer/SectionA/GhostIcon") var mini_powerup_bar = entry.get_node_or_null("SplitterContainer/SectionB/MiniPowerUpBar") if name_label: # Use display_name if available, otherwise fallback to node name var player_display_name = player.display_name if player and player.get("display_name") else "" if player_display_name.is_empty(): player_display_name = str(player.name) if player else "Player " + str(i + 1) name_label.text = player_display_name if score_label: score_label.text = str(player.score) if player and player.get("score") else "0" if portrait_rect: var character_name = "Pip" # Default fallback var peer_id = player.name.to_int() if player else 0 var lobby_manager = get_node_or_null("/root/LobbyManager") if lobby_manager: var lobby_players = lobby_manager.get_players() for p in lobby_players: if p.get("id") == peer_id: character_name = p.get("character", "Pip") break var avatar_url = "res://assets/graphics/character_potrait/sc_%s.png" % character_name.to_lower() if ResourceLoader.exists(avatar_url): portrait_rect.texture = load(avatar_url) if ghost_icon: # Hidden by default. The live update loop will populate the correct texture. ghost_icon.modulate = Color(1, 1, 1, 0) if mini_powerup_bar: # Initialize to empty segments for j in range(3): var seg = mini_powerup_bar.get_node_or_null("Segment" + str(j)) if seg: var style = StyleBoxFlat.new() style.bg_color = Color(0.2, 0.2, 0.2, 0.8) style.border_color = Color(0.3, 0.7, 0.3, 1.0) style.set_border_width_all(2) seg.add_theme_stylebox_override("panel", style) entry.visible = true else: entry.visible = false func update_live_leaderboard(players: Array): """Update the leaderboard during gameplay (scores, ghosts, powerups).""" if not leaderboard_panel: return var vbox = leaderboard_panel.get_node_or_null("MarginContainer/VBox") if not vbox: vbox = leaderboard_panel.get_node_or_null("VBox") if not vbox: return var sorted_players = players.duplicate() sorted_players.sort_custom(func(a, b): var score_a = a.score if "score" in a else 0 var score_b = b.score if "score" in b else 0 return score_a > score_b ) var my_id = -1 if leaderboard_panel.is_inside_tree() and leaderboard_panel.get_tree().get_multiplayer(): my_id = leaderboard_panel.get_tree().get_multiplayer().get_unique_id() var my_index = -1 for i in range(sorted_players.size()): if sorted_players[i] and sorted_players[i].name == str(my_id): my_index = i break var items_to_display = [] for i in range(min(3, sorted_players.size())): items_to_display.append({"player": sorted_players[i], "rank": i + 1}) if sorted_players.size() >= 4: if my_index > 3: items_to_display.append({"player": sorted_players[my_index], "rank": my_index + 1}) else: items_to_display.append({"player": sorted_players[3], "rank": 4}) for i in range(4): var entry_root = vbox.get_node_or_null("Entry" + str(i + 1)) if not entry_root or i >= items_to_display.size(): if entry_root: entry_root.visible = false continue entry_root.visible = true var entry = entry_root.get_node_or_null("HBox") if not entry: entry = entry_root var item = items_to_display[i] var player = item.player var rank = item.rank var rank_label = entry.get_node_or_null("RankLabel") if rank_label: match rank: 1: rank_label.text = "1st" 2: rank_label.text = "2nd" 3: rank_label.text = "3rd" _: rank_label.text = str(rank) + "th" if player and player.name == str(my_id): entry_root.modulate = Color(0.3, 0.7, 1.0) # Blue highlight for local player else: entry_root.modulate = Color.WHITE var score_label = entry.get_node_or_null("SplitterContainer/SectionB/ScoreLabel") var ghost_icon = entry.get_node_or_null("SplitterContainer/SectionA/GhostIcon") var mini_powerup_bar = entry.get_node_or_null("SplitterContainer/SectionB/MiniPowerUpBar") var portrait_rect = entry.get_node_or_null("PortraitRect") var name_label = entry.get_node_or_null("SplitterContainer/SectionA/NameLabel") if name_label: var default_name = player.name if player else "Unknown" name_label.text = player.get("display_name") if (player and player.get("display_name")) else default_name if portrait_rect: var character_name = "Pip" # Default fallback if player and player.get("selected_character"): var sc = player.selected_character match sc: "Bob": character_name = "Pip" "Masbro": character_name = "Dabro" "Gatot": character_name = "Gatot" "Oldpop": character_name = "Copper" _: character_name = sc var avatar_url = "res://assets/graphics/character_potrait/sc_%s.png" % character_name.to_lower() if ResourceLoader.exists(avatar_url): portrait_rect.texture = load(avatar_url) if score_label: score_label.text = str(player.score) if player and player.get("score") else "0" if ghost_icon: var active_skill_id = -1 var is_blinking = false if player.get("special_tiles_manager"): var stm = player.special_tiles_manager # Check if any skill is CURRENTLY active (User requesting blinking state) if stm.get("active_buffs") and stm.active_buffs.has(0): # FASTER_SPEED active_skill_id = 0 is_blinking = true elif stm.get("active_freeze_zones") and stm.active_freeze_zones.size() > 0: active_skill_id = 1 # AREA_FREEZE is_blinking = true elif stm.get("blocked_tiles") != null and stm.get("blocked_tiles").size() > 0: active_skill_id = 2 # BLOCK_FLOOR is_blinking = true elif typeof(stm.get("invisible_timer")) in [TYPE_FLOAT, TYPE_INT] and float(stm.get("invisible_timer")) > 0.0: active_skill_id = 3 # INVISIBLE_MODE is_blinking = true else: var inv = stm.get("inventory") if inv: for effect_idx in inv: if inv[effect_idx]: active_skill_id = effect_idx break if active_skill_id != -1: var tex_path = "res://assets/textures/player_board_and_blue_print/tile_null.tres" match int(active_skill_id): 0: tex_path = "res://assets/graphics/touch_control/speed.png" 1: tex_path = "res://assets/graphics/touch_control/freeze_area.png" 2: tex_path = "res://assets/graphics/touch_control/wall.png" 3: tex_path = "res://assets/graphics/touch_control/ghost.png" if ResourceLoader.exists(tex_path): ghost_icon.texture = load(tex_path) if is_blinking: var alpha = 1.0 if (Time.get_ticks_msec() % 500) > 250 else 0.3 ghost_icon.modulate = Color(1, 1, 1, alpha) else: ghost_icon.modulate = Color(1, 1, 1, 1) else: ghost_icon.modulate = Color(1, 1, 1, 0) if mini_powerup_bar and player.get("powerup_manager"): var p_mgr = player.powerup_manager if p_mgr: var max_pts = p_mgr.get_max_points() var current_pts = p_mgr.get_points() var points_per_segment = max_pts / 3.0 var bars_filled = int(current_pts / points_per_segment) for j in range(3): var seg = mini_powerup_bar.get_node_or_null("Segment" + str(j)) if seg: var style = seg.get_theme_stylebox("panel").duplicate() if j < bars_filled: style.bg_color = Color(0.3, 0.9, 0.3, 1.0) else: style.bg_color = Color(0.2, 0.2, 0.2, 0.8) seg.add_theme_stylebox_override("panel", style)