# fx_test —— P5:.mse 解析 + EffectPlayer 构建 headless 自检。 # godot --headless --path project --script fx_test.gd extends SceneTree const Mse = preload("res://fx/mse.gd") const Mde = preload("res://fx/mde.gd") const EffectPlayer = preload("res://fx/effect_player.gd") const EffectRegistry = preload("res://fx/effect_registry.gd") const FIXTURE := """ BoundingSphereRadius 120.000000 BoundingSpherePosition 0.000000 0.000000 100.000000 Group Particle { StartTime 0.000000 List TimeEventPosition { 0.000000 "MOVING_TYPE_DIRECT" 8.000000 0.000000 25.000000 } Group EmitterProperty { MaxEmissionCount 5 CycleLength 0.400000 CycleLoopEnable 1 LoopCount 2 EmitterShape 3 EmittingRadius 10.000000 List TimeEventLifeTime { 0.000000 0.077320 } List TimeEventSizeX { 0.000000 64.000000 } } Group ParticleProperty { SrcBlendType 5 DestBlendType 2 BillboardType 1 RotationSpeed 500.000000 List TimeEventScaleX { 0.000000 1.000000 1.000000 0.000000 } List TimeEventColorRed { 0.000000 0.886275 } List TimeEventColorGreen { 0.000000 0.086275 } List TimeEventColorBlue { 0.000000 0.239216 } List TimeEventAlpha { 0.000000 0.710000 0.864103 0.000000 } List TextureFiles { "bottom_under.dds" } } } Group Particle { StartTime 0.200000 Group EmitterProperty { MaxEmissionCount 10 EmitterShape 3 EmittingRadius 20.0 EmitterAdvancedType 1 EmitterEmitFromEdgeFlag 1 } Group ParticleProperty { SrcBlendType 5 DestBlendType 4 BillboardType 4 } } Group Light { StartTime 0.100000 Duration 0.500000 LoopFlag 0 LoopCount 0 MaxRange 300.000000 AmbientColor 0.100000 0.200000 0.300000 1.000000 DiffuseColor 0.800000 0.400000 0.100000 1.000000 Attenuation0 0.000000 Attenuation1 0.100000 Attenuation2 0.000000 List TimeEventRange { 0.000000 0.000000 0.500000 1.000000 } } """ var _fail := 0 func _ck(c: bool, m: String) -> void: if not c: _fail += 1 printerr("FAIL: " + m) func _init() -> void: _test_parser() _test_builder() _test_mde() _test_registry() if _fail == 0: print("PASS: fx_test (.mse parser + EffectPlayer + registry)") quit(0) else: printerr("%d check(s) failed" % _fail) quit(1) func _test_parser() -> void: var m := Mse.new() var s := m.parse_text(FIXTURE) _ck(m.last_error == "", "parse: no error (%s)" % m.last_error) _ck(abs(float(s.bsphere_r) - 120.0) < 0.01, "bsphere radius") _ck(s.bsphere_pos == Vector3(0, 0, 100), "bsphere pos = (0,0,100)") _ck(s.particles.size() == 2, "2 Particle groups (%d)" % s.particles.size()) var p0: Dictionary = s.particles[0] _ck(abs(p0.start_time) < 0.01, "p0 start_time 0") _ck(p0.position.size() == 1 and String(p0.position[0][1]) == "MOVING_TYPE_DIRECT", "p0 TimeEventPosition row parsed with quoted enum") _ck(int(p0.emitter.get("MaxEmissionCount")) == 5, "p0 emitter MaxEmissionCount 5") _ck(int(p0.emitter.get("EmitterShape")) == 3, "p0 EmitterShape 3") var scale_rows = p0.particle.get("TimeEventScaleX") _ck(scale_rows is Array and scale_rows.size() == 2 and float(scale_rows[1][1]) == 0.0, "p0 ScaleX list = 2 rows, last v=0") var tex = p0.particle.get("TextureFiles") _ck(tex is Array and tex.size() == 1 and String(tex[0][0]) == "bottom_under.dds", "p0 TextureFiles") _ck(abs(float(s.particles[1].start_time) - 0.2) < 0.01, "p1 start_time 0.2") _ck(s.lights.size() == 1, "1 SimpleLight group") _ck(abs(float(s.lights[0].duration) - 0.5) < 0.01 and s.lights[0].range.size() == 2, "light duration and range curve") func _test_builder() -> void: var m := Mse.new() var s := m.parse_text(FIXTURE) var fx: Node3D = EffectPlayer.new() get_root().add_child(fx) fx.build(s, "") _ck(fx._emitters.size() == 2, "EffectPlayer built 2 GPUParticles3D") var g0: GPUParticles3D = fx._emitters[0] _ck(g0.amount == 5, "emitter 0 amount = 5") _ck(abs(float(g0.get_meta("cycle_length", 0.0)) - 0.4) < 0.001, "emitter 0 cycle length = 0.4") _ck(bool(g0.get_meta("cycle_loop", false)), "emitter 0 cycle loop enabled") _ck(int(g0.get_meta("loop_count", 0)) == 2, "emitter 0 loop count = 2") _ck(abs(g0.lifetime - 0.07732) < 0.01, "particle lifetime is not replaced by cycle length") _ck(g0.process_material is ParticleProcessMaterial, "emitter 0 has process material") var pm: ParticleProcessMaterial = g0.process_material _ck(pm.emission_shape == ParticleProcessMaterial.EMISSION_SHAPE_SPHERE, "emitter 0 sphere emission (shape 3)") _ck(g0.material_override is StandardMaterial3D, "emitter 0 draw material") _ck((g0.material_override as StandardMaterial3D).blend_mode == BaseMaterial3D.BLEND_MODE_ADD, "src5/dst2 -> additive blend") _ck(float(fx._emitters[1].get_meta("start_time")) == 0.2, "emitter 1 start_time meta = 0.2") _ck(bool(fx._emitters[1].get_meta("emitter_emit_from_edge", false)), "emitter 1 edge emission metadata") _ck(int(fx._emitters[1].get_meta("emitter_advanced_type", -1)) == 1, "emitter 1 advanced type metadata") var pm1: ParticleProcessMaterial = fx._emitters[1].process_material _ck(pm1.emission_shape == ParticleProcessMaterial.EMISSION_SHAPE_SPHERE_SURFACE, "emitter 1 edge sphere uses surface emission") _ck(abs(pm1.spread - 180.0) < 0.01, "OUTER emitter uses radial-velocity fallback spread") _ck(fx._lights.size() == 1, "EffectPlayer built 1 OmniLight3D") fx._process(0.11) _ck(fx._lights[0].visible, "SimpleLight becomes visible after start time") _ck(abs(fx._lights[0].omni_range - 0.06) < 0.02, "SimpleLight range follows TimeEventRange") var moving_rows: Array = [ [0.0, "MOVING_TYPE_DIRECT", 0.0, 0.0, 0.0], [1.0, "MOVING_TYPE_BEZIER_CURVE", 100.0, 0.0, 0.0, 0.0, 100.0, 0.0], [2.0, "MOVING_TYPE_DIRECT", 200.0, 0.0, 0.0], ] var direct_mid: Vector3 = fx._position_at(moving_rows, 0.5) var bezier_mid: Vector3 = fx._position_at(moving_rows, 1.5) _ck(abs(direct_mid.x - 0.5) < 0.001 and abs(direct_mid.y) < 0.001, "MOVING_TYPE_DIRECT interpolates in metres") _ck(abs(bezier_mid.x - 1.25) < 0.001 and abs(bezier_mid.y - 0.5) < 0.001, "MOVING_TYPE_BEZIER_CURVE uses previous control point") fx.play() _ck(g0.emitting, "emitter 0 emitting after play() (start_time 0)") fx._process(0.81) _ck(not g0.emitting, "finite particle cycle stops after LoopCount cycles") fx.queue_free() func _test_registry() -> void: var assets := AssetRoot.path() if not DirAccess.dir_exists_absolute(assets): print(" (skip registry: no assets)") return var fxr := EffectRegistry.new() fxr.setup(assets) # 已知存在: PC/ymir work/pc/warrior/effect/geompung_3_sword.mse var p := fxr.resolve("PC/ymir work/pc/warrior/effect/geompung_3_sword.mse") if p == "": p = fxr.resolve("geompung_3_sword") _ck(p != "" and FileAccess.file_exists(p), "registry resolved a real .mse (%s)" % p) if p != "": var s := fxr.spec_for("PC/ymir work/pc/warrior/effect/geompung.mse") _ck(s.get("particles", []).size() > 0, "real .mse parsed to >=1 particle (%d)" % s.get("particles", []).size()) var parent := Node3D.new() get_root().add_child(parent) var fx := fxr.spawn("PC/ymir work/pc/warrior/effect/geompung.mse", parent, true) _ck(fx != null and fx.get_parent() == parent, "spawn attached EffectPlayer") _ck(fx._mesh_nodes.size() > 0, "spawn built real .mde mesh instead of placeholder") if fx._mesh_nodes.size() > 0: var mn: Node3D = fx._mesh_nodes[0] _ck(int(mn.get_meta("mde_version", 0)) == 1, "real geompung.mde uses EffectData v1") _ck(mn.get_child_count() > 0 and mn.get_child(0) is MeshInstance3D, "mde geometry is MeshInstance3D") func _test_mde() -> void: var assets := AssetRoot.path() if not DirAccess.dir_exists_absolute(assets): print(" (skip mde: no assets)") return var path := assets.path_join("PC/ymir work/pc/warrior/effect/geompung.mde") if not FileAccess.file_exists(path): print(" (skip mde: fixture missing)") return var data := Mde.new() var mesh := data.parse_file(path) _ck(data.last_error == "", "mde parses EffectData v1 (%s)" % data.last_error) _ck(int(mesh.get("version", 0)) == 1, "mde version 1") _ck(int(mesh.get("geometry_count", 0)) == 3, "mde geometry count = 3") _ck(int(mesh.get("frame_count", 0)) == 2, "mde frame count = 2") var geometries: Array = mesh.get("geometries", []) _ck(geometries.size() == 3 and geometries[0].frames.size() == 2, "mde keeps every geometry frame") if geometries.size() > 0 and geometries[0].frames.size() > 0: var frame: Dictionary = geometries[0].frames[0] _ck(frame.vertices.size() == frame.uvs.size() and frame.vertices.size() > 0, "mde expands indexed vertices with UVs")