- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
154 lines
6.1 KiB
GDScript
154 lines
6.1 KiB
GDScript
# test_taskbar_quickslot_parity.gd —— 验证方向 3:底部主任务栏、4 珍珠经验球与鼠标控制器 40250 1:1 对齐
|
|
extends SceneTree
|
|
|
|
const Quickbar = preload("res://ui/quickbar.gd")
|
|
|
|
class DummySkillTable extends RefCounted:
|
|
func motion_idx_of(_id: int) -> int: return 1
|
|
func is_fan_range(_id: int) -> bool: return false
|
|
func is_circle_range(_id: int) -> bool: return false
|
|
func target_range(_id: int) -> float: return 500.0
|
|
func target_count(_id: int, _lvl: int) -> int: return 1
|
|
func fly_shape(_id: int) -> int: return 0
|
|
func skill_name(id: int) -> String:
|
|
if id == 1: return "三连斩"
|
|
if id == 2: return "剑气"
|
|
return "技能 #%d" % id
|
|
|
|
class DummyClient extends Node:
|
|
signal quickslots_changed
|
|
signal skills_changed
|
|
signal points_changed(pts: Dictionary)
|
|
var _qs: Array = []
|
|
func get_quickslots() -> Array: return _qs
|
|
func get_points() -> Dictionary: return {"hp": 1000, "max_hp": 1000, "sp": 500, "max_sp": 500, "exp": 0, "next_exp": 1000}
|
|
func get_main_vid() -> int: return 1
|
|
func get_entity(_vid: int) -> Dictionary: return {"race": 0, "dead": false}
|
|
func get_skills() -> Array: return []
|
|
func get_skill_group() -> int: return 1
|
|
func quickslot_add(pos: int, type: int, ref: int) -> bool:
|
|
_qs.append({"pos": pos, "type": type, "ref": ref})
|
|
quickslots_changed.emit()
|
|
return true
|
|
|
|
var _passed := 0
|
|
var _failed := 0
|
|
|
|
func _assert(cond: bool, msg: String) -> void:
|
|
if cond:
|
|
_passed += 1
|
|
print(" [PASS] %s" % msg)
|
|
else:
|
|
_failed += 1
|
|
printerr(" [FAIL] %s" % msg)
|
|
|
|
func _init() -> void:
|
|
print("\n=== Running test_taskbar_quickslot_parity (Direction 3: Taskbar & Quickslot 1:1) ===")
|
|
var root := Control.new()
|
|
root.size = Vector2(800, 600)
|
|
|
|
var client := DummyClient.new()
|
|
var table := DummySkillTable.new()
|
|
var qb := Quickbar.new()
|
|
|
|
qb.setup(client, table, root, func(): return null, "metin2-client/assets")
|
|
|
|
test_exp_four_pearls_parity(qb)
|
|
test_page_flip_parity(qb)
|
|
test_mouse_mode_controllers(qb)
|
|
|
|
print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed])
|
|
if _failed > 0:
|
|
printerr("TEST FAILED!")
|
|
quit(1)
|
|
else:
|
|
print("ALL TESTS PASSED!")
|
|
quit(0)
|
|
|
|
func test_exp_four_pearls_parity(qb: Node) -> void:
|
|
print("\n--- Test 1: 40250 uitaskbar.py 4-Pearl EXP Gauge Calculation ---")
|
|
# Case 1: 0% EXP
|
|
qb.set_exp(0, 1000)
|
|
var exp_board: Control = qb.get("_exp_board")
|
|
_assert(exp_board != null, "EXP gauge board exists")
|
|
_assert(exp_board.tooltip_text == "EXP : 0.00%", "0% EXP tooltip formatted 1:1")
|
|
|
|
var clips: Array = qb.get("_exp_clips")
|
|
_assert(clips.size() == 4, "4 pearl clip controls present")
|
|
_assert(not clips[0].visible and not clips[1].visible and not clips[2].visible and not clips[3].visible,
|
|
"At 0% EXP, all 4 pearls are hidden/empty")
|
|
|
|
# Case 2: 25% EXP (1st quarter full)
|
|
qb.set_exp(250, 1000)
|
|
_assert(exp_board.tooltip_text == "EXP : 25.00%", "25% EXP tooltip matches")
|
|
_assert(clips[0].visible, "Pearl 1 is fully filled and visible")
|
|
_assert(clips[0].size == Vector2(19, 19), "Pearl 1 is full 19x19 dimensions")
|
|
_assert(not clips[1].visible and not clips[2].visible and not clips[3].visible,
|
|
"Pearls 2..4 remain empty at 25%")
|
|
|
|
# Case 3: 62.5% EXP (2 quarters full, 3rd quarter 50% filled)
|
|
# 625 / 1000 = quarter_pt 250, full_count = 2 (pearls 0 and 1 full). Pearl 2 fill_h = round(19 * 125/250) = 10 px
|
|
qb.set_exp(625, 1000)
|
|
_assert(exp_board.tooltip_text == "EXP : 62.50%", "62.5% EXP tooltip matches")
|
|
_assert(clips[0].visible and clips[0].size.y == 19, "Pearl 1 is full (19px)")
|
|
_assert(clips[1].visible and clips[1].size.y == 19, "Pearl 2 is full (19px)")
|
|
_assert(clips[2].visible and clips[2].size.y == 10, "Pearl 3 is partially filled (10px)")
|
|
_assert(not clips[3].visible, "Pearl 4 is empty")
|
|
|
|
# Case 4: 100% EXP (All 4 pearls full)
|
|
qb.set_exp(1000, 1000)
|
|
_assert(exp_board.tooltip_text == "EXP : 100.00%", "100% EXP tooltip matches")
|
|
_assert(clips[0].visible and clips[1].visible and clips[2].visible and clips[3].visible,
|
|
"All 4 pearls are visible and full at 100% EXP")
|
|
|
|
func test_page_flip_parity(qb: Node) -> void:
|
|
print("\n--- Test 2: Quickslot Multi-Page Flipping (Shift+1..4) ---")
|
|
_assert(qb.get_page() == 0, "Initial page is 0 (Page 1)")
|
|
|
|
qb.set_page(1)
|
|
_assert(qb.get_page() == 1, "Flipped to page 1 (Page 2)")
|
|
|
|
qb.set_page(2)
|
|
_assert(qb.get_page() == 2, "Flipped to page 2 (Page 3)")
|
|
|
|
qb.set_page(3)
|
|
_assert(qb.get_page() == 3, "Flipped to page 3 (Page 4)")
|
|
|
|
# Out of bounds ignored
|
|
qb.set_page(4)
|
|
_assert(qb.get_page() == 3, "Out of bounds page 4 rejected")
|
|
qb.set_page(-1)
|
|
_assert(qb.get_page() == 3, "Out of bounds page -1 rejected")
|
|
|
|
# Flipped back to page 0
|
|
qb.set_page(0)
|
|
_assert(qb.get_page() == 0, "Flipped back to page 0")
|
|
|
|
func test_mouse_mode_controllers(qb: Node) -> void:
|
|
print("\n--- Test 3: Mouse Left/Right Button Mode Controllers ---")
|
|
var btn_left: Control = qb.get_button("mouse_left")
|
|
var btn_right: Control = qb.get_button("mouse_right")
|
|
_assert(btn_left != null, "mouse_left button exists")
|
|
_assert(btn_right != null, "mouse_right button exists")
|
|
|
|
# Left mouse mode cycle: 0 -> 1 -> 2 -> 0
|
|
_assert(qb.get_left_mouse_mode() == 0, "Initial left mouse mode is 0 (Move & Attack)")
|
|
qb.set_left_mouse_mode(1)
|
|
_assert(qb.get_left_mouse_mode() == 1, "Left mouse mode changed to 1 (Auto Attack)")
|
|
_assert("自动攻击" in btn_left.tooltip_text, "Left mouse button tooltip updated to Auto Attack")
|
|
qb.set_left_mouse_mode(2)
|
|
_assert(qb.get_left_mouse_mode() == 2, "Left mouse mode changed to 2 (Camera)")
|
|
_assert("镜头旋转" in btn_left.tooltip_text, "Left mouse button tooltip updated to Camera")
|
|
|
|
# Right mouse mode & skill binding
|
|
qb.set_right_mouse_mode(0)
|
|
_assert(qb.get_right_mouse_mode() == 0, "Right mouse mode changed to 0 (Move & Attack)")
|
|
qb.set_right_mouse_mode(1)
|
|
_assert(qb.get_right_mouse_mode() == 1, "Right mouse mode changed to 1 (Camera)")
|
|
qb.set_right_mouse_mode(2)
|
|
_assert(qb.get_right_mouse_mode() == 2, "Right mouse mode changed to 2 (Skill)")
|
|
|
|
qb.set_right_mouse_skill(1)
|
|
_assert(qb.get_right_mouse_skill() == 1, "Right mouse skill set to 1 (三连斩)")
|
|
_assert("三连斩" in btn_right.tooltip_text, "Right mouse button tooltip reflects selected skill name")
|