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:
@@ -6,10 +6,23 @@ signal player_moved(position: Vector2)
|
||||
|
||||
# 常量定义
|
||||
const MOVE_SPEED = 200.0
|
||||
const PLAYER_COLLISION_LAYER = 1
|
||||
const PLAYER_COLLISION_MASK = 3
|
||||
const WORLD_SORT_Z_OFFSET = 2048
|
||||
const INTERACTION_COLLISION_MASK = 2
|
||||
const WALK_ANIMATION_LENGTH = 0.8
|
||||
const NAME_LABEL_OFFSET: Vector2 = Vector2(-70, -112)
|
||||
const NAME_LABEL_RENDER_SCALE: float = 0.5
|
||||
const NAME_LABEL_FONT_SIZE: int = 16
|
||||
const NAME_LABEL_VISUAL_HEIGHT: int = 15
|
||||
const NAME_LABEL_VISUAL_MIN_WIDTH: int = 64
|
||||
const NAME_LABEL_VISUAL_MAX_WIDTH: int = 100
|
||||
const NAME_LABEL_VISUAL_CHAR_WIDTH: int = 10
|
||||
const NAME_LABEL_OFFSET_Y: float = -72.0
|
||||
const NAME_LABEL_SIDE_OFFSET_Y: float = -84.0
|
||||
const NAME_LABEL_BACK_OFFSET_Y: float = -78.0
|
||||
const WORLD_TEXT_THEME = preload("res://assets/ui/world_text_theme.tres")
|
||||
const NAME_LABEL_FONT_ZH: FontFile = preload("res://assets/fonts/fusion-pixel-12px/fusion-pixel-12px-proportional-zh_hans.ttf.woff2")
|
||||
const NAME_LABEL_FONT_LATIN: FontFile = preload("res://assets/fonts/fusion-pixel-12px/fusion-pixel-12px-proportional-latin.ttf.woff2")
|
||||
const DIRECTION_ROWS: Dictionary = {
|
||||
"down": 0,
|
||||
"up": 1,
|
||||
@@ -24,9 +37,32 @@ const DIRECTION_ROWS: Dictionary = {
|
||||
|
||||
var lastDirection: String = "down"
|
||||
var _nameLabel: Label
|
||||
var _nicknameFont: FontFile
|
||||
var _cachedNameLabelText: String = ""
|
||||
var _cachedNameLabelDirection: String = ""
|
||||
var _cachedNameLabelVisible: bool = false
|
||||
var _movementLocked: bool = false
|
||||
var _lastEmittedMovementState: String = "idle"
|
||||
var _spectator_mode: bool = false
|
||||
|
||||
func set_spectator_mode(enabled: bool) -> void:
|
||||
_spectator_mode = enabled
|
||||
if is_instance_valid(sprite):
|
||||
sprite.visible = not enabled
|
||||
if is_instance_valid(_nameLabel):
|
||||
_nameLabel.visible = not enabled
|
||||
if is_instance_valid(ray_cast):
|
||||
ray_cast.enabled = not enabled
|
||||
if enabled:
|
||||
collision_layer = 0
|
||||
collision_mask = 0
|
||||
else:
|
||||
collision_layer = PLAYER_COLLISION_LAYER
|
||||
collision_mask = PLAYER_COLLISION_MASK
|
||||
|
||||
func _ready() -> void:
|
||||
collision_layer = PLAYER_COLLISION_LAYER
|
||||
collision_mask = PLAYER_COLLISION_MASK
|
||||
_reset_movement_input_state()
|
||||
_apply_current_appearance()
|
||||
_subscribe_to_appearance_events()
|
||||
@@ -81,6 +117,8 @@ func _physics_process(delta: float) -> void:
|
||||
_handle_interaction()
|
||||
|
||||
func _handle_interaction() -> void:
|
||||
if _spectator_mode:
|
||||
return
|
||||
if _is_text_input_focused():
|
||||
return
|
||||
if Input.is_action_just_pressed("interact"):
|
||||
@@ -100,6 +138,7 @@ func _handle_movement(_delta: float) -> void:
|
||||
velocity = Vector2.ZERO
|
||||
_play_idle_animation()
|
||||
move_and_slide()
|
||||
_emit_movement_sync("idle")
|
||||
return
|
||||
|
||||
# 输入框获得焦点时禁止移动,避免聊天/表单输入影响角色
|
||||
@@ -108,6 +147,7 @@ func _handle_movement(_delta: float) -> void:
|
||||
velocity = Vector2.ZERO
|
||||
_play_idle_animation()
|
||||
move_and_slide()
|
||||
_emit_movement_sync("idle")
|
||||
return
|
||||
|
||||
# 获取移动向量 (参考 docs/02-开发规范/输入映射配置.md)
|
||||
@@ -126,17 +166,28 @@ func _handle_movement(_delta: float) -> void:
|
||||
|
||||
move_and_slide()
|
||||
|
||||
# 发送移动事件 (如果位置发生明显变化)
|
||||
if velocity.length() > 0:
|
||||
# 移动中持续发送位置;停止时额外发送一次 idle,供远端切回待机动画。
|
||||
var movementState := "walk" if velocity.length() > 0 else "idle"
|
||||
_emit_movement_sync(movementState)
|
||||
|
||||
func _emit_movement_sync(movementState: String) -> void:
|
||||
if _spectator_mode:
|
||||
return
|
||||
if movementState == "walk":
|
||||
player_moved.emit(global_position)
|
||||
if movementState == "walk" or movementState != _lastEmittedMovementState:
|
||||
EventSystem.emit_event(EventNames.PLAYER_MOVED, {
|
||||
"position": global_position
|
||||
"position": global_position,
|
||||
"direction": lastDirection,
|
||||
"movement_state": movementState
|
||||
})
|
||||
_lastEmittedMovementState = movementState
|
||||
|
||||
func _update_animation_state(direction: Vector2) -> void:
|
||||
if not animation_player:
|
||||
return
|
||||
|
||||
var previousDirection := lastDirection
|
||||
# Determine primary direction
|
||||
if abs(direction.x) > abs(direction.y):
|
||||
if direction.x > 0:
|
||||
@@ -153,6 +204,8 @@ func _update_animation_state(direction: Vector2) -> void:
|
||||
lastDirection = "up"
|
||||
ray_cast.target_position = Vector2(0, -60)
|
||||
|
||||
if lastDirection != previousDirection:
|
||||
_update_name_label()
|
||||
animation_player.play("walk_" + lastDirection)
|
||||
|
||||
func _play_idle_animation() -> void:
|
||||
@@ -241,19 +294,61 @@ func _create_name_label() -> void:
|
||||
return
|
||||
_nameLabel = Label.new()
|
||||
_nameLabel.name = "NameLabel"
|
||||
_nameLabel.position = NAME_LABEL_OFFSET
|
||||
_nameLabel.custom_minimum_size = Vector2(140, 28)
|
||||
_nameLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_nameLabel.add_theme_color_override("font_color", Color(0.188, 0.294, 0.424))
|
||||
_nameLabel.add_theme_font_size_override("font_size", 14)
|
||||
_nameLabel.add_theme_stylebox_override("normal", _create_name_label_style())
|
||||
add_child(_nameLabel)
|
||||
|
||||
func _update_name_label() -> void:
|
||||
if not is_instance_valid(_nameLabel):
|
||||
return
|
||||
_nameLabel.text = _current_username()
|
||||
_nameLabel.visible = _settings_bool("show_name_always", false)
|
||||
var displayName := _current_username()
|
||||
var showName := _settings_bool("show_name_always", false)
|
||||
if displayName == _cachedNameLabelText and lastDirection == _cachedNameLabelDirection and showName == _cachedNameLabelVisible:
|
||||
return
|
||||
var visualWidth := _name_label_visual_width(displayName)
|
||||
var renderWidth := ceili(float(visualWidth) / NAME_LABEL_RENDER_SCALE)
|
||||
var renderHeight := ceili(float(NAME_LABEL_VISUAL_HEIGHT) / NAME_LABEL_RENDER_SCALE)
|
||||
|
||||
_nameLabel.theme = WORLD_TEXT_THEME
|
||||
_nameLabel.text = displayName
|
||||
_nameLabel.visible = showName
|
||||
_nameLabel.z_index = 30
|
||||
_nameLabel.scale = Vector2.ONE * NAME_LABEL_RENDER_SCALE
|
||||
_nameLabel.position = Vector2(float(visualWidth) * -0.5, _name_label_offset_y())
|
||||
_nameLabel.custom_minimum_size = Vector2(renderWidth, renderHeight)
|
||||
_nameLabel.size = _nameLabel.custom_minimum_size
|
||||
_nameLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_nameLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_nameLabel.clip_text = true
|
||||
_nameLabel.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
_nameLabel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_nameLabel.add_theme_color_override("font_color", Color(0.09, 0.25, 0.31, 1.0))
|
||||
_nameLabel.add_theme_color_override("font_outline_color", Color(1.0, 0.965, 0.88, 0.98))
|
||||
_nameLabel.add_theme_constant_override("outline_size", 3)
|
||||
_nameLabel.add_theme_font_override("font", _get_nickname_font())
|
||||
_nameLabel.add_theme_font_size_override("font_size", NAME_LABEL_FONT_SIZE)
|
||||
_nameLabel.add_theme_stylebox_override("normal", StyleBoxEmpty.new())
|
||||
_cachedNameLabelText = displayName
|
||||
_cachedNameLabelDirection = lastDirection
|
||||
_cachedNameLabelVisible = showName
|
||||
|
||||
func _name_label_visual_width(displayName: String) -> int:
|
||||
var measuredWidth := _get_nickname_font().get_string_size(displayName, HORIZONTAL_ALIGNMENT_LEFT, -1, NAME_LABEL_FONT_SIZE).x
|
||||
var visualWidth := ceili(measuredWidth * NAME_LABEL_RENDER_SCALE + 20.0)
|
||||
return clampi(visualWidth, NAME_LABEL_VISUAL_MIN_WIDTH, NAME_LABEL_VISUAL_MAX_WIDTH)
|
||||
|
||||
func _get_nickname_font() -> FontFile:
|
||||
if _nicknameFont == null:
|
||||
_nicknameFont = NAME_LABEL_FONT_ZH.duplicate() as FontFile
|
||||
_nicknameFont.fallbacks = [NAME_LABEL_FONT_LATIN]
|
||||
return _nicknameFont
|
||||
|
||||
func _name_label_offset_y() -> float:
|
||||
match lastDirection:
|
||||
"left", "right":
|
||||
return NAME_LABEL_SIDE_OFFSET_Y
|
||||
"up":
|
||||
return NAME_LABEL_BACK_OFFSET_Y
|
||||
_:
|
||||
return NAME_LABEL_OFFSET_Y
|
||||
|
||||
func _current_username() -> String:
|
||||
var authManager := get_node_or_null("/root/AuthManager")
|
||||
@@ -268,16 +363,3 @@ func _settings_bool(key: String, defaultValue: bool) -> bool:
|
||||
if settingsManager != null and settingsManager.has_method("get_bool"):
|
||||
return bool(settingsManager.call("get_bool", key))
|
||||
return defaultValue
|
||||
|
||||
func _create_name_label_style() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(1.0, 1.0, 1.0, 0.74)
|
||||
style.corner_radius_top_left = 10
|
||||
style.corner_radius_top_right = 10
|
||||
style.corner_radius_bottom_left = 10
|
||||
style.corner_radius_bottom_right = 10
|
||||
style.content_margin_left = 8
|
||||
style.content_margin_right = 8
|
||||
style.content_margin_top = 4
|
||||
style.content_margin_bottom = 4
|
||||
return style
|
||||
|
||||
Reference in New Issue
Block a user