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