Files
mtgodot-poc/project/fly_test.gd
T

242 lines
8.4 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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(本 POCworld.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()