Files
whale-town/scripts/CharacterController.gd
2025-12-05 19:00:14 +08:00

543 lines
16 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends CharacterBody2D
class_name CharacterController
## 角色控制器
## 处理角色的移动、动画和状态
# 角色属性
var character_id: String = ""
var character_name: String = ""
var is_online: bool = true
var move_speed: float = preload("res://scripts/GameConfig.gd").get_character_move_speed()
# 个性化属性
var character_level: int = 1
var character_experience: int = 0
var character_status: String = "active"
var character_mood: String = "neutral"
var character_appearance: Dictionary = {}
var character_personality: Dictionary = {}
var character_attributes: Dictionary = {}
var character_skills: Dictionary = {}
var character_achievements: Array = []
# 状态管理器
var status_manager: CharacterStatusManager
# 角色状态枚举
enum CharacterState {
IDLE,
WALKING,
TALKING
}
var current_state: CharacterState = CharacterState.IDLE
# 移动相关
var current_direction: Vector2 = Vector2.ZERO
var target_position: Vector2 = Vector2.ZERO
var is_moving_smooth: bool = false
var position_tween: Tween = null
# 交互相关
var interaction_range: float = preload("res://scripts/GameConfig.gd").get_interaction_range()
var nearby_character: CharacterController = null
# 信号
signal state_changed(old_state: CharacterState, new_state: CharacterState)
signal position_updated(new_position: Vector2)
signal interaction_available(target_character: CharacterController)
signal interaction_unavailable()
func _ready():
"""初始化角色控制器"""
# 设置碰撞层
collision_layer = 4 # Layer 3 (2^2 = 4): 角色层
collision_mask = 3 # Layers 1-2: 墙壁和家具
# 确保有碰撞形状
_ensure_collision_shape()
# 初始化位置和速度
target_position = global_position
velocity = Vector2.ZERO # 确保初始速度为零
# 初始化状态管理器
status_manager = CharacterStatusManager.new()
add_child(status_manager)
# 连接状态管理器信号
status_manager.status_changed.connect(_on_status_changed)
status_manager.mood_changed.connect(_on_mood_changed)
## 确保角色有碰撞形状
func _ensure_collision_shape() -> void:
"""
确保角色有碰撞形状,如果没有则创建默认的
"""
var collision_shape = get_node_or_null("CollisionShape2D")
if not collision_shape:
# 创建默认碰撞形状
collision_shape = CollisionShape2D.new()
var shape = CircleShape2D.new()
shape.radius = 16.0 # 默认半径
collision_shape.shape = shape
add_child(collision_shape)
collision_shape.name = "CollisionShape2D"
func _physics_process(delta: float):
"""物理帧更新"""
if is_moving_smooth:
_update_smooth_movement(delta)
else:
# 总是更新直接移动,即使方向为零(这样可以停止角色)
_update_direct_movement(delta)
## 直接移动(用于本地玩家输入)
func move_to(direction: Vector2) -> void:
"""
根据方向向量移动角色
@param direction: 移动方向(已归一化)
"""
current_direction = direction
if direction != Vector2.ZERO:
_change_state(CharacterState.WALKING)
else:
_change_state(CharacterState.IDLE)
velocity = Vector2.ZERO # 立即停止移动
## 平滑移动到目标位置(用于网络同步)
func set_position_smooth(target_pos: Vector2, use_tween: bool = false) -> void:
"""
平滑移动到目标位置(用于网络延迟补偿)
@param target_pos: 目标位置
@param use_tween: 是否使用 Tween 动画(更平滑但可能有延迟)
"""
target_position = target_pos
if use_tween:
# 使用 Tween 实现更平滑的移动
if position_tween:
position_tween.kill()
position_tween = create_tween()
var distance = global_position.distance_to(target_pos)
var duration = distance / move_speed
position_tween.tween_property(self, "global_position", target_pos, duration)
position_tween.finished.connect(_on_tween_finished)
_change_state(CharacterState.WALKING)
else:
# 使用物理移动(更适合实时网络同步)
is_moving_smooth = true
_change_state(CharacterState.WALKING)
## Tween 完成回调
func _on_tween_finished() -> void:
"""Tween 动画完成时调用"""
_change_state(CharacterState.IDLE)
position_updated.emit(global_position)
## 直接设置位置(瞬移)
func set_position_immediate(pos: Vector2) -> void:
"""
立即设置角色位置(无动画)
@param pos: 新位置
"""
global_position = pos
target_position = pos
is_moving_smooth = false
position_updated.emit(pos)
## 播放动画
func play_animation(anim_name: String) -> void:
"""
播放指定动画
@param anim_name: 动画名称idle, walking, talking
"""
# 查找 AnimationPlayer 节点
var anim_player = get_node_or_null("AnimationPlayer")
if anim_player and anim_player is AnimationPlayer:
# 检查动画是否存在
if anim_player.has_animation(anim_name):
anim_player.play(anim_name)
# 如果没有 AnimationPlayer使用简单的视觉反馈
_update_animation_state(anim_name)
## 设置在线状态
func set_online_status(online: bool) -> void:
"""
设置角色在线/离线状态
@param online: 是否在线
"""
is_online = online
_update_visual_status()
## 获取当前状态
func get_current_state() -> CharacterState:
"""
获取当前角色状态
@return: 当前状态
"""
return current_state
## 初始化角色数据
func initialize(data: Dictionary) -> void:
"""
使用角色数据初始化控制器
@param data: 角色数据字典
"""
print("CharacterController.initialize() called with data: ", data)
if data.has(CharacterData.FIELD_ID):
character_id = data[CharacterData.FIELD_ID]
if data.has(CharacterData.FIELD_NAME):
character_name = data[CharacterData.FIELD_NAME]
if data.has(CharacterData.FIELD_IS_ONLINE):
is_online = data[CharacterData.FIELD_IS_ONLINE]
# 加载个性化数据
_load_personalization_data(data)
# 设置初始位置
var pos = CharacterData.get_position(data)
print("Setting character position from data: ", pos)
set_position_immediate(pos)
# 完全重置所有移动状态
_reset_movement_state()
# 设置状态管理器数据
if status_manager:
status_manager.set_character_data(data)
# 更新名称标签
_update_name_label()
_update_visual_status()
_update_appearance()
## 重置移动状态
func _reset_movement_state() -> void:
"""完全重置角色的移动状态"""
velocity = Vector2.ZERO
current_direction = Vector2.ZERO
target_position = global_position
is_moving_smooth = false
# 停止任何正在进行的Tween
if position_tween:
position_tween.kill()
position_tween = null
# 设置为空闲状态
_change_state(CharacterState.IDLE)
print("Character movement state completely reset")
## 更新名称标签
func _update_name_label() -> void:
"""更新角色名称标签"""
var name_label = get_node_or_null("NameLabel")
if not name_label:
# 使用工具类创建带阴影的标签
name_label = preload("res://scripts/Utils.gd").create_label_with_shadow("", 14)
name_label.name = "NameLabel"
add_child(name_label)
# 设置名称文本(包含等级和心情)
var display_name = character_name if not character_name.is_empty() else "Unknown"
var personalization = preload("res://scripts/CharacterPersonalization.gd")
var mood_emoji = personalization.get_mood_emoji(character_mood)
var level_text = "Lv.%d" % character_level if character_level > 1 else ""
var full_text = display_name
if not level_text.is_empty():
full_text += " " + level_text
full_text += " " + mood_emoji
name_label.text = full_text
# 调整位置(在角色上方)
name_label.position = Vector2(-60, -40) # 居中并在角色上方
name_label.size = Vector2(120, 20)
## 私有方法:更新直接移动
func _update_direct_movement(_delta: float) -> void:
"""
更新基于输入的直接移动
@param _delta: 帧时间(保留用于未来扩展)
"""
# 计算速度
if current_direction == Vector2.ZERO:
velocity = Vector2.ZERO
else:
velocity = current_direction.normalized() * move_speed
# 执行移动并处理碰撞
var previous_position = global_position
move_and_slide()
# 检查是否发生了碰撞
if get_slide_collision_count() > 0:
# 发生碰撞,角色被阻挡
pass
# 发射位置更新信号(只在位置改变时)
if global_position != previous_position:
target_position = global_position
position_updated.emit(global_position)
## 私有方法:更新平滑移动
func _update_smooth_movement(_delta: float) -> void:
"""
更新平滑移动到目标位置
@param _delta: 帧时间(未使用,保留用于未来扩展)
"""
var distance = global_position.distance_to(target_position)
# 如果已经很接近目标位置,停止移动
if distance < 1.0:
global_position = target_position
is_moving_smooth = false
_change_state(CharacterState.IDLE)
position_updated.emit(global_position)
return
# 计算移动方向和速度
var direction = (target_position - global_position).normalized()
velocity = direction * move_speed
# 执行移动
move_and_slide()
position_updated.emit(global_position)
## 私有方法:改变状态
func _change_state(new_state: CharacterState) -> void:
"""
改变角色状态
@param new_state: 新状态
"""
if current_state == new_state:
return
var old_state = current_state
current_state = new_state
state_changed.emit(old_state, new_state)
# 根据状态播放动画
match new_state:
CharacterState.IDLE:
play_animation("idle")
CharacterState.WALKING:
play_animation("walking")
CharacterState.TALKING:
play_animation("talking")
## 私有方法:更新动画状态
func _update_animation_state(_anim_name: String) -> void:
"""
更新动画状态的视觉反馈(当没有 AnimationPlayer 时)
@param _anim_name: 动画名称(未使用,保留用于未来扩展)
"""
# 这是一个占位实现,用于在没有实际动画资源时提供反馈
# 实际的精灵动画将在任务 11 中添加
pass
## 检测附近角色
func check_nearby_characters(all_characters: Array) -> void:
"""
检测附近是否有可交互的角色
@param all_characters: 所有角色的数组
"""
var closest_character: CharacterController = null
var closest_distance: float = interaction_range
for character in all_characters:
if character == self or not character is CharacterController:
continue
var distance = global_position.distance_to(character.global_position)
if distance < closest_distance:
closest_distance = distance
closest_character = character
# 检查是否有变化
if closest_character != nearby_character:
nearby_character = closest_character
if nearby_character:
interaction_available.emit(nearby_character)
else:
interaction_unavailable.emit()
## 获取附近角色
func get_nearby_character() -> CharacterController:
"""
获取当前附近的角色
@return: 附近的角色,如果没有则返回 null
"""
return nearby_character
## 私有方法:更新视觉状态
func _update_visual_status() -> void:
"""
更新角色的视觉状态(在线/离线)
"""
# 查找或创建状态指示器
var status_indicator = get_node_or_null("StatusIndicator")
if not status_indicator:
# 使用工具类创建状态指示器
status_indicator = preload("res://scripts/Utils.gd").create_status_indicator(is_online)
status_indicator.name = "StatusIndicator"
status_indicator.position = Vector2(-4, -24) # 在角色上方
add_child(status_indicator)
else:
# 更新现有指示器的颜色
var personalization = preload("res://scripts/CharacterPersonalization.gd")
var color = personalization.get_status_color(character_status) if is_online else Color.GRAY
status_indicator.color = color
## 加载个性化数据
func _load_personalization_data(data: Dictionary) -> void:
"""
从角色数据中加载个性化信息
@param data: 角色数据字典
"""
character_level = data.get(CharacterData.FIELD_LEVEL, 1)
character_experience = data.get(CharacterData.FIELD_EXPERIENCE, 0)
character_status = data.get(CharacterData.FIELD_STATUS, "active")
character_mood = data.get(CharacterData.FIELD_MOOD, "neutral")
character_appearance = data.get(CharacterData.FIELD_APPEARANCE, {})
character_personality = data.get(CharacterData.FIELD_PERSONALITY, {})
character_attributes = data.get(CharacterData.FIELD_ATTRIBUTES, {})
character_skills = data.get(CharacterData.FIELD_SKILLS, {})
character_achievements = data.get(CharacterData.FIELD_ACHIEVEMENTS, [])
## 更新外观
func _update_appearance() -> void:
"""更新角色外观"""
var sprite = get_node_or_null("CharacterSprite")
if sprite and sprite is CharacterSprite:
var personalization = preload("res://scripts/CharacterPersonalization.gd")
personalization.apply_appearance_to_sprite(sprite, character_appearance)
## 状态变化回调
func _on_status_changed(old_status: String, new_status: String) -> void:
"""状态变化时的回调"""
character_status = new_status
_update_visual_status()
print("Character %s status changed: %s -> %s" % [character_name, old_status, new_status])
## 心情变化回调
func _on_mood_changed(old_mood: String, new_mood: String) -> void:
"""心情变化时的回调"""
character_mood = new_mood
_update_name_label() # 更新名称标签以显示心情
print("Character %s mood changed: %s -> %s" % [character_name, old_mood, new_mood])
## 获取角色信息摘要
func get_character_summary() -> String:
"""
获取角色信息摘要
@return: 角色摘要文本
"""
var summary = []
summary.append("等级 %d" % character_level)
if character_achievements.size() > 0:
summary.append("%d 个成就" % character_achievements.size())
var personalization = preload("res://scripts/CharacterPersonalization.gd")
var mood_emoji = personalization.get_mood_emoji(character_mood)
summary.append("心情 %s" % mood_emoji)
return " | ".join(summary)
## 设置角色状态
func set_character_status(status: String) -> void:
"""
设置角色状态
@param status: 新状态
"""
if status_manager:
status_manager.set_status(status)
## 设置角色心情
func set_character_mood(mood: String) -> void:
"""
设置角色心情
@param mood: 新心情
"""
if status_manager:
status_manager.set_mood(mood)
## 触发活动事件
func trigger_activity_event(event_type: String, data: Dictionary = {}) -> void:
"""
触发角色活动事件
@param event_type: 事件类型
@param data: 事件数据
"""
if status_manager:
status_manager.handle_activity_event(event_type, data)
## 增加经验值
func add_experience(experience: int) -> bool:
"""
增加经验值
@param experience: 经验值
@return: 是否升级
"""
character_experience += experience
# 检查升级
var required_exp = CharacterData.get_required_experience(character_level)
if character_experience >= required_exp:
character_level += 1
character_experience -= required_exp
# 触发升级事件
trigger_activity_event("level_up")
print("Character %s leveled up to %d!" % [character_name, character_level])
return true
return false
## 添加成就
func add_achievement(achievement: Dictionary) -> void:
"""
添加成就
@param achievement: 成就数据
"""
# 检查是否已有此成就
for existing in character_achievements:
if existing.get("id") == achievement.get("id"):
return
character_achievements.append(achievement)
trigger_activity_event("achievement_earned")
print("Character %s earned achievement: %s" % [character_name, achievement.get("name", "Unknown")])
## 获取个性化数据
func get_personalization_data() -> Dictionary:
"""
获取当前的个性化数据
@return: 个性化数据字典
"""
return {
CharacterData.FIELD_LEVEL: character_level,
CharacterData.FIELD_EXPERIENCE: character_experience,
CharacterData.FIELD_STATUS: character_status,
CharacterData.FIELD_MOOD: character_mood,
CharacterData.FIELD_APPEARANCE: character_appearance,
CharacterData.FIELD_PERSONALITY: character_personality,
CharacterData.FIELD_ATTRIBUTES: character_attributes,
CharacterData.FIELD_SKILLS: character_skills,
CharacterData.FIELD_ACHIEVEMENTS: character_achievements
}