Files

202 lines
10 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# chat_test —— P3:聊天窗路由 + 前缀解析 + 气泡 headless 自检。
# godot --headless --path project --script chat_test.gd
extends SceneTree
const ChatUI = preload("res://ui/chat_ui.gd")
const NetWorld = preload("res://net_world.gd")
class FakeClient extends Node:
signal chat(type: int, vid: int, text: String)
# 注:GDScript 不允许 signal 和 func 同名;真 M2Client 是 GDExtension,两者可共存。
# 这里 whisper 用作方法(chat_ui 调 client.whisper(...));入站 whisper 直接调 chat._on_whisper。
signal item_picked_up(vnum: int, count: int, from: String)
signal fishing_event(subheader: int, info: int, dir: int)
# net_world 也连这些:
signal entity_spawned(entity: Dictionary)
signal entity_despawned(vid: int)
signal entity_moved(vid: int)
signal entity_main_set(vid: int)
signal entity_dead(vid: int)
signal vitals_changed(vid: int)
signal damage(vid: int, amount: int, flag: int)
var ents := {1000: {"vid": 1000, "name": "Me"}, 2000: {"vid": 2000, "name": "Bob"}}
var said := []
var whispered := []
func get_entity(vid: int) -> Dictionary: return ents.get(vid, {})
func get_entities() -> Array: return ents.values()
func say(t, s) -> bool: said.append([t, s]); return true
func whisper(to, s) -> bool: whispered.append([to, s]); return true
func get_guild_name(gid: int) -> String: return "Camelot" if gid != 0 else ""
class FakeProto extends Node:
func item(vnum: int) -> Dictionary:
if vnum == 30001:
return {"vnum": vnum, "type": 12, "locale_name": "Trout"}
return {}
var _fail := 0
func _ck(c: bool, m: String) -> void:
if not c:
_fail += 1
printerr("FAIL: " + m)
func _init() -> void:
_run()
if _fail == 0:
print("PASS: chat_test (routing + prefixes + bubble)")
quit(0)
else:
printerr("%d check(s) failed" % _fail)
quit(1)
func _last(rt: RichTextLabel) -> String:
return rt.get_parsed_text().strip_edges().split("\n")[-1]
func _run() -> void:
var canvas := CanvasLayer.new()
get_root().add_child(canvas)
var fc := FakeClient.new()
get_root().add_child(fc)
var chat: Node = ChatUI.new()
get_root().add_child(chat)
chat.setup(fc, canvas, AssetRoot.path())
var proto := FakeProto.new()
get_root().add_child(proto)
chat.set_proto(proto)
_ck(chat._log.size() == 4, "4 tabs built")
var tAll: RichTextLabel = chat._log[0]
var tWhisper: RichTextLabel = chat._log[1]
var tSys: RichTextLabel = chat._log[2]
var tBattle: RichTextLabel = chat._log[3]
# 普通聊天 -> 全部标签,带名字
fc.chat.emit(0, 2000, "hi all")
_ck(_last(tAll).contains("Bob: hi all"), "talking -> All tab with name")
_ck(chat._buffers[2].size() == 0, "talking not in System tab")
# info -> 系统 + 全部
fc.chat.emit(1, 0, "you leveled up")
_ck(_last(tSys).contains("you leveled up"), "info -> System tab")
_ck(_last(tAll).contains("you leveled up"), "info -> All tab too")
# whisper -> 私聊 + 全部(入站直接调 handler,见 FakeClient 注释)
chat._on_whisper(0, "Alice", "secret")
_ck(_last(tWhisper).contains("Alice: secret"), "whisper -> Whisper tab")
_ck(_last(tAll).contains("Alice: secret"), "whisper -> All tab")
chat._on_whisper(1, "", "server notice")
_ck(_last(tWhisper).contains("[系统]: server notice"), "system whisper -> [系统]")
# 拾取 -> 系统 + 战斗
fc.item_picked_up.emit(27, 3, "")
_ck(_last(tSys).contains("拾取 #27 x3"), "pickup -> System")
_ck(_last(tBattle).contains("拾取 #27 x3"), "pickup -> Battle")
# 钓鱼服务端事件:SUCCESS 只切动画;FISH 按 IsFishing() + item type
# 走 ClientVS22 的 notify/success locale 分支。
fc.fishing_event.emit(0, 1000, 0)
fc.fishing_event.emit(5, 30001, 0)
_ck(_last(tBattle).contains("Trout") and _last(tBattle).contains("hooked"),
"fishing notify fish -> Battle locale")
fc.fishing_event.emit(3, 1000, 0)
fc.fishing_event.emit(5, 30001, 0)
_ck(_last(tSys).contains("You captured Trout!"), "fishing success fish -> System locale")
chat.on_fishing_feedback("FISHING_WRONG_PLACE")
_ck(_last(tSys).contains("You cannot go fishing here."), "wrong place -> locale")
# --- 前缀解析 ---
chat._on_submit("hello world")
_ck(fc.said.size() == 1 and fc.said[0] == [0, "hello world"], "plain -> say(TALKING)")
chat._on_submit("/g guild msg")
_ck(fc.said.size() == 2 and fc.said[1] == [4, "guild msg"], "/g -> say(GUILD)")
chat._on_submit("/s shout!")
_ck(fc.said[2] == [6, "shout!"], "/s -> say(SHOUT)")
chat._on_submit("/w Bob psst hey")
_ck(fc.whispered.size() == 1 and fc.whispered[0] == ["Bob", "psst hey"], "/w Bob ... -> whisper('Bob','psst hey')")
_ck(_last(tWhisper).contains("→ Bob: psst hey"), "/w -> local echo in Whisper tab")
# game.py OpenWhisperDialog:目标框独立于公共聊天输入行,回车发送后关闭。
chat.start_whisper("Carol")
_ck(chat.is_whisper_open() and chat.whisper_target() == "Carol",
"start_whisper -> 独立私聊窗口打开并锁定目标")
chat._on_whisper_submit("private window")
_ck(fc.whispered.size() == 2 and fc.whispered[1] == ["Carol", "private window"],
"独立私聊窗口发送 -> whisper('Carol', msg)")
_ck(not chat.is_whisper_open(), "私聊发送后关闭独立窗口")
# --- 气泡 ---
var nw: Node = NetWorld.new()
get_root().add_child(nw)
var mount := Node3D.new()
get_root().add_child(mount)
nw.setup(fc, mount)
fc.entity_spawned.emit({"vid": 2000, "name": "Bob", "pos": Vector3.ZERO, "func": 0, "angle_deg": 0.0})
await process_frame
var got_main := [""]
nw.main_bubble.connect(func(t): got_main[0] = t)
nw._bubble(2000, "hi there")
var bob: Node3D = nw.node_for(2000)
_ck(bob != null and bob.get_node_or_null("bubble") != null, "entity 2000 got a bubble node")
# §8.8 seam ⑨(UpdateShowingTextTail :110-114):有「聊天」气泡时名字尾标顶到气泡上方 17px。
var bob_name := bob.get_node_or_null("Label3D") as Label3D
var bob_bubble := bob.get_node_or_null("bubble") as Label3D
_ck(bob_name != null and bob_bubble != null, "entity 2000 has Label3D + bubble")
_ck(absf(bob_name.position.y - (bob_bubble.position.y + 17.0 * bob_name.pixel_size)) < 0.0001,
"聊天气泡在 -> 名字 y = 气泡 y + 17*pixel_size")
_ck(bob_name.position.y > bob_bubble.position.y, "名字尾标顶到气泡上方")
# §8.8 seam ⑦ 收口(增量 119 / ArrangeTextTail :228/:258/:283):公会名 / 称号 / 等级
# 子标签锚在名字尾标上,名字被气泡顶起时三者整体平移同一段。
var g_tag := bob.get_node_or_null("GuildTag") as Label3D
var t_tag := bob.get_node_or_null("TitleTag") as Label3D
var l_tag := bob.get_node_or_null("LevelTag") as Label3D
_ck(g_tag != null and t_tag != null and l_tag != null, "entity 2000 has 公会/称号/等级子标签")
var lift := bob_name.position.y - 2.1
_ck(lift > 0.0, "名字尾标确有上移量")
_ck(absf(g_tag.position.y - (2.34 + lift)) < 0.0001, "GuildTag 随名字上移同一段")
_ck(absf(t_tag.position.y - (1.86 + lift)) < 0.0001, "TitleTag 随名字上移同一段")
_ck(absf(l_tag.position.y - (1.74 + lift)) < 0.0001, "LevelTag 随名字上移同一段")
# 信息尾标(bNameFlag FALSE)不顶名字 —— 复用同一张 _chat_tails,覆盖成 info 后名字回默认。
nw.register_info_tail(2000, "skill fizzled")
_ck(absf(bob_name.position.y - 2.1) < 0.0001, "信息尾标覆盖 -> 名字回默认局部 y 2.1")
_ck(absf(g_tag.position.y - 2.34) < 0.0001, "信息尾标覆盖 -> GuildTag 回默认 2.34")
_ck(absf(t_tag.position.y - 1.86) < 0.0001, "信息尾标覆盖 -> TitleTag 回默认 1.86")
_ck(absf(l_tag.position.y - 1.74) < 0.0001, "信息尾标覆盖 -> LevelTag 回默认 1.74")
# --- §8.8 seam ⑦ 水平排版(增量 121 / ArrangeTextTail :211-320 非 EUROPE 分支)------
# charlie 有公会 + 善恶度(称号) + 等级 -> 三子标签沿名字锚点水平排开:
# 等级在最左、称号居中偏左、名字本身右移 fxAdd(=8);公会名(无徽记)留 x = 0。
fc.ents[3000] = {"vid": 3000, "name": "Charlie", "guild": 7, "alignment": 15000, "level": 42}
fc.entity_spawned.emit({"vid": 3000, "name": "Charlie", "pos": Vector3(3, 0, 0), "func": 0, "angle_deg": 0.0})
await process_frame
var charlie: Node3D = nw.node_for(3000)
_ck(charlie != null, "entity 3000 spawned")
nw._apply_text_tail(3000)
var c_name := charlie.get_node_or_null("Label3D") as Label3D
var c_g := charlie.get_node_or_null("GuildTag") as Label3D
var c_t := charlie.get_node_or_null("TitleTag") as Label3D
var c_l := charlie.get_node_or_null("LevelTag") as Label3D
_ck(c_g.text == "Camelot", "GuildTag 文本经 get_guild_name 解出")
_ck(c_t.text != "", "TitleTag 文本非空(alignment 15000 -> grade 0 -> 有称号)")
_ck(c_l.text != "", "LevelTag 文本非空(level 42")
# 名字本身右移 fxAdd —— 有称号 -> 8px 折世界。
_ck(absf(c_name.position.x - 8.0 * c_name.pixel_size) < 0.0001,
"名字 x = name_x_shift(有称号)=8 * pixel_size")
# 称号 / 等级在名字左侧(x < 0),等级比称号更靠左。
_ck(c_t.position.x < 0.0, "称号在名字锚点左侧")
_ck(c_l.position.x < c_t.position.x, "等级比称号更靠左(-nameW/2 - 8 - titleW")
# 公会名无徽记 -> 留在名字中线正上方。
_ck(absf(c_g.position.x) < 0.0001, "GuildTag x = 0poc 无公会徽记)")
# 精确值:nameW = len("Charlie")*7 = 49,称号 x 偏移 = -(49/2) - 8 = -32.5。
_ck(absf(c_t.position.x - (-(7.0 * 7.0 / 2.0) - 8.0) * c_t.pixel_size) < 0.0001,
"称号 x = (-nameW/2 - 8) * pixel_size 精确")
# 撤销称号(alignment -> 0grade 4 = TITLE_NONE)后称号 x 归零、名字改右移 4(只剩等级)。
fc.ents[3000]["alignment"] = 0
nw._apply_text_tail(3000)
_ck(c_t.text == "" and absf(c_t.position.x) < 0.0001, "称号消失 -> TitleTag x 归零")
_ck(absf(c_name.position.x - 4.0 * c_name.pixel_size) < 0.0001,
"只剩等级 -> 名字 x = name_x_shift(仅等级)=4 * pixel_size")
# 精确值(无称号):等级 x 偏移 = -(nameW/2) - 4 = -(49/2) - 4 = -28.5。
_ck(absf(c_l.position.x - (-(7.0 * 7.0 / 2.0) - 4.0) * c_l.pixel_size) < 0.0001,
"只剩等级:等级 x = (-nameW/2 - 4) * pixel_size 精确")
nw.set_local_vid(1000)
nw._bubble(1000, "my line")
_ck(got_main[0] == "my line", "local vid bubble -> main_bubble signal")