Files
mtgodot-poc/project/effect_rotation_test.gd
T

104 lines
5.7 KiB
GDScript

extends SceneTree
const EffectPlayer = preload("res://fx/effect_player.gd")
var failures := 0
func check(ok: bool, label: String) -> void:
if not ok:
failures += 1
printerr("FAIL: " + label)
func _init() -> void:
var fx := EffectPlayer.new()
for kind in [0, 2, 3]:
fx.build({"particles": [{"particle": {"RotationType": kind, "RotationSpeed": 180,
"RotationRandomStartingBegin": 30, "RotationRandomStartingEnd": 60, "BillboardType": 1}}]})
var g: GPUParticles3D = fx._emitters[-1]
var pm: ParticleProcessMaterial = g.process_material
var speed := 0.0 if kind == 0 else (180.0 if kind == 2 else -180.0)
check(pm.angular_velocity_min == speed and pm.angular_velocity_max == speed,
"constant rotation type %d uses degrees without random slowing" % kind)
check(pm.angle_min == 30.0 and pm.angle_max == 60.0, "initial angle range type %d" % kind)
check(g.material_override.billboard_mode == BaseMaterial3D.BILLBOARD_PARTICLES,
"camera billboard consumes particle rotation type %d" % kind)
check(fx._billboard(0) == BaseMaterial3D.BILLBOARD_DISABLED, "non-billboard unchanged")
check(fx._billboard(1) == BaseMaterial3D.BILLBOARD_ENABLED, "mesh camera billboard unchanged")
check(fx._billboard(2) == BaseMaterial3D.BILLBOARD_FIXED_Y, "source Y enum is two")
check(fx._billboard(4) != BaseMaterial3D.BILLBOARD_FIXED_Y, "two-face is not misclassified as Y")
fx.free()
var path := AssetRoot.path().path_join("PC/ymir work/pc/shaman/effect/7budongbakbu_throw.mse")
var spec: Dictionary = preload("res://fx/mse.gd").new().parse_file(path)
check(not spec.get("particles", []).is_empty(), "real shaman rotation resource loaded")
var count := 0
for particle in spec.get("particles", []):
var prop: Dictionary = particle.particle.duplicate(true)
var kind := int(prop.get("RotationType", 0))
if kind != 2 and kind != 3:
continue
# Isolate rotation/material configuration without loading original textures.
prop.erase("TextureFiles")
var real_fx := EffectPlayer.new()
real_fx.build({"particles": [{"particle": prop}]})
var pm: ParticleProcessMaterial = real_fx._emitters[0].process_material
var expected := float(prop.get("RotationSpeed", 0)) * (1.0 if kind == 2 else -1.0)
check(pm.angular_velocity_min == expected and pm.angular_velocity_max == expected, "real constant speed")
check(pm.angle_min == float(prop.get("RotationRandomStartingBegin", 0)) and
pm.angle_max == float(prop.get("RotationRandomStartingEnd", 0)), "real initial angle range")
count += 1
real_fx.free()
check(count > 0, "real constant-rotation coverage")
print("real rotation tracks: particles=%d" % count)
time_event_rotation()
real_time_event_rotation()
print("effect_rotation_test: failures=%d" % failures)
quit(1 if failures else 0)
func time_event_rotation() -> void:
var fx := EffectPlayer.new()
fx.build({"particles": [{"particle": {"RotationType": 1, "RotationSpeed": 999,
"TimeEventRotation": [[0.0, 180.0], [0.5, -180.0], [1.0, 0.0]],
"RotationRandomStartingBegin": 30, "RotationRandomStartingEnd": 30}},
{"particle": {"RotationType": 1, "RotationSpeed": 999}}]})
var pm: ParticleProcessMaterial = fx._emitters[0].process_material
check(pm.angular_velocity_curve is CurveTexture, "TIME_EVENT has a speed curve")
if pm.angular_velocity_curve is CurveTexture:
var curve: Curve = pm.angular_velocity_curve.curve
# Godot 4.7 curve branch integrates sampled value * base rate directly
# into CUSTOM.x radians; unlike the constant branch it does not convert.
check(is_equal_approx(curve.sample(0.0) * pm.angular_velocity_min, PI), "birth rate is pi radians/sec")
check(is_zero_approx(curve.sample(0.25)), "speed passes through zero")
check(is_equal_approx(curve.sample(0.75) * pm.angular_velocity_min, -PI / 2.0), "negative rate preserved")
check(is_zero_approx(curve.sample(1.0)), "end stops without erasing earlier motion")
var angle := deg_to_rad(pm.angle_min)
for i in range(2000):
angle += curve.sample((float(i) + 0.5) / 2000.0) * pm.angular_velocity_min * 2.0 / 2000.0
check(absf(angle - deg_to_rad(-60.0)) < 0.0001, "two-second lifetime integrates speed plus initial angle")
var empty: ParticleProcessMaterial = fx._emitters[1].process_material
check(empty.angular_velocity_curve is CurveTexture, "empty TIME_EVENT remains an explicit zero curve")
if empty.angular_velocity_curve is CurveTexture:
check(is_zero_approx(empty.angular_velocity_curve.curve.sample(0.5)), "empty track ignores RotationSpeed fallback")
fx.free()
func real_time_event_rotation() -> void:
var count := 0
var samples := 0
for name in ["jeungsok_blow", "8chojeonbu", "jeungryeok_hand"]:
var path := AssetRoot.path().path_join("PC/ymir work/pc/shaman/effect/" + name + ".mse")
var spec: Dictionary = preload("res://fx/mse.gd").new().parse_file(path)
check(not spec.get("particles", []).is_empty(), "real TIME_EVENT file " + name)
for particle in spec.get("particles", []):
if int(particle.particle.get("RotationType", 0)) != 1:
continue
var rows: Array = particle.particle.get("TimeEventRotation", [])
var fx := EffectPlayer.new()
fx.build({"particles": [{"particle": {"RotationType": 1, "TimeEventRotation": rows}}]})
var pm: ParticleProcessMaterial = fx._emitters[0].process_material
check(pm.angular_velocity_curve is CurveTexture, "real TIME_EVENT material")
if pm.angular_velocity_curve is CurveTexture:
for row in rows:
if float(row[0]) >= 0.0 and float(row[0]) <= 1.0:
var actual: float = pm.angular_velocity_curve.curve.sample(float(row[0])) * pm.angular_velocity_min
check(is_equal_approx(actual, deg_to_rad(float(row[1]))), "real TIME_EVENT radians " + name)
samples += 1
count += 1
fx.free()
check(count > 0 and samples > 0, "real time-dependent speed coverage")
print("real time-event tracks: particles=%d keys=%d" % [count, samples])