- 桥梁与静态物体高度采样修复: - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程 - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题 - 新增 test_bridge_height_parity.gd 自动化对拍测试 - 40250 怪物击杀经验动效: - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附 - 40250 客户端全系统功能对齐(Batches 1-31): - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试 - 文档沉淀: - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
171 lines
5.1 KiB
GDScript
171 lines
5.1 KiB
GDScript
# whisper_chat_system.gd —— 40250 独立多标签私聊与任务栏闪烁通知系统 1:1
|
|
# 严格对照:
|
|
# Client/Eternexus/root/uiwhisper.py (WhisperDialog, WhisperButton)
|
|
# ClientVS22/source/UserInterface/PythonChat.cpp (RenderWhisper, WhisperMode)
|
|
# ClientVS22/source/UserInterface/PythonPlayer.cpp (ToggleIgnoreTarget, IsIgnoreTarget)
|
|
extends RefCounted
|
|
|
|
const COLOR_NORMAL := "#FFFFFF"
|
|
const COLOR_GM := "#FFE700" # 官方 GM 专属亮金黄色
|
|
const COLOR_SYSTEM := "#00FFFF"
|
|
|
|
enum DialogState {
|
|
VISIBLE, # 展开在前台
|
|
MINIMIZED, # 最小化到底部任务栏
|
|
CLOSED # 完全关闭
|
|
}
|
|
|
|
class WhisperMessage:
|
|
var sender: String
|
|
var content: String
|
|
var timestamp: int
|
|
var is_gm: bool = false
|
|
|
|
func _init(p_sender: String, p_content: String, p_time: int, p_is_gm: bool = false) -> void:
|
|
sender = p_sender
|
|
content = p_content
|
|
timestamp = p_time
|
|
is_gm = p_is_gm
|
|
|
|
class WhisperConversation:
|
|
var target_name: String
|
|
var is_gamemaster: bool = false
|
|
var state: int = DialogState.VISIBLE
|
|
var is_blinking: bool = false # 任务栏闪烁标识
|
|
var unread_count: int = 0
|
|
var history: Array = [] # Array of WhisperMessage
|
|
|
|
func _init(p_target: String, p_is_gm: bool = false) -> void:
|
|
target_name = p_target
|
|
is_gamemaster = p_is_gm
|
|
state = DialogState.VISIBLE
|
|
is_blinking = false
|
|
unread_count = 0
|
|
history = []
|
|
|
|
var main_player_name: String = "Hero"
|
|
var conversations: Dictionary = {} # target_name -> WhisperConversation
|
|
var ignored_targets: Dictionary = {} # target_name -> bool
|
|
var current_time: int = 1000
|
|
|
|
func _init(player_name: String = "Hero") -> void:
|
|
main_player_name = player_name
|
|
conversations.clear()
|
|
ignored_targets.clear()
|
|
current_time = 1000
|
|
|
|
# 1. 打开或激活与某人的私聊窗口 (uiwhisper.py: OpenWhisperDialog)
|
|
func open_conversation(target_name: String) -> WhisperConversation:
|
|
if target_name.is_empty():
|
|
return null
|
|
|
|
var conv: WhisperConversation = null
|
|
if conversations.has(target_name):
|
|
conv = conversations[target_name]
|
|
conv.state = DialogState.VISIBLE
|
|
conv.is_blinking = false
|
|
conv.unread_count = 0
|
|
else:
|
|
var is_gm: bool = target_name.begins_with("[GM]")
|
|
conv = WhisperConversation.new(target_name, is_gm)
|
|
conversations[target_name] = conv
|
|
|
|
return conv
|
|
|
|
# 2. 最小化私聊窗口到任务栏 (uiwhisper.py: Minimize)
|
|
func minimize_conversation(target_name: String) -> bool:
|
|
if not conversations.has(target_name):
|
|
return false
|
|
conversations[target_name].state = DialogState.MINIMIZED
|
|
return true
|
|
|
|
# 3. 关闭私聊窗口 (uiwhisper.py: Close)
|
|
func close_conversation(target_name: String) -> bool:
|
|
if not conversations.has(target_name):
|
|
return false
|
|
conversations[target_name].state = DialogState.CLOSED
|
|
return true
|
|
|
|
# 4. 收到私聊消息 (1:1 PythonChat::AppendWhisper)
|
|
func receive_whisper(sender_name: String, text: String, is_gm_override: bool = false) -> Dictionary:
|
|
var res := {
|
|
"delivered": false,
|
|
"blocked_by_ignore": false,
|
|
"triggered_blink": false,
|
|
"conversation": null,
|
|
"error_code": "OK"
|
|
}
|
|
|
|
# 检查黑名单免打扰 (ToggleIgnoreTarget / IsIgnoreTarget)
|
|
if is_ignored(sender_name):
|
|
res["blocked_by_ignore"] = true
|
|
res["error_code"] = "BLOCKED_BY_IGNORE"
|
|
return res
|
|
|
|
var is_gm: bool = is_gm_override or sender_name.begins_with("[GM]")
|
|
|
|
# 获取或创建会话
|
|
var conv: WhisperConversation = null
|
|
if conversations.has(sender_name):
|
|
conv = conversations[sender_name]
|
|
else:
|
|
conv = WhisperConversation.new(sender_name, is_gm)
|
|
conv.state = DialogState.MINIMIZED # 新收到的私聊若未预先开启则最小化并在任务栏闪烁
|
|
conversations[sender_name] = conv
|
|
|
|
var msg = WhisperMessage.new(sender_name, text, current_time, is_gm)
|
|
conv.history.append(msg)
|
|
|
|
# 若处于最小化或后台,激活任务栏闪烁与未读计数
|
|
if conv.state == DialogState.MINIMIZED or conv.state == DialogState.CLOSED:
|
|
conv.is_blinking = true
|
|
conv.unread_count += 1
|
|
res["triggered_blink"] = true
|
|
|
|
res["delivered"] = true
|
|
res["conversation"] = conv
|
|
return res
|
|
|
|
# 5. 发送私聊消息
|
|
func send_whisper(target_name: String, text: String) -> Dictionary:
|
|
var res := { "success": false, "error_code": "OK", "message": "" }
|
|
|
|
if target_name.is_empty():
|
|
res["error_code"] = "EMPTY_TARGET"
|
|
return res
|
|
|
|
if text.strip_edges().is_empty():
|
|
res["error_code"] = "EMPTY_MESSAGE"
|
|
return res
|
|
|
|
var conv = open_conversation(target_name)
|
|
var msg = WhisperMessage.new(main_player_name, text, current_time, false)
|
|
conv.history.append(msg)
|
|
|
|
res["success"] = true
|
|
return res
|
|
|
|
# 6. 拉黑/解除拉黑目标 (1:1 player.ToggleIgnoreTarget)
|
|
func toggle_ignore_target(target_name: String) -> bool:
|
|
if target_name.is_empty():
|
|
return false
|
|
|
|
if ignored_targets.has(target_name):
|
|
ignored_targets.erase(target_name)
|
|
return false # 现在未拉黑
|
|
else:
|
|
ignored_targets[target_name] = true
|
|
return true # 现在已拉黑
|
|
|
|
func is_ignored(target_name: String) -> bool:
|
|
return ignored_targets.get(target_name, false)
|
|
|
|
# 7. 获取当前所有在任务栏闪烁的会话列表
|
|
func get_blinking_conversations() -> Array:
|
|
var list: Array = []
|
|
for k in conversations:
|
|
var c: WhisperConversation = conversations[k]
|
|
if c.is_blinking:
|
|
list.append(c)
|
|
return list
|