Files
whale-town-front-v2/scenes/ui/PlayerHud.gd
2026-07-21 23:07:31 +08:00

496 lines
20 KiB
GDScript

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 _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)
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))
row.add_child(_create_shortcut_button("task", "任务", _on_task_pressed))
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)
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()
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:
_emit_status_message("任务入口稍后接入")
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")