# screen_hp_bar_test —— §8.8 屏幕空间 HP 条 headless 自检。 # godot --headless --path project --script screen_hp_bar_test.gd # 覆盖:不再创建 3D Sprite3D、实体血量同步、世界坐标投影、镜头后/死亡隐藏、地图清理。 extends SceneTree const NetWorld = preload("res://net_world.gd") const ScreenHpBar = preload("res://screen_hp_bar.gd") 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 entities := {} func get_entity(vid: int) -> Dictionary: return entities.get(vid, {}) func get_entities() -> Array: return entities.values() func get_pvp_relations() -> Array: return [] func get_duel() -> Dictionary: return {} func add(vid: int, pos: Vector3, main := false) -> Dictionary: var d := {"vid": vid, "name": "e%d" % vid, "is_main": main, "pos": pos, "pos_cm": Vector3(pos.x * 100.0, -pos.z * 100.0, 0.0), "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]} entities[vid] = d return d var _fail := 0 func _ck(condition: bool, message: String) -> void: if not condition: _fail += 1 printerr("FAIL: " + message) func _init() -> void: await _run() if _fail == 0: print("PASS: screen_hp_bar_test (§8.8 screen-space HP bar)") quit(0) else: printerr("%d check(s) failed" % _fail) quit(1) func _run() -> void: var mount := Node3D.new() var client := FakeClient.new() var camera := Camera3D.new() var nw: Node = NetWorld.new() get_root().add_child(mount) get_root().add_child(client) get_root().add_child(camera) get_root().add_child(nw) camera.position = Vector3(0.0, 2.0, 6.0) camera.look_at_from_position(Vector3(0.0, 2.0, 6.0), Vector3.ZERO, Vector3.UP) camera.current = true nw.setup(client, mount) nw.set_camera(camera) client.add(1, Vector3.ZERO, true) client.entity_main_set.emit(1) nw.set_local_vid(1) var entity := client.add(2, Vector3.ZERO) client.entity_spawned.emit(entity) await process_frame await process_frame var node: Node3D = nw.node_for(2) var bar := nw._hp_bars.get(2, null) as Control _ck(node != null, "remote entity exists") _ck(bar != null and bar is ScreenHpBar, "entity owns a screen Control HP bar") _ck(node.get_node_or_null("HpBar") == null, "legacy Sprite3D HpBar removed") _ck(bar != null and bar.visible, "front-facing living entity shows HP bar") if bar != null: var expected := camera.unproject_position(node.global_position + Vector3(0, nw.HP_BAR_WORLD_Y, 0)) _ck(is_equal_approx(bar.position.x, expected.x - bar.size.x * 0.5), "bar x is centered on projected world anchor") _ck(is_equal_approx(bar.ratio, 1.0), "initial hp ratio = 1") entity["hp"] = 25 client.vitals_changed.emit(2) await process_frame _ck(is_equal_approx(bar.ratio, 0.25), "vitals update changes screen bar ratio") entity["dead"] = true client.vitals_changed.emit(2) await process_frame _ck(not bar.visible, "dead entity hides screen bar") entity["dead"] = false entity["pos"] = Vector3(0, 0, -12) client.vitals_changed.emit(2) await process_frame _ck(not bar.visible, "entity behind camera hides screen bar") nw.clear_for_map_change() _ck(nw._hp_bars.is_empty(), "map change clears screen bars")