- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
176 lines
5.7 KiB
GDScript
176 lines
5.7 KiB
GDScript
# test_levelup_combat_parity.gd —— 40250 升级流与战斗打击感 1:1 纯净无头自检套件
|
|
# 验证:
|
|
# 1. POINT_LEVEL 跃迁升级检测
|
|
# 2. EFFECT_LEVELUP 冲天金光粒子挂载 (level_up_beam)
|
|
# 3. levelup1_1.wav 升级号角音效播放
|
|
# 4. 升级瞬间 100% HP 与 SP 回满规则
|
|
# 5. HUD 等级与加点通知
|
|
# 6. 暴击 DAMAGE_CRITICAL 镜头瞬时微震 (camera.shake)
|
|
# 7. 暴击大号金色高亮飘字与普通飘字样式分流
|
|
extends SceneTree
|
|
|
|
const NetPlay = preload("res://net_play.gd")
|
|
const NetWorld = preload("res://net_world.gd")
|
|
const DamageEffect = preload("res://damage_effect.gd")
|
|
|
|
class FakeAudio extends Node:
|
|
var calls: Array[String] = []
|
|
func play_ui(name: String) -> void:
|
|
calls.append(name)
|
|
|
|
class FakeCamera extends Camera3D:
|
|
var shake_calls: Array = []
|
|
func shake(strength: float, decay := 8.0) -> void:
|
|
shake_calls.append([strength, decay])
|
|
|
|
class FakePlayerController extends Node:
|
|
signal target_selected(target: Node)
|
|
signal ground_item_clicked(vid: int)
|
|
signal hover_entity_changed(vid: int)
|
|
signal moved(pos: Vector3)
|
|
signal anim_state(state: String)
|
|
var player: Node3D
|
|
|
|
class FakeHud extends Node:
|
|
var vitals: Array = []
|
|
var levels: Array[int] = []
|
|
var levelup_events: Array[int] = []
|
|
|
|
func set_vitals(hp: int, max_hp: int, sp: int, max_sp: int) -> void:
|
|
vitals.append([hp, max_hp, sp, max_sp])
|
|
func set_stamina(_st: int, _max_st: int) -> void: pass
|
|
func set_exp(_exp: int, _next_exp: int) -> void: pass
|
|
func set_level(lvl: int) -> void:
|
|
levels.append(lvl)
|
|
func set_energy(_e: int, _max_e: int) -> void: pass
|
|
func on_level_up(lvl: int) -> void:
|
|
levelup_events.append(lvl)
|
|
|
|
class FakeClient extends Node:
|
|
signal entity_spawned(e: Dictionary)
|
|
signal entity_despawned(vid: int)
|
|
signal entity_main_set(vid: int)
|
|
signal vitals_changed(vid: int)
|
|
signal entity_dead(vid: int)
|
|
signal damage(vid: int, amount: int, flag: int)
|
|
signal points_changed(p: Dictionary)
|
|
signal chat(type: int, vid: int, msg: String)
|
|
signal entity_moved(vid: int)
|
|
signal target_info(info: Dictionary)
|
|
|
|
func is_in_game() -> bool: return true
|
|
func get_main_vid() -> int: return 1001
|
|
func get_entity(_vid: int) -> Dictionary: return {}
|
|
|
|
var _fail := 0
|
|
func _ck(c: bool, m: String) -> void:
|
|
if not c:
|
|
_fail += 1
|
|
printerr("FAIL: " + m)
|
|
|
|
var _to_free: Array[Node] = []
|
|
|
|
func _init() -> void:
|
|
_run()
|
|
for n in _to_free:
|
|
if is_instance_valid(n):
|
|
n.queue_free()
|
|
if _fail == 0:
|
|
print("PASS: test_levelup_combat_parity (40250 LevelUp & Combat Parity)")
|
|
quit(0)
|
|
else:
|
|
printerr("%d check(s) failed in test_levelup_combat_parity" % _fail)
|
|
quit(1)
|
|
|
|
func _run() -> void:
|
|
var fc := FakeClient.new()
|
|
get_root().add_child(fc)
|
|
var fa := FakeAudio.new()
|
|
get_root().add_child(fa)
|
|
var fcam := FakeCamera.new()
|
|
get_root().add_child(fcam)
|
|
var hud := FakeHud.new()
|
|
get_root().add_child(hud)
|
|
|
|
var player := Node3D.new()
|
|
player.name = "Player"
|
|
get_root().add_child(player)
|
|
|
|
var pc := FakePlayerController.new()
|
|
pc.player = player
|
|
get_root().add_child(pc)
|
|
|
|
var mount := Node3D.new()
|
|
get_root().add_child(mount)
|
|
|
|
var nw: Node = NetWorld.new()
|
|
get_root().add_child(nw)
|
|
nw.setup(fc, mount)
|
|
nw.set_camera(fcam)
|
|
|
|
var np: Node = NetPlay.new()
|
|
get_root().add_child(np)
|
|
np.setup(fc, pc, nw, hud, fa)
|
|
np.camera = fcam
|
|
_to_free = [np, nw, mount, pc, player, hud, fcam, fa, fc]
|
|
|
|
var levelup_signals: Array[int] = []
|
|
np.level_up.connect(func(lvl: int): levelup_signals.append(lvl))
|
|
|
|
# --- 1. 初始点数同步 (不触发升级) ---
|
|
np._on_points({
|
|
"level": 1,
|
|
"hp": 200, "max_hp": 300,
|
|
"sp": 50, "max_sp": 100,
|
|
"exp": 0, "next_exp": 1000,
|
|
})
|
|
_ck(levelup_signals.is_empty(), "1. 初始 Lv.1 同步不触发升级信号")
|
|
_ck(fa.calls.is_empty(), "2. 初始不播放升级音效")
|
|
_ck(player.get_node_or_null("level_up_beam") == null, "3. 初始无升级光柱")
|
|
_ck(hud.levels == [1], "4. HUD 记录初始等级")
|
|
hud.vitals.clear()
|
|
|
|
# --- 2. 角色等级跃迁 (Lv.1 -> Lv.2) ---
|
|
# 在残血状态下升级:当前 HP 80 / Max HP 350
|
|
np._on_points({
|
|
"level": 2,
|
|
"hp": 80, "max_hp": 350,
|
|
"sp": 20, "max_sp": 120,
|
|
"exp": 120, "next_exp": 2500,
|
|
})
|
|
_ck(levelup_signals == [2], "5. 等级增长触发 level_up 信号 (Lv.2)")
|
|
_ck(fa.calls.has("levelup1_1.wav"), "6. 播放 40250 专属升级音效 levelup1_1.wav")
|
|
_ck(player.get_node_or_null("level_up_beam") != null, "7. 角色脚底挂载冲天金光特效 (level_up_beam)")
|
|
_ck(hud.levelup_events == [2], "8. 通知 HUD 刷新升级事件")
|
|
|
|
# 40250 核心规则:升级瞬间 HP / SP 立即 100% 满状态恢复
|
|
# 检查 hud.set_vitals 是否收到了满血满蓝调用 [350, 350, 120, 120]
|
|
var got_full_restore := false
|
|
for vit in hud.vitals:
|
|
if vit[0] == 350 and vit[1] == 350 and vit[2] == 120 and vit[3] == 120:
|
|
got_full_restore = true
|
|
break
|
|
_ck(got_full_restore, "9. 40250 规则:升级瞬间 HP/SP 立即全满回血回蓝")
|
|
|
|
# --- 3. 战斗暴击反馈 (DAMAGE_CRITICAL) ---
|
|
# 普通受击:flag = DAMAGE_NORMAL
|
|
fcam.shake_calls.clear()
|
|
nw._on_damage(2001, 150, DamageEffect.DAMAGE_NORMAL)
|
|
_ck(fcam.shake_calls.is_empty(), "10. 普通伤害不震屏")
|
|
|
|
# 暴击命中:flag = DAMAGE_NORMAL | DAMAGE_CRITICAL
|
|
nw._on_damage(2001, 450, DamageEffect.DAMAGE_NORMAL | DamageEffect.DAMAGE_CRITICAL)
|
|
_ck(not fcam.shake_calls.is_empty(), "11. 暴击 DAMAGE_CRITICAL 触发镜头震颤")
|
|
_ck(fcam.shake_calls[0][0] == 0.05, "12. 震颤振幅精确匹配 0.05m")
|
|
|
|
# 暴击飘字样式验证
|
|
nw._show_damage = true
|
|
nw._process_damage_queue()
|
|
var dmg_node: Node3D = mount.get_node_or_null("dmg_2001")
|
|
# 如果没有 DDS 纹理降级至 Label3D
|
|
if dmg_node:
|
|
var lbl := dmg_node.get_node_or_null("Label3D") as Label3D
|
|
if lbl:
|
|
_ck(lbl.modulate == Color(1.0, 0.85, 0.2), "13. 暴击飘字为金色")
|
|
_ck(lbl.font_size == 46, "14. 暴击字体放大至 46px")
|