创建新工程
This commit is contained in:
542
scripts/CharacterController.gd
Normal file
542
scripts/CharacterController.gd
Normal file
@@ -0,0 +1,542 @@
|
||||
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
|
||||
}
|
||||
1
scripts/CharacterController.gd.uid
Normal file
1
scripts/CharacterController.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cp5md2i8wxniy
|
||||
330
scripts/CharacterCreation.gd
Normal file
330
scripts/CharacterCreation.gd
Normal file
@@ -0,0 +1,330 @@
|
||||
extends Control
|
||||
class_name CharacterCreation
|
||||
## 角色创建界面
|
||||
## 处理新角色的创建
|
||||
|
||||
# UI 元素
|
||||
var character_name_input: LineEdit
|
||||
var create_button: Button
|
||||
var back_button: Button
|
||||
var customize_button: Button
|
||||
var status_label: Label
|
||||
var container: VBoxContainer
|
||||
var status_timer: Timer
|
||||
|
||||
# 个性化数据
|
||||
var character_appearance: Dictionary = {}
|
||||
var character_personality: Dictionary = {}
|
||||
|
||||
# 自定义界面
|
||||
var customization_ui: Control
|
||||
|
||||
# 信号
|
||||
signal character_created(character_name: String, personalization_data: Dictionary)
|
||||
signal back_requested()
|
||||
|
||||
func _ready():
|
||||
"""初始化角色创建界面"""
|
||||
_create_ui()
|
||||
_setup_timer()
|
||||
_setup_animations()
|
||||
_initialize_personalization()
|
||||
update_layout()
|
||||
|
||||
## 设置动画效果
|
||||
func _setup_animations():
|
||||
"""设置界面动画效果"""
|
||||
# 为移动设备优化触摸体验
|
||||
TouchFeedbackManager.optimize_ui_for_touch(self)
|
||||
|
||||
# 界面入场动画
|
||||
UIAnimationManager.fade_slide_in(container, "right", 0.4)
|
||||
|
||||
## 设置定时器
|
||||
func _setup_timer():
|
||||
"""设置状态标签自动清除定时器"""
|
||||
status_timer = Timer.new()
|
||||
status_timer.one_shot = true
|
||||
status_timer.timeout.connect(_on_status_timer_timeout)
|
||||
add_child(status_timer)
|
||||
|
||||
## 定时器超时
|
||||
func _on_status_timer_timeout():
|
||||
"""定时器超时,清除状态标签"""
|
||||
clear_status()
|
||||
|
||||
## 创建 UI 元素
|
||||
func _create_ui():
|
||||
"""创建角色创建界面的所有 UI 元素"""
|
||||
# 设置为全屏
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
|
||||
# 创建中心容器
|
||||
container = VBoxContainer.new()
|
||||
container.name = "Container"
|
||||
container.custom_minimum_size = Vector2(300, 0)
|
||||
# 设置容器锚点为居中
|
||||
container.anchor_left = 0.5
|
||||
container.anchor_right = 0.5
|
||||
container.anchor_top = 0.5
|
||||
container.anchor_bottom = 0.5
|
||||
container.offset_left = -150 # 容器宽度的一半
|
||||
container.offset_right = 150
|
||||
container.offset_top = -200 # 估算容器高度的一半
|
||||
container.offset_bottom = 200
|
||||
add_child(container)
|
||||
|
||||
# 标题
|
||||
var title = Label.new()
|
||||
title.text = "创建角色"
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
title.add_theme_font_size_override("font_size", 32)
|
||||
container.add_child(title)
|
||||
|
||||
# 间距
|
||||
container.add_child(_create_spacer(20))
|
||||
|
||||
# 说明文本
|
||||
var description = Label.new()
|
||||
description.text = "请输入你的角色名称(2-20个字符)"
|
||||
description.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
container.add_child(description)
|
||||
|
||||
# 间距
|
||||
container.add_child(_create_spacer(10))
|
||||
|
||||
# 角色名称标签
|
||||
var name_label = Label.new()
|
||||
name_label.text = "角色名称:"
|
||||
container.add_child(name_label)
|
||||
|
||||
# 角色名称输入框
|
||||
character_name_input = LineEdit.new()
|
||||
character_name_input.placeholder_text = "输入角色名称"
|
||||
character_name_input.custom_minimum_size = Vector2(0, 40)
|
||||
character_name_input.max_length = 20
|
||||
character_name_input.text_changed.connect(_on_name_changed)
|
||||
character_name_input.text_submitted.connect(_on_name_submitted)
|
||||
container.add_child(character_name_input)
|
||||
|
||||
# 状态标签(放在输入框下方)
|
||||
status_label = Label.new()
|
||||
status_label.text = ""
|
||||
status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
status_label.custom_minimum_size = Vector2(0, 30)
|
||||
status_label.add_theme_font_size_override("font_size", 16)
|
||||
container.add_child(status_label)
|
||||
|
||||
# 间距
|
||||
container.add_child(_create_spacer(10))
|
||||
|
||||
# 自定义按钮(默认禁用)
|
||||
customize_button = Button.new()
|
||||
customize_button.text = "自定义外观(暂未开放)"
|
||||
customize_button.custom_minimum_size = Vector2(0, 50)
|
||||
customize_button.disabled = true # 默认禁用
|
||||
customize_button.pressed.connect(_on_customize_pressed)
|
||||
container.add_child(customize_button)
|
||||
|
||||
# 间距
|
||||
container.add_child(_create_spacer(10))
|
||||
|
||||
# 创建按钮
|
||||
create_button = Button.new()
|
||||
create_button.text = "创建角色"
|
||||
create_button.custom_minimum_size = Vector2(0, 50)
|
||||
create_button.pressed.connect(_on_create_pressed)
|
||||
container.add_child(create_button)
|
||||
|
||||
# 间距
|
||||
container.add_child(_create_spacer(10))
|
||||
|
||||
# 返回按钮
|
||||
back_button = Button.new()
|
||||
back_button.text = "返回"
|
||||
back_button.custom_minimum_size = Vector2(0, 50)
|
||||
back_button.pressed.connect(_on_back_pressed)
|
||||
container.add_child(back_button)
|
||||
|
||||
## 创建间距
|
||||
func _create_spacer(height: float) -> Control:
|
||||
"""创建垂直间距"""
|
||||
var spacer = Control.new()
|
||||
spacer.custom_minimum_size = Vector2(0, height)
|
||||
return spacer
|
||||
|
||||
## 更新布局
|
||||
func update_layout():
|
||||
"""更新布局以适应窗口大小"""
|
||||
if not container:
|
||||
return
|
||||
|
||||
# 容器已经通过锚点设置为居中,无需手动计算位置
|
||||
# 锚点会自动适应窗口大小变化
|
||||
|
||||
## 显示状态消息
|
||||
func show_status(message: String, is_error: bool = false):
|
||||
"""
|
||||
显示状态消息
|
||||
@param message: 消息文本
|
||||
@param is_error: 是否为错误消息
|
||||
"""
|
||||
if status_label:
|
||||
status_label.text = message
|
||||
if is_error:
|
||||
status_label.add_theme_color_override("font_color", Color(1, 0.3, 0.3))
|
||||
else:
|
||||
status_label.add_theme_color_override("font_color", Color(0.3, 1, 0.3))
|
||||
|
||||
# 启动定时器,1秒后自动清除(与ErrorNotification保持一致)
|
||||
if status_timer:
|
||||
status_timer.start(1.0)
|
||||
|
||||
## 清除状态消息
|
||||
func clear_status():
|
||||
"""清除状态消息"""
|
||||
if status_timer:
|
||||
status_timer.stop()
|
||||
if status_label:
|
||||
status_label.text = ""
|
||||
|
||||
## 验证角色名称
|
||||
func validate_character_name(char_name: String) -> String:
|
||||
"""
|
||||
验证角色名称
|
||||
@param char_name: 角色名称
|
||||
@return: 错误消息,如果有效则返回空字符串
|
||||
"""
|
||||
# 使用安全管理器进行验证
|
||||
var validation_result = SecurityManager.validate_input(char_name, "character_name")
|
||||
|
||||
if not validation_result.valid:
|
||||
return validation_result.error
|
||||
|
||||
# 使用 CharacterData 的验证函数作为额外检查
|
||||
if not CharacterData.validate_name(validation_result.sanitized):
|
||||
return "角色名称格式无效"
|
||||
|
||||
return ""
|
||||
|
||||
## 名称输入变化
|
||||
func _on_name_changed(new_text: String):
|
||||
"""名称输入框内容变化"""
|
||||
# 实时验证
|
||||
var error = validate_character_name(new_text)
|
||||
if error.is_empty():
|
||||
clear_status()
|
||||
create_button.disabled = false
|
||||
else:
|
||||
show_status(error, true)
|
||||
create_button.disabled = true
|
||||
|
||||
## 创建按钮点击
|
||||
func _on_create_pressed():
|
||||
"""创建按钮被点击"""
|
||||
var char_name = character_name_input.text
|
||||
|
||||
# 验证名称
|
||||
var error = validate_character_name(char_name)
|
||||
if not error.is_empty():
|
||||
show_status(error, true)
|
||||
# 添加错误动画反馈
|
||||
UIAnimationManager.shake_error(character_name_input, 8.0, 0.4)
|
||||
return
|
||||
|
||||
# 获取清理后的名称
|
||||
var validation_result = SecurityManager.validate_input(char_name, "character_name")
|
||||
var sanitized_name = validation_result.sanitized
|
||||
|
||||
# 添加成功反馈动画
|
||||
UIAnimationManager.button_press_feedback(create_button)
|
||||
clear_status()
|
||||
character_created.emit(sanitized_name, get_personalization_data())
|
||||
|
||||
## 返回按钮点击
|
||||
func _on_back_pressed():
|
||||
"""返回按钮被点击"""
|
||||
back_requested.emit()
|
||||
|
||||
## 名称输入框回车
|
||||
func _on_name_submitted(_text: String):
|
||||
"""名称输入框按下回车"""
|
||||
if not create_button.disabled:
|
||||
_on_create_pressed()
|
||||
|
||||
## 初始化个性化数据
|
||||
func _initialize_personalization():
|
||||
"""初始化默认的个性化数据"""
|
||||
var personalization = preload("res://scripts/CharacterPersonalization.gd")
|
||||
character_appearance = personalization.generate_random_appearance()
|
||||
character_personality = personalization.generate_random_personality()
|
||||
|
||||
## 自定义按钮点击
|
||||
func _on_customize_pressed():
|
||||
"""自定义按钮被点击"""
|
||||
# 检查按钮是否被禁用
|
||||
if customize_button.disabled:
|
||||
show_status("自定义外观功能暂未开放", true)
|
||||
return
|
||||
|
||||
if customization_ui:
|
||||
customization_ui.queue_free()
|
||||
|
||||
var CharacterCustomizationClass = preload("res://scripts/CharacterCustomization.gd")
|
||||
customization_ui = CharacterCustomizationClass.new()
|
||||
get_tree().root.add_child(customization_ui)
|
||||
|
||||
# 创建临时角色数据用于自定义
|
||||
var temp_data = CharacterData.create("临时角色", "temp")
|
||||
CharacterData.set_appearance(temp_data, character_appearance)
|
||||
temp_data[CharacterData.FIELD_PERSONALITY] = character_personality
|
||||
|
||||
customization_ui.load_character_data(temp_data)
|
||||
customization_ui.customization_saved.connect(_on_customization_saved)
|
||||
customization_ui.customization_cancelled.connect(_on_customization_cancelled)
|
||||
|
||||
## 自定义保存回调
|
||||
func _on_customization_saved(data: Dictionary):
|
||||
"""自定义数据保存回调"""
|
||||
character_appearance = data.get(CharacterData.FIELD_APPEARANCE, {})
|
||||
character_personality = data.get(CharacterData.FIELD_PERSONALITY, {})
|
||||
|
||||
show_status("外观和个性已自定义", false)
|
||||
|
||||
if customization_ui:
|
||||
customization_ui.queue_free()
|
||||
customization_ui = null
|
||||
|
||||
## 自定义取消回调
|
||||
func _on_customization_cancelled():
|
||||
"""自定义取消回调"""
|
||||
if customization_ui:
|
||||
customization_ui.queue_free()
|
||||
customization_ui = null
|
||||
|
||||
## 获取个性化数据
|
||||
func get_personalization_data() -> Dictionary:
|
||||
"""
|
||||
获取当前的个性化数据
|
||||
@return: 个性化数据字典
|
||||
"""
|
||||
return {
|
||||
CharacterData.FIELD_APPEARANCE: character_appearance,
|
||||
CharacterData.FIELD_PERSONALITY: character_personality
|
||||
}
|
||||
|
||||
## 启用/禁用自定义外观功能
|
||||
func set_customization_enabled(enabled: bool):
|
||||
"""
|
||||
启用或禁用自定义外观功能
|
||||
@param enabled: 是否启用
|
||||
"""
|
||||
if customize_button:
|
||||
customize_button.disabled = not enabled
|
||||
if enabled:
|
||||
customize_button.text = "自定义外观"
|
||||
else:
|
||||
customize_button.text = "自定义外观(暂未开放)"
|
||||
1
scripts/CharacterCreation.gd.uid
Normal file
1
scripts/CharacterCreation.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://1grjr2nf466x
|
||||
385
scripts/CharacterCustomization.gd
Normal file
385
scripts/CharacterCustomization.gd
Normal file
@@ -0,0 +1,385 @@
|
||||
extends Control
|
||||
## 角色自定义界面
|
||||
## 允许玩家自定义角色外观和个性
|
||||
|
||||
# 预加载必要的类
|
||||
const CharacterDataClass = preload("res://scripts/CharacterData.gd")
|
||||
|
||||
# UI 元素
|
||||
var container: VBoxContainer
|
||||
var tabs: TabContainer
|
||||
|
||||
# 外观自定义
|
||||
var appearance_container: VBoxContainer
|
||||
var body_color_picker: ColorPicker
|
||||
var head_color_picker: ColorPicker
|
||||
var hair_color_picker: ColorPicker
|
||||
var clothing_color_picker: ColorPicker
|
||||
var randomize_appearance_button: Button
|
||||
|
||||
# 个性自定义
|
||||
var personality_container: VBoxContainer
|
||||
var trait_checkboxes: Dictionary = {}
|
||||
var activity_option: OptionButton
|
||||
var bio_text: TextEdit
|
||||
|
||||
# 预览
|
||||
var preview_character: CharacterSprite
|
||||
|
||||
# 按钮
|
||||
var save_button: Button
|
||||
var cancel_button: Button
|
||||
|
||||
# 数据
|
||||
var character_data: Dictionary = {}
|
||||
|
||||
# 信号
|
||||
signal customization_saved(character_data: Dictionary)
|
||||
signal customization_cancelled()
|
||||
|
||||
func _ready():
|
||||
## 初始化自定义界面
|
||||
_create_ui()
|
||||
_setup_connections()
|
||||
|
||||
## 创建UI
|
||||
func _create_ui():
|
||||
## 创建自定义界面的所有UI元素
|
||||
# 设置为全屏
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
|
||||
# 添加半透明背景
|
||||
var background = ColorRect.new()
|
||||
background.color = Color(0, 0, 0, 0.7)
|
||||
background.anchor_right = 1.0
|
||||
background.anchor_bottom = 1.0
|
||||
add_child(background)
|
||||
|
||||
# 主容器
|
||||
container = VBoxContainer.new()
|
||||
container.anchor_right = 1.0
|
||||
container.anchor_bottom = 1.0
|
||||
container.add_theme_constant_override("separation", 10)
|
||||
add_child(container)
|
||||
|
||||
# 标题栏
|
||||
var title_container = HBoxContainer.new()
|
||||
container.add_child(title_container)
|
||||
|
||||
var title = Label.new()
|
||||
title.text = "角色自定义"
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
title.add_theme_font_size_override("font_size", 24)
|
||||
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
title_container.add_child(title)
|
||||
|
||||
var close_button = Button.new()
|
||||
close_button.text = "✕"
|
||||
close_button.custom_minimum_size = Vector2(40, 40)
|
||||
close_button.pressed.connect(_on_cancel_pressed)
|
||||
title_container.add_child(close_button)
|
||||
|
||||
# 滚动容器
|
||||
var scroll_container = ScrollContainer.new()
|
||||
scroll_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
container.add_child(scroll_container)
|
||||
|
||||
# 标签页容器
|
||||
tabs = TabContainer.new()
|
||||
tabs.custom_minimum_size = Vector2(0, 500)
|
||||
scroll_container.add_child(tabs)
|
||||
|
||||
# 创建外观标签页
|
||||
_create_appearance_tab()
|
||||
|
||||
# 创建个性标签页
|
||||
_create_personality_tab()
|
||||
|
||||
# 预览区域
|
||||
_create_preview_area()
|
||||
|
||||
# 按钮区域
|
||||
_create_button_area()
|
||||
|
||||
## 创建外观标签页
|
||||
func _create_appearance_tab():
|
||||
## 创建外观自定义标签页
|
||||
appearance_container = VBoxContainer.new()
|
||||
appearance_container.name = "外观"
|
||||
tabs.add_child(appearance_container)
|
||||
|
||||
# 身体颜色
|
||||
var body_section = _create_color_section("身体颜色", Color(0.29, 0.56, 0.89))
|
||||
appearance_container.add_child(body_section.container)
|
||||
body_color_picker = body_section.picker
|
||||
|
||||
# 头部颜色
|
||||
var head_section = _create_color_section("头部颜色", Color(0.96, 0.90, 0.83))
|
||||
appearance_container.add_child(head_section.container)
|
||||
head_color_picker = head_section.picker
|
||||
|
||||
# 头发颜色
|
||||
var hair_section = _create_color_section("头发颜色", Color(0.55, 0.27, 0.07))
|
||||
appearance_container.add_child(hair_section.container)
|
||||
hair_color_picker = hair_section.picker
|
||||
|
||||
# 服装颜色
|
||||
var clothing_section = _create_color_section("服装颜色", Color(0.18, 0.55, 0.34))
|
||||
appearance_container.add_child(clothing_section.container)
|
||||
clothing_color_picker = clothing_section.picker
|
||||
|
||||
# 随机化按钮
|
||||
randomize_appearance_button = Button.new()
|
||||
randomize_appearance_button.text = "随机外观"
|
||||
randomize_appearance_button.custom_minimum_size = Vector2(0, 40)
|
||||
appearance_container.add_child(randomize_appearance_button)
|
||||
|
||||
## 创建颜色选择区域
|
||||
func _create_color_section(label_text: String, default_color: Color) -> Dictionary:
|
||||
## 创建颜色选择区域
|
||||
var section_container = VBoxContainer.new()
|
||||
|
||||
var label = Label.new()
|
||||
label.text = label_text
|
||||
section_container.add_child(label)
|
||||
|
||||
var picker = ColorPicker.new()
|
||||
picker.color = default_color
|
||||
picker.custom_minimum_size = Vector2(300, 200)
|
||||
picker.edit_alpha = false # 不需要透明度调节
|
||||
section_container.add_child(picker)
|
||||
|
||||
return {
|
||||
"container": section_container,
|
||||
"picker": picker
|
||||
}
|
||||
|
||||
## 创建个性标签页
|
||||
func _create_personality_tab():
|
||||
## 创建个性自定义标签页
|
||||
personality_container = VBoxContainer.new()
|
||||
personality_container.name = "个性"
|
||||
tabs.add_child(personality_container)
|
||||
|
||||
# 个性特征
|
||||
var traits_label = Label.new()
|
||||
traits_label.text = "个性特征 (最多选择4个):"
|
||||
personality_container.add_child(traits_label)
|
||||
|
||||
var traits_grid = GridContainer.new()
|
||||
traits_grid.columns = 3
|
||||
personality_container.add_child(traits_grid)
|
||||
|
||||
var personalization = preload("res://scripts/CharacterPersonalization.gd")
|
||||
for trait_in in personalization.PERSONALITY_TRAITS:
|
||||
var checkbox = CheckBox.new()
|
||||
checkbox.text = trait_in
|
||||
traits_grid.add_child(checkbox)
|
||||
trait_checkboxes[trait_in] = checkbox
|
||||
|
||||
# 喜欢的活动
|
||||
var activity_label = Label.new()
|
||||
activity_label.text = "喜欢的活动:"
|
||||
personality_container.add_child(activity_label)
|
||||
|
||||
activity_option = OptionButton.new()
|
||||
for activity in personalization.ACTIVITIES:
|
||||
activity_option.add_item(activity)
|
||||
personality_container.add_child(activity_option)
|
||||
|
||||
# 个人简介
|
||||
var bio_label = Label.new()
|
||||
bio_label.text = "个人简介 (可选):"
|
||||
personality_container.add_child(bio_label)
|
||||
|
||||
bio_text = TextEdit.new()
|
||||
bio_text.custom_minimum_size = Vector2(0, 100)
|
||||
bio_text.placeholder_text = "介绍一下你的角色..."
|
||||
personality_container.add_child(bio_text)
|
||||
|
||||
## 创建预览区域
|
||||
func _create_preview_area():
|
||||
## 创建角色预览区域
|
||||
var preview_label = Label.new()
|
||||
preview_label.text = "预览:"
|
||||
preview_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
container.add_child(preview_label)
|
||||
|
||||
var preview_container = CenterContainer.new()
|
||||
preview_container.custom_minimum_size = Vector2(0, 100)
|
||||
container.add_child(preview_container)
|
||||
|
||||
preview_character = CharacterSprite.new()
|
||||
preview_container.add_child(preview_character)
|
||||
|
||||
## 创建按钮区域
|
||||
func _create_button_area():
|
||||
## 创建按钮区域
|
||||
var button_container = HBoxContainer.new()
|
||||
button_container.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
container.add_child(button_container)
|
||||
|
||||
save_button = Button.new()
|
||||
save_button.text = "保存"
|
||||
save_button.custom_minimum_size = Vector2(100, 40)
|
||||
button_container.add_child(save_button)
|
||||
|
||||
var spacer = Control.new()
|
||||
spacer.custom_minimum_size = Vector2(20, 0)
|
||||
button_container.add_child(spacer)
|
||||
|
||||
cancel_button = Button.new()
|
||||
cancel_button.text = "取消"
|
||||
cancel_button.custom_minimum_size = Vector2(100, 40)
|
||||
button_container.add_child(cancel_button)
|
||||
|
||||
## 设置连接
|
||||
func _setup_connections():
|
||||
## 设置信号连接
|
||||
# 颜色选择器变化
|
||||
body_color_picker.color_changed.connect(_on_appearance_changed)
|
||||
head_color_picker.color_changed.connect(_on_appearance_changed)
|
||||
hair_color_picker.color_changed.connect(_on_appearance_changed)
|
||||
clothing_color_picker.color_changed.connect(_on_appearance_changed)
|
||||
|
||||
# 随机化按钮
|
||||
randomize_appearance_button.pressed.connect(_on_randomize_appearance)
|
||||
|
||||
# 个性特征复选框
|
||||
for checkbox in trait_checkboxes.values():
|
||||
checkbox.toggled.connect(_on_trait_toggled)
|
||||
|
||||
# 按钮
|
||||
save_button.pressed.connect(_on_save_pressed)
|
||||
cancel_button.pressed.connect(_on_cancel_pressed)
|
||||
|
||||
## 加载角色数据
|
||||
func load_character_data(data: Dictionary):
|
||||
## 加载角色数据到界面
|
||||
## @param data: 角色数据
|
||||
character_data = data.duplicate(true)
|
||||
|
||||
# 加载外观数据
|
||||
var appearance = data.get(CharacterDataClass.FIELD_APPEARANCE, {})
|
||||
if appearance.has("body_color"):
|
||||
body_color_picker.color = Color(appearance["body_color"])
|
||||
if appearance.has("head_color"):
|
||||
head_color_picker.color = Color(appearance["head_color"])
|
||||
if appearance.has("hair_color"):
|
||||
hair_color_picker.color = Color(appearance["hair_color"])
|
||||
if appearance.has("clothing_color"):
|
||||
clothing_color_picker.color = Color(appearance["clothing_color"])
|
||||
|
||||
# 加载个性数据
|
||||
var personality = data.get(CharacterDataClass.FIELD_PERSONALITY, {})
|
||||
var traits = personality.get("traits", [])
|
||||
|
||||
# 设置特征复选框
|
||||
for trait_in in trait_checkboxes:
|
||||
trait_checkboxes[trait_in].button_pressed = trait_in in traits
|
||||
|
||||
# 设置活动选项
|
||||
var activity = personality.get("favorite_activity", "exploring")
|
||||
var personalization = preload("res://scripts/CharacterPersonalization.gd")
|
||||
var activity_index = personalization.ACTIVITIES.find(activity)
|
||||
if activity_index >= 0:
|
||||
activity_option.selected = activity_index
|
||||
|
||||
# 设置简介
|
||||
bio_text.text = personality.get("bio", "")
|
||||
|
||||
# 更新预览
|
||||
_update_preview()
|
||||
|
||||
## 外观变化回调
|
||||
func _on_appearance_changed(_color: Color):
|
||||
## 外观变化时更新预览
|
||||
_update_preview()
|
||||
|
||||
## 随机化外观
|
||||
func _on_randomize_appearance():
|
||||
## 随机化角色外观
|
||||
var personalization = preload("res://scripts/CharacterPersonalization.gd")
|
||||
var random_appearance = personalization.generate_random_appearance()
|
||||
|
||||
body_color_picker.color = Color(random_appearance["body_color"])
|
||||
head_color_picker.color = Color(random_appearance["head_color"])
|
||||
hair_color_picker.color = Color(random_appearance["hair_color"])
|
||||
clothing_color_picker.color = Color(random_appearance["clothing_color"])
|
||||
|
||||
_update_preview()
|
||||
|
||||
## 特征切换回调
|
||||
func _on_trait_toggled(_pressed: bool):
|
||||
## 特征复选框切换时检查数量限制
|
||||
var selected_traits = []
|
||||
for trait_in in trait_checkboxes:
|
||||
if trait_checkboxes[trait_in].button_pressed:
|
||||
selected_traits.append(trait_in)
|
||||
|
||||
# 限制最多4个特征
|
||||
if selected_traits.size() > 4:
|
||||
# 找到最后选中的并取消
|
||||
for trait_in in trait_checkboxes:
|
||||
if trait_checkboxes[trait_in].button_pressed and trait_in not in selected_traits.slice(0, 4):
|
||||
trait_checkboxes[trait_in].button_pressed = false
|
||||
break
|
||||
|
||||
## 保存按钮回调
|
||||
func _on_save_pressed():
|
||||
## 保存自定义设置
|
||||
# 更新外观数据
|
||||
var appearance = {
|
||||
"sprite": "character_01",
|
||||
"color": "#FFFFFF",
|
||||
"body_color": body_color_picker.color.to_html(),
|
||||
"head_color": head_color_picker.color.to_html(),
|
||||
"hair_color": hair_color_picker.color.to_html(),
|
||||
"clothing_color": clothing_color_picker.color.to_html()
|
||||
}
|
||||
CharacterDataClass.set_appearance(character_data, appearance)
|
||||
|
||||
# 更新个性数据
|
||||
var selected_traits = []
|
||||
for trait_in in trait_checkboxes:
|
||||
if trait_checkboxes[trait_in].button_pressed:
|
||||
selected_traits.append(trait_in)
|
||||
|
||||
var personality = {
|
||||
"traits": selected_traits,
|
||||
"bio": bio_text.text,
|
||||
"favorite_activity": preload("res://scripts/CharacterPersonalization.gd").ACTIVITIES[activity_option.selected]
|
||||
}
|
||||
character_data[CharacterDataClass.FIELD_PERSONALITY] = personality
|
||||
|
||||
customization_saved.emit(character_data)
|
||||
|
||||
## 取消按钮回调
|
||||
func _on_cancel_pressed():
|
||||
## 取消自定义
|
||||
customization_cancelled.emit()
|
||||
|
||||
## 处理输入事件
|
||||
func _input(event):
|
||||
## 处理ESC键关闭界面
|
||||
if event is InputEventKey and event.pressed:
|
||||
if event.keycode == KEY_ESCAPE:
|
||||
_on_cancel_pressed()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
## 更新预览
|
||||
func _update_preview():
|
||||
## 更新角色预览
|
||||
if not preview_character:
|
||||
return
|
||||
|
||||
var appearance = {
|
||||
"body_color": body_color_picker.color.to_html(),
|
||||
"head_color": head_color_picker.color.to_html(),
|
||||
"hair_color": hair_color_picker.color.to_html(),
|
||||
"clothing_color": clothing_color_picker.color.to_html()
|
||||
}
|
||||
|
||||
var personalization = preload("res://scripts/CharacterPersonalization.gd")
|
||||
personalization.apply_appearance_to_sprite(preview_character, appearance)
|
||||
1
scripts/CharacterCustomization.gd.uid
Normal file
1
scripts/CharacterCustomization.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cdqyp5bllxe3c
|
||||
386
scripts/CharacterData.gd
Normal file
386
scripts/CharacterData.gd
Normal file
@@ -0,0 +1,386 @@
|
||||
extends Node
|
||||
class_name CharacterData
|
||||
## 角色数据模型
|
||||
## 定义角色数据结构和验证函数
|
||||
|
||||
# 角色数据字段
|
||||
const FIELD_ID = "id"
|
||||
const FIELD_NAME = "name"
|
||||
const FIELD_OWNER_ID = "owner_id"
|
||||
const FIELD_POSITION = "position"
|
||||
const FIELD_IS_ONLINE = "is_online"
|
||||
const FIELD_APPEARANCE = "appearance"
|
||||
const FIELD_CREATED_AT = "created_at"
|
||||
const FIELD_LAST_SEEN = "last_seen"
|
||||
# 个性化字段
|
||||
const FIELD_PERSONALITY = "personality"
|
||||
const FIELD_STATUS = "status"
|
||||
const FIELD_MOOD = "mood"
|
||||
const FIELD_ATTRIBUTES = "attributes"
|
||||
const FIELD_SKILLS = "skills"
|
||||
const FIELD_ACHIEVEMENTS = "achievements"
|
||||
const FIELD_LEVEL = "level"
|
||||
const FIELD_EXPERIENCE = "experience"
|
||||
|
||||
# 名称验证规则
|
||||
const MIN_NAME_LENGTH = 2
|
||||
const MAX_NAME_LENGTH = 20
|
||||
|
||||
## 生成唯一角色 ID
|
||||
static func generate_id() -> String:
|
||||
"""
|
||||
生成唯一的角色 ID(使用时间戳 + 随机数)
|
||||
@return: 唯一 ID 字符串
|
||||
"""
|
||||
var timestamp = Time.get_unix_time_from_system()
|
||||
var random_part = randi()
|
||||
return "char_%d_%d" % [timestamp, random_part]
|
||||
|
||||
## 创建角色数据
|
||||
static func create(char_name: String, owner_id: String, position: Vector2 = Vector2.ZERO) -> Dictionary:
|
||||
"""
|
||||
创建新的角色数据
|
||||
@param char_name: 角色名称
|
||||
@param owner_id: 所有者 ID
|
||||
@param position: 初始位置
|
||||
@return: 角色数据字典
|
||||
"""
|
||||
var now = Time.get_unix_time_from_system()
|
||||
|
||||
return {
|
||||
FIELD_ID: generate_id(),
|
||||
FIELD_NAME: char_name,
|
||||
FIELD_OWNER_ID: owner_id,
|
||||
FIELD_POSITION: {
|
||||
"x": position.x,
|
||||
"y": position.y
|
||||
},
|
||||
FIELD_IS_ONLINE: true,
|
||||
FIELD_APPEARANCE: {
|
||||
"sprite": "character_01",
|
||||
"color": "#FFFFFF",
|
||||
"body_color": "#4A90E2",
|
||||
"head_color": "#F5E6D3",
|
||||
"hair_color": "#8B4513",
|
||||
"clothing_color": "#2E8B57"
|
||||
},
|
||||
FIELD_PERSONALITY: {
|
||||
"traits": ["friendly", "curious"],
|
||||
"bio": "",
|
||||
"favorite_activity": "exploring"
|
||||
},
|
||||
FIELD_STATUS: "active", # active, busy, away, offline
|
||||
FIELD_MOOD: "neutral", # happy, sad, excited, tired, neutral
|
||||
FIELD_ATTRIBUTES: {
|
||||
"charisma": 5,
|
||||
"intelligence": 5,
|
||||
"creativity": 5,
|
||||
"energy": 5
|
||||
},
|
||||
FIELD_SKILLS: {
|
||||
"communication": 1,
|
||||
"problem_solving": 1,
|
||||
"leadership": 1,
|
||||
"collaboration": 1
|
||||
},
|
||||
FIELD_ACHIEVEMENTS: [],
|
||||
FIELD_LEVEL: 1,
|
||||
FIELD_EXPERIENCE: 0,
|
||||
FIELD_CREATED_AT: now,
|
||||
FIELD_LAST_SEEN: now
|
||||
}
|
||||
|
||||
## 验证角色名称
|
||||
static func validate_name(char_name: String) -> bool:
|
||||
"""
|
||||
验证角色名称是否有效
|
||||
@param char_name: 角色名称
|
||||
@return: 是否有效
|
||||
"""
|
||||
# 检查是否为空或仅包含空白字符
|
||||
if char_name.strip_edges().is_empty():
|
||||
return false
|
||||
|
||||
# 检查长度
|
||||
var trimmed_name = char_name.strip_edges()
|
||||
if trimmed_name.length() < MIN_NAME_LENGTH or trimmed_name.length() > MAX_NAME_LENGTH:
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
## 验证角色数据完整性
|
||||
static func validate(data: Dictionary) -> bool:
|
||||
"""
|
||||
验证角色数据是否完整且有效
|
||||
@param data: 角色数据字典
|
||||
@return: 是否有效
|
||||
"""
|
||||
# 检查必需字段
|
||||
if not data.has(FIELD_ID) or not data[FIELD_ID] is String:
|
||||
return false
|
||||
if not data.has(FIELD_NAME) or not data[FIELD_NAME] is String:
|
||||
return false
|
||||
if not data.has(FIELD_OWNER_ID) or not data[FIELD_OWNER_ID] is String:
|
||||
return false
|
||||
if not data.has(FIELD_POSITION) or not data[FIELD_POSITION] is Dictionary:
|
||||
return false
|
||||
if not data.has(FIELD_IS_ONLINE) or not data[FIELD_IS_ONLINE] is bool:
|
||||
return false
|
||||
|
||||
# 验证名称
|
||||
if not validate_name(data[FIELD_NAME]):
|
||||
return false
|
||||
|
||||
# 验证位置数据
|
||||
var pos = data[FIELD_POSITION]
|
||||
if not pos.has("x") or not pos.has("y"):
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
## 从字典创建 Vector2 位置
|
||||
static func get_position(data: Dictionary) -> Vector2:
|
||||
"""
|
||||
从角色数据中提取位置
|
||||
@param data: 角色数据字典
|
||||
@return: Vector2 位置
|
||||
"""
|
||||
if data.has(FIELD_POSITION):
|
||||
var pos = data[FIELD_POSITION]
|
||||
return Vector2(pos.get("x", 0), pos.get("y", 0))
|
||||
return Vector2.ZERO
|
||||
|
||||
## 更新角色位置
|
||||
static func set_position(data: Dictionary, position: Vector2) -> void:
|
||||
"""
|
||||
更新角色数据中的位置
|
||||
@param data: 角色数据字典
|
||||
@param position: 新位置
|
||||
"""
|
||||
data[FIELD_POSITION] = {
|
||||
"x": position.x,
|
||||
"y": position.y
|
||||
}
|
||||
|
||||
## 更新在线状态
|
||||
static func set_online_status(data: Dictionary, is_online: bool) -> void:
|
||||
"""
|
||||
更新角色在线状态
|
||||
@param data: 角色数据字典
|
||||
@param is_online: 是否在线
|
||||
"""
|
||||
data[FIELD_IS_ONLINE] = is_online
|
||||
data[FIELD_LAST_SEEN] = Time.get_unix_time_from_system()
|
||||
|
||||
## 序列化为 JSON
|
||||
static func to_json(data: Dictionary) -> String:
|
||||
"""
|
||||
将角色数据序列化为 JSON
|
||||
@param data: 角色数据字典
|
||||
@return: JSON 字符串
|
||||
"""
|
||||
return JSON.stringify(data)
|
||||
|
||||
## 从 JSON 反序列化
|
||||
static func from_json(json_string: String) -> Dictionary:
|
||||
"""
|
||||
从 JSON 反序列化角色数据
|
||||
@param json_string: JSON 字符串
|
||||
@return: 角色数据字典,失败返回空字典
|
||||
"""
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(json_string)
|
||||
|
||||
if parse_result == OK:
|
||||
var data = json.data
|
||||
if validate(data):
|
||||
return data
|
||||
else:
|
||||
push_error("Invalid character data in JSON")
|
||||
else:
|
||||
push_error("Failed to parse character JSON")
|
||||
|
||||
return {}
|
||||
|
||||
## 克隆角色数据
|
||||
static func clone(data: Dictionary) -> Dictionary:
|
||||
"""
|
||||
深度克隆角色数据
|
||||
@param data: 原始角色数据
|
||||
@return: 克隆的角色数据
|
||||
"""
|
||||
return from_json(to_json(data))
|
||||
|
||||
## 个性化相关函数
|
||||
|
||||
## 更新角色外观
|
||||
static func set_appearance(data: Dictionary, appearance: Dictionary) -> void:
|
||||
"""
|
||||
更新角色外观
|
||||
@param data: 角色数据字典
|
||||
@param appearance: 外观数据
|
||||
"""
|
||||
if not data.has(FIELD_APPEARANCE):
|
||||
data[FIELD_APPEARANCE] = {}
|
||||
|
||||
for key in appearance:
|
||||
data[FIELD_APPEARANCE][key] = appearance[key]
|
||||
|
||||
## 设置角色状态
|
||||
static func set_status(data: Dictionary, status: String) -> void:
|
||||
"""
|
||||
设置角色状态
|
||||
@param data: 角色数据字典
|
||||
@param status: 状态 (active, busy, away, offline)
|
||||
"""
|
||||
var valid_statuses = ["active", "busy", "away", "offline"]
|
||||
if status in valid_statuses:
|
||||
data[FIELD_STATUS] = status
|
||||
|
||||
## 设置角色心情
|
||||
static func set_mood(data: Dictionary, mood: String) -> void:
|
||||
"""
|
||||
设置角色心情
|
||||
@param data: 角色数据字典
|
||||
@param mood: 心情 (happy, sad, excited, tired, neutral)
|
||||
"""
|
||||
var valid_moods = ["happy", "sad", "excited", "tired", "neutral"]
|
||||
if mood in valid_moods:
|
||||
data[FIELD_MOOD] = mood
|
||||
|
||||
## 更新角色属性
|
||||
static func set_attribute(data: Dictionary, attribute: String, value: int) -> void:
|
||||
"""
|
||||
设置角色属性值
|
||||
@param data: 角色数据字典
|
||||
@param attribute: 属性名称
|
||||
@param value: 属性值 (1-10)
|
||||
"""
|
||||
if not data.has(FIELD_ATTRIBUTES):
|
||||
data[FIELD_ATTRIBUTES] = {}
|
||||
|
||||
# 限制属性值范围
|
||||
value = clamp(value, 1, 10)
|
||||
data[FIELD_ATTRIBUTES][attribute] = value
|
||||
|
||||
## 更新角色技能
|
||||
static func set_skill(data: Dictionary, skill: String, level: int) -> void:
|
||||
"""
|
||||
设置角色技能等级
|
||||
@param data: 角色数据字典
|
||||
@param skill: 技能名称
|
||||
@param level: 技能等级 (1-10)
|
||||
"""
|
||||
if not data.has(FIELD_SKILLS):
|
||||
data[FIELD_SKILLS] = {}
|
||||
|
||||
# 限制技能等级范围
|
||||
level = clamp(level, 1, 10)
|
||||
data[FIELD_SKILLS][skill] = level
|
||||
|
||||
## 添加成就
|
||||
static func add_achievement(data: Dictionary, achievement: Dictionary) -> void:
|
||||
"""
|
||||
添加成就
|
||||
@param data: 角色数据字典
|
||||
@param achievement: 成就数据 {id, name, description, earned_at}
|
||||
"""
|
||||
if not data.has(FIELD_ACHIEVEMENTS):
|
||||
data[FIELD_ACHIEVEMENTS] = []
|
||||
|
||||
# 检查是否已有此成就
|
||||
for existing in data[FIELD_ACHIEVEMENTS]:
|
||||
if existing.get("id") == achievement.get("id"):
|
||||
return # 已有此成就
|
||||
|
||||
achievement["earned_at"] = Time.get_unix_time_from_system()
|
||||
data[FIELD_ACHIEVEMENTS].append(achievement)
|
||||
|
||||
## 增加经验值
|
||||
static func add_experience(data: Dictionary, experience: int) -> bool:
|
||||
"""
|
||||
增加经验值,如果升级返回 true
|
||||
@param data: 角色数据字典
|
||||
@param experience: 经验值
|
||||
@return: 是否升级
|
||||
"""
|
||||
if not data.has(FIELD_EXPERIENCE):
|
||||
data[FIELD_EXPERIENCE] = 0
|
||||
if not data.has(FIELD_LEVEL):
|
||||
data[FIELD_LEVEL] = 1
|
||||
|
||||
data[FIELD_EXPERIENCE] += experience
|
||||
|
||||
# 检查是否升级
|
||||
var current_level = data[FIELD_LEVEL]
|
||||
var required_exp = get_required_experience(current_level)
|
||||
|
||||
if data[FIELD_EXPERIENCE] >= required_exp:
|
||||
data[FIELD_LEVEL] += 1
|
||||
data[FIELD_EXPERIENCE] -= required_exp
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
## 获取升级所需经验值
|
||||
static func get_required_experience(level: int) -> int:
|
||||
"""
|
||||
获取升级到下一级所需的经验值
|
||||
@param level: 当前等级
|
||||
@return: 所需经验值
|
||||
"""
|
||||
return level * 100 + (level - 1) * 50 # 递增的经验需求
|
||||
|
||||
## 获取角色总体评分
|
||||
static func get_character_score(data: Dictionary) -> int:
|
||||
"""
|
||||
计算角色的总体评分
|
||||
@param data: 角色数据字典
|
||||
@return: 总体评分
|
||||
"""
|
||||
var score = 0
|
||||
|
||||
# 等级贡献
|
||||
score += data.get(FIELD_LEVEL, 1) * 10
|
||||
|
||||
# 属性贡献
|
||||
var attributes = data.get(FIELD_ATTRIBUTES, {})
|
||||
for value in attributes.values():
|
||||
score += value
|
||||
|
||||
# 技能贡献
|
||||
var skills = data.get(FIELD_SKILLS, {})
|
||||
for value in skills.values():
|
||||
score += value * 2
|
||||
|
||||
# 成就贡献
|
||||
var achievements = data.get(FIELD_ACHIEVEMENTS, [])
|
||||
score += achievements.size() * 5
|
||||
|
||||
return score
|
||||
|
||||
## 获取个性化数据摘要
|
||||
static func get_personality_summary(data: Dictionary) -> String:
|
||||
"""
|
||||
获取角色个性化数据的摘要
|
||||
@param data: 角色数据字典
|
||||
@return: 个性化摘要文本
|
||||
"""
|
||||
var summary = []
|
||||
|
||||
# 等级和经验
|
||||
var level = data.get(FIELD_LEVEL, 1)
|
||||
var experience = data.get(FIELD_EXPERIENCE, 0)
|
||||
summary.append("等级 %d (%d 经验)" % [level, experience])
|
||||
|
||||
# 状态和心情
|
||||
var status = data.get(FIELD_STATUS, "active")
|
||||
var mood = data.get(FIELD_MOOD, "neutral")
|
||||
summary.append("状态: %s, 心情: %s" % [status, mood])
|
||||
|
||||
# 成就数量
|
||||
var achievements = data.get(FIELD_ACHIEVEMENTS, [])
|
||||
if achievements.size() > 0:
|
||||
summary.append("%d 个成就" % achievements.size())
|
||||
|
||||
return " | ".join(summary)
|
||||
1
scripts/CharacterData.gd.uid
Normal file
1
scripts/CharacterData.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bo5u3j1ktg6k6
|
||||
292
scripts/CharacterPersonalization.gd
Normal file
292
scripts/CharacterPersonalization.gd
Normal file
@@ -0,0 +1,292 @@
|
||||
## 角色个性化管理器
|
||||
## 处理角色外观、属性、技能和成就系统
|
||||
## 这是一个静态工具类,所有方法都是静态的
|
||||
|
||||
# 预定义的个性特征
|
||||
const PERSONALITY_TRAITS = [
|
||||
"friendly", "curious", "creative", "analytical", "energetic",
|
||||
"calm", "adventurous", "thoughtful", "optimistic", "practical",
|
||||
"artistic", "logical", "social", "independent", "collaborative"
|
||||
]
|
||||
|
||||
# 预定义的活动类型
|
||||
const ACTIVITIES = [
|
||||
"exploring", "learning", "creating", "socializing", "problem_solving",
|
||||
"teaching", "researching", "building", "organizing", "innovating"
|
||||
]
|
||||
|
||||
# 成就定义
|
||||
const ACHIEVEMENTS = {
|
||||
"first_login": {
|
||||
"name": "初来乍到",
|
||||
"description": "首次登录游戏",
|
||||
"icon": "🎉"
|
||||
},
|
||||
"social_butterfly": {
|
||||
"name": "社交达人",
|
||||
"description": "与10个不同角色对话",
|
||||
"icon": "🦋"
|
||||
},
|
||||
"explorer": {
|
||||
"name": "探索者",
|
||||
"description": "访问场景的所有区域",
|
||||
"icon": "🗺️"
|
||||
},
|
||||
"communicator": {
|
||||
"name": "沟通专家",
|
||||
"description": "发送100条消息",
|
||||
"icon": "💬"
|
||||
},
|
||||
"veteran": {
|
||||
"name": "老玩家",
|
||||
"description": "连续登录7天",
|
||||
"icon": "⭐"
|
||||
},
|
||||
"level_5": {
|
||||
"name": "小有成就",
|
||||
"description": "达到5级",
|
||||
"icon": "🏆"
|
||||
},
|
||||
"level_10": {
|
||||
"name": "经验丰富",
|
||||
"description": "达到10级",
|
||||
"icon": "👑"
|
||||
}
|
||||
}
|
||||
|
||||
# 心情对应的表情符号
|
||||
const MOOD_EMOJIS = {
|
||||
"happy": "😊",
|
||||
"sad": "😢",
|
||||
"excited": "🤩",
|
||||
"tired": "😴",
|
||||
"neutral": "😐"
|
||||
}
|
||||
|
||||
# 状态对应的颜色
|
||||
const STATUS_COLORS = {
|
||||
"active": Color.GREEN,
|
||||
"busy": Color.ORANGE,
|
||||
"away": Color.YELLOW,
|
||||
"offline": Color.GRAY
|
||||
}
|
||||
|
||||
## 生成随机外观
|
||||
static func generate_random_appearance() -> Dictionary:
|
||||
## 生成随机的角色外观
|
||||
## @return: 外观数据字典
|
||||
var colors = [
|
||||
"#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7",
|
||||
"#DDA0DD", "#98D8C8", "#F7DC6F", "#BB8FCE", "#85C1E9"
|
||||
]
|
||||
|
||||
return {
|
||||
"sprite": "character_01",
|
||||
"color": "#FFFFFF",
|
||||
"body_color": colors[randi() % colors.size()],
|
||||
"head_color": "#F5E6D3",
|
||||
"hair_color": ["#8B4513", "#000000", "#FFD700", "#FF4500", "#4B0082"][randi() % 5],
|
||||
"clothing_color": colors[randi() % colors.size()]
|
||||
}
|
||||
|
||||
## 生成随机个性
|
||||
static func generate_random_personality() -> Dictionary:
|
||||
## 生成随机的角色个性
|
||||
## @return: 个性数据字典
|
||||
var traits = []
|
||||
var num_traits = randi_range(2, 4)
|
||||
|
||||
for i in range(num_traits):
|
||||
var trait_in = PERSONALITY_TRAITS[randi() % PERSONALITY_TRAITS.size()]
|
||||
if trait_in not in traits:
|
||||
traits.append(trait_in)
|
||||
|
||||
return {
|
||||
"traits": traits,
|
||||
"bio": "",
|
||||
"favorite_activity": ACTIVITIES[randi() % ACTIVITIES.size()]
|
||||
}
|
||||
|
||||
## 生成随机属性
|
||||
static func generate_random_attributes() -> Dictionary:
|
||||
## 生成随机的角色属性
|
||||
## @return: 属性数据字典
|
||||
return {
|
||||
"charisma": randi_range(3, 7),
|
||||
"intelligence": randi_range(3, 7),
|
||||
"creativity": randi_range(3, 7),
|
||||
"energy": randi_range(3, 7)
|
||||
}
|
||||
|
||||
## 检查并授予成就
|
||||
static func check_achievements(character_data: Dictionary, action: String, value: int = 1) -> Array:
|
||||
## 检查并授予成就
|
||||
## @param character_data: 角色数据
|
||||
## @param action: 触发的动作类型
|
||||
## @param value: 动作的值
|
||||
## @return: 新获得的成就列表
|
||||
var new_achievements = []
|
||||
var current_achievements = character_data.get(CharacterData.FIELD_ACHIEVEMENTS, [])
|
||||
var achievement_ids = []
|
||||
|
||||
# 获取已有成就的ID列表
|
||||
for achievement in current_achievements:
|
||||
achievement_ids.append(achievement.get("id", ""))
|
||||
|
||||
match action:
|
||||
"first_login":
|
||||
if "first_login" not in achievement_ids:
|
||||
new_achievements.append(_create_achievement("first_login"))
|
||||
|
||||
"dialogue_sent":
|
||||
# 检查消息数量成就
|
||||
if value >= 100 and "communicator" not in achievement_ids:
|
||||
new_achievements.append(_create_achievement("communicator"))
|
||||
|
||||
"character_met":
|
||||
# 检查社交成就
|
||||
if value >= 10 and "social_butterfly" not in achievement_ids:
|
||||
new_achievements.append(_create_achievement("social_butterfly"))
|
||||
|
||||
"level_up":
|
||||
var level = character_data.get(CharacterData.FIELD_LEVEL, 1)
|
||||
if level >= 5 and "level_5" not in achievement_ids:
|
||||
new_achievements.append(_create_achievement("level_5"))
|
||||
if level >= 10 and "level_10" not in achievement_ids:
|
||||
new_achievements.append(_create_achievement("level_10"))
|
||||
|
||||
# 添加新成就到角色数据
|
||||
for achievement in new_achievements:
|
||||
CharacterData.add_achievement(character_data, achievement)
|
||||
|
||||
return new_achievements
|
||||
|
||||
## 创建成就对象
|
||||
static func _create_achievement(achievement_id: String) -> Dictionary:
|
||||
## 创建成就对象
|
||||
## @param achievement_id: 成就ID
|
||||
## @return: 成就数据字典
|
||||
var achievement_data = ACHIEVEMENTS.get(achievement_id, {})
|
||||
return {
|
||||
"id": achievement_id,
|
||||
"name": achievement_data.get("name", "未知成就"),
|
||||
"description": achievement_data.get("description", ""),
|
||||
"icon": achievement_data.get("icon", "🏆"),
|
||||
"earned_at": Time.get_unix_time_from_system()
|
||||
}
|
||||
|
||||
## 计算技能经验奖励
|
||||
static func calculate_skill_experience(action: String) -> Dictionary:
|
||||
## 根据动作计算技能经验奖励
|
||||
## @param action: 动作类型
|
||||
## @return: 技能经验字典 {skill_name: exp_amount}
|
||||
match action:
|
||||
"dialogue_sent":
|
||||
return {"communication": 2}
|
||||
"problem_solved":
|
||||
return {"problem_solving": 5}
|
||||
"helped_player":
|
||||
return {"collaboration": 3, "leadership": 1}
|
||||
"creative_action":
|
||||
return {"creativity": 3}
|
||||
_:
|
||||
return {}
|
||||
|
||||
## 更新角色技能
|
||||
static func update_character_skills(character_data: Dictionary, skill_exp: Dictionary) -> Array:
|
||||
## 更新角色技能并返回升级的技能
|
||||
## @param character_data: 角色数据
|
||||
## @param skill_exp: 技能经验字典
|
||||
## @return: 升级的技能列表
|
||||
var leveled_skills = []
|
||||
|
||||
if not character_data.has(CharacterData.FIELD_SKILLS):
|
||||
character_data[CharacterData.FIELD_SKILLS] = {
|
||||
"communication": 1,
|
||||
"problem_solving": 1,
|
||||
"leadership": 1,
|
||||
"collaboration": 1
|
||||
}
|
||||
|
||||
for skill in skill_exp:
|
||||
var current_level = character_data[CharacterData.FIELD_SKILLS].get(skill, 1)
|
||||
var exp_needed = current_level * 10 # 每级需要更多经验
|
||||
|
||||
# 简化:直接根据经验值判断是否升级
|
||||
if skill_exp[skill] >= exp_needed and current_level < 10:
|
||||
character_data[CharacterData.FIELD_SKILLS][skill] = current_level + 1
|
||||
leveled_skills.append({
|
||||
"skill": skill,
|
||||
"old_level": current_level,
|
||||
"new_level": current_level + 1
|
||||
})
|
||||
|
||||
return leveled_skills
|
||||
|
||||
## 获取心情表情符号
|
||||
static func get_mood_emoji(mood: String) -> String:
|
||||
## 获取心情对应的表情符号
|
||||
## @param mood: 心情名称
|
||||
## @return: 表情符号
|
||||
return MOOD_EMOJIS.get(mood, "😐")
|
||||
|
||||
## 获取状态颜色
|
||||
static func get_status_color(status: String) -> Color:
|
||||
## 获取状态对应的颜色
|
||||
## @param status: 状态名称
|
||||
## @return: 颜色
|
||||
return STATUS_COLORS.get(status, Color.WHITE)
|
||||
|
||||
## 生成个性化描述
|
||||
static func generate_personality_description(character_data: Dictionary) -> String:
|
||||
## 生成角色的个性化描述
|
||||
## @param character_data: 角色数据
|
||||
## @return: 描述文本
|
||||
var personality = character_data.get(CharacterData.FIELD_PERSONALITY, {})
|
||||
var traits = personality.get("traits", [])
|
||||
var activity = personality.get("favorite_activity", "exploring")
|
||||
var level = character_data.get(CharacterData.FIELD_LEVEL, 1)
|
||||
var mood = character_data.get(CharacterData.FIELD_MOOD, "neutral")
|
||||
|
||||
var description = "这是一个"
|
||||
|
||||
if traits.size() > 0:
|
||||
description += "、".join(traits) + "的"
|
||||
|
||||
description += "角色,"
|
||||
description += "喜欢" + activity + ","
|
||||
description += "目前是" + str(level) + "级,"
|
||||
description += "心情" + mood + "。"
|
||||
|
||||
return description
|
||||
|
||||
## 自定义外观验证
|
||||
static func validate_appearance(appearance: Dictionary) -> bool:
|
||||
## 验证外观数据是否有效
|
||||
## @param appearance: 外观数据
|
||||
## @return: 是否有效
|
||||
# 检查必需字段
|
||||
var required_fields = ["body_color", "head_color", "hair_color", "clothing_color"]
|
||||
for field in required_fields:
|
||||
if not appearance.has(field):
|
||||
return false
|
||||
|
||||
# 验证颜色格式
|
||||
var color_str = appearance[field]
|
||||
if not color_str is String or not color_str.begins_with("#") or color_str.length() != 7:
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
## 应用外观到精灵
|
||||
static func apply_appearance_to_sprite(sprite: CharacterSprite, appearance: Dictionary) -> void:
|
||||
## 将外观数据应用到角色精灵
|
||||
## @param sprite: 角色精灵
|
||||
## @param appearance: 外观数据
|
||||
if not sprite or not validate_appearance(appearance):
|
||||
return
|
||||
|
||||
var body_color = Color(appearance.get("body_color", "#4A90E2"))
|
||||
var head_color = Color(appearance.get("head_color", "#F5E6D3"))
|
||||
|
||||
sprite.set_character_color(body_color, head_color)
|
||||
1
scripts/CharacterPersonalization.gd.uid
Normal file
1
scripts/CharacterPersonalization.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b8vw1w7qwkma8
|
||||
313
scripts/CharacterProfile.gd
Normal file
313
scripts/CharacterProfile.gd
Normal file
@@ -0,0 +1,313 @@
|
||||
extends Control
|
||||
class_name CharacterProfile
|
||||
## 角色档案界面
|
||||
## 显示角色的详细信息和个性化数据
|
||||
|
||||
# UI 元素
|
||||
var container: VBoxContainer
|
||||
var character_name_label: Label
|
||||
var level_exp_label: Label
|
||||
var status_mood_label: Label
|
||||
var traits_label: Label
|
||||
var bio_label: Label
|
||||
var attributes_container: VBoxContainer
|
||||
var skills_container: VBoxContainer
|
||||
var achievements_container: VBoxContainer
|
||||
var close_button: Button
|
||||
|
||||
# 角色数据
|
||||
var character_data: Dictionary = {}
|
||||
|
||||
# 信号
|
||||
signal profile_closed()
|
||||
|
||||
func _ready():
|
||||
"""初始化档案界面"""
|
||||
_create_ui()
|
||||
_setup_connections()
|
||||
|
||||
## 创建UI
|
||||
func _create_ui():
|
||||
"""创建档案界面的所有UI元素"""
|
||||
# 设置背景
|
||||
var panel = Panel.new()
|
||||
panel.anchor_right = 1.0
|
||||
panel.anchor_bottom = 1.0
|
||||
add_child(panel)
|
||||
|
||||
# 主容器
|
||||
container = VBoxContainer.new()
|
||||
container.anchor_left = 0.1
|
||||
container.anchor_right = 0.9
|
||||
container.anchor_top = 0.1
|
||||
container.anchor_bottom = 0.9
|
||||
add_child(container)
|
||||
|
||||
# 标题
|
||||
var title = Label.new()
|
||||
title.text = "角色档案"
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
title.add_theme_font_size_override("font_size", 24)
|
||||
container.add_child(title)
|
||||
|
||||
container.add_child(_create_spacer(10))
|
||||
|
||||
# 角色名称
|
||||
character_name_label = Label.new()
|
||||
character_name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
character_name_label.add_theme_font_size_override("font_size", 20)
|
||||
container.add_child(character_name_label)
|
||||
|
||||
# 等级和经验
|
||||
level_exp_label = Label.new()
|
||||
level_exp_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
container.add_child(level_exp_label)
|
||||
|
||||
# 状态和心情
|
||||
status_mood_label = Label.new()
|
||||
status_mood_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
container.add_child(status_mood_label)
|
||||
|
||||
container.add_child(_create_spacer(10))
|
||||
|
||||
# 个性特征
|
||||
var traits_title = Label.new()
|
||||
traits_title.text = "个性特征:"
|
||||
traits_title.add_theme_font_size_override("font_size", 16)
|
||||
container.add_child(traits_title)
|
||||
|
||||
traits_label = Label.new()
|
||||
traits_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
container.add_child(traits_label)
|
||||
|
||||
container.add_child(_create_spacer(5))
|
||||
|
||||
# 个人简介
|
||||
var bio_title = Label.new()
|
||||
bio_title.text = "个人简介:"
|
||||
bio_title.add_theme_font_size_override("font_size", 16)
|
||||
container.add_child(bio_title)
|
||||
|
||||
bio_label = Label.new()
|
||||
bio_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
container.add_child(bio_label)
|
||||
|
||||
container.add_child(_create_spacer(10))
|
||||
|
||||
# 属性
|
||||
var attributes_title = Label.new()
|
||||
attributes_title.text = "属性:"
|
||||
attributes_title.add_theme_font_size_override("font_size", 16)
|
||||
container.add_child(attributes_title)
|
||||
|
||||
attributes_container = VBoxContainer.new()
|
||||
container.add_child(attributes_container)
|
||||
|
||||
container.add_child(_create_spacer(5))
|
||||
|
||||
# 技能
|
||||
var skills_title = Label.new()
|
||||
skills_title.text = "技能:"
|
||||
skills_title.add_theme_font_size_override("font_size", 16)
|
||||
container.add_child(skills_title)
|
||||
|
||||
skills_container = VBoxContainer.new()
|
||||
container.add_child(skills_container)
|
||||
|
||||
container.add_child(_create_spacer(5))
|
||||
|
||||
# 成就
|
||||
var achievements_title = Label.new()
|
||||
achievements_title.text = "成就:"
|
||||
achievements_title.add_theme_font_size_override("font_size", 16)
|
||||
container.add_child(achievements_title)
|
||||
|
||||
achievements_container = VBoxContainer.new()
|
||||
container.add_child(achievements_container)
|
||||
|
||||
container.add_child(_create_spacer(10))
|
||||
|
||||
# 关闭按钮
|
||||
close_button = Button.new()
|
||||
close_button.text = "关闭"
|
||||
close_button.custom_minimum_size = Vector2(100, 40)
|
||||
var button_center = CenterContainer.new()
|
||||
button_center.add_child(close_button)
|
||||
container.add_child(button_center)
|
||||
|
||||
## 创建间距
|
||||
func _create_spacer(height: float) -> Control:
|
||||
"""创建垂直间距"""
|
||||
var spacer = Control.new()
|
||||
spacer.custom_minimum_size = Vector2(0, height)
|
||||
return spacer
|
||||
|
||||
## 设置连接
|
||||
func _setup_connections():
|
||||
"""设置信号连接"""
|
||||
close_button.pressed.connect(_on_close_pressed)
|
||||
|
||||
## 加载角色数据
|
||||
func load_character_data(data: Dictionary):
|
||||
"""
|
||||
加载并显示角色数据
|
||||
@param data: 角色数据字典
|
||||
"""
|
||||
character_data = data
|
||||
_update_display()
|
||||
|
||||
## 更新显示
|
||||
func _update_display():
|
||||
"""更新所有显示内容"""
|
||||
if character_data.is_empty():
|
||||
return
|
||||
|
||||
# 角色名称
|
||||
var name = character_data.get(CharacterData.FIELD_NAME, "Unknown")
|
||||
character_name_label.text = name
|
||||
|
||||
# 等级和经验
|
||||
var level = character_data.get(CharacterData.FIELD_LEVEL, 1)
|
||||
var exp = character_data.get(CharacterData.FIELD_EXPERIENCE, 0)
|
||||
var required_exp = CharacterData.get_required_experience(level)
|
||||
level_exp_label.text = "等级 %d (%d/%d 经验)" % [level, exp, required_exp]
|
||||
|
||||
# 状态和心情
|
||||
var status = character_data.get(CharacterData.FIELD_STATUS, "active")
|
||||
var mood = character_data.get(CharacterData.FIELD_MOOD, "neutral")
|
||||
var personalization = preload("res://scripts/CharacterPersonalization.gd")
|
||||
var mood_emoji = personalization.get_mood_emoji(mood)
|
||||
status_mood_label.text = "状态: %s | 心情: %s %s" % [status, mood, mood_emoji]
|
||||
|
||||
# 个性特征
|
||||
var personality = character_data.get(CharacterData.FIELD_PERSONALITY, {})
|
||||
var traits = personality.get("traits", [])
|
||||
var activity = personality.get("favorite_activity", "exploring")
|
||||
|
||||
if traits.size() > 0:
|
||||
traits_label.text = "、".join(traits) + " | 喜欢: " + activity
|
||||
else:
|
||||
traits_label.text = "喜欢: " + activity
|
||||
|
||||
# 个人简介
|
||||
var bio = personality.get("bio", "")
|
||||
if bio.is_empty():
|
||||
bio_label.text = "这个角色还没有写个人简介。"
|
||||
else:
|
||||
bio_label.text = bio
|
||||
|
||||
# 属性
|
||||
_update_attributes()
|
||||
|
||||
# 技能
|
||||
_update_skills()
|
||||
|
||||
# 成就
|
||||
_update_achievements()
|
||||
|
||||
## 更新属性显示
|
||||
func _update_attributes():
|
||||
"""更新属性显示"""
|
||||
# 清除现有内容
|
||||
for child in attributes_container.get_children():
|
||||
child.queue_free()
|
||||
|
||||
var attributes = character_data.get(CharacterData.FIELD_ATTRIBUTES, {})
|
||||
|
||||
for attr_name in attributes:
|
||||
var value = attributes[attr_name]
|
||||
var attr_container = HBoxContainer.new()
|
||||
|
||||
var name_label = Label.new()
|
||||
name_label.text = attr_name + ":"
|
||||
name_label.custom_minimum_size = Vector2(100, 0)
|
||||
attr_container.add_child(name_label)
|
||||
|
||||
var progress_bar = ProgressBar.new()
|
||||
progress_bar.min_value = 1
|
||||
progress_bar.max_value = 10
|
||||
progress_bar.value = value
|
||||
progress_bar.custom_minimum_size = Vector2(200, 20)
|
||||
attr_container.add_child(progress_bar)
|
||||
|
||||
var value_label = Label.new()
|
||||
value_label.text = str(value) + "/10"
|
||||
attr_container.add_child(value_label)
|
||||
|
||||
attributes_container.add_child(attr_container)
|
||||
|
||||
## 更新技能显示
|
||||
func _update_skills():
|
||||
"""更新技能显示"""
|
||||
# 清除现有内容
|
||||
for child in skills_container.get_children():
|
||||
child.queue_free()
|
||||
|
||||
var skills = character_data.get(CharacterData.FIELD_SKILLS, {})
|
||||
|
||||
for skill_name in skills:
|
||||
var level = skills[skill_name]
|
||||
var skill_container = HBoxContainer.new()
|
||||
|
||||
var name_label = Label.new()
|
||||
name_label.text = skill_name + ":"
|
||||
name_label.custom_minimum_size = Vector2(100, 0)
|
||||
skill_container.add_child(name_label)
|
||||
|
||||
var progress_bar = ProgressBar.new()
|
||||
progress_bar.min_value = 1
|
||||
progress_bar.max_value = 10
|
||||
progress_bar.value = level
|
||||
progress_bar.custom_minimum_size = Vector2(200, 20)
|
||||
skill_container.add_child(progress_bar)
|
||||
|
||||
var level_label = Label.new()
|
||||
level_label.text = "等级 " + str(level)
|
||||
skill_container.add_child(level_label)
|
||||
|
||||
skills_container.add_child(skill_container)
|
||||
|
||||
## 更新成就显示
|
||||
func _update_achievements():
|
||||
"""更新成就显示"""
|
||||
# 清除现有内容
|
||||
for child in achievements_container.get_children():
|
||||
child.queue_free()
|
||||
|
||||
var achievements = character_data.get(CharacterData.FIELD_ACHIEVEMENTS, [])
|
||||
|
||||
if achievements.is_empty():
|
||||
var no_achievements = Label.new()
|
||||
no_achievements.text = "还没有获得任何成就。"
|
||||
achievements_container.add_child(no_achievements)
|
||||
return
|
||||
|
||||
for achievement in achievements:
|
||||
var achievement_container = HBoxContainer.new()
|
||||
|
||||
var icon_label = Label.new()
|
||||
icon_label.text = achievement.get("icon", "🏆")
|
||||
icon_label.add_theme_font_size_override("font_size", 20)
|
||||
achievement_container.add_child(icon_label)
|
||||
|
||||
var info_container = VBoxContainer.new()
|
||||
|
||||
var name_label = Label.new()
|
||||
name_label.text = achievement.get("name", "Unknown Achievement")
|
||||
name_label.add_theme_font_size_override("font_size", 14)
|
||||
info_container.add_child(name_label)
|
||||
|
||||
var desc_label = Label.new()
|
||||
desc_label.text = achievement.get("description", "")
|
||||
desc_label.add_theme_font_size_override("font_size", 12)
|
||||
desc_label.add_theme_color_override("font_color", Color.GRAY)
|
||||
info_container.add_child(desc_label)
|
||||
|
||||
achievement_container.add_child(info_container)
|
||||
achievements_container.add_child(achievement_container)
|
||||
|
||||
## 关闭按钮回调
|
||||
func _on_close_pressed():
|
||||
"""关闭档案界面"""
|
||||
profile_closed.emit()
|
||||
queue_free()
|
||||
1
scripts/CharacterProfile.gd.uid
Normal file
1
scripts/CharacterProfile.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://depubcgpi7yn7
|
||||
104
scripts/CharacterSprite.gd
Normal file
104
scripts/CharacterSprite.gd
Normal file
@@ -0,0 +1,104 @@
|
||||
extends Node2D
|
||||
class_name CharacterSprite
|
||||
## 程序化角色精灵
|
||||
## 在没有实际精灵图时使用,可以轻松替换为真实精灵
|
||||
|
||||
# 角色颜色
|
||||
var body_color: Color = Color(0.4, 0.6, 0.8) # 身体颜色
|
||||
var head_color: Color = Color(0.9, 0.8, 0.7) # 头部颜色
|
||||
|
||||
# 角色尺寸
|
||||
const BODY_WIDTH = 32
|
||||
const BODY_HEIGHT = 48
|
||||
const HEAD_SIZE = 24
|
||||
|
||||
# 节点引用
|
||||
var body: ColorRect
|
||||
var head: ColorRect
|
||||
var eyes: Node2D
|
||||
|
||||
func _ready():
|
||||
_create_character()
|
||||
|
||||
## 创建角色视觉
|
||||
func _create_character():
|
||||
"""程序化创建角色外观"""
|
||||
# 身体
|
||||
body = ColorRect.new()
|
||||
body.size = Vector2(BODY_WIDTH, BODY_HEIGHT)
|
||||
body.position = Vector2(-BODY_WIDTH / 2.0, -BODY_HEIGHT)
|
||||
body.color = body_color
|
||||
add_child(body)
|
||||
|
||||
# 头部
|
||||
head = ColorRect.new()
|
||||
head.size = Vector2(HEAD_SIZE, HEAD_SIZE)
|
||||
head.position = Vector2(-HEAD_SIZE / 2.0, -BODY_HEIGHT - HEAD_SIZE)
|
||||
head.color = head_color
|
||||
add_child(head)
|
||||
|
||||
# 眼睛
|
||||
eyes = Node2D.new()
|
||||
eyes.position = Vector2(0, -BODY_HEIGHT - HEAD_SIZE / 2.0)
|
||||
add_child(eyes)
|
||||
|
||||
# 左眼
|
||||
var left_eye = ColorRect.new()
|
||||
left_eye.size = Vector2(4, 4)
|
||||
left_eye.position = Vector2(-8, -2)
|
||||
left_eye.color = Color.BLACK
|
||||
eyes.add_child(left_eye)
|
||||
|
||||
# 右眼
|
||||
var right_eye = ColorRect.new()
|
||||
right_eye.size = Vector2(4, 4)
|
||||
right_eye.position = Vector2(4, -2)
|
||||
right_eye.color = Color.BLACK
|
||||
eyes.add_child(right_eye)
|
||||
|
||||
## 设置角色颜色
|
||||
func set_character_color(new_body_color: Color, new_head_color: Color = Color(0.9, 0.8, 0.7)):
|
||||
"""
|
||||
设置角色颜色
|
||||
@param new_body_color: 身体颜色
|
||||
@param new_head_color: 头部颜色
|
||||
"""
|
||||
body_color = new_body_color
|
||||
head_color = new_head_color
|
||||
|
||||
if body:
|
||||
body.color = body_color
|
||||
if head:
|
||||
head.color = head_color
|
||||
|
||||
## 播放行走动画
|
||||
func play_walk_animation(direction: Vector2):
|
||||
"""
|
||||
播放行走动画(简单的摇摆效果)
|
||||
@param direction: 移动方向
|
||||
"""
|
||||
if direction.length() > 0:
|
||||
# 简单的左右摇摆
|
||||
var tween = create_tween()
|
||||
tween.set_loops()
|
||||
tween.tween_property(self, "rotation", 0.1, 0.3)
|
||||
tween.tween_property(self, "rotation", -0.1, 0.3)
|
||||
else:
|
||||
# 停止动画
|
||||
rotation = 0
|
||||
|
||||
## 播放空闲动画
|
||||
func play_idle_animation():
|
||||
"""播放空闲动画(轻微上下浮动)"""
|
||||
var tween = create_tween()
|
||||
tween.set_loops()
|
||||
tween.tween_property(self, "position:y", -2, 1.0)
|
||||
tween.tween_property(self, "position:y", 0, 1.0)
|
||||
|
||||
## 生成随机角色颜色
|
||||
static func generate_random_color() -> Color:
|
||||
"""生成随机但好看的角色颜色"""
|
||||
var hue = randf()
|
||||
var saturation = randf_range(0.4, 0.8)
|
||||
var value = randf_range(0.6, 0.9)
|
||||
return Color.from_hsv(hue, saturation, value)
|
||||
1
scripts/CharacterSprite.gd.uid
Normal file
1
scripts/CharacterSprite.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://djfkqn1n4ulio
|
||||
288
scripts/CharacterStatusManager.gd
Normal file
288
scripts/CharacterStatusManager.gd
Normal file
@@ -0,0 +1,288 @@
|
||||
extends Node
|
||||
class_name CharacterStatusManager
|
||||
## 角色状态和心情管理器
|
||||
## 处理角色的状态变化、心情系统和相关UI
|
||||
|
||||
# 状态持续时间(秒)
|
||||
const STATUS_DURATIONS = {
|
||||
"busy": 300, # 5分钟
|
||||
"away": 900, # 15分钟
|
||||
}
|
||||
|
||||
# 心情自动变化的触发条件
|
||||
const MOOD_TRIGGERS = {
|
||||
"achievement_earned": "happy",
|
||||
"level_up": "excited",
|
||||
"long_idle": "tired",
|
||||
"social_interaction": "happy",
|
||||
"problem_solved": "excited"
|
||||
}
|
||||
|
||||
# 心情持续时间(秒)
|
||||
const MOOD_DURATION = 600 # 10分钟
|
||||
|
||||
# 当前管理的角色数据
|
||||
var character_data: Dictionary = {}
|
||||
var status_timer: Timer
|
||||
var mood_timer: Timer
|
||||
|
||||
# 信号
|
||||
signal status_changed(old_status: String, new_status: String)
|
||||
signal mood_changed(old_mood: String, new_mood: String)
|
||||
signal status_expired(status: String)
|
||||
|
||||
func _ready():
|
||||
"""初始化状态管理器"""
|
||||
_setup_timers()
|
||||
|
||||
## 设置定时器
|
||||
func _setup_timers():
|
||||
"""设置状态和心情定时器"""
|
||||
# 状态定时器
|
||||
status_timer = Timer.new()
|
||||
status_timer.one_shot = true
|
||||
status_timer.timeout.connect(_on_status_timer_timeout)
|
||||
add_child(status_timer)
|
||||
|
||||
# 心情定时器
|
||||
mood_timer = Timer.new()
|
||||
mood_timer.one_shot = true
|
||||
mood_timer.timeout.connect(_on_mood_timer_timeout)
|
||||
add_child(mood_timer)
|
||||
|
||||
## 设置角色数据
|
||||
func set_character_data(data: Dictionary):
|
||||
"""
|
||||
设置要管理的角色数据
|
||||
@param data: 角色数据字典
|
||||
"""
|
||||
character_data = data
|
||||
|
||||
# 检查当前状态是否需要定时器
|
||||
var current_status = data.get(CharacterData.FIELD_STATUS, "active")
|
||||
if current_status in STATUS_DURATIONS:
|
||||
var duration = STATUS_DURATIONS[current_status]
|
||||
status_timer.start(duration)
|
||||
|
||||
## 设置角色状态
|
||||
func set_status(new_status: String, auto_revert: bool = true):
|
||||
"""
|
||||
设置角色状态
|
||||
@param new_status: 新状态
|
||||
@param auto_revert: 是否自动恢复到active状态
|
||||
"""
|
||||
if character_data.is_empty():
|
||||
push_error("No character data set")
|
||||
return
|
||||
|
||||
var old_status = character_data.get(CharacterData.FIELD_STATUS, "active")
|
||||
|
||||
if old_status == new_status:
|
||||
return
|
||||
|
||||
# 更新状态
|
||||
CharacterData.set_status(character_data, new_status)
|
||||
status_changed.emit(old_status, new_status)
|
||||
|
||||
# 停止现有定时器
|
||||
status_timer.stop()
|
||||
|
||||
# 如果需要自动恢复且状态有持续时间
|
||||
if auto_revert and new_status in STATUS_DURATIONS:
|
||||
var duration = STATUS_DURATIONS[new_status]
|
||||
status_timer.start(duration)
|
||||
|
||||
print("Character status changed from %s to %s" % [old_status, new_status])
|
||||
|
||||
## 设置角色心情
|
||||
func set_mood(new_mood: String, duration: float = MOOD_DURATION):
|
||||
"""
|
||||
设置角色心情
|
||||
@param new_mood: 新心情
|
||||
@param duration: 心情持续时间(秒)
|
||||
"""
|
||||
if character_data.is_empty():
|
||||
push_error("No character data set")
|
||||
return
|
||||
|
||||
var old_mood = character_data.get(CharacterData.FIELD_MOOD, "neutral")
|
||||
|
||||
if old_mood == new_mood:
|
||||
return
|
||||
|
||||
# 更新心情
|
||||
CharacterData.set_mood(character_data, new_mood)
|
||||
mood_changed.emit(old_mood, new_mood)
|
||||
|
||||
# 设置心情恢复定时器
|
||||
mood_timer.stop()
|
||||
if new_mood != "neutral" and duration > 0:
|
||||
mood_timer.start(duration)
|
||||
|
||||
print("Character mood changed from %s to %s" % [old_mood, new_mood])
|
||||
|
||||
## 触发心情变化
|
||||
func trigger_mood_change(trigger: String):
|
||||
"""
|
||||
根据触发条件改变心情
|
||||
@param trigger: 触发条件
|
||||
"""
|
||||
if trigger in MOOD_TRIGGERS:
|
||||
var new_mood = MOOD_TRIGGERS[trigger]
|
||||
set_mood(new_mood)
|
||||
|
||||
## 获取当前状态
|
||||
func get_current_status() -> String:
|
||||
"""获取当前状态"""
|
||||
return character_data.get(CharacterData.FIELD_STATUS, "active")
|
||||
|
||||
## 获取当前心情
|
||||
func get_current_mood() -> String:
|
||||
"""获取当前心情"""
|
||||
return character_data.get(CharacterData.FIELD_MOOD, "neutral")
|
||||
|
||||
## 获取状态显示文本
|
||||
func get_status_display_text() -> String:
|
||||
"""获取状态的显示文本"""
|
||||
var status = get_current_status()
|
||||
var mood = get_current_mood()
|
||||
|
||||
var status_text = ""
|
||||
match status:
|
||||
"active":
|
||||
status_text = "在线"
|
||||
"busy":
|
||||
status_text = "忙碌"
|
||||
"away":
|
||||
status_text = "离开"
|
||||
"offline":
|
||||
status_text = "离线"
|
||||
_:
|
||||
status_text = status
|
||||
|
||||
var personalization = preload("res://scripts/CharacterPersonalization.gd")
|
||||
var mood_emoji = personalization.get_mood_emoji(mood)
|
||||
return "%s %s" % [status_text, mood_emoji]
|
||||
|
||||
## 获取状态颜色
|
||||
func get_status_color() -> Color:
|
||||
"""获取状态对应的颜色"""
|
||||
var status = get_current_status()
|
||||
var personalization = preload("res://scripts/CharacterPersonalization.gd")
|
||||
return personalization.get_status_color(status)
|
||||
|
||||
## 状态定时器超时
|
||||
func _on_status_timer_timeout():
|
||||
"""状态定时器超时,恢复到active状态"""
|
||||
var current_status = get_current_status()
|
||||
set_status("active", false)
|
||||
status_expired.emit(current_status)
|
||||
|
||||
## 心情定时器超时
|
||||
func _on_mood_timer_timeout():
|
||||
"""心情定时器超时,恢复到neutral心情"""
|
||||
set_mood("neutral", 0)
|
||||
|
||||
## 处理活动事件
|
||||
func handle_activity_event(event_type: String, _data: Dictionary = {}):
|
||||
"""
|
||||
处理角色活动事件,可能触发状态或心情变化
|
||||
@param event_type: 事件类型
|
||||
@param _data: 事件数据(暂未使用)
|
||||
"""
|
||||
match event_type:
|
||||
"dialogue_started":
|
||||
# 开始对话时设为忙碌
|
||||
set_status("busy")
|
||||
trigger_mood_change("social_interaction")
|
||||
|
||||
"dialogue_ended":
|
||||
# 对话结束后恢复活跃
|
||||
set_status("active")
|
||||
|
||||
"achievement_earned":
|
||||
trigger_mood_change("achievement_earned")
|
||||
|
||||
"level_up":
|
||||
trigger_mood_change("level_up")
|
||||
|
||||
"idle_too_long":
|
||||
trigger_mood_change("long_idle")
|
||||
|
||||
"problem_solved":
|
||||
trigger_mood_change("problem_solved")
|
||||
|
||||
"manual_away":
|
||||
# 手动设置离开状态
|
||||
set_status("away")
|
||||
|
||||
"manual_busy":
|
||||
# 手动设置忙碌状态
|
||||
set_status("busy")
|
||||
|
||||
## 创建状态选择菜单
|
||||
func create_status_menu() -> PopupMenu:
|
||||
"""
|
||||
创建状态选择菜单
|
||||
@return: PopupMenu节点
|
||||
"""
|
||||
var menu = PopupMenu.new()
|
||||
|
||||
menu.add_item("🟢 在线", 0)
|
||||
menu.add_item("🟡 忙碌", 1)
|
||||
menu.add_item("🟠 离开", 2)
|
||||
|
||||
menu.id_pressed.connect(_on_status_menu_selected)
|
||||
|
||||
return menu
|
||||
|
||||
## 状态菜单选择回调
|
||||
func _on_status_menu_selected(id: int):
|
||||
"""状态菜单选择回调"""
|
||||
match id:
|
||||
0:
|
||||
set_status("active", false)
|
||||
1:
|
||||
handle_activity_event("manual_busy")
|
||||
2:
|
||||
handle_activity_event("manual_away")
|
||||
|
||||
## 创建心情选择菜单
|
||||
func create_mood_menu() -> PopupMenu:
|
||||
"""
|
||||
创建心情选择菜单
|
||||
@return: PopupMenu节点
|
||||
"""
|
||||
var menu = PopupMenu.new()
|
||||
|
||||
menu.add_item("😊 开心", 0)
|
||||
menu.add_item("😢 难过", 1)
|
||||
menu.add_item("🤩 兴奋", 2)
|
||||
menu.add_item("😴 疲惫", 3)
|
||||
menu.add_item("😐 平静", 4)
|
||||
|
||||
menu.id_pressed.connect(_on_mood_menu_selected)
|
||||
|
||||
return menu
|
||||
|
||||
## 心情菜单选择回调
|
||||
func _on_mood_menu_selected(id: int):
|
||||
"""心情菜单选择回调"""
|
||||
var moods = ["happy", "sad", "excited", "tired", "neutral"]
|
||||
if id >= 0 and id < moods.size():
|
||||
set_mood(moods[id])
|
||||
|
||||
## 获取状态统计信息
|
||||
func get_status_stats() -> Dictionary:
|
||||
"""
|
||||
获取状态统计信息
|
||||
@return: 统计信息字典
|
||||
"""
|
||||
return {
|
||||
"current_status": get_current_status(),
|
||||
"current_mood": get_current_mood(),
|
||||
"status_display": get_status_display_text(),
|
||||
"status_color": get_status_color(),
|
||||
"has_active_timer": not status_timer.is_stopped(),
|
||||
"timer_remaining": status_timer.time_left if not status_timer.is_stopped() else 0.0
|
||||
}
|
||||
1
scripts/CharacterStatusManager.gd.uid
Normal file
1
scripts/CharacterStatusManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dtgrgcn1c5mvx
|
||||
109
scripts/ChatBubble.gd
Normal file
109
scripts/ChatBubble.gd
Normal file
@@ -0,0 +1,109 @@
|
||||
extends Control
|
||||
class_name ChatBubble
|
||||
## 对话气泡
|
||||
## 在角色上方显示短暂的对话消息
|
||||
|
||||
# UI 元素
|
||||
var background: Panel
|
||||
var label: Label
|
||||
var timer: Timer
|
||||
|
||||
# 配置
|
||||
var bubble_padding: Vector2 = Vector2(10, 5)
|
||||
var max_width: float = 200.0
|
||||
var default_duration: float = 3.0
|
||||
|
||||
func _ready():
|
||||
"""初始化对话气泡"""
|
||||
_create_ui()
|
||||
|
||||
## 创建 UI 元素
|
||||
func _create_ui():
|
||||
"""创建气泡的所有 UI 元素"""
|
||||
# 设置为不阻挡输入
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
# 创建背景面板
|
||||
background = Panel.new()
|
||||
background.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(background)
|
||||
|
||||
# 创建文本标签
|
||||
label = Label.new()
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.add_theme_font_size_override("font_size", 14)
|
||||
label.add_theme_color_override("font_color", Color(0, 0, 0))
|
||||
background.add_child(label)
|
||||
|
||||
# 创建计时器
|
||||
timer = Timer.new()
|
||||
timer.one_shot = true
|
||||
timer.timeout.connect(_on_timer_timeout)
|
||||
add_child(timer)
|
||||
|
||||
## 显示气泡
|
||||
func show_bubble(message: String, duration: float = 0.0) -> void:
|
||||
"""
|
||||
显示对话气泡
|
||||
@param message: 消息内容
|
||||
@param duration: 显示时长(秒),0 表示使用默认时长
|
||||
"""
|
||||
if duration <= 0:
|
||||
duration = default_duration
|
||||
|
||||
# 设置文本
|
||||
label.text = message
|
||||
|
||||
# 计算大小
|
||||
var text_size = label.get_combined_minimum_size()
|
||||
text_size.x = min(text_size.x, max_width)
|
||||
|
||||
# 设置标签大小
|
||||
label.custom_minimum_size = text_size
|
||||
label.size = text_size
|
||||
|
||||
# 设置背景大小
|
||||
var panel_size = text_size + bubble_padding * 2
|
||||
background.custom_minimum_size = panel_size
|
||||
background.size = panel_size
|
||||
|
||||
# 设置标签位置(居中)
|
||||
label.position = bubble_padding
|
||||
|
||||
# 设置整体大小
|
||||
custom_minimum_size = panel_size
|
||||
size = panel_size
|
||||
|
||||
# 显示气泡
|
||||
show()
|
||||
modulate = Color(1, 1, 1, 1)
|
||||
|
||||
# 启动计时器
|
||||
timer.start(duration)
|
||||
|
||||
# 淡入动画
|
||||
var tween = create_tween()
|
||||
tween.tween_property(self, "modulate:a", 1.0, 0.2)
|
||||
|
||||
## 隐藏气泡
|
||||
func hide_bubble() -> void:
|
||||
"""隐藏对话气泡(带淡出动画)"""
|
||||
var tween = create_tween()
|
||||
tween.tween_property(self, "modulate:a", 0.0, 0.3)
|
||||
tween.finished.connect(func(): hide())
|
||||
|
||||
## 计时器超时
|
||||
func _on_timer_timeout():
|
||||
"""计时器超时,隐藏气泡"""
|
||||
hide_bubble()
|
||||
|
||||
## 设置气泡位置(相对于角色)
|
||||
func set_bubble_position(character_position: Vector2, offset: Vector2 = Vector2(0, -40)):
|
||||
"""
|
||||
设置气泡位置
|
||||
@param character_position: 角色位置
|
||||
@param offset: 偏移量(默认在角色上方)
|
||||
"""
|
||||
global_position = character_position + offset - size / 2
|
||||
1
scripts/ChatBubble.gd.uid
Normal file
1
scripts/ChatBubble.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dtl86f6ro3s6l
|
||||
864
scripts/CommunityEventSystem.gd
Normal file
864
scripts/CommunityEventSystem.gd
Normal file
@@ -0,0 +1,864 @@
|
||||
extends Node
|
||||
class_name CommunityEventSystem
|
||||
## 社区活动和事件系统
|
||||
## 管理社区活动、事件和集体互动
|
||||
|
||||
# 事件类型枚举
|
||||
enum EventType {
|
||||
SOCIAL_GATHERING, # 社交聚会
|
||||
LEARNING_SESSION, # 学习会议
|
||||
COMPETITION, # 竞赛活动
|
||||
CELEBRATION, # 庆祝活动
|
||||
WORKSHOP, # 工作坊
|
||||
DISCUSSION, # 讨论会
|
||||
GAME_ACTIVITY, # 游戏活动
|
||||
COMMUNITY_PROJECT, # 社区项目
|
||||
ANNOUNCEMENT, # 公告
|
||||
MILESTONE # 里程碑事件
|
||||
}
|
||||
|
||||
# 事件状态枚举
|
||||
enum EventStatus {
|
||||
PLANNED, # 计划中
|
||||
ACTIVE, # 进行中
|
||||
COMPLETED, # 已完成
|
||||
CANCELLED # 已取消
|
||||
}
|
||||
|
||||
# 事件数据结构
|
||||
class CommunityEvent:
|
||||
var event_id: String
|
||||
var title: String
|
||||
var description: String
|
||||
var event_type: EventType
|
||||
var status: EventStatus
|
||||
var organizer_id: String
|
||||
var participants: Array[String] = []
|
||||
var max_participants: int = 0 # 0表示无限制
|
||||
var start_time: float
|
||||
var end_time: float
|
||||
var location: String = "" # 游戏内位置
|
||||
var rewards: Dictionary = {}
|
||||
var requirements: Dictionary = {}
|
||||
var created_at: float
|
||||
var updated_at: float
|
||||
var tags: Array[String] = []
|
||||
var metadata: Dictionary = {}
|
||||
|
||||
func _init(id: String, event_title: String, type: EventType, organizer: String):
|
||||
event_id = id
|
||||
title = event_title
|
||||
event_type = type
|
||||
organizer_id = organizer
|
||||
status = EventStatus.PLANNED
|
||||
created_at = Time.get_unix_time_from_system()
|
||||
updated_at = created_at
|
||||
start_time = created_at + 3600 # 默认1小时后开始
|
||||
end_time = start_time + 3600 # 默认持续1小时
|
||||
|
||||
# 活动数据存储
|
||||
var events: Dictionary = {} # event_id -> CommunityEvent
|
||||
var active_events: Array[String] = []
|
||||
var user_events: Dictionary = {} # user_id -> Array[event_id]
|
||||
var event_history: Array[Dictionary] = []
|
||||
|
||||
# 系统配置
|
||||
var max_events_per_user: int = 10
|
||||
var max_active_events: int = 20
|
||||
var auto_cleanup_days: int = 30
|
||||
|
||||
# 关系网络引用
|
||||
var relationship_network: RelationshipNetwork
|
||||
|
||||
# 数据持久化
|
||||
var events_file_path: String = "user://community_events.json"
|
||||
|
||||
# 信号
|
||||
signal event_created(event_id: String, title: String, organizer_id: String)
|
||||
signal event_started(event_id: String, title: String)
|
||||
signal event_completed(event_id: String, title: String, participants: Array[String])
|
||||
signal event_cancelled(event_id: String, title: String, reason: String)
|
||||
signal participant_joined(event_id: String, participant_id: String)
|
||||
signal participant_left(event_id: String, participant_id: String)
|
||||
signal event_reminder(event_id: String, title: String, minutes_until_start: int)
|
||||
signal milestone_achieved(milestone_type: String, data: Dictionary)
|
||||
|
||||
func _ready():
|
||||
"""初始化社区事件系统"""
|
||||
load_events_data()
|
||||
|
||||
# 启动定时器检查事件状态
|
||||
var timer = Timer.new()
|
||||
timer.wait_time = 60.0 # 每分钟检查一次
|
||||
timer.timeout.connect(_check_event_status)
|
||||
timer.autostart = true
|
||||
add_child(timer)
|
||||
|
||||
print("CommunityEventSystem initialized")
|
||||
|
||||
## 设置关系网络引用
|
||||
func set_relationship_network(rn: RelationshipNetwork) -> void:
|
||||
"""
|
||||
设置关系网络引用
|
||||
@param rn: 关系网络实例
|
||||
"""
|
||||
relationship_network = rn
|
||||
|
||||
## 创建事件
|
||||
func create_event(title: String, description: String, event_type: EventType, organizer_id: String, start_time: float = 0.0, duration: float = 3600.0) -> String:
|
||||
"""
|
||||
创建新的社区事件
|
||||
@param title: 事件标题
|
||||
@param description: 事件描述
|
||||
@param event_type: 事件类型
|
||||
@param organizer_id: 组织者ID
|
||||
@param start_time: 开始时间(Unix时间戳,0表示使用默认时间)
|
||||
@param duration: 持续时间(秒)
|
||||
@return: 事件ID,失败返回空字符串
|
||||
"""
|
||||
# 验证输入
|
||||
if title.strip_edges().is_empty():
|
||||
print("Event title cannot be empty")
|
||||
return ""
|
||||
|
||||
if title.length() > 100:
|
||||
print("Event title too long")
|
||||
return ""
|
||||
|
||||
# 检查用户事件数量限制
|
||||
var user_event_count = user_events.get(organizer_id, []).size()
|
||||
if user_event_count >= max_events_per_user:
|
||||
print("User has reached maximum events limit")
|
||||
return ""
|
||||
|
||||
# 检查活跃事件数量限制
|
||||
if active_events.size() >= max_active_events:
|
||||
print("Maximum active events limit reached")
|
||||
return ""
|
||||
|
||||
# 生成事件ID
|
||||
var event_id = generate_event_id()
|
||||
|
||||
# 创建事件
|
||||
var event = CommunityEvent.new(event_id, title.strip_edges(), event_type, organizer_id)
|
||||
event.description = description.strip_edges()
|
||||
|
||||
if start_time > 0:
|
||||
event.start_time = start_time
|
||||
event.end_time = start_time + duration
|
||||
|
||||
# 组织者自动参与
|
||||
event.participants.append(organizer_id)
|
||||
|
||||
# 存储事件
|
||||
events[event_id] = event
|
||||
active_events.append(event_id)
|
||||
|
||||
# 更新用户事件索引
|
||||
_add_event_to_user_index(organizer_id, event_id)
|
||||
|
||||
# 保存数据
|
||||
save_events_data()
|
||||
|
||||
# 发射信号
|
||||
event_created.emit(event_id, title, organizer_id)
|
||||
|
||||
print("Event created: ", title, " (", event_id, ") by ", organizer_id)
|
||||
return event_id
|
||||
|
||||
## 加入事件
|
||||
func join_event(event_id: String, participant_id: String) -> bool:
|
||||
"""
|
||||
加入事件
|
||||
@param event_id: 事件ID
|
||||
@param participant_id: 参与者ID
|
||||
@return: 是否成功加入
|
||||
"""
|
||||
if not events.has(event_id):
|
||||
print("Event not found: ", event_id)
|
||||
return false
|
||||
|
||||
var event = events[event_id]
|
||||
|
||||
# 检查事件状态
|
||||
if event.status != EventStatus.PLANNED and event.status != EventStatus.ACTIVE:
|
||||
print("Cannot join event: event is not active")
|
||||
return false
|
||||
|
||||
# 检查是否已经参与
|
||||
if participant_id in event.participants:
|
||||
print("Already participating in event: ", event_id)
|
||||
return true
|
||||
|
||||
# 检查参与者数量限制
|
||||
if event.max_participants > 0 and event.participants.size() >= event.max_participants:
|
||||
print("Event is full")
|
||||
return false
|
||||
|
||||
# 检查参与要求
|
||||
if not _check_event_requirements(event, participant_id):
|
||||
print("Participant does not meet event requirements")
|
||||
return false
|
||||
|
||||
# 加入事件
|
||||
event.participants.append(participant_id)
|
||||
event.updated_at = Time.get_unix_time_from_system()
|
||||
|
||||
# 更新用户事件索引
|
||||
_add_event_to_user_index(participant_id, event_id)
|
||||
|
||||
# 记录关系网络互动
|
||||
if relationship_network:
|
||||
for other_participant in event.participants:
|
||||
if other_participant != participant_id:
|
||||
relationship_network.record_interaction(participant_id, other_participant, "community_event", {"event_id": event_id})
|
||||
|
||||
# 保存数据
|
||||
save_events_data()
|
||||
|
||||
# 发射信号
|
||||
participant_joined.emit(event_id, participant_id)
|
||||
|
||||
print("Participant ", participant_id, " joined event ", event_id)
|
||||
return true
|
||||
|
||||
## 离开事件
|
||||
func leave_event(event_id: String, participant_id: String) -> bool:
|
||||
"""
|
||||
离开事件
|
||||
@param event_id: 事件ID
|
||||
@param participant_id: 参与者ID
|
||||
@return: 是否成功离开
|
||||
"""
|
||||
if not events.has(event_id):
|
||||
print("Event not found: ", event_id)
|
||||
return false
|
||||
|
||||
var event = events[event_id]
|
||||
|
||||
# 检查是否参与
|
||||
if not participant_id in event.participants:
|
||||
print("Not participating in event: ", event_id)
|
||||
return false
|
||||
|
||||
# 组织者不能离开自己的事件
|
||||
if participant_id == event.organizer_id:
|
||||
print("Organizer cannot leave their own event")
|
||||
return false
|
||||
|
||||
# 离开事件
|
||||
event.participants.erase(participant_id)
|
||||
event.updated_at = Time.get_unix_time_from_system()
|
||||
|
||||
# 更新用户事件索引
|
||||
_remove_event_from_user_index(participant_id, event_id)
|
||||
|
||||
# 保存数据
|
||||
save_events_data()
|
||||
|
||||
# 发射信号
|
||||
participant_left.emit(event_id, participant_id)
|
||||
|
||||
print("Participant ", participant_id, " left event ", event_id)
|
||||
return true
|
||||
|
||||
## 开始事件
|
||||
func start_event(event_id: String) -> bool:
|
||||
"""
|
||||
开始事件
|
||||
@param event_id: 事件ID
|
||||
@return: 是否成功开始
|
||||
"""
|
||||
if not events.has(event_id):
|
||||
print("Event not found: ", event_id)
|
||||
return false
|
||||
|
||||
var event = events[event_id]
|
||||
|
||||
if event.status != EventStatus.PLANNED:
|
||||
print("Event cannot be started: not in planned status")
|
||||
return false
|
||||
|
||||
# 更新状态
|
||||
event.status = EventStatus.ACTIVE
|
||||
event.start_time = Time.get_unix_time_from_system()
|
||||
event.updated_at = event.start_time
|
||||
|
||||
# 保存数据
|
||||
save_events_data()
|
||||
|
||||
# 发射信号
|
||||
event_started.emit(event_id, event.title)
|
||||
|
||||
print("Event started: ", event.title, " (", event_id, ")")
|
||||
return true
|
||||
|
||||
## 完成事件
|
||||
func complete_event(event_id: String, results: Dictionary = {}) -> bool:
|
||||
"""
|
||||
完成事件
|
||||
@param event_id: 事件ID
|
||||
@param results: 事件结果数据
|
||||
@return: 是否成功完成
|
||||
"""
|
||||
if not events.has(event_id):
|
||||
print("Event not found: ", event_id)
|
||||
return false
|
||||
|
||||
var event = events[event_id]
|
||||
|
||||
if event.status != EventStatus.ACTIVE:
|
||||
print("Event cannot be completed: not active")
|
||||
return false
|
||||
|
||||
# 更新状态
|
||||
event.status = EventStatus.COMPLETED
|
||||
event.end_time = Time.get_unix_time_from_system()
|
||||
event.updated_at = event.end_time
|
||||
event.metadata["results"] = results
|
||||
|
||||
# 从活跃事件列表移除
|
||||
active_events.erase(event_id)
|
||||
|
||||
# 添加到历史记录
|
||||
_add_to_event_history(event)
|
||||
|
||||
# 分发奖励
|
||||
_distribute_event_rewards(event)
|
||||
|
||||
# 记录关系网络互动
|
||||
if relationship_network:
|
||||
_record_event_relationships(event)
|
||||
|
||||
# 检查里程碑
|
||||
_check_milestones(event)
|
||||
|
||||
# 保存数据
|
||||
save_events_data()
|
||||
|
||||
# 发射信号
|
||||
event_completed.emit(event_id, event.title, event.participants)
|
||||
|
||||
print("Event completed: ", event.title, " (", event_id, ") with ", event.participants.size(), " participants")
|
||||
return true
|
||||
|
||||
## 取消事件
|
||||
func cancel_event(event_id: String, reason: String = "") -> bool:
|
||||
"""
|
||||
取消事件
|
||||
@param event_id: 事件ID
|
||||
@param reason: 取消原因
|
||||
@return: 是否成功取消
|
||||
"""
|
||||
if not events.has(event_id):
|
||||
print("Event not found: ", event_id)
|
||||
return false
|
||||
|
||||
var event = events[event_id]
|
||||
|
||||
if event.status == EventStatus.COMPLETED or event.status == EventStatus.CANCELLED:
|
||||
print("Event cannot be cancelled: already completed or cancelled")
|
||||
return false
|
||||
|
||||
# 更新状态
|
||||
event.status = EventStatus.CANCELLED
|
||||
event.updated_at = Time.get_unix_time_from_system()
|
||||
event.metadata["cancel_reason"] = reason
|
||||
|
||||
# 从活跃事件列表移除
|
||||
active_events.erase(event_id)
|
||||
|
||||
# 保存数据
|
||||
save_events_data()
|
||||
|
||||
# 发射信号
|
||||
event_cancelled.emit(event_id, event.title, reason)
|
||||
|
||||
print("Event cancelled: ", event.title, " (", event_id, ") - ", reason)
|
||||
return true
|
||||
|
||||
## 获取事件列表
|
||||
func get_events_list(filter_type: EventType = EventType.SOCIAL_GATHERING, filter_status: EventStatus = EventStatus.PLANNED, include_all_types: bool = true, include_all_statuses: bool = true) -> Array[Dictionary]:
|
||||
"""
|
||||
获取事件列表
|
||||
@param filter_type: 过滤事件类型
|
||||
@param filter_status: 过滤事件状态
|
||||
@param include_all_types: 是否包含所有类型
|
||||
@param include_all_statuses: 是否包含所有状态
|
||||
@return: 事件信息数组
|
||||
"""
|
||||
var events_list = []
|
||||
|
||||
for event_id in events:
|
||||
var event = events[event_id]
|
||||
|
||||
# 应用过滤器
|
||||
if not include_all_types and event.event_type != filter_type:
|
||||
continue
|
||||
|
||||
if not include_all_statuses and event.status != filter_status:
|
||||
continue
|
||||
|
||||
events_list.append(_get_event_info(event))
|
||||
|
||||
# 按开始时间排序
|
||||
events_list.sort_custom(func(a, b): return a.start_time < b.start_time)
|
||||
|
||||
return events_list
|
||||
|
||||
## 获取用户事件
|
||||
func get_user_events(user_id: String) -> Array[Dictionary]:
|
||||
"""
|
||||
获取用户参与的事件
|
||||
@param user_id: 用户ID
|
||||
@return: 事件信息数组
|
||||
"""
|
||||
var user_event_ids = user_events.get(user_id, [])
|
||||
var user_events_list = []
|
||||
|
||||
for event_id in user_event_ids:
|
||||
if events.has(event_id):
|
||||
user_events_list.append(_get_event_info(events[event_id]))
|
||||
|
||||
# 按开始时间排序
|
||||
user_events_list.sort_custom(func(a, b): return a.start_time < b.start_time)
|
||||
|
||||
return user_events_list
|
||||
|
||||
## 搜索事件
|
||||
func search_events(query: String, event_type: EventType = EventType.SOCIAL_GATHERING, include_all_types: bool = true) -> Array[Dictionary]:
|
||||
"""
|
||||
搜索事件
|
||||
@param query: 搜索关键词
|
||||
@param event_type: 事件类型过滤
|
||||
@param include_all_types: 是否包含所有类型
|
||||
@return: 匹配的事件信息数组
|
||||
"""
|
||||
var results = []
|
||||
var search_query = query.to_lower()
|
||||
|
||||
for event_id in events:
|
||||
var event = events[event_id]
|
||||
|
||||
# 类型过滤
|
||||
if not include_all_types and event.event_type != event_type:
|
||||
continue
|
||||
|
||||
# 搜索标题和描述
|
||||
if event.title.to_lower().contains(search_query) or event.description.to_lower().contains(search_query):
|
||||
results.append(_get_event_info(event))
|
||||
# 搜索标签
|
||||
else:
|
||||
for tag in event.tags:
|
||||
if tag.to_lower().contains(search_query):
|
||||
results.append(_get_event_info(event))
|
||||
break
|
||||
|
||||
return results
|
||||
|
||||
## 获取推荐事件
|
||||
func get_recommended_events(user_id: String, limit: int = 5) -> Array[Dictionary]:
|
||||
"""
|
||||
获取推荐事件
|
||||
@param user_id: 用户ID
|
||||
@param limit: 限制返回数量
|
||||
@return: 推荐事件信息数组
|
||||
"""
|
||||
var recommendations = []
|
||||
var user_interests = _get_user_interests(user_id)
|
||||
|
||||
for event_id in events:
|
||||
var event = events[event_id]
|
||||
|
||||
# 只推荐计划中的事件
|
||||
if event.status != EventStatus.PLANNED:
|
||||
continue
|
||||
|
||||
# 不推荐已参与的事件
|
||||
if user_id in event.participants:
|
||||
continue
|
||||
|
||||
var score = _calculate_recommendation_score(event, user_id, user_interests)
|
||||
recommendations.append({
|
||||
"event": _get_event_info(event),
|
||||
"score": score
|
||||
})
|
||||
|
||||
# 按推荐分数排序
|
||||
recommendations.sort_custom(func(a, b): return a.score > b.score)
|
||||
|
||||
# 提取事件信息
|
||||
var recommended_events = []
|
||||
for i in range(min(limit, recommendations.size())):
|
||||
recommended_events.append(recommendations[i].event)
|
||||
|
||||
return recommended_events
|
||||
|
||||
## 检查事件状态
|
||||
func _check_event_status() -> void:
|
||||
"""定期检查事件状态并处理自动转换"""
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
|
||||
for event_id in active_events.duplicate(): # 使用副本避免修改时的问题
|
||||
if not events.has(event_id):
|
||||
active_events.erase(event_id)
|
||||
continue
|
||||
|
||||
var event = events[event_id]
|
||||
|
||||
# 检查是否应该开始
|
||||
if event.status == EventStatus.PLANNED and current_time >= event.start_time:
|
||||
start_event(event_id)
|
||||
|
||||
# 检查是否应该结束
|
||||
elif event.status == EventStatus.ACTIVE and current_time >= event.end_time:
|
||||
complete_event(event_id)
|
||||
|
||||
# 发送提醒
|
||||
elif event.status == EventStatus.PLANNED:
|
||||
var minutes_until_start = (event.start_time - current_time) / 60.0
|
||||
if minutes_until_start <= 15 and minutes_until_start > 14: # 15分钟提醒
|
||||
event_reminder.emit(event_id, event.title, 15)
|
||||
elif minutes_until_start <= 5 and minutes_until_start > 4: # 5分钟提醒
|
||||
event_reminder.emit(event_id, event.title, 5)
|
||||
|
||||
## 检查事件要求
|
||||
func _check_event_requirements(_event: CommunityEvent, _participant_id: String) -> bool:
|
||||
"""
|
||||
检查参与者是否满足事件要求
|
||||
@param _event: 事件对象 (暂未使用)
|
||||
@param _participant_id: 参与者ID (暂未使用)
|
||||
@return: 是否满足要求
|
||||
"""
|
||||
# 这里可以添加各种要求检查,比如等级、成就、关系等
|
||||
# 目前返回true,表示没有特殊要求
|
||||
return true
|
||||
|
||||
## 分发事件奖励
|
||||
func _distribute_event_rewards(event: CommunityEvent) -> void:
|
||||
"""
|
||||
分发事件奖励
|
||||
@param event: 事件对象
|
||||
"""
|
||||
if event.rewards.is_empty():
|
||||
return
|
||||
|
||||
for participant_id in event.participants:
|
||||
# 这里可以实现具体的奖励分发逻辑
|
||||
# 比如经验值、成就、物品等
|
||||
print("Distributing rewards to ", participant_id, " for event ", event.title)
|
||||
|
||||
## 记录事件关系
|
||||
func _record_event_relationships(event: CommunityEvent) -> void:
|
||||
"""
|
||||
记录事件中的关系互动
|
||||
@param event: 事件对象
|
||||
"""
|
||||
if not relationship_network:
|
||||
return
|
||||
|
||||
# 为所有参与者之间记录互动
|
||||
for i in range(event.participants.size()):
|
||||
for j in range(i + 1, event.participants.size()):
|
||||
var participant1 = event.participants[i]
|
||||
var participant2 = event.participants[j]
|
||||
|
||||
var interaction_data = {
|
||||
"event_id": event.event_id,
|
||||
"event_type": EventType.keys()[event.event_type],
|
||||
"event_title": event.title
|
||||
}
|
||||
|
||||
relationship_network.record_interaction(participant1, participant2, "community_event", interaction_data)
|
||||
relationship_network.record_interaction(participant2, participant1, "community_event", interaction_data)
|
||||
|
||||
## 检查里程碑
|
||||
func _check_milestones(event: CommunityEvent) -> void:
|
||||
"""
|
||||
检查并触发里程碑事件
|
||||
@param event: 完成的事件对象
|
||||
"""
|
||||
# 检查各种里程碑条件
|
||||
var total_events = event_history.size()
|
||||
|
||||
# 事件数量里程碑
|
||||
if total_events == 10:
|
||||
milestone_achieved.emit("events_10", {"count": total_events})
|
||||
elif total_events == 50:
|
||||
milestone_achieved.emit("events_50", {"count": total_events})
|
||||
elif total_events == 100:
|
||||
milestone_achieved.emit("events_100", {"count": total_events})
|
||||
|
||||
# 参与者数量里程碑
|
||||
if event.participants.size() >= 20:
|
||||
milestone_achieved.emit("large_event", {"participants": event.participants.size(), "event_id": event.event_id})
|
||||
|
||||
## 获取用户兴趣
|
||||
func _get_user_interests(user_id: String) -> Dictionary:
|
||||
"""
|
||||
获取用户兴趣(基于历史参与)
|
||||
@param user_id: 用户ID
|
||||
@return: 兴趣数据字典
|
||||
"""
|
||||
var interests = {}
|
||||
var user_event_ids = user_events.get(user_id, [])
|
||||
|
||||
for event_id in user_event_ids:
|
||||
if events.has(event_id):
|
||||
var event = events[event_id]
|
||||
var type_name = EventType.keys()[event.event_type]
|
||||
interests[type_name] = interests.get(type_name, 0) + 1
|
||||
|
||||
return interests
|
||||
|
||||
## 计算推荐分数
|
||||
func _calculate_recommendation_score(event: CommunityEvent, user_id: String, user_interests: Dictionary) -> float:
|
||||
"""
|
||||
计算事件推荐分数
|
||||
@param event: 事件对象
|
||||
@param user_id: 用户ID
|
||||
@param user_interests: 用户兴趣数据
|
||||
@return: 推荐分数
|
||||
"""
|
||||
var score = 0.0
|
||||
|
||||
# 基于用户兴趣
|
||||
var type_name = EventType.keys()[event.event_type]
|
||||
score += user_interests.get(type_name, 0) * 10.0
|
||||
|
||||
# 基于关系网络
|
||||
if relationship_network:
|
||||
var user_relationships = relationship_network.get_character_relationships(user_id)
|
||||
for relationship in user_relationships:
|
||||
if relationship.to_character in event.participants:
|
||||
score += relationship.strength * 0.5
|
||||
|
||||
# 基于事件规模(适中规模更受欢迎)
|
||||
var participant_count = event.participants.size()
|
||||
if participant_count >= 3 and participant_count <= 10:
|
||||
score += 5.0
|
||||
|
||||
# 基于时间(即将开始的事件更相关)
|
||||
var time_until_start = event.start_time - Time.get_unix_time_from_system()
|
||||
if time_until_start > 0 and time_until_start <= 86400: # 24小时内
|
||||
score += 10.0 - (time_until_start / 8640.0) # 越近分数越高
|
||||
|
||||
return score
|
||||
|
||||
## 获取事件信息
|
||||
func _get_event_info(event: CommunityEvent) -> Dictionary:
|
||||
"""
|
||||
获取事件信息字典
|
||||
@param event: 事件对象
|
||||
@return: 事件信息字典
|
||||
"""
|
||||
return {
|
||||
"id": event.event_id,
|
||||
"title": event.title,
|
||||
"description": event.description,
|
||||
"type": event.event_type,
|
||||
"type_name": EventType.keys()[event.event_type],
|
||||
"status": event.status,
|
||||
"status_name": EventStatus.keys()[event.status],
|
||||
"organizer_id": event.organizer_id,
|
||||
"participants": event.participants.duplicate(),
|
||||
"participant_count": event.participants.size(),
|
||||
"max_participants": event.max_participants,
|
||||
"start_time": event.start_time,
|
||||
"end_time": event.end_time,
|
||||
"location": event.location,
|
||||
"rewards": event.rewards.duplicate(),
|
||||
"requirements": event.requirements.duplicate(),
|
||||
"created_at": event.created_at,
|
||||
"updated_at": event.updated_at,
|
||||
"tags": event.tags.duplicate(),
|
||||
"metadata": event.metadata.duplicate()
|
||||
}
|
||||
|
||||
## 添加到事件历史
|
||||
func _add_to_event_history(event: CommunityEvent) -> void:
|
||||
"""
|
||||
添加事件到历史记录
|
||||
@param event: 事件对象
|
||||
"""
|
||||
var history_entry = {
|
||||
"event_id": event.event_id,
|
||||
"title": event.title,
|
||||
"type": event.event_type,
|
||||
"organizer_id": event.organizer_id,
|
||||
"participant_count": event.participants.size(),
|
||||
"start_time": event.start_time,
|
||||
"end_time": event.end_time,
|
||||
"completed_at": Time.get_unix_time_from_system()
|
||||
}
|
||||
|
||||
event_history.append(history_entry)
|
||||
|
||||
# 限制历史记录长度
|
||||
if event_history.size() > 1000:
|
||||
event_history.pop_front()
|
||||
|
||||
## 添加事件到用户索引
|
||||
func _add_event_to_user_index(user_id: String, event_id: String) -> void:
|
||||
"""
|
||||
添加事件到用户索引
|
||||
@param user_id: 用户ID
|
||||
@param event_id: 事件ID
|
||||
"""
|
||||
if not user_events.has(user_id):
|
||||
user_events[user_id] = []
|
||||
|
||||
var user_event_list = user_events[user_id]
|
||||
if not event_id in user_event_list:
|
||||
user_event_list.append(event_id)
|
||||
|
||||
## 从用户索引移除事件
|
||||
func _remove_event_from_user_index(user_id: String, event_id: String) -> void:
|
||||
"""
|
||||
从用户索引移除事件
|
||||
@param user_id: 用户ID
|
||||
@param event_id: 事件ID
|
||||
"""
|
||||
if user_events.has(user_id):
|
||||
var user_event_list = user_events[user_id]
|
||||
user_event_list.erase(event_id)
|
||||
|
||||
## 生成事件ID
|
||||
func generate_event_id() -> String:
|
||||
"""生成唯一的事件ID"""
|
||||
var timestamp = Time.get_unix_time_from_system()
|
||||
var random = randi()
|
||||
return "event_%d_%d" % [timestamp, random]
|
||||
|
||||
## 保存事件数据
|
||||
func save_events_data() -> void:
|
||||
"""保存事件数据到本地文件"""
|
||||
var data = {
|
||||
"events": {},
|
||||
"active_events": active_events,
|
||||
"user_events": user_events,
|
||||
"event_history": event_history
|
||||
}
|
||||
|
||||
# 序列化事件数据
|
||||
for event_id in events:
|
||||
var event = events[event_id]
|
||||
data.events[event_id] = {
|
||||
"title": event.title,
|
||||
"description": event.description,
|
||||
"event_type": event.event_type,
|
||||
"status": event.status,
|
||||
"organizer_id": event.organizer_id,
|
||||
"participants": event.participants,
|
||||
"max_participants": event.max_participants,
|
||||
"start_time": event.start_time,
|
||||
"end_time": event.end_time,
|
||||
"location": event.location,
|
||||
"rewards": event.rewards,
|
||||
"requirements": event.requirements,
|
||||
"created_at": event.created_at,
|
||||
"updated_at": event.updated_at,
|
||||
"tags": event.tags,
|
||||
"metadata": event.metadata
|
||||
}
|
||||
|
||||
var file = FileAccess.open(events_file_path, FileAccess.WRITE)
|
||||
if file:
|
||||
var json_string = JSON.stringify(data)
|
||||
file.store_string(json_string)
|
||||
file.close()
|
||||
print("Community events data saved")
|
||||
else:
|
||||
print("Failed to save community events data")
|
||||
|
||||
## 加载事件数据
|
||||
func load_events_data() -> void:
|
||||
"""从本地文件加载事件数据"""
|
||||
if not FileAccess.file_exists(events_file_path):
|
||||
print("No community events data file found, starting fresh")
|
||||
return
|
||||
|
||||
var file = FileAccess.open(events_file_path, FileAccess.READ)
|
||||
if file:
|
||||
var json_string = file.get_as_text()
|
||||
file.close()
|
||||
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(json_string)
|
||||
|
||||
if parse_result == OK:
|
||||
var data = json.data
|
||||
|
||||
# 加载事件数据
|
||||
if data.has("events"):
|
||||
for event_id in data.events:
|
||||
var event_data = data.events[event_id]
|
||||
var event = CommunityEvent.new(
|
||||
event_id,
|
||||
event_data.get("title", ""),
|
||||
event_data.get("event_type", EventType.SOCIAL_GATHERING),
|
||||
event_data.get("organizer_id", "")
|
||||
)
|
||||
event.description = event_data.get("description", "")
|
||||
event.status = event_data.get("status", EventStatus.PLANNED)
|
||||
event.participants = event_data.get("participants", [])
|
||||
event.max_participants = event_data.get("max_participants", 0)
|
||||
event.start_time = event_data.get("start_time", Time.get_unix_time_from_system())
|
||||
event.end_time = event_data.get("end_time", event.start_time + 3600)
|
||||
event.location = event_data.get("location", "")
|
||||
event.rewards = event_data.get("rewards", {})
|
||||
event.requirements = event_data.get("requirements", {})
|
||||
event.created_at = event_data.get("created_at", Time.get_unix_time_from_system())
|
||||
event.updated_at = event_data.get("updated_at", event.created_at)
|
||||
event.tags = event_data.get("tags", [])
|
||||
event.metadata = event_data.get("metadata", {})
|
||||
events[event_id] = event
|
||||
|
||||
# 加载活跃事件列表
|
||||
if data.has("active_events"):
|
||||
active_events = data.active_events
|
||||
|
||||
# 加载用户事件索引
|
||||
if data.has("user_events"):
|
||||
user_events = data.user_events
|
||||
|
||||
# 加载事件历史
|
||||
if data.has("event_history"):
|
||||
event_history = data.event_history
|
||||
|
||||
print("Community events data loaded: ", events.size(), " events, ", active_events.size(), " active")
|
||||
else:
|
||||
print("Failed to parse community events data JSON")
|
||||
else:
|
||||
print("Failed to open community events data file")
|
||||
|
||||
## 获取统计信息
|
||||
func get_statistics() -> Dictionary:
|
||||
"""
|
||||
获取社区事件系统统计信息
|
||||
@return: 统计信息字典
|
||||
"""
|
||||
var type_counts = {}
|
||||
var status_counts = {}
|
||||
var total_participants = 0
|
||||
|
||||
for event_id in events:
|
||||
var event = events[event_id]
|
||||
var type_name = EventType.keys()[event.event_type]
|
||||
var status_name = EventStatus.keys()[event.status]
|
||||
|
||||
type_counts[type_name] = type_counts.get(type_name, 0) + 1
|
||||
status_counts[status_name] = status_counts.get(status_name, 0) + 1
|
||||
total_participants += event.participants.size()
|
||||
|
||||
return {
|
||||
"total_events": events.size(),
|
||||
"active_events": active_events.size(),
|
||||
"completed_events": event_history.size(),
|
||||
"total_participants": total_participants,
|
||||
"average_participants": float(total_participants) / max(events.size(), 1),
|
||||
"event_types": type_counts,
|
||||
"event_statuses": status_counts,
|
||||
"max_events_per_user": max_events_per_user,
|
||||
"max_active_events": max_active_events
|
||||
}
|
||||
1
scripts/CommunityEventSystem.gd.uid
Normal file
1
scripts/CommunityEventSystem.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cx5nxyt4bohe0
|
||||
473
scripts/DatawhaleOffice.gd
Normal file
473
scripts/DatawhaleOffice.gd
Normal file
@@ -0,0 +1,473 @@
|
||||
extends Node2D
|
||||
class_name DatawhaleOffice
|
||||
## Datawhale 办公室场景
|
||||
## 游戏的主要场景
|
||||
|
||||
# 场景配置
|
||||
const SCENE_WIDTH = 2000
|
||||
const SCENE_HEIGHT = 1500
|
||||
|
||||
# Datawhale 品牌色
|
||||
const COLOR_PRIMARY = Color(0.118, 0.565, 1.0) # 蓝色 #1E90FF
|
||||
const COLOR_SECONDARY = Color(0.0, 0.4, 0.8) # 深蓝 #0066CC
|
||||
const COLOR_ACCENT = Color(0.0, 0.749, 1.0) # 亮蓝 #00BFFF
|
||||
const COLOR_FLOOR = Color(0.9, 0.9, 0.9) # 浅灰色地板
|
||||
const COLOR_WALL = Color(0.4, 0.4, 0.4) # 深灰色墙壁
|
||||
const COLOR_FURNITURE = Color(0.545, 0.271, 0.075) # 棕色家具
|
||||
|
||||
# 节点引用
|
||||
@onready var tile_map = $TileMap
|
||||
@onready var characters_container = $Characters
|
||||
@onready var camera = $Camera2D
|
||||
|
||||
func _ready():
|
||||
"""初始化场景"""
|
||||
print("DatawhaleOffice scene loaded")
|
||||
print("Scene size: ", SCENE_WIDTH, "x", SCENE_HEIGHT)
|
||||
|
||||
# 程序化生成场景元素
|
||||
_create_scene_elements()
|
||||
|
||||
# 设置相机限制
|
||||
_setup_camera_limits()
|
||||
|
||||
## 创建场景元素
|
||||
func _create_scene_elements():
|
||||
"""程序化创建场景的所有视觉元素和碰撞"""
|
||||
# 创建地板
|
||||
_create_floor()
|
||||
|
||||
# 创建墙壁(带碰撞)
|
||||
_create_walls()
|
||||
|
||||
# 创建家具(带碰撞)
|
||||
_create_furniture()
|
||||
|
||||
# 创建 Datawhale 品牌元素
|
||||
_create_branding()
|
||||
|
||||
print("Scene elements created")
|
||||
|
||||
## 设置相机限制
|
||||
func _setup_camera_limits():
|
||||
"""设置相机边界,防止看到场景外"""
|
||||
# 计算视口尺寸(假设默认分辨率 1280x720)
|
||||
var viewport_width = 1280
|
||||
var viewport_height = 720
|
||||
|
||||
# 为缩放留出更大的缓冲区,避免缩放时触碰边界造成颠簸
|
||||
var buffer_x = viewport_width * 0.8 # 增加水平缓冲区
|
||||
var buffer_y = viewport_height * 0.8 # 增加垂直缓冲区
|
||||
|
||||
# 设置更宽松的相机限制
|
||||
camera.limit_left = -buffer_x
|
||||
camera.limit_top = -buffer_y
|
||||
camera.limit_right = SCENE_WIDTH + buffer_x
|
||||
camera.limit_bottom = SCENE_HEIGHT + buffer_y
|
||||
|
||||
# 启用平滑跟随
|
||||
camera.position_smoothing_enabled = true
|
||||
camera.position_smoothing_speed = 5.0
|
||||
|
||||
print("Camera limits set with buffer - Left: ", camera.limit_left, " Top: ", camera.limit_top, " Right: ", camera.limit_right, " Bottom: ", camera.limit_bottom)
|
||||
|
||||
# 注意:相机的调试控制脚本已在场景文件中直接添加
|
||||
# 使用 WASD/方向键移动,Q/E 缩放,R 重置
|
||||
|
||||
## 获取角色容器
|
||||
func get_characters_container() -> Node2D:
|
||||
"""
|
||||
获取角色容器节点
|
||||
@return: 角色容器
|
||||
"""
|
||||
return characters_container
|
||||
|
||||
## 设置相机跟随目标
|
||||
func set_camera_target(target: Node2D):
|
||||
"""
|
||||
设置相机跟随的目标
|
||||
@param target: 要跟随的节点(通常是玩家角色)
|
||||
"""
|
||||
if target:
|
||||
# 使用 RemoteTransform2D 实现平滑跟随
|
||||
var remote_transform = RemoteTransform2D.new()
|
||||
remote_transform.remote_path = camera.get_path()
|
||||
remote_transform.update_position = true
|
||||
remote_transform.update_rotation = false
|
||||
remote_transform.update_scale = false
|
||||
target.add_child(remote_transform)
|
||||
print("Camera following: ", target.name)
|
||||
|
||||
## 获取场景尺寸
|
||||
func get_scene_size() -> Vector2:
|
||||
"""
|
||||
获取场景尺寸
|
||||
@return: 场景尺寸
|
||||
"""
|
||||
return Vector2(SCENE_WIDTH, SCENE_HEIGHT)
|
||||
|
||||
## 创建地板
|
||||
func _create_floor():
|
||||
"""创建地板(纯视觉,无碰撞)"""
|
||||
var floor_rect = ColorRect.new()
|
||||
floor_rect.name = "Floor"
|
||||
floor_rect.size = Vector2(SCENE_WIDTH, SCENE_HEIGHT)
|
||||
floor_rect.color = COLOR_FLOOR
|
||||
floor_rect.z_index = -10 # 放在最底层
|
||||
add_child(floor_rect)
|
||||
|
||||
## 创建墙壁
|
||||
func _create_walls():
|
||||
"""创建墙壁(带碰撞)"""
|
||||
var walls_container = Node2D.new()
|
||||
walls_container.name = "Walls"
|
||||
add_child(walls_container)
|
||||
|
||||
# 墙壁厚度
|
||||
var wall_thickness = 20
|
||||
|
||||
# 上墙
|
||||
_create_wall(walls_container, Vector2(0, 0), Vector2(SCENE_WIDTH, wall_thickness))
|
||||
# 下墙
|
||||
_create_wall(walls_container, Vector2(0, SCENE_HEIGHT - wall_thickness), Vector2(SCENE_WIDTH, wall_thickness))
|
||||
# 左墙
|
||||
_create_wall(walls_container, Vector2(0, 0), Vector2(wall_thickness, SCENE_HEIGHT))
|
||||
# 右墙
|
||||
_create_wall(walls_container, Vector2(SCENE_WIDTH - wall_thickness, 0), Vector2(wall_thickness, SCENE_HEIGHT))
|
||||
|
||||
# 内部分隔墙(创建房间)
|
||||
# 垂直分隔墙
|
||||
_create_wall(walls_container, Vector2(800, 200), Vector2(wall_thickness, 600))
|
||||
# 水平分隔墙
|
||||
_create_wall(walls_container, Vector2(200, 800), Vector2(1600, wall_thickness))
|
||||
|
||||
## 创建单个墙壁
|
||||
func _create_wall(parent: Node2D, pos: Vector2, size: Vector2):
|
||||
"""创建单个墙壁块"""
|
||||
var wall = StaticBody2D.new()
|
||||
wall.collision_layer = 1 # Layer 1: Walls
|
||||
wall.collision_mask = 0
|
||||
|
||||
# 视觉表现
|
||||
var visual = ColorRect.new()
|
||||
visual.size = size
|
||||
visual.color = COLOR_WALL
|
||||
wall.add_child(visual)
|
||||
|
||||
# 碰撞形状
|
||||
var collision = CollisionShape2D.new()
|
||||
var shape = RectangleShape2D.new()
|
||||
shape.size = size
|
||||
collision.shape = shape
|
||||
collision.position = size / 2 # 中心点
|
||||
wall.add_child(collision)
|
||||
|
||||
wall.position = pos
|
||||
parent.add_child(wall)
|
||||
|
||||
## 创建家具
|
||||
func _create_furniture():
|
||||
"""创建家具(办公桌、椅子等)"""
|
||||
var furniture_container = Node2D.new()
|
||||
furniture_container.name = "Furniture"
|
||||
add_child(furniture_container)
|
||||
|
||||
# 入口区域 - 欢迎台
|
||||
_create_furniture_piece(furniture_container, Vector2(100, 100), Vector2(200, 80), "Reception")
|
||||
|
||||
# 工作区 - 办公桌(6个)
|
||||
for i in range(3):
|
||||
for j in range(2):
|
||||
var desk_pos = Vector2(100 + i * 250, 300 + j * 200)
|
||||
_create_furniture_piece(furniture_container, desk_pos, Vector2(120, 60), "Desk")
|
||||
|
||||
# 会议区 - 会议桌
|
||||
_create_furniture_piece(furniture_container, Vector2(900, 300), Vector2(400, 200), "Meeting Table")
|
||||
|
||||
# 休息区 - 沙发
|
||||
_create_furniture_piece(furniture_container, Vector2(100, 900), Vector2(300, 100), "Sofa")
|
||||
_create_furniture_piece(furniture_container, Vector2(100, 1050), Vector2(100, 100), "Coffee Table")
|
||||
|
||||
## 创建单个家具
|
||||
func _create_furniture_piece(parent: Node2D, pos: Vector2, size: Vector2, label: String):
|
||||
"""创建单个家具块"""
|
||||
var furniture = StaticBody2D.new()
|
||||
furniture.name = label
|
||||
furniture.collision_layer = 2 # Layer 2: Furniture
|
||||
furniture.collision_mask = 0
|
||||
|
||||
# 视觉表现
|
||||
var visual = ColorRect.new()
|
||||
visual.size = size
|
||||
visual.color = COLOR_FURNITURE
|
||||
furniture.add_child(visual)
|
||||
|
||||
# 碰撞形状
|
||||
var collision = CollisionShape2D.new()
|
||||
var shape = RectangleShape2D.new()
|
||||
shape.size = size
|
||||
collision.shape = shape
|
||||
collision.position = size / 2
|
||||
furniture.add_child(collision)
|
||||
|
||||
furniture.position = pos
|
||||
parent.add_child(furniture)
|
||||
|
||||
## 创建品牌元素
|
||||
func _create_branding():
|
||||
"""创建 Datawhale 品牌元素"""
|
||||
var branding_container = Node2D.new()
|
||||
branding_container.name = "Branding"
|
||||
branding_container.z_index = 5 # 确保品牌元素在上层
|
||||
add_child(branding_container)
|
||||
|
||||
# 1. 主 Logo 展示区(展示区中心)
|
||||
_create_main_logo(branding_container, Vector2(1400, 400))
|
||||
|
||||
# 2. 欢迎标识(入口区域)
|
||||
_create_welcome_sign(branding_container, Vector2(100, 50))
|
||||
|
||||
# 3. 成就墙(展示区)
|
||||
_create_achievement_wall(branding_container, Vector2(1200, 900))
|
||||
|
||||
# 4. 装饰性品牌元素
|
||||
_create_decorative_elements(branding_container)
|
||||
|
||||
# 5. 地板品牌标识
|
||||
_create_floor_branding(branding_container)
|
||||
|
||||
## 创建主 Logo
|
||||
func _create_main_logo(parent: Node2D, pos: Vector2):
|
||||
"""创建主 Datawhale Logo(使用真实图片)"""
|
||||
var logo_container = Node2D.new()
|
||||
logo_container.name = "MainLogo"
|
||||
logo_container.position = pos
|
||||
parent.add_child(logo_container)
|
||||
|
||||
# Logo 路径
|
||||
var logo_path = "res://assets/ui/datawhale_logo.png"
|
||||
|
||||
# 检查 Logo 文件是否存在
|
||||
if ResourceLoader.exists(logo_path):
|
||||
# 创建白色背景板(让蓝色 logo 更突出)
|
||||
var bg_panel = ColorRect.new()
|
||||
bg_panel.size = Vector2(450, 120)
|
||||
bg_panel.color = Color.WHITE
|
||||
bg_panel.position = Vector2(-225, -60)
|
||||
logo_container.add_child(bg_panel)
|
||||
|
||||
# 添加边框效果
|
||||
var border = ColorRect.new()
|
||||
border.size = Vector2(460, 130)
|
||||
border.color = COLOR_PRIMARY
|
||||
border.position = Vector2(-230, -65)
|
||||
border.z_index = -1
|
||||
logo_container.add_child(border)
|
||||
|
||||
# 使用真实的 Logo 图片
|
||||
var logo_sprite = Sprite2D.new()
|
||||
logo_sprite.texture = load(logo_path)
|
||||
# 362x53 像素的 logo,缩放到合适大小
|
||||
# 目标宽度约 400 像素,所以 scale = 400/362 ≈ 1.1
|
||||
logo_sprite.scale = Vector2(1.1, 1.1)
|
||||
logo_sprite.position = Vector2(0, 0)
|
||||
logo_container.add_child(logo_sprite)
|
||||
|
||||
print("✓ Datawhale logo loaded (362x53 px, scaled to fit)")
|
||||
else:
|
||||
# 如果 logo 文件不存在,使用占位符
|
||||
print("⚠ Logo file not found at: ", logo_path)
|
||||
print(" Please place logo at: assets/ui/datawhale_logo.png")
|
||||
|
||||
# 占位符
|
||||
var placeholder = ColorRect.new()
|
||||
placeholder.size = Vector2(400, 60)
|
||||
placeholder.color = COLOR_PRIMARY
|
||||
placeholder.position = Vector2(-200, -30)
|
||||
logo_container.add_child(placeholder)
|
||||
|
||||
var placeholder_text = Label.new()
|
||||
placeholder_text.text = "DATAWHALE LOGO HERE (362x53)"
|
||||
placeholder_text.position = Vector2(-150, -10)
|
||||
placeholder_text.add_theme_font_size_override("font_size", 20)
|
||||
placeholder_text.add_theme_color_override("font_color", Color.WHITE)
|
||||
logo_container.add_child(placeholder_text)
|
||||
|
||||
# 副标题(在 logo 下方)
|
||||
var subtitle = Label.new()
|
||||
subtitle.text = "AI Learning Community"
|
||||
subtitle.position = Vector2(-80, 80)
|
||||
subtitle.add_theme_font_size_override("font_size", 18)
|
||||
subtitle.add_theme_color_override("font_color", COLOR_SECONDARY)
|
||||
logo_container.add_child(subtitle)
|
||||
|
||||
## 创建欢迎标识
|
||||
func _create_welcome_sign(parent: Node2D, pos: Vector2):
|
||||
"""创建入口欢迎标识(带 logo)"""
|
||||
var sign_container = Node2D.new()
|
||||
sign_container.name = "WelcomeSign"
|
||||
sign_container.position = pos
|
||||
parent.add_child(sign_container)
|
||||
|
||||
# 标识背景(白色,让蓝色 logo 更突出)
|
||||
var bg = ColorRect.new()
|
||||
bg.size = Vector2(600, 100)
|
||||
bg.color = Color.WHITE
|
||||
sign_container.add_child(bg)
|
||||
|
||||
# 蓝色边框
|
||||
var border = ColorRect.new()
|
||||
border.size = Vector2(610, 110)
|
||||
border.color = COLOR_PRIMARY
|
||||
border.position = Vector2(-5, -5)
|
||||
border.z_index = -1
|
||||
sign_container.add_child(border)
|
||||
|
||||
# Logo(如果存在)
|
||||
var logo_path = "res://assets/ui/datawhale_logo.png"
|
||||
if ResourceLoader.exists(logo_path):
|
||||
var logo_sprite = Sprite2D.new()
|
||||
logo_sprite.texture = load(logo_path)
|
||||
# 小尺寸显示在欢迎标识中
|
||||
logo_sprite.scale = Vector2(0.5, 0.5) # 约 181x26.5 像素
|
||||
logo_sprite.position = Vector2(120, 50)
|
||||
sign_container.add_child(logo_sprite)
|
||||
|
||||
# 欢迎文字
|
||||
var welcome_text = Label.new()
|
||||
welcome_text.text = "Welcome to"
|
||||
welcome_text.position = Vector2(20, 15)
|
||||
welcome_text.add_theme_font_size_override("font_size", 20)
|
||||
welcome_text.add_theme_color_override("font_color", COLOR_SECONDARY)
|
||||
sign_container.add_child(welcome_text)
|
||||
|
||||
# Office 文字
|
||||
var office_text = Label.new()
|
||||
office_text.text = "Office"
|
||||
office_text.position = Vector2(250, 50)
|
||||
office_text.add_theme_font_size_override("font_size", 24)
|
||||
office_text.add_theme_color_override("font_color", COLOR_SECONDARY)
|
||||
sign_container.add_child(office_text)
|
||||
|
||||
## 创建成就墙
|
||||
func _create_achievement_wall(parent: Node2D, pos: Vector2):
|
||||
"""创建成就展示墙"""
|
||||
var wall_container = Node2D.new()
|
||||
wall_container.name = "AchievementWall"
|
||||
wall_container.position = pos
|
||||
parent.add_child(wall_container)
|
||||
|
||||
# 墙面背景
|
||||
var wall_bg = ColorRect.new()
|
||||
wall_bg.size = Vector2(600, 450)
|
||||
wall_bg.color = Color(0.95, 0.95, 0.95)
|
||||
wall_container.add_child(wall_bg)
|
||||
|
||||
# 顶部 Logo
|
||||
var logo_path = "res://assets/ui/datawhale_logo.png"
|
||||
if ResourceLoader.exists(logo_path):
|
||||
var logo_sprite = Sprite2D.new()
|
||||
logo_sprite.texture = load(logo_path)
|
||||
logo_sprite.scale = Vector2(0.6, 0.6) # 约 217x32 像素
|
||||
logo_sprite.position = Vector2(300, 30)
|
||||
wall_container.add_child(logo_sprite)
|
||||
|
||||
# 标题
|
||||
var title = Label.new()
|
||||
title.text = "Our Achievements"
|
||||
title.position = Vector2(200, 60)
|
||||
title.add_theme_font_size_override("font_size", 24)
|
||||
title.add_theme_color_override("font_color", COLOR_PRIMARY)
|
||||
wall_container.add_child(title)
|
||||
|
||||
# 成就卡片
|
||||
var achievements = [
|
||||
{"title": "10K+ Members", "icon_color": COLOR_PRIMARY},
|
||||
{"title": "500+ Projects", "icon_color": COLOR_SECONDARY},
|
||||
{"title": "100+ Courses", "icon_color": COLOR_ACCENT},
|
||||
{"title": "AI Excellence", "icon_color": COLOR_PRIMARY}
|
||||
]
|
||||
|
||||
for i in range(achievements.size()):
|
||||
@warning_ignore("integer_division")
|
||||
var row = i / 2 # 整数除法
|
||||
var col = i % 2
|
||||
var card_pos = Vector2(50 + col * 280, 110 + row * 140)
|
||||
_create_achievement_card(wall_container, card_pos, achievements[i])
|
||||
|
||||
## 创建成就卡片
|
||||
func _create_achievement_card(parent: Node2D, pos: Vector2, data: Dictionary):
|
||||
"""创建单个成就卡片"""
|
||||
var card = ColorRect.new()
|
||||
card.size = Vector2(240, 100)
|
||||
card.color = Color.WHITE
|
||||
card.position = pos
|
||||
parent.add_child(card)
|
||||
|
||||
# 图标
|
||||
var icon = ColorRect.new()
|
||||
icon.size = Vector2(60, 60)
|
||||
icon.color = data["icon_color"]
|
||||
icon.position = Vector2(20, 20)
|
||||
card.add_child(icon)
|
||||
|
||||
# 文字
|
||||
var text = Label.new()
|
||||
text.text = data["title"]
|
||||
text.position = Vector2(90, 35)
|
||||
text.add_theme_font_size_override("font_size", 18)
|
||||
text.add_theme_color_override("font_color", COLOR_SECONDARY)
|
||||
card.add_child(text)
|
||||
|
||||
## 创建装饰性元素
|
||||
func _create_decorative_elements(parent: Node2D):
|
||||
"""创建装饰性品牌元素"""
|
||||
# 蓝色装饰条纹(右侧墙面)
|
||||
for i in range(8):
|
||||
var stripe = ColorRect.new()
|
||||
stripe.size = Vector2(40, 200)
|
||||
stripe.color = COLOR_ACCENT
|
||||
stripe.color.a = 0.2 + (i % 3) * 0.1 # 渐变透明度
|
||||
stripe.position = Vector2(1700 + i * 50, 100 + (i % 2) * 100)
|
||||
parent.add_child(stripe)
|
||||
|
||||
# 品牌色圆点装饰(散布在场景中)
|
||||
var dot_positions = [
|
||||
Vector2(500, 200), Vector2(700, 250), Vector2(900, 150),
|
||||
Vector2(300, 600), Vector2(600, 650), Vector2(1100, 600)
|
||||
]
|
||||
|
||||
for pos in dot_positions:
|
||||
var dot = ColorRect.new()
|
||||
dot.size = Vector2(30, 30)
|
||||
dot.color = COLOR_PRIMARY
|
||||
dot.color.a = 0.3
|
||||
dot.position = pos
|
||||
parent.add_child(dot)
|
||||
|
||||
## 创建地板品牌标识
|
||||
func _create_floor_branding(parent: Node2D):
|
||||
"""在地板上创建品牌标识"""
|
||||
var logo_path = "res://assets/ui/datawhale_logo.png"
|
||||
|
||||
if ResourceLoader.exists(logo_path):
|
||||
# 使用真实 logo 作为地板水印
|
||||
var floor_logo = Sprite2D.new()
|
||||
floor_logo.texture = load(logo_path)
|
||||
floor_logo.position = Vector2(1000, 700)
|
||||
floor_logo.rotation = -0.1
|
||||
floor_logo.scale = Vector2(3.0, 3.0) # 大尺寸水印
|
||||
floor_logo.modulate.a = 0.08 # 非常淡的水印效果
|
||||
floor_logo.z_index = -5
|
||||
parent.add_child(floor_logo)
|
||||
else:
|
||||
# 文字水印作为后备
|
||||
var floor_logo = Label.new()
|
||||
floor_logo.text = "DATAWHALE"
|
||||
floor_logo.position = Vector2(800, 600)
|
||||
floor_logo.rotation = -0.1
|
||||
floor_logo.add_theme_font_size_override("font_size", 120)
|
||||
floor_logo.add_theme_color_override("font_color", COLOR_PRIMARY)
|
||||
floor_logo.modulate.a = 0.05
|
||||
floor_logo.z_index = -5
|
||||
parent.add_child(floor_logo)
|
||||
1
scripts/DatawhaleOffice.gd.uid
Normal file
1
scripts/DatawhaleOffice.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://5wfrobimvgpr
|
||||
398
scripts/DatawhaleOffice_with_logo.gd
Normal file
398
scripts/DatawhaleOffice_with_logo.gd
Normal file
@@ -0,0 +1,398 @@
|
||||
extends Node2D
|
||||
class_name DatawhaleOfficeWithLogo
|
||||
## Datawhale 办公室场景(使用真实 Logo 版本)
|
||||
## 将 logo 文件放在 assets/ui/datawhale_logo.png 后使用此版本
|
||||
|
||||
# 场景配置
|
||||
const SCENE_WIDTH = 2000
|
||||
const SCENE_HEIGHT = 1500
|
||||
|
||||
# Datawhale 品牌色
|
||||
const COLOR_PRIMARY = Color(0.118, 0.565, 1.0) # 蓝色 #1E90FF
|
||||
const COLOR_SECONDARY = Color(0.0, 0.4, 0.8) # 深蓝 #0066CC
|
||||
const COLOR_ACCENT = Color(0.0, 0.749, 1.0) # 亮蓝 #00BFFF
|
||||
const COLOR_FLOOR = Color(0.9, 0.9, 0.9) # 浅灰色地板
|
||||
const COLOR_WALL = Color(0.4, 0.4, 0.4) # 深灰色墙壁
|
||||
const COLOR_FURNITURE = Color(0.545, 0.271, 0.075) # 棕色家具
|
||||
|
||||
# Logo 资源路径
|
||||
const LOGO_PATH = "res://assets/ui/datawhale_logo.png"
|
||||
const LOGO_ICON_PATH = "res://assets/ui/datawhale_icon.png" # 可选
|
||||
|
||||
# 节点引用
|
||||
@onready var tile_map = $TileMap
|
||||
@onready var characters_container = $Characters
|
||||
@onready var camera = $Camera2D
|
||||
|
||||
func _ready():
|
||||
"""初始化场景"""
|
||||
print("DatawhaleOffice scene loaded (with real logo)")
|
||||
print("Scene size: ", SCENE_WIDTH, "x", SCENE_HEIGHT)
|
||||
|
||||
# 程序化生成场景元素
|
||||
_create_scene_elements()
|
||||
|
||||
# 设置相机限制
|
||||
_setup_camera_limits()
|
||||
|
||||
## 创建场景元素
|
||||
func _create_scene_elements():
|
||||
"""程序化创建场景的所有视觉元素和碰撞"""
|
||||
# 创建地板
|
||||
_create_floor()
|
||||
|
||||
# 创建墙壁(带碰撞)
|
||||
_create_walls()
|
||||
|
||||
# 创建家具(带碰撞)
|
||||
_create_furniture()
|
||||
|
||||
# 创建 Datawhale 品牌元素
|
||||
_create_branding()
|
||||
|
||||
print("Scene elements created")
|
||||
|
||||
## 设置相机限制
|
||||
func _setup_camera_limits():
|
||||
"""设置相机边界,防止看到场景外"""
|
||||
camera.limit_left = 0
|
||||
camera.limit_top = 0
|
||||
camera.limit_right = SCENE_WIDTH
|
||||
camera.limit_bottom = SCENE_HEIGHT
|
||||
|
||||
# 启用平滑跟随
|
||||
camera.position_smoothing_enabled = true
|
||||
camera.position_smoothing_speed = 5.0
|
||||
|
||||
## 获取角色容器
|
||||
func get_characters_container() -> Node2D:
|
||||
"""获取角色容器节点"""
|
||||
return characters_container
|
||||
|
||||
## 设置相机跟随目标
|
||||
func set_camera_target(target: Node2D):
|
||||
"""设置相机跟随的目标"""
|
||||
if target:
|
||||
var remote_transform = RemoteTransform2D.new()
|
||||
remote_transform.remote_path = camera.get_path()
|
||||
remote_transform.update_position = true
|
||||
remote_transform.update_rotation = false
|
||||
remote_transform.update_scale = false
|
||||
target.add_child(remote_transform)
|
||||
print("Camera following: ", target.name)
|
||||
|
||||
## 获取场景尺寸
|
||||
func get_scene_size() -> Vector2:
|
||||
"""获取场景尺寸"""
|
||||
return Vector2(SCENE_WIDTH, SCENE_HEIGHT)
|
||||
|
||||
## 创建地板
|
||||
func _create_floor():
|
||||
"""创建地板(纯视觉,无碰撞)"""
|
||||
var floor = ColorRect.new()
|
||||
floor.name = "Floor"
|
||||
floor.size = Vector2(SCENE_WIDTH, SCENE_HEIGHT)
|
||||
floor.color = COLOR_FLOOR
|
||||
floor.z_index = -10
|
||||
add_child(floor)
|
||||
|
||||
## 创建墙壁
|
||||
func _create_walls():
|
||||
"""创建墙壁(带碰撞)"""
|
||||
var walls_container = Node2D.new()
|
||||
walls_container.name = "Walls"
|
||||
add_child(walls_container)
|
||||
|
||||
var wall_thickness = 20
|
||||
|
||||
# 外墙
|
||||
_create_wall(walls_container, Vector2(0, 0), Vector2(SCENE_WIDTH, wall_thickness))
|
||||
_create_wall(walls_container, Vector2(0, SCENE_HEIGHT - wall_thickness), Vector2(SCENE_WIDTH, wall_thickness))
|
||||
_create_wall(walls_container, Vector2(0, 0), Vector2(wall_thickness, SCENE_HEIGHT))
|
||||
_create_wall(walls_container, Vector2(SCENE_WIDTH - wall_thickness, 0), Vector2(wall_thickness, SCENE_HEIGHT))
|
||||
|
||||
# 内部分隔墙
|
||||
_create_wall(walls_container, Vector2(800, 200), Vector2(wall_thickness, 600))
|
||||
_create_wall(walls_container, Vector2(200, 800), Vector2(1600, wall_thickness))
|
||||
|
||||
## 创建单个墙壁
|
||||
func _create_wall(parent: Node2D, pos: Vector2, size: Vector2):
|
||||
"""创建单个墙壁块"""
|
||||
var wall = StaticBody2D.new()
|
||||
wall.collision_layer = 1
|
||||
wall.collision_mask = 0
|
||||
|
||||
var visual = ColorRect.new()
|
||||
visual.size = size
|
||||
visual.color = COLOR_WALL
|
||||
wall.add_child(visual)
|
||||
|
||||
var collision = CollisionShape2D.new()
|
||||
var shape = RectangleShape2D.new()
|
||||
shape.size = size
|
||||
collision.shape = shape
|
||||
collision.position = size / 2
|
||||
wall.add_child(collision)
|
||||
|
||||
wall.position = pos
|
||||
parent.add_child(wall)
|
||||
|
||||
## 创建家具
|
||||
func _create_furniture():
|
||||
"""创建家具(办公桌、椅子等)"""
|
||||
var furniture_container = Node2D.new()
|
||||
furniture_container.name = "Furniture"
|
||||
add_child(furniture_container)
|
||||
|
||||
# 入口区域 - 欢迎台
|
||||
_create_furniture_piece(furniture_container, Vector2(100, 100), Vector2(200, 80), "Reception")
|
||||
|
||||
# 工作区 - 办公桌(6个)
|
||||
for i in range(3):
|
||||
for j in range(2):
|
||||
var desk_pos = Vector2(100 + i * 250, 300 + j * 200)
|
||||
_create_furniture_piece(furniture_container, desk_pos, Vector2(120, 60), "Desk")
|
||||
|
||||
# 会议区 - 会议桌
|
||||
_create_furniture_piece(furniture_container, Vector2(900, 300), Vector2(400, 200), "Meeting Table")
|
||||
|
||||
# 休息区 - 沙发
|
||||
_create_furniture_piece(furniture_container, Vector2(100, 900), Vector2(300, 100), "Sofa")
|
||||
_create_furniture_piece(furniture_container, Vector2(100, 1050), Vector2(100, 100), "Coffee Table")
|
||||
|
||||
## 创建单个家具
|
||||
func _create_furniture_piece(parent: Node2D, pos: Vector2, size: Vector2, label: String):
|
||||
"""创建单个家具块"""
|
||||
var furniture = StaticBody2D.new()
|
||||
furniture.name = label
|
||||
furniture.collision_layer = 2
|
||||
furniture.collision_mask = 0
|
||||
|
||||
var visual = ColorRect.new()
|
||||
visual.size = size
|
||||
visual.color = COLOR_FURNITURE
|
||||
furniture.add_child(visual)
|
||||
|
||||
var collision = CollisionShape2D.new()
|
||||
var shape = RectangleShape2D.new()
|
||||
shape.size = size
|
||||
collision.shape = shape
|
||||
collision.position = size / 2
|
||||
furniture.add_child(collision)
|
||||
|
||||
furniture.position = pos
|
||||
parent.add_child(furniture)
|
||||
|
||||
## 创建品牌元素
|
||||
func _create_branding():
|
||||
"""创建 Datawhale 品牌元素"""
|
||||
var branding_container = Node2D.new()
|
||||
branding_container.name = "Branding"
|
||||
branding_container.z_index = 5
|
||||
add_child(branding_container)
|
||||
|
||||
# 1. 主 Logo 展示区
|
||||
_create_main_logo(branding_container, Vector2(1400, 400))
|
||||
|
||||
# 2. 欢迎标识
|
||||
_create_welcome_sign(branding_container, Vector2(100, 50))
|
||||
|
||||
# 3. 成就墙
|
||||
_create_achievement_wall(branding_container, Vector2(1200, 900))
|
||||
|
||||
# 4. 装饰性品牌元素
|
||||
_create_decorative_elements(branding_container)
|
||||
|
||||
# 5. 地板品牌标识
|
||||
_create_floor_branding(branding_container)
|
||||
|
||||
## 创建主 Logo(使用真实图片)
|
||||
func _create_main_logo(parent: Node2D, pos: Vector2):
|
||||
"""创建主 Datawhale Logo(使用真实图片)"""
|
||||
var logo_container = Node2D.new()
|
||||
logo_container.name = "MainLogo"
|
||||
logo_container.position = pos
|
||||
parent.add_child(logo_container)
|
||||
|
||||
# 检查 Logo 文件是否存在
|
||||
if ResourceLoader.exists(LOGO_PATH):
|
||||
# 使用真实的 Logo 图片
|
||||
var logo_sprite = Sprite2D.new()
|
||||
logo_sprite.texture = load(LOGO_PATH)
|
||||
# 调整大小(根据你的 logo 实际尺寸调整这个值)
|
||||
logo_sprite.scale = Vector2(0.4, 0.4)
|
||||
logo_sprite.position = Vector2(0, 0)
|
||||
logo_container.add_child(logo_sprite)
|
||||
|
||||
print("✓ Real Datawhale logo loaded successfully")
|
||||
else:
|
||||
# 如果 logo 文件不存在,使用占位符
|
||||
print("⚠ Logo file not found at: ", LOGO_PATH)
|
||||
print(" Using placeholder graphics instead")
|
||||
|
||||
# Logo 背景板
|
||||
var bg = ColorRect.new()
|
||||
bg.size = Vector2(300, 300)
|
||||
bg.color = Color.WHITE
|
||||
bg.position = Vector2(-150, -150)
|
||||
logo_container.add_child(bg)
|
||||
|
||||
# 占位符图形
|
||||
var placeholder = ColorRect.new()
|
||||
placeholder.size = Vector2(200, 120)
|
||||
placeholder.color = COLOR_PRIMARY
|
||||
placeholder.position = Vector2(-100, -80)
|
||||
logo_container.add_child(placeholder)
|
||||
|
||||
var placeholder_text = Label.new()
|
||||
placeholder_text.text = "LOGO HERE"
|
||||
placeholder_text.position = Vector2(-50, -20)
|
||||
placeholder_text.add_theme_font_size_override("font_size", 24)
|
||||
placeholder_text.add_theme_color_override("font_color", Color.WHITE)
|
||||
logo_container.add_child(placeholder_text)
|
||||
|
||||
# Logo 文字(可选)
|
||||
var logo_text = Label.new()
|
||||
logo_text.text = "DATAWHALE"
|
||||
logo_text.position = Vector2(-100, 150)
|
||||
logo_text.add_theme_font_size_override("font_size", 32)
|
||||
logo_text.add_theme_color_override("font_color", COLOR_PRIMARY)
|
||||
logo_container.add_child(logo_text)
|
||||
|
||||
# 副标题
|
||||
var subtitle = Label.new()
|
||||
subtitle.text = "AI Learning Community"
|
||||
subtitle.position = Vector2(-80, 190)
|
||||
subtitle.add_theme_font_size_override("font_size", 16)
|
||||
subtitle.add_theme_color_override("font_color", COLOR_SECONDARY)
|
||||
logo_container.add_child(subtitle)
|
||||
|
||||
## 创建欢迎标识(带小 logo)
|
||||
func _create_welcome_sign(parent: Node2D, pos: Vector2):
|
||||
"""创建入口欢迎标识"""
|
||||
var sign_container = Node2D.new()
|
||||
sign_container.name = "WelcomeSign"
|
||||
sign_container.position = pos
|
||||
parent.add_child(sign_container)
|
||||
|
||||
# 标识背景
|
||||
var bg = ColorRect.new()
|
||||
bg.size = Vector2(400, 80)
|
||||
bg.color = COLOR_PRIMARY
|
||||
sign_container.add_child(bg)
|
||||
|
||||
# 小 logo 图标(如果有)
|
||||
if ResourceLoader.exists(LOGO_ICON_PATH):
|
||||
var logo_icon = Sprite2D.new()
|
||||
logo_icon.texture = load(LOGO_ICON_PATH)
|
||||
logo_icon.scale = Vector2(0.15, 0.15)
|
||||
logo_icon.position = Vector2(30, 40)
|
||||
sign_container.add_child(logo_icon)
|
||||
|
||||
# 欢迎文字
|
||||
var welcome_text = Label.new()
|
||||
welcome_text.text = "Welcome to Datawhale Office"
|
||||
welcome_text.position = Vector2(70, 20)
|
||||
welcome_text.add_theme_font_size_override("font_size", 24)
|
||||
welcome_text.add_theme_color_override("font_color", Color.WHITE)
|
||||
sign_container.add_child(welcome_text)
|
||||
|
||||
## 创建成就墙
|
||||
func _create_achievement_wall(parent: Node2D, pos: Vector2):
|
||||
"""创建成就展示墙"""
|
||||
var wall_container = Node2D.new()
|
||||
wall_container.name = "AchievementWall"
|
||||
wall_container.position = pos
|
||||
parent.add_child(wall_container)
|
||||
|
||||
# 墙面背景
|
||||
var wall_bg = ColorRect.new()
|
||||
wall_bg.size = Vector2(600, 400)
|
||||
wall_bg.color = Color(0.95, 0.95, 0.95)
|
||||
wall_container.add_child(wall_bg)
|
||||
|
||||
# 标题
|
||||
var title = Label.new()
|
||||
title.text = "Our Achievements"
|
||||
title.position = Vector2(200, 20)
|
||||
title.add_theme_font_size_override("font_size", 28)
|
||||
title.add_theme_color_override("font_color", COLOR_PRIMARY)
|
||||
wall_container.add_child(title)
|
||||
|
||||
# 成就卡片
|
||||
var achievements = [
|
||||
{"title": "10K+ Members", "icon_color": COLOR_PRIMARY},
|
||||
{"title": "500+ Projects", "icon_color": COLOR_SECONDARY},
|
||||
{"title": "100+ Courses", "icon_color": COLOR_ACCENT},
|
||||
{"title": "AI Excellence", "icon_color": COLOR_PRIMARY}
|
||||
]
|
||||
|
||||
for i in range(achievements.size()):
|
||||
var row = i / 2
|
||||
var col = i % 2
|
||||
var card_pos = Vector2(50 + col * 280, 80 + row * 140)
|
||||
_create_achievement_card(wall_container, card_pos, achievements[i])
|
||||
|
||||
## 创建成就卡片
|
||||
func _create_achievement_card(parent: Node2D, pos: Vector2, data: Dictionary):
|
||||
"""创建单个成就卡片"""
|
||||
var card = ColorRect.new()
|
||||
card.size = Vector2(240, 100)
|
||||
card.color = Color.WHITE
|
||||
card.position = pos
|
||||
parent.add_child(card)
|
||||
|
||||
# 图标
|
||||
var icon = ColorRect.new()
|
||||
icon.size = Vector2(60, 60)
|
||||
icon.color = data["icon_color"]
|
||||
icon.position = Vector2(20, 20)
|
||||
card.add_child(icon)
|
||||
|
||||
# 文字
|
||||
var text = Label.new()
|
||||
text.text = data["title"]
|
||||
text.position = Vector2(90, 35)
|
||||
text.add_theme_font_size_override("font_size", 18)
|
||||
text.add_theme_color_override("font_color", COLOR_SECONDARY)
|
||||
card.add_child(text)
|
||||
|
||||
## 创建装饰性元素
|
||||
func _create_decorative_elements(parent: Node2D):
|
||||
"""创建装饰性品牌元素"""
|
||||
# 蓝色装饰条纹
|
||||
for i in range(8):
|
||||
var stripe = ColorRect.new()
|
||||
stripe.size = Vector2(40, 200)
|
||||
stripe.color = COLOR_ACCENT
|
||||
stripe.color.a = 0.2 + (i % 3) * 0.1
|
||||
stripe.position = Vector2(1700 + i * 50, 100 + (i % 2) * 100)
|
||||
parent.add_child(stripe)
|
||||
|
||||
# 品牌色圆点装饰
|
||||
var dot_positions = [
|
||||
Vector2(500, 200), Vector2(700, 250), Vector2(900, 150),
|
||||
Vector2(300, 600), Vector2(600, 650), Vector2(1100, 600)
|
||||
]
|
||||
|
||||
for pos in dot_positions:
|
||||
var dot = ColorRect.new()
|
||||
dot.size = Vector2(30, 30)
|
||||
dot.color = COLOR_PRIMARY
|
||||
dot.color.a = 0.3
|
||||
dot.position = pos
|
||||
parent.add_child(dot)
|
||||
|
||||
## 创建地板品牌标识
|
||||
func _create_floor_branding(parent: Node2D):
|
||||
"""在地板上创建品牌标识"""
|
||||
# 中央大型 Logo 水印
|
||||
var floor_logo = Label.new()
|
||||
floor_logo.text = "DATAWHALE"
|
||||
floor_logo.position = Vector2(800, 600)
|
||||
floor_logo.rotation = -0.1
|
||||
floor_logo.add_theme_font_size_override("font_size", 120)
|
||||
floor_logo.add_theme_color_override("font_color", COLOR_PRIMARY)
|
||||
floor_logo.modulate.a = 0.05
|
||||
floor_logo.z_index = -5
|
||||
parent.add_child(floor_logo)
|
||||
1
scripts/DatawhaleOffice_with_logo.gd.uid
Normal file
1
scripts/DatawhaleOffice_with_logo.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dguq2cbn64jx5
|
||||
210
scripts/DebugCamera.gd
Normal file
210
scripts/DebugCamera.gd
Normal file
@@ -0,0 +1,210 @@
|
||||
extends Camera2D
|
||||
## 调试相机控制
|
||||
## 用于在测试场景时移动相机查看不同区域
|
||||
|
||||
# 相机移动速度
|
||||
var move_speed = 500.0
|
||||
var zoom_speed = 1.2 # 大幅增加缩放速度,让Q/E响应更快
|
||||
|
||||
# 最小和最大缩放
|
||||
var min_zoom = 0.3
|
||||
var max_zoom = 2.0
|
||||
|
||||
# 重置动画相关
|
||||
var is_resetting = false
|
||||
var reset_target_position = Vector2.ZERO
|
||||
var reset_target_zoom = Vector2.ONE
|
||||
var reset_duration = 1.2 # 进一步增加重置动画时间,让动画更慢更优雅
|
||||
|
||||
# 流畅滚轮缩放相关
|
||||
var scroll_velocity = 0.0 # 当前滚轮速度
|
||||
var scroll_acceleration = 0.0 # 滚轮加速度
|
||||
var scroll_friction = 8.0 # 摩擦力,控制减速
|
||||
var scroll_sensitivity = 0.8 # 滚轮敏感度
|
||||
var max_scroll_speed = 3.0 # 最大滚轮速度
|
||||
var zoom_smoothness = 12.0 # 缩放平滑度
|
||||
var current_target_zoom = 1.0 # 当前目标缩放值
|
||||
|
||||
# UI状态检测
|
||||
var ui_focused = false
|
||||
|
||||
func _ready():
|
||||
print("Debug Camera Controls:")
|
||||
print(" WASD / Arrow Keys - Move camera")
|
||||
print(" Mouse Wheel - Zoom in/out (smooth)")
|
||||
print(" R - Reset camera position")
|
||||
|
||||
# 初始化目标缩放值
|
||||
current_target_zoom = zoom.x
|
||||
|
||||
func _process(delta):
|
||||
# 如果正在重置,跳过手动控制
|
||||
if is_resetting:
|
||||
return
|
||||
|
||||
# 检查UI焦点状态
|
||||
_update_ui_focus_status()
|
||||
|
||||
# 处理流畅滚轮缩放
|
||||
_update_smooth_zoom(delta)
|
||||
|
||||
# 相机移动(只有在UI未获得焦点时)
|
||||
var move_direction = Vector2.ZERO
|
||||
|
||||
if not ui_focused:
|
||||
# WASD 或方向键移动
|
||||
if Input.is_action_pressed("ui_right") or Input.is_key_pressed(KEY_D):
|
||||
move_direction.x += 1
|
||||
if Input.is_action_pressed("ui_left") or Input.is_key_pressed(KEY_A):
|
||||
move_direction.x -= 1
|
||||
if Input.is_action_pressed("ui_down") or Input.is_key_pressed(KEY_S):
|
||||
move_direction.y += 1
|
||||
if Input.is_action_pressed("ui_up") or Input.is_key_pressed(KEY_W):
|
||||
move_direction.y -= 1
|
||||
|
||||
# 重置相机
|
||||
if Input.is_action_just_pressed("ui_accept") or Input.is_key_pressed(KEY_R):
|
||||
reset_camera_smooth()
|
||||
|
||||
# 应用移动
|
||||
if move_direction.length() > 0:
|
||||
move_direction = move_direction.normalized()
|
||||
position += move_direction * move_speed * delta
|
||||
|
||||
func _input(event):
|
||||
# 流畅鼠标滚轮缩放
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
||||
_add_scroll_impulse(scroll_sensitivity)
|
||||
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
||||
_add_scroll_impulse(-scroll_sensitivity)
|
||||
|
||||
func zoom_camera(amount: float):
|
||||
"""缩放相机(平滑缩放,避免颠簸)"""
|
||||
var new_zoom = zoom.x + amount
|
||||
new_zoom = clamp(new_zoom, min_zoom, max_zoom)
|
||||
|
||||
# 使用短暂的平滑动画来避免突兀的缩放
|
||||
var tween = create_tween()
|
||||
tween.tween_property(self, "zoom", Vector2(new_zoom, new_zoom), 0.05)
|
||||
tween.set_ease(Tween.EASE_OUT)
|
||||
tween.set_trans(Tween.TRANS_QUART)
|
||||
|
||||
func _add_scroll_impulse(impulse: float):
|
||||
"""添加滚轮冲量,实现流畅缩放"""
|
||||
# 根据滚轮滑动距离调整速度(滑动越远,速度越快)
|
||||
var speed_multiplier = abs(impulse) * 1.5 + 1.0
|
||||
scroll_velocity += impulse * speed_multiplier
|
||||
|
||||
# 限制最大滚轮速度
|
||||
scroll_velocity = clamp(scroll_velocity, -max_scroll_speed, max_scroll_speed)
|
||||
|
||||
func _update_smooth_zoom(delta: float):
|
||||
"""更新流畅缩放效果"""
|
||||
# 如果有滚轮速度,更新目标缩放值
|
||||
if abs(scroll_velocity) > 0.01:
|
||||
# 根据速度更新目标缩放
|
||||
var zoom_change = scroll_velocity * delta * 2.0
|
||||
current_target_zoom += zoom_change
|
||||
current_target_zoom = clamp(current_target_zoom, min_zoom, max_zoom)
|
||||
|
||||
# 应用摩擦力减速
|
||||
scroll_velocity = lerp(scroll_velocity, 0.0, scroll_friction * delta)
|
||||
else:
|
||||
# 速度很小时直接停止
|
||||
scroll_velocity = 0.0
|
||||
|
||||
# 平滑插值到目标缩放值
|
||||
var current_zoom = zoom.x
|
||||
var new_zoom = lerp(current_zoom, current_target_zoom, zoom_smoothness * delta)
|
||||
|
||||
# 只有当缩放值有明显变化时才更新
|
||||
if abs(new_zoom - current_zoom) > 0.001:
|
||||
zoom = Vector2(new_zoom, new_zoom)
|
||||
|
||||
func reset_camera():
|
||||
"""重置相机到初始位置(立即)"""
|
||||
position = Vector2(640, 360)
|
||||
zoom = Vector2(1.0, 1.0)
|
||||
current_target_zoom = 1.0
|
||||
scroll_velocity = 0.0
|
||||
print("Camera reset to default position")
|
||||
|
||||
func reset_camera_smooth():
|
||||
"""平滑重置相机到初始位置"""
|
||||
if is_resetting:
|
||||
return # 如果已经在重置中,忽略
|
||||
|
||||
# 检查是否有跟随的角色
|
||||
var player_character = _find_player_character()
|
||||
if player_character:
|
||||
# 如果有玩家角色,重置到角色位置
|
||||
reset_target_position = player_character.global_position
|
||||
print("Resetting camera to player position: ", reset_target_position)
|
||||
else:
|
||||
# 否则重置到默认位置
|
||||
reset_target_position = Vector2(640, 360)
|
||||
print("Resetting camera to default position: ", reset_target_position)
|
||||
|
||||
reset_target_zoom = Vector2(1.0, 1.0)
|
||||
|
||||
# 创建平滑动画
|
||||
is_resetting = true
|
||||
var tween = create_tween()
|
||||
tween.set_parallel(true) # 允许同时进行位置和缩放动画
|
||||
|
||||
# 位置动画 - 使用更平滑的缓动效果
|
||||
var position_tween = tween.tween_property(self, "position", reset_target_position, reset_duration)
|
||||
position_tween.set_ease(Tween.EASE_OUT) # 开始快,结束慢,更自然
|
||||
position_tween.set_trans(Tween.TRANS_CUBIC) # 使用三次方过渡,更平滑
|
||||
|
||||
# 缩放动画 - 同样使用平滑缓动效果
|
||||
var zoom_tween = tween.tween_property(self, "zoom", reset_target_zoom, reset_duration)
|
||||
zoom_tween.set_ease(Tween.EASE_OUT) # 开始快,结束慢,更自然
|
||||
zoom_tween.set_trans(Tween.TRANS_CUBIC) # 使用三次方过渡,更平滑
|
||||
|
||||
# 动画完成后的回调
|
||||
tween.finished.connect(_on_reset_finished)
|
||||
|
||||
func _on_reset_finished():
|
||||
"""重置动画完成回调"""
|
||||
is_resetting = false
|
||||
current_target_zoom = reset_target_zoom.x
|
||||
scroll_velocity = 0.0
|
||||
print("Camera reset animation completed")
|
||||
|
||||
func _update_ui_focus_status():
|
||||
"""更新UI焦点状态"""
|
||||
ui_focused = _is_ui_focused()
|
||||
|
||||
func _is_ui_focused() -> bool:
|
||||
"""
|
||||
检查是否有UI控件获得焦点(如输入框)
|
||||
@return: 是否有UI控件获得焦点
|
||||
"""
|
||||
var focused_control = get_viewport().gui_get_focus_owner()
|
||||
|
||||
# 如果有控件获得焦点,且是输入类控件,则认为UI处于活动状态
|
||||
if focused_control:
|
||||
return focused_control is LineEdit or focused_control is TextEdit
|
||||
|
||||
return false
|
||||
|
||||
func _find_player_character() -> Node2D:
|
||||
"""查找玩家角色"""
|
||||
# 尝试从场景树中找到玩家角色
|
||||
var main_scene = get_tree().root.get_node_or_null("Main")
|
||||
if main_scene:
|
||||
# 查找 GameWorld 下的角色
|
||||
var game_world = main_scene.get_node_or_null("GameWorld")
|
||||
if game_world:
|
||||
# 查找 DatawhaleOffice 场景
|
||||
var office_scene = game_world.get_child(0) if game_world.get_child_count() > 0 else null
|
||||
if office_scene:
|
||||
# 查找 Characters 容器
|
||||
var characters_container = office_scene.get_node_or_null("Characters")
|
||||
if characters_container and characters_container.get_child_count() > 0:
|
||||
# 返回第一个角色(通常是玩家)
|
||||
return characters_container.get_child(0)
|
||||
|
||||
return null
|
||||
1
scripts/DebugCamera.gd.uid
Normal file
1
scripts/DebugCamera.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://8i0rt2thwpkb
|
||||
262
scripts/DialogueBox.gd
Normal file
262
scripts/DialogueBox.gd
Normal file
@@ -0,0 +1,262 @@
|
||||
extends Control
|
||||
class_name DialogueBox
|
||||
## 对话框 UI
|
||||
## 显示对话消息和输入界面
|
||||
|
||||
# UI 元素
|
||||
var message_display: RichTextLabel
|
||||
var message_input: LineEdit
|
||||
var send_button: Button
|
||||
var close_button: Button
|
||||
var target_label: Label
|
||||
var container: VBoxContainer
|
||||
var scroll_container: ScrollContainer
|
||||
|
||||
# 状态
|
||||
var current_target_name: String = ""
|
||||
|
||||
# 信号
|
||||
signal message_sent(message: String)
|
||||
signal dialogue_closed()
|
||||
|
||||
func _ready():
|
||||
"""初始化对话框"""
|
||||
_create_ui()
|
||||
hide()
|
||||
|
||||
func _process(_delta):
|
||||
"""每帧检查焦点状态"""
|
||||
# 简化焦点管理,避免干扰按钮点击
|
||||
pass
|
||||
|
||||
func _input(event):
|
||||
"""处理对话框的输入事件"""
|
||||
# 只有在对话框可见时才处理输入
|
||||
if not visible:
|
||||
return
|
||||
|
||||
# ESC键关闭对话框
|
||||
if event is InputEventKey and event.pressed:
|
||||
if event.keycode == KEY_ESCAPE:
|
||||
_on_close_pressed()
|
||||
get_viewport().set_input_as_handled() # 阻止事件继续传播
|
||||
return
|
||||
|
||||
# 不要处理鼠标事件,让按钮自己处理
|
||||
if event is InputEventMouseButton:
|
||||
return
|
||||
|
||||
## 创建 UI 元素
|
||||
func _create_ui():
|
||||
"""创建对话框的所有 UI 元素"""
|
||||
# 设置为居中显示
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -250
|
||||
offset_top = -200
|
||||
offset_right = 250
|
||||
offset_bottom = 200
|
||||
|
||||
# 创建背景
|
||||
var panel = Panel.new()
|
||||
panel.anchor_right = 1.0
|
||||
panel.anchor_bottom = 1.0
|
||||
add_child(panel)
|
||||
|
||||
# 创建主容器
|
||||
container = VBoxContainer.new()
|
||||
container.anchor_right = 1.0
|
||||
container.anchor_bottom = 1.0
|
||||
container.offset_left = 10
|
||||
container.offset_top = 10
|
||||
container.offset_right = -10
|
||||
container.offset_bottom = -10
|
||||
add_child(container)
|
||||
|
||||
# 标题栏
|
||||
var title_bar = HBoxContainer.new()
|
||||
container.add_child(title_bar)
|
||||
|
||||
# 对话目标标签
|
||||
target_label = Label.new()
|
||||
target_label.text = "对话"
|
||||
target_label.add_theme_font_size_override("font_size", 18)
|
||||
target_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
title_bar.add_child(target_label)
|
||||
|
||||
# 关闭按钮
|
||||
close_button = Button.new()
|
||||
close_button.text = "X"
|
||||
close_button.custom_minimum_size = Vector2(30, 30)
|
||||
close_button.mouse_filter = Control.MOUSE_FILTER_STOP # 确保按钮能接收鼠标事件
|
||||
close_button.focus_mode = Control.FOCUS_ALL # 确保按钮可以获得焦点
|
||||
close_button.pressed.connect(_on_close_pressed)
|
||||
title_bar.add_child(close_button)
|
||||
|
||||
# 间距
|
||||
container.add_child(_create_spacer(10))
|
||||
|
||||
# 消息显示区域(带滚动)
|
||||
scroll_container = ScrollContainer.new()
|
||||
scroll_container.custom_minimum_size = Vector2(0, 250)
|
||||
scroll_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
container.add_child(scroll_container)
|
||||
|
||||
message_display = RichTextLabel.new()
|
||||
message_display.bbcode_enabled = true
|
||||
message_display.scroll_following = true
|
||||
message_display.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
message_display.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
scroll_container.add_child(message_display)
|
||||
|
||||
# 间距
|
||||
container.add_child(_create_spacer(10))
|
||||
|
||||
# 输入区域
|
||||
var input_container = HBoxContainer.new()
|
||||
container.add_child(input_container)
|
||||
|
||||
# 消息输入框
|
||||
message_input = LineEdit.new()
|
||||
message_input.placeholder_text = "输入消息..."
|
||||
message_input.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
message_input.max_length = 500
|
||||
message_input.text_submitted.connect(_on_message_submitted)
|
||||
# 连接焦点丢失信号,自动重新获取焦点
|
||||
message_input.focus_exited.connect(_on_input_focus_exited)
|
||||
input_container.add_child(message_input)
|
||||
|
||||
# 发送按钮
|
||||
send_button = Button.new()
|
||||
send_button.text = "发送"
|
||||
send_button.custom_minimum_size = Vector2(80, 0)
|
||||
send_button.mouse_filter = Control.MOUSE_FILTER_STOP # 确保按钮能接收鼠标事件
|
||||
send_button.focus_mode = Control.FOCUS_ALL # 确保按钮可以获得焦点
|
||||
send_button.pressed.connect(_on_send_pressed)
|
||||
input_container.add_child(send_button)
|
||||
|
||||
print("Send button created and connected")
|
||||
|
||||
## 创建间距
|
||||
func _create_spacer(height: float) -> Control:
|
||||
"""创建垂直间距"""
|
||||
var spacer = Control.new()
|
||||
spacer.custom_minimum_size = Vector2(0, height)
|
||||
return spacer
|
||||
|
||||
## 开始对话
|
||||
func start_dialogue(target_name: String):
|
||||
"""
|
||||
开始对话并显示对话框
|
||||
@param target_name: 对话目标的名称
|
||||
"""
|
||||
current_target_name = target_name
|
||||
target_label.text = "与 " + target_name + " 对话"
|
||||
message_display.clear()
|
||||
message_input.clear()
|
||||
|
||||
show()
|
||||
# 延迟获取焦点,确保对话框完全显示后再获取
|
||||
call_deferred("_ensure_input_focus")
|
||||
|
||||
## 添加消息到显示区域
|
||||
func add_message(sender: String, message: String):
|
||||
"""
|
||||
添加消息到对话显示区域
|
||||
@param sender: 发送者名称
|
||||
@param message: 消息内容
|
||||
"""
|
||||
var timestamp = Time.get_time_string_from_system()
|
||||
var color = "[color=cyan]" if sender == "你" else "[color=yellow]"
|
||||
var formatted_message = "%s[%s] %s:[/color] %s\n" % [color, timestamp, sender, message]
|
||||
|
||||
# 使用call_deferred确保UI更新不会被阻塞
|
||||
message_display.call_deferred("append_text", formatted_message)
|
||||
|
||||
# 延迟滚动到底部,确保文本已添加
|
||||
call_deferred("_scroll_to_bottom")
|
||||
|
||||
## 关闭对话框
|
||||
func close_dialogue():
|
||||
"""关闭对话框"""
|
||||
hide()
|
||||
message_input.clear()
|
||||
current_target_name = ""
|
||||
|
||||
## 发送按钮点击
|
||||
func _on_send_pressed():
|
||||
"""发送按钮被点击"""
|
||||
print("=== SEND BUTTON PRESSED ===")
|
||||
|
||||
if not message_input:
|
||||
print("ERROR: message_input is null")
|
||||
return
|
||||
|
||||
var message = message_input.text.strip_edges()
|
||||
print("Message text: '", message, "'")
|
||||
|
||||
if message.is_empty():
|
||||
print("Empty message, focusing input")
|
||||
_ensure_input_focus()
|
||||
return
|
||||
|
||||
print("Processing message: ", message)
|
||||
|
||||
# 立即显示玩家消息
|
||||
add_message("你", message)
|
||||
|
||||
# 发射信号给对话系统处理
|
||||
print("Emitting message_sent signal")
|
||||
message_sent.emit(message)
|
||||
|
||||
# 清空输入框并重新获取焦点
|
||||
message_input.clear()
|
||||
call_deferred("_ensure_input_focus")
|
||||
|
||||
print("=== SEND BUTTON PROCESSING COMPLETE ===")
|
||||
|
||||
## 关闭按钮点击
|
||||
func _on_close_pressed():
|
||||
"""关闭按钮被点击"""
|
||||
print("=== CLOSE BUTTON PRESSED ===")
|
||||
dialogue_closed.emit()
|
||||
close_dialogue()
|
||||
print("=== CLOSE BUTTON PROCESSING COMPLETE ===")
|
||||
|
||||
## 消息输入框回车
|
||||
func _on_message_submitted(_text: String):
|
||||
"""消息输入框按下回车"""
|
||||
print("Enter key pressed") # 调试日志
|
||||
_on_send_pressed()
|
||||
|
||||
## 滚动到底部
|
||||
func _scroll_to_bottom():
|
||||
"""滚动消息显示区域到底部"""
|
||||
if scroll_container and scroll_container.get_v_scroll_bar():
|
||||
var v_scroll = scroll_container.get_v_scroll_bar()
|
||||
v_scroll.value = v_scroll.max_value
|
||||
|
||||
|
||||
|
||||
## 确保输入框焦点
|
||||
func _ensure_input_focus():
|
||||
"""确保输入框获得并保持焦点"""
|
||||
if message_input and visible:
|
||||
# 使用call_deferred确保在UI更新完成后获取焦点
|
||||
message_input.call_deferred("grab_focus")
|
||||
|
||||
## 输入框焦点丢失处理
|
||||
func _on_input_focus_exited():
|
||||
"""当输入框失去焦点时的处理"""
|
||||
# 简化焦点管理,避免干扰按钮操作
|
||||
# 只在特定情况下重新获取焦点
|
||||
if visible and message_input:
|
||||
# 延迟检查,给按钮操作时间完成
|
||||
await get_tree().create_timer(0.1).timeout
|
||||
if visible and message_input:
|
||||
var focused_control = get_viewport().gui_get_focus_owner()
|
||||
# 只有当没有任何控件获得焦点时才重新获取
|
||||
if not focused_control:
|
||||
message_input.grab_focus()
|
||||
1
scripts/DialogueBox.gd.uid
Normal file
1
scripts/DialogueBox.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cdn1q2kkqnknj
|
||||
448
scripts/DialogueFilter.gd
Normal file
448
scripts/DialogueFilter.gd
Normal file
@@ -0,0 +1,448 @@
|
||||
extends Node
|
||||
class_name DialogueFilter
|
||||
## 对话过滤器
|
||||
## 提供内容审核、过滤和安全检查功能
|
||||
|
||||
# 过滤规则配置
|
||||
var enable_profanity_filter: bool = true
|
||||
var enable_spam_detection: bool = true
|
||||
var enable_length_limit: bool = true
|
||||
var enable_rate_limiting: bool = true
|
||||
|
||||
# 长度限制
|
||||
var max_message_length: int = 500
|
||||
var min_message_length: int = 1
|
||||
|
||||
# 垃圾信息检测
|
||||
var spam_detection_window: float = 10.0 # 10秒窗口
|
||||
var max_messages_per_window: int = 5
|
||||
var max_duplicate_messages: int = 3
|
||||
|
||||
# 违禁词列表(示例,实际使用时应该从配置文件加载)
|
||||
var profanity_words: Array[String] = [
|
||||
# 基础违禁词(示例)
|
||||
"垃圾", "废物", "白痴", "蠢货", "混蛋",
|
||||
# 可以根据需要添加更多
|
||||
]
|
||||
|
||||
# 敏感词替换
|
||||
var profanity_replacement: String = "***"
|
||||
|
||||
# 消息历史(用于垃圾信息检测)
|
||||
var message_history: Dictionary = {} # user_id -> Array[Dictionary]
|
||||
|
||||
# 过滤统计
|
||||
var filter_stats: Dictionary = {
|
||||
"total_messages": 0,
|
||||
"filtered_messages": 0,
|
||||
"profanity_blocked": 0,
|
||||
"spam_blocked": 0,
|
||||
"length_violations": 0,
|
||||
"rate_limit_violations": 0
|
||||
}
|
||||
|
||||
# 信号
|
||||
signal message_filtered(user_id: String, original_message: String, filtered_message: String, reason: String)
|
||||
signal message_blocked(user_id: String, message: String, reason: String)
|
||||
|
||||
func _ready():
|
||||
"""初始化对话过滤器"""
|
||||
load_filter_config()
|
||||
print("DialogueFilter initialized")
|
||||
|
||||
## 过滤消息
|
||||
func filter_message(user_id: String, message: String) -> Dictionary:
|
||||
"""
|
||||
过滤和验证消息
|
||||
@param user_id: 用户ID
|
||||
@param message: 原始消息
|
||||
@return: 过滤结果 {allowed: bool, filtered_message: String, reason: String}
|
||||
"""
|
||||
filter_stats.total_messages += 1
|
||||
|
||||
var result = {
|
||||
"allowed": true,
|
||||
"filtered_message": message,
|
||||
"reason": ""
|
||||
}
|
||||
|
||||
# 1. 长度检查
|
||||
if enable_length_limit:
|
||||
var length_check = _check_message_length(message)
|
||||
if not length_check.valid:
|
||||
result.allowed = false
|
||||
result.reason = length_check.reason
|
||||
filter_stats.length_violations += 1
|
||||
message_blocked.emit(user_id, message, result.reason)
|
||||
return result
|
||||
|
||||
# 2. 速率限制检查
|
||||
if enable_rate_limiting:
|
||||
var rate_check = _check_rate_limit(user_id, message)
|
||||
if not rate_check.valid:
|
||||
result.allowed = false
|
||||
result.reason = rate_check.reason
|
||||
filter_stats.rate_limit_violations += 1
|
||||
message_blocked.emit(user_id, message, result.reason)
|
||||
return result
|
||||
|
||||
# 3. 垃圾信息检测
|
||||
if enable_spam_detection:
|
||||
var spam_check = _check_spam(user_id, message)
|
||||
if not spam_check.valid:
|
||||
result.allowed = false
|
||||
result.reason = spam_check.reason
|
||||
filter_stats.spam_blocked += 1
|
||||
message_blocked.emit(user_id, message, result.reason)
|
||||
return result
|
||||
|
||||
# 4. 违禁词过滤
|
||||
if enable_profanity_filter:
|
||||
var profanity_result = _filter_profanity(message)
|
||||
if profanity_result.has_profanity:
|
||||
result.filtered_message = profanity_result.filtered_message
|
||||
filter_stats.profanity_blocked += 1
|
||||
message_filtered.emit(user_id, message, result.filtered_message, "违禁词过滤")
|
||||
|
||||
# 5. 记录消息历史(用于垃圾信息检测)
|
||||
_record_message(user_id, result.filtered_message)
|
||||
|
||||
if result.filtered_message != message:
|
||||
filter_stats.filtered_messages += 1
|
||||
|
||||
return result
|
||||
|
||||
## 检查消息长度
|
||||
func _check_message_length(message: String) -> Dictionary:
|
||||
"""
|
||||
检查消息长度是否符合要求
|
||||
@param message: 消息内容
|
||||
@return: 验证结果
|
||||
"""
|
||||
var trimmed = message.strip_edges()
|
||||
|
||||
if trimmed.length() < min_message_length:
|
||||
return {
|
||||
"valid": false,
|
||||
"reason": "消息不能为空"
|
||||
}
|
||||
|
||||
if trimmed.length() > max_message_length:
|
||||
return {
|
||||
"valid": false,
|
||||
"reason": "消息长度超过限制(最多%d个字符)" % max_message_length
|
||||
}
|
||||
|
||||
return {"valid": true, "reason": ""}
|
||||
|
||||
## 检查速率限制
|
||||
func _check_rate_limit(user_id: String, _message: String) -> Dictionary:
|
||||
"""
|
||||
检查用户是否超过消息发送速率限制
|
||||
@param user_id: 用户ID
|
||||
@param _message: 消息内容(暂未使用)
|
||||
@return: 验证结果
|
||||
"""
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
|
||||
if not message_history.has(user_id):
|
||||
return {"valid": true, "reason": ""}
|
||||
|
||||
var user_messages = message_history[user_id]
|
||||
var recent_messages = []
|
||||
|
||||
# 统计时间窗口内的消息
|
||||
for msg_record in user_messages:
|
||||
if current_time - msg_record.timestamp <= spam_detection_window:
|
||||
recent_messages.append(msg_record)
|
||||
|
||||
if recent_messages.size() >= max_messages_per_window:
|
||||
return {
|
||||
"valid": false,
|
||||
"reason": "发送消息过于频繁,请稍后再试"
|
||||
}
|
||||
|
||||
return {"valid": true, "reason": ""}
|
||||
|
||||
## 检查垃圾信息
|
||||
func _check_spam(user_id: String, message: String) -> Dictionary:
|
||||
"""
|
||||
检查是否为垃圾信息
|
||||
@param user_id: 用户ID
|
||||
@param message: 消息内容
|
||||
@return: 验证结果
|
||||
"""
|
||||
if not message_history.has(user_id):
|
||||
return {"valid": true, "reason": ""}
|
||||
|
||||
var user_messages = message_history[user_id]
|
||||
var duplicate_count = 0
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
|
||||
# 检查重复消息
|
||||
for msg_record in user_messages:
|
||||
# 只检查最近的消息
|
||||
if current_time - msg_record.timestamp <= spam_detection_window:
|
||||
if msg_record.message.to_lower() == message.to_lower():
|
||||
duplicate_count += 1
|
||||
|
||||
if duplicate_count >= max_duplicate_messages:
|
||||
return {
|
||||
"valid": false,
|
||||
"reason": "请不要重复发送相同的消息"
|
||||
}
|
||||
|
||||
# 检查是否全是重复字符
|
||||
if _is_repetitive_text(message):
|
||||
return {
|
||||
"valid": false,
|
||||
"reason": "请发送有意义的消息内容"
|
||||
}
|
||||
|
||||
# 检查是否全是大写字母(可能是刷屏)
|
||||
if message.length() > 10 and message == message.to_upper():
|
||||
return {
|
||||
"valid": false,
|
||||
"reason": "请不要使用全大写字母"
|
||||
}
|
||||
|
||||
return {"valid": true, "reason": ""}
|
||||
|
||||
## 过滤违禁词
|
||||
func _filter_profanity(message: String) -> Dictionary:
|
||||
"""
|
||||
过滤消息中的违禁词
|
||||
@param message: 原始消息
|
||||
@return: 过滤结果
|
||||
"""
|
||||
var filtered_message = message
|
||||
var has_profanity = false
|
||||
|
||||
for word in profanity_words:
|
||||
if filtered_message.to_lower().contains(word.to_lower()):
|
||||
# 替换违禁词
|
||||
var regex = RegEx.new()
|
||||
regex.compile("(?i)" + word) # 不区分大小写
|
||||
filtered_message = regex.sub(filtered_message, profanity_replacement, true)
|
||||
has_profanity = true
|
||||
|
||||
return {
|
||||
"has_profanity": has_profanity,
|
||||
"filtered_message": filtered_message
|
||||
}
|
||||
|
||||
## 检查是否为重复字符文本
|
||||
func _is_repetitive_text(text: String) -> bool:
|
||||
"""
|
||||
检查文本是否主要由重复字符组成
|
||||
@param text: 输入文本
|
||||
@return: 是否为重复字符文本
|
||||
"""
|
||||
if text.length() < 5:
|
||||
return false
|
||||
|
||||
var char_counts = {}
|
||||
for character in text:
|
||||
char_counts[character] = char_counts.get(character, 0) + 1
|
||||
|
||||
# 如果任何字符占比超过70%,认为是重复文本
|
||||
var threshold = text.length() * 0.7
|
||||
for count in char_counts.values():
|
||||
if count > threshold:
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
## 记录消息历史
|
||||
func _record_message(user_id: String, message: String) -> void:
|
||||
"""
|
||||
记录用户消息历史(用于垃圾信息检测)
|
||||
@param user_id: 用户ID
|
||||
@param message: 消息内容
|
||||
"""
|
||||
if not message_history.has(user_id):
|
||||
message_history[user_id] = []
|
||||
|
||||
var user_messages = message_history[user_id]
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
|
||||
# 添加新消息
|
||||
user_messages.append({
|
||||
"message": message,
|
||||
"timestamp": current_time
|
||||
})
|
||||
|
||||
# 清理过期消息(保留最近1小时的消息)
|
||||
var one_hour_ago = current_time - 3600
|
||||
var filtered_messages = []
|
||||
for msg_record in user_messages:
|
||||
if msg_record.timestamp > one_hour_ago:
|
||||
filtered_messages.append(msg_record)
|
||||
|
||||
message_history[user_id] = filtered_messages
|
||||
|
||||
## 添加违禁词
|
||||
func add_profanity_word(word: String) -> void:
|
||||
"""
|
||||
添加违禁词到过滤列表
|
||||
@param word: 违禁词
|
||||
"""
|
||||
var clean_word = word.strip_edges().to_lower()
|
||||
if not clean_word.is_empty() and not clean_word in profanity_words:
|
||||
profanity_words.append(clean_word)
|
||||
save_filter_config()
|
||||
|
||||
## 移除违禁词
|
||||
func remove_profanity_word(word: String) -> void:
|
||||
"""
|
||||
从过滤列表中移除违禁词
|
||||
@param word: 违禁词
|
||||
"""
|
||||
var clean_word = word.strip_edges().to_lower()
|
||||
if clean_word in profanity_words:
|
||||
profanity_words.erase(clean_word)
|
||||
save_filter_config()
|
||||
|
||||
## 设置过滤配置
|
||||
func set_filter_config(config: Dictionary) -> void:
|
||||
"""
|
||||
设置过滤器配置
|
||||
@param config: 配置字典
|
||||
"""
|
||||
if config.has("enable_profanity_filter"):
|
||||
enable_profanity_filter = config.enable_profanity_filter
|
||||
|
||||
if config.has("enable_spam_detection"):
|
||||
enable_spam_detection = config.enable_spam_detection
|
||||
|
||||
if config.has("enable_length_limit"):
|
||||
enable_length_limit = config.enable_length_limit
|
||||
|
||||
if config.has("enable_rate_limiting"):
|
||||
enable_rate_limiting = config.enable_rate_limiting
|
||||
|
||||
if config.has("max_message_length"):
|
||||
max_message_length = config.max_message_length
|
||||
|
||||
if config.has("max_messages_per_window"):
|
||||
max_messages_per_window = config.max_messages_per_window
|
||||
|
||||
save_filter_config()
|
||||
|
||||
## 获取过滤配置
|
||||
func get_filter_config() -> Dictionary:
|
||||
"""
|
||||
获取当前过滤器配置
|
||||
@return: 配置字典
|
||||
"""
|
||||
return {
|
||||
"enable_profanity_filter": enable_profanity_filter,
|
||||
"enable_spam_detection": enable_spam_detection,
|
||||
"enable_length_limit": enable_length_limit,
|
||||
"enable_rate_limiting": enable_rate_limiting,
|
||||
"max_message_length": max_message_length,
|
||||
"min_message_length": min_message_length,
|
||||
"max_messages_per_window": max_messages_per_window,
|
||||
"spam_detection_window": spam_detection_window,
|
||||
"profanity_words_count": profanity_words.size()
|
||||
}
|
||||
|
||||
## 获取过滤统计
|
||||
func get_filter_statistics() -> Dictionary:
|
||||
"""
|
||||
获取过滤统计信息
|
||||
@return: 统计信息字典
|
||||
"""
|
||||
var stats = filter_stats.duplicate()
|
||||
|
||||
if stats.total_messages > 0:
|
||||
stats["filter_rate"] = float(stats.filtered_messages) / float(stats.total_messages)
|
||||
stats["block_rate"] = float(stats.profanity_blocked + stats.spam_blocked + stats.length_violations + stats.rate_limit_violations) / float(stats.total_messages)
|
||||
else:
|
||||
stats["filter_rate"] = 0.0
|
||||
stats["block_rate"] = 0.0
|
||||
|
||||
return stats
|
||||
|
||||
## 重置统计信息
|
||||
func reset_statistics() -> void:
|
||||
"""重置过滤统计信息"""
|
||||
filter_stats = {
|
||||
"total_messages": 0,
|
||||
"filtered_messages": 0,
|
||||
"profanity_blocked": 0,
|
||||
"spam_blocked": 0,
|
||||
"length_violations": 0,
|
||||
"rate_limit_violations": 0
|
||||
}
|
||||
|
||||
## 清理用户历史
|
||||
func clear_user_history(user_id: String) -> void:
|
||||
"""
|
||||
清理指定用户的消息历史
|
||||
@param user_id: 用户ID
|
||||
"""
|
||||
if message_history.has(user_id):
|
||||
message_history.erase(user_id)
|
||||
|
||||
## 保存过滤器配置
|
||||
func save_filter_config() -> void:
|
||||
"""保存过滤器配置到本地文件"""
|
||||
var config = {
|
||||
"filter_settings": get_filter_config(),
|
||||
"profanity_words": profanity_words
|
||||
}
|
||||
|
||||
var file = FileAccess.open("user://dialogue_filter_config.json", FileAccess.WRITE)
|
||||
if file:
|
||||
var json_string = JSON.stringify(config)
|
||||
file.store_string(json_string)
|
||||
file.close()
|
||||
print("Filter config saved")
|
||||
|
||||
## 加载过滤器配置
|
||||
func load_filter_config() -> void:
|
||||
"""从本地文件加载过滤器配置"""
|
||||
if not FileAccess.file_exists("user://dialogue_filter_config.json"):
|
||||
print("No filter config found, using defaults")
|
||||
return
|
||||
|
||||
var file = FileAccess.open("user://dialogue_filter_config.json", FileAccess.READ)
|
||||
if file:
|
||||
var json_string = file.get_as_text()
|
||||
file.close()
|
||||
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(json_string)
|
||||
|
||||
if parse_result == OK:
|
||||
var config = json.data
|
||||
|
||||
if config.has("filter_settings"):
|
||||
set_filter_config(config.filter_settings)
|
||||
|
||||
if config.has("profanity_words") and config.profanity_words is Array:
|
||||
profanity_words = config.profanity_words
|
||||
|
||||
print("Filter config loaded")
|
||||
else:
|
||||
print("Failed to parse filter config")
|
||||
|
||||
## 定期清理过期数据
|
||||
func _on_cleanup_timer():
|
||||
"""定期清理过期的消息历史数据"""
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
var one_hour_ago = current_time - 3600
|
||||
|
||||
for user_id in message_history.keys():
|
||||
var user_messages = message_history[user_id]
|
||||
var filtered_messages = []
|
||||
|
||||
for msg_record in user_messages:
|
||||
if msg_record.timestamp > one_hour_ago:
|
||||
filtered_messages.append(msg_record)
|
||||
|
||||
if filtered_messages.is_empty():
|
||||
message_history.erase(user_id)
|
||||
else:
|
||||
message_history[user_id] = filtered_messages
|
||||
1
scripts/DialogueFilter.gd.uid
Normal file
1
scripts/DialogueFilter.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://y3kno87ni5aa
|
||||
258
scripts/DialogueHistoryManager.gd
Normal file
258
scripts/DialogueHistoryManager.gd
Normal file
@@ -0,0 +1,258 @@
|
||||
extends Node
|
||||
class_name DialogueHistoryManager
|
||||
## 对话历史管理器
|
||||
## 负责保存、加载和管理对话历史记录
|
||||
|
||||
# 历史记录存储
|
||||
var dialogue_histories: Dictionary = {} # character_id -> Array[Dictionary]
|
||||
var max_history_per_character: int = 100
|
||||
var history_file_path: String = "user://dialogue_history.json"
|
||||
|
||||
# 信号
|
||||
signal history_loaded()
|
||||
signal history_saved()
|
||||
|
||||
func _ready():
|
||||
"""初始化对话历史管理器"""
|
||||
load_history()
|
||||
print("DialogueHistoryManager initialized")
|
||||
|
||||
## 添加消息到历史记录
|
||||
func add_message_to_history(character_id: String, sender: String, message: String, timestamp: float = 0.0) -> void:
|
||||
"""
|
||||
添加消息到指定角色的对话历史
|
||||
@param character_id: 角色ID
|
||||
@param sender: 发送者("player" 或角色ID)
|
||||
@param message: 消息内容
|
||||
@param timestamp: 时间戳(0表示使用当前时间)
|
||||
"""
|
||||
if timestamp <= 0:
|
||||
timestamp = Time.get_unix_time_from_system()
|
||||
|
||||
# 确保角色历史记录存在
|
||||
if not dialogue_histories.has(character_id):
|
||||
dialogue_histories[character_id] = []
|
||||
|
||||
var history = dialogue_histories[character_id]
|
||||
|
||||
# 创建消息记录
|
||||
var message_record = {
|
||||
"sender": sender,
|
||||
"message": message,
|
||||
"timestamp": timestamp,
|
||||
"id": generate_message_id()
|
||||
}
|
||||
|
||||
# 添加到历史记录
|
||||
history.append(message_record)
|
||||
|
||||
# 限制历史记录长度
|
||||
if history.size() > max_history_per_character:
|
||||
history.pop_front()
|
||||
|
||||
# 自动保存(异步)
|
||||
call_deferred("save_history")
|
||||
|
||||
## 获取角色的对话历史
|
||||
func get_character_history(character_id: String, limit: int = 0) -> Array[Dictionary]:
|
||||
"""
|
||||
获取指定角色的对话历史
|
||||
@param character_id: 角色ID
|
||||
@param limit: 限制返回的消息数量(0表示返回全部)
|
||||
@return: 消息历史数组
|
||||
"""
|
||||
if not dialogue_histories.has(character_id):
|
||||
return []
|
||||
|
||||
var history = dialogue_histories[character_id]
|
||||
|
||||
if limit <= 0 or limit >= history.size():
|
||||
return history.duplicate()
|
||||
|
||||
# 返回最近的消息
|
||||
return history.slice(history.size() - limit, history.size())
|
||||
|
||||
## 获取最近的对话记录
|
||||
func get_recent_conversations(limit: int = 10) -> Array[Dictionary]:
|
||||
"""
|
||||
获取最近的对话记录(按时间排序)
|
||||
@param limit: 限制返回的对话数量
|
||||
@return: 最近对话的摘要数组
|
||||
"""
|
||||
var recent_conversations = []
|
||||
|
||||
for character_id in dialogue_histories:
|
||||
var history = dialogue_histories[character_id]
|
||||
if history.size() > 0:
|
||||
var last_message = history[-1] # 最后一条消息
|
||||
recent_conversations.append({
|
||||
"character_id": character_id,
|
||||
"last_message": last_message.message,
|
||||
"last_sender": last_message.sender,
|
||||
"timestamp": last_message.timestamp,
|
||||
"message_count": history.size()
|
||||
})
|
||||
|
||||
# 按时间戳排序(最新的在前)
|
||||
recent_conversations.sort_custom(func(a, b): return a.timestamp > b.timestamp)
|
||||
|
||||
# 限制数量
|
||||
if limit > 0 and recent_conversations.size() > limit:
|
||||
recent_conversations = recent_conversations.slice(0, limit)
|
||||
|
||||
return recent_conversations
|
||||
|
||||
## 搜索对话历史
|
||||
func search_messages(query: String, character_id: String = "") -> Array[Dictionary]:
|
||||
"""
|
||||
在对话历史中搜索消息
|
||||
@param query: 搜索关键词
|
||||
@param character_id: 指定角色ID(空字符串表示搜索所有角色)
|
||||
@return: 匹配的消息数组
|
||||
"""
|
||||
var results = []
|
||||
var search_query = query.to_lower()
|
||||
|
||||
var characters_to_search = []
|
||||
if character_id.is_empty():
|
||||
characters_to_search = dialogue_histories.keys()
|
||||
else:
|
||||
characters_to_search = [character_id]
|
||||
|
||||
for char_id in characters_to_search:
|
||||
if not dialogue_histories.has(char_id):
|
||||
continue
|
||||
|
||||
var history = dialogue_histories[char_id]
|
||||
for message_record in history:
|
||||
if message_record.message.to_lower().contains(search_query):
|
||||
var result = message_record.duplicate()
|
||||
result["character_id"] = char_id
|
||||
results.append(result)
|
||||
|
||||
# 按时间戳排序(最新的在前)
|
||||
results.sort_custom(func(a, b): return a.timestamp > b.timestamp)
|
||||
|
||||
return results
|
||||
|
||||
## 清除角色的对话历史
|
||||
func clear_character_history(character_id: String) -> void:
|
||||
"""
|
||||
清除指定角色的对话历史
|
||||
@param character_id: 角色ID
|
||||
"""
|
||||
if dialogue_histories.has(character_id):
|
||||
dialogue_histories.erase(character_id)
|
||||
save_history()
|
||||
|
||||
## 清除所有对话历史
|
||||
func clear_all_history() -> void:
|
||||
"""清除所有对话历史"""
|
||||
dialogue_histories.clear()
|
||||
save_history()
|
||||
|
||||
## 保存历史记录到文件
|
||||
func save_history() -> void:
|
||||
"""保存对话历史到本地文件"""
|
||||
var file = FileAccess.open(history_file_path, FileAccess.WRITE)
|
||||
if file:
|
||||
var json_string = JSON.stringify(dialogue_histories)
|
||||
file.store_string(json_string)
|
||||
file.close()
|
||||
history_saved.emit()
|
||||
print("Dialogue history saved")
|
||||
else:
|
||||
print("Failed to save dialogue history")
|
||||
|
||||
## 从文件加载历史记录
|
||||
func load_history() -> void:
|
||||
"""从本地文件加载对话历史"""
|
||||
if not FileAccess.file_exists(history_file_path):
|
||||
print("No dialogue history file found, starting fresh")
|
||||
return
|
||||
|
||||
var file = FileAccess.open(history_file_path, FileAccess.READ)
|
||||
if file:
|
||||
var json_string = file.get_as_text()
|
||||
file.close()
|
||||
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(json_string)
|
||||
|
||||
if parse_result == OK:
|
||||
dialogue_histories = json.data
|
||||
history_loaded.emit()
|
||||
print("Dialogue history loaded: ", dialogue_histories.size(), " characters")
|
||||
else:
|
||||
print("Failed to parse dialogue history JSON")
|
||||
else:
|
||||
print("Failed to open dialogue history file")
|
||||
|
||||
## 生成消息ID
|
||||
func generate_message_id() -> String:
|
||||
"""生成唯一的消息ID"""
|
||||
var timestamp = Time.get_unix_time_from_system()
|
||||
var random = randi()
|
||||
return "msg_%d_%d" % [timestamp, random]
|
||||
|
||||
## 获取统计信息
|
||||
func get_statistics() -> Dictionary:
|
||||
"""
|
||||
获取对话历史统计信息
|
||||
@return: 统计信息字典
|
||||
"""
|
||||
var total_messages = 0
|
||||
var total_characters = dialogue_histories.size()
|
||||
var oldest_message_time = 0.0
|
||||
var newest_message_time = 0.0
|
||||
|
||||
for character_id in dialogue_histories:
|
||||
var history = dialogue_histories[character_id]
|
||||
total_messages += history.size()
|
||||
|
||||
if history.size() > 0:
|
||||
var first_msg_time = history[0].timestamp
|
||||
var last_msg_time = history[-1].timestamp
|
||||
|
||||
if oldest_message_time == 0.0 or first_msg_time < oldest_message_time:
|
||||
oldest_message_time = first_msg_time
|
||||
|
||||
if newest_message_time == 0.0 or last_msg_time > newest_message_time:
|
||||
newest_message_time = last_msg_time
|
||||
|
||||
return {
|
||||
"total_messages": total_messages,
|
||||
"total_characters": total_characters,
|
||||
"oldest_message_time": oldest_message_time,
|
||||
"newest_message_time": newest_message_time,
|
||||
"file_path": history_file_path
|
||||
}
|
||||
|
||||
## 导出历史记录
|
||||
func export_history(export_path: String, character_id: String = "") -> bool:
|
||||
"""
|
||||
导出对话历史到指定文件
|
||||
@param export_path: 导出文件路径
|
||||
@param character_id: 指定角色ID(空字符串表示导出所有)
|
||||
@return: 是否成功
|
||||
"""
|
||||
var export_data = {}
|
||||
|
||||
if character_id.is_empty():
|
||||
export_data = dialogue_histories
|
||||
elif dialogue_histories.has(character_id):
|
||||
export_data[character_id] = dialogue_histories[character_id]
|
||||
else:
|
||||
print("Character not found: ", character_id)
|
||||
return false
|
||||
|
||||
var file = FileAccess.open(export_path, FileAccess.WRITE)
|
||||
if file:
|
||||
var json_string = JSON.stringify(export_data)
|
||||
file.store_string(json_string)
|
||||
file.close()
|
||||
print("History exported to: ", export_path)
|
||||
return true
|
||||
else:
|
||||
print("Failed to export history to: ", export_path)
|
||||
return false
|
||||
1
scripts/DialogueHistoryManager.gd.uid
Normal file
1
scripts/DialogueHistoryManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cu15s7u88m8so
|
||||
362
scripts/DialogueSystem.gd
Normal file
362
scripts/DialogueSystem.gd
Normal file
@@ -0,0 +1,362 @@
|
||||
extends Node
|
||||
class_name DialogueSystem
|
||||
## 对话系统
|
||||
## 管理角色之间的对话交互
|
||||
|
||||
# 对话状态
|
||||
var is_in_dialogue: bool = false
|
||||
var current_target_id: String = ""
|
||||
var dialogue_history: Array[Dictionary] = []
|
||||
|
||||
# 增强功能组件
|
||||
var history_manager: DialogueHistoryManager
|
||||
var emoji_manager: EmojiManager
|
||||
var group_manager: GroupDialogueManager
|
||||
var dialogue_filter: DialogueFilter
|
||||
|
||||
# 社交系统引用
|
||||
var social_manager: SocialManager
|
||||
|
||||
# 信号
|
||||
signal dialogue_started(character_id: String)
|
||||
signal dialogue_ended()
|
||||
signal message_received(sender: String, message: String)
|
||||
signal bubble_requested(character_id: String, message: String, duration: float)
|
||||
signal message_filtered(original: String, filtered: String, reason: String)
|
||||
signal message_blocked(message: String, reason: String)
|
||||
|
||||
func _ready():
|
||||
"""初始化对话系统"""
|
||||
# 初始化增强功能组件
|
||||
history_manager = DialogueHistoryManager.new()
|
||||
add_child(history_manager)
|
||||
|
||||
emoji_manager = EmojiManager.new()
|
||||
add_child(emoji_manager)
|
||||
|
||||
group_manager = GroupDialogueManager.new()
|
||||
add_child(group_manager)
|
||||
|
||||
dialogue_filter = DialogueFilter.new()
|
||||
add_child(dialogue_filter)
|
||||
|
||||
# 连接信号
|
||||
dialogue_filter.message_filtered.connect(_on_message_filtered)
|
||||
dialogue_filter.message_blocked.connect(_on_message_blocked)
|
||||
group_manager.group_message_received.connect(_on_group_message_received)
|
||||
|
||||
print("DialogueSystem initialized with enhanced features")
|
||||
|
||||
## 设置社交管理器引用
|
||||
func set_social_manager(sm: SocialManager) -> void:
|
||||
"""
|
||||
设置社交管理器引用
|
||||
@param sm: 社交管理器实例
|
||||
"""
|
||||
social_manager = sm
|
||||
|
||||
## 开始对话
|
||||
func start_dialogue(target_character_id: String) -> void:
|
||||
"""
|
||||
开始与指定角色的对话
|
||||
@param target_character_id: 目标角色 ID
|
||||
"""
|
||||
if is_in_dialogue:
|
||||
print("Already in dialogue, ending current dialogue first")
|
||||
end_dialogue()
|
||||
|
||||
is_in_dialogue = true
|
||||
current_target_id = target_character_id
|
||||
dialogue_history.clear()
|
||||
|
||||
dialogue_started.emit(target_character_id)
|
||||
print("Dialogue started with: ", target_character_id)
|
||||
|
||||
## 发送消息
|
||||
func send_message(message: String) -> bool:
|
||||
"""
|
||||
发送对话消息
|
||||
@param message: 消息内容
|
||||
@return: 是否成功发送
|
||||
"""
|
||||
if not is_in_dialogue:
|
||||
push_warning("Not in dialogue, cannot send message")
|
||||
return false
|
||||
|
||||
# 使用对话过滤器验证和过滤消息
|
||||
var filter_result = dialogue_filter.filter_message("player", message)
|
||||
|
||||
if not filter_result.allowed:
|
||||
message_blocked.emit(message, filter_result.reason)
|
||||
return false
|
||||
|
||||
var filtered_message = filter_result.filtered_message
|
||||
|
||||
# 转换表情符号
|
||||
var final_message = EmojiManager.convert_text_to_emoji(filtered_message)
|
||||
|
||||
# 记录到历史
|
||||
var message_data = {
|
||||
"sender": "player",
|
||||
"receiver": current_target_id,
|
||||
"message": final_message,
|
||||
"timestamp": Time.get_unix_time_from_system()
|
||||
}
|
||||
dialogue_history.append(message_data)
|
||||
|
||||
# 保存到历史管理器
|
||||
history_manager.add_message_to_history(current_target_id, "player", final_message)
|
||||
|
||||
# 记录社交互动
|
||||
if social_manager:
|
||||
social_manager.record_social_interaction(current_target_id, "chat", {"message_length": final_message.length()})
|
||||
|
||||
# 发射信号
|
||||
message_received.emit("player", final_message)
|
||||
|
||||
print("Message sent: ", final_message)
|
||||
return true
|
||||
|
||||
## 接收消息
|
||||
func receive_message(sender_id: String, message: String) -> void:
|
||||
"""
|
||||
接收来自其他角色的消息
|
||||
@param sender_id: 发送者 ID
|
||||
@param message: 消息内容
|
||||
"""
|
||||
# 转换表情符号
|
||||
var final_message = EmojiManager.convert_text_to_emoji(message)
|
||||
|
||||
# 记录到历史
|
||||
var message_data = {
|
||||
"sender": sender_id,
|
||||
"receiver": "player",
|
||||
"message": final_message,
|
||||
"timestamp": Time.get_unix_time_from_system()
|
||||
}
|
||||
dialogue_history.append(message_data)
|
||||
|
||||
# 保存到历史管理器
|
||||
history_manager.add_message_to_history(sender_id, sender_id, final_message)
|
||||
|
||||
# 发射信号
|
||||
message_received.emit(sender_id, final_message)
|
||||
|
||||
print("Message received from ", sender_id, ": ", final_message)
|
||||
|
||||
## 结束对话
|
||||
func end_dialogue() -> void:
|
||||
"""结束当前对话"""
|
||||
if not is_in_dialogue:
|
||||
return
|
||||
|
||||
is_in_dialogue = false
|
||||
var ended_with = current_target_id
|
||||
current_target_id = ""
|
||||
|
||||
dialogue_ended.emit()
|
||||
print("Dialogue ended with: ", ended_with)
|
||||
|
||||
## 显示对话气泡
|
||||
func show_bubble(character_id: String, message: String, duration: float = 3.0) -> void:
|
||||
"""
|
||||
在角色上方显示对话气泡
|
||||
@param character_id: 角色 ID
|
||||
@param message: 消息内容
|
||||
@param duration: 显示时长(秒)
|
||||
"""
|
||||
bubble_requested.emit(character_id, message, duration)
|
||||
print("Bubble requested for ", character_id, ": ", message)
|
||||
|
||||
## 获取对话历史
|
||||
func get_dialogue_history() -> Array[Dictionary]:
|
||||
"""
|
||||
获取当前对话的历史记录
|
||||
@return: 消息历史数组
|
||||
"""
|
||||
return dialogue_history
|
||||
|
||||
## 检查是否在对话中
|
||||
func is_dialogue_active() -> bool:
|
||||
"""
|
||||
检查是否正在进行对话
|
||||
@return: 是否在对话中
|
||||
"""
|
||||
return is_in_dialogue
|
||||
|
||||
## 获取当前对话目标
|
||||
func get_current_target() -> String:
|
||||
"""
|
||||
获取当前对话的目标角色 ID
|
||||
@return: 目标角色 ID,如果不在对话中则返回空字符串
|
||||
"""
|
||||
return current_target_id
|
||||
|
||||
## 获取对话历史(增强版)
|
||||
func get_enhanced_dialogue_history(character_id: String, limit: int = 50) -> Array[Dictionary]:
|
||||
"""
|
||||
获取增强的对话历史记录
|
||||
@param character_id: 角色ID
|
||||
@param limit: 限制返回的消息数量
|
||||
@return: 消息历史数组
|
||||
"""
|
||||
return history_manager.get_character_history(character_id, limit)
|
||||
|
||||
## 搜索对话历史
|
||||
func search_dialogue_history(query: String, character_id: String = "") -> Array[Dictionary]:
|
||||
"""
|
||||
搜索对话历史
|
||||
@param query: 搜索关键词
|
||||
@param character_id: 指定角色ID(空字符串表示搜索所有角色)
|
||||
@return: 匹配的消息数组
|
||||
"""
|
||||
return history_manager.search_messages(query, character_id)
|
||||
|
||||
## 获取最近对话
|
||||
func get_recent_conversations(limit: int = 10) -> Array[Dictionary]:
|
||||
"""
|
||||
获取最近的对话记录
|
||||
@param limit: 限制返回的对话数量
|
||||
@return: 最近对话的摘要数组
|
||||
"""
|
||||
return history_manager.get_recent_conversations(limit)
|
||||
|
||||
## 创建群组对话
|
||||
func create_group_dialogue(group_name: String) -> String:
|
||||
"""
|
||||
创建群组对话
|
||||
@param group_name: 群组名称
|
||||
@return: 群组ID,失败返回空字符串
|
||||
"""
|
||||
return group_manager.create_group(group_name, "player")
|
||||
|
||||
## 加入群组对话
|
||||
func join_group_dialogue(group_id: String) -> bool:
|
||||
"""
|
||||
加入群组对话
|
||||
@param group_id: 群组ID
|
||||
@return: 是否成功加入
|
||||
"""
|
||||
return group_manager.join_group(group_id, "player")
|
||||
|
||||
## 发送群组消息
|
||||
func send_group_message(group_id: String, message: String) -> bool:
|
||||
"""
|
||||
发送群组消息
|
||||
@param group_id: 群组ID
|
||||
@param message: 消息内容
|
||||
@return: 是否成功发送
|
||||
"""
|
||||
# 使用对话过滤器验证和过滤消息
|
||||
var filter_result = dialogue_filter.filter_message("player", message)
|
||||
|
||||
if not filter_result.allowed:
|
||||
message_blocked.emit(message, filter_result.reason)
|
||||
return false
|
||||
|
||||
var filtered_message = filter_result.filtered_message
|
||||
|
||||
# 转换表情符号
|
||||
var final_message = EmojiManager.convert_text_to_emoji(filtered_message)
|
||||
|
||||
# 发送群组消息
|
||||
return group_manager.send_group_message(group_id, "player", final_message)
|
||||
|
||||
## 获取表情符号建议
|
||||
func get_emoji_suggestions(partial_code: String) -> Array[Dictionary]:
|
||||
"""
|
||||
获取表情符号建议
|
||||
@param partial_code: 部分表情符号代码
|
||||
@return: 建议数组
|
||||
"""
|
||||
return EmojiManager.get_emoji_suggestions(partial_code)
|
||||
|
||||
## 获取表情符号选择器数据
|
||||
func get_emoji_picker_data() -> Dictionary:
|
||||
"""
|
||||
获取表情符号选择器数据
|
||||
@return: 表情符号数据
|
||||
"""
|
||||
return emoji_manager.create_emoji_picker_data()
|
||||
|
||||
## 使用表情符号
|
||||
func use_emoji(emoji: String) -> void:
|
||||
"""
|
||||
记录表情符号使用(添加到最近使用)
|
||||
@param emoji: 表情符号
|
||||
"""
|
||||
emoji_manager.add_to_recent(emoji)
|
||||
|
||||
## 获取过滤器配置
|
||||
func get_filter_config() -> Dictionary:
|
||||
"""
|
||||
获取对话过滤器配置
|
||||
@return: 配置字典
|
||||
"""
|
||||
return dialogue_filter.get_filter_config()
|
||||
|
||||
## 设置过滤器配置
|
||||
func set_filter_config(config: Dictionary) -> void:
|
||||
"""
|
||||
设置对话过滤器配置
|
||||
@param config: 配置字典
|
||||
"""
|
||||
dialogue_filter.set_filter_config(config)
|
||||
|
||||
## 获取过滤统计
|
||||
func get_filter_statistics() -> Dictionary:
|
||||
"""
|
||||
获取过滤统计信息
|
||||
@return: 统计信息字典
|
||||
"""
|
||||
return dialogue_filter.get_filter_statistics()
|
||||
|
||||
## 获取群组列表
|
||||
func get_player_groups() -> Array[Dictionary]:
|
||||
"""
|
||||
获取玩家参与的群组列表
|
||||
@return: 群组信息数组
|
||||
"""
|
||||
return group_manager.get_player_groups()
|
||||
|
||||
## 获取群组信息
|
||||
func get_group_info(group_id: String) -> Dictionary:
|
||||
"""
|
||||
获取群组信息
|
||||
@param group_id: 群组ID
|
||||
@return: 群组信息字典
|
||||
"""
|
||||
return group_manager.get_group_info(group_id)
|
||||
|
||||
## 离开群组
|
||||
func leave_group(group_id: String) -> bool:
|
||||
"""
|
||||
离开群组
|
||||
@param group_id: 群组ID
|
||||
@return: 是否成功离开
|
||||
"""
|
||||
return group_manager.leave_group(group_id, "player")
|
||||
|
||||
## 设置当前群组
|
||||
func set_current_group(group_id: String) -> bool:
|
||||
"""
|
||||
设置当前活跃的群组
|
||||
@param group_id: 群组ID
|
||||
@return: 是否成功设置
|
||||
"""
|
||||
return group_manager.set_current_group(group_id)
|
||||
|
||||
## 消息过滤信号处理
|
||||
func _on_message_filtered(_user_id: String, original: String, filtered: String, reason: String):
|
||||
"""处理消息过滤信号"""
|
||||
message_filtered.emit(original, filtered, reason)
|
||||
|
||||
## 消息阻止信号处理
|
||||
func _on_message_blocked(_user_id: String, message: String, reason: String):
|
||||
"""处理消息阻止信号"""
|
||||
message_blocked.emit(message, reason)
|
||||
|
||||
## 群组消息接收信号处理
|
||||
func _on_group_message_received(group_id: String, sender_id: String, message: String):
|
||||
"""处理群组消息接收信号"""
|
||||
print("Group message received in ", group_id, " from ", sender_id, ": ", message)
|
||||
1
scripts/DialogueSystem.gd.uid
Normal file
1
scripts/DialogueSystem.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dtgvd4g1earxp
|
||||
295
scripts/DialogueTestManager.gd
Normal file
295
scripts/DialogueTestManager.gd
Normal file
@@ -0,0 +1,295 @@
|
||||
extends Node
|
||||
class_name DialogueTestManager
|
||||
## 对话系统测试管理器
|
||||
## 用于测试对话功能,生成测试NPC和模拟对话
|
||||
|
||||
# 测试NPC数据
|
||||
var test_npcs: Array[Dictionary] = []
|
||||
var spawned_npcs: Dictionary = {}
|
||||
|
||||
# 引用
|
||||
var world_manager: WorldManager
|
||||
var dialogue_system: DialogueSystem
|
||||
|
||||
# 测试配置
|
||||
const TEST_NPC_COUNT = 3
|
||||
const NPC_SPAWN_RADIUS = 200.0
|
||||
|
||||
func _ready():
|
||||
"""初始化测试管理器"""
|
||||
print("DialogueTestManager initialized")
|
||||
|
||||
## 设置引用
|
||||
func setup_references(world_mgr: WorldManager, dialogue_sys: DialogueSystem):
|
||||
"""
|
||||
设置必要的引用
|
||||
@param world_mgr: 世界管理器
|
||||
@param dialogue_sys: 对话系统
|
||||
"""
|
||||
world_manager = world_mgr
|
||||
dialogue_system = dialogue_sys
|
||||
|
||||
# 连接对话系统信号
|
||||
if dialogue_system:
|
||||
dialogue_system.dialogue_started.connect(_on_dialogue_started)
|
||||
dialogue_system.dialogue_ended.connect(_on_dialogue_ended)
|
||||
dialogue_system.message_received.connect(_on_message_received)
|
||||
|
||||
## 生成测试NPC
|
||||
func spawn_test_npcs(player_position: Vector2 = Vector2(640, 360)):
|
||||
"""
|
||||
在玩家周围生成测试NPC
|
||||
@param player_position: 玩家位置
|
||||
"""
|
||||
if not world_manager:
|
||||
print("WorldManager not set, cannot spawn NPCs")
|
||||
return
|
||||
|
||||
# 清除已存在的测试NPC
|
||||
clear_test_npcs()
|
||||
|
||||
# 生成测试NPC数据
|
||||
_generate_test_npc_data()
|
||||
|
||||
# 在玩家周围生成NPC
|
||||
for i in range(test_npcs.size()):
|
||||
var npc_data = test_npcs[i]
|
||||
|
||||
# 计算NPC位置(围绕玩家分布)
|
||||
var angle = (2.0 * PI * i) / test_npcs.size()
|
||||
var offset = Vector2(cos(angle), sin(angle)) * NPC_SPAWN_RADIUS
|
||||
var npc_position = player_position + offset
|
||||
|
||||
# 更新NPC位置
|
||||
npc_data[CharacterData.FIELD_POSITION] = {
|
||||
"x": npc_position.x,
|
||||
"y": npc_position.y
|
||||
}
|
||||
|
||||
# 生成NPC
|
||||
var npc_character = world_manager.spawn_character(npc_data, false)
|
||||
if npc_character:
|
||||
spawned_npcs[npc_data[CharacterData.FIELD_ID]] = npc_character
|
||||
print("Test NPC spawned: ", npc_data[CharacterData.FIELD_NAME], " at ", npc_position)
|
||||
|
||||
## 生成测试NPC数据
|
||||
func _generate_test_npc_data():
|
||||
"""生成测试NPC的数据"""
|
||||
test_npcs.clear()
|
||||
|
||||
var npc_names = [
|
||||
"测试小助手",
|
||||
"友好的机器人",
|
||||
"聊天达人",
|
||||
"表情包大师",
|
||||
"知识渊博者"
|
||||
]
|
||||
|
||||
var npc_responses = [
|
||||
["你好!我是测试小助手,很高兴见到你!", "有什么可以帮助你的吗?", "今天天气真不错呢!"],
|
||||
["哔哔!我是友好的机器人!", "正在运行对话测试程序...", "系统状态:一切正常!"],
|
||||
["嗨!想聊什么呢?", "我最喜欢和大家聊天了!", "你知道吗,聊天是最好的交流方式!"],
|
||||
["😊 表情包来了!", "😂 哈哈哈,太有趣了!", "🎉 让我们用表情包交流吧!"],
|
||||
["你知道吗?对话系统很复杂呢!", "我了解很多有趣的知识!", "想听听关于AI的故事吗?"]
|
||||
]
|
||||
|
||||
for i in range(min(TEST_NPC_COUNT, npc_names.size())):
|
||||
var npc_id = "test_npc_" + str(i + 1)
|
||||
var npc_data = CharacterData.create(npc_names[i], "system", Vector2.ZERO)
|
||||
npc_data[CharacterData.FIELD_ID] = npc_id
|
||||
|
||||
# 添加测试专用数据
|
||||
npc_data["test_responses"] = npc_responses[i]
|
||||
npc_data["response_index"] = 0
|
||||
|
||||
test_npcs.append(npc_data)
|
||||
|
||||
## 清除测试NPC
|
||||
func clear_test_npcs():
|
||||
"""清除所有测试NPC"""
|
||||
for npc_id in spawned_npcs.keys():
|
||||
if world_manager:
|
||||
world_manager.remove_character(npc_id)
|
||||
|
||||
spawned_npcs.clear()
|
||||
print("Test NPCs cleared")
|
||||
|
||||
## 开始与NPC对话
|
||||
func start_dialogue_with_npc(npc_id: String):
|
||||
"""
|
||||
开始与指定NPC对话
|
||||
@param npc_id: NPC ID
|
||||
"""
|
||||
if not dialogue_system:
|
||||
print("DialogueSystem not set")
|
||||
return
|
||||
|
||||
if not spawned_npcs.has(npc_id):
|
||||
print("NPC not found: ", npc_id)
|
||||
return
|
||||
|
||||
dialogue_system.start_dialogue(npc_id)
|
||||
|
||||
# 发送NPC的欢迎消息
|
||||
_send_npc_response(npc_id, true)
|
||||
|
||||
## 发送NPC响应
|
||||
func _send_npc_response(npc_id: String, is_greeting: bool = false):
|
||||
"""
|
||||
发送NPC的自动响应
|
||||
@param npc_id: NPC ID
|
||||
@param is_greeting: 是否为问候语
|
||||
"""
|
||||
# 找到对应的NPC数据
|
||||
var npc_data: Dictionary
|
||||
for data in test_npcs:
|
||||
if data[CharacterData.FIELD_ID] == npc_id:
|
||||
npc_data = data
|
||||
break
|
||||
|
||||
if npc_data.is_empty():
|
||||
return
|
||||
|
||||
var responses = npc_data.get("test_responses", [])
|
||||
if responses.is_empty():
|
||||
return
|
||||
|
||||
var response_index = npc_data.get("response_index", 0)
|
||||
var message = responses[response_index]
|
||||
|
||||
# 更新响应索引
|
||||
npc_data["response_index"] = (response_index + 1) % responses.size()
|
||||
|
||||
# 延迟发送响应(模拟真实对话)
|
||||
await get_tree().create_timer(randf_range(0.5, 1.5)).timeout
|
||||
|
||||
if dialogue_system:
|
||||
dialogue_system.receive_message(npc_id, message)
|
||||
dialogue_system.show_bubble(npc_id, message, 3.0)
|
||||
|
||||
## 获取附近的NPC
|
||||
func get_nearby_npcs(position: Vector2, radius: float = 100.0) -> Array[Dictionary]:
|
||||
"""
|
||||
获取附近的测试NPC
|
||||
@param position: 中心位置
|
||||
@param radius: 搜索半径
|
||||
@return: 附近NPC的信息数组
|
||||
"""
|
||||
var nearby_npcs: Array[Dictionary] = []
|
||||
|
||||
for npc_id in spawned_npcs.keys():
|
||||
var npc_character = spawned_npcs[npc_id]
|
||||
if npc_character and npc_character.global_position.distance_to(position) <= radius:
|
||||
# 找到NPC数据
|
||||
for data in test_npcs:
|
||||
if data[CharacterData.FIELD_ID] == npc_id:
|
||||
nearby_npcs.append({
|
||||
"id": npc_id,
|
||||
"name": data[CharacterData.FIELD_NAME],
|
||||
"position": npc_character.global_position,
|
||||
"distance": npc_character.global_position.distance_to(position)
|
||||
})
|
||||
break
|
||||
|
||||
# 按距离排序
|
||||
nearby_npcs.sort_custom(func(a, b): return a.distance < b.distance)
|
||||
|
||||
return nearby_npcs
|
||||
|
||||
## 测试表情符号功能
|
||||
func test_emoji_system():
|
||||
"""测试表情符号系统"""
|
||||
print("=== 测试表情符号系统 ===")
|
||||
|
||||
var test_messages = [
|
||||
":smile: 你好!",
|
||||
"今天心情很好 :happy:",
|
||||
":laugh: 哈哈哈",
|
||||
"加油! :thumbsup:",
|
||||
":heart: 爱你哦"
|
||||
]
|
||||
|
||||
for message in test_messages:
|
||||
var converted = EmojiManager.convert_text_to_emoji(message)
|
||||
print("原文: ", message)
|
||||
print("转换后: ", converted)
|
||||
print("---")
|
||||
|
||||
## 测试群组对话功能
|
||||
func test_group_dialogue():
|
||||
"""测试群组对话功能"""
|
||||
print("=== 测试群组对话功能 ===")
|
||||
|
||||
if not dialogue_system:
|
||||
print("DialogueSystem not available")
|
||||
return
|
||||
|
||||
# 创建测试群组
|
||||
var group_id = dialogue_system.create_group_dialogue("测试群组")
|
||||
if group_id.is_empty():
|
||||
print("Failed to create group")
|
||||
return
|
||||
|
||||
print("Created group: ", group_id)
|
||||
|
||||
# 发送测试消息
|
||||
var test_messages = [
|
||||
"大家好!",
|
||||
"这是群组对话测试",
|
||||
":wave: 欢迎大家!"
|
||||
]
|
||||
|
||||
for message in test_messages:
|
||||
var success = dialogue_system.send_group_message(group_id, message)
|
||||
print("Group message sent: ", message, " (success: ", success, ")")
|
||||
|
||||
## 显示测试帮助
|
||||
func show_test_help():
|
||||
"""显示测试帮助信息"""
|
||||
print("=== 对话系统测试帮助 ===")
|
||||
print("1. 使用 spawn_test_npcs() 生成测试NPC")
|
||||
print("2. 使用 start_dialogue_with_npc(npc_id) 开始对话")
|
||||
print("3. 使用 get_nearby_npcs(position) 查找附近NPC")
|
||||
print("4. 使用 test_emoji_system() 测试表情符号")
|
||||
print("5. 使用 test_group_dialogue() 测试群组对话")
|
||||
print("6. 使用 clear_test_npcs() 清除测试NPC")
|
||||
print("========================")
|
||||
|
||||
## 信号处理
|
||||
func _on_dialogue_started(character_id: String):
|
||||
"""对话开始时的处理"""
|
||||
print("Dialogue started with: ", character_id)
|
||||
|
||||
func _on_dialogue_ended():
|
||||
"""对话结束时的处理"""
|
||||
print("Dialogue ended")
|
||||
|
||||
func _on_message_received(sender: String, message: String):
|
||||
"""收到消息时的处理"""
|
||||
print("Message from ", sender, ": ", message)
|
||||
|
||||
# 如果是玩家发送的消息,让NPC自动回复
|
||||
if sender == "player" and dialogue_system and dialogue_system.is_dialogue_active():
|
||||
var target_id = dialogue_system.get_current_target()
|
||||
if spawned_npcs.has(target_id):
|
||||
_send_npc_response(target_id)
|
||||
|
||||
## 快速测试函数
|
||||
func quick_test():
|
||||
"""快速测试所有功能"""
|
||||
print("=== 开始快速测试 ===")
|
||||
|
||||
# 生成NPC
|
||||
spawn_test_npcs()
|
||||
|
||||
# 等待一秒
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
|
||||
# 测试表情符号
|
||||
test_emoji_system()
|
||||
|
||||
# 测试群组对话
|
||||
test_group_dialogue()
|
||||
|
||||
print("=== 快速测试完成 ===")
|
||||
show_test_help()
|
||||
1
scripts/DialogueTestManager.gd.uid
Normal file
1
scripts/DialogueTestManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://vg852e2naf3r
|
||||
327
scripts/EmojiManager.gd
Normal file
327
scripts/EmojiManager.gd
Normal file
@@ -0,0 +1,327 @@
|
||||
extends Node
|
||||
class_name EmojiManager
|
||||
## 表情符号管理器
|
||||
## 处理表情符号的显示、转换和管理
|
||||
|
||||
# 表情符号映射表
|
||||
const EMOJI_MAP = {
|
||||
# 基础表情
|
||||
":)": "😊",
|
||||
":-)": "😊",
|
||||
":(": "😢",
|
||||
":-(": "😢",
|
||||
":D": "😃",
|
||||
":-D": "😃",
|
||||
";)": "😉",
|
||||
";-)": "😉",
|
||||
":P": "😛",
|
||||
":-P": "😛",
|
||||
":o": "😮",
|
||||
":-o": "😮",
|
||||
":O": "😲",
|
||||
":-O": "😲",
|
||||
":|": "😐",
|
||||
":-|": "😐",
|
||||
":/": "😕",
|
||||
":-/": "😕",
|
||||
"<3": "❤️",
|
||||
"</3": "💔",
|
||||
|
||||
# 命名表情符号
|
||||
":smile:": "😊",
|
||||
":happy:": "😊",
|
||||
":sad:": "😢",
|
||||
":cry:": "😭",
|
||||
":laugh:": "😂",
|
||||
":love:": "😍",
|
||||
":angry:": "😠",
|
||||
":mad:": "😡",
|
||||
":cool:": "😎",
|
||||
":wink:": "😉",
|
||||
":tongue:": "😛",
|
||||
":surprised:": "😲",
|
||||
":shocked:": "😱",
|
||||
":confused:": "😕",
|
||||
":thinking:": "🤔",
|
||||
":sleepy:": "😴",
|
||||
":sick:": "🤒",
|
||||
":dizzy:": "😵",
|
||||
":party:": "🥳",
|
||||
":celebrate:": "🎉",
|
||||
|
||||
# 手势和动作
|
||||
":thumbsup:": "👍",
|
||||
":thumbsdown:": "👎",
|
||||
":clap:": "👏",
|
||||
":wave:": "👋",
|
||||
":peace:": "✌️",
|
||||
":ok:": "👌",
|
||||
":point:": "👉",
|
||||
":fist:": "✊",
|
||||
":pray:": "🙏",
|
||||
":muscle:": "💪",
|
||||
|
||||
# 物品和符号
|
||||
":fire:": "🔥",
|
||||
":star:": "⭐",
|
||||
":heart:": "❤️",
|
||||
":broken_heart:": "💔",
|
||||
":diamond:": "💎",
|
||||
":crown:": "👑",
|
||||
":gift:": "🎁",
|
||||
":cake:": "🎂",
|
||||
":coffee:": "☕",
|
||||
":pizza:": "🍕",
|
||||
":music:": "🎵",
|
||||
":game:": "🎮",
|
||||
":book:": "📚",
|
||||
":phone:": "📱",
|
||||
":computer:": "💻",
|
||||
":car:": "🚗",
|
||||
":house:": "🏠",
|
||||
":sun:": "☀️",
|
||||
":moon:": "🌙",
|
||||
":cloud:": "☁️",
|
||||
":rain:": "🌧️",
|
||||
":snow:": "❄️",
|
||||
":lightning:": "⚡",
|
||||
":rainbow:": "🌈"
|
||||
}
|
||||
|
||||
# 表情符号类别
|
||||
const EMOJI_CATEGORIES = {
|
||||
"faces": ["😊", "😢", "😃", "😉", "😛", "😮", "😲", "😐", "😕", "😭", "😂", "😍", "😠", "😡", "😎", "😱", "🤔", "😴", "🤒", "😵", "🥳"],
|
||||
"gestures": ["👍", "👎", "👏", "👋", "✌️", "👌", "👉", "✊", "🙏", "💪"],
|
||||
"objects": ["🔥", "⭐", "❤️", "💔", "💎", "👑", "🎁", "🎂", "☕", "🍕", "🎵", "🎮", "📚", "📱", "💻"],
|
||||
"nature": ["☀️", "🌙", "☁️", "🌧️", "❄️", "⚡", "🌈"],
|
||||
"transport": ["🚗", "🏠"]
|
||||
}
|
||||
|
||||
# 最近使用的表情符号
|
||||
var recent_emojis: Array[String] = []
|
||||
var max_recent_emojis: int = 20
|
||||
|
||||
func _ready():
|
||||
"""初始化表情符号管理器"""
|
||||
load_recent_emojis()
|
||||
print("EmojiManager initialized with ", EMOJI_MAP.size(), " emojis")
|
||||
|
||||
## 转换文本中的表情符号
|
||||
static func convert_text_to_emoji(text: String) -> String:
|
||||
"""
|
||||
将文本中的表情符号代码转换为实际的表情符号
|
||||
@param text: 输入文本
|
||||
@return: 转换后的文本
|
||||
"""
|
||||
var result = text
|
||||
|
||||
# 按长度排序,优先匹配长的表情符号代码
|
||||
var sorted_keys = EMOJI_MAP.keys()
|
||||
sorted_keys.sort_custom(func(a, b): return a.length() > b.length())
|
||||
|
||||
for emoji_code in sorted_keys:
|
||||
var emoji = EMOJI_MAP[emoji_code]
|
||||
result = result.replace(emoji_code, emoji)
|
||||
|
||||
return result
|
||||
|
||||
## 获取表情符号建议
|
||||
static func get_emoji_suggestions(partial_code: String) -> Array[Dictionary]:
|
||||
"""
|
||||
根据部分输入获取表情符号建议
|
||||
@param partial_code: 部分表情符号代码
|
||||
@return: 建议数组,包含代码和对应的表情符号
|
||||
"""
|
||||
var suggestions = []
|
||||
var partial_lower = partial_code.to_lower()
|
||||
|
||||
for emoji_code in EMOJI_MAP:
|
||||
if emoji_code.to_lower().begins_with(partial_lower):
|
||||
suggestions.append({
|
||||
"code": emoji_code,
|
||||
"emoji": EMOJI_MAP[emoji_code],
|
||||
"description": _get_emoji_description(emoji_code)
|
||||
})
|
||||
|
||||
# 限制建议数量
|
||||
if suggestions.size() > 10:
|
||||
suggestions = suggestions.slice(0, 10)
|
||||
|
||||
return suggestions
|
||||
|
||||
## 获取表情符号描述
|
||||
static func _get_emoji_description(emoji_code: String) -> String:
|
||||
"""
|
||||
获取表情符号的描述
|
||||
@param emoji_code: 表情符号代码
|
||||
@return: 描述文本
|
||||
"""
|
||||
match emoji_code:
|
||||
":smile:", ":)", ":-)":
|
||||
return "微笑"
|
||||
":sad:", ":(", ":-(":
|
||||
return "伤心"
|
||||
":laugh:", ":D", ":-D":
|
||||
return "大笑"
|
||||
":love:":
|
||||
return "爱心眼"
|
||||
":angry:":
|
||||
return "生气"
|
||||
":cool:":
|
||||
return "酷"
|
||||
":wink:", ";)", ";-)":
|
||||
return "眨眼"
|
||||
":thinking:":
|
||||
return "思考"
|
||||
":thumbsup:":
|
||||
return "点赞"
|
||||
":thumbsdown:":
|
||||
return "点踩"
|
||||
":clap:":
|
||||
return "鼓掌"
|
||||
":fire:":
|
||||
return "火"
|
||||
":heart:", "<3":
|
||||
return "爱心"
|
||||
":star:":
|
||||
return "星星"
|
||||
_:
|
||||
return "表情符号"
|
||||
|
||||
## 按类别获取表情符号
|
||||
static func get_emojis_by_category(category: String) -> Array[String]:
|
||||
"""
|
||||
获取指定类别的表情符号
|
||||
@param category: 类别名称
|
||||
@return: 表情符号数组
|
||||
"""
|
||||
if EMOJI_CATEGORIES.has(category):
|
||||
return EMOJI_CATEGORIES[category].duplicate()
|
||||
return []
|
||||
|
||||
## 获取所有类别
|
||||
static func get_all_categories() -> Array[String]:
|
||||
"""
|
||||
获取所有表情符号类别
|
||||
@return: 类别名称数组
|
||||
"""
|
||||
return EMOJI_CATEGORIES.keys()
|
||||
|
||||
## 添加到最近使用
|
||||
func add_to_recent(emoji: String) -> void:
|
||||
"""
|
||||
添加表情符号到最近使用列表
|
||||
@param emoji: 表情符号
|
||||
"""
|
||||
# 如果已存在,先移除
|
||||
if emoji in recent_emojis:
|
||||
recent_emojis.erase(emoji)
|
||||
|
||||
# 添加到开头
|
||||
recent_emojis.push_front(emoji)
|
||||
|
||||
# 限制数量
|
||||
if recent_emojis.size() > max_recent_emojis:
|
||||
recent_emojis.pop_back()
|
||||
|
||||
# 保存到本地
|
||||
save_recent_emojis()
|
||||
|
||||
## 获取最近使用的表情符号
|
||||
func get_recent_emojis() -> Array[String]:
|
||||
"""
|
||||
获取最近使用的表情符号
|
||||
@return: 最近使用的表情符号数组
|
||||
"""
|
||||
return recent_emojis.duplicate()
|
||||
|
||||
## 检查是否包含表情符号
|
||||
static func contains_emoji(text: String) -> bool:
|
||||
"""
|
||||
检查文本是否包含表情符号代码
|
||||
@param text: 输入文本
|
||||
@return: 是否包含表情符号
|
||||
"""
|
||||
for emoji_code in EMOJI_MAP:
|
||||
if text.contains(emoji_code):
|
||||
return true
|
||||
return false
|
||||
|
||||
## 提取文本中的表情符号代码
|
||||
static func extract_emoji_codes(text: String) -> Array[String]:
|
||||
"""
|
||||
提取文本中的所有表情符号代码
|
||||
@param text: 输入文本
|
||||
@return: 表情符号代码数组
|
||||
"""
|
||||
var codes = []
|
||||
|
||||
for emoji_code in EMOJI_MAP:
|
||||
if text.contains(emoji_code):
|
||||
codes.append(emoji_code)
|
||||
|
||||
return codes
|
||||
|
||||
## 获取随机表情符号
|
||||
static func get_random_emoji(category: String = "") -> String:
|
||||
"""
|
||||
获取随机表情符号
|
||||
@param category: 指定类别(空字符串表示从所有表情符号中选择)
|
||||
@return: 随机表情符号
|
||||
"""
|
||||
var emoji_list = []
|
||||
|
||||
if category.is_empty():
|
||||
emoji_list = EMOJI_MAP.values()
|
||||
elif EMOJI_CATEGORIES.has(category):
|
||||
emoji_list = EMOJI_CATEGORIES[category]
|
||||
else:
|
||||
return "😊" # 默认表情符号
|
||||
|
||||
if emoji_list.size() > 0:
|
||||
return emoji_list[randi() % emoji_list.size()]
|
||||
|
||||
return "😊"
|
||||
|
||||
## 保存最近使用的表情符号
|
||||
func save_recent_emojis() -> void:
|
||||
"""保存最近使用的表情符号到本地文件"""
|
||||
var file = FileAccess.open("user://recent_emojis.json", FileAccess.WRITE)
|
||||
if file:
|
||||
var json_string = JSON.stringify(recent_emojis)
|
||||
file.store_string(json_string)
|
||||
file.close()
|
||||
|
||||
## 加载最近使用的表情符号
|
||||
func load_recent_emojis() -> void:
|
||||
"""从本地文件加载最近使用的表情符号"""
|
||||
if not FileAccess.file_exists("user://recent_emojis.json"):
|
||||
return
|
||||
|
||||
var file = FileAccess.open("user://recent_emojis.json", FileAccess.READ)
|
||||
if file:
|
||||
var json_string = file.get_as_text()
|
||||
file.close()
|
||||
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(json_string)
|
||||
|
||||
if parse_result == OK and json.data is Array:
|
||||
recent_emojis = json.data
|
||||
print("Loaded ", recent_emojis.size(), " recent emojis")
|
||||
|
||||
## 创建表情符号选择器数据
|
||||
func create_emoji_picker_data() -> Dictionary:
|
||||
"""
|
||||
创建表情符号选择器所需的数据
|
||||
@return: 包含分类表情符号的字典
|
||||
"""
|
||||
var picker_data = {
|
||||
"recent": get_recent_emojis(),
|
||||
"categories": {}
|
||||
}
|
||||
|
||||
for category in EMOJI_CATEGORIES:
|
||||
picker_data.categories[category] = EMOJI_CATEGORIES[category].duplicate()
|
||||
|
||||
return picker_data
|
||||
1
scripts/EmojiManager.gd.uid
Normal file
1
scripts/EmojiManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dv6j3x3hmn8kv
|
||||
470
scripts/EnhancedDialogueBox.gd
Normal file
470
scripts/EnhancedDialogueBox.gd
Normal file
@@ -0,0 +1,470 @@
|
||||
extends Control
|
||||
class_name EnhancedDialogueBox
|
||||
## 增强对话框 UI
|
||||
## 支持表情符号、群组对话、历史记录等功能
|
||||
|
||||
# UI 元素
|
||||
var message_display: RichTextLabel
|
||||
var message_input: LineEdit
|
||||
var send_button: Button
|
||||
var emoji_button: Button
|
||||
var history_button: Button
|
||||
var group_button: Button
|
||||
var close_button: Button
|
||||
var target_label: Label
|
||||
var container: VBoxContainer
|
||||
var scroll_container: ScrollContainer
|
||||
|
||||
# 表情符号选择器
|
||||
var emoji_picker: Control
|
||||
var emoji_picker_visible: bool = false
|
||||
|
||||
# 历史记录面板
|
||||
var history_panel: Control
|
||||
var history_list: ItemList
|
||||
var history_visible: bool = false
|
||||
|
||||
# 群组面板
|
||||
var group_panel: Control
|
||||
var group_list: ItemList
|
||||
var create_group_button: Button
|
||||
var join_group_button: Button
|
||||
var group_visible: bool = false
|
||||
|
||||
# 管理器引用
|
||||
var emoji_manager: EmojiManager
|
||||
var dialogue_filter: DialogueFilter
|
||||
var group_manager: GroupDialogueManager
|
||||
var history_manager: DialogueHistoryManager
|
||||
|
||||
# 当前对话目标
|
||||
var current_target_id: String = ""
|
||||
var current_target_name: String = ""
|
||||
var is_group_chat: bool = false
|
||||
|
||||
# 信号
|
||||
signal message_sent(target_id: String, message: String, is_group: bool)
|
||||
signal dialogue_closed()
|
||||
signal emoji_selected(emoji: String)
|
||||
|
||||
func _ready():
|
||||
"""初始化增强对话框"""
|
||||
setup_ui()
|
||||
setup_managers()
|
||||
connect_signals()
|
||||
print("EnhancedDialogueBox initialized")
|
||||
|
||||
## 设置UI元素
|
||||
func setup_ui() -> void:
|
||||
"""设置UI元素和布局"""
|
||||
# 主容器
|
||||
container = VBoxContainer.new()
|
||||
add_child(container)
|
||||
container.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
|
||||
# 目标标签
|
||||
target_label = Label.new()
|
||||
target_label.text = "对话"
|
||||
target_label.add_theme_font_size_override("font_size", 16)
|
||||
container.add_child(target_label)
|
||||
|
||||
# 消息显示区域
|
||||
scroll_container = ScrollContainer.new()
|
||||
scroll_container.custom_minimum_size = Vector2(400, 300)
|
||||
container.add_child(scroll_container)
|
||||
|
||||
message_display = RichTextLabel.new()
|
||||
message_display.bbcode_enabled = true
|
||||
message_display.fit_content = true
|
||||
scroll_container.add_child(message_display)
|
||||
|
||||
# 按钮行
|
||||
var button_row = HBoxContainer.new()
|
||||
container.add_child(button_row)
|
||||
|
||||
emoji_button = Button.new()
|
||||
emoji_button.text = "😊"
|
||||
emoji_button.custom_minimum_size = Vector2(40, 30)
|
||||
button_row.add_child(emoji_button)
|
||||
|
||||
history_button = Button.new()
|
||||
history_button.text = "历史"
|
||||
history_button.custom_minimum_size = Vector2(60, 30)
|
||||
button_row.add_child(history_button)
|
||||
|
||||
group_button = Button.new()
|
||||
group_button.text = "群组"
|
||||
group_button.custom_minimum_size = Vector2(60, 30)
|
||||
button_row.add_child(group_button)
|
||||
|
||||
close_button = Button.new()
|
||||
close_button.text = "关闭"
|
||||
close_button.custom_minimum_size = Vector2(60, 30)
|
||||
button_row.add_child(close_button)
|
||||
|
||||
# 输入行
|
||||
var input_row = HBoxContainer.new()
|
||||
container.add_child(input_row)
|
||||
|
||||
message_input = LineEdit.new()
|
||||
message_input.placeholder_text = "输入消息..."
|
||||
message_input.custom_minimum_size = Vector2(300, 30)
|
||||
input_row.add_child(message_input)
|
||||
|
||||
send_button = Button.new()
|
||||
send_button.text = "发送"
|
||||
send_button.custom_minimum_size = Vector2(60, 30)
|
||||
input_row.add_child(send_button)
|
||||
|
||||
# 创建弹出面板
|
||||
create_emoji_picker()
|
||||
create_history_panel()
|
||||
create_group_panel()
|
||||
|
||||
## 创建表情符号选择器
|
||||
func create_emoji_picker() -> void:
|
||||
"""创建表情符号选择器面板"""
|
||||
emoji_picker = Control.new()
|
||||
emoji_picker.visible = false
|
||||
add_child(emoji_picker)
|
||||
|
||||
var picker_bg = NinePatchRect.new()
|
||||
picker_bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
emoji_picker.add_child(picker_bg)
|
||||
|
||||
var picker_container = VBoxContainer.new()
|
||||
picker_container.position = Vector2(10, 10)
|
||||
picker_container.custom_minimum_size = Vector2(300, 200)
|
||||
emoji_picker.add_child(picker_container)
|
||||
|
||||
# 表情符号网格
|
||||
var emoji_grid = GridContainer.new()
|
||||
emoji_grid.columns = 8
|
||||
picker_container.add_child(emoji_grid)
|
||||
|
||||
# 添加常用表情符号
|
||||
var common_emojis = ["😊", "😢", "😃", "😉", "😛", "😮", "😲", "😐", "😕", "😭", "😂", "😍", "😠", "😡", "😎", "👍", "👎", "👏", "❤️", "🔥"]
|
||||
for emoji in common_emojis:
|
||||
var emoji_btn = Button.new()
|
||||
emoji_btn.text = emoji
|
||||
emoji_btn.custom_minimum_size = Vector2(30, 30)
|
||||
emoji_btn.pressed.connect(_on_emoji_button_pressed.bind(emoji))
|
||||
emoji_grid.add_child(emoji_btn)
|
||||
|
||||
## 创建历史记录面板
|
||||
func create_history_panel() -> void:
|
||||
"""创建历史记录面板"""
|
||||
history_panel = Control.new()
|
||||
history_panel.visible = false
|
||||
add_child(history_panel)
|
||||
|
||||
var history_bg = NinePatchRect.new()
|
||||
history_bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
history_panel.add_child(history_bg)
|
||||
|
||||
var history_container = VBoxContainer.new()
|
||||
history_container.position = Vector2(10, 10)
|
||||
history_container.custom_minimum_size = Vector2(350, 250)
|
||||
history_panel.add_child(history_container)
|
||||
|
||||
var history_title = Label.new()
|
||||
history_title.text = "对话历史"
|
||||
history_title.add_theme_font_size_override("font_size", 14)
|
||||
history_container.add_child(history_title)
|
||||
|
||||
history_list = ItemList.new()
|
||||
history_list.custom_minimum_size = Vector2(330, 200)
|
||||
history_container.add_child(history_list)
|
||||
|
||||
## 创建群组面板
|
||||
func create_group_panel() -> void:
|
||||
"""创建群组管理面板"""
|
||||
group_panel = Control.new()
|
||||
group_panel.visible = false
|
||||
add_child(group_panel)
|
||||
|
||||
var group_bg = NinePatchRect.new()
|
||||
group_bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
group_panel.add_child(group_bg)
|
||||
|
||||
var group_container = VBoxContainer.new()
|
||||
group_container.position = Vector2(10, 10)
|
||||
group_container.custom_minimum_size = Vector2(300, 250)
|
||||
group_panel.add_child(group_container)
|
||||
|
||||
var group_title = Label.new()
|
||||
group_title.text = "群组管理"
|
||||
group_title.add_theme_font_size_override("font_size", 14)
|
||||
group_container.add_child(group_title)
|
||||
|
||||
var group_buttons = HBoxContainer.new()
|
||||
group_container.add_child(group_buttons)
|
||||
|
||||
create_group_button = Button.new()
|
||||
create_group_button.text = "创建群组"
|
||||
group_buttons.add_child(create_group_button)
|
||||
|
||||
join_group_button = Button.new()
|
||||
join_group_button.text = "加入群组"
|
||||
group_buttons.add_child(join_group_button)
|
||||
|
||||
group_list = ItemList.new()
|
||||
group_list.custom_minimum_size = Vector2(280, 180)
|
||||
group_container.add_child(group_list)
|
||||
|
||||
## 设置管理器
|
||||
func setup_managers() -> void:
|
||||
"""设置各种管理器"""
|
||||
# 延迟初始化管理器以避免性能问题
|
||||
call_deferred("_initialize_managers")
|
||||
|
||||
func _initialize_managers() -> void:
|
||||
"""延迟初始化管理器"""
|
||||
emoji_manager = EmojiManager.new()
|
||||
add_child(emoji_manager)
|
||||
|
||||
dialogue_filter = DialogueFilter.new()
|
||||
add_child(dialogue_filter)
|
||||
|
||||
group_manager = GroupDialogueManager.new()
|
||||
add_child(group_manager)
|
||||
|
||||
history_manager = DialogueHistoryManager.new()
|
||||
add_child(history_manager)
|
||||
|
||||
## 连接信号
|
||||
func connect_signals() -> void:
|
||||
"""连接UI信号"""
|
||||
send_button.pressed.connect(_on_send_button_pressed)
|
||||
message_input.text_submitted.connect(_on_message_submitted)
|
||||
emoji_button.pressed.connect(_on_emoji_button_pressed_toggle)
|
||||
history_button.pressed.connect(_on_history_button_pressed)
|
||||
group_button.pressed.connect(_on_group_button_pressed)
|
||||
close_button.pressed.connect(_on_close_button_pressed)
|
||||
|
||||
# 管理器信号
|
||||
dialogue_filter.message_blocked.connect(_on_message_blocked)
|
||||
group_manager.group_message_received.connect(_on_group_message_received)
|
||||
|
||||
## 开始对话
|
||||
func start_dialogue(target_id: String, target_name: String, is_group: bool = false) -> void:
|
||||
"""
|
||||
开始与指定目标的对话
|
||||
@param target_id: 目标ID
|
||||
@param target_name: 目标名称
|
||||
@param is_group: 是否为群组对话
|
||||
"""
|
||||
current_target_id = target_id
|
||||
current_target_name = target_name
|
||||
is_group_chat = is_group
|
||||
|
||||
# 更新UI
|
||||
if is_group:
|
||||
target_label.text = "群组: " + target_name
|
||||
else:
|
||||
target_label.text = "对话: " + target_name
|
||||
|
||||
# 加载历史记录
|
||||
load_dialogue_history()
|
||||
|
||||
# 显示对话框
|
||||
visible = true
|
||||
message_input.grab_focus()
|
||||
|
||||
## 发送消息
|
||||
func send_message(message: String) -> void:
|
||||
"""
|
||||
发送消息
|
||||
@param message: 消息内容
|
||||
"""
|
||||
if message.strip_edges().is_empty():
|
||||
return
|
||||
|
||||
var sender_id = "player"
|
||||
|
||||
# 过滤消息(添加安全检查)
|
||||
if not dialogue_filter:
|
||||
show_error_message("对话过滤器未初始化")
|
||||
return
|
||||
|
||||
var filter_result = dialogue_filter.filter_message(sender_id, message)
|
||||
if not filter_result.allowed:
|
||||
show_error_message(filter_result.reason)
|
||||
return
|
||||
|
||||
var filtered_message = filter_result.filtered_message
|
||||
|
||||
# 转换表情符号
|
||||
var final_message = EmojiManager.convert_text_to_emoji(filtered_message)
|
||||
|
||||
# 发送消息
|
||||
if is_group_chat:
|
||||
group_manager.send_group_message(current_target_id, sender_id, final_message)
|
||||
else:
|
||||
# 通过NetworkManager发送消息(如果连接到服务器)
|
||||
var network_manager = get_node("/root/Main/NetworkManager")
|
||||
if network_manager and network_manager.is_server_connected():
|
||||
var dialogue_message = MessageProtocol.create_dialogue_send(sender_id, current_target_id, final_message)
|
||||
network_manager.send_message(dialogue_message)
|
||||
|
||||
message_sent.emit(current_target_id, final_message, false)
|
||||
|
||||
# 添加到历史记录
|
||||
history_manager.add_message_to_history(current_target_id, sender_id, final_message)
|
||||
|
||||
# 显示在对话框中
|
||||
display_message(sender_id, final_message)
|
||||
|
||||
# 清空输入框
|
||||
message_input.text = ""
|
||||
|
||||
## 接收消息
|
||||
func receive_message(sender_id: String, message: String) -> void:
|
||||
"""
|
||||
接收消息
|
||||
@param sender_id: 发送者ID
|
||||
@param message: 消息内容
|
||||
"""
|
||||
# 添加到历史记录
|
||||
history_manager.add_message_to_history(current_target_id, sender_id, message)
|
||||
|
||||
# 显示在对话框中
|
||||
display_message(sender_id, message)
|
||||
|
||||
## 显示消息
|
||||
func display_message(sender_id: String, message: String) -> void:
|
||||
"""
|
||||
在对话框中显示消息
|
||||
@param sender_id: 发送者ID
|
||||
@param message: 消息内容
|
||||
"""
|
||||
var timestamp = Time.get_datetime_string_from_system()
|
||||
var sender_name = sender_id if sender_id != "player" else "我"
|
||||
|
||||
var formatted_message = "[color=gray][%s][/color] [color=blue]%s[/color]: %s\n" % [timestamp, sender_name, message]
|
||||
message_display.append_text(formatted_message)
|
||||
|
||||
# 滚动到底部
|
||||
await get_tree().process_frame
|
||||
scroll_container.scroll_vertical = scroll_container.get_v_scroll_bar().max_value
|
||||
|
||||
## 加载对话历史
|
||||
func load_dialogue_history() -> void:
|
||||
"""加载当前目标的对话历史"""
|
||||
message_display.clear()
|
||||
|
||||
var history = history_manager.get_character_history(current_target_id, 20)
|
||||
for message_record in history:
|
||||
var sender_name = message_record.sender if message_record.sender != "player" else "我"
|
||||
var timestamp = Time.get_datetime_string_from_unix_time(message_record.timestamp)
|
||||
var formatted_message = "[color=gray][%s][/color] [color=blue]%s[/color]: %s\n" % [timestamp, sender_name, message_record.message]
|
||||
message_display.append_text(formatted_message)
|
||||
|
||||
## 显示错误消息
|
||||
func show_error_message(error_text: String) -> void:
|
||||
"""
|
||||
显示错误消息
|
||||
@param error_text: 错误文本
|
||||
"""
|
||||
var error_message = "[color=red]系统: %s[/color]\n" % error_text
|
||||
message_display.append_text(error_message)
|
||||
|
||||
## 信号处理函数
|
||||
func _on_send_button_pressed() -> void:
|
||||
"""发送按钮点击处理"""
|
||||
send_message(message_input.text)
|
||||
|
||||
func _on_message_submitted(text: String) -> void:
|
||||
"""消息输入提交处理"""
|
||||
send_message(text)
|
||||
|
||||
func _on_emoji_button_pressed_toggle() -> void:
|
||||
"""表情符号按钮点击处理"""
|
||||
emoji_picker_visible = not emoji_picker_visible
|
||||
emoji_picker.visible = emoji_picker_visible
|
||||
|
||||
# 隐藏其他面板
|
||||
if emoji_picker_visible:
|
||||
history_panel.visible = false
|
||||
group_panel.visible = false
|
||||
history_visible = false
|
||||
group_visible = false
|
||||
|
||||
func _on_emoji_button_pressed(emoji: String) -> void:
|
||||
"""表情符号选择处理"""
|
||||
message_input.text += emoji
|
||||
emoji_manager.add_to_recent(emoji)
|
||||
emoji_selected.emit(emoji)
|
||||
|
||||
# 隐藏选择器
|
||||
emoji_picker.visible = false
|
||||
emoji_picker_visible = false
|
||||
|
||||
# 聚焦输入框
|
||||
message_input.grab_focus()
|
||||
|
||||
func _on_history_button_pressed() -> void:
|
||||
"""历史记录按钮点击处理"""
|
||||
history_visible = not history_visible
|
||||
history_panel.visible = history_visible
|
||||
|
||||
# 隐藏其他面板
|
||||
if history_visible:
|
||||
emoji_picker.visible = false
|
||||
group_panel.visible = false
|
||||
emoji_picker_visible = false
|
||||
group_visible = false
|
||||
|
||||
# 更新历史记录列表
|
||||
update_history_list()
|
||||
|
||||
func _on_group_button_pressed() -> void:
|
||||
"""群组按钮点击处理"""
|
||||
group_visible = not group_visible
|
||||
group_panel.visible = group_visible
|
||||
|
||||
# 隐藏其他面板
|
||||
if group_visible:
|
||||
emoji_picker.visible = false
|
||||
history_panel.visible = false
|
||||
emoji_picker_visible = false
|
||||
history_visible = false
|
||||
|
||||
# 更新群组列表
|
||||
update_group_list()
|
||||
|
||||
func _on_close_button_pressed() -> void:
|
||||
"""关闭按钮点击处理"""
|
||||
visible = false
|
||||
dialogue_closed.emit()
|
||||
|
||||
func _on_message_blocked(user_id: String, message: String, reason: String) -> void:
|
||||
"""消息被阻止处理"""
|
||||
show_error_message("消息被阻止: " + reason)
|
||||
|
||||
func _on_group_message_received(group_id: String, sender_id: String, message: String) -> void:
|
||||
"""群组消息接收处理"""
|
||||
if group_id == current_target_id and is_group_chat:
|
||||
receive_message(sender_id, message)
|
||||
|
||||
## 更新历史记录列表
|
||||
func update_history_list() -> void:
|
||||
"""更新历史记录列表"""
|
||||
history_list.clear()
|
||||
|
||||
var recent_conversations = history_manager.get_recent_conversations(10)
|
||||
for conversation in recent_conversations:
|
||||
var item_text = "%s: %s" % [conversation.character_id, conversation.last_message]
|
||||
if item_text.length() > 50:
|
||||
item_text = item_text.substr(0, 47) + "..."
|
||||
history_list.add_item(item_text)
|
||||
|
||||
## 更新群组列表
|
||||
func update_group_list() -> void:
|
||||
"""更新群组列表"""
|
||||
group_list.clear()
|
||||
|
||||
var player_groups = group_manager.get_player_groups()
|
||||
for group_info in player_groups:
|
||||
var item_text = "%s (%d人)" % [group_info.name, group_info.participant_count]
|
||||
group_list.add_item(item_text)
|
||||
1
scripts/EnhancedDialogueBox.gd.uid
Normal file
1
scripts/EnhancedDialogueBox.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://fch68l3jc8j7
|
||||
243
scripts/ErrorHandler.gd
Normal file
243
scripts/ErrorHandler.gd
Normal file
@@ -0,0 +1,243 @@
|
||||
extends Node
|
||||
## 统一错误处理类
|
||||
## 提供项目中统一的错误处理和日志记录功能
|
||||
|
||||
# 错误级别枚举
|
||||
enum ErrorLevel {
|
||||
INFO,
|
||||
WARNING,
|
||||
ERROR,
|
||||
CRITICAL
|
||||
}
|
||||
|
||||
# 错误类别
|
||||
enum ErrorCategory {
|
||||
NETWORK,
|
||||
GAME_LOGIC,
|
||||
UI,
|
||||
DATA,
|
||||
SYSTEM
|
||||
}
|
||||
|
||||
# 错误处理器实例(单例模式)
|
||||
static var instance: ErrorHandler = null
|
||||
|
||||
# 错误日志
|
||||
var error_log: Array[Dictionary] = []
|
||||
var max_log_entries: int = 1000
|
||||
|
||||
func _init():
|
||||
"""初始化错误处理器"""
|
||||
if instance == null:
|
||||
instance = self
|
||||
|
||||
## 获取错误处理器实例
|
||||
static func get_instance() -> ErrorHandler:
|
||||
"""获取错误处理器单例实例"""
|
||||
if instance == null:
|
||||
instance = ErrorHandler.new()
|
||||
return instance
|
||||
|
||||
## 记录错误
|
||||
static func log_error(
|
||||
message: String,
|
||||
level: ErrorLevel = ErrorLevel.ERROR,
|
||||
category: ErrorCategory = ErrorCategory.SYSTEM,
|
||||
details: Dictionary = {}
|
||||
) -> void:
|
||||
"""
|
||||
记录错误信息
|
||||
@param message: 错误消息
|
||||
@param level: 错误级别
|
||||
@param category: 错误类别
|
||||
@param details: 错误详细信息
|
||||
"""
|
||||
var handler = get_instance()
|
||||
var error_entry = {
|
||||
"timestamp": Time.get_unix_time_from_system(),
|
||||
"level": level,
|
||||
"category": category,
|
||||
"message": message,
|
||||
"details": details
|
||||
}
|
||||
|
||||
# 添加到日志
|
||||
handler.error_log.append(error_entry)
|
||||
|
||||
# 限制日志大小
|
||||
if handler.error_log.size() > handler.max_log_entries:
|
||||
handler.error_log.pop_front()
|
||||
|
||||
# 输出到控制台
|
||||
_print_error(error_entry)
|
||||
|
||||
## 打印错误到控制台
|
||||
static func _print_error(error_entry: Dictionary) -> void:
|
||||
"""将错误打印到控制台"""
|
||||
var level_str = ErrorLevel.keys()[error_entry.level]
|
||||
var category_str = ErrorCategory.keys()[error_entry.category]
|
||||
var timestamp_str = preload("res://scripts/Utils.gd").format_timestamp(error_entry.timestamp)
|
||||
|
||||
var log_message = "[%s] [%s] [%s] %s" % [
|
||||
timestamp_str,
|
||||
level_str,
|
||||
category_str,
|
||||
error_entry.message
|
||||
]
|
||||
|
||||
# 根据错误级别选择输出方式
|
||||
match error_entry.level:
|
||||
ErrorLevel.INFO:
|
||||
print(log_message)
|
||||
ErrorLevel.WARNING:
|
||||
print_rich("[color=yellow]%s[/color]" % log_message)
|
||||
ErrorLevel.ERROR:
|
||||
print_rich("[color=red]%s[/color]" % log_message)
|
||||
ErrorLevel.CRITICAL:
|
||||
print_rich("[color=purple]%s[/color]" % log_message)
|
||||
push_error(error_entry.message)
|
||||
|
||||
## 记录网络错误
|
||||
static func log_network_error(message: String, details: Dictionary = {}) -> void:
|
||||
"""记录网络相关错误"""
|
||||
log_error(message, ErrorLevel.ERROR, ErrorCategory.NETWORK, details)
|
||||
|
||||
## 记录网络警告
|
||||
static func log_network_warning(message: String, details: Dictionary = {}) -> void:
|
||||
"""记录网络相关警告"""
|
||||
log_error(message, ErrorLevel.WARNING, ErrorCategory.NETWORK, details)
|
||||
|
||||
## 记录游戏逻辑错误
|
||||
static func log_game_error(message: String, details: Dictionary = {}) -> void:
|
||||
"""记录游戏逻辑错误"""
|
||||
log_error(message, ErrorLevel.ERROR, ErrorCategory.GAME_LOGIC, details)
|
||||
|
||||
## 记录UI错误
|
||||
static func log_ui_error(message: String, details: Dictionary = {}) -> void:
|
||||
"""记录UI相关错误"""
|
||||
log_error(message, ErrorLevel.ERROR, ErrorCategory.UI, details)
|
||||
|
||||
## 记录数据错误
|
||||
static func log_data_error(message: String, details: Dictionary = {}) -> void:
|
||||
"""记录数据相关错误"""
|
||||
log_error(message, ErrorLevel.ERROR, ErrorCategory.DATA, details)
|
||||
|
||||
## 记录信息
|
||||
static func log_info(message: String, category: ErrorCategory = ErrorCategory.SYSTEM, details: Dictionary = {}) -> void:
|
||||
"""记录信息级别的日志"""
|
||||
log_error(message, ErrorLevel.INFO, category, details)
|
||||
|
||||
## 获取错误日志
|
||||
static func get_error_log() -> Array[Dictionary]:
|
||||
"""获取所有错误日志"""
|
||||
return get_instance().error_log
|
||||
|
||||
## 获取特定级别的错误
|
||||
static func get_errors_by_level(level: ErrorLevel) -> Array[Dictionary]:
|
||||
"""
|
||||
获取特定级别的错误
|
||||
@param level: 错误级别
|
||||
@return: 错误列表
|
||||
"""
|
||||
var handler = get_instance()
|
||||
var filtered_errors: Array[Dictionary] = []
|
||||
|
||||
for error in handler.error_log:
|
||||
if error.level == level:
|
||||
filtered_errors.append(error)
|
||||
|
||||
return filtered_errors
|
||||
|
||||
## 获取特定类别的错误
|
||||
static func get_errors_by_category(category: ErrorCategory) -> Array[Dictionary]:
|
||||
"""
|
||||
获取特定类别的错误
|
||||
@param category: 错误类别
|
||||
@return: 错误列表
|
||||
"""
|
||||
var handler = get_instance()
|
||||
var filtered_errors: Array[Dictionary] = []
|
||||
|
||||
for error in handler.error_log:
|
||||
if error.category == category:
|
||||
filtered_errors.append(error)
|
||||
|
||||
return filtered_errors
|
||||
|
||||
## 清除错误日志
|
||||
static func clear_log() -> void:
|
||||
"""清除所有错误日志"""
|
||||
get_instance().error_log.clear()
|
||||
|
||||
## 导出错误日志
|
||||
static func export_log_to_file(file_path: String = "user://error_log.json") -> bool:
|
||||
"""
|
||||
将错误日志导出到文件
|
||||
@param file_path: 文件路径
|
||||
@return: 是否成功
|
||||
"""
|
||||
var handler = get_instance()
|
||||
var file = FileAccess.open(file_path, FileAccess.WRITE)
|
||||
|
||||
if file:
|
||||
var json_string = JSON.stringify(handler.error_log)
|
||||
file.store_string(json_string)
|
||||
file.close()
|
||||
log_info("Error log exported to: " + file_path)
|
||||
return true
|
||||
else:
|
||||
log_error("Failed to export error log to: " + file_path)
|
||||
return false
|
||||
|
||||
## 处理未捕获的错误
|
||||
static func handle_uncaught_error(error_message: String, stack_trace: Array = []) -> void:
|
||||
"""
|
||||
处理未捕获的错误
|
||||
@param error_message: 错误消息
|
||||
@param stack_trace: 堆栈跟踪
|
||||
"""
|
||||
var details = {
|
||||
"stack_trace": stack_trace,
|
||||
"is_uncaught": true
|
||||
}
|
||||
log_error("Uncaught error: " + error_message, ErrorLevel.CRITICAL, ErrorCategory.SYSTEM, details)
|
||||
|
||||
## 获取错误统计
|
||||
static func get_error_statistics() -> Dictionary:
|
||||
"""
|
||||
获取错误统计信息
|
||||
@return: 统计信息字典
|
||||
"""
|
||||
var handler = get_instance()
|
||||
var stats = {
|
||||
"total_errors": handler.error_log.size(),
|
||||
"by_level": {},
|
||||
"by_category": {},
|
||||
"recent_errors": 0 # 最近1小时的错误数
|
||||
}
|
||||
|
||||
# 初始化计数器
|
||||
for level in ErrorLevel.values():
|
||||
stats.by_level[ErrorLevel.keys()[level]] = 0
|
||||
|
||||
for category in ErrorCategory.values():
|
||||
stats.by_category[ErrorCategory.keys()[category]] = 0
|
||||
|
||||
# 统计错误
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
var one_hour_ago = current_time - 3600 # 1小时前
|
||||
|
||||
for error in handler.error_log:
|
||||
# 按级别统计
|
||||
var level_key = ErrorLevel.keys()[error.level]
|
||||
stats.by_level[level_key] += 1
|
||||
|
||||
# 按类别统计
|
||||
var category_key = ErrorCategory.keys()[error.category]
|
||||
stats.by_category[category_key] += 1
|
||||
|
||||
# 最近错误统计
|
||||
if error.timestamp > one_hour_ago:
|
||||
stats.recent_errors += 1
|
||||
|
||||
return stats
|
||||
1
scripts/ErrorHandler.gd.uid
Normal file
1
scripts/ErrorHandler.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dprpl5ckgohif
|
||||
239
scripts/ErrorNotification.gd
Normal file
239
scripts/ErrorNotification.gd
Normal file
@@ -0,0 +1,239 @@
|
||||
extends Control
|
||||
class_name ErrorNotification
|
||||
## 错误通知系统
|
||||
## 显示网络错误、操作失败等提示信息
|
||||
|
||||
# 通知类型
|
||||
enum NotificationType {
|
||||
INFO,
|
||||
WARNING,
|
||||
ERROR,
|
||||
SUCCESS
|
||||
}
|
||||
|
||||
# UI 元素
|
||||
@onready var notification_panel: Panel = $NotificationPanel
|
||||
@onready var message_label: Label = $NotificationPanel/MessageLabel
|
||||
@onready var timer: Timer = $Timer
|
||||
|
||||
# 信号
|
||||
signal notification_closed()
|
||||
|
||||
# 当前通知配置
|
||||
var current_notification_type: NotificationType = NotificationType.INFO
|
||||
var auto_hide: bool = true
|
||||
var auto_hide_duration: float = 3.0
|
||||
|
||||
func _ready():
|
||||
"""初始化错误通知系统"""
|
||||
# 默认隐藏
|
||||
hide()
|
||||
|
||||
# 连接信号
|
||||
if timer:
|
||||
timer.timeout.connect(_on_timer_timeout)
|
||||
|
||||
# 设置统一的字体样式
|
||||
_setup_font_style()
|
||||
|
||||
print("ErrorNotification initialized")
|
||||
|
||||
## 显示错误通知
|
||||
func show_error(message: String, auto_hide_after: float = 5.0) -> void:
|
||||
"""
|
||||
显示错误通知
|
||||
@param message: 错误消息
|
||||
@param auto_hide_after: 自动隐藏时间(默认5秒,0表示不自动隐藏)
|
||||
"""
|
||||
_show_notification(message, NotificationType.ERROR, auto_hide_after)
|
||||
|
||||
## 显示警告通知
|
||||
func show_warning(message: String, auto_hide_after: float = 3.0) -> void:
|
||||
"""
|
||||
显示警告通知
|
||||
@param message: 警告消息
|
||||
@param auto_hide_after: 自动隐藏时间
|
||||
"""
|
||||
_show_notification(message, NotificationType.WARNING, auto_hide_after)
|
||||
|
||||
## 显示信息通知
|
||||
func show_info(message: String, auto_hide_after: float = 2.0) -> void:
|
||||
"""
|
||||
显示信息通知
|
||||
@param message: 信息消息
|
||||
@param auto_hide_after: 自动隐藏时间
|
||||
"""
|
||||
_show_notification(message, NotificationType.INFO, auto_hide_after)
|
||||
|
||||
## 显示成功通知
|
||||
func show_success(message: String, auto_hide_after: float = 2.0) -> void:
|
||||
"""
|
||||
显示成功通知
|
||||
@param message: 成功消息
|
||||
@param auto_hide_after: 自动隐藏时间
|
||||
"""
|
||||
_show_notification(message, NotificationType.SUCCESS, auto_hide_after)
|
||||
|
||||
## 显示网络错误
|
||||
func show_network_error(error_message: String = "网络连接失败") -> void:
|
||||
"""
|
||||
显示网络错误通知
|
||||
@param error_message: 错误消息
|
||||
"""
|
||||
# 将技术错误转换为用户友好的消息
|
||||
var user_friendly_message = _make_error_user_friendly(error_message)
|
||||
show_error(user_friendly_message, 8.0) # 8秒后自动隐藏
|
||||
|
||||
## 将错误消息转换为用户友好的格式
|
||||
func _make_error_user_friendly(error_message: String) -> String:
|
||||
"""
|
||||
将技术错误消息转换为用户友好的格式
|
||||
@param error_message: 原始错误消息
|
||||
@return: 用户友好的错误消息
|
||||
"""
|
||||
var message = error_message.to_lower()
|
||||
|
||||
# 网络相关错误
|
||||
if "connection" in message or "连接" in message:
|
||||
if "timeout" in message or "超时" in message:
|
||||
return "连接超时,请检查网络连接后重试"
|
||||
elif "refused" in message or "拒绝" in message:
|
||||
return "无法连接到服务器,服务器可能暂时不可用"
|
||||
else:
|
||||
return "网络连接出现问题,请检查网络设置"
|
||||
|
||||
# 认证相关错误
|
||||
elif "auth" in message or "认证" in message or "登录" in message:
|
||||
return "登录失败,请检查用户名或稍后重试"
|
||||
|
||||
# 角色创建相关错误
|
||||
elif "character" in message or "角色" in message:
|
||||
if "exists" in message or "存在" in message:
|
||||
return "角色名称已被使用,请选择其他名称"
|
||||
elif "invalid" in message or "无效" in message:
|
||||
return "角色名称格式不正确,请使用2-20个字符"
|
||||
else:
|
||||
return "角色创建失败,请稍后重试"
|
||||
|
||||
# 服务器相关错误
|
||||
elif "server" in message or "服务器" in message:
|
||||
return "服务器暂时不可用,请稍后重试"
|
||||
|
||||
# 如果无法识别,返回通用友好消息
|
||||
else:
|
||||
return "操作失败,请稍后重试。如果问题持续存在,请联系客服"
|
||||
|
||||
## 显示重连提示
|
||||
func show_reconnecting(attempt: int, max_attempts: int) -> void:
|
||||
"""
|
||||
显示重连提示
|
||||
@param attempt: 当前尝试次数
|
||||
@param max_attempts: 最大尝试次数
|
||||
"""
|
||||
var message = "正在重新连接... (%d/%d)" % [attempt, max_attempts]
|
||||
show_info(message, 0.0) # 不自动隐藏
|
||||
|
||||
## 显示操作失败提示
|
||||
func show_operation_failed(operation: String, reason: String = "") -> void:
|
||||
"""
|
||||
显示操作失败提示
|
||||
@param operation: 操作名称
|
||||
@param reason: 失败原因
|
||||
"""
|
||||
var message = "操作失败: " + operation
|
||||
if not reason.is_empty():
|
||||
message += "\n原因: " + reason
|
||||
show_error(message, 5.0)
|
||||
|
||||
## 隐藏通知
|
||||
func hide_notification() -> void:
|
||||
"""隐藏通知(带淡出效果)"""
|
||||
if timer:
|
||||
timer.stop()
|
||||
|
||||
# 使用UIAnimationManager的淡出动画
|
||||
var tween = UIAnimationManager.fade_out(self, 0.3)
|
||||
if tween:
|
||||
tween.tween_callback(func():
|
||||
notification_closed.emit()
|
||||
)
|
||||
|
||||
## 内部方法:显示通知
|
||||
func _show_notification(message: String, type: NotificationType, auto_hide_after: float) -> void:
|
||||
"""
|
||||
内部方法:显示通知
|
||||
@param message: 消息内容
|
||||
@param type: 通知类型
|
||||
@param auto_hide_after: 自动隐藏时间(0 表示不自动隐藏)
|
||||
"""
|
||||
current_notification_type = type
|
||||
|
||||
# 设置消息文本
|
||||
if message_label:
|
||||
message_label.text = message
|
||||
|
||||
# 设置颜色主题
|
||||
_apply_notification_style(type)
|
||||
|
||||
# 使用UIAnimationManager的动画效果
|
||||
if type == NotificationType.ERROR:
|
||||
# 错误通知使用摇摆+淡入效果
|
||||
UIAnimationManager.fade_slide_in(self, "top", 0.4)
|
||||
# 添加轻微摇摆效果强调错误
|
||||
await get_tree().create_timer(0.4).timeout
|
||||
UIAnimationManager.shake_error(self, 5.0, 0.3)
|
||||
else:
|
||||
# 其他通知使用平滑滑入效果
|
||||
UIAnimationManager.fade_slide_in(self, "top", 0.3)
|
||||
|
||||
# 设置自动隐藏
|
||||
if auto_hide_after > 0.0 and timer:
|
||||
timer.wait_time = auto_hide_after
|
||||
timer.start()
|
||||
|
||||
## 应用通知样式
|
||||
func _apply_notification_style(type: NotificationType) -> void:
|
||||
"""
|
||||
根据通知类型应用样式
|
||||
@param type: 通知类型
|
||||
"""
|
||||
if not notification_panel:
|
||||
return
|
||||
|
||||
# 根据类型设置颜色
|
||||
var style_box = StyleBoxFlat.new()
|
||||
|
||||
match type:
|
||||
NotificationType.ERROR:
|
||||
style_box.bg_color = Color(0.8, 0.2, 0.2, 0.9) # 红色
|
||||
NotificationType.WARNING:
|
||||
style_box.bg_color = Color(0.9, 0.7, 0.2, 0.9) # 黄色
|
||||
NotificationType.INFO:
|
||||
style_box.bg_color = Color(0.2, 0.5, 0.8, 0.9) # 蓝色
|
||||
NotificationType.SUCCESS:
|
||||
style_box.bg_color = Color(0.2, 0.8, 0.3, 0.9) # 绿色
|
||||
|
||||
style_box.corner_radius_top_left = 8
|
||||
style_box.corner_radius_top_right = 8
|
||||
style_box.corner_radius_bottom_left = 8
|
||||
style_box.corner_radius_bottom_right = 8
|
||||
|
||||
notification_panel.add_theme_stylebox_override("panel", style_box)
|
||||
|
||||
## 设置字体样式
|
||||
func _setup_font_style() -> void:
|
||||
"""设置统一的字体样式"""
|
||||
if message_label:
|
||||
# 设置字体大小
|
||||
message_label.add_theme_font_size_override("font_size", 18)
|
||||
# 设置文字颜色
|
||||
message_label.add_theme_color_override("font_color", Color.WHITE)
|
||||
# 添加阴影效果使文字更清晰
|
||||
message_label.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.8))
|
||||
message_label.add_theme_constant_override("shadow_offset_x", 1)
|
||||
message_label.add_theme_constant_override("shadow_offset_y", 1)
|
||||
|
||||
## 定时器超时
|
||||
func _on_timer_timeout() -> void:
|
||||
"""定时器超时,自动隐藏通知"""
|
||||
hide_notification()
|
||||
1
scripts/ErrorNotification.gd.uid
Normal file
1
scripts/ErrorNotification.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cbxi6uspmwlpd
|
||||
583
scripts/FriendSystem.gd
Normal file
583
scripts/FriendSystem.gd
Normal file
@@ -0,0 +1,583 @@
|
||||
extends Node
|
||||
class_name FriendSystem
|
||||
## 好友系统
|
||||
## 管理玩家之间的好友关系和社交功能
|
||||
|
||||
# 好友关系数据结构
|
||||
class FriendRelationship:
|
||||
var friend_id: String
|
||||
var friend_name: String
|
||||
var status: String # "pending", "accepted", "blocked"
|
||||
var added_at: float
|
||||
var last_interaction: float
|
||||
var relationship_level: int = 1 # 关系等级 1-5
|
||||
var interaction_count: int = 0
|
||||
var shared_activities: Array[String] = []
|
||||
|
||||
func _init(id: String, name: String, relationship_status: String = "pending"):
|
||||
friend_id = id
|
||||
friend_name = name
|
||||
status = relationship_status
|
||||
added_at = Time.get_unix_time_from_system()
|
||||
last_interaction = added_at
|
||||
|
||||
# 好友数据存储
|
||||
var friends: Dictionary = {} # friend_id -> FriendRelationship
|
||||
var friend_requests: Dictionary = {} # requester_id -> FriendRelationship
|
||||
var blocked_users: Dictionary = {} # blocked_id -> timestamp
|
||||
var max_friends: int = 100
|
||||
var max_pending_requests: int = 20
|
||||
|
||||
# 数据持久化
|
||||
var friends_file_path: String = "user://friends_data.json"
|
||||
|
||||
# 信号
|
||||
signal friend_request_sent(friend_id: String, friend_name: String)
|
||||
# signal friend_request_received(requester_id: String, requester_name: String) # 暂时未使用
|
||||
signal friend_request_accepted(friend_id: String, friend_name: String)
|
||||
signal friend_request_declined(requester_id: String)
|
||||
signal friend_removed(friend_id: String, friend_name: String)
|
||||
signal friend_blocked(friend_id: String, friend_name: String)
|
||||
signal friend_unblocked(friend_id: String, friend_name: String)
|
||||
signal friend_online(friend_id: String, friend_name: String)
|
||||
signal friend_offline(friend_id: String, friend_name: String)
|
||||
signal relationship_level_changed(friend_id: String, old_level: int, new_level: int)
|
||||
|
||||
func _ready():
|
||||
"""初始化好友系统"""
|
||||
load_friends_data()
|
||||
print("FriendSystem initialized")
|
||||
|
||||
## 发送好友请求
|
||||
func send_friend_request(target_id: String, target_name: String) -> bool:
|
||||
"""
|
||||
向指定玩家发送好友请求
|
||||
@param target_id: 目标玩家ID
|
||||
@param target_name: 目标玩家名称
|
||||
@return: 是否成功发送
|
||||
"""
|
||||
# 验证输入
|
||||
if target_id.is_empty() or target_name.is_empty():
|
||||
print("Invalid target for friend request")
|
||||
return false
|
||||
|
||||
# 检查是否已经是好友
|
||||
if friends.has(target_id):
|
||||
print("Already friends with: ", target_name)
|
||||
return false
|
||||
|
||||
# 检查是否已发送过请求
|
||||
if friend_requests.has(target_id):
|
||||
print("Friend request already sent to: ", target_name)
|
||||
return false
|
||||
|
||||
# 检查是否被屏蔽
|
||||
if blocked_users.has(target_id):
|
||||
print("Cannot send friend request to blocked user: ", target_name)
|
||||
return false
|
||||
|
||||
# 检查好友数量限制
|
||||
if friends.size() >= max_friends:
|
||||
print("Friend list is full")
|
||||
return false
|
||||
|
||||
# 检查待处理请求数量
|
||||
if friend_requests.size() >= max_pending_requests:
|
||||
print("Too many pending friend requests")
|
||||
return false
|
||||
|
||||
# 创建好友请求
|
||||
var relationship = FriendRelationship.new(target_id, target_name, "pending")
|
||||
friend_requests[target_id] = relationship
|
||||
|
||||
# 保存数据
|
||||
save_friends_data()
|
||||
|
||||
# 发射信号
|
||||
friend_request_sent.emit(target_id, target_name)
|
||||
|
||||
print("Friend request sent to: ", target_name)
|
||||
return true
|
||||
|
||||
## 接受好友请求
|
||||
func accept_friend_request(requester_id: String) -> bool:
|
||||
"""
|
||||
接受好友请求
|
||||
@param requester_id: 请求者ID
|
||||
@return: 是否成功接受
|
||||
"""
|
||||
if not friend_requests.has(requester_id):
|
||||
print("No friend request from: ", requester_id)
|
||||
return false
|
||||
|
||||
var relationship = friend_requests[requester_id]
|
||||
|
||||
# 检查好友数量限制
|
||||
if friends.size() >= max_friends:
|
||||
print("Friend list is full, cannot accept request")
|
||||
return false
|
||||
|
||||
# 移动到好友列表
|
||||
relationship.status = "accepted"
|
||||
friends[requester_id] = relationship
|
||||
friend_requests.erase(requester_id)
|
||||
|
||||
# 保存数据
|
||||
save_friends_data()
|
||||
|
||||
# 发射信号
|
||||
friend_request_accepted.emit(requester_id, relationship.friend_name)
|
||||
|
||||
print("Friend request accepted from: ", relationship.friend_name)
|
||||
return true
|
||||
|
||||
## 拒绝好友请求
|
||||
func decline_friend_request(requester_id: String) -> bool:
|
||||
"""
|
||||
拒绝好友请求
|
||||
@param requester_id: 请求者ID
|
||||
@return: 是否成功拒绝
|
||||
"""
|
||||
if not friend_requests.has(requester_id):
|
||||
print("No friend request from: ", requester_id)
|
||||
return false
|
||||
|
||||
var relationship = friend_requests[requester_id]
|
||||
friend_requests.erase(requester_id)
|
||||
|
||||
# 保存数据
|
||||
save_friends_data()
|
||||
|
||||
# 发射信号
|
||||
friend_request_declined.emit(requester_id)
|
||||
|
||||
print("Friend request declined from: ", relationship.friend_name)
|
||||
return true
|
||||
|
||||
## 移除好友
|
||||
func remove_friend(friend_id: String) -> bool:
|
||||
"""
|
||||
移除好友
|
||||
@param friend_id: 好友ID
|
||||
@return: 是否成功移除
|
||||
"""
|
||||
if not friends.has(friend_id):
|
||||
print("Not friends with: ", friend_id)
|
||||
return false
|
||||
|
||||
var relationship = friends[friend_id]
|
||||
friends.erase(friend_id)
|
||||
|
||||
# 保存数据
|
||||
save_friends_data()
|
||||
|
||||
# 发射信号
|
||||
friend_removed.emit(friend_id, relationship.friend_name)
|
||||
|
||||
print("Friend removed: ", relationship.friend_name)
|
||||
return true
|
||||
|
||||
## 屏蔽用户
|
||||
func block_user(user_id: String, user_name: String) -> bool:
|
||||
"""
|
||||
屏蔽用户
|
||||
@param user_id: 用户ID
|
||||
@param user_name: 用户名称
|
||||
@return: 是否成功屏蔽
|
||||
"""
|
||||
if blocked_users.has(user_id):
|
||||
print("User already blocked: ", user_name)
|
||||
return false
|
||||
|
||||
# 添加到屏蔽列表
|
||||
blocked_users[user_id] = Time.get_unix_time_from_system()
|
||||
|
||||
# 如果是好友,移除好友关系
|
||||
if friends.has(user_id):
|
||||
remove_friend(user_id)
|
||||
|
||||
# 如果有待处理的好友请求,移除
|
||||
if friend_requests.has(user_id):
|
||||
friend_requests.erase(user_id)
|
||||
|
||||
# 保存数据
|
||||
save_friends_data()
|
||||
|
||||
# 发射信号
|
||||
friend_blocked.emit(user_id, user_name)
|
||||
|
||||
print("User blocked: ", user_name)
|
||||
return true
|
||||
|
||||
## 解除屏蔽
|
||||
func unblock_user(user_id: String, user_name: String) -> bool:
|
||||
"""
|
||||
解除用户屏蔽
|
||||
@param user_id: 用户ID
|
||||
@param user_name: 用户名称
|
||||
@return: 是否成功解除屏蔽
|
||||
"""
|
||||
if not blocked_users.has(user_id):
|
||||
print("User not blocked: ", user_name)
|
||||
return false
|
||||
|
||||
blocked_users.erase(user_id)
|
||||
|
||||
# 保存数据
|
||||
save_friends_data()
|
||||
|
||||
# 发射信号
|
||||
friend_unblocked.emit(user_id, user_name)
|
||||
|
||||
print("User unblocked: ", user_name)
|
||||
return true
|
||||
|
||||
## 记录互动
|
||||
func record_interaction(friend_id: String, activity_type: String = "chat") -> void:
|
||||
"""
|
||||
记录与好友的互动
|
||||
@param friend_id: 好友ID
|
||||
@param activity_type: 活动类型
|
||||
"""
|
||||
if not friends.has(friend_id):
|
||||
return
|
||||
|
||||
var relationship = friends[friend_id]
|
||||
relationship.last_interaction = Time.get_unix_time_from_system()
|
||||
relationship.interaction_count += 1
|
||||
|
||||
# 添加到共同活动
|
||||
if not activity_type in relationship.shared_activities:
|
||||
relationship.shared_activities.append(activity_type)
|
||||
|
||||
# 检查关系等级提升
|
||||
_check_relationship_level_up(friend_id)
|
||||
|
||||
# 保存数据
|
||||
save_friends_data()
|
||||
|
||||
## 检查关系等级提升
|
||||
func _check_relationship_level_up(friend_id: String) -> void:
|
||||
"""
|
||||
检查并处理关系等级提升
|
||||
@param friend_id: 好友ID
|
||||
"""
|
||||
if not friends.has(friend_id):
|
||||
return
|
||||
|
||||
var relationship = friends[friend_id]
|
||||
var old_level = relationship.relationship_level
|
||||
var new_level = _calculate_relationship_level(relationship)
|
||||
|
||||
if new_level > old_level:
|
||||
relationship.relationship_level = new_level
|
||||
relationship_level_changed.emit(friend_id, old_level, new_level)
|
||||
print("Relationship level increased with ", relationship.friend_name, ": ", old_level, " -> ", new_level)
|
||||
|
||||
## 计算关系等级
|
||||
func _calculate_relationship_level(relationship: FriendRelationship) -> int:
|
||||
"""
|
||||
根据互动数据计算关系等级
|
||||
@param relationship: 好友关系数据
|
||||
@return: 关系等级 (1-5)
|
||||
"""
|
||||
var level = 1
|
||||
|
||||
# 基于互动次数
|
||||
if relationship.interaction_count >= 10:
|
||||
level = 2
|
||||
if relationship.interaction_count >= 50:
|
||||
level = 3
|
||||
if relationship.interaction_count >= 100:
|
||||
level = 4
|
||||
if relationship.interaction_count >= 200:
|
||||
level = 5
|
||||
|
||||
# 基于共同活动种类
|
||||
var activity_bonus = min(relationship.shared_activities.size() / 3.0, 1)
|
||||
level = min(level + activity_bonus, 5)
|
||||
|
||||
# 基于关系持续时间(天数)
|
||||
var days_since_added = (Time.get_unix_time_from_system() - relationship.added_at) / 86400.0
|
||||
if days_since_added >= 7: # 一周
|
||||
level = min(level + 1, 5)
|
||||
|
||||
return level
|
||||
|
||||
## 获取好友列表
|
||||
func get_friends_list() -> Array[Dictionary]:
|
||||
"""
|
||||
获取好友列表
|
||||
@return: 好友信息数组
|
||||
"""
|
||||
var friends_list = []
|
||||
|
||||
for friend_id in friends:
|
||||
var relationship = friends[friend_id]
|
||||
friends_list.append({
|
||||
"id": friend_id,
|
||||
"name": relationship.friend_name,
|
||||
"status": relationship.status,
|
||||
"relationship_level": relationship.relationship_level,
|
||||
"last_interaction": relationship.last_interaction,
|
||||
"interaction_count": relationship.interaction_count,
|
||||
"shared_activities": relationship.shared_activities.duplicate(),
|
||||
"added_at": relationship.added_at
|
||||
})
|
||||
|
||||
# 按关系等级和最后互动时间排序
|
||||
friends_list.sort_custom(func(a, b):
|
||||
if a.relationship_level != b.relationship_level:
|
||||
return a.relationship_level > b.relationship_level
|
||||
return a.last_interaction > b.last_interaction
|
||||
)
|
||||
|
||||
return friends_list
|
||||
|
||||
## 获取好友请求列表
|
||||
func get_friend_requests() -> Array[Dictionary]:
|
||||
"""
|
||||
获取待处理的好友请求列表
|
||||
@return: 好友请求信息数组
|
||||
"""
|
||||
var requests = []
|
||||
|
||||
for requester_id in friend_requests:
|
||||
var relationship = friend_requests[requester_id]
|
||||
requests.append({
|
||||
"id": requester_id,
|
||||
"name": relationship.friend_name,
|
||||
"requested_at": relationship.added_at
|
||||
})
|
||||
|
||||
# 按请求时间排序(最新的在前)
|
||||
requests.sort_custom(func(a, b): return a.requested_at > b.requested_at)
|
||||
|
||||
return requests
|
||||
|
||||
## 获取屏蔽列表
|
||||
func get_blocked_users() -> Array[Dictionary]:
|
||||
"""
|
||||
获取屏蔽用户列表
|
||||
@return: 屏蔽用户信息数组
|
||||
"""
|
||||
var blocked = []
|
||||
|
||||
for user_id in blocked_users:
|
||||
blocked.append({
|
||||
"id": user_id,
|
||||
"blocked_at": blocked_users[user_id]
|
||||
})
|
||||
|
||||
# 按屏蔽时间排序(最新的在前)
|
||||
blocked.sort_custom(func(a, b): return a.blocked_at > b.blocked_at)
|
||||
|
||||
return blocked
|
||||
|
||||
## 检查是否为好友
|
||||
func is_friend(user_id: String) -> bool:
|
||||
"""
|
||||
检查是否为好友
|
||||
@param user_id: 用户ID
|
||||
@return: 是否为好友
|
||||
"""
|
||||
return friends.has(user_id) and friends[user_id].status == "accepted"
|
||||
|
||||
## 检查是否被屏蔽
|
||||
func is_blocked(user_id: String) -> bool:
|
||||
"""
|
||||
检查用户是否被屏蔽
|
||||
@param user_id: 用户ID
|
||||
@return: 是否被屏蔽
|
||||
"""
|
||||
return blocked_users.has(user_id)
|
||||
|
||||
## 获取好友信息
|
||||
func get_friend_info(friend_id: String) -> Dictionary:
|
||||
"""
|
||||
获取好友详细信息
|
||||
@param friend_id: 好友ID
|
||||
@return: 好友信息字典
|
||||
"""
|
||||
if not friends.has(friend_id):
|
||||
return {}
|
||||
|
||||
var relationship = friends[friend_id]
|
||||
return {
|
||||
"id": friend_id,
|
||||
"name": relationship.friend_name,
|
||||
"status": relationship.status,
|
||||
"relationship_level": relationship.relationship_level,
|
||||
"last_interaction": relationship.last_interaction,
|
||||
"interaction_count": relationship.interaction_count,
|
||||
"shared_activities": relationship.shared_activities.duplicate(),
|
||||
"added_at": relationship.added_at,
|
||||
"days_since_added": (Time.get_unix_time_from_system() - relationship.added_at) / 86400.0
|
||||
}
|
||||
|
||||
## 搜索好友
|
||||
func search_friends(query: String) -> Array[Dictionary]:
|
||||
"""
|
||||
搜索好友
|
||||
@param query: 搜索关键词
|
||||
@return: 匹配的好友信息数组
|
||||
"""
|
||||
var results = []
|
||||
var search_query = query.to_lower()
|
||||
|
||||
for friend_id in friends:
|
||||
var relationship = friends[friend_id]
|
||||
if relationship.friend_name.to_lower().contains(search_query):
|
||||
results.append(get_friend_info(friend_id))
|
||||
|
||||
return results
|
||||
|
||||
## 获取在线好友
|
||||
func get_online_friends(online_characters: Array[String]) -> Array[Dictionary]:
|
||||
"""
|
||||
获取当前在线的好友列表
|
||||
@param online_characters: 在线角色ID数组
|
||||
@return: 在线好友信息数组
|
||||
"""
|
||||
var online_friends = []
|
||||
|
||||
for friend_id in friends:
|
||||
if friend_id in online_characters:
|
||||
online_friends.append(get_friend_info(friend_id))
|
||||
|
||||
return online_friends
|
||||
|
||||
## 处理好友上线
|
||||
func handle_friend_online(friend_id: String) -> void:
|
||||
"""
|
||||
处理好友上线事件
|
||||
@param friend_id: 好友ID
|
||||
"""
|
||||
if friends.has(friend_id):
|
||||
var relationship = friends[friend_id]
|
||||
friend_online.emit(friend_id, relationship.friend_name)
|
||||
print("Friend came online: ", relationship.friend_name)
|
||||
|
||||
## 处理好友下线
|
||||
func handle_friend_offline(friend_id: String) -> void:
|
||||
"""
|
||||
处理好友下线事件
|
||||
@param friend_id: 好友ID
|
||||
"""
|
||||
if friends.has(friend_id):
|
||||
var relationship = friends[friend_id]
|
||||
friend_offline.emit(friend_id, relationship.friend_name)
|
||||
print("Friend went offline: ", relationship.friend_name)
|
||||
|
||||
## 保存好友数据
|
||||
func save_friends_data() -> void:
|
||||
"""保存好友数据到本地文件"""
|
||||
var data = {
|
||||
"friends": {},
|
||||
"friend_requests": {},
|
||||
"blocked_users": blocked_users
|
||||
}
|
||||
|
||||
# 序列化好友数据
|
||||
for friend_id in friends:
|
||||
var relationship = friends[friend_id]
|
||||
data.friends[friend_id] = {
|
||||
"friend_name": relationship.friend_name,
|
||||
"status": relationship.status,
|
||||
"added_at": relationship.added_at,
|
||||
"last_interaction": relationship.last_interaction,
|
||||
"relationship_level": relationship.relationship_level,
|
||||
"interaction_count": relationship.interaction_count,
|
||||
"shared_activities": relationship.shared_activities
|
||||
}
|
||||
|
||||
# 序列化好友请求数据
|
||||
for requester_id in friend_requests:
|
||||
var relationship = friend_requests[requester_id]
|
||||
data.friend_requests[requester_id] = {
|
||||
"friend_name": relationship.friend_name,
|
||||
"status": relationship.status,
|
||||
"added_at": relationship.added_at
|
||||
}
|
||||
|
||||
var file = FileAccess.open(friends_file_path, FileAccess.WRITE)
|
||||
if file:
|
||||
var json_string = JSON.stringify(data)
|
||||
file.store_string(json_string)
|
||||
file.close()
|
||||
print("Friends data saved")
|
||||
else:
|
||||
print("Failed to save friends data")
|
||||
|
||||
## 加载好友数据
|
||||
func load_friends_data() -> void:
|
||||
"""从本地文件加载好友数据"""
|
||||
if not FileAccess.file_exists(friends_file_path):
|
||||
print("No friends data file found, starting fresh")
|
||||
return
|
||||
|
||||
var file = FileAccess.open(friends_file_path, FileAccess.READ)
|
||||
if file:
|
||||
var json_string = file.get_as_text()
|
||||
file.close()
|
||||
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(json_string)
|
||||
|
||||
if parse_result == OK:
|
||||
var data = json.data
|
||||
|
||||
# 加载好友数据
|
||||
if data.has("friends"):
|
||||
for friend_id in data.friends:
|
||||
var friend_data = data.friends[friend_id]
|
||||
var relationship = FriendRelationship.new(friend_id, friend_data.friend_name, friend_data.status)
|
||||
relationship.added_at = friend_data.get("added_at", Time.get_unix_time_from_system())
|
||||
relationship.last_interaction = friend_data.get("last_interaction", relationship.added_at)
|
||||
relationship.relationship_level = friend_data.get("relationship_level", 1)
|
||||
relationship.interaction_count = friend_data.get("interaction_count", 0)
|
||||
relationship.shared_activities = friend_data.get("shared_activities", [])
|
||||
friends[friend_id] = relationship
|
||||
|
||||
# 加载好友请求数据
|
||||
if data.has("friend_requests"):
|
||||
for requester_id in data.friend_requests:
|
||||
var request_data = data.friend_requests[requester_id]
|
||||
var relationship = FriendRelationship.new(requester_id, request_data.friend_name, request_data.status)
|
||||
relationship.added_at = request_data.get("added_at", Time.get_unix_time_from_system())
|
||||
friend_requests[requester_id] = relationship
|
||||
|
||||
# 加载屏蔽数据
|
||||
if data.has("blocked_users"):
|
||||
blocked_users = data.blocked_users
|
||||
|
||||
print("Friends data loaded: ", friends.size(), " friends, ", friend_requests.size(), " requests, ", blocked_users.size(), " blocked")
|
||||
else:
|
||||
print("Failed to parse friends data JSON")
|
||||
else:
|
||||
print("Failed to open friends data file")
|
||||
|
||||
## 获取统计信息
|
||||
func get_statistics() -> Dictionary:
|
||||
"""
|
||||
获取好友系统统计信息
|
||||
@return: 统计信息字典
|
||||
"""
|
||||
var level_counts = {}
|
||||
var total_interactions = 0
|
||||
|
||||
for friend_id in friends:
|
||||
var relationship = friends[friend_id]
|
||||
var level = relationship.relationship_level
|
||||
level_counts[level] = level_counts.get(level, 0) + 1
|
||||
total_interactions += relationship.interaction_count
|
||||
|
||||
return {
|
||||
"total_friends": friends.size(),
|
||||
"pending_requests": friend_requests.size(),
|
||||
"blocked_users": blocked_users.size(),
|
||||
"total_interactions": total_interactions,
|
||||
"level_distribution": level_counts,
|
||||
"max_friends": max_friends,
|
||||
"max_pending_requests": max_pending_requests
|
||||
}
|
||||
1
scripts/FriendSystem.gd.uid
Normal file
1
scripts/FriendSystem.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://1amqu0q2sosf
|
||||
205
scripts/GameConfig.gd
Normal file
205
scripts/GameConfig.gd
Normal file
@@ -0,0 +1,205 @@
|
||||
extends Node
|
||||
## 游戏配置管理类
|
||||
## 集中管理游戏的各种配置参数
|
||||
|
||||
# 网络配置
|
||||
const NETWORK = {
|
||||
"server_url": "ws://localhost:8080",
|
||||
"connection_timeout": 10.0,
|
||||
"max_reconnect_attempts": 3,
|
||||
"reconnect_base_delay": 1.0,
|
||||
"heartbeat_interval": 30.0
|
||||
}
|
||||
|
||||
# 角色配置
|
||||
const CHARACTER = {
|
||||
"move_speed": 200.0,
|
||||
"interaction_range": 80.0,
|
||||
"name_min_length": 2,
|
||||
"name_max_length": 20,
|
||||
"position_sync_threshold": 1.0 # 位置同步的最小距离阈值
|
||||
}
|
||||
|
||||
# UI配置
|
||||
const UI = {
|
||||
"notification_duration": 5.0,
|
||||
"error_notification_duration": 8.0,
|
||||
"loading_timeout": 15.0,
|
||||
"font_sizes": {
|
||||
"small": 12,
|
||||
"normal": 16,
|
||||
"large": 20,
|
||||
"title": 24
|
||||
},
|
||||
"animations": {
|
||||
"enable_animations": true,
|
||||
"animation_speed": 1.0,
|
||||
"reduce_motion": false
|
||||
},
|
||||
"accessibility": {
|
||||
"high_contrast": false,
|
||||
"large_text": false,
|
||||
"screen_reader_support": false
|
||||
},
|
||||
"mobile_optimizations": {
|
||||
"larger_touch_targets": true,
|
||||
"haptic_feedback": true,
|
||||
"auto_zoom_inputs": true
|
||||
},
|
||||
"performance": {
|
||||
"reduce_transparency": false,
|
||||
"disable_particles": false,
|
||||
"low_quality_mode": false
|
||||
}
|
||||
}
|
||||
|
||||
# 场景配置
|
||||
const SCENE = {
|
||||
"world_size": Vector2(2000, 1500),
|
||||
"spawn_position": Vector2(1000, 750),
|
||||
"camera_follow_speed": 5.0,
|
||||
"camera_zoom_speed": 2.0,
|
||||
"camera_min_zoom": 0.5,
|
||||
"camera_max_zoom": 2.0
|
||||
}
|
||||
|
||||
# 输入配置
|
||||
const INPUT = {
|
||||
"virtual_joystick_size": 100,
|
||||
"virtual_button_size": 80,
|
||||
"touch_deadzone": 0.1
|
||||
}
|
||||
|
||||
# 动画配置
|
||||
const ANIMATION = {
|
||||
"default_tween_duration": 0.3,
|
||||
"position_smooth_duration": 0.2,
|
||||
"ui_fade_duration": 0.25,
|
||||
"camera_reset_duration": 0.5
|
||||
}
|
||||
|
||||
# 调试配置
|
||||
const DEBUG = {
|
||||
"enable_debug_prints": true,
|
||||
"show_collision_shapes": false,
|
||||
"show_fps": false,
|
||||
"log_network_messages": true
|
||||
}
|
||||
|
||||
# 性能配置
|
||||
const PERFORMANCE = {
|
||||
"max_characters_visible": 50,
|
||||
"update_frequency": 60, # FPS
|
||||
"network_sync_rate": 20, # 网络同步频率
|
||||
"physics_fps": 60
|
||||
}
|
||||
|
||||
## 获取网络配置
|
||||
static func get_network_config() -> Dictionary:
|
||||
"""获取网络相关配置"""
|
||||
return NETWORK
|
||||
|
||||
## 获取角色配置
|
||||
static func get_character_config() -> Dictionary:
|
||||
"""获取角色相关配置"""
|
||||
return CHARACTER
|
||||
|
||||
## 获取UI配置
|
||||
static func get_ui_config() -> Dictionary:
|
||||
"""获取UI相关配置"""
|
||||
return UI
|
||||
|
||||
## 获取场景配置
|
||||
static func get_scene_config() -> Dictionary:
|
||||
"""获取场景相关配置"""
|
||||
return SCENE
|
||||
|
||||
## 获取输入配置
|
||||
static func get_input_config() -> Dictionary:
|
||||
"""获取输入相关配置"""
|
||||
return INPUT
|
||||
|
||||
## 获取动画配置
|
||||
static func get_animation_config() -> Dictionary:
|
||||
"""获取动画相关配置"""
|
||||
return ANIMATION
|
||||
|
||||
## 获取调试配置
|
||||
static func get_debug_config() -> Dictionary:
|
||||
"""获取调试相关配置"""
|
||||
return DEBUG
|
||||
|
||||
## 获取性能配置
|
||||
static func get_performance_config() -> Dictionary:
|
||||
"""获取性能相关配置"""
|
||||
return PERFORMANCE
|
||||
|
||||
## 是否启用调试模式
|
||||
static func is_debug_enabled() -> bool:
|
||||
"""检查是否启用调试模式"""
|
||||
return DEBUG.enable_debug_prints
|
||||
|
||||
## 获取服务器URL
|
||||
static func get_server_url() -> String:
|
||||
"""获取服务器URL"""
|
||||
return NETWORK.server_url
|
||||
|
||||
## 获取角色移动速度
|
||||
static func get_character_move_speed() -> float:
|
||||
"""获取角色移动速度"""
|
||||
return CHARACTER.move_speed
|
||||
|
||||
## 获取交互范围
|
||||
static func get_interaction_range() -> float:
|
||||
"""获取角色交互范围"""
|
||||
return CHARACTER.interaction_range
|
||||
|
||||
## 验证角色名称长度
|
||||
static func is_valid_character_name_length(character_name: String) -> bool:
|
||||
"""
|
||||
验证角色名称长度是否有效
|
||||
@param character_name: 角色名称
|
||||
@return: 是否有效
|
||||
"""
|
||||
var length = character_name.length()
|
||||
return length >= CHARACTER.name_min_length and length <= CHARACTER.name_max_length
|
||||
|
||||
## 获取默认生成位置
|
||||
static func get_default_spawn_position() -> Vector2:
|
||||
"""获取默认角色生成位置"""
|
||||
return SCENE.spawn_position
|
||||
|
||||
## 获取世界大小
|
||||
static func get_world_size() -> Vector2:
|
||||
"""获取游戏世界大小"""
|
||||
return SCENE.world_size
|
||||
|
||||
## 获取通知显示时长
|
||||
static func get_notification_duration(is_error: bool = false) -> float:
|
||||
"""
|
||||
获取通知显示时长
|
||||
@param is_error: 是否为错误通知
|
||||
@return: 显示时长(秒)
|
||||
"""
|
||||
return UI.error_notification_duration if is_error else UI.notification_duration
|
||||
|
||||
## 获取相机配置
|
||||
static func get_camera_config() -> Dictionary:
|
||||
"""获取相机相关配置"""
|
||||
return {
|
||||
"follow_speed": SCENE.camera_follow_speed,
|
||||
"zoom_speed": SCENE.camera_zoom_speed,
|
||||
"min_zoom": SCENE.camera_min_zoom,
|
||||
"max_zoom": SCENE.camera_max_zoom,
|
||||
"reset_duration": ANIMATION.camera_reset_duration
|
||||
}
|
||||
|
||||
## 获取网络同步配置
|
||||
static func get_network_sync_config() -> Dictionary:
|
||||
"""获取网络同步相关配置"""
|
||||
return {
|
||||
"sync_rate": PERFORMANCE.network_sync_rate,
|
||||
"position_threshold": CHARACTER.position_sync_threshold,
|
||||
"timeout": NETWORK.connection_timeout,
|
||||
"max_reconnects": NETWORK.max_reconnect_attempts
|
||||
}
|
||||
1
scripts/GameConfig.gd.uid
Normal file
1
scripts/GameConfig.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://rrtsch4kftjg
|
||||
85
scripts/GameStateManager.gd
Normal file
85
scripts/GameStateManager.gd
Normal file
@@ -0,0 +1,85 @@
|
||||
extends Node
|
||||
class_name GameStateManager
|
||||
## 游戏状态管理器
|
||||
## 负责管理游戏的全局状态和场景切换
|
||||
|
||||
# 游戏状态枚举
|
||||
enum GameState {
|
||||
LOGIN, # 登录状态
|
||||
CHARACTER_CREATION, # 角色创建状态
|
||||
IN_GAME, # 游戏中
|
||||
DISCONNECTED # 断线状态
|
||||
}
|
||||
|
||||
# 信号定义
|
||||
signal state_changed(old_state: GameState, new_state: GameState)
|
||||
|
||||
# 当前状态
|
||||
var current_state: GameState = GameState.LOGIN
|
||||
|
||||
# 玩家数据
|
||||
var player_data: Dictionary = {}
|
||||
|
||||
# 数据文件路径
|
||||
const SAVE_PATH = "user://player_data.json"
|
||||
|
||||
func _ready():
|
||||
print("GameStateManager initialized")
|
||||
print("Initial state: ", GameState.keys()[current_state])
|
||||
|
||||
# 尝试加载玩家数据
|
||||
load_player_data()
|
||||
|
||||
func change_state(new_state: GameState) -> void:
|
||||
"""切换游戏状态"""
|
||||
if new_state == current_state:
|
||||
return
|
||||
|
||||
var old_state = current_state
|
||||
current_state = new_state
|
||||
|
||||
print("State changed: ", GameState.keys()[old_state], " -> ", GameState.keys()[new_state])
|
||||
state_changed.emit(old_state, new_state)
|
||||
|
||||
func save_player_data() -> void:
|
||||
"""保存玩家数据到本地"""
|
||||
var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
|
||||
if file:
|
||||
var json_string = JSON.stringify(player_data)
|
||||
file.store_string(json_string)
|
||||
file.close()
|
||||
print("Player data saved")
|
||||
else:
|
||||
print("Failed to save player data")
|
||||
|
||||
func load_player_data() -> Dictionary:
|
||||
"""从本地加载玩家数据"""
|
||||
if not FileAccess.file_exists(SAVE_PATH):
|
||||
print("No saved player data found")
|
||||
return {}
|
||||
|
||||
var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
|
||||
if file:
|
||||
var json_string = file.get_as_text()
|
||||
file.close()
|
||||
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(json_string)
|
||||
|
||||
if parse_result == OK:
|
||||
player_data = json.data
|
||||
print("Player data loaded: ", player_data.keys())
|
||||
return player_data
|
||||
else:
|
||||
print("Failed to parse player data")
|
||||
else:
|
||||
print("Failed to open player data file")
|
||||
|
||||
return {}
|
||||
|
||||
func clear_player_data() -> void:
|
||||
"""清除玩家数据"""
|
||||
player_data.clear()
|
||||
if FileAccess.file_exists(SAVE_PATH):
|
||||
DirAccess.remove_absolute(SAVE_PATH)
|
||||
print("Player data cleared")
|
||||
1
scripts/GameStateManager.gd.uid
Normal file
1
scripts/GameStateManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bi68fb55yixi3
|
||||
550
scripts/GameStatistics.gd
Normal file
550
scripts/GameStatistics.gd
Normal file
@@ -0,0 +1,550 @@
|
||||
extends Node
|
||||
class_name GameStatistics
|
||||
## 游戏统计和分析功能
|
||||
## 收集和分析游戏整体运行数据和玩家统计信息
|
||||
|
||||
# 统计数据类型
|
||||
enum StatType {
|
||||
PLAYER_COUNT, # 玩家数量
|
||||
SESSION_DURATION, # 会话时长
|
||||
FEATURE_USAGE, # 功能使用率
|
||||
PERFORMANCE, # 性能指标
|
||||
ERROR_RATE, # 错误率
|
||||
ENGAGEMENT, # 参与度
|
||||
RETENTION, # 留存率
|
||||
SOCIAL_ACTIVITY # 社交活动
|
||||
}
|
||||
|
||||
# 统计数据结构
|
||||
class GameStat:
|
||||
var stat_type: StatType
|
||||
var value: float
|
||||
var timestamp: float
|
||||
var metadata: Dictionary = {}
|
||||
|
||||
func _init(type: StatType, val: float, meta: Dictionary = {}):
|
||||
stat_type = type
|
||||
value = val
|
||||
timestamp = Time.get_unix_time_from_system()
|
||||
metadata = meta.duplicate()
|
||||
|
||||
# 数据存储
|
||||
var statistics_history: Array[GameStat] = []
|
||||
var daily_statistics: Dictionary = {} # date_string -> Dictionary
|
||||
var feature_usage_stats: Dictionary = {}
|
||||
var performance_metrics: Dictionary = {}
|
||||
var player_statistics: Dictionary = {}
|
||||
|
||||
# 配置
|
||||
var max_history_entries: int = 10000
|
||||
var statistics_enabled: bool = true
|
||||
var collection_interval: float = 300.0 # 5分钟收集一次
|
||||
|
||||
# 引用其他系统
|
||||
var user_behavior_analytics: UserBehaviorAnalytics
|
||||
var social_manager: SocialManager
|
||||
|
||||
# 数据持久化
|
||||
var stats_file_path: String = "user://game_statistics.json"
|
||||
|
||||
# 信号
|
||||
signal statistic_recorded(stat_type: StatType, value: float)
|
||||
signal daily_report_generated(date: String, report: Dictionary)
|
||||
signal performance_alert(metric: String, value: float, threshold: float)
|
||||
|
||||
func _ready():
|
||||
"""初始化游戏统计系统"""
|
||||
_load_statistics_data()
|
||||
|
||||
# 设置定时统计收集
|
||||
var collection_timer = Timer.new()
|
||||
collection_timer.wait_time = collection_interval
|
||||
collection_timer.timeout.connect(_collect_periodic_statistics)
|
||||
collection_timer.autostart = true
|
||||
add_child(collection_timer)
|
||||
|
||||
# 设置每日报告生成
|
||||
var daily_timer = Timer.new()
|
||||
daily_timer.wait_time = 86400.0 # 24小时
|
||||
daily_timer.timeout.connect(_generate_daily_report)
|
||||
daily_timer.autostart = true
|
||||
add_child(daily_timer)
|
||||
|
||||
print("GameStatistics initialized")
|
||||
|
||||
## 设置系统引用
|
||||
func set_system_references(uba: UserBehaviorAnalytics, sm: SocialManager) -> void:
|
||||
"""
|
||||
设置其他系统的引用
|
||||
@param uba: 用户行为分析系统
|
||||
@param sm: 社交管理器
|
||||
"""
|
||||
user_behavior_analytics = uba
|
||||
social_manager = sm
|
||||
|
||||
# 连接信号
|
||||
if user_behavior_analytics:
|
||||
user_behavior_analytics.behavior_recorded.connect(_on_behavior_recorded)
|
||||
|
||||
if social_manager:
|
||||
social_manager.friend_activity.connect(_on_social_activity)
|
||||
|
||||
## 记录统计数据
|
||||
func record_statistic(stat_type: StatType, value: float, metadata: Dictionary = {}) -> void:
|
||||
"""
|
||||
记录统计数据
|
||||
@param stat_type: 统计类型
|
||||
@param value: 统计值
|
||||
@param metadata: 元数据
|
||||
"""
|
||||
if not statistics_enabled:
|
||||
return
|
||||
|
||||
var stat = GameStat.new(stat_type, value, metadata)
|
||||
statistics_history.append(stat)
|
||||
|
||||
# 限制历史记录数量
|
||||
if statistics_history.size() > max_history_entries:
|
||||
statistics_history.pop_front()
|
||||
|
||||
# 更新当日统计
|
||||
_update_daily_statistics(stat)
|
||||
|
||||
# 检查性能警报
|
||||
_check_performance_alerts(stat_type, value)
|
||||
|
||||
# 发射信号
|
||||
statistic_recorded.emit(stat_type, value)
|
||||
|
||||
print("Statistic recorded: ", StatType.keys()[stat_type], " = ", value)
|
||||
|
||||
## 记录玩家数量
|
||||
func record_player_count(online_count: int, total_count: int) -> void:
|
||||
"""
|
||||
记录玩家数量统计
|
||||
@param online_count: 在线玩家数
|
||||
@param total_count: 总玩家数
|
||||
"""
|
||||
record_statistic(StatType.PLAYER_COUNT, online_count, {
|
||||
"total_players": total_count,
|
||||
"online_ratio": float(online_count) / max(total_count, 1)
|
||||
})
|
||||
|
||||
## 记录功能使用情况
|
||||
func record_feature_usage(feature_name: String, usage_count: int, user_count: int) -> void:
|
||||
"""
|
||||
记录功能使用统计
|
||||
@param feature_name: 功能名称
|
||||
@param usage_count: 使用次数
|
||||
@param user_count: 使用用户数
|
||||
"""
|
||||
# 更新功能使用统计
|
||||
if not feature_usage_stats.has(feature_name):
|
||||
feature_usage_stats[feature_name] = {
|
||||
"total_usage": 0,
|
||||
"unique_users": {},
|
||||
"daily_usage": {}
|
||||
}
|
||||
|
||||
var feature_stat = feature_usage_stats[feature_name]
|
||||
feature_stat.total_usage += usage_count
|
||||
|
||||
# 记录到统计历史
|
||||
record_statistic(StatType.FEATURE_USAGE, usage_count, {
|
||||
"feature": feature_name,
|
||||
"user_count": user_count,
|
||||
"usage_rate": float(usage_count) / max(user_count, 1)
|
||||
})
|
||||
|
||||
## 记录性能指标
|
||||
func record_performance_metric(metric_name: String, value: float, threshold: float = 0.0) -> void:
|
||||
"""
|
||||
记录性能指标
|
||||
@param metric_name: 指标名称
|
||||
@param value: 指标值
|
||||
@param threshold: 警报阈值
|
||||
"""
|
||||
# 更新性能指标历史
|
||||
if not performance_metrics.has(metric_name):
|
||||
performance_metrics[metric_name] = []
|
||||
|
||||
var metric_history = performance_metrics[metric_name]
|
||||
metric_history.append({
|
||||
"value": value,
|
||||
"timestamp": Time.get_unix_time_from_system()
|
||||
})
|
||||
|
||||
# 限制历史记录长度
|
||||
if metric_history.size() > 100:
|
||||
metric_history.pop_front()
|
||||
|
||||
# 记录统计
|
||||
record_statistic(StatType.PERFORMANCE, value, {
|
||||
"metric": metric_name,
|
||||
"threshold": threshold
|
||||
})
|
||||
|
||||
## 记录参与度指标
|
||||
func record_engagement_metric(session_duration: float, actions_count: int, features_used: int) -> void:
|
||||
"""
|
||||
记录用户参与度指标
|
||||
@param session_duration: 会话时长
|
||||
@param actions_count: 操作次数
|
||||
@param features_used: 使用功能数
|
||||
"""
|
||||
var engagement_score = _calculate_engagement_score(session_duration, actions_count, features_used)
|
||||
|
||||
record_statistic(StatType.ENGAGEMENT, engagement_score, {
|
||||
"session_duration": session_duration,
|
||||
"actions_count": actions_count,
|
||||
"features_used": features_used
|
||||
})
|
||||
|
||||
## 计算参与度分数
|
||||
func _calculate_engagement_score(duration: float, actions: int, features: int) -> float:
|
||||
"""
|
||||
计算参与度分数
|
||||
@param duration: 会话时长(秒)
|
||||
@param actions: 操作次数
|
||||
@param features: 使用功能数
|
||||
@return: 参与度分数(0-100)
|
||||
"""
|
||||
# 时长分数(最多30分钟满分)
|
||||
var duration_score = min(duration / 1800.0, 1.0) * 40.0
|
||||
|
||||
# 操作频率分数
|
||||
var action_rate = actions / max(duration / 60.0, 1.0) # 每分钟操作数
|
||||
var action_score = min(action_rate / 10.0, 1.0) * 30.0
|
||||
|
||||
# 功能多样性分数
|
||||
var feature_score = min(features / 5.0, 1.0) * 30.0
|
||||
|
||||
return duration_score + action_score + feature_score
|
||||
|
||||
## 定期收集统计数据
|
||||
func _collect_periodic_statistics() -> void:
|
||||
"""定期收集系统统计数据"""
|
||||
if not statistics_enabled:
|
||||
return
|
||||
|
||||
# 收集性能数据
|
||||
var fps = Engine.get_frames_per_second()
|
||||
var memory = OS.get_static_memory_usage_by_type()
|
||||
|
||||
record_performance_metric("fps", fps, 30.0)
|
||||
record_performance_metric("memory_mb", memory / 1024.0 / 1024.0, 512.0)
|
||||
|
||||
# 收集用户行为数据
|
||||
if user_behavior_analytics:
|
||||
var behavior_stats = user_behavior_analytics.get_realtime_statistics()
|
||||
record_statistic(StatType.SESSION_DURATION, behavior_stats.get("current_session_duration", 0.0))
|
||||
|
||||
# 收集社交活动数据
|
||||
if social_manager:
|
||||
var social_stats = social_manager.get_statistics()
|
||||
_record_social_statistics(social_stats)
|
||||
|
||||
## 记录社交统计
|
||||
func _record_social_statistics(social_stats: Dictionary) -> void:
|
||||
"""记录社交活动统计"""
|
||||
if social_stats.has("friend_system"):
|
||||
var friend_stats = social_stats.friend_system
|
||||
record_statistic(StatType.SOCIAL_ACTIVITY, friend_stats.get("total_friends", 0), {
|
||||
"type": "friends",
|
||||
"pending_requests": friend_stats.get("pending_requests", 0)
|
||||
})
|
||||
|
||||
if social_stats.has("community_events"):
|
||||
var event_stats = social_stats.community_events
|
||||
record_statistic(StatType.SOCIAL_ACTIVITY, event_stats.get("active_events", 0), {
|
||||
"type": "events",
|
||||
"total_events": event_stats.get("total_events", 0)
|
||||
})
|
||||
|
||||
## 更新每日统计
|
||||
func _update_daily_statistics(stat: GameStat) -> void:
|
||||
"""更新每日统计数据"""
|
||||
var date_string = Time.get_date_string_from_unix_time(stat.timestamp)
|
||||
|
||||
if not daily_statistics.has(date_string):
|
||||
daily_statistics[date_string] = {
|
||||
"date": date_string,
|
||||
"stats_by_type": {},
|
||||
"total_records": 0,
|
||||
"performance_summary": {},
|
||||
"feature_usage_summary": {}
|
||||
}
|
||||
|
||||
var daily_data = daily_statistics[date_string]
|
||||
daily_data.total_records += 1
|
||||
|
||||
# 按类型统计
|
||||
var type_name = StatType.keys()[stat.stat_type]
|
||||
if not daily_data.stats_by_type.has(type_name):
|
||||
daily_data.stats_by_type[type_name] = {
|
||||
"count": 0,
|
||||
"total_value": 0.0,
|
||||
"min_value": stat.value,
|
||||
"max_value": stat.value
|
||||
}
|
||||
|
||||
var type_stats = daily_data.stats_by_type[type_name]
|
||||
type_stats.count += 1
|
||||
type_stats.total_value += stat.value
|
||||
type_stats.min_value = min(type_stats.min_value, stat.value)
|
||||
type_stats.max_value = max(type_stats.max_value, stat.value)
|
||||
|
||||
## 生成每日报告
|
||||
func _generate_daily_report() -> void:
|
||||
"""生成每日统计报告"""
|
||||
var yesterday = Time.get_date_string_from_unix_time(Time.get_unix_time_from_system() - 86400)
|
||||
|
||||
if not daily_statistics.has(yesterday):
|
||||
return
|
||||
|
||||
var daily_data = daily_statistics[yesterday]
|
||||
var report = _create_daily_report(daily_data)
|
||||
|
||||
daily_report_generated.emit(yesterday, report)
|
||||
print("Daily report generated for: ", yesterday)
|
||||
|
||||
## 创建每日报告
|
||||
func _create_daily_report(daily_data: Dictionary) -> Dictionary:
|
||||
"""创建每日报告数据"""
|
||||
var report = daily_data.duplicate()
|
||||
|
||||
# 计算平均值
|
||||
for type_name in daily_data.stats_by_type:
|
||||
var type_stats = daily_data.stats_by_type[type_name]
|
||||
type_stats["average_value"] = type_stats.total_value / max(type_stats.count, 1)
|
||||
|
||||
# 添加趋势分析
|
||||
report["trends"] = _analyze_daily_trends(daily_data.date)
|
||||
|
||||
# 添加关键指标摘要
|
||||
report["key_metrics"] = _extract_key_metrics(daily_data)
|
||||
|
||||
return report
|
||||
|
||||
## 分析每日趋势
|
||||
func _analyze_daily_trends(date: String) -> Dictionary:
|
||||
"""分析每日数据趋势"""
|
||||
var trends = {}
|
||||
|
||||
# 获取前一天数据进行对比
|
||||
var prev_date_time = Time.get_unix_time_from_datetime_string(date + "T00:00:00") - 86400
|
||||
var prev_date = Time.get_date_string_from_unix_time(prev_date_time)
|
||||
|
||||
if daily_statistics.has(prev_date):
|
||||
var current_data = daily_statistics[date]
|
||||
var prev_data = daily_statistics[prev_date]
|
||||
|
||||
# 比较总记录数
|
||||
var current_total = current_data.get("total_records", 0)
|
||||
var prev_total = prev_data.get("total_records", 0)
|
||||
|
||||
if prev_total > 0:
|
||||
trends["activity_change"] = (float(current_total - prev_total) / prev_total) * 100.0
|
||||
|
||||
# 比较各类型统计
|
||||
trends["type_changes"] = {}
|
||||
for type_name in current_data.get("stats_by_type", {}):
|
||||
var current_count = current_data.stats_by_type[type_name].get("count", 0)
|
||||
var prev_count = prev_data.get("stats_by_type", {}).get(type_name, {}).get("count", 0)
|
||||
|
||||
if prev_count > 0:
|
||||
trends.type_changes[type_name] = (float(current_count - prev_count) / prev_count) * 100.0
|
||||
|
||||
return trends
|
||||
|
||||
## 提取关键指标
|
||||
func _extract_key_metrics(daily_data: Dictionary) -> Dictionary:
|
||||
"""提取每日关键指标"""
|
||||
var metrics = {}
|
||||
|
||||
var stats_by_type = daily_data.get("stats_by_type", {})
|
||||
|
||||
# 性能指标
|
||||
if stats_by_type.has("PERFORMANCE"):
|
||||
metrics["performance"] = {
|
||||
"average": stats_by_type.PERFORMANCE.get("average_value", 0.0),
|
||||
"min": stats_by_type.PERFORMANCE.get("min_value", 0.0),
|
||||
"max": stats_by_type.PERFORMANCE.get("max_value", 0.0)
|
||||
}
|
||||
|
||||
# 参与度指标
|
||||
if stats_by_type.has("ENGAGEMENT"):
|
||||
metrics["engagement"] = {
|
||||
"average_score": stats_by_type.ENGAGEMENT.get("average_value", 0.0),
|
||||
"peak_score": stats_by_type.ENGAGEMENT.get("max_value", 0.0)
|
||||
}
|
||||
|
||||
# 社交活动指标
|
||||
if stats_by_type.has("SOCIAL_ACTIVITY"):
|
||||
metrics["social"] = {
|
||||
"activity_count": stats_by_type.SOCIAL_ACTIVITY.get("count", 0),
|
||||
"average_activity": stats_by_type.SOCIAL_ACTIVITY.get("average_value", 0.0)
|
||||
}
|
||||
|
||||
return metrics
|
||||
|
||||
## 检查性能警报
|
||||
func _check_performance_alerts(stat_type: StatType, value: float) -> void:
|
||||
"""检查性能警报条件"""
|
||||
if stat_type == StatType.PERFORMANCE:
|
||||
# FPS过低警报
|
||||
if value < 20.0:
|
||||
performance_alert.emit("low_fps", value, 20.0)
|
||||
|
||||
# 内存使用过高警报(假设单位是MB)
|
||||
if value > 1000.0:
|
||||
performance_alert.emit("high_memory", value, 1000.0)
|
||||
|
||||
## 获取统计摘要
|
||||
func get_statistics_summary(days: int = 7) -> Dictionary:
|
||||
"""
|
||||
获取统计摘要
|
||||
@param days: 统计天数
|
||||
@return: 统计摘要
|
||||
"""
|
||||
var summary = {}
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
var start_time = current_time - (days * 86400)
|
||||
|
||||
# 获取时间范围内的统计
|
||||
var recent_stats = statistics_history.filter(func(stat): return stat.timestamp >= start_time)
|
||||
|
||||
summary["period_days"] = days
|
||||
summary["total_records"] = recent_stats.size()
|
||||
summary["start_date"] = Time.get_date_string_from_unix_time(start_time)
|
||||
summary["end_date"] = Time.get_date_string_from_unix_time(current_time)
|
||||
|
||||
# 按类型汇总
|
||||
var type_summary = {}
|
||||
for stat in recent_stats:
|
||||
var type_name = StatType.keys()[stat.stat_type]
|
||||
if not type_summary.has(type_name):
|
||||
type_summary[type_name] = {
|
||||
"count": 0,
|
||||
"values": []
|
||||
}
|
||||
|
||||
type_summary[type_name].count += 1
|
||||
type_summary[type_name].values.append(stat.value)
|
||||
|
||||
# 计算统计指标
|
||||
for type_name in type_summary:
|
||||
var values = type_summary[type_name].values
|
||||
if values.size() > 0:
|
||||
type_summary[type_name]["average"] = _calculate_average(values)
|
||||
type_summary[type_name]["min"] = values.min()
|
||||
type_summary[type_name]["max"] = values.max()
|
||||
|
||||
summary["by_type"] = type_summary
|
||||
|
||||
return summary
|
||||
|
||||
## 计算平均值
|
||||
func _calculate_average(values: Array) -> float:
|
||||
"""计算数组平均值"""
|
||||
if values.is_empty():
|
||||
return 0.0
|
||||
|
||||
var sum = 0.0
|
||||
for value in values:
|
||||
sum += float(value)
|
||||
|
||||
return sum / values.size()
|
||||
|
||||
## 信号处理函数
|
||||
func _on_behavior_recorded(event_type: UserBehaviorAnalytics.EventType, data: Dictionary):
|
||||
"""处理用户行为记录事件"""
|
||||
# 将用户行为转换为游戏统计
|
||||
match event_type:
|
||||
UserBehaviorAnalytics.EventType.LOGIN:
|
||||
record_statistic(StatType.PLAYER_COUNT, 1.0, {"action": "login"})
|
||||
UserBehaviorAnalytics.EventType.UI_ACTION:
|
||||
var element = data.get("element", "unknown")
|
||||
record_feature_usage(element, 1, 1)
|
||||
|
||||
func _on_social_activity(activity_type: String, data: Dictionary):
|
||||
"""处理社交活动事件"""
|
||||
record_statistic(StatType.SOCIAL_ACTIVITY, 1.0, {
|
||||
"activity_type": activity_type,
|
||||
"data": data
|
||||
})
|
||||
|
||||
## 保存统计数据
|
||||
func _save_statistics_data() -> void:
|
||||
"""保存统计数据到本地文件"""
|
||||
var data = {
|
||||
"statistics_history": [],
|
||||
"daily_statistics": daily_statistics,
|
||||
"feature_usage_stats": feature_usage_stats,
|
||||
"performance_metrics": performance_metrics,
|
||||
"saved_at": Time.get_unix_time_from_system()
|
||||
}
|
||||
|
||||
# 序列化统计历史(只保存最近的数据)
|
||||
var stats_to_save = statistics_history.slice(max(0, statistics_history.size() - 1000), statistics_history.size())
|
||||
|
||||
for stat in stats_to_save:
|
||||
data.statistics_history.append({
|
||||
"stat_type": stat.stat_type,
|
||||
"value": stat.value,
|
||||
"timestamp": stat.timestamp,
|
||||
"metadata": stat.metadata
|
||||
})
|
||||
|
||||
var file = FileAccess.open(stats_file_path, FileAccess.WRITE)
|
||||
if file:
|
||||
var json_string = JSON.stringify(data)
|
||||
file.store_string(json_string)
|
||||
file.close()
|
||||
print("Game statistics saved: ", stats_to_save.size(), " records")
|
||||
else:
|
||||
print("Failed to save game statistics")
|
||||
|
||||
## 加载统计数据
|
||||
func _load_statistics_data() -> void:
|
||||
"""从本地文件加载统计数据"""
|
||||
if not FileAccess.file_exists(stats_file_path):
|
||||
print("No game statistics file found, starting fresh")
|
||||
return
|
||||
|
||||
var file = FileAccess.open(stats_file_path, FileAccess.READ)
|
||||
if file:
|
||||
var json_string = file.get_as_text()
|
||||
file.close()
|
||||
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(json_string)
|
||||
|
||||
if parse_result == OK:
|
||||
var data = json.data
|
||||
|
||||
# 加载统计历史
|
||||
if data.has("statistics_history"):
|
||||
for stat_data in data.statistics_history:
|
||||
var stat = GameStat.new(
|
||||
stat_data.get("stat_type", StatType.PERFORMANCE),
|
||||
stat_data.get("value", 0.0),
|
||||
stat_data.get("metadata", {})
|
||||
)
|
||||
stat.timestamp = stat_data.get("timestamp", 0.0)
|
||||
statistics_history.append(stat)
|
||||
|
||||
# 加载其他数据
|
||||
daily_statistics = data.get("daily_statistics", {})
|
||||
feature_usage_stats = data.get("feature_usage_stats", {})
|
||||
performance_metrics = data.get("performance_metrics", {})
|
||||
|
||||
print("Game statistics loaded: ", statistics_history.size(), " records")
|
||||
else:
|
||||
print("Failed to parse game statistics JSON")
|
||||
else:
|
||||
print("Failed to open game statistics file")
|
||||
|
||||
func _exit_tree():
|
||||
"""节点退出时保存数据"""
|
||||
_save_statistics_data()
|
||||
1
scripts/GameStatistics.gd.uid
Normal file
1
scripts/GameStatistics.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://buanrk8sfjqxi
|
||||
432
scripts/GroupDialogueManager.gd
Normal file
432
scripts/GroupDialogueManager.gd
Normal file
@@ -0,0 +1,432 @@
|
||||
extends Node
|
||||
class_name GroupDialogueManager
|
||||
## 群组对话管理器
|
||||
## 管理多人对话和群组聊天功能
|
||||
|
||||
# 群组数据结构
|
||||
class DialogueGroup:
|
||||
var id: String
|
||||
var name: String
|
||||
var participants: Array[String] = [] # 参与者ID列表
|
||||
var creator_id: String
|
||||
var created_at: float
|
||||
var is_active: bool = true
|
||||
var max_participants: int = 10
|
||||
var message_history: Array[Dictionary] = []
|
||||
|
||||
func _init(group_id: String, group_name: String, creator: String):
|
||||
id = group_id
|
||||
name = group_name
|
||||
creator_id = creator
|
||||
created_at = Time.get_unix_time_from_system()
|
||||
participants.append(creator)
|
||||
|
||||
# 群组管理
|
||||
var active_groups: Dictionary = {} # group_id -> DialogueGroup
|
||||
var player_groups: Array[String] = [] # 玩家参与的群组ID列表
|
||||
var current_group_id: String = "" # 当前活跃的群组
|
||||
|
||||
# 配置
|
||||
var max_groups_per_player: int = 5
|
||||
var max_message_history: int = 100
|
||||
|
||||
# 信号
|
||||
signal group_created(group_id: String, group_name: String)
|
||||
signal group_joined(group_id: String, participant_id: String)
|
||||
signal group_left(group_id: String, participant_id: String)
|
||||
signal group_message_received(group_id: String, sender_id: String, message: String)
|
||||
signal group_disbanded(group_id: String)
|
||||
|
||||
func _ready():
|
||||
"""初始化群组对话管理器"""
|
||||
print("GroupDialogueManager initialized")
|
||||
|
||||
## 创建新群组
|
||||
func create_group(group_name: String, creator_id: String) -> String:
|
||||
"""
|
||||
创建新的对话群组
|
||||
@param group_name: 群组名称
|
||||
@param creator_id: 创建者ID
|
||||
@return: 群组ID,失败返回空字符串
|
||||
"""
|
||||
# 检查玩家群组数量限制
|
||||
if player_groups.size() >= max_groups_per_player:
|
||||
print("Cannot create group: player has reached maximum groups limit")
|
||||
return ""
|
||||
|
||||
# 验证群组名称
|
||||
if group_name.strip_edges().is_empty():
|
||||
print("Cannot create group: invalid group name")
|
||||
return ""
|
||||
|
||||
if group_name.length() > 50:
|
||||
print("Cannot create group: group name too long")
|
||||
return ""
|
||||
|
||||
# 生成群组ID
|
||||
var group_id = generate_group_id()
|
||||
|
||||
# 创建群组
|
||||
var group = DialogueGroup.new(group_id, group_name.strip_edges(), creator_id)
|
||||
active_groups[group_id] = group
|
||||
|
||||
# 添加到玩家群组列表
|
||||
if not group_id in player_groups:
|
||||
player_groups.append(group_id)
|
||||
|
||||
group_created.emit(group_id, group_name)
|
||||
print("Group created: ", group_name, " (", group_id, ")")
|
||||
|
||||
return group_id
|
||||
|
||||
## 加入群组
|
||||
func join_group(group_id: String, participant_id: String) -> bool:
|
||||
"""
|
||||
加入指定群组
|
||||
@param group_id: 群组ID
|
||||
@param participant_id: 参与者ID
|
||||
@return: 是否成功加入
|
||||
"""
|
||||
if not active_groups.has(group_id):
|
||||
print("Cannot join group: group not found")
|
||||
return false
|
||||
|
||||
var group = active_groups[group_id]
|
||||
|
||||
# 检查群组是否活跃
|
||||
if not group.is_active:
|
||||
print("Cannot join group: group is not active")
|
||||
return false
|
||||
|
||||
# 检查是否已经在群组中
|
||||
if participant_id in group.participants:
|
||||
print("Already in group: ", group_id)
|
||||
return true
|
||||
|
||||
# 检查群组人数限制
|
||||
if group.participants.size() >= group.max_participants:
|
||||
print("Cannot join group: group is full")
|
||||
return false
|
||||
|
||||
# 加入群组
|
||||
group.participants.append(participant_id)
|
||||
|
||||
# 如果是玩家,添加到玩家群组列表
|
||||
if participant_id == "player" and not group_id in player_groups:
|
||||
player_groups.append(group_id)
|
||||
|
||||
group_joined.emit(group_id, participant_id)
|
||||
print("Participant ", participant_id, " joined group ", group_id)
|
||||
|
||||
return true
|
||||
|
||||
## 离开群组
|
||||
func leave_group(group_id: String, participant_id: String) -> bool:
|
||||
"""
|
||||
离开指定群组
|
||||
@param group_id: 群组ID
|
||||
@param participant_id: 参与者ID
|
||||
@return: 是否成功离开
|
||||
"""
|
||||
if not active_groups.has(group_id):
|
||||
print("Cannot leave group: group not found")
|
||||
return false
|
||||
|
||||
var group = active_groups[group_id]
|
||||
|
||||
# 检查是否在群组中
|
||||
if not participant_id in group.participants:
|
||||
print("Not in group: ", group_id)
|
||||
return false
|
||||
|
||||
# 离开群组
|
||||
group.participants.erase(participant_id)
|
||||
|
||||
# 如果是玩家,从玩家群组列表中移除
|
||||
if participant_id == "player":
|
||||
player_groups.erase(group_id)
|
||||
|
||||
# 如果当前活跃群组是这个,清除当前群组
|
||||
if current_group_id == group_id:
|
||||
current_group_id = ""
|
||||
|
||||
group_left.emit(group_id, participant_id)
|
||||
print("Participant ", participant_id, " left group ", group_id)
|
||||
|
||||
# 如果群组为空或创建者离开,解散群组
|
||||
if group.participants.is_empty() or participant_id == group.creator_id:
|
||||
disband_group(group_id)
|
||||
|
||||
return true
|
||||
|
||||
## 发送群组消息
|
||||
func send_group_message(group_id: String, sender_id: String, message: String) -> bool:
|
||||
"""
|
||||
向群组发送消息
|
||||
@param group_id: 群组ID
|
||||
@param sender_id: 发送者ID
|
||||
@param message: 消息内容
|
||||
@return: 是否成功发送
|
||||
"""
|
||||
if not active_groups.has(group_id):
|
||||
print("Cannot send message: group not found")
|
||||
return false
|
||||
|
||||
var group = active_groups[group_id]
|
||||
|
||||
# 检查发送者是否在群组中
|
||||
if not sender_id in group.participants:
|
||||
print("Cannot send message: sender not in group")
|
||||
return false
|
||||
|
||||
# 验证消息
|
||||
if message.strip_edges().is_empty():
|
||||
print("Cannot send empty message")
|
||||
return false
|
||||
|
||||
if message.length() > 500:
|
||||
print("Message too long")
|
||||
return false
|
||||
|
||||
# 创建消息记录
|
||||
var message_record = {
|
||||
"sender_id": sender_id,
|
||||
"message": message,
|
||||
"timestamp": Time.get_unix_time_from_system(),
|
||||
"id": generate_message_id()
|
||||
}
|
||||
|
||||
# 添加到群组历史
|
||||
group.message_history.append(message_record)
|
||||
|
||||
# 限制历史记录长度
|
||||
if group.message_history.size() > max_message_history:
|
||||
group.message_history.pop_front()
|
||||
|
||||
# 发射信号
|
||||
group_message_received.emit(group_id, sender_id, message)
|
||||
print("Group message sent in ", group_id, " by ", sender_id, ": ", message)
|
||||
|
||||
return true
|
||||
|
||||
## 解散群组
|
||||
func disband_group(group_id: String) -> bool:
|
||||
"""
|
||||
解散指定群组
|
||||
@param group_id: 群组ID
|
||||
@return: 是否成功解散
|
||||
"""
|
||||
if not active_groups.has(group_id):
|
||||
print("Cannot disband group: group not found")
|
||||
return false
|
||||
|
||||
var group = active_groups[group_id]
|
||||
|
||||
# 通知所有参与者
|
||||
for participant_id in group.participants:
|
||||
group_left.emit(group_id, participant_id)
|
||||
|
||||
# 从玩家群组列表中移除
|
||||
player_groups.erase(group_id)
|
||||
|
||||
# 如果是当前活跃群组,清除
|
||||
if current_group_id == group_id:
|
||||
current_group_id = ""
|
||||
|
||||
# 移除群组
|
||||
active_groups.erase(group_id)
|
||||
|
||||
group_disbanded.emit(group_id)
|
||||
print("Group disbanded: ", group_id)
|
||||
|
||||
return true
|
||||
|
||||
## 设置当前活跃群组
|
||||
func set_current_group(group_id: String) -> bool:
|
||||
"""
|
||||
设置当前活跃的群组
|
||||
@param group_id: 群组ID(空字符串表示清除当前群组)
|
||||
@return: 是否成功设置
|
||||
"""
|
||||
if group_id.is_empty():
|
||||
current_group_id = ""
|
||||
return true
|
||||
|
||||
if not active_groups.has(group_id):
|
||||
print("Cannot set current group: group not found")
|
||||
return false
|
||||
|
||||
var group = active_groups[group_id]
|
||||
|
||||
# 检查玩家是否在群组中
|
||||
if not "player" in group.participants:
|
||||
print("Cannot set current group: player not in group")
|
||||
return false
|
||||
|
||||
current_group_id = group_id
|
||||
print("Current group set to: ", group_id)
|
||||
|
||||
return true
|
||||
|
||||
## 获取群组信息
|
||||
func get_group_info(group_id: String) -> Dictionary:
|
||||
"""
|
||||
获取群组信息
|
||||
@param group_id: 群组ID
|
||||
@return: 群组信息字典
|
||||
"""
|
||||
if not active_groups.has(group_id):
|
||||
return {}
|
||||
|
||||
var group = active_groups[group_id]
|
||||
|
||||
return {
|
||||
"id": group.id,
|
||||
"name": group.name,
|
||||
"participants": group.participants.duplicate(),
|
||||
"creator_id": group.creator_id,
|
||||
"created_at": group.created_at,
|
||||
"is_active": group.is_active,
|
||||
"participant_count": group.participants.size(),
|
||||
"message_count": group.message_history.size()
|
||||
}
|
||||
|
||||
## 获取群组消息历史
|
||||
func get_group_history(group_id: String, limit: int = 0) -> Array[Dictionary]:
|
||||
"""
|
||||
获取群组消息历史
|
||||
@param group_id: 群组ID
|
||||
@param limit: 限制返回的消息数量(0表示返回全部)
|
||||
@return: 消息历史数组
|
||||
"""
|
||||
if not active_groups.has(group_id):
|
||||
return []
|
||||
|
||||
var group = active_groups[group_id]
|
||||
var history = group.message_history
|
||||
|
||||
if limit <= 0 or limit >= history.size():
|
||||
return history.duplicate()
|
||||
|
||||
# 返回最近的消息
|
||||
return history.slice(history.size() - limit, history.size())
|
||||
|
||||
## 获取玩家的群组列表
|
||||
func get_player_groups() -> Array[Dictionary]:
|
||||
"""
|
||||
获取玩家参与的群组列表
|
||||
@return: 群组信息数组
|
||||
"""
|
||||
var groups = []
|
||||
|
||||
for group_id in player_groups:
|
||||
if active_groups.has(group_id):
|
||||
groups.append(get_group_info(group_id))
|
||||
|
||||
return groups
|
||||
|
||||
## 搜索群组
|
||||
func search_groups(query: String) -> Array[Dictionary]:
|
||||
"""
|
||||
搜索群组(按名称)
|
||||
@param query: 搜索关键词
|
||||
@return: 匹配的群组信息数组
|
||||
"""
|
||||
var results = []
|
||||
var search_query = query.to_lower()
|
||||
|
||||
for group_id in active_groups:
|
||||
var group = active_groups[group_id]
|
||||
if group.is_active and group.name.to_lower().contains(search_query):
|
||||
results.append(get_group_info(group_id))
|
||||
|
||||
return results
|
||||
|
||||
## 获取附近的角色(用于邀请加入群组)
|
||||
func get_nearby_characters_for_invite(_player_position: Vector2, _radius: float = 100.0) -> Array[String]:
|
||||
"""
|
||||
获取附近可以邀请的角色列表
|
||||
@param _player_position: 玩家位置(暂未使用)
|
||||
@param _radius: 搜索半径(暂未使用)
|
||||
@return: 角色ID数组
|
||||
"""
|
||||
# 这里需要与WorldManager集成来获取附近角色
|
||||
# 暂时返回空数组,实际实现时需要调用WorldManager
|
||||
return []
|
||||
|
||||
## 邀请角色加入群组
|
||||
func invite_to_group(group_id: String, inviter_id: String, invitee_id: String) -> bool:
|
||||
"""
|
||||
邀请角色加入群组
|
||||
@param group_id: 群组ID
|
||||
@param inviter_id: 邀请者ID
|
||||
@param invitee_id: 被邀请者ID
|
||||
@return: 是否成功发送邀请
|
||||
"""
|
||||
if not active_groups.has(group_id):
|
||||
print("Cannot invite: group not found")
|
||||
return false
|
||||
|
||||
var group = active_groups[group_id]
|
||||
|
||||
# 检查邀请者是否在群组中
|
||||
if not inviter_id in group.participants:
|
||||
print("Cannot invite: inviter not in group")
|
||||
return false
|
||||
|
||||
# 检查被邀请者是否已在群组中
|
||||
if invitee_id in group.participants:
|
||||
print("Cannot invite: invitee already in group")
|
||||
return false
|
||||
|
||||
# 检查群组是否已满
|
||||
if group.participants.size() >= group.max_participants:
|
||||
print("Cannot invite: group is full")
|
||||
return false
|
||||
|
||||
# 这里应该发送邀请消息给被邀请者
|
||||
# 实际实现时需要与网络系统集成
|
||||
print("Invitation sent to ", invitee_id, " for group ", group_id)
|
||||
|
||||
return true
|
||||
|
||||
## 生成群组ID
|
||||
func generate_group_id() -> String:
|
||||
"""生成唯一的群组ID"""
|
||||
var timestamp = Time.get_unix_time_from_system()
|
||||
var random = randi()
|
||||
return "group_%d_%d" % [timestamp, random]
|
||||
|
||||
## 生成消息ID
|
||||
func generate_message_id() -> String:
|
||||
"""生成唯一的消息ID"""
|
||||
var timestamp = Time.get_unix_time_from_system()
|
||||
var random = randi()
|
||||
return "msg_%d_%d" % [timestamp, random]
|
||||
|
||||
## 获取统计信息
|
||||
func get_statistics() -> Dictionary:
|
||||
"""
|
||||
获取群组对话统计信息
|
||||
@return: 统计信息字典
|
||||
"""
|
||||
var total_groups = active_groups.size()
|
||||
var active_group_count = 0
|
||||
var total_participants = 0
|
||||
var total_messages = 0
|
||||
|
||||
for group_id in active_groups:
|
||||
var group = active_groups[group_id]
|
||||
if group.is_active:
|
||||
active_group_count += 1
|
||||
total_participants += group.participants.size()
|
||||
total_messages += group.message_history.size()
|
||||
|
||||
return {
|
||||
"total_groups": total_groups,
|
||||
"active_groups": active_group_count,
|
||||
"player_groups": player_groups.size(),
|
||||
"total_participants": total_participants,
|
||||
"total_messages": total_messages,
|
||||
"current_group": current_group_id
|
||||
}
|
||||
1
scripts/GroupDialogueManager.gd.uid
Normal file
1
scripts/GroupDialogueManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d1d68ckx5tgcs
|
||||
129
scripts/HUD.gd
Normal file
129
scripts/HUD.gd
Normal file
@@ -0,0 +1,129 @@
|
||||
extends Control
|
||||
class_name HUD
|
||||
## 游戏内 HUD
|
||||
## 显示在线玩家数量、网络状态等信息
|
||||
|
||||
# UI 元素
|
||||
var online_players_label: Label
|
||||
var network_status_indicator: ColorRect
|
||||
var network_status_label: Label
|
||||
var interaction_hint: Label
|
||||
var container: VBoxContainer
|
||||
|
||||
# 状态
|
||||
var online_player_count: int = 0
|
||||
var network_connected: bool = false
|
||||
|
||||
func _ready():
|
||||
"""初始化 HUD"""
|
||||
_create_ui()
|
||||
update_layout()
|
||||
|
||||
## 创建 UI 元素
|
||||
func _create_ui():
|
||||
"""创建 HUD 的所有 UI 元素"""
|
||||
# 设置为全屏但不阻挡输入
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
# 创建顶部容器
|
||||
var top_container = HBoxContainer.new()
|
||||
top_container.name = "TopContainer"
|
||||
top_container.position = Vector2(10, 10)
|
||||
add_child(top_container)
|
||||
|
||||
# 网络状态指示器
|
||||
network_status_indicator = ColorRect.new()
|
||||
network_status_indicator.custom_minimum_size = Vector2(16, 16)
|
||||
network_status_indicator.color = Color(1, 0, 0, 0.8) # 红色 - 断开
|
||||
top_container.add_child(network_status_indicator)
|
||||
|
||||
# 间距
|
||||
top_container.add_child(_create_spacer(10))
|
||||
|
||||
# 网络状态标签
|
||||
network_status_label = Label.new()
|
||||
network_status_label.text = "未连接"
|
||||
top_container.add_child(network_status_label)
|
||||
|
||||
# 间距
|
||||
top_container.add_child(_create_spacer(20))
|
||||
|
||||
# 在线玩家数量标签
|
||||
online_players_label = Label.new()
|
||||
online_players_label.text = "在线玩家: 0"
|
||||
top_container.add_child(online_players_label)
|
||||
|
||||
# 创建底部容器(交互提示)
|
||||
var bottom_container = VBoxContainer.new()
|
||||
bottom_container.name = "BottomContainer"
|
||||
bottom_container.anchor_top = 1.0
|
||||
bottom_container.anchor_bottom = 1.0
|
||||
bottom_container.offset_top = -100
|
||||
bottom_container.offset_left = 10
|
||||
add_child(bottom_container)
|
||||
|
||||
# 交互提示
|
||||
interaction_hint = Label.new()
|
||||
interaction_hint.text = ""
|
||||
interaction_hint.add_theme_font_size_override("font_size", 18)
|
||||
interaction_hint.add_theme_color_override("font_color", Color(1, 1, 0.5))
|
||||
bottom_container.add_child(interaction_hint)
|
||||
|
||||
## 创建间距
|
||||
func _create_spacer(width: float) -> Control:
|
||||
"""创建水平间距"""
|
||||
var spacer = Control.new()
|
||||
spacer.custom_minimum_size = Vector2(width, 0)
|
||||
return spacer
|
||||
|
||||
## 更新布局
|
||||
func update_layout():
|
||||
"""更新布局以适应窗口大小"""
|
||||
# HUD 元素使用锚点,会自动适应
|
||||
pass
|
||||
|
||||
## 更新在线玩家数量
|
||||
func update_online_players(count: int):
|
||||
"""
|
||||
更新在线玩家数量显示
|
||||
@param count: 在线玩家数量
|
||||
"""
|
||||
online_player_count = count
|
||||
if online_players_label:
|
||||
online_players_label.text = "在线玩家: %d" % count
|
||||
|
||||
## 更新网络状态
|
||||
func update_network_status(connected: bool):
|
||||
"""
|
||||
更新网络连接状态
|
||||
@param connected: 是否已连接
|
||||
"""
|
||||
network_connected = connected
|
||||
|
||||
if network_status_indicator:
|
||||
if connected:
|
||||
network_status_indicator.color = Color(0.2, 1, 0.2, 0.8) # 绿色 - 已连接
|
||||
else:
|
||||
network_status_indicator.color = Color(1, 0, 0, 0.8) # 红色 - 断开
|
||||
|
||||
if network_status_label:
|
||||
network_status_label.text = "已连接" if connected else "未连接"
|
||||
|
||||
## 显示交互提示
|
||||
func show_interaction_hint(hint_text: String):
|
||||
"""
|
||||
显示交互提示
|
||||
@param hint_text: 提示文本
|
||||
"""
|
||||
if interaction_hint:
|
||||
interaction_hint.text = hint_text
|
||||
interaction_hint.show()
|
||||
|
||||
## 隐藏交互提示
|
||||
func hide_interaction_hint():
|
||||
"""隐藏交互提示"""
|
||||
if interaction_hint:
|
||||
interaction_hint.text = ""
|
||||
interaction_hint.hide()
|
||||
1
scripts/HUD.gd.uid
Normal file
1
scripts/HUD.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b85r5ndu5d7xh
|
||||
267
scripts/InputHandler.gd
Normal file
267
scripts/InputHandler.gd
Normal file
@@ -0,0 +1,267 @@
|
||||
extends Node
|
||||
class_name InputHandler
|
||||
## 输入处理器
|
||||
## 处理多平台输入(键盘、触摸、虚拟摇杆)
|
||||
|
||||
# 设备类型枚举
|
||||
enum DeviceType {
|
||||
DESKTOP,
|
||||
MOBILE,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
var current_device: DeviceType = DeviceType.UNKNOWN
|
||||
|
||||
# 信号
|
||||
signal move_input(direction: Vector2)
|
||||
signal interact_input()
|
||||
signal device_detected(device: DeviceType)
|
||||
# signal ui_input(action: String) # 预留用于 UI 导航(暂未使用)
|
||||
|
||||
# 虚拟控制器引用
|
||||
var virtual_joystick: Control = null
|
||||
var virtual_interact_button: Control = null
|
||||
|
||||
# 输入状态控制
|
||||
var movement_enabled: bool = true
|
||||
var dialogue_box_active: bool = false
|
||||
|
||||
func _ready():
|
||||
"""初始化输入处理器"""
|
||||
# 检测设备类型
|
||||
_detect_device()
|
||||
|
||||
# 根据设备类型设置虚拟控制器
|
||||
if current_device == DeviceType.MOBILE:
|
||||
setup_virtual_controls()
|
||||
|
||||
func _process(_delta: float):
|
||||
"""每帧处理输入"""
|
||||
# 检查对话框状态
|
||||
_update_dialogue_box_status()
|
||||
|
||||
# 获取移动输入(只有在移动启用且对话框未激活时)
|
||||
var direction = Vector2.ZERO
|
||||
if movement_enabled and not dialogue_box_active:
|
||||
direction = get_move_direction()
|
||||
|
||||
move_input.emit(direction)
|
||||
|
||||
# 检查交互输入(只有在对话框未激活时才处理)
|
||||
if not dialogue_box_active and is_interact_pressed():
|
||||
interact_input.emit()
|
||||
|
||||
## 获取移动方向
|
||||
func get_move_direction() -> Vector2:
|
||||
"""
|
||||
获取当前的移动方向输入
|
||||
@return: 归一化的方向向量
|
||||
"""
|
||||
var direction = Vector2.ZERO
|
||||
|
||||
if current_device == DeviceType.DESKTOP:
|
||||
# 键盘输入
|
||||
direction = _get_keyboard_direction()
|
||||
elif current_device == DeviceType.MOBILE:
|
||||
# 触摸/虚拟摇杆输入
|
||||
direction = _get_virtual_joystick_direction()
|
||||
|
||||
return direction.normalized() if direction.length() > 0 else Vector2.ZERO
|
||||
|
||||
## 检查交互键是否按下
|
||||
func is_interact_pressed() -> bool:
|
||||
"""
|
||||
检查交互键是否刚被按下
|
||||
@return: 是否按下
|
||||
"""
|
||||
if current_device == DeviceType.DESKTOP:
|
||||
return Input.is_action_just_pressed("interact")
|
||||
elif current_device == DeviceType.MOBILE:
|
||||
return _is_virtual_interact_pressed()
|
||||
|
||||
return false
|
||||
|
||||
## 设置虚拟控制器
|
||||
func setup_virtual_controls() -> void:
|
||||
"""
|
||||
设置移动端虚拟控制器
|
||||
创建虚拟摇杆和交互按钮
|
||||
"""
|
||||
# 查找或创建 UI 容器
|
||||
var ui_layer = get_tree().root.get_node_or_null("Main/UILayer")
|
||||
if not ui_layer:
|
||||
push_warning("UILayer not found, cannot setup virtual controls")
|
||||
return
|
||||
|
||||
# 创建虚拟摇杆
|
||||
if not virtual_joystick:
|
||||
virtual_joystick = VirtualJoystick.new()
|
||||
virtual_joystick.name = "VirtualJoystick"
|
||||
virtual_joystick.position = Vector2(50, get_viewport().get_visible_rect().size.y - 150)
|
||||
ui_layer.add_child(virtual_joystick)
|
||||
|
||||
# 创建虚拟交互按钮
|
||||
if not virtual_interact_button:
|
||||
virtual_interact_button = VirtualButton.new()
|
||||
virtual_interact_button.name = "VirtualInteractButton"
|
||||
virtual_interact_button.button_text = "E"
|
||||
virtual_interact_button.position = Vector2(
|
||||
get_viewport().get_visible_rect().size.x - 130,
|
||||
get_viewport().get_visible_rect().size.y - 130
|
||||
)
|
||||
ui_layer.add_child(virtual_interact_button)
|
||||
|
||||
print("Virtual controls setup complete")
|
||||
|
||||
## 检测设备类型
|
||||
func _detect_device() -> void:
|
||||
"""
|
||||
检测当前设备类型(桌面或移动)
|
||||
"""
|
||||
# 检查是否在移动平台上运行
|
||||
var os_name = OS.get_name()
|
||||
|
||||
if os_name in ["Android", "iOS"]:
|
||||
current_device = DeviceType.MOBILE
|
||||
elif os_name in ["Windows", "macOS", "Linux", "FreeBSD", "NetBSD", "OpenBSD", "BSD"]:
|
||||
current_device = DeviceType.DESKTOP
|
||||
else:
|
||||
# 默认为桌面
|
||||
current_device = DeviceType.DESKTOP
|
||||
|
||||
# 也可以通过触摸屏检测
|
||||
if DisplayServer.is_touchscreen_available():
|
||||
current_device = DeviceType.MOBILE
|
||||
|
||||
device_detected.emit(current_device)
|
||||
print("Device detected: ", DeviceType.keys()[current_device])
|
||||
|
||||
## 获取键盘方向输入
|
||||
func _get_keyboard_direction() -> Vector2:
|
||||
"""
|
||||
从键盘获取方向输入
|
||||
@return: 方向向量
|
||||
"""
|
||||
var direction = Vector2.ZERO
|
||||
|
||||
# 使用独立的移动动作(不影响 UI 输入)
|
||||
if Input.is_action_pressed("move_right"):
|
||||
direction.x += 1
|
||||
if Input.is_action_pressed("move_left"):
|
||||
direction.x -= 1
|
||||
if Input.is_action_pressed("move_down"):
|
||||
direction.y += 1
|
||||
if Input.is_action_pressed("move_up"):
|
||||
direction.y -= 1
|
||||
|
||||
return direction
|
||||
|
||||
## 获取虚拟摇杆方向
|
||||
func _get_virtual_joystick_direction() -> Vector2:
|
||||
"""
|
||||
从虚拟摇杆获取方向输入
|
||||
@return: 方向向量
|
||||
"""
|
||||
# 将在子任务 6.3 中实现
|
||||
# 现在返回零向量
|
||||
if virtual_joystick and virtual_joystick.has_method("get_direction"):
|
||||
return virtual_joystick.get_direction()
|
||||
|
||||
return Vector2.ZERO
|
||||
|
||||
## 检查虚拟交互按钮
|
||||
func _is_virtual_interact_pressed() -> bool:
|
||||
"""
|
||||
检查虚拟交互按钮是否按下
|
||||
@return: 是否按下
|
||||
"""
|
||||
# 将在子任务 6.3 中实现
|
||||
if virtual_interact_button and virtual_interact_button.has_method("is_pressed"):
|
||||
return virtual_interact_button.is_pressed()
|
||||
|
||||
return false
|
||||
|
||||
## 获取当前设备类型
|
||||
func get_device_type() -> DeviceType:
|
||||
"""
|
||||
获取当前检测到的设备类型
|
||||
@return: 设备类型
|
||||
"""
|
||||
return current_device
|
||||
|
||||
## 启用/禁用输入处理
|
||||
func set_input_enabled(enabled: bool) -> void:
|
||||
"""
|
||||
启用或禁用输入处理
|
||||
@param enabled: 是否启用
|
||||
"""
|
||||
set_process(enabled)
|
||||
|
||||
# 如果禁用,清除所有输入状态
|
||||
if not enabled:
|
||||
move_input.emit(Vector2.ZERO)
|
||||
|
||||
## 启用/禁用移动输入
|
||||
func set_movement_enabled(enabled: bool) -> void:
|
||||
"""
|
||||
启用或禁用移动输入
|
||||
@param enabled: 是否启用移动
|
||||
"""
|
||||
movement_enabled = enabled
|
||||
|
||||
# 如果禁用移动,立即发送零向量停止角色移动
|
||||
if not enabled:
|
||||
move_input.emit(Vector2.ZERO)
|
||||
|
||||
## 更新对话框状态
|
||||
func _update_dialogue_box_status() -> void:
|
||||
"""检测对话框是否处于活动状态"""
|
||||
var was_active = dialogue_box_active
|
||||
|
||||
# 使用更通用的方法检测UI焦点状态
|
||||
dialogue_box_active = _is_ui_focused()
|
||||
|
||||
# 如果对话框状态发生变化,立即停止移动
|
||||
if was_active != dialogue_box_active:
|
||||
# 无论是激活还是关闭对话框,都强制停止移动并清除输入状态
|
||||
_clear_all_movement_state()
|
||||
if dialogue_box_active:
|
||||
print("UI focused - movement disabled and cleared")
|
||||
else:
|
||||
print("UI unfocused - movement cleared and reset")
|
||||
|
||||
## 检查是否有UI控件获得焦点
|
||||
func _is_ui_focused() -> bool:
|
||||
"""
|
||||
检查是否有UI控件获得焦点(如输入框)或对话框是否可见
|
||||
@return: 是否有UI控件获得焦点
|
||||
"""
|
||||
# 首先检查对话框是否可见
|
||||
var main_scene = get_tree().root.get_node_or_null("Main")
|
||||
if main_scene:
|
||||
var ui_layer = main_scene.get_node_or_null("UILayer")
|
||||
if ui_layer:
|
||||
var dialogue_box = ui_layer.get_node_or_null("DialogueBox")
|
||||
if dialogue_box and dialogue_box.visible:
|
||||
return true
|
||||
|
||||
# 然后检查是否有输入控件获得焦点
|
||||
var focused_control = get_viewport().gui_get_focus_owner()
|
||||
if focused_control:
|
||||
return focused_control is LineEdit or focused_control is TextEdit
|
||||
|
||||
return false
|
||||
|
||||
## 清除所有移动状态
|
||||
func _clear_all_movement_state() -> void:
|
||||
"""清除所有移动相关的状态和输入"""
|
||||
# 发送零向量停止移动
|
||||
move_input.emit(Vector2.ZERO)
|
||||
|
||||
# 清除所有可能的输入状态
|
||||
Input.action_release("move_up")
|
||||
Input.action_release("move_down")
|
||||
Input.action_release("move_left")
|
||||
Input.action_release("move_right")
|
||||
|
||||
print("All movement state cleared")
|
||||
1
scripts/InputHandler.gd.uid
Normal file
1
scripts/InputHandler.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dmuulnhvm6d0r
|
||||
113
scripts/LoadingIndicator.gd
Normal file
113
scripts/LoadingIndicator.gd
Normal file
@@ -0,0 +1,113 @@
|
||||
extends Control
|
||||
class_name LoadingIndicator
|
||||
## 加载状态指示器
|
||||
## 显示加载动画和状态文本
|
||||
|
||||
# UI 元素
|
||||
@onready var loading_panel: Panel = $LoadingPanel
|
||||
@onready var loading_label: Label = $LoadingPanel/VBoxContainer/LoadingLabel
|
||||
@onready var spinner: ColorRect = $LoadingPanel/VBoxContainer/Spinner
|
||||
@onready var progress_bar: ProgressBar = $LoadingPanel/VBoxContainer/ProgressBar
|
||||
|
||||
# 加载状态
|
||||
var is_loading: bool = false
|
||||
var rotation_speed: float = 3.0 # 旋转速度(弧度/秒)
|
||||
|
||||
func _ready():
|
||||
"""初始化加载指示器"""
|
||||
# 默认隐藏
|
||||
hide()
|
||||
|
||||
# 设置统一的字体样式
|
||||
_setup_font_style()
|
||||
|
||||
print("LoadingIndicator initialized")
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
"""更新加载动画"""
|
||||
if is_loading and spinner:
|
||||
# 旋转加载图标
|
||||
spinner.rotation += rotation_speed * delta
|
||||
|
||||
## 显示加载状态
|
||||
func show_loading(message: String = "加载中...", show_progress: bool = false) -> void:
|
||||
"""
|
||||
显示加载状态
|
||||
@param message: 加载消息
|
||||
@param show_progress: 是否显示进度条
|
||||
"""
|
||||
is_loading = true
|
||||
|
||||
# 设置消息
|
||||
if loading_label:
|
||||
loading_label.text = message
|
||||
|
||||
# 设置进度条可见性
|
||||
if progress_bar:
|
||||
progress_bar.visible = show_progress
|
||||
if show_progress:
|
||||
progress_bar.value = 0
|
||||
|
||||
# 使用动画显示面板
|
||||
UIAnimationManager.scale_popup(self, 0.3, UIAnimationManager.EaseType.BOUNCE)
|
||||
|
||||
preload("res://scripts/Utils.gd").debug_print("Loading: " + message, "UI")
|
||||
|
||||
## 更新加载进度
|
||||
func update_progress(progress: float, message: String = "") -> void:
|
||||
"""
|
||||
更新加载进度
|
||||
@param progress: 进度值(0-100)
|
||||
@param message: 可选的消息更新
|
||||
"""
|
||||
if progress_bar:
|
||||
progress_bar.value = progress
|
||||
|
||||
if not message.is_empty() and loading_label:
|
||||
loading_label.text = message
|
||||
|
||||
## 隐藏加载状态
|
||||
func hide_loading() -> void:
|
||||
"""隐藏加载状态"""
|
||||
is_loading = false
|
||||
|
||||
# 使用动画隐藏面板
|
||||
UIAnimationManager.fade_out(self, 0.25)
|
||||
|
||||
preload("res://scripts/Utils.gd").debug_print("Loading complete", "UI")
|
||||
|
||||
## 显示连接中状态
|
||||
func show_connecting() -> void:
|
||||
"""显示连接中状态"""
|
||||
show_loading("正在连接服务器...", false)
|
||||
|
||||
## 显示登录中状态
|
||||
func show_logging_in() -> void:
|
||||
"""显示登录中状态"""
|
||||
show_loading("正在登录...", false)
|
||||
|
||||
## 显示加载世界状态
|
||||
func show_loading_world() -> void:
|
||||
"""显示加载世界状态"""
|
||||
show_loading("正在加载游戏世界...", true)
|
||||
|
||||
## 显示创建角色状态
|
||||
func show_creating_character() -> void:
|
||||
"""显示创建角色状态"""
|
||||
show_loading("正在创建角色...", false)
|
||||
|
||||
## 设置字体样式
|
||||
func _setup_font_style() -> void:
|
||||
"""设置统一的字体样式"""
|
||||
if loading_label:
|
||||
# 设置字体大小
|
||||
loading_label.add_theme_font_size_override("font_size", 18)
|
||||
# 设置文字颜色
|
||||
loading_label.add_theme_color_override("font_color", Color.WHITE)
|
||||
# 添加阴影效果使文字更清晰
|
||||
loading_label.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.8))
|
||||
loading_label.add_theme_constant_override("shadow_offset_x", 1)
|
||||
loading_label.add_theme_constant_override("shadow_offset_y", 1)
|
||||
# 确保文字居中
|
||||
loading_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
loading_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
1
scripts/LoadingIndicator.gd.uid
Normal file
1
scripts/LoadingIndicator.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://benobiu5q018p
|
||||
177
scripts/LoginScreen.gd
Normal file
177
scripts/LoginScreen.gd
Normal file
@@ -0,0 +1,177 @@
|
||||
extends Control
|
||||
class_name LoginScreen
|
||||
## 登录界面
|
||||
## 处理用户登录和角色创建入口
|
||||
|
||||
# UI 元素
|
||||
var username_input: LineEdit
|
||||
var login_button: Button
|
||||
var create_character_button: Button
|
||||
var status_label: Label
|
||||
var container: VBoxContainer
|
||||
|
||||
# 信号
|
||||
signal login_requested(username: String)
|
||||
signal create_character_requested()
|
||||
|
||||
func _ready():
|
||||
"""初始化登录界面"""
|
||||
_create_ui()
|
||||
_setup_animations()
|
||||
update_layout()
|
||||
|
||||
## 设置动画效果
|
||||
func _setup_animations():
|
||||
"""设置界面动画效果"""
|
||||
# 为移动设备优化触摸体验
|
||||
TouchFeedbackManager.optimize_ui_for_touch(self)
|
||||
|
||||
# 界面入场动画
|
||||
UIAnimationManager.fade_slide_in(container, "bottom", 0.5)
|
||||
|
||||
## 创建 UI 元素
|
||||
func _create_ui():
|
||||
"""创建登录界面的所有 UI 元素"""
|
||||
# 设置为全屏
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
|
||||
# 创建中心容器
|
||||
container = VBoxContainer.new()
|
||||
container.name = "Container"
|
||||
container.custom_minimum_size = Vector2(300, 0)
|
||||
# 设置容器锚点为居中
|
||||
container.anchor_left = 0.5
|
||||
container.anchor_right = 0.5
|
||||
container.anchor_top = 0.5
|
||||
container.anchor_bottom = 0.5
|
||||
container.offset_left = -150 # 容器宽度的一半
|
||||
container.offset_right = 150
|
||||
container.offset_top = -200 # 估算容器高度的一半
|
||||
container.offset_bottom = 200
|
||||
add_child(container)
|
||||
|
||||
# 标题
|
||||
var title = Label.new()
|
||||
title.text = "AI Town Game"
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
title.add_theme_font_size_override("font_size", 32)
|
||||
container.add_child(title)
|
||||
|
||||
# 间距
|
||||
container.add_child(_create_spacer(20))
|
||||
|
||||
# 用户名标签
|
||||
var username_label = Label.new()
|
||||
username_label.text = "用户名:"
|
||||
container.add_child(username_label)
|
||||
|
||||
# 用户名输入框
|
||||
username_input = LineEdit.new()
|
||||
username_input.placeholder_text = "输入你的用户名"
|
||||
username_input.custom_minimum_size = Vector2(0, 40)
|
||||
username_input.text_submitted.connect(_on_username_submitted)
|
||||
container.add_child(username_input)
|
||||
|
||||
# 间距
|
||||
container.add_child(_create_spacer(10))
|
||||
|
||||
# 登录按钮
|
||||
login_button = Button.new()
|
||||
login_button.text = "登录"
|
||||
login_button.custom_minimum_size = Vector2(0, 50)
|
||||
login_button.pressed.connect(_on_login_pressed)
|
||||
container.add_child(login_button)
|
||||
|
||||
# 间距
|
||||
container.add_child(_create_spacer(10))
|
||||
|
||||
# 创建角色按钮
|
||||
create_character_button = Button.new()
|
||||
create_character_button.text = "创建新角色"
|
||||
create_character_button.custom_minimum_size = Vector2(0, 50)
|
||||
create_character_button.pressed.connect(_on_create_character_pressed)
|
||||
container.add_child(create_character_button)
|
||||
|
||||
# 间距
|
||||
container.add_child(_create_spacer(20))
|
||||
|
||||
# 状态标签
|
||||
status_label = Label.new()
|
||||
status_label.text = ""
|
||||
status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
status_label.add_theme_color_override("font_color", Color(1, 0.5, 0.5))
|
||||
container.add_child(status_label)
|
||||
|
||||
## 创建间距
|
||||
func _create_spacer(height: float) -> Control:
|
||||
"""创建垂直间距"""
|
||||
var spacer = Control.new()
|
||||
spacer.custom_minimum_size = Vector2(0, height)
|
||||
return spacer
|
||||
|
||||
## 更新布局
|
||||
func update_layout():
|
||||
"""更新布局以适应窗口大小"""
|
||||
if not container:
|
||||
return
|
||||
|
||||
# 容器已经通过锚点设置为居中,无需手动计算位置
|
||||
# 锚点会自动适应窗口大小变化
|
||||
|
||||
## 显示状态消息
|
||||
func show_status(message: String, is_error: bool = false):
|
||||
"""
|
||||
显示状态消息
|
||||
@param message: 消息文本
|
||||
@param is_error: 是否为错误消息
|
||||
"""
|
||||
if status_label:
|
||||
status_label.text = message
|
||||
if is_error:
|
||||
status_label.add_theme_color_override("font_color", Color(1, 0.3, 0.3))
|
||||
else:
|
||||
status_label.add_theme_color_override("font_color", Color(0.3, 1, 0.3))
|
||||
|
||||
## 清除状态消息
|
||||
func clear_status():
|
||||
"""清除状态消息"""
|
||||
if status_label:
|
||||
status_label.text = ""
|
||||
|
||||
## 设置按钮启用状态
|
||||
func set_buttons_enabled(enabled: bool):
|
||||
"""
|
||||
设置所有按钮的启用状态
|
||||
@param enabled: 是否启用
|
||||
"""
|
||||
if login_button:
|
||||
login_button.disabled = not enabled
|
||||
if create_character_button:
|
||||
create_character_button.disabled = not enabled
|
||||
|
||||
## 登录按钮点击
|
||||
func _on_login_pressed():
|
||||
"""登录按钮被点击"""
|
||||
var username = username_input.text.strip_edges()
|
||||
|
||||
if username.is_empty():
|
||||
show_status("请输入用户名", true)
|
||||
# 添加错误动画反馈
|
||||
UIAnimationManager.shake_error(username_input, 8.0, 0.4)
|
||||
return
|
||||
|
||||
# 添加成功反馈动画
|
||||
UIAnimationManager.button_press_feedback(login_button)
|
||||
clear_status()
|
||||
login_requested.emit(username)
|
||||
|
||||
## 创建角色按钮点击
|
||||
func _on_create_character_pressed():
|
||||
"""创建角色按钮被点击"""
|
||||
create_character_requested.emit()
|
||||
|
||||
## 用户名输入框回车
|
||||
func _on_username_submitted(_text: String):
|
||||
"""用户名输入框按下回车"""
|
||||
_on_login_pressed()
|
||||
1
scripts/LoginScreen.gd.uid
Normal file
1
scripts/LoginScreen.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cfd7hksgbm8wj
|
||||
749
scripts/Main.gd
Normal file
749
scripts/Main.gd
Normal file
@@ -0,0 +1,749 @@
|
||||
extends Node
|
||||
## Main scene script
|
||||
## 主场景脚本,负责初始化和协调各个管理器
|
||||
|
||||
# 引用各个管理器节点
|
||||
@onready var network_manager = $NetworkManager
|
||||
@onready var game_state_manager = $GameStateManager
|
||||
@onready var ui_layer = $UILayer
|
||||
@onready var game_world = $GameWorld
|
||||
@onready var error_notification: ErrorNotification = null
|
||||
@onready var loading_indicator: LoadingIndicator = null
|
||||
|
||||
# 游戏场景和角色
|
||||
var office_scene: Node2D = null
|
||||
var world_manager: Node = null
|
||||
var input_handler: Node = null
|
||||
var player_character: CharacterBody2D = null
|
||||
|
||||
# 测试管理器
|
||||
var dialogue_test_manager = null
|
||||
|
||||
# 社交系统
|
||||
var social_manager: SocialManager = null
|
||||
|
||||
# 服务器配置
|
||||
var server_url: String = "ws://localhost:8080"
|
||||
|
||||
func _ready():
|
||||
"""主场景初始化"""
|
||||
print("[MAIN] AI Town Game - Main scene loaded")
|
||||
print("[MAIN] Godot version: ", Engine.get_version_info())
|
||||
|
||||
# 初始化游戏
|
||||
_initialize_game()
|
||||
|
||||
func _initialize_game():
|
||||
"""初始化游戏系统"""
|
||||
print("[MAIN] Initializing game systems...")
|
||||
|
||||
# 初始化安全系统
|
||||
_initialize_security()
|
||||
|
||||
# 初始化社交系统
|
||||
_initialize_social_systems()
|
||||
|
||||
# 初始化UI组件
|
||||
_setup_ui_components()
|
||||
|
||||
# 连接信号
|
||||
_connect_all_signals()
|
||||
|
||||
# 显示登录界面
|
||||
ui_layer.show_screen("login")
|
||||
|
||||
print("[MAIN] Game initialization complete")
|
||||
|
||||
## 初始化安全系统
|
||||
func _initialize_security():
|
||||
"""初始化安全配置和系统"""
|
||||
print("[SECURITY] Initializing security systems...")
|
||||
|
||||
# 初始化安全配置
|
||||
SecurityConfig.initialize()
|
||||
|
||||
# 验证安全配置
|
||||
if not SecurityConfig.validate_config():
|
||||
print("[ERROR] Security configuration validation failed")
|
||||
if error_notification:
|
||||
error_notification.show_error("安全系统初始化失败")
|
||||
|
||||
var security_level = SecurityConfig.get_security_level()
|
||||
print("[SECURITY] Security level: ", security_level)
|
||||
|
||||
# 如果是高安全级别,记录安全事件
|
||||
if security_level == "high":
|
||||
print("[INFO] [SYSTEM] High security mode activated")
|
||||
|
||||
## 初始化社交系统
|
||||
func _initialize_social_systems():
|
||||
"""初始化社交系统"""
|
||||
print("[SOCIAL] Initializing social systems...")
|
||||
|
||||
# 创建社交管理器
|
||||
social_manager = SocialManager.new()
|
||||
social_manager.name = "SocialManager"
|
||||
add_child(social_manager)
|
||||
|
||||
# 设置网络管理器引用
|
||||
social_manager.set_network_manager(network_manager)
|
||||
|
||||
# 连接社交系统信号
|
||||
social_manager.social_notification.connect(_on_social_notification)
|
||||
social_manager.friend_activity.connect(_on_friend_activity)
|
||||
|
||||
print("[SOCIAL] Social systems initialized")
|
||||
|
||||
## 设置UI组件
|
||||
func _setup_ui_components():
|
||||
"""设置UI组件(错误通知和加载指示器)"""
|
||||
_setup_error_notification()
|
||||
_setup_loading_indicator()
|
||||
|
||||
## 连接所有信号
|
||||
func _connect_all_signals():
|
||||
"""连接所有必要的信号"""
|
||||
# 游戏状态管理器信号
|
||||
if not game_state_manager.state_changed.is_connected(_on_game_state_changed):
|
||||
game_state_manager.state_changed.connect(_on_game_state_changed)
|
||||
|
||||
# 网络管理器信号
|
||||
_connect_network_signals()
|
||||
|
||||
# UI信号(延迟连接)
|
||||
_connect_ui_signals()
|
||||
|
||||
## 连接网络信号
|
||||
func _connect_network_signals():
|
||||
"""连接网络管理器的所有信号"""
|
||||
var signals_to_connect = [
|
||||
["connected_to_server", _on_connected_to_server],
|
||||
["disconnected_from_server", _on_disconnected_from_server],
|
||||
["connection_error", _on_connection_error],
|
||||
["message_received", _on_message_received]
|
||||
]
|
||||
|
||||
for signal_data in signals_to_connect:
|
||||
var signal_name = signal_data[0]
|
||||
var callback = signal_data[1]
|
||||
var signal_obj = network_manager.get(signal_name)
|
||||
if signal_obj and not signal_obj.is_connected(callback):
|
||||
signal_obj.connect(callback)
|
||||
|
||||
## 设置错误通知系统
|
||||
func _setup_error_notification():
|
||||
"""设置错误通知系统"""
|
||||
var error_scene = load("res://scenes/ErrorNotification.tscn")
|
||||
if error_scene:
|
||||
error_notification = error_scene.instantiate()
|
||||
ui_layer.add_child(error_notification)
|
||||
print("[MAIN] Error notification system initialized")
|
||||
else:
|
||||
print("[ERROR] Failed to load ErrorNotification scene")
|
||||
|
||||
## 设置加载指示器
|
||||
func _setup_loading_indicator():
|
||||
"""设置加载指示器"""
|
||||
var loading_scene = load("res://scenes/LoadingIndicator.tscn")
|
||||
if loading_scene:
|
||||
loading_indicator = loading_scene.instantiate()
|
||||
ui_layer.add_child(loading_indicator)
|
||||
print("[MAIN] Loading indicator initialized")
|
||||
else:
|
||||
print("[ERROR] Failed to load LoadingIndicator scene")
|
||||
|
||||
## 连接 UI 信号
|
||||
func _connect_ui_signals():
|
||||
"""连接 UI 层的信号"""
|
||||
# 延迟一帧,确保 UI 元素已创建
|
||||
await get_tree().process_frame
|
||||
|
||||
# 连接登录界面信号
|
||||
if ui_layer.login_screen:
|
||||
ui_layer.login_screen.login_requested.connect(login_to_server)
|
||||
ui_layer.login_screen.create_character_requested.connect(_on_create_character_ui_requested)
|
||||
print("Login screen signals connected")
|
||||
|
||||
# 注意:角色创建界面会在切换到 CHARACTER_CREATION 状态时创建和连接
|
||||
|
||||
## 处理创建角色 UI 请求
|
||||
func _on_create_character_ui_requested():
|
||||
"""处理创建角色 UI 请求"""
|
||||
# 检查是否已连接并认证
|
||||
if not network_manager.is_server_connected():
|
||||
# 未连接,显示错误提示
|
||||
if error_notification:
|
||||
error_notification.show_error("请先登录后再创建角色")
|
||||
return
|
||||
|
||||
# 已连接,切换到角色创建界面
|
||||
game_state_manager.change_state(GameStateManager.GameState.CHARACTER_CREATION)
|
||||
|
||||
## 处理返回登录请求
|
||||
func _on_back_to_login_requested():
|
||||
"""处理返回登录请求"""
|
||||
game_state_manager.change_state(GameStateManager.GameState.LOGIN)
|
||||
|
||||
## 游戏状态变化处理
|
||||
func _on_game_state_changed(old_state, new_state):
|
||||
"""
|
||||
处理游戏状态变化
|
||||
@param old_state: 旧状态
|
||||
@param new_state: 新状态
|
||||
"""
|
||||
print("Game state changed: ", GameStateManager.GameState.keys()[old_state], " -> ", GameStateManager.GameState.keys()[new_state])
|
||||
|
||||
# 根据状态切换 UI 和场景
|
||||
match new_state:
|
||||
GameStateManager.GameState.LOGIN:
|
||||
ui_layer.show_screen("login")
|
||||
_cleanup_game_world()
|
||||
|
||||
GameStateManager.GameState.CHARACTER_CREATION:
|
||||
ui_layer.show_screen("character_creation")
|
||||
# 连接角色创建界面的信号(如果还没连接)
|
||||
_connect_character_creation_signals()
|
||||
|
||||
GameStateManager.GameState.IN_GAME:
|
||||
ui_layer.show_screen("hud")
|
||||
_load_game_world()
|
||||
# 更新HUD的网络状态
|
||||
_update_hud_network_status()
|
||||
|
||||
GameStateManager.GameState.DISCONNECTED:
|
||||
ui_layer.show_screen("login")
|
||||
_cleanup_game_world()
|
||||
|
||||
## 连接角色创建界面信号
|
||||
func _connect_character_creation_signals():
|
||||
"""连接角色创建界面的信号"""
|
||||
if ui_layer.character_creation and not ui_layer.character_creation.character_created.is_connected(create_character):
|
||||
ui_layer.character_creation.character_created.connect(create_character)
|
||||
ui_layer.character_creation.back_requested.connect(_on_back_to_login_requested)
|
||||
print("Character creation signals connected")
|
||||
|
||||
## 网络连接成功
|
||||
func _on_connected_to_server():
|
||||
"""服务器连接成功"""
|
||||
print("Connected to server successfully")
|
||||
if loading_indicator:
|
||||
loading_indicator.hide_loading()
|
||||
if error_notification:
|
||||
error_notification.show_success("连接成功!")
|
||||
# 更新HUD网络状态
|
||||
_update_hud_network_status()
|
||||
|
||||
## 更新HUD网络状态
|
||||
func _update_hud_network_status():
|
||||
"""更新HUD的网络状态显示"""
|
||||
if ui_layer.hud:
|
||||
ui_layer.hud.update_network_status(network_manager.is_server_connected())
|
||||
|
||||
## 网络断开连接
|
||||
func _on_disconnected_from_server():
|
||||
"""服务器断开连接"""
|
||||
print("Disconnected from server")
|
||||
if loading_indicator:
|
||||
loading_indicator.hide_loading()
|
||||
|
||||
# 根据当前状态决定如何处理断线
|
||||
var current_state = game_state_manager.current_state
|
||||
if current_state == GameStateManager.GameState.IN_GAME:
|
||||
# 在游戏中断线,显示警告但不退出游戏
|
||||
if error_notification:
|
||||
error_notification.show_warning("与服务器断开连接,正在尝试重连...", 3.0)
|
||||
# 不改变游戏状态,让玩家继续游戏(离线模式)
|
||||
else:
|
||||
# 在登录或角色创建时断线,返回登录界面
|
||||
if error_notification:
|
||||
error_notification.show_warning("与服务器断开连接")
|
||||
game_state_manager.change_state(GameStateManager.GameState.DISCONNECTED)
|
||||
|
||||
## 网络连接错误
|
||||
func _on_connection_error(error: String):
|
||||
"""网络连接错误"""
|
||||
print("Connection error: ", error)
|
||||
if loading_indicator:
|
||||
loading_indicator.hide_loading()
|
||||
if error_notification:
|
||||
error_notification.show_network_error(error)
|
||||
|
||||
## 接收到网络消息
|
||||
func _on_message_received(message: Dictionary):
|
||||
"""
|
||||
处理接收到的网络消息
|
||||
@param message: 消息字典
|
||||
"""
|
||||
if not message.has("type"):
|
||||
print("Warning: Received message without type")
|
||||
return
|
||||
|
||||
var msg_type = message["type"]
|
||||
print("Received message: ", msg_type)
|
||||
|
||||
# 根据消息类型处理
|
||||
match msg_type:
|
||||
"auth_response":
|
||||
_handle_auth_response(message)
|
||||
"character_create":
|
||||
_handle_character_create_response(message)
|
||||
"character_move":
|
||||
_handle_character_move(message)
|
||||
"character_state":
|
||||
_handle_character_state(message)
|
||||
"world_state":
|
||||
_handle_world_state(message)
|
||||
"friend_request", "friend_response", "private_message", "event_invitation", "social_update":
|
||||
# 社交相关消息由SocialManager处理
|
||||
pass
|
||||
_:
|
||||
print("Unknown message type: ", msg_type)
|
||||
|
||||
## 处理认证响应
|
||||
func _handle_auth_response(message: Dictionary):
|
||||
"""处理认证响应"""
|
||||
var data = message.get("data", {})
|
||||
if data.get("success", false):
|
||||
print("Authentication successful")
|
||||
# 进入角色创建或游戏
|
||||
game_state_manager.change_state(GameStateManager.GameState.CHARACTER_CREATION)
|
||||
else:
|
||||
var error_msg = data.get("error", "Unknown error")
|
||||
print("Authentication failed: ", error_msg)
|
||||
if error_notification:
|
||||
error_notification.show_error("登录失败: " + error_msg)
|
||||
|
||||
## 处理角色创建响应
|
||||
func _handle_character_create_response(message: Dictionary):
|
||||
"""处理角色创建响应"""
|
||||
var data = message.get("data", {})
|
||||
if data.get("success", false):
|
||||
print("Character created successfully")
|
||||
var character_data = data.get("character", {})
|
||||
game_state_manager.player_data = character_data
|
||||
|
||||
# 隐藏加载指示器
|
||||
if loading_indicator:
|
||||
loading_indicator.hide_loading()
|
||||
|
||||
game_state_manager.change_state(GameStateManager.GameState.IN_GAME)
|
||||
else:
|
||||
var error_msg = data.get("error", "Unknown error")
|
||||
print("Character creation failed: ", error_msg)
|
||||
|
||||
# 隐藏加载指示器并显示错误
|
||||
if loading_indicator:
|
||||
loading_indicator.hide_loading()
|
||||
if error_notification:
|
||||
error_notification.show_error("创建角色失败: " + error_msg)
|
||||
|
||||
## 处理角色移动消息
|
||||
func _handle_character_move(message: Dictionary):
|
||||
"""处理其他角色的移动"""
|
||||
if not world_manager:
|
||||
return
|
||||
|
||||
var data = message.get("data", {})
|
||||
var character_id = data.get("characterId", "")
|
||||
var position = data.get("position", {})
|
||||
|
||||
print("Received character_move message - ID: ", character_id, " Position: ", position)
|
||||
|
||||
# 检查是否是玩家自己的角色
|
||||
var player_id = game_state_manager.player_data.get("id", "") if game_state_manager.player_data else ""
|
||||
if character_id == player_id:
|
||||
print("IGNORED: Received move message for player's own character - preventing auto-movement")
|
||||
return # 忽略玩家自己的移动消息,避免网络回环导致自动移动
|
||||
|
||||
if character_id and position:
|
||||
var pos = Vector2(position.get("x", 0), position.get("y", 0))
|
||||
world_manager.update_character_position(character_id, pos)
|
||||
|
||||
## 处理角色状态更新
|
||||
func _handle_character_state(message: Dictionary):
|
||||
"""处理角色状态更新"""
|
||||
if not world_manager:
|
||||
return
|
||||
|
||||
var data = message.get("data", {})
|
||||
var character_id = data.get("characterId", "")
|
||||
var character_name = data.get("name", "")
|
||||
var position = data.get("position", {})
|
||||
var is_online = data.get("isOnline", false)
|
||||
|
||||
# 检查是否是玩家自己的角色,避免处理自己的状态更新
|
||||
var player_id = game_state_manager.player_data.get("id", "") if game_state_manager.player_data else ""
|
||||
if character_id == player_id:
|
||||
print("Ignoring state update for player's own character: ", character_id)
|
||||
return
|
||||
|
||||
if character_id:
|
||||
var state = {
|
||||
"id": character_id,
|
||||
"name": character_name,
|
||||
"position": position,
|
||||
"isOnline": is_online
|
||||
}
|
||||
|
||||
print("Processing character state update - ID: ", character_id, " Online: ", is_online)
|
||||
world_manager.update_character_state(character_id, state)
|
||||
|
||||
## 处理世界状态同步
|
||||
func _handle_world_state(message: Dictionary):
|
||||
"""处理世界状态同步"""
|
||||
print("Received world state")
|
||||
var data = message.get("data", {})
|
||||
var characters = data.get("characters", [])
|
||||
|
||||
if world_manager:
|
||||
# 更新所有角色
|
||||
for character_data in characters:
|
||||
var char_id = character_data.get("id", "")
|
||||
if char_id and char_id != game_state_manager.player_data.get("id", ""):
|
||||
# 不是玩家自己,生成或更新远程角色
|
||||
world_manager.spawn_or_update_character(character_data)
|
||||
|
||||
## 加载游戏世界
|
||||
func _load_game_world():
|
||||
"""加载游戏世界场景"""
|
||||
print("Loading game world...")
|
||||
|
||||
# 加载 Datawhale 办公室场景
|
||||
var office_scene_path = "res://scenes/DatawhaleOffice.tscn"
|
||||
var office_packed = load(office_scene_path)
|
||||
|
||||
if office_packed:
|
||||
office_scene = office_packed.instantiate()
|
||||
game_world.add_child(office_scene)
|
||||
|
||||
# 获取世界管理器
|
||||
world_manager = office_scene.get_node_or_null("WorldManager")
|
||||
if not world_manager:
|
||||
# 如果场景中没有,创建一个
|
||||
world_manager = preload("res://scripts/WorldManager.gd").new()
|
||||
world_manager.name = "WorldManager"
|
||||
office_scene.add_child(world_manager)
|
||||
|
||||
# 设置角色容器
|
||||
var characters_container = office_scene.get_node_or_null("Characters")
|
||||
if characters_container:
|
||||
world_manager.set_character_container(characters_container)
|
||||
print("Character container set for WorldManager")
|
||||
else:
|
||||
print("Warning: Characters container not found in office scene")
|
||||
|
||||
# 创建玩家角色
|
||||
_spawn_player_character()
|
||||
|
||||
# 创建输入处理器
|
||||
_setup_input_handler()
|
||||
|
||||
# 初始化对话测试管理器
|
||||
_setup_dialogue_test_manager()
|
||||
|
||||
print("Game world loaded")
|
||||
else:
|
||||
print("Error: Failed to load office scene")
|
||||
|
||||
## 生成玩家角色
|
||||
func _spawn_player_character():
|
||||
"""生成玩家角色"""
|
||||
print("Spawning player character...")
|
||||
|
||||
var player_scene_path = "res://scenes/PlayerCharacter.tscn"
|
||||
var player_packed = load(player_scene_path)
|
||||
|
||||
if player_packed and office_scene:
|
||||
player_character = player_packed.instantiate()
|
||||
|
||||
# 获取角色容器
|
||||
var characters_container = office_scene.get_characters_container()
|
||||
if characters_container:
|
||||
characters_container.add_child(player_character)
|
||||
else:
|
||||
office_scene.add_child(player_character)
|
||||
|
||||
# 初始化玩家角色
|
||||
var player_data = game_state_manager.player_data
|
||||
print("Player data from game_state_manager: ", player_data)
|
||||
|
||||
if player_data:
|
||||
player_character.initialize(player_data)
|
||||
|
||||
# 设置初始位置(场景中央)- 覆盖服务器位置
|
||||
print("Setting player position to scene center: Vector2(1000, 750)")
|
||||
player_character.global_position = Vector2(1000, 750)
|
||||
|
||||
# 确保角色完全静止 - 延迟执行确保所有系统都已初始化
|
||||
_ensure_player_character_stopped()
|
||||
|
||||
# 设置相机跟随
|
||||
if office_scene.has_method("set_camera_target"):
|
||||
office_scene.set_camera_target(player_character)
|
||||
|
||||
# 连接角色信号
|
||||
player_character.position_updated.connect(_on_player_position_updated)
|
||||
|
||||
print("Player character spawned")
|
||||
else:
|
||||
print("Error: Failed to load player character scene")
|
||||
|
||||
## 确保玩家角色停止移动
|
||||
func _ensure_player_character_stopped():
|
||||
"""确保玩家角色完全停止移动"""
|
||||
if player_character and player_character.has_method("_reset_movement_state"):
|
||||
# 延迟执行,确保所有初始化完成
|
||||
await get_tree().process_frame
|
||||
player_character._reset_movement_state()
|
||||
print("Player character movement state ensured to be stopped")
|
||||
|
||||
# 同时清除输入处理器的状态
|
||||
if input_handler and input_handler.has_method("_clear_all_movement_state"):
|
||||
input_handler._clear_all_movement_state()
|
||||
print("Input handler movement state cleared")
|
||||
|
||||
## 设置输入处理器
|
||||
func _setup_input_handler():
|
||||
"""设置输入处理器"""
|
||||
print("Setting up input handler...")
|
||||
|
||||
# 创建输入处理器
|
||||
input_handler = preload("res://scripts/InputHandler.gd").new()
|
||||
input_handler.name = "InputHandler"
|
||||
add_child(input_handler)
|
||||
|
||||
# 连接输入信号
|
||||
input_handler.move_input.connect(_on_move_input)
|
||||
input_handler.interact_input.connect(_on_interact_input)
|
||||
|
||||
print("Input handler ready")
|
||||
|
||||
## 玩家移动输入
|
||||
func _on_move_input(direction: Vector2):
|
||||
"""
|
||||
处理玩家移动输入
|
||||
@param direction: 移动方向
|
||||
"""
|
||||
if direction != Vector2.ZERO:
|
||||
print("Player input received - Direction: ", direction)
|
||||
|
||||
if player_character:
|
||||
player_character.move_to(direction)
|
||||
|
||||
## 玩家交互输入
|
||||
func _on_interact_input():
|
||||
"""处理玩家交互输入"""
|
||||
print("Interact key pressed (E)")
|
||||
|
||||
if not player_character:
|
||||
print("No player character")
|
||||
return
|
||||
|
||||
# 获取附近的NPC
|
||||
if dialogue_test_manager:
|
||||
var player_pos = player_character.global_position
|
||||
var nearby_npcs = dialogue_test_manager.get_nearby_npcs(player_pos, 100.0)
|
||||
|
||||
if nearby_npcs.size() > 0:
|
||||
var closest_npc = nearby_npcs[0]
|
||||
print("Starting dialogue with: ", closest_npc.name)
|
||||
|
||||
# 开始对话
|
||||
dialogue_test_manager.start_dialogue_with_npc(closest_npc.id)
|
||||
|
||||
# 显示对话框
|
||||
if ui_layer and ui_layer.dialogue_box:
|
||||
ui_layer.show_dialogue(closest_npc.name)
|
||||
else:
|
||||
print("Dialogue box not available")
|
||||
else:
|
||||
print("No NPCs nearby to interact with")
|
||||
# 显示提示
|
||||
if ui_layer and ui_layer.hud:
|
||||
ui_layer.hud.show_interaction_hint("附近没有可对话的角色")
|
||||
# 2秒后隐藏提示
|
||||
await get_tree().create_timer(2.0).timeout
|
||||
ui_layer.hud.hide_interaction_hint()
|
||||
else:
|
||||
print("Dialogue test manager not available")
|
||||
|
||||
## 对话消息接收处理
|
||||
func _on_dialogue_message_received(sender: String, message: String):
|
||||
"""处理接收到的对话消息,显示在UI中"""
|
||||
if ui_layer and ui_layer.dialogue_box:
|
||||
# 只显示NPC的消息,玩家消息已经在DialogueBox中直接显示了
|
||||
if sender != "player":
|
||||
var display_name = sender
|
||||
ui_layer.dialogue_box.add_message(display_name, message)
|
||||
else:
|
||||
# 玩家消息已经显示,这里只做日志记录
|
||||
print("Player message processed: ", message)
|
||||
|
||||
## 玩家位置更新
|
||||
func _on_player_position_updated(new_position: Vector2):
|
||||
"""
|
||||
玩家位置更新时发送到服务器
|
||||
@param new_position: 新位置
|
||||
"""
|
||||
if network_manager.is_server_connected():
|
||||
var message = MessageProtocol.create_character_move("", new_position, Vector2.ZERO)
|
||||
network_manager.send_message(message)
|
||||
|
||||
## 清理游戏世界
|
||||
func _cleanup_game_world():
|
||||
"""清理游戏世界"""
|
||||
print("Cleaning up game world...")
|
||||
|
||||
if input_handler:
|
||||
input_handler.queue_free()
|
||||
input_handler = null
|
||||
|
||||
if office_scene:
|
||||
office_scene.queue_free()
|
||||
office_scene = null
|
||||
|
||||
player_character = null
|
||||
world_manager = null
|
||||
|
||||
print("Game world cleaned up")
|
||||
|
||||
## 登录到服务器
|
||||
func login_to_server(username: String):
|
||||
"""
|
||||
登录到服务器
|
||||
@param username: 用户名
|
||||
"""
|
||||
print("Attempting to login as: ", username)
|
||||
|
||||
# 显示加载状态
|
||||
if loading_indicator:
|
||||
loading_indicator.show_connecting()
|
||||
|
||||
# 如果已经连接,直接发送认证请求
|
||||
if network_manager.is_server_connected():
|
||||
_send_auth_request(username)
|
||||
return
|
||||
|
||||
# 否则先连接到服务器
|
||||
network_manager.connect_to_server(server_url)
|
||||
|
||||
# 等待连接成功
|
||||
await network_manager.connected_to_server
|
||||
|
||||
# 连接成功后发送认证请求
|
||||
_send_auth_request(username)
|
||||
|
||||
## 发送认证请求
|
||||
func _send_auth_request(username: String):
|
||||
"""发送认证请求到服务器"""
|
||||
print("Sending auth request for: ", username)
|
||||
var auth_message = MessageProtocol.create_auth_request(username)
|
||||
network_manager.send_message(auth_message)
|
||||
|
||||
## 创建角色
|
||||
func create_character(character_name: String, personalization_data: Dictionary = {}):
|
||||
"""
|
||||
创建新角色
|
||||
@param character_name: 角色名称
|
||||
@param personalization_data: 个性化数据(外观、个性等)
|
||||
"""
|
||||
print("Creating character: ", character_name)
|
||||
print("Personalization data: ", personalization_data)
|
||||
|
||||
# 检查是否已连接
|
||||
if not network_manager.is_server_connected():
|
||||
if error_notification:
|
||||
error_notification.show_error("未连接到服务器,无法创建角色")
|
||||
# 返回登录界面
|
||||
game_state_manager.change_state(GameStateManager.GameState.LOGIN)
|
||||
return
|
||||
|
||||
# 显示加载状态
|
||||
if loading_indicator:
|
||||
loading_indicator.show_creating_character()
|
||||
|
||||
# 创建包含个性化数据的角色创建消息
|
||||
var create_message = MessageProtocol.create_character_create(character_name, personalization_data)
|
||||
network_manager.send_message(create_message)
|
||||
|
||||
## 设置对话测试管理器
|
||||
func _setup_dialogue_test_manager():
|
||||
"""设置对话测试管理器"""
|
||||
print("Setting up dialogue test manager...")
|
||||
|
||||
# 创建简单对话测试脚本
|
||||
var test_script = load("res://scripts/SimpleDialogueTest.gd")
|
||||
dialogue_test_manager = test_script.new()
|
||||
dialogue_test_manager.name = "SimpleDialogueTest"
|
||||
add_child(dialogue_test_manager)
|
||||
|
||||
# 获取对话系统引用
|
||||
var dialogue_system = office_scene.get_node_or_null("DialogueSystem")
|
||||
if not dialogue_system:
|
||||
# 如果场景中没有,创建一个
|
||||
var dialogue_script = load("res://scripts/DialogueSystem.gd")
|
||||
dialogue_system = dialogue_script.new()
|
||||
dialogue_system.name = "DialogueSystem"
|
||||
office_scene.add_child(dialogue_system)
|
||||
|
||||
# 设置测试环境
|
||||
dialogue_test_manager.setup_test(world_manager, dialogue_system)
|
||||
|
||||
# 连接对话系统信号
|
||||
if dialogue_system.message_received.connect(dialogue_test_manager.handle_player_message) != OK:
|
||||
print("Failed to connect dialogue signal")
|
||||
|
||||
# 连接对话系统消息到UI
|
||||
if dialogue_system.message_received.connect(_on_dialogue_message_received) != OK:
|
||||
print("Failed to connect dialogue UI signal")
|
||||
|
||||
# 设置对话系统的社交管理器引用
|
||||
if social_manager:
|
||||
dialogue_system.set_social_manager(social_manager)
|
||||
|
||||
# 延迟生成测试NPC,确保所有系统都已初始化
|
||||
if player_character:
|
||||
var player_pos = player_character.global_position
|
||||
# 使用call_deferred确保在下一帧执行
|
||||
dialogue_test_manager.call_deferred("spawn_test_npcs", player_pos)
|
||||
|
||||
print("Dialogue test manager setup complete")
|
||||
|
||||
## 处理社交通知
|
||||
func _on_social_notification(notification_type: String, title: String, message: String, _data: Dictionary):
|
||||
"""
|
||||
处理社交系统通知
|
||||
@param notification_type: 通知类型
|
||||
@param title: 通知标题
|
||||
@param message: 通知消息
|
||||
@param _data: 通知数据 (暂未使用)
|
||||
"""
|
||||
print("[SOCIAL] Notification: ", notification_type, " - ", title, ": ", message)
|
||||
|
||||
# 显示通知给用户
|
||||
if error_notification:
|
||||
match notification_type:
|
||||
"friend_request", "friend_accepted", "event_invitation":
|
||||
error_notification.show_success(message, 5.0)
|
||||
"friend_online":
|
||||
error_notification.show_info(message, 3.0)
|
||||
"milestone":
|
||||
error_notification.show_success(message, 8.0)
|
||||
_:
|
||||
error_notification.show_info(message, 4.0)
|
||||
|
||||
## 处理好友活动
|
||||
func _on_friend_activity(activity_type: String, data: Dictionary):
|
||||
"""
|
||||
处理好友活动事件
|
||||
@param activity_type: 活动类型
|
||||
@param data: 活动数据
|
||||
"""
|
||||
print("[SOCIAL] Friend activity: ", activity_type, " - ", data)
|
||||
|
||||
# 这里可以更新UI显示好友活动
|
||||
# 比如更新好友列表、活动日志等
|
||||
|
||||
func _process(_delta):
|
||||
# 主循环处理
|
||||
pass
|
||||
1
scripts/Main.gd.uid
Normal file
1
scripts/Main.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://o0k8pitjgwpm
|
||||
170
scripts/MessageProtocol.gd
Normal file
170
scripts/MessageProtocol.gd
Normal file
@@ -0,0 +1,170 @@
|
||||
extends Node
|
||||
class_name MessageProtocol
|
||||
## 消息协议定义
|
||||
## 定义客户端和服务器之间的消息类型和格式
|
||||
|
||||
# 消息类型常量
|
||||
enum MessageType {
|
||||
AUTH_REQUEST, # 身份验证请求
|
||||
AUTH_RESPONSE, # 身份验证响应
|
||||
CHARACTER_CREATE, # 创建角色
|
||||
CHARACTER_MOVE, # 角色移动
|
||||
CHARACTER_STATE, # 角色状态更新
|
||||
DIALOGUE_SEND, # 发送对话
|
||||
WORLD_STATE, # 世界状态同步
|
||||
PING, # 心跳包
|
||||
PONG, # 心跳响应
|
||||
ERROR # 错误消息
|
||||
}
|
||||
|
||||
# 消息类型字符串映射
|
||||
const MESSAGE_TYPE_STRINGS = {
|
||||
MessageType.AUTH_REQUEST: "auth_request",
|
||||
MessageType.AUTH_RESPONSE: "auth_response",
|
||||
MessageType.CHARACTER_CREATE: "character_create",
|
||||
MessageType.CHARACTER_MOVE: "character_move",
|
||||
MessageType.CHARACTER_STATE: "character_state",
|
||||
MessageType.DIALOGUE_SEND: "dialogue_send",
|
||||
MessageType.WORLD_STATE: "world_state",
|
||||
MessageType.PING: "ping",
|
||||
MessageType.PONG: "pong",
|
||||
MessageType.ERROR: "error"
|
||||
}
|
||||
|
||||
# 反向映射:字符串到枚举
|
||||
static var STRING_TO_MESSAGE_TYPE = {}
|
||||
|
||||
static func _ensure_initialized():
|
||||
"""确保反向映射已初始化"""
|
||||
if STRING_TO_MESSAGE_TYPE.is_empty():
|
||||
for type in MESSAGE_TYPE_STRINGS:
|
||||
STRING_TO_MESSAGE_TYPE[MESSAGE_TYPE_STRINGS[type]] = type
|
||||
|
||||
## 创建消息
|
||||
static func create_message(type: MessageType, data: Dictionary = {}) -> Dictionary:
|
||||
"""
|
||||
创建标准格式的消息
|
||||
@param type: 消息类型
|
||||
@param data: 消息数据
|
||||
@return: 格式化的消息字典
|
||||
"""
|
||||
return {
|
||||
"type": MESSAGE_TYPE_STRINGS[type],
|
||||
"data": data,
|
||||
"timestamp": Time.get_unix_time_from_system() * 1000
|
||||
}
|
||||
|
||||
## 序列化消息为 JSON
|
||||
static func serialize(message: Dictionary) -> String:
|
||||
"""
|
||||
将消息字典序列化为 JSON 字符串
|
||||
@param message: 消息字典
|
||||
@return: JSON 字符串
|
||||
"""
|
||||
return JSON.stringify(message)
|
||||
|
||||
## 反序列化 JSON 为消息
|
||||
static func deserialize(json_string: String) -> Dictionary:
|
||||
"""
|
||||
将 JSON 字符串反序列化为消息字典
|
||||
@param json_string: JSON 字符串
|
||||
@return: 消息字典,如果解析失败返回空字典
|
||||
"""
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(json_string)
|
||||
|
||||
if parse_result == OK:
|
||||
return json.data
|
||||
else:
|
||||
push_error("Failed to parse JSON: " + json_string)
|
||||
return {}
|
||||
|
||||
## 验证消息格式
|
||||
static func validate_message(message: Dictionary) -> bool:
|
||||
"""
|
||||
验证消息是否符合协议格式
|
||||
@param message: 消息字典
|
||||
@return: 是否有效
|
||||
"""
|
||||
if not message.has("type"):
|
||||
return false
|
||||
if not message.has("data"):
|
||||
return false
|
||||
if not message.has("timestamp"):
|
||||
return false
|
||||
|
||||
# 确保初始化并验证类型是否有效
|
||||
_ensure_initialized()
|
||||
if not STRING_TO_MESSAGE_TYPE.has(message["type"]):
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
## 获取消息类型枚举
|
||||
static func get_message_type(message: Dictionary) -> MessageType:
|
||||
"""
|
||||
从消息中获取类型枚举
|
||||
@param message: 消息字典
|
||||
@return: 消息类型枚举
|
||||
"""
|
||||
_ensure_initialized()
|
||||
if message.has("type") and STRING_TO_MESSAGE_TYPE.has(message["type"]):
|
||||
return STRING_TO_MESSAGE_TYPE[message["type"]]
|
||||
return MessageType.ERROR
|
||||
|
||||
## 创建身份验证请求
|
||||
static func create_auth_request(username: String) -> Dictionary:
|
||||
"""创建身份验证请求消息"""
|
||||
return create_message(MessageType.AUTH_REQUEST, {
|
||||
"username": username
|
||||
})
|
||||
|
||||
## 创建角色创建请求
|
||||
static func create_character_create(character_name: String, personalization_data: Dictionary = {}) -> Dictionary:
|
||||
"""创建角色创建请求消息"""
|
||||
var data = {
|
||||
"name": character_name
|
||||
}
|
||||
|
||||
# 如果有个性化数据,添加到消息中
|
||||
if not personalization_data.is_empty():
|
||||
data["personalization"] = personalization_data
|
||||
|
||||
return create_message(MessageType.CHARACTER_CREATE, data)
|
||||
|
||||
## 创建角色移动消息
|
||||
static func create_character_move(character_id: String, position: Vector2, direction: Vector2) -> Dictionary:
|
||||
"""创建角色移动消息"""
|
||||
return create_message(MessageType.CHARACTER_MOVE, {
|
||||
"character_id": character_id,
|
||||
"position": {
|
||||
"x": position.x,
|
||||
"y": position.y
|
||||
},
|
||||
"direction": {
|
||||
"x": direction.x,
|
||||
"y": direction.y
|
||||
}
|
||||
})
|
||||
|
||||
## 创建对话消息
|
||||
static func create_dialogue_send(sender_id: String, receiver_id: String, message_text: String) -> Dictionary:
|
||||
"""创建对话消息"""
|
||||
return create_message(MessageType.DIALOGUE_SEND, {
|
||||
"sender_id": sender_id,
|
||||
"receiver_id": receiver_id,
|
||||
"message": message_text
|
||||
})
|
||||
|
||||
## 创建心跳包
|
||||
static func create_ping() -> Dictionary:
|
||||
"""创建心跳包"""
|
||||
return create_message(MessageType.PING, {})
|
||||
|
||||
## 创建错误消息
|
||||
static func create_error(error_message: String, error_code: int = 0) -> Dictionary:
|
||||
"""创建错误消息"""
|
||||
return create_message(MessageType.ERROR, {
|
||||
"message": error_message,
|
||||
"code": error_code
|
||||
})
|
||||
1
scripts/MessageProtocol.gd.uid
Normal file
1
scripts/MessageProtocol.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dkpetm4jfpanc
|
||||
241
scripts/NetworkManager.gd
Normal file
241
scripts/NetworkManager.gd
Normal file
@@ -0,0 +1,241 @@
|
||||
extends Node
|
||||
class_name NetworkManager
|
||||
## 网络管理器
|
||||
## 负责管理客户端与服务器的 WebSocket 连接
|
||||
|
||||
# 信号定义
|
||||
signal connected_to_server()
|
||||
signal disconnected_from_server()
|
||||
signal connection_error(error: String)
|
||||
signal message_received(message: Dictionary)
|
||||
|
||||
# 安全管理器
|
||||
var security_manager: SecurityManager
|
||||
var rate_limiter: RateLimiter
|
||||
|
||||
# WebSocket 客户端
|
||||
var _client: WebSocketPeer = null
|
||||
var _server_url: String = "ws://localhost:8080"
|
||||
var _is_connected: bool = false
|
||||
|
||||
# 重连相关
|
||||
var _reconnect_attempts: int = 0
|
||||
var _max_reconnect_attempts: int = 5
|
||||
var _reconnect_delay: float = 1.0
|
||||
var _reconnect_timer: float = 0.0
|
||||
var _should_reconnect: bool = false
|
||||
|
||||
# 连接超时相关
|
||||
var _connection_timeout: float = 10.0
|
||||
var _connection_timer: float = 0.0
|
||||
var _is_connecting: bool = false
|
||||
|
||||
func _ready():
|
||||
"""初始化网络管理器"""
|
||||
# 使用默认网络设置(避免GameConfig依赖)
|
||||
_max_reconnect_attempts = 3
|
||||
_reconnect_delay = 1.0
|
||||
_connection_timeout = 10.0
|
||||
|
||||
# 初始化安全组件
|
||||
security_manager = SecurityManager.new()
|
||||
add_child(security_manager)
|
||||
|
||||
rate_limiter = RateLimiter.new()
|
||||
add_child(rate_limiter)
|
||||
|
||||
print("[NETWORK] NetworkManager initialized")
|
||||
|
||||
func connect_to_server(url: String = "") -> void:
|
||||
"""连接到服务器"""
|
||||
if not url.is_empty():
|
||||
_server_url = url
|
||||
|
||||
print("[NETWORK] Connecting to server: ", _server_url)
|
||||
|
||||
# 如果已经在连接中,先断开
|
||||
if _is_connecting:
|
||||
disconnect_from_server()
|
||||
|
||||
_client = WebSocketPeer.new()
|
||||
var err = _client.connect_to_url(_server_url)
|
||||
|
||||
if err != OK:
|
||||
var error_msg = "连接失败: " + str(err)
|
||||
print("[NETWORK] Failed to connect: ", err)
|
||||
connection_error.emit(error_msg)
|
||||
return
|
||||
|
||||
# 开始连接超时计时
|
||||
_is_connecting = true
|
||||
_connection_timer = _connection_timeout
|
||||
print("[NETWORK] Connection initiated (timeout: ", _connection_timeout, "s)")
|
||||
|
||||
func disconnect_from_server() -> void:
|
||||
"""断开服务器连接"""
|
||||
if _client:
|
||||
_client.close()
|
||||
_client = null
|
||||
_is_connected = false
|
||||
_is_connecting = false
|
||||
_connection_timer = 0.0
|
||||
print("Disconnected from server")
|
||||
disconnected_from_server.emit()
|
||||
|
||||
func send_message(message: Dictionary) -> void:
|
||||
"""发送消息到服务器(使用消息协议)"""
|
||||
if not _is_connected or not _client:
|
||||
print("Cannot send message: not connected")
|
||||
return
|
||||
|
||||
# 速率限制检查
|
||||
var client_id = "local_client" # 本地客户端标识
|
||||
if not rate_limiter.is_message_allowed(client_id):
|
||||
print("[WARNING] [NETWORK] Message blocked by rate limiter")
|
||||
return
|
||||
|
||||
# 安全验证消息格式
|
||||
if not SecurityManager.validate_message_format(message):
|
||||
print("[ERROR] [NETWORK] Invalid message format blocked: ", message.get("type", "unknown"))
|
||||
return
|
||||
|
||||
# 使用 MessageProtocol 序列化
|
||||
var json_string = MessageProtocol.serialize(message)
|
||||
_client.send_text(json_string)
|
||||
print("Sent message: ", message.get("type", "unknown"))
|
||||
|
||||
func send_typed_message(type: MessageProtocol.MessageType, data: Dictionary = {}) -> void:
|
||||
"""发送指定类型的消息"""
|
||||
var message = MessageProtocol.create_message(type, data)
|
||||
send_message(message)
|
||||
|
||||
func is_server_connected() -> bool:
|
||||
"""检查是否已连接到服务器"""
|
||||
return _is_connected
|
||||
|
||||
func _process(delta):
|
||||
"""处理网络消息和重连逻辑"""
|
||||
# 处理重连计时器
|
||||
if _should_reconnect and _reconnect_timer > 0:
|
||||
_reconnect_timer -= delta
|
||||
if _reconnect_timer <= 0:
|
||||
_attempt_reconnect()
|
||||
|
||||
# 处理连接超时
|
||||
if _is_connecting and _connection_timer > 0:
|
||||
_connection_timer -= delta
|
||||
if _connection_timer <= 0:
|
||||
_handle_connection_timeout()
|
||||
return
|
||||
|
||||
if not _client:
|
||||
return
|
||||
|
||||
_client.poll()
|
||||
var state = _client.get_ready_state()
|
||||
|
||||
# 检查连接状态
|
||||
if state == WebSocketPeer.STATE_OPEN:
|
||||
if not _is_connected:
|
||||
_is_connected = true
|
||||
_is_connecting = false # 连接成功,停止超时计时
|
||||
_connection_timer = 0.0
|
||||
_reconnect_attempts = 0 # 重置重连计数
|
||||
_should_reconnect = false
|
||||
print("Connected to server!")
|
||||
connected_to_server.emit()
|
||||
|
||||
# 接收消息
|
||||
while _client.get_available_packet_count() > 0:
|
||||
var packet = _client.get_packet()
|
||||
var json_string = packet.get_string_from_utf8()
|
||||
|
||||
# 检查消息长度(防止DoS攻击)
|
||||
if json_string.length() > 10000: # 10KB限制
|
||||
print("[ERROR] [NETWORK] Message too large, potential DoS attack. Size: ", json_string.length())
|
||||
continue
|
||||
|
||||
# 使用 MessageProtocol 反序列化
|
||||
var message = MessageProtocol.deserialize(json_string)
|
||||
|
||||
if not message.is_empty():
|
||||
# 验证消息格式(基础验证)
|
||||
if MessageProtocol.validate_message(message):
|
||||
# 安全验证消息格式(增强验证)
|
||||
if SecurityManager.validate_message_format(message):
|
||||
print("Received message: ", message.get("type", "unknown"))
|
||||
message_received.emit(message)
|
||||
else:
|
||||
print("[ERROR] [NETWORK] Security validation failed for message: ", message.get("type", "unknown"))
|
||||
else:
|
||||
print("[ERROR] [NETWORK] Invalid message format: ", json_string)
|
||||
else:
|
||||
print("[ERROR] [NETWORK] Failed to parse message: ", json_string)
|
||||
|
||||
elif state == WebSocketPeer.STATE_CLOSING:
|
||||
pass
|
||||
|
||||
elif state == WebSocketPeer.STATE_CLOSED:
|
||||
if _is_connected:
|
||||
_is_connected = false
|
||||
print("Connection closed")
|
||||
disconnected_from_server.emit()
|
||||
|
||||
# 触发重连
|
||||
_trigger_reconnect()
|
||||
elif _is_connecting:
|
||||
# 连接过程中关闭,可能是连接失败
|
||||
_handle_connection_failure()
|
||||
_client = null
|
||||
|
||||
func _trigger_reconnect() -> void:
|
||||
"""触发重连逻辑"""
|
||||
if _reconnect_attempts < _max_reconnect_attempts:
|
||||
_should_reconnect = true
|
||||
# 指数退避:1秒、2秒、4秒
|
||||
_reconnect_timer = _reconnect_delay * pow(2, _reconnect_attempts)
|
||||
print("Will attempt reconnect in ", _reconnect_timer, " seconds (attempt ", _reconnect_attempts + 1, "/", _max_reconnect_attempts, ")")
|
||||
else:
|
||||
print("Max reconnect attempts reached")
|
||||
connection_error.emit("Failed to reconnect after " + str(_max_reconnect_attempts) + " attempts")
|
||||
|
||||
func _attempt_reconnect() -> void:
|
||||
"""尝试重新连接"""
|
||||
_reconnect_attempts += 1
|
||||
print("Attempting to reconnect... (attempt ", _reconnect_attempts, "/", _max_reconnect_attempts, ")")
|
||||
connect_to_server()
|
||||
|
||||
func reset_reconnect() -> void:
|
||||
"""重置重连状态"""
|
||||
_reconnect_attempts = 0
|
||||
_should_reconnect = false
|
||||
_reconnect_timer = 0.0
|
||||
|
||||
func _handle_connection_timeout() -> void:
|
||||
"""处理连接超时"""
|
||||
print("[ERROR] [NETWORK] Connection timeout after ", _connection_timeout, " seconds")
|
||||
_is_connecting = false
|
||||
_connection_timer = 0.0
|
||||
|
||||
# 关闭WebSocketPeer连接
|
||||
if _client:
|
||||
_client.close()
|
||||
_client = null
|
||||
|
||||
connection_error.emit("连接超时:无法连接到服务器,请检查服务器是否启动")
|
||||
|
||||
func _handle_connection_failure() -> void:
|
||||
"""处理连接失败"""
|
||||
print("[ERROR] [NETWORK] Connection failed during handshake")
|
||||
_is_connecting = false
|
||||
_connection_timer = 0.0
|
||||
|
||||
connection_error.emit("连接失败:服务器拒绝连接或服务器未启动")
|
||||
|
||||
func set_connection_timeout(timeout: float) -> void:
|
||||
"""设置连接超时时间"""
|
||||
_connection_timeout = timeout
|
||||
|
||||
func get_connection_timeout() -> float:
|
||||
"""获取连接超时时间"""
|
||||
return _connection_timeout
|
||||
1
scripts/NetworkManager.gd.uid
Normal file
1
scripts/NetworkManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bff86rwwknn3a
|
||||
265
scripts/PerformanceMonitor.gd
Normal file
265
scripts/PerformanceMonitor.gd
Normal file
@@ -0,0 +1,265 @@
|
||||
extends Node
|
||||
## 性能监控类
|
||||
## 监控游戏性能指标,包括FPS、内存使用、网络延迟等
|
||||
|
||||
# 单例实例
|
||||
static var instance: PerformanceMonitor = null
|
||||
|
||||
# 性能数据
|
||||
var fps_history: Array[float] = []
|
||||
var memory_history: Array[int] = []
|
||||
var network_latency_history: Array[float] = []
|
||||
|
||||
# 监控配置
|
||||
var max_history_size: int = 300 # 保存5分钟的数据(60fps)
|
||||
var update_interval: float = 1.0 # 更新间隔(秒)
|
||||
var last_update_time: float = 0.0
|
||||
var monitoring_enabled: bool = true # 性能监控开关
|
||||
|
||||
# 性能阈值
|
||||
var fps_warning_threshold: float = 20.0
|
||||
var fps_critical_threshold: float = 5.0
|
||||
var memory_warning_threshold: int = 512 * 1024 * 1024 # 512MB
|
||||
var latency_warning_threshold: float = 200.0 # 200ms
|
||||
|
||||
# 统计数据
|
||||
var frame_count: int = 0
|
||||
var total_frame_time: float = 0.0
|
||||
|
||||
func _init():
|
||||
"""初始化性能监控器"""
|
||||
if instance == null:
|
||||
instance = self
|
||||
|
||||
func _ready():
|
||||
"""准备性能监控"""
|
||||
set_process(true)
|
||||
print("[PERF] Performance monitor initialized")
|
||||
|
||||
func _process(delta: float):
|
||||
"""每帧更新性能数据"""
|
||||
frame_count += 1
|
||||
total_frame_time += delta
|
||||
|
||||
# 延迟启动性能监控,避免初始化时的误报
|
||||
if frame_count < 120: # 前2秒不监控(60fps * 2秒)
|
||||
return
|
||||
|
||||
# 定期更新性能统计
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
if current_time - last_update_time >= update_interval:
|
||||
_update_performance_stats()
|
||||
last_update_time = current_time
|
||||
|
||||
## 获取性能监控器实例
|
||||
static func get_instance() -> PerformanceMonitor:
|
||||
"""获取性能监控器单例实例"""
|
||||
if instance == null:
|
||||
instance = PerformanceMonitor.new()
|
||||
return instance
|
||||
|
||||
## 更新性能统计
|
||||
func _update_performance_stats():
|
||||
"""更新性能统计数据"""
|
||||
if not monitoring_enabled:
|
||||
return
|
||||
|
||||
# 更新FPS
|
||||
var current_fps = Engine.get_frames_per_second()
|
||||
_add_to_history(fps_history, current_fps)
|
||||
|
||||
# 更新内存使用(使用可用的内存API)
|
||||
var total_memory = OS.get_static_memory_peak_usage()
|
||||
_add_to_history(memory_history, total_memory)
|
||||
|
||||
# 检查性能警告
|
||||
_check_performance_warnings(current_fps, total_memory)
|
||||
|
||||
## 添加数据到历史记录
|
||||
func _add_to_history(history: Array, value) -> void:
|
||||
"""
|
||||
添加数据到历史记录数组
|
||||
@param history: 历史记录数组
|
||||
@param value: 要添加的值
|
||||
"""
|
||||
history.append(value)
|
||||
if history.size() > max_history_size:
|
||||
history.pop_front()
|
||||
|
||||
## 检查性能警告
|
||||
func _check_performance_warnings(fps: float, memory: int) -> void:
|
||||
"""
|
||||
检查性能是否达到警告阈值
|
||||
@param fps: 当前FPS
|
||||
@param memory: 当前内存使用
|
||||
"""
|
||||
# 临时禁用FPS警告,因为游戏启动时可能有短暂的FPS下降
|
||||
# 只在FPS持续很低时才报告
|
||||
if fps < 1.0 and fps > 0: # 只有在FPS极低且不为0时才报告
|
||||
print("[WARNING] [SYSTEM] Severe FPS drop detected: ", fps)
|
||||
|
||||
# 内存警告
|
||||
if memory > memory_warning_threshold:
|
||||
print("[WARNING] [SYSTEM] High memory usage: ", memory / (1024.0 * 1024.0), "MB")
|
||||
|
||||
## 记录网络延迟
|
||||
static func record_network_latency(latency_ms: float) -> void:
|
||||
"""
|
||||
记录网络延迟
|
||||
@param latency_ms: 延迟时间(毫秒)
|
||||
"""
|
||||
var monitor = get_instance()
|
||||
monitor._add_to_history(monitor.network_latency_history, latency_ms)
|
||||
|
||||
# 检查延迟警告
|
||||
if latency_ms > monitor.latency_warning_threshold:
|
||||
preload("res://scripts/ErrorHandler.gd").log_network_warning(
|
||||
"High network latency: " + str(latency_ms) + "ms",
|
||||
{"latency": latency_ms, "threshold": monitor.latency_warning_threshold}
|
||||
)
|
||||
|
||||
## 获取当前FPS
|
||||
static func get_current_fps() -> float:
|
||||
"""获取当前FPS"""
|
||||
return Engine.get_frames_per_second()
|
||||
|
||||
## 获取平均FPS
|
||||
static func get_average_fps() -> float:
|
||||
"""获取平均FPS"""
|
||||
var monitor = get_instance()
|
||||
if monitor.fps_history.is_empty():
|
||||
return 0.0
|
||||
|
||||
var total = 0.0
|
||||
for fps in monitor.fps_history:
|
||||
total += fps
|
||||
|
||||
return total / monitor.fps_history.size()
|
||||
|
||||
## 获取最低FPS
|
||||
static func get_min_fps() -> float:
|
||||
"""获取最低FPS"""
|
||||
var monitor = get_instance()
|
||||
if monitor.fps_history.is_empty():
|
||||
return 0.0
|
||||
|
||||
var min_fps = monitor.fps_history[0]
|
||||
for fps in monitor.fps_history:
|
||||
if fps < min_fps:
|
||||
min_fps = fps
|
||||
|
||||
return min_fps
|
||||
|
||||
## 获取内存使用情况
|
||||
static func get_memory_usage() -> Dictionary:
|
||||
"""
|
||||
获取内存使用情况
|
||||
@return: 内存使用信息字典
|
||||
"""
|
||||
var total_memory = OS.get_static_memory_peak_usage()
|
||||
|
||||
return {
|
||||
"total_bytes": total_memory,
|
||||
"total_mb": total_memory / (1024.0 * 1024.0),
|
||||
"peak_usage": total_memory
|
||||
}
|
||||
|
||||
## 获取网络延迟统计
|
||||
static func get_network_latency_stats() -> Dictionary:
|
||||
"""
|
||||
获取网络延迟统计
|
||||
@return: 延迟统计信息
|
||||
"""
|
||||
var monitor = get_instance()
|
||||
if monitor.network_latency_history.is_empty():
|
||||
return {
|
||||
"average": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 0.0,
|
||||
"samples": 0
|
||||
}
|
||||
|
||||
var total = 0.0
|
||||
var min_latency = monitor.network_latency_history[0]
|
||||
var max_latency = monitor.network_latency_history[0]
|
||||
|
||||
for latency in monitor.network_latency_history:
|
||||
total += latency
|
||||
if latency < min_latency:
|
||||
min_latency = latency
|
||||
if latency > max_latency:
|
||||
max_latency = latency
|
||||
|
||||
return {
|
||||
"average": total / monitor.network_latency_history.size(),
|
||||
"min": min_latency,
|
||||
"max": max_latency,
|
||||
"samples": monitor.network_latency_history.size()
|
||||
}
|
||||
|
||||
## 获取性能报告
|
||||
static func get_performance_report() -> Dictionary:
|
||||
"""
|
||||
获取完整的性能报告
|
||||
@return: 性能报告字典
|
||||
"""
|
||||
return {
|
||||
"fps": {
|
||||
"current": get_current_fps(),
|
||||
"average": get_average_fps(),
|
||||
"minimum": get_min_fps()
|
||||
},
|
||||
"memory": get_memory_usage(),
|
||||
"network": get_network_latency_stats(),
|
||||
"timestamp": Time.get_unix_time_from_system()
|
||||
}
|
||||
|
||||
## 重置性能数据
|
||||
static func reset_performance_data() -> void:
|
||||
"""重置所有性能数据"""
|
||||
var monitor = get_instance()
|
||||
monitor.fps_history.clear()
|
||||
monitor.memory_history.clear()
|
||||
monitor.network_latency_history.clear()
|
||||
monitor.frame_count = 0
|
||||
monitor.total_frame_time = 0.0
|
||||
preload("res://scripts/Utils.gd").debug_print("Performance data reset", "PERF")
|
||||
|
||||
## 启用/禁用性能监控
|
||||
static func set_monitoring_enabled(enabled: bool) -> void:
|
||||
"""启用或禁用性能监控"""
|
||||
var monitor = get_instance()
|
||||
monitor.monitoring_enabled = enabled
|
||||
preload("res://scripts/Utils.gd").debug_print("Performance monitoring " + ("enabled" if enabled else "disabled"), "PERF")
|
||||
|
||||
## 检查性能监控是否启用
|
||||
static func is_monitoring_enabled() -> bool:
|
||||
"""检查性能监控是否启用"""
|
||||
return get_instance().monitoring_enabled
|
||||
|
||||
## 导出性能数据
|
||||
static func export_performance_data(file_path: String = "user://performance_log.json") -> bool:
|
||||
"""
|
||||
导出性能数据到文件
|
||||
@param file_path: 文件路径
|
||||
@return: 是否成功
|
||||
"""
|
||||
var monitor = get_instance()
|
||||
var data = {
|
||||
"fps_history": monitor.fps_history,
|
||||
"memory_history": monitor.memory_history,
|
||||
"network_latency_history": monitor.network_latency_history,
|
||||
"export_timestamp": Time.get_unix_time_from_system(),
|
||||
"performance_report": get_performance_report()
|
||||
}
|
||||
|
||||
var file = FileAccess.open(file_path, FileAccess.WRITE)
|
||||
if file:
|
||||
var json_string = JSON.stringify(data)
|
||||
file.store_string(json_string)
|
||||
file.close()
|
||||
preload("res://scripts/Utils.gd").debug_print("Performance data exported to: " + file_path, "PERF")
|
||||
return true
|
||||
else:
|
||||
preload("res://scripts/ErrorHandler.gd").log_error("Failed to export performance data to: " + file_path)
|
||||
return false
|
||||
1
scripts/PerformanceMonitor.gd.uid
Normal file
1
scripts/PerformanceMonitor.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dtw8y6rp3686j
|
||||
84
scripts/PerformanceMonitoringSystem.gd
Normal file
84
scripts/PerformanceMonitoringSystem.gd
Normal file
@@ -0,0 +1,84 @@
|
||||
extends Node
|
||||
class_name PerformanceMonitoringSystem
|
||||
## 性能监控和报警系统
|
||||
## 实时监控游戏性能并在出现问题时发出警报
|
||||
|
||||
# 性能指标类型
|
||||
enum MetricType {
|
||||
FPS, # 帧率
|
||||
MEMORY_USAGE, # 内存使用
|
||||
NETWORK_LATENCY, # 网络延迟
|
||||
LOAD_TIME, # 加载时间
|
||||
RENDER_TIME, # 渲染时间
|
||||
SCRIPT_TIME, # 脚本执行时间
|
||||
PHYSICS_TIME, # 物理计算时间
|
||||
AUDIO_LATENCY, # 音频延迟
|
||||
DISK_IO, # 磁盘IO
|
||||
CPU_USAGE # CPU使用率
|
||||
}
|
||||
|
||||
# 性能警报级别
|
||||
enum AlertLevel {
|
||||
INFO, # 信息
|
||||
WARNING, # 警告
|
||||
CRITICAL, # 严重
|
||||
EMERGENCY # 紧急
|
||||
}
|
||||
|
||||
# 性能指标数据结构
|
||||
class PerformanceMetric:
|
||||
var metric_type: MetricType
|
||||
var value: float
|
||||
var timestamp: float
|
||||
var threshold_warning: float
|
||||
var threshold_critical: float
|
||||
var unit: String
|
||||
|
||||
func _init(type: MetricType, val: float, warn_threshold: float = 0.0, crit_threshold: float = 0.0, metric_unit: String = ""):
|
||||
metric_type = type
|
||||
value = val
|
||||
timestamp = Time.get_unix_time_from_system()
|
||||
threshold_warning = warn_threshold
|
||||
threshold_critical = crit_threshold
|
||||
unit = metric_unit
|
||||
|
||||
# 性能警报数据结构
|
||||
class PerformanceAlert:
|
||||
var alert_id: String
|
||||
var metric_type: MetricType
|
||||
var level: AlertLevel
|
||||
var message: String
|
||||
var value: float
|
||||
var threshold: float
|
||||
var timestamp: float
|
||||
var resolved: bool = false
|
||||
|
||||
func _init(type: MetricType, alert_level: AlertLevel, msg: String, val: float, thresh: float):
|
||||
alert_id = _generate_alert_id()
|
||||
metric_type = type
|
||||
level = alert_level
|
||||
message = msg
|
||||
value = val
|
||||
threshold = thresh
|
||||
timestamp = Time.get_unix_time_from_system()
|
||||
|
||||
func _generate_alert_id() -> String:
|
||||
return "alert_%d_%d" % [Time.get_unix_time_from_system(), randi()]
|
||||
|
||||
# 数据存储
|
||||
var performance_history: Dictionary = {} # MetricType -> Array[PerformanceMetric]
|
||||
var active_alerts: Array[PerformanceAlert] = []
|
||||
var resolved_alerts: Array[PerformanceAlert] = []
|
||||
var monitoring_enabled: bool = true
|
||||
|
||||
# 监控配置
|
||||
var monitoring_interval: float = 1.0 # 监控间隔(秒)
|
||||
var history_retention_time: float = 3600.0 # 历史数据保留时间(1小时)
|
||||
var max_history_entries: int = 1000
|
||||
|
||||
# 性能阈值配置
|
||||
var performance_thresholds: Dictionary = {
|
||||
MetricType.FPS: {"warning": 30.0, "critical": 15.0},
|
||||
MetricType.MEMORY_USAGE: {"warning": 512.0, "critical": 1024.0}, # MB
|
||||
MetricType.NETWORK_LATENCY: {"warning": 200.0, "critical": 500.0}, # ms
|
||||
MetricType.LOAD_TIME: {"warning": 5.0, "critical": 10.0}, # seconds
|
||||
1
scripts/PerformanceMonitoringSystem.gd.uid
Normal file
1
scripts/PerformanceMonitoringSystem.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://0lcffylnv5st
|
||||
570
scripts/PrivateChatSystem.gd
Normal file
570
scripts/PrivateChatSystem.gd
Normal file
@@ -0,0 +1,570 @@
|
||||
extends Node
|
||||
class_name PrivateChatSystem
|
||||
## 私聊系统
|
||||
## 管理玩家之间的私人聊天功能
|
||||
|
||||
# 私聊会话数据结构
|
||||
class PrivateConversation:
|
||||
var conversation_id: String
|
||||
var participant_ids: Array[String] = []
|
||||
var participant_names: Dictionary = {} # id -> name
|
||||
var messages: Array[Dictionary] = []
|
||||
var created_at: float
|
||||
var last_activity: float
|
||||
var is_active: bool = true
|
||||
var unread_count: int = 0
|
||||
|
||||
func _init(conv_id: String, participants: Array[String], names: Dictionary):
|
||||
conversation_id = conv_id
|
||||
participant_ids = participants.duplicate()
|
||||
participant_names = names.duplicate()
|
||||
created_at = Time.get_unix_time_from_system()
|
||||
last_activity = created_at
|
||||
|
||||
# 私聊数据存储
|
||||
var conversations: Dictionary = {} # conversation_id -> PrivateConversation
|
||||
var user_conversations: Dictionary = {} # user_id -> Array[conversation_id]
|
||||
var active_conversation: String = ""
|
||||
var max_conversations: int = 50
|
||||
var max_messages_per_conversation: int = 500
|
||||
|
||||
# 好友系统引用
|
||||
var friend_system: FriendSystem
|
||||
|
||||
# 数据持久化
|
||||
var chat_file_path: String = "user://private_chats.json"
|
||||
|
||||
# 信号
|
||||
signal conversation_created(conversation_id: String, participants: Array[String])
|
||||
signal message_sent(conversation_id: String, sender_id: String, message: String)
|
||||
signal message_received(conversation_id: String, sender_id: String, message: String)
|
||||
signal conversation_opened(conversation_id: String)
|
||||
signal conversation_closed(conversation_id: String)
|
||||
signal unread_count_changed(conversation_id: String, count: int)
|
||||
signal typing_indicator(conversation_id: String, user_id: String, is_typing: bool)
|
||||
|
||||
func _ready():
|
||||
"""初始化私聊系统"""
|
||||
load_chat_data()
|
||||
print("PrivateChatSystem initialized")
|
||||
|
||||
## 设置好友系统引用
|
||||
func set_friend_system(fs: FriendSystem) -> void:
|
||||
"""
|
||||
设置好友系统引用
|
||||
@param fs: 好友系统实例
|
||||
"""
|
||||
friend_system = fs
|
||||
|
||||
## 开始私聊
|
||||
func start_private_chat(target_id: String, target_name: String) -> String:
|
||||
"""
|
||||
开始与指定用户的私聊
|
||||
@param target_id: 目标用户ID
|
||||
@param target_name: 目标用户名称
|
||||
@return: 会话ID,失败返回空字符串
|
||||
"""
|
||||
# 检查是否为好友(如果有好友系统)
|
||||
if friend_system and not friend_system.is_friend(target_id):
|
||||
print("Cannot start private chat: not friends with ", target_name)
|
||||
return ""
|
||||
|
||||
# 检查是否被屏蔽
|
||||
if friend_system and friend_system.is_blocked(target_id):
|
||||
print("Cannot start private chat: user is blocked")
|
||||
return ""
|
||||
|
||||
# 查找现有会话
|
||||
var existing_conversation = find_conversation_with_user(target_id)
|
||||
if not existing_conversation.is_empty():
|
||||
print("Using existing conversation with ", target_name)
|
||||
return existing_conversation
|
||||
|
||||
# 检查会话数量限制
|
||||
if conversations.size() >= max_conversations:
|
||||
print("Cannot create new conversation: limit reached")
|
||||
return ""
|
||||
|
||||
# 创建新会话
|
||||
var conversation_id = generate_conversation_id()
|
||||
var participants = ["player", target_id]
|
||||
var names = {"player": "You", target_id: target_name}
|
||||
|
||||
var conversation = PrivateConversation.new(conversation_id, participants, names)
|
||||
conversations[conversation_id] = conversation
|
||||
|
||||
# 更新用户会话索引
|
||||
_add_conversation_to_user_index("player", conversation_id)
|
||||
_add_conversation_to_user_index(target_id, conversation_id)
|
||||
|
||||
# 保存数据
|
||||
save_chat_data()
|
||||
|
||||
# 发射信号
|
||||
conversation_created.emit(conversation_id, participants)
|
||||
|
||||
print("Private chat started with ", target_name, " (", conversation_id, ")")
|
||||
return conversation_id
|
||||
|
||||
## 发送私聊消息
|
||||
func send_private_message(conversation_id: String, message: String) -> bool:
|
||||
"""
|
||||
发送私聊消息
|
||||
@param conversation_id: 会话ID
|
||||
@param message: 消息内容
|
||||
@return: 是否成功发送
|
||||
"""
|
||||
if not conversations.has(conversation_id):
|
||||
print("Conversation not found: ", conversation_id)
|
||||
return false
|
||||
|
||||
var conversation = conversations[conversation_id]
|
||||
|
||||
# 验证消息
|
||||
if message.strip_edges().is_empty():
|
||||
print("Cannot send empty message")
|
||||
return false
|
||||
|
||||
if message.length() > 1000:
|
||||
print("Message too long")
|
||||
return false
|
||||
|
||||
# 创建消息记录
|
||||
var message_record = {
|
||||
"id": generate_message_id(),
|
||||
"sender_id": "player",
|
||||
"message": message,
|
||||
"timestamp": Time.get_unix_time_from_system(),
|
||||
"read": false
|
||||
}
|
||||
|
||||
# 添加到会话历史
|
||||
conversation.messages.append(message_record)
|
||||
conversation.last_activity = message_record.timestamp
|
||||
|
||||
# 限制消息历史长度
|
||||
if conversation.messages.size() > max_messages_per_conversation:
|
||||
conversation.messages.pop_front()
|
||||
|
||||
# 记录好友互动
|
||||
if friend_system:
|
||||
for participant_id in conversation.participant_ids:
|
||||
if participant_id != "player":
|
||||
friend_system.record_interaction(participant_id, "private_chat")
|
||||
|
||||
# 保存数据
|
||||
save_chat_data()
|
||||
|
||||
# 发射信号
|
||||
message_sent.emit(conversation_id, "player", message)
|
||||
|
||||
print("Private message sent in ", conversation_id, ": ", message)
|
||||
return true
|
||||
|
||||
## 接收私聊消息
|
||||
func receive_private_message(conversation_id: String, sender_id: String, message: String) -> void:
|
||||
"""
|
||||
接收私聊消息
|
||||
@param conversation_id: 会话ID
|
||||
@param sender_id: 发送者ID
|
||||
@param message: 消息内容
|
||||
"""
|
||||
if not conversations.has(conversation_id):
|
||||
print("Conversation not found for received message: ", conversation_id)
|
||||
return
|
||||
|
||||
var conversation = conversations[conversation_id]
|
||||
|
||||
# 创建消息记录
|
||||
var message_record = {
|
||||
"id": generate_message_id(),
|
||||
"sender_id": sender_id,
|
||||
"message": message,
|
||||
"timestamp": Time.get_unix_time_from_system(),
|
||||
"read": false
|
||||
}
|
||||
|
||||
# 添加到会话历史
|
||||
conversation.messages.append(message_record)
|
||||
conversation.last_activity = message_record.timestamp
|
||||
|
||||
# 更新未读计数(如果不是当前活跃会话)
|
||||
if active_conversation != conversation_id:
|
||||
conversation.unread_count += 1
|
||||
unread_count_changed.emit(conversation_id, conversation.unread_count)
|
||||
|
||||
# 限制消息历史长度
|
||||
if conversation.messages.size() > max_messages_per_conversation:
|
||||
conversation.messages.pop_front()
|
||||
|
||||
# 保存数据
|
||||
save_chat_data()
|
||||
|
||||
# 发射信号
|
||||
message_received.emit(conversation_id, sender_id, message)
|
||||
|
||||
print("Private message received in ", conversation_id, " from ", sender_id, ": ", message)
|
||||
|
||||
## 打开会话
|
||||
func open_conversation(conversation_id: String) -> bool:
|
||||
"""
|
||||
打开指定会话
|
||||
@param conversation_id: 会话ID
|
||||
@return: 是否成功打开
|
||||
"""
|
||||
if not conversations.has(conversation_id):
|
||||
print("Conversation not found: ", conversation_id)
|
||||
return false
|
||||
|
||||
# 关闭当前活跃会话
|
||||
if not active_conversation.is_empty():
|
||||
close_conversation(active_conversation)
|
||||
|
||||
# 设置为活跃会话
|
||||
active_conversation = conversation_id
|
||||
var conversation = conversations[conversation_id]
|
||||
|
||||
# 标记所有消息为已读
|
||||
for message in conversation.messages:
|
||||
message.read = true
|
||||
|
||||
# 重置未读计数
|
||||
if conversation.unread_count > 0:
|
||||
conversation.unread_count = 0
|
||||
unread_count_changed.emit(conversation_id, 0)
|
||||
|
||||
# 保存数据
|
||||
save_chat_data()
|
||||
|
||||
# 发射信号
|
||||
conversation_opened.emit(conversation_id)
|
||||
|
||||
print("Conversation opened: ", conversation_id)
|
||||
return true
|
||||
|
||||
## 关闭会话
|
||||
func close_conversation(conversation_id: String) -> void:
|
||||
"""
|
||||
关闭指定会话
|
||||
@param conversation_id: 会话ID
|
||||
"""
|
||||
if active_conversation == conversation_id:
|
||||
active_conversation = ""
|
||||
conversation_closed.emit(conversation_id)
|
||||
print("Conversation closed: ", conversation_id)
|
||||
|
||||
## 删除会话
|
||||
func delete_conversation(conversation_id: String) -> bool:
|
||||
"""
|
||||
删除会话
|
||||
@param conversation_id: 会话ID
|
||||
@return: 是否成功删除
|
||||
"""
|
||||
if not conversations.has(conversation_id):
|
||||
print("Conversation not found: ", conversation_id)
|
||||
return false
|
||||
|
||||
var conversation = conversations[conversation_id]
|
||||
|
||||
# 从用户会话索引中移除
|
||||
for participant_id in conversation.participant_ids:
|
||||
_remove_conversation_from_user_index(participant_id, conversation_id)
|
||||
|
||||
# 如果是当前活跃会话,关闭它
|
||||
if active_conversation == conversation_id:
|
||||
close_conversation(conversation_id)
|
||||
|
||||
# 删除会话
|
||||
conversations.erase(conversation_id)
|
||||
|
||||
# 保存数据
|
||||
save_chat_data()
|
||||
|
||||
print("Conversation deleted: ", conversation_id)
|
||||
return true
|
||||
|
||||
## 获取会话列表
|
||||
func get_conversations_list() -> Array[Dictionary]:
|
||||
"""
|
||||
获取用户的会话列表
|
||||
@return: 会话信息数组
|
||||
"""
|
||||
var conversations_list = []
|
||||
|
||||
# 获取用户参与的所有会话
|
||||
var user_conv_ids = user_conversations.get("player", [])
|
||||
|
||||
for conv_id in user_conv_ids:
|
||||
if conversations.has(conv_id):
|
||||
var conversation = conversations[conv_id]
|
||||
var last_message = ""
|
||||
var last_sender = ""
|
||||
|
||||
if conversation.messages.size() > 0:
|
||||
var last_msg = conversation.messages[-1]
|
||||
last_message = last_msg.message
|
||||
last_sender = last_msg.sender_id
|
||||
|
||||
# 获取对方用户信息
|
||||
var other_participants = []
|
||||
for participant_id in conversation.participant_ids:
|
||||
if participant_id != "player":
|
||||
other_participants.append({
|
||||
"id": participant_id,
|
||||
"name": conversation.participant_names.get(participant_id, "Unknown")
|
||||
})
|
||||
|
||||
conversations_list.append({
|
||||
"id": conv_id,
|
||||
"participants": other_participants,
|
||||
"last_message": last_message,
|
||||
"last_sender": last_sender,
|
||||
"last_activity": conversation.last_activity,
|
||||
"unread_count": conversation.unread_count,
|
||||
"message_count": conversation.messages.size(),
|
||||
"is_active": conversation.is_active
|
||||
})
|
||||
|
||||
# 按最后活动时间排序(最新的在前)
|
||||
conversations_list.sort_custom(func(a, b): return a.last_activity > b.last_activity)
|
||||
|
||||
return conversations_list
|
||||
|
||||
## 获取会话消息
|
||||
func get_conversation_messages(conversation_id: String, limit: int = 0) -> Array[Dictionary]:
|
||||
"""
|
||||
获取会话消息历史
|
||||
@param conversation_id: 会话ID
|
||||
@param limit: 限制返回的消息数量(0表示返回全部)
|
||||
@return: 消息数组
|
||||
"""
|
||||
if not conversations.has(conversation_id):
|
||||
return []
|
||||
|
||||
var conversation = conversations[conversation_id]
|
||||
var messages = conversation.messages
|
||||
|
||||
if limit <= 0 or limit >= messages.size():
|
||||
return messages.duplicate()
|
||||
|
||||
# 返回最近的消息
|
||||
return messages.slice(messages.size() - limit, messages.size())
|
||||
|
||||
## 搜索会话消息
|
||||
func search_conversation_messages(conversation_id: String, query: String) -> Array[Dictionary]:
|
||||
"""
|
||||
在会话中搜索消息
|
||||
@param conversation_id: 会话ID
|
||||
@param query: 搜索关键词
|
||||
@return: 匹配的消息数组
|
||||
"""
|
||||
if not conversations.has(conversation_id):
|
||||
return []
|
||||
|
||||
var conversation = conversations[conversation_id]
|
||||
var results = []
|
||||
var search_query = query.to_lower()
|
||||
|
||||
for message in conversation.messages:
|
||||
if message.message.to_lower().contains(search_query):
|
||||
results.append(message.duplicate())
|
||||
|
||||
return results
|
||||
|
||||
## 查找与用户的会话
|
||||
func find_conversation_with_user(user_id: String) -> String:
|
||||
"""
|
||||
查找与指定用户的会话
|
||||
@param user_id: 用户ID
|
||||
@return: 会话ID,如果不存在则返回空字符串
|
||||
"""
|
||||
var user_conv_ids = user_conversations.get("player", [])
|
||||
|
||||
for conv_id in user_conv_ids:
|
||||
if conversations.has(conv_id):
|
||||
var conversation = conversations[conv_id]
|
||||
if user_id in conversation.participant_ids:
|
||||
return conv_id
|
||||
|
||||
return ""
|
||||
|
||||
## 获取未读消息总数
|
||||
func get_total_unread_count() -> int:
|
||||
"""
|
||||
获取所有会话的未读消息总数
|
||||
@return: 未读消息总数
|
||||
"""
|
||||
var total = 0
|
||||
var user_conv_ids = user_conversations.get("player", [])
|
||||
|
||||
for conv_id in user_conv_ids:
|
||||
if conversations.has(conv_id):
|
||||
total += conversations[conv_id].unread_count
|
||||
|
||||
return total
|
||||
|
||||
## 设置输入状态
|
||||
func set_typing_status(conversation_id: String, is_typing: bool) -> void:
|
||||
"""
|
||||
设置输入状态
|
||||
@param conversation_id: 会话ID
|
||||
@param is_typing: 是否正在输入
|
||||
"""
|
||||
if conversations.has(conversation_id):
|
||||
typing_indicator.emit(conversation_id, "player", is_typing)
|
||||
|
||||
## 处理输入状态通知
|
||||
func handle_typing_notification(conversation_id: String, user_id: String, is_typing: bool) -> void:
|
||||
"""
|
||||
处理其他用户的输入状态通知
|
||||
@param conversation_id: 会话ID
|
||||
@param user_id: 用户ID
|
||||
@param is_typing: 是否正在输入
|
||||
"""
|
||||
if conversations.has(conversation_id):
|
||||
typing_indicator.emit(conversation_id, user_id, is_typing)
|
||||
|
||||
## 生成会话ID
|
||||
func generate_conversation_id() -> String:
|
||||
"""生成唯一的会话ID"""
|
||||
var timestamp = Time.get_unix_time_from_system()
|
||||
var random = randi()
|
||||
return "conv_%d_%d" % [timestamp, random]
|
||||
|
||||
## 生成消息ID
|
||||
func generate_message_id() -> String:
|
||||
"""生成唯一的消息ID"""
|
||||
var timestamp = Time.get_unix_time_from_system()
|
||||
var random = randi()
|
||||
return "msg_%d_%d" % [timestamp, random]
|
||||
|
||||
## 添加会话到用户索引
|
||||
func _add_conversation_to_user_index(user_id: String, conversation_id: String) -> void:
|
||||
"""
|
||||
添加会话到用户索引
|
||||
@param user_id: 用户ID
|
||||
@param conversation_id: 会话ID
|
||||
"""
|
||||
if not user_conversations.has(user_id):
|
||||
user_conversations[user_id] = []
|
||||
|
||||
var user_convs = user_conversations[user_id]
|
||||
if not conversation_id in user_convs:
|
||||
user_convs.append(conversation_id)
|
||||
|
||||
## 从用户索引移除会话
|
||||
func _remove_conversation_from_user_index(user_id: String, conversation_id: String) -> void:
|
||||
"""
|
||||
从用户索引移除会话
|
||||
@param user_id: 用户ID
|
||||
@param conversation_id: 会话ID
|
||||
"""
|
||||
if user_conversations.has(user_id):
|
||||
var user_convs = user_conversations[user_id]
|
||||
user_convs.erase(conversation_id)
|
||||
|
||||
## 保存聊天数据
|
||||
func save_chat_data() -> void:
|
||||
"""保存聊天数据到本地文件"""
|
||||
var data = {
|
||||
"conversations": {},
|
||||
"user_conversations": user_conversations,
|
||||
"active_conversation": active_conversation
|
||||
}
|
||||
|
||||
# 序列化会话数据
|
||||
for conv_id in conversations:
|
||||
var conversation = conversations[conv_id]
|
||||
data.conversations[conv_id] = {
|
||||
"participant_ids": conversation.participant_ids,
|
||||
"participant_names": conversation.participant_names,
|
||||
"messages": conversation.messages,
|
||||
"created_at": conversation.created_at,
|
||||
"last_activity": conversation.last_activity,
|
||||
"is_active": conversation.is_active,
|
||||
"unread_count": conversation.unread_count
|
||||
}
|
||||
|
||||
var file = FileAccess.open(chat_file_path, FileAccess.WRITE)
|
||||
if file:
|
||||
var json_string = JSON.stringify(data)
|
||||
file.store_string(json_string)
|
||||
file.close()
|
||||
print("Private chat data saved")
|
||||
else:
|
||||
print("Failed to save private chat data")
|
||||
|
||||
## 加载聊天数据
|
||||
func load_chat_data() -> void:
|
||||
"""从本地文件加载聊天数据"""
|
||||
if not FileAccess.file_exists(chat_file_path):
|
||||
print("No private chat data file found, starting fresh")
|
||||
return
|
||||
|
||||
var file = FileAccess.open(chat_file_path, FileAccess.READ)
|
||||
if file:
|
||||
var json_string = file.get_as_text()
|
||||
file.close()
|
||||
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(json_string)
|
||||
|
||||
if parse_result == OK:
|
||||
var data = json.data
|
||||
|
||||
# 加载会话数据
|
||||
if data.has("conversations"):
|
||||
for conv_id in data.conversations:
|
||||
var conv_data = data.conversations[conv_id]
|
||||
var conversation = PrivateConversation.new(
|
||||
conv_id,
|
||||
conv_data.get("participant_ids", []),
|
||||
conv_data.get("participant_names", {})
|
||||
)
|
||||
conversation.messages = conv_data.get("messages", [])
|
||||
conversation.created_at = conv_data.get("created_at", Time.get_unix_time_from_system())
|
||||
conversation.last_activity = conv_data.get("last_activity", conversation.created_at)
|
||||
conversation.is_active = conv_data.get("is_active", true)
|
||||
conversation.unread_count = conv_data.get("unread_count", 0)
|
||||
conversations[conv_id] = conversation
|
||||
|
||||
# 加载用户会话索引
|
||||
if data.has("user_conversations"):
|
||||
user_conversations = data.user_conversations
|
||||
|
||||
# 加载活跃会话
|
||||
if data.has("active_conversation"):
|
||||
active_conversation = data.active_conversation
|
||||
|
||||
print("Private chat data loaded: ", conversations.size(), " conversations")
|
||||
else:
|
||||
print("Failed to parse private chat data JSON")
|
||||
else:
|
||||
print("Failed to open private chat data file")
|
||||
|
||||
## 获取统计信息
|
||||
func get_statistics() -> Dictionary:
|
||||
"""
|
||||
获取私聊系统统计信息
|
||||
@return: 统计信息字典
|
||||
"""
|
||||
var total_messages = 0
|
||||
var total_unread = 0
|
||||
var active_conversations = 0
|
||||
|
||||
for conv_id in conversations:
|
||||
var conversation = conversations[conv_id]
|
||||
total_messages += conversation.messages.size()
|
||||
total_unread += conversation.unread_count
|
||||
if conversation.is_active:
|
||||
active_conversations += 1
|
||||
|
||||
return {
|
||||
"total_conversations": conversations.size(),
|
||||
"active_conversations": active_conversations,
|
||||
"total_messages": total_messages,
|
||||
"total_unread": total_unread,
|
||||
"current_active": active_conversation,
|
||||
"max_conversations": max_conversations,
|
||||
"max_messages_per_conversation": max_messages_per_conversation
|
||||
}
|
||||
1
scripts/PrivateChatSystem.gd.uid
Normal file
1
scripts/PrivateChatSystem.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://tq01oyfan4j2
|
||||
33
scripts/QuickPerformanceFix.gd
Normal file
33
scripts/QuickPerformanceFix.gd
Normal file
@@ -0,0 +1,33 @@
|
||||
extends Node
|
||||
## 快速性能修复脚本
|
||||
## 临时禁用性能监控以避免启动时的误报
|
||||
|
||||
func _ready():
|
||||
"""启动时禁用性能监控"""
|
||||
print("QuickPerformanceFix: Disabling performance monitoring for 5 seconds...")
|
||||
|
||||
# 禁用性能监控
|
||||
PerformanceMonitor.set_monitoring_enabled(false)
|
||||
|
||||
# 5秒后重新启用
|
||||
await get_tree().create_timer(5.0).timeout
|
||||
|
||||
print("QuickPerformanceFix: Re-enabling performance monitoring...")
|
||||
PerformanceMonitor.set_monitoring_enabled(true)
|
||||
|
||||
func _input(event):
|
||||
"""处理输入事件"""
|
||||
if event is InputEventKey and event.pressed:
|
||||
match event.keycode:
|
||||
KEY_F1:
|
||||
# F1键切换性能监控
|
||||
var enabled = PerformanceMonitor.is_monitoring_enabled()
|
||||
PerformanceMonitor.set_monitoring_enabled(not enabled)
|
||||
print("Performance monitoring: ", "ON" if not enabled else "OFF")
|
||||
KEY_F2:
|
||||
# F2键显示性能报告
|
||||
var report = PerformanceMonitor.get_performance_report()
|
||||
print("Performance Report:")
|
||||
print(" Current FPS: ", report.fps.current)
|
||||
print(" Average FPS: ", report.fps.average)
|
||||
print(" Memory: ", report.memory.total_mb, "MB")
|
||||
1
scripts/QuickPerformanceFix.gd.uid
Normal file
1
scripts/QuickPerformanceFix.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bo0m373k8hi1
|
||||
230
scripts/RateLimiter.gd
Normal file
230
scripts/RateLimiter.gd
Normal file
@@ -0,0 +1,230 @@
|
||||
extends Node
|
||||
class_name RateLimiter
|
||||
## 速率限制器
|
||||
## 防止消息洪水攻击和垃圾消息
|
||||
|
||||
# 速率限制配置
|
||||
var max_messages_per_window: int = 10
|
||||
var time_window: float = 1.0 # 1秒窗口
|
||||
var cleanup_interval: float = 60.0 # 1分钟清理间隔
|
||||
|
||||
# 客户端消息记录
|
||||
var client_message_history: Dictionary = {}
|
||||
|
||||
# 清理定时器
|
||||
var cleanup_timer: Timer
|
||||
|
||||
## 初始化速率限制器
|
||||
func _ready():
|
||||
"""初始化速率限制器"""
|
||||
# 从配置获取参数
|
||||
max_messages_per_window = SecurityConfig.get_config("network_security", "max_message_rate", 10)
|
||||
time_window = SecurityConfig.get_config("network_security", "rate_limit_window", 1.0)
|
||||
|
||||
# 设置清理定时器
|
||||
cleanup_timer = Timer.new()
|
||||
cleanup_timer.wait_time = cleanup_interval
|
||||
cleanup_timer.timeout.connect(_cleanup_old_records)
|
||||
cleanup_timer.autostart = true
|
||||
add_child(cleanup_timer)
|
||||
|
||||
print("RateLimiter initialized - Max: %d msgs/%s sec" % [max_messages_per_window, time_window])
|
||||
|
||||
## 检查是否允许消息
|
||||
func is_message_allowed(client_id: String) -> bool:
|
||||
"""
|
||||
检查客户端是否允许发送消息
|
||||
@param client_id: 客户端ID
|
||||
@return: 是否允许
|
||||
"""
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
|
||||
# 获取或创建客户端记录
|
||||
if not client_message_history.has(client_id):
|
||||
client_message_history[client_id] = {
|
||||
"messages": [],
|
||||
"last_cleanup": current_time
|
||||
}
|
||||
|
||||
var client_record = client_message_history[client_id]
|
||||
var messages = client_record.messages
|
||||
|
||||
# 清理过期消息
|
||||
_cleanup_client_messages(client_id, current_time)
|
||||
|
||||
# 检查是否超过限制
|
||||
if messages.size() >= max_messages_per_window:
|
||||
print("WARNING: Rate limit exceeded for client: " + client_id + " (" + str(messages.size()) + " messages)")
|
||||
return false
|
||||
|
||||
# 记录新消息
|
||||
messages.append(current_time)
|
||||
return true
|
||||
|
||||
## 清理客户端的过期消息记录
|
||||
func _cleanup_client_messages(client_id: String, current_time: float):
|
||||
"""
|
||||
清理客户端的过期消息记录
|
||||
@param client_id: 客户端ID
|
||||
@param current_time: 当前时间
|
||||
"""
|
||||
if not client_message_history.has(client_id):
|
||||
return
|
||||
|
||||
var client_record = client_message_history[client_id]
|
||||
var messages = client_record.messages
|
||||
var cutoff_time = current_time - time_window
|
||||
|
||||
# 移除过期消息
|
||||
var valid_messages = []
|
||||
for timestamp in messages:
|
||||
if timestamp > cutoff_time:
|
||||
valid_messages.append(timestamp)
|
||||
|
||||
client_record.messages = valid_messages
|
||||
client_record.last_cleanup = current_time
|
||||
|
||||
## 清理所有过期记录
|
||||
func _cleanup_old_records():
|
||||
"""清理所有客户端的过期记录"""
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
var cleaned_clients = 0
|
||||
var removed_clients = []
|
||||
|
||||
for client_id in client_message_history:
|
||||
var client_record = client_message_history[client_id]
|
||||
|
||||
# 如果客户端超过5分钟没有活动,移除记录
|
||||
if current_time - client_record.last_cleanup > 300.0:
|
||||
removed_clients.append(client_id)
|
||||
else:
|
||||
# 清理过期消息
|
||||
_cleanup_client_messages(client_id, current_time)
|
||||
cleaned_clients += 1
|
||||
|
||||
# 移除不活跃的客户端记录
|
||||
for client_id in removed_clients:
|
||||
client_message_history.erase(client_id)
|
||||
|
||||
if removed_clients.size() > 0:
|
||||
print("Rate limiter cleaned up %d inactive clients, %d active clients remain" % [removed_clients.size(), cleaned_clients])
|
||||
|
||||
## 重置客户端限制
|
||||
func reset_client_limit(client_id: String):
|
||||
"""
|
||||
重置客户端的速率限制(用于特殊情况)
|
||||
@param client_id: 客户端ID
|
||||
"""
|
||||
if client_message_history.has(client_id):
|
||||
client_message_history[client_id].messages.clear()
|
||||
print("Rate limit reset for client: " + client_id)
|
||||
|
||||
## 获取客户端消息统计
|
||||
func get_client_stats(client_id: String) -> Dictionary:
|
||||
"""
|
||||
获取客户端的消息统计
|
||||
@param client_id: 客户端ID
|
||||
@return: 统计信息
|
||||
"""
|
||||
if not client_message_history.has(client_id):
|
||||
return {
|
||||
"message_count": 0,
|
||||
"remaining_quota": max_messages_per_window,
|
||||
"window_reset_time": 0
|
||||
}
|
||||
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
_cleanup_client_messages(client_id, current_time)
|
||||
|
||||
var client_record = client_message_history[client_id]
|
||||
var message_count = client_record.messages.size()
|
||||
var remaining_quota = max(0, max_messages_per_window - message_count)
|
||||
|
||||
# 计算窗口重置时间
|
||||
var oldest_message_time = 0.0
|
||||
if client_record.messages.size() > 0:
|
||||
oldest_message_time = client_record.messages[0]
|
||||
|
||||
var window_reset_time = oldest_message_time + time_window
|
||||
|
||||
return {
|
||||
"message_count": message_count,
|
||||
"remaining_quota": remaining_quota,
|
||||
"window_reset_time": window_reset_time,
|
||||
"current_time": current_time
|
||||
}
|
||||
|
||||
## 获取全局统计
|
||||
func get_global_stats() -> Dictionary:
|
||||
"""
|
||||
获取全局速率限制统计
|
||||
@return: 全局统计信息
|
||||
"""
|
||||
var total_clients = client_message_history.size()
|
||||
var active_clients = 0
|
||||
var total_messages = 0
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
|
||||
for client_id in client_message_history:
|
||||
var client_record = client_message_history[client_id]
|
||||
if current_time - client_record.last_cleanup < 60.0: # 1分钟内活跃
|
||||
active_clients += 1
|
||||
total_messages += client_record.messages.size()
|
||||
|
||||
return {
|
||||
"total_clients": total_clients,
|
||||
"active_clients": active_clients,
|
||||
"total_messages_in_window": total_messages,
|
||||
"max_rate": max_messages_per_window,
|
||||
"time_window": time_window
|
||||
}
|
||||
|
||||
## 设置速率限制参数
|
||||
func set_rate_limit(max_messages: int, window_seconds: float):
|
||||
"""
|
||||
动态设置速率限制参数
|
||||
@param max_messages: 最大消息数
|
||||
@param window_seconds: 时间窗口(秒)
|
||||
"""
|
||||
max_messages_per_window = max_messages
|
||||
time_window = window_seconds
|
||||
|
||||
print("Rate limit updated - Max: %d msgs/%s sec" % [max_messages, window_seconds])
|
||||
|
||||
## 检查是否为可疑活动
|
||||
func is_suspicious_activity(client_id: String) -> bool:
|
||||
"""
|
||||
检查客户端是否有可疑活动
|
||||
@param client_id: 客户端ID
|
||||
@return: 是否可疑
|
||||
"""
|
||||
var stats = get_client_stats(client_id)
|
||||
|
||||
# 如果消息数量接近限制,认为可疑
|
||||
if stats.message_count >= max_messages_per_window * 0.8:
|
||||
return true
|
||||
|
||||
# 检查消息发送模式是否异常(过于规律可能是机器人)
|
||||
if client_message_history.has(client_id):
|
||||
var messages = client_message_history[client_id].messages
|
||||
if messages.size() >= 3:
|
||||
var intervals = []
|
||||
for i in range(1, messages.size()):
|
||||
intervals.append(messages[i] - messages[i-1])
|
||||
|
||||
# 如果所有间隔都非常相似(差异小于0.1秒),可能是机器人
|
||||
if intervals.size() >= 2:
|
||||
var avg_interval = 0.0
|
||||
for interval in intervals:
|
||||
avg_interval += interval
|
||||
avg_interval /= intervals.size()
|
||||
|
||||
var variance = 0.0
|
||||
for interval in intervals:
|
||||
variance += (interval - avg_interval) * (interval - avg_interval)
|
||||
variance /= intervals.size()
|
||||
|
||||
if variance < 0.01: # 方差很小,模式过于规律
|
||||
return true
|
||||
|
||||
return false
|
||||
1
scripts/RateLimiter.gd.uid
Normal file
1
scripts/RateLimiter.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://duywoukj58tpx
|
||||
661
scripts/RelationshipNetwork.gd
Normal file
661
scripts/RelationshipNetwork.gd
Normal file
@@ -0,0 +1,661 @@
|
||||
extends Node
|
||||
class_name RelationshipNetwork
|
||||
## 角色关系网络
|
||||
## 管理角色之间的复杂关系和社交网络
|
||||
|
||||
# 关系类型枚举
|
||||
enum RelationshipType {
|
||||
FRIEND, # 好友
|
||||
CLOSE_FRIEND, # 密友
|
||||
ACQUAINTANCE, # 熟人
|
||||
COLLEAGUE, # 同事
|
||||
MENTOR, # 导师
|
||||
STUDENT, # 学生
|
||||
RIVAL, # 竞争对手
|
||||
NEUTRAL, # 中性
|
||||
DISLIKE # 不喜欢
|
||||
}
|
||||
|
||||
# 关系数据结构
|
||||
class Relationship:
|
||||
var from_character: String
|
||||
var to_character: String
|
||||
var relationship_type: RelationshipType
|
||||
var strength: float = 0.0 # 关系强度 -100 到 100
|
||||
var trust_level: float = 0.0 # 信任度 0 到 100
|
||||
var interaction_history: Array[Dictionary] = []
|
||||
var shared_experiences: Array[String] = []
|
||||
var created_at: float
|
||||
var last_updated: float
|
||||
var tags: Array[String] = [] # 关系标签
|
||||
|
||||
func _init(from_char: String, to_char: String, rel_type: RelationshipType = RelationshipType.NEUTRAL):
|
||||
from_character = from_char
|
||||
to_character = to_char
|
||||
relationship_type = rel_type
|
||||
created_at = Time.get_unix_time_from_system()
|
||||
last_updated = created_at
|
||||
strength = _get_default_strength(rel_type)
|
||||
trust_level = _get_default_trust(rel_type)
|
||||
|
||||
func _get_default_strength(rel_type: RelationshipType) -> float:
|
||||
match rel_type:
|
||||
RelationshipType.FRIEND: return 30.0
|
||||
RelationshipType.CLOSE_FRIEND: return 60.0
|
||||
RelationshipType.ACQUAINTANCE: return 10.0
|
||||
RelationshipType.COLLEAGUE: return 20.0
|
||||
RelationshipType.MENTOR: return 40.0
|
||||
RelationshipType.STUDENT: return 25.0
|
||||
RelationshipType.RIVAL: return -20.0
|
||||
RelationshipType.DISLIKE: return -40.0
|
||||
_: return 0.0
|
||||
|
||||
func _get_default_trust(rel_type: RelationshipType) -> float:
|
||||
match rel_type:
|
||||
RelationshipType.FRIEND: return 50.0
|
||||
RelationshipType.CLOSE_FRIEND: return 80.0
|
||||
RelationshipType.ACQUAINTANCE: return 20.0
|
||||
RelationshipType.COLLEAGUE: return 40.0
|
||||
RelationshipType.MENTOR: return 70.0
|
||||
RelationshipType.STUDENT: return 30.0
|
||||
RelationshipType.RIVAL: return 10.0
|
||||
RelationshipType.DISLIKE: return 5.0
|
||||
_: return 25.0
|
||||
|
||||
# 网络数据存储
|
||||
var relationships: Dictionary = {} # "from_id:to_id" -> Relationship
|
||||
var character_connections: Dictionary = {} # character_id -> Array[character_id]
|
||||
var relationship_groups: Dictionary = {} # group_name -> Array[character_id]
|
||||
var influence_scores: Dictionary = {} # character_id -> float
|
||||
|
||||
# 数据持久化
|
||||
var network_file_path: String = "user://relationship_network.json"
|
||||
|
||||
# 信号
|
||||
signal relationship_created(from_character: String, to_character: String, relationship_type: RelationshipType)
|
||||
signal relationship_updated(from_character: String, to_character: String, old_strength: float, new_strength: float)
|
||||
signal relationship_type_changed(from_character: String, to_character: String, old_type: RelationshipType, new_type: RelationshipType)
|
||||
signal influence_score_changed(character_id: String, old_score: float, new_score: float)
|
||||
signal group_formed(group_name: String, members: Array[String])
|
||||
signal group_disbanded(group_name: String)
|
||||
|
||||
func _ready():
|
||||
"""初始化关系网络"""
|
||||
load_network_data()
|
||||
print("RelationshipNetwork initialized")
|
||||
|
||||
## 创建或更新关系
|
||||
func create_relationship(from_character: String, to_character: String, rel_type: RelationshipType = RelationshipType.NEUTRAL) -> bool:
|
||||
"""
|
||||
创建或更新角色之间的关系
|
||||
@param from_character: 源角色ID
|
||||
@param to_character: 目标角色ID
|
||||
@param rel_type: 关系类型
|
||||
@return: 是否成功创建/更新
|
||||
"""
|
||||
if from_character == to_character:
|
||||
print("Cannot create relationship with self")
|
||||
return false
|
||||
|
||||
var relationship_key = _get_relationship_key(from_character, to_character)
|
||||
var existing_relationship = relationships.get(relationship_key)
|
||||
|
||||
if existing_relationship:
|
||||
# 更新现有关系
|
||||
var old_type = existing_relationship.relationship_type
|
||||
existing_relationship.relationship_type = rel_type
|
||||
existing_relationship.strength = existing_relationship._get_default_strength(rel_type)
|
||||
existing_relationship.trust_level = existing_relationship._get_default_trust(rel_type)
|
||||
existing_relationship.last_updated = Time.get_unix_time_from_system()
|
||||
|
||||
relationship_type_changed.emit(from_character, to_character, old_type, rel_type)
|
||||
print("Relationship updated: ", from_character, " -> ", to_character, " (", RelationshipType.keys()[rel_type], ")")
|
||||
else:
|
||||
# 创建新关系
|
||||
var relationship = Relationship.new(from_character, to_character, rel_type)
|
||||
relationships[relationship_key] = relationship
|
||||
|
||||
# 更新连接索引
|
||||
_add_connection(from_character, to_character)
|
||||
|
||||
relationship_created.emit(from_character, to_character, rel_type)
|
||||
print("Relationship created: ", from_character, " -> ", to_character, " (", RelationshipType.keys()[rel_type], ")")
|
||||
|
||||
# 重新计算影响力分数
|
||||
_recalculate_influence_scores()
|
||||
|
||||
# 保存数据
|
||||
save_network_data()
|
||||
|
||||
return true
|
||||
|
||||
## 记录互动
|
||||
func record_interaction(from_character: String, to_character: String, interaction_type: String, data: Dictionary = {}) -> void:
|
||||
"""
|
||||
记录角色之间的互动
|
||||
@param from_character: 源角色ID
|
||||
@param to_character: 目标角色ID
|
||||
@param interaction_type: 互动类型
|
||||
@param data: 互动数据
|
||||
"""
|
||||
var relationship_key = _get_relationship_key(from_character, to_character)
|
||||
var relationship = relationships.get(relationship_key)
|
||||
|
||||
if not relationship:
|
||||
# 如果关系不存在,创建一个中性关系
|
||||
create_relationship(from_character, to_character, RelationshipType.NEUTRAL)
|
||||
relationship = relationships[relationship_key]
|
||||
|
||||
# 记录互动
|
||||
var interaction_record = {
|
||||
"type": interaction_type,
|
||||
"timestamp": Time.get_unix_time_from_system(),
|
||||
"data": data
|
||||
}
|
||||
|
||||
relationship.interaction_history.append(interaction_record)
|
||||
relationship.last_updated = interaction_record.timestamp
|
||||
|
||||
# 限制历史记录长度
|
||||
if relationship.interaction_history.size() > 100:
|
||||
relationship.interaction_history.pop_front()
|
||||
|
||||
# 根据互动类型调整关系强度
|
||||
_adjust_relationship_strength(relationship, interaction_type, data)
|
||||
|
||||
# 保存数据
|
||||
save_network_data()
|
||||
|
||||
## 调整关系强度
|
||||
func _adjust_relationship_strength(relationship: Relationship, interaction_type: String, _data: Dictionary) -> void:
|
||||
"""
|
||||
根据互动类型调整关系强度
|
||||
@param relationship: 关系对象
|
||||
@param interaction_type: 互动类型
|
||||
@param _data: 互动数据 (暂未使用)
|
||||
"""
|
||||
var old_strength = relationship.strength
|
||||
var strength_change = 0.0
|
||||
var trust_change = 0.0
|
||||
|
||||
match interaction_type:
|
||||
"chat":
|
||||
strength_change = 1.0
|
||||
trust_change = 0.5
|
||||
"private_chat":
|
||||
strength_change = 2.0
|
||||
trust_change = 1.0
|
||||
"group_activity":
|
||||
strength_change = 1.5
|
||||
trust_change = 0.8
|
||||
"help_given":
|
||||
strength_change = 5.0
|
||||
trust_change = 3.0
|
||||
"help_received":
|
||||
strength_change = 3.0
|
||||
trust_change = 2.0
|
||||
"conflict":
|
||||
strength_change = -3.0
|
||||
trust_change = -2.0
|
||||
"collaboration":
|
||||
strength_change = 4.0
|
||||
trust_change = 2.5
|
||||
"gift_given":
|
||||
strength_change = 3.0
|
||||
trust_change = 1.5
|
||||
"shared_achievement":
|
||||
strength_change = 6.0
|
||||
trust_change = 3.0
|
||||
_:
|
||||
strength_change = 0.5
|
||||
|
||||
# 应用变化
|
||||
relationship.strength = clamp(relationship.strength + strength_change, -100.0, 100.0)
|
||||
relationship.trust_level = clamp(relationship.trust_level + trust_change, 0.0, 100.0)
|
||||
|
||||
# 检查关系类型是否需要更新
|
||||
_check_relationship_type_change(relationship)
|
||||
|
||||
# 发射信号
|
||||
if abs(relationship.strength - old_strength) > 0.1:
|
||||
relationship_updated.emit(relationship.from_character, relationship.to_character, old_strength, relationship.strength)
|
||||
|
||||
## 检查关系类型变化
|
||||
func _check_relationship_type_change(relationship: Relationship) -> void:
|
||||
"""
|
||||
根据关系强度检查是否需要改变关系类型
|
||||
@param relationship: 关系对象
|
||||
"""
|
||||
var old_type = relationship.relationship_type
|
||||
var new_type = old_type
|
||||
|
||||
# 根据强度和信任度确定新的关系类型
|
||||
if relationship.strength >= 70 and relationship.trust_level >= 70:
|
||||
new_type = RelationshipType.CLOSE_FRIEND
|
||||
elif relationship.strength >= 40 and relationship.trust_level >= 50:
|
||||
new_type = RelationshipType.FRIEND
|
||||
elif relationship.strength >= 15:
|
||||
new_type = RelationshipType.ACQUAINTANCE
|
||||
elif relationship.strength <= -30:
|
||||
new_type = RelationshipType.DISLIKE
|
||||
elif relationship.strength <= -10:
|
||||
new_type = RelationshipType.RIVAL
|
||||
else:
|
||||
new_type = RelationshipType.NEUTRAL
|
||||
|
||||
# 如果类型发生变化,更新并发射信号
|
||||
if new_type != old_type:
|
||||
relationship.relationship_type = new_type
|
||||
relationship_type_changed.emit(relationship.from_character, relationship.to_character, old_type, new_type)
|
||||
print("Relationship type changed: ", relationship.from_character, " -> ", relationship.to_character,
|
||||
" (", RelationshipType.keys()[old_type], " -> ", RelationshipType.keys()[new_type], ")")
|
||||
|
||||
## 获取关系信息
|
||||
func get_relationship(from_character: String, to_character: String) -> Dictionary:
|
||||
"""
|
||||
获取两个角色之间的关系信息
|
||||
@param from_character: 源角色ID
|
||||
@param to_character: 目标角色ID
|
||||
@return: 关系信息字典
|
||||
"""
|
||||
var relationship_key = _get_relationship_key(from_character, to_character)
|
||||
var relationship = relationships.get(relationship_key)
|
||||
|
||||
if not relationship:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"from_character": relationship.from_character,
|
||||
"to_character": relationship.to_character,
|
||||
"type": relationship.relationship_type,
|
||||
"type_name": RelationshipType.keys()[relationship.relationship_type],
|
||||
"strength": relationship.strength,
|
||||
"trust_level": relationship.trust_level,
|
||||
"interaction_count": relationship.interaction_history.size(),
|
||||
"shared_experiences": relationship.shared_experiences.duplicate(),
|
||||
"created_at": relationship.created_at,
|
||||
"last_updated": relationship.last_updated,
|
||||
"tags": relationship.tags.duplicate()
|
||||
}
|
||||
|
||||
## 获取角色的所有关系
|
||||
func get_character_relationships(character_id: String) -> Array[Dictionary]:
|
||||
"""
|
||||
获取指定角色的所有关系
|
||||
@param character_id: 角色ID
|
||||
@return: 关系信息数组
|
||||
"""
|
||||
var character_relationships = []
|
||||
|
||||
for relationship_key in relationships:
|
||||
var relationship = relationships[relationship_key]
|
||||
if relationship.from_character == character_id:
|
||||
character_relationships.append(get_relationship(relationship.from_character, relationship.to_character))
|
||||
|
||||
# 按关系强度排序
|
||||
character_relationships.sort_custom(func(a, b): return a.strength > b.strength)
|
||||
|
||||
return character_relationships
|
||||
|
||||
## 获取最强关系
|
||||
func get_strongest_relationships(character_id: String, limit: int = 5) -> Array[Dictionary]:
|
||||
"""
|
||||
获取指定角色的最强关系
|
||||
@param character_id: 角色ID
|
||||
@param limit: 限制返回数量
|
||||
@return: 关系信息数组
|
||||
"""
|
||||
var all_relationships = get_character_relationships(character_id)
|
||||
|
||||
if limit <= 0 or limit >= all_relationships.size():
|
||||
return all_relationships
|
||||
|
||||
return all_relationships.slice(0, limit)
|
||||
|
||||
## 查找共同好友
|
||||
func find_mutual_connections(character1: String, character2: String) -> Array[String]:
|
||||
"""
|
||||
查找两个角色的共同好友
|
||||
@param character1: 角色1 ID
|
||||
@param character2: 角色2 ID
|
||||
@return: 共同好友ID数组
|
||||
"""
|
||||
var connections1 = character_connections.get(character1, [])
|
||||
var connections2 = character_connections.get(character2, [])
|
||||
var mutual = []
|
||||
|
||||
for connection in connections1:
|
||||
if connection in connections2:
|
||||
mutual.append(connection)
|
||||
|
||||
return mutual
|
||||
|
||||
## 计算关系路径
|
||||
func find_relationship_path(from_character: String, to_character: String, max_depth: int = 3) -> Array[String]:
|
||||
"""
|
||||
查找两个角色之间的关系路径
|
||||
@param from_character: 源角色ID
|
||||
@param to_character: 目标角色ID
|
||||
@param max_depth: 最大搜索深度
|
||||
@return: 关系路径(角色ID数组)
|
||||
"""
|
||||
if from_character == to_character:
|
||||
return [from_character]
|
||||
|
||||
# 使用广度优先搜索
|
||||
var queue = [[from_character]]
|
||||
var visited = {from_character: true}
|
||||
|
||||
while queue.size() > 0:
|
||||
var path = queue.pop_front()
|
||||
var current = path[-1]
|
||||
|
||||
if path.size() > max_depth:
|
||||
continue
|
||||
|
||||
var connections = character_connections.get(current, [])
|
||||
for connection in connections:
|
||||
if connection == to_character:
|
||||
path.append(connection)
|
||||
return path
|
||||
|
||||
if not visited.has(connection):
|
||||
visited[connection] = true
|
||||
var new_path = path.duplicate()
|
||||
new_path.append(connection)
|
||||
queue.append(new_path)
|
||||
|
||||
return [] # 没有找到路径
|
||||
|
||||
## 创建关系群组
|
||||
func create_relationship_group(group_name: String, members: Array[String]) -> bool:
|
||||
"""
|
||||
创建关系群组
|
||||
@param group_name: 群组名称
|
||||
@param members: 成员ID数组
|
||||
@return: 是否成功创建
|
||||
"""
|
||||
if relationship_groups.has(group_name):
|
||||
print("Group already exists: ", group_name)
|
||||
return false
|
||||
|
||||
if members.size() < 2:
|
||||
print("Group must have at least 2 members")
|
||||
return false
|
||||
|
||||
relationship_groups[group_name] = members.duplicate()
|
||||
|
||||
# 为群组成员之间创建或加强关系
|
||||
for i in range(members.size()):
|
||||
for j in range(i + 1, members.size()):
|
||||
var member1 = members[i]
|
||||
var member2 = members[j]
|
||||
|
||||
# 记录群组活动互动
|
||||
record_interaction(member1, member2, "group_activity", {"group": group_name})
|
||||
record_interaction(member2, member1, "group_activity", {"group": group_name})
|
||||
|
||||
# 保存数据
|
||||
save_network_data()
|
||||
|
||||
# 发射信号
|
||||
group_formed.emit(group_name, members)
|
||||
|
||||
print("Relationship group created: ", group_name, " with ", members.size(), " members")
|
||||
return true
|
||||
|
||||
## 解散关系群组
|
||||
func disband_relationship_group(group_name: String) -> bool:
|
||||
"""
|
||||
解散关系群组
|
||||
@param group_name: 群组名称
|
||||
@return: 是否成功解散
|
||||
"""
|
||||
if not relationship_groups.has(group_name):
|
||||
print("Group not found: ", group_name)
|
||||
return false
|
||||
|
||||
relationship_groups.erase(group_name)
|
||||
|
||||
# 保存数据
|
||||
save_network_data()
|
||||
|
||||
# 发射信号
|
||||
group_disbanded.emit(group_name)
|
||||
|
||||
print("Relationship group disbanded: ", group_name)
|
||||
return true
|
||||
|
||||
## 计算影响力分数
|
||||
func _recalculate_influence_scores() -> void:
|
||||
"""重新计算所有角色的影响力分数"""
|
||||
var new_scores = {}
|
||||
|
||||
# 为每个角色计算影响力分数
|
||||
for character_id in character_connections:
|
||||
var score = _calculate_influence_score(character_id)
|
||||
var old_score = influence_scores.get(character_id, 0.0)
|
||||
|
||||
new_scores[character_id] = score
|
||||
|
||||
if abs(score - old_score) > 0.1:
|
||||
influence_score_changed.emit(character_id, old_score, score)
|
||||
|
||||
influence_scores = new_scores
|
||||
|
||||
## 计算单个角色的影响力分数
|
||||
func _calculate_influence_score(character_id: String) -> float:
|
||||
"""
|
||||
计算角色的影响力分数
|
||||
@param character_id: 角色ID
|
||||
@return: 影响力分数
|
||||
"""
|
||||
var score = 0.0
|
||||
var connections = character_connections.get(character_id, [])
|
||||
|
||||
# 基础分数:连接数量
|
||||
score += connections.size() * 10.0
|
||||
|
||||
# 关系质量加成
|
||||
for connection in connections:
|
||||
var relationship_key = _get_relationship_key(character_id, connection)
|
||||
var relationship = relationships.get(relationship_key)
|
||||
|
||||
if relationship:
|
||||
# 根据关系强度和类型加分
|
||||
score += relationship.strength * 0.5
|
||||
|
||||
match relationship.relationship_type:
|
||||
RelationshipType.CLOSE_FRIEND:
|
||||
score += 20.0
|
||||
RelationshipType.FRIEND:
|
||||
score += 15.0
|
||||
RelationshipType.MENTOR:
|
||||
score += 25.0
|
||||
RelationshipType.COLLEAGUE:
|
||||
score += 10.0
|
||||
|
||||
# 群组参与加成
|
||||
for group_name in relationship_groups:
|
||||
var members = relationship_groups[group_name]
|
||||
if character_id in members:
|
||||
score += members.size() * 5.0
|
||||
|
||||
return score
|
||||
|
||||
## 获取影响力排行
|
||||
func get_influence_ranking(limit: int = 10) -> Array[Dictionary]:
|
||||
"""
|
||||
获取影响力排行榜
|
||||
@param limit: 限制返回数量
|
||||
@return: 排行信息数组
|
||||
"""
|
||||
var ranking = []
|
||||
|
||||
for character_id in influence_scores:
|
||||
ranking.append({
|
||||
"character_id": character_id,
|
||||
"influence_score": influence_scores[character_id]
|
||||
})
|
||||
|
||||
# 按影响力分数排序
|
||||
ranking.sort_custom(func(a, b): return a.influence_score > b.influence_score)
|
||||
|
||||
if limit > 0 and limit < ranking.size():
|
||||
ranking = ranking.slice(0, limit)
|
||||
|
||||
return ranking
|
||||
|
||||
## 添加连接
|
||||
func _add_connection(from_character: String, to_character: String) -> void:
|
||||
"""
|
||||
添加角色连接到索引
|
||||
@param from_character: 源角色ID
|
||||
@param to_character: 目标角色ID
|
||||
"""
|
||||
if not character_connections.has(from_character):
|
||||
character_connections[from_character] = []
|
||||
|
||||
var connections = character_connections[from_character]
|
||||
if not to_character in connections:
|
||||
connections.append(to_character)
|
||||
|
||||
## 获取关系键
|
||||
func _get_relationship_key(from_character: String, to_character: String) -> String:
|
||||
"""
|
||||
生成关系键(确保一致性)
|
||||
@param from_character: 源角色ID
|
||||
@param to_character: 目标角色ID
|
||||
@return: 关系键
|
||||
"""
|
||||
return from_character + ":" + to_character
|
||||
|
||||
## 保存网络数据
|
||||
func save_network_data() -> void:
|
||||
"""保存关系网络数据到本地文件"""
|
||||
var data = {
|
||||
"relationships": {},
|
||||
"character_connections": character_connections,
|
||||
"relationship_groups": relationship_groups,
|
||||
"influence_scores": influence_scores
|
||||
}
|
||||
|
||||
# 序列化关系数据
|
||||
for relationship_key in relationships:
|
||||
var relationship = relationships[relationship_key]
|
||||
data.relationships[relationship_key] = {
|
||||
"from_character": relationship.from_character,
|
||||
"to_character": relationship.to_character,
|
||||
"relationship_type": relationship.relationship_type,
|
||||
"strength": relationship.strength,
|
||||
"trust_level": relationship.trust_level,
|
||||
"interaction_history": relationship.interaction_history,
|
||||
"shared_experiences": relationship.shared_experiences,
|
||||
"created_at": relationship.created_at,
|
||||
"last_updated": relationship.last_updated,
|
||||
"tags": relationship.tags
|
||||
}
|
||||
|
||||
var file = FileAccess.open(network_file_path, FileAccess.WRITE)
|
||||
if file:
|
||||
var json_string = JSON.stringify(data)
|
||||
file.store_string(json_string)
|
||||
file.close()
|
||||
print("Relationship network data saved")
|
||||
else:
|
||||
print("Failed to save relationship network data")
|
||||
|
||||
## 加载网络数据
|
||||
func load_network_data() -> void:
|
||||
"""从本地文件加载关系网络数据"""
|
||||
if not FileAccess.file_exists(network_file_path):
|
||||
print("No relationship network data file found, starting fresh")
|
||||
return
|
||||
|
||||
var file = FileAccess.open(network_file_path, FileAccess.READ)
|
||||
if file:
|
||||
var json_string = file.get_as_text()
|
||||
file.close()
|
||||
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(json_string)
|
||||
|
||||
if parse_result == OK:
|
||||
var data = json.data
|
||||
|
||||
# 加载关系数据
|
||||
if data.has("relationships"):
|
||||
for relationship_key in data.relationships:
|
||||
var rel_data = data.relationships[relationship_key]
|
||||
var relationship = Relationship.new(
|
||||
rel_data.get("from_character", ""),
|
||||
rel_data.get("to_character", ""),
|
||||
rel_data.get("relationship_type", RelationshipType.NEUTRAL)
|
||||
)
|
||||
relationship.strength = rel_data.get("strength", 0.0)
|
||||
relationship.trust_level = rel_data.get("trust_level", 0.0)
|
||||
|
||||
# 正确处理类型化数组的赋值
|
||||
var history_data = rel_data.get("interaction_history", [])
|
||||
relationship.interaction_history.clear()
|
||||
for item in history_data:
|
||||
if item is Dictionary:
|
||||
relationship.interaction_history.append(item)
|
||||
|
||||
var experiences_data = rel_data.get("shared_experiences", [])
|
||||
relationship.shared_experiences.clear()
|
||||
for item in experiences_data:
|
||||
if item is String:
|
||||
relationship.shared_experiences.append(item)
|
||||
|
||||
var tags_data = rel_data.get("tags", [])
|
||||
relationship.tags.clear()
|
||||
for item in tags_data:
|
||||
if item is String:
|
||||
relationship.tags.append(item)
|
||||
|
||||
relationship.created_at = rel_data.get("created_at", Time.get_unix_time_from_system())
|
||||
relationship.last_updated = rel_data.get("last_updated", relationship.created_at)
|
||||
relationships[relationship_key] = relationship
|
||||
|
||||
# 加载连接索引
|
||||
if data.has("character_connections"):
|
||||
character_connections = data.character_connections
|
||||
|
||||
# 加载群组数据
|
||||
if data.has("relationship_groups"):
|
||||
relationship_groups = data.relationship_groups
|
||||
|
||||
# 加载影响力分数
|
||||
if data.has("influence_scores"):
|
||||
influence_scores = data.influence_scores
|
||||
|
||||
print("Relationship network data loaded: ", relationships.size(), " relationships, ", relationship_groups.size(), " groups")
|
||||
else:
|
||||
print("Failed to parse relationship network data JSON")
|
||||
else:
|
||||
print("Failed to open relationship network data file")
|
||||
|
||||
## 获取统计信息
|
||||
func get_statistics() -> Dictionary:
|
||||
"""
|
||||
获取关系网络统计信息
|
||||
@return: 统计信息字典
|
||||
"""
|
||||
var type_counts = {}
|
||||
var total_interactions = 0
|
||||
|
||||
for relationship_key in relationships:
|
||||
var relationship = relationships[relationship_key]
|
||||
var type_name = RelationshipType.keys()[relationship.relationship_type]
|
||||
type_counts[type_name] = type_counts.get(type_name, 0) + 1
|
||||
total_interactions += relationship.interaction_history.size()
|
||||
|
||||
return {
|
||||
"total_relationships": relationships.size(),
|
||||
"total_characters": character_connections.size(),
|
||||
"total_groups": relationship_groups.size(),
|
||||
"total_interactions": total_interactions,
|
||||
"relationship_types": type_counts,
|
||||
"average_connections": float(relationships.size()) / max(character_connections.size(), 1)
|
||||
}
|
||||
1
scripts/RelationshipNetwork.gd.uid
Normal file
1
scripts/RelationshipNetwork.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://oarhnakohrx
|
||||
170
scripts/SecurityConfig.gd
Normal file
170
scripts/SecurityConfig.gd
Normal file
@@ -0,0 +1,170 @@
|
||||
extends Node
|
||||
class_name SecurityConfig
|
||||
## 安全配置类
|
||||
## 集中管理所有安全相关的配置和常量
|
||||
|
||||
# 输入验证配置
|
||||
const INPUT_VALIDATION = {
|
||||
"max_message_length": 500,
|
||||
"max_username_length": 50,
|
||||
"max_character_name_length": 20,
|
||||
"min_character_name_length": 2,
|
||||
"max_json_size": 10000 # 10KB
|
||||
}
|
||||
|
||||
# 会话管理配置
|
||||
const SESSION_MANAGEMENT = {
|
||||
"session_timeout": 1800.0, # 30分钟
|
||||
"max_failed_attempts": 5,
|
||||
"lockout_duration": 300.0, # 5分钟
|
||||
"cleanup_interval": 300.0 # 5分钟清理间隔
|
||||
}
|
||||
|
||||
# 网络安全配置
|
||||
const NETWORK_SECURITY = {
|
||||
"max_message_rate": 10, # 每秒最大消息数
|
||||
"rate_limit_window": 1.0, # 速率限制窗口(秒)
|
||||
"connection_timeout": 10.0, # 连接超时
|
||||
"heartbeat_interval": 30.0 # 心跳间隔
|
||||
}
|
||||
|
||||
# 内容过滤配置
|
||||
const CONTENT_FILTERING = {
|
||||
"enable_html_filtering": true,
|
||||
"enable_script_detection": true,
|
||||
"enable_injection_detection": true,
|
||||
"max_repetition_ratio": 0.7, # 最大重复字符比例
|
||||
"enable_profanity_filter": false # 可选:脏话过滤
|
||||
}
|
||||
|
||||
# 日志和监控配置
|
||||
const LOGGING = {
|
||||
"log_security_events": true,
|
||||
"log_failed_attempts": true,
|
||||
"log_suspicious_activity": true,
|
||||
"max_log_entries": 1000
|
||||
}
|
||||
|
||||
# 加密和哈希配置
|
||||
const ENCRYPTION = {
|
||||
"use_secure_tokens": true,
|
||||
"token_complexity": "high", # low, medium, high
|
||||
"hash_algorithm": "sha256"
|
||||
}
|
||||
|
||||
## 获取配置值
|
||||
static func get_config(category: String, key: String, default_value = null):
|
||||
"""
|
||||
获取配置值
|
||||
@param category: 配置类别
|
||||
@param key: 配置键
|
||||
@param default_value: 默认值
|
||||
@return: 配置值
|
||||
"""
|
||||
var config_dict = null
|
||||
|
||||
match category:
|
||||
"input_validation":
|
||||
config_dict = INPUT_VALIDATION
|
||||
"session_management":
|
||||
config_dict = SESSION_MANAGEMENT
|
||||
"network_security":
|
||||
config_dict = NETWORK_SECURITY
|
||||
"content_filtering":
|
||||
config_dict = CONTENT_FILTERING
|
||||
"logging":
|
||||
config_dict = LOGGING
|
||||
"encryption":
|
||||
config_dict = ENCRYPTION
|
||||
_:
|
||||
return default_value
|
||||
|
||||
if config_dict and config_dict.has(key):
|
||||
return config_dict[key]
|
||||
|
||||
return default_value
|
||||
|
||||
## 验证配置完整性
|
||||
static func validate_config() -> bool:
|
||||
"""
|
||||
验证安全配置的完整性
|
||||
@return: 配置是否有效
|
||||
"""
|
||||
# 检查关键配置项
|
||||
var critical_configs = [
|
||||
["input_validation", "max_message_length"],
|
||||
["session_management", "session_timeout"],
|
||||
["network_security", "connection_timeout"],
|
||||
["content_filtering", "enable_script_detection"]
|
||||
]
|
||||
|
||||
for config in critical_configs:
|
||||
var value = get_config(config[0], config[1])
|
||||
if value == null:
|
||||
print("ERROR: Missing critical security config: %s.%s" % [config[0], config[1]])
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
## 获取安全级别
|
||||
static func get_security_level() -> String:
|
||||
"""
|
||||
获取当前安全级别
|
||||
@return: 安全级别 ("low", "medium", "high")
|
||||
"""
|
||||
# 基于配置确定安全级别
|
||||
var script_detection = get_config("content_filtering", "enable_script_detection", false)
|
||||
var injection_detection = get_config("content_filtering", "enable_injection_detection", false)
|
||||
var secure_tokens = get_config("encryption", "use_secure_tokens", false)
|
||||
var max_attempts = get_config("session_management", "max_failed_attempts", 10)
|
||||
|
||||
if script_detection and injection_detection and secure_tokens and max_attempts <= 5:
|
||||
return "high"
|
||||
elif (script_detection or injection_detection) and max_attempts <= 10:
|
||||
return "medium"
|
||||
else:
|
||||
return "low"
|
||||
|
||||
## 应用安全配置到游戏配置
|
||||
static func apply_to_game_config():
|
||||
"""将安全配置应用到GameConfig"""
|
||||
# GameConfig可能不存在,这是正常的
|
||||
pass
|
||||
|
||||
## 获取推荐的安全设置
|
||||
static func get_recommended_settings() -> Dictionary:
|
||||
"""
|
||||
获取推荐的安全设置
|
||||
@return: 推荐设置字典
|
||||
"""
|
||||
return {
|
||||
"description": "推荐的高安全级别设置",
|
||||
"settings": {
|
||||
"input_validation": {
|
||||
"max_message_length": 300, # 更严格的消息长度限制
|
||||
"max_username_length": 30,
|
||||
"enable_strict_validation": true
|
||||
},
|
||||
"session_management": {
|
||||
"session_timeout": 900.0, # 15分钟更短的会话
|
||||
"max_failed_attempts": 3, # 更严格的失败尝试限制
|
||||
"lockout_duration": 600.0 # 10分钟锁定
|
||||
},
|
||||
"content_filtering": {
|
||||
"enable_html_filtering": true,
|
||||
"enable_script_detection": true,
|
||||
"enable_injection_detection": true,
|
||||
"max_repetition_ratio": 0.5 # 更严格的重复检测
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
## 初始化安全配置
|
||||
static func initialize():
|
||||
"""初始化安全配置"""
|
||||
if validate_config():
|
||||
var security_level = get_security_level()
|
||||
print("Security configuration initialized - Level: " + security_level)
|
||||
apply_to_game_config()
|
||||
else:
|
||||
print("ERROR: Security configuration validation failed")
|
||||
1
scripts/SecurityConfig.gd.uid
Normal file
1
scripts/SecurityConfig.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://duxl8lgavgaw7
|
||||
466
scripts/SecurityManager.gd
Normal file
466
scripts/SecurityManager.gd
Normal file
@@ -0,0 +1,466 @@
|
||||
extends Node
|
||||
class_name SecurityManager
|
||||
## 安全管理器
|
||||
## 提供输入验证、防护措施和安全检查功能
|
||||
|
||||
# 安全配置常量(从SecurityConfig获取)
|
||||
const DEFAULT_MAX_MESSAGE_LENGTH = 500
|
||||
const DEFAULT_MAX_USERNAME_LENGTH = 50
|
||||
const DEFAULT_MAX_CHARACTER_NAME_LENGTH = 20
|
||||
const DEFAULT_MIN_CHARACTER_NAME_LENGTH = 2
|
||||
|
||||
# 恶意模式检测
|
||||
const MALICIOUS_PATTERNS = [
|
||||
"<script",
|
||||
"javascript:",
|
||||
"vbscript:",
|
||||
"onload=",
|
||||
"onerror=",
|
||||
"onclick=",
|
||||
"eval(",
|
||||
"document.cookie",
|
||||
"window.location",
|
||||
"alert(",
|
||||
"confirm(",
|
||||
"prompt(",
|
||||
"innerHTML",
|
||||
"outerHTML"
|
||||
]
|
||||
|
||||
# SQL注入模式(虽然我们使用JSON,但仍需防护)
|
||||
const SQL_INJECTION_PATTERNS = [
|
||||
"'",
|
||||
"\"",
|
||||
";",
|
||||
"--",
|
||||
"/*",
|
||||
"*/",
|
||||
"union",
|
||||
"select",
|
||||
"insert",
|
||||
"update",
|
||||
"delete",
|
||||
"drop",
|
||||
"create",
|
||||
"alter"
|
||||
]
|
||||
|
||||
# 会话管理
|
||||
var active_sessions: Dictionary = {}
|
||||
var session_timeout: float = 1800.0 # 30分钟会话超时
|
||||
var max_failed_attempts: int = 5
|
||||
var failed_attempts: Dictionary = {}
|
||||
var lockout_duration: float = 300.0 # 5分钟锁定时间
|
||||
|
||||
## 验证用户输入
|
||||
static func validate_input(input: String, input_type: String) -> Dictionary:
|
||||
"""
|
||||
验证用户输入的安全性和有效性
|
||||
@param input: 输入字符串
|
||||
@param input_type: 输入类型 ("username", "character_name", "message")
|
||||
@return: 验证结果 {valid: bool, error: String, sanitized: String}
|
||||
"""
|
||||
var result = {
|
||||
"valid": false,
|
||||
"error": "",
|
||||
"sanitized": ""
|
||||
}
|
||||
|
||||
# 基础检查
|
||||
if input == null:
|
||||
result.error = "输入不能为空"
|
||||
return result
|
||||
|
||||
# 转换为字符串并去除首尾空格
|
||||
var clean_input = str(input).strip_edges()
|
||||
|
||||
# 获取配置值
|
||||
var max_username_length = SecurityConfig.get_config("input_validation", "max_username_length", DEFAULT_MAX_USERNAME_LENGTH)
|
||||
var max_character_name_length = SecurityConfig.get_config("input_validation", "max_character_name_length", DEFAULT_MAX_CHARACTER_NAME_LENGTH)
|
||||
var min_character_name_length = SecurityConfig.get_config("input_validation", "min_character_name_length", DEFAULT_MIN_CHARACTER_NAME_LENGTH)
|
||||
var max_message_length = SecurityConfig.get_config("input_validation", "max_message_length", DEFAULT_MAX_MESSAGE_LENGTH)
|
||||
|
||||
# 长度检查
|
||||
match input_type:
|
||||
"username":
|
||||
if clean_input.length() == 0:
|
||||
result.error = "用户名不能为空"
|
||||
return result
|
||||
if clean_input.length() > max_username_length:
|
||||
result.error = "用户名长度不能超过 %d 个字符" % max_username_length
|
||||
return result
|
||||
|
||||
"character_name":
|
||||
if clean_input.length() < min_character_name_length:
|
||||
result.error = "角色名称至少需要 %d 个字符" % min_character_name_length
|
||||
return result
|
||||
if clean_input.length() > max_character_name_length:
|
||||
result.error = "角色名称不能超过 %d 个字符" % max_character_name_length
|
||||
return result
|
||||
|
||||
"message":
|
||||
if clean_input.length() == 0:
|
||||
result.error = "消息不能为空"
|
||||
return result
|
||||
if clean_input.length() > max_message_length:
|
||||
result.error = "消息长度不能超过 %d 个字符" % max_message_length
|
||||
return result
|
||||
|
||||
# 恶意内容检测
|
||||
var malicious_check = detect_malicious_content(clean_input)
|
||||
if not malicious_check.safe:
|
||||
result.error = "输入包含不安全内容: " + malicious_check.reason
|
||||
return result
|
||||
|
||||
# 清理输入
|
||||
var sanitized = sanitize_input(clean_input)
|
||||
|
||||
result.valid = true
|
||||
result.sanitized = sanitized
|
||||
return result
|
||||
|
||||
## 检测恶意内容
|
||||
static func detect_malicious_content(input: String) -> Dictionary:
|
||||
"""
|
||||
检测输入中的恶意内容
|
||||
@param input: 输入字符串
|
||||
@return: {safe: bool, reason: String}
|
||||
"""
|
||||
var input_lower = input.to_lower()
|
||||
|
||||
# 检查脚本注入
|
||||
for pattern in MALICIOUS_PATTERNS:
|
||||
if input_lower.contains(pattern.to_lower()):
|
||||
return {
|
||||
"safe": false,
|
||||
"reason": "检测到潜在的脚本注入: " + pattern
|
||||
}
|
||||
|
||||
# 检查SQL注入(虽然我们不使用SQL,但作为额外防护)
|
||||
for pattern in SQL_INJECTION_PATTERNS:
|
||||
if input_lower.contains(pattern.to_lower()):
|
||||
return {
|
||||
"safe": false,
|
||||
"reason": "检测到潜在的注入攻击: " + pattern
|
||||
}
|
||||
|
||||
# 检查过长的重复字符(可能的DoS攻击)
|
||||
if has_excessive_repetition(input):
|
||||
return {
|
||||
"safe": false,
|
||||
"reason": "检测到异常的重复字符模式"
|
||||
}
|
||||
|
||||
return {"safe": true, "reason": ""}
|
||||
|
||||
## 检查过度重复字符
|
||||
static func has_excessive_repetition(input: String) -> bool:
|
||||
"""
|
||||
检查字符串是否包含过度重复的字符
|
||||
@param input: 输入字符串
|
||||
@return: 是否包含过度重复
|
||||
"""
|
||||
if input.length() < 10:
|
||||
return false
|
||||
|
||||
var char_counts = {}
|
||||
for character in input:
|
||||
char_counts[character] = char_counts.get(character, 0) + 1
|
||||
|
||||
# 如果任何字符重复超过输入长度的70%,认为是异常
|
||||
var threshold = input.length() * 0.7
|
||||
for count in char_counts.values():
|
||||
if count > threshold:
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
## 清理输入
|
||||
static func sanitize_input(input: String) -> String:
|
||||
"""
|
||||
清理输入字符串,移除潜在的危险字符
|
||||
@param input: 输入字符串
|
||||
@return: 清理后的字符串
|
||||
"""
|
||||
var sanitized = input
|
||||
|
||||
# 移除控制字符(除了换行和制表符)
|
||||
var clean_chars = []
|
||||
for i in range(sanitized.length()):
|
||||
var char_code = sanitized.unicode_at(i)
|
||||
# 保留可打印字符、空格、换行、制表符,以及Unicode字符(支持中文等)
|
||||
if (char_code >= 32 and char_code <= 126) or char_code == 10 or char_code == 9 or char_code > 127:
|
||||
clean_chars.append(sanitized[i])
|
||||
|
||||
sanitized = "".join(clean_chars)
|
||||
|
||||
# 移除HTML标签
|
||||
sanitized = remove_html_tags(sanitized)
|
||||
|
||||
# 限制连续空格
|
||||
sanitized = limit_consecutive_spaces(sanitized)
|
||||
|
||||
return sanitized.strip_edges()
|
||||
|
||||
## 移除HTML标签
|
||||
static func remove_html_tags(input: String) -> String:
|
||||
"""
|
||||
移除HTML标签
|
||||
@param input: 输入字符串
|
||||
@return: 移除标签后的字符串
|
||||
"""
|
||||
var regex = RegEx.new()
|
||||
regex.compile("<[^>]*>")
|
||||
return regex.sub(input, "", true)
|
||||
|
||||
## 限制连续空格
|
||||
static func limit_consecutive_spaces(input: String) -> String:
|
||||
"""
|
||||
将多个连续空格替换为单个空格
|
||||
@param input: 输入字符串
|
||||
@return: 处理后的字符串
|
||||
"""
|
||||
var regex = RegEx.new()
|
||||
regex.compile("\\s+")
|
||||
return regex.sub(input, " ", true)
|
||||
|
||||
## 验证消息格式
|
||||
static func validate_message_format(message: Dictionary) -> bool:
|
||||
"""
|
||||
验证网络消息格式的安全性
|
||||
@param message: 消息字典
|
||||
@return: 是否安全
|
||||
"""
|
||||
# 基础格式检查
|
||||
if not message.has("type") or not message.has("data") or not message.has("timestamp"):
|
||||
return false
|
||||
|
||||
# 检查消息类型是否在允许列表中
|
||||
var allowed_types = [
|
||||
"auth_request",
|
||||
"auth_response",
|
||||
"character_create",
|
||||
"character_move",
|
||||
"character_state",
|
||||
"dialogue_send",
|
||||
"world_state",
|
||||
"ping",
|
||||
"pong",
|
||||
"error"
|
||||
]
|
||||
|
||||
if not message.type in allowed_types:
|
||||
return false
|
||||
|
||||
# 检查时间戳是否合理(不能太旧或太新)
|
||||
var current_time = Time.get_unix_time_from_system() * 1000 # 转换为毫秒
|
||||
var msg_time = message.get("timestamp", 0)
|
||||
var time_diff = abs(current_time - msg_time)
|
||||
|
||||
# 允许5分钟的时间差(毫秒)
|
||||
if time_diff > 300000: # 5分钟 = 300000毫秒
|
||||
return false
|
||||
|
||||
# 检查数据字段
|
||||
if typeof(message.data) != TYPE_DICTIONARY:
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
## 创建会话
|
||||
func create_session(client_id: String, username: String) -> String:
|
||||
"""
|
||||
创建新的用户会话
|
||||
@param client_id: 客户端ID
|
||||
@param username: 用户名
|
||||
@return: 会话令牌
|
||||
"""
|
||||
var session_token = generate_session_token()
|
||||
var session_data = {
|
||||
"client_id": client_id,
|
||||
"username": username,
|
||||
"created_at": Time.get_unix_time_from_system(),
|
||||
"last_activity": Time.get_unix_time_from_system(),
|
||||
"is_active": true
|
||||
}
|
||||
|
||||
active_sessions[session_token] = session_data
|
||||
print("Session created for user: " + username)
|
||||
|
||||
return session_token
|
||||
|
||||
## 验证会话
|
||||
func validate_session(session_token: String) -> bool:
|
||||
"""
|
||||
验证会话是否有效
|
||||
@param session_token: 会话令牌
|
||||
@return: 是否有效
|
||||
"""
|
||||
if not active_sessions.has(session_token):
|
||||
return false
|
||||
|
||||
var session = active_sessions[session_token]
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
|
||||
# 检查会话是否过期
|
||||
if current_time - session.last_activity > session_timeout:
|
||||
invalidate_session(session_token)
|
||||
return false
|
||||
|
||||
# 更新最后活动时间
|
||||
session.last_activity = current_time
|
||||
return session.is_active
|
||||
|
||||
## 使会话无效
|
||||
func invalidate_session(session_token: String) -> void:
|
||||
"""
|
||||
使会话无效
|
||||
@param session_token: 会话令牌
|
||||
"""
|
||||
if active_sessions.has(session_token):
|
||||
var session = active_sessions[session_token]
|
||||
print("Session invalidated for user: " + session.get("username", "unknown"))
|
||||
active_sessions.erase(session_token)
|
||||
|
||||
## 生成会话令牌
|
||||
func generate_session_token() -> String:
|
||||
"""
|
||||
生成安全的会话令牌
|
||||
@return: 会话令牌
|
||||
"""
|
||||
var timestamp = Time.get_unix_time_from_system()
|
||||
var random1 = randi()
|
||||
var random2 = randi()
|
||||
var random3 = randi()
|
||||
|
||||
# 创建更复杂的令牌
|
||||
var token_data = str(timestamp) + "_" + str(random1) + "_" + str(random2) + "_" + str(random3)
|
||||
return token_data.sha256_text()
|
||||
|
||||
## 记录失败尝试
|
||||
func record_failed_attempt(identifier: String) -> bool:
|
||||
"""
|
||||
记录失败的认证尝试
|
||||
@param identifier: 标识符(IP地址或客户端ID)
|
||||
@return: 是否应该锁定
|
||||
"""
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
|
||||
if not failed_attempts.has(identifier):
|
||||
failed_attempts[identifier] = {
|
||||
"count": 0,
|
||||
"first_attempt": current_time,
|
||||
"last_attempt": current_time,
|
||||
"locked_until": 0
|
||||
}
|
||||
|
||||
var attempt_data = failed_attempts[identifier]
|
||||
|
||||
# 检查是否仍在锁定期
|
||||
if current_time < attempt_data.locked_until:
|
||||
return true # 仍被锁定
|
||||
|
||||
# 如果距离第一次尝试超过1小时,重置计数
|
||||
if current_time - attempt_data.first_attempt > 3600:
|
||||
attempt_data.count = 0
|
||||
attempt_data.first_attempt = current_time
|
||||
|
||||
attempt_data.count += 1
|
||||
attempt_data.last_attempt = current_time
|
||||
|
||||
# 检查是否需要锁定
|
||||
if attempt_data.count >= max_failed_attempts:
|
||||
attempt_data.locked_until = current_time + lockout_duration
|
||||
print("WARNING: Client locked due to too many failed attempts: " + identifier)
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
## 检查是否被锁定
|
||||
func is_locked(identifier: String) -> bool:
|
||||
"""
|
||||
检查标识符是否被锁定
|
||||
@param identifier: 标识符
|
||||
@return: 是否被锁定
|
||||
"""
|
||||
if not failed_attempts.has(identifier):
|
||||
return false
|
||||
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
var attempt_data = failed_attempts[identifier]
|
||||
|
||||
return current_time < attempt_data.locked_until
|
||||
|
||||
## 清除失败尝试记录
|
||||
func clear_failed_attempts(identifier: String) -> void:
|
||||
"""
|
||||
清除失败尝试记录(成功认证后调用)
|
||||
@param identifier: 标识符
|
||||
"""
|
||||
if failed_attempts.has(identifier):
|
||||
failed_attempts.erase(identifier)
|
||||
|
||||
## 清理过期会话
|
||||
func cleanup_expired_sessions() -> void:
|
||||
"""清理过期的会话"""
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
var expired_tokens = []
|
||||
|
||||
for token in active_sessions:
|
||||
var session = active_sessions[token]
|
||||
if current_time - session.last_activity > session_timeout:
|
||||
expired_tokens.append(token)
|
||||
|
||||
for token in expired_tokens:
|
||||
invalidate_session(token)
|
||||
|
||||
if expired_tokens.size() > 0:
|
||||
print("Cleaned up %d expired sessions" % expired_tokens.size())
|
||||
|
||||
## 获取安全统计
|
||||
func get_security_stats() -> Dictionary:
|
||||
"""
|
||||
获取安全统计信息
|
||||
@return: 统计信息
|
||||
"""
|
||||
return {
|
||||
"active_sessions": active_sessions.size(),
|
||||
"failed_attempts": failed_attempts.size(),
|
||||
"locked_clients": _count_locked_clients()
|
||||
}
|
||||
|
||||
## 计算被锁定的客户端数量
|
||||
func _count_locked_clients() -> int:
|
||||
"""计算当前被锁定的客户端数量"""
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
var locked_count = 0
|
||||
|
||||
for identifier in failed_attempts:
|
||||
var attempt_data = failed_attempts[identifier]
|
||||
if current_time < attempt_data.locked_until:
|
||||
locked_count += 1
|
||||
|
||||
return locked_count
|
||||
|
||||
## 定期清理任务
|
||||
func _ready():
|
||||
"""初始化安全管理器"""
|
||||
# 初始化安全配置
|
||||
_initialize_config()
|
||||
|
||||
# 每5分钟清理一次过期会话
|
||||
var cleanup_interval = SecurityConfig.get_config("session_management", "cleanup_interval", 300.0)
|
||||
var cleanup_timer = Timer.new()
|
||||
cleanup_timer.wait_time = cleanup_interval
|
||||
cleanup_timer.timeout.connect(cleanup_expired_sessions)
|
||||
cleanup_timer.autostart = true
|
||||
add_child(cleanup_timer)
|
||||
|
||||
print("SecurityManager initialized with security level: " + SecurityConfig.get_security_level())
|
||||
|
||||
## 初始化配置
|
||||
func _initialize_config():
|
||||
"""从SecurityConfig初始化配置值"""
|
||||
session_timeout = SecurityConfig.get_config("session_management", "session_timeout", 1800.0)
|
||||
max_failed_attempts = SecurityConfig.get_config("session_management", "max_failed_attempts", 5)
|
||||
lockout_duration = SecurityConfig.get_config("session_management", "lockout_duration", 300.0)
|
||||
1
scripts/SecurityManager.gd.uid
Normal file
1
scripts/SecurityManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://xx7bykd7yh7r
|
||||
174
scripts/SimpleDialogueTest.gd
Normal file
174
scripts/SimpleDialogueTest.gd
Normal file
@@ -0,0 +1,174 @@
|
||||
extends Node
|
||||
## 简单对话测试脚本
|
||||
## 直接在Main场景中使用,无需复杂的类型声明
|
||||
|
||||
# 测试NPC数据
|
||||
var test_npcs = []
|
||||
var spawned_npcs = {}
|
||||
|
||||
# 引用
|
||||
var world_manager = null
|
||||
var dialogue_system = null
|
||||
|
||||
## 初始化测试
|
||||
func setup_test(world_mgr, dialogue_sys):
|
||||
"""设置测试环境"""
|
||||
world_manager = world_mgr
|
||||
dialogue_system = dialogue_sys
|
||||
print("Simple dialogue test setup complete")
|
||||
|
||||
## 生成测试NPC
|
||||
func spawn_test_npcs(player_position: Vector2 = Vector2(640, 360)):
|
||||
"""生成测试NPC"""
|
||||
if not world_manager:
|
||||
print("WorldManager not available")
|
||||
return
|
||||
|
||||
# 检查WorldManager是否已正确设置
|
||||
if not world_manager.character_container:
|
||||
print("WorldManager character container not set, waiting...")
|
||||
# 延迟一秒后重试
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
if not world_manager.character_container:
|
||||
print("Error: WorldManager character container still not set")
|
||||
return
|
||||
|
||||
# 清除已存在的NPC
|
||||
clear_test_npcs()
|
||||
|
||||
# 创建测试NPC数据
|
||||
var npc_data = [
|
||||
{
|
||||
"id": "test_npc_1",
|
||||
"name": "测试小助手",
|
||||
"responses": ["你好!我是测试小助手!", "有什么可以帮助你的吗?", "今天天气真不错呢!"]
|
||||
},
|
||||
{
|
||||
"id": "test_npc_2",
|
||||
"name": "友好机器人",
|
||||
"responses": ["哔哔!我是友好的机器人!", "正在运行对话测试程序...", "系统状态:一切正常!"]
|
||||
},
|
||||
{
|
||||
"id": "test_npc_3",
|
||||
"name": "聊天达人",
|
||||
"responses": ["嗨!想聊什么呢?", "我最喜欢和大家聊天了!", "你知道吗,聊天是最好的交流方式!"]
|
||||
}
|
||||
]
|
||||
|
||||
# 生成NPC
|
||||
for i in range(npc_data.size()):
|
||||
var npc = npc_data[i]
|
||||
|
||||
# 计算位置
|
||||
var angle = (2.0 * PI * i) / npc_data.size()
|
||||
var offset = Vector2(cos(angle), sin(angle)) * 200.0
|
||||
var npc_position = player_position + offset
|
||||
|
||||
# 创建角色数据
|
||||
var character_data = CharacterData.create(npc["name"], "system", npc_position)
|
||||
character_data[CharacterData.FIELD_ID] = npc["id"]
|
||||
|
||||
# 生成角色
|
||||
var character = world_manager.spawn_character(character_data, false)
|
||||
if character:
|
||||
spawned_npcs[npc["id"]] = {
|
||||
"character": character,
|
||||
"responses": npc["responses"],
|
||||
"response_index": 0
|
||||
}
|
||||
print("Test NPC spawned: ", npc["name"], " at ", npc_position)
|
||||
|
||||
print("Generated ", spawned_npcs.size(), " test NPCs")
|
||||
show_help()
|
||||
|
||||
## 清除测试NPC
|
||||
func clear_test_npcs():
|
||||
"""清除所有测试NPC"""
|
||||
for npc_id in spawned_npcs.keys():
|
||||
if world_manager:
|
||||
world_manager.remove_character(npc_id)
|
||||
|
||||
spawned_npcs.clear()
|
||||
print("Test NPCs cleared")
|
||||
|
||||
## 开始对话
|
||||
func start_dialogue_with_npc(npc_id: String):
|
||||
"""开始与NPC对话"""
|
||||
if not dialogue_system or not spawned_npcs.has(npc_id):
|
||||
print("Cannot start dialogue with: ", npc_id)
|
||||
return
|
||||
|
||||
dialogue_system.start_dialogue(npc_id)
|
||||
|
||||
# 发送欢迎消息
|
||||
send_npc_response(npc_id)
|
||||
|
||||
## 发送NPC响应
|
||||
func send_npc_response(npc_id: String):
|
||||
"""发送NPC响应"""
|
||||
if not spawned_npcs.has(npc_id):
|
||||
return
|
||||
|
||||
var npc_data = spawned_npcs[npc_id]
|
||||
var responses = npc_data["responses"]
|
||||
var index = npc_data["response_index"]
|
||||
|
||||
var message = responses[index]
|
||||
npc_data["response_index"] = (index + 1) % responses.size()
|
||||
|
||||
# 延迟发送
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
|
||||
if dialogue_system:
|
||||
dialogue_system.receive_message(npc_id, message)
|
||||
dialogue_system.show_bubble(npc_id, message, 3.0)
|
||||
|
||||
## 获取附近NPC
|
||||
func get_nearby_npcs(position: Vector2, radius: float = 100.0):
|
||||
"""获取附近的NPC"""
|
||||
var nearby = []
|
||||
|
||||
for npc_id in spawned_npcs.keys():
|
||||
var npc_data = spawned_npcs[npc_id]
|
||||
var character = npc_data["character"]
|
||||
if character and character.global_position.distance_to(position) <= radius:
|
||||
nearby.append({
|
||||
"id": npc_id,
|
||||
"name": character.name,
|
||||
"distance": character.global_position.distance_to(position)
|
||||
})
|
||||
|
||||
return nearby
|
||||
|
||||
## 显示帮助
|
||||
func show_help():
|
||||
"""显示测试帮助"""
|
||||
print("=== 对话系统测试帮助 ===")
|
||||
print("1. 已生成 ", spawned_npcs.size(), " 个测试NPC")
|
||||
print("2. 走近NPC按E键开始对话")
|
||||
print("3. 在对话框中输入消息测试")
|
||||
print("4. NPC会自动回复")
|
||||
print("5. 支持表情符号::smile: :happy: :laugh:")
|
||||
print("========================")
|
||||
|
||||
## 测试表情符号
|
||||
func test_emoji():
|
||||
"""测试表情符号转换"""
|
||||
print("=== 表情符号测试 ===")
|
||||
var test_messages = [
|
||||
":smile: 你好!",
|
||||
"今天心情很好 :happy:",
|
||||
":laugh: 哈哈哈"
|
||||
]
|
||||
|
||||
for msg in test_messages:
|
||||
var converted = EmojiManager.convert_text_to_emoji(msg)
|
||||
print("原文: ", msg, " -> 转换: ", converted)
|
||||
|
||||
## 处理玩家消息
|
||||
func handle_player_message(sender: String, _message: String):
|
||||
"""处理玩家发送的消息,让NPC自动回复"""
|
||||
if sender == "player" and dialogue_system and dialogue_system.is_dialogue_active():
|
||||
var target_id = dialogue_system.get_current_target()
|
||||
if spawned_npcs.has(target_id):
|
||||
send_npc_response(target_id)
|
||||
1
scripts/SimpleDialogueTest.gd.uid
Normal file
1
scripts/SimpleDialogueTest.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cyr2ca8bxclbj
|
||||
603
scripts/SocialManager.gd
Normal file
603
scripts/SocialManager.gd
Normal file
@@ -0,0 +1,603 @@
|
||||
extends Node
|
||||
class_name SocialManager
|
||||
## 社交管理器
|
||||
## 统一管理所有社交功能和系统集成
|
||||
|
||||
# 社交系统组件
|
||||
var friend_system: FriendSystem
|
||||
var private_chat_system: PrivateChatSystem
|
||||
var relationship_network: RelationshipNetwork
|
||||
var community_event_system: CommunityEventSystem
|
||||
|
||||
# 网络管理器引用
|
||||
var network_manager: NetworkManager
|
||||
|
||||
# 社交状态
|
||||
var is_initialized: bool = false
|
||||
var current_user_id: String = "player"
|
||||
|
||||
# 信号
|
||||
signal social_systems_ready()
|
||||
signal friend_activity(activity_type: String, data: Dictionary)
|
||||
signal social_notification(notification_type: String, title: String, message: String, data: Dictionary)
|
||||
|
||||
func _ready():
|
||||
"""初始化社交管理器"""
|
||||
print("SocialManager initializing...")
|
||||
_initialize_social_systems()
|
||||
|
||||
## 初始化社交系统
|
||||
func _initialize_social_systems() -> void:
|
||||
"""初始化所有社交系统组件"""
|
||||
# 创建好友系统
|
||||
friend_system = FriendSystem.new()
|
||||
friend_system.name = "FriendSystem"
|
||||
add_child(friend_system)
|
||||
|
||||
# 创建私聊系统
|
||||
private_chat_system = PrivateChatSystem.new()
|
||||
private_chat_system.name = "PrivateChatSystem"
|
||||
add_child(private_chat_system)
|
||||
|
||||
# 创建关系网络
|
||||
relationship_network = RelationshipNetwork.new()
|
||||
relationship_network.name = "RelationshipNetwork"
|
||||
add_child(relationship_network)
|
||||
|
||||
# 创建社区事件系统
|
||||
community_event_system = CommunityEventSystem.new()
|
||||
community_event_system.name = "CommunityEventSystem"
|
||||
add_child(community_event_system)
|
||||
|
||||
# 设置系统间的引用
|
||||
private_chat_system.set_friend_system(friend_system)
|
||||
community_event_system.set_relationship_network(relationship_network)
|
||||
|
||||
# 连接信号
|
||||
_connect_system_signals()
|
||||
|
||||
is_initialized = true
|
||||
social_systems_ready.emit()
|
||||
|
||||
print("SocialManager initialized with all social systems")
|
||||
|
||||
## 连接系统信号
|
||||
func _connect_system_signals() -> void:
|
||||
"""连接各个社交系统的信号"""
|
||||
# 好友系统信号
|
||||
friend_system.friend_request_sent.connect(_on_friend_request_sent)
|
||||
# friend_system.friend_request_received.connect(_on_friend_request_received) # 信号暂时未使用
|
||||
friend_system.friend_request_accepted.connect(_on_friend_request_accepted)
|
||||
friend_system.friend_removed.connect(_on_friend_removed)
|
||||
friend_system.friend_online.connect(_on_friend_online)
|
||||
friend_system.friend_offline.connect(_on_friend_offline)
|
||||
friend_system.relationship_level_changed.connect(_on_relationship_level_changed)
|
||||
|
||||
# 私聊系统信号
|
||||
private_chat_system.conversation_created.connect(_on_conversation_created)
|
||||
private_chat_system.message_received.connect(_on_private_message_received)
|
||||
private_chat_system.unread_count_changed.connect(_on_unread_count_changed)
|
||||
|
||||
# 关系网络信号
|
||||
relationship_network.relationship_created.connect(_on_relationship_created)
|
||||
relationship_network.relationship_updated.connect(_on_relationship_updated)
|
||||
relationship_network.influence_score_changed.connect(_on_influence_score_changed)
|
||||
|
||||
# 社区事件系统信号
|
||||
community_event_system.event_created.connect(_on_event_created)
|
||||
community_event_system.participant_joined.connect(_on_participant_joined)
|
||||
community_event_system.event_started.connect(_on_event_started)
|
||||
community_event_system.event_completed.connect(_on_event_completed)
|
||||
community_event_system.milestone_achieved.connect(_on_milestone_achieved)
|
||||
|
||||
## 设置网络管理器
|
||||
func set_network_manager(nm: NetworkManager) -> void:
|
||||
"""
|
||||
设置网络管理器引用
|
||||
@param nm: 网络管理器实例
|
||||
"""
|
||||
network_manager = nm
|
||||
if network_manager:
|
||||
network_manager.message_received.connect(_on_network_message_received)
|
||||
|
||||
## 处理网络消息
|
||||
func _on_network_message_received(message: Dictionary) -> void:
|
||||
"""
|
||||
处理来自网络的社交相关消息
|
||||
@param message: 网络消息
|
||||
"""
|
||||
var message_type = message.get("type", "")
|
||||
var data = message.get("data", {})
|
||||
|
||||
match message_type:
|
||||
"friend_request":
|
||||
_handle_friend_request_message(data)
|
||||
"friend_response":
|
||||
_handle_friend_response_message(data)
|
||||
"private_message":
|
||||
_handle_private_message(data)
|
||||
"event_invitation":
|
||||
_handle_event_invitation(data)
|
||||
"social_update":
|
||||
_handle_social_update(data)
|
||||
|
||||
## 发送好友请求
|
||||
func send_friend_request(target_id: String, target_name: String) -> bool:
|
||||
"""
|
||||
发送好友请求
|
||||
@param target_id: 目标用户ID
|
||||
@param target_name: 目标用户名称
|
||||
@return: 是否成功发送
|
||||
"""
|
||||
if not is_initialized:
|
||||
print("Social systems not initialized")
|
||||
return false
|
||||
|
||||
# 本地处理
|
||||
var success = friend_system.send_friend_request(target_id, target_name)
|
||||
|
||||
if success and network_manager:
|
||||
# 发送网络消息
|
||||
var message = {
|
||||
"type": "friend_request",
|
||||
"data": {
|
||||
"target_id": target_id,
|
||||
"requester_name": "Player" # 这里应该从用户数据获取
|
||||
}
|
||||
}
|
||||
network_manager.send_message(message)
|
||||
|
||||
return success
|
||||
|
||||
## 接受好友请求
|
||||
func accept_friend_request(requester_id: String) -> bool:
|
||||
"""
|
||||
接受好友请求
|
||||
@param requester_id: 请求者ID
|
||||
@return: 是否成功接受
|
||||
"""
|
||||
if not is_initialized:
|
||||
return false
|
||||
|
||||
var success = friend_system.accept_friend_request(requester_id)
|
||||
|
||||
if success and network_manager:
|
||||
# 发送网络消息
|
||||
var message = {
|
||||
"type": "friend_response",
|
||||
"data": {
|
||||
"requester_id": requester_id,
|
||||
"accepted": true
|
||||
}
|
||||
}
|
||||
network_manager.send_message(message)
|
||||
|
||||
# 在关系网络中创建好友关系
|
||||
relationship_network.create_relationship(current_user_id, requester_id, RelationshipNetwork.RelationshipType.FRIEND)
|
||||
|
||||
return success
|
||||
|
||||
## 开始私聊
|
||||
func start_private_chat(target_id: String, target_name: String) -> String:
|
||||
"""
|
||||
开始私聊
|
||||
@param target_id: 目标用户ID
|
||||
@param target_name: 目标用户名称
|
||||
@return: 会话ID
|
||||
"""
|
||||
if not is_initialized:
|
||||
return ""
|
||||
|
||||
return private_chat_system.start_private_chat(target_id, target_name)
|
||||
|
||||
## 发送私聊消息
|
||||
func send_private_message(conversation_id: String, message: String) -> bool:
|
||||
"""
|
||||
发送私聊消息
|
||||
@param conversation_id: 会话ID
|
||||
@param message: 消息内容
|
||||
@return: 是否成功发送
|
||||
"""
|
||||
if not is_initialized:
|
||||
return false
|
||||
|
||||
var success = private_chat_system.send_private_message(conversation_id, message)
|
||||
|
||||
if success and network_manager:
|
||||
# 发送网络消息
|
||||
var network_message = {
|
||||
"type": "private_message",
|
||||
"data": {
|
||||
"conversation_id": conversation_id,
|
||||
"message": message,
|
||||
"sender_id": current_user_id
|
||||
}
|
||||
}
|
||||
network_manager.send_message(network_message)
|
||||
|
||||
# 记录关系网络互动
|
||||
var conversation_info = private_chat_system.get_conversation_messages(conversation_id, 1)
|
||||
if conversation_info.size() > 0:
|
||||
# 这里需要获取对方ID,简化处理
|
||||
relationship_network.record_interaction(current_user_id, "other_user", "private_chat")
|
||||
|
||||
return success
|
||||
|
||||
## 创建社区事件
|
||||
func create_community_event(title: String, description: String, event_type: CommunityEventSystem.EventType, start_time: float = 0.0) -> String:
|
||||
"""
|
||||
创建社区事件
|
||||
@param title: 事件标题
|
||||
@param description: 事件描述
|
||||
@param event_type: 事件类型
|
||||
@param start_time: 开始时间
|
||||
@return: 事件ID
|
||||
"""
|
||||
if not is_initialized:
|
||||
return ""
|
||||
|
||||
var event_id = community_event_system.create_event(title, description, event_type, current_user_id, start_time)
|
||||
|
||||
if not event_id.is_empty() and network_manager:
|
||||
# 发送网络消息
|
||||
var message = {
|
||||
"type": "event_created",
|
||||
"data": {
|
||||
"event_id": event_id,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"event_type": event_type,
|
||||
"organizer_id": current_user_id,
|
||||
"start_time": start_time
|
||||
}
|
||||
}
|
||||
network_manager.send_message(message)
|
||||
|
||||
return event_id
|
||||
|
||||
## 加入社区事件
|
||||
func join_community_event(event_id: String) -> bool:
|
||||
"""
|
||||
加入社区事件
|
||||
@param event_id: 事件ID
|
||||
@return: 是否成功加入
|
||||
"""
|
||||
if not is_initialized:
|
||||
return false
|
||||
|
||||
var success = community_event_system.join_event(event_id, current_user_id)
|
||||
|
||||
if success and network_manager:
|
||||
# 发送网络消息
|
||||
var message = {
|
||||
"type": "event_join",
|
||||
"data": {
|
||||
"event_id": event_id,
|
||||
"participant_id": current_user_id
|
||||
}
|
||||
}
|
||||
network_manager.send_message(message)
|
||||
|
||||
return success
|
||||
|
||||
## 获取社交摘要
|
||||
func get_social_summary() -> Dictionary:
|
||||
"""
|
||||
获取社交活动摘要
|
||||
@return: 社交摘要数据
|
||||
"""
|
||||
if not is_initialized:
|
||||
return {}
|
||||
|
||||
var friends_list = friend_system.get_friends_list()
|
||||
var friend_requests = friend_system.get_friend_requests()
|
||||
var conversations = private_chat_system.get_conversations_list()
|
||||
var unread_messages = private_chat_system.get_total_unread_count()
|
||||
var user_events = community_event_system.get_user_events(current_user_id)
|
||||
var influence_ranking = relationship_network.get_influence_ranking(10)
|
||||
|
||||
return {
|
||||
"friends_count": friends_list.size(),
|
||||
"pending_requests": friend_requests.size(),
|
||||
"active_conversations": conversations.size(),
|
||||
"unread_messages": unread_messages,
|
||||
"upcoming_events": user_events.size(),
|
||||
"influence_rank": _get_user_influence_rank(influence_ranking),
|
||||
"recent_activity": _get_recent_social_activity()
|
||||
}
|
||||
|
||||
## 获取推荐内容
|
||||
func get_social_recommendations() -> Dictionary:
|
||||
"""
|
||||
获取社交推荐内容
|
||||
@return: 推荐内容数据
|
||||
"""
|
||||
if not is_initialized:
|
||||
return {}
|
||||
|
||||
var recommended_events = community_event_system.get_recommended_events(current_user_id, 5)
|
||||
var mutual_friends = _get_potential_friends()
|
||||
var active_conversations = private_chat_system.get_conversations_list()
|
||||
|
||||
# 过滤最近活跃的会话
|
||||
var recent_conversations = []
|
||||
for conv in active_conversations:
|
||||
if conv.unread_count > 0 or (Time.get_unix_time_from_system() - conv.last_activity) < 86400: # 24小时内
|
||||
recent_conversations.append(conv)
|
||||
|
||||
return {
|
||||
"recommended_events": recommended_events,
|
||||
"potential_friends": mutual_friends,
|
||||
"active_conversations": recent_conversations.slice(0, 5),
|
||||
"trending_topics": _get_trending_topics()
|
||||
}
|
||||
|
||||
## 处理角色上线
|
||||
func handle_character_online(character_id: String, character_name: String) -> void:
|
||||
"""
|
||||
处理角色上线事件
|
||||
@param character_id: 角色ID
|
||||
@param character_name: 角色名称
|
||||
"""
|
||||
if not is_initialized:
|
||||
return
|
||||
|
||||
# 检查是否为好友
|
||||
if friend_system.is_friend(character_id):
|
||||
friend_system.handle_friend_online(character_id)
|
||||
|
||||
# 发送通知
|
||||
social_notification.emit("friend_online", "好友上线", character_name + " 上线了", {"character_id": character_id})
|
||||
|
||||
## 处理角色下线
|
||||
func handle_character_offline(character_id: String, _character_name: String) -> void:
|
||||
"""
|
||||
处理角色下线事件
|
||||
@param character_id: 角色ID
|
||||
@param _character_name: 角色名称 (暂未使用)
|
||||
"""
|
||||
if not is_initialized:
|
||||
return
|
||||
|
||||
# 检查是否为好友
|
||||
if friend_system.is_friend(character_id):
|
||||
friend_system.handle_friend_offline(character_id)
|
||||
|
||||
## 记录社交互动
|
||||
func record_social_interaction(target_id: String, interaction_type: String, data: Dictionary = {}) -> void:
|
||||
"""
|
||||
记录社交互动
|
||||
@param target_id: 目标角色ID
|
||||
@param interaction_type: 互动类型
|
||||
@param data: 互动数据
|
||||
"""
|
||||
if not is_initialized:
|
||||
return
|
||||
|
||||
# 记录到关系网络
|
||||
relationship_network.record_interaction(current_user_id, target_id, interaction_type, data)
|
||||
|
||||
# 如果是好友,记录到好友系统
|
||||
if friend_system.is_friend(target_id):
|
||||
friend_system.record_interaction(target_id, interaction_type)
|
||||
|
||||
## 获取用户影响力排名
|
||||
func _get_user_influence_rank(ranking: Array[Dictionary]) -> int:
|
||||
"""
|
||||
获取用户在影响力排行榜中的排名
|
||||
@param ranking: 影响力排行数据
|
||||
@return: 排名(从1开始,0表示未上榜)
|
||||
"""
|
||||
for i in range(ranking.size()):
|
||||
if ranking[i].character_id == current_user_id:
|
||||
return i + 1
|
||||
return 0
|
||||
|
||||
## 获取潜在好友
|
||||
func _get_potential_friends() -> Array[Dictionary]:
|
||||
"""
|
||||
获取潜在好友推荐
|
||||
@return: 潜在好友数组
|
||||
"""
|
||||
var potential_friends = []
|
||||
var user_relationships = relationship_network.get_character_relationships(current_user_id)
|
||||
|
||||
# 基于共同好友推荐
|
||||
for relationship in user_relationships:
|
||||
if relationship.type == RelationshipNetwork.RelationshipType.FRIEND:
|
||||
var mutual_connections = relationship_network.find_mutual_connections(current_user_id, relationship.to_character)
|
||||
for connection in mutual_connections:
|
||||
if not friend_system.is_friend(connection) and not friend_system.is_blocked(connection):
|
||||
potential_friends.append({
|
||||
"character_id": connection,
|
||||
"reason": "共同好友: " + relationship.to_character,
|
||||
"mutual_friends": 1
|
||||
})
|
||||
|
||||
# 去重并限制数量
|
||||
var unique_friends = {}
|
||||
for friend in potential_friends:
|
||||
var id = friend.character_id
|
||||
if not unique_friends.has(id):
|
||||
unique_friends[id] = friend
|
||||
else:
|
||||
unique_friends[id].mutual_friends += 1
|
||||
|
||||
var result = unique_friends.values()
|
||||
result.sort_custom(func(a, b): return a.mutual_friends > b.mutual_friends)
|
||||
|
||||
return result.slice(0, 5)
|
||||
|
||||
## 获取最近社交活动
|
||||
func _get_recent_social_activity() -> Array[Dictionary]:
|
||||
"""
|
||||
获取最近的社交活动
|
||||
@return: 活动数组
|
||||
"""
|
||||
var activities = []
|
||||
|
||||
# 获取最近的好友互动
|
||||
var friends_list = friend_system.get_friends_list()
|
||||
for friend in friends_list:
|
||||
if (Time.get_unix_time_from_system() - friend.last_interaction) < 86400: # 24小时内
|
||||
activities.append({
|
||||
"type": "friend_interaction",
|
||||
"description": "与 " + friend.name + " 互动",
|
||||
"timestamp": friend.last_interaction
|
||||
})
|
||||
|
||||
# 获取最近的私聊
|
||||
var conversations = private_chat_system.get_conversations_list()
|
||||
for conv in conversations:
|
||||
if (Time.get_unix_time_from_system() - conv.last_activity) < 86400: # 24小时内
|
||||
activities.append({
|
||||
"type": "private_chat",
|
||||
"description": "与 " + conv.participants[0].name + " 私聊",
|
||||
"timestamp": conv.last_activity
|
||||
})
|
||||
|
||||
# 按时间排序
|
||||
activities.sort_custom(func(a, b): return a.timestamp > b.timestamp)
|
||||
|
||||
return activities.slice(0, 10)
|
||||
|
||||
## 获取热门话题
|
||||
func _get_trending_topics() -> Array[String]:
|
||||
"""
|
||||
获取热门话题
|
||||
@return: 话题数组
|
||||
"""
|
||||
# 这里可以基于最近的事件、对话等分析热门话题
|
||||
# 简化实现,返回一些示例话题
|
||||
return ["AI学习", "技术分享", "游戏开发", "社区建设", "创新项目"]
|
||||
|
||||
## 处理网络消息 - 好友请求
|
||||
func _handle_friend_request_message(data: Dictionary) -> void:
|
||||
"""处理好友请求网络消息"""
|
||||
var requester_id = data.get("requester_id", "")
|
||||
var requester_name = data.get("requester_name", "Unknown")
|
||||
|
||||
if not requester_id.is_empty():
|
||||
# 这里应该显示好友请求通知给用户
|
||||
social_notification.emit("friend_request", "好友请求", requester_name + " 想要添加您为好友", data)
|
||||
|
||||
## 处理网络消息 - 好友响应
|
||||
func _handle_friend_response_message(data: Dictionary) -> void:
|
||||
"""处理好友响应网络消息"""
|
||||
var accepted = data.get("accepted", false)
|
||||
var responder_name = data.get("responder_name", "Unknown")
|
||||
|
||||
if accepted:
|
||||
social_notification.emit("friend_accepted", "好友请求已接受", responder_name + " 接受了您的好友请求", data)
|
||||
else:
|
||||
social_notification.emit("friend_declined", "好友请求被拒绝", responder_name + " 拒绝了您的好友请求", data)
|
||||
|
||||
## 处理网络消息 - 私聊消息
|
||||
func _handle_private_message(data: Dictionary) -> void:
|
||||
"""处理私聊消息网络消息"""
|
||||
var conversation_id = data.get("conversation_id", "")
|
||||
var sender_id = data.get("sender_id", "")
|
||||
var message = data.get("message", "")
|
||||
|
||||
if not conversation_id.is_empty() and not sender_id.is_empty():
|
||||
private_chat_system.receive_private_message(conversation_id, sender_id, message)
|
||||
|
||||
## 处理网络消息 - 事件邀请
|
||||
func _handle_event_invitation(data: Dictionary) -> void:
|
||||
"""处理事件邀请网络消息"""
|
||||
var _event_id = data.get("event_id", "")
|
||||
var event_title = data.get("event_title", "")
|
||||
var inviter_name = data.get("inviter_name", "Unknown")
|
||||
|
||||
social_notification.emit("event_invitation", "活动邀请", inviter_name + " 邀请您参加 " + event_title, data)
|
||||
|
||||
## 处理网络消息 - 社交更新
|
||||
func _handle_social_update(data: Dictionary) -> void:
|
||||
"""处理社交更新网络消息"""
|
||||
var update_type = data.get("update_type", "")
|
||||
|
||||
match update_type:
|
||||
"character_online":
|
||||
handle_character_online(data.get("character_id", ""), data.get("character_name", ""))
|
||||
"character_offline":
|
||||
handle_character_offline(data.get("character_id", ""), data.get("character_name", ""))
|
||||
|
||||
# 信号处理函数
|
||||
func _on_friend_request_sent(friend_id: String, friend_name: String):
|
||||
friend_activity.emit("friend_request_sent", {"friend_id": friend_id, "friend_name": friend_name})
|
||||
|
||||
# func _on_friend_request_received(requester_id: String, requester_name: String):
|
||||
# social_notification.emit("friend_request", "新的好友请求", requester_name + " 想要添加您为好友", {"requester_id": requester_id})
|
||||
|
||||
func _on_friend_request_accepted(friend_id: String, friend_name: String):
|
||||
friend_activity.emit("friend_accepted", {"friend_id": friend_id, "friend_name": friend_name})
|
||||
social_notification.emit("friend_accepted", "好友请求已接受", "您与 " + friend_name + " 现在是好友了", {"friend_id": friend_id})
|
||||
|
||||
func _on_friend_removed(friend_id: String, friend_name: String):
|
||||
friend_activity.emit("friend_removed", {"friend_id": friend_id, "friend_name": friend_name})
|
||||
|
||||
func _on_friend_online(friend_id: String, friend_name: String):
|
||||
friend_activity.emit("friend_online", {"friend_id": friend_id, "friend_name": friend_name})
|
||||
|
||||
func _on_friend_offline(friend_id: String, friend_name: String):
|
||||
friend_activity.emit("friend_offline", {"friend_id": friend_id, "friend_name": friend_name})
|
||||
|
||||
func _on_relationship_level_changed(friend_id: String, old_level: int, new_level: int):
|
||||
if new_level > old_level:
|
||||
var friend_info = friend_system.get_friend_info(friend_id)
|
||||
var friend_name = friend_info.get("name", "Unknown")
|
||||
social_notification.emit("relationship_improved", "关系提升", "您与 " + friend_name + " 的关系等级提升了", {"friend_id": friend_id, "new_level": new_level})
|
||||
|
||||
func _on_conversation_created(conversation_id: String, participants: Array[String]):
|
||||
friend_activity.emit("conversation_created", {"conversation_id": conversation_id, "participants": participants})
|
||||
|
||||
func _on_private_message_received(_conversation_id: String, _sender_id: String, _message: String):
|
||||
# 这里可以显示消息通知
|
||||
pass
|
||||
|
||||
func _on_unread_count_changed(_conversation_id: String, count: int):
|
||||
if count > 0:
|
||||
# 更新UI显示未读消息数量
|
||||
pass
|
||||
|
||||
func _on_relationship_created(from_character: String, to_character: String, relationship_type: RelationshipNetwork.RelationshipType):
|
||||
friend_activity.emit("relationship_created", {"from": from_character, "to": to_character, "type": relationship_type})
|
||||
|
||||
func _on_relationship_updated(from_character: String, to_character: String, old_strength: float, new_strength: float):
|
||||
friend_activity.emit("relationship_updated", {"from": from_character, "to": to_character, "strength_change": new_strength - old_strength})
|
||||
|
||||
func _on_influence_score_changed(character_id: String, old_score: float, new_score: float):
|
||||
if character_id == current_user_id and new_score > old_score:
|
||||
social_notification.emit("influence_increased", "影响力提升", "您的社区影响力增加了", {"score_change": new_score - old_score})
|
||||
|
||||
func _on_event_created(event_id: String, title: String, organizer_id: String):
|
||||
friend_activity.emit("event_created", {"event_id": event_id, "title": title, "organizer_id": organizer_id})
|
||||
|
||||
func _on_participant_joined(event_id: String, participant_id: String):
|
||||
friend_activity.emit("event_joined", {"event_id": event_id, "participant_id": participant_id})
|
||||
|
||||
func _on_event_started(event_id: String, title: String):
|
||||
social_notification.emit("event_started", "活动开始", title + " 活动现在开始了", {"event_id": event_id})
|
||||
|
||||
func _on_event_completed(event_id: String, title: String, participants: Array[String]):
|
||||
if current_user_id in participants:
|
||||
social_notification.emit("event_completed", "活动完成", "您参与的 " + title + " 活动已完成", {"event_id": event_id})
|
||||
|
||||
func _on_milestone_achieved(milestone_type: String, data: Dictionary):
|
||||
social_notification.emit("milestone", "里程碑达成", "社区达成了新的里程碑: " + milestone_type, data)
|
||||
|
||||
## 获取统计信息
|
||||
func get_statistics() -> Dictionary:
|
||||
"""
|
||||
获取社交系统统计信息
|
||||
@return: 统计信息字典
|
||||
"""
|
||||
if not is_initialized:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"friend_system": friend_system.get_statistics(),
|
||||
"private_chat": private_chat_system.get_statistics(),
|
||||
"relationship_network": relationship_network.get_statistics(),
|
||||
"community_events": community_event_system.get_statistics()
|
||||
}
|
||||
1
scripts/SocialManager.gd.uid
Normal file
1
scripts/SocialManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bx0o7146y1hm3
|
||||
93
scripts/TestEnhancedDialogue.gd
Normal file
93
scripts/TestEnhancedDialogue.gd
Normal file
@@ -0,0 +1,93 @@
|
||||
extends Control
|
||||
## 增强对话系统测试脚本
|
||||
## 用于独立测试增强对话功能
|
||||
|
||||
var enhanced_dialogue_box: EnhancedDialogueBox
|
||||
|
||||
func _ready():
|
||||
"""初始化测试场景"""
|
||||
print("Starting Enhanced Dialogue Test")
|
||||
|
||||
# 创建增强对话框
|
||||
enhanced_dialogue_box = EnhancedDialogueBox.new()
|
||||
add_child(enhanced_dialogue_box)
|
||||
|
||||
# 设置对话框位置和大小
|
||||
enhanced_dialogue_box.position = Vector2(50, 50)
|
||||
enhanced_dialogue_box.size = Vector2(500, 400)
|
||||
|
||||
# 等待一帧后开始测试
|
||||
await get_tree().process_frame
|
||||
start_test()
|
||||
|
||||
func start_test():
|
||||
"""开始测试"""
|
||||
print("Enhanced Dialogue Box created successfully")
|
||||
|
||||
# 测试开始对话
|
||||
enhanced_dialogue_box.start_dialogue("test_character", "测试角色")
|
||||
|
||||
# 测试发送消息
|
||||
test_basic_functionality()
|
||||
|
||||
func test_basic_functionality():
|
||||
"""测试基础功能"""
|
||||
print("Testing basic functionality...")
|
||||
|
||||
# 测试表情符号转换
|
||||
test_emoji_conversion()
|
||||
|
||||
# 测试消息过滤
|
||||
test_message_filtering()
|
||||
|
||||
print("Basic functionality test completed")
|
||||
|
||||
func test_emoji_conversion():
|
||||
"""测试表情符号转换"""
|
||||
print("Testing emoji conversion...")
|
||||
|
||||
var test_messages = [
|
||||
":)",
|
||||
":D",
|
||||
":fire:",
|
||||
"Hello :smile: world!"
|
||||
]
|
||||
|
||||
for message in test_messages:
|
||||
var converted = EmojiManager.convert_text_to_emoji(message)
|
||||
print("Original: '%s' -> Converted: '%s'" % [message, converted])
|
||||
|
||||
func test_message_filtering():
|
||||
"""测试消息过滤"""
|
||||
print("Testing message filtering...")
|
||||
|
||||
if not enhanced_dialogue_box.dialogue_filter:
|
||||
print("Dialogue filter not initialized yet, waiting...")
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
|
||||
if enhanced_dialogue_box.dialogue_filter:
|
||||
var test_messages = [
|
||||
"", # 空消息
|
||||
"Hello world", # 正常消息
|
||||
"垃圾信息", # 包含违禁词
|
||||
]
|
||||
|
||||
for message in test_messages:
|
||||
var result = enhanced_dialogue_box.dialogue_filter.filter_message("test_user", message)
|
||||
print("Message: '%s' -> Allowed: %s, Filtered: '%s'" % [message, result.allowed, result.get("filtered_message", "")])
|
||||
else:
|
||||
print("Dialogue filter still not available")
|
||||
|
||||
func _input(event):
|
||||
"""处理输入事件"""
|
||||
if event is InputEventKey and event.pressed:
|
||||
match event.keycode:
|
||||
KEY_T:
|
||||
print("Testing emoji conversion...")
|
||||
test_emoji_conversion()
|
||||
KEY_F:
|
||||
print("Testing message filtering...")
|
||||
test_message_filtering()
|
||||
KEY_Q:
|
||||
print("Quitting test...")
|
||||
get_tree().quit()
|
||||
1
scripts/TestEnhancedDialogue.gd.uid
Normal file
1
scripts/TestEnhancedDialogue.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cxidptngh58jl
|
||||
81
scripts/TestGameplay.gd
Normal file
81
scripts/TestGameplay.gd
Normal file
@@ -0,0 +1,81 @@
|
||||
extends Node
|
||||
## 临时测试场景脚本
|
||||
## 用于在没有服务器的情况下测试游戏玩法
|
||||
|
||||
@onready var office = $DatawhaleOffice
|
||||
@onready var player = $DatawhaleOffice/Characters/PlayerCharacter
|
||||
|
||||
var input_handler: Node = null
|
||||
|
||||
func _ready():
|
||||
print("=== Test Gameplay Scene ===")
|
||||
print("Controls:")
|
||||
print(" WASD / Arrow Keys - Move player")
|
||||
print(" E - Interact")
|
||||
print(" ESC - Quit")
|
||||
print("========================")
|
||||
|
||||
# 初始化玩家
|
||||
_setup_player()
|
||||
|
||||
# 创建输入处理器
|
||||
_setup_input()
|
||||
|
||||
# 设置相机跟随
|
||||
if office.has_method("set_camera_target"):
|
||||
office.set_camera_target(player)
|
||||
print("Camera following player")
|
||||
|
||||
func _setup_player():
|
||||
"""设置玩家角色"""
|
||||
if player:
|
||||
# 设置玩家数据
|
||||
var player_data = {
|
||||
"id": "test_player",
|
||||
"name": "Test Player",
|
||||
"position": {"x": 1000, "y": 750},
|
||||
"is_online": true
|
||||
}
|
||||
player.initialize(player_data)
|
||||
|
||||
# 设置随机颜色
|
||||
var sprite = player.get_node_or_null("CharacterSprite")
|
||||
if sprite and sprite.has_method("set_character_color"):
|
||||
var random_color = CharacterSprite.generate_random_color()
|
||||
sprite.set_character_color(random_color)
|
||||
|
||||
print("Player initialized at position: ", player.global_position)
|
||||
|
||||
func _setup_input():
|
||||
"""设置输入处理"""
|
||||
input_handler = preload("res://scripts/InputHandler.gd").new()
|
||||
input_handler.name = "InputHandler"
|
||||
add_child(input_handler)
|
||||
|
||||
# 连接输入信号
|
||||
input_handler.move_input.connect(_on_move_input)
|
||||
input_handler.interact_input.connect(_on_interact_input)
|
||||
|
||||
print("Input handler ready")
|
||||
|
||||
func _on_move_input(direction: Vector2):
|
||||
"""处理移动输入"""
|
||||
if player:
|
||||
player.move_to(direction)
|
||||
|
||||
func _on_interact_input():
|
||||
"""处理交互输入"""
|
||||
print("Interact pressed!")
|
||||
# 可以在这里添加交互逻辑
|
||||
|
||||
func _input(event):
|
||||
"""处理输入事件"""
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
print("Quitting test scene...")
|
||||
get_tree().quit()
|
||||
|
||||
func _process(_delta):
|
||||
"""每帧更新"""
|
||||
# 显示玩家位置(用于调试)
|
||||
if player and Input.is_action_just_pressed("ui_select"):
|
||||
print("Player position: ", player.global_position)
|
||||
1
scripts/TestGameplay.gd.uid
Normal file
1
scripts/TestGameplay.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://t3ybfpqkqwp8
|
||||
271
scripts/TouchFeedbackManager.gd
Normal file
271
scripts/TouchFeedbackManager.gd
Normal file
@@ -0,0 +1,271 @@
|
||||
extends Node
|
||||
## 触摸反馈管理器
|
||||
## 为移动端提供触摸反馈和优化的触摸体验
|
||||
|
||||
# 触摸反馈配置
|
||||
const HAPTIC_LIGHT = 0.3
|
||||
const HAPTIC_MEDIUM = 0.6
|
||||
const HAPTIC_HEAVY = 1.0
|
||||
|
||||
const TOUCH_SCALE_FACTOR = 0.95
|
||||
const TOUCH_FEEDBACK_DURATION = 0.1
|
||||
|
||||
# 触摸区域扩展(像素)
|
||||
const TOUCH_AREA_EXPANSION = 20
|
||||
|
||||
## 为按钮添加触摸反馈
|
||||
func setup_button_feedback(button: Button) -> void:
|
||||
"""
|
||||
为按钮设置触摸反馈
|
||||
@param button: 按钮节点
|
||||
"""
|
||||
if not button:
|
||||
return
|
||||
|
||||
# 扩展触摸区域
|
||||
_expand_touch_area(button)
|
||||
|
||||
# 连接触摸事件
|
||||
if not button.button_down.is_connected(_on_button_pressed):
|
||||
button.button_down.connect(_on_button_pressed.bind(button))
|
||||
if not button.button_up.is_connected(_on_button_released):
|
||||
button.button_up.connect(_on_button_released.bind(button))
|
||||
|
||||
## 为输入框添加触摸反馈
|
||||
func setup_input_feedback(line_edit: LineEdit) -> void:
|
||||
"""
|
||||
为输入框设置触摸反馈
|
||||
@param line_edit: 输入框节点
|
||||
"""
|
||||
if not line_edit:
|
||||
return
|
||||
|
||||
# 扩展触摸区域
|
||||
_expand_touch_area(line_edit)
|
||||
|
||||
# 连接焦点事件
|
||||
if not line_edit.focus_entered.is_connected(_on_input_focused):
|
||||
line_edit.focus_entered.connect(_on_input_focused.bind(line_edit))
|
||||
|
||||
## 扩展控件的触摸区域
|
||||
func _expand_touch_area(control: Control) -> void:
|
||||
"""
|
||||
扩展控件的触摸区域,使其更容易点击
|
||||
@param control: 控件节点
|
||||
"""
|
||||
if not control:
|
||||
return
|
||||
|
||||
# 只在移动设备上扩展触摸区域
|
||||
if not DisplayServer.is_touchscreen_available():
|
||||
return
|
||||
|
||||
# 增加最小尺寸以扩展触摸区域
|
||||
var current_size = control.custom_minimum_size
|
||||
var expanded_size = Vector2(
|
||||
max(current_size.x, control.size.x + TOUCH_AREA_EXPANSION),
|
||||
max(current_size.y, control.size.y + TOUCH_AREA_EXPANSION)
|
||||
)
|
||||
|
||||
control.custom_minimum_size = expanded_size
|
||||
|
||||
## 按钮按下反馈
|
||||
func _on_button_pressed(button: Button) -> void:
|
||||
"""
|
||||
按钮按下时的反馈
|
||||
@param button: 按钮节点
|
||||
"""
|
||||
# 视觉反馈:缩放效果
|
||||
UIAnimationManager.button_press_feedback(button, TOUCH_SCALE_FACTOR, TOUCH_FEEDBACK_DURATION)
|
||||
|
||||
# 触觉反馈(如果支持)
|
||||
_trigger_haptic_feedback(HAPTIC_LIGHT)
|
||||
|
||||
# 音效反馈(可选)
|
||||
_play_touch_sound("button_press")
|
||||
|
||||
## 按钮释放反馈
|
||||
func _on_button_released(button: Button) -> void:
|
||||
"""
|
||||
按钮释放时的反馈
|
||||
@param button: 按钮节点
|
||||
"""
|
||||
# 确保按钮恢复原始大小
|
||||
if button:
|
||||
var tween = button.create_tween()
|
||||
tween.tween_property(button, "scale", Vector2.ONE, TOUCH_FEEDBACK_DURATION)
|
||||
|
||||
## 输入框获得焦点反馈
|
||||
func _on_input_focused(line_edit: LineEdit) -> void:
|
||||
"""
|
||||
输入框获得焦点时的反馈
|
||||
@param line_edit: 输入框节点
|
||||
"""
|
||||
# 轻微的脉冲效果
|
||||
UIAnimationManager.pulse_highlight(line_edit, 1.05, 0.4)
|
||||
|
||||
# 触觉反馈
|
||||
_trigger_haptic_feedback(HAPTIC_LIGHT)
|
||||
|
||||
# 音效反馈
|
||||
_play_touch_sound("input_focus")
|
||||
|
||||
## 触发触觉反馈
|
||||
func _trigger_haptic_feedback(intensity: float) -> void:
|
||||
"""
|
||||
触发触觉反馈(震动)
|
||||
@param intensity: 震动强度
|
||||
"""
|
||||
# 检查是否支持触觉反馈
|
||||
if not DisplayServer.is_touchscreen_available():
|
||||
return
|
||||
|
||||
# 在支持的平台上触发震动
|
||||
if OS.get_name() == "Android":
|
||||
# Android震动 - 在Godot 4中使用Input.vibrate_handheld
|
||||
var duration_ms = int(intensity * 100) # 转换为毫秒
|
||||
Input.vibrate_handheld(duration_ms)
|
||||
elif OS.get_name() == "iOS":
|
||||
# iOS触觉反馈 - 在Godot 4中也使用Input.vibrate_handheld
|
||||
var duration_ms = int(intensity * 100)
|
||||
Input.vibrate_handheld(duration_ms)
|
||||
|
||||
## 播放触摸音效
|
||||
func _play_touch_sound(_sound_name: String) -> void:
|
||||
"""
|
||||
播放触摸音效
|
||||
@param _sound_name: 音效名称(暂未使用,预留给未来的音效系统)
|
||||
"""
|
||||
# 检查是否启用音效
|
||||
if not preload("res://scripts/GameConfig.gd").get_ui_config().get("enable_sound_effects", true):
|
||||
return
|
||||
|
||||
# 这里可以添加音效播放逻辑
|
||||
# 例如:AudioManager.play_ui_sound(_sound_name)
|
||||
pass
|
||||
|
||||
## 创建触摸友好的按钮
|
||||
func create_touch_friendly_button(text: String, size: Vector2 = Vector2(200, 60)) -> Button:
|
||||
"""
|
||||
创建触摸友好的按钮
|
||||
@param text: 按钮文本
|
||||
@param size: 按钮大小
|
||||
@return: 配置好的按钮
|
||||
"""
|
||||
var button = Button.new()
|
||||
button.text = text
|
||||
button.custom_minimum_size = size
|
||||
|
||||
# 设置触摸友好的样式
|
||||
_apply_touch_friendly_style(button)
|
||||
|
||||
# 添加触摸反馈
|
||||
setup_button_feedback(button)
|
||||
|
||||
return button
|
||||
|
||||
## 创建触摸友好的输入框
|
||||
func create_touch_friendly_input(placeholder: String = "", size: Vector2 = Vector2(300, 50)) -> LineEdit:
|
||||
"""
|
||||
创建触摸友好的输入框
|
||||
@param placeholder: 占位符文本
|
||||
@param size: 输入框大小
|
||||
@return: 配置好的输入框
|
||||
"""
|
||||
var input = LineEdit.new()
|
||||
input.placeholder_text = placeholder
|
||||
input.custom_minimum_size = size
|
||||
|
||||
# 设置触摸友好的样式
|
||||
_apply_touch_friendly_style(input)
|
||||
|
||||
# 添加触摸反馈
|
||||
setup_input_feedback(input)
|
||||
|
||||
return input
|
||||
|
||||
## 应用触摸友好的样式
|
||||
func _apply_touch_friendly_style(control: Control) -> void:
|
||||
"""
|
||||
为控件应用触摸友好的样式
|
||||
@param control: 控件节点
|
||||
"""
|
||||
if not control:
|
||||
return
|
||||
|
||||
# 只在移动设备上应用特殊样式
|
||||
if not DisplayServer.is_touchscreen_available():
|
||||
return
|
||||
|
||||
# 增加字体大小以便触摸设备阅读
|
||||
if control.has_method("add_theme_font_size_override"):
|
||||
control.add_theme_font_size_override("font_size", 18)
|
||||
|
||||
# 为按钮添加更明显的边框
|
||||
if control is Button:
|
||||
var style_box = StyleBoxFlat.new()
|
||||
style_box.bg_color = Color(0.2, 0.4, 0.8, 0.8)
|
||||
style_box.border_width_left = 2
|
||||
style_box.border_width_right = 2
|
||||
style_box.border_width_top = 2
|
||||
style_box.border_width_bottom = 2
|
||||
style_box.border_color = Color(0.3, 0.5, 0.9, 1.0)
|
||||
style_box.corner_radius_top_left = 8
|
||||
style_box.corner_radius_top_right = 8
|
||||
style_box.corner_radius_bottom_left = 8
|
||||
style_box.corner_radius_bottom_right = 8
|
||||
|
||||
control.add_theme_stylebox_override("normal", style_box)
|
||||
|
||||
# 按下状态的样式
|
||||
var pressed_style = style_box.duplicate()
|
||||
pressed_style.bg_color = Color(0.15, 0.3, 0.6, 0.9)
|
||||
control.add_theme_stylebox_override("pressed", pressed_style)
|
||||
|
||||
## 检查是否为移动设备
|
||||
func is_mobile_device() -> bool:
|
||||
"""
|
||||
检查当前是否为移动设备
|
||||
@return: 是否为移动设备
|
||||
"""
|
||||
var os_name = OS.get_name()
|
||||
return os_name in ["Android", "iOS"] or DisplayServer.is_touchscreen_available()
|
||||
|
||||
## 获取推荐的触摸目标大小
|
||||
func get_recommended_touch_size() -> Vector2:
|
||||
"""
|
||||
获取推荐的触摸目标大小
|
||||
@return: 推荐的最小触摸大小
|
||||
"""
|
||||
if is_mobile_device():
|
||||
return Vector2(60, 60) # 移动设备推荐最小44-60像素
|
||||
else:
|
||||
return Vector2(40, 40) # 桌面设备可以更小
|
||||
|
||||
## 自动优化界面的触摸体验
|
||||
func optimize_ui_for_touch(root_node: Control) -> void:
|
||||
"""
|
||||
自动优化界面的触摸体验
|
||||
@param root_node: 根节点
|
||||
"""
|
||||
if not is_mobile_device():
|
||||
return
|
||||
|
||||
_optimize_node_recursive(root_node)
|
||||
|
||||
## 递归优化节点
|
||||
func _optimize_node_recursive(node: Node) -> void:
|
||||
"""
|
||||
递归优化节点的触摸体验
|
||||
@param node: 节点
|
||||
"""
|
||||
if node is Button:
|
||||
setup_button_feedback(node as Button)
|
||||
_apply_touch_friendly_style(node as Control)
|
||||
elif node is LineEdit:
|
||||
setup_input_feedback(node as LineEdit)
|
||||
_apply_touch_friendly_style(node as Control)
|
||||
|
||||
# 递归处理子节点
|
||||
for child in node.get_children():
|
||||
_optimize_node_recursive(child)
|
||||
1
scripts/TouchFeedbackManager.gd.uid
Normal file
1
scripts/TouchFeedbackManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dlqwi3cmfwmd2
|
||||
348
scripts/UIAnimationManager.gd
Normal file
348
scripts/UIAnimationManager.gd
Normal file
@@ -0,0 +1,348 @@
|
||||
extends Node
|
||||
## UI动画管理器
|
||||
## 统一管理UI动画效果,提升用户体验
|
||||
|
||||
# 动画配置
|
||||
const FADE_DURATION = 0.25
|
||||
const SLIDE_DURATION = 0.3
|
||||
const SCALE_DURATION = 0.2
|
||||
const BOUNCE_DURATION = 0.4
|
||||
|
||||
# 缓动类型
|
||||
enum EaseType {
|
||||
EASE_IN,
|
||||
EASE_OUT,
|
||||
EASE_IN_OUT,
|
||||
BOUNCE,
|
||||
ELASTIC
|
||||
}
|
||||
|
||||
## 淡入动画
|
||||
func fade_in(node: Control, duration: float = FADE_DURATION, ease_type: EaseType = EaseType.EASE_OUT) -> Tween:
|
||||
"""
|
||||
淡入动画
|
||||
@param node: 要动画的节点
|
||||
@param duration: 动画时长
|
||||
@param ease_type: 缓动类型
|
||||
@return: Tween对象
|
||||
"""
|
||||
if not node:
|
||||
return null
|
||||
|
||||
node.modulate.a = 0.0
|
||||
node.show()
|
||||
|
||||
var tween = node.create_tween()
|
||||
tween.set_ease(_get_tween_ease(ease_type))
|
||||
tween.tween_property(node, "modulate:a", 1.0, duration)
|
||||
|
||||
return tween
|
||||
|
||||
## 淡出动画
|
||||
func fade_out(node: Control, duration: float = FADE_DURATION, ease_type: EaseType = EaseType.EASE_IN) -> Tween:
|
||||
"""
|
||||
淡出动画
|
||||
@param node: 要动画的节点
|
||||
@param duration: 动画时长
|
||||
@param ease_type: 缓动类型
|
||||
@return: Tween对象
|
||||
"""
|
||||
if not node:
|
||||
return null
|
||||
|
||||
var tween = node.create_tween()
|
||||
tween.set_ease(_get_tween_ease(ease_type))
|
||||
tween.tween_property(node, "modulate:a", 0.0, duration)
|
||||
tween.tween_callback(func(): node.hide())
|
||||
|
||||
return tween
|
||||
|
||||
## 滑入动画(从左侧)
|
||||
func slide_in_left(node: Control, duration: float = SLIDE_DURATION, ease_type: EaseType = EaseType.EASE_OUT) -> Tween:
|
||||
"""
|
||||
从左侧滑入动画
|
||||
@param node: 要动画的节点
|
||||
@param duration: 动画时长
|
||||
@param ease_type: 缓动类型
|
||||
@return: Tween对象
|
||||
"""
|
||||
if not node:
|
||||
return null
|
||||
|
||||
var original_pos = node.position
|
||||
node.position.x = -node.size.x
|
||||
node.show()
|
||||
|
||||
var tween = node.create_tween()
|
||||
tween.set_ease(_get_tween_ease(ease_type))
|
||||
tween.tween_property(node, "position", original_pos, duration)
|
||||
|
||||
return tween
|
||||
|
||||
## 滑入动画(从右侧)
|
||||
func slide_in_right(node: Control, duration: float = SLIDE_DURATION, ease_type: EaseType = EaseType.EASE_OUT) -> Tween:
|
||||
"""
|
||||
从右侧滑入动画
|
||||
@param node: 要动画的节点
|
||||
@param duration: 动画时长
|
||||
@param ease_type: 缓动类型
|
||||
@return: Tween对象
|
||||
"""
|
||||
if not node:
|
||||
return null
|
||||
|
||||
var original_pos = node.position
|
||||
var viewport_width = node.get_viewport().get_visible_rect().size.x
|
||||
node.position.x = viewport_width
|
||||
node.show()
|
||||
|
||||
var tween = node.create_tween()
|
||||
tween.set_ease(_get_tween_ease(ease_type))
|
||||
tween.tween_property(node, "position", original_pos, duration)
|
||||
|
||||
return tween
|
||||
|
||||
## 滑入动画(从上方)
|
||||
func slide_in_top(node: Control, duration: float = SLIDE_DURATION, ease_type: EaseType = EaseType.EASE_OUT) -> Tween:
|
||||
"""
|
||||
从上方滑入动画
|
||||
@param node: 要动画的节点
|
||||
@param duration: 动画时长
|
||||
@param ease_type: 缓动类型
|
||||
@return: Tween对象
|
||||
"""
|
||||
if not node:
|
||||
return null
|
||||
|
||||
var original_pos = node.position
|
||||
node.position.y = -node.size.y
|
||||
node.show()
|
||||
|
||||
var tween = node.create_tween()
|
||||
tween.set_ease(_get_tween_ease(ease_type))
|
||||
tween.tween_property(node, "position", original_pos, duration)
|
||||
|
||||
return tween
|
||||
|
||||
## 滑入动画(从下方)
|
||||
func slide_in_bottom(node: Control, duration: float = SLIDE_DURATION, ease_type: EaseType = EaseType.EASE_OUT) -> Tween:
|
||||
"""
|
||||
从下方滑入动画
|
||||
@param node: 要动画的节点
|
||||
@param duration: 动画时长
|
||||
@param ease_type: 缓动类型
|
||||
@return: Tween对象
|
||||
"""
|
||||
if not node:
|
||||
return null
|
||||
|
||||
var original_pos = node.position
|
||||
var viewport_height = node.get_viewport().get_visible_rect().size.y
|
||||
node.position.y = viewport_height
|
||||
node.show()
|
||||
|
||||
var tween = node.create_tween()
|
||||
tween.set_ease(_get_tween_ease(ease_type))
|
||||
tween.tween_property(node, "position", original_pos, duration)
|
||||
|
||||
return tween
|
||||
|
||||
## 缩放动画(弹出效果)
|
||||
func scale_popup(node: Control, duration: float = SCALE_DURATION, ease_type: EaseType = EaseType.BOUNCE) -> Tween:
|
||||
"""
|
||||
缩放弹出动画
|
||||
@param node: 要动画的节点
|
||||
@param duration: 动画时长
|
||||
@param ease_type: 缓动类型
|
||||
@return: Tween对象
|
||||
"""
|
||||
if not node:
|
||||
return null
|
||||
|
||||
node.scale = Vector2.ZERO
|
||||
node.show()
|
||||
|
||||
var tween = node.create_tween()
|
||||
tween.set_ease(_get_tween_ease(ease_type))
|
||||
tween.tween_property(node, "scale", Vector2.ONE, duration)
|
||||
|
||||
return tween
|
||||
|
||||
## 按钮点击反馈动画
|
||||
func button_press_feedback(button: Button, scale_factor: float = 0.95, duration: float = 0.1) -> Tween:
|
||||
"""
|
||||
按钮点击反馈动画
|
||||
@param button: 按钮节点
|
||||
@param scale_factor: 缩放因子
|
||||
@param duration: 动画时长
|
||||
@return: Tween对象
|
||||
"""
|
||||
if not button:
|
||||
return null
|
||||
|
||||
var tween = button.create_tween()
|
||||
tween.tween_property(button, "scale", Vector2.ONE * scale_factor, duration)
|
||||
tween.tween_property(button, "scale", Vector2.ONE, duration)
|
||||
|
||||
return tween
|
||||
|
||||
## 摇摆动画(错误提示)
|
||||
func shake_error(node: Control, intensity: float = 10.0, duration: float = 0.5) -> Tween:
|
||||
"""
|
||||
摇摆动画(用于错误提示)
|
||||
@param node: 要动画的节点
|
||||
@param intensity: 摇摆强度
|
||||
@param duration: 动画时长
|
||||
@return: Tween对象
|
||||
"""
|
||||
if not node:
|
||||
return null
|
||||
|
||||
var original_pos = node.position
|
||||
var tween = node.create_tween()
|
||||
|
||||
# 创建摇摆效果
|
||||
for i in range(4):
|
||||
var offset = Vector2(randf_range(-intensity, intensity), 0)
|
||||
tween.tween_property(node, "position", original_pos + offset, duration / 8)
|
||||
|
||||
tween.tween_property(node, "position", original_pos, duration / 8)
|
||||
|
||||
return tween
|
||||
|
||||
## 脉冲动画(强调效果)
|
||||
func pulse_highlight(node: Control, scale_max: float = 1.1, duration: float = 0.6) -> Tween:
|
||||
"""
|
||||
脉冲动画(强调效果)
|
||||
@param node: 要动画的节点
|
||||
@param scale_max: 最大缩放
|
||||
@param duration: 动画时长
|
||||
@return: Tween对象
|
||||
"""
|
||||
if not node:
|
||||
return null
|
||||
|
||||
var tween = node.create_tween()
|
||||
tween.set_loops() # 无限循环
|
||||
tween.tween_property(node, "scale", Vector2.ONE * scale_max, duration / 2)
|
||||
tween.tween_property(node, "scale", Vector2.ONE, duration / 2)
|
||||
|
||||
return tween
|
||||
|
||||
## 停止脉冲动画
|
||||
func stop_pulse(node: Control) -> void:
|
||||
"""
|
||||
停止脉冲动画并恢复原始缩放
|
||||
@param node: 节点
|
||||
"""
|
||||
if not node:
|
||||
return
|
||||
|
||||
# 停止所有tween
|
||||
node.get_tree().call_group("tween", "kill")
|
||||
|
||||
# 恢复原始缩放
|
||||
var tween = node.create_tween()
|
||||
tween.tween_property(node, "scale", Vector2.ONE, 0.2)
|
||||
|
||||
## 组合动画:淡入+滑入
|
||||
func fade_slide_in(node: Control, direction: String = "bottom", duration: float = SLIDE_DURATION) -> Tween:
|
||||
"""
|
||||
组合动画:淡入+滑入
|
||||
@param node: 要动画的节点
|
||||
@param direction: 滑入方向("left", "right", "top", "bottom")
|
||||
@param duration: 动画时长
|
||||
@return: Tween对象
|
||||
"""
|
||||
if not node:
|
||||
return null
|
||||
|
||||
# 设置初始状态
|
||||
node.modulate.a = 0.0
|
||||
var original_pos = node.position
|
||||
|
||||
match direction:
|
||||
"left":
|
||||
node.position.x = -node.size.x
|
||||
"right":
|
||||
node.position.x = node.get_viewport().get_visible_rect().size.x
|
||||
"top":
|
||||
node.position.y = -node.size.y
|
||||
"bottom":
|
||||
node.position.y = node.get_viewport().get_visible_rect().size.y
|
||||
|
||||
node.show()
|
||||
|
||||
# 创建并行动画
|
||||
var tween = node.create_tween()
|
||||
tween.set_parallel(true)
|
||||
tween.tween_property(node, "modulate:a", 1.0, duration)
|
||||
tween.tween_property(node, "position", original_pos, duration)
|
||||
|
||||
return tween
|
||||
|
||||
## 获取Tween缓动类型
|
||||
func _get_tween_ease(ease_type: EaseType) -> Tween.EaseType:
|
||||
"""
|
||||
将自定义缓动类型转换为Tween缓动类型
|
||||
@param ease_type: 自定义缓动类型
|
||||
@return: Tween缓动类型
|
||||
"""
|
||||
match ease_type:
|
||||
EaseType.EASE_IN:
|
||||
return Tween.EASE_IN
|
||||
EaseType.EASE_OUT:
|
||||
return Tween.EASE_OUT
|
||||
EaseType.EASE_IN_OUT:
|
||||
return Tween.EASE_IN_OUT
|
||||
EaseType.BOUNCE:
|
||||
return Tween.EASE_OUT # Godot没有内置bounce,使用ease_out
|
||||
EaseType.ELASTIC:
|
||||
return Tween.EASE_OUT # Godot没有内置elastic,使用ease_out
|
||||
_:
|
||||
return Tween.EASE_OUT
|
||||
|
||||
## 创建加载动画
|
||||
func create_loading_spinner(parent: Control, size: Vector2 = Vector2(32, 32)) -> Control:
|
||||
"""
|
||||
创建加载旋转动画
|
||||
@param parent: 父节点
|
||||
@param size: 大小
|
||||
@return: 旋转节点
|
||||
"""
|
||||
var spinner = ColorRect.new()
|
||||
spinner.size = size
|
||||
spinner.color = Color.WHITE
|
||||
|
||||
# 创建简单的旋转图形
|
||||
var style = StyleBoxFlat.new()
|
||||
style.bg_color = Color.TRANSPARENT
|
||||
style.border_width_top = 3
|
||||
style.border_width_right = 1
|
||||
style.border_width_bottom = 1
|
||||
style.border_width_left = 1
|
||||
style.border_color = Color.WHITE
|
||||
style.corner_radius_top_left = size.x / 2
|
||||
style.corner_radius_top_right = size.x / 2
|
||||
style.corner_radius_bottom_left = size.x / 2
|
||||
style.corner_radius_bottom_right = size.x / 2
|
||||
|
||||
spinner.add_theme_stylebox_override("panel", style)
|
||||
|
||||
parent.add_child(spinner)
|
||||
|
||||
# 添加旋转动画
|
||||
var tween = spinner.create_tween()
|
||||
tween.set_loops()
|
||||
tween.tween_property(spinner, "rotation", TAU, 1.0)
|
||||
|
||||
return spinner
|
||||
|
||||
## 移除加载动画
|
||||
func remove_loading_spinner(spinner: Control) -> void:
|
||||
"""
|
||||
移除加载旋转动画
|
||||
@param spinner: 旋转节点
|
||||
"""
|
||||
if spinner and is_instance_valid(spinner):
|
||||
spinner.queue_free()
|
||||
1
scripts/UIAnimationManager.gd.uid
Normal file
1
scripts/UIAnimationManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://1tdrmkmbfwl8
|
||||
244
scripts/UILayer.gd
Normal file
244
scripts/UILayer.gd
Normal file
@@ -0,0 +1,244 @@
|
||||
extends CanvasLayer
|
||||
class_name UILayer
|
||||
## UI 层管理器
|
||||
## 管理所有 UI 界面的显示和切换
|
||||
|
||||
# UI 界面引用
|
||||
var login_screen: Control = null
|
||||
var character_creation: Control = null
|
||||
var hud: Control = null
|
||||
var dialogue_box: DialogueBox = null
|
||||
|
||||
# 当前显示的界面
|
||||
var current_screen: Control = null
|
||||
|
||||
func _ready():
|
||||
"""初始化 UI 层"""
|
||||
# 设置为最上层
|
||||
layer = 100
|
||||
|
||||
# 创建响应式布局容器
|
||||
_setup_responsive_layout()
|
||||
|
||||
# 创建对话框(始终存在但默认隐藏)
|
||||
_create_dialogue_box()
|
||||
|
||||
print("UILayer initialized")
|
||||
|
||||
## 创建对话框
|
||||
func _create_dialogue_box():
|
||||
"""创建对话框"""
|
||||
dialogue_box = DialogueBox.new()
|
||||
dialogue_box.name = "DialogueBox"
|
||||
add_child(dialogue_box)
|
||||
|
||||
# 连接信号
|
||||
dialogue_box.message_sent.connect(_on_dialogue_message_sent)
|
||||
dialogue_box.dialogue_closed.connect(_on_dialogue_closed)
|
||||
|
||||
## 对话消息发送处理
|
||||
func _on_dialogue_message_sent(message: String):
|
||||
"""处理对话消息发送"""
|
||||
print("Dialogue message sent: ", message)
|
||||
|
||||
# 获取Main节点并发送消息到对话系统
|
||||
var main = get_node("/root/Main")
|
||||
if not main:
|
||||
print("ERROR: Main node not found")
|
||||
return
|
||||
|
||||
if not main.dialogue_test_manager:
|
||||
print("ERROR: Dialogue test manager not found")
|
||||
return
|
||||
|
||||
var office_scene = main.office_scene
|
||||
if not office_scene:
|
||||
print("ERROR: Office scene not found")
|
||||
return
|
||||
|
||||
var dialogue_system = office_scene.get_node_or_null("DialogueSystem")
|
||||
if not dialogue_system:
|
||||
print("ERROR: Dialogue system not found")
|
||||
return
|
||||
|
||||
if not dialogue_system.is_dialogue_active():
|
||||
print("WARNING: Dialogue not active")
|
||||
return
|
||||
|
||||
# 发送消息到对话系统
|
||||
var success = dialogue_system.send_message(message)
|
||||
if not success:
|
||||
print("ERROR: Failed to send message through dialogue system")
|
||||
# 可以在这里添加错误提示给用户
|
||||
if dialogue_box:
|
||||
dialogue_box.add_message("系统", "消息发送失败,请重试")
|
||||
|
||||
## 对话关闭处理
|
||||
func _on_dialogue_closed():
|
||||
"""处理对话关闭"""
|
||||
print("Dialogue closed - clearing movement state")
|
||||
|
||||
# 结束对话系统中的对话
|
||||
var main = get_node("/root/Main")
|
||||
if main and main.dialogue_test_manager:
|
||||
var office_scene = main.office_scene
|
||||
if office_scene:
|
||||
var dialogue_system = office_scene.get_node_or_null("DialogueSystem")
|
||||
if dialogue_system:
|
||||
dialogue_system.end_dialogue()
|
||||
|
||||
# 强制清除玩家角色的移动状态
|
||||
if main and main.player_character:
|
||||
if main.player_character.has_method("_reset_movement_state"):
|
||||
main.player_character._reset_movement_state()
|
||||
print("Player character movement reset after dialogue close")
|
||||
|
||||
# 清除输入处理器的移动状态
|
||||
if main and main.input_handler:
|
||||
if main.input_handler.has_method("_clear_all_movement_state"):
|
||||
main.input_handler._clear_all_movement_state()
|
||||
print("Input handler movement cleared after dialogue close")
|
||||
|
||||
## 设置响应式布局
|
||||
func _setup_responsive_layout():
|
||||
"""
|
||||
设置响应式布局容器
|
||||
确保 UI 在不同分辨率下正确显示
|
||||
"""
|
||||
# 监听窗口大小变化
|
||||
get_viewport().size_changed.connect(_on_viewport_size_changed)
|
||||
|
||||
## 显示指定界面
|
||||
func show_screen(screen_name: String) -> void:
|
||||
"""
|
||||
显示指定的 UI 界面
|
||||
@param screen_name: 界面名称(login, character_creation, hud)
|
||||
"""
|
||||
# 隐藏当前界面
|
||||
if current_screen:
|
||||
current_screen.hide()
|
||||
|
||||
# 显示新界面
|
||||
match screen_name:
|
||||
"login":
|
||||
if not login_screen:
|
||||
_create_login_screen()
|
||||
current_screen = login_screen
|
||||
"character_creation":
|
||||
if not character_creation:
|
||||
_create_character_creation()
|
||||
current_screen = character_creation
|
||||
"hud":
|
||||
if not hud:
|
||||
_create_hud()
|
||||
current_screen = hud
|
||||
_:
|
||||
push_warning("Unknown screen: ", screen_name)
|
||||
return
|
||||
|
||||
if current_screen:
|
||||
current_screen.show()
|
||||
|
||||
## 隐藏所有界面
|
||||
func hide_all_screens() -> void:
|
||||
"""隐藏所有 UI 界面"""
|
||||
if login_screen:
|
||||
login_screen.hide()
|
||||
if character_creation:
|
||||
character_creation.hide()
|
||||
if hud:
|
||||
hud.hide()
|
||||
# 注意:对话框不在这里隐藏,它独立管理
|
||||
|
||||
current_screen = null
|
||||
|
||||
## 显示对话框
|
||||
func show_dialogue(target_name: String) -> void:
|
||||
"""
|
||||
显示对话框
|
||||
@param target_name: 对话目标的名称
|
||||
"""
|
||||
if dialogue_box:
|
||||
dialogue_box.start_dialogue(target_name)
|
||||
|
||||
## 隐藏对话框
|
||||
func hide_dialogue() -> void:
|
||||
"""隐藏对话框"""
|
||||
if dialogue_box:
|
||||
dialogue_box.close_dialogue()
|
||||
|
||||
## 创建登录界面
|
||||
func _create_login_screen() -> void:
|
||||
"""创建登录界面"""
|
||||
login_screen = LoginScreen.new()
|
||||
login_screen.name = "LoginScreen"
|
||||
login_screen.hide()
|
||||
add_child(login_screen)
|
||||
|
||||
# 连接信号
|
||||
login_screen.login_requested.connect(_on_login_requested)
|
||||
login_screen.create_character_requested.connect(_on_create_character_requested)
|
||||
|
||||
## 登录请求处理
|
||||
func _on_login_requested(_username: String):
|
||||
"""处理登录请求"""
|
||||
# 信号会传递到 Main.gd 处理
|
||||
pass
|
||||
|
||||
## 创建角色请求处理
|
||||
func _on_create_character_requested():
|
||||
"""处理创建角色请求"""
|
||||
# 信号会传递到 Main.gd 处理
|
||||
pass
|
||||
|
||||
## 创建角色创建界面
|
||||
func _create_character_creation() -> void:
|
||||
"""创建角色创建界面"""
|
||||
character_creation = CharacterCreation.new()
|
||||
character_creation.name = "CharacterCreation"
|
||||
character_creation.hide()
|
||||
add_child(character_creation)
|
||||
|
||||
# 连接信号
|
||||
character_creation.character_created.connect(_on_character_created)
|
||||
character_creation.back_requested.connect(_on_back_to_login)
|
||||
|
||||
## 角色创建完成处理
|
||||
func _on_character_created(_character_name: String, _personalization_data: Dictionary = {}):
|
||||
"""处理角色创建完成"""
|
||||
# 信号会传递到 Main.gd 处理
|
||||
pass
|
||||
|
||||
## 返回登录界面
|
||||
func _on_back_to_login():
|
||||
"""返回登录界面"""
|
||||
show_screen("login")
|
||||
|
||||
## 创建游戏内 HUD
|
||||
func _create_hud() -> void:
|
||||
"""创建游戏内 HUD"""
|
||||
hud = HUD.new()
|
||||
hud.name = "HUD"
|
||||
hud.hide()
|
||||
add_child(hud)
|
||||
|
||||
## 窗口大小变化回调
|
||||
func _on_viewport_size_changed():
|
||||
"""
|
||||
窗口大小变化时调整 UI 布局
|
||||
"""
|
||||
var viewport_size = get_viewport().get_visible_rect().size
|
||||
print("Viewport size changed: ", viewport_size)
|
||||
|
||||
# 通知所有子界面更新布局
|
||||
_update_all_layouts()
|
||||
|
||||
## 更新所有界面布局
|
||||
func _update_all_layouts():
|
||||
"""更新所有界面的布局以适应新的窗口大小"""
|
||||
if login_screen and login_screen.has_method("update_layout"):
|
||||
login_screen.update_layout()
|
||||
if character_creation and character_creation.has_method("update_layout"):
|
||||
character_creation.update_layout()
|
||||
if hud and hud.has_method("update_layout"):
|
||||
hud.update_layout()
|
||||
1
scripts/UILayer.gd.uid
Normal file
1
scripts/UILayer.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://qef0lslx1f0d
|
||||
669
scripts/UserBehaviorAnalytics.gd
Normal file
669
scripts/UserBehaviorAnalytics.gd
Normal file
@@ -0,0 +1,669 @@
|
||||
extends Node
|
||||
class_name UserBehaviorAnalytics
|
||||
## 用户行为分析系统
|
||||
## 收集、分析和报告用户在游戏中的行为数据
|
||||
|
||||
# 行为事件类型枚举
|
||||
enum EventType {
|
||||
LOGIN, # 登录
|
||||
LOGOUT, # 登出
|
||||
CHARACTER_CREATED, # 角色创建
|
||||
MOVEMENT, # 移动
|
||||
DIALOGUE_STARTED, # 开始对话
|
||||
DIALOGUE_ENDED, # 结束对话
|
||||
MESSAGE_SENT, # 发送消息
|
||||
FRIEND_REQUEST, # 好友请求
|
||||
EVENT_JOINED, # 参加活动
|
||||
INTERACTION, # 交互行为
|
||||
UI_ACTION, # UI操作
|
||||
ERROR_OCCURRED, # 错误发生
|
||||
PERFORMANCE_METRIC # 性能指标
|
||||
}
|
||||
|
||||
# 行为数据结构
|
||||
class BehaviorEvent:
|
||||
var event_id: String
|
||||
var event_type: EventType
|
||||
var user_id: String
|
||||
var timestamp: float
|
||||
var session_id: String
|
||||
var data: Dictionary = {}
|
||||
var context: Dictionary = {}
|
||||
|
||||
func _init(type: EventType, uid: String, sid: String, event_data: Dictionary = {}):
|
||||
event_id = generate_event_id()
|
||||
event_type = type
|
||||
user_id = uid
|
||||
session_id = sid
|
||||
timestamp = Time.get_unix_time_from_system()
|
||||
data = event_data.duplicate()
|
||||
context = _get_context_data()
|
||||
|
||||
func generate_event_id() -> String:
|
||||
return "evt_%d_%d" % [Time.get_unix_time_from_system(), randi()]
|
||||
|
||||
func _get_context_data() -> Dictionary:
|
||||
return {
|
||||
"platform": OS.get_name(),
|
||||
"viewport_size": get_viewport().get_visible_rect().size if get_viewport() else Vector2.ZERO,
|
||||
"fps": Engine.get_frames_per_second()
|
||||
}
|
||||
|
||||
# 数据存储和管理
|
||||
var behavior_events: Array[BehaviorEvent] = []
|
||||
var current_session_id: String = ""
|
||||
var current_user_id: String = "player"
|
||||
var session_start_time: float = 0.0
|
||||
var max_events_in_memory: int = 1000
|
||||
var analytics_enabled: bool = true
|
||||
|
||||
# 数据持久化
|
||||
var analytics_file_path: String = "user://user_analytics.json"
|
||||
var session_file_path: String = "user://session_data.json"
|
||||
|
||||
# 统计数据缓存
|
||||
var cached_statistics: Dictionary = {}
|
||||
var statistics_cache_time: float = 0.0
|
||||
var cache_duration: float = 300.0 # 5分钟缓存
|
||||
|
||||
# 信号
|
||||
signal behavior_recorded(event_type: EventType, data: Dictionary)
|
||||
signal session_started(session_id: String)
|
||||
signal session_ended(session_id: String, duration: float)
|
||||
signal analytics_report_generated(report: Dictionary)
|
||||
|
||||
func _ready():
|
||||
"""初始化用户行为分析系统"""
|
||||
_start_new_session()
|
||||
_load_analytics_data()
|
||||
|
||||
# 设置定时保存
|
||||
var save_timer = Timer.new()
|
||||
save_timer.wait_time = 60.0 # 每分钟保存一次
|
||||
save_timer.timeout.connect(_save_analytics_data)
|
||||
save_timer.autostart = true
|
||||
add_child(save_timer)
|
||||
|
||||
print("UserBehaviorAnalytics initialized")
|
||||
|
||||
## 开始新会话
|
||||
func _start_new_session() -> void:
|
||||
"""开始新的用户会话"""
|
||||
current_session_id = _generate_session_id()
|
||||
session_start_time = Time.get_unix_time_from_system()
|
||||
|
||||
# 记录会话开始事件
|
||||
record_event(EventType.LOGIN, {"session_start": true})
|
||||
|
||||
session_started.emit(current_session_id)
|
||||
print("New session started: ", current_session_id)
|
||||
|
||||
## 结束当前会话
|
||||
func end_current_session() -> void:
|
||||
"""结束当前用户会话"""
|
||||
if current_session_id.is_empty():
|
||||
return
|
||||
|
||||
var session_duration = Time.get_unix_time_from_system() - session_start_time
|
||||
|
||||
# 记录会话结束事件
|
||||
record_event(EventType.LOGOUT, {
|
||||
"session_duration": session_duration,
|
||||
"events_count": behavior_events.size()
|
||||
})
|
||||
|
||||
session_ended.emit(current_session_id, session_duration)
|
||||
|
||||
# 保存数据
|
||||
_save_analytics_data()
|
||||
|
||||
print("Session ended: ", current_session_id, " Duration: ", session_duration, "s")
|
||||
|
||||
## 记录行为事件
|
||||
func record_event(event_type: EventType, event_data: Dictionary = {}) -> void:
|
||||
"""
|
||||
记录用户行为事件
|
||||
@param event_type: 事件类型
|
||||
@param event_data: 事件数据
|
||||
"""
|
||||
if not analytics_enabled:
|
||||
return
|
||||
|
||||
var event = BehaviorEvent.new(event_type, current_user_id, current_session_id, event_data)
|
||||
behavior_events.append(event)
|
||||
|
||||
# 限制内存中的事件数量
|
||||
if behavior_events.size() > max_events_in_memory:
|
||||
behavior_events.pop_front()
|
||||
|
||||
# 清除统计缓存
|
||||
_clear_statistics_cache()
|
||||
|
||||
# 发射信号
|
||||
behavior_recorded.emit(event_type, event_data)
|
||||
|
||||
# 调试输出(仅在开发模式)
|
||||
if OS.is_debug_build():
|
||||
print("Behavior recorded: ", EventType.keys()[event_type], " - ", event_data)
|
||||
|
||||
## 记录移动行为
|
||||
func record_movement(from_position: Vector2, to_position: Vector2, duration: float) -> void:
|
||||
"""
|
||||
记录角色移动行为
|
||||
@param from_position: 起始位置
|
||||
@param to_position: 目标位置
|
||||
@param duration: 移动时长
|
||||
"""
|
||||
var distance = from_position.distance_to(to_position)
|
||||
record_event(EventType.MOVEMENT, {
|
||||
"from": {"x": from_position.x, "y": from_position.y},
|
||||
"to": {"x": to_position.x, "y": to_position.y},
|
||||
"distance": distance,
|
||||
"duration": duration,
|
||||
"speed": distance / max(duration, 0.1)
|
||||
})
|
||||
|
||||
## 记录对话行为
|
||||
func record_dialogue_interaction(target_character: String, message_count: int, duration: float) -> void:
|
||||
"""
|
||||
记录对话交互行为
|
||||
@param target_character: 对话目标角色
|
||||
@param message_count: 消息数量
|
||||
@param duration: 对话时长
|
||||
"""
|
||||
record_event(EventType.DIALOGUE_STARTED, {
|
||||
"target": target_character,
|
||||
"message_count": message_count,
|
||||
"duration": duration
|
||||
})
|
||||
|
||||
## 记录UI操作
|
||||
func record_ui_action(action_type: String, ui_element: String, additional_data: Dictionary = {}) -> void:
|
||||
"""
|
||||
记录UI操作行为
|
||||
@param action_type: 操作类型(click, hover, input等)
|
||||
@param ui_element: UI元素标识
|
||||
@param additional_data: 额外数据
|
||||
"""
|
||||
var ui_data = {
|
||||
"action": action_type,
|
||||
"element": ui_element
|
||||
}
|
||||
ui_data.merge(additional_data)
|
||||
|
||||
record_event(EventType.UI_ACTION, ui_data)
|
||||
|
||||
## 记录性能指标
|
||||
func record_performance_metric(metric_name: String, value: float, unit: String = "") -> void:
|
||||
"""
|
||||
记录性能指标
|
||||
@param metric_name: 指标名称
|
||||
@param value: 指标值
|
||||
@param unit: 单位
|
||||
"""
|
||||
record_event(EventType.PERFORMANCE_METRIC, {
|
||||
"metric": metric_name,
|
||||
"value": value,
|
||||
"unit": unit,
|
||||
"fps": Engine.get_frames_per_second(),
|
||||
"memory_usage": OS.get_static_memory_usage_by_type()
|
||||
})
|
||||
|
||||
## 记录错误事件
|
||||
func record_error(error_type: String, error_message: String, context_data: Dictionary = {}) -> void:
|
||||
"""
|
||||
记录错误事件
|
||||
@param error_type: 错误类型
|
||||
@param error_message: 错误消息
|
||||
@param context_data: 上下文数据
|
||||
"""
|
||||
var error_data = {
|
||||
"error_type": error_type,
|
||||
"message": error_message,
|
||||
"stack_trace": get_stack() if OS.is_debug_build() else []
|
||||
}
|
||||
error_data.merge(context_data)
|
||||
|
||||
record_event(EventType.ERROR_OCCURRED, error_data)
|
||||
|
||||
## 生成会话ID
|
||||
func _generate_session_id() -> String:
|
||||
"""生成唯一的会话ID"""
|
||||
var timestamp = Time.get_unix_time_from_system()
|
||||
var random = randi()
|
||||
return "session_%d_%d" % [timestamp, random]
|
||||
|
||||
## 清除统计缓存
|
||||
func _clear_statistics_cache() -> void:
|
||||
"""清除统计数据缓存"""
|
||||
cached_statistics.clear()
|
||||
statistics_cache_time = 0.0## 获
|
||||
取基础统计信息
|
||||
func get_basic_statistics() -> Dictionary:
|
||||
"""
|
||||
获取基础统计信息
|
||||
@return: 统计数据字典
|
||||
"""
|
||||
# 检查缓存
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
if not cached_statistics.is_empty() and (current_time - statistics_cache_time) < cache_duration:
|
||||
return cached_statistics
|
||||
|
||||
var stats = {}
|
||||
|
||||
# 事件总数统计
|
||||
stats["total_events"] = behavior_events.size()
|
||||
stats["current_session_events"] = _count_session_events(current_session_id)
|
||||
stats["session_duration"] = current_time - session_start_time
|
||||
|
||||
# 事件类型分布
|
||||
var event_type_counts = {}
|
||||
for event in behavior_events:
|
||||
var type_name = EventType.keys()[event.event_type]
|
||||
event_type_counts[type_name] = event_type_counts.get(type_name, 0) + 1
|
||||
stats["event_types"] = event_type_counts
|
||||
|
||||
# 活跃度分析
|
||||
stats["activity_metrics"] = _calculate_activity_metrics()
|
||||
|
||||
# 性能统计
|
||||
stats["performance_metrics"] = _calculate_performance_metrics()
|
||||
|
||||
# 错误统计
|
||||
stats["error_metrics"] = _calculate_error_metrics()
|
||||
|
||||
# 缓存结果
|
||||
cached_statistics = stats
|
||||
statistics_cache_time = current_time
|
||||
|
||||
return stats
|
||||
|
||||
## 计算活跃度指标
|
||||
func _calculate_activity_metrics() -> Dictionary:
|
||||
"""计算用户活跃度指标"""
|
||||
var metrics = {}
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
|
||||
# 最近1小时的活动
|
||||
var recent_events = _get_events_in_timeframe(current_time - 3600)
|
||||
metrics["events_last_hour"] = recent_events.size()
|
||||
|
||||
# 移动活跃度
|
||||
var movement_events = _get_events_by_type(EventType.MOVEMENT)
|
||||
metrics["total_movements"] = movement_events.size()
|
||||
|
||||
if movement_events.size() > 0:
|
||||
var total_distance = 0.0
|
||||
for event in movement_events:
|
||||
total_distance += event.data.get("distance", 0.0)
|
||||
metrics["total_distance_moved"] = total_distance
|
||||
metrics["average_movement_distance"] = total_distance / movement_events.size()
|
||||
|
||||
# 社交活跃度
|
||||
var dialogue_events = _get_events_by_type(EventType.DIALOGUE_STARTED)
|
||||
metrics["dialogue_sessions"] = dialogue_events.size()
|
||||
|
||||
var message_events = _get_events_by_type(EventType.MESSAGE_SENT)
|
||||
metrics["messages_sent"] = message_events.size()
|
||||
|
||||
# UI交互频率
|
||||
var ui_events = _get_events_by_type(EventType.UI_ACTION)
|
||||
metrics["ui_interactions"] = ui_events.size()
|
||||
|
||||
return metrics
|
||||
|
||||
## 计算性能指标
|
||||
func _calculate_performance_metrics() -> Dictionary:
|
||||
"""计算性能相关指标"""
|
||||
var metrics = {}
|
||||
var performance_events = _get_events_by_type(EventType.PERFORMANCE_METRIC)
|
||||
|
||||
if performance_events.size() > 0:
|
||||
var fps_values = []
|
||||
var memory_values = []
|
||||
|
||||
for event in performance_events:
|
||||
if event.context.has("fps"):
|
||||
fps_values.append(event.context.fps)
|
||||
if event.data.has("memory_usage"):
|
||||
memory_values.append(event.data.memory_usage)
|
||||
|
||||
if fps_values.size() > 0:
|
||||
metrics["average_fps"] = _calculate_average(fps_values)
|
||||
metrics["min_fps"] = fps_values.min()
|
||||
metrics["max_fps"] = fps_values.max()
|
||||
|
||||
if memory_values.size() > 0:
|
||||
metrics["average_memory"] = _calculate_average(memory_values)
|
||||
metrics["peak_memory"] = memory_values.max()
|
||||
|
||||
# 当前性能状态
|
||||
metrics["current_fps"] = Engine.get_frames_per_second()
|
||||
metrics["current_memory"] = OS.get_static_memory_usage_by_type()
|
||||
|
||||
return metrics
|
||||
|
||||
## 计算错误指标
|
||||
func _calculate_error_metrics() -> Dictionary:
|
||||
"""计算错误相关指标"""
|
||||
var metrics = {}
|
||||
var error_events = _get_events_by_type(EventType.ERROR_OCCURRED)
|
||||
|
||||
metrics["total_errors"] = error_events.size()
|
||||
|
||||
# 错误类型分布
|
||||
var error_types = {}
|
||||
for event in error_events:
|
||||
var error_type = event.data.get("error_type", "unknown")
|
||||
error_types[error_type] = error_types.get(error_type, 0) + 1
|
||||
|
||||
metrics["error_types"] = error_types
|
||||
|
||||
# 最近错误
|
||||
if error_events.size() > 0:
|
||||
var latest_error = error_events[-1]
|
||||
metrics["latest_error"] = {
|
||||
"type": latest_error.data.get("error_type", "unknown"),
|
||||
"message": latest_error.data.get("message", ""),
|
||||
"timestamp": latest_error.timestamp
|
||||
}
|
||||
|
||||
return metrics
|
||||
|
||||
## 生成用户行为报告
|
||||
func generate_behavior_report(timeframe_hours: float = 24.0) -> Dictionary:
|
||||
"""
|
||||
生成用户行为分析报告
|
||||
@param timeframe_hours: 分析时间范围(小时)
|
||||
@return: 行为报告
|
||||
"""
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
var start_time = current_time - (timeframe_hours * 3600)
|
||||
|
||||
var report = {}
|
||||
report["report_generated_at"] = current_time
|
||||
report["timeframe_hours"] = timeframe_hours
|
||||
report["user_id"] = current_user_id
|
||||
report["session_id"] = current_session_id
|
||||
|
||||
# 获取时间范围内的事件
|
||||
var timeframe_events = _get_events_in_timeframe(start_time)
|
||||
report["events_in_timeframe"] = timeframe_events.size()
|
||||
|
||||
# 活动模式分析
|
||||
report["activity_patterns"] = _analyze_activity_patterns(timeframe_events)
|
||||
|
||||
# 用户偏好分析
|
||||
report["user_preferences"] = _analyze_user_preferences(timeframe_events)
|
||||
|
||||
# 会话质量分析
|
||||
report["session_quality"] = _analyze_session_quality(timeframe_events)
|
||||
|
||||
# 发射报告生成信号
|
||||
analytics_report_generated.emit(report)
|
||||
|
||||
return report
|
||||
|
||||
## 分析活动模式
|
||||
func _analyze_activity_patterns(events: Array[BehaviorEvent]) -> Dictionary:
|
||||
"""分析用户活动模式"""
|
||||
var patterns = {}
|
||||
|
||||
# 按小时分组活动
|
||||
var hourly_activity = {}
|
||||
for event in events:
|
||||
var hour = Time.get_datetime_dict_from_unix_time(event.timestamp).hour
|
||||
hourly_activity[hour] = hourly_activity.get(hour, 0) + 1
|
||||
|
||||
patterns["hourly_distribution"] = hourly_activity
|
||||
|
||||
# 最活跃时段
|
||||
var max_activity = 0
|
||||
var peak_hour = 0
|
||||
for hour in hourly_activity:
|
||||
if hourly_activity[hour] > max_activity:
|
||||
max_activity = hourly_activity[hour]
|
||||
peak_hour = hour
|
||||
|
||||
patterns["peak_activity_hour"] = peak_hour
|
||||
patterns["peak_activity_count"] = max_activity
|
||||
|
||||
# 活动类型偏好
|
||||
var type_preferences = {}
|
||||
for event in events:
|
||||
var type_name = EventType.keys()[event.event_type]
|
||||
type_preferences[type_name] = type_preferences.get(type_name, 0) + 1
|
||||
|
||||
patterns["activity_type_preferences"] = type_preferences
|
||||
|
||||
return patterns
|
||||
|
||||
## 分析用户偏好
|
||||
func _analyze_user_preferences(events: Array[BehaviorEvent]) -> Dictionary:
|
||||
"""分析用户偏好和兴趣"""
|
||||
var preferences = {}
|
||||
|
||||
# 对话偏好
|
||||
var dialogue_targets = {}
|
||||
var dialogue_events = events.filter(func(e): return e.event_type == EventType.DIALOGUE_STARTED)
|
||||
|
||||
for event in dialogue_events:
|
||||
var target = event.data.get("target", "unknown")
|
||||
dialogue_targets[target] = dialogue_targets.get(target, 0) + 1
|
||||
|
||||
preferences["preferred_dialogue_targets"] = dialogue_targets
|
||||
|
||||
# UI使用偏好
|
||||
var ui_preferences = {}
|
||||
var ui_events = events.filter(func(e): return e.event_type == EventType.UI_ACTION)
|
||||
|
||||
for event in ui_events:
|
||||
var element = event.data.get("element", "unknown")
|
||||
ui_preferences[element] = ui_preferences.get(element, 0) + 1
|
||||
|
||||
preferences["ui_usage_patterns"] = ui_preferences
|
||||
|
||||
# 移动模式偏好
|
||||
var movement_events = events.filter(func(e): return e.event_type == EventType.MOVEMENT)
|
||||
if movement_events.size() > 0:
|
||||
var distances = movement_events.map(func(e): return e.data.get("distance", 0.0))
|
||||
preferences["average_movement_distance"] = _calculate_average(distances)
|
||||
preferences["movement_frequency"] = movement_events.size()
|
||||
|
||||
return preferences
|
||||
|
||||
## 分析会话质量
|
||||
func _analyze_session_quality(events: Array[BehaviorEvent]) -> Dictionary:
|
||||
"""分析会话质量指标"""
|
||||
var quality = {}
|
||||
|
||||
# 错误率
|
||||
var error_events = events.filter(func(e): return e.event_type == EventType.ERROR_OCCURRED)
|
||||
quality["error_rate"] = float(error_events.size()) / max(events.size(), 1)
|
||||
|
||||
# 参与度(基于事件多样性)
|
||||
var unique_event_types = {}
|
||||
for event in events:
|
||||
unique_event_types[event.event_type] = true
|
||||
|
||||
quality["engagement_score"] = float(unique_event_types.size()) / EventType.size()
|
||||
|
||||
# 会话连续性(事件间隔分析)
|
||||
if events.size() > 1:
|
||||
var intervals = []
|
||||
for i in range(1, events.size()):
|
||||
intervals.append(events[i].timestamp - events[i-1].timestamp)
|
||||
|
||||
quality["average_event_interval"] = _calculate_average(intervals)
|
||||
quality["session_continuity"] = 1.0 / (1.0 + _calculate_average(intervals))
|
||||
|
||||
return quality
|
||||
|
||||
## 辅助函数:计算平均值
|
||||
func _calculate_average(values: Array) -> float:
|
||||
"""计算数组平均值"""
|
||||
if values.is_empty():
|
||||
return 0.0
|
||||
|
||||
var sum = 0.0
|
||||
for value in values:
|
||||
sum += float(value)
|
||||
|
||||
return sum / values.size()
|
||||
|
||||
## 辅助函数:按类型获取事件
|
||||
func _get_events_by_type(event_type: EventType) -> Array[BehaviorEvent]:
|
||||
"""获取指定类型的事件"""
|
||||
return behavior_events.filter(func(event): return event.event_type == event_type)
|
||||
|
||||
## 辅助函数:获取时间范围内的事件
|
||||
func _get_events_in_timeframe(start_time: float) -> Array[BehaviorEvent]:
|
||||
"""获取指定时间范围内的事件"""
|
||||
return behavior_events.filter(func(event): return event.timestamp >= start_time)
|
||||
|
||||
## 辅助函数:统计会话事件数
|
||||
func _count_session_events(session_id: String) -> int:
|
||||
"""统计指定会话的事件数量"""
|
||||
return behavior_events.filter(func(event): return event.session_id == session_id).size()
|
||||
|
||||
## 保存分析数据
|
||||
func _save_analytics_data() -> void:
|
||||
"""保存分析数据到本地文件"""
|
||||
var data = {
|
||||
"events": [],
|
||||
"session_id": current_session_id,
|
||||
"session_start_time": session_start_time,
|
||||
"user_id": current_user_id,
|
||||
"saved_at": Time.get_unix_time_from_system()
|
||||
}
|
||||
|
||||
# 序列化事件数据(只保存最近的事件)
|
||||
var events_to_save = behavior_events.slice(max(0, behavior_events.size() - 500), behavior_events.size())
|
||||
|
||||
for event in events_to_save:
|
||||
data.events.append({
|
||||
"event_id": event.event_id,
|
||||
"event_type": event.event_type,
|
||||
"user_id": event.user_id,
|
||||
"session_id": event.session_id,
|
||||
"timestamp": event.timestamp,
|
||||
"data": event.data,
|
||||
"context": event.context
|
||||
})
|
||||
|
||||
var file = FileAccess.open(analytics_file_path, FileAccess.WRITE)
|
||||
if file:
|
||||
var json_string = JSON.stringify(data)
|
||||
file.store_string(json_string)
|
||||
file.close()
|
||||
print("Analytics data saved: ", events_to_save.size(), " events")
|
||||
else:
|
||||
print("Failed to save analytics data")
|
||||
|
||||
## 加载分析数据
|
||||
func _load_analytics_data() -> void:
|
||||
"""从本地文件加载分析数据"""
|
||||
if not FileAccess.file_exists(analytics_file_path):
|
||||
print("No analytics data file found, starting fresh")
|
||||
return
|
||||
|
||||
var file = FileAccess.open(analytics_file_path, FileAccess.READ)
|
||||
if file:
|
||||
var json_string = file.get_as_text()
|
||||
file.close()
|
||||
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(json_string)
|
||||
|
||||
if parse_result == OK:
|
||||
var data = json.data
|
||||
|
||||
# 加载事件数据
|
||||
if data.has("events"):
|
||||
for event_data in data.events:
|
||||
var event = BehaviorEvent.new(
|
||||
event_data.get("event_type", EventType.UI_ACTION),
|
||||
event_data.get("user_id", ""),
|
||||
event_data.get("session_id", "")
|
||||
)
|
||||
event.event_id = event_data.get("event_id", "")
|
||||
event.timestamp = event_data.get("timestamp", 0.0)
|
||||
event.data = event_data.get("data", {})
|
||||
event.context = event_data.get("context", {})
|
||||
behavior_events.append(event)
|
||||
|
||||
print("Analytics data loaded: ", behavior_events.size(), " events")
|
||||
else:
|
||||
print("Failed to parse analytics data JSON")
|
||||
else:
|
||||
print("Failed to open analytics data file")
|
||||
|
||||
## 导出分析报告
|
||||
func export_analytics_report(export_path: String, timeframe_hours: float = 24.0) -> bool:
|
||||
"""
|
||||
导出分析报告到文件
|
||||
@param export_path: 导出文件路径
|
||||
@param timeframe_hours: 分析时间范围
|
||||
@return: 是否成功导出
|
||||
"""
|
||||
var report = generate_behavior_report(timeframe_hours)
|
||||
|
||||
var file = FileAccess.open(export_path, FileAccess.WRITE)
|
||||
if file:
|
||||
var json_string = JSON.stringify(report)
|
||||
file.store_string(json_string)
|
||||
file.close()
|
||||
print("Analytics report exported to: ", export_path)
|
||||
return true
|
||||
else:
|
||||
print("Failed to export analytics report to: ", export_path)
|
||||
return false
|
||||
|
||||
## 清理旧数据
|
||||
func cleanup_old_data(days_to_keep: int = 7) -> void:
|
||||
"""
|
||||
清理旧的分析数据
|
||||
@param days_to_keep: 保留天数
|
||||
"""
|
||||
var cutoff_time = Time.get_unix_time_from_system() - (days_to_keep * 86400)
|
||||
var original_count = behavior_events.size()
|
||||
|
||||
behavior_events = behavior_events.filter(func(event): return event.timestamp >= cutoff_time)
|
||||
|
||||
var removed_count = original_count - behavior_events.size()
|
||||
if removed_count > 0:
|
||||
print("Cleaned up ", removed_count, " old analytics events")
|
||||
_save_analytics_data()
|
||||
|
||||
## 获取实时统计
|
||||
func get_realtime_statistics() -> Dictionary:
|
||||
"""获取实时统计信息(不使用缓存)"""
|
||||
var stats = {}
|
||||
|
||||
stats["current_session_duration"] = Time.get_unix_time_from_system() - session_start_time
|
||||
stats["events_this_session"] = _count_session_events(current_session_id)
|
||||
stats["current_fps"] = Engine.get_frames_per_second()
|
||||
stats["memory_usage"] = OS.get_static_memory_usage_by_type()
|
||||
|
||||
# 最近5分钟的活动
|
||||
var recent_time = Time.get_unix_time_from_system() - 300
|
||||
var recent_events = _get_events_in_timeframe(recent_time)
|
||||
stats["recent_activity"] = recent_events.size()
|
||||
|
||||
return stats
|
||||
|
||||
## 设置分析配置
|
||||
func set_analytics_config(enabled: bool, max_events: int = 1000) -> void:
|
||||
"""
|
||||
设置分析系统配置
|
||||
@param enabled: 是否启用分析
|
||||
@param max_events: 最大事件数量
|
||||
"""
|
||||
analytics_enabled = enabled
|
||||
max_events_in_memory = max_events
|
||||
|
||||
print("Analytics config updated - Enabled: ", enabled, " Max events: ", max_events)
|
||||
|
||||
func _exit_tree():
|
||||
"""节点退出时保存数据"""
|
||||
end_current_session()
|
||||
1
scripts/UserBehaviorAnalytics.gd.uid
Normal file
1
scripts/UserBehaviorAnalytics.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d4k5lf3q8pktv
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user