extends Control # ============================================================================ # PlayerHud.gd - 右上角玩家头像与快捷入口 # ============================================================================ # 展示当前玩家头像入口,以及地图/任务/背包/好友/设置入口。 # 好友入口只负责打开或收起右下角好友列表。 # ============================================================================ const HUD_MARGIN: Vector2 = Vector2(16, 16) const SHORTCUT_BAR_SIZE: Vector2 = Vector2(406, 86) const PLAYER_PROFILE_BUTTON_SIZE: Vector2 = Vector2(210, 78) const PLAYER_AVATAR_SIZE: Vector2 = Vector2(58, 58) const HUD_SEPARATION: int = 18 const HUD_TOTAL_WIDTH: float = SHORTCUT_BAR_SIZE.x + PLAYER_PROFILE_BUTTON_SIZE.x + HUD_SEPARATION const TEXT_COLOR: Color = Color(0.188, 0.294, 0.424) const MUTED_COLOR: Color = Color(0.560, 0.639, 0.733) const ACCENT_COLOR: Color = Color(0.258824, 0.627451, 0.913725) const WALLET_REFRESH_INTERVAL: float = 15.0 const HUD_SHORTCUT_ICON_SCRIPT: Script = preload("res://scenes/ui/HudShortcutIcon.gd") var _usernameLabel: Label var _walletLabel: Label var _avatarLabel: Label var _avatarPanel: PanelContainer var _walletRefreshTimer: Timer var _taskShortcutButton: Button var _taskRewardBadge: Label var _currentUsername: String = "玩家" var _walletBalance: int = 0 var _walletLoaded: bool = false var _walletLoading: bool = false func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE _build_ui() _build_wallet_refresh_timer() _subscribe_to_events() _load_current_user() _refresh_wallet() func _exit_tree() -> void: var eventSystem := _get_event_system() if eventSystem != null: eventSystem.call("disconnect_event", EventNames.CHAT_LOGIN_SUCCESS, _on_chat_login_success, self) eventSystem.call("disconnect_event", EventNames.AUTH_LOGIN_SUCCESS, _on_auth_login_success, self) eventSystem.call("disconnect_event", EventNames.AUTH_REGISTER_SUCCESS, _on_auth_register_success, self) eventSystem.call("disconnect_event", EventNames.AUTH_LOGOUT, _on_auth_logout, self) eventSystem.call("disconnect_event", EventNames.APPEARANCE_AVATAR_CHANGED, _on_appearance_avatar_changed, self) eventSystem.call("disconnect_event", EventNames.MALL_PURCHASE_SUCCEEDED, _on_wallet_mutation_event, self) eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_CHAT_TIME_PURCHASED, _on_wallet_mutation_event, self) eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_EMPLOYMENT_RESIGNED, _on_wallet_mutation_event, self) var playerStateManager := get_node_or_null("/root/PlayerStateManager") if playerStateManager != null and playerStateManager.has_signal("wallet_changed"): var walletCallback := Callable(self, "_on_player_wallet_changed") if playerStateManager.is_connected("wallet_changed", walletCallback): playerStateManager.disconnect("wallet_changed", walletCallback) var authManager := get_node_or_null("/root/AuthManager") if authManager != null and authManager.has_signal("auth_state_changed"): var callback := Callable(self, "_on_auth_state_changed") if authManager.is_connected("auth_state_changed", callback): authManager.disconnect("auth_state_changed", callback) var taskManager := get_node_or_null("/root/TaskManager") if taskManager != null and taskManager.has_signal("board_changed"): var taskCallback := Callable(self, "_on_task_board_changed") if taskManager.is_connected("board_changed", taskCallback): taskManager.disconnect("board_changed", taskCallback) func _build_ui() -> void: var rootRow := HBoxContainer.new() rootRow.name = "HudRoot" rootRow.set_anchors_preset(Control.PRESET_TOP_RIGHT) rootRow.offset_left = -HUD_TOTAL_WIDTH - HUD_MARGIN.x rootRow.offset_top = HUD_MARGIN.y rootRow.offset_right = -HUD_MARGIN.x rootRow.add_theme_constant_override("separation", HUD_SEPARATION) add_child(rootRow) rootRow.add_child(_build_shortcut_bar()) rootRow.add_child(_build_player_avatar_button()) func _build_shortcut_bar() -> PanelContainer: var panel := PanelContainer.new() panel.custom_minimum_size = SHORTCUT_BAR_SIZE panel.mouse_filter = Control.MOUSE_FILTER_STOP panel.add_theme_stylebox_override("panel", _create_panel_style(32)) var margin := MarginContainer.new() margin.add_theme_constant_override("margin_left", 18) margin.add_theme_constant_override("margin_top", 12) margin.add_theme_constant_override("margin_right", 18) margin.add_theme_constant_override("margin_bottom", 12) panel.add_child(margin) var row := HBoxContainer.new() row.alignment = BoxContainer.ALIGNMENT_CENTER row.add_theme_constant_override("separation", 8) margin.add_child(row) row.add_child(_create_shortcut_button("map", "地图", _on_map_pressed)) _taskShortcutButton = _create_shortcut_button("task", "任务", _on_task_pressed) _taskShortcutButton.tooltip_text = "打开任务书" _taskRewardBadge = Label.new() _taskRewardBadge.mouse_filter = Control.MOUSE_FILTER_IGNORE _taskRewardBadge.visible = false _taskRewardBadge.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _taskRewardBadge.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _taskRewardBadge.add_theme_color_override("font_color", Color.WHITE) _taskRewardBadge.add_theme_font_size_override("font_size", 12) _taskRewardBadge.add_theme_stylebox_override("normal", _create_pill_style(Color(0.92, 0.30, 0.24), 12)) _taskRewardBadge.set_anchors_preset(Control.PRESET_TOP_RIGHT) _taskRewardBadge.offset_left = -20 _taskRewardBadge.offset_top = 1 _taskRewardBadge.offset_right = 4 _taskRewardBadge.offset_bottom = 25 _taskRewardBadge.z_index = 1 _taskShortcutButton.add_child(_taskRewardBadge) row.add_child(_taskShortcutButton) row.add_child(_create_shortcut_button("backpack", "背包", _on_backpack_pressed)) row.add_child(_create_shortcut_button("friends", "好友", _on_friends_pressed)) row.add_child(_create_shortcut_button("activity", "通知", _on_notifications_pressed)) row.add_child(_create_shortcut_button("settings", "设置", _on_settings_pressed)) return panel func _build_player_avatar_button() -> Button: var button := Button.new() button.custom_minimum_size = PLAYER_PROFILE_BUTTON_SIZE button.tooltip_text = "角色设置" button.focus_mode = Control.FOCUS_NONE button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND button.text = "" button.add_theme_stylebox_override("normal", _create_avatar_button_style(Color(1, 1, 1, 0.94), 39)) button.add_theme_stylebox_override("hover", _create_avatar_button_style(Color(0.918, 0.961, 0.992, 0.98), 39)) button.add_theme_stylebox_override("pressed", _create_avatar_button_style(Color(0.858, 0.925, 0.980, 1.0), 39)) button.add_theme_stylebox_override("focus", _create_empty_style()) button.pressed.connect(_on_avatar_pressed) var margin := MarginContainer.new() margin.mouse_filter = Control.MOUSE_FILTER_IGNORE margin.set_anchors_preset(Control.PRESET_FULL_RECT) margin.add_theme_constant_override("margin_left", 10) margin.add_theme_constant_override("margin_top", 10) margin.add_theme_constant_override("margin_right", 14) margin.add_theme_constant_override("margin_bottom", 10) button.add_child(margin) var row := HBoxContainer.new() row.mouse_filter = Control.MOUSE_FILTER_IGNORE row.alignment = BoxContainer.ALIGNMENT_CENTER row.add_theme_constant_override("separation", 10) margin.add_child(row) var avatar := PanelContainer.new() _avatarPanel = avatar avatar.custom_minimum_size = PLAYER_AVATAR_SIZE avatar.mouse_filter = Control.MOUSE_FILTER_IGNORE avatar.clip_contents = true row.add_child(avatar) _avatarLabel = Label.new() _avatarLabel.mouse_filter = Control.MOUSE_FILTER_IGNORE _avatarLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _avatarLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _avatarLabel.add_theme_color_override("font_color", Color.WHITE) _avatarLabel.add_theme_font_size_override("font_size", 22) avatar.add_child(_avatarLabel) _apply_current_avatar() _usernameLabel = Label.new() _usernameLabel.mouse_filter = Control.MOUSE_FILTER_IGNORE _usernameLabel.text = _currentUsername _usernameLabel.custom_minimum_size = Vector2(96, 0) _usernameLabel.size_flags_horizontal = Control.SIZE_EXPAND_FILL _usernameLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _usernameLabel.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS _usernameLabel.add_theme_color_override("font_color", TEXT_COLOR) _usernameLabel.add_theme_font_size_override("font_size", 18) var info := VBoxContainer.new() info.mouse_filter = Control.MOUSE_FILTER_IGNORE info.size_flags_horizontal = Control.SIZE_EXPAND_FILL info.alignment = BoxContainer.ALIGNMENT_CENTER info.add_theme_constant_override("separation", 2) row.add_child(info) info.add_child(_usernameLabel) _walletLabel = Label.new() _walletLabel.mouse_filter = Control.MOUSE_FILTER_IGNORE _walletLabel.text = "鲸币 --" _walletLabel.custom_minimum_size = Vector2(112, 0) _walletLabel.size_flags_horizontal = Control.SIZE_EXPAND_FILL _walletLabel.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS _walletLabel.add_theme_color_override("font_color", ACCENT_COLOR) _walletLabel.add_theme_font_size_override("font_size", 14) info.add_child(_walletLabel) return button func _create_shortcut_button(iconName: String, text: String, callback: Callable) -> Button: var button := Button.new() button.custom_minimum_size = Vector2(56, 60) button.focus_mode = Control.FOCUS_NONE button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND button.text = "" button.add_theme_stylebox_override("normal", _create_empty_style()) button.add_theme_stylebox_override("hover", _create_pill_style(Color(0.918, 0.961, 0.992, 0.86), 18)) button.add_theme_stylebox_override("pressed", _create_pill_style(Color(0.858, 0.925, 0.980, 0.98), 18)) button.add_theme_stylebox_override("focus", _create_empty_style()) button.pressed.connect(callback) var content := VBoxContainer.new() content.mouse_filter = Control.MOUSE_FILTER_IGNORE content.set_anchors_preset(Control.PRESET_FULL_RECT) content.alignment = BoxContainer.ALIGNMENT_CENTER content.add_theme_constant_override("separation", 1) button.add_child(content) var icon := HUD_SHORTCUT_ICON_SCRIPT.new() as Control icon.set("iconName", iconName) icon.custom_minimum_size = Vector2(31, 31) content.add_child(icon) var label := Label.new() label.mouse_filter = Control.MOUSE_FILTER_IGNORE label.text = text label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.add_theme_color_override("font_color", MUTED_COLOR) label.add_theme_font_size_override("font_size", 14) content.add_child(label) button.mouse_entered.connect(func() -> void: if is_instance_valid(label): label.add_theme_color_override("font_color", ACCENT_COLOR) ) button.mouse_exited.connect(func() -> void: if is_instance_valid(label): label.add_theme_color_override("font_color", MUTED_COLOR) ) return button func _subscribe_to_events() -> void: var eventSystem := _get_event_system() if eventSystem == null: push_warning("PlayerHud: EventSystem autoload is not available.") return eventSystem.call("connect_event", EventNames.CHAT_LOGIN_SUCCESS, _on_chat_login_success, self) eventSystem.call("connect_event", EventNames.AUTH_LOGIN_SUCCESS, _on_auth_login_success, self) eventSystem.call("connect_event", EventNames.AUTH_REGISTER_SUCCESS, _on_auth_register_success, self) eventSystem.call("connect_event", EventNames.AUTH_LOGOUT, _on_auth_logout, self) eventSystem.call("connect_event", EventNames.APPEARANCE_AVATAR_CHANGED, _on_appearance_avatar_changed, self) eventSystem.call("connect_event", EventNames.MALL_PURCHASE_SUCCEEDED, _on_wallet_mutation_event, self) eventSystem.call("connect_event", EventNames.CAFE_COMPANION_CHAT_TIME_PURCHASED, _on_wallet_mutation_event, self) eventSystem.call("connect_event", EventNames.CAFE_COMPANION_EMPLOYMENT_RESIGNED, _on_wallet_mutation_event, self) var playerStateManager := get_node_or_null("/root/PlayerStateManager") if playerStateManager != null and playerStateManager.has_signal("wallet_changed"): var walletCallback := Callable(self, "_on_player_wallet_changed") if not playerStateManager.is_connected("wallet_changed", walletCallback): playerStateManager.connect("wallet_changed", walletCallback) var authManager := get_node_or_null("/root/AuthManager") if authManager != null and authManager.has_signal("auth_state_changed"): var callback := Callable(self, "_on_auth_state_changed") if not authManager.is_connected("auth_state_changed", callback): authManager.connect("auth_state_changed", callback) var taskManager := get_node_or_null("/root/TaskManager") if taskManager != null and taskManager.has_signal("board_changed"): var taskCallback := Callable(self, "_on_task_board_changed") if not taskManager.is_connected("board_changed", taskCallback): taskManager.connect("board_changed", taskCallback) if taskManager.has_method("get_board"): var boardVariant: Variant = taskManager.call("get_board") if boardVariant is Dictionary: _on_task_board_changed(boardVariant as Dictionary) func _load_current_user() -> void: var authManager := get_node_or_null("/root/AuthManager") if authManager != null and authManager.has_method("get_current_username"): var username := str(authManager.call("get_current_username")).strip_edges() if not username.is_empty(): _set_username(username) func _on_chat_login_success(data: Dictionary) -> void: var username := str(data.get("username", "")).strip_edges() if not username.is_empty(): _set_username(username) func _on_auth_login_success(data: Dictionary) -> void: _apply_auth_user_payload(data) func _on_auth_register_success(data: Dictionary) -> void: _apply_auth_user_payload(data) func _on_auth_state_changed(isAuthenticated: bool, user: Dictionary) -> void: if not isAuthenticated: _set_username("玩家") _reset_wallet() return _apply_auth_user_payload({"user": user}) _refresh_wallet() func _apply_auth_user_payload(data: Dictionary) -> void: var userVariant: Variant = data.get("user", {}) if userVariant is Dictionary: var username := str((userVariant as Dictionary).get("username", "")).strip_edges() if not username.is_empty(): _set_username(username) func _on_auth_logout(_data: Variant = null) -> void: _set_username("玩家") _reset_wallet() _on_task_board_changed({}) func _on_task_board_changed(board: Dictionary) -> void: if not is_instance_valid(_taskShortcutButton): return var claimableCount: int = _count_claimable_task_rewards(board) if claimableCount > 0: _taskShortcutButton.tooltip_text = "任务书:有 %d 项奖励可领取" % claimableCount _taskShortcutButton.modulate = Color(1.0, 0.855, 0.470, 1.0) if is_instance_valid(_taskRewardBadge): _taskRewardBadge.text = str(mini(claimableCount, 9)) if claimableCount < 10 else "9+" _taskRewardBadge.visible = true return _taskShortcutButton.tooltip_text = "打开任务书" _taskShortcutButton.modulate = Color.WHITE if is_instance_valid(_taskRewardBadge): _taskRewardBadge.visible = false func _count_claimable_task_rewards(board: Dictionary) -> int: var claimableCount: int = 0 claimableCount += _count_claimable_tasks(board.get("newbie_tasks", [])) claimableCount += _count_claimable_tasks(board.get("weekly_tasks", [])) var weeklyBonusVariant: Variant = board.get("weekly_bonus", {}) if weeklyBonusVariant is Dictionary: var weeklyBonus: Dictionary = weeklyBonusVariant as Dictionary if bool(weeklyBonus.get("claimable", false)): claimableCount += 1 return claimableCount func _count_claimable_tasks(tasksVariant: Variant) -> int: if not (tasksVariant is Array): return 0 var claimableCount: int = 0 for taskVariant: Variant in tasksVariant as Array: if taskVariant is Dictionary: var task: Dictionary = taskVariant as Dictionary if bool(task.get("claimable", false)): claimableCount += 1 return claimableCount func _on_appearance_avatar_changed(_data: Dictionary) -> void: _apply_current_avatar() func _set_username(username: String) -> void: _currentUsername = username.strip_edges() if _currentUsername.is_empty(): _currentUsername = "玩家" if is_instance_valid(_usernameLabel): _usernameLabel.text = _currentUsername _apply_current_avatar() func _on_wallet_mutation_event(data: Dictionary) -> void: var balanceVariant: Variant = data.get("balance", null) if balanceVariant != null and (balanceVariant is int or balanceVariant is float): _apply_wallet_balance(int(balanceVariant)) return _refresh_wallet() func _build_wallet_refresh_timer() -> void: _walletRefreshTimer = Timer.new() _walletRefreshTimer.name = "HudWalletRefreshTimer" _walletRefreshTimer.wait_time = WALLET_REFRESH_INTERVAL _walletRefreshTimer.autostart = true _walletRefreshTimer.timeout.connect(_on_wallet_refresh_timer_timeout) add_child(_walletRefreshTimer) func _on_wallet_refresh_timer_timeout() -> void: _refresh_wallet() func _refresh_wallet() -> void: if not _is_wallet_available(): _reset_wallet() return var playerStateManager := get_node_or_null("/root/PlayerStateManager") if playerStateManager == null: _reset_wallet() return if playerStateManager.has_method("get_wallet"): var walletVariant: Variant = playerStateManager.call("get_wallet") if walletVariant is Dictionary and not (walletVariant as Dictionary).is_empty(): _on_player_wallet_changed(walletVariant as Dictionary) return if _walletLoading: return _walletLoading = true _update_wallet_label() if playerStateManager.has_method("refresh_snapshot"): playerStateManager.call("refresh_snapshot") func _on_player_wallet_changed(wallet: Dictionary) -> void: if wallet.is_empty(): _reset_wallet() return _apply_wallet_balance(int(wallet.get("balance", 0))) func _apply_wallet_balance(balance: int) -> void: _walletBalance = max(0, balance) _walletLoaded = true _walletLoading = false _update_wallet_label() func _reset_wallet() -> void: _walletBalance = 0 _walletLoaded = false _walletLoading = false _update_wallet_label() func _update_wallet_label(overrideText: String = "") -> void: if not is_instance_valid(_walletLabel): return if not overrideText.is_empty(): _walletLabel.text = "鲸币 %s" % overrideText _walletLabel.tooltip_text = "鲸币余额" return if _walletLoading: _walletLabel.text = "鲸币 读取中" elif _walletLoaded: _walletLabel.text = "鲸币 %s" % _format_number(_walletBalance) else: _walletLabel.text = "鲸币 --" _walletLabel.tooltip_text = "鲸币余额" func _format_number(value: int) -> String: var text := str(value) var result := "" var count := 0 for i in range(text.length() - 1, -1, -1): if count > 0 and count % 3 == 0: result = "," + result result = text[i] + result count += 1 return result func _is_wallet_available() -> bool: var authManager := get_node_or_null("/root/AuthManager") return authManager != null and authManager.has_method("is_authenticated") and bool(authManager.call("is_authenticated")) and authManager.has_method("get_access_token") func _on_map_pressed() -> void: var eventSystem := _get_event_system() if eventSystem != null: eventSystem.call("emit_event", EventNames.HUD_MAP_TOGGLE, {}) func _on_notifications_pressed() -> void: var socialManager := get_node_or_null("/root/SocialManager") if socialManager != null and socialManager.has_method("toggle_notifications"): socialManager.call("toggle_notifications") func _on_task_pressed() -> void: var eventSystem := _get_event_system() if eventSystem != null: eventSystem.call("emit_event", EventNames.HUD_TASK_BOOK_REQUESTED, {}) func _on_backpack_pressed() -> void: var eventSystem := _get_event_system() if eventSystem != null: eventSystem.call("emit_event", EventNames.HUD_BACKPACK_TOGGLE, {}) func _on_friends_pressed() -> void: var eventSystem := _get_event_system() if eventSystem != null: eventSystem.call("emit_event", EventNames.HUD_FRIEND_LIST_TOGGLE, {}) func _on_settings_pressed() -> void: var eventSystem := _get_event_system() if eventSystem != null: eventSystem.call("emit_event", EventNames.HUD_SETTINGS_REQUESTED, {}) func _on_avatar_pressed() -> void: var eventSystem := _get_event_system() if eventSystem != null: eventSystem.call("emit_event", EventNames.HUD_SETTINGS_REQUESTED, {}) func _emit_status_message(message: String) -> void: var eventSystem := _get_event_system() if eventSystem != null: eventSystem.call("emit_event", EventNames.CHAT_MESSAGE_RECEIVED, { "from_user": "系统", "content": message, "timestamp": Time.get_unix_time_from_system(), "is_self": false, "scope": "system", "tab": "world" }) func _create_panel_style(radius: int) -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = Color(1, 1, 1, 0.925) style.corner_radius_top_left = radius style.corner_radius_top_right = radius style.corner_radius_bottom_left = radius style.corner_radius_bottom_right = radius style.shadow_color = Color(0.12549, 0.282353, 0.407843, 0.15) style.shadow_size = 18 style.shadow_offset = Vector2(0, 7) return style func _create_avatar_style() -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = Color(0.361, 0.690, 0.914) style.corner_radius_top_left = 32 style.corner_radius_top_right = 32 style.corner_radius_bottom_left = 32 style.corner_radius_bottom_right = 32 return style func _apply_current_avatar() -> void: if not is_instance_valid(_avatarPanel) or not is_instance_valid(_avatarLabel): return var appearanceManager := get_node_or_null("/root/AppearanceManager") if appearanceManager != null and appearanceManager.has_method("apply_avatar_to_panel"): appearanceManager.call("apply_avatar_to_panel", _avatarPanel, _avatarLabel, "", _currentUsername) _avatarPanel.add_theme_stylebox_override("panel", _create_round_avatar_style(_avatarPanel)) return _avatarPanel.add_theme_stylebox_override("panel", _create_avatar_style()) _avatarLabel.visible = true _avatarLabel.text = _currentUsername.substr(0, 1).to_upper() func _create_avatar_button_style(color: Color, radius: int) -> StyleBoxFlat: var style := _create_pill_style(color, radius) style.shadow_color = Color(0.12549, 0.282353, 0.407843, 0.14) style.shadow_size = 16 style.shadow_offset = Vector2(0, 6) return style func _create_round_avatar_style(panel: PanelContainer) -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = Color(0.361, 0.690, 0.914) var radius := int(round(minf(panel.custom_minimum_size.x, panel.custom_minimum_size.y) * 0.5)) style.corner_radius_top_left = radius style.corner_radius_top_right = radius style.corner_radius_bottom_left = radius style.corner_radius_bottom_right = radius style.border_width_left = 2 style.border_width_top = 2 style.border_width_right = 2 style.border_width_bottom = 2 style.border_color = Color(1, 1, 1, 0.78) return style func _create_pill_style(color: Color, radius: int) -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = color style.corner_radius_top_left = radius style.corner_radius_top_right = radius style.corner_radius_bottom_left = radius style.corner_radius_bottom_right = radius style.content_margin_left = 8 style.content_margin_right = 8 style.content_margin_top = 5 style.content_margin_bottom = 5 return style func _create_empty_style() -> StyleBoxEmpty: return StyleBoxEmpty.new() func _get_event_system() -> Node: return get_node_or_null("/root/EventSystem")