feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
This commit is contained in:
+55
-77
@@ -25,6 +25,33 @@ var _hp_label: Label
|
||||
var _hp_w := 240.0
|
||||
var _energy_fill: ColorRect
|
||||
var _energy_label: Label
|
||||
var _status_box: VBoxContainer
|
||||
var quickbar: Node:
|
||||
set(v):
|
||||
quickbar = v
|
||||
if quickbar:
|
||||
_sync_vitals_to_quickbar()
|
||||
|
||||
var _cached_hp: int = 0
|
||||
var _cached_max_hp: int = 0
|
||||
var _cached_sp: int = 0
|
||||
var _cached_max_sp: int = 0
|
||||
var _cached_st: int = 0
|
||||
var _cached_max_st: int = 0
|
||||
var _cached_exp: int = 0
|
||||
var _cached_next_exp: int = 0
|
||||
|
||||
func _sync_vitals_to_quickbar() -> void:
|
||||
if not quickbar:
|
||||
return
|
||||
if _cached_max_hp > 0 and quickbar.has_method("set_hp"):
|
||||
quickbar.set_hp(_cached_hp, _cached_max_hp)
|
||||
if _cached_max_sp > 0 and quickbar.has_method("set_sp"):
|
||||
quickbar.set_sp(_cached_sp, _cached_max_sp)
|
||||
if _cached_max_st > 0 and quickbar.has_method("set_st"):
|
||||
quickbar.set_st(_cached_st, _cached_max_st)
|
||||
if _cached_next_exp > 0 and quickbar.has_method("set_exp"):
|
||||
quickbar.set_exp(_cached_exp, _cached_next_exp)
|
||||
# 目标血条(P0)
|
||||
var _target_panel: Control
|
||||
var _target_name: Label
|
||||
@@ -191,84 +218,40 @@ func _build_minimap(nx: int, ny: int) -> void:
|
||||
_minimap_rect.add_child(_dot)
|
||||
|
||||
func _build_status() -> void:
|
||||
var box := VBoxContainer.new()
|
||||
box.set_anchors_preset(Control.PRESET_BOTTOM_LEFT)
|
||||
box.position = Vector2(18, -122)
|
||||
box.add_theme_constant_override("separation", 6)
|
||||
add_child(box)
|
||||
for spec in [["gauge_red", 0.82], ["gauge_blue", 0.60]]:
|
||||
var slot := _gauge("gauge_slot", 24)
|
||||
slot.custom_minimum_size = Vector2(_hp_w, 24)
|
||||
slot.size = Vector2(_hp_w, 24)
|
||||
var fill := _gauge(spec[0], 24)
|
||||
fill.position = Vector2(0, 0)
|
||||
fill.size = Vector2(_hp_w * float(spec[1]), 24)
|
||||
slot.add_child(fill)
|
||||
box.add_child(slot)
|
||||
if spec[0] == "gauge_red":
|
||||
_hp_fill = fill
|
||||
_hp_label = Label.new()
|
||||
_hp_label.add_theme_font_size_override("font_size", 11)
|
||||
_hp_label.position = Vector2(6, 5)
|
||||
slot.add_child(_hp_label)
|
||||
else:
|
||||
_mp_fill = fill
|
||||
# 体力条(40250 POINT_STAMINA,冲刺/骑乘时由 ServerCommand 驱动)。
|
||||
var stamina_slot := ColorRect.new()
|
||||
stamina_slot.color = Color(0, 0, 0, 0.55)
|
||||
stamina_slot.custom_minimum_size = Vector2(_hp_w, 10)
|
||||
stamina_slot.size = Vector2(_hp_w, 10)
|
||||
_stamina_fill = ColorRect.new()
|
||||
_stamina_fill.color = Color(0.25, 0.85, 0.35)
|
||||
_stamina_fill.size = Vector2(0, 10)
|
||||
stamina_slot.add_child(_stamina_fill)
|
||||
_stamina_label = Label.new()
|
||||
_stamina_label.add_theme_font_size_override("font_size", 9)
|
||||
_stamina_label.position = Vector2(6, -1)
|
||||
stamina_slot.add_child(_stamina_label)
|
||||
box.add_child(stamina_slot)
|
||||
# 能量条(原客户端 POINT_ENERGY,0..100)。资源缺失时仍用纯色条保持可读。
|
||||
var energy_slot := ColorRect.new()
|
||||
energy_slot.color = Color(0, 0, 0, 0.55)
|
||||
energy_slot.custom_minimum_size = Vector2(_hp_w, 10)
|
||||
energy_slot.size = Vector2(_hp_w, 10)
|
||||
_energy_fill = ColorRect.new()
|
||||
_energy_fill.color = Color(0.95, 0.65, 0.18)
|
||||
_energy_fill.size = Vector2(0, 10)
|
||||
energy_slot.add_child(_energy_fill)
|
||||
_energy_label = Label.new()
|
||||
_energy_label.add_theme_font_size_override("font_size", 9)
|
||||
_energy_label.position = Vector2(6, -1)
|
||||
energy_slot.add_child(_energy_label)
|
||||
box.add_child(energy_slot)
|
||||
# 经验条(细)+ 等级
|
||||
var exp_slot := ColorRect.new()
|
||||
exp_slot.color = Color(0, 0, 0, 0.5)
|
||||
exp_slot.custom_minimum_size = Vector2(_hp_w, 6)
|
||||
exp_slot.size = Vector2(_hp_w, 6)
|
||||
_exp_fill = ColorRect.new()
|
||||
_exp_fill.color = Color(0.9, 0.8, 0.2)
|
||||
_exp_fill.size = Vector2(0, 6)
|
||||
exp_slot.add_child(_exp_fill)
|
||||
box.add_child(exp_slot)
|
||||
_lvl_label = Label.new()
|
||||
_lvl_label.add_theme_font_size_override("font_size", 12)
|
||||
_lvl_label.text = "Lv 1"
|
||||
box.add_child(_lvl_label)
|
||||
# 40250 标准:血量、法力、体力、经验均由底部任务栏 Quickbar 的 Gauge_Board 与 EXP_Gauge_Board 承载,
|
||||
# HUD 不再在左下角渲染浮动的原型状态条。
|
||||
pass
|
||||
|
||||
# --- P0: live data setters -------------------------------------------------
|
||||
|
||||
func set_status_visible(v: bool) -> void:
|
||||
if _status_box and is_instance_valid(_status_box):
|
||||
_status_box.visible = v
|
||||
|
||||
func set_vitals(hp: int, max_hp: int, sp: int, max_sp: int) -> void:
|
||||
_cached_hp = hp
|
||||
_cached_max_hp = max_hp
|
||||
_cached_sp = sp
|
||||
_cached_max_sp = max_sp
|
||||
if _hp_fill:
|
||||
_hp_fill.size.x = _hp_w * clampf(float(hp) / maxf(1.0, max_hp), 0.0, 1.0)
|
||||
if _mp_fill:
|
||||
_mp_fill.size.x = _hp_w * clampf(float(sp) / maxf(1.0, max_sp), 0.0, 1.0)
|
||||
if _hp_label:
|
||||
_hp_label.text = "%d / %d" % [hp, max_hp]
|
||||
if quickbar:
|
||||
if quickbar.has_method("set_hp"):
|
||||
quickbar.set_hp(hp, max_hp)
|
||||
if quickbar.has_method("set_sp"):
|
||||
quickbar.set_sp(sp, max_sp)
|
||||
|
||||
func set_exp(xp: int, next_xp: int) -> void:
|
||||
_cached_exp = xp
|
||||
_cached_next_exp = next_xp
|
||||
if _exp_fill:
|
||||
_exp_fill.size.x = _hp_w * clampf(float(xp) / maxf(1.0, next_xp), 0.0, 1.0)
|
||||
if quickbar and quickbar.has_method("set_exp"):
|
||||
quickbar.set_exp(xp, next_xp)
|
||||
|
||||
func set_level(lv: int) -> void:
|
||||
if _lvl_label:
|
||||
@@ -283,12 +266,16 @@ func set_energy(value: int, max_value: int = 100) -> void:
|
||||
_energy_label.text = "%d / %d" % [v, cap]
|
||||
|
||||
func set_stamina(value: int, max_value: int) -> void:
|
||||
_cached_st = value
|
||||
_cached_max_st = max_value
|
||||
var cap := maxi(1, max_value)
|
||||
var v := clampi(value, 0, cap)
|
||||
if _stamina_fill:
|
||||
_stamina_fill.size.x = _hp_w * float(v) / float(cap)
|
||||
if _stamina_label:
|
||||
_stamina_label.text = "%d / %d" % [v, cap]
|
||||
if quickbar and quickbar.has_method("set_st"):
|
||||
quickbar.set_st(value, max_value)
|
||||
|
||||
# P4:状态图标条。affects = [{type, point_idx, value, flag, duration}, ...]
|
||||
func set_affects(affects: Array) -> void:
|
||||
@@ -371,20 +358,11 @@ func _build_target_panel() -> void:
|
||||
slot.add_child(_target_fill)
|
||||
|
||||
func _build_inventory() -> void:
|
||||
_inv = _nine("board", 32, 128)
|
||||
_inv.set_anchors_preset(Control.PRESET_CENTER_RIGHT)
|
||||
_inv.position = Vector2(-280, -200)
|
||||
_inv.size = Vector2(256, 384)
|
||||
_inv.visible = false
|
||||
var t := Label.new()
|
||||
t.text = " Inventory (I to close)"
|
||||
t.position = Vector2(16, 12)
|
||||
_inv.add_child(t)
|
||||
add_child(_inv)
|
||||
# 40250 标准:背包由 ui/inventory_ui.gd 统一管理,不再在此创建原型窗口。
|
||||
pass
|
||||
|
||||
func _input(ev: InputEvent) -> void:
|
||||
if ev is InputEventKey and ev.pressed and ev.keycode == KEY_I:
|
||||
_inv.visible = not _inv.visible
|
||||
func _input(_ev: InputEvent) -> void:
|
||||
pass
|
||||
|
||||
func _process(_dt: float) -> void:
|
||||
if _player:
|
||||
|
||||
Reference in New Issue
Block a user