242 lines
8.4 KiB
GDScript
242 lines
8.4 KiB
GDScript
# 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()
|