# net_world_vis_test —— CLIENT-GAP §2.1 两层模型 / 可见性剔除的 headless 自检。 # godot --headless --path project --script net_world_vis_test.gd # 用带 pos_cm 的假 client 驱动 net_world.gd 的每帧可见性管理: # - 数据层始终有全部实体;场景节点只在进入 CHAR_STAGE_VIEW_BOUND 时创建 # - 主角走近 -> 远端实体补建;主角走远 -> 节点淡出(数据保留) # - AFFECT_SHOW_ALWAYS / IsWall 的实体无视距离常驻 # 退出码 0 = 全过。 extends SceneTree const NetWorld = preload("res://net_world.gd") const BOUND := 200 * 100 # CHAR_STAGE_VIEW_BOUND const NEAR_CM := Vector2(100 * 100, 0) # 10000 cm < BOUND const FAR_CM := Vector2(500 * 100, 0) # 50000 cm > BOUND + 10 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_info(vid: int, entity: Dictionary) signal chat(type: int, vid: int, text: String) signal vitals_changed(vid: int) signal entity_dead(vid: int) signal damage(vid: int, amount: int, flag: int) var ents := {} func get_entity(vid: int) -> Dictionary: return ents.get(vid, {}) func get_entities() -> Array: return ents.values() func get_pvp_relations() -> Array: return [] func get_duel() -> Dictionary: return {} func add(vid: int, cm: Vector2, extra := {}) -> Dictionary: var d := {"vid": vid, "name": "e%d" % vid, "is_main": false, "func": 0, "moving": false, "angle_deg": 0.0, "hp": 100, "max_hp": 100, "dead": false, "race": 0, "affect_flags": 0, "parts": [0, 0, 0, 0], "pos": Vector3(cm.x * 0.01, 0.0, -cm.y * 0.01), "pos_cm": Vector3(cm.x, cm.y, 0.0)} d.merge(extra, true) ents[vid] = d return d func move_main(cm: Vector2) -> void: ents[1]["pos_cm"] = Vector3(cm.x, cm.y, 0.0) ents[1]["pos"] = Vector3(cm.x * 0.01, 0.0, -cm.y * 0.01) 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: net_world_vis_test (§2.1 two-layer visibility cull / re-show)") quit(0) else: printerr("%d check(s) failed" % _fail) quit(1) func _run() -> void: var mount := Node3D.new() get_root().add_child(mount) var fc := FakeClient.new() get_root().add_child(fc) var nw: Node = NetWorld.new() get_root().add_child(nw) nw.setup(fc, mount) var added := {} var removed := {} nw.entity_added.connect(func(_n, v): added[v] = int(added.get(v, 0)) + 1) nw.entity_removed.connect(func(v): removed[v] = int(removed.get(v, 0)) + 1) # main character at origin fc.add(1, Vector2.ZERO, {"is_main": true}) fc.entity_main_set.emit(1) # 1) a near entity spawns a node immediately (signal path, in view) var near := fc.add(2, NEAR_CM) fc.entity_spawned.emit(near) _ck(nw.node_for(2) != null, "near entity (10000cm) -> node built on spawn") # 2) a far entity is data-only: the spawn signal must NOT build a node var far := fc.add(3, FAR_CM) fc.entity_spawned.emit(far) _ck(nw.node_for(3) == null, "far entity (50000cm) -> spawn signal builds no node (§2.1)") await process_frame _ck(nw.node_for(3) == null, "far entity still nodeless after a visibility pass") _ck(nw.node_for(2) != null, "near entity keeps its node while main is close") # 3) main walks over to the far entity -> per-frame manager builds its node fc.move_main(FAR_CM) await process_frame await process_frame _ck(nw.node_for(3) != null, "main in range -> _update_visibility builds the far node") _ck(int(added.get(3, 0)) == 1, "far node built exactly once") # 4) entity 2 is now out of range -> node blends out, data row stays _ck(nw.node_for(2) == null, "entity 2 out of range -> node culled") _ck(int(removed.get(2, 0)) == 1, "cull emits entity_removed once") _ck(fc.get_entity(2).has("vid"), "culled entity still present in the data layer") await process_frame _ck(nw.node_for(2) == null, "culled entity is not rebuilt while it stays out of range") # 5) AFFECT_SHOW_ALWAYS (bit 7) -> force-visible regardless of distance var beacon := fc.add(4, FAR_CM + Vector2(90000, 0), {"affect_flags": 1 << 7}) fc.entity_spawned.emit(beacon) _ck(nw.node_for(4) != null, "AFFECT_SHOW_ALWAYS entity -> node built though far") await process_frame _ck(nw.node_for(4) != null, "force-visible entity survives the cull loop") # 6) IsWall race -> always mirrored var wall := fc.add(5, FAR_CM + Vector2(120000, 0), {"race": 14202}) fc.entity_spawned.emit(wall) await process_frame _ck(nw.node_for(5) != null, "wall race (14202) -> node built and kept regardless of distance") # 7) main walks back -> entity 2 comes into view again and is rebuilt fc.move_main(NEAR_CM) # entity 2's fade tween (0.3s) must finish before it can be rebuilt await create_tween().tween_interval(0.5).finished await process_frame _ck(nw.node_for(2) != null, "main back in range -> culled entity rebuilt after its fade") _ck(int(added.get(2, 0)) == 2, "entity 2 rebuilt exactly once on re-entry")