创建新工程

This commit is contained in:
moyin
2025-12-05 19:00:14 +08:00
commit ff4fa5fffd
227 changed files with 32804 additions and 0 deletions

View 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
}

View File

@@ -0,0 +1 @@
uid://cp5md2i8wxniy

View 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 = "自定义外观(暂未开放)"

View File

@@ -0,0 +1 @@
uid://1grjr2nf466x

View 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)

View File

@@ -0,0 +1 @@
uid://cdqyp5bllxe3c

386
scripts/CharacterData.gd Normal file
View 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)

View File

@@ -0,0 +1 @@
uid://bo5u3j1ktg6k6

View 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)

View File

@@ -0,0 +1 @@
uid://b8vw1w7qwkma8

313
scripts/CharacterProfile.gd Normal file
View 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()

View File

@@ -0,0 +1 @@
uid://depubcgpi7yn7

104
scripts/CharacterSprite.gd Normal file
View 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)

View File

@@ -0,0 +1 @@
uid://djfkqn1n4ulio

View 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
}

View File

@@ -0,0 +1 @@
uid://dtgrgcn1c5mvx

109
scripts/ChatBubble.gd Normal file
View 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

View File

@@ -0,0 +1 @@
uid://dtl86f6ro3s6l

View 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
}

View File

@@ -0,0 +1 @@
uid://cx5nxyt4bohe0

473
scripts/DatawhaleOffice.gd Normal file
View 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)

View File

@@ -0,0 +1 @@
uid://5wfrobimvgpr

View 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)

View File

@@ -0,0 +1 @@
uid://dguq2cbn64jx5

210
scripts/DebugCamera.gd Normal file
View 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

View File

@@ -0,0 +1 @@
uid://8i0rt2thwpkb

262
scripts/DialogueBox.gd Normal file
View 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()

View File

@@ -0,0 +1 @@
uid://cdn1q2kkqnknj

448
scripts/DialogueFilter.gd Normal file
View 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

View File

@@ -0,0 +1 @@
uid://y3kno87ni5aa

View 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

View File

@@ -0,0 +1 @@
uid://cu15s7u88m8so

362
scripts/DialogueSystem.gd Normal file
View 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)

View File

@@ -0,0 +1 @@
uid://dtgvd4g1earxp

View 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()

View File

@@ -0,0 +1 @@
uid://vg852e2naf3r

327
scripts/EmojiManager.gd Normal file
View 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

View File

@@ -0,0 +1 @@
uid://dv6j3x3hmn8kv

View 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)

View File

@@ -0,0 +1 @@
uid://fch68l3jc8j7

243
scripts/ErrorHandler.gd Normal file
View 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

View File

@@ -0,0 +1 @@
uid://dprpl5ckgohif

View 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()

View File

@@ -0,0 +1 @@
uid://cbxi6uspmwlpd

583
scripts/FriendSystem.gd Normal file
View 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
}

View File

@@ -0,0 +1 @@
uid://1amqu0q2sosf

205
scripts/GameConfig.gd Normal file
View 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
}

View File

@@ -0,0 +1 @@
uid://rrtsch4kftjg

View 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")

View File

@@ -0,0 +1 @@
uid://bi68fb55yixi3

550
scripts/GameStatistics.gd Normal file
View 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()

View File

@@ -0,0 +1 @@
uid://buanrk8sfjqxi

View 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
}

View File

@@ -0,0 +1 @@
uid://d1d68ckx5tgcs

129
scripts/HUD.gd Normal file
View 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
View File

@@ -0,0 +1 @@
uid://b85r5ndu5d7xh

267
scripts/InputHandler.gd Normal file
View 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")

View File

@@ -0,0 +1 @@
uid://dmuulnhvm6d0r

113
scripts/LoadingIndicator.gd Normal file
View 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

View File

@@ -0,0 +1 @@
uid://benobiu5q018p

177
scripts/LoginScreen.gd Normal file
View 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()

View File

@@ -0,0 +1 @@
uid://cfd7hksgbm8wj

749
scripts/Main.gd Normal file
View 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
View File

@@ -0,0 +1 @@
uid://o0k8pitjgwpm

170
scripts/MessageProtocol.gd Normal file
View 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
})

View File

@@ -0,0 +1 @@
uid://dkpetm4jfpanc

241
scripts/NetworkManager.gd Normal file
View 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

View File

@@ -0,0 +1 @@
uid://bff86rwwknn3a

View 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

View File

@@ -0,0 +1 @@
uid://dtw8y6rp3686j

View 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

View File

@@ -0,0 +1 @@
uid://0lcffylnv5st

View 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
}

View File

@@ -0,0 +1 @@
uid://tq01oyfan4j2

View 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")

View File

@@ -0,0 +1 @@
uid://bo0m373k8hi1

230
scripts/RateLimiter.gd Normal file
View 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

View File

@@ -0,0 +1 @@
uid://duywoukj58tpx

View 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)
}

View File

@@ -0,0 +1 @@
uid://oarhnakohrx

170
scripts/SecurityConfig.gd Normal file
View 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")

View File

@@ -0,0 +1 @@
uid://duxl8lgavgaw7

466
scripts/SecurityManager.gd Normal file
View 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)

View File

@@ -0,0 +1 @@
uid://xx7bykd7yh7r

View 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)

View File

@@ -0,0 +1 @@
uid://cyr2ca8bxclbj

603
scripts/SocialManager.gd Normal file
View 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()
}

View File

@@ -0,0 +1 @@
uid://bx0o7146y1hm3

View 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()

View File

@@ -0,0 +1 @@
uid://cxidptngh58jl

81
scripts/TestGameplay.gd Normal file
View 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)

View File

@@ -0,0 +1 @@
uid://t3ybfpqkqwp8

View 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)

View File

@@ -0,0 +1 @@
uid://dlqwi3cmfwmd2

View 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()

View File

@@ -0,0 +1 @@
uid://1tdrmkmbfwl8

244
scripts/UILayer.gd Normal file
View 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
View File

@@ -0,0 +1 @@
uid://qef0lslx1f0d

View 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()

View File

@@ -0,0 +1 @@
uid://d4k5lf3q8pktv

Some files were not shown because too many files have changed in this diff Show More