Complete remaining client gap features
This commit is contained in:
+342
-41
@@ -21,7 +21,12 @@ const NameColor = preload("res://name_color.gd")
|
||||
const TextTail = preload("res://text_tail.gd")
|
||||
const ChatTail = preload("res://chat_tail.gd")
|
||||
const NameShow = preload("res://name_show.gd")
|
||||
const DamageEffect = preload("res://damage_effect.gd")
|
||||
const EntityRules = preload("res://entity_rules.gd")
|
||||
const PickShow = preload("res://pick_show.gd")
|
||||
const NameTailLayout = preload("res://name_tail_layout.gd")
|
||||
const TextMetrics = preload("res://text_metrics.gd")
|
||||
const ScreenHpBar = preload("res://screen_hp_bar.gd")
|
||||
|
||||
signal entity_added(node: Node3D, vid: int)
|
||||
signal entity_removed(vid: int)
|
||||
@@ -43,9 +48,25 @@ const FUNC_MOB_SKILL := 4
|
||||
const FUNC_EMOTION := 5
|
||||
const FUNC_SKILL := 0x80
|
||||
|
||||
# EDamageFlag
|
||||
const DAMAGE_DODGE := 1 << 2
|
||||
const DAMAGE_CRITICAL := 1 << 5
|
||||
# EDamageFlag / IsShowDamage / ProcessDamage 分类见 damage_effect.gd(DamageEffect.DAMAGE_*)。
|
||||
|
||||
# §8.8 头顶名字 / 聊天气泡 billboard 的默认局部 y(世界单位)。seam ⑨(name_tail_layout.gd):
|
||||
# 有「聊天」气泡时名字尾标顶到气泡上方 17px(换算按名字 Label3D 的 pixel_size)。
|
||||
const NAME_TAG_DEFAULT_Y := 2.1
|
||||
const BUBBLE_TAG_Y := 2.35
|
||||
|
||||
# §8.8 公会名 / 称号 / 等级子标签的默认局部 y(竖排近似:公会名在名字上、称号 / 等级在下)。
|
||||
# seam ⑦(name_tail_layout.gd):参考端 ArrangeTextTail 把它们锚在名字尾标 pTextTail->y 上,
|
||||
# 名字因聊天气泡上移时三者整体跟随——_reposition_name_tag 按 name_tail_lift_delta 补这段位移。
|
||||
const SUBTAG_DEFAULT_Y := {"GuildTag": 2.34, "TitleTag": 1.86, "LevelTag": 1.74}
|
||||
# seam ⑦ 水平排版(增量 121):ArrangeTextTail 按 GetTextSize 精确像素排称号 / 等级,
|
||||
# poc 无字体度量 -> 按「字符数 * 约 7px」估文本宽(与 ground_items 的 TAG_CHAR_W_PX 同源)。
|
||||
const SUBTAG_CHAR_W_PX := 7.0
|
||||
|
||||
# §8.8:参考端的实体 HP 条属于屏幕尾标,而不是 3D billboard。条的锚点
|
||||
# 保持在原占位条高度附近;实际像素位置由 Camera3D.unproject_position() 计算。
|
||||
const HP_BAR_WORLD_Y := 1.95
|
||||
const HP_BAR_SCREEN_MARGIN := 32.0
|
||||
|
||||
# CLIENT-GAP §2.1 two-layer model. The data layer (M2Client / EntityStore) always
|
||||
# holds every known entity; a scene node under `parent` exists only while the
|
||||
@@ -88,11 +109,22 @@ var _fading := {} # vid:int -> true while a culled node twee
|
||||
# §8.4 / §8.6 头顶名字「是否显示」(name_show.gd)。game.py::__IsShowName() 的两个来源:
|
||||
# _always_show_name —— uigameoption「总显示名字」radio(CPythonSystem::IsAlwaysShowName)
|
||||
# _alt_held —— LALT 按住(game.py ShowName/HideName 的 ShowNameFlag)
|
||||
# 另加 forced 路径:_target_vid(targetBoard.GetTargetVID())+ chat 尾标 bNameFlag。
|
||||
# 另加 forced 路径:_target_vid(targetBoard.GetTargetVID())+ chat 尾标 bNameFlag +
|
||||
# _hover_vid(game.py OnRender chr.Pick() -> ShowCharacterTextTail,见 pick_show.gd)。
|
||||
var _always_show_name := NameShow.ALWAYS_SHOW_NAME_DEFAULT
|
||||
var _alt_held := false
|
||||
var _target_vid := 0
|
||||
var _hover_vid := 0 # player_controller._update_hover_cursor 射线拾到的角色 vid(0 = 无)
|
||||
# §8.8 / §5.4 伤害飘字(damage_effect.gd)。CPythonSystem::IsShowDamage 门(uigameoption
|
||||
# 「伤害数字」radio)+ CInstanceBase::m_DamageQueue(每帧 pop 一条)+ ProcessDamage 分类。
|
||||
var _show_damage := DamageEffect.SHOW_DAMAGE_DEFAULT
|
||||
var _damage_queue: Array = [] # m_DamageQueue:{vid, amount, flag, self, target}
|
||||
var _dmg_self_toggle := false # m_bDamageEffectType(SELFDAMAGE / SELFDAMAGE2 交替)
|
||||
var _fly_mgr: Node # FlyManager(fly_object.gd)—— GC_CREATE_FLY 的真实弹道 + 命中判定。§3.6
|
||||
var _hp_layer: CanvasLayer # 屏幕空间尾标层(低于 HUD / UiManager)
|
||||
var _hp_overlay: Control
|
||||
var _hp_bars := {} # vid:int -> ScreenHpBar
|
||||
var _camera: Camera3D
|
||||
|
||||
# Fishing surface provider bridge. NetWorld owns entity presentation while
|
||||
# Metin2World owns map data; keep the ClientVS22 GetFishingRot query on the
|
||||
@@ -140,12 +172,28 @@ func setup(m2client: Node, mount: Node3D) -> void:
|
||||
client.pvp_changed.connect(_on_pvp_changed)
|
||||
if client.has_signal("duel_changed"):
|
||||
client.duel_changed.connect(_on_duel_changed)
|
||||
# Guild marks are downloaded by AppFlow on the side connection. The mark
|
||||
# store is independent from the entity stream, so a character can already
|
||||
# be visible when its 16x12 image arrives; refresh the affected text tails
|
||||
# on both the initial completion and an incremental GC_MARK_UPDATE.
|
||||
if client.has_signal("guild_marks_ready"):
|
||||
client.guild_marks_ready.connect(func(_count: int): _retint_all_text_tails())
|
||||
if client.has_signal("guild_mark_updated"):
|
||||
client.guild_mark_updated.connect(func(guild_id: int, _img_idx: int): _refresh_guild_mark(guild_id))
|
||||
# §3.6 飞行物实体:GC_CREATE_FLY 之后由 FlyManager 跑真实弹道 + 线段命中判定。
|
||||
_fly_mgr = FlyObject.new()
|
||||
_fly_mgr.name = "FlyManager"
|
||||
add_child(_fly_mgr)
|
||||
_fly_mgr.setup(parent, world)
|
||||
_fly_mgr.exploded.connect(func(wp: Vector3, cause: String): fly_exploded.emit(wp, cause))
|
||||
_ensure_hp_overlay()
|
||||
|
||||
# GameScene normally calls this after creating the orbit camera. Keeping the
|
||||
# setter explicit makes camera hand-off / map reload deterministic and leaves
|
||||
# headless tests independent of the active viewport camera.
|
||||
func set_camera(camera: Camera3D) -> void:
|
||||
_camera = camera
|
||||
_update_hp_bars()
|
||||
|
||||
# 已在局内(重连 / setup 是协程,进来时 spawn burst 已被 pump 抽干)——
|
||||
# 把当前所有实体补建一遍,别漏掉进游戏那一批怪 / NPC / 玩家。
|
||||
@@ -173,6 +221,7 @@ func set_local_vid(vid: int) -> void:
|
||||
if n:
|
||||
_by_vid.erase(vid)
|
||||
n.queue_free()
|
||||
_remove_hp_bar(vid)
|
||||
|
||||
func set_local_node(node: Node3D) -> void:
|
||||
_local_node = node
|
||||
@@ -192,6 +241,10 @@ func set_always_show_name(on: bool) -> void:
|
||||
_always_show_name = on
|
||||
_refresh_all_name_visibility()
|
||||
|
||||
# uigameoption「伤害数字」radio -> CPythonSystem::SetShowDamageFlag(AddDamageEffect 的入队门)。
|
||||
func set_show_damage(on: bool) -> void:
|
||||
_show_damage = on
|
||||
|
||||
# targetBoard.GetTargetVID():目标始终强显名字(game.py OnRender ShowCharacterTextTail(targetVID))。
|
||||
func set_target_vid(vid: int) -> void:
|
||||
if _target_vid == vid:
|
||||
@@ -202,6 +255,18 @@ func set_target_vid(vid: int) -> void:
|
||||
if v != 0 and _by_vid.has(v):
|
||||
_set_name_visibility(_by_vid[v])
|
||||
|
||||
# game.py OnRender:wndMgr.IsPickedWindow 门内 chr.Pick() 命中的角色 -> ShowCharacterTextTail
|
||||
# (强显名字,绕过 __IsShowName())。poc 由 player_controller 的射线悬停拾取喂入。
|
||||
# 见 pick_show.gd(第 27 个 canonical)与增量 113 的 _target_vid 同路数。
|
||||
func set_hover_vid(vid: int) -> void:
|
||||
if _hover_vid == vid:
|
||||
return
|
||||
var prev := _hover_vid
|
||||
_hover_vid = vid
|
||||
for v in [prev, vid]:
|
||||
if v != 0 and _by_vid.has(v):
|
||||
_set_name_visibility(_by_vid[v])
|
||||
|
||||
func _refresh_all_name_visibility() -> void:
|
||||
for node in _by_vid.values():
|
||||
_set_name_visibility(node)
|
||||
@@ -215,7 +280,7 @@ func _main_planar_cm() -> Vector2:
|
||||
return Vector2(mc.x, mc.y)
|
||||
return Vector2(INF, INF)
|
||||
|
||||
# name_show.name_visible_for 的 net_world 侧组装:forced(target / chat bNameFlag)+
|
||||
# name_show.name_visible_for 的 net_world 侧组装:forced(target / hover / chat bNameFlag)+
|
||||
# __IsShowName()(_always_show_name or _alt_held)+ 3500 平面距 + isShow/wall/CanPick 资格。
|
||||
func _name_shown(root: Node3D) -> bool:
|
||||
if root == null or not is_instance_valid(root):
|
||||
@@ -228,7 +293,9 @@ func _name_shown(root: Node3D) -> bool:
|
||||
var is_wall := _is_wall(int(e.get("race", 0)))
|
||||
# CanPickInstance() 更细(隐身 / 特定 affect 亦不可拾取)—— 这里只按死透近似,记 seam ⑩。
|
||||
var alive := not dead
|
||||
var forced := vid != 0 and vid == _target_vid
|
||||
# game.py OnRender:targetBoard.GetTargetVID()(增量 113)+ chr.Pick() 悬停命中
|
||||
# (_hover_vid,增量 116 / pick_show.gd)都走 ShowCharacterTextTail 强显。
|
||||
var forced := vid != 0 and (vid == _target_vid or vid == _hover_vid)
|
||||
var ct: Variant = _chat_tails.get(vid, null)
|
||||
if ct is Dictionary and bool(ct.get("name_flag", false)):
|
||||
forced = true
|
||||
@@ -253,6 +320,10 @@ func _set_name_visibility(root: Node3D) -> void:
|
||||
var st := root.get_node_or_null(sub) as Label3D
|
||||
if st:
|
||||
st.visible = shown and st.text != ""
|
||||
var mark := root.get_node_or_null("GuildMark") as Sprite3D
|
||||
if mark:
|
||||
var guild_tag := root.get_node_or_null("GuildTag") as Label3D
|
||||
mark.visible = shown and guild_tag != null and guild_tag.text != ""
|
||||
|
||||
# ClientVS22 destroys the network actor manager when Loading starts. This is
|
||||
# called from M2Client.world_reset before the replacement connection sends its
|
||||
@@ -264,7 +335,9 @@ func clear_for_map_change() -> void:
|
||||
node.queue_free()
|
||||
entity_removed.emit(int(vid))
|
||||
_by_vid.clear()
|
||||
_clear_hp_bars()
|
||||
_fading.clear()
|
||||
_damage_queue.clear()
|
||||
_main_vid = 0
|
||||
_pvp_relations.clear()
|
||||
_duel_opponents.clear()
|
||||
@@ -325,6 +398,7 @@ func _on_spawn(d: Dictionary) -> void:
|
||||
var ch_type := int(d.get("ch_type", 0))
|
||||
node.set_meta("cursor_shape", "TALK" if ch_type in [1, 4] else "ATTACK")
|
||||
_by_vid[vid] = node
|
||||
_sync_hp_bar(vid, d)
|
||||
_set_name_visibility(node)
|
||||
_refresh_shop_sign(node, str(d.get("shop_sign", "")))
|
||||
_refresh_pvp_tag(vid)
|
||||
@@ -340,6 +414,7 @@ func _on_info(vid: int, d: Dictionary) -> void:
|
||||
var n: Node3D = _by_vid.get(vid, null)
|
||||
if n == null:
|
||||
return
|
||||
_sync_hp_bar(vid, d)
|
||||
var nm := _display_name(d)
|
||||
if nm != "":
|
||||
if n.has_node("Label3D"):
|
||||
@@ -628,6 +703,90 @@ func _apply_text_tail_to(n: Node3D, e: Dictionary) -> void:
|
||||
ltag.modulate = lv["color"]
|
||||
ltag.visible = shown and ltag.text != ""
|
||||
|
||||
_apply_guild_mark(n, guild_id, gtag, shown)
|
||||
_layout_subtags_x(n, ttag, ltag)
|
||||
# Keep the mark on the same adjusted screen row as the name immediately on
|
||||
# spawn/info refresh, not only after the next chat-tail update.
|
||||
_reposition_name_tag(n)
|
||||
|
||||
# ArrangeTextTail 角色块(PythonTextTail.cpp:211-320 非 EUROPE 分支)的逐像素水平排布:
|
||||
# 称号在名字锚点左侧 nameW/2 + 8、等级再往左一个称号宽(无称号时 nameW/2 + 4)、
|
||||
# 名字本身右移 fxAdd(8 / 4 / 0)。公会名的水平锚点参考端只在「有公会徽记」时才改写,
|
||||
# poc 无徽记 -> GuildTag 留 x = 0(名字中线正上方)。文本宽度取各 Label3D 的真实字体度量
|
||||
# (TextMetrics.label_width = GetTextSize 1:1,增量 123;SUBTAG_CHAR_W_PX 仅作无字体兜底)。
|
||||
# 经 name_tail_layout.gd 纯静态镜像。
|
||||
func _layout_subtags_x(n: Node3D, ttag: Label3D, ltag: Label3D) -> void:
|
||||
var nlbl := n.get_node_or_null("Label3D") as Label3D
|
||||
var gtag := n.get_node_or_null("GuildTag") as Label3D
|
||||
var mark := n.get_node_or_null("GuildMark") as Sprite3D
|
||||
var has_title := ttag != null and ttag.text != ""
|
||||
var has_level := ltag != null and ltag.text != ""
|
||||
var name_w := 0.0
|
||||
if nlbl:
|
||||
name_w = maxf(1.0, TextMetrics.label_width(nlbl, SUBTAG_CHAR_W_PX))
|
||||
var title_w := 0.0
|
||||
if has_title:
|
||||
title_w = maxf(1.0, TextMetrics.label_width(ttag, SUBTAG_CHAR_W_PX))
|
||||
if nlbl:
|
||||
nlbl.position.x = NameTailLayout.subtag_world_x(
|
||||
NameTailLayout.name_x_shift(has_title, has_level), nlbl.pixel_size)
|
||||
if ttag:
|
||||
ttag.position.x = NameTailLayout.subtag_world_x(
|
||||
NameTailLayout.title_x_offset(name_w), ttag.pixel_size) if has_title else 0.0
|
||||
if ltag:
|
||||
ltag.position.x = NameTailLayout.subtag_world_x(
|
||||
NameTailLayout.level_x_offset(name_w, title_w, has_title), ltag.pixel_size) if has_level else 0.0
|
||||
if gtag:
|
||||
if mark != null and mark.texture != null and gtag.text != "":
|
||||
var guild_w := maxf(1.0, TextMetrics.label_width(gtag, SUBTAG_CHAR_W_PX))
|
||||
var mark_w := maxf(1.0, float(mark.texture.get_width()))
|
||||
gtag.position.x = NameTailLayout.subtag_world_x(
|
||||
NameTailLayout.guild_name_x_offset(mark_w), gtag.pixel_size)
|
||||
mark.position.x = NameTailLayout.subtag_world_x(
|
||||
NameTailLayout.mark_x_offset(guild_w, mark_w), mark.pixel_size)
|
||||
else:
|
||||
gtag.position.x = 0.0 # 无已下载会徽时保持参考端 if(pMark && pGuild) 不成立
|
||||
if mark:
|
||||
mark.position.x = 0.0
|
||||
|
||||
func _apply_guild_mark(n: Node3D, guild_id: int, guild_tag: Label3D, shown: bool) -> void:
|
||||
# CPythonTextTail only creates pMarkInstance when a guild id resolves to a
|
||||
# downloaded mark. Do not leave a stale image after a guild change or a
|
||||
# mark-table refresh that no longer contains this guild.
|
||||
var mark := n.get_node_or_null("GuildMark") as Sprite3D
|
||||
if guild_id == 0 or guild_tag == null or guild_tag.text == "" \
|
||||
or client == null or not client.has_method("get_guild_mark_image"):
|
||||
if mark:
|
||||
mark.queue_free()
|
||||
return
|
||||
var img: Image = client.get_guild_mark_image(guild_id)
|
||||
if img == null or img.get_width() <= 0 or img.get_height() <= 0:
|
||||
if mark:
|
||||
mark.queue_free()
|
||||
return
|
||||
if mark == null:
|
||||
mark = Sprite3D.new()
|
||||
mark.name = "GuildMark"
|
||||
mark.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
mark.no_depth_test = true
|
||||
mark.pixel_size = 0.005
|
||||
mark.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST
|
||||
n.add_child(mark)
|
||||
mark.texture = ImageTexture.create_from_image(img)
|
||||
mark.visible = shown
|
||||
mark.set_meta("guild_id", guild_id)
|
||||
|
||||
func _refresh_guild_mark(guild_id: int) -> void:
|
||||
if guild_id == 0 or client == null or not client.has_method("get_entities"):
|
||||
return
|
||||
for vid in _by_vid.keys():
|
||||
var n: Node3D = _by_vid[vid]
|
||||
if not is_instance_valid(n):
|
||||
continue
|
||||
var e: Dictionary = client.get_entity(int(vid))
|
||||
if int(e.get("guild", 0)) == guild_id:
|
||||
_apply_text_tail_to(n, e)
|
||||
|
||||
func _apply_text_tail(vid: int) -> void:
|
||||
if vid == 0 or client == null or not client.has_method("get_entity"):
|
||||
return
|
||||
@@ -660,6 +819,7 @@ func _on_despawn(vid: int) -> void:
|
||||
_duel_opponents.erase(vid)
|
||||
_fading.erase(vid)
|
||||
_chat_tails.erase(vid)
|
||||
_remove_hp_bar(vid)
|
||||
var n: Node3D = _by_vid.get(vid, null)
|
||||
if n:
|
||||
_by_vid.erase(vid)
|
||||
@@ -822,7 +982,7 @@ func _apply_chat_tail(n: Node3D, tail: Dictionary) -> void:
|
||||
if lbl == null:
|
||||
lbl = Label3D.new()
|
||||
lbl.name = "bubble"
|
||||
lbl.position.y = 2.35
|
||||
lbl.position.y = BUBBLE_TAG_Y
|
||||
lbl.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
lbl.no_depth_test = true
|
||||
lbl.pixel_size = 0.0055
|
||||
@@ -832,6 +992,8 @@ func _apply_chat_tail(n: Node3D, tail: Dictionary) -> void:
|
||||
lbl.modulate = tail.get("color", ChatTail.CHAT_COLOR)
|
||||
# UpdateAllTextTail:chat 尾标 bNameFlag TRUE 时强制显示该角色名字(不受 3500 限制)。
|
||||
_set_name_visibility(n)
|
||||
# UpdateShowingTextTail :110-114:有「聊天」气泡(bNameFlag)时名字尾标顶到气泡上方 17px。
|
||||
_reposition_name_tag(n)
|
||||
|
||||
func _expire_chat_tails() -> void:
|
||||
# UpdateAllTextTail:LivingTime < now 即删除对应 "bubble" 节点
|
||||
@@ -843,6 +1005,40 @@ func _expire_chat_tails() -> void:
|
||||
lbl.queue_free()
|
||||
# 尾标没了 -> bNameFlag 强显撤销,名字回到 __IsShowName / 距离裁决。
|
||||
_set_name_visibility(n)
|
||||
# 气泡没了 -> 名字尾标从「贴气泡上方」回默认局部 y(chat 尾标不在
|
||||
# m_ChatTailMap 里,UpdateShowingTextTail :110 的 find 落空)。
|
||||
_reposition_name_tag(n)
|
||||
|
||||
# CPythonTextTail::UpdateShowingTextTail :101-118 —— 角色名字尾标投影后,若该 VID 有一条
|
||||
# 「聊天」气泡(m_ChatTailMap 命中且 bNameFlag TRUE,即 RegisterChatTail 而非
|
||||
# RegisterInfoTail),把名字尾标顶到气泡上方 17px(屏幕 y - 17 → poc 世界 y + 17*pixel_size)。
|
||||
# 经 name_tail_layout.gd 纯静态镜像。
|
||||
func _reposition_name_tag(n: Node3D) -> void:
|
||||
if n == null or not is_instance_valid(n):
|
||||
return
|
||||
var tag := n.get_node_or_null("Label3D") as Label3D
|
||||
if tag == null:
|
||||
return
|
||||
var vid := int(n.get_meta("vid", 0))
|
||||
var ct: Variant = _chat_tails.get(vid, null)
|
||||
var has_tail := ct is Dictionary
|
||||
var name_flag := has_tail and bool((ct as Dictionary).get("name_flag", false))
|
||||
var follows := NameTailLayout.name_follows_bubble(has_tail, name_flag)
|
||||
tag.position.y = NameTailLayout.name_tail_world_y(
|
||||
NAME_TAG_DEFAULT_Y, BUBBLE_TAG_Y, follows, tag.pixel_size)
|
||||
# ArrangeTextTail :228/:258/:283 —— 公会名 / 称号 / 等级子标签都锚在名字尾标 pTextTail->y
|
||||
# 上,名字因聊天气泡上移时三者整体跟随同一段位移(seam ⑦ 仅剩竖排像素排布近似)。
|
||||
var lift := NameTailLayout.name_tail_lift_delta(
|
||||
NAME_TAG_DEFAULT_Y, BUBBLE_TAG_Y, follows, tag.pixel_size)
|
||||
for sub_name in SUBTAG_DEFAULT_Y:
|
||||
var st := n.get_node_or_null(sub_name) as Label3D
|
||||
if st:
|
||||
st.position.y = NameTailLayout.subtag_world_y(SUBTAG_DEFAULT_Y[sub_name], lift)
|
||||
var mark := n.get_node_or_null("GuildMark") as Sprite3D
|
||||
if mark:
|
||||
# ArrangeTextTail :227 puts the mark 26 px above the name instance; it
|
||||
# follows the same already-adjusted name y when a chat tail is active.
|
||||
mark.position.y = NAME_TAG_DEFAULT_Y + NameTailLayout.MARK_ABOVE_NAME_PX * tag.pixel_size + lift
|
||||
|
||||
func _on_vitals(vid: int) -> void:
|
||||
var n: Node3D = _by_vid.get(vid, null)
|
||||
@@ -853,10 +1049,7 @@ func _on_vitals(vid: int) -> void:
|
||||
var max_hp := int(e.get("max_hp", 0))
|
||||
var dead := bool(e.get("dead", false))
|
||||
vitals.emit(vid, hp, max_hp, dead)
|
||||
if n and n.has_node("HpBar") and max_hp > 0:
|
||||
var bar := n.get_node("HpBar") as Sprite3D
|
||||
bar.scale.x = clampf(float(hp) / float(max_hp), 0.0, 1.0)
|
||||
bar.modulate = Color(0.9, 0.2, 0.2) if not dead else Color(0.3, 0.3, 0.3)
|
||||
_sync_hp_bar_values(vid, hp, max_hp, dead)
|
||||
|
||||
func _on_dead(vid: int) -> void:
|
||||
var n: Node3D = _by_vid.get(vid, null)
|
||||
@@ -984,22 +1177,54 @@ func _on_fishing_event(subheader: int, info: int, dir: int) -> void:
|
||||
n.call("set_anim_state", state)
|
||||
fishing_event.emit(info, subheader, dir)
|
||||
|
||||
# GC_DAMAGE_INFO -> RecvDamageInfoPacket -> CInstanceBase::AddDamageEffect。
|
||||
# damage_number 信号是本移植扩展(战斗日志 / 测试消费),始终发;
|
||||
# 飘字本体照 AddDamageEffect:仅 IsShowDamage() 时入 m_DamageQueue,ProcessDamage 每帧 pop 一条。
|
||||
func _on_damage(vid: int, amount: int, flag: int) -> void:
|
||||
var crit := (flag & DAMAGE_CRITICAL) != 0
|
||||
var dodge := (flag & DAMAGE_DODGE) != 0
|
||||
var crit := (flag & DamageEffect.DAMAGE_CRITICAL) != 0
|
||||
var dodge := (flag & DamageEffect.DAMAGE_DODGE) != 0
|
||||
damage_number.emit(vid, amount, crit, dodge)
|
||||
var n: Node3D = _by_vid.get(vid, null)
|
||||
if n == null or parent == null:
|
||||
if not DamageEffect.valid_amount(amount):
|
||||
return # RecvDamageInfoPacket:damage < 0 -> TraceError,不处理
|
||||
if not DamageEffect.should_queue(_show_damage):
|
||||
return # AddDamageEffect:!IsShowDamage() -> 不入队
|
||||
_damage_queue.append({
|
||||
"vid": vid, "amount": amount, "flag": flag,
|
||||
"self": DamageEffect.is_self(vid, _main_vid),
|
||||
"target": DamageEffect.is_target(vid, _target_vid),
|
||||
})
|
||||
|
||||
# ProcessDamage:每帧 pop_front 一条。DODGE|BLOCK -> "MISS";nontarget 数字 -> 不渲染;
|
||||
# 其余把 amount 铺一块 billboard Label3D(seam ⑪:无 damagevalue dds 图集 / 逐位特效)。
|
||||
func _process_damage_queue() -> void:
|
||||
if _damage_queue.is_empty():
|
||||
return
|
||||
var d: Dictionary = _damage_queue.pop_front()
|
||||
var n: Node3D = _by_vid.get(int(d["vid"]), null)
|
||||
if n == null or not is_instance_valid(n) or parent == null:
|
||||
return
|
||||
var info := DamageEffect.classify(int(d["flag"]), bool(d["self"]), bool(d["target"]))
|
||||
if not info["show"]:
|
||||
return # ProcessDamage:nontarget_ 后直接 return(不渲染)
|
||||
var text := "MISS"
|
||||
if info["kind"] == DamageEffect.Kind.DIGITS:
|
||||
if DamageEffect.digits(int(d["amount"])).is_empty():
|
||||
return # while(damage>0) 零次 -> 无贴图
|
||||
text = str(int(d["amount"]))
|
||||
# self 数字:SELFDAMAGE / SELFDAMAGE2 交替(seam ⑪:退化成落字水平抖动符号交替)。
|
||||
var jitter := 0.0
|
||||
if bool(d["self"]) and info["kind"] == DamageEffect.Kind.DIGITS:
|
||||
jitter = 0.3 if DamageEffect.self_damage_slot(_dmg_self_toggle) == 0 else -0.3
|
||||
_dmg_self_toggle = not _dmg_self_toggle
|
||||
var lbl := Label3D.new()
|
||||
lbl.text = "MISS" if dodge else str(amount)
|
||||
lbl.modulate = Color(1.0, 0.85, 0.2) if crit else Color(1, 1, 1)
|
||||
lbl.font_size = 48 if crit else 32
|
||||
lbl.text = text
|
||||
lbl.modulate = Color(1, 1, 1) # 该 build 无独立暴击 / self 颜色(ProcessDamage)
|
||||
lbl.font_size = 32
|
||||
lbl.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
lbl.no_depth_test = true
|
||||
lbl.pixel_size = 0.007
|
||||
parent.add_child(lbl)
|
||||
lbl.global_position = n.global_position + Vector3(randf_range(-0.3, 0.3), 1.9, 0)
|
||||
lbl.global_position = n.global_position + Vector3(jitter, 1.9, 0)
|
||||
var tw := create_tween()
|
||||
tw.set_parallel(true)
|
||||
tw.tween_property(lbl, "global_position:y", lbl.global_position.y + 1.2, 0.8)
|
||||
@@ -1010,6 +1235,7 @@ func _process(dt: float) -> void:
|
||||
if client == null or parent == null:
|
||||
return
|
||||
_update_visibility()
|
||||
_process_damage_queue() # §8.8:m_DamageQueue 每帧 pop 一条(CInstanceBase::ProcessDamage)
|
||||
# §8.8:名字色随队伍 / 帝国名色模式 / PVP·决斗边沿变化——用 ~2Hz 节流整表刷新
|
||||
# 兜底(单发路径已覆盖 spawn/info/pvp/duel/main_set;参考端每帧都算 GetNameColor)。
|
||||
_name_color_accum += dt
|
||||
@@ -1047,6 +1273,7 @@ func _process(dt: float) -> void:
|
||||
n.set_meta("moving", moving)
|
||||
n.set_meta("walk_mode", walk_mode)
|
||||
_apply_anim(n, f, moving, walk_mode)
|
||||
_update_hp_bars()
|
||||
|
||||
func _apply_anim(n: Node3D, f: int, moving: bool, walk_mode := 1) -> void:
|
||||
var state := "wait"
|
||||
@@ -1086,13 +1313,14 @@ func _make_placeholder(d: Dictionary) -> Node3D:
|
||||
_attach_nameplate(root, d)
|
||||
return root
|
||||
|
||||
# 头顶名字 Label3D + HP 条 Sprite3D(占位胶囊和真模型共用)。
|
||||
# 头顶名字 Label3D + 屏幕空间 HP 条(占位胶囊和真模型共用)。HP 条由
|
||||
# _ensure_hp_overlay() 创建到 CanvasLayer,_update_hp_bars() 负责世界坐标投影。
|
||||
func _attach_nameplate(root: Node3D, d: Dictionary) -> void:
|
||||
var tag := Label3D.new()
|
||||
tag.name = "Label3D"
|
||||
var nm := _display_name(d)
|
||||
tag.text = nm if nm != "" else "vid %d" % int(d.get("vid", 0))
|
||||
tag.position.y = 2.1
|
||||
tag.position.y = NAME_TAG_DEFAULT_Y
|
||||
tag.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
tag.no_depth_test = true
|
||||
tag.pixel_size = 0.006
|
||||
@@ -1103,19 +1331,9 @@ func _attach_nameplate(root: Node3D, d: Dictionary) -> void:
|
||||
# c_fyGuildNamePosition(15px)、称号 / 等级贴右侧。这里在 billboard 3D 里近似成竖排
|
||||
# (公会名上、称号 / 等级下),精确像素排版差异记为 §8.8 seam ⑦。内容 / 颜色由
|
||||
# _apply_text_tail 按 RefreshTextTail / AttachTitle / AttachLevel 决定,初始空 = 隐藏。
|
||||
root.add_child(_mk_sub_tag("GuildTag", 2.34, TextTail.GUILD_NAME_COLOR))
|
||||
root.add_child(_mk_sub_tag("TitleTag", 1.86, Color(1, 1, 1)))
|
||||
root.add_child(_mk_sub_tag("LevelTag", 1.74, TextTail.LEVEL_COLOR))
|
||||
var bar := Sprite3D.new()
|
||||
bar.name = "HpBar"
|
||||
bar.texture = _white_1px()
|
||||
bar.pixel_size = 0.01 # 100px -> 1m
|
||||
bar.position.y = 1.95
|
||||
bar.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
bar.no_depth_test = true
|
||||
bar.modulate = Color(0.9, 0.2, 0.2)
|
||||
bar.scale.x = 1.0
|
||||
root.add_child(bar)
|
||||
root.add_child(_mk_sub_tag("GuildTag", SUBTAG_DEFAULT_Y["GuildTag"], TextTail.GUILD_NAME_COLOR))
|
||||
root.add_child(_mk_sub_tag("TitleTag", SUBTAG_DEFAULT_Y["TitleTag"], Color(1, 1, 1)))
|
||||
root.add_child(_mk_sub_tag("LevelTag", SUBTAG_DEFAULT_Y["LevelTag"], TextTail.LEVEL_COLOR))
|
||||
|
||||
func _mk_sub_tag(sub_name: String, y: float, col: Color) -> Label3D:
|
||||
var t := Label3D.new()
|
||||
@@ -1129,10 +1347,93 @@ func _mk_sub_tag(sub_name: String, y: float, col: Color) -> Label3D:
|
||||
t.visible = false
|
||||
return t
|
||||
|
||||
static var _white_tex: Texture2D
|
||||
static func _white_1px() -> Texture2D:
|
||||
if _white_tex == null:
|
||||
var img := Image.create(100, 6, false, Image.FORMAT_RGBA8)
|
||||
img.fill(Color.WHITE)
|
||||
_white_tex = ImageTexture.create_from_image(img)
|
||||
return _white_tex
|
||||
func _ensure_hp_overlay() -> void:
|
||||
if _hp_layer != null and is_instance_valid(_hp_layer):
|
||||
return
|
||||
_hp_layer = CanvasLayer.new()
|
||||
_hp_layer.name = "ScreenHpLayer"
|
||||
_hp_layer.layer = 5
|
||||
add_child(_hp_layer)
|
||||
_hp_overlay = Control.new()
|
||||
_hp_overlay.name = "HpOverlay"
|
||||
_hp_overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
_hp_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_hp_layer.add_child(_hp_overlay)
|
||||
|
||||
func _make_hp_bar(vid: int, e: Dictionary) -> Control:
|
||||
_ensure_hp_overlay()
|
||||
var bar := _hp_bars.get(vid, null) as Control
|
||||
if bar == null or not is_instance_valid(bar):
|
||||
bar = ScreenHpBar.new()
|
||||
bar.name = "hp_%d" % vid
|
||||
_hp_overlay.add_child(bar)
|
||||
_hp_bars[vid] = bar
|
||||
_sync_hp_bar_values(vid, int(e.get("hp", 0)), int(e.get("max_hp", 0)),
|
||||
bool(e.get("dead", false)))
|
||||
return bar
|
||||
|
||||
func _sync_hp_bar(vid: int, e: Dictionary) -> void:
|
||||
if vid == 0 or e.is_empty():
|
||||
return
|
||||
_make_hp_bar(vid, e)
|
||||
_sync_hp_bar_values(vid, int(e.get("hp", 0)), int(e.get("max_hp", 0)),
|
||||
bool(e.get("dead", false)))
|
||||
|
||||
func _sync_hp_bar_values(vid: int, hp: int, max_hp: int, dead: bool) -> void:
|
||||
var bar := _hp_bars.get(vid, null) as Control
|
||||
if bar == null or not is_instance_valid(bar):
|
||||
return
|
||||
if bar.has_method("set_vitals"):
|
||||
bar.call("set_vitals", hp, max_hp, dead)
|
||||
bar.visible = max_hp > 0 and not dead
|
||||
|
||||
func _remove_hp_bar(vid: int) -> void:
|
||||
var bar := _hp_bars.get(vid, null) as Control
|
||||
_hp_bars.erase(vid)
|
||||
if bar != null and is_instance_valid(bar):
|
||||
bar.queue_free()
|
||||
|
||||
func _clear_hp_bars() -> void:
|
||||
for vid in _hp_bars.keys():
|
||||
var bar := _hp_bars[vid] as Control
|
||||
if bar != null and is_instance_valid(bar):
|
||||
bar.queue_free()
|
||||
_hp_bars.clear()
|
||||
|
||||
func _update_hp_bars() -> void:
|
||||
if _hp_bars.is_empty():
|
||||
return
|
||||
var cam := _camera
|
||||
if cam == null or not is_instance_valid(cam):
|
||||
cam = get_viewport().get_camera_3d()
|
||||
var viewport_size := get_viewport().get_visible_rect().size
|
||||
if cam == null or viewport_size.x <= 0.0 or viewport_size.y <= 0.0:
|
||||
for bar in _hp_bars.values():
|
||||
if is_instance_valid(bar):
|
||||
bar.visible = false
|
||||
return
|
||||
var viewport_rect := Rect2(Vector2.ZERO, viewport_size).grow(HP_BAR_SCREEN_MARGIN)
|
||||
for vid in _hp_bars.keys():
|
||||
var bar := _hp_bars[vid] as Control
|
||||
var node := _by_vid.get(vid, null) as Node3D
|
||||
if bar == null or not is_instance_valid(bar) or node == null \
|
||||
or not is_instance_valid(node):
|
||||
if bar != null and is_instance_valid(bar):
|
||||
bar.visible = false
|
||||
continue
|
||||
var e: Dictionary = client.get_entity(int(vid))
|
||||
var max_hp := int(e.get("max_hp", 0))
|
||||
var dead := bool(e.get("dead", false))
|
||||
if max_hp <= 0 or dead:
|
||||
bar.visible = false
|
||||
continue
|
||||
var anchor := node.global_position + Vector3(0.0, HP_BAR_WORLD_Y, 0.0)
|
||||
if cam.is_position_behind(anchor):
|
||||
bar.visible = false
|
||||
continue
|
||||
var screen_pos := cam.unproject_position(anchor)
|
||||
if not viewport_rect.has_point(screen_pos):
|
||||
bar.visible = false
|
||||
continue
|
||||
bar.position = screen_pos - Vector2(bar.size.x * 0.5, 0.0)
|
||||
bar.visible = true
|
||||
|
||||
Reference in New Issue
Block a user