# remote_motion_splash_test —— 40250 ActorInstanceMotionEvent::SPECIAL_ATTACKING # / InstanceBase::AttackProcess 的远端 actor 回归。 # # 远端攻击事件必须建立同样的 SplashArea 并执行碰撞、命中上限和去重; # 但远端事件不能伪造本地主角的 CG_ATTACK。 extends SceneTree const NetPlay = preload("res://net_play.gd") class HitActor extends Node3D: var hits := 0 var delays: Array = [] var shakes := 0 var spheres := [{"radius": 60.0, "pos": Vector3.ZERO, "bone": ""}] func get_defending_spheres() -> Array: return spheres func insert_delay(value: float) -> void: delays.append(value) func hit_good(_scalar: float, _stunned: bool) -> void: hits += 1 func shake() -> void: shakes += 1 class ClientStub extends Node: var entities: Dictionary = {} var attacks: Array = [] func is_in_game() -> bool: return true func get_entity(vid: int) -> Dictionary: return entities.get(vid, {}) func get_entities() -> Array: return entities.values() func attack(skill: int, vid: int) -> bool: attacks.append([skill, vid]) return true class PCStub extends Node: var player: Node3D class WorldStub extends Node: var nodes: Dictionary = {} func node_for(vid: int) -> Node3D: return nodes.get(vid, null) var _fail := 0 func _ck(ok: bool, message: String) -> void: if not ok: _fail += 1 printerr("FAIL: " + message) func _init() -> void: await _run() if _fail == 0: print("PASS: remote_motion_splash_test (remote SPECIAL_ATTACKING)") quit(0) else: printerr("%d check(s) failed" % _fail) quit(1) func _run() -> void: var client := ClientStub.new() var pc := PCStub.new() var world := WorldStub.new() var local := HitActor.new() var remote := HitActor.new() local.position = Vector3(-1.0, 0.0, 0.0) remote.position = Vector3.ZERO pc.player = local for n in [client, pc, world, local, remote]: get_root().add_child(n) client.entities[1] = {"vid": 1, "kind": 0, "race": 0, "dead": false, "in_safe": false, "pk_mode": 0} client.entities[2] = {"vid": 2, "kind": 2, "race": 101, "dead": false, "in_safe": false, "owner_vid": 0} world.nodes[1] = local world.nodes[2] = remote var np: Node = NetPlay.new() get_root().add_child(np) np.client = client np.pc = pc np.net_world = world np._main_vid = 1 np._local_time = 1.0 var event := { "type": 4, "skill_index": 51, "duration_time": 0.5, "enable_hit_process": true, "attack_type": 0, "hitting_type": 2, "stiffen_time": 0.1, "invisible_time": 0.2, "external_force": 0.0, "hit_limit_count": 1, "collision_spheres": [{"radius": 60.0, "pos": Vector3(100, 0, 0)}], } np.start_remote_motion_splash(2, event) _ck(np._remote_motion_splashes.has(2), "remote SPECIAL_ATTACKING creates an actor-owned SplashArea") np._remote_splash_attack_process() _ck(local.hits == 1, "remote splash applies the normal hit reaction to the local actor") _ck(local.delays.size() == 1 and is_equal_approx(float(local.delays[0]), 0.1), "remote splash applies the server motion stiffen time") _ck(remote.delays.size() == 1 and is_equal_approx(float(remote.delays[0]), 0.1), "remote splash inserts delay on the attacking actor, not the local input actor") _ck(client.attacks.is_empty(), "remote splash does not synthesize a local CG_ATTACK packet") # Clearing the victim's temporary invisible window must still not produce a # second hit: CActorInstance::m_HitDataMap deduplicates the same VID. np._victim_invisible_until.clear() np._remote_splash_attack_process() _ck(local.hits == 1, "remote splash deduplicates a victim for the area lifetime") _ck(np._remote_motion_splashes.has(2), "active remote splash remains until DuringTime expires") np._local_time = 1.5 np._remote_splash_attack_process() _ck(not np._remote_motion_splashes.has(2), "remote splash is cleaned up at DuringTime") np.free() remote.free() local.free() world.free() pc.free() client.free() await process_frame