feat: add taskbook onboarding UI

This commit is contained in:
ANG-Server
2026-07-22 00:45:59 +08:00
parent bb91ccea21
commit 9a3248abd6
15 changed files with 598 additions and 2 deletions

View File

@@ -107,6 +107,7 @@ const HUD_FRIEND_LIST_TOGGLE = "hud_friend_list_toggle"
const HUD_SETTINGS_REQUESTED = "hud_settings_requested" const HUD_SETTINGS_REQUESTED = "hud_settings_requested"
const HUD_BACKPACK_TOGGLE = "hud_backpack_toggle" const HUD_BACKPACK_TOGGLE = "hud_backpack_toggle"
const HUD_MAP_TOGGLE = "hud_map_toggle" const HUD_MAP_TOGGLE = "hud_map_toggle"
const HUD_TASK_BOOK_REQUESTED = "hud_task_book_requested"
# ============================================================================ # ============================================================================
# 商城事件 # 商城事件

View File

@@ -0,0 +1,128 @@
extends Node
# 玩家任务书状态与后端任务接口的唯一入口。
signal board_changed(board: Dictionary)
signal board_failed(message: String)
signal reward_claimed(task_id: String, wallet: Dictionary)
var _board: Dictionary = {}
var _loading_board: bool = false
var _account_generation: int = -1
func _ready() -> void:
var auth_manager := get_node_or_null("/root/AuthManager")
if auth_manager != null and auth_manager.has_signal("auth_state_changed"):
var callback := Callable(self, "_on_auth_state_changed")
if not auth_manager.is_connected("auth_state_changed", callback):
auth_manager.connect("auth_state_changed", callback)
call_deferred("_refresh_after_ready")
func get_board() -> Dictionary:
return _board.duplicate(true)
func is_loaded() -> bool:
return not _board.is_empty()
func refresh_board() -> void:
if not _is_authenticated() or _loading_board:
return
var api := _api_client()
if api == null:
board_failed.emit("任务服务不可用")
return
_loading_board = true
_account_generation = _current_account_generation()
api.call("get_json", "/tasks/board", Callable(self, "_on_board_response").bind(_account_generation), true)
func report_activity(activity: String, target_id: String = "") -> void:
if not _is_authenticated():
return
var api := _api_client()
if api == null:
return
var payload := {"activity": activity}
if not target_id.strip_edges().is_empty():
payload["target_id"] = target_id.strip_edges()
var request_generation := _current_account_generation()
api.call("post_json", "/tasks/activities", payload, Callable(self, "_on_activity_response").bind(request_generation), true)
func claim_task(task_id: String) -> void:
if task_id.strip_edges().is_empty() or not _is_authenticated():
return
var api := _api_client()
if api == null:
board_failed.emit("任务服务不可用")
return
var request_generation := _current_account_generation()
api.call("post_json", "/tasks/%s/claim" % task_id.uri_encode(), {}, Callable(self, "_on_claim_response").bind(task_id, request_generation), true)
func _refresh_after_ready() -> void:
if _is_authenticated():
refresh_board()
func _on_auth_state_changed(is_authenticated: bool, _user: Dictionary) -> void:
if not is_authenticated:
_board.clear()
_loading_board = false
_account_generation = -1
board_changed.emit({})
return
refresh_board()
func _on_board_response(success: bool, response: Dictionary, error_info: Dictionary, request_generation: int) -> void:
if request_generation != _current_account_generation():
return
_loading_board = false
if not success:
board_failed.emit(str(error_info.get("message", "任务书读取失败")))
return
_apply_board_response(response)
func _on_activity_response(success: bool, response: Dictionary, _error_info: Dictionary, request_generation: int) -> void:
if not success or request_generation != _current_account_generation():
return
_apply_board_response(response)
func _on_claim_response(success: bool, response: Dictionary, error_info: Dictionary, task_id: String, request_generation: int) -> void:
if request_generation != _current_account_generation():
return
if not success:
board_failed.emit(str(error_info.get("message", "奖励领取失败")))
return
var data_variant: Variant = response.get("data", {})
if not (data_variant is Dictionary):
board_failed.emit("任务奖励响应格式错误")
return
var data: Dictionary = data_variant as Dictionary
var board_variant: Variant = data.get("board", {})
if board_variant is Dictionary:
_apply_board(board_variant as Dictionary)
var wallet_variant: Variant = data.get("wallet", {})
if wallet_variant is Dictionary:
var wallet: Dictionary = wallet_variant as Dictionary
var player_state := get_node_or_null("/root/PlayerStateManager")
if player_state != null and player_state.has_method("apply_wallet"):
player_state.call("apply_wallet", wallet)
reward_claimed.emit(task_id, wallet.duplicate(true))
func _apply_board_response(response: Dictionary) -> void:
var data_variant: Variant = response.get("data", {})
if data_variant is Dictionary:
_apply_board(data_variant as Dictionary)
else:
board_failed.emit("任务书响应格式错误")
func _apply_board(board: Dictionary) -> void:
_board = board.duplicate(true)
board_changed.emit(get_board())
func _api_client() -> Node:
return get_node_or_null("/root/ApiClient")
func _is_authenticated() -> bool:
var auth_manager := get_node_or_null("/root/AuthManager")
return auth_manager != null and auth_manager.has_method("is_authenticated") and bool(auth_manager.call("is_authenticated"))
func _current_account_generation() -> int:
var auth_manager := get_node_or_null("/root/AuthManager")
return int(auth_manager.call("get_account_generation")) if auth_manager != null and auth_manager.has_method("get_account_generation") else 0

View File

@@ -0,0 +1 @@
uid://byurfu7d8qyfb

View File

@@ -35,6 +35,8 @@ SettingsManager="*res://_Core/managers/SettingsManager.gd"
NotificationSoundManager="*res://_Core/managers/NotificationSoundManager.gd" NotificationSoundManager="*res://_Core/managers/NotificationSoundManager.gd"
SocialManager="*res://_Core/managers/SocialManager.gd" SocialManager="*res://_Core/managers/SocialManager.gd"
InteractionManager="*res://_Core/managers/InteractionManager.gd" InteractionManager="*res://_Core/managers/InteractionManager.gd"
TaskManager="*res://_Core/managers/TaskManager.gd"
TaskBookPanel="*res://scenes/ui/TaskBookPanel.gd"
[display] [display]

View File

@@ -47,6 +47,12 @@ func _ready() -> void:
_connect_cafe_companion_events() _connect_cafe_companion_events()
_register_interactables() _register_interactables()
_discover_destination() _discover_destination()
_report_task_map_visit()
func _report_task_map_visit() -> void:
var taskManager := get_node_or_null("/root/TaskManager")
if taskManager != null and taskManager.has_method("report_activity"):
taskManager.call("report_activity", "map_visited", "whale_cafe")
func _discover_destination() -> void: func _discover_destination() -> void:
var destination_id := SceneManager.get_next_destination_id() var destination_id := SceneManager.get_next_destination_id()

View File

@@ -24,6 +24,12 @@ func _ready() -> void:
_configure_camera() _configure_camera()
_discover_destination() _discover_destination()
call_deferred("_show_registration_welcome") call_deferred("_show_registration_welcome")
_report_task_map_visit()
func _report_task_map_visit() -> void:
var taskManager := get_node_or_null("/root/TaskManager")
if taskManager != null and taskManager.has_method("report_activity"):
taskManager.call("report_activity", "map_visited", "square")
func _show_registration_welcome() -> void: func _show_registration_welcome() -> void:
var auth_manager: Node = get_node_or_null("/root/AuthManager") var auth_manager: Node = get_node_or_null("/root/AuthManager")

View File

@@ -39,6 +39,12 @@ func _ready() -> void:
EventSystem.connect_event(EventNames.OBJECT_INTERACTED, _on_object_interacted, self) EventSystem.connect_event(EventNames.OBJECT_INTERACTED, _on_object_interacted, self)
EventSystem.connect_event(EventNames.MALL_CLOSED, _on_mall_closed, self) EventSystem.connect_event(EventNames.MALL_CLOSED, _on_mall_closed, self)
_discover_destination() _discover_destination()
_report_task_map_visit()
func _report_task_map_visit() -> void:
var taskManager := get_node_or_null("/root/TaskManager")
if taskManager != null and taskManager.has_method("report_activity"):
taskManager.call("report_activity", "map_visited", "work_zone")
func _discover_destination() -> void: func _discover_destination() -> void:
var destination_id := SceneManager.get_next_destination_id() var destination_id := SceneManager.get_next_destination_id()
@@ -135,6 +141,11 @@ func _on_object_interacted(data: Dictionary) -> void:
if buildingId == "virtual_whale_recruitment_board": if buildingId == "virtual_whale_recruitment_board":
_open_course_board() _open_course_board()
return return
if buildingId == "whale_job_center":
var taskBook := get_node_or_null("/root/TaskBookPanel")
if taskBook != null and taskBook.has_method("show_panel"):
taskBook.call("show_panel")
return
if not [ if not [
"whale_job_center", "whale_job_center",
"ai_service_station", "ai_service_station",

View File

@@ -64,6 +64,9 @@ func _physics_process(_delta: float) -> void:
# 处理玩家交互,展示气泡并向全局事件系统广播。 # 处理玩家交互,展示气泡并向全局事件系统广播。
func interact() -> void: func interact() -> void:
var taskManager := get_node_or_null("/root/TaskManager")
if taskManager != null and taskManager.has_method("report_activity"):
taskManager.call("report_activity", "facility_interacted", "npc")
show_bubble(dialogue) show_bubble(dialogue)
var eventSystem: Node = get_node_or_null("/root/EventSystem") var eventSystem: Node = get_node_or_null("/root/EventSystem")
if eventSystem != null: if eventSystem != null:

View File

@@ -17,6 +17,10 @@ func _ready() -> void:
add_child(interactable) add_child(interactable)
func interact() -> void: func interact() -> void:
var taskManager := get_node_or_null("/root/TaskManager")
if taskManager != null and taskManager.has_method("report_activity"):
taskManager.call("report_activity", "notice_viewed")
taskManager.call("report_activity", "facility_interacted", "notice_board")
var root: Window = get_tree().root var root: Window = get_tree().root
if root.has_node(NOTICE_DIALOG_NAME): if root.has_node(NOTICE_DIALOG_NAME):
return return

View File

@@ -17,6 +17,9 @@ func _ready() -> void:
add_child(interactable) add_child(interactable)
func interact() -> void: func interact() -> void:
var taskManager := get_node_or_null("/root/TaskManager")
if taskManager != null and taskManager.has_method("report_activity"):
taskManager.call("report_activity", "facility_interacted", "welcome_board")
var root: Window = get_tree().root var root: Window = get_tree().root
if root.has_node(WELCOME_DIALOG_NAME): if root.has_node(WELCOME_DIALOG_NAME):
return return

View File

@@ -62,6 +62,9 @@ func show_panel() -> void:
mouse_filter = Control.MOUSE_FILTER_STOP mouse_filter = Control.MOUSE_FILTER_STOP
set_process(true) set_process(true)
_releaseMovementInputState() _releaseMovementInputState()
var taskManager := get_node_or_null("/root/TaskManager")
if taskManager != null and taskManager.has_method("report_activity"):
taskManager.call("report_activity", "course_board_opened")
_fetchCourses() _fetchCourses()
_animatePanel(true) _animatePanel(true)

View File

@@ -24,6 +24,8 @@ var _walletLabel: Label
var _avatarLabel: Label var _avatarLabel: Label
var _avatarPanel: PanelContainer var _avatarPanel: PanelContainer
var _walletRefreshTimer: Timer var _walletRefreshTimer: Timer
var _taskShortcutButton: Button
var _taskRewardBadge: Label
var _currentUsername: String = "玩家" var _currentUsername: String = "玩家"
var _walletBalance: int = 0 var _walletBalance: int = 0
@@ -59,6 +61,11 @@ func _exit_tree() -> void:
var callback := Callable(self, "_on_auth_state_changed") var callback := Callable(self, "_on_auth_state_changed")
if authManager.is_connected("auth_state_changed", callback): if authManager.is_connected("auth_state_changed", callback):
authManager.disconnect("auth_state_changed", callback) authManager.disconnect("auth_state_changed", callback)
var taskManager := get_node_or_null("/root/TaskManager")
if taskManager != null and taskManager.has_signal("board_changed"):
var taskCallback := Callable(self, "_on_task_board_changed")
if taskManager.is_connected("board_changed", taskCallback):
taskManager.disconnect("board_changed", taskCallback)
func _build_ui() -> void: func _build_ui() -> void:
var rootRow := HBoxContainer.new() var rootRow := HBoxContainer.new()
@@ -92,7 +99,24 @@ func _build_shortcut_bar() -> PanelContainer:
margin.add_child(row) margin.add_child(row)
row.add_child(_create_shortcut_button("map", "地图", _on_map_pressed)) row.add_child(_create_shortcut_button("map", "地图", _on_map_pressed))
row.add_child(_create_shortcut_button("task", "任务", _on_task_pressed)) _taskShortcutButton = _create_shortcut_button("task", "任务", _on_task_pressed)
_taskShortcutButton.tooltip_text = "打开任务书"
_taskRewardBadge = Label.new()
_taskRewardBadge.mouse_filter = Control.MOUSE_FILTER_IGNORE
_taskRewardBadge.visible = false
_taskRewardBadge.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_taskRewardBadge.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_taskRewardBadge.add_theme_color_override("font_color", Color.WHITE)
_taskRewardBadge.add_theme_font_size_override("font_size", 12)
_taskRewardBadge.add_theme_stylebox_override("normal", _create_pill_style(Color(0.92, 0.30, 0.24), 12))
_taskRewardBadge.set_anchors_preset(Control.PRESET_TOP_RIGHT)
_taskRewardBadge.offset_left = -20
_taskRewardBadge.offset_top = 1
_taskRewardBadge.offset_right = 4
_taskRewardBadge.offset_bottom = 25
_taskRewardBadge.z_index = 1
_taskShortcutButton.add_child(_taskRewardBadge)
row.add_child(_taskShortcutButton)
row.add_child(_create_shortcut_button("backpack", "背包", _on_backpack_pressed)) row.add_child(_create_shortcut_button("backpack", "背包", _on_backpack_pressed))
row.add_child(_create_shortcut_button("friends", "好友", _on_friends_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("activity", "通知", _on_notifications_pressed))
@@ -240,6 +264,15 @@ func _subscribe_to_events() -> void:
var callback := Callable(self, "_on_auth_state_changed") var callback := Callable(self, "_on_auth_state_changed")
if not authManager.is_connected("auth_state_changed", callback): if not authManager.is_connected("auth_state_changed", callback):
authManager.connect("auth_state_changed", callback) authManager.connect("auth_state_changed", callback)
var taskManager := get_node_or_null("/root/TaskManager")
if taskManager != null and taskManager.has_signal("board_changed"):
var taskCallback := Callable(self, "_on_task_board_changed")
if not taskManager.is_connected("board_changed", taskCallback):
taskManager.connect("board_changed", taskCallback)
if taskManager.has_method("get_board"):
var boardVariant: Variant = taskManager.call("get_board")
if boardVariant is Dictionary:
_on_task_board_changed(boardVariant as Dictionary)
func _load_current_user() -> void: func _load_current_user() -> void:
var authManager := get_node_or_null("/root/AuthManager") var authManager := get_node_or_null("/root/AuthManager")
@@ -277,6 +310,45 @@ func _apply_auth_user_payload(data: Dictionary) -> void:
func _on_auth_logout(_data: Variant = null) -> void: func _on_auth_logout(_data: Variant = null) -> void:
_set_username("玩家") _set_username("玩家")
_reset_wallet() _reset_wallet()
_on_task_board_changed({})
func _on_task_board_changed(board: Dictionary) -> void:
if not is_instance_valid(_taskShortcutButton):
return
var claimableCount: int = _count_claimable_task_rewards(board)
if claimableCount > 0:
_taskShortcutButton.tooltip_text = "任务书:有 %d 项奖励可领取" % claimableCount
_taskShortcutButton.modulate = Color(1.0, 0.855, 0.470, 1.0)
if is_instance_valid(_taskRewardBadge):
_taskRewardBadge.text = str(mini(claimableCount, 9)) if claimableCount < 10 else "9+"
_taskRewardBadge.visible = true
return
_taskShortcutButton.tooltip_text = "打开任务书"
_taskShortcutButton.modulate = Color.WHITE
if is_instance_valid(_taskRewardBadge):
_taskRewardBadge.visible = false
func _count_claimable_task_rewards(board: Dictionary) -> int:
var claimableCount: int = 0
claimableCount += _count_claimable_tasks(board.get("newbie_tasks", []))
claimableCount += _count_claimable_tasks(board.get("weekly_tasks", []))
var weeklyBonusVariant: Variant = board.get("weekly_bonus", {})
if weeklyBonusVariant is Dictionary:
var weeklyBonus: Dictionary = weeklyBonusVariant as Dictionary
if bool(weeklyBonus.get("claimable", false)):
claimableCount += 1
return claimableCount
func _count_claimable_tasks(tasksVariant: Variant) -> int:
if not (tasksVariant is Array):
return 0
var claimableCount: int = 0
for taskVariant: Variant in tasksVariant as Array:
if taskVariant is Dictionary:
var task: Dictionary = taskVariant as Dictionary
if bool(task.get("claimable", false)):
claimableCount += 1
return claimableCount
func _on_appearance_avatar_changed(_data: Dictionary) -> void: func _on_appearance_avatar_changed(_data: Dictionary) -> void:
_apply_current_avatar() _apply_current_avatar()
@@ -386,7 +458,9 @@ func _on_notifications_pressed() -> void:
socialManager.call("toggle_notifications") socialManager.call("toggle_notifications")
func _on_task_pressed() -> void: func _on_task_pressed() -> void:
_emit_status_message("任务入口稍后接入") var eventSystem := _get_event_system()
if eventSystem != null:
eventSystem.call("emit_event", EventNames.HUD_TASK_BOOK_REQUESTED, {})
func _on_backpack_pressed() -> void: func _on_backpack_pressed() -> void:
var eventSystem := _get_event_system() var eventSystem := _get_event_system()

337
scenes/ui/TaskBookPanel.gd Normal file
View File

@@ -0,0 +1,337 @@
extends CanvasLayer
const PANEL_SIZE := Vector2(980, 860)
const TEXT_COLOR := Color(0.10, 0.18, 0.27)
const MUTED_COLOR := Color(0.38, 0.48, 0.58)
const ACCENT_COLOR := Color(0.10, 0.49, 0.85)
const COIN_COLOR := Color(0.94, 0.58, 0.08)
const WELCOME_DIALOG_SCENE: PackedScene = preload("res://scenes/ui/welcome_dialog.tscn")
const WELCOME_DIALOG_NAME := "WelcomeDialog"
var _root: Control
var _panel: PanelContainer
var _content: VBoxContainer
var _status_label: Label
var _is_open: bool = false
func _ready() -> void:
layer = 32
add_to_group("whaletown_escape_dismissible")
_build_ui()
_connect_services()
func _exit_tree() -> void:
var event_system := get_node_or_null("/root/EventSystem")
if event_system != null:
event_system.call("disconnect_event", EventNames.HUD_TASK_BOOK_REQUESTED, _on_task_book_requested, self)
var task_manager := get_node_or_null("/root/TaskManager")
if task_manager != null:
var board_callback := Callable(self, "_on_board_changed")
if task_manager.has_signal("board_changed") and task_manager.is_connected("board_changed", board_callback):
task_manager.disconnect("board_changed", board_callback)
var failed_callback := Callable(self, "_on_board_failed")
if task_manager.has_signal("board_failed") and task_manager.is_connected("board_failed", failed_callback):
task_manager.disconnect("board_failed", failed_callback)
func show_panel() -> void:
_is_open = true
_root.visible = true
var task_manager := get_node_or_null("/root/TaskManager")
if task_manager != null:
if task_manager.has_method("is_loaded") and bool(task_manager.call("is_loaded")):
var board_variant: Variant = task_manager.call("get_board")
if board_variant is Dictionary:
_render_board(board_variant as Dictionary)
if task_manager.has_method("refresh_board"):
task_manager.call("refresh_board")
func hide_panel() -> void:
_is_open = false
_root.visible = false
func is_escape_dismissible() -> bool:
return _is_open
func get_escape_priority() -> int:
return 760
func request_escape_close() -> void:
hide_panel()
func _input(event: InputEvent) -> void:
if not _is_open or get_viewport().is_input_handled():
return
if event.is_action_pressed("ui_cancel"):
hide_panel()
get_viewport().set_input_as_handled()
func _connect_services() -> void:
var event_system := get_node_or_null("/root/EventSystem")
if event_system != null:
event_system.call("connect_event", EventNames.HUD_TASK_BOOK_REQUESTED, _on_task_book_requested, self)
var task_manager := get_node_or_null("/root/TaskManager")
if task_manager != null:
if task_manager.has_signal("board_changed"):
task_manager.connect("board_changed", _on_board_changed)
if task_manager.has_signal("board_failed"):
task_manager.connect("board_failed", _on_board_failed)
func _build_ui() -> void:
_root = Control.new()
_root.name = "TaskBookRoot"
_root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_root.visible = false
add_child(_root)
var dim := ColorRect.new()
dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
dim.color = Color(0.02, 0.05, 0.09, 0.68)
dim.mouse_filter = Control.MOUSE_FILTER_STOP
dim.gui_input.connect(func(event: InputEvent) -> void:
if event is InputEventMouseButton and (event as InputEventMouseButton).pressed:
hide_panel()
)
_root.add_child(dim)
_panel = PanelContainer.new()
_panel.custom_minimum_size = PANEL_SIZE
_panel.set_anchors_preset(Control.PRESET_CENTER)
_panel.offset_left = -PANEL_SIZE.x * 0.5
_panel.offset_top = -PANEL_SIZE.y * 0.5
_panel.offset_right = PANEL_SIZE.x * 0.5
_panel.offset_bottom = PANEL_SIZE.y * 0.5
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
_panel.add_theme_stylebox_override("panel", _panel_style(Color(0.96, 0.985, 1.0, 1.0), 28))
_root.add_child(_panel)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 28)
margin.add_theme_constant_override("margin_top", 24)
margin.add_theme_constant_override("margin_right", 28)
margin.add_theme_constant_override("margin_bottom", 24)
_panel.add_child(margin)
var layout := VBoxContainer.new()
layout.add_theme_constant_override("separation", 16)
margin.add_child(layout)
layout.add_child(_build_header())
_status_label = Label.new()
_status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
_status_label.add_theme_color_override("font_color", MUTED_COLOR)
_status_label.add_theme_font_size_override("font_size", 15)
layout.add_child(_status_label)
var scroll := ScrollContainer.new()
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
layout.add_child(scroll)
_content = VBoxContainer.new()
_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_content.add_theme_constant_override("separation", 12)
scroll.add_child(_content)
var footer := Label.new()
footer.text = "[Esc] 关闭任务书"
footer.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
footer.add_theme_color_override("font_color", MUTED_COLOR)
footer.add_theme_font_size_override("font_size", 15)
layout.add_child(footer)
func _build_header() -> Control:
var header := PanelContainer.new()
header.custom_minimum_size = Vector2(0, 96)
header.add_theme_stylebox_override("panel", _panel_style(ACCENT_COLOR, 20))
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 24)
margin.add_theme_constant_override("margin_top", 14)
margin.add_theme_constant_override("margin_right", 16)
margin.add_theme_constant_override("margin_bottom", 14)
header.add_child(margin)
var row := HBoxContainer.new()
row.alignment = BoxContainer.ALIGNMENT_CENTER
margin.add_child(row)
var title_box := VBoxContainer.new()
title_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(title_box)
var title := Label.new()
title.text = "鲸镇任务书"
title.add_theme_color_override("font_color", Color.WHITE)
title.add_theme_font_size_override("font_size", 30)
title_box.add_child(title)
var subtitle := Label.new()
subtitle.text = "完成探索、学习与社交任务,领取鲸币奖励"
subtitle.add_theme_color_override("font_color", Color(0.88, 0.95, 1.0, 1.0))
subtitle.add_theme_font_size_override("font_size", 16)
title_box.add_child(subtitle)
var guide := Button.new()
guide.text = "新人引导"
guide.focus_mode = Control.FOCUS_NONE
guide.custom_minimum_size = Vector2(104, 42)
guide.add_theme_stylebox_override("normal", _panel_style(Color(1, 1, 1, 0.18), 14))
guide.add_theme_stylebox_override("hover", _panel_style(Color(1, 1, 1, 0.30), 14))
guide.add_theme_color_override("font_color", Color.WHITE)
guide.pressed.connect(_on_guide_pressed)
row.add_child(guide)
var close := Button.new()
close.text = "关闭"
close.focus_mode = Control.FOCUS_NONE
close.custom_minimum_size = Vector2(84, 42)
close.add_theme_stylebox_override("normal", _panel_style(Color(1, 1, 1, 0.18), 14))
close.add_theme_stylebox_override("hover", _panel_style(Color(1, 1, 1, 0.30), 14))
close.add_theme_color_override("font_color", Color.WHITE)
close.pressed.connect(hide_panel)
row.add_child(close)
return header
func _on_task_book_requested(_data: Dictionary = {}) -> void:
show_panel()
func _on_guide_pressed() -> void:
hide_panel()
var root: Window = get_tree().root
var dialog: Node = root.get_node_or_null(WELCOME_DIALOG_NAME)
if dialog == null:
dialog = WELCOME_DIALOG_SCENE.instantiate()
dialog.name = WELCOME_DIALOG_NAME
root.add_child(dialog)
if dialog.has_method("_showGuide"):
dialog.call_deferred("_showGuide")
func _on_board_changed(board: Dictionary) -> void:
if _is_open:
_render_board(board)
func _on_board_failed(message: String) -> void:
if _is_open and is_instance_valid(_status_label):
_status_label.text = message
_status_label.add_theme_color_override("font_color", Color(0.86, 0.28, 0.24))
func _render_board(board: Dictionary) -> void:
_clear_content()
if board.is_empty():
_status_label.text = "正在读取任务书…"
return
_status_label.add_theme_color_override("font_color", MUTED_COLOR)
var cycle_variant: Variant = board.get("weekly_cycle", {})
var cycle: Dictionary = cycle_variant as Dictionary if cycle_variant is Dictionary else {}
_status_label.text = "周常重置:%s" % _format_reset(str(cycle.get("ends_at", "")))
_add_section("新人主线", "可自由完成,首次购买皮肤为可跳过任务")
_add_tasks(board.get("newbie_tasks", []))
_add_section("本周任务", "四项全部完成后可领取额外 200 鲸币")
_add_tasks(board.get("weekly_tasks", []))
var bonus_variant: Variant = board.get("weekly_bonus", {})
if bonus_variant is Dictionary:
_content.add_child(_build_task_card(bonus_variant as Dictionary))
func _add_section(title_text: String, description: String) -> void:
var section := VBoxContainer.new()
section.add_theme_constant_override("separation", 2)
var title := Label.new()
title.text = title_text
title.add_theme_color_override("font_color", TEXT_COLOR)
title.add_theme_font_size_override("font_size", 23)
section.add_child(title)
var subtitle := Label.new()
subtitle.text = description
subtitle.add_theme_color_override("font_color", MUTED_COLOR)
subtitle.add_theme_font_size_override("font_size", 15)
section.add_child(subtitle)
_content.add_child(section)
func _add_tasks(tasks_variant: Variant) -> void:
if not (tasks_variant is Array):
return
for task_variant: Variant in tasks_variant as Array:
if task_variant is Dictionary:
_content.add_child(_build_task_card(task_variant as Dictionary))
func _build_task_card(task: Dictionary) -> Control:
var completed: bool = bool(task.get("completed", false))
var claimed: bool = bool(task.get("claimed", false))
var optional: bool = bool(task.get("optional", false))
var card := PanelContainer.new()
card.add_theme_stylebox_override("panel", _panel_style(Color(0.90, 0.96, 1.0, 0.92) if completed else Color.WHITE, 18))
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 18)
margin.add_theme_constant_override("margin_top", 14)
margin.add_theme_constant_override("margin_right", 18)
margin.add_theme_constant_override("margin_bottom", 14)
card.add_child(margin)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 14)
margin.add_child(row)
var state := Label.new()
state.custom_minimum_size = Vector2(30, 0)
state.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
state.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
state.text = "" if claimed else ("" if completed else "")
state.add_theme_color_override("font_color", Color(0.17, 0.66, 0.41) if completed else MUTED_COLOR)
state.add_theme_font_size_override("font_size", 24)
row.add_child(state)
var detail := VBoxContainer.new()
detail.size_flags_horizontal = Control.SIZE_EXPAND_FILL
detail.add_theme_constant_override("separation", 4)
row.add_child(detail)
var title := Label.new()
title.text = "%s%s" % [str(task.get("title", "任务")), "(可跳过)" if optional else ""]
title.add_theme_color_override("font_color", TEXT_COLOR)
title.add_theme_font_size_override("font_size", 19)
detail.add_child(title)
var description := Label.new()
description.text = str(task.get("description", ""))
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
description.add_theme_color_override("font_color", MUTED_COLOR)
description.add_theme_font_size_override("font_size", 15)
detail.add_child(description)
var progress: int = int(task.get("progress", 0))
var target: int = max(1, int(task.get("target", 1)))
var progress_label := Label.new()
progress_label.text = "进度 %d / %d" % [min(progress, target), target]
progress_label.add_theme_color_override("font_color", ACCENT_COLOR)
progress_label.add_theme_font_size_override("font_size", 14)
detail.add_child(progress_label)
var reward := Label.new()
reward.text = "+%d 鲸币" % int(task.get("reward", 0))
reward.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
reward.add_theme_color_override("font_color", COIN_COLOR)
reward.add_theme_font_size_override("font_size", 17)
row.add_child(reward)
var action := Button.new()
action.focus_mode = Control.FOCUS_NONE
action.custom_minimum_size = Vector2(88, 42)
if claimed:
action.text = "已领取"
action.disabled = true
elif completed:
action.text = "领取"
action.pressed.connect(func() -> void:
var task_manager := get_node_or_null("/root/TaskManager")
if task_manager != null and task_manager.has_method("claim_task"):
task_manager.call("claim_task", str(task.get("id", "")))
)
else:
action.text = "进行中"
action.disabled = true
action.add_theme_stylebox_override("normal", _panel_style(ACCENT_COLOR if completed and not claimed else Color(0.80, 0.85, 0.89), 12))
action.add_theme_stylebox_override("hover", _panel_style(Color(0.06, 0.39, 0.71), 12))
action.add_theme_color_override("font_color", Color.WHITE)
row.add_child(action)
return card
func _clear_content() -> void:
for child: Node in _content.get_children():
child.queue_free()
func _format_reset(value: String) -> String:
if value.is_empty():
return "下周一 00:00"
return value.replace("T", " ").left(16)
func _panel_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.shadow_color = Color(0.04, 0.13, 0.22, 0.24)
style.shadow_size = 16
style.shadow_offset = Vector2(0, 6)
return style

View File

@@ -0,0 +1 @@
uid://d2b7h12k8nqoi

View File

@@ -34,6 +34,7 @@ var _guideFooter: HBoxContainer
var _guidePrevButton: Button var _guidePrevButton: Button
var _guideNextButton: Button var _guideNextButton: Button
var _guideDotsContainer: HBoxContainer var _guideDotsContainer: HBoxContainer
var _guideTaskBookButton: Button
var _guideCurrentPage: int = 0 var _guideCurrentPage: int = 0
var _guideTween: Tween var _guideTween: Tween
@@ -75,6 +76,9 @@ func _onStartPressed() -> void:
_showGuide() _showGuide()
func _showGuide() -> void: func _showGuide() -> void:
var taskManager := get_node_or_null("/root/TaskManager")
if taskManager != null and taskManager.has_method("report_activity"):
taskManager.call("report_activity", "guide_opened")
panelContainer.custom_minimum_size = Vector2(720, 560) panelContainer.custom_minimum_size = Vector2(720, 560)
titleLabel.text = "新人引导手册" titleLabel.text = "新人引导手册"
logoContainer.visible = false logoContainer.visible = false
@@ -162,6 +166,12 @@ func _buildGuideUi() -> void:
_guideNextButton.pressed.connect(_onGuideNextPressed) _guideNextButton.pressed.connect(_onGuideNextPressed)
_guideFooter.add_child(_guideNextButton) _guideFooter.add_child(_guideNextButton)
_guideTaskBookButton = _createGuideButton("任务书")
_guideTaskBookButton.name = "TaskBookButton"
_guideTaskBookButton.custom_minimum_size = Vector2(96, 44)
_guideTaskBookButton.pressed.connect(_onGuideTaskBookPressed)
_guideFooter.add_child(_guideTaskBookButton)
func _createGuideButton(text: String) -> Button: func _createGuideButton(text: String) -> Button:
var button := Button.new() var button := Button.new()
button.custom_minimum_size = Vector2(56, 44) button.custom_minimum_size = Vector2(56, 44)
@@ -285,6 +295,12 @@ func _onGuideNextPressed() -> void:
_guideCurrentPage += 1 _guideCurrentPage += 1
_updateGuideUi() _updateGuideUi()
func _onGuideTaskBookPressed() -> void:
var taskBook := get_node_or_null("/root/TaskBookPanel")
if taskBook != null and taskBook.has_method("show_panel"):
taskBook.call("show_panel")
queue_free()
func _disableChatUiMouseInput() -> void: func _disableChatUiMouseInput() -> void:
var currentScene := get_tree().current_scene var currentScene := get_tree().current_scene
if currentScene != null: if currentScene != null: