Files
whale-town-front-v2/scenes/characters/PlayerController.gd
2026-07-21 23:07:31 +08:00

306 lines
10 KiB
GDScript

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