diff --git a/_Core/EventNames.gd b/_Core/EventNames.gd index 873a292..5926a77 100644 --- a/_Core/EventNames.gd +++ b/_Core/EventNames.gd @@ -107,6 +107,7 @@ const HUD_FRIEND_LIST_TOGGLE = "hud_friend_list_toggle" const HUD_SETTINGS_REQUESTED = "hud_settings_requested" const HUD_BACKPACK_TOGGLE = "hud_backpack_toggle" const HUD_MAP_TOGGLE = "hud_map_toggle" +const HUD_TASK_BOOK_REQUESTED = "hud_task_book_requested" # ============================================================================ # 商城事件 diff --git a/_Core/managers/TaskManager.gd b/_Core/managers/TaskManager.gd new file mode 100644 index 0000000..92108bf --- /dev/null +++ b/_Core/managers/TaskManager.gd @@ -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 diff --git a/_Core/managers/TaskManager.gd.uid b/_Core/managers/TaskManager.gd.uid new file mode 100644 index 0000000..68e09bd --- /dev/null +++ b/_Core/managers/TaskManager.gd.uid @@ -0,0 +1 @@ +uid://byurfu7d8qyfb diff --git a/project.godot b/project.godot index 59c2aba..b91bd68 100644 --- a/project.godot +++ b/project.godot @@ -35,6 +35,8 @@ SettingsManager="*res://_Core/managers/SettingsManager.gd" NotificationSoundManager="*res://_Core/managers/NotificationSoundManager.gd" SocialManager="*res://_Core/managers/SocialManager.gd" InteractionManager="*res://_Core/managers/InteractionManager.gd" +TaskManager="*res://_Core/managers/TaskManager.gd" +TaskBookPanel="*res://scenes/ui/TaskBookPanel.gd" [display] diff --git a/scenes/Maps/CafeInterior.gd b/scenes/Maps/CafeInterior.gd index 1615300..6e68a99 100644 --- a/scenes/Maps/CafeInterior.gd +++ b/scenes/Maps/CafeInterior.gd @@ -47,6 +47,12 @@ func _ready() -> void: _connect_cafe_companion_events() _register_interactables() _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: var destination_id := SceneManager.get_next_destination_id() diff --git a/scenes/Maps/Square.gd b/scenes/Maps/Square.gd index b73c233..d1424b8 100644 --- a/scenes/Maps/Square.gd +++ b/scenes/Maps/Square.gd @@ -24,6 +24,12 @@ func _ready() -> void: _configure_camera() _discover_destination() 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: var auth_manager: Node = get_node_or_null("/root/AuthManager") diff --git a/scenes/Maps/WorkZone.gd b/scenes/Maps/WorkZone.gd index dedf552..d7c1d98 100644 --- a/scenes/Maps/WorkZone.gd +++ b/scenes/Maps/WorkZone.gd @@ -39,6 +39,12 @@ func _ready() -> void: EventSystem.connect_event(EventNames.OBJECT_INTERACTED, _on_object_interacted, self) EventSystem.connect_event(EventNames.MALL_CLOSED, _on_mall_closed, self) _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: 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": _open_course_board() 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 [ "whale_job_center", "ai_service_station", diff --git a/scenes/characters/NPCController.gd b/scenes/characters/NPCController.gd index 78048d8..3e748ef 100644 --- a/scenes/characters/NPCController.gd +++ b/scenes/characters/NPCController.gd @@ -64,6 +64,9 @@ func _physics_process(_delta: float) -> 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) var eventSystem: Node = get_node_or_null("/root/EventSystem") if eventSystem != null: diff --git a/scenes/prefabs/items/NoticeBoard.gd b/scenes/prefabs/items/NoticeBoard.gd index ff7c7bd..d6c881c 100644 --- a/scenes/prefabs/items/NoticeBoard.gd +++ b/scenes/prefabs/items/NoticeBoard.gd @@ -17,6 +17,10 @@ func _ready() -> void: add_child(interactable) 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 if root.has_node(NOTICE_DIALOG_NAME): return diff --git a/scenes/prefabs/items/WelcomeBoard.gd b/scenes/prefabs/items/WelcomeBoard.gd index 09176d5..1cd1224 100644 --- a/scenes/prefabs/items/WelcomeBoard.gd +++ b/scenes/prefabs/items/WelcomeBoard.gd @@ -17,6 +17,9 @@ func _ready() -> void: add_child(interactable) 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 if root.has_node(WELCOME_DIALOG_NAME): return diff --git a/scenes/ui/CourseBoardPanel.gd b/scenes/ui/CourseBoardPanel.gd index 3aa22d1..ea096c5 100644 --- a/scenes/ui/CourseBoardPanel.gd +++ b/scenes/ui/CourseBoardPanel.gd @@ -62,6 +62,9 @@ func show_panel() -> void: mouse_filter = Control.MOUSE_FILTER_STOP set_process(true) _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() _animatePanel(true) diff --git a/scenes/ui/PlayerHud.gd b/scenes/ui/PlayerHud.gd index 9f213f5..b243cda 100644 --- a/scenes/ui/PlayerHud.gd +++ b/scenes/ui/PlayerHud.gd @@ -24,6 +24,8 @@ var _walletLabel: Label var _avatarLabel: Label var _avatarPanel: PanelContainer var _walletRefreshTimer: Timer +var _taskShortcutButton: Button +var _taskRewardBadge: Label var _currentUsername: String = "玩家" var _walletBalance: int = 0 @@ -59,6 +61,11 @@ func _exit_tree() -> void: var callback := Callable(self, "_on_auth_state_changed") if authManager.is_connected("auth_state_changed", callback): authManager.disconnect("auth_state_changed", callback) + var taskManager := get_node_or_null("/root/TaskManager") + if taskManager != null and taskManager.has_signal("board_changed"): + var taskCallback := Callable(self, "_on_task_board_changed") + if taskManager.is_connected("board_changed", taskCallback): + taskManager.disconnect("board_changed", taskCallback) func _build_ui() -> void: var rootRow := HBoxContainer.new() @@ -92,7 +99,24 @@ func _build_shortcut_bar() -> PanelContainer: margin.add_child(row) 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("friends", "好友", _on_friends_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") if not authManager.is_connected("auth_state_changed", callback): authManager.connect("auth_state_changed", callback) + var taskManager := get_node_or_null("/root/TaskManager") + if taskManager != null and taskManager.has_signal("board_changed"): + var taskCallback := Callable(self, "_on_task_board_changed") + if not taskManager.is_connected("board_changed", taskCallback): + taskManager.connect("board_changed", taskCallback) + if taskManager.has_method("get_board"): + var boardVariant: Variant = taskManager.call("get_board") + if boardVariant is Dictionary: + _on_task_board_changed(boardVariant as Dictionary) func _load_current_user() -> void: var authManager := get_node_or_null("/root/AuthManager") @@ -277,6 +310,45 @@ func _apply_auth_user_payload(data: Dictionary) -> void: func _on_auth_logout(_data: Variant = null) -> void: _set_username("玩家") _reset_wallet() + _on_task_board_changed({}) + +func _on_task_board_changed(board: Dictionary) -> void: + if not is_instance_valid(_taskShortcutButton): + return + var claimableCount: int = _count_claimable_task_rewards(board) + if claimableCount > 0: + _taskShortcutButton.tooltip_text = "任务书:有 %d 项奖励可领取" % claimableCount + _taskShortcutButton.modulate = Color(1.0, 0.855, 0.470, 1.0) + if is_instance_valid(_taskRewardBadge): + _taskRewardBadge.text = str(mini(claimableCount, 9)) if claimableCount < 10 else "9+" + _taskRewardBadge.visible = true + return + _taskShortcutButton.tooltip_text = "打开任务书" + _taskShortcutButton.modulate = Color.WHITE + if is_instance_valid(_taskRewardBadge): + _taskRewardBadge.visible = false + +func _count_claimable_task_rewards(board: Dictionary) -> int: + var claimableCount: int = 0 + claimableCount += _count_claimable_tasks(board.get("newbie_tasks", [])) + claimableCount += _count_claimable_tasks(board.get("weekly_tasks", [])) + var weeklyBonusVariant: Variant = board.get("weekly_bonus", {}) + if weeklyBonusVariant is Dictionary: + var weeklyBonus: Dictionary = weeklyBonusVariant as Dictionary + if bool(weeklyBonus.get("claimable", false)): + claimableCount += 1 + return claimableCount + +func _count_claimable_tasks(tasksVariant: Variant) -> int: + if not (tasksVariant is Array): + return 0 + var claimableCount: int = 0 + for taskVariant: Variant in tasksVariant as Array: + if taskVariant is Dictionary: + var task: Dictionary = taskVariant as Dictionary + if bool(task.get("claimable", false)): + claimableCount += 1 + return claimableCount func _on_appearance_avatar_changed(_data: Dictionary) -> void: _apply_current_avatar() @@ -386,7 +458,9 @@ func _on_notifications_pressed() -> void: socialManager.call("toggle_notifications") 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: var eventSystem := _get_event_system() diff --git a/scenes/ui/TaskBookPanel.gd b/scenes/ui/TaskBookPanel.gd new file mode 100644 index 0000000..f19cff7 --- /dev/null +++ b/scenes/ui/TaskBookPanel.gd @@ -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 diff --git a/scenes/ui/TaskBookPanel.gd.uid b/scenes/ui/TaskBookPanel.gd.uid new file mode 100644 index 0000000..4a2f736 --- /dev/null +++ b/scenes/ui/TaskBookPanel.gd.uid @@ -0,0 +1 @@ +uid://d2b7h12k8nqoi diff --git a/scenes/ui/WelcomeDialog.gd b/scenes/ui/WelcomeDialog.gd index 7b42a84..8c0d1e7 100644 --- a/scenes/ui/WelcomeDialog.gd +++ b/scenes/ui/WelcomeDialog.gd @@ -34,6 +34,7 @@ var _guideFooter: HBoxContainer var _guidePrevButton: Button var _guideNextButton: Button var _guideDotsContainer: HBoxContainer +var _guideTaskBookButton: Button var _guideCurrentPage: int = 0 var _guideTween: Tween @@ -75,6 +76,9 @@ func _onStartPressed() -> void: _showGuide() 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) titleLabel.text = "新人引导手册" logoContainer.visible = false @@ -162,6 +166,12 @@ func _buildGuideUi() -> void: _guideNextButton.pressed.connect(_onGuideNextPressed) _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: var button := Button.new() button.custom_minimum_size = Vector2(56, 44) @@ -285,6 +295,12 @@ func _onGuideNextPressed() -> void: _guideCurrentPage += 1 _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: var currentScene := get_tree().current_scene if currentScene != null: