1384 lines
52 KiB
GDScript
1384 lines
52 KiB
GDScript
extends CanvasLayer
|
||
class_name DatawhaleHonorRankingPanel
|
||
|
||
const NetworkConfig = preload("res://_Core/utils/NetworkConfig.gd")
|
||
const TEX_PANEL_FRAME = preload("res://assets/ui/datawhale_honor/honor_panel_wood_frame.png")
|
||
const TEX_WINDOW_SHELL = preload("res://assets/ui/datawhale_honor/honor_window_shell.png")
|
||
const TEX_PARCHMENT_PANEL = preload("res://assets/ui/datawhale_honor/honor_parchment_panel.png")
|
||
const TEX_HEADER_PLANK = preload("res://assets/ui/datawhale_honor/honor_header_plank.png")
|
||
const TEX_TITLE_DATAWHALE = preload("res://assets/ui/datawhale_honor/honor_title_datawhale.png")
|
||
const TEX_TAB_NORMAL = preload("res://assets/ui/datawhale_honor/honor_tab_normal.png")
|
||
const TEX_TAB_ACTIVE = preload("res://assets/ui/datawhale_honor/honor_tab_active.png")
|
||
const TEX_CLAIM_BUTTON = preload("res://assets/ui/datawhale_honor/honor_claim_button.png")
|
||
const TEX_CLOSE_BUTTON = preload("res://assets/ui/datawhale_honor/honor_close_button.png")
|
||
const TEX_WHALE_CREST = preload("res://assets/ui/datawhale_honor/honor_whale_crest.png")
|
||
const TEX_MEDAL_GOLD = preload("res://assets/ui/datawhale_honor/honor_medal_gold.png")
|
||
const TEX_MEDAL_SILVER = preload("res://assets/ui/datawhale_honor/honor_medal_silver.png")
|
||
const TEX_MEDAL_BRONZE = preload("res://assets/ui/datawhale_honor/honor_medal_bronze.png")
|
||
const TEX_ICON_SCORE = preload("res://assets/ui/datawhale_honor/honor_icon_score_globe.png")
|
||
const TEX_ICON_CONTRIBUTION = preload("res://assets/ui/datawhale_honor/honor_icon_contribution_whale.png")
|
||
const TEX_ICON_REPO = preload("res://assets/ui/datawhale_honor/honor_icon_repo_chest.png")
|
||
const TEX_ICON_STAR = preload("res://assets/ui/datawhale_honor/honor_icon_star.png")
|
||
const TEX_ICON_REWARD = preload("res://assets/ui/datawhale_honor/honor_icon_reward_gem.png")
|
||
const TEX_ICON_GIFT = preload("res://assets/ui/datawhale_honor/honor_icon_gift.png")
|
||
const TEX_RANK_BADGE_LOWER = preload("res://assets/ui/datawhale_honor/honor_rank_badge_lower.png")
|
||
const TEX_TAB_LABEL_WEEKLY = preload("res://assets/ui/datawhale_honor/honor_tab_label_weekly_commits.png")
|
||
const TEX_TAB_LABEL_NIGHT = preload("res://assets/ui/datawhale_honor/honor_tab_label_night_owl.png")
|
||
const TEX_TAB_LABEL_POPULARITY = preload("res://assets/ui/datawhale_honor/honor_tab_label_popularity.png")
|
||
const TEX_TAB_LABEL_PRODUCTIVE = preload("res://assets/ui/datawhale_honor/honor_tab_label_productive.png")
|
||
const TEX_TAB_LABEL_SOCIAL = preload("res://assets/ui/datawhale_honor/honor_tab_label_social.png")
|
||
const TEX_TAB_LABEL_RISING = preload("res://assets/ui/datawhale_honor/honor_tab_label_rising.png")
|
||
const TEX_TAB_LABEL_COMPREHENSIVE = preload("res://assets/ui/datawhale_honor/honor_tab_label_comprehensive.png")
|
||
const FONT_UI = preload("res://assets/fonts/msyh.ttc")
|
||
const TEX_AVATAR_FALLBACK_1 = preload("res://assets/ui/auth/generated/auth_character_preview_front.png")
|
||
const TEX_AVATAR_FALLBACK_2 = preload("res://assets/ui/auth/generated/auth_character_preview_left.png")
|
||
const TEX_AVATAR_FALLBACK_3 = preload("res://assets/ui/auth/generated/auth_character_preview_right.png")
|
||
|
||
const API_PATH: String = "/rankings/datawhale-honor"
|
||
const PANEL_SIZE: Vector2 = Vector2(1500, 850)
|
||
const REQUEST_LIMIT: int = 9
|
||
const LOWER_RANK_LIMIT: int = 6
|
||
const LOWER_RANK_SLOT_HEIGHT: float = 45.0
|
||
const LOWER_RANK_AVATAR_SIZE: float = 30.0
|
||
const LOWER_RANK_AVATAR_SLOT_SIZE: float = 34.0
|
||
const LOWER_RANK_SLOT_MARGIN_LEFT: int = 18
|
||
const LOWER_RANK_SLOT_MARGIN_TOP: int = 12
|
||
const LOWER_RANK_SLOT_MARGIN_RIGHT: int = 22
|
||
const RANK_LIST_MIN_HEIGHT: float = 282.0
|
||
const PODIUM_PANEL_HEIGHT: float = 154.0
|
||
const PODIUM_CARD_TOP: float = -2.0
|
||
const PODIUM_CARD_HEIGHT: float = 143.0
|
||
const PODIUM_SLOT_BOUNDS: Dictionary = {
|
||
2: Vector2(14, 428),
|
||
1: Vector2(441, 892),
|
||
3: Vector2(899, 1308),
|
||
}
|
||
const TAB_LABEL_SIZE: Vector2 = Vector2(140, 36)
|
||
const TAB_BUTTON_TOP: float = 0.0
|
||
const TAB_BUTTON_BOTTOM: float = 60.0
|
||
const TAB_SLOT_BOUNDS: Dictionary = {
|
||
"weekly_commits": Vector2(88, 262),
|
||
"night_owl": Vector2(266, 475),
|
||
"popularity": Vector2(484, 658),
|
||
"productive": Vector2(668, 868),
|
||
"social": Vector2(878, 1058),
|
||
"rising": Vector2(1068, 1224),
|
||
"comprehensive": Vector2(1234, 1410),
|
||
}
|
||
const TEXT_COLOR: Color = Color(0.235, 0.125, 0.052)
|
||
const MUTED_COLOR: Color = Color(0.455, 0.312, 0.170)
|
||
const ACCENT_COLOR: Color = Color(0.075, 0.435, 0.740)
|
||
const DEEP_BLUE: Color = Color(0.030, 0.285, 0.560)
|
||
const GOLD: Color = Color(1.0, 0.710, 0.125)
|
||
const SILVER: Color = Color(0.695, 0.802, 0.940)
|
||
const BRONZE: Color = Color(0.885, 0.520, 0.285)
|
||
const CREAM: Color = Color(0.992, 0.900, 0.705)
|
||
const PARCHMENT_TINT: Color = Color(0.972, 0.842, 0.560)
|
||
const WOOD_DARK: Color = Color(0.320, 0.160, 0.070)
|
||
|
||
var _rootControl: Control
|
||
var _panel: Control
|
||
var _tabsRow: Control
|
||
var _podiumPanel: PanelContainer
|
||
var _rankScroll: ScrollContainer
|
||
var _podiumRow: Control
|
||
var _rankList: VBoxContainer
|
||
var _footerLabel: Label
|
||
var _statusLabel: Label
|
||
var _request: HTTPRequest
|
||
var _avatarRequests: Array[HTTPRequest] = []
|
||
var _chatUi: Control
|
||
var _chatUiPrevMouseFilter: Control.MouseFilter = Control.MOUSE_FILTER_STOP
|
||
var _chatUiMouseDisabled: bool = false
|
||
var _categories: Array = []
|
||
var _activeCategory: String = "weekly_commits"
|
||
var _isLoading: bool = false
|
||
var _debugShowRankSlots: bool = false
|
||
|
||
func _ready() -> void:
|
||
add_to_group("whaletown_escape_dismissible")
|
||
get_tree().paused = true
|
||
_disableChatUiMouseInput()
|
||
_buildUi()
|
||
get_tree().root.size_changed.connect(_positionPanel)
|
||
_fetchRanking(_activeCategory)
|
||
|
||
func _exit_tree() -> void:
|
||
if get_tree().root.size_changed.is_connected(_positionPanel):
|
||
get_tree().root.size_changed.disconnect(_positionPanel)
|
||
_clearAvatarRequests()
|
||
_restoreChatUiMouseInput()
|
||
|
||
func _input(event: InputEvent) -> void:
|
||
if get_viewport().is_input_handled():
|
||
return
|
||
if event is InputEventKey:
|
||
var keyEvent := event as InputEventKey
|
||
if keyEvent.pressed and not keyEvent.echo and keyEvent.keycode == KEY_ESCAPE:
|
||
_onClosePressed()
|
||
get_viewport().set_input_as_handled()
|
||
|
||
func is_escape_dismissible() -> bool:
|
||
return is_inside_tree()
|
||
|
||
func get_escape_priority() -> int:
|
||
return 1000
|
||
|
||
func request_escape_close() -> void:
|
||
_onClosePressed()
|
||
|
||
func _buildUi() -> void:
|
||
_rootControl = Control.new()
|
||
_rootControl.name = "RankingPanelRoot"
|
||
_rootControl.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
add_child(_rootControl)
|
||
|
||
var dimmer := ColorRect.new()
|
||
dimmer.name = "Dimmer"
|
||
dimmer.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
dimmer.color = Color(0.026, 0.055, 0.105, 0.58)
|
||
dimmer.mouse_filter = Control.MOUSE_FILTER_STOP
|
||
_rootControl.add_child(dimmer)
|
||
|
||
_panel = Control.new()
|
||
_panel.name = "HonorRankingPanel"
|
||
_panel.custom_minimum_size = PANEL_SIZE
|
||
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||
_rootControl.add_child(_panel)
|
||
_positionPanel()
|
||
|
||
var frame := PanelContainer.new()
|
||
frame.name = "MainFrame"
|
||
frame.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
frame.add_theme_stylebox_override("panel", _createPanelStyle(Color(0, 0, 0, 0), 0, Color(0, 0, 0, 0), 0, false))
|
||
frame.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
_panel.add_child(frame)
|
||
|
||
var shell := TextureRect.new()
|
||
shell.name = "WindowShell"
|
||
shell.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
shell.texture = TEX_WINDOW_SHELL
|
||
shell.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
shell.stretch_mode = TextureRect.STRETCH_SCALE
|
||
shell.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
_panel.add_child(shell)
|
||
|
||
_panel.add_child(_buildTitleOverlay())
|
||
|
||
var margin := MarginContainer.new()
|
||
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
margin.add_theme_constant_override("margin_left", 86)
|
||
margin.add_theme_constant_override("margin_top", 92)
|
||
margin.add_theme_constant_override("margin_right", 86)
|
||
margin.add_theme_constant_override("margin_bottom", 76)
|
||
_panel.add_child(margin)
|
||
|
||
var content := VBoxContainer.new()
|
||
content.add_theme_constant_override("separation", 4)
|
||
content.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
content.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
margin.add_child(content)
|
||
|
||
content.add_child(_buildHeader())
|
||
content.add_child(_buildBoardBody())
|
||
|
||
_panel.add_child(_buildTabsOverlay())
|
||
_panel.add_child(_buildRuleOverlay())
|
||
_panel.add_child(_buildCloseButton())
|
||
|
||
_request = HTTPRequest.new()
|
||
_request.timeout = 18.0
|
||
_request.request_completed.connect(_onRequestCompleted)
|
||
add_child(_request)
|
||
_setLoadingState()
|
||
|
||
func _buildBoardBody() -> Control:
|
||
var body := PanelContainer.new()
|
||
body.name = "BoardBody"
|
||
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
body.add_theme_stylebox_override("panel", _createPanelStyle(Color(0, 0, 0, 0), 0, Color(0, 0, 0, 0), 0, false))
|
||
|
||
var margin := MarginContainer.new()
|
||
margin.add_theme_constant_override("margin_left", 0)
|
||
margin.add_theme_constant_override("margin_top", 0)
|
||
margin.add_theme_constant_override("margin_right", 0)
|
||
margin.add_theme_constant_override("margin_bottom", 0)
|
||
body.add_child(margin)
|
||
|
||
var content := VBoxContainer.new()
|
||
content.add_theme_constant_override("separation", 4)
|
||
content.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
margin.add_child(content)
|
||
|
||
var tabsSpacer := Control.new()
|
||
tabsSpacer.name = "TabsSpacer"
|
||
tabsSpacer.custom_minimum_size = Vector2(0, 58)
|
||
content.add_child(tabsSpacer)
|
||
|
||
_podiumPanel = _buildPodiumPanel()
|
||
content.add_child(_podiumPanel)
|
||
|
||
_rankScroll = ScrollContainer.new()
|
||
_rankScroll.name = "RankScroll"
|
||
_rankScroll.custom_minimum_size = Vector2(0, RANK_LIST_MIN_HEIGHT)
|
||
_rankScroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
_rankScroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
_rankScroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||
_rankScroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||
content.add_child(_rankScroll)
|
||
|
||
var rankSlotMargin := MarginContainer.new()
|
||
rankSlotMargin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
rankSlotMargin.add_theme_constant_override("margin_left", LOWER_RANK_SLOT_MARGIN_LEFT)
|
||
rankSlotMargin.add_theme_constant_override("margin_top", LOWER_RANK_SLOT_MARGIN_TOP)
|
||
rankSlotMargin.add_theme_constant_override("margin_right", LOWER_RANK_SLOT_MARGIN_RIGHT)
|
||
_rankScroll.add_child(rankSlotMargin)
|
||
|
||
_rankList = VBoxContainer.new()
|
||
_rankList.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
_rankList.add_theme_constant_override("separation", 7)
|
||
rankSlotMargin.add_child(_rankList)
|
||
|
||
content.add_child(_buildFooter())
|
||
return body
|
||
|
||
func _positionPanel() -> void:
|
||
if _rootControl == null:
|
||
return
|
||
var viewportSize := _rootControl.get_viewport_rect().size
|
||
var scaleFactor: float = minf(1.0, minf((viewportSize.x - 64.0) / PANEL_SIZE.x, (viewportSize.y - 64.0) / PANEL_SIZE.y))
|
||
_panel.size = PANEL_SIZE
|
||
_panel.scale = Vector2(scaleFactor, scaleFactor)
|
||
_panel.position = (viewportSize - PANEL_SIZE * scaleFactor) * 0.5
|
||
|
||
func _fetchRanking(categoryId: String) -> void:
|
||
if _request == null:
|
||
return
|
||
if _isLoading:
|
||
_request.cancel_request()
|
||
_activeCategory = categoryId
|
||
_isLoading = true
|
||
_setLoadingState()
|
||
var endpoint := "%s%s?category=%s&limit=%d&refresh=true" % [
|
||
NetworkConfig.get_api_base_url(),
|
||
API_PATH,
|
||
_urlEncode(_activeCategory),
|
||
REQUEST_LIMIT,
|
||
]
|
||
var err := _request.request(endpoint, PackedStringArray(), HTTPClient.METHOD_GET, "")
|
||
if err != OK:
|
||
_isLoading = false
|
||
_setErrorState("荣誉榜请求失败:%s" % error_string(err))
|
||
|
||
func _onRequestCompleted(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||
_isLoading = false
|
||
if result != HTTPRequest.RESULT_SUCCESS or responseCode < 200 or responseCode >= 300:
|
||
_setErrorState("荣誉榜加载失败,请稍后再试")
|
||
return
|
||
|
||
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
|
||
if not parsed is Dictionary:
|
||
_setErrorState("荣誉榜数据解析失败")
|
||
return
|
||
|
||
var payload := parsed as Dictionary
|
||
if not bool(payload.get("success", true)):
|
||
_setErrorState(str(payload.get("message", "荣誉榜加载失败")))
|
||
return
|
||
|
||
var dataVariant: Variant = payload.get("data", {})
|
||
if not dataVariant is Dictionary:
|
||
_setErrorState("荣誉榜数据为空")
|
||
return
|
||
|
||
_renderRanking(dataVariant as Dictionary)
|
||
|
||
func _renderRanking(data: Dictionary) -> void:
|
||
_activeCategory = str(data.get("activeCategory", _activeCategory))
|
||
var categoriesVariant: Variant = data.get("categories", [])
|
||
_categories = categoriesVariant as Array if categoriesVariant is Array else []
|
||
_renderTabs()
|
||
|
||
var topRankers: Array = data.get("topRankers", []) as Array if data.get("topRankers", []) is Array else []
|
||
var rankers: Array = data.get("rankers", []) as Array if data.get("rankers", []) is Array else []
|
||
_renderPodium(topRankers)
|
||
_renderRankList(rankers)
|
||
|
||
if is_instance_valid(_statusLabel):
|
||
_statusLabel.text = _buildStatusText(data)
|
||
|
||
func _renderTabs() -> void:
|
||
_clearChildren(_tabsRow)
|
||
for category in _categories:
|
||
if not category is Dictionary:
|
||
continue
|
||
var categoryData := category as Dictionary
|
||
var id := str(categoryData.get("id", ""))
|
||
var tabIndex := _tabsRow.get_child_count()
|
||
var selected := id == _activeCategory
|
||
var tabBounds := _tabSlotBounds(id, tabIndex)
|
||
var button := Button.new()
|
||
button.text = str(categoryData.get("label", id))
|
||
button.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||
button.offset_left = tabBounds.x
|
||
button.offset_top = TAB_BUTTON_TOP
|
||
button.offset_right = tabBounds.y
|
||
button.offset_bottom = TAB_BUTTON_BOTTOM
|
||
button.focus_mode = Control.FOCUS_NONE
|
||
button.alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
button.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
button.disabled = selected or _isLoading
|
||
_applyTransparentButtonText(button)
|
||
button.add_theme_stylebox_override("normal", StyleBoxEmpty.new())
|
||
button.add_theme_stylebox_override("hover", StyleBoxEmpty.new())
|
||
button.add_theme_stylebox_override("pressed", _createTabSelectedStyle())
|
||
button.add_theme_stylebox_override("disabled", _createTabSelectedStyle() if selected else StyleBoxEmpty.new())
|
||
button.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
|
||
button.add_child(_buildTabContent(id))
|
||
button.pressed.connect(func() -> void:
|
||
_fetchRanking(id)
|
||
)
|
||
_tabsRow.add_child(button)
|
||
|
||
func _renderPodium(rows: Array) -> void:
|
||
_clearChildren(_podiumRow)
|
||
_clearAvatarRequests()
|
||
if rows.is_empty():
|
||
_podiumRow.add_child(_buildEmptyLabel("正在等待荣誉榜数据..."))
|
||
return
|
||
|
||
var orderedRows := rows.duplicate()
|
||
orderedRows.sort_custom(func(a: Variant, b: Variant) -> bool:
|
||
var rankA := int((a as Dictionary).get("rank", 999)) if a is Dictionary else 999
|
||
var rankB := int((b as Dictionary).get("rank", 999)) if b is Dictionary else 999
|
||
var visualOrder := {2: 0, 1: 1, 3: 2}
|
||
return int(visualOrder.get(rankA, rankA + 10)) < int(visualOrder.get(rankB, rankB + 10))
|
||
)
|
||
for row in orderedRows:
|
||
if row is Dictionary:
|
||
var rank := int((row as Dictionary).get("rank", 0))
|
||
if rank < 1 or rank > 3:
|
||
continue
|
||
var bounds := _podiumSlotBounds(rank)
|
||
var card := _buildPodiumCard(row as Dictionary, Vector2(bounds.y - bounds.x, PODIUM_CARD_HEIGHT), 1 if rank == 1 else 0)
|
||
card.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||
card.offset_left = bounds.x
|
||
card.offset_top = PODIUM_CARD_TOP
|
||
card.offset_right = bounds.y
|
||
card.offset_bottom = PODIUM_CARD_TOP + PODIUM_CARD_HEIGHT
|
||
_podiumRow.add_child(card)
|
||
|
||
func _renderRankList(rows: Array) -> void:
|
||
_clearChildren(_rankList)
|
||
if rows.is_empty():
|
||
_rankList.add_child(_buildEmptyLabel("暂无更多上榜成员"))
|
||
return
|
||
var shownCount := 0
|
||
for row in rows:
|
||
if shownCount >= LOWER_RANK_LIMIT:
|
||
break
|
||
if row is Dictionary:
|
||
_rankList.add_child(_buildRankRow(row as Dictionary))
|
||
shownCount += 1
|
||
|
||
func _setLoadingState() -> void:
|
||
_clearChildren(_rankList)
|
||
_renderTabs()
|
||
_clearChildren(_podiumRow)
|
||
_podiumRow.add_child(_buildEmptyLabel("正在同步 Datawhale 荣誉榜..."))
|
||
_rankList.add_child(_buildEmptyLabel("数据来自 WhaleTown 后端同步接口"))
|
||
if is_instance_valid(_footerLabel):
|
||
_footerLabel.text = "我的排名: 正在同步..."
|
||
if is_instance_valid(_statusLabel):
|
||
_statusLabel.text = "正在从后端获取真实排行榜数据..."
|
||
|
||
func _setErrorState(message: String) -> void:
|
||
_clearChildren(_rankList)
|
||
_clearChildren(_podiumRow)
|
||
_podiumRow.add_child(_buildEmptyLabel("荣誉榜暂时不可用"))
|
||
var retry := Button.new()
|
||
retry.text = "重试同步"
|
||
retry.custom_minimum_size = Vector2(180, 48)
|
||
retry.focus_mode = Control.FOCUS_NONE
|
||
retry.add_theme_font_size_override("font_size", 20)
|
||
retry.add_theme_color_override("font_color", Color.WHITE)
|
||
retry.add_theme_stylebox_override("normal", _createPanelStyle(ACCENT_COLOR, 22, Color(0.500, 0.820, 1.0, 1.0), 2, false))
|
||
retry.add_theme_stylebox_override("hover", _createPanelStyle(Color(0.180, 0.650, 1.0, 1.0), 22, Color(0.650, 0.900, 1.0, 1.0), 2, false))
|
||
retry.pressed.connect(func() -> void:
|
||
_fetchRanking(_activeCategory)
|
||
)
|
||
_rankList.add_child(_buildEmptyLabel(message))
|
||
_rankList.add_child(retry)
|
||
if is_instance_valid(_footerLabel):
|
||
_footerLabel.text = "我的排名: 暂无"
|
||
if is_instance_valid(_statusLabel):
|
||
_statusLabel.text = message
|
||
|
||
func _buildHeader() -> Control:
|
||
var header := Control.new()
|
||
header.custom_minimum_size = Vector2(0, 66)
|
||
return header
|
||
|
||
func _buildTitleOverlay() -> Control:
|
||
var overlay := Control.new()
|
||
overlay.name = "TitleOverlay"
|
||
overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
overlay.set_anchors_preset(Control.PRESET_TOP_WIDE)
|
||
overlay.offset_left = 392
|
||
overlay.offset_top = 62
|
||
overlay.offset_right = -356
|
||
overlay.offset_bottom = 110
|
||
|
||
var title := TextureRect.new()
|
||
title.texture = TEX_TITLE_DATAWHALE
|
||
title.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
title.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
title.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
title.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
overlay.add_child(title)
|
||
|
||
var titleTextProbe := Label.new()
|
||
titleTextProbe.name = "TitleTextProbe"
|
||
titleTextProbe.text = "Datawhale Honor Ranking"
|
||
titleTextProbe.visible = false
|
||
overlay.add_child(titleTextProbe)
|
||
|
||
return overlay
|
||
|
||
func _buildTabsOverlay() -> Control:
|
||
_tabsRow = Control.new()
|
||
_tabsRow.name = "TabsOverlay"
|
||
_tabsRow.mouse_filter = Control.MOUSE_FILTER_PASS
|
||
_tabsRow.set_anchors_preset(Control.PRESET_TOP_WIDE)
|
||
_tabsRow.offset_left = 0
|
||
_tabsRow.offset_top = 138
|
||
_tabsRow.offset_right = 0
|
||
_tabsRow.offset_bottom = 198
|
||
_tabsRow.clip_contents = false
|
||
return _tabsRow
|
||
|
||
func _buildTabContent(categoryId: String) -> Control:
|
||
var center := Control.new()
|
||
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
center.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
|
||
var labelImage := TextureRect.new()
|
||
labelImage.texture = _tabLabelTexture(categoryId)
|
||
labelImage.set_anchors_preset(Control.PRESET_CENTER)
|
||
var visualOffset := _tabLabelOffset(categoryId)
|
||
labelImage.offset_left = -TAB_LABEL_SIZE.x * 0.5 + visualOffset.x
|
||
labelImage.offset_top = -TAB_LABEL_SIZE.y * 0.5 + visualOffset.y
|
||
labelImage.offset_right = TAB_LABEL_SIZE.x * 0.5 + visualOffset.x
|
||
labelImage.offset_bottom = TAB_LABEL_SIZE.y * 0.5 + visualOffset.y
|
||
labelImage.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
labelImage.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
labelImage.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
center.add_child(labelImage)
|
||
return center
|
||
|
||
func _buildWhaleBadge() -> Control:
|
||
var badge := TextureRect.new()
|
||
badge.custom_minimum_size = Vector2(82, 72)
|
||
badge.texture = TEX_WHALE_CREST
|
||
badge.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
badge.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
return badge
|
||
|
||
func _buildSeasonPill() -> Control:
|
||
var pill := PanelContainer.new()
|
||
pill.custom_minimum_size = Vector2(150, 42)
|
||
pill.add_theme_stylebox_override("panel", _createPanelStyle(Color(0.105, 0.385, 0.610, 0.96), 16, Color(0.840, 0.610, 0.250, 0.95), 2, false))
|
||
|
||
var label := Label.new()
|
||
label.text = "◷ 周更新"
|
||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
label.add_theme_font_size_override("font_size", 17)
|
||
label.add_theme_color_override("font_color", Color.WHITE)
|
||
pill.add_child(label)
|
||
return pill
|
||
|
||
func _buildCloseButton() -> Button:
|
||
var button := Button.new()
|
||
button.name = "CloseButton"
|
||
button.text = "×"
|
||
button.tooltip_text = "关闭荣誉榜"
|
||
button.custom_minimum_size = Vector2(62, 62)
|
||
button.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||
button.offset_left = -174
|
||
button.offset_top = 46
|
||
button.offset_right = -118
|
||
button.offset_bottom = 102
|
||
button.focus_mode = Control.FOCUS_NONE
|
||
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||
button.add_theme_font_size_override("font_size", 42)
|
||
button.add_theme_color_override("font_color", Color(1, 1, 1, 0))
|
||
button.add_theme_color_override("font_hover_color", Color(1, 1, 1, 0))
|
||
button.add_theme_color_override("font_pressed_color", Color(1, 1, 1, 0))
|
||
button.add_theme_stylebox_override("normal", StyleBoxEmpty.new())
|
||
button.add_theme_stylebox_override("hover", StyleBoxEmpty.new())
|
||
button.add_theme_stylebox_override("pressed", StyleBoxEmpty.new())
|
||
button.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
|
||
button.pressed.connect(_onClosePressed)
|
||
return button
|
||
|
||
func _buildPodiumPanel() -> PanelContainer:
|
||
var panel := PanelContainer.new()
|
||
panel.custom_minimum_size = Vector2(0, PODIUM_PANEL_HEIGHT)
|
||
panel.add_theme_stylebox_override("panel", _createPanelStyle(Color(0, 0, 0, 0), 0, Color(0, 0, 0, 0), 0, false))
|
||
|
||
var margin := MarginContainer.new()
|
||
margin.add_theme_constant_override("margin_left", 0)
|
||
margin.add_theme_constant_override("margin_top", 0)
|
||
margin.add_theme_constant_override("margin_right", 0)
|
||
margin.add_theme_constant_override("margin_bottom", 0)
|
||
panel.add_child(margin)
|
||
|
||
_podiumRow = Control.new()
|
||
_podiumRow.custom_minimum_size = Vector2(0, PODIUM_PANEL_HEIGHT)
|
||
_podiumRow.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
_podiumRow.clip_contents = false
|
||
margin.add_child(_podiumRow)
|
||
return panel
|
||
|
||
func _buildPodiumCard(data: Dictionary, minSize: Vector2, lift: int) -> Control:
|
||
var rank := int(data.get("rank", 0))
|
||
var card := Control.new()
|
||
card.custom_minimum_size = minSize
|
||
card.clip_contents = false
|
||
|
||
var width := minSize.x
|
||
var contentInset := Vector2(8, 6)
|
||
var mainContentShift := 12.0
|
||
var rightInfoShift := 36.0
|
||
var rightInfoLeft := 156 + contentInset.x + rightInfoShift + mainContentShift
|
||
var avatarLeft := 64 + contentInset.x + mainContentShift
|
||
var avatarTop := 17 + contentInset.y
|
||
var medalSize := Vector2(56, 68) if rank == 1 else Vector2(50, 62)
|
||
var medal := _createIcon(_rankMedalTexture(rank), medalSize)
|
||
medal.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||
medal.offset_left = 8 + contentInset.x
|
||
medal.offset_top = (18 if rank == 1 else 21) + contentInset.y
|
||
medal.offset_right = medal.offset_left + medalSize.x
|
||
medal.offset_bottom = medal.offset_top + medalSize.y
|
||
card.add_child(medal)
|
||
|
||
var avatarSize := 64.0
|
||
var avatar := _createPodiumAvatar(data, avatarSize, 25 if rank == 1 else 23)
|
||
avatar.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||
avatar.offset_left = avatarLeft
|
||
avatar.offset_top = avatarTop
|
||
avatar.offset_right = avatar.offset_left + avatarSize
|
||
avatar.offset_bottom = avatar.offset_top + avatarSize
|
||
card.add_child(avatar)
|
||
|
||
var info := VBoxContainer.new()
|
||
info.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||
info.offset_left = avatarLeft - 18
|
||
info.offset_top = avatarTop + avatarSize + 4
|
||
info.offset_right = avatarLeft + avatarSize + 18
|
||
info.offset_bottom = info.offset_top + 36
|
||
info.add_theme_constant_override("separation", 1)
|
||
card.add_child(info)
|
||
|
||
var tagsRow := HBoxContainer.new()
|
||
tagsRow.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||
tagsRow.offset_left = rightInfoLeft
|
||
tagsRow.offset_top = 17 + contentInset.y
|
||
tagsRow.offset_right = width - 68 + contentInset.x + rightInfoShift
|
||
tagsRow.offset_bottom = 37 + contentInset.y
|
||
tagsRow.add_theme_constant_override("separation", 5)
|
||
card.add_child(tagsRow)
|
||
for tagText in _podiumTags(data):
|
||
tagsRow.add_child(_createPodiumTag(tagText))
|
||
|
||
var nameLabel := Label.new()
|
||
nameLabel.text = str(data.get("name", "未知用户"))
|
||
nameLabel.custom_minimum_size = Vector2(0, 18)
|
||
nameLabel.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
nameLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
nameLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
_applyTextStyle(nameLabel, 13, TEXT_COLOR)
|
||
info.add_child(nameLabel)
|
||
|
||
var locationLabel := Label.new()
|
||
locationLabel.text = _podiumMetaLine(data)
|
||
locationLabel.custom_minimum_size = Vector2(0, 15)
|
||
locationLabel.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
locationLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
locationLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
_applyTextStyle(locationLabel, 10, MUTED_COLOR)
|
||
info.add_child(locationLabel)
|
||
|
||
var scoreBlock := VBoxContainer.new()
|
||
scoreBlock.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||
scoreBlock.offset_left = rightInfoLeft
|
||
scoreBlock.offset_top = 39 + contentInset.y
|
||
scoreBlock.offset_right = width - 110 + contentInset.x + rightInfoShift
|
||
scoreBlock.offset_bottom = 83 + contentInset.y
|
||
scoreBlock.add_theme_constant_override("separation", 0)
|
||
card.add_child(scoreBlock)
|
||
|
||
var scoreCaption := Label.new()
|
||
scoreCaption.text = _scoreCaption(_activeCategory)
|
||
scoreCaption.custom_minimum_size = Vector2(0, 18)
|
||
scoreCaption.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||
scoreCaption.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
_applyTextStyle(scoreCaption, 11, MUTED_COLOR)
|
||
scoreBlock.add_child(scoreCaption)
|
||
|
||
var scoreLabel := Label.new()
|
||
scoreLabel.text = _formatNumber(int(data.get("score", 0)))
|
||
scoreLabel.custom_minimum_size = Vector2(0, 26)
|
||
scoreLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||
scoreLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
_applyTextStyle(scoreLabel, 19, DEEP_BLUE)
|
||
scoreBlock.add_child(scoreLabel)
|
||
|
||
var scoreSubLabel := Label.new()
|
||
scoreSubLabel.text = str(data.get("scoreLabel", "")).strip_edges()
|
||
scoreSubLabel.custom_minimum_size = Vector2(0, 18)
|
||
scoreSubLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||
scoreSubLabel.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
_applyTextStyle(scoreSubLabel, 11, MUTED_COLOR)
|
||
scoreBlock.add_child(scoreSubLabel)
|
||
|
||
var statRow := HBoxContainer.new()
|
||
statRow.set_anchors_preset(Control.PRESET_TOP_WIDE)
|
||
statRow.offset_left = rightInfoLeft
|
||
statRow.offset_top = 96 + contentInset.y
|
||
statRow.offset_right = -128 + contentInset.x + rightInfoShift
|
||
statRow.offset_bottom = 120 + contentInset.y
|
||
statRow.alignment = BoxContainer.ALIGNMENT_BEGIN
|
||
statRow.add_theme_constant_override("separation", 4)
|
||
card.add_child(statRow)
|
||
statRow.add_child(_createPodiumMiniStat(TEX_ICON_CONTRIBUTION, int(data.get("contrib", 0))))
|
||
statRow.add_child(_createPodiumMiniStat(TEX_ICON_REPO, int(data.get("answers", 0))))
|
||
statRow.add_child(_createPodiumMiniStat(TEX_ICON_STAR, int(data.get("likes", 0))))
|
||
|
||
return card
|
||
|
||
func _buildRankRow(data: Dictionary) -> Control:
|
||
var rank := int(data.get("rank", 0))
|
||
var rowPanel := PanelContainer.new()
|
||
rowPanel.name = "LowerRankSlot%d" % rank
|
||
rowPanel.custom_minimum_size = Vector2(0, LOWER_RANK_SLOT_HEIGHT)
|
||
rowPanel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
if _debugShowRankSlots:
|
||
rowPanel.add_theme_stylebox_override("panel", _createPanelStyle(Color(0.050, 0.440, 1.0, 0.10), 8, Color(0.050, 0.440, 1.0, 0.95), 2, false))
|
||
else:
|
||
rowPanel.add_theme_stylebox_override("panel", StyleBoxEmpty.new())
|
||
|
||
var margin := MarginContainer.new()
|
||
margin.add_theme_constant_override("margin_left", 34)
|
||
margin.add_theme_constant_override("margin_right", 34)
|
||
rowPanel.add_child(margin)
|
||
|
||
var row := HBoxContainer.new()
|
||
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||
row.add_theme_constant_override("separation", 10)
|
||
margin.add_child(row)
|
||
|
||
row.add_child(_createRankBadge(rank))
|
||
row.add_child(_createRankAvatarSlot(data))
|
||
row.add_child(_createFixedLabel(str(data.get("name", "未知用户")), 232, 18, TEXT_COLOR, HORIZONTAL_ALIGNMENT_LEFT))
|
||
row.add_child(_createScoreMetricText(_scoreCaption(_activeCategory), _formatNumber(int(data.get("score", 0))), 172))
|
||
row.add_child(_createMetricPill(TEX_ICON_CONTRIBUTION, _contribLabel(_activeCategory), int(data.get("contrib", 0))))
|
||
row.add_child(_createMetricPill(TEX_ICON_REPO, _repoLabel(_activeCategory), int(data.get("answers", 0))))
|
||
row.add_child(_createMetricPill(TEX_ICON_STAR, _starLabel(_activeCategory), int(data.get("likes", 0))))
|
||
return rowPanel
|
||
|
||
func _buildFooter() -> Control:
|
||
var footerPanel := PanelContainer.new()
|
||
footerPanel.custom_minimum_size = Vector2(0, 62)
|
||
footerPanel.add_theme_stylebox_override("panel", _createPanelStyle(Color(0, 0, 0, 0), 0, Color(0, 0, 0, 0), 0, false))
|
||
return footerPanel
|
||
|
||
func _buildRuleOverlay() -> Control:
|
||
var overlay := Control.new()
|
||
overlay.name = "RuleOverlay"
|
||
overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
overlay.set_anchors_preset(Control.PRESET_TOP_WIDE)
|
||
overlay.offset_left = 96
|
||
overlay.offset_top = 750
|
||
overlay.offset_right = -96
|
||
overlay.offset_bottom = 814
|
||
|
||
_statusLabel = Label.new()
|
||
_statusLabel.text = _rankingRuleText(_activeCategory)
|
||
_statusLabel.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
_statusLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
_statusLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
_statusLabel.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
_statusLabel.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
_statusLabel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
_applyTextStyle(_statusLabel, 16, MUTED_COLOR)
|
||
overlay.add_child(_statusLabel)
|
||
return overlay
|
||
|
||
func _buildEmptyLabel(text: String) -> Label:
|
||
var label := Label.new()
|
||
label.text = text
|
||
label.custom_minimum_size = Vector2(0, 52)
|
||
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
label.add_theme_font_size_override("font_size", 20)
|
||
label.add_theme_color_override("font_color", MUTED_COLOR)
|
||
return label
|
||
|
||
func _buildStatusText(data: Dictionary) -> String:
|
||
return _rankingRuleText(str(data.get("activeCategory", _activeCategory)))
|
||
|
||
func _createRankBadge(rank: int) -> Control:
|
||
var badge := Control.new()
|
||
badge.custom_minimum_size = Vector2(44, 42)
|
||
|
||
var texture := TextureRect.new()
|
||
texture.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
texture.texture = TEX_RANK_BADGE_LOWER
|
||
texture.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
texture.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
texture.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
badge.add_child(texture)
|
||
|
||
var label := Label.new()
|
||
label.text = "%d" % rank
|
||
label.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
label.offset_top = 7
|
||
label.offset_bottom = 0
|
||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
_applyTextStyle(label, 17, DEEP_BLUE)
|
||
label.add_theme_color_override("font_shadow_color", Color(1.0, 0.940, 0.700, 0.96))
|
||
label.add_theme_constant_override("shadow_offset_y", 1)
|
||
label.add_theme_constant_override("shadow_outline_size", 2)
|
||
badge.add_child(label)
|
||
return badge
|
||
|
||
func _createPodiumAvatar(data: Dictionary, size: float, fontSize: int) -> PanelContainer:
|
||
var panel := PanelContainer.new()
|
||
panel.custom_minimum_size = Vector2(size, size)
|
||
panel.clip_contents = true
|
||
panel.add_theme_stylebox_override("panel", _createAvatarFrameStyle(int(round(size * 0.5))))
|
||
|
||
var textureRect := TextureRect.new()
|
||
textureRect.name = "AvatarTexture"
|
||
textureRect.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
textureRect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
textureRect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
|
||
textureRect.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
textureRect.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||
panel.add_child(textureRect)
|
||
|
||
var label := Label.new()
|
||
label.name = "AvatarFallbackLabel"
|
||
label.text = str(data.get("avatarText", "鲸"))
|
||
label.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
_applyTextStyle(label, fontSize, TEXT_COLOR)
|
||
panel.add_child(label)
|
||
|
||
var texture := _avatarPrimaryTextureFromData(data)
|
||
if texture != null:
|
||
textureRect.texture = texture
|
||
label.visible = false
|
||
else:
|
||
var avatarUrl := _avatarUrlFromData(data)
|
||
if not avatarUrl.is_empty():
|
||
_requestAvatarTexture(avatarUrl, textureRect, label, null)
|
||
else:
|
||
label.visible = true
|
||
return panel
|
||
|
||
func _createRankAvatarSlot(data: Dictionary) -> Control:
|
||
var slot := CenterContainer.new()
|
||
slot.custom_minimum_size = Vector2(LOWER_RANK_AVATAR_SLOT_SIZE, LOWER_RANK_AVATAR_SLOT_SIZE)
|
||
slot.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||
|
||
var avatar := _createPodiumAvatar(data, LOWER_RANK_AVATAR_SIZE, 14)
|
||
avatar.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||
avatar.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||
slot.add_child(avatar)
|
||
return slot
|
||
|
||
func _createAvatarLabel(text: String, size: float, fontSize: int) -> Label:
|
||
var label := Label.new()
|
||
label.text = text
|
||
label.custom_minimum_size = Vector2(size, size)
|
||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
_applyTextStyle(label, fontSize, TEXT_COLOR)
|
||
return label
|
||
|
||
func _createMetricPill(iconTexture: Texture2D, labelText: String, value: int) -> Control:
|
||
var pill := PanelContainer.new()
|
||
pill.custom_minimum_size = Vector2(132, 34)
|
||
pill.add_theme_stylebox_override("panel", StyleBoxEmpty.new())
|
||
|
||
var row := HBoxContainer.new()
|
||
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||
row.add_theme_constant_override("separation", 6)
|
||
pill.add_child(row)
|
||
row.add_child(_createIcon(iconTexture, Vector2(20, 20)))
|
||
row.add_child(_createFixedLabel("%s %s" % [labelText, _compactNumber(value)], 90, 16, Color(0.270, 0.150, 0.055), HORIZONTAL_ALIGNMENT_CENTER))
|
||
return pill
|
||
|
||
func _createPodiumTag(text: String) -> Control:
|
||
var tag := PanelContainer.new()
|
||
var width := clampf(float(text.length()) * 7.0 + 16.0, 42.0, 76.0)
|
||
tag.custom_minimum_size = Vector2(width, 19)
|
||
tag.add_theme_stylebox_override("panel", _createPanelStyle(Color(0.965, 0.900, 0.700, 0.92), 7, Color(0.760, 0.510, 0.200, 0.52), 1, false))
|
||
|
||
var label := Label.new()
|
||
label.text = text
|
||
label.clip_text = true
|
||
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
_applyTextStyle(label, 9, Color(0.365, 0.205, 0.060))
|
||
tag.add_child(label)
|
||
return tag
|
||
|
||
func _createPodiumMiniStat(iconTexture: Texture2D, value: int) -> Control:
|
||
var row := HBoxContainer.new()
|
||
row.custom_minimum_size = Vector2(54, 20)
|
||
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||
row.add_theme_constant_override("separation", 3)
|
||
row.add_child(_createIcon(iconTexture, Vector2(15, 15)))
|
||
var label := Label.new()
|
||
label.text = _compactNumber(value)
|
||
label.custom_minimum_size = Vector2(34, 18)
|
||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
_applyTextStyle(label, 11, Color(0.335, 0.230, 0.120))
|
||
row.add_child(label)
|
||
return row
|
||
|
||
func _createFixedLabel(text: String, width: float, fontSize: int, color: Color, align: HorizontalAlignment) -> Label:
|
||
var label := Label.new()
|
||
label.text = text
|
||
label.custom_minimum_size = Vector2(width, 34)
|
||
label.horizontal_alignment = align
|
||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
_applyTextStyle(label, fontSize, color)
|
||
return label
|
||
|
||
func _applyTextStyle(control: Control, fontSize: int, color: Color) -> void:
|
||
control.add_theme_font_override("font", FONT_UI)
|
||
control.add_theme_font_size_override("font_size", fontSize)
|
||
control.add_theme_color_override("font_color", color)
|
||
|
||
func _applyTransparentButtonText(button: Button) -> void:
|
||
button.add_theme_color_override("font_color", Color(1, 1, 1, 0))
|
||
button.add_theme_color_override("font_disabled_color", Color(1, 1, 1, 0))
|
||
button.add_theme_color_override("font_hover_color", Color(1, 1, 1, 0))
|
||
button.add_theme_color_override("font_pressed_color", Color(1, 1, 1, 0))
|
||
|
||
func _tabSlotBounds(categoryId: String, tabIndex: int) -> Vector2:
|
||
if TAB_SLOT_BOUNDS.has(categoryId):
|
||
return TAB_SLOT_BOUNDS[categoryId] as Vector2
|
||
var slotWidth: float = (_tabsRow.size.x if is_instance_valid(_tabsRow) and _tabsRow.size.x > 0.0 else 1334.0) / 7.0
|
||
return Vector2(tabIndex * slotWidth + 8.0, (tabIndex + 1) * slotWidth - 8.0)
|
||
|
||
func _tabLabelOffset(categoryId: String) -> Vector2:
|
||
match categoryId:
|
||
"weekly_commits":
|
||
return Vector2.ZERO
|
||
"night_owl":
|
||
return Vector2.ZERO
|
||
"popularity":
|
||
return Vector2.ZERO
|
||
"productive":
|
||
return Vector2.ZERO
|
||
"social":
|
||
return Vector2.ZERO
|
||
"rising":
|
||
return Vector2.ZERO
|
||
"comprehensive":
|
||
return Vector2.ZERO
|
||
_:
|
||
return Vector2.ZERO
|
||
|
||
func _tabLabelTexture(categoryId: String) -> Texture2D:
|
||
match categoryId:
|
||
"weekly_commits":
|
||
return TEX_TAB_LABEL_WEEKLY
|
||
"night_owl":
|
||
return TEX_TAB_LABEL_NIGHT
|
||
"popularity":
|
||
return TEX_TAB_LABEL_POPULARITY
|
||
"productive":
|
||
return TEX_TAB_LABEL_PRODUCTIVE
|
||
"social":
|
||
return TEX_TAB_LABEL_SOCIAL
|
||
"rising":
|
||
return TEX_TAB_LABEL_RISING
|
||
"comprehensive":
|
||
return TEX_TAB_LABEL_COMPREHENSIVE
|
||
_:
|
||
return TEX_TAB_LABEL_WEEKLY
|
||
|
||
func _createTabSelectedStyle() -> StyleBoxFlat:
|
||
var style := StyleBoxFlat.new()
|
||
style.bg_color = Color(0.060, 0.355, 0.655, 0.88)
|
||
style.border_color = Color(0.690, 0.840, 1.0, 0.58)
|
||
style.border_width_left = 2
|
||
style.border_width_top = 2
|
||
style.border_width_right = 2
|
||
style.border_width_bottom = 2
|
||
style.corner_radius_top_left = 8
|
||
style.corner_radius_top_right = 8
|
||
style.corner_radius_bottom_left = 5
|
||
style.corner_radius_bottom_right = 5
|
||
return style
|
||
|
||
func _rankColor(rank: int) -> Color:
|
||
if rank == 1:
|
||
return GOLD
|
||
if rank == 2:
|
||
return SILVER
|
||
if rank == 3:
|
||
return BRONZE
|
||
return ACCENT_COLOR
|
||
|
||
func _rankFillColor(rank: int) -> Color:
|
||
if rank == 1:
|
||
return Color(1.0, 0.790, 0.260, 0.94)
|
||
if rank == 2:
|
||
return Color(0.760, 0.830, 0.910, 0.94)
|
||
if rank == 3:
|
||
return Color(0.890, 0.520, 0.285, 0.94)
|
||
return Color(0.250, 0.570, 0.800, 0.94)
|
||
|
||
func _rankMedalTexture(rank: int) -> Texture2D:
|
||
if rank == 1:
|
||
return TEX_MEDAL_GOLD
|
||
if rank == 2:
|
||
return TEX_MEDAL_SILVER
|
||
return TEX_MEDAL_BRONZE
|
||
|
||
func _createTabStyle(selected: bool) -> StyleBoxTexture:
|
||
var style := StyleBoxTexture.new()
|
||
style.texture = TEX_TAB_ACTIVE if selected else TEX_TAB_NORMAL
|
||
style.modulate_color = Color(0.960, 0.760, 0.360, 0.0)
|
||
return style
|
||
|
||
func _createTabFlatStyle(selected: bool) -> StyleBoxFlat:
|
||
var style := StyleBoxFlat.new()
|
||
style.bg_color = Color(0.115, 0.420, 0.690, 1.0) if selected else Color(0.965, 0.875, 0.665, 0.98)
|
||
style.border_color = Color(0.870, 0.625, 0.265, 1.0) if selected else Color(0.710, 0.455, 0.170, 0.86)
|
||
style.border_width_left = 2
|
||
style.border_width_top = 2
|
||
style.border_width_right = 2
|
||
style.border_width_bottom = 2
|
||
style.corner_radius_top_left = 6
|
||
style.corner_radius_top_right = 6
|
||
style.corner_radius_bottom_left = 4
|
||
style.corner_radius_bottom_right = 4
|
||
style.content_margin_left = 10
|
||
style.content_margin_right = 10
|
||
style.content_margin_top = 6
|
||
style.content_margin_bottom = 6
|
||
return style
|
||
|
||
func _createParchmentStyle(margins: Vector4, contentMargins: Vector4 = Vector4(0, 0, 0, 0)) -> StyleBoxTexture:
|
||
return _createTextureStyle(TEX_PARCHMENT_PANEL, margins, contentMargins)
|
||
|
||
func _createTextureStyle(texture: Texture2D, margins: Vector4, contentMargins: Vector4 = Vector4(0, 0, 0, 0), modulate: Color = Color.WHITE) -> StyleBoxTexture:
|
||
var style := StyleBoxTexture.new()
|
||
style.texture = texture
|
||
style.modulate_color = modulate
|
||
style.axis_stretch_horizontal = StyleBoxTexture.AXIS_STRETCH_MODE_STRETCH
|
||
style.axis_stretch_vertical = StyleBoxTexture.AXIS_STRETCH_MODE_STRETCH
|
||
style.texture_margin_left = margins.x
|
||
style.texture_margin_top = margins.y
|
||
style.texture_margin_right = margins.z
|
||
style.texture_margin_bottom = margins.w
|
||
style.content_margin_left = contentMargins.x
|
||
style.content_margin_top = contentMargins.y
|
||
style.content_margin_right = contentMargins.z
|
||
style.content_margin_bottom = contentMargins.w
|
||
return style
|
||
|
||
func _createIcon(texture: Texture2D, size: Vector2) -> TextureRect:
|
||
var icon := TextureRect.new()
|
||
icon.custom_minimum_size = size
|
||
icon.texture = texture
|
||
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
return icon
|
||
|
||
func _createIconText(iconTexture: Texture2D, label: Label, iconSize: Vector2, separation: int) -> Control:
|
||
var row := HBoxContainer.new()
|
||
row.alignment = BoxContainer.ALIGNMENT_BEGIN
|
||
row.add_theme_constant_override("separation", separation)
|
||
row.add_child(_createIcon(iconTexture, iconSize))
|
||
row.add_child(label)
|
||
return row
|
||
|
||
func _createMetricIconText(iconTexture: Texture2D, text: String, width: float, iconSize: float) -> Control:
|
||
var row := HBoxContainer.new()
|
||
row.custom_minimum_size = Vector2(width, 38)
|
||
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||
row.add_theme_constant_override("separation", 5)
|
||
row.add_child(_createIcon(iconTexture, Vector2(iconSize, iconSize)))
|
||
row.add_child(_createFixedLabel(text, maxf(42.0, width - iconSize - 12.0), 19, DEEP_BLUE, HORIZONTAL_ALIGNMENT_LEFT))
|
||
return row
|
||
|
||
func _createScoreMetricText(caption: String, text: String, width: float) -> Control:
|
||
var row := HBoxContainer.new()
|
||
row.custom_minimum_size = Vector2(width, 38)
|
||
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||
row.add_theme_constant_override("separation", 5)
|
||
row.add_child(_createIcon(TEX_ICON_SCORE, Vector2(20, 20)))
|
||
row.add_child(_createFixedLabel(caption, 50, 14, MUTED_COLOR, HORIZONTAL_ALIGNMENT_LEFT))
|
||
row.add_child(_createFixedLabel(text, maxf(44.0, width - 20.0 - 50.0 - 18.0), 19, DEEP_BLUE, HORIZONTAL_ALIGNMENT_LEFT))
|
||
return row
|
||
|
||
func _scoreCaption(categoryId: String) -> String:
|
||
match categoryId:
|
||
"weekly_commits":
|
||
return "卷王分"
|
||
"night_owl":
|
||
return "夜猫分"
|
||
"popularity":
|
||
return "人气分"
|
||
"productive":
|
||
return "多产分"
|
||
"social":
|
||
return "社交分"
|
||
"rising":
|
||
return "新星分"
|
||
"comprehensive":
|
||
return "综合分"
|
||
_:
|
||
return "荣誉分"
|
||
|
||
func _contribLabel(categoryId: String) -> String:
|
||
match categoryId:
|
||
"weekly_commits":
|
||
return "总贡献"
|
||
"night_owl":
|
||
return "总贡献"
|
||
"popularity":
|
||
return "总贡献"
|
||
"productive":
|
||
return "总贡献"
|
||
"social":
|
||
return "总贡献"
|
||
"rising":
|
||
return "总贡献"
|
||
"comprehensive":
|
||
return "总贡献"
|
||
_:
|
||
return "总贡献"
|
||
|
||
func _repoLabel(categoryId: String) -> String:
|
||
match categoryId:
|
||
"weekly_commits":
|
||
return "本周仓"
|
||
"night_owl":
|
||
return "深夜仓"
|
||
"popularity":
|
||
return "组织仓"
|
||
"productive":
|
||
return "组织仓"
|
||
"social":
|
||
return "组织仓"
|
||
"rising":
|
||
return "组织仓"
|
||
"comprehensive":
|
||
return "组织仓"
|
||
_:
|
||
return "仓库"
|
||
|
||
func _starLabel(categoryId: String) -> String:
|
||
match categoryId:
|
||
"popularity":
|
||
return "星标"
|
||
"rising":
|
||
return "星标"
|
||
"comprehensive":
|
||
return "星标"
|
||
_:
|
||
return "星标"
|
||
|
||
func _rankingRuleText(categoryId: String) -> String:
|
||
match categoryId:
|
||
"weekly_commits":
|
||
return "计算规则:近 7 天 commit 数 + 活跃天数奖励 + 多仓库奖励 + 日均提交质量奖励"
|
||
"night_owl":
|
||
return "计算规则:深夜 22:00-06:00 commit 数 × 2 + 深夜比例奖励 + 活跃天数奖励 + 多仓库奖励"
|
||
"popularity":
|
||
return "计算规则:GitHub Followers × 0.6 + 参与组织仓库总 Stars × 0.4"
|
||
"productive":
|
||
return "计算规则:参与 Datawhale 组织仓库数量,仓库越多排名越高"
|
||
"social":
|
||
return "计算规则:GitHub Following 数量,关注互动越多排名越高"
|
||
"rising":
|
||
return "计算规则:(Followers + 组织仓库总 Stars) / 参与仓库数 × 新人加成"
|
||
"comprehensive":
|
||
return "计算规则:Stars × 0.3 + Followers × 0.25 + 仓库数 × 0.2 + Following × 0.15 + 贡献数 × 0.1"
|
||
_:
|
||
return "计算规则:依据当前榜单维度综合计算排名"
|
||
|
||
func _podiumSlotBounds(rank: int) -> Vector2:
|
||
if PODIUM_SLOT_BOUNDS.has(rank):
|
||
return PODIUM_SLOT_BOUNDS[rank] as Vector2
|
||
return Vector2(492, 920)
|
||
|
||
func _podiumMetaLine(data: Dictionary) -> String:
|
||
var location := str(data.get("location", "")).strip_edges()
|
||
var domain := str(data.get("domain", "")).strip_edges()
|
||
if location.is_empty() and domain.is_empty():
|
||
return "Datawhale 成员"
|
||
if location.is_empty():
|
||
return domain
|
||
if domain.is_empty():
|
||
return "📍 %s" % location
|
||
return "📍 %s · %s" % [location, domain]
|
||
|
||
func _podiumTags(data: Dictionary) -> Array[String]:
|
||
var tags: Array[String] = []
|
||
var domainsVariant: Variant = data.get("domains", [])
|
||
if domainsVariant is Array:
|
||
for item in domainsVariant as Array:
|
||
var domain := str(item).strip_edges()
|
||
if not domain.is_empty() and not tags.has(domain):
|
||
tags.append(domain)
|
||
if tags.size() >= 3:
|
||
break
|
||
if tags.is_empty():
|
||
var domain := str(data.get("domain", "")).strip_edges()
|
||
if not domain.is_empty():
|
||
tags.append(domain)
|
||
while tags.size() > 3:
|
||
tags.pop_back()
|
||
return tags
|
||
|
||
func _avatarPrimaryTextureFromData(data: Dictionary) -> Texture2D:
|
||
var avatarBase64 := _firstNonEmptyString(data, ["avatarBase64", "avatar_base64"])
|
||
if not avatarBase64.is_empty():
|
||
var textureFromBase64 := _textureFromBase64(avatarBase64)
|
||
if textureFromBase64 != null:
|
||
return textureFromBase64
|
||
|
||
var localPath := _firstNonEmptyString(data, ["avatarTexture", "avatarTexturePath", "avatar_texture", "avatar_texture_path", "avatarLocalPath"])
|
||
if not localPath.is_empty():
|
||
var loaded := load(localPath) as Texture2D
|
||
if loaded != null:
|
||
var loadedImage := loaded.get_image()
|
||
if loadedImage != null:
|
||
return _createCircularAvatarTexture(loadedImage)
|
||
|
||
return null
|
||
|
||
func _avatarUrlFromData(data: Dictionary) -> String:
|
||
return _firstNonEmptyString(data, ["avatarUrl", "avatar_url", "avatar", "avatarURL"])
|
||
|
||
func _firstNonEmptyString(data: Dictionary, keys: Array[String]) -> String:
|
||
for key in keys:
|
||
var value := str(data.get(key, "")).strip_edges()
|
||
if not value.is_empty():
|
||
return value
|
||
return ""
|
||
|
||
func _textureFromBase64(base64Text: String) -> Texture2D:
|
||
var bytes := Marshalls.base64_to_raw(base64Text.strip_edges())
|
||
if bytes.is_empty():
|
||
return null
|
||
var image := Image.new()
|
||
var err := image.load_png_from_buffer(bytes)
|
||
if err != OK:
|
||
err = image.load_jpg_from_buffer(bytes)
|
||
if err != OK:
|
||
return null
|
||
return _createCircularAvatarTexture(image)
|
||
|
||
func _requestAvatarTexture(url: String, textureRect: TextureRect, fallbackLabel: Label, fallbackTexture: Texture2D) -> void:
|
||
var request := HTTPRequest.new()
|
||
request.timeout = 10.0
|
||
request.request_completed.connect(_onAvatarRequestCompleted.bind(textureRect, fallbackLabel, fallbackTexture, request))
|
||
_avatarRequests.append(request)
|
||
add_child(request)
|
||
var err := request.request(url)
|
||
if err != OK:
|
||
textureRect.texture = fallbackTexture
|
||
fallbackLabel.visible = fallbackTexture == null
|
||
_avatarRequests.erase(request)
|
||
request.queue_free()
|
||
|
||
func _onAvatarRequestCompleted(result: int, responseCode: int, headers: PackedStringArray, body: PackedByteArray, textureRect: TextureRect, fallbackLabel: Label, fallbackTexture: Texture2D, request: HTTPRequest) -> void:
|
||
_avatarRequests.erase(request)
|
||
request.queue_free()
|
||
if result != HTTPRequest.RESULT_SUCCESS or responseCode < 200 or responseCode >= 300:
|
||
if is_instance_valid(textureRect):
|
||
textureRect.texture = fallbackTexture
|
||
if is_instance_valid(fallbackLabel):
|
||
fallbackLabel.visible = fallbackTexture == null
|
||
return
|
||
if not is_instance_valid(textureRect):
|
||
return
|
||
var image := Image.new()
|
||
var err := _loadAvatarImageFromResponse(image, body, headers)
|
||
if err != OK:
|
||
textureRect.texture = fallbackTexture
|
||
if is_instance_valid(fallbackLabel):
|
||
fallbackLabel.visible = fallbackTexture == null
|
||
return
|
||
textureRect.texture = _createCircularAvatarTexture(image)
|
||
if is_instance_valid(fallbackLabel):
|
||
fallbackLabel.visible = false
|
||
|
||
func _createCircularAvatarTexture(sourceImage: Image) -> Texture2D:
|
||
var sourceWidth := sourceImage.get_width()
|
||
var sourceHeight := sourceImage.get_height()
|
||
if sourceWidth <= 0 or sourceHeight <= 0:
|
||
return null
|
||
|
||
var cropSize := mini(sourceWidth, sourceHeight)
|
||
var cropOrigin := Vector2i((sourceWidth - cropSize) / 2, (sourceHeight - cropSize) / 2)
|
||
var outputSize := 96
|
||
var cropped := Image.create(cropSize, cropSize, false, Image.FORMAT_RGBA8)
|
||
if sourceImage.get_format() != Image.FORMAT_RGBA8:
|
||
sourceImage.convert(Image.FORMAT_RGBA8)
|
||
cropped.blit_rect(sourceImage, Rect2i(cropOrigin, Vector2i(cropSize, cropSize)), Vector2i.ZERO)
|
||
cropped.resize(outputSize, outputSize, Image.INTERPOLATE_LANCZOS)
|
||
cropped.convert(Image.FORMAT_RGBA8)
|
||
|
||
var radius := float(outputSize) * 0.5
|
||
var center := Vector2(radius, radius)
|
||
var edge := 2.0
|
||
for y in range(outputSize):
|
||
for x in range(outputSize):
|
||
var distance := Vector2(float(x) + 0.5, float(y) + 0.5).distance_to(center)
|
||
if distance <= radius - edge:
|
||
continue
|
||
var color := cropped.get_pixel(x, y)
|
||
var alphaScale := clampf((radius - distance) / edge, 0.0, 1.0)
|
||
color.a *= alphaScale
|
||
cropped.set_pixel(x, y, color)
|
||
return ImageTexture.create_from_image(cropped)
|
||
|
||
func _loadAvatarImageFromResponse(image: Image, body: PackedByteArray, headers: PackedStringArray) -> Error:
|
||
if body.size() >= 8 and body[0] == 0x89 and body[1] == 0x50 and body[2] == 0x4e and body[3] == 0x47:
|
||
return image.load_png_from_buffer(body)
|
||
if body.size() >= 3 and body[0] == 0xff and body[1] == 0xd8 and body[2] == 0xff:
|
||
return image.load_jpg_from_buffer(body)
|
||
if body.size() >= 12 and body[0] == 0x52 and body[1] == 0x49 and body[2] == 0x46 and body[3] == 0x46 and body[8] == 0x57 and body[9] == 0x45 and body[10] == 0x42 and body[11] == 0x50:
|
||
return image.load_webp_from_buffer(body)
|
||
|
||
var contentType := _responseContentType(headers)
|
||
if contentType.contains("jpeg") or contentType.contains("jpg"):
|
||
var jpgError := image.load_jpg_from_buffer(body)
|
||
if jpgError == OK:
|
||
return OK
|
||
if contentType.contains("webp"):
|
||
var webpError := image.load_webp_from_buffer(body)
|
||
if webpError == OK:
|
||
return OK
|
||
if contentType.contains("png"):
|
||
var pngError := image.load_png_from_buffer(body)
|
||
if pngError == OK:
|
||
return OK
|
||
|
||
return image.load_png_from_buffer(body)
|
||
|
||
func _responseContentType(headers: PackedStringArray) -> String:
|
||
for header in headers:
|
||
var lower := header.to_lower()
|
||
if lower.begins_with("content-type:"):
|
||
return lower.get_slice(":", 1).strip_edges()
|
||
return ""
|
||
|
||
func _clearAvatarRequests() -> void:
|
||
for request in _avatarRequests:
|
||
if is_instance_valid(request):
|
||
request.cancel_request()
|
||
request.queue_free()
|
||
_avatarRequests.clear()
|
||
|
||
func _createAvatarFrameStyle(radius: int) -> StyleBoxFlat:
|
||
var style := StyleBoxFlat.new()
|
||
style.bg_color = Color(0.996, 0.875, 0.620, 0.96)
|
||
style.border_color = Color(0.580, 0.365, 0.145, 0.92)
|
||
style.border_width_left = 2
|
||
style.border_width_top = 2
|
||
style.border_width_right = 2
|
||
style.border_width_bottom = 2
|
||
style.corner_radius_top_left = radius
|
||
style.corner_radius_top_right = radius
|
||
style.corner_radius_bottom_left = radius
|
||
style.corner_radius_bottom_right = radius
|
||
return style
|
||
|
||
func _createPanelStyle(fill: Color, radius: int, border: Color, borderWidth: int, shadow: bool) -> StyleBoxFlat:
|
||
var style := StyleBoxFlat.new()
|
||
style.bg_color = fill
|
||
style.corner_radius_top_left = radius
|
||
style.corner_radius_top_right = radius
|
||
style.corner_radius_bottom_right = radius
|
||
style.corner_radius_bottom_left = radius
|
||
style.border_width_left = borderWidth
|
||
style.border_width_top = borderWidth
|
||
style.border_width_right = borderWidth
|
||
style.border_width_bottom = borderWidth
|
||
style.border_color = border
|
||
if shadow:
|
||
style.shadow_color = Color(0.030, 0.150, 0.320, 0.20)
|
||
style.shadow_size = 16
|
||
style.shadow_offset = Vector2(0, 7)
|
||
return style
|
||
|
||
func _formatNumber(value: int) -> String:
|
||
var raw := str(value)
|
||
var output := ""
|
||
var count := 0
|
||
for index in range(raw.length() - 1, -1, -1):
|
||
if count > 0 and count % 3 == 0:
|
||
output = "," + output
|
||
output = raw[index] + output
|
||
count += 1
|
||
return output
|
||
|
||
func _compactNumber(value: int) -> String:
|
||
if value >= 1000000:
|
||
return "%.1fM" % (float(value) / 1000000.0)
|
||
if value >= 10000:
|
||
return "%.1f万" % (float(value) / 10000.0)
|
||
return str(value)
|
||
|
||
func _clearChildren(node: Node) -> void:
|
||
for child in node.get_children():
|
||
child.queue_free()
|
||
|
||
func _urlEncode(value: String) -> String:
|
||
return value.uri_encode()
|
||
|
||
func _onClosePressed() -> void:
|
||
get_tree().paused = false
|
||
queue_free()
|
||
|
||
func _disableChatUiMouseInput() -> void:
|
||
var currentScene := get_tree().current_scene
|
||
if currentScene != null:
|
||
_chatUi = currentScene.get_node_or_null("UILayer/ChatUI") as Control
|
||
|
||
if _chatUi == null:
|
||
_chatUi = get_tree().root.find_child("ChatUI", true, false) as Control
|
||
|
||
if _chatUi != null:
|
||
_chatUiPrevMouseFilter = _chatUi.mouse_filter
|
||
_chatUi.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
_chatUiMouseDisabled = true
|
||
|
||
func _restoreChatUiMouseInput() -> void:
|
||
if _chatUiMouseDisabled and is_instance_valid(_chatUi):
|
||
_chatUi.mouse_filter = _chatUiPrevMouseFilter
|
||
|
||
_chatUiMouseDisabled = false
|
||
_chatUi = null
|