Files
whale-town-front-v2/scenes/ui/AuthScene.gd

2449 lines
98 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)