- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
216 lines
6.8 KiB
GDScript
216 lines
6.8 KiB
GDScript
# target_ui.gd —— 40250 目标信息板(TargetBoard)1:1 实现
|
||
# 严格对照:
|
||
# root/uitarget.py:14-340 (TargetBoard)
|
||
# UserInterface/InstanceBase.cpp (CanViewTargetHP)
|
||
# root/localeinfo.py (TARGET_LEVEL_PAWN..KING)
|
||
extends Node
|
||
|
||
const UiAssets = preload("res://ui/ui_assets.gd")
|
||
|
||
const GRADE_NAMES := {
|
||
0: "1阶", # nonplayer.PAWN: TARGET_LEVEL_PAWN
|
||
1: "2阶", # nonplayer.S_PAWN: TARGET_LEVEL_S_PAWN
|
||
2: "3阶", # nonplayer.KNIGHT: TARGET_LEVEL_KNIGHT
|
||
3: "4阶", # nonplayer.S_KNIGHT: TARGET_LEVEL_S_KNIGHT
|
||
4: "5阶首领", # nonplayer.BOSS: TARGET_LEVEL_BOSS
|
||
5: "6阶王", # nonplayer.KING: TARGET_LEVEL_KING
|
||
}
|
||
|
||
const DEFAULT_BOARD_WIDTH := 250.0
|
||
const BOARD_HEIGHT := 40.0
|
||
const GAUGE_WIDTH := 130.0
|
||
|
||
signal whisper_requested(name: String)
|
||
signal exchange_requested(vid: int)
|
||
signal fight_requested(vid: int)
|
||
signal view_equipment_requested(vid: int)
|
||
|
||
var client: Node
|
||
var _root: Control
|
||
var _board: Panel
|
||
var _name_label: Label
|
||
var _hp_gauge_bg: ColorRect
|
||
var _hp_gauge_fill: ColorRect
|
||
var _hp_text: Label
|
||
var _close_button: Button
|
||
var _button_container: HBoxContainer
|
||
var _action_buttons: Dictionary = {}
|
||
|
||
var _current_vid := 0
|
||
var _current_name := ""
|
||
var _current_hp_pct := 100
|
||
var _is_pc := false
|
||
var _shown := false
|
||
var _assets_root := ""
|
||
|
||
func setup(m2client: Node, parent: Node, assets := "") -> void:
|
||
client = m2client
|
||
_assets_root = assets
|
||
_build(parent)
|
||
|
||
func is_open() -> bool:
|
||
return _shown and _root != null and _root.visible
|
||
|
||
func get_target_vid() -> int:
|
||
return _current_vid
|
||
|
||
func get_target_name() -> String:
|
||
return _current_name
|
||
|
||
func get_hp_percentage() -> int:
|
||
return _current_hp_pct
|
||
|
||
func open(vid: int, target_name: String, level := -1, grade := -1, is_pc := false) -> void:
|
||
_current_vid = vid
|
||
_is_pc = is_pc
|
||
|
||
var display_name := target_name
|
||
var prefix := ""
|
||
if level > 0:
|
||
prefix += "[Lv. %d] " % level
|
||
if grade in GRADE_NAMES:
|
||
prefix += "(%s) " % GRADE_NAMES[grade]
|
||
|
||
_current_name = prefix + display_name
|
||
_name_label.text = _current_name
|
||
|
||
# 40250 uitarget.py:301 板面自适应宽度:200 + 7 * nameLength
|
||
var calculated_width := maxf(DEFAULT_BOARD_WIDTH, 200.0 + 7.0 * float(_current_name.length()))
|
||
_update_layout(calculated_width, is_pc)
|
||
|
||
_shown = true
|
||
_root.visible = true
|
||
_update_screen_position()
|
||
|
||
func set_hp(hp_pct: int) -> void:
|
||
_current_hp_pct = clampi(hp_pct, 0, 100)
|
||
if _hp_gauge_fill:
|
||
_hp_gauge_fill.size.x = GAUGE_WIDTH * (_current_hp_pct / 100.0)
|
||
if _hp_text:
|
||
_hp_text.text = "%d%%" % _current_hp_pct
|
||
if _hp_gauge_bg and not _is_pc:
|
||
_hp_gauge_bg.visible = true
|
||
|
||
func close() -> void:
|
||
_shown = false
|
||
_current_vid = 0
|
||
_current_name = ""
|
||
if _root:
|
||
_root.visible = false
|
||
|
||
func _on_close_button_pressed() -> void:
|
||
if client and client.has_method("clear_target"):
|
||
client.clear_target()
|
||
close()
|
||
|
||
func _update_layout(width: float, is_pc: bool) -> void:
|
||
_board.size = Vector2(width, BOARD_HEIGHT)
|
||
if is_pc:
|
||
_hp_gauge_bg.visible = false
|
||
_button_container.visible = true
|
||
_button_container.position = Vector2(width - 200, 8)
|
||
else:
|
||
_hp_gauge_bg.visible = true
|
||
_button_container.visible = false
|
||
_hp_gauge_bg.position = Vector2(width - 175, 14)
|
||
_close_button.position = Vector2(width - 28, 10)
|
||
|
||
func _update_screen_position() -> void:
|
||
if _root == null or not is_open():
|
||
return
|
||
var screen_w := 800.0
|
||
var vp := _root.get_viewport()
|
||
if vp:
|
||
screen_w = vp.get_visible_rect().size.x
|
||
# 40250 uitarget.py:254 UpdatePosition: ScreenWidth // 2 - Width // 2, 10
|
||
_root.position = Vector2(screen_w / 2.0 - _board.size.x / 2.0, 10.0)
|
||
|
||
func _build(parent: Node) -> void:
|
||
_root = Control.new()
|
||
_root.name = "TargetBoard"
|
||
_root.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||
_root.size = Vector2(DEFAULT_BOARD_WIDTH, BOARD_HEIGHT)
|
||
_root.visible = false
|
||
parent.add_child(_root)
|
||
|
||
# 经典 ThinBoard 风格
|
||
_board = Panel.new()
|
||
_board.name = "Board"
|
||
_board.size = Vector2(DEFAULT_BOARD_WIDTH, BOARD_HEIGHT)
|
||
var bg := StyleBoxFlat.new()
|
||
bg.bg_color = Color(0.04, 0.05, 0.07, 0.88)
|
||
bg.border_color = Color(0.45, 0.38, 0.22, 0.95)
|
||
bg.set_border_width_all(1)
|
||
bg.set_corner_radius_all(2)
|
||
_board.add_theme_stylebox_override("panel", bg)
|
||
_root.add_child(_board)
|
||
|
||
# 目标名称标签
|
||
_name_label = Label.new()
|
||
_name_label.name = "Name"
|
||
_name_label.position = Vector2(23, 11)
|
||
_name_label.size = Vector2(200, 20)
|
||
_name_label.add_theme_font_size_override("font_size", 11)
|
||
_name_label.add_theme_color_override("font_color", Color(1.0, 0.92, 0.75))
|
||
_name_label.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
|
||
_name_label.add_theme_constant_override("outline_size", 2)
|
||
_board.add_child(_name_label)
|
||
|
||
# 红色 HP Gauge(40250 130px 经典红槽)
|
||
_hp_gauge_bg = ColorRect.new()
|
||
_hp_gauge_bg.name = "HPGaugeBG"
|
||
_hp_gauge_bg.position = Vector2(DEFAULT_BOARD_WIDTH - 175, 14)
|
||
_hp_gauge_bg.size = Vector2(GAUGE_WIDTH, 12)
|
||
_hp_gauge_bg.color = Color(0.12, 0.02, 0.02, 0.95)
|
||
_board.add_child(_hp_gauge_bg)
|
||
|
||
_hp_gauge_fill = ColorRect.new()
|
||
_hp_gauge_fill.name = "HPGaugeFill"
|
||
_hp_gauge_fill.position = Vector2.ZERO
|
||
_hp_gauge_fill.size = Vector2(GAUGE_WIDTH, 12)
|
||
_hp_gauge_fill.color = Color(0.85, 0.12, 0.12, 1.0)
|
||
_hp_gauge_bg.add_child(_hp_gauge_fill)
|
||
|
||
_hp_text = Label.new()
|
||
_hp_text.name = "HPText"
|
||
_hp_text.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
_hp_text.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
_hp_text.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
_hp_text.add_theme_font_size_override("font_size", 9)
|
||
_hp_text.add_theme_color_override("font_color", Color(1, 1, 1, 0.95))
|
||
_hp_text.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.8))
|
||
_hp_text.add_theme_constant_override("outline_size", 1)
|
||
_hp_text.text = "100%"
|
||
_hp_gauge_bg.add_child(_hp_text)
|
||
|
||
# 关闭按钮 (X)
|
||
_close_button = Button.new()
|
||
_close_button.name = "CloseButton"
|
||
_close_button.position = Vector2(DEFAULT_BOARD_WIDTH - 28, 10)
|
||
_close_button.size = Vector2(18, 18)
|
||
_close_button.text = "×"
|
||
_close_button.add_theme_font_size_override("font_size", 12)
|
||
_close_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||
_close_button.pressed.connect(_on_close_button_pressed)
|
||
_board.add_child(_close_button)
|
||
|
||
# PC 交互按钮条
|
||
_button_container = HBoxContainer.new()
|
||
_button_container.name = "ButtonContainer"
|
||
_button_container.position = Vector2(DEFAULT_BOARD_WIDTH - 200, 8)
|
||
_button_container.visible = false
|
||
_board.add_child(_button_container)
|
||
|
||
_add_action_btn("私聊", func(): whisper_requested.emit(_current_name))
|
||
_add_action_btn("交易", func(): exchange_requested.emit(_current_vid))
|
||
_add_action_btn("装备", func(): view_equipment_requested.emit(_current_vid))
|
||
|
||
func _add_action_btn(text: String, callback: Callable) -> void:
|
||
var btn := Button.new()
|
||
btn.text = text
|
||
btn.custom_minimum_size = Vector2(42, 22)
|
||
btn.add_theme_font_size_override("font_size", 10)
|
||
btn.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||
btn.pressed.connect(callback)
|
||
_button_container.add_child(btn)
|