Files
mtgodot-poc/project/test_levelup_combat_parity.gd
T
shenandshen 66d217b313 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 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

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")