2449 lines
98 KiB
GDScript
2449 lines
98 KiB
GDScript
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 NetworkConfig = preload("res://_Core/utils/NetworkConfig.gd")
|
||
const WebFilePicker = preload("res://_Core/utils/WebFilePicker.gd")
|
||
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"]
|
||
const WEB_FILE_MAX_BYTES: int = 8 * 1024 * 1024
|
||
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: Font
|
||
var _resuming_cached_session: bool = false
|
||
var _cached_resume_start_generation: int = 0
|
||
var _skin_generation_create_request: HTTPRequest
|
||
var _skin_generation_poll_request: HTTPRequest
|
||
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
|
||
var _web_file_picker: RefCounted = WebFilePicker.new()
|
||
|
||
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"])
|
||
if OS.get_name() == "Web":
|
||
_choice_font = _ui_font
|
||
else:
|
||
var systemFont := SystemFont.new()
|
||
systemFont.font_names = PackedStringArray(["STHeiti Medium", "Hiragino Sans GB", "PingFang SC", "Microsoft YaHei"])
|
||
_choice_font = systemFont
|
||
_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:
|
||
_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")
|
||
|
||
_setup_skin_generation_requests()
|
||
_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()
|
||
elif _auth_manager != null and _auth_manager.has_method("consume_browser_bootstrap_kind"):
|
||
var bootstrapKind := str(_auth_manager.call("consume_browser_bootstrap_kind"))
|
||
if not bootstrapKind.is_empty():
|
||
call_deferred("_complete_browser_bootstrap", bootstrapKind)
|
||
else:
|
||
login_identifier_input.grab_focus()
|
||
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)
|
||
if _auth_manager.has_signal("browser_bootstrap_received"):
|
||
_auth_manager.connect("browser_bootstrap_received", _complete_browser_bootstrap)
|
||
|
||
func _complete_browser_bootstrap(kind: String) -> void:
|
||
if _is_submitting:
|
||
return
|
||
_set_submitting(true, "正在准备小镇...")
|
||
if kind == "register":
|
||
_on_register_succeeded(_auth_manager.call("get_current_user"))
|
||
return
|
||
_on_login_succeeded(_auth_manager.call("get_current_user"))
|
||
|
||
func _setup_skin_generation_requests() -> void:
|
||
_skin_generation_create_request = HTTPRequest.new()
|
||
_skin_generation_create_request.name = "SkinGenerationCreateRequest"
|
||
_skin_generation_create_request.timeout = SKIN_GENERATION_REQUEST_TIMEOUT
|
||
_skin_generation_create_request.request_completed.connect(_on_skin_generation_create_completed)
|
||
add_child(_skin_generation_create_request)
|
||
|
||
_skin_generation_poll_request = HTTPRequest.new()
|
||
_skin_generation_poll_request.name = "SkinGenerationPollRequest"
|
||
_skin_generation_poll_request.timeout = SKIN_GENERATION_REQUEST_TIMEOUT
|
||
_skin_generation_poll_request.request_completed.connect(_on_skin_generation_poll_completed)
|
||
add_child(_skin_generation_poll_request)
|
||
|
||
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:
|
||
if is_instance_valid(_skin_generation_create_request):
|
||
_skin_generation_create_request.cancel_request()
|
||
if is_instance_valid(_skin_generation_poll_request):
|
||
_skin_generation_poll_request.cancel_request()
|
||
_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 err := _skin_generation_create_request.request(
|
||
"%s%s" % [NetworkConfig.get_api_base_url(), SKIN_GENERATION_CREATE_ENDPOINT],
|
||
_auth_json_headers(),
|
||
HTTPClient.METHOD_POST,
|
||
JSON.stringify(payload)
|
||
)
|
||
if err != OK:
|
||
_skin_generation_active = false
|
||
_set_skin_generation_controls_enabled(true)
|
||
_set_workshop_generation_status("角色生成请求发送失败:%s" % error_string(err))
|
||
|
||
func _on_skin_generation_create_completed(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||
var parsed := _parse_skin_generation_response(result, responseCode, body)
|
||
if not bool(parsed.get("ok", false)):
|
||
_skin_generation_active = false
|
||
_set_skin_generation_controls_enabled(true)
|
||
var errorMessage := str(parsed.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: Dictionary = parsed.get("data", {})
|
||
_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 err := _skin_generation_poll_request.request(
|
||
"%s%s" % [NetworkConfig.get_api_base_url(), endpoint],
|
||
_auth_json_headers(),
|
||
HTTPClient.METHOD_GET
|
||
)
|
||
if err != OK:
|
||
_skin_generation_poll_in_flight = false
|
||
_set_workshop_generation_status("查询生成状态失败:%s" % error_string(err))
|
||
|
||
func _on_skin_generation_poll_completed(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||
_skin_generation_poll_in_flight = false
|
||
var parsed := _parse_skin_generation_response(result, responseCode, body)
|
||
if not bool(parsed.get("ok", false)):
|
||
if responseCode == 401 or responseCode == 403 or responseCode == 404:
|
||
_skin_generation_active = false
|
||
_set_skin_generation_controls_enabled(true)
|
||
_set_workshop_generation_status(str(parsed.get("message", "查询生成状态失败")))
|
||
return
|
||
|
||
var data: Dictionary = parsed.get("data", {})
|
||
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 _json_headers() -> PackedStringArray:
|
||
return PackedStringArray([
|
||
"Content-Type: application/json",
|
||
"Accept: application/json",
|
||
])
|
||
|
||
func _auth_json_headers() -> PackedStringArray:
|
||
var headers := _json_headers()
|
||
if _auth_manager != null and _auth_manager.has_method("get_access_token"):
|
||
var token := str(_auth_manager.call("get_access_token")).strip_edges()
|
||
if not token.is_empty():
|
||
headers.append("Authorization: Bearer %s" % token)
|
||
return headers
|
||
|
||
func _parse_skin_generation_response(result: int, responseCode: int, body: PackedByteArray) -> Dictionary:
|
||
if result != HTTPRequest.RESULT_SUCCESS:
|
||
return {
|
||
"ok": false,
|
||
"message": "网络请求失败:%s" % _http_result_to_string(result),
|
||
}
|
||
|
||
var bodyText := body.get_string_from_utf8()
|
||
var json := JSON.new()
|
||
var error := json.parse(bodyText)
|
||
if error != OK or not (json.data is Dictionary):
|
||
return {
|
||
"ok": false,
|
||
"message": "服务器响应解析失败",
|
||
}
|
||
|
||
var response: Dictionary = json.data as Dictionary
|
||
var success := responseCode >= 200 and responseCode < 300 and bool(response.get("success", true))
|
||
if not success:
|
||
return {
|
||
"ok": false,
|
||
"message": str(response.get("message", "请求失败")),
|
||
"response_code": responseCode,
|
||
}
|
||
|
||
var dataVariant: Variant = response.get("data", response)
|
||
if not (dataVariant is Dictionary):
|
||
return {
|
||
"ok": false,
|
||
"message": "服务器响应格式错误",
|
||
}
|
||
|
||
return {
|
||
"ok": true,
|
||
"data": dataVariant as Dictionary,
|
||
}
|
||
|
||
func _http_result_to_string(result: int) -> String:
|
||
match result:
|
||
HTTPRequest.RESULT_SUCCESS:
|
||
return "SUCCESS"
|
||
HTTPRequest.RESULT_TIMEOUT:
|
||
return "TIMEOUT"
|
||
HTTPRequest.RESULT_CANT_CONNECT:
|
||
return "CANT_CONNECT"
|
||
HTTPRequest.RESULT_CANT_RESOLVE:
|
||
return "CANT_RESOLVE"
|
||
HTTPRequest.RESULT_CONNECTION_ERROR:
|
||
return "CONNECTION_ERROR"
|
||
HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR:
|
||
return "TLS_HANDSHAKE_ERROR"
|
||
_:
|
||
return "UNKNOWN_%d" % result
|
||
|
||
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 _on_workshop_source_pressed() -> void:
|
||
if _open_web_file_picker("workshop"):
|
||
_set_workshop_generation_status("请选择角色参考图片")
|
||
return
|
||
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:
|
||
if _open_web_file_picker("avatar"):
|
||
status_label.text = "请选择头像图片"
|
||
return
|
||
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:
|
||
if _open_web_file_picker("skin"):
|
||
status_label.text = "请选择8x4角色皮肤PNG"
|
||
if is_instance_valid(workshop_status_label):
|
||
workshop_status_label.text = "等待选择8x4透明PNG"
|
||
return
|
||
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_web_file_picker(kind: String) -> bool:
|
||
if OS.get_name() != "Web" or _web_file_picker == null:
|
||
return false
|
||
var accept := ".png" if kind == "skin" else ".png,.jpg,.jpeg,.webp"
|
||
return bool(_web_file_picker.call("open", accept, WEB_FILE_MAX_BYTES, _on_web_file_selected.bind(kind)))
|
||
|
||
func _on_web_file_selected(status: String, fileName: String, _mimeType: String, base64Data: String, kind: String) -> void:
|
||
if status == "cancel":
|
||
if kind == "workshop":
|
||
_set_workshop_generation_status("已取消角色参考图选择")
|
||
else:
|
||
status_label.text = "已取消图片选择"
|
||
return
|
||
if status == "too_large":
|
||
var message := "图片不能超过8MB"
|
||
if kind == "workshop":
|
||
_set_workshop_generation_status(message)
|
||
else:
|
||
status_label.text = message
|
||
return
|
||
if status != "ok":
|
||
var message := "浏览器读取图片失败"
|
||
if kind == "workshop":
|
||
_set_workshop_generation_status(message)
|
||
else:
|
||
status_label.text = message
|
||
return
|
||
|
||
var path := WebFilePicker.save_base64_file(base64Data, fileName, kind)
|
||
if path.is_empty():
|
||
status_label.text = "图片暂存失败"
|
||
return
|
||
match kind:
|
||
"avatar":
|
||
_on_native_avatar_file_selected(true, PackedStringArray([path]), 0)
|
||
"skin":
|
||
_on_native_skin_file_selected(true, PackedStringArray([path]), 0)
|
||
"workshop":
|
||
_on_native_workshop_source_file_selected(true, PackedStringArray([path]), 0)
|
||
|
||
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)
|