forked from xiangwang25/whale-town-front-v2
406 lines
16 KiB
GDScript
406 lines
16 KiB
GDScript
extends CharacterBody2D
|
|
class_name RemotePlayer
|
|
|
|
# 远程玩家脚本
|
|
# 负责处理位置同步和动画播放
|
|
# 严格遵循 Visual Only 原则:无输入处理,无物理碰撞
|
|
|
|
var userId: String = ""
|
|
var username: String = ""
|
|
var skinId: String = ""
|
|
var skinAsset: Dictionary = {}
|
|
var avatarId: String = ""
|
|
var targetPosition: Vector2 = Vector2.ZERO
|
|
var cafeCompanionData: Dictionary = {}
|
|
|
|
# 内部状态
|
|
var lastDirection: String = "down"
|
|
const WORLD_SORT_Z_OFFSET = 2048
|
|
const WALK_ANIMATION_LENGTH = 0.8
|
|
const NAME_LABEL_OFFSET: Vector2 = Vector2(-70, -112)
|
|
const CAFE_NAME_LABEL_RENDER_SCALE: float = 0.5
|
|
const CAFE_NAME_LABEL_FONT_SIZE: int = 24
|
|
const CAFE_NAME_LABEL_VISUAL_HEIGHT: int = 22
|
|
const CAFE_NAME_LABEL_VISUAL_MIN_WIDTH: int = 76
|
|
const CAFE_NAME_LABEL_VISUAL_MAX_WIDTH: int = 118
|
|
const CAFE_NAME_LABEL_VISUAL_CHAR_WIDTH: int = 12
|
|
const CAFE_NAME_LABEL_OFFSET_Y: float = -96.0
|
|
const WORLD_TEXT_THEME = preload("res://assets/ui/world_text_theme.tres")
|
|
const DIRECTION_ROWS: Dictionary = {
|
|
"down": 0,
|
|
"up": 1,
|
|
"right": 2,
|
|
"left": 3,
|
|
}
|
|
@onready var animation_player: AnimationPlayer = $AnimationPlayer
|
|
@onready var sprite: Sprite2D = $Sprite2D
|
|
var _nameLabel: Label
|
|
var _cafeCompanionTarget: CafeCompanionTarget
|
|
var _interactable: InteractableComponent
|
|
|
|
func _ready() -> void:
|
|
_interactable = InteractableComponent.new()
|
|
_interactable.interaction_distance = 160.0
|
|
add_child(_interactable)
|
|
# 初始化时确保无物理处理
|
|
set_physics_process(false)
|
|
# 初始位置设为当前位置
|
|
targetPosition = global_position
|
|
_apply_appearance()
|
|
_update_world_sort_z()
|
|
_create_name_label()
|
|
_update_name_label()
|
|
_subscribe_to_settings_events()
|
|
|
|
# 确保禁用物理碰撞 (双重保险)
|
|
if has_node("CollisionShape2D"):
|
|
$CollisionShape2D.disabled = true
|
|
|
|
func is_interaction_active(_component: InteractableComponent) -> bool:
|
|
return not userId.strip_edges().is_empty()
|
|
|
|
func build_interaction_actions(_component: InteractableComponent, _player: Node2D) -> Array[InteractionAction]:
|
|
var actions: Array[InteractionAction] = []
|
|
var display_name := username if not username.strip_edges().is_empty() else "玩家"
|
|
actions.append(InteractionAction.create("player_card:%s" % userId, "查看 %s 的社区名片" % display_name, 60, Callable(self, "_show_community_profile")))
|
|
actions.append(InteractionAction.create("player_dm:%s" % userId, "私聊 %s" % display_name, 61, Callable(self, "_open_private_chat")))
|
|
actions.append(InteractionAction.create("player_friend:%s" % userId, "申请添加 %s 为好友" % display_name, 62, Callable(self, "_request_friend")))
|
|
return actions
|
|
|
|
func _show_community_profile() -> void:
|
|
var socialManager := get_node_or_null("/root/SocialManager")
|
|
if socialManager != null and socialManager.has_method("show_profile"):
|
|
socialManager.call("show_profile", userId)
|
|
|
|
func _open_private_chat() -> void:
|
|
var socialManager := get_node_or_null("/root/SocialManager")
|
|
if socialManager != null and socialManager.has_method("open_private_chat"):
|
|
socialManager.call("open_private_chat", userId, username)
|
|
|
|
func _request_friend() -> void:
|
|
var socialManager := get_node_or_null("/root/SocialManager")
|
|
if socialManager != null and socialManager.has_method("request_friend"):
|
|
socialManager.call("request_friend", userId, username)
|
|
|
|
func _exit_tree() -> void:
|
|
var eventSystem := get_node_or_null("/root/EventSystem")
|
|
if eventSystem != null:
|
|
eventSystem.call("disconnect_event", EventNames.SETTINGS_CHANGED, _on_settings_changed, self)
|
|
|
|
func _process(delta: float) -> void:
|
|
# 1. 平滑移动插值
|
|
var currentPos: Vector2 = global_position
|
|
var distance: float = currentPos.distance_to(targetPosition)
|
|
|
|
if distance > 1.0:
|
|
# 简单的线性插值,速度系数 10.0 可根据需要调整
|
|
var newPos: Vector2 = currentPos.lerp(targetPosition, 10.0 * delta)
|
|
|
|
# 计算移动向量用于动画朝向
|
|
var moveVec: Vector2 = newPos - currentPos
|
|
_update_animation(moveVec)
|
|
|
|
global_position = newPos
|
|
_update_world_sort_z()
|
|
else:
|
|
# 距离很近时直接吸附并播放待机动画
|
|
global_position = targetPosition
|
|
_update_world_sort_z()
|
|
_play_idle_animation()
|
|
|
|
# 统一初始化方法
|
|
# data: 包含 camelCase 字段的字典 (userId, username, position 等)
|
|
func setup(data: Dictionary) -> void:
|
|
if data.has("userId"):
|
|
userId = data.userId
|
|
if data.has("username"):
|
|
username = str(data.username)
|
|
if data.has("skin_id"):
|
|
skinId = str(data.get("skin_id", ""))
|
|
elif data.has("skinId"):
|
|
skinId = str(data.get("skinId", ""))
|
|
if data.has("skin_asset"):
|
|
var skinAssetData: Variant = data.get("skin_asset", {})
|
|
skinAsset = skinAssetData if skinAssetData is Dictionary else {}
|
|
elif data.has("skinAsset"):
|
|
var skinAssetPayload: Variant = data.get("skinAsset", {})
|
|
skinAsset = skinAssetPayload if skinAssetPayload is Dictionary else {}
|
|
if data.has("avatar_id"):
|
|
avatarId = str(data.get("avatar_id", ""))
|
|
elif data.has("avatarId"):
|
|
avatarId = str(data.get("avatarId", ""))
|
|
if data.has("cafe_companion") or data.has("cafeCompanion"):
|
|
cafeCompanionData = _normalize_cafe_companion_data(data.get("cafe_companion", data.get("cafeCompanion", null)))
|
|
_apply_appearance()
|
|
_configure_cafe_companion_target()
|
|
_update_name_label()
|
|
|
|
if data.has("position"):
|
|
var positionData: Variant = data.position
|
|
if positionData is Vector2:
|
|
global_position = positionData
|
|
targetPosition = positionData
|
|
_update_world_sort_z()
|
|
elif positionData.has("x") and positionData.has("y"):
|
|
var newPos := Vector2(positionData.x, positionData.y)
|
|
global_position = newPos
|
|
targetPosition = newPos
|
|
_update_world_sort_z()
|
|
|
|
# 更新目标位置
|
|
func update_position(newPos: Vector2) -> void:
|
|
targetPosition = newPos
|
|
|
|
func _update_world_sort_z() -> void:
|
|
z_index = WORLD_SORT_Z_OFFSET + int(round(global_position.y))
|
|
|
|
func _update_animation(moveVec: Vector2) -> void:
|
|
if not animation_player:
|
|
return
|
|
|
|
# 确定主方向
|
|
if abs(moveVec.x) > abs(moveVec.y):
|
|
if moveVec.x > 0:
|
|
lastDirection = "right"
|
|
else:
|
|
lastDirection = "left"
|
|
else:
|
|
if moveVec.y > 0:
|
|
lastDirection = "down"
|
|
else:
|
|
lastDirection = "up"
|
|
|
|
animation_player.play("walk_" + lastDirection)
|
|
|
|
func _play_idle_animation() -> void:
|
|
if animation_player:
|
|
animation_player.play("idle_" + lastDirection)
|
|
|
|
func _apply_appearance() -> void:
|
|
var appearanceManager := get_node_or_null("/root/AppearanceManager")
|
|
if appearanceManager == null:
|
|
return
|
|
if not skinAsset.is_empty() and appearanceManager.has_method("apply_skin_asset_to_sprite"):
|
|
appearanceManager.call("apply_skin_asset_to_sprite", sprite, skinId, skinAsset)
|
|
elif appearanceManager.has_method("apply_skin_to_sprite"):
|
|
appearanceManager.call("apply_skin_to_sprite", sprite, skinId)
|
|
_configure_directional_animations()
|
|
|
|
func _configure_directional_animations() -> void:
|
|
if animation_player == null or sprite == null:
|
|
return
|
|
|
|
var library := animation_player.get_animation_library("")
|
|
if library == null:
|
|
library = AnimationLibrary.new()
|
|
animation_player.add_animation_library("", library)
|
|
|
|
var frameCount: int = max(1, sprite.hframes)
|
|
for direction in DIRECTION_ROWS.keys():
|
|
var row := int(DIRECTION_ROWS[direction])
|
|
_set_frame_animation(library, "idle_%s" % direction, [row * frameCount], 0.1)
|
|
var walkFrames: Array[int] = []
|
|
for column in range(frameCount):
|
|
walkFrames.append(row * frameCount + column)
|
|
_set_frame_animation(library, "walk_%s" % direction, walkFrames, WALK_ANIMATION_LENGTH)
|
|
|
|
func _set_frame_animation(library: AnimationLibrary, animationName: String, frameValues: Array[int], length: float) -> void:
|
|
if library.has_animation(animationName):
|
|
library.remove_animation(animationName)
|
|
|
|
var animation := Animation.new()
|
|
animation.resource_name = animationName
|
|
animation.length = length
|
|
animation.loop_mode = Animation.LOOP_LINEAR
|
|
|
|
var trackIndex := animation.add_track(Animation.TYPE_VALUE)
|
|
animation.track_set_path(trackIndex, NodePath("Sprite2D:frame"))
|
|
animation.value_track_set_update_mode(trackIndex, Animation.UPDATE_DISCRETE)
|
|
|
|
var frameStep: float = length / float(max(1, frameValues.size()))
|
|
for index in range(frameValues.size()):
|
|
animation.track_insert_key(trackIndex, index * frameStep, frameValues[index])
|
|
|
|
library.add_animation(animationName, animation)
|
|
|
|
func _subscribe_to_settings_events() -> void:
|
|
var eventSystem := get_node_or_null("/root/EventSystem")
|
|
if eventSystem != null:
|
|
eventSystem.call("connect_event", EventNames.SETTINGS_CHANGED, _on_settings_changed, self)
|
|
|
|
func _on_settings_changed(_data: Dictionary) -> void:
|
|
_update_name_label()
|
|
|
|
func _create_name_label() -> void:
|
|
if is_instance_valid(_nameLabel):
|
|
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
|
|
var personaName := str(cafeCompanionData.get("persona_name", "")).strip_edges()
|
|
if not personaName.is_empty():
|
|
_configure_cafe_name_label(personaName)
|
|
_nameLabel.visible = true
|
|
return
|
|
_configure_default_name_label()
|
|
_nameLabel.text = username if not username.strip_edges().is_empty() else "玩家"
|
|
_nameLabel.visible = _settings_bool("show_name_always", false)
|
|
|
|
func _configure_cafe_name_label(personaName: String) -> void:
|
|
var displayName := personaName.strip_edges()
|
|
if displayName.is_empty():
|
|
displayName = "陪伴机器人"
|
|
var visualWidth := _cafe_name_label_visual_width(displayName)
|
|
var renderWidth := ceili(float(visualWidth) / CAFE_NAME_LABEL_RENDER_SCALE)
|
|
var renderHeight := ceili(float(CAFE_NAME_LABEL_VISUAL_HEIGHT) / CAFE_NAME_LABEL_RENDER_SCALE)
|
|
|
|
_nameLabel.theme = WORLD_TEXT_THEME
|
|
_nameLabel.text = displayName
|
|
_nameLabel.z_index = 30
|
|
_nameLabel.scale = Vector2.ONE * CAFE_NAME_LABEL_RENDER_SCALE
|
|
_nameLabel.position = Vector2(float(visualWidth) * -0.5, CAFE_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.12, 0.20, 0.24, 1.0))
|
|
_nameLabel.add_theme_color_override("font_shadow_color", Color(1.0, 1.0, 1.0, 0.85))
|
|
_nameLabel.add_theme_constant_override("shadow_offset_x", 0)
|
|
_nameLabel.add_theme_constant_override("shadow_offset_y", 1)
|
|
_nameLabel.add_theme_font_size_override("font_size", CAFE_NAME_LABEL_FONT_SIZE)
|
|
_nameLabel.add_theme_stylebox_override("normal", _create_cafe_name_label_style())
|
|
|
|
func _configure_default_name_label() -> void:
|
|
_nameLabel.theme = null
|
|
_nameLabel.position = NAME_LABEL_OFFSET
|
|
_nameLabel.scale = Vector2.ONE
|
|
_nameLabel.custom_minimum_size = Vector2(140, 28)
|
|
_nameLabel.size = _nameLabel.custom_minimum_size
|
|
_nameLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
_nameLabel.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
|
_nameLabel.clip_text = false
|
|
_nameLabel.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING
|
|
_nameLabel.mouse_filter = Control.MOUSE_FILTER_STOP
|
|
_nameLabel.add_theme_color_override("font_color", Color(0.188, 0.294, 0.424))
|
|
_nameLabel.remove_theme_color_override("font_shadow_color")
|
|
_nameLabel.remove_theme_constant_override("shadow_offset_x")
|
|
_nameLabel.remove_theme_constant_override("shadow_offset_y")
|
|
_nameLabel.add_theme_font_size_override("font_size", 14)
|
|
_nameLabel.add_theme_stylebox_override("normal", _create_name_label_style())
|
|
|
|
func _cafe_name_label_visual_width(displayName: String) -> int:
|
|
var estimatedWidth := displayName.length() * CAFE_NAME_LABEL_VISUAL_CHAR_WIDTH + 28
|
|
return clampi(estimatedWidth, CAFE_NAME_LABEL_VISUAL_MIN_WIDTH, CAFE_NAME_LABEL_VISUAL_MAX_WIDTH)
|
|
|
|
func _configure_cafe_companion_target() -> void:
|
|
if cafeCompanionData.is_empty():
|
|
_clear_cafe_companion_target()
|
|
return
|
|
|
|
if not is_instance_valid(_cafeCompanionTarget):
|
|
_cafeCompanionTarget = CafeCompanionTarget.new()
|
|
_cafeCompanionTarget.name = "CafeCompanionTarget"
|
|
_cafeCompanionTarget.position = Vector2(0, -58)
|
|
_cafeCompanionTarget.collision_layer = 2
|
|
_cafeCompanionTarget.collision_mask = 0
|
|
add_child(_cafeCompanionTarget)
|
|
|
|
var shapeNode := CollisionShape2D.new()
|
|
shapeNode.name = "CollisionShape2D"
|
|
var shape := RectangleShape2D.new()
|
|
shape.size = Vector2(112, 148)
|
|
shapeNode.shape = shape
|
|
_cafeCompanionTarget.add_child(shapeNode)
|
|
|
|
_cafeCompanionTarget.servicePointId = str(cafeCompanionData.get("service_point_id", "")).strip_edges()
|
|
_cafeCompanionTarget.companionId = str(cafeCompanionData.get("companion_id", "")).strip_edges()
|
|
_cafeCompanionTarget.companionType = str(cafeCompanionData.get("companion_type", "hired_player")).strip_edges()
|
|
_cafeCompanionTarget.personaName = str(cafeCompanionData.get("persona_name", "")).strip_edges()
|
|
_cafeCompanionTarget.ownerUserId = str(cafeCompanionData.get("owner_user_id", "")).strip_edges()
|
|
_cafeCompanionTarget.employmentEndsAt = str(cafeCompanionData.get("employment_ends_at", "")).strip_edges()
|
|
_cafeCompanionTarget.selfTarget = false
|
|
_cafeCompanionTarget.input_pickable = true
|
|
|
|
var shapeNode := _cafeCompanionTarget.get_node_or_null("CollisionShape2D") as CollisionShape2D
|
|
if shapeNode != null:
|
|
shapeNode.disabled = false
|
|
|
|
func _clear_cafe_companion_target() -> void:
|
|
if is_instance_valid(_cafeCompanionTarget):
|
|
_cafeCompanionTarget.queue_free()
|
|
_cafeCompanionTarget = null
|
|
|
|
func _normalize_cafe_companion_data(value: Variant) -> Dictionary:
|
|
if not (value is Dictionary):
|
|
return {}
|
|
|
|
var raw: Dictionary = value
|
|
var servicePointId := str(raw.get("servicePointId", raw.get("service_point_id", ""))).strip_edges()
|
|
var companionId := str(raw.get("companionId", raw.get("companion_id", ""))).strip_edges()
|
|
var personaName := str(raw.get("personaName", raw.get("persona_name", ""))).strip_edges()
|
|
if servicePointId.is_empty() or companionId.is_empty() or personaName.is_empty():
|
|
return {}
|
|
|
|
return {
|
|
"cafe_id": str(raw.get("cafeId", raw.get("cafe_id", "whale_cafe"))).strip_edges(),
|
|
"service_point_id": servicePointId,
|
|
"companion_id": companionId,
|
|
"companion_type": str(raw.get("companionType", raw.get("companion_type", "hired_player"))).strip_edges(),
|
|
"persona_name": personaName,
|
|
"employment_ends_at": str(raw.get("employmentEndsAt", raw.get("employment_ends_at", ""))).strip_edges(),
|
|
"owner_user_id": str(raw.get("ownerUserId", raw.get("owner_user_id", ""))).strip_edges(),
|
|
}
|
|
|
|
func _settings_bool(key: String, defaultValue: bool) -> bool:
|
|
var settingsManager := get_node_or_null("/root/SettingsManager")
|
|
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
|
|
|
|
func _create_cafe_name_label_style() -> StyleBoxFlat:
|
|
var style := StyleBoxFlat.new()
|
|
style.bg_color = Color(1.0, 0.988, 0.955, 0.96)
|
|
style.border_color = Color(0.18, 0.34, 0.38, 0.92)
|
|
style.border_width_left = 2
|
|
style.border_width_top = 2
|
|
style.border_width_right = 2
|
|
style.border_width_bottom = 2
|
|
style.corner_radius_top_left = 12
|
|
style.corner_radius_top_right = 12
|
|
style.corner_radius_bottom_left = 12
|
|
style.corner_radius_bottom_right = 12
|
|
style.content_margin_left = 12
|
|
style.content_margin_top = 4
|
|
style.content_margin_right = 12
|
|
style.content_margin_bottom = 4
|
|
style.shadow_color = Color(0.05, 0.08, 0.09, 0.18)
|
|
style.shadow_size = 4
|
|
style.shadow_offset = Vector2(0, 2)
|
|
return style
|