# hit_view_test —— CLIENT-GAP §3.7 受击方 / 攻击方视图(PlayerView / MobView)headless 自检: # __HitGood / __HitGreate / __HitStone 动作链(ActorInstanceBattle.cpp:769-890)、__Shake、 # InsertDelay(动作冻结)、攻击动作按 (motion mode, motion index, fSpeedRatio) 绑定、 # .msm 防御球、motion_bound(__SetMotion 尾给 net_play 的钩子)。 # godot --headless --path project --script hit_view_test.gd extends SceneTree const PlayerView = preload("res://ui/player_view.gd") const MobView = preload("res://ui/mob_view.gd") # Metin2AnimPlayer 桩:只记属性(anim_path 每次赋值都记,验证「同路径先清再设」) class FakeAnim extends Node: signal playback_finished var sets := [] var anim_path := "": set(v): anim_path = v sets.append(v) var loop := true var time_scale := 1.0 var playing := true var blend_time := 0.15 func get_motion_data() -> Dictionary: return {} const MSM := "ScriptType CharacterModelScript\nGroup AttachingData\n{\n\tGroup AttachingData00\n\t{\n\t\tAttachingDataType 0\n\t\tIsAttaching 0\n\t\tGroup CollisionData\n\t\t{\n\t\t\tCollisionType 3\n\t\t\tGroup SphereData\n\t\t\t{\n\t\t\t\tSphereDataCount 1\n\t\t\t\tGroup SphereData00\n\t\t\t\t{\n\t\t\t\t\tRadius %.1f\n\t\t\t\t\tPosition 0.0 0.0 %.1f\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n" var _fail := 0 func _ck(c: bool, m: String) -> void: if not c: _fail += 1 printerr("FAIL: " + m) func _touch(p: String, body := "") -> void: DirAccess.make_dir_recursive_absolute(p.get_base_dir()) var f := FileAccess.open(p, FileAccess.WRITE) f.store_string(body) f.close() var _done := false func _init() -> void: await _run() _ck(_done, "test body ran to completion (a SCRIPT ERROR aborted _run)") if _fail == 0: print("PASS: hit_view_test (hit reactions / shake / delay / attack motion / defending spheres)") quit(0) else: printerr("%d check(s) failed" % _fail) quit(1) func _run() -> void: var root := ProjectSettings.globalize_path("user://hit_view_test") var gen := root.path_join("general") for n in ["wait", "run", "attack", "attack_1", "damage", "damage_1", "damage_2", "damage_3", "damage_flying", "falling_stand", "back_damage_flying", "back_falling_stand"]: _touch(gen.path_join(n + ".msa")) for n in ["combo_01", "combo_02", "attack"]: _touch(root.path_join("onehand_sword/%s.msa" % n)) _touch(root.path_join("root/msm/warrior_m.msm"), MSM % [50.0, 90.0]) _touch(root.path_join("root/msm/warrior_w.msm"), MSM % [40.0, 80.0]) var gen_attack := [gen.path_join("attack.msa"), gen.path_join("attack_1.msa")] # --- PlayerView --------------------------------------------------------- var pv: Node3D = PlayerView.new() get_root().add_child(pv) var model := Node3D.new() pv.add_child(model) pv.model = model var anim := FakeAnim.new() pv.add_child(anim) pv.anim = anim anim.playback_finished.connect(pv._on_playback_finished) pv.motion_dir = gen pv._assets_root = root pv._race = 0 var bound := [] pv.motion_bound.connect(func(s: String): bound.append(s)) await process_frame pv.set_anim_state("wait") _ck(anim.anim_path == gen.path_join("wait.msa") and bound == ["wait"], "set_anim_state binds wait.msa -> motion_bound(wait), got %s" % [bound]) # __SetMotion(MAKE_MOTION_KEY(mode, index), fSpeedRatio = m_fAtkSpd) var c1 := root.path_join("onehand_sword/combo_01.msa") pv.play_attack_motion(2, 14, 1.5) _ck(anim.anim_path == c1 and not anim.loop and is_equal_approx(anim.time_scale, 1.5) \ and bound.back() == "attack", "ONEHAND_SWORD COMBO_ATTACK_1 -> combo_01.msa one-shot at speed 1.5, got %s" % anim.anim_path) anim.sets.clear() pv.play_attack_motion(2, 14, 1.5) _ck(anim.sets == ["", c1], "same seg again -> re-bound from frame 0, got %s" % [anim.sets]) pv.play_attack_motion(2, 15, 1.0) _ck(anim.anim_path == root.path_join("onehand_sword/combo_02.msa"), "index 15 -> combo_02.msa") pv.play_attack_motion(1, 13, 1.0) _ck(anim.anim_path in gen_attack, "GENERAL NORMAL_ATTACK -> attack / attack_1, got %s" % anim.anim_path) pv.play_attack_motion(1, 14, 1.0) _ck(anim.anim_path in gen_attack, "GENERAL COMBO_ATTACK_1 -> attack / attack_1, got %s" % anim.anim_path) pv.play_attack_motion(6, 14, 1.0) _ck(anim.anim_path in gen_attack, "missing mode folder -> general attack fallback, got %s" % anim.anim_path) _ck(not pv.is_in_hit_reaction(), "attack motion is not a hit reaction") # CGraphicThingInstance::InsertDelay:动作冻结 fDelay 秒后恢复 fSpeedRatio pv.play_attack_motion(2, 14, 1.5) pv.insert_delay(0.1) _ck(anim.time_scale == 0.0, "InsertDelay -> motion frozen") pv._process(0.05) _ck(anim.time_scale == 0.0, "inside the delay -> still frozen") pv._process(0.06) _ck(is_equal_approx(anim.time_scale, 1.5), "delay over -> fSpeedRatio restored, got %s" % anim.time_scale) # isLock(攻击动作中):__HitGood 只 __Shake(100) var atk_path: String = anim.anim_path pv.hit_good(-1.0, false) _ck(anim.anim_path == atk_path and pv._hit.shake_left > 0.0, "isLock (attack) -> shake only, no damage motion") pv._process(0.05) var off: Vector3 = model.position _ck(absf(off.x) <= 0.09 and absf(off.y) <= 0.09 and absf(off.z) <= 0.09, "ShakeProcess offset within rand()%%10 cm, got %s" % off) pv._process(0.2) _ck(model.position == Vector3.ZERO, "shake over -> model back on its ground offset, got %s" % model.position) # __HitGood:fScalar < 0 -> NAME_DAMAGE;>= 0 -> NAME_DAMAGE_BACK;之后 PushLoopMotion(WAIT) pv.set_anim_state("wait") pv._hit.shake_left = 0.0 pv.hit_good(-1.0, false) _ck(anim.anim_path in [gen.path_join("damage.msa"), gen.path_join("damage_1.msa")] and not anim.loop \ and pv.is_in_hit_reaction() and bound.back() == "damage", "fScalar < 0 -> NAME_DAMAGE (damage / damage_1), got %s" % anim.anim_path) anim.playback_finished.emit() _ck(anim.anim_path == gen.path_join("wait.msa") and not pv.is_in_hit_reaction(), "damage done -> WAIT") pv.hit_good(1.0, false) _ck(anim.anim_path in [gen.path_join("damage_2.msa"), gen.path_join("damage_3.msa")], "fScalar >= 0 -> NAME_DAMAGE_BACK (damage_2 / damage_3), got %s" % anim.anim_path) anim.playback_finished.emit() pv._process(0.2) pv._hit.shake_left = 0.0 var before: String = anim.anim_path pv.hit_good(-1.0, true) _ck(anim.anim_path == before and pv._hit.shake_left == 0.0, "stunned -> Die() branch: no shake / damage motion") # __HitGreate:DAMAGE_FLYING -> STAND_UP -> 回循环动作;击倒 / 起身中不再受 Good / Greate pv.set_anim_state("run") pv.hit_greate(-1.0, false) _ck(anim.anim_path == gen.path_join("damage_flying.msa") and pv.is_in_hit_reaction(), "GREAT fScalar < 0 -> NAME_DAMAGE_FLYING, got %s" % anim.anim_path) before = anim.anim_path pv.hit_good(-1.0, false) _ck(anim.anim_path == before, "IsKnockDown -> __HitGood returns") pv.hit_greate(1.0, false) _ck(anim.anim_path == before, "IsKnockDown -> __HitGreate returns") anim.playback_finished.emit() _ck(anim.anim_path == gen.path_join("falling_stand.msa"), "knockdown done -> PushOnceMotion(NAME_STAND_UP)") pv.hit_greate(-1.0, false) _ck(anim.anim_path == gen.path_join("falling_stand.msa"), "__IsStandUpMotion -> __HitGreate returns") anim.playback_finished.emit() _ck(anim.anim_path == gen.path_join("run.msa") and not pv.is_in_hit_reaction(), "stand up done -> loop motion resumes (run), got %s" % anim.anim_path) pv.hit_greate(1.0, false) _ck(anim.anim_path == gen.path_join("back_damage_flying.msa"), "GREAT fScalar >= 0 -> NAME_DAMAGE_FLYING_BACK") anim.playback_finished.emit() _ck(anim.anim_path == gen.path_join("back_falling_stand.msa"), "-> NAME_STAND_UP_BACK") anim.playback_finished.emit() pv.hit_greate(1.0, true) _ck(anim.anim_path == gen.path_join("back_damage_flying.msa"), "stunned GREAT -> DAMAGE_FLYING_BACK") anim.playback_finished.emit() _ck(anim.anim_path == gen.path_join("back_damage_flying.msa"), "stunned knockdown -> m_isRealDead: no stand up") # IsUsingSkill -> 只 __Shake pv.set_anim_state("skill") pv._hit.shake_left = 0.0 before = anim.anim_path pv.hit_greate(-1.0, false) _ck(anim.anim_path == before and pv._hit.shake_left > 0.0, "IsUsingSkill -> __HitGreate shakes only") pv.set_anim_state("wait") pv._hit.shake_left = 0.0 pv.hit_stone(false) _ck(pv._hit.shake_left > 0.0 and anim.anim_path == gen.path_join("wait.msa"), "__HitStone -> shake only") pv._hit.shake_left = 0.0 pv.hit_stone(true) _ck(pv._hit.shake_left == 0.0, "stunned stone -> Die() branch, no shake") # .msm 防御球:root/msm/_.msm var sp: Array = pv.get_defending_spheres() _ck(sp.size() == 1 and is_equal_approx(float(sp[0]["radius"]), 50.0) \ and Vector3(sp[0]["pos"]).is_equal_approx(Vector3(0, 0, 90)), "race 0 -> warrior_m.msm defending sphere, got %s" % [sp]) pv._race = 4 sp = pv.get_defending_spheres() _ck(sp.size() == 1 and is_equal_approx(float(sp[0]["radius"]), 40.0), "race 4 -> warrior_w.msm, got %s" % [sp]) # --- MobView ------------------------------------------------------------ var mdir := root.path_join("mob") var files := {"WAIT": "00.msa", "RUN": "03.msa", "NORMAL_ATTACK": "20.msa", "FRONT_DAMAGE": "30.msa", "FRONT_KNOCKDOWN": "32.msa", "FRONT_STANDUP": "33.msa", "BACK_DAMAGE": "34.msa", "BACK_KNOCKDOWN": "35.msa"} var mv: Node3D = MobView.new() get_root().add_child(mv) var mmodel := Node3D.new() mv.add_child(mmodel) mv.model = mmodel var manim := FakeAnim.new() mv.add_child(manim) mv.anim = manim manim.playback_finished.connect(mv._on_playback_finished) mv._dir = mdir mv._mesh_stem = "mobx" for k in files: _touch(mdir.path_join(files[k])) mv._motions[k] = mdir.path_join(files[k]) _touch(mdir.path_join("mobx.msm"), MSM % [60.0, 120.0]) var mbound := [] mv.motion_bound.connect(func(s: String): mbound.append(s)) var M := func(n: String) -> String: return mdir.path_join(files[n]) mv.set_anim_state("wait") _ck(manim.anim_path == M.call("WAIT") and mbound == ["wait"], "mob set_anim_state -> motion_bound(wait)") mv.hit_good(1.0, false) _ck(manim.anim_path == M.call("BACK_DAMAGE") and mv.is_in_hit_reaction(), "mob fScalar >= 0 -> BACK_DAMAGE") manim.playback_finished.emit() _ck(manim.anim_path == M.call("WAIT") and not mv.is_in_hit_reaction(), "mob damage done -> WAIT") mv.hit_good(-1.0, false) _ck(manim.anim_path == M.call("FRONT_DAMAGE"), "mob fScalar < 0 -> FRONT_DAMAGE") manim.playback_finished.emit() mv._motions.erase("BACK_DAMAGE") mv.hit_good(1.0, false) _ck(manim.anim_path == M.call("FRONT_DAMAGE"), "no DAMAGE_BACK motion -> InterceptOnceMotion(NAME_DAMAGE)") manim.playback_finished.emit() mv.hit_greate(1.0, false) _ck(manim.anim_path == M.call("BACK_KNOCKDOWN"), "mob GREAT fScalar >= 0 -> BACK_KNOCKDOWN") manim.playback_finished.emit() _ck(manim.anim_path == M.call("WAIT"), "no BACK_STANDUP -> PushOnceMotion fails -> WAIT, got %s" % manim.anim_path) mv.hit_greate(-1.0, false) _ck(manim.anim_path == M.call("FRONT_KNOCKDOWN"), "mob GREAT fScalar < 0 -> FRONT_KNOCKDOWN") manim.playback_finished.emit() _ck(manim.anim_path == M.call("FRONT_STANDUP"), "-> FRONT_STANDUP") manim.playback_finished.emit() _ck(manim.anim_path == M.call("WAIT"), "-> WAIT") mv._motions.erase("FRONT_KNOCKDOWN") mv._motions.erase("BACK_KNOCKDOWN") before = manim.anim_path mv.hit_greate(1.0, false) _ck(manim.anim_path == before, "no knockdown motions -> no motion change") mv.set_anim_state("attack") mv.hit_good(-1.0, false) _ck(manim.anim_path == M.call("NORMAL_ATTACK"), "mob attacking -> isLock, no damage motion") mv.insert_delay(0.05) _ck(manim.time_scale == 0.0, "mob InsertDelay -> frozen") mv._process(0.06) _ck(manim.time_scale == 1.0, "mob delay over -> speed restored") sp = mv.get_defending_spheres() _ck(sp.size() == 1 and is_equal_approx(float(sp[0]["radius"]), 60.0), "mob -> /.msm, got %s" % [sp]) pv.queue_free() mv.queue_free() _done = true