extends Control # ============================================================================ # AuthScene.gd - V2 登录与角色自定义入口 # ============================================================================ # 使用 V1 登录页背景和鲸鱼登录框作为主视觉,保留 V2 认证流程,并在登录框内 # 注册时提供头像上传、8x4 自定义角色皮肤上传、官方皮肤切换和角色工坊入口。 # ============================================================================ const BG_TEXTURE_PATH: String = "res://assets/ui/auth/v1/bg_auth_scene.png" const LOGIN_PANEL_TEXTURE_PATH: String = "res://assets/ui/auth/redesign/whaletown_login_panel_hd.png" const REGISTER_PANEL_TEXTURE_PATH: String = "res://assets/ui/auth/redesign/whaletown_register_panel_compact_hd.png" const REGISTRATION_CHOICE_ASSET_DIR: String = "res://assets/ui/auth/registration_choice/redesign" const REGISTRATION_CHOICE_MAIN_PANEL_PATH: String = REGISTRATION_CHOICE_ASSET_DIR + "/main_panel.png" const REGISTRATION_CHOICE_WHALE_PATH: String = REGISTRATION_CHOICE_ASSET_DIR + "/top_whale.png" const REGISTRATION_CHOICE_CARD_PATH: String = REGISTRATION_CHOICE_ASSET_DIR + "/card_base.png" const REGISTRATION_CHOICE_CARD_SELECTED_PATH: String = REGISTRATION_CHOICE_ASSET_DIR + "/card_selected.png" const REGISTRATION_CHOICE_HEADER_PILL_PATH: String = REGISTRATION_CHOICE_ASSET_DIR + "/header_pill.png" const REGISTRATION_CHOICE_BUTTON_PATH: String = REGISTRATION_CHOICE_ASSET_DIR + "/primary_button.png" const REGISTRATION_CHOICE_FOOTER_BAR_PATH: String = REGISTRATION_CHOICE_ASSET_DIR + "/footer_bar.png" const REGISTRATION_CHOICE_SKIN_SLOT_PATH: String = REGISTRATION_CHOICE_ASSET_DIR + "/skin_slot_base.png" const REGISTRATION_CHOICE_SKIN_SLOT_SELECTED_PATH: String = REGISTRATION_CHOICE_ASSET_DIR + "/skin_slot_selected.png" const REGISTRATION_CHOICE_UPLOAD_FILE_BOX_PATH: String = REGISTRATION_CHOICE_ASSET_DIR + "/upload_file_box.png" const REGISTRATION_CHOICE_SPRITESHEET_GRID_BOX_PATH: String = REGISTRATION_CHOICE_ASSET_DIR + "/spritesheet_grid_box.png" const REGISTRATION_CHOICE_REFERENCE_UPLOAD_BOX_PATH: String = REGISTRATION_CHOICE_ASSET_DIR + "/reference_upload_box.png" const REGISTRATION_CHOICE_IMAGE_PLACEHOLDER_BOX_PATH: String = REGISTRATION_CHOICE_ASSET_DIR + "/image_placeholder_box.png" const UI_FONT_PATH: String = "res://assets/fonts/msyh.ttc" const SKIN_GENERATION_CREATE_ENDPOINT: String = "/api/skin-generation/jobs" const SKIN_GENERATION_POLL_ENDPOINT_TEMPLATE: String = "/api/skin-generation/jobs/%s" const SKIN_GENERATION_POLL_INTERVAL: float = 2.0 const SKIN_GENERATION_REQUEST_TIMEOUT: float = 24.0 const AVATAR_NATIVE_FILE_FILTERS: Array[String] = ["*.png,*.jpg,*.jpeg,*.webp"] const SKIN_NATIVE_FILE_FILTERS: Array[String] = ["*.png,*.jpg,*.jpeg,*.webp"] const TEXT_COLOR: Color = Color(0.03, 0.12, 0.23, 1.0) const MUTED_COLOR: Color = Color(0.26, 0.34, 0.44, 1.0) const ACCENT_COLOR: Color = Color(0.10, 0.38, 0.68, 1.0) const MAIN_PREVIEW_HEIGHT: float = 160.0 const SKIN_THUMB_HEIGHT: float = 164.0 const SKIN_LIBRARY_MAX_VISIBLE: int = 3 const REGISTRATION_OFFICIAL_SKIN_IDS: Array[String] = ["classic_whale", "human_whale_directional_v2_8x4", "girl_sailor_turnaround_v2_8x4"] const AUTH_STAGE_SIZE: Vector2 = Vector2(1320, 760) const AUTH_STAGE_VIEWPORT_RATIO: float = 0.92 const LOGIN_FRAME_RECT: Rect2 = Rect2(270, 40, 780, 680) const REGISTER_FRAME_RECT: Rect2 = Rect2(150, 15, 1020, 727) const LOGIN_FORM_RECT: Rect2 = Rect2(432, 210, 430, 430) const REGISTER_FORM_RECT: Rect2 = Rect2(292, 155, 446, 500) @onready var whale_frame: TextureRect = %WhaleFrame @onready var login_panel: PanelContainer = %LoginPanel @onready var register_panel: PanelContainer = %RegisterPanel @onready var status_label: Label = %StatusLabel @onready var login_identifier_input: LineEdit = %LoginIdentifierInput @onready var login_password_input: LineEdit = %LoginPasswordInput @onready var login_button: Button = %LoginButton @onready var show_register_button: Button = %ShowRegisterButton @onready var register_username_input: LineEdit = %RegisterUsernameInput @onready var register_email_input: LineEdit = %RegisterEmailInput @onready var send_register_code_button: Button = %SendRegisterCodeButton @onready var register_verification_code_input: LineEdit = %RegisterVerificationCodeInput @onready var register_password_input: LineEdit = %RegisterPasswordInput @onready var register_confirm_input: LineEdit = %RegisterConfirmInput @onready var register_button: Button = %RegisterButton @onready var show_login_button: Button = %ShowLoginButton @onready var registration_character_area: Control = %RegistrationCharacterArea @onready var character_sprite: Sprite2D = %CharacterSprite @onready var skin_library_scroll: ScrollContainer = %SkinLibraryScroll @onready var skin_library_row: HBoxContainer = %SkinLibraryRow @onready var skin_library_prev_button: Button = %SkinLibraryPrevButton @onready var skin_library_next_button: Button = %SkinLibraryNextButton @onready var avatar_preview: PanelContainer = %AvatarPreview @onready var avatar_label: Label = %AvatarLabel @onready var workshop_button: Button = %WorkshopButton @onready var upload_avatar_button: Button = %UploadAvatarButton @onready var upload_skin_button: Button = %UploadSkinButton @onready var enter_with_default_skin_button: Button = %EnterWithDefaultSkinButton @onready var default_skin_hint_label: Label = %DefaultSkinHintLabel @onready var custom_skin_hint_label: Label = %CustomSkinHintLabel @onready var skin_workshop_overlay: Control = %SkinWorkshopOverlay @onready var workshop_preview_sprite: Sprite2D = %WorkshopPreviewSprite @onready var workshop_skin_name_input: LineEdit = %WorkshopSkinNameInput @onready var workshop_status_label: Label = %WorkshopStatusLabel @onready var workshop_source_button: Button = %WorkshopSourceButton @onready var workshop_source_label: Label = %WorkshopSourceLabel @onready var workshop_generate_button: Button = %WorkshopGenerateButton var _is_submitting: bool = false var _auth_manager: Node var _chat_manager: Node var _scene_manager: Node var _appearance_manager: Node var _ui_font: FontFile var _brand_font: SystemFont var _choice_font: SystemFont var _resuming_cached_session: bool = false var _cached_resume_start_generation: int = 0 var _skin_generation_active: bool = false var _skin_generation_job_id: String = "" var _skin_generation_poll_elapsed: float = 0.0 var _skin_generation_poll_in_flight: bool = false var _registration_avatar_texture: Texture2D var _registration_skin_texture: Texture2D var _registration_custom_skin_active: bool = false var _registration_selected_skin_id: String = "" var _registration_official_skins: Array = [] var _registration_official_skin_index: int = 0 var _registration_avatar_source_path: String = "" var _registration_skin_source_path: String = "" var _workshop_source_image_path: String = "" var _awaiting_registration_skin_generation: bool = false var _awaiting_registration_skin_choice: bool = false var _pending_profile_sync_after_auth: bool = false var _deferred_enter_square_after_profile_sync: bool = false var _is_sending_register_code: bool = false func _enter_tree() -> void: _ui_font = load(UI_FONT_PATH) as FontFile _brand_font = SystemFont.new() _brand_font.font_names = PackedStringArray(["Arial Rounded MT Bold", "Avenir Next", "Trebuchet MS", "Arial"]) _choice_font = SystemFont.new() _choice_font.font_names = PackedStringArray(["STHeiti Medium", "Hiragino Sans GB", "PingFang SC", "Microsoft YaHei"]) _build_v1_auth_ui() func _build_v1_auth_ui() -> void: if get_node_or_null("BackgroundImage") != null: return var background := TextureRect.new() background.name = "BackgroundImage" background.texture = load(BG_TEXTURE_PATH) as Texture2D background.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST background.expand_mode = TextureRect.EXPAND_IGNORE_SIZE background.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED background.set_anchors_preset(Control.PRESET_FULL_RECT) add_child(background) var tint := ColorRect.new() tint.name = "ColorRect" tint.color = Color(0.84, 0.93, 0.98, 0.22) tint.set_anchors_preset(Control.PRESET_FULL_RECT) add_child(tint) var center := CenterContainer.new() center.name = "CenterContainer" center.set_anchors_preset(Control.PRESET_FULL_RECT) add_child(center) var stageHolder := Control.new() stageHolder.name = "LoginStageHolder" center.add_child(stageHolder) var stage := Control.new() stage.name = "LoginStage" stage.custom_minimum_size = AUTH_STAGE_SIZE stage.size = AUTH_STAGE_SIZE stageHolder.add_child(stage) var frame := TextureRect.new() frame.name = "WhaleFrame" frame.unique_name_in_owner = true frame.texture = load(LOGIN_PANEL_TEXTURE_PATH) as Texture2D frame.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST frame.expand_mode = TextureRect.EXPAND_IGNORE_SIZE frame.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT frame.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR frame.mouse_filter = Control.MOUSE_FILTER_IGNORE _place(frame, LOGIN_FRAME_RECT) stage.add_child(frame) var loginPanel := PanelContainer.new() loginPanel.name = "LoginPanel" loginPanel.unique_name_in_owner = true loginPanel.add_theme_stylebox_override("panel", StyleBoxEmpty.new()) _place(loginPanel, LOGIN_FORM_RECT) stage.add_child(loginPanel) _build_login_form(loginPanel) var registerPanel := PanelContainer.new() registerPanel.name = "RegisterPanel" registerPanel.unique_name_in_owner = true registerPanel.add_theme_stylebox_override("panel", StyleBoxEmpty.new()) _place(registerPanel, REGISTER_FORM_RECT) stage.add_child(registerPanel) _build_register_form(registerPanel) _build_custom_character_panel(stage) _build_status_bar(stage) _build_skin_workshop_overlay(stage) _adopt_ui_tree(self) _fit_auth_stage_to_viewport() if not get_viewport().size_changed.is_connected(_fit_auth_stage_to_viewport): get_viewport().size_changed.connect(_fit_auth_stage_to_viewport) func _build_login_form(parent: Control) -> void: var box := VBoxContainer.new() box.name = "LoginVBox" box.add_theme_constant_override("separation", 10) box.set_anchors_preset(Control.PRESET_FULL_RECT) box.offset_top = 16 box.offset_left = 12 box.offset_right = -12 parent.add_child(box) var title := _brand_label("TitleLabel", "WhaleTown V2") title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER box.add_child(title) box.add_child(HSeparator.new()) box.add_child(_form_label("用户名/手机/邮箱")) var loginInput := _line_edit("LoginIdentifierInput", "用户名/手机/邮箱", false) loginInput.unique_name_in_owner = true box.add_child(loginInput) box.add_child(_form_label("密码")) var passwordInput := _line_edit("LoginPasswordInput", "请输入密码", true) passwordInput.unique_name_in_owner = true box.add_child(passwordInput) var options := HBoxContainer.new() options.name = "LoginOptions" options.add_theme_constant_override("separation", 4) box.add_child(options) var remember := CheckBox.new() remember.name = "Remember" remember.text = "记住密码" remember.add_theme_color_override("font_color", TEXT_COLOR) remember.add_theme_font_size_override("font_size", 15) _apply_font(remember) options.add_child(remember) var autoLogin := CheckBox.new() autoLogin.name = "AutoLogin" autoLogin.text = "自动登录" autoLogin.add_theme_color_override("font_color", TEXT_COLOR) autoLogin.add_theme_font_size_override("font_size", 15) _apply_font(autoLogin) options.add_child(autoLogin) var loginButton := _primary_button("LoginButton", "进入小镇", 25, Vector2(396, 58)) loginButton.unique_name_in_owner = true box.add_child(loginButton) var links := HBoxContainer.new() links.name = "BottomLinks" links.alignment = BoxContainer.ALIGNMENT_CENTER links.add_theme_constant_override("separation", 4) box.add_child(links) var forgot := _flat_button("ForgotPassword", "忘记密码?") links.add_child(forgot) var register := _flat_button("ShowRegisterButton", "注册居民身份") register.unique_name_in_owner = true links.add_child(register) func _build_register_form(parent: Control) -> void: var content := Control.new() content.name = "RegisterContent" content.set_anchors_preset(Control.PRESET_FULL_RECT) parent.add_child(content) var box := VBoxContainer.new() box.name = "RegisterVBox" box.add_theme_constant_override("separation", 3) _place(box, Rect2(8, 40, 422, 448)) content.add_child(box) var title := _label("RegisterTitleLabel", "注册新居民", 22, TEXT_COLOR, HORIZONTAL_ALIGNMENT_CENTER) title.custom_minimum_size = Vector2(0, 24) box.add_child(title) box.add_child(HSeparator.new()) box.add_child(_register_form_label("用户名")) var usernameInput := _register_line_edit("RegisterUsernameInput", "请输入用户名", false) usernameInput.unique_name_in_owner = true box.add_child(usernameInput) box.add_child(_register_form_label("邮箱")) var emailRow := HBoxContainer.new() emailRow.name = "RegisterEmailRow" emailRow.add_theme_constant_override("separation", 6) box.add_child(emailRow) var emailInput := _register_line_edit("RegisterEmailInput", "请输入邮箱地址", false) emailInput.unique_name_in_owner = true emailInput.size_flags_horizontal = Control.SIZE_EXPAND_FILL emailRow.add_child(emailInput) var sendCodeButton := _secondary_button("SendRegisterCodeButton", "发送验证码", 14, Vector2(112, 34)) sendCodeButton.unique_name_in_owner = true emailRow.add_child(sendCodeButton) box.add_child(_register_form_label("邮箱验证码")) var verificationCodeInput := _register_line_edit("RegisterVerificationCodeInput", "输入邮件中的6位数字", false) verificationCodeInput.unique_name_in_owner = true verificationCodeInput.max_length = 6 box.add_child(verificationCodeInput) box.add_child(_register_form_label("密码")) var passwordInput := _register_line_edit("RegisterPasswordInput", "至少8位且包含字母数字", true) passwordInput.unique_name_in_owner = true box.add_child(passwordInput) box.add_child(_register_form_label("确认密码")) var confirmInput := _register_line_edit("RegisterConfirmInput", "请再次输入密码", true) confirmInput.unique_name_in_owner = true box.add_child(confirmInput) var buttonSpacer := Control.new() buttonSpacer.name = "RegisterButtonSpacer" buttonSpacer.custom_minimum_size = Vector2(0, 12) box.add_child(buttonSpacer) var buttons := HBoxContainer.new() buttons.name = "RegisterButtons" buttons.alignment = BoxContainer.ALIGNMENT_CENTER buttons.add_theme_constant_override("separation", 8) box.add_child(buttons) var registerButton := _primary_button("RegisterButton", "注册", 16, Vector2(204, 38)) registerButton.unique_name_in_owner = true buttons.add_child(registerButton) var backButton := _primary_button("ShowLoginButton", "返回登录", 16, Vector2(204, 38)) backButton.unique_name_in_owner = true buttons.add_child(backButton) func _build_custom_character_panel(stage: Control) -> void: var area := Control.new() area.name = "RegistrationCharacterArea" area.unique_name_in_owner = true area.mouse_filter = Control.MOUSE_FILTER_IGNORE _place(area, Rect2(0, 0, 1320, 760)) stage.add_child(area) var panel := TextureRect.new() panel.name = "CustomCharacterPanel" panel.unique_name_in_owner = true panel.texture = load(REGISTRATION_CHOICE_MAIN_PANEL_PATH) as Texture2D panel.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR panel.expand_mode = TextureRect.EXPAND_IGNORE_SIZE panel.stretch_mode = TextureRect.STRETCH_SCALE panel.mouse_filter = Control.MOUSE_FILTER_IGNORE _place(panel, Rect2(82, 96, 1156, 566)) panel.hide() area.add_child(panel) var whaleDecor := _registration_texture_rect("ChoiceTopWhaleDecor", REGISTRATION_CHOICE_WHALE_PATH, Rect2(574, 12, 172, 108)) whaleDecor.hide() area.add_child(whaleDecor) var title := _label("CharacterTitle", "注册成功,选择你的角色", 30, ACCENT_COLOR, HORIZONTAL_ALIGNMENT_CENTER) title.unique_name_in_owner = true _apply_choice_font(title) title.add_theme_color_override("font_outline_color", Color.WHITE) title.add_theme_constant_override("outline_size", 6) title.add_theme_font_size_override("font_size", 42) title.add_theme_color_override("font_shadow_color", Color(0.04, 0.22, 0.48, 0.20)) title.add_theme_constant_override("shadow_offset_y", 2) _place(title, Rect2(330, 134, 660, 64)) title.hide() area.add_child(title) _add_choice_card(area, "DefaultRoleCard", Rect2(160, 188, 326, 356)) _add_choice_card(area, "UploadRoleCard", Rect2(497, 188, 326, 356)) _add_choice_card(area, "GenerateRoleCard", Rect2(834, 188, 326, 356)) _add_registration_header(area, "DefaultRoleHeaderPill", Rect2(223, 216, 198, 38)) _add_registration_header(area, "UploadRoleHeaderPill", Rect2(561, 216, 198, 38)) _add_registration_header(area, "GenerateRoleHeaderPill", Rect2(898, 216, 198, 38)) var defaultHint := _label("DefaultSkinHintLabel", "官方角色", 19, Color.WHITE, HORIZONTAL_ALIGNMENT_CENTER) defaultHint.unique_name_in_owner = true _apply_choice_font(defaultHint) defaultHint.vertical_alignment = VERTICAL_ALIGNMENT_CENTER defaultHint.add_theme_color_override("font_outline_color", Color(0.10, 0.34, 0.66, 0.70)) defaultHint.add_theme_constant_override("outline_size", 2) _place(defaultHint, Rect2(223, 219, 198, 28)) defaultHint.hide() area.add_child(defaultHint) var characterCanvas := Control.new() characterCanvas.name = "CharacterCanvas" characterCanvas.unique_name_in_owner = true _place(characterCanvas, Rect2(268, 288, 100, 118)) characterCanvas.hide() area.add_child(characterCanvas) var sprite := Sprite2D.new() sprite.name = "CharacterSprite" sprite.unique_name_in_owner = true sprite.centered = true sprite.position = Vector2(50, 70) sprite.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR characterCanvas.add_child(sprite) var uploadHint := _label("UploadRoleHintLabel", "上传8x4皮肤", 18, Color.WHITE, HORIZONTAL_ALIGNMENT_CENTER) uploadHint.unique_name_in_owner = true _apply_choice_font(uploadHint) uploadHint.vertical_alignment = VERTICAL_ALIGNMENT_CENTER uploadHint.add_theme_color_override("font_outline_color", Color(0.10, 0.34, 0.66, 0.70)) uploadHint.add_theme_constant_override("outline_size", 2) _place(uploadHint, Rect2(561, 219, 198, 28)) uploadHint.hide() area.add_child(uploadHint) var uploadFileBox := _registration_texture_rect("UploadFileBox", REGISTRATION_CHOICE_UPLOAD_FILE_BOX_PATH, Rect2(532, 284, 96, 154)) uploadFileBox.hide() area.add_child(uploadFileBox) var uploadFileLabel := _choice_label("UploadFileLabel", "上传PNG\n八列×四行", 14, ACCENT_COLOR, HORIZONTAL_ALIGNMENT_CENTER) uploadFileLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _place(uploadFileLabel, Rect2(532, 372, 96, 54)) uploadFileLabel.hide() area.add_child(uploadFileLabel) var spritesheetGrid := _registration_texture_rect("SpritesheetGridBox", REGISTRATION_CHOICE_SPRITESHEET_GRID_BOX_PATH, Rect2(640, 284, 160, 154)) spritesheetGrid.hide() area.add_child(spritesheetGrid) var uploadSkinBackground := _add_registration_button_background(area, "UploadSkinButtonBackground", Rect2(570, 466, 180, 50)) uploadSkinBackground.hide() var uploadSkin := _secondary_button("UploadSkinButton", "使用上传皮肤功能", 18, Vector2(150, 42)) uploadSkin.unique_name_in_owner = true _apply_registration_choice_button_style(uploadSkin) _apply_choice_font(uploadSkin) _place(uploadSkin, Rect2(570, 466, 180, 50)) uploadSkin.hide() area.add_child(uploadSkin) var generateHint := _label("GenerateRoleHintLabel", "自定义生成角色", 17, Color.WHITE, HORIZONTAL_ALIGNMENT_CENTER) generateHint.unique_name_in_owner = true _apply_choice_font(generateHint) generateHint.vertical_alignment = VERTICAL_ALIGNMENT_CENTER generateHint.add_theme_color_override("font_outline_color", Color(0.10, 0.34, 0.66, 0.70)) generateHint.add_theme_constant_override("outline_size", 2) _place(generateHint, Rect2(898, 219, 198, 28)) generateHint.hide() area.add_child(generateHint) var referenceUploadBox := _registration_texture_rect("ReferenceUploadBox", REGISTRATION_CHOICE_REFERENCE_UPLOAD_BOX_PATH, Rect2(870, 284, 106, 154)) referenceUploadBox.hide() area.add_child(referenceUploadBox) var referenceUploadLabel := _choice_label("ReferenceUploadLabel", "上传参考图", 15, ACCENT_COLOR, HORIZONTAL_ALIGNMENT_CENTER) referenceUploadLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _place(referenceUploadLabel, Rect2(870, 390, 106, 30)) referenceUploadLabel.hide() area.add_child(referenceUploadLabel) var imagePlaceholderBox := _registration_texture_rect("ImagePlaceholderBox", REGISTRATION_CHOICE_IMAGE_PLACEHOLDER_BOX_PATH, Rect2(990, 284, 146, 154)) imagePlaceholderBox.hide() area.add_child(imagePlaceholderBox) var workshopBackground := _add_registration_button_background(area, "WorkshopButtonBackground", Rect2(908, 466, 180, 50)) workshopBackground.hide() var workshop := _secondary_button("WorkshopButton", "使用自定义生成", 18, Vector2(150, 42)) workshop.unique_name_in_owner = true _apply_choice_font(workshop) _apply_registration_choice_button_style(workshop) _place(workshop, Rect2(908, 466, 180, 50)) workshop.hide() area.add_child(workshop) var customHint := _label("CustomSkinHintLabel", "上传参考图,由系统生成账号角色", 12, MUTED_COLOR, HORIZONTAL_ALIGNMENT_CENTER) customHint.unique_name_in_owner = true _place(customHint, Rect2(878, 440, 250, 22)) customHint.hide() area.add_child(customHint) var enterWithDefaultBackground := _add_registration_button_background(area, "EnterWithDefaultSkinButtonBackground", Rect2(232, 466, 180, 50)) enterWithDefaultBackground.hide() var enterWithDefault := _primary_button("EnterWithDefaultSkinButton", "使用所选角色", 18, Vector2(150, 42)) enterWithDefault.unique_name_in_owner = true _apply_registration_choice_button_style(enterWithDefault) _apply_choice_font(enterWithDefault) _place(enterWithDefault, Rect2(232, 466, 180, 50)) enterWithDefault.hide() area.add_child(enterWithDefault) var footerBar := _registration_texture_rect("RegistrationChoiceFooterBar", REGISTRATION_CHOICE_FOOTER_BAR_PATH, Rect2(170, 562, 980, 72)) footerBar.hide() area.add_child(footerBar) var footerHint := _label("RegistrationChoiceFooterHint", "仅注册时免费选择或自定义生成一次皮肤,后续需在商城内购买替换", 15, ACCENT_COLOR, HORIZONTAL_ALIGNMENT_CENTER) footerHint.unique_name_in_owner = true _apply_choice_font(footerHint) footerHint.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _place(footerHint, Rect2(310, 584, 700, 28)) footerHint.hide() area.add_child(footerHint) var avatarPanel := PanelContainer.new() avatarPanel.name = "AvatarPanel" avatarPanel.unique_name_in_owner = true avatarPanel.add_theme_stylebox_override("panel", _soft_panel_style()) _place(avatarPanel, Rect2(758, 251, 210, 300)) area.add_child(avatarPanel) var avatarTitle := _label("AvatarTitle", "头像", 20, ACCENT_COLOR, HORIZONTAL_ALIGNMENT_CENTER) avatarTitle.unique_name_in_owner = true _place(avatarTitle, Rect2(807, 278, 112, 30)) area.add_child(avatarTitle) var avatarPreview := PanelContainer.new() avatarPreview.name = "AvatarPreview" avatarPreview.unique_name_in_owner = true avatarPreview.visible = false avatarPreview.add_theme_stylebox_override("panel", _panel_style(Color(0.39, 0.72, 0.95, 1.0), 18)) _place(avatarPreview, Rect2(788, 320, 150, 150)) area.add_child(avatarPreview) var avatarLabel := _label("AvatarLabel", "鲸", 32, Color.WHITE, HORIZONTAL_ALIGNMENT_CENTER) avatarLabel.unique_name_in_owner = true avatarLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER avatarPreview.add_child(avatarLabel) var uploadAvatar := _secondary_button("UploadAvatarButton", "上传头像", 18, Vector2(130, 42)) uploadAvatar.unique_name_in_owner = true _place(uploadAvatar, Rect2(798, 486, 130, 42)) area.add_child(uploadAvatar) var scroll := ScrollContainer.new() scroll.name = "SkinLibraryScroll" scroll.unique_name_in_owner = true scroll.visible = false scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_SHOW_NEVER scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED _place(scroll, Rect2(235, 274, 230, 182)) area.add_child(scroll) var row := HBoxContainer.new() row.name = "SkinLibraryRow" row.unique_name_in_owner = true row.alignment = BoxContainer.ALIGNMENT_CENTER row.add_theme_constant_override("separation", 0) scroll.add_child(row) var prev := _secondary_button("SkinLibraryPrevButton", "‹", 20, Vector2(24, 34)) prev.unique_name_in_owner = true _place(prev, Rect2(201, 345, 30, 44)) prev.visible = false area.add_child(prev) var next := _secondary_button("SkinLibraryNextButton", "›", 20, Vector2(24, 34)) next.unique_name_in_owner = true _place(next, Rect2(413, 345, 30, 44)) next.visible = false area.add_child(next) func _build_status_bar(stage: Control) -> void: var statusBox := PanelContainer.new() statusBox.name = "StatusBox" statusBox.add_theme_stylebox_override("panel", _panel_style(Color(1, 1, 1, 0.0), 8, Color(0.17, 0.47, 0.76, 0.0), 0)) _place(statusBox, Rect2(250, 660, 820, 34)) stage.add_child(statusBox) var status := _label("StatusLabel", "", 13, Color(0.65, 0.18, 0.12, 1.0), HORIZONTAL_ALIGNMENT_CENTER) status.unique_name_in_owner = true status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER status.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS _place(status, Rect2(270, 664, 780, 24)) stage.add_child(status) func _build_skin_workshop_overlay(stage: Control) -> void: var overlay := Control.new() overlay.name = "SkinWorkshopOverlay" overlay.unique_name_in_owner = true overlay.visible = false overlay.mouse_filter = Control.MOUSE_FILTER_STOP _place(overlay, Rect2(0, 0, 1320, 760)) stage.add_child(overlay) var dim := ColorRect.new() dim.name = "WorkshopDim" dim.color = Color(0.02, 0.08, 0.16, 0.38) dim.set_anchors_preset(Control.PRESET_FULL_RECT) overlay.add_child(dim) var panel := PanelContainer.new() panel.name = "WorkshopPanel" panel.add_theme_stylebox_override("panel", _panel_style(Color(0.96, 0.99, 1.0, 0.98), 10, Color(0.12, 0.38, 0.68, 0.72), 2)) _place(panel, Rect2(365, 160, 590, 430)) overlay.add_child(panel) var title := _label("WorkshopTitle", "角色工坊", 28, ACCENT_COLOR, HORIZONTAL_ALIGNMENT_CENTER) _place(title, Rect2(407, 186, 420, 42)) overlay.add_child(title) var closeButton := _secondary_button("WorkshopCloseButton", "关闭", 16, Vector2(86, 36)) _place(closeButton, Rect2(838, 190, 86, 36)) closeButton.pressed.connect(_hide_skin_workshop) overlay.add_child(closeButton) var previewPanel := PanelContainer.new() previewPanel.name = "WorkshopPreviewPanel" previewPanel.add_theme_stylebox_override("panel", _panel_style(Color(0.90, 0.96, 1.0, 0.72), 8, Color(0.45, 0.66, 0.84, 0.58), 1)) _place(previewPanel, Rect2(415, 254, 150, 190)) overlay.add_child(previewPanel) var previewSprite := Sprite2D.new() previewSprite.name = "WorkshopPreviewSprite" previewSprite.unique_name_in_owner = true previewSprite.centered = true previewSprite.position = Vector2(75, 105) previewSprite.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR previewSprite.visible = false previewPanel.add_child(previewSprite) var nameInput := _line_edit("WorkshopSkinNameInput", "角色名字", false) nameInput.unique_name_in_owner = true nameInput.alignment = HORIZONTAL_ALIGNMENT_CENTER nameInput.add_theme_font_size_override("font_size", 16) _place(nameInput, Rect2(396, 456, 190, 38)) overlay.add_child(nameInput) var sourceLabel := _label("WorkshopSourceLabel", "选择一张角色图片作为参考", 17, TEXT_COLOR, HORIZONTAL_ALIGNMENT_LEFT) sourceLabel.unique_name_in_owner = true sourceLabel.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _place(sourceLabel, Rect2(610, 252, 290, 58)) overlay.add_child(sourceLabel) var sourceButton := _secondary_button("WorkshopSourceButton", "选择角色图片", 18, Vector2(210, 46)) sourceButton.unique_name_in_owner = true _place(sourceButton, Rect2(650, 326, 210, 46)) sourceButton.pressed.connect(_on_workshop_source_pressed) overlay.add_child(sourceButton) var generateButton := _primary_button("WorkshopGenerateButton", "生成角色皮肤", 18, Vector2(210, 46)) generateButton.unique_name_in_owner = true _place(generateButton, Rect2(650, 414, 210, 46)) generateButton.pressed.connect(_on_generate_skin_pressed) overlay.add_child(generateButton) var status := _label("WorkshopStatusLabel", "", 14, MUTED_COLOR, HORIZONTAL_ALIGNMENT_CENTER) status.unique_name_in_owner = true status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _place(status, Rect2(430, 512, 460, 50)) overlay.add_child(status) func _add_choice_card(parent: Control, nodeName: String, rect: Rect2) -> PanelContainer: var card := PanelContainer.new() card.name = nodeName card.unique_name_in_owner = true card.visible = false card.mouse_filter = Control.MOUSE_FILTER_IGNORE card.add_theme_stylebox_override("panel", StyleBoxEmpty.new()) _place(card, rect) parent.add_child(card) var texture := _registration_texture_rect("CardTexture", REGISTRATION_CHOICE_CARD_PATH, Rect2(Vector2.ZERO, rect.size)) texture.unique_name_in_owner = false texture.set_anchors_preset(Control.PRESET_FULL_RECT) texture.offset_left = 0 texture.offset_top = 0 texture.offset_right = 0 texture.offset_bottom = 0 card.add_child(texture) return card func _add_registration_header(parent: Control, nodeName: String, rect: Rect2) -> TextureRect: var header := _registration_texture_rect(nodeName, REGISTRATION_CHOICE_HEADER_PILL_PATH, rect) header.hide() parent.add_child(header) return header func _add_registration_button_background(parent: Control, nodeName: String, rect: Rect2) -> TextureRect: var background := _registration_texture_rect(nodeName, REGISTRATION_CHOICE_BUTTON_PATH, rect) parent.add_child(background) return background func _registration_texture_rect(nodeName: String, texturePath: String, rect: Rect2) -> TextureRect: var textureRect := TextureRect.new() textureRect.name = nodeName textureRect.unique_name_in_owner = true textureRect.texture = load(texturePath) as Texture2D textureRect.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR textureRect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE textureRect.stretch_mode = TextureRect.STRETCH_SCALE textureRect.mouse_filter = Control.MOUSE_FILTER_IGNORE _place(textureRect, rect) return textureRect func _ready() -> void: add_to_group("whaletown_escape_dismissible") _auth_manager = get_node_or_null("/root/AuthManager") _chat_manager = get_node_or_null("/root/ChatManager") _scene_manager = get_node_or_null("/root/SceneManager") _appearance_manager = get_node_or_null("/root/AppearanceManager") _connect_signals() _refresh_appearance_ui() _show_login() if _auth_manager != null and bool(_auth_manager.call("is_authenticated")) and _should_auto_resume_cached_session(): _resume_cached_session() else: login_identifier_input.grab_focus() func _connect_signals() -> void: login_button.pressed.connect(_on_login_pressed) show_register_button.pressed.connect(_show_register) send_register_code_button.pressed.connect(_on_send_register_code_pressed) register_button.pressed.connect(_on_register_pressed) show_login_button.pressed.connect(_show_login) workshop_button.pressed.connect(_on_workshop_pressed) upload_avatar_button.pressed.connect(_on_upload_avatar_pressed) upload_skin_button.pressed.connect(_on_upload_skin_pressed) enter_with_default_skin_button.pressed.connect(_on_enter_with_selected_skin_pressed) skin_library_prev_button.pressed.connect(func() -> void: _scroll_skin_library(-1)) skin_library_next_button.pressed.connect(func() -> void: _scroll_skin_library(1)) login_password_input.text_submitted.connect(func(_text: String) -> void: _on_login_pressed()) register_verification_code_input.text_submitted.connect(func(_text: String) -> void: _on_register_pressed()) register_confirm_input.text_submitted.connect(func(_text: String) -> void: _on_register_pressed()) if _auth_manager == null: status_label.text = "认证系统未加载" return _auth_manager.connect("login_succeeded", _on_login_succeeded) _auth_manager.connect("login_failed", _on_login_failed) _auth_manager.connect("register_succeeded", _on_register_succeeded) _auth_manager.connect("register_failed", _on_register_failed) _auth_manager.connect("email_verification_sent", _on_email_verification_sent) _auth_manager.connect("email_verification_failed", _on_email_verification_failed) _auth_manager.connect("profile_update_succeeded", _on_profile_update_succeeded) _auth_manager.connect("profile_update_failed", _on_profile_update_failed) _auth_manager.connect("auth_state_changed", _on_auth_state_changed) func _process(delta: float) -> void: if not _skin_generation_active or _skin_generation_job_id.is_empty(): return _skin_generation_poll_elapsed += delta if _skin_generation_poll_elapsed >= SKIN_GENERATION_POLL_INTERVAL: _skin_generation_poll_elapsed = 0.0 _poll_skin_generation_job() func _exit_tree() -> void: _skin_generation_active = false _skin_generation_job_id = "" func _show_login() -> void: _awaiting_registration_skin_choice = false _apply_auth_layout(false) if is_instance_valid(whale_frame): whale_frame.show() login_panel.show() register_panel.hide() if is_instance_valid(registration_character_area): registration_character_area.hide() if is_instance_valid(skin_workshop_overlay): skin_workshop_overlay.hide() login_panel.mouse_filter = Control.MOUSE_FILTER_STOP register_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE login_panel.move_to_front() status_label.text = "" status_label.show() _set_registration_avatar_controls_visible(false) call_deferred("_focus_login") func _show_register() -> void: _awaiting_registration_skin_generation = false _awaiting_registration_skin_choice = false _skin_generation_active = false _skin_generation_job_id = "" _skin_generation_poll_in_flight = false _reset_registration_upload_state() _apply_auth_layout(true) if is_instance_valid(whale_frame): whale_frame.show() login_panel.hide() register_panel.show() if is_instance_valid(registration_character_area): registration_character_area.show() if is_instance_valid(skin_workshop_overlay): skin_workshop_overlay.hide() login_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE register_panel.mouse_filter = Control.MOUSE_FILTER_STOP register_panel.move_to_front() register_button.disabled = false show_login_button.disabled = false status_label.text = "" status_label.show() _set_registration_character_controls_visible(false) _set_registration_avatar_controls_visible(true) call_deferred("_focus_register") func _apply_auth_layout(registerMode: bool) -> void: if is_instance_valid(whale_frame): whale_frame.texture = load(REGISTER_PANEL_TEXTURE_PATH if registerMode else LOGIN_PANEL_TEXTURE_PATH) as Texture2D whale_frame.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT _place(whale_frame, REGISTER_FRAME_RECT if registerMode else LOGIN_FRAME_RECT) if is_instance_valid(login_panel): _place(login_panel, LOGIN_FORM_RECT) if is_instance_valid(register_panel): _place(register_panel, REGISTER_FORM_RECT) func _fit_auth_stage_to_viewport() -> void: var holder := get_node_or_null("CenterContainer/LoginStageHolder") as Control var stage := get_node_or_null("CenterContainer/LoginStageHolder/LoginStage") as Control if holder == null or stage == null: return var viewportSize := Vector2(get_viewport_rect().size) if viewportSize.x <= 0.0 or viewportSize.y <= 0.0: return var scaleFactor: float = minf( (viewportSize.x * AUTH_STAGE_VIEWPORT_RATIO) / AUTH_STAGE_SIZE.x, (viewportSize.y * AUTH_STAGE_VIEWPORT_RATIO) / AUTH_STAGE_SIZE.y ) scaleFactor = maxf(0.1, scaleFactor) var holderSize := AUTH_STAGE_SIZE * scaleFactor holder.custom_minimum_size = holderSize holder.size = holderSize stage.position = Vector2.ZERO stage.size = AUTH_STAGE_SIZE stage.scale = Vector2(scaleFactor, scaleFactor) func _focus_login() -> void: if is_instance_valid(login_identifier_input): login_identifier_input.grab_focus() func _focus_register() -> void: if is_instance_valid(register_username_input): register_username_input.grab_focus() func _refresh_appearance_ui() -> void: _refresh_character_preview() _refresh_skin_library() _refresh_avatar_preview() _refresh_choice_cards() func _refresh_choice_cards() -> void: if not is_instance_valid(registration_character_area): return var selectedSkinId := _current_selected_skin_id() var defaultCard := registration_character_area.get_node_or_null("DefaultRoleCard") as PanelContainer if defaultCard != null: _set_choice_card_selected(defaultCard, selectedSkinId != "custom_skin") var uploadCard := registration_character_area.get_node_or_null("UploadRoleCard") as PanelContainer if uploadCard != null: _set_choice_card_selected(uploadCard, selectedSkinId == "custom_skin" and _registration_skin_texture != null) var generateCard := registration_character_area.get_node_or_null("GenerateRoleCard") as PanelContainer if generateCard != null: _set_choice_card_selected(generateCard, false) if is_instance_valid(enter_with_default_skin_button): enter_with_default_skin_button.text = "使用所选角色" func _set_registration_character_controls_visible(visible: bool) -> void: var controlNames := [ "CustomCharacterPanel", "ChoiceTopWhaleDecor", "CharacterTitle", "DefaultRoleCard", "UploadRoleCard", "GenerateRoleCard", "DefaultRoleHeaderPill", "UploadRoleHeaderPill", "GenerateRoleHeaderPill", "DefaultSkinHintLabel", "UploadRoleHintLabel", "UploadFileBox", "UploadFileLabel", "SpritesheetGridBox", "UploadSkinButtonBackground", "WorkshopButton", "GenerateRoleHintLabel", "ReferenceUploadBox", "ReferenceUploadLabel", "ImagePlaceholderBox", "WorkshopButtonBackground", "CustomSkinHintLabel", "EnterWithDefaultSkinButtonBackground", "EnterWithDefaultSkinButton", "SkinLibraryScroll", "UploadSkinButton", "RegistrationChoiceFooterBar", "RegistrationChoiceFooterHint", ] for controlName in controlNames: var control := registration_character_area.get_node_or_null(controlName) if is_instance_valid(registration_character_area) else null if control is CanvasItem: (control as CanvasItem).visible = visible if is_instance_valid(skin_library_prev_button): skin_library_prev_button.visible = false if is_instance_valid(skin_library_next_button): skin_library_next_button.visible = false if visible: _set_registration_avatar_controls_visible(false) _refresh_appearance_ui() func _set_registration_avatar_controls_visible(visible: bool) -> void: if not is_instance_valid(registration_character_area): return var controlNames := [ "AvatarPanel", "AvatarTitle", "AvatarPreview", "UploadAvatarButton", ] for controlName in controlNames: var control := registration_character_area.get_node_or_null(controlName) if control is CanvasItem: (control as CanvasItem).visible = visible if visible: _refresh_avatar_preview() func _set_choice_card_selected(card: PanelContainer, selected: bool) -> void: var textureRect := card.get_node_or_null("CardTexture") as TextureRect if textureRect == null: card.add_theme_stylebox_override("panel", _choice_card_style(selected)) return textureRect.texture = load(REGISTRATION_CHOICE_CARD_SELECTED_PATH if selected else REGISTRATION_CHOICE_CARD_PATH) as Texture2D func _refresh_character_preview() -> void: if _appearance_manager == null or not is_instance_valid(character_sprite): return if _registration_custom_skin_active and _registration_skin_texture != null: character_sprite.texture = _registration_skin_texture character_sprite.hframes = 8 character_sprite.vframes = 4 character_sprite.modulate = Color.WHITE character_sprite.frame = 0 character_sprite.scale = _sprite_scale_for_height(character_sprite.texture, character_sprite.vframes, MAIN_PREVIEW_HEIGHT) _clear_workshop_preview() return if _appearance_manager.has_method("apply_skin_to_sprite"): var selectedSkinId := _current_selected_skin_id() var previewSkinId := _first_registration_skin_id() if selectedSkinId == "custom_skin" else selectedSkinId _appearance_manager.call("apply_skin_to_sprite", character_sprite, previewSkinId) character_sprite.frame = 0 character_sprite.scale = _sprite_scale_for_height(character_sprite.texture, character_sprite.vframes, MAIN_PREVIEW_HEIGHT) _clear_workshop_preview() func _clear_workshop_preview() -> void: if not is_instance_valid(workshop_preview_sprite): return workshop_preview_sprite.texture = null workshop_preview_sprite.visible = false workshop_preview_sprite.frame = 0 workshop_preview_sprite.hframes = 8 workshop_preview_sprite.vframes = 4 workshop_preview_sprite.scale = Vector2.ONE func _show_workshop_generated_preview(path: String) -> void: if not is_instance_valid(workshop_preview_sprite): return var image := Image.load_from_file(path) if image == null: _clear_workshop_preview() return _show_workshop_generated_preview_from_image(image) func _show_workshop_generated_preview_from_image(image: Image) -> void: if not is_instance_valid(workshop_preview_sprite): return var texture := ImageTexture.create_from_image(image) workshop_preview_sprite.texture = texture workshop_preview_sprite.hframes = 8 workshop_preview_sprite.vframes = 4 workshop_preview_sprite.frame = 0 workshop_preview_sprite.scale = _sprite_scale_for_height(texture, 4, 156.0) workshop_preview_sprite.visible = true func _show_workshop_source_preview(path: String) -> void: if not is_instance_valid(workshop_preview_sprite): return var image := Image.load_from_file(path) if image == null: _clear_workshop_preview() return var texture := ImageTexture.create_from_image(image) workshop_preview_sprite.texture = texture workshop_preview_sprite.hframes = 1 workshop_preview_sprite.vframes = 1 workshop_preview_sprite.frame = 0 workshop_preview_sprite.scale = _sprite_scale_for_height(texture, 1, 156.0) workshop_preview_sprite.visible = true func _refresh_skin_library() -> void: if not is_instance_valid(skin_library_row): return for child in skin_library_row.get_children(): child.queue_free() _registration_official_skins.clear() if _appearance_manager == null: return var selectedSkinId := _current_selected_skin_id() var skins: Array = [] if _appearance_manager.has_method("get_registration_skin_catalog"): skins = _appearance_manager.call("get_registration_skin_catalog") elif _appearance_manager.has_method("get_skin_catalog"): skins = _appearance_manager.call("get_skin_catalog") if _registration_skin_texture != null: skins.append({ "id": "custom_skin", "name": "上传角色", "desc": "注册后保存到账号", "hframes": 8, "vframes": 4, "owned": true, "locked": false, "session_preview": true, }) var visibleCount := 0 for skinVariant in skins: if not (skinVariant is Dictionary): continue var skinDictionary := skinVariant as Dictionary var skinId := str(skinDictionary.get("id", "")).strip_edges() if bool(skinDictionary.get("hidden", false)) or not REGISTRATION_OFFICIAL_SKIN_IDS.has(skinId): continue _registration_official_skins.append(skinDictionary) visibleCount += 1 if visibleCount >= SKIN_LIBRARY_MAX_VISIBLE: break if _registration_official_skins.is_empty(): _update_skin_library_buttons() return _registration_official_skin_index = _selected_official_skin_index(selectedSkinId) var currentSkin := _registration_official_skins[_registration_official_skin_index] as Dictionary skin_library_row.add_child(_create_skin_button(currentSkin, selectedSkinId)) call_deferred("_layout_skin_library_row") func _create_skin_button(skin: Dictionary, selectedSkinId: String) -> Button: var skinId := str(skin.get("id", "")) var selected := skinId == selectedSkinId var button := Button.new() button.custom_minimum_size = Vector2(180, 182) button.clip_contents = false button.focus_mode = Control.FOCUS_NONE button.text = "" button.tooltip_text = str(skin.get("name", "角色皮肤")) button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND button.add_theme_stylebox_override("normal", StyleBoxEmpty.new()) button.add_theme_stylebox_override("hover", StyleBoxEmpty.new()) button.add_theme_stylebox_override("pressed", StyleBoxEmpty.new()) button.add_theme_stylebox_override("focus", StyleBoxEmpty.new()) button.pressed.connect(func() -> void: _select_skin(skinId)) var background := TextureRect.new() background.name = "SkinSlotBackground" background.texture = load(REGISTRATION_CHOICE_SKIN_SLOT_SELECTED_PATH if selected else REGISTRATION_CHOICE_SKIN_SLOT_PATH) as Texture2D background.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR background.expand_mode = TextureRect.EXPAND_IGNORE_SIZE background.stretch_mode = TextureRect.STRETCH_SCALE background.mouse_filter = Control.MOUSE_FILTER_IGNORE background.set_anchors_preset(Control.PRESET_FULL_RECT) button.add_child(background) var preview := Sprite2D.new() preview.texture = _skin_texture(skin) preview.hframes = int(skin.get("hframes", 4)) preview.vframes = int(skin.get("vframes", 4)) preview.frame = 0 preview.position = Vector2(90, 99) preview.scale = _sprite_scale_for_frame_bounds(preview.texture, preview.hframes, preview.vframes, Vector2(136, SKIN_THUMB_HEIGHT)) preview.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR button.add_child(preview) var label := Label.new() label.text = "使用中" if selected else str(skin.get("name", "皮肤")) label.visible = false label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS label.add_theme_color_override("font_color", ACCENT_COLOR if selected else MUTED_COLOR) label.add_theme_font_size_override("font_size", 9) label.set_anchors_preset(Control.PRESET_FULL_RECT) label.offset_top = 90 label.offset_left = 2 label.offset_right = -2 button.add_child(label) return button func _layout_skin_library_row() -> void: if not is_instance_valid(skin_library_row) or not is_instance_valid(skin_library_scroll): return if not skin_library_scroll.visible: skin_library_row.alignment = BoxContainer.ALIGNMENT_CENTER return var visibleWidth := skin_library_scroll.size.x if visibleWidth <= 0.0: visibleWidth = skin_library_scroll.custom_minimum_size.x var contentWidth := 0.0 var visibleChildren := 0 var separation := float(skin_library_row.get_theme_constant("separation")) for child in skin_library_row.get_children(): if child is Control and (child as Control).visible: contentWidth += (child as Control).custom_minimum_size.x visibleChildren += 1 if visibleChildren > 1: contentWidth += separation * float(visibleChildren - 1) skin_library_row.custom_minimum_size.x = contentWidth skin_library_row.size.x = contentWidth skin_library_row.position.x = maxf(0.0, (visibleWidth - contentWidth) * 0.5) var rowHeight := 0.0 for child in skin_library_row.get_children(): if child is Control and (child as Control).visible: rowHeight = maxf(rowHeight, (child as Control).custom_minimum_size.y) skin_library_row.position.y = maxf(0.0, (skin_library_scroll.size.y - rowHeight) * 0.5) skin_library_row.alignment = BoxContainer.ALIGNMENT_BEGIN skin_library_scroll.scroll_horizontal = 0 var bar := skin_library_scroll.get_h_scroll_bar() if bar != null: bar.value = bar.min_value _update_skin_library_buttons() func _selected_official_skin_index(selectedSkinId: String) -> int: for index in range(_registration_official_skins.size()): var skin := _registration_official_skins[index] as Dictionary if str(skin.get("id", "")).strip_edges() == selectedSkinId: return index return 0 func _skin_texture(skin: Dictionary) -> Texture2D: if str(skin.get("id", "")) == "custom_skin": return _registration_skin_texture if bool(skin.get("account_asset", false)) and _appearance_manager != null and _appearance_manager.has_method("get_account_skin_texture"): var accountTexture := _appearance_manager.call("get_account_skin_texture", str(skin.get("id", ""))) as Texture2D if accountTexture != null: return accountTexture return load(str(skin.get("texture", "res://assets/characters/player_pixel_spritesheet.png"))) as Texture2D func _refresh_avatar_preview() -> void: avatar_preview.custom_minimum_size = Vector2(34, 34) if _registration_avatar_texture == null: _clear_registration_avatar_preview() return _show_registration_avatar_preview(_registration_avatar_texture) func _reset_registration_upload_state() -> void: _registration_avatar_texture = null _registration_skin_texture = null _registration_custom_skin_active = false _registration_avatar_source_path = "" _registration_skin_source_path = "" _registration_selected_skin_id = "" _clear_registration_avatar_preview() func _clear_registration_avatar_preview() -> void: if not is_instance_valid(avatar_preview): return avatar_preview.hide() avatar_preview.clip_contents = true avatar_preview.add_theme_stylebox_override("panel", _panel_style(Color(0.39, 0.72, 0.95, 1.0), 18)) if is_instance_valid(avatar_label): avatar_label.hide() var textureRect := avatar_preview.get_node_or_null("RegistrationAvatarTexture") as TextureRect if textureRect != null: textureRect.texture = null textureRect.hide() func _show_registration_avatar_preview(texture: Texture2D) -> void: if not is_instance_valid(avatar_preview) or texture == null: return avatar_preview.show() avatar_preview.clip_contents = true avatar_preview.add_theme_stylebox_override("panel", _panel_style(Color.WHITE, 18)) if is_instance_valid(avatar_label): avatar_label.hide() var textureRect := _get_or_create_registration_avatar_texture_rect() textureRect.texture = texture textureRect.show() func _get_or_create_registration_avatar_texture_rect() -> TextureRect: var existing := avatar_preview.get_node_or_null("RegistrationAvatarTexture") as TextureRect if existing != null: return existing var textureRect := TextureRect.new() textureRect.name = "RegistrationAvatarTexture" textureRect.mouse_filter = Control.MOUSE_FILTER_IGNORE textureRect.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR textureRect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE textureRect.stretch_mode = TextureRect.STRETCH_SCALE textureRect.set_anchors_preset(Control.PRESET_FULL_RECT) avatar_preview.add_child(textureRect) return textureRect func _load_registration_avatar_texture(path: String) -> Texture2D: var cropped := _load_registration_avatar_image(path) if cropped == null: return null return ImageTexture.create_from_image(cropped) func _load_registration_avatar_image(path: String) -> Image: var image := Image.load_from_file(path) if image == null: return null var cropped := _create_square_avatar_image(image) _apply_rounded_rect_alpha(cropped) return cropped func _load_workshop_source_image(path: String) -> Image: var image := Image.load_from_file(path) if image == null: return null if image.get_format() != Image.FORMAT_RGBA8: image.convert(Image.FORMAT_RGBA8) var maxSide := 768 var width := image.get_width() var height := image.get_height() if width <= 0 or height <= 0: return null var largest := maxi(width, height) if largest > maxSide: var scale := float(maxSide) / float(largest) image.resize(maxi(1, int(round(float(width) * scale))), maxi(1, int(round(float(height) * scale))), Image.INTERPOLATE_LANCZOS) return image func _image_to_png_base64(image: Image) -> String: var bytes := image.save_png_to_buffer() return Marshalls.raw_to_base64(bytes) func _create_square_avatar_image(image: Image) -> Image: var side: int = mini(image.get_width(), image.get_height()) var cropX: int = int((image.get_width() - side) * 0.5) var cropY: int = int((image.get_height() - side) * 0.5) var cropped := image.get_region(Rect2i(cropX, cropY, side, side)) cropped.resize(256, 256, Image.INTERPOLATE_LANCZOS) return cropped func _apply_rounded_rect_alpha(image: Image) -> void: var width := image.get_width() var height := image.get_height() if width <= 0 or height <= 0: return if image.get_format() != Image.FORMAT_RGBA8: image.convert(Image.FORMAT_RGBA8) var radius: float = maxf(1.0, minf(float(width), float(height)) * 0.22) var feather: float = 1.5 var maxX: float = float(width - 1) var maxY: float = float(height - 1) for x in range(width): for y in range(height): var px: float = float(x) var py: float = float(y) var cornerCenter: Vector2 = Vector2(px, py) var inCorner := false if px < radius and py < radius: cornerCenter = Vector2(radius, radius) inCorner = true elif px > maxX - radius and py < radius: cornerCenter = Vector2(maxX - radius, radius) inCorner = true elif px < radius and py > maxY - radius: cornerCenter = Vector2(radius, maxY - radius) inCorner = true elif px > maxX - radius and py > maxY - radius: cornerCenter = Vector2(maxX - radius, maxY - radius) inCorner = true if not inCorner: continue var distance := Vector2(px, py).distance_to(cornerCenter) if distance > radius: var pixel := image.get_pixel(x, y) var alpha: float = clampf(1.0 - ((distance - radius) / feather), 0.0, 1.0) pixel.a *= alpha image.set_pixel(x, y, pixel) func _select_skin(skinId: String) -> void: var normalizedSkinId := skinId.strip_edges() if normalizedSkinId == "custom_skin" and _registration_skin_texture != null: _registration_custom_skin_active = true _registration_selected_skin_id = "custom_skin" _refresh_appearance_ui() status_label.text = "已切换到上传角色皮肤" if is_instance_valid(workshop_status_label): workshop_status_label.text = "当前角色皮肤已切换" return if _is_in_registration_choice_flow(): _registration_custom_skin_active = false _registration_selected_skin_id = normalizedSkinId _refresh_appearance_ui() status_label.text = "已选择角色,点击使用所选角色进入" return if _appearance_manager == null or not _appearance_manager.has_method("set_selected_skin"): return if bool(_appearance_manager.call("set_selected_skin", normalizedSkinId, true)): if normalizedSkinId != "custom_skin": _registration_custom_skin_active = false _registration_selected_skin_id = normalizedSkinId _refresh_appearance_ui() status_label.text = "已切换角色皮肤" if is_instance_valid(workshop_status_label): workshop_status_label.text = "当前角色皮肤已切换" func _on_workshop_pressed() -> void: _show_skin_workshop() func _is_in_registration_choice_flow() -> bool: return _awaiting_registration_skin_choice func _show_skin_workshop() -> void: if not is_instance_valid(skin_workshop_overlay): return _clear_workshop_preview() _workshop_source_image_path = "" if is_instance_valid(workshop_source_label): workshop_source_label.text = "选择一张角色图片作为参考" if is_instance_valid(workshop_status_label): workshop_status_label.text = "上传角色参考图后会生成四方向8帧皮肤,并自动应用到当前角色" skin_workshop_overlay.show() skin_workshop_overlay.move_to_front() func _open_registration_skin_generation_step() -> void: _set_submitting(false, "请上传参考图生成自定义角色,或关闭返回初始角色选择") if is_instance_valid(status_label): status_label.hide() _awaiting_registration_skin_choice = true if is_instance_valid(whale_frame): whale_frame.hide() if is_instance_valid(login_panel): login_panel.hide() if is_instance_valid(register_panel): register_panel.hide() if is_instance_valid(registration_character_area): registration_character_area.show() _set_registration_character_controls_visible(true) _show_skin_workshop() func _open_registration_skin_choice_step() -> void: _set_submitting(false, "请选择初始角色,或进入自定义生成角色") if is_instance_valid(status_label): status_label.hide() _awaiting_registration_skin_choice = true if is_instance_valid(whale_frame): whale_frame.hide() if is_instance_valid(login_panel): login_panel.hide() if is_instance_valid(register_panel): register_panel.hide() if is_instance_valid(registration_character_area): registration_character_area.show() if is_instance_valid(skin_workshop_overlay): skin_workshop_overlay.hide() _set_registration_character_controls_visible(true) func _on_enter_with_selected_skin_pressed() -> void: if _skin_generation_active or _is_submitting: return var selectedSkinId := _current_selected_skin_id() if selectedSkinId == "custom_skin": status_label.text = "上传皮肤需要先保存到账号" return _awaiting_registration_skin_generation = false _awaiting_registration_skin_choice = false if _auth_manager != null and _auth_manager.has_method("update_profile"): _pending_profile_sync_after_auth = true _deferred_enter_square_after_profile_sync = true _set_submitting(true, "正在保存角色选择...") _auth_manager.call("update_profile", { "skin_id": selectedSkinId, }) return status_label.text = "正在进入小镇..." _enter_square() func _on_generate_skin_pressed() -> void: if _skin_generation_active: return if _auth_manager == null or not _auth_manager.has_method("is_authenticated") or not bool(_auth_manager.call("is_authenticated")): _set_workshop_generation_status("请先完成注册或登录,再生成账号角色") return if _workshop_source_image_path.is_empty(): _set_workshop_generation_status("请先选择一张角色参考图片") return var sourceImage := _load_workshop_source_image(_workshop_source_image_path) if sourceImage == null: _set_workshop_generation_status("角色参考图读取失败") return var fallbackName := "我的角色" var requestedName := workshop_skin_name_input.text.strip_edges() if is_instance_valid(workshop_skin_name_input) else "" var runName := requestedName if not requestedName.is_empty() else fallbackName _skin_generation_active = true _skin_generation_job_id = "" _skin_generation_poll_elapsed = 0.0 _skin_generation_poll_in_flight = false _set_skin_generation_controls_enabled(false) _set_workshop_generation_status("正在提交角色生成任务") var payload := { "name": runName, "source_image_base64": _image_to_png_base64(sourceImage), "source_mime_type": "image/png", } var api_client := get_node_or_null("/root/ApiClient") if api_client == null or not api_client.has_method("request_json"): _skin_generation_active = false _set_skin_generation_controls_enabled(true) _set_workshop_generation_status("角色生成服务不可用") return api_client.call( "request_json", SKIN_GENERATION_CREATE_ENDPOINT, payload, _on_skin_generation_create_completed, HTTPClient.METHOD_POST, true, SKIN_GENERATION_REQUEST_TIMEOUT ) func _on_skin_generation_create_completed(success: bool, response: Dictionary, error_info: Dictionary) -> void: if not success: _skin_generation_active = false _set_skin_generation_controls_enabled(true) var errorMessage := str(error_info.get("message", "角色生成任务创建失败")) if errorMessage.contains("已经使用过注册角色生成机会") or errorMessage.contains("没有可用的注册角色生成机会"): _awaiting_registration_skin_generation = false _set_workshop_generation_status("该账号已完成注册角色生成,正在进入小镇...") _enter_square() return _set_workshop_generation_status(errorMessage) return var data_variant: Variant = response.get("data", {}) var data: Dictionary = data_variant as Dictionary if data_variant is Dictionary else {} _skin_generation_job_id = str(data.get("job_id", "")).strip_edges() if _skin_generation_job_id.is_empty(): _skin_generation_active = false _set_skin_generation_controls_enabled(true) _set_workshop_generation_status("服务器没有返回生成任务ID") return _set_workshop_generation_status(str(data.get("message", "生成任务已创建,正在排队"))) _poll_skin_generation_job() func _poll_skin_generation_job() -> void: if _skin_generation_poll_in_flight or _skin_generation_job_id.is_empty(): return _skin_generation_poll_in_flight = true var endpoint := SKIN_GENERATION_POLL_ENDPOINT_TEMPLATE % _skin_generation_job_id var api_client := get_node_or_null("/root/ApiClient") if api_client == null or not api_client.has_method("request_json"): _skin_generation_poll_in_flight = false _set_workshop_generation_status("角色生成服务不可用") return api_client.call( "request_json", endpoint, {}, _on_skin_generation_poll_completed, HTTPClient.METHOD_GET, true, SKIN_GENERATION_REQUEST_TIMEOUT ) func _on_skin_generation_poll_completed(success: bool, response: Dictionary, error_info: Dictionary) -> void: _skin_generation_poll_in_flight = false if not success: var response_code := int(error_info.get("response_code", 0)) if response_code == 401 or response_code == 403 or response_code == 404: _skin_generation_active = false _set_skin_generation_controls_enabled(true) _set_workshop_generation_status(str(error_info.get("message", "查询生成状态失败"))) return var data_variant: Variant = response.get("data", {}) var data: Dictionary = data_variant as Dictionary if data_variant is Dictionary else {} var status := str(data.get("status", "")).strip_edges() var message := str(data.get("message", "")).strip_edges() if not message.is_empty(): _set_workshop_generation_status(message) if status == "completed": _finish_backend_skin_generation(data) return if status == "failed": _skin_generation_active = false _set_skin_generation_controls_enabled(true) var errorMessage := str(data.get("error", message)).strip_edges() if errorMessage.contains("已经使用过注册角色生成机会") or errorMessage.contains("没有可用的注册角色生成机会"): _awaiting_registration_skin_generation = false _set_workshop_generation_status("该账号已完成注册角色生成,正在进入小镇...") _enter_square() return _set_workshop_generation_status("生成失败:%s" % errorMessage) func _finish_backend_skin_generation(data: Dictionary) -> void: var spritesheetBase64 := str(data.get("spritesheet_base64", "")).strip_edges() if spritesheetBase64.is_empty(): _skin_generation_active = false _set_skin_generation_controls_enabled(true) _set_workshop_generation_status("生成完成,但服务器没有返回皮肤图片") return var bytes := Marshalls.base64_to_raw(spritesheetBase64) var image := Image.new() var loadError := image.load_png_from_buffer(bytes) if loadError != OK: _skin_generation_active = false _set_skin_generation_controls_enabled(true) _set_workshop_generation_status("服务器返回的皮肤PNG读取失败") return var generatedSkinId := str(data.get("skin_id", "")).strip_edges() if generatedSkinId.is_empty(): _skin_generation_active = false _set_skin_generation_controls_enabled(true) _set_workshop_generation_status("生成完成,但服务器没有返回账号皮肤ID") return var accountSkin := { "id": generatedSkinId, "name": str(data.get("name", "生成角色")), "texture_base64": spritesheetBase64, "texture_url": str(data.get("texture_url", "")), "hframes": 8, "vframes": 4, "source": "generated", } _skin_generation_active = false _skin_generation_job_id = "" _set_skin_generation_controls_enabled(true) if _appearance_manager != null and _appearance_manager.has_method("apply_account_skin_assets"): _appearance_manager.call("apply_account_skin_assets", [accountSkin]) if _appearance_manager != null and _appearance_manager.has_method("set_selected_skin"): _appearance_manager.call("set_selected_skin", generatedSkinId, true) _registration_custom_skin_active = false _registration_skin_source_path = "" _registration_selected_skin_id = generatedSkinId _refresh_appearance_ui() _show_workshop_generated_preview_from_image(image) _awaiting_registration_skin_generation = false _set_workshop_generation_status("新角色皮肤已生成并保存到账号,正在进入小镇...") if _auth_manager != null and _auth_manager.has_method("fetch_profile"): _auth_manager.call("fetch_profile") _enter_square() func _set_workshop_generation_status(message: String) -> void: status_label.text = message if is_instance_valid(workshop_status_label): workshop_status_label.text = message func _set_skin_generation_controls_enabled(enabled: bool) -> void: if is_instance_valid(workshop_source_button): workshop_source_button.disabled = not enabled if is_instance_valid(workshop_skin_name_input): workshop_skin_name_input.editable = enabled if is_instance_valid(workshop_generate_button): workshop_generate_button.disabled = not enabled func _hide_skin_workshop() -> void: if _awaiting_registration_skin_generation: _awaiting_registration_skin_generation = false _set_workshop_generation_status("可以选择初始角色进入,也可以继续生成角色") if is_instance_valid(skin_workshop_overlay): skin_workshop_overlay.hide() return if is_instance_valid(skin_workshop_overlay): skin_workshop_overlay.hide() func is_escape_dismissible() -> bool: return is_instance_valid(skin_workshop_overlay) and skin_workshop_overlay.visible func get_escape_priority() -> int: return 900 func request_escape_close() -> void: _hide_skin_workshop() func _on_workshop_source_pressed() -> void: var err := DisplayServer.file_dialog_show( "选择角色参考图片", _default_picker_dir(), "", false, DisplayServer.FILE_DIALOG_MODE_OPEN_FILE, PackedStringArray(SKIN_NATIVE_FILE_FILTERS), _on_native_workshop_source_file_selected ) if err == OK: _set_workshop_generation_status("请选择角色参考图片") return if _open_macos_file_dialog("选择角色参考图片", "_on_native_workshop_source_file_selected"): return _set_workshop_generation_status("系统文件选择器未打开:%s / %s" % [DisplayServer.get_name(), error_string(err)]) func _on_native_workshop_source_file_selected(status: bool, selectedPaths: PackedStringArray, _selectedFilterIndex: int) -> void: if not status or selectedPaths.is_empty(): _set_workshop_generation_status("已取消角色参考图选择") return var path := selectedPaths[0] var image := Image.load_from_file(path) if image == null: _set_workshop_generation_status("角色参考图读取失败") return _workshop_source_image_path = path if is_instance_valid(workshop_source_label): workshop_source_label.text = path.get_file() _show_workshop_source_preview(path) _set_workshop_generation_status("参考图已选择,可以生成角色皮肤") func _scroll_skin_library(direction: int) -> void: if _registration_official_skins.size() <= 1: return var nextIndex := wrapi(_registration_official_skin_index + direction, 0, _registration_official_skins.size()) var skin := _registration_official_skins[nextIndex] as Dictionary var skinId := str(skin.get("id", "")).strip_edges() if skinId.is_empty(): return _registration_official_skin_index = nextIndex _select_skin(skinId) func _update_skin_library_buttons() -> void: var canScroll := _registration_official_skins.size() > 1 if is_instance_valid(skin_library_prev_button): skin_library_prev_button.visible = canScroll if is_instance_valid(skin_library_next_button): skin_library_next_button.visible = canScroll if not canScroll: return _apply_library_arrow_style(skin_library_prev_button, true) _apply_library_arrow_style(skin_library_next_button, true) func _on_upload_avatar_pressed() -> void: var err := DisplayServer.file_dialog_show( "选择头像图片", _default_picker_dir(), "", false, DisplayServer.FILE_DIALOG_MODE_OPEN_FILE, PackedStringArray(AVATAR_NATIVE_FILE_FILTERS), _on_native_avatar_file_selected ) if err == OK: status_label.text = "请选择头像图片" return if _open_macos_file_dialog("选择头像图片", "_on_native_avatar_file_selected"): status_label.text = "请选择头像图片" return status_label.text = "系统文件选择器未打开:%s / %s" % [DisplayServer.get_name(), error_string(err)] func _on_upload_skin_pressed() -> void: var err := DisplayServer.file_dialog_show( "选择8x4角色皮肤PNG", _default_picker_dir(), "", false, DisplayServer.FILE_DIALOG_MODE_OPEN_FILE, PackedStringArray(SKIN_NATIVE_FILE_FILTERS), _on_native_skin_file_selected ) if err == OK: status_label.text = "请选择8x4角色皮肤PNG" if is_instance_valid(workshop_status_label): workshop_status_label.text = "等待选择8x4透明PNG" return if _open_macos_file_dialog("选择8x4角色皮肤PNG", "_on_native_skin_file_selected"): status_label.text = "请选择8x4角色皮肤PNG" if is_instance_valid(workshop_status_label): workshop_status_label.text = "等待选择8x4透明PNG" return status_label.text = "系统文件选择器未打开:%s / %s" % [DisplayServer.get_name(), error_string(err)] func _open_macos_file_dialog(title: String, callbackMethod: String) -> bool: if OS.get_name() != "macOS": return false var output: Array = [] var exitCode := OS.execute("/usr/bin/osascript", [ "-e", "set selectedFile to choose file", "-e", "POSIX path of selectedFile", ], output, true) if exitCode != OK: var errorText := "" for line in output: errorText += str(line) errorText = errorText.strip_edges() var message := "macOS 文件选择器启动失败" if not errorText.is_empty(): message = "%s:%s" % [message, errorText] if callbackMethod == "_on_native_workshop_source_file_selected": _set_workshop_generation_status(message) else: status_label.text = message return true var selectedPath := "" for line in output: selectedPath += str(line) selectedPath = selectedPath.strip_edges() if selectedPath.is_empty(): if has_method(callbackMethod): call(callbackMethod, false, PackedStringArray(), 0) return true if has_method(callbackMethod): call(callbackMethod, true, PackedStringArray([selectedPath]), 0) return true func _on_native_avatar_file_selected(status: bool, selectedPaths: PackedStringArray, _selectedFilterIndex: int) -> void: if not status or selectedPaths.is_empty(): status_label.text = "已取消头像选择" return _apply_avatar_file(selectedPaths[0]) func _on_native_skin_file_selected(status: bool, selectedPaths: PackedStringArray, _selectedFilterIndex: int) -> void: if not status or selectedPaths.is_empty(): status_label.text = "已取消角色皮肤选择" if is_instance_valid(workshop_status_label): workshop_status_label.text = "已取消角色皮肤选择" return _apply_skin_file(selectedPaths[0]) func _apply_avatar_file(path: String) -> void: var texture := _load_registration_avatar_texture(path) if texture != null: _registration_avatar_texture = texture _registration_avatar_source_path = path _show_registration_avatar_preview(texture) _refresh_appearance_ui() status_label.text = "头像已更新" else: status_label.text = "头像图片读取失败" func _apply_skin_file(path: String) -> void: var validationError := _validate_skin_file(path) if not validationError.is_empty(): status_label.text = validationError if is_instance_valid(workshop_status_label): workshop_status_label.text = validationError return var image := Image.load_from_file(path) if image == null: status_label.text = "角色皮肤图片读取失败" return if image.get_format() != Image.FORMAT_RGBA8: image.convert(Image.FORMAT_RGBA8) _registration_skin_texture = ImageTexture.create_from_image(image) _registration_custom_skin_active = true _registration_skin_source_path = path _registration_selected_skin_id = "custom_skin" _refresh_appearance_ui() if _is_in_registration_choice_flow(): _save_uploaded_skin_and_enter() return status_label.text = "角色皮肤已导入,注册后会保存到账号" if is_instance_valid(workshop_status_label): workshop_status_label.text = "已导入,注册成功后会保存到后端账号" func _save_uploaded_skin_and_enter() -> void: if _auth_manager == null or not _auth_manager.has_method("update_profile"): status_label.text = "认证系统未加载,无法保存上传皮肤" return if _registration_skin_source_path.is_empty(): status_label.text = "请先选择8x4角色皮肤PNG" return var image := Image.load_from_file(_registration_skin_source_path) if image == null: status_label.text = "角色皮肤图片读取失败" return if image.get_format() != Image.FORMAT_RGBA8: image.convert(Image.FORMAT_RGBA8) _pending_profile_sync_after_auth = true _deferred_enter_square_after_profile_sync = true _awaiting_registration_skin_choice = false _set_submitting(true, "正在保存上传角色...") _auth_manager.call("update_profile", { "skin_image_base64": _image_to_png_base64(image), "skin_mime_type": "image/png", "skin_name": "上传角色", }) func _validate_skin_file(path: String) -> String: if path.get_extension().to_lower() != "png": return "角色皮肤必须是PNG文件" var image := Image.load_from_file(path) if image == null: return "角色皮肤图片读取失败" if image.get_width() <= 0 or image.get_height() <= 0: return "角色皮肤图片尺寸无效" if image.get_width() % 8 != 0 or image.get_height() % 4 != 0: return "角色皮肤需为8列x4行PNG" return "" func _default_picker_dir() -> String: var desktop := OS.get_system_dir(OS.SYSTEM_DIR_DESKTOP) if desktop.strip_edges().is_empty(): return OS.get_user_data_dir() return desktop func _apply_library_arrow_style(button: Button, active: bool) -> void: if not is_instance_valid(button): return button.disabled = false button.modulate = Color(1, 1, 1, 1.0 if active else 0.42) func _on_login_pressed() -> void: if _is_submitting: return _set_submitting(true, "登录中...") if _auth_manager == null: _set_submitting(false, "认证系统未加载") return _auth_manager.call("login", login_identifier_input.text, login_password_input.text) func _on_send_register_code_pressed() -> void: if _is_submitting or _is_sending_register_code: return if _auth_manager == null: status_label.text = "认证系统未加载" return _is_sending_register_code = true send_register_code_button.disabled = true status_label.text = "正在发送邮箱验证码..." _auth_manager.call("send_email_verification", register_email_input.text) func _on_register_pressed() -> void: if _is_submitting: return var password := register_password_input.text var confirm := register_confirm_input.text if password != confirm: status_label.text = "两次输入的密码不一致" register_confirm_input.grab_focus() return _set_submitting(true, "注册中...") if _auth_manager == null: _set_submitting(false, "认证系统未加载") return _auth_manager.call("register", register_username_input.text, password, "", register_email_input.text, register_verification_code_input.text, _registration_backend_initial_skin_id() ) func _on_login_succeeded(_user: Dictionary) -> void: _apply_backend_profile_from_auth_manager() _apply_chat_token() status_label.text = "登录成功,正在进入小镇..." if _auth_manager != null and _auth_manager.has_method("fetch_profile"): _auth_manager.call("fetch_profile") _enter_square() func _on_login_failed(message: String) -> void: _set_submitting(false, message) login_password_input.grab_focus() func _on_register_succeeded(_user: Dictionary) -> void: _apply_chat_token() _awaiting_registration_skin_generation = false _awaiting_registration_skin_choice = true _apply_backend_profile_from_auth_manager(false) status_label.text = "注册成功,请选择角色进入小镇" if _sync_registration_assets_to_profile(): status_label.text = "注册成功,正在保存角色..." return _open_registration_skin_choice_step() func _on_register_failed(message: String) -> void: _set_submitting(false, message) if message.contains("验证码"): register_verification_code_input.grab_focus() else: register_username_input.grab_focus() func _on_email_verification_sent(email: String) -> void: _is_sending_register_code = false send_register_code_button.disabled = _is_submitting status_label.text = "验证码已发送到 %s,请查收邮件" % email register_verification_code_input.grab_focus() func _on_email_verification_failed(message: String) -> void: _is_sending_register_code = false send_register_code_button.disabled = _is_submitting status_label.text = message register_email_input.grab_focus() func _resume_cached_session() -> void: _resuming_cached_session = true _cached_resume_start_generation = int(_auth_manager.call("get_session_generation")) if _auth_manager != null else 0 _set_submitting(true, "正在恢复登录状态...") if _auth_manager == null: _resuming_cached_session = false _cached_resume_start_generation = 0 _set_submitting(false, "认证系统未加载") return if str(_auth_manager.call("get_refresh_token")).strip_edges().is_empty(): _resuming_cached_session = false _cached_resume_start_generation = 0 _set_submitting(false, "登录状态已过期,请重新登录") _auth_manager.call("logout") login_identifier_input.grab_focus() return _auth_manager.call("refresh_session") func _should_auto_resume_cached_session() -> bool: return false func _on_auth_state_changed(is_authenticated: bool, _user: Dictionary) -> void: if _pending_profile_sync_after_auth: _pending_profile_sync_after_auth = false if _awaiting_registration_skin_choice: _apply_backend_profile_from_auth_manager(false) _open_registration_skin_choice_step() return _apply_backend_profile_from_auth_manager() if _deferred_enter_square_after_profile_sync: _deferred_enter_square_after_profile_sync = false status_label.text = "正在进入小镇..." _enter_square() return if not _resuming_cached_session: return if not is_authenticated: _resuming_cached_session = false _cached_resume_start_generation = 0 _set_submitting(false, "登录状态已过期,请重新登录") login_identifier_input.grab_focus() return var current_generation := int(_auth_manager.call("get_session_generation")) if current_generation <= _cached_resume_start_generation: return _resuming_cached_session = false _cached_resume_start_generation = 0 _apply_backend_profile_from_auth_manager() _apply_chat_token() if _auth_manager != null and _auth_manager.has_method("fetch_profile"): _auth_manager.call("fetch_profile") status_label.text = "正在进入小镇..." _enter_square() func _on_profile_update_succeeded(_profile: Dictionary) -> void: if not _pending_profile_sync_after_auth: return _pending_profile_sync_after_auth = false if _awaiting_registration_skin_choice: _apply_backend_profile_from_auth_manager(false) _open_registration_skin_choice_step() return _apply_backend_profile_from_auth_manager() if _deferred_enter_square_after_profile_sync: _deferred_enter_square_after_profile_sync = false status_label.text = "正在进入小镇..." _enter_square() func _on_profile_update_failed(message: String) -> void: if not _pending_profile_sync_after_auth: return _pending_profile_sync_after_auth = false _deferred_enter_square_after_profile_sync = false _set_submitting(false, message) status_label.show() if is_instance_valid(registration_character_area): registration_character_area.show() _set_registration_character_controls_visible(true) func _current_selected_skin_id() -> String: var registrationSkinId := _registration_selected_skin_id.strip_edges() if not registrationSkinId.is_empty(): return registrationSkinId if _is_in_registration_choice_flow(): return _first_registration_skin_id() if _appearance_manager == null or not _appearance_manager.has_method("get_selected_skin_id"): return _first_registration_skin_id() var skinId := str(_appearance_manager.call("get_selected_skin_id")).strip_edges() return skinId if not skinId.is_empty() else _first_registration_skin_id() func _first_registration_skin_id() -> String: if _appearance_manager != null: var skins: Array = [] if _appearance_manager.has_method("get_registration_skin_catalog"): skins = _appearance_manager.call("get_registration_skin_catalog") elif _appearance_manager.has_method("get_skin_catalog"): skins = _appearance_manager.call("get_skin_catalog") for skinVariant in skins: if skinVariant is Dictionary and not bool((skinVariant as Dictionary).get("hidden", false)): var skinId := str((skinVariant as Dictionary).get("id", "")).strip_edges() if not skinId.is_empty() and REGISTRATION_OFFICIAL_SKIN_IDS.has(skinId): return skinId return "human_whale_directional_v2_8x4" func _registration_backend_initial_skin_id() -> String: return "" func _apply_backend_profile_from_auth_manager(applySkin: bool = true) -> void: if _auth_manager == null or not _auth_manager.has_method("get_current_profile"): return if _appearance_manager == null or not _appearance_manager.has_method("apply_account_profile"): return var profile_variant: Variant = _auth_manager.call("get_current_profile") if not (profile_variant is Dictionary): return var profile := (profile_variant as Dictionary).duplicate(true) if not applySkin: profile.erase("skin_id") _appearance_manager.call("apply_account_profile", profile) func _apply_chat_token() -> void: if _auth_manager == null or _chat_manager == null: return var token := str(_auth_manager.call("get_access_token")) if not token.is_empty(): _chat_manager.call("set_game_token", token) _chat_manager.call("connect_to_chat_server") func _sync_registration_assets_to_profile() -> bool: if _auth_manager == null or not _auth_manager.has_method("update_profile"): return false if _registration_avatar_source_path.is_empty() and _registration_skin_source_path.is_empty(): return false var payload := {} if not _registration_avatar_source_path.is_empty(): var avatarImage := _load_registration_avatar_image(_registration_avatar_source_path) if avatarImage != null: payload["avatar_image_base64"] = _image_to_png_base64(avatarImage) payload["avatar_mime_type"] = "image/png" if not _registration_skin_source_path.is_empty(): var skinImage := Image.load_from_file(_registration_skin_source_path) if skinImage != null: if skinImage.get_format() != Image.FORMAT_RGBA8: skinImage.convert(Image.FORMAT_RGBA8) payload["skin_image_base64"] = _image_to_png_base64(skinImage) payload["skin_mime_type"] = "image/png" payload["skin_name"] = "上传角色" if payload.is_empty(): return false _pending_profile_sync_after_auth = true if payload.has("skin_image_base64"): _awaiting_registration_skin_choice = false _deferred_enter_square_after_profile_sync = true _auth_manager.call("update_profile", payload) return true func _enter_square() -> void: if _scene_manager != null: _scene_manager.call("change_scene", "square", false) func _set_submitting(is_submitting: bool, message: String) -> void: _is_submitting = is_submitting status_label.text = message login_button.disabled = is_submitting register_button.disabled = is_submitting send_register_code_button.disabled = is_submitting or _is_sending_register_code show_register_button.disabled = is_submitting show_login_button.disabled = is_submitting if is_instance_valid(workshop_button): workshop_button.disabled = is_submitting if is_instance_valid(upload_avatar_button): upload_avatar_button.disabled = is_submitting if is_instance_valid(upload_skin_button): upload_skin_button.disabled = is_submitting if is_instance_valid(enter_with_default_skin_button): enter_with_default_skin_button.disabled = is_submitting func _adopt_ui_tree(node: Node) -> void: for child in node.get_children(): child.owner = self _adopt_ui_tree(child) func _place(control: Control, rect: Rect2) -> void: control.set_anchors_preset(Control.PRESET_TOP_LEFT) control.custom_minimum_size = rect.size control.position = rect.position control.size = rect.size func _label(nodeName: String, text: String, fontSize: int, color: Color, alignment: HorizontalAlignment) -> Label: var label := Label.new() label.name = nodeName label.text = text label.horizontal_alignment = alignment label.add_theme_font_size_override("font_size", fontSize) label.add_theme_color_override("font_color", color) _apply_font(label) return label func _choice_label(nodeName: String, text: String, fontSize: int, color: Color, alignment: HorizontalAlignment) -> Label: var label := _label(nodeName, text, fontSize, color, alignment) _apply_choice_font(label) label.add_theme_color_override("font_outline_color", Color.WHITE) label.add_theme_constant_override("outline_size", 3) return label func _brand_label(nodeName: String, text: String) -> Label: var label := Label.new() label.name = nodeName label.text = text label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.custom_minimum_size = Vector2(0, 56) label.add_theme_font_size_override("font_size", 44) label.add_theme_color_override("font_color", Color(0.045, 0.30, 0.62, 1.0)) label.add_theme_color_override("font_outline_color", Color(1.0, 1.0, 1.0, 0.96)) label.add_theme_color_override("font_shadow_color", Color(0.04, 0.18, 0.36, 0.22)) label.add_theme_constant_override("outline_size", 5) label.add_theme_constant_override("shadow_offset_x", 0) label.add_theme_constant_override("shadow_offset_y", 3) if _brand_font != null: label.add_theme_font_override("font", _brand_font) return label func _form_label(text: String) -> Label: return _label(text.replace("/", "_") + "Label", text, 15, TEXT_COLOR, HORIZONTAL_ALIGNMENT_LEFT) func _register_form_label(text: String) -> Label: var label := _label(text.replace("/", "_") + "RegisterLabel", text, 13, TEXT_COLOR, HORIZONTAL_ALIGNMENT_LEFT) label.custom_minimum_size = Vector2(0, 16) return label func _line_edit(nodeName: String, placeholder: String, secret: bool) -> LineEdit: var input := LineEdit.new() input.name = nodeName input.custom_minimum_size = Vector2(0, 46) input.placeholder_text = placeholder input.secret = secret input.caret_blink = true input.add_theme_color_override("font_color", TEXT_COLOR) input.add_theme_color_override("font_placeholder_color", Color(0.36, 0.40, 0.46, 1.0)) input.add_theme_font_size_override("font_size", 18) input.add_theme_stylebox_override("normal", _input_style()) input.add_theme_stylebox_override("focus", _input_style()) _apply_font(input) return input func _text_edit(nodeName: String, placeholder: String) -> TextEdit: var input := TextEdit.new() input.name = nodeName input.custom_minimum_size = Vector2(0, 112) input.placeholder_text = placeholder input.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY input.caret_blink = true input.scroll_fit_content_height = false input.draw_tabs = false input.draw_spaces = false input.add_theme_color_override("font_color", TEXT_COLOR) input.add_theme_color_override("font_placeholder_color", Color(0.36, 0.40, 0.46, 1.0)) input.add_theme_font_size_override("font_size", 15) input.add_theme_stylebox_override("normal", _input_style()) input.add_theme_stylebox_override("focus", _input_style()) _apply_font(input) return input func _register_line_edit(nodeName: String, placeholder: String, secret: bool) -> LineEdit: var input := _line_edit(nodeName, placeholder, secret) input.custom_minimum_size = Vector2(0, 38) input.add_theme_font_size_override("font_size", 16) return input func _primary_button(nodeName: String, text: String, fontSize: int, minSize: Vector2) -> Button: var button := Button.new() button.name = nodeName button.custom_minimum_size = minSize button.text = text button.focus_mode = Control.FOCUS_NONE button.add_theme_font_size_override("font_size", fontSize) button.add_theme_color_override("font_color", Color.WHITE) _apply_font(button) button.add_theme_stylebox_override("normal", _panel_style(Color(0.20, 0.50, 0.80, 1.0), 8, Color(0.15, 0.40, 0.70, 1.0), 2)) button.add_theme_stylebox_override("hover", _panel_style(Color(0.30, 0.60, 0.90, 1.0), 8, Color(0.20, 0.50, 0.80, 1.0), 2)) button.add_theme_stylebox_override("pressed", _panel_style(Color(0.40, 0.70, 1.0, 1.0), 8, Color(0.30, 0.60, 0.90, 1.0), 2)) return button func _secondary_button(nodeName: String, text: String, fontSize: int, minSize: Vector2) -> Button: var button := Button.new() button.name = nodeName button.custom_minimum_size = minSize button.text = text button.focus_mode = Control.FOCUS_NONE button.add_theme_font_size_override("font_size", fontSize) button.add_theme_color_override("font_color", ACCENT_COLOR) button.add_theme_color_override("font_hover_color", ACCENT_COLOR) button.add_theme_color_override("font_pressed_color", Color(0.06, 0.27, 0.54, 1.0)) button.add_theme_color_override("font_disabled_color", Color(0.52, 0.60, 0.68, 0.55)) _apply_font(button) button.add_theme_stylebox_override("normal", _panel_style(Color(1, 1, 1, 0.88), 7, Color(0.45, 0.66, 0.84, 1.0), 1)) button.add_theme_stylebox_override("hover", _panel_style(Color(0.92, 0.97, 1.0, 0.96), 7, ACCENT_COLOR, 1)) button.add_theme_stylebox_override("pressed", _panel_style(Color(0.85, 0.94, 1.0, 0.98), 7, ACCENT_COLOR, 1)) return button func _apply_registration_choice_button_style(button: Button) -> void: button.add_theme_color_override("font_color", Color.WHITE) button.add_theme_color_override("font_outline_color", Color(0.08, 0.29, 0.60, 0.72)) button.add_theme_constant_override("outline_size", 2) button.add_theme_color_override("font_hover_color", Color(1.0, 1.0, 1.0, 1.0)) button.add_theme_color_override("font_pressed_color", Color(0.86, 0.95, 1.0, 1.0)) button.add_theme_stylebox_override("normal", StyleBoxEmpty.new()) button.add_theme_stylebox_override("hover", StyleBoxEmpty.new()) button.add_theme_stylebox_override("pressed", StyleBoxEmpty.new()) button.add_theme_stylebox_override("focus", StyleBoxEmpty.new()) func _skin_slot_texture_style(selected: bool) -> StyleBoxTexture: var style := StyleBoxTexture.new() style.texture = load(REGISTRATION_CHOICE_SKIN_SLOT_SELECTED_PATH if selected else REGISTRATION_CHOICE_SKIN_SLOT_PATH) as Texture2D style.axis_stretch_horizontal = StyleBoxTexture.AXIS_STRETCH_MODE_STRETCH style.axis_stretch_vertical = StyleBoxTexture.AXIS_STRETCH_MODE_STRETCH style.texture_margin_left = 18.0 style.texture_margin_top = 24.0 style.texture_margin_right = 18.0 style.texture_margin_bottom = 24.0 style.content_margin_left = 4.0 style.content_margin_top = 4.0 style.content_margin_right = 4.0 style.content_margin_bottom = 4.0 return style func _registration_texture_style(texturePath: String, modulate: Color = Color.WHITE) -> StyleBoxTexture: var style := StyleBoxTexture.new() style.texture = load(texturePath) as Texture2D style.modulate_color = modulate style.axis_stretch_horizontal = StyleBoxTexture.AXIS_STRETCH_MODE_STRETCH style.axis_stretch_vertical = StyleBoxTexture.AXIS_STRETCH_MODE_STRETCH style.texture_margin_left = 42.0 style.texture_margin_top = 18.0 style.texture_margin_right = 42.0 style.texture_margin_bottom = 18.0 style.content_margin_left = 16.0 style.content_margin_top = 6.0 style.content_margin_right = 16.0 style.content_margin_bottom = 8.0 return style func _flat_button(nodeName: String, text: String) -> Button: var button := Button.new() button.name = nodeName button.text = text button.flat = true button.focus_mode = Control.FOCUS_NONE button.add_theme_font_size_override("font_size", 13) button.add_theme_color_override("font_color", TEXT_COLOR) button.add_theme_color_override("font_hover_color", ACCENT_COLOR) button.add_theme_color_override("font_pressed_color", Color(0.06, 0.27, 0.54, 1.0)) button.add_theme_color_override("font_disabled_color", Color(0.52, 0.60, 0.68, 0.55)) _apply_font(button) return button func _soft_panel_style() -> StyleBoxFlat: return _panel_style(Color(0.96, 0.99, 1.0, 0.78), 10, Color(0.18, 0.49, 0.78, 0.38), 1) func _choice_card_style(selected: bool) -> StyleBoxFlat: return _panel_style( Color(0.97, 0.995, 1.0, 0.94), 12, ACCENT_COLOR if selected else Color(0.48, 0.72, 0.92, 0.78), 3 if selected else 2 ) func _skin_slot_style(selected: bool, fillColor: Color = Color(0.96, 0.99, 1.0, 0.94)) -> StyleBoxFlat: var style := _panel_style(fillColor, 7, Color(0.25, 0.55, 0.88, 0.95) if selected else Color(0.56, 0.76, 0.94, 0.72), 3 if selected else 1) style.content_margin_left = 3 style.content_margin_top = 3 style.content_margin_right = 3 style.content_margin_bottom = 3 return style func _input_style() -> StyleBoxFlat: var style := _panel_style(Color(0.96, 0.99, 1.0, 0.98), 6, Color(0.055, 0.35, 0.59, 1.0), 3) style.content_margin_left = 14 style.content_margin_top = 8 style.content_margin_right = 14 style.content_margin_bottom = 8 return style func _apply_font(control: Control) -> void: if _ui_font != null: control.add_theme_font_override("font", _ui_font) func _apply_choice_font(control: Control) -> void: if _choice_font != null: control.add_theme_font_override("font", _choice_font) func _panel_style(color: Color, radius: int = 8, borderColor: Color = Color.TRANSPARENT, borderWidth: int = 0) -> 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_right = radius style.corner_radius_bottom_left = radius style.border_width_left = borderWidth style.border_width_top = borderWidth style.border_width_right = borderWidth style.border_width_bottom = borderWidth style.border_color = borderColor return style func _sprite_scale_for_height(texture: Texture2D, vframes: int, targetHeight: float) -> Vector2: if texture == null: return Vector2.ONE var safeVframes: int = maxi(1, vframes) var frameHeight := float(texture.get_height()) / float(safeVframes) if frameHeight <= 0.0: return Vector2.ONE var scale: float = targetHeight / frameHeight return Vector2(scale, scale) func _sprite_scale_for_frame_bounds(texture: Texture2D, hframes: int, vframes: int, bounds: Vector2) -> Vector2: if texture == null: return Vector2.ONE var safeHframes: int = maxi(1, hframes) var safeVframes: int = maxi(1, vframes) var frameSize := Vector2(float(texture.get_width()) / float(safeHframes), float(texture.get_height()) / float(safeVframes)) if frameSize.x <= 0.0 or frameSize.y <= 0.0: return Vector2.ONE var scale: float = minf(bounds.x / frameSize.x, bounds.y / frameSize.y) return Vector2(scale, scale)