feat: add nearby social interactions
This commit is contained in:
438
_Core/managers/SocialManager.gd
Normal file
438
_Core/managers/SocialManager.gd
Normal file
@@ -0,0 +1,438 @@
|
||||
extends Node
|
||||
|
||||
# 社区资料、通知与旅行 API 的客户端协调器。
|
||||
const ACCENT := Color("58c7db")
|
||||
const PANEL_COLOR := Color(0.035, 0.071, 0.106, 0.97)
|
||||
const DISCOVERY_DISTANCE: float = 180.0
|
||||
const DISCOVERY_POINTS: Array[Dictionary] = [
|
||||
{"id": "square_center", "mapId": "whale_port", "position": Vector2(0, 30)},
|
||||
{"id": "square_dock", "mapId": "whale_port", "position": Vector2(-870, -202)},
|
||||
{"id": "square_headquarters", "mapId": "whale_port", "position": Vector2(13, -600)},
|
||||
{"id": "square_cottage", "mapId": "whale_port", "position": Vector2(845, -192)},
|
||||
{"id": "square_workshop", "mapId": "whale_port", "position": Vector2(645, 500)},
|
||||
{"id": "square_notice", "mapId": "whale_port", "position": Vector2(-542, 648)},
|
||||
{"id": "square_work_zone_gate", "mapId": "whale_port", "position": Vector2(0, 775)},
|
||||
{"id": "work_entrance", "mapId": "work_zone", "position": Vector2(0, 755)},
|
||||
{"id": "work_mall", "mapId": "work_zone", "position": Vector2(0, -614)},
|
||||
{"id": "work_cafe_gate", "mapId": "work_zone", "position": Vector2(-1044, 222)},
|
||||
{"id": "work_jobs", "mapId": "work_zone", "position": Vector2(-502, 192)},
|
||||
{"id": "work_courses", "mapId": "work_zone", "position": Vector2(496, 0)},
|
||||
{"id": "work_ai", "mapId": "work_zone", "position": Vector2(452, 548)},
|
||||
{"id": "work_exchange", "mapId": "work_zone", "position": Vector2(1075, 548)},
|
||||
{"id": "cafe_entrance", "mapId": "whale_cafe", "position": Vector2(0, 363)},
|
||||
{"id": "cafe_counter", "mapId": "whale_cafe", "position": Vector2(0, -37)},
|
||||
{"id": "cafe_companion", "mapId": "whale_cafe", "position": Vector2(-398, -226)},
|
||||
{"id": "personal_room", "mapId": "personal_space", "position": Vector2.ZERO},
|
||||
]
|
||||
|
||||
var _canvas: CanvasLayer
|
||||
var _profile_panel: PanelContainer
|
||||
var _profile_content: VBoxContainer
|
||||
var _notification_panel: PanelContainer
|
||||
var _notification_content: VBoxContainer
|
||||
var _current_profile: Dictionary = {}
|
||||
var _discoveryRequests: Dictionary = {}
|
||||
var _discoveredDestinations: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("whaletown_escape_dismissible")
|
||||
_build_ui()
|
||||
var event_system := get_node_or_null("/root/EventSystem")
|
||||
if event_system != null:
|
||||
event_system.call("connect_event", EventNames.SOCIAL_NOTIFICATION_RECEIVED, _on_social_realtime, self)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
var event_system := get_node_or_null("/root/EventSystem")
|
||||
if event_system != null:
|
||||
event_system.call("disconnect_event", EventNames.SOCIAL_NOTIFICATION_RECEIVED, _on_social_realtime, self)
|
||||
|
||||
func show_profile(user_id: String) -> void:
|
||||
var normalized := user_id.strip_edges()
|
||||
if normalized.is_empty():
|
||||
return
|
||||
_request_get("/social/profiles/%s" % normalized, func(success: bool, response: Dictionary, error: Dictionary) -> void:
|
||||
if not success:
|
||||
_show_status("无法查看名片:%s" % str(error.get("message", "请求失败")))
|
||||
return
|
||||
var data_variant: Variant = response.get("data", {})
|
||||
if data_variant is Dictionary:
|
||||
_current_profile = data_variant as Dictionary
|
||||
_render_profile()
|
||||
)
|
||||
|
||||
func show_own_profile() -> void:
|
||||
_request_get("/social/profile", func(success: bool, response: Dictionary, error: Dictionary) -> void:
|
||||
if not success:
|
||||
_show_status("无法读取个人名片:%s" % str(error.get("message", "请求失败")))
|
||||
return
|
||||
var data_variant: Variant = response.get("data", {})
|
||||
if data_variant is Dictionary:
|
||||
_current_profile = data_variant as Dictionary
|
||||
_render_profile()
|
||||
)
|
||||
|
||||
func request_friend(user_id: String, username: String = "") -> void:
|
||||
var chat := get_node_or_null("/root/ChatManager")
|
||||
if chat != null and chat.has_method("request_friend"):
|
||||
chat.call("request_friend", user_id, username)
|
||||
|
||||
func open_private_chat(user_id: String, username: String) -> void:
|
||||
var event_system := get_node_or_null("/root/EventSystem")
|
||||
if event_system != null:
|
||||
event_system.call("emit_event", EventNames.CHAT_PRIVATE_TARGET_SELECTED, {"userId": user_id, "username": username})
|
||||
_close_profile()
|
||||
|
||||
func request_travel(destination_id: String, completion: Callable = Callable()) -> void:
|
||||
_request_post("/world/travel-destinations/%s/travel" % destination_id.uri_encode(), {}, func(success: bool, response: Dictionary, error: Dictionary) -> void:
|
||||
if not success:
|
||||
_show_status(str(error.get("message", "该地点尚未解锁")))
|
||||
if completion.is_valid(): completion.call(false, {})
|
||||
return
|
||||
var data_variant: Variant = response.get("data", {})
|
||||
var data: Dictionary = data_variant if data_variant is Dictionary else {}
|
||||
if completion.is_valid(): completion.call(true, data)
|
||||
)
|
||||
|
||||
func discover_destination(destination_id: String) -> void:
|
||||
var normalized := destination_id.strip_edges()
|
||||
if normalized.is_empty() or _discoveredDestinations.has(normalized) or _discoveryRequests.has(normalized):
|
||||
return
|
||||
_discoveryRequests[normalized] = true
|
||||
_request_post("/world/travel-destinations/%s/discover" % normalized.uri_encode(), {}, func(success: bool, _response: Dictionary, _error: Dictionary) -> void:
|
||||
_discoveryRequests.erase(normalized)
|
||||
if success:
|
||||
_discoveredDestinations[normalized] = true
|
||||
)
|
||||
|
||||
func discover_nearby_destinations(map_id: String, position: Vector2) -> void:
|
||||
for point in DISCOVERY_POINTS:
|
||||
if str(point.get("mapId", "")) != map_id:
|
||||
continue
|
||||
var target := point.get("position", Vector2.ZERO) as Vector2
|
||||
if position.distance_to(target) <= DISCOVERY_DISTANCE:
|
||||
discover_destination(str(point.get("id", "")))
|
||||
|
||||
func toggle_notifications() -> void:
|
||||
_notification_panel.visible = not _notification_panel.visible
|
||||
if _notification_panel.visible:
|
||||
_refresh_notifications()
|
||||
|
||||
func is_escape_dismissible() -> bool:
|
||||
return (is_instance_valid(_profile_panel) and _profile_panel.visible) or (is_instance_valid(_notification_panel) and _notification_panel.visible)
|
||||
|
||||
func get_escape_priority() -> int:
|
||||
return 800
|
||||
|
||||
func request_escape_close() -> void:
|
||||
if is_instance_valid(_profile_panel) and _profile_panel.visible:
|
||||
_close_profile()
|
||||
return
|
||||
if is_instance_valid(_notification_panel):
|
||||
_notification_panel.visible = false
|
||||
|
||||
func _on_social_realtime(_payload: Dictionary) -> void:
|
||||
if _notification_panel.visible:
|
||||
_refresh_notifications()
|
||||
|
||||
func _refresh_notifications() -> void:
|
||||
_request_get("/social/notifications?limit=30", func(success: bool, response: Dictionary, error: Dictionary) -> void:
|
||||
if not success:
|
||||
_show_notification_rows(["通知暂时无法读取:%s" % str(error.get("message", "请求失败"))])
|
||||
return
|
||||
var data_variant: Variant = response.get("data", {})
|
||||
var data: Dictionary = data_variant if data_variant is Dictionary else {}
|
||||
var rows: Array[String] = []
|
||||
var notifications_variant: Variant = data.get("notifications", [])
|
||||
if notifications_variant is Array:
|
||||
for item in notifications_variant as Array:
|
||||
if item is Dictionary:
|
||||
var notification: Dictionary = item
|
||||
rows.append("%s\n%s" % [str(notification.get("title", "通知")), str(notification.get("content", ""))])
|
||||
if rows.is_empty(): rows.append("暂无通知")
|
||||
_show_notification_rows(rows)
|
||||
)
|
||||
|
||||
func _render_profile() -> void:
|
||||
_profile_panel.visible = true
|
||||
for child in _profile_content.get_children():
|
||||
child.queue_free()
|
||||
var nickname := str(_current_profile.get("nickname", "玩家"))
|
||||
var username := str(_current_profile.get("username", ""))
|
||||
var online := bool(_current_profile.get("online", false))
|
||||
var area := str(_current_profile.get("currentArea", ""))
|
||||
var identity := HBoxContainer.new()
|
||||
identity.add_theme_constant_override("separation", 12)
|
||||
identity.add_child(_avatar_badge(nickname))
|
||||
var identity_text := VBoxContainer.new()
|
||||
identity_text.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
identity_text.add_child(_label(nickname, 26, ACCENT))
|
||||
identity_text.add_child(_label("@%s · %s" % [username, "在线" if online else "离线"], 15, Color(0.72, 0.82, 0.87, 1)))
|
||||
identity.add_child(identity_text)
|
||||
_profile_content.add_child(identity)
|
||||
_profile_content.add_child(_label("当前区域:%s" % _area_label(area), 16, Color(0.88, 0.94, 0.96, 1)))
|
||||
var skin_id := str(_current_profile.get("skinId", "")).strip_edges()
|
||||
_profile_content.add_child(_label("角色:%s" % (skin_id if not skin_id.is_empty() else "默认角色"), 14, Color(0.50, 0.76, 0.80, 1)))
|
||||
var bio := str(_current_profile.get("bio", "")).strip_edges()
|
||||
_profile_content.add_child(_label(bio if not bio.is_empty() else "这个玩家还没有留下简介。", 16, Color(0.84, 0.90, 0.92, 1)))
|
||||
var interests_variant: Variant = _current_profile.get("interests", [])
|
||||
var interests: Array[String] = []
|
||||
if interests_variant is Array:
|
||||
for interest in interests_variant as Array: interests.append(str(interest))
|
||||
_profile_content.add_child(_label("兴趣:%s" % (" · ".join(interests) if not interests.is_empty() else "未设置"), 14, Color(0.50, 0.76, 0.80, 1)))
|
||||
var user_id := str(_current_profile.get("id", ""))
|
||||
var self_profile := user_id == _current_user_id()
|
||||
if self_profile:
|
||||
_profile_content.add_child(_label("编辑社区资料", 17, ACCENT))
|
||||
var nickname_input := LineEdit.new()
|
||||
nickname_input.placeholder_text = "昵称(首次可立即修改,此后每 7 天一次)"
|
||||
nickname_input.text = nickname
|
||||
_profile_content.add_child(nickname_input)
|
||||
var bio_input := TextEdit.new()
|
||||
bio_input.placeholder_text = "简介(最多 160 字)"
|
||||
bio_input.text = bio
|
||||
bio_input.custom_minimum_size = Vector2(0, 82)
|
||||
_profile_content.add_child(bio_input)
|
||||
var current_interests: Array[String] = []
|
||||
for interest in interests: current_interests.append(interest)
|
||||
var interest_row := HBoxContainer.new()
|
||||
interest_row.add_theme_constant_override("separation", 6)
|
||||
var interest_selects: Array[OptionButton] = []
|
||||
for slot in 3:
|
||||
var selector := OptionButton.new()
|
||||
selector.custom_minimum_size = Vector2(145, 34)
|
||||
selector.add_item("兴趣标签")
|
||||
for tag in _interest_catalog():
|
||||
selector.add_item(str(tag.get("label", "")))
|
||||
selector.set_item_metadata(selector.item_count - 1, str(tag.get("id", "")))
|
||||
if slot < current_interests.size():
|
||||
for item_index in range(1, selector.item_count):
|
||||
if str(selector.get_item_metadata(item_index)) == current_interests[slot]:
|
||||
selector.select(item_index)
|
||||
break
|
||||
interest_selects.append(selector)
|
||||
interest_row.add_child(selector)
|
||||
_profile_content.add_child(interest_row)
|
||||
_profile_content.add_child(_button("保存资料", func() -> void: _save_own_profile(nickname_input.text, bio_input.text, interest_selects)))
|
||||
else:
|
||||
var actions := HBoxContainer.new()
|
||||
actions.add_theme_constant_override("separation", 8)
|
||||
actions.add_child(_button("私聊", func() -> void: open_private_chat(user_id, nickname)))
|
||||
actions.add_child(_button("加好友", func() -> void: request_friend(user_id, nickname)))
|
||||
_profile_content.add_child(actions)
|
||||
var safety := HBoxContainer.new()
|
||||
safety.add_theme_constant_override("separation", 8)
|
||||
safety.add_child(_button("拉黑", func() -> void: _block_user(user_id)))
|
||||
var report_form := _report_form(user_id)
|
||||
var report_button := _button("举报", func() -> void: report_form.visible = not report_form.visible)
|
||||
safety.add_child(report_button)
|
||||
_profile_content.add_child(safety)
|
||||
_profile_content.add_child(report_form)
|
||||
_profile_content.add_child(_button("关闭", _close_profile))
|
||||
|
||||
func _block_user(user_id: String) -> void:
|
||||
_request_post("/social/blocks", {"userId": user_id}, func(success: bool, _response: Dictionary, error: Dictionary) -> void:
|
||||
_show_status("已拉黑该玩家" if success else str(error.get("message", "拉黑失败")))
|
||||
if success: _close_profile()
|
||||
)
|
||||
|
||||
func _save_own_profile(nickname: String, bio: String, interest_selects: Array[OptionButton]) -> void:
|
||||
var interests: Array[String] = []
|
||||
for selector in interest_selects:
|
||||
var index := selector.selected
|
||||
if index > 0:
|
||||
var value := str(selector.get_item_metadata(index)).strip_edges()
|
||||
if not value.is_empty() and not interests.has(value): interests.append(value)
|
||||
_request_patch("/social/profile", {"nickname": nickname.strip_edges(), "bio": bio.strip_edges(), "interests": interests}, func(success: bool, response: Dictionary, error: Dictionary) -> void:
|
||||
if not success:
|
||||
_show_status(str(error.get("message", "资料保存失败")))
|
||||
return
|
||||
var data_variant: Variant = response.get("data", {})
|
||||
if data_variant is Dictionary:
|
||||
_current_profile = data_variant as Dictionary
|
||||
_render_profile()
|
||||
var event_system := get_node_or_null("/root/EventSystem")
|
||||
if event_system != null: event_system.call("emit_event", EventNames.SOCIAL_PROFILE_UPDATED, _current_profile)
|
||||
)
|
||||
|
||||
func _report_form(user_id: String) -> VBoxContainer:
|
||||
var form := VBoxContainer.new()
|
||||
form.visible = false
|
||||
form.add_theme_constant_override("separation", 6)
|
||||
form.add_theme_stylebox_override("panel", _subpanel_style())
|
||||
form.add_child(_label("举报原因", 15, ACCENT))
|
||||
var reason := OptionButton.new()
|
||||
for item in [
|
||||
{"id": "harassment", "label": "骚扰或辱骂"},
|
||||
{"id": "spam", "label": "垃圾信息"},
|
||||
{"id": "inappropriate_content", "label": "不当内容"},
|
||||
{"id": "impersonation", "label": "冒充他人"},
|
||||
{"id": "other", "label": "其他"},
|
||||
]:
|
||||
reason.add_item(str(item.get("label", "其他")))
|
||||
reason.set_item_metadata(reason.item_count - 1, str(item.get("id", "other")))
|
||||
form.add_child(reason)
|
||||
var note := TextEdit.new()
|
||||
note.placeholder_text = "补充说明(可选,最多 500 字)"
|
||||
note.custom_minimum_size = Vector2(0, 68)
|
||||
form.add_child(note)
|
||||
var block_also := CheckBox.new()
|
||||
block_also.text = "同时拉黑此玩家"
|
||||
block_also.button_pressed = false
|
||||
form.add_child(block_also)
|
||||
form.add_child(_button("提交举报", func() -> void:
|
||||
_submit_report(user_id, str(reason.get_item_metadata(reason.selected)), note.text, block_also.button_pressed)
|
||||
))
|
||||
return form
|
||||
|
||||
func _submit_report(user_id: String, reason: String, note: String, block_also: bool) -> void:
|
||||
_request_post("/social/reports", {
|
||||
"userId": user_id,
|
||||
"reason": reason,
|
||||
"note": note.strip_edges(),
|
||||
"blockAlso": block_also,
|
||||
}, func(success: bool, _response: Dictionary, error: Dictionary) -> void:
|
||||
_show_status("举报已提交" if success else str(error.get("message", "举报失败")))
|
||||
if success and block_also:
|
||||
_close_profile()
|
||||
)
|
||||
|
||||
func _build_ui() -> void:
|
||||
_canvas = CanvasLayer.new()
|
||||
_canvas.layer = 88
|
||||
add_child(_canvas)
|
||||
_profile_panel = _panel(Vector2(540, 610))
|
||||
_profile_panel.set_anchors_preset(Control.PRESET_CENTER)
|
||||
_profile_panel.position = Vector2(-270, -305)
|
||||
_canvas.add_child(_profile_panel)
|
||||
var profile_margin := _margin(_profile_panel)
|
||||
var profile_scroll := ScrollContainer.new()
|
||||
profile_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
profile_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
profile_margin.add_child(profile_scroll)
|
||||
_profile_content = VBoxContainer.new()
|
||||
_profile_content.add_theme_constant_override("separation", 12)
|
||||
_profile_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
profile_scroll.add_child(_profile_content)
|
||||
_notification_panel = _panel(Vector2(390, 430))
|
||||
_notification_panel.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||||
_notification_panel.position = Vector2(-414, 110)
|
||||
_canvas.add_child(_notification_panel)
|
||||
var notification_margin := _margin(_notification_panel)
|
||||
_notification_content = VBoxContainer.new()
|
||||
_notification_content.add_theme_constant_override("separation", 9)
|
||||
notification_margin.add_child(_notification_content)
|
||||
_notification_panel.visible = false
|
||||
_profile_panel.visible = false
|
||||
|
||||
func _show_notification_rows(rows: Array[String]) -> void:
|
||||
for child in _notification_content.get_children(): child.queue_free()
|
||||
_notification_content.add_child(_label("通知中心", 22, ACCENT))
|
||||
for row in rows:
|
||||
_notification_content.add_child(_label(row, 15, Color(0.87, 0.93, 0.95, 1)))
|
||||
_notification_content.add_child(_button("全部标为已读", func() -> void:
|
||||
_request_patch("/social/notifications/read-all", {}, func(_success: bool, _response: Dictionary, _error: Dictionary) -> void: _refresh_notifications())
|
||||
))
|
||||
|
||||
func _panel(panel_size: Vector2) -> PanelContainer:
|
||||
var panel := PanelContainer.new()
|
||||
panel.size = panel_size
|
||||
panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = PANEL_COLOR
|
||||
style.border_color = Color(0.20, 0.52, 0.61, 0.9)
|
||||
style.set_border_width_all(1)
|
||||
style.set_corner_radius_all(12)
|
||||
style.shadow_color = Color(0, 0, 0, 0.45)
|
||||
style.shadow_size = 16
|
||||
panel.add_theme_stylebox_override("panel", style)
|
||||
return panel
|
||||
|
||||
func _margin(parent: Control) -> MarginContainer:
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 20)
|
||||
margin.add_theme_constant_override("margin_top", 18)
|
||||
margin.add_theme_constant_override("margin_right", 20)
|
||||
margin.add_theme_constant_override("margin_bottom", 18)
|
||||
parent.add_child(margin)
|
||||
return margin
|
||||
|
||||
func _label(text: String, font_size: int, color: Color) -> Label:
|
||||
var label := Label.new()
|
||||
label.text = text
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.add_theme_font_size_override("font_size", font_size)
|
||||
label.add_theme_color_override("font_color", color)
|
||||
return label
|
||||
|
||||
func _button(text: String, callback: Callable) -> Button:
|
||||
var button := Button.new()
|
||||
button.text = text
|
||||
button.custom_minimum_size = Vector2(92, 34)
|
||||
button.pressed.connect(callback)
|
||||
return button
|
||||
|
||||
func _avatar_badge(nickname: String) -> Label:
|
||||
var badge := Label.new()
|
||||
badge.text = nickname.left(1).to_upper() if not nickname.is_empty() else "玩"
|
||||
badge.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
badge.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
badge.custom_minimum_size = Vector2(52, 52)
|
||||
badge.add_theme_font_size_override("font_size", 22)
|
||||
badge.add_theme_color_override("font_color", Color(0.92, 0.99, 1.0, 1.0))
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.12, 0.40, 0.48, 1.0)
|
||||
style.set_corner_radius_all(26)
|
||||
style.border_color = ACCENT
|
||||
style.set_border_width_all(1)
|
||||
badge.add_theme_stylebox_override("normal", style)
|
||||
return badge
|
||||
|
||||
func _subpanel_style() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.06, 0.13, 0.18, 0.88)
|
||||
style.set_border_width_all(1)
|
||||
style.border_color = Color(0.18, 0.45, 0.52, 0.75)
|
||||
style.set_corner_radius_all(8)
|
||||
style.content_margin_left = 10
|
||||
style.content_margin_right = 10
|
||||
style.content_margin_top = 8
|
||||
style.content_margin_bottom = 8
|
||||
return style
|
||||
|
||||
func _close_profile() -> void:
|
||||
_profile_panel.visible = false
|
||||
|
||||
func _show_status(message: String) -> void:
|
||||
push_warning("Social: %s" % message)
|
||||
|
||||
func _request_get(endpoint: String, callback: Callable) -> void:
|
||||
var api := get_node_or_null("/root/ApiClient")
|
||||
if api != null: api.call("get_json", endpoint, callback, true)
|
||||
|
||||
func _request_post(endpoint: String, payload: Dictionary, callback: Callable) -> void:
|
||||
var api := get_node_or_null("/root/ApiClient")
|
||||
if api != null: api.call("post_json", endpoint, payload, callback, true)
|
||||
|
||||
func _request_patch(endpoint: String, payload: Dictionary, callback: Callable) -> void:
|
||||
var api := get_node_or_null("/root/ApiClient")
|
||||
if api != null: api.call("patch_json", endpoint, payload, callback, true)
|
||||
|
||||
func _current_user_id() -> String:
|
||||
var auth := get_node_or_null("/root/AuthManager")
|
||||
if auth != null and auth.has_method("get_current_user"):
|
||||
var user_variant: Variant = auth.call("get_current_user")
|
||||
if user_variant is Dictionary: return str((user_variant as Dictionary).get("id", ""))
|
||||
return ""
|
||||
|
||||
func _area_label(map_id: String) -> String:
|
||||
return {"whale_port": "中心广场", "work_zone": "打工区", "whale_cafe": "鲸鱼咖啡馆", "personal_space": "个人空间"}.get(map_id, map_id)
|
||||
|
||||
func _interest_catalog() -> Array[Dictionary]:
|
||||
return [
|
||||
{"id": "ai", "label": "AI/大模型"}, {"id": "programming", "label": "编程开发"},
|
||||
{"id": "data_science", "label": "数据科学"}, {"id": "open_source", "label": "开源协作"},
|
||||
{"id": "product", "label": "产品"}, {"id": "design", "label": "设计"},
|
||||
{"id": "game_dev", "label": "游戏开发"}, {"id": "content_creation", "label": "内容创作"},
|
||||
{"id": "community", "label": "社区活动"}, {"id": "learning_partner", "label": "学习搭子"},
|
||||
{"id": "career", "label": "职业成长"}, {"id": "casual_chat", "label": "轻松闲聊"},
|
||||
]
|
||||
Reference in New Issue
Block a user