完善客户端功能并同步差距文档

This commit is contained in:
shenlei
2026-09-03 18:40:01 +09:00
parent f93a172296
commit 13ccb8d02d
135 changed files with 21881 additions and 1259 deletions
+241
View File
@@ -0,0 +1,241 @@
# 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()