feat: expand multiplayer, chat, and release support

- add network NPC synchronization and dialogue interactions
- add world bulletin publishing and display
- improve authentication, session refresh, and appearance sync
- synchronize player direction and movement animations
- improve input focus and progressive Web loading
- add macOS/Windows builds and deployment configuration
- include required fonts, shaders, and runtime assets
This commit is contained in:
2026-09-08 21:37:43 +08:00
parent 175621f66c
commit fc6c3c1bd3
87 changed files with 4546 additions and 506 deletions

View File

@@ -21,6 +21,7 @@ const REMOTE_SKIN_MAX_RETRIES: int = 3
const REMOTE_SKIN_CACHE_DIR: String = "user://skin_cache"
const AVATAR_CORNER_RADIUS_RATIO: float = 0.22
const DEFAULT_CHARACTER_TEXTURE_PATH: String = "res://assets/characters/player_pixel_spritesheet.png"
const AVATAR_MASK_SHADER = preload("res://assets/shaders/avatar_round_mask.gdshader")
const NetworkConfig = preload("res://_Core/utils/NetworkConfig.gd")
const SKINS: Array[Dictionary] = [
@@ -99,9 +100,12 @@ var _remoteSkinRetryCounts: Dictionary = {}
var _accountAvatarUrl: String = ""
var _accountAvatarTexture: Texture2D
var _profileSyncSuppressed: bool = false
var _confirmedSkinId: String = FALLBACK_SKIN_ID
var _pendingSkinId: String = ""
func _ready() -> void:
_load()
_confirmedSkinId = selectedSkinId
_connect_auth_events()
_emit_profile_changed()
@@ -226,12 +230,17 @@ func set_selected_skin(skinId: String, allowLocked: bool = false) -> bool:
if normalizedId == selectedSkinId:
return true
var shouldSync := not _profileSyncSuppressed and _is_account_authenticated()
if shouldSync:
_pendingSkinId = normalizedId
selectedSkinId = normalizedId
_save()
_emit_skin_changed()
_emit_profile_changed()
if not _profileSyncSuppressed:
if shouldSync:
_sync_account_skin_profile()
else:
_confirmedSkinId = normalizedId
return true
func apply_account_profile(profile: Dictionary) -> void:
@@ -240,23 +249,28 @@ func apply_account_profile(profile: Dictionary) -> void:
_clear_session_custom_skin()
var skinId := str(profile.get("skin_id", "")).strip_edges()
_accountAvatarUrl = _absolute_backend_url(str(profile.get("avatar_url", "")).strip_edges())
_accountAvatarTexture = _texture_from_base64(str(profile.get("avatar_base64", "")).strip_edges())
_accountAvatarTexture = _texture_from_base64(str(profile.get("avatar_base64", "")).strip_edges(), true)
var ownedSkinsVariant: Variant = profile.get("owned_skins", [])
if ownedSkinsVariant is Array:
apply_account_skin_assets(ownedSkinsVariant as Array)
var ownedVariant: Variant = profile.get("owned_skin_ids", [])
if ownedVariant is Array:
set_owned_skin_ids(ownedVariant)
if not skinId.is_empty():
var shouldApplySkin := _pendingSkinId.is_empty() or skinId == _pendingSkinId
if not skinId.is_empty() and shouldApplySkin:
grant_owned_skin(skinId)
set_selected_skin(skinId, true)
else:
_confirmedSkinId = skinId
if skinId == _pendingSkinId:
_pendingSkinId = ""
elif skinId.is_empty() and _pendingSkinId.is_empty():
var fallbackSkinId := _first_owned_skin_id()
if selectedSkinId != fallbackSkinId:
selectedSkinId = fallbackSkinId
_save()
_emit_skin_changed()
_emit_profile_changed()
_confirmedSkinId = fallbackSkinId
_profileSyncSuppressed = false
if _accountAvatarTexture != null or not _accountAvatarUrl.is_empty():
selectedAvatarId = CUSTOM_AVATAR_ID
@@ -306,7 +320,7 @@ func apply_account_avatar_base64(avatarUrl: String, avatarBase64: String) -> voi
_accountAvatarUrl = _absolute_backend_url(avatarUrl.strip_edges())
_customAvatarTexture = null
_customAvatarActive = false
_accountAvatarTexture = _texture_from_base64(avatarBase64)
_accountAvatarTexture = _texture_from_base64(avatarBase64, true)
if _accountAvatarTexture != null or not _accountAvatarUrl.is_empty():
selectedAvatarId = CUSTOM_AVATAR_ID
else:
@@ -715,11 +729,45 @@ func _sync_account_skin_profile() -> void:
func _connect_auth_events() -> void:
var eventSystem := get_node_or_null("/root/EventSystem")
if eventSystem == null:
if eventSystem != null:
eventSystem.call("connect_event", EventNames.AUTH_LOGOUT, _on_auth_logout, self)
var authManager := get_node_or_null("/root/AuthManager")
if authManager != null and authManager.has_signal("appearance_update_succeeded"):
authManager.connect("appearance_update_succeeded", _on_appearance_update_succeeded)
if authManager != null and authManager.has_signal("appearance_update_failed"):
authManager.connect("appearance_update_failed", _on_appearance_update_failed)
func _on_appearance_update_succeeded(requestedSkinId: String, profile: Dictionary) -> void:
if requestedSkinId != _pendingSkinId:
return
eventSystem.call("connect_event", EventNames.AUTH_LOGOUT, _on_auth_logout, self)
var confirmedSkinId := str(profile.get("skin_id", requestedSkinId)).strip_edges()
if confirmedSkinId.is_empty():
confirmedSkinId = requestedSkinId
_pendingSkinId = ""
_confirmedSkinId = confirmedSkinId
if selectedSkinId != confirmedSkinId:
_apply_selected_skin_locally(confirmedSkinId)
func _on_appearance_update_failed(requestedSkinId: String, _message: String) -> void:
if requestedSkinId != _pendingSkinId:
return
_pendingSkinId = ""
if not _confirmedSkinId.is_empty() and selectedSkinId == requestedSkinId:
_apply_selected_skin_locally(_confirmedSkinId)
func _apply_selected_skin_locally(skinId: String) -> void:
selectedSkinId = skinId
_save()
_emit_skin_changed()
_emit_profile_changed()
func _is_account_authenticated() -> bool:
var authManager := get_node_or_null("/root/AuthManager")
return authManager != null and authManager.has_method("is_authenticated") and bool(authManager.call("is_authenticated"))
func _on_auth_logout(_data: Variant = null) -> void:
_pendingSkinId = ""
_confirmedSkinId = FALLBACK_SKIN_ID
clear_account_profile()
func get_profile_payload() -> Dictionary:
@@ -762,11 +810,19 @@ func _get_or_create_avatar_texture_rect(panel: PanelContainer) -> TextureRect:
textureRect.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
textureRect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
textureRect.stretch_mode = TextureRect.STRETCH_SCALE
textureRect.material = _create_avatar_mask_material()
textureRect.custom_minimum_size = panel.custom_minimum_size
textureRect.set_anchors_preset(Control.PRESET_FULL_RECT)
panel.add_child(textureRect)
return textureRect
func _create_avatar_mask_material() -> ShaderMaterial:
var material := ShaderMaterial.new()
material.shader = AVATAR_MASK_SHADER
material.set_shader_parameter("corner_radius", 0.22)
material.set_shader_parameter("edge_feather", 0.008)
return material
func _normalize_account_skin_asset(skin: Dictionary) -> Dictionary:
var normalized := skin.duplicate(true)
var skinId := str(normalized.get("id", normalized.get("skin_id", ""))).strip_edges()
@@ -784,7 +840,7 @@ func _normalize_account_skin_asset(skin: Dictionary) -> Dictionary:
normalized["account_asset"] = true
return normalized
func _texture_from_base64(base64Text: String) -> Texture2D:
func _texture_from_base64(base64Text: String, roundAvatar: bool = false) -> Texture2D:
var bytes := Marshalls.base64_to_raw(base64Text.strip_edges())
if bytes.is_empty():
return null
@@ -792,6 +848,9 @@ func _texture_from_base64(base64Text: String) -> Texture2D:
var err := image.load_png_from_buffer(bytes)
if err != OK:
return null
if roundAvatar:
image = _create_square_avatar_image(image)
_apply_rounded_rect_alpha(image)
return ImageTexture.create_from_image(image)
func _absolute_backend_url(path: String) -> String: