extends CharacterBody2D class_name PlayerController # 信号定义 signal player_moved(position: Vector2) # 常量定义 const MOVE_SPEED = 200.0 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 DIRECTION_ROWS: Dictionary = { "down": 0, "up": 1, "right": 2, "left": 3, } # 节点引用 @onready var animation_player: AnimationPlayer = $AnimationPlayer @onready var sprite: Sprite2D = $Sprite2D @onready var ray_cast: RayCast2D = $RayCast2D var lastDirection: String = "down" var _nameLabel: Label var _movementLocked: bool = false var has_scene_position_override: bool = false var _discoveryElapsed: float = 0.0 func _ready() -> void: add_to_group("whaletown_local_player") _reset_movement_input_state() _apply_current_appearance() _subscribe_to_appearance_events() _create_name_label() _update_name_label() # 检查是否有初始位置设置 call_deferred("_check_spawn_position") # 播放初始动画 if animation_player.has_animation("idle_down"): animation_player.play("idle_down") # Initialize RayCast ray_cast.add_exception(self) # Ignore local player ray_cast.enabled = true ray_cast.collide_with_areas = true ray_cast.collide_with_bodies = true ray_cast.collision_mask = INTERACTION_COLLISION_MASK ray_cast.target_position = Vector2(0, 60) _update_world_sort_z() func _exit_tree() -> void: var eventSystem := get_node_or_null("/root/EventSystem") if eventSystem != null: eventSystem.call("disconnect_event", EventNames.APPEARANCE_SKIN_CHANGED, _on_appearance_skin_changed, self) eventSystem.call("disconnect_event", EventNames.SETTINGS_CHANGED, _on_settings_changed, self) func _notification(what: int) -> void: if what == NOTIFICATION_APPLICATION_FOCUS_IN: _reset_movement_input_state() func _reset_movement_input_state() -> void: Input.flush_buffered_events() _release_movement_actions() func _release_movement_actions() -> void: Input.action_release("move_left") Input.action_release("move_right") Input.action_release("move_up") Input.action_release("move_down") func _check_spawn_position() -> void: var spawnPos: Variant = SceneManager.get_next_scene_position() if spawnPos is Vector2: global_position = spawnPos has_scene_position_override = true _update_world_sort_z() func _physics_process(delta: float) -> void: _handle_movement(delta) _update_world_sort_z() _handle_interaction() _report_nearby_discoveries(delta) func _handle_interaction() -> void: # 统一交互由 /root/InteractionManager 收集附近候选并处理 E 键。 return func _report_nearby_discoveries(delta: float) -> void: _discoveryElapsed += delta if _discoveryElapsed < 0.35: return _discoveryElapsed = 0.0 var scene := get_tree().current_scene if scene == null: return var map_id: String = str({ "Square": "whale_port", "WorkZone": "work_zone", "CafeInterior": "whale_cafe", "PersonalSpace": "personal_space", }.get(str(scene.name), "")) if map_id.is_empty(): return var social_manager := get_node_or_null("/root/SocialManager") if social_manager != null and social_manager.has_method("discover_nearby_destinations"): social_manager.call("discover_nearby_destinations", map_id, global_position) func _handle_movement(_delta: float) -> void: if _movementLocked: _release_movement_actions() velocity = Vector2.ZERO _play_idle_animation() move_and_slide() return # 输入框获得焦点时禁止移动,避免聊天/表单输入影响角色 if _is_text_input_focused(): _release_movement_actions() velocity = Vector2.ZERO _play_idle_animation() move_and_slide() return # 获取移动向量 (参考 docs/02-开发规范/输入映射配置.md) var direction := _movement_direction() # 应用移动 if direction != Vector2.ZERO: velocity = direction * MOVE_SPEED _update_animation_state(direction) else: velocity = Vector2.ZERO _play_idle_animation() move_and_slide() # 发送移动事件 (如果位置发生明显变化) if velocity.length() > 0: player_moved.emit(global_position) EventSystem.emit_event(EventNames.PLAYER_MOVED, { "position": global_position }) func _movement_direction() -> Vector2: var interactionManager := get_node_or_null("/root/InteractionManager") if interactionManager != null and interactionManager.has_method("is_selection_active") and bool(interactionManager.call("is_selection_active")): # 有交互候选时方向键用于选择,保留 WASD 移动。 return Vector2( (-1.0 if Input.is_key_pressed(KEY_A) else 0.0) + (1.0 if Input.is_key_pressed(KEY_D) else 0.0), (-1.0 if Input.is_key_pressed(KEY_W) else 0.0) + (1.0 if Input.is_key_pressed(KEY_S) else 0.0) ).normalized() return Input.get_vector("move_left", "move_right", "move_up", "move_down") func _update_animation_state(direction: Vector2) -> void: if not animation_player: return # Determine primary direction if abs(direction.x) > abs(direction.y): if direction.x > 0: lastDirection = "right" ray_cast.target_position = Vector2(60, 0) else: lastDirection = "left" ray_cast.target_position = Vector2(-60, 0) else: if direction.y > 0: lastDirection = "down" ray_cast.target_position = Vector2(0, 60) else: lastDirection = "up" ray_cast.target_position = Vector2(0, -60) animation_player.play("walk_" + lastDirection) func _play_idle_animation() -> void: if animation_player: animation_player.play("idle_" + lastDirection) func set_movement_locked(locked: bool) -> void: _movementLocked = locked if locked: _release_movement_actions() velocity = Vector2.ZERO _play_idle_animation() func _update_world_sort_z() -> void: z_index = WORLD_SORT_Z_OFFSET + int(round(global_position.y)) func _subscribe_to_appearance_events() -> void: var eventSystem := get_node_or_null("/root/EventSystem") if eventSystem != null: eventSystem.call("connect_event", EventNames.APPEARANCE_SKIN_CHANGED, _on_appearance_skin_changed, self) eventSystem.call("connect_event", EventNames.SETTINGS_CHANGED, _on_settings_changed, self) func _on_appearance_skin_changed(_data: Dictionary) -> void: _apply_current_appearance() func _on_settings_changed(_data: Dictionary) -> void: _update_name_label() func _apply_current_appearance() -> void: var appearanceManager := get_node_or_null("/root/AppearanceManager") if appearanceManager != null and appearanceManager.has_method("apply_skin_to_sprite"): appearanceManager.call("apply_skin_to_sprite", sprite) _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 _is_text_input_focused() -> bool: var focusOwner: Control = get_viewport().gui_get_focus_owner() if focusOwner == null or not focusOwner.is_inside_tree() or not focusOwner.is_visible_in_tree(): return false if focusOwner is LineEdit: var lineEdit := focusOwner as LineEdit return lineEdit.editable if focusOwner is TextEdit: var textEdit := focusOwner as TextEdit return textEdit.editable return false 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 _nameLabel.text = _current_username() _nameLabel.visible = _settings_bool("show_name_always", false) func _current_username() -> String: var authManager := get_node_or_null("/root/AuthManager") if authManager != null and authManager.has_method("get_current_username"): var username := str(authManager.call("get_current_username")).strip_edges() if not username.is_empty(): return username return "玩家" 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