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 effective(pm: ParticleProcessMaterial, age: float) -> Color: return pm.color * pm.color_ramp.gradient.sample(age) if pm.color_ramp else pm.color func _init() -> void: var fx := EffectPlayer.new() fx.build({"particles": [{"particle": { "TimeEventColorRed": [[0.0, 0.2], [0.5, 0.8], [1.0, 0.4]], "TimeEventColorGreen": [[0.25, 0.4], [0.75, 0.8]], "TimeEventColorBlue": [[0.0, 0.5]], "TimeEventAlpha": [[0.0, 1.0], [1.0, 0.0]]}}, {"particle": {"TimeEventColorRed": [[0.0, 0.5]], "TimeEventAlpha": [[0.0, 1.0]]}}, {"particle": {"TimeEventColorBlue": [[0.0, 0.2], [1.0, 0.8]]}}, {"particle": {}}]}) var pm: ParticleProcessMaterial = fx._emitters[0].process_material check(pm.color == Color.WHITE, "neutral base prevents double tint") check(effective(pm, 0.0).is_equal_approx(Color(0.2, 0.4, 0.5, 1.0)), "birth RGBA") check(effective(pm, 0.25).is_equal_approx(Color(0.5, 0.4, 0.5, 0.75)), "independent green knot") check(effective(pm, 0.5).is_equal_approx(Color(0.8, 0.6, 0.5, 0.5)), "middle RGBA") check(effective(pm, 0.75).is_equal_approx(Color(0.6, 0.8, 0.5, 0.25)), "late RGBA") check(effective(pm, 1.0).is_equal_approx(Color(0.4, 0.8, 0.5, 0.0)), "end RGBA") check(effective(fx._emitters[1].process_material, 0.5).is_equal_approx(Color(0.5, 1.0, 1.0)), "constant 0.5 tint remains 0.5, not 0.25") check(effective(fx._emitters[2].process_material, 0.5).is_equal_approx(Color(1.0, 1.0, 0.5)), "RGB animates without alpha track") check(effective(fx._emitters[3].process_material, 0.5) == Color.WHITE, "missing tracks default white") fx.free() var malformed := EffectPlayer.new() var cleaned: GradientTexture1D = malformed._color_ramp({"TimeEventColorRed": [ [1.0, 0.8], [], [0.0, 0.1], [0.0, 0.2], [NAN, 0.5], [0.5, INF], ["bad", 0.4]]}) check(cleaned.gradient.sample(0.5).is_equal_approx(Color(0.5, 1, 1)), "unordered/duplicate keys normalized and invalid values ignored") malformed.free() real_resources() print("effect_color_test: failures=%d" % failures) quit(1 if failures else 0) # Independent scalar interpolation oracle for the sorted, valid asset tracks. func reference(rows: Array, age: float) -> float: if rows.is_empty(): return 1.0 if age <= float(rows[0][0]): return float(rows[0][1]) for i in range(1, rows.size()): if age <= float(rows[i][0]): var fraction := (age - float(rows[i - 1][0])) / (float(rows[i][0]) - float(rows[i - 1][0])) return float(rows[i - 1][1]) * (1.0 - fraction) + float(rows[i][1]) * fraction return float(rows[-1][1]) func real_resources() -> void: var count := 0 for name in ["warrior/effect/palbang_4_sword.mse", "assassin/effect/beam_blow.mse", "sura/effect/pabeopsul_4_blow.mse", "shaman/effect/noejeon_making_1.mse"]: var path := AssetRoot.path().path_join("PC/ymir work/pc/" + name) var parsed: Dictionary = preload("res://fx/mse.gd").new().parse_file(path) check(not parsed.get("particles", []).is_empty(), "real resource loaded: " + name) for particle in parsed.get("particles", []): # Exercise real RGBA through material construction; texture loading and # emission geometry are deliberately outside this color-only regression. var prop: Dictionary = {} for key in ["TimeEventColorRed", "TimeEventColorGreen", "TimeEventColorBlue", "TimeEventAlpha"]: prop[key] = particle.particle.get(key, []) var fx := EffectPlayer.new() fx.build({"particles": [{"particle": prop}]}) var pm: ParticleProcessMaterial = fx._emitters[0].process_material for step in range(41): var age := float(step) / 40.0 var expected := Color(reference(prop.TimeEventColorRed, age), reference(prop.TimeEventColorGreen, age), reference(prop.TimeEventColorBlue, age), reference(prop.TimeEventAlpha, age)) check(effective(pm, age).is_equal_approx(expected), "real RGBA %s particle %d t=%f" % [name, count, age]) count += 1 fx.free() print("real color tracks: particles=%d samples=%d" % [count, count * 41])