{
"position": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"parameters": [
{ "name": "scale", "itemSize": 1, "defaultTrackValue": { "itemSize": 1, "data": [1], "positions": [0] } },
{ "name": "color", "itemSize": 4, "defaultTrackValue": { "itemSize": 4, "data": [1, 1, 1, 1], "positions": [0] } }
],
"blendingMode": 0,
"layers": [
{
"imageURL": "data/textures/particle/UETools/x64/Circle_02.png",
"particleLife": { "min": 0.5, "max": 0.5 },
"particleSize": { "min": 0.5, "max": 0.5 },
"particleRotation": { "min": 0, "max": 0 },
"particleRotationSpeed": { "min": 0, "max": 0 },
"emissionShape": 3,
"emissionFrom": 1,
"emissionRate": 120,
"emissionImmediate": 0,
"parameterTracks": [
{ "name": "color", "track": { "itemSize": 4, "data": [0.9803921568627451, 0.023529411764705882, 0.8431372549019608, 0, 0.9803921568627451, 0.023529411764705882, 0.8431372549019608, 0.8392857142857143, 0.9803921568627451, 0.4734827264239028, 0.6531979458450047, 0.39880952380952384, 0.9803921568627451, 0.9803921568627451, 0.4392156862745098, 0], "positions": [0, 0.10869565217391304, 0.33152173913043476, 1] } },
{ "name": "scale", "track": { "itemSize": 1, "data": [0.8289938271604937, 0.8963271604938272, 0.8959259259259259, 0.6997001028806585, 0.4099999999999999, 0.3222222222222222], "positions": [0.005, 0.05, 0.14, 0.3, 0.7200000000000001, 0.98] } }
],
"position": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.1, "y": 0.1, "z": 0.1 },
"particleVelocityDirection": { "direction": { "x": 0, "y": 1, "z": 0 }, "angle": 0 },
"particleSpeed": { "min": 0.2, "max": 0.2 }
},
{
"imageURL": "data/textures/particle/UETools/x64/Star_12.png",
"particleLife": { "min": 1, "max": 1 },
"particleSize": { "min": 0.1, "max": 0.2 },
"particleRotation": { "min": -3, "max": 3 },
"particleRotationSpeed": { "min": 1, "max": 2 },
"emissionShape": 0,
"emissionFrom": 1,
"emissionRate": 34,
"emissionImmediate": 0,
"parameterTracks": [
{ "name": "color", "track": { "itemSize": 4, "data": [0.9882352941176471, 0.9490196078431372, 0.4549019607843137, 0, 0.9882352941176471, 0.9490196078431372, 0.4549019607843137, 0.9880952380952381, 0.9882352941176471, 0.9490196078431372, 0.4549019607843137, 0.9880952380952381, 0.9882352941176471, 0.9490196078431372, 0.4549019607843137, 0.1488095238095238], "positions": [0, 0.11956521739130435, 0.31521739130434784, 1] } },
{ "name": "scale", "track": { "itemSize": 1, "data": [1, 1, 0.9383333333333334], "positions": [0, 0.565, 0.995] } }
],
"position": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.2, "y": 0.2, "z": 0.2 },
"particleVelocityDirection": { "direction": { "x": 0, "y": 1, "z": 0 }, "angle": 0 },
"particleSpeed": { "min": 0, "max": 0 }
},
{
"imageURL": "data/textures/particle/UETools/x64/Star_12.png",
"particleLife": { "min": 1.7, "max": 2 },
"particleSize": { "min": 0.1, "max": 0.2 },
"particleRotation": { "min": 0, "max": 0 },
"particleRotationSpeed": { "min": 0, "max": 0 },
"emissionShape": 0,
"emissionFrom": 1,
"emissionRate": 10,
"emissionImmediate": 0,
"parameterTracks": [
{ "name": "color", "track": { "itemSize": 4, "data": [0.9882352941176471, 0.7098039215686275, 0.011764705882352941, 0, 0.9882352941176471, 0.3843137254901961, 0.6627450980392157, 0.6607142857142857, 0.9882352941176471, 0.33725490196078434, 0.7294117647058823, 0.9821428571428571, 0.9882352941176471, 0.011764705882352941, 0.6039215686274509, 0], "positions": [0, 0.1902173913043478, 0.5054347826086957, 0.983695652173913] } },
{ "name": "scale", "track": { "itemSize": 1, "data": [1.5050000000000001], "positions": [0.33999999999999997] } }
],
"position": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.3, "y": 0.3, "z": 0.3 },
"particleVelocityDirection": { "direction": { "x": 0, "y": 1, "z": 0 }, "angle": 3 },
"particleSpeed": { "min": 0.03, "max": 0.2 }
}
]
}
{
"position": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"parameters": [
{ "name": "scale", "itemSize": 1, "defaultTrackValue": { "itemSize": 1, "data": [1], "positions": [0] } },
{ "name": "color", "itemSize": 4, "defaultTrackValue": { "itemSize": 4, "data": [1, 1, 1, 1], "positions": [0] } }
],
"blendingMode": 0,
"layers": [
{
"imageURL": "data/textures/particle/UETools/x64/Smoke_14.png",
"particleLife": { "min": 9, "max": 9 },
"particleSize": { "min": 0.3, "max": 0.3 },
"particleRotation": { "min": 0, "max": 6.3 },
"particleRotationSpeed": { "min": 0, "max": 0 },
"emissionShape": 3,
"emissionFrom": 1,
"emissionRate": 9.3,
"emissionImmediate": 0,
"parameterTracks": [
{ "name": "scale", "track": { "itemSize": 1, "data": [2, 2.6, 2.9], "positions": [0, 0.5, 1] } },
{ "name": "color", "track": { "itemSize": 4, "data": [0.6666666666666666, 0.6666666666666666, 0.6666666666666666, 0, 0.6666666666666666, 0.6666666666666666, 0.6666666666666666, 0.7, 0.6666666666666666, 0.6666666666666666, 0.6666666666666666, 0.6, 0.6666666666666666, 0.6666666666666666, 0.6666666666666666, 0.125, 0.6666666666666666, 0.6666666666666666, 0.6666666666666666, 0], "positions": [0, 0.25, 0.5, 0.75, 1] } }
],
"position": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"particleVelocityDirection": { "direction": { "x": 0, "y": 1, "z": 0 }, "angle": 0.17481119672686515 },
"particleSpeed": { "min": 0.315, "max": 0.48500000000000004 }
}
]
}
{
"position": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"parameters": [
{ "name": "scale", "itemSize": 1, "defaultTrackValue": { "itemSize": 1, "data": [1], "positions": [0] } },
{ "name": "color", "itemSize": 4, "defaultTrackValue": { "itemSize": 4, "data": [1, 1, 1, 1], "positions": [0] } }
],
"preWarm": false,
"blendingMode": 0,
"layers": [
{
"imageURL": "data/textures/particle/UETools/x64/Stain_04.png",
"particleLife": { "min": 0.8, "max": 1 },
"particleSize": { "min": 0.2, "max": 0.25 },
"particleRotation": { "min": 0, "max": 6.283185307179586 },
"particleRotationSpeed": { "min": 0, "max": 0 },
"emissionShape": 0,
"emissionFrom": 1,
"emissionRate": 50,
"emissionImmediate": 0,
"parameterTracks": [
{ "name": "color", "track": { "itemSize": 4, "data": [0.5490196078431373, 0.40784313725490196, 0.30980392156862746, 0.8035714285714286, 0.2549019607843137, 0.19607843137254902, 0.17647058823529413, 0.9880952380952381, 0.21568627450980393, 0.1803921568627451, 0.0784313725490196, 0.9880952380952381, 0, 0, 0, 0], "positions": [0, 0.09239130434782611, 0.7821620011911852, 1] } },
{ "name": "scale", "track": { "itemSize": 1, "data": [1], "positions": [1] } }
],
"position": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.5, "y": 0.5, "z": 0.5 },
"particleVelocityDirection": { "direction": { "x": 0, "y": -1, "z": 0 }, "angle": 0 },
"particleSpeed": { "min": 2.2, "max": 2.6 }
},
{
"imageURL": "data/textures/particle/UETools/x64/Smoke_14.png",
"particleLife": { "min": 0.5, "max": 0.6 },
"particleSize": { "min": 0.03, "max": 0.05 },
"particleRotation": { "min": 0, "max": 0 },
"particleRotationSpeed": { "min": 0, "max": 0 },
"emissionShape": 0,
"emissionFrom": 1,
"emissionRate": 50,
"emissionImmediate": 0,
"parameterTracks": [
{ "name": "color", "track": { "itemSize": 4, "data": [0.596078431372549, 0.49019607843137253, 0.3058823529411765, 0.05952380952380952, 0.6235294117647059, 0.592156862745098, 0.5294117647058824, 0.48214285714285715, 0.5764705882352941, 0.5686274509803921, 0.5529411764705883, 0.34523809523809523, 0, 0, 0, 0], "positions": [0, 0.1793478260869565, 0.7119565217391305, 1] } }
],
"position": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.5, "y": 0.5, "z": 0.5 },
"particleVelocityDirection": { "direction": { "x": 0, "y": 1, "z": 0 }, "angle": 3 },
"particleSpeed": { "min": 0.44, "max": 0.5 }
},
{
"imageURL": "data/textures/particle/UETools/x64/Smoke_08.png",
"particleLife": { "min": 2, "max": 2 },
"particleSize": { "min": 0.7, "max": 0.7 },
"particleRotation": { "min": 0, "max": 6.3 },
"particleRotationSpeed": { "min": -0.1, "max": 0.1 },
"emissionShape": 0,
"emissionFrom": 1,
"emissionRate": 50,
"emissionImmediate": 0,
"parameterTracks": [
{ "name": "color", "track": { "itemSize": 4, "data": [0.9882352941176471, 0.9882352941176471, 0.9882352941176471, 0.005952380952380952, 0.9882352941176471, 0.9137254901960784, 0.8196078431372549, 0.25595238095238093, 0.011764705882352941, 0.011764705882352941, 0.011764705882352941, 0.017857142857142856], "positions": [0, 0.1739130434782609, 1] } },
{ "name": "scale", "track": { "itemSize": 1, "data": [0.5, 0.8626041666666667, 0.9834722222222223], "positions": [0.2, 0.62, 0.9750000000000001] } }
],
"position": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.5, "y": 0.5, "z": 0.5 },
"particleVelocityDirection": { "direction": { "x": 0, "y": 1, "z": 0 }, "angle": 0 },
"particleSpeed": { "min": -0.5, "max": -0.5 }
}
]
}
{
"position": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"parameters": [
{ "name": "scale", "itemSize": 1, "defaultTrackValue": { "itemSize": 1, "data": [1], "positions": [0] } },
{ "name": "color", "itemSize": 4, "defaultTrackValue": { "itemSize": 4, "data": [1, 1, 1, 1], "positions": [0] } }
],
"blendingMode": 0,
"layers": [
{
"imageURL": "data/textures/particle/UETools/x64/Light_Beam_04.png",
"particleLife": { "min": 0.2, "max": 0.2 },
"particleSize": { "min": 0.35, "max": 0.35 },
"particleRotation": { "min": -3, "max": 3 },
"particleRotationSpeed": { "min": 0, "max": 0 },
"emissionShape": 3,
"emissionFrom": 1,
"emissionRate": 180,
"emissionImmediate": 0,
"parameterTracks": [
{ "name": "color", "track": { "itemSize": 4, "data": [0.9882352941176471, 0.9882352941176471, 0.9882352941176471, 0.875, 0.9490196078431372, 0.9882352941176471, 0.807843137254902, 0.8035714285714286, 0.9529411764705882, 0.9882352941176471, 0.011764705882352941, 0.8928571428571429, 0.9490196078431372, 0.9882352941176471, 0.5568627450980392, 0.831874810506877, 0.9254901960784314, 0.9882352941176471, 0.07058823529411765, 0.8630952380952381, 0.8549019607843137, 0.9882352941176471, 0.01568627450980392, 0], "positions": [0, 0.043478260869565216, 0.42934782608695654, 0.16579809410363308, 0.6191185229303157, 1] } },
{ "name": "scale", "track": { "itemSize": 1, "data": [1.0000284682291665, 1.0000373125771604, 0.9483778345360352, 0.8304531540639897, 0.7711138941647975, 0.6556243239902346], "positions": [0, 0.105, 0.265, 0.41999999999999993, 0.6449999999999999, 0.955] } }
],
"position": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.1, "y": 0.1, "z": 0.1 },
"particleVelocityDirection": { "direction": { "x": 0, "y": 1, "z": 0 }, "angle": 6.283185307179586 },
"particleSpeed": { "min": 0.15, "max": 0.25 }
},
{
"imageURL": "data/textures/particle/travnik/plus_1.png",
"particleLife": { "min": 0.55, "max": 0.7 },
"particleSize": { "min": 0.05, "max": 0.1 },
"particleRotation": { "min": 0, "max": 0 },
"particleRotationSpeed": { "min": 0, "max": 0 },
"emissionShape": 0,
"emissionFrom": 1,
"emissionRate": 43,
"emissionImmediate": 0,
"parameterTracks": [
{ "name": "color", "track": { "itemSize": 4, "data": [0.45098039215686275, 0.9882352941176471, 0.15294117647058825, 0.9821428571428571, 0.45098039215686275, 0.9882352941176471, 0.15294117647058825, 0], "positions": [0.5706521739130435, 0.9836956521739131] } },
{ "name": "scale", "track": { "itemSize": 1, "data": [0.8566666666666667, 1.1319203456790123], "positions": [0.13, 1] } }
],
"position": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.15, "y": 0.15, "z": 0.15 },
"particleVelocityDirection": { "direction": { "x": 0, "y": 1, "z": 0 }, "angle": 3 },
"particleSpeed": { "min": 0.03, "max": 0.1 }
},
{
"imageURL": "data/textures/particle/UETools/x64/Star_24.png",
"particleLife": { "min": 0.17, "max": 0.2 },
"particleSize": { "min": 0.26, "max": 0.35 },
"particleRotation": { "min": 0, "max": 0 },
"particleRotationSpeed": { "min": 0, "max": 0 },
"emissionShape": 0,
"emissionFrom": 1,
"emissionRate": 23,
"emissionImmediate": 0,
"parameterTracks": [
{ "name": "color", "track": { "itemSize": 4, "data": [0.9882352941176471, 0.9882352941176471, 0.9882352941176471, 0.6726190476190477, 0.9882352941176471, 0.9882352941176471, 0.9882352941176471, 0.9880952380952381, 0.9882352941176471, 0.9882352941176471, 0.9882352941176471, 0.9620584572947379, 0.9882352941176471, 0.9882352941176471, 0.9882352941176471, 0.5535714285714286, 0.9882352941176471, 0.9882352941176471, 0.9882352941176471, 0], "positions": [0, 0.10326086956521738, 0.6304347826086957, 0.7381625967837998, 1] } },
{ "name": "scale", "track": { "itemSize": 1, "data": [0.9699871782407408, 1.859251561547733, 1.9704398895068476, 1.9227992842084616, 1.673623753579152, 1], "positions": [0.015434782608695685, 0.07782608695652167, 0.19, 0.625, 0.7849999999999999, 1] } }
],
"position": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.3, "y": 0.3, "z": 0.3 },
"particleVelocityDirection": { "direction": { "x": 0, "y": 1, "z": 0 }, "angle": 0 },
"particleSpeed": { "min": 0, "max": 0 }
}
]
}
{
"position": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 10, "y": 10, "z": 10 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"parameters": [
{ "name": "scale", "itemSize": 1, "defaultTrackValue": { "itemSize": 1, "data": [1], "positions": [0] } },
{ "name": "color", "itemSize": 4, "defaultTrackValue": { "itemSize": 4, "data": [1, 1, 1, 1], "positions": [0] } }
],
"blendingMode": 0,
"layers": [
{
"imageURL": "data/textures/particle/UETools/x64/Star_42.png",
"particleLife": { "min": 0.23, "max": 0.23 },
"particleSize": { "min": 0.18, "max": 0.23 },
"particleRotation": { "min": -3, "max": 3 },
"particleRotationSpeed": { "min": 0, "max": 0 },
"emissionShape": 3,
"emissionFrom": 1,
"emissionRate": 120,
"emissionImmediate": 0,
"parameterTracks": [
{ "name": "color", "track": { "itemSize": 4, "data": [0.9882352941176471, 0.9882352941176471, 0.9882352941176471, 0.9880952380952381, 0.8705882352941177, 0.9333333333333333, 0.9882352941176471, 0.9880952380952381, 0.6705882352941176, 0.796078431372549, 0.9882352941176471, 0.9880952380952381, 0.7764705882352941, 0.9450980392156862, 0.9882352941176471, 0.5416666666666666, 0.1450980392156863, 0.5372549019607843, 0.7568627450980392, 0.3333333333333333, 0, 0, 0, 0], "positions": [0, 0.11956521739130435, 0.2282608695652174, 0.4592763549731983, 0.7006402620607505, 1] } },
{ "name": "scale", "track": { "itemSize": 1, "data": [1.0295461111111108, 1.0194342777777776, 0.899218762345679, 0.8037858445216051, 0.6352494305555556], "positions": [0.005, 0.24000000000000002, 0.3799999999999999, 0.71, 1] } }
],
"position": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.12, "y": 0.12, "z": 0.12 },
"particleVelocityDirection": { "direction": { "x": 0, "y": 1, "z": 0 }, "angle": 0.3141592653589793 },
"particleSpeed": { "min": 0.15, "max": 0.25 }
},
{
"imageURL": "data/textures/particle/UETools/x64/Star_12.png",
"particleLife": { "min": 1.7, "max": 2 },
"particleSize": { "min": 0.1, "max": 0.15 },
"particleRotation": { "min": 0, "max": 0 },
"particleRotationSpeed": { "min": 0, "max": 0 },
"emissionShape": 0,
"emissionFrom": 1,
"emissionRate": 15,
"emissionImmediate": 0,
"parameterTracks": [
{ "name": "color", "track": { "itemSize": 4, "data": [0.0196078431372549, 0.6431372549019608, 0.9882352941176471, 0, 0.9882352941176471, 0.9882352941176471, 0.9882352941176471, 0.9880952380952381, 0.8470588235294118, 0.8470588235294118, 0.9882352941176471, 0.9880952380952381, 0.396078431372549, 0.011764705882352941, 0.9882352941176471, 0], "positions": [0, 0.21195652173913043, 0.5760869565217391, 0.9836956521739131] } },
{ "name": "scale", "track": { "itemSize": 1, "data": [1, 1, 0.5], "positions": [0, 0.6, 1] } }
],
"position": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.01, "y": 0.01, "z": 0.01 },
"particleVelocityDirection": { "direction": { "x": 0, "y": 1, "z": 0 }, "angle": 3 },
"particleSpeed": { "min": 0.05, "max": 0.3 }
},
{
"imageURL": "data/textures/particle/UETools/x64/Arc_01.png",
"particleLife": { "min": 0.6, "max": 0.6 },
"particleSize": { "min": 0.3, "max": 0.35 },
"particleRotation": { "min": -3, "max": 3 },
"particleRotationSpeed": { "min": 0, "max": 0 },
"emissionShape": 3,
"emissionFrom": 1,
"emissionRate": 80,
"emissionImmediate": 0,
"parameterTracks": [
{ "name": "color", "track": { "itemSize": 4, "data": [0.5882352941176471, 0.803921568627451, 0.9686274509803922, 0.8630952380952381, 0.4196078431372549, 0.8549019607843137, 0.9686274509803922, 0.07142857142857142], "positions": [0.6684782608695652, 0.967391304347826] } },
{ "name": "scale", "track": { "itemSize": 1, "data": [0.41000000000000003], "positions": [0.31] } }
],
"position": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"particleVelocityDirection": { "direction": { "x": 0, "y": 1, "z": 0 }, "angle": 0 },
"particleSpeed": { "min": 0, "max": 0 }
}
]
}
// Five particle emitters on Meep's Particular engine.
//
// Each emitter is defined entirely by a JSON spec — the JSON describes
// blending, parameters (color / scale tracks), and a stack of particle
// layers (texture, emission shape/rate, life, size, velocity, etc.).
// `ParticleEmitter.fromJSON` reconstructs the runtime object.
//
// Engine pieces in use:
// - EngineHarness.bootstrap / .buildBasics — default orbital camera and lights.
// - ParticleEmitterSystem — drives all visible emitters.
// - ShadedGeometrySystem — renders the ground plane.
import { OrbitingBehavior } from "@woosh/meep-engine/src/engine/intelligence/behavior/ecs/OrbitingBehavior.js";
import * as THREE from "three";
import { EngineHarness } from "@woosh/meep-engine/src/engine/EngineHarness.js";
import Entity from "@woosh/meep-engine/src/engine/ecs/Entity.js";
import { Transform } from "@woosh/meep-engine/src/engine/ecs/transform/Transform.js";
import Vector3 from "@woosh/meep-engine/src/core/geom/Vector3.js";
import { ShadedGeometry } from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/ShadedGeometry.js";
import { ShadedGeometryFlags } from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/ShadedGeometryFlags.js";
import { ShadedGeometrySystem } from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js";
import { ParticleEmitter } from "@woosh/meep-engine/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitter.js";
import { ParticleEmitterSystem } from "@woosh/meep-engine/src/engine/graphics/particles/ecs/ParticleEmitterSystem.js";
import { BehaviorComponent } from "@woosh/meep-engine/src/engine/intelligence/behavior/ecs/BehaviorComponent.js";
import { BehaviorSystem } from "@woosh/meep-engine/src/engine/intelligence/behavior/ecs/BehaviorSystem.js";
import { EntityBehavior } from "@woosh/meep-engine/src/engine/intelligence/behavior/ecs/EntityBehavior.js";
import { BehaviorStatus } from "@woosh/meep-engine/src/engine/intelligence/behavior/BehaviorStatus.js";
import buffProjectile from "./specs/buff-projectile.json";
import magicAttackProjectile from "./specs/magic-attack-projectile.json";
import healingProjectile from "./specs/healing-projectile.json";
import earthProjectile from "./specs/earth-projectile.json";
import chimneySmokeLight from "./specs/chimney-smoke-light.json";
// ─── Scene parameters ───────────────────────────────────────────────────────
const GROUND_SIZE = 60; // small ground; camera sits close
const HALF_GROUND = GROUND_SIZE / 2;
// 5 emitters in a row along X, hovering just above the ground.
// `period` is the duration of one full orbit in seconds.
const EMITTERS = [
{ name: "buff", spec: buffProjectile, x: -6, y: 1.0, z: 0, period: 1.1113 },
{ name: "magic-attack", spec: magicAttackProjectile, x: -3, y: 1.0, z: 0, period: 1.237 },
{ name: "healing", spec: healingProjectile, x: 0, y: 1.0, z: 0, period: 1.391 },
{ name: "earth", spec: earthProjectile, x: 3, y: 1.0, z: 0, period: 1.6 },
{ name: "chimney-smoke", spec: chimneySmokeLight, x: 6, y: 0.5, z: 0, period: 4 },
];
// ─── OrbitingBehavior ───────────────────────────────────────────────────────
//
// A minimal EntityBehavior that drives the entity's Transform around a fixed
// world-space center at constant angular rate. Drop it into a
// BehaviorComponent and the engine's BehaviorSystem ticks it.
// ─── Procedural ground texture ──────────────────────────────────────────────
// Same two-scale grid as the other example. Olive base, minor lines every
// 2 world units, major lines every 8.
const GROUND_MINOR_WORLD_UNITS = 2;
const GROUND_MAJOR_WORLD_UNITS = 8;
const GROUND_MINOR_PER_TILE = 8;
const GROUND_TILE_WORLD_UNITS = GROUND_MINOR_PER_TILE * GROUND_MINOR_WORLD_UNITS;
const MAJOR_STEP_CELLS = GROUND_MAJOR_WORLD_UNITS / GROUND_MINOR_WORLD_UNITS;
function makeGroundTexture() {
const size = 256;
const cellPx = size / GROUND_MINOR_PER_TILE;
const canvas = document.createElement("canvas");
canvas.width = canvas.height = size;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#3c4838";
ctx.fillRect(0, 0, size, size);
function crosshatch(positions, lineWidth, color) {
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
for (const p of positions) {
ctx.beginPath();
ctx.moveTo(p, 0); ctx.lineTo(p, size);
ctx.moveTo(0, p); ctx.lineTo(size, p);
ctx.stroke();
}
}
const minorPositions = [];
for (let i = 1; i < GROUND_MINOR_PER_TILE; i++) {
if (i % MAJOR_STEP_CELLS === 0) continue;
minorPositions.push(Math.floor(i * cellPx) + 0.5);
}
crosshatch(minorPositions, 1, "rgba(255, 255, 255, 0.09)");
const majorPositions = [];
for (let i = 0; i < GROUND_MINOR_PER_TILE; i += MAJOR_STEP_CELLS) {
majorPositions.push(i === 0 ? 1.5 : Math.floor(i * cellPx) + 0.5);
}
crosshatch(majorPositions, 1.5, "rgba(255, 255, 255, 0.20)");
const texture = new THREE.CanvasTexture(canvas);
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
const tiles = GROUND_SIZE / GROUND_TILE_WORLD_UNITS;
texture.repeat.set(tiles, tiles);
texture.anisotropy = 16;
// Up close the camera magnifies the texture; bilinear blurs the crisp
// grid lines, NearestFilter keeps them sharp. Mipmapped LinearMipmapLinear
// is still used for minification so distance still looks smooth.
texture.magFilter = THREE.NearestFilter;
return texture;
}
// ─── Bootstrap ──────────────────────────────────────────────────────────────
const engine = await EngineHarness.bootstrap({
configuration: (config, engine) => {
config.addSystem(new ShadedGeometrySystem(engine));
config.addSystem(new ParticleEmitterSystem(engine));
config.addSystem(new BehaviorSystem(engine));
},
});
await EngineHarness.buildBasics({
engine,
enableTerrain: false,
enableWater: false,
enableLights: true,
enableShadows: false,
focus: new Vector3(0, 1, 0),
distance: 14,
pitch: 0.6,
yaw: 0.4,
cameraFieldOfView: 45,
cameraFarDistance: 200,
// default orbital + keyboard controller — drag to orbit, WASD to pan,
// wheel to zoom.
showFps: false,
});
const ecd = engine.entityManager.dataset;
// ─── Ground plane ───────────────────────────────────────────────────────────
const groundGeometry = new THREE.PlaneGeometry(GROUND_SIZE, GROUND_SIZE);
groundGeometry.rotateX(-Math.PI / 2);
const groundMaterial = new THREE.MeshLambertMaterial({ map: makeGroundTexture() });
const groundSG = ShadedGeometry.from(groundGeometry, groundMaterial);
groundSG.clearFlag(ShadedGeometryFlags.CastShadow);
groundSG.setFlag(ShadedGeometryFlags.ReceiveShadow);
new Entity().add(new Transform()).add(groundSG).build(ecd);
// ─── Spawn the emitters ─────────────────────────────────────────────────────
for (let i = 0; i < EMITTERS.length; i++) {
const def = EMITTERS[i];
const emitter = new ParticleEmitter();
emitter.fromJSON(def.spec);
// The specs were authored with arbitrary world positions baked in; the
// Transform on the entity is what controls the actual world position.
// Zero the emitter's own position so we don't double up.
emitter.position.set(0, 0, 0);
// Orbit each emitter around its base position. Per-emitter rate and phase
// so they don't move in lockstep.
const orbit = OrbitingBehavior.from({
center: new Vector3(def.x, def.y, def.z),
radius: 0.6,
rate: (2 * Math.PI) / def.period,
phase: (i / EMITTERS.length) * Math.PI * 2,
});
new Entity()
.add(Transform.fromJSON({ position: { x: def.x, y: def.y, z: def.z } }))
.add(emitter)
.add(BehaviorComponent.from(orbit))
.build(ecd);
}
// ─── HUD: FPS readout ──────────────────────────────────────────────────────
const fpsEl = document.getElementById("fps");
let fpsWindow = 0;
let fpsFrames = 0;
let lastFrame = performance.now();
engine.graphics.on.postRender.add(() => {
const now = performance.now();
const dt = (now - lastFrame) / 1000;
lastFrame = now;
fpsWindow += dt;
fpsFrames++;
if (fpsWindow >= 0.5) {
fpsEl.textContent = (fpsFrames / fpsWindow).toFixed(0);
fpsWindow = 0;
fpsFrames = 0;
}
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Particle emitters · Meep</title>
<meta name="robots" content="noindex">
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body {
margin: 0; padding: 0;
width: 100%; height: 100%;
overflow: hidden;
background: #07090c;
color: #e6edf3;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
.panel {
position: fixed;
z-index: 100;
background: rgba(7, 9, 12, 0.72);
border: 1px solid #1f2731;
border-radius: 10px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 12px 32px rgba(0,0,0,0.4);
}
.hud {
top: 1rem;
left: 1rem;
padding: 0.8rem 1rem;
font-family: ui-monospace, "JetBrains Mono", monospace;
font-size: 0.82rem;
line-height: 1.7;
color: #9aa5b1;
}
.hud .label {
color: #6b7785;
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 0.65rem;
margin-right: 0.5rem;
}
.hud .value { color: #4ef0a8; }
.back {
top: 1rem; right: 1rem;
font-family: ui-monospace, monospace;
font-size: 0.78rem;
color: #4ef0a8;
text-decoration: none;
padding: 0.55em 0.95em;
}
.back:hover { color: #2dd185; }
.legend {
bottom: 1rem; left: 1rem;
padding: 0.8rem 1rem;
font-size: 0.82rem;
color: #9aa5b1;
}
.legend kbd {
font-family: ui-monospace, monospace;
font-size: 0.78em;
color: #e6edf3;
background: #11161d;
border: 1px solid #2a3441;
border-bottom-width: 2px;
border-radius: 3px;
padding: 0.05em 0.4em;
}
</style>
</head>
<body>
<div class="panel hud">
<div><span class="label">fps</span><span class="value" id="fps">--</span></div>
<div><span class="label">emitters</span><span class="value" id="count">5</span></div>
</div>
<div class="panel legend">
drag to orbit · <kbd>WASD</kbd> to pan · scroll to zoom
</div>
<script type="module" src="./src/main.js"></script>
</body>
</html>
{
"name": "@meep-examples/particles",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Five particle emitters from JSON specs, on the Particular engine.",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@woosh/meep-engine": "^2.138.3",
"three": "0.136.0"
},
"devDependencies": {
"vite": "^8.0.13"
}
}
# particles
Five particle emitters running on Meep's Particular engine. Each emitter is reconstructed from a JSON spec via `ParticleEmitter.fromJSON`.
## Run locally
```bash
npm install
npm run dev
```
## Build
```bash
npm run build
```
Output goes to `../../public/examples/particles/demo.html`.
## Assets
The JSON specs in `src/specs/` reference textures via relative paths like `data/textures/particle/UETools/x64/Circle_02.png`. Those paths are resolved against the demo's URL at runtime, so the matching files need to live under this example's `public/data/textures/particle/...` tree before `npm run dev` / `npm run build`.
Vite copies the whole `public/` folder verbatim into the build output, so after a build the textures end up at `/examples/particles/data/textures/particle/...` and the JSON `imageURL` paths resolve correctly relative to `demo.html`.
The textures themselves are not redistributed with the example source — bring your own particle atlases or substitute paths in the specs.
## What this demonstrates
- **`ParticleEmitter` + JSON specs.** Each spec is a complete description of an emitter: blending mode, parameter tracks, and a stack of particle layers. `ParticleEmitter.fromJSON(spec)` reconstructs the runtime; the engine handles texture loading, shader compilation, and per-frame simulation.
- **`ParticleEmitterSystem`.** Registered once at engine bootstrap. Tracks every `[ParticleEmitter, Transform]` entity, drives their per-frame updates, and renders all of them as part of the engine's render pipeline.
- **Behavior trees.** Each emitter orbits a small radius via an inline `OrbitingBehavior extends EntityBehavior`, driven by `BehaviorSystem`.
- **Spec-based authoring.** The five effects here (buff, magic attack, healing, earth, chimney smoke) come from an in-house authoring tool. The same JSON shape is what you'd hand-write or export from that tool.
import { defineConfig } from "vite";
import { fileURLToPath } from "node:url";
import { resolve, dirname } from "node:path";
const __dirname = dirname(fileURLToPath(import.meta.url));
export default defineConfig({
base: "./",
build: {
outDir: resolve(__dirname, "../../public/examples/particles"),
emptyOutDir: false,
rollupOptions: {
input: resolve(__dirname, "demo.html"),
},
target: "es2022",
},
});
plus_1.png · 0.2 KB
Arc_01.png · 1.3 KB
Circle_02.png · 2.7 KB
Light_Beam_04.png · 3.0 KB
Smoke_08.png · 3.4 KB
Smoke_14.png · 3.1 KB
Stain_04.png · 3.6 KB
Star_12.png · 2.3 KB
Star_24.png · 1.5 KB
Star_42.png · 3.0 KB