# fly_test —— §3.6 飞行物实体(fly_object.gd / FlyManager / FlyInstance)+ net_play._can_shot() # 的 headless 自检。 # godot --headless --path project --script fly_test.gd # 退出码 0 = 全过。 extends SceneTree const FlyObject = preload("res://fly_object.gd") const NetPlay = preload("res://net_play.gd") var _fail := 0 func _ck(c: bool, m: String) -> void: if not c: _fail += 1 printerr("FAIL: " + m) func _init() -> void: await _run() if _fail == 0: print("PASS: fly_test (§3.6 CFlyingManager/CFlyingInstance + __CanShot)") quit(0) else: printerr("%d check(s) failed" % _fail) quit(1) func _mk_mgr() -> Node: var m := FlyObject.new() m.manual_step = true root.add_child(m) return m func _node_at(p: Vector3) -> Node3D: var n := Node3D.new() root.add_child(n) n.global_position = p return n func _drain(mgr: Node, steps: int, dt: float) -> void: for i in steps: if mgr.active_count() == 0: return mgr.step(dt) func _run() -> void: # SceneTree._init() 跑得比 root 就绪早;等一帧再建 Node3D,避免 !is_inside_tree()。 await process_frame _test_hit_object() _test_out_of_range() _test_can_attack_damage() _test_pierce() _test_position_target() _test_hit_background() _test_can_shot() _test_seg_point_dist() # 直线箭命中对象目标 —— CFlyingInstance::Update 的线段-点判定 < m_fBombRange²。 func _test_hit_object() -> void: var mgr := _mk_mgr() var tgt := _node_at(Vector3(10, 1, 0)) var events: Array = [] mgr.exploded.connect(func(wp: Vector3, cause: String): events.append([cause, wp])) var d = FlyObject.FlyData.new() d.init_vel = 45.0 d.flat_range = 30.0 d.bomb_range = 0.6 mgr.spawn(Vector3(0, 1, 0), tgt, false, d) _ck(mgr.active_count() == 1, "hit_object: instance registered") _drain(mgr, 60, 0.05) _ck(events.size() == 1 and events[0][0] == "at_target", "hit_object: exploded at_target once") if events.size() == 1: _ck((events[0][1] as Vector3).distance_to(Vector3(10, 1, 0)) < 2.0, "hit_object: explosion near the target") _ck(mgr.active_count() == 0, "hit_object: instance removed after non-pierce hit") mgr.free() tgt.free() # 超程 —— m_fRemainRange < 0 → OnExplodingOutOfRange + __Explode(false)(不 __Bomb)。 func _test_out_of_range() -> void: var mgr := _mk_mgr() var tgt := _node_at(Vector3(100, 1, 0)) var causes: Array = [] var dmg := [0] mgr.exploded.connect(func(_wp: Vector3, cause: String): causes.append(cause)) mgr.shoot_damage.connect(func(_v: int): dmg[0] += 1) var d = FlyObject.FlyData.new() d.init_vel = 45.0 d.flat_range = 5.0 d.bomb_range = 0.6 mgr.spawn(Vector3(0, 1, 0), tgt, true, d) _drain(mgr, 40, 0.05) _ck(causes == ["out_of_range"], "out_of_range: exploded out_of_range, no target hit") _ck(dmg[0] == 0, "out_of_range: no shoot_damage") _ck(mgr.active_count() == 0, "out_of_range: instance removed") mgr.free() tgt.free() # canAttack=true 且对象目标 → OnShootDamage(本 POC 转成 shoot_damage(vid) 信号)。 func _test_can_attack_damage() -> void: var mgr := _mk_mgr() var tgt := _node_at(Vector3(5, 1, 0)) var hits: Array = [] mgr.shoot_damage.connect(func(v: int): hits.append(v)) var d = FlyObject.FlyData.new() d.init_vel = 45.0 d.flat_range = 20.0 d.bomb_range = 0.6 mgr.spawn(Vector3(0, 1, 0), {"vid": 4242, "node": tgt}, true, d) _drain(mgr, 40, 0.05) _ck(hits == [4242], "can_attack: shoot_damage(4242) exactly once") mgr.free() tgt.free() # 穿透 —— m_iPierceCount>0 时命中后 --、__Bomb()、继续飞(Update 返 true),最终超程。 func _test_pierce() -> void: var mgr := _mk_mgr() var tgt := _node_at(Vector3(6, 1, 0)) var causes: Array = [] mgr.exploded.connect(func(_wp: Vector3, cause: String): causes.append(cause)) var d = FlyObject.FlyData.new() d.init_vel = 45.0 d.flat_range = 15.0 d.bomb_range = 0.6 d.pierce_count = 1 mgr.spawn(Vector3(0, 1, 0), tgt, false, d) # 命中前几步:命中那一步实例仍存活(pierce 消耗),不立即移除 var seen_alive_after_hit := false for i in 60: mgr.step(0.05) if causes.size() == 1 and causes[0] == "at_target" and mgr.active_count() == 1: seen_alive_after_hit = true if mgr.active_count() == 0: break _ck(seen_alive_after_hit, "pierce: instance survives the first hit") _ck(causes == ["at_target", "out_of_range"], "pierce: at_target then out_of_range") mgr.free() tgt.free() # 位置目标 —— IsPosition 分支同样线段判定 → __Explode()。 func _test_position_target() -> void: var mgr := _mk_mgr() var causes: Array = [] mgr.exploded.connect(func(_wp: Vector3, cause: String): causes.append(cause)) var d = FlyObject.FlyData.new() d.init_vel = 45.0 d.flat_range = 30.0 d.bomb_range = 0.6 mgr.spawn(Vector3(0, 1, 0), Vector3(8, 1, 0), false, d) _drain(mgr, 40, 0.05) _ck(causes == ["at_position"], "position_target: exploded at_position") mgr.free() # hit_on_background —— GetTerrainHeight(x,-y) > z(本 POC:world.sample_height(x,z) > pos.y)。 func _test_hit_background() -> void: var mgr := _mk_mgr() var world := _FakeWorld.new() world.h = 0.9 mgr.world = world var causes: Array = [] mgr.exploded.connect(func(_wp: Vector3, cause: String): causes.append(cause)) var d = FlyObject.FlyData.new() d.init_vel = 45.0 d.flat_range = 60.0 d.bomb_range = 0.05 d.gravity = -20.0 # 向下拉,几步内 y 跌破地面高度 d.hit_on_background = true mgr.spawn(Vector3(0, 1, 0), Vector3(40, 1, 0), false, d) _drain(mgr, 40, 0.05) _ck(causes.size() == 1 and causes[0] == "at_background", "hit_background: exploded at_background") _ck(mgr.active_count() == 0, "hit_background: instance removed") mgr.free() # net_play._can_shot() —— __CanShot 防御式重建(弓模式 && 有箭 && 有可攻击目标 && 在弓程内)。 func _test_can_shot() -> void: var fc := _FakeClient.new() get_root().add_child(fc) var np: Node = NetPlay.new() get_root().add_child(np) np.client = fc np.net_world = null np._main_vid = 1 fc.ents[1] = {"vid": 1, "pos": Vector3.ZERO, "hp": 100, "max_hp": 100} np.combo_motion_mode = np.MOTION_MODE_GENERAL _ck(np._can_shot() == "NOT_BOW_MODE", "can_shot: non-bow -> NOT_BOW_MODE") np.combo_motion_mode = np.MOTION_MODE_BOW np._arrow_count = 0 _ck(np._can_shot() == "EMPTY_ARROW", "can_shot: bow + 0 arrows -> EMPTY_ARROW") np._arrow_count = 5 np._target_vid = 0 np._vid_reserved = 0 _ck(np._can_shot() == "NO_TARGET", "can_shot: bow + arrows + no target -> NO_TARGET") np._arrow_count = -1 # 未跟踪 = 无限 fc.ents[900] = {"vid": 900, "ch_type": 2, "pos": Vector3(5, 0, 0), "hp": 30, "max_hp": 30, "dead": false} np._target_vid = 900 _ck(np._can_shot() == "", "can_shot: bow + target in range -> OK") fc.ents[901] = {"vid": 901, "ch_type": 2, "pos": Vector3(400, 0, 0), "hp": 30, "max_hp": 30, "dead": false} np._target_vid = 901 _ck(np._can_shot() == "OUT_OF_RANGE", "can_shot: target beyond __GetBowRange -> OUT_OF_RANGE") fc.ents[902] = {"vid": 902, "ch_type": 2, "pos": Vector3(3, 0, 0), "hp": 0, "max_hp": 30, "dead": true} np._target_vid = 902 _ck(np._can_shot() == "NO_TARGET", "can_shot: dead target -> NO_TARGET") np.free() fc.free() func _test_seg_point_dist() -> void: var f = FlyObject.FlyInstance # 垂足在段内:点到直线距离 _ck(is_equal_approx(f._seg_point_sq_dist(Vector3(0, 0, 0), Vector3(10, 0, 0), Vector3(5, 3, 0)), 9.0), "seg_dist: foot inside segment -> perpendicular²") # 垂足在 p1 外:取端点 p1 _ck(is_equal_approx(f._seg_point_sq_dist(Vector3(0, 0, 0), Vector3(10, 0, 0), Vector3(-4, 0, 0)), 16.0), "seg_dist: past p1 -> |x-p1|²") # 垂足在 p2 外:取端点 p2 _ck(is_equal_approx(f._seg_point_sq_dist(Vector3(0, 0, 0), Vector3(10, 0, 0), Vector3(13, 4, 0)), 25.0), "seg_dist: past p2 -> |x-p2|²") class _FakeWorld extends RefCounted: var h := 0.0 func sample_height(_x: float, _z: float) -> float: return h class _FakeClient extends Node: signal entity_spawned(entity: Dictionary) signal entity_despawned(vid: int) signal entity_moved(vid: int) signal entity_main_set(vid: int) signal entity_dead(vid: int) signal vitals_changed(vid: int) signal points_changed(points: Dictionary) signal chat(type: int, vid: int, text: String) signal damage(vid: int, amount: int, flag: int) var ents := {} func is_in_game() -> bool: return true func get_main_vid() -> int: return 1 func get_entity(vid: int) -> Dictionary: return ents.get(vid, {}) func get_entities() -> Array: return ents.values()