diff --git a/build.gradle b/build.gradle index 7244c66e81..830f716d9b 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,6 @@ plugins { } repositories { - mavenLocal() maven { url = 'https://repo.runelite.net' } diff --git a/src/main/java/rs117/hd/HdPlugin.java b/src/main/java/rs117/hd/HdPlugin.java index 01f211c80d..f961968433 100644 --- a/src/main/java/rs117/hd/HdPlugin.java +++ b/src/main/java/rs117/hd/HdPlugin.java @@ -68,7 +68,11 @@ import net.runelite.client.plugins.PluginDescriptor; import net.runelite.client.plugins.PluginManager; import net.runelite.client.plugins.entityhider.EntityHiderPlugin; +import net.runelite.client.ui.ClientToolbar; import net.runelite.client.ui.ClientUI; +import net.runelite.client.ui.NavigationButton; +import net.runelite.client.ui.components.colorpicker.ColorPickerManager; +import net.runelite.client.util.ImageUtil; import net.runelite.client.util.LinkBrowser; import net.runelite.client.util.OSType; import net.runelite.rlawt.AWTContext; @@ -110,6 +114,9 @@ import rs117.hd.scene.ModelOverrideManager; import rs117.hd.scene.ProceduralGenerator; import rs117.hd.scene.SceneContext; +import rs117.hd.scene.particles.ParticleManager; +import rs117.hd.scene.particles.debug.ParticleGizmoOverlay; +import rs117.hd.scene.particles.debug.ParticleSidebarPanel; import rs117.hd.scene.TextureManager; import rs117.hd.scene.TileOverrideManager; import rs117.hd.scene.WaterTypeManager; @@ -165,6 +172,7 @@ public class HdPlugin extends Plugin { public static final int TEXTURE_UNIT_SHADOW_MAP = GL_TEXTURE0 + TEXTURE_UNIT_COUNT++; public static final int TEXTURE_UNIT_TILE_HEIGHT_MAP = GL_TEXTURE0 + TEXTURE_UNIT_COUNT++; public static final int TEXTURE_UNIT_TILED_LIGHTING_MAP = GL_TEXTURE0 + TEXTURE_UNIT_COUNT++; + public static final int TEXTURE_UNIT_PARTICLE = GL_TEXTURE0 + TEXTURE_UNIT_COUNT++; public static int MAX_IMAGE_UNITS; public static int IMAGE_UNIT_COUNT = 0; @@ -224,14 +232,15 @@ public class HdPlugin extends Plugin { TextureManager.class, TileOverrideManager.class, WaterTypeManager.class, - SceneManager.class + SceneManager.class, + ParticleManager.class ); @Getter private Gson gson; @Inject - private Client client; + public Client client; @Inject private ClientUI clientUI; @@ -287,6 +296,20 @@ public class HdPlugin extends Plugin { @Inject private DeveloperTools developerTools; + @Inject + private ClientToolbar clientToolbar; + + @Getter + private ParticleSidebarPanel particleSidebarPanel; + + @Inject + private ParticleGizmoOverlay particleGizmoOverlay; + + @Inject + private ColorPickerManager colorPickerManager; + + private NavigationButton particleNavButton; + @Inject private FrameTimer frameTimer; @@ -296,6 +319,10 @@ public class HdPlugin extends Plugin { @Inject private SceneManager sceneManager; + @Getter + @Inject + private ParticleManager particleManager; + @Inject private JobSystem jobSystem; @@ -420,6 +447,7 @@ public class HdPlugin extends Plugin { public ShadingMode configShadingMode; public ColorFilter configColorFilter = ColorFilter.NONE; public ColorFilter configColorFilterPrevious; + public boolean configParticleAmbientLight; public boolean useLowMemoryMode; public boolean enableDetailedTimers; @@ -690,11 +718,14 @@ protected void startUp() { tileOverrideManager.startUp(); modelOverrideManager.startUp(); lightManager.startUp(); + particleManager.startUp(); environmentManager.startUp(); fishingSpotReplacer.startUp(); gammaCalibrationOverlay.initialize(); npcDisplacementCache.initialize(); + isActive = true; + updateCachedConfigs(); hasLoggedIn = client.getGameState().getState() > GameState.LOGGING_IN.getState(); redrawPreviousFrame = false; skipScene = null; @@ -706,6 +737,20 @@ protected void startUp() { checkGLErrors(); clientThread.invokeLater(this::displayUpdateMessage); + + SwingUtilities.invokeLater(() -> { + particleSidebarPanel = new ParticleSidebarPanel(this, particleManager, clientThread, client, colorPickerManager, particleGizmoOverlay); + if (particleNavButton == null) { + BufferedImage icon = ImageUtil.loadImageResource(HdPlugin.class, "icon.png"); + particleNavButton = NavigationButton.builder() + .tooltip("117 HD Particles") + .icon(icon) + .panel(particleSidebarPanel) + .build(); + clientToolbar.addNavigation(particleNavButton); + } + }); + } catch (Throwable err) { log.error("Error while starting 117 HD", err); stopPlugin(); @@ -753,10 +798,17 @@ protected void shutDown() { } developerTools.deactivate(); - tileOverrideManager.shutDown(); + particleGizmoOverlay.setActive(false); + SwingUtilities.invokeLater(() -> { + if (particleNavButton != null) { + clientToolbar.removeNavigation(particleNavButton); + particleNavButton = null; + } + }); groundMaterialManager.shutDown(); modelOverrideManager.shutDown(); lightManager.shutDown(); + particleManager.shutDown(); environmentManager.shutDown(); fishingSpotReplacer.shutDown(); areaManager.shutDown(); @@ -818,6 +870,18 @@ public SceneContext getSceneContext() { return renderer == null ? null : renderer.getSceneContext(); } + /** Open the particle sidebar panel to the Particles tab and select the given particle definition. */ + public void openParticleConfig(String particleId) { + SwingUtilities.invokeLater(() -> { + if (particleNavButton != null) { + clientToolbar.openPanel(particleNavButton); + } + if (particleSidebarPanel != null && particleId != null) { + particleSidebarPanel.openToParticleConfig(particleId); + } + }); + } + public void toggleFreezeFrame() { clientThread.invoke(() -> { enableFreezeFrame = !enableFreezeFrame; @@ -869,6 +933,7 @@ public ShaderIncludes getShaderIncludes() { .define("UI_SCALING_MODE", config.uiScalingMode()) .define("COLOR_BLINDNESS", config.colorBlindness()) .define("APPLY_COLOR_FILTER", configColorFilter != ColorFilter.NONE) + .define("GLOBAL_PARTICLE_AMBIENT_LIGHT", config.particleAmbientLight()) .define("MATERIAL_COUNT", MaterialManager.MATERIALS.length) .define("WATER_TYPE_COUNT", waterTypeManager.uboWaterTypes.getCount()) .define("DYNAMIC_LIGHTS", configDynamicLights != DynamicLights.NONE) @@ -1634,6 +1699,7 @@ private void updateCachedConfigs() { configHideVanillaWaterEffects = config.hideVanillaWaterEffects(); configSeasonalTheme = config.seasonalTheme(); configSeasonalHemisphere = config.seasonalHemisphere(); + configParticleAmbientLight = config.particleAmbientLight(); var newColorFilter = config.colorFilter(); if (newColorFilter != configColorFilter) { @@ -1779,6 +1845,7 @@ public void processPendingConfigChanges() { case KEY_WIREFRAME: case KEY_SHADOW_FILTERING: case KEY_WINDOWS_HDR_CORRECTION: + case KEY_PARTICLE_AMBIENT_LIGHT: recompilePrograms = true; break; case KEY_ANTI_ALIASING_MODE: diff --git a/src/main/java/rs117/hd/HdPluginConfig.java b/src/main/java/rs117/hd/HdPluginConfig.java index 2a2a000d2b..f7bceff37c 100644 --- a/src/main/java/rs117/hd/HdPluginConfig.java +++ b/src/main/java/rs117/hd/HdPluginConfig.java @@ -785,12 +785,34 @@ default boolean characterDisplacement() { ) default boolean hideVanillaWaterEffects() { return true; } + /*====== Particles settings ======*/ + + @ConfigSection( + name = "Particles", + description = "Particle effect settings", + position = 4, + closedByDefault = true + ) + String particlesSettings = "particlesSettings"; + + String KEY_PARTICLE_AMBIENT_LIGHT = "particleAmbientLight"; + @ConfigItem( + keyName = KEY_PARTICLE_AMBIENT_LIGHT, + name = "Scene ambient light", + description = "Apply scene ambient lighting to particles so they match the area's light and color.", + section = particlesSettings, + position = 0 + ) + default boolean particleAmbientLight() { + return true; + } + /*====== Miscellaneous settings ======*/ @ConfigSection( name = "Miscellaneous", description = "Miscellaneous settings", - position = 4, + position = 5, closedByDefault = true ) String miscellaneousSettings = "miscellaneousSettings"; @@ -944,7 +966,7 @@ default boolean windowsHdrCorrection() { @ConfigSection( name = "Legacy", description = "Legacy options. If you dislike a change, you might find an option to change it back here.", - position = 5, + position = 6, closedByDefault = true ) String legacySettings = "legacySettings"; @@ -1097,7 +1119,7 @@ default boolean legacyTzHaarReskin() { @ConfigSection( name = "Experimental", description = "Experimental features - if you're experiencing issues you should consider disabling these.", - position = 6, + position = 7, closedByDefault = true ) String experimentalSettings = "experimentalSettings"; diff --git a/src/main/java/rs117/hd/opengl/shader/ParticleShaderProgram.java b/src/main/java/rs117/hd/opengl/shader/ParticleShaderProgram.java new file mode 100644 index 0000000000..bbf1314c26 --- /dev/null +++ b/src/main/java/rs117/hd/opengl/shader/ParticleShaderProgram.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2025, Hooder + * All rights reserved. + */ +package rs117.hd.opengl.shader; + +import java.io.IOException; +import org.lwjgl.opengl.GL33C; +import rs117.hd.opengl.shader.ShaderException; +import rs117.hd.opengl.shader.ShaderIncludes; + +public class ParticleShaderProgram extends ShaderProgram { + private ShaderProgram.UniformTexture uParticleTexture; + + public ParticleShaderProgram() { + super(t -> t + .add(GL33C.GL_VERTEX_SHADER, "particle_vert.glsl") + .add(GL33C.GL_FRAGMENT_SHADER, "particle_frag.glsl")); + } + + @Override + protected void initialize() { + uParticleTexture = addUniformTexture("uParticleTexture"); + } + + public void setParticleTextureUnit(int textureUnit) { + if (uParticleTexture != null && isValid()) + uParticleTexture.set(textureUnit); + } +} diff --git a/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java b/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java index d596fc9efb..ba7eeb300c 100644 --- a/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java +++ b/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java @@ -17,6 +17,8 @@ import net.runelite.client.ui.overlay.components.LineComponent; import net.runelite.client.ui.overlay.components.TitleComponent; import rs117.hd.HdPlugin; +import rs117.hd.renderer.zone.pass.impl.ParticlePass; +import rs117.hd.scene.particles.ParticleManager; import rs117.hd.renderer.zone.SceneManager; import rs117.hd.renderer.zone.WorldViewContext; import rs117.hd.renderer.zone.ZoneRenderer; @@ -50,6 +52,12 @@ public class FrameTimerOverlay extends OverlayPanel implements FrameTimer.Listen @Inject private SceneManager sceneManager; + @Inject + private ParticleManager particleManager; + + @Inject + private ParticlePass particlePass; + private final ArrayDeque frames = new ArrayDeque<>(); private final long[] timings = new long[Timer.TIMERS.length]; private float cpuLoad; @@ -168,10 +176,12 @@ public Dimension render(Graphics2D g) { if (plugin.getSceneContext() != null) { var sceneContext = plugin.getSceneContext(); - children.add(LineComponent.builder() - .left("Lights:") - .right(format("%d/%d", sceneContext.numVisibleLights, sceneContext.lights.size())) - .build()); + + children.add(LineComponent.builder() + .left("Lights:") + .right(String.format("%d/%d", sceneContext.numVisibleLights, sceneContext.lights.size())) + .build()); + } if (plugin.renderer instanceof ZoneRenderer) { @@ -206,6 +216,33 @@ public Dimension render(Graphics2D g) { .build()); } + children.add(LineComponent.builder() + .leftFont(boldFont) + .left("Particle stats:") + .build()); + int totalEmitters = particleManager.getSceneEmitters().size(); + children.add(LineComponent.builder() + .left("Emitters Updating:") + .right(String.format("%d/%d", particleManager.getLastEmittersUpdating(), totalEmitters)) + .build()); + children.add(LineComponent.builder() + .left("Emitters (culled):") + .right(String.valueOf(particleManager.getLastEmittersCulled())) + .build()); + if (plugin.renderer instanceof ZoneRenderer) { + int drawn = particlePass.getLastParticleDrawn(); + int totalOnPlane = particlePass.getLastParticleTotalOnPlane(); + int culled = particlePass.getLastParticleCulledDistance() + particlePass.getLastParticleCulledFrustum(); + children.add(LineComponent.builder() + .left("Particles (drawn):") + .right(String.format("%d/%d", drawn, totalOnPlane)) + .build()); + children.add(LineComponent.builder() + .left("Particles (culled):") + .right(String.valueOf(culled)) + .build()); + } + children.add(LineComponent.builder() .leftFont(boldFont) .left("Streaming Stats:") diff --git a/src/main/java/rs117/hd/overlays/TileInfoOverlay.java b/src/main/java/rs117/hd/overlays/TileInfoOverlay.java index 322b81d873..09950d2545 100644 --- a/src/main/java/rs117/hd/overlays/TileInfoOverlay.java +++ b/src/main/java/rs117/hd/overlays/TileInfoOverlay.java @@ -1002,6 +1002,37 @@ public Polygon getCanvasTilePoly(@Nonnull Client client, SceneContext ctx, Tile return getCanvasTilePoly(client, ctx, l.getX(), l.getY(), tile.getPlane()); } + /** + * Draws a filled rectangle for a world AABB. Uses same projection as tile outlines (localToCanvas). + * Call for weather area debug overlay. + */ + public void drawFilledWorldAabb(Graphics2D g, SceneContext ctx, AABB worldAabb, int plane, Color fillColor) { + if (ctx == null || ctx.sceneBase == null || !ctx.intersects(worldAabb)) + return; + int x1 = (worldAabb.minX - ctx.sceneBase[0]) * LOCAL_TILE_SIZE; + int y1 = (worldAabb.minY - ctx.sceneBase[1]) * LOCAL_TILE_SIZE; + int x2 = (worldAabb.maxX + 1 - ctx.sceneBase[0]) * LOCAL_TILE_SIZE; + int y2 = (worldAabb.maxY + 1 - ctx.sceneBase[1]) * LOCAL_TILE_SIZE; + int[][] corners = {{x1, y1}, {x2, y1}, {x2, y2}, {x1, y2}}; + int[] polyX = new int[4]; + int[] polyY = new int[4]; + int valid = 0; + for (int i = 0; i < 4; i++) { + int z = getHeight(ctx, corners[i][0], corners[i][1], plane); + float[] p = localToCanvas(client, corners[i][0], corners[i][1], z); + if (p != null) { + polyX[valid] = round(p[0]); + polyY[valid] = round(p[1]); + valid++; + } + } + if (valid >= 3) { + setAntiAliasing(g, true); + g.setColor(fillColor); + g.fillPolygon(polyX, polyY, valid); + } + } + public Polygon getCanvasTilePoly(@Nonnull Client client, SceneContext ctx, int... sceneXYplane) { final int wx = sceneXYplane[0] * LOCAL_TILE_SIZE; final int sy = sceneXYplane[1] * LOCAL_TILE_SIZE; diff --git a/src/main/java/rs117/hd/overlays/Timer.java b/src/main/java/rs117/hd/overlays/Timer.java index 5f998a8651..760e82da98 100644 --- a/src/main/java/rs117/hd/overlays/Timer.java +++ b/src/main/java/rs117/hd/overlays/Timer.java @@ -36,6 +36,7 @@ public enum Timer { // Logic VISIBILITY_CHECK, UPDATE_SCENE, + UPDATE_PARTICLES("Update Particles"), UPDATE_ENVIRONMENT, UPDATE_LIGHTS, UPDATE_AREA_HIDING, @@ -70,6 +71,7 @@ public enum Timer { CLEAR_SCENE(GPU_TIMER), RENDER_SHADOWS(GPU_TIMER), RENDER_SCENE(GPU_TIMER), + RENDER_PARTICLES(GPU_TIMER, "Render Particles"), RENDER_UI(GPU_TIMER, "Render UI"), ; diff --git a/src/main/java/rs117/hd/renderer/zone/SceneManager.java b/src/main/java/rs117/hd/renderer/zone/SceneManager.java index 6370b07e71..9d169c74da 100644 --- a/src/main/java/rs117/hd/renderer/zone/SceneManager.java +++ b/src/main/java/rs117/hd/renderer/zone/SceneManager.java @@ -30,6 +30,7 @@ import rs117.hd.scene.ProceduralGenerator; import rs117.hd.scene.areas.AABB; import rs117.hd.scene.areas.Area; +import rs117.hd.scene.particles.ParticleManager; import rs117.hd.utils.NpcDisplacementCache; import rs117.hd.utils.RenderState; import rs117.hd.utils.jobs.GenericJob; @@ -80,6 +81,9 @@ public class SceneManager { @Inject private FishingSpotReplacer fishingSpotReplacer; + @Inject + private ParticleManager particleManager; + @Inject private FrameTimer frameTimer; @@ -333,6 +337,12 @@ private static boolean isEdgeTile(Zone[][] zones, int zx, int zz) { task -> lightManager.loadSceneLights(nextSceneContext) ); + @Getter + private final GenericJob loadSceneParticlesTask = GenericJob.build( + "ParticleManager::loadSceneParticles", + task -> particleManager.loadSceneParticles(nextSceneContext) + ); + private final GenericJob calculateRoofChangesTask = GenericJob.build( "calculateRoofChanges", (task) -> { @@ -422,10 +432,12 @@ public synchronized void loadScene(WorldView worldView, Scene scene) { environmentManager.loadSceneEnvironments(nextSceneContext); loadSceneLightsTask.cancel(); + loadSceneParticlesTask.cancel(); calculateRoofChangesTask.cancel(); generateSceneDataTask.queue(); loadSceneLightsTask.queue(); + loadSceneParticlesTask.queue(); if (nextSceneContext.enableAreaHiding) { assert nextSceneContext.sceneBase != null; @@ -610,6 +622,7 @@ public void swapScene(Scene scene) { // Handle object spawns that must be processed on the client thread loadSceneLightsTask.waitForCompletion(); + loadSceneParticlesTask.waitForCompletion(); lightManager.swapSceneLights(nextSceneContext, root.sceneContext); for (var tileObject : nextSceneContext.lightSpawnsToHandleOnClientThread) diff --git a/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java b/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java index 2161b66977..46d86d0b25 100644 --- a/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java +++ b/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java @@ -25,7 +25,9 @@ package rs117.hd.renderer.zone; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Set; import javax.inject.Inject; import javax.inject.Singleton; @@ -50,6 +52,10 @@ import rs117.hd.overlays.FrameTimer; import rs117.hd.overlays.Timer; import rs117.hd.renderer.Renderer; +import rs117.hd.renderer.zone.pass.impl.ParticlePass; +import rs117.hd.renderer.zone.pass.ScenePass; +import rs117.hd.renderer.zone.pass.ScenePassContext; +import rs117.hd.renderer.zone.pass.ScenePassListener; import rs117.hd.scene.EnvironmentManager; import rs117.hd.scene.LightManager; import rs117.hd.scene.ProceduralGenerator; @@ -136,7 +142,19 @@ public class ZoneRenderer implements Renderer { @Inject private UBOWorldViews uboWorldViews; + @Inject + private ParticlePass particlePass; + + private ScenePass[] scenePasses; + private final List scenePassListeners = new ArrayList<>(); + + /** Register a listener for before/after the scene pass loop (e.g. from another component's startUp). */ + public void addScenePassListener(ScenePassListener listener) { + scenePassListeners.add(listener); + } + public final Camera sceneCamera = new Camera().setReverseZ(true); + public final Camera directionalCamera = new Camera().setOrthographic(true); public final ShadowCasterVolume directionalShadowCasterVolume = new ShadowCasterVolume(directionalCamera); @@ -172,6 +190,11 @@ public int gpuFlags() { @Override public void initialize() { initializeBuffers(); + scenePasses = new ScenePass[] { particlePass }; + java.util.Arrays.sort(scenePasses, ScenePass.ORDER_COMPARATOR); + for (ScenePass pass : scenePasses) { + pass.initialize(); + } SceneUploader.POOL = new ConcurrentPool<>(plugin.getInjector(), SceneUploader.class); FacePrioritySorter.POOL = new ConcurrentPool<>(plugin.getInjector(), FacePrioritySorter.class); @@ -192,6 +215,13 @@ public void initialize() { @Override public void destroy() { destroyBuffers(); + if (scenePasses != null) { + for (ScenePass pass : scenePasses) { + pass.destroy(); + } + scenePasses = null; + } + scenePassListeners.clear(); jobSystem.shutDown(); modelStreamingManager.destroy(); @@ -221,6 +251,11 @@ public void initializeShaders(ShaderIncludes includes) throws ShaderException, I sceneProgram.compile(includes); fastShadowProgram.compile(includes); detailedShadowProgram.compile(includes); + if (scenePasses != null) { + for (ScenePass pass : scenePasses) { + pass.initializeShaders(includes); + } + } } @Override @@ -228,6 +263,11 @@ public void destroyShaders() { sceneProgram.destroy(); fastShadowProgram.destroy(); detailedShadowProgram.destroy(); + if (scenePasses != null) { + for (ScenePass pass : scenePasses) { + pass.destroyShaders(); + } + } } private void initializeBuffers() { @@ -713,10 +753,16 @@ private void directionalShadowPass() { frameTimer.end(Timer.RENDER_SHADOWS); } - private void scenePass() { + private void scenePass(ScenePassContext passCtx) { sceneProgram.use(); frameTimer.begin(Timer.DRAW_SCENE); + for (ScenePass pass : scenePasses) { + if (pass.shouldDraw(passCtx)) { + pass.beforeDraw(passCtx); + } + } + renderState.framebuffer.set(GL_DRAW_FRAMEBUFFER, plugin.fboScene); if (plugin.msaaSamples > 1) { renderState.enable.set(GL_MULTISAMPLE); @@ -753,9 +799,28 @@ private void scenePass() { // Render the scene sceneCmd.execute(); + for (ScenePassListener listener : scenePassListeners) { + listener.beforeScenePasses(passCtx); + } + + for (ScenePass pass : scenePasses) { + if (pass.shouldDraw(passCtx)) { + pass.draw(passCtx); + } + } + // TODO: Filler tiles frameTimer.end(Timer.RENDER_SCENE); + for (ScenePass pass : scenePasses) { + if (pass.shouldDraw(passCtx)) { + pass.afterDraw(passCtx); + } + } + for (ScenePassListener listener : scenePassListeners) { + listener.afterScenePasses(passCtx); + } + // Done rendering the scene renderState.disable.set(GL_BLEND); renderState.disable.set(GL_CULL_FACE); @@ -1019,11 +1084,20 @@ public void draw(int overlayColor) { return; } + + WorldViewContext root = sceneManager.getRoot(); + + ScenePassContext passCtx = new ScenePassContext( + renderState, + frameTimer, + root != null ? root.sceneContext : null + ); + frameTimer.begin(Timer.DRAW_SUBMIT); if (shouldRenderScene) { tiledLightingPass(); directionalShadowPass(); - scenePass(); + scenePass(passCtx); } if (sceneFboValid && plugin.sceneResolution != null && plugin.sceneViewport != null) { diff --git a/src/main/java/rs117/hd/renderer/zone/pass/ScenePass.java b/src/main/java/rs117/hd/renderer/zone/pass/ScenePass.java new file mode 100644 index 0000000000..8084493641 --- /dev/null +++ b/src/main/java/rs117/hd/renderer/zone/pass/ScenePass.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025, Mark7625 (https://github.com/Mark7625/) + * All rights reserved. + */ +package rs117.hd.renderer.zone.pass; + +import java.io.IOException; +import java.util.Comparator; +import rs117.hd.opengl.shader.ShaderException; +import rs117.hd.opengl.shader.ShaderIncludes; + +/** + * A single pass run during the scene render (e.g. particles, zoom overlay). + * Lifecycle: initialize → initializeShaders → [beforeDraw → draw → afterDraw] each frame → destroyShaders → destroy. + *

+ * Use {@link #order()} to control draw order (lower runs first). Override {@link #shouldDraw(ScenePassContext)} + * to skip the pass conditionally. Override {@link #beforeDraw(ScenePassContext)} / {@link #afterDraw(ScenePassContext)} + * for setup/teardown around draw. + */ +public interface ScenePass { + + default int order() { + return 0; + } + + default String passName() { + return getClass().getSimpleName(); + } + + default boolean shouldDraw(ScenePassContext ctx) { + return true; + } + + default void beforeDraw(ScenePassContext ctx) { + } + + default void afterDraw(ScenePassContext ctx) { + } + + void initialize(); + + void destroy(); + + void initializeShaders(ShaderIncludes includes) throws ShaderException, IOException; + + void destroyShaders(); + + void draw(ScenePassContext ctx); + + Comparator ORDER_COMPARATOR = Comparator.comparingInt(ScenePass::order); +} diff --git a/src/main/java/rs117/hd/renderer/zone/pass/ScenePassContext.java b/src/main/java/rs117/hd/renderer/zone/pass/ScenePassContext.java new file mode 100644 index 0000000000..80c726745c --- /dev/null +++ b/src/main/java/rs117/hd/renderer/zone/pass/ScenePassContext.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2025, Mark7625 (https://github.com/Mark7625/) + * All rights reserved. + */ +package rs117.hd.renderer.zone.pass; + +import javax.annotation.Nullable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import rs117.hd.overlays.FrameTimer; +import rs117.hd.overlays.Timer; +import rs117.hd.scene.SceneContext; +import rs117.hd.utils.RenderState; + +/** + * Immutable context provided to scene passes each frame. Contains shared render state + */ +@Getter +@RequiredArgsConstructor +public class ScenePassContext { + + private final RenderState renderState; + private final FrameTimer frameTimer; + + /** Current scene context for the root world view, or null if not available. */ + @Nullable + private final SceneContext sceneContext; + + public void beginTimer(Timer timer) { + frameTimer.begin(timer); + } + + public void endTimer(Timer timer) { + frameTimer.end(timer); + } +} diff --git a/src/main/java/rs117/hd/renderer/zone/pass/ScenePassListener.java b/src/main/java/rs117/hd/renderer/zone/pass/ScenePassListener.java new file mode 100644 index 0000000000..ac65b0174c --- /dev/null +++ b/src/main/java/rs117/hd/renderer/zone/pass/ScenePassListener.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025, Mark7625 (https://github.com/Mark7625/) + * All rights reserved. + */ +package rs117.hd.renderer.zone.pass; + +/** + * Optional hook for code that needs to run before or after the entire scene pass loop. + * Implement and register (e.g. via injector) to receive callbacks each frame. + */ +public interface ScenePassListener { + + default void beforeScenePasses(ScenePassContext ctx) { + } + + default void afterScenePasses(ScenePassContext ctx) { + } +} diff --git a/src/main/java/rs117/hd/renderer/zone/pass/impl/ParticlePass.java b/src/main/java/rs117/hd/renderer/zone/pass/impl/ParticlePass.java new file mode 100644 index 0000000000..8215964058 --- /dev/null +++ b/src/main/java/rs117/hd/renderer/zone/pass/impl/ParticlePass.java @@ -0,0 +1,365 @@ +/* + * Copyright (c) 2025, Mark7625 (https://github.com/Mark7625/) + * All rights reserved. + */ +package rs117.hd.renderer.zone.pass.impl; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.util.Arrays; +import javax.inject.Inject; +import javax.inject.Singleton; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.*; +import org.lwjgl.BufferUtils; +import rs117.hd.HdPlugin; +import rs117.hd.opengl.GLFence; +import rs117.hd.opengl.shader.ParticleShaderProgram; +import rs117.hd.opengl.shader.ShaderException; +import rs117.hd.opengl.shader.ShaderIncludes; +import rs117.hd.overlays.Timer; +import rs117.hd.renderer.zone.pass.ScenePass; +import rs117.hd.renderer.zone.pass.ScenePassContext; +import rs117.hd.scene.particles.ParticleManager; +import rs117.hd.scene.particles.core.buffer.ParticleBuffer; +import rs117.hd.scene.particles.definition.ParticleDefinition; +import rs117.hd.scene.particles.core.ParticleTextureLoader; +import rs117.hd.scene.particles.emitter.ParticleEmitter; +import rs117.hd.utils.buffer.GLBuffer; +import static net.runelite.api.Perspective.LOCAL_TILE_SIZE; +import static org.lwjgl.opengl.GL15.glUnmapBuffer; +import static org.lwjgl.opengl.GL30.GL_MAP_INVALIDATE_RANGE_BIT; +import static org.lwjgl.opengl.GL30.GL_MAP_WRITE_BIT; +import static org.lwjgl.opengl.GL30.GL_TEXTURE_2D_ARRAY; +import static org.lwjgl.opengl.GL30.glMapBufferRange; +import static org.lwjgl.opengl.GL32C.GL_SYNC_GPU_COMMANDS_COMPLETE; +import static org.lwjgl.opengl.GL33C.*; +import static rs117.hd.HdPlugin.TEXTURE_UNIT_PARTICLE; + +@Slf4j +@Singleton +public class ParticlePass implements ScenePass { + + @Inject + private Client client; + + @Inject + private HdPlugin plugin; + + private static final int MAX_PARTICLES = 4096; + private static final int MAX_DRAWN = 2048; + + private final ParticleManager.ParticleRenderContext renderContext = new ParticleManager.ParticleRenderContext(); + private final int[] visibleIndices = new int[MAX_DRAWN]; + private static final int INSTANCE_BUFFER_COUNT = 3; + private static final int QUAD_VERTS = 6; + private static final int FLOATS_PER_INSTANCE = 14; + private static final int INSTANCE_STRIDE_BYTES = 64; + private static final int INSTANCE_PADDING_BYTES = INSTANCE_STRIDE_BYTES - FLOATS_PER_INSTANCE * 4; + private static final float[] PARTICLE_QUAD_CORNERS = { + -1, -1, 1, -1, 1, 1, + -1, -1, 1, 1, -1, 1 + }; + + @Inject + private ParticleManager particleManager; + + @Inject + private ParticleTextureLoader particleTextureLoader; + + @Inject + private ParticleShaderProgram particleProgram; + + private int vaoParticles; + private int vboParticleQuad; + private int[] vboParticleInstances; + private GLBuffer[] particleInstanceBuffers; + private final GLFence[] instanceFences = new GLFence[INSTANCE_BUFFER_COUNT]; + private int instanceBufferSlot; + private FloatBuffer particleStagingBuffer; + private final float[] particleDistSq = new float[MAX_PARTICLES]; + private final Integer[] particleSortOrder = new Integer[MAX_PARTICLES]; + + private final String[] textureForVisibleIndex = new String[MAX_PARTICLES]; + private ByteBuffer batchUploadBuffer; + + @Getter + private int lastParticleTotalOnPlane; + @Getter + private int lastParticleCulledDistance; + @Getter + private int lastParticleCulledFrustum; + @Getter + private int lastParticleDrawn; + private int lastUploadedInstanceCount; + + @Override + public String passName() { + return "Particles"; + } + + public void initialize() { + vaoParticles = glGenVertexArrays(); + vboParticleQuad = glGenBuffers(); + long instanceVboBytes = (long) MAX_DRAWN * INSTANCE_STRIDE_BYTES; + vboParticleInstances = new int[INSTANCE_BUFFER_COUNT]; + if (GLBuffer.supportsStorageBuffers()) { + particleInstanceBuffers = new GLBuffer[INSTANCE_BUFFER_COUNT]; + for (int i = 0; i < INSTANCE_BUFFER_COUNT; i++) { + particleInstanceBuffers[i] = new GLBuffer("particle instances " + i, GL_ARRAY_BUFFER, GL_STREAM_DRAW, GLBuffer.STORAGE_PERSISTENT | GLBuffer.STORAGE_WRITE); + particleInstanceBuffers[i].initialize(instanceVboBytes); + vboParticleInstances[i] = particleInstanceBuffers[i].id; + instanceFences[i] = new GLFence(); + } + } else { + particleInstanceBuffers = null; + for (int i = 0; i < INSTANCE_BUFFER_COUNT; i++) { + vboParticleInstances[i] = glGenBuffers(); + glBindBuffer(GL_ARRAY_BUFFER, vboParticleInstances[i]); + glBufferData(GL_ARRAY_BUFFER, instanceVboBytes, GL_STREAM_DRAW); + instanceFences[i] = new GLFence(); + } + glBindBuffer(GL_ARRAY_BUFFER, 0); + } + FloatBuffer quadBuffer = BufferUtils.createFloatBuffer(PARTICLE_QUAD_CORNERS.length).put(PARTICLE_QUAD_CORNERS).flip(); + glBindBuffer(GL_ARRAY_BUFFER, vboParticleQuad); + glBufferData(GL_ARRAY_BUFFER, quadBuffer, GL_STATIC_DRAW); + glBindVertexArray(vaoParticles); + glBindBuffer(GL_ARRAY_BUFFER, vboParticleQuad); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, false, 0, 0); + glVertexAttribDivisor(0, 0); + bindInstanceBuffer(0); + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 3, GL_FLOAT, false, INSTANCE_STRIDE_BYTES, 0); + glVertexAttribDivisor(1, 1); + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 4, GL_FLOAT, false, INSTANCE_STRIDE_BYTES, 12); + glVertexAttribDivisor(2, 1); + glEnableVertexAttribArray(3); + glVertexAttribPointer(3, 1, GL_FLOAT, false, INSTANCE_STRIDE_BYTES, 28); + glVertexAttribDivisor(3, 1); + glEnableVertexAttribArray(4); + glVertexAttribPointer(4, 1, GL_FLOAT, false, INSTANCE_STRIDE_BYTES, 32); + glVertexAttribDivisor(4, 1); + glEnableVertexAttribArray(5); + glVertexAttribPointer(5, 1, GL_FLOAT, false, INSTANCE_STRIDE_BYTES, 36); + glVertexAttribDivisor(5, 1); + glEnableVertexAttribArray(6); + glVertexAttribPointer(6, 1, GL_FLOAT, false, INSTANCE_STRIDE_BYTES, 40); + glVertexAttribDivisor(6, 1); + glEnableVertexAttribArray(7); + glVertexAttribPointer(7, 1, GL_FLOAT, false, INSTANCE_STRIDE_BYTES, 44); + glVertexAttribDivisor(7, 1); + glEnableVertexAttribArray(8); + glVertexAttribPointer(8, 1, GL_FLOAT, false, INSTANCE_STRIDE_BYTES, 48); + glVertexAttribDivisor(8, 1); + glEnableVertexAttribArray(9); + glVertexAttribPointer(9, 1, GL_FLOAT, false, INSTANCE_STRIDE_BYTES, 52); + glVertexAttribDivisor(9, 1); + glBindVertexArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + particleStagingBuffer = BufferUtils.createFloatBuffer(MAX_PARTICLES * FLOATS_PER_INSTANCE); + batchUploadBuffer = BufferUtils.createByteBuffer(MAX_DRAWN * INSTANCE_STRIDE_BYTES); + } + + private void bindInstanceBuffer(int slot) { + glBindBuffer(GL_ARRAY_BUFFER, vboParticleInstances[slot]); + } + + public void destroy() { + if (vaoParticles != 0) { + glDeleteVertexArrays(vaoParticles); + vaoParticles = 0; + } + if (vboParticleQuad != 0) { + glDeleteBuffers(vboParticleQuad); + vboParticleQuad = 0; + } + if (vboParticleInstances != null) { + for (int i = 0; i < INSTANCE_BUFFER_COUNT; i++) { + if (particleInstanceBuffers != null) { + particleInstanceBuffers[i].destroy(); + } else { + glDeleteBuffers(vboParticleInstances[i]); + } + vboParticleInstances[i] = 0; + } + vboParticleInstances = null; + particleInstanceBuffers = null; + } + particleStagingBuffer = null; + batchUploadBuffer = null; + particleTextureLoader.dispose(); + } + + public void initializeShaders(ShaderIncludes includes) throws ShaderException, IOException { + particleProgram.compile(includes); + } + + public void destroyShaders() { + particleProgram.destroy(); + } + + @Override + public void beforeDraw(ScenePassContext ctx) { + if (ctx.getSceneContext() != null) { + ctx.beginTimer(Timer.UPDATE_PARTICLES); + particleManager.update(ctx.getSceneContext(), plugin.deltaTime); + ctx.endTimer(Timer.UPDATE_PARTICLES); + } + } + + @Override + public void draw(ScenePassContext ctx) { + int currentPlane = client.getTopLevelWorldView().getPlane(); + int instanceCount = prepareBatches(currentPlane); + if (instanceCount == 0) + return; + var renderState = ctx.getRenderState(); + renderState.program.set(particleProgram); + renderState.enable.set(GL_BLEND); + renderState.blendFunc.reset(); + renderState.blendFunc.set(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ZERO, GL_ONE); + renderState.disable.set(GL_CULL_FACE); + renderState.depthMask.set(false); + renderState.apply(); + glActiveTexture(TEXTURE_UNIT_PARTICLE); + glBindTexture(GL_TEXTURE_2D_ARRAY, particleTextureLoader.getTextureArrayId()); + particleProgram.setParticleTextureUnit(TEXTURE_UNIT_PARTICLE); + glBindVertexArray(vaoParticles); + ctx.beginTimer(Timer.RENDER_PARTICLES); + int slot = instanceBufferSlot; + if (particleInstanceBuffers != null) { + instanceFences[slot].sync(); + } + uploadInstanceDataToVbo(instanceCount, slot); + bindInstanceBuffer(slot); + glVertexAttribPointer(1, 3, GL_FLOAT, false, INSTANCE_STRIDE_BYTES, 0); + glVertexAttribPointer(2, 4, GL_FLOAT, false, INSTANCE_STRIDE_BYTES, 12); + glVertexAttribPointer(3, 1, GL_FLOAT, false, INSTANCE_STRIDE_BYTES, 28); + glVertexAttribPointer(4, 1, GL_FLOAT, false, INSTANCE_STRIDE_BYTES, 32); + glVertexAttribPointer(5, 1, GL_FLOAT, false, INSTANCE_STRIDE_BYTES, 36); + glVertexAttribPointer(6, 1, GL_FLOAT, false, INSTANCE_STRIDE_BYTES, 40); + glVertexAttribPointer(7, 1, GL_FLOAT, false, INSTANCE_STRIDE_BYTES, 44); + glVertexAttribPointer(8, 1, GL_FLOAT, false, INSTANCE_STRIDE_BYTES, 48); + glVertexAttribPointer(9, 1, GL_FLOAT, false, INSTANCE_STRIDE_BYTES, 52); + glDrawArraysInstanced(GL_TRIANGLES, 0, QUAD_VERTS, instanceCount); + if (particleInstanceBuffers != null) { + instanceFences[slot].handle = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + } + instanceBufferSlot = (instanceBufferSlot + 1) % INSTANCE_BUFFER_COUNT; + ctx.endTimer(Timer.RENDER_PARTICLES); + glBindVertexArray(0); + } + + @Override + public void afterDraw(ScenePassContext ctx) { + ctx.getRenderState().depthMask.set(true); + } + + private int prepareBatches(int currentPlane) { + ParticleBuffer buf = particleManager.getParticleBuffer(); + renderContext.cameraX = plugin.cameraPosition[0]; + renderContext.cameraY = plugin.cameraPosition[1]; + renderContext.cameraZ = plugin.cameraPosition[2]; + renderContext.maxDistSq = (float) (plugin.getDrawDistance() * LOCAL_TILE_SIZE); + renderContext.maxDistSq *= renderContext.maxDistSq; + renderContext.frustum = plugin.cameraFrustum; + + int instanceCount = particleManager.filterVisibleParticles(buf, renderContext, currentPlane, 0L, visibleIndices, particleDistSq); + lastParticleTotalOnPlane = renderContext.totalOnPlane; + lastParticleCulledDistance = renderContext.culledDistance; + lastParticleCulledFrustum = renderContext.culledFrustum; + lastParticleDrawn = instanceCount; + + if (instanceCount == 0) + return 0; + + for (int i = 0; i < instanceCount; i++) + particleSortOrder[i] = i; + Arrays.sort(particleSortOrder, 0, instanceCount, (a, b) -> Float.compare(particleDistSq[b], particleDistSq[a])); + + // Build render list from filtered particles, back-to-front order + float[] particleColor = new float[4]; + particleStagingBuffer.clear(); + for (int k = 0; k < instanceCount; k++) { + int bufIndex = visibleIndices[particleSortOrder[k]]; + textureForVisibleIndex[k] = getTextureNameForParticle(buf, bufIndex); + buf.getCurrentColor(bufIndex, particleColor); + float cx = buf.posX[bufIndex] + plugin.cameraShift[0]; + float cy = buf.posY[bufIndex]; + float cz = buf.posZ[bufIndex] + plugin.cameraShift[1]; + particleStagingBuffer.put(cx).put(cy).put(cz); + particleStagingBuffer.put(particleColor[0]).put(particleColor[1]).put(particleColor[2]).put(particleColor[3]); + particleStagingBuffer.put(buf.size[bufIndex]); + String tex = textureForVisibleIndex[k]; + particleStagingBuffer.put((float) particleTextureLoader.getTextureLayer(tex != null ? tex : "")); + float flipbookCols = 0f; + float flipbookRows = 0f; + float flipbookFrameVal = 0f; + ParticleDefinition def = getDefinitionForParticle(buf, bufIndex); + if (def != null && def.texture.flipbook.flipbookColumns > 0 && def.texture.flipbook.flipbookRows > 0) { + flipbookCols = def.texture.flipbook.flipbookColumns; + flipbookRows = def.texture.flipbook.flipbookRows; + String mode = def.texture.flipbook.flipbookMode; + if (mode != null && "order".equalsIgnoreCase(mode)) { + float maxL = buf.maxLife[bufIndex]; + flipbookFrameVal = maxL > 0 ? (1f - buf.life[bufIndex] / maxL) : 0f; + } else if (mode != null && "random".equalsIgnoreCase(mode) && buf.flipbookFrame[bufIndex] >= 0f) { + flipbookFrameVal = 1f + buf.flipbookFrame[bufIndex]; + } + } + particleStagingBuffer.put(flipbookCols).put(flipbookRows).put(flipbookFrameVal); + float useSceneAmbient = (def != null && def.colours.useSceneAmbientLight) ? 1f : 0f; + particleStagingBuffer.put(useSceneAmbient); + particleStagingBuffer.put(buf.yaw[bufIndex]); + } + + // Fill upload buffer, 64 bytes per instance (12 floats + 16 padding) + batchUploadBuffer.clear(); + for (int k = 0; k < instanceCount; k++) { + int src = k * FLOATS_PER_INSTANCE; + for (int f = 0; f < FLOATS_PER_INSTANCE; f++) + batchUploadBuffer.putFloat(particleStagingBuffer.get(src + f)); + for (int p = 0; p < INSTANCE_PADDING_BYTES; p++) + batchUploadBuffer.put((byte) 0); + } + lastUploadedInstanceCount = instanceCount; + return instanceCount; + } + + private static String getTextureNameForParticle(ParticleBuffer buf, int bufIndex) { + ParticleEmitter emitter = buf.emitter[bufIndex]; + if (emitter == null) return null; + var def = emitter.getDefinition(); + String file = def != null ? def.texture.file : null; + if (file == null || file.isEmpty()) return null; + return file; + } + + private static ParticleDefinition getDefinitionForParticle(ParticleBuffer buf, int bufIndex) { + ParticleEmitter emitter = buf.emitter[bufIndex]; + return emitter != null ? emitter.getDefinition() : null; + } + + private void uploadInstanceDataToVbo(int instanceCount, int slot) { + int bytes = instanceCount * INSTANCE_STRIDE_BYTES; + batchUploadBuffer.flip(); + if (particleInstanceBuffers != null && particleInstanceBuffers[slot].isMapped()) { + particleInstanceBuffers[slot].upload(batchUploadBuffer); + } else { + glBindBuffer(GL_ARRAY_BUFFER, vboParticleInstances[slot]); + ByteBuffer mapped = glMapBufferRange(GL_ARRAY_BUFFER, 0, bytes, GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_RANGE_BIT); + if (mapped != null) { + mapped.put(batchUploadBuffer); + glUnmapBuffer(GL_ARRAY_BUFFER); + } else { + glBufferSubData(GL_ARRAY_BUFFER, 0, batchUploadBuffer); + } + glBindBuffer(GL_ARRAY_BUFFER, 0); + } + } +} diff --git a/src/main/java/rs117/hd/scene/SceneContext.java b/src/main/java/rs117/hd/scene/SceneContext.java index 1852f4c08a..c94b0445c9 100644 --- a/src/main/java/rs117/hd/scene/SceneContext.java +++ b/src/main/java/rs117/hd/scene/SceneContext.java @@ -171,18 +171,60 @@ public Stream worldToLocals(WorldPoint worldPoint) { )); } + /** + * Returns the first local coordinate for the given world point without allocating a Stream. + * Use this in hot paths instead of worldToLocals(wp).findFirst().orElse(null). + * When out is non-null and length >= 3, writes into out and returns it to avoid allocation. + */ + @Nullable + public int[] worldToLocalFirst(WorldPoint worldPoint) { + return worldToLocalFirst(worldPoint, null); + } + + @Nullable + public int[] worldToLocalFirst(WorldPoint worldPoint, int[] out) { + if (sceneBase != null) + return worldToLocal(worldPoint, out); + var coll = WorldPoint.toLocalInstance(scene, worldPoint); + var it = coll.iterator(); + if (!it.hasNext()) return null; + WorldPoint ip = it.next(); + if (ip == null) return null; + int x = (ip.getX() - scene.getBaseX()) * LOCAL_TILE_SIZE; + int y = (ip.getY() - scene.getBaseY()) * LOCAL_TILE_SIZE; + int p = ip.getPlane(); + if (out != null && out.length >= 3) { + out[0] = x; + out[1] = y; + out[2] = p; + return out; + } + return ivec(x, y, p); + } + /** * Gets the local coordinate at the south-western corner of the tile, if the scene is contiguous, otherwise null + * When out is non-null and length >= 3, writes into out and returns it to avoid allocation. */ @Nullable public int[] worldToLocal(WorldPoint worldPoint) { + return worldToLocal(worldPoint, null); + } + + @Nullable + public int[] worldToLocal(WorldPoint worldPoint, int[] out) { if (sceneBase == null) return null; - return ivec( - (worldPoint.getX() - sceneBase[0]) * LOCAL_TILE_SIZE, - (worldPoint.getY() - sceneBase[1]) * LOCAL_TILE_SIZE, - worldPoint.getPlane() - ); + int x = (worldPoint.getX() - sceneBase[0]) * LOCAL_TILE_SIZE; + int y = (worldPoint.getY() - sceneBase[1]) * LOCAL_TILE_SIZE; + int p = worldPoint.getPlane(); + if (out != null && out.length >= 3) { + out[0] = x; + out[1] = y; + out[2] = p; + return out; + } + return ivec(x, y, p); } public boolean intersects(Area area) { diff --git a/src/main/java/rs117/hd/scene/particles/ParticleManager.java b/src/main/java/rs117/hd/scene/particles/ParticleManager.java new file mode 100644 index 0000000000..759d98fd9b --- /dev/null +++ b/src/main/java/rs117/hd/scene/particles/ParticleManager.java @@ -0,0 +1,968 @@ +/* + * Copyright (c) 2025, Mark7625 (https://github.com/Mark7625/) + * All rights reserved. + */ +package rs117.hd.scene.particles; + +import com.google.common.base.Stopwatch; +import rs117.hd.scene.particles.core.buffer.ParticleBuffer; +import rs117.hd.scene.particles.core.MovingParticle; +import rs117.hd.scene.particles.core.Particle; +import rs117.hd.scene.particles.core.ParticleSystem; +import rs117.hd.scene.particles.definition.ParticleDefinition; +import rs117.hd.scene.particles.core.ParticleTextureLoader; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.ThreadLocalRandom; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.inject.Inject; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.*; +import net.runelite.api.coords.*; +import net.runelite.api.events.*; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.eventbus.Subscribe; +import rs117.hd.HdPlugin; +import rs117.hd.scene.SceneContext; +import rs117.hd.scene.lights.Alignment; +import rs117.hd.scene.particles.emitter.EmitterDefinitionManager; +import rs117.hd.scene.particles.emitter.EmitterConfigEntry; +import rs117.hd.scene.particles.emitter.EmitterPlacement; +import rs117.hd.scene.particles.emitter.ParticleEmitter; +import rs117.hd.scene.particles.emitter.WeatherAreaConfig; +import rs117.hd.data.ObjectType; +import rs117.hd.utils.HDUtils; + +import static net.runelite.api.Constants.*; +import static net.runelite.api.Perspective.*; +import static rs117.hd.utils.MathUtils.*; + +@Slf4j +public class ParticleManager { + + private static final int MAX_PARTICLES = 7096; + + public ParticleManager() { + this.particleSystem = new ParticleSystem(MAX_PARTICLES); + } + private static final float UNITS_TO_RAD = (float) (2 * Math.PI / 2048); + + public static final class ParticleRenderContext { + public float cameraX, cameraY, cameraZ; + public float maxDistSq; + public float[][] frustum; + public int totalOnPlane; + public int culledDistance; + public int culledFrustum; + } + + /** + * Filters particles by plane, distance, frustum. Fills visible indices and distance-squared; returns count. + */ + public final int filterVisibleParticles(ParticleBuffer buf, ParticleRenderContext ctx, int plane, long l, + int[] outVisibleIndices, float[] outDistSq) { + ctx.totalOnPlane = 0; + ctx.culledDistance = 0; + ctx.culledFrustum = 0; + int n = 0; + int maxOut = outVisibleIndices != null ? outVisibleIndices.length : 0; + int maxDistSqLen = outDistSq != null ? outDistSq.length : 0; + int frustumLen = ctx.frustum != null ? ctx.frustum.length : 0; + for (int i = 0; i < buf.count; i++) { + if (buf.plane[i] != plane) + continue; + ctx.totalOnPlane++; + float dx = buf.posX[i] - ctx.cameraX; + float dy = buf.posY[i] - ctx.cameraY; + float dz = buf.posZ[i] - ctx.cameraZ; + float dSq = dx * dx + dy * dy + dz * dz; + if (dSq > ctx.maxDistSq) { + ctx.culledDistance++; + continue; + } + if (frustumLen > 0 && !HDUtils.isSphereIntersectingFrustum(buf.posX[i], buf.posY[i], buf.posZ[i], buf.size[i], ctx.frustum, frustumLen)) { + ctx.culledFrustum++; + continue; + } + if (n < maxOut && outVisibleIndices != null) + outVisibleIndices[n] = i; + if (n < maxDistSqLen && outDistSq != null) + outDistSq[n] = dSq; + n++; + if (maxOut > 0 && n >= maxOut) + break; + } + return n; + } + + @Inject + private Client client; + + @Inject + private EventBus eventBus; + + @Inject + private HdPlugin plugin; + + @Inject + private EmitterDefinitionManager emitterDefinitionManager; + + @Inject + private ParticleDefinition particleDefinitions; + + @Inject + private ParticleTextureLoader particleTextureLoader; + + @Inject + private ClientThread clientThread; + + @Getter + private final ParticleSystem particleSystem; + + private final float[] spawnOrigin = new float[3]; + private final float[] spawnPosScratch = new float[3]; + private final int[] spawnOffsetScratch = new int[2]; + private final int[] localScratch = new int[3]; + private final float[] updatePosOut = new float[3]; + private final int[] planeOutScratch = new int[1]; + private ParticleEmitter[] emitterIterationArray = new ParticleEmitter[0]; + private final List performanceTestEmitters = new ArrayList<>(); + + @Getter + private boolean continuousRandomSpawn; + + @Getter + private int lastEmittersUpdating, lastEmittersCulled; + + public void setContinuousRandomSpawn(boolean on) { + this.continuousRandomSpawn = on; + } + + public List getSceneEmitters() { + return particleSystem.getEmitters(); + } + + public Map> getEmittersByTileObject() { + return particleSystem.getEmittersByTileObject(); + } + + public ParticleBuffer getParticleBuffer() { + return particleSystem.getRenderBuffer(); + } + + public Set getEmittersCulledThisFrame() { + return particleSystem.getEmittersCulledThisFrame(); + } + + public Particle obtainParticle() { + return particleSystem.getPool().obtain(); + } + + public void releaseParticle(Particle p) { + particleSystem.getPool().release(p); + } + + public int getMaxParticles() { + return MAX_PARTICLES; + } + + public void addSpawnedParticleToBuffer(Particle p, float ox, float oy, float oz, ParticleEmitter emitter) { + ParticleDefinition def = emitter.getDefinition(); + p.emitter = emitter; + p.emitterOriginX = ox; + p.emitterOriginY = oy; + p.emitterOriginZ = oz; + if (def != null) { + if (def.colourIncrementPerSecond != null) { + p.colourIncrementPerSecond = def.colourIncrementPerSecond; + p.colourTransitionEndLife = p.maxLife - def.colourTransitionSecondsConstant; + } + if (def.scale.targetScale >= 0) { + p.scaleIncrementPerSecond = def.scaleIncrementPerSecondCached; + p.scaleTransitionEndLife = p.maxLife - def.scaleTransitionSecondsConstant; + } + if (def.speed.targetSpeed >= 0) { + p.speedIncrementPerSecond = def.speedIncrementPerSecondCached; + p.speedTransitionEndLife = p.maxLife - def.speedTransitionSecondsConstant; + } + p.distanceFalloffType = def.physics.distanceFalloffType; + p.distanceFalloffStrength = def.physics.distanceFalloffStrength; + p.clipToTerrain = def.physics.clipToTerrain; + p.hasLevelBounds = def.hasLevelBounds; + p.upperBoundLevel = def.physics.upperBoundLevel; + p.lowerBoundLevel = def.physics.lowerBoundLevel; + } + particleSystem.getRenderBuffer().addFrom(p); + releaseParticle(p); + } + + public void addEmitter(ParticleEmitter emitter) { + particleSystem.addEmitter(emitter); + } + + public void removeEmitter(ParticleEmitter emitter) { + particleSystem.removeEmitter(emitter); + } + + public void clear() { + removeAllObjectSpawnedEmitters(); + particleSystem.getEmitters().clear(); + emitterDefinitionManager.getDefinitionEmitters().clear(); + particleSystem.getRenderBuffer().clear(); + } + + public void startUp() { + eventBus.register(this); + loadConfig(); + } + + public void shutDown() { + eventBus.unregister(this); + removeAllObjectSpawnedEmitters(); + } + + private void removeAllObjectSpawnedEmitters() { + for (List list : particleSystem.getEmittersByTileObject().values()) + particleSystem.removeEmitters(list); + particleSystem.getEmittersByTileObject().clear(); + } + + public void loadSceneParticles(@Nullable SceneContext ctx) { + removeAllObjectSpawnedEmitters(); + recreateEmittersFromPlacements(ctx); + if (ctx == null || emitterDefinitionManager.getObjectBindingsByType().isEmpty()) return; + Stopwatch sw = Stopwatch.createStarted(); + for (Tile[][] plane : ctx.scene.getExtendedTiles()) { + for (Tile[] column : plane) { + for (Tile tile : column) { + if (tile == null) continue; + DecorativeObject deco = tile.getDecorativeObject(); + if (deco != null) handleObjectSpawn(ctx, deco); + WallObject wall = tile.getWallObject(); + if (wall != null) handleObjectSpawn(ctx, wall); + GroundObject ground = tile.getGroundObject(); + if (ground != null && ground.getRenderable() != null) handleObjectSpawn(ctx, ground); + for (GameObject go : tile.getGameObjects()) { + if (go == null || go.getRenderable() instanceof Actor) continue; + handleObjectSpawn(ctx, go); + } + } + } + } + log.info("Finished loading scene particle emitters (count={}, took={}ms)", particleSystem.getEmitters().size(), sw.elapsed(TimeUnit.MILLISECONDS)); + } + + private void loadConfig() { + Runnable onReload = () -> clientThread.invoke(this::applyConfig); + emitterDefinitionManager.startup(onReload); + particleDefinitions.startup(onReload); + applyConfig(); + log.info("[Particles] Loaded: Textures(size={}, time={}ms), Definitions(size={}, time={}ms), Emitters(placements={}, objects={}, time={}ms)", + particleTextureLoader.getLastTextureCount(), particleTextureLoader.getLastLoadTimeMs(), + particleDefinitions.getLastDefinitionCount(), particleDefinitions.getLastLoadTimeMs(), + emitterDefinitionManager.getLastPlacements(), emitterDefinitionManager.getLastObjectBindings(), emitterDefinitionManager.getLastLoadTimeMs()); + } + + private void applyConfig() { + emitterDefinitionManager.loadConfig(); + particleDefinitions.loadConfig(); + // Do not preload particle textures at all; they are lazily loaded on first use in ParticleTextureLoader.getTextureId. + // Preloading (even only emitter-used textures) breaks shadows on some setups. + loadSceneParticles(plugin.getSceneContext()); + } + + public void recreateEmittersFromPlacements(@Nullable SceneContext ctx) { + final List definitionEmitters = emitterDefinitionManager.getDefinitionEmitters(); + if (!definitionEmitters.isEmpty()) { + particleSystem.removeEmitters(definitionEmitters); + definitionEmitters.clear(); + } + + particleTextureLoader.setActiveTextureName(particleDefinitions.getDefaultTexturePath()); + + final Map definitions = particleDefinitions.getDefinitions(); + if (definitions.isEmpty()) { + log.debug("[Particles] No particle definitions loaded, skipping emitter recreation."); + return; + } + + final List placements = emitterDefinitionManager.getPlacements(); + final List weatherPlacements = emitterDefinitionManager.getWeatherPlacements(); + if (placements.isEmpty() && weatherPlacements.isEmpty()) { + return; + } + + for (EmitterPlacement place : placements) { + if (place.getParticleId() == null) continue; + final String pid = place.getParticleId().toUpperCase(); + final ParticleDefinition def = definitions.get(pid); + if (def == null) continue; + final WorldPoint wp = new WorldPoint(place.getWorldX(), place.getWorldY(), place.getPlane()); + final ParticleEmitter emitter = createEmitterFromDefinition(def, wp); + emitter.particleId(def.id); + particleSystem.addEmitter(emitter); + definitionEmitters.add(emitter); + } + + // Weather: random point within area, height offset 1700, pitch 1024 (down) + final float weatherHeightOffset = 1700f; + final float weatherPitch = 1024 * UNITS_TO_RAD; + long baseCycle = client.getGameCycle(); + ThreadLocalRandom rng = ThreadLocalRandom.current(); + for (EmitterPlacement place : weatherPlacements) { + if (place.getParticleId() == null) continue; + final String pid = place.getParticleId().toUpperCase(); + final ParticleDefinition def = definitions.get(pid); + if (def == null) continue; + final WorldPoint wp = new WorldPoint(place.getWorldX(), place.getWorldY(), place.getPlane()); + final ParticleEmitter emitter = createEmitterFromDefinition(def, wp); + emitter.particleId(def.id); + emitter.setHeightOffset(weatherHeightOffset); + emitter.setDirectionPitch(weatherPitch); + emitter.setAlphaScale(place.getEdgeFadeFactor()); + emitter.setSpawnAtTopOfWorld(true); + emitter.emissionBurst(Math.max(4, def.emission.minSpawn), Math.max(8, def.emission.maxSpawn), 0); + int phaseOffset = rng.nextInt(0, 2000); + emitter.setEmissionTime(baseCycle - phaseOffset, def.emission.emissionCycleDuration, def.emission.emissionTimeThreshold, def.emission.emitOnlyBeforeTime, def.emission.loopEmission); + emitter.setEmissionAccum(rng.nextFloat()); + emitter.particleLifetime(3f, 25f); + particleSystem.addEmitter(emitter); + definitionEmitters.add(emitter); + } + } + + private ParticleEmitter createEmitterFromDefinition(ParticleDefinition def, WorldPoint wp) { + def.postDecode(); + float syMin = def.spread.yawMin * UNITS_TO_RAD; + float syMax = def.spread.yawMax * UNITS_TO_RAD; + float spMin = def.spread.pitchMin * UNITS_TO_RAD; + float spMax = def.spread.pitchMax * UNITS_TO_RAD; + float sizeMin = def.scale.minScale; + float sizeMax = def.scale.maxScale; + float scaleTrans = def.scale.scaleTransitionPercent > 0 ? def.scale.scaleTransitionPercent / 100f * 2f : 1f; + float[] colorMin = argbToFloat(def.colours.minColourArgb); + float[] colorMax = argbToFloat(def.colours.maxColourArgb); + float[] targetColor = def.colours.targetColourArgb != 0 ? argbToFloat(def.colours.targetColourArgb) : null; + float lifeMin = def.emission.minDelay / 64f; + float lifeMax = Math.max(lifeMin, def.emission.maxDelay / 64f); + ParticleEmitter e = new ParticleEmitter() + .at(wp) + .heightOffset(def.general.heightOffset) + .direction((float) def.general.directionYaw * UNITS_TO_RAD, (float) def.general.directionPitch * UNITS_TO_RAD) + .spreadYaw(syMin, syMax) + .spreadPitch(spMin, spMax) + .speed(def.speed.minSpeed, def.speed.maxSpeed) + .particleLifetime(lifeMin, lifeMax) + .size(sizeMin, sizeMax) + .color(colorMin[0], colorMin[1], colorMin[2], colorMin[3]) + .colorRange(colorMin, colorMax) + .uniformColorVariation(def.colours.uniformColourVariation) + .emissionBurst(def.emission.minSpawn, def.emission.maxSpawn, def.emission.initialSpawn); + if (def.speed.targetSpeed >= 0) + e.targetSpeed(def.speed.targetSpeed, def.speed.speedTransitionPercent / 100f * 2f); + if (def.scale.targetScale >= 0) + e.targetScale(def.scale.targetScale, scaleTrans); + if (targetColor != null) + e.targetColor(targetColor, def.colours.colourTransitionPercent, def.colours.alphaTransitionPercent); + e.setDefinition(def); + e.setEmissionTime(client.getGameCycle(), def.emission.emissionCycleDuration, def.emission.emissionTimeThreshold, def.emission.emitOnlyBeforeTime, def.emission.loopEmission); + return e; + } + + private static float[] argbToFloat(int argb) { + return new float[] { + ((argb >> 16) & 0xff) / 255f, + ((argb >> 8) & 0xff) / 255f, + (argb & 0xff) / 255f, + ((argb >> 24) & 0xff) / 255f + }; + } + + public Map getDefinitions() { + return particleDefinitions.getDefinitions(); + } + + @Nullable + public ParticleDefinition getDefinition(String id) { + return particleDefinitions.getDefinition(id); + } + + public List getDefinitionIdsOrdered() { + return particleDefinitions.getDefinitionIdsOrdered(); + } + + /** + * Apply the current definition for the given particle id to all emitters that use that id, + * including both tile emitters (from emitters.json) and object emitters (bound to game objects). + * Use after editing the definition in the panel so emitters update in-game. + */ + public void applyDefinitionToEmittersWithId(String particleId) { + if (particleId == null || particleId.isEmpty()) return; + ParticleDefinition def = particleDefinitions.getDefinition(particleId); + if (def == null) return; + String pid = particleId.toUpperCase(); + for (ParticleEmitter emitter : particleSystem.getEmitters()) { + String eid = emitter.getParticleId(); + if (eid != null && eid.equalsIgnoreCase(pid)) + applyDefinitionToEmitter(emitter, def); + } + } + + public void applyDefinitionToEmitter(ParticleEmitter emitter, ParticleDefinition def) { + if (emitter == null || def == null) return; + def.postDecode(); + // spread 0–2048 angle units (float), speed/scale from JSON as-is + float syMin = def.spread.yawMin * UNITS_TO_RAD; + float syMax = def.spread.yawMax * UNITS_TO_RAD; + float spMin = def.spread.pitchMin * UNITS_TO_RAD; + float spMax = def.spread.pitchMax * UNITS_TO_RAD; + float sizeMin = def.scale.minScale; + float sizeMax = def.scale.maxScale; + float scaleTrans = def.scale.scaleTransitionPercent > 0 ? def.scale.scaleTransitionPercent / 100f * 2f : 1f; + float[] colorMin = argbToFloat(def.colours.minColourArgb); + float[] colorMax = argbToFloat(def.colours.maxColourArgb); + float[] targetColor = def.colours.targetColourArgb != 0 ? argbToFloat(def.colours.targetColourArgb) : null; + float lifeMin = def.emission.minDelay / 64f; + float lifeMax = Math.max(lifeMin, def.emission.maxDelay / 64f); + WorldPoint wp = emitter.getWorldPoint(); + if (wp != null) + emitter.at(wp); + emitter.heightOffset(def.general.heightOffset); + emitter.direction((float) def.general.directionYaw * UNITS_TO_RAD, (float) def.general.directionPitch * UNITS_TO_RAD); + emitter.spreadYaw(syMin, syMax); + emitter.spreadPitch(spMin, spMax); + emitter.speed(def.speed.minSpeed, def.speed.maxSpeed); + emitter.particleLifetime(lifeMin, lifeMax); + emitter.size(sizeMin, sizeMax); + emitter.color(colorMin[0], colorMin[1], colorMin[2], colorMin[3]); + emitter.colorRange(colorMin, colorMax); + emitter.uniformColorVariation(def.colours.uniformColourVariation); + emitter.emissionBurst(def.emission.minSpawn, def.emission.maxSpawn, def.emission.initialSpawn); + if (def.speed.targetSpeed >= 0) + emitter.targetSpeed(def.speed.targetSpeed, def.speed.speedTransitionPercent / 100f * 2f); + else + emitter.targetSpeed(ParticleDefinition.NO_TARGET, 1f); + if (def.scale.targetScale >= 0) + emitter.targetScale(def.scale.targetScale, scaleTrans); + if (targetColor != null) + emitter.targetColor(targetColor, def.colours.colourTransitionPercent, def.colours.alphaTransitionPercent); + emitter.particleId(def.id); + emitter.setDefinition(def); + emitter.setEmissionTime(client.getGameCycle(), def.emission.emissionCycleDuration, def.emission.emissionTimeThreshold, def.emission.emitOnlyBeforeTime, def.emission.loopEmission); + String tex = def.texture.file; + if (tex != null && !tex.isEmpty()) + particleTextureLoader.setActiveTextureName(tex); + } + + public List getPlacements() { + return emitterDefinitionManager.getPlacements(); + } + + @Nullable + public String getActiveTextureName() { + return particleTextureLoader.getActiveTextureName(); + } + + public List getAvailableTextureNames() { + return particleDefinitions.getAvailableTextureNames(); + } + + public ParticleEmitter placeEmitter(WorldPoint worldPoint) { + ParticleEmitter e = new ParticleEmitter().at(worldPoint); + addEmitter(e); + log.info("[Particles] Placed emitter at world ({}, {}, {}), total emitters={}", worldPoint.getX(), worldPoint.getY(), worldPoint.getPlane(), particleSystem.getEmitters().size()); + return e; + } + + @Nullable + public ParticleEmitter spawnEmitterFromDefinition(String definitionId, WorldPoint wp) { + ParticleDefinition def = particleDefinitions.getDefinition(definitionId); + if (def == null) return null; + ParticleEmitter e = createEmitterFromDefinition(def, wp); + e.particleId(def.id); + addEmitter(e); + return e; + } + + public int spawnPerformanceTestEmitters() { + despawnPerformanceTestEmitters(); + String[] ids = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" }; + int[][] offsets = { + { 0, 0 }, { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 }, + { 1, 1 }, { -1, 1 }, { 1, -1 }, { -1, -1 }, { 2, 0 } + }; + Player player = client.getLocalPlayer(); + if (player == null) return 0; + WorldPoint base = player.getWorldLocation(); + for (int i = 0; i < ids.length; i++) { + ParticleEmitter e = spawnEmitterFromDefinition(ids[i], base.dx(offsets[i][0]).dy(offsets[i][1])); + if (e != null) performanceTestEmitters.add(e); + } + log.info("[Particles] Performance test: spawned {} emitters around player", performanceTestEmitters.size()); + return performanceTestEmitters.size(); + } + + public void despawnPerformanceTestEmitters() { + for (ParticleEmitter e : performanceTestEmitters) + particleSystem.removeEmitter(e); + performanceTestEmitters.clear(); + } + + public boolean hasPerformanceTestEmitters() { + return !performanceTestEmitters.isEmpty(); + } + + /** + * Spawns up to count moving particles at random positions around the player. + * Uses definition "0" for particle appearance. Returns number spawned. + */ + public int spawnRandomParticles(int count) { + return spawnRandomParticlesInternal(count, true); + } + + private int spawnRandomParticlesInternal(int count, boolean logResult) { + SceneContext ctx = plugin.getSceneContext(); + Player player = client.getLocalPlayer(); + if (ctx == null || ctx.sceneBase == null || player == null) + return 0; + WorldPoint wp = player.getWorldLocation(); + int[] local = ctx.worldToLocalFirst(wp); + if (local == null) return 0; + ParticleDefinition def = particleDefinitions.getDefinition("0"); + if (def == null) def = particleDefinitions.getDefinitions().values().stream().findFirst().orElse(null); + if (def == null) return 0; + int plane = local[2]; + float halfTile = LOCAL_TILE_SIZE / 2f; + float baseX = local[0] + halfTile; + float baseZ = local[1] + halfTile; + float baseY = getTerrainHeight(ctx, (int) baseX, (int) baseZ, plane) + 64; + ParticleEmitter emitter = createEmitterFromDefinition(def, wp); + emitter.particleId(def.id); + emitter.setDefinition(def); + float radius = 6f * LOCAL_TILE_SIZE; + ThreadLocalRandom rng = ThreadLocalRandom.current(); + ParticleBuffer buf = particleSystem.getRenderBuffer(); + int spawned = 0; + int target = Math.min(count, MAX_PARTICLES); + for (int i = 0; i < target; i++) { + if (buf.count >= MAX_PARTICLES) break; + Particle p = obtainParticle(); + if (p == null) break; + float ox = baseX + (rng.nextFloat() * 2f - 1f) * radius; + float oy = baseY + (rng.nextFloat() * 2f - 1f) * radius * 0.5f; + float oz = baseZ + (rng.nextFloat() * 2f - 1f) * radius; + if (!emitter.spawnInto(p, ox, oy, oz, plane)) { + releaseParticle(p); + continue; + } + addSpawnedParticleToBuffer(p, ox, oy, oz, emitter); + spawned++; + } + if (logResult && spawned > 0) + log.info("[Particles] Spawned {} random particles around player", spawned); + return spawned; + } + + private int getImpostorId(TileObject tileObject) { + ObjectComposition def = client.getObjectDefinition(tileObject.getId()); + if (def == null) return tileObject.getId(); + var impostorIds = def.getImpostorIds(); + if (impostorIds != null) { + try { + int impostorVarbit = def.getVarbitId(); + int impostorVarp = def.getVarPlayerId(); + int impostorIndex = -1; + if (impostorVarbit != -1) { + impostorIndex = client.getVarbitValue(impostorVarbit); + } else if (impostorVarp != -1) { + impostorIndex = client.getVarpValue(impostorVarp); + } + if (impostorIndex >= 0) + return impostorIds[min(impostorIndex, impostorIds.length - 1)]; + } catch (Exception ignored) {} + } + return tileObject.getId(); + } + + private void handleObjectSpawn(TileObject tileObject) { + SceneContext sceneContext = plugin.getSceneContext(); + if (sceneContext != null) + handleObjectSpawn(sceneContext, tileObject); + } + + private void handleObjectSpawn(@Nonnull SceneContext sceneContext, @Nonnull TileObject tileObject) { + if (tileObject.getPlane() < 0 || emitterDefinitionManager.getObjectBindingsByType().isEmpty()) + return; + int tileObjectId = tileObject.getId(); + if (tileObject instanceof GameObject && + ((GameObject) tileObject).getRenderable() instanceof DynamicObject) { + if (client.isClientThread()) { + tileObjectId = getImpostorId(tileObject); + } else { + return; + } + } + spawnEmittersForObject(tileObject, tileObjectId); + } + + private void spawnEmittersForObject(TileObject tileObject, int objectId) { + handleObjectDespawn(tileObject); + List bindings = emitterDefinitionManager.getObjectBindingsByType().get(objectId); + if (bindings == null || bindings.isEmpty()) return; + WorldPoint wp = tileObject.getWorldLocation(); + List created = new ArrayList<>(); + for (EmitterConfigEntry.ObjectBinding binding : bindings) { + ParticleDefinition def = particleDefinitions.getDefinition(binding.particleId); + if (def == null) continue; + ParticleEmitter e = createEmitterFromDefinition(def, wp); + e.particleId(def.id); + e.positionOffset(binding.offsetX, binding.offsetY, binding.offsetZ); + e.setAlignment(binding.getAlignment()); + e.setTileObject(tileObject); + if (tileObject instanceof GameObject) { + GameObject go = (GameObject) tileObject; + e.setOrientation(HDUtils.getModelOrientation(go.getConfig())); + e.setSizeX(go.sizeX()); + e.setSizeY(go.sizeY()); + } else { + e.setSizeX(1); + e.setSizeY(1); + } + particleSystem.addEmitter(e); + created.add(e); + String tex = def.texture.file; + if (tex != null && !tex.isEmpty()) + particleTextureLoader.setActiveTextureName(tex); + } + if (!created.isEmpty()) + particleSystem.getEmittersByTileObject().put(tileObject, created); + } + + private void handleObjectDespawn(TileObject tileObject) { + List list = particleSystem.getEmittersByTileObject().remove(tileObject); + if (list != null) + particleSystem.removeEmitters(list); + } + + @Subscribe + public void onGameObjectSpawned(GameObjectSpawned e) { + handleObjectSpawn(e.getGameObject()); + } + + @Subscribe + public void onGameObjectDespawned(GameObjectDespawned e) { + handleObjectDespawn(e.getGameObject()); + } + + @Subscribe + public void onWallObjectSpawned(WallObjectSpawned e) { + handleObjectSpawn(e.getWallObject()); + } + + @Subscribe + public void onWallObjectDespawned(WallObjectDespawned e) { + handleObjectDespawn(e.getWallObject()); + } + + @Subscribe + public void onDecorativeObjectSpawned(DecorativeObjectSpawned e) { + handleObjectSpawn(e.getDecorativeObject()); + } + + @Subscribe + public void onDecorativeObjectDespawned(DecorativeObjectDespawned e) { + handleObjectDespawn(e.getDecorativeObject()); + } + + @Subscribe + public void onGroundObjectSpawned(GroundObjectSpawned e) { + handleObjectSpawn(e.getGroundObject()); + } + + @Subscribe + public void onGroundObjectDespawned(GroundObjectDespawned e) { + handleObjectDespawn(e.getGroundObject()); + } + + public void update(@Nullable SceneContext ctx, float dt) { + particleSystem.getEmittersCulledThisFrame().clear(); + if (ctx != null && ctx.sceneBase != null) { + particleSystem.getObjectPositionCache().clear(); + final long gameCycle = client.getGameCycle(); + final int[][][] tileHeights = ctx.scene.getTileHeights(); + ParticleBuffer buf = particleSystem.getRenderBuffer(); + + int numEmitters = particleSystem.getEmitters().size(); + if (emitterIterationArray.length < numEmitters) + emitterIterationArray = new ParticleEmitter[numEmitters]; + particleSystem.getEmitters().toArray(emitterIterationArray); + for (int e = 0; e < numEmitters; e++) { + ParticleEmitter emitter = emitterIterationArray[e]; + ParticleDefinition def = emitter.getDefinition(); + + if (!getEmitterSpawnPosition(ctx, emitter, updatePosOut, planeOutScratch, tileHeights, spawnOrigin, spawnPosScratch, spawnOffsetScratch, localScratch, particleSystem.getObjectPositionCache())) + continue; + int plane = planeOutScratch[0]; + + if (buf.count >= MAX_PARTICLES) continue; + if (def != null) + emitter.setDirectionYaw((float) def.general.directionYaw * UNITS_TO_RAD); + emitter.tick(dt, gameCycle, updatePosOut[0], updatePosOut[1], updatePosOut[2], plane, this); + } + lastEmittersCulled = 0; + lastEmittersUpdating = numEmitters; + + if (continuousRandomSpawn && buf.count < MAX_PARTICLES) { + int toSpawn = Math.min(150, MAX_PARTICLES - buf.count); + if (toSpawn > 0) + spawnRandomParticlesInternal(toSpawn, false); + } + } else { + lastEmittersUpdating = 0; + lastEmittersCulled = particleSystem.getEmitters().size(); + } + + ParticleBuffer buf = particleSystem.getRenderBuffer(); + int n = buf.count; + int tickDelta = Math.max(1, (int) Math.round(dt * 50)); + tickDelta = Math.min(tickDelta, 10); + for (int i = 0; i < n; ) { + if (MovingParticle.tick(buf, i, tickDelta)) { + buf.swap(i, n - 1); + n--; + } else { + i++; + } + } + buf.count = n; + + for (int i = 0; i < buf.count; i++) + buf.syncRefToFloat(i); + } + + @Nullable + private static Model getModelFromTileObject(TileObject obj) { + Renderable r = null; + if (obj instanceof GameObject) + r = ((GameObject) obj).getRenderable(); + else if (obj instanceof DecorativeObject) + r = ((DecorativeObject) obj).getRenderable(); + else if (obj instanceof WallObject) + r = ((WallObject) obj).getRenderable1(); + else if (obj instanceof GroundObject) + r = ((GroundObject) obj).getRenderable(); + if (r == null) return null; + if (r instanceof Model) return (Model) r; + if (r instanceof DynamicObject) return ((DynamicObject) r).getModelZbuf(); + return r.getModel(); + } + + /** + * Current orientation of the tile object in JAU (0–2048). Used so emitter offsets rotate with the object. + */ + private static int getObjectOrientation(TileObject tileObject) { + if (tileObject instanceof GroundObject) + return HDUtils.getModelOrientation(((GroundObject) tileObject).getConfig()); + if (tileObject instanceof DecorativeObject) + return HDUtils.getModelOrientation(((DecorativeObject) tileObject).getConfig()); + if (tileObject instanceof WallObject) + return HDUtils.convertWallObjectOrientation(((WallObject) tileObject).getOrientationA()); + if (tileObject instanceof GameObject) + return HDUtils.getModelOrientation(((GameObject) tileObject).getConfig()); + return 0; + } + + /** + * Same object-type position offset as LightManager.spawnLights (for position/height parity with lights). + */ + private static void getObjectPositionOffset(TileObject tileObject, int[] outOffset) { + outOffset[0] = 0; + outOffset[1] = 0; + if (tileObject instanceof GroundObject) { + // no offset + } else if (tileObject instanceof DecorativeObject) { + var object = (DecorativeObject) tileObject; + int ori = HDUtils.getModelOrientation(object.getConfig()); + switch (ObjectType.fromConfig(object.getConfig())) { + case WallDecorDiagonalNoOffset: + case WallDecorDiagonalOffset: + case WallDecorDiagonalBoth: + ori = (ori + 512) % 2048; + outOffset[0] = SINE[ori] * 64 >> 16; + outOffset[1] = COSINE[ori] * 64 >> 16; + break; + } + outOffset[0] += object.getXOffset(); + outOffset[1] += object.getYOffset(); + } else if (tileObject instanceof WallObject) { + // no offset + } else if (tileObject instanceof GameObject) { + var object = (GameObject) tileObject; + int ori = HDUtils.getModelOrientation(object.getConfig()); + int offsetDist = 64; + switch (ObjectType.fromConfig(object.getConfig())) { + case RoofEdgeDiagonalCorner: + case RoofDiagonalWithRoofEdge: + ori += 1024; + offsetDist = round(offsetDist / sqrt(2)); + case WallDiagonal: + ori = (ori + 2048 - 256) % 2048; + outOffset[0] = SINE[ori] * offsetDist >> 16; + outOffset[1] = COSINE[ori] * offsetDist >> 16; + break; + } + } + } + + /** + * Computes the spawn position (local x, y, z) for an emitter — same as used in update(). + * Used by ParticleGizmoOverlay to draw the gizmo where particles start. + */ + public boolean getEmitterSpawnPosition(@Nullable SceneContext ctx, ParticleEmitter emitter, float[] outPos, int[] outPlane) { + if (ctx == null || ctx.sceneBase == null || outPos == null || outPos.length < 3) + return false; + int[][][] tileHeights = ctx.scene.getTileHeights(); + return getEmitterSpawnPosition(ctx, emitter, outPos, outPlane, tileHeights, null, null, null, null, null); + } + + private boolean getEmitterSpawnPosition(@Nullable SceneContext ctx, ParticleEmitter emitter, float[] outPos, int[] outPlane, + int[][][] tileHeights, float[] scratchOrigin, float[] scratchPos, int[] scratchOffset, int[] scratchLocal, + @Nullable Map positionCache) { + if (ctx == null || ctx.sceneBase == null || outPos == null || outPos.length < 3 || tileHeights == null) + return false; + float halfTile = LOCAL_TILE_SIZE / 2f; + int sceneOffset = ctx.sceneOffset; + float[] origin = scratchOrigin != null ? scratchOrigin : new float[3]; + float[] pos = scratchPos != null ? scratchPos : new float[3]; + int[] offset = scratchOffset != null ? scratchOffset : new int[2]; + TileObject obj = emitter.getTileObject(); + int plane; + if (obj != null) { + if (positionCache != null) { + float[] cached = positionCache.get(obj); + if (cached != null) { + outPos[0] = cached[0]; + outPos[1] = cached[1]; + outPos[2] = cached[2]; + if (outPlane != null && outPlane.length >= 1) + outPlane[0] = (int) cached[3]; + return true; + } + } + LocalPoint lp = obj.getLocalLocation(); + plane = obj.getPlane(); + getObjectPositionOffset(obj, offset); + int posX = lp.getX() + offset[0]; + int posZ = lp.getY() + offset[1]; + origin[0] = posX; + origin[2] = posZ; + float baseTerrain = getTerrainHeight(ctx, posX, posZ, plane, tileHeights); + Model model = getModelFromTileObject(obj); + float objHeight = baseTerrain + (model != null ? model.getBottomY() : 0); + origin[1] = Math.max(objHeight, baseTerrain); + } else { + WorldPoint wp = emitter.getWorldPoint(); + if (wp == null) return false; + int[] local = scratchLocal != null ? ctx.worldToLocalFirst(wp, scratchLocal) : ctx.worldToLocalFirst(wp); + if (local == null) return false; + plane = local[2]; + int tileExX = local[0] / LOCAL_TILE_SIZE + sceneOffset; + int tileExY = local[1] / LOCAL_TILE_SIZE + sceneOffset; + origin[0] = local[0] + halfTile; + origin[2] = local[1] + halfTile; + if (emitter.isSpawnAtTopOfWorld()) { + origin[1] = plugin.cameraPosition[1] - 800f; + } else { + if (tileExX >= 0 && tileExY >= 0 && tileExX < EXTENDED_SCENE_SIZE && tileExY < EXTENDED_SCENE_SIZE) { + origin[1] = tileHeights[plane][tileExX][tileExY] - emitter.getHeightOffset(); + } else + origin[1] = 0; + } + } + pos[0] = origin[0]; + pos[1] = origin[1]; + pos[2] = origin[2]; + int baseOrientation = obj != null ? getObjectOrientation(obj) : emitter.getOrientation(); + int orientation = emitter.getAlignment().relative ? mod(baseOrientation + emitter.getAlignment().orientation, 2048) : emitter.getAlignment().orientation; + if (emitter.getAlignment() != Alignment.CUSTOM) { + int localSizeX = emitter.getSizeX() * LOCAL_TILE_SIZE; + int localSizeY = emitter.getSizeY() * LOCAL_TILE_SIZE; + float radius = emitter.getAlignment().radial ? localSizeX / 2f : (float) Math.sqrt(localSizeX * localSizeX + localSizeY * localSizeY) / 2; + float sine = sin(orientation * JAU_TO_RAD); + float cosine = cos(orientation * JAU_TO_RAD); + cosine /= (float) localSizeX / (float) Math.max(localSizeY, 1); + pos[0] += (int) (radius * sine); + pos[2] += (int) (radius * cosine); + } + if (obj != null) { + float sin = sin(orientation * JAU_TO_RAD); + float cos = cos(orientation * JAU_TO_RAD); + float x = emitter.getOffsetX(); + float z = emitter.getOffsetY(); + pos[0] += -cos * x - sin * z; + pos[1] -= emitter.getOffsetZ(); + pos[2] += -cos * z + sin * x; + } + outPos[0] = pos[0]; + outPos[1] = pos[1]; + outPos[2] = pos[2]; + if (outPlane != null && outPlane.length >= 1) + outPlane[0] = plane; + if (obj != null && positionCache != null) { + float[] cached = new float[] { outPos[0], outPos[1], outPos[2], plane }; + positionCache.put(obj, cached); + } + return true; + } + + /** + * Returns emitters whose spawn position is on the given tile. Used by particle gizmo right-click menu. + */ + public List getEmittersAtTile(@Nullable SceneContext ctx, @Nullable Tile tile) { + if (tile == null || particleSystem.getEmitters().isEmpty()) + return List.of(); + LocalPoint lp = tile.getLocalLocation(); + int tileX = lp.getX() / LOCAL_TILE_SIZE; + int tileY = lp.getY() / LOCAL_TILE_SIZE; + int plane = tile.getPlane(); + List result = new ArrayList<>(); + for (ParticleEmitter em : particleSystem.getEmitters()) { + TileObject obj = em.getTileObject(); + if (obj != null) { + LocalPoint objLp = obj.getLocalLocation(); + if (objLp.getX() / LOCAL_TILE_SIZE == tileX && objLp.getY() / LOCAL_TILE_SIZE == tileY && obj.getPlane() == plane) + result.add(em); + } else { + WorldPoint wp = em.getWorldPoint(); + if (wp != null && ctx != null) { + int[] local = ctx.worldToLocalFirst(wp); + if (local != null && local[0] / LOCAL_TILE_SIZE == tileX && local[1] / LOCAL_TILE_SIZE == tileY && local[2] == plane) + result.add(em); + } + } + } + return result; + } + + public static float getTerrainHeight(SceneContext ctx, int localX, int localZ, int plane) { + return getTerrainHeight(ctx, localX, localZ, plane, ctx != null && ctx.sceneBase != null ? ctx.scene.getTileHeights() : null); + } + + private static float getTerrainHeight(SceneContext ctx, int localX, int localZ, int plane, int[][][] tileHeights) { + if (tileHeights == null) + tileHeights = ctx.scene.getTileHeights(); + int sceneExX = Math.max(0, Math.min(EXTENDED_SCENE_SIZE - 2, (localX >> LOCAL_COORD_BITS) + ctx.sceneOffset)); + int sceneExY = Math.max(0, Math.min(EXTENDED_SCENE_SIZE - 2, (localZ >> LOCAL_COORD_BITS) + ctx.sceneOffset)); + int x = localX & (LOCAL_TILE_SIZE - 1); + int y = localZ & (LOCAL_TILE_SIZE - 1); + int h0 = (x * tileHeights[plane][sceneExX + 1][sceneExY] + (LOCAL_TILE_SIZE - x) * tileHeights[plane][sceneExX][sceneExY]) >> LOCAL_COORD_BITS; + int h1 = (x * tileHeights[plane][sceneExX + 1][sceneExY + 1] + (LOCAL_TILE_SIZE - x) * tileHeights[plane][sceneExX][sceneExY + 1]) >> LOCAL_COORD_BITS; + return (float) ((y * h1 + (LOCAL_TILE_SIZE - y) * h0) >> LOCAL_COORD_BITS); + } +} diff --git a/src/main/java/rs117/hd/scene/particles/core/MovingParticle.java b/src/main/java/rs117/hd/scene/particles/core/MovingParticle.java new file mode 100644 index 0000000000..813d8deaf4 --- /dev/null +++ b/src/main/java/rs117/hd/scene/particles/core/MovingParticle.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2025, Mark7625 (https://github.com/Mark7625/) + * All rights reserved. + */ +package rs117.hd.scene.particles.core; + +import static net.runelite.api.Constants.EXTENDED_SCENE_SIZE; +import static net.runelite.api.Perspective.LOCAL_TILE_SIZE; + +import java.util.concurrent.ThreadLocalRandom; +import rs117.hd.scene.particles.core.buffer.ParticleBuffer; +import rs117.hd.scene.particles.definition.ParticleDefinition; +import rs117.hd.scene.particles.emitter.ParticleEmitter; + +/** + * Advances a single particle. Returns true if it should be removed. + */ +public final class MovingParticle { + + private static int clampColour16(int value) { + if (value < 0) return 0; + if (value > 65535) return 65535; + return value; + } + + public static boolean tick(ParticleBuffer buf, int i, int tickDelta) { + buf.remainingTicks[i] -= tickDelta; + if (buf.remainingTicks[i] <= 0) { + return true; + } + int posX = (int) (buf.xFixed[i] >> 12); + int posY = (int) (buf.yFixed[i] >> 12); + int posZ = (int) (buf.zFixed[i] >> 12); + ParticleEmitter emitter = buf.emitter[i]; + ParticleDefinition config = emitter != null ? emitter.getDefinition() : null; + + if (config != null && config.colours.targetColourArgb != 0) { + int elapsed = buf.lifetimeTicks[i] - buf.remainingTicks[i]; + if (elapsed <= config.colourTransitionTicks) { + int red16 = (buf.colourArgbRef[i] >> 8 & 0xff00) + (buf.colourRgbLowRef[i] >> 16 & 0xff) + config.redIncrementPerTick * tickDelta; + int green16 = (buf.colourArgbRef[i] & 0xff00) + (buf.colourRgbLowRef[i] >> 8 & 0xff) + config.greenIncrementPerTick * tickDelta; + int blue16 = (buf.colourArgbRef[i] << 8 & 0xff00) + (buf.colourRgbLowRef[i] & 0xff) + config.blueIncrementPerTick * tickDelta; + red16 = clampColour16(red16); + green16 = clampColour16(green16); + blue16 = clampColour16(blue16); + buf.colourArgbRef[i] &= ~0xffffff; + buf.colourArgbRef[i] |= ((red16 & 0xff00) << 8) + (green16 & 0xff00) + ((blue16 & 0xff00) >> 8); + buf.colourRgbLowRef[i] &= ~0xffffff; + buf.colourRgbLowRef[i] |= ((red16 & 0xff) << 16) + ((green16 & 0xff) << 8) + (blue16 & 0xff); + } + if (elapsed <= config.alphaTransitionTicks) { + int alpha16 = (buf.colourArgbRef[i] >> 16 & 0xff00) + (buf.colourRgbLowRef[i] >> 24 & 0xff) + config.alphaIncrementPerTick * tickDelta; + alpha16 = clampColour16(alpha16); + buf.colourArgbRef[i] &= 0xffffff; + buf.colourArgbRef[i] |= (alpha16 & 0xff00) << 16; + buf.colourRgbLowRef[i] &= 0xffffff; + buf.colourRgbLowRef[i] |= (alpha16 & 0xff) << 24; + } + } + if (config != null && config.speed.targetSpeed >= 0 && buf.lifetimeTicks[i] - buf.remainingTicks[i] <= config.speedTransitionTicks) { + buf.speedRef[i] += config.speedIncrementPerTick * tickDelta; + } + if (config != null && config.scale.targetScale >= 0) { + int elapsed = buf.lifetimeTicks[i] - buf.remainingTicks[i]; + if (elapsed <= config.scaleTransitionTicks) { + int ticksRemaining = config.scaleTransitionTicks - elapsed; + int targetRef = config.targetScaleRef; + int delta = targetRef - buf.scaleRef[i]; + if (ticksRemaining > 0 && delta != 0) { + int step = (int) Math.round((double) delta * tickDelta / ticksRemaining); + buf.scaleRef[i] += step; + if (delta > 0 && buf.scaleRef[i] > targetRef) buf.scaleRef[i] = targetRef; + else if (delta < 0 && buf.scaleRef[i] < targetRef) buf.scaleRef[i] = targetRef; + } + } + } + + if (config != null && config.physics.distanceFalloffType == 1) { + int dx = posX - (int) buf.emitterOriginX[i]; + int dy = posY - (int) buf.emitterOriginY[i]; + int dz = posZ - (int) buf.emitterOriginZ[i]; + int dist = (int) Math.sqrt((double) (dx * dx + dy * dy + dz * dz)) >> 2; + long falloff = (long) (config.physics.distanceFalloffStrength * dist * tickDelta); + buf.speedRef[i] -= (long) buf.speedRef[i] * falloff >> 18; + } else if (config != null && config.physics.distanceFalloffType == 2) { + int dx = posX - (int) buf.emitterOriginX[i]; + int dy = posY - (int) buf.emitterOriginY[i]; + int dz = posZ - (int) buf.emitterOriginZ[i]; + int distSq = dx * dx + dy * dy + dz * dz; + long falloff = (long) (config.physics.distanceFalloffStrength * distSq * tickDelta); + buf.speedRef[i] -= (long) buf.speedRef[i] * falloff >> 28; + } + + if (config != null && config.general.randomYawRotation > 0f) { + float maxDelta = config.general.randomYawRotation * (tickDelta / 50f); + buf.yaw[i] += (ThreadLocalRandom.current().nextFloat() * 2f - 1f) * maxDelta; + } + + buf.xFixed[i] += ((long) buf.velocityX[i] * (long) (buf.speedRef[i] << 2) >> 23) * (long) tickDelta; + buf.yFixed[i] += ((long) buf.velocityY[i] * (long) (buf.speedRef[i] << 2) >> 23) * (long) tickDelta; + buf.zFixed[i] += ((long) buf.velocityZ[i] * (long) (buf.speedRef[i] << 2) >> 23) * (long) tickDelta; + + int east = (int) (buf.xFixed[i] >> 12); + int north = (int) (buf.zFixed[i] >> 12); + int height = (int) (buf.yFixed[i] >> 12); + int sceneSizeXZ = EXTENDED_SCENE_SIZE * LOCAL_TILE_SIZE; + if (east < 0 || east >= sceneSizeXZ || north < 0 || north >= sceneSizeXZ + || height < -262144 || height > 262144) { + return true; + } + return false; + } +} diff --git a/src/main/java/rs117/hd/scene/particles/core/Particle.java b/src/main/java/rs117/hd/scene/particles/core/Particle.java new file mode 100644 index 0000000000..0b0e66f86b --- /dev/null +++ b/src/main/java/rs117/hd/scene/particles/core/Particle.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2025, Mark7625 (https://github.com/Mark7625/) + * All rights reserved. + */ +package rs117.hd.scene.particles.core; + +import javax.annotation.Nullable; +import lombok.NoArgsConstructor; +import rs117.hd.scene.particles.emitter.ParticleEmitter; + +@NoArgsConstructor +public class Particle { + public final float[] position = new float[3]; + public final float[] velocity = new float[3]; + public float life; + public float maxLife; + public float size; + public float targetScale; + public float scaleTransition; + public float targetSpeed; + public float speedTransition; + public final float[] color = new float[4]; + public final float[] initialColor = new float[4]; + public float[] targetColor; + public float colorTransitionPct = 100f; + public float alphaTransitionPct = 100f; + public int plane; + + @Nullable + public float[] colourIncrementPerSecond; + public float scaleIncrementPerSecond; + public float speedIncrementPerSecond; + public float colourTransitionEndLife = -1f; + public float scaleTransitionEndLife = -1f; + public float speedTransitionEndLife = -1f; + public float emitterOriginX, emitterOriginY, emitterOriginZ; + public int distanceFalloffType; + public int distanceFalloffStrength; + public boolean clipToTerrain; + public boolean hasLevelBounds; + public int upperBoundLevel = -2; + public int lowerBoundLevel = -2; + + @Nullable + public ParticleEmitter emitter; + + public int flipbookRandomFrame = -1; + + /** Current yaw rotation (radians) for billboard. Used when randomYawRotation enabled. */ + public float yaw; + + /** Pool slot index; used by ParticlePool for bookkeeping. */ + public int poolIndex; + + public void resetForPool() { + emitter = null; + flipbookRandomFrame = -1; + yaw = 0f; + colourIncrementPerSecond = null; + scaleIncrementPerSecond = 0f; + speedIncrementPerSecond = 0f; + colourTransitionEndLife = -1f; + scaleTransitionEndLife = -1f; + speedTransitionEndLife = -1f; + distanceFalloffType = 0; + distanceFalloffStrength = 0; + clipToTerrain = false; + hasLevelBounds = false; + upperBoundLevel = -2; + lowerBoundLevel = -2; + targetColor = null; + } + + public void setPosition(float x, float y, float z) { + position[0] = x; + position[1] = y; + position[2] = z; + } + + public void setVelocity(float vx, float vy, float vz) { + velocity[0] = vx; + velocity[1] = vy; + velocity[2] = vz; + } + + public void setColor(float r, float g, float b, float a) { + color[0] = r; + color[1] = g; + color[2] = b; + color[3] = a; + } + + public float getElapsedFraction() { + if (maxLife <= 0) return 1f; + return 1f - life / maxLife; + } + + public static float transitionBlend(float elapsedFraction, float transitionPct) { + if (transitionPct <= 0) return 0f; + return Math.min(1f, elapsedFraction / (transitionPct / 100f)); + } + + public float getAlpha() { + if (maxLife <= 0) return 1f; + if (targetColor == null) return Math.max(0f, Math.min(1f, life / maxLife)); + float blend = transitionBlend(getElapsedFraction(), alphaTransitionPct); + return initialColor[3] + (targetColor[3] - initialColor[3]) * blend; + } + + public void getCurrentColor(float[] out) { + if (colourIncrementPerSecond != null) { + out[0] = color[0]; + out[1] = color[1]; + out[2] = color[2]; + out[3] = color[3]; + return; + } + float t = getElapsedFraction(); + if (targetColor == null || colorTransitionPct <= 0) { + out[0] = color[0]; + out[1] = color[1]; + out[2] = color[2]; + out[3] = getAlpha(); + return; + } + float blend = transitionBlend(t, colorTransitionPct); + float aBlend = transitionBlend(t, alphaTransitionPct); + out[0] = initialColor[0] + (targetColor[0] - initialColor[0]) * blend; + out[1] = initialColor[1] + (targetColor[1] - initialColor[1]) * blend; + out[2] = initialColor[2] + (targetColor[2] - initialColor[2]) * blend; + out[3] = initialColor[3] + (targetColor[3] - initialColor[3]) * aBlend; + } +} diff --git a/src/main/java/rs117/hd/scene/particles/core/ParticleSystem.java b/src/main/java/rs117/hd/scene/particles/core/ParticleSystem.java new file mode 100644 index 0000000000..7f8c42d42a --- /dev/null +++ b/src/main/java/rs117/hd/scene/particles/core/ParticleSystem.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2025, Mark7625 (https://github.com/Mark7625/) + * All rights reserved. + */ +package rs117.hd.scene.particles.core; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nullable; +import lombok.Getter; +import lombok.Setter; +import rs117.hd.scene.particles.core.buffer.ParticleBuffer; +import rs117.hd.scene.particles.core.buffer.ParticlePool; +import rs117.hd.scene.particles.emitter.ParticleEmitter; + +/** + * Holds emitters, pre-allocated pool, and render buffer. + */ +public final class ParticleSystem { + + @Getter + private final List emitters = new ArrayList<>(); + + @Getter + private final Map> emittersByTileObject = new LinkedHashMap<>(); + + @Getter + private final ParticleBuffer renderBuffer; + + @Getter + private final ParticlePool pool; + + @Getter + private final Set emittersCulledThisFrame = new HashSet<>(); + + @Getter + private final Map objectPositionCache = new HashMap<>(); + + @Setter + @Getter + private int lastEmittersUpdating; + @Setter + @Getter + private int lastEmittersCulled; + + public ParticleSystem(int maxParticles) { + this.pool = new ParticlePool(maxParticles); + this.renderBuffer = new ParticleBuffer(maxParticles); + } + + public void addEmitter(@Nullable ParticleEmitter emitter) { + if (emitter != null && !emitters.contains(emitter)) { + emitters.add(emitter); + } + } + + public void removeEmitter(ParticleEmitter emitter) { + emitters.remove(emitter); + } + + public void removeEmitters(Collection toRemove) { + emitters.removeAll(toRemove); + } + + public void clear() { + emittersByTileObject.clear(); + emitters.clear(); + renderBuffer.clear(); + objectPositionCache.clear(); + } +} diff --git a/src/main/java/rs117/hd/scene/particles/core/ParticleTextureLoader.java b/src/main/java/rs117/hd/scene/particles/core/ParticleTextureLoader.java new file mode 100644 index 0000000000..016559d08d --- /dev/null +++ b/src/main/java/rs117/hd/scene/particles/core/ParticleTextureLoader.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2025, Mark7625 (https://github.com/Mark7625/) + * All rights reserved. + */ +package rs117.hd.scene.particles.core; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import javax.imageio.ImageIO; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import javax.inject.Singleton; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.lwjgl.BufferUtils; +import rs117.hd.utils.Props; +import rs117.hd.utils.ResourcePath; + +import static rs117.hd.utils.ResourcePath.path; +import static org.lwjgl.opengl.GL33C.*; + +/** + * Particle textures: single 2D textures (getTextureId) plus optional texture array for single-draw batching. + * Texture array: layer 0 = white fallback; layers 1..N = lazily loaded, scaled to LAYER_SIZE. + */ +@Slf4j +@Singleton +public class ParticleTextureLoader { + + private static final ResourcePath PARTICLE_TEXTURES_PATH = Props.getFolder( + "rlhd.particle-texture-path", + () -> path(ParticleTextureLoader.class, "..", "..", "textures", "particles") + ); + private static final int LAYER_SIZE = 256; + private static final int MAX_LAYERS = 32; + + @Getter + @Setter + private String activeTextureName; + + private final Map textureIds = new HashMap<>(); + private int texArrayId; + private final Map nameToLayer = new HashMap<>(); + private int nextLayer = 1; + + @Getter + private int lastTextureCount; + @Getter + private long lastLoadTimeMs; + + /** + * Preload all given texture names: load from disk and upload to GL. Call when particle config is loaded. + * Existing preloaded textures are disposed first. Safe to call on client/GL thread. + */ + public void preload(List textureNames) { + long start = System.nanoTime(); + dispose(); + lastTextureCount = 0; + lastLoadTimeMs = (System.nanoTime() - start) / 1_000_000; + if (textureNames == null) return; + for (String name : textureNames) { + if (name == null || name.isEmpty() || textureIds.containsKey(name)) continue; + try { + ResourcePath resPath = PARTICLE_TEXTURES_PATH.resolve(name); + BufferedImage img = loadImageExact(resPath); + if (img == null) continue; + int id = uploadToGl(img); + textureIds.put(name, id); + } catch (IOException e) { + log.warn("[Particles] Failed to preload texture: {}", name, e); + } + } + lastTextureCount = textureIds.size(); + lastLoadTimeMs = (System.nanoTime() - start) / 1_000_000; + } + + @Nullable + public Integer getTextureId(String textureFileName) { + if (textureFileName == null || textureFileName.isEmpty()) + return null; + + Integer existing = textureIds.get(textureFileName); + if (existing != null && existing != 0) + return existing; + + long start = System.nanoTime(); + try { + ResourcePath resPath = PARTICLE_TEXTURES_PATH.resolve(textureFileName); + BufferedImage img = loadImageExact(resPath); + if (img == null) + return null; + int id = uploadToGl(img); + textureIds.put(textureFileName, id); + lastTextureCount = textureIds.size(); + lastLoadTimeMs = (System.nanoTime() - start) / 1_000_000; + return id; + } catch (IOException e) { + log.warn("[Particles] Failed to lazily load texture: {}", textureFileName, e); + return null; + } + } + + public int getTextureArrayId() { + ensureArrayCreated(); + return texArrayId; + } + + public int getTextureLayer(String textureName) { + if (textureName == null || textureName.isEmpty()) return 0; + Integer layer = nameToLayer.get(textureName); + if (layer != null) return layer; + ensureArrayCreated(); + if (nextLayer >= MAX_LAYERS) { + log.warn("[Particles] Texture array full, using layer 0 for: {}", textureName); + return 0; + } + try { + ResourcePath resPath = PARTICLE_TEXTURES_PATH.resolve(textureName); + BufferedImage img = loadImageExact(resPath); + if (img == null) return 0; + int layerIdx = nextLayer++; + uploadToLayer(layerIdx, img); + nameToLayer.put(textureName, layerIdx); + lastTextureCount = nameToLayer.size(); + return layerIdx; + } catch (IOException e) { + log.warn("[Particles] Failed to load texture: {}", textureName, e); + return 0; + } + } + + private void ensureArrayCreated() { + if (texArrayId != 0) return; + texArrayId = glGenTextures(); + glBindTexture(GL_TEXTURE_2D_ARRAY, texArrayId); + ByteBuffer white = BufferUtils.createByteBuffer(LAYER_SIZE * LAYER_SIZE * 4); + for (int i = 0; i < LAYER_SIZE * LAYER_SIZE; i++) + white.put((byte) 255).put((byte) 255).put((byte) 255).put((byte) 255); + white.flip(); + glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, LAYER_SIZE, LAYER_SIZE, MAX_LAYERS, 0, GL_RGBA, GL_UNSIGNED_BYTE, (ByteBuffer) null); + glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, LAYER_SIZE, LAYER_SIZE, 1, GL_RGBA, GL_UNSIGNED_BYTE, white); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glGenerateMipmap(GL_TEXTURE_2D_ARRAY); + glBindTexture(GL_TEXTURE_2D_ARRAY, 0); + } + + private void uploadToLayer(int layer, BufferedImage img) { + BufferedImage scaled = img; + if (img.getWidth() != LAYER_SIZE || img.getHeight() != LAYER_SIZE) { + scaled = new BufferedImage(LAYER_SIZE, LAYER_SIZE, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = scaled.createGraphics(); + g.drawImage(img, 0, 0, LAYER_SIZE, LAYER_SIZE, null); + g.dispose(); + } + ByteBuffer pixels = BufferUtils.createByteBuffer(LAYER_SIZE * LAYER_SIZE * 4); + for (int y = LAYER_SIZE - 1; y >= 0; y--) + for (int x = 0; x < LAYER_SIZE; x++) { + int argb = scaled.getRGB(x, y); + pixels.put((byte) ((argb >> 16) & 0xFF)); + pixels.put((byte) ((argb >> 8) & 0xFF)); + pixels.put((byte) (argb & 0xFF)); + pixels.put((byte) ((argb >> 24) & 0xFF)); + } + pixels.flip(); + glBindTexture(GL_TEXTURE_2D_ARRAY, texArrayId); + glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, LAYER_SIZE, LAYER_SIZE, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + glGenerateMipmap(GL_TEXTURE_2D_ARRAY); + glBindTexture(GL_TEXTURE_2D_ARRAY, 0); + } + + public void dispose() { + for (int id : textureIds.values()) { + if (id != 0) glDeleteTextures(id); + } + textureIds.clear(); + if (texArrayId != 0) { + glDeleteTextures(texArrayId); + texArrayId = 0; + } + nameToLayer.clear(); + nextLayer = 1; + } + + /** + * Load image at exact file resolution (no Toolkit scaling). Returns null if read fails. + */ + @Nullable + private BufferedImage loadImageExact(ResourcePath path) throws IOException { + try (InputStream is = path.toInputStream()) { + BufferedImage img = ImageIO.read(is); + if (img == null) { + log.warn("[Particles] ImageIO.read returned null for: {}", path); + return null; + } + return img; + } + } + + private int uploadToGl(BufferedImage img) { + int w = img.getWidth(); + int h = img.getHeight(); + int id = glGenTextures(); + glBindTexture(GL_TEXTURE_2D, id); + ByteBuffer pixels = BufferUtils.createByteBuffer(w * h * 4); + for (int y = h - 1; y >= 0; y--) + for (int x = 0; x < w; x++) { + int argb = img.getRGB(x, y); + pixels.put((byte) ((argb >> 16) & 0xFF)); + pixels.put((byte) ((argb >> 8) & 0xFF)); + pixels.put((byte) (argb & 0xFF)); + pixels.put((byte) ((argb >> 24) & 0xFF)); + } + pixels.flip(); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + glGenerateMipmap(GL_TEXTURE_2D); + // NEAREST keeps sharp edges; mipmaps improve quality when particle is drawn small + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, 0); + return id; + } + + @Nullable + public ResourcePath getTextureResourcePath() { + if (activeTextureName == null || activeTextureName.isEmpty()) + return null; + return PARTICLE_TEXTURES_PATH.resolve(activeTextureName); + } + + public static ResourcePath getParticleTexturesPath() { + return PARTICLE_TEXTURES_PATH; + } +} diff --git a/src/main/java/rs117/hd/scene/particles/core/buffer/ParticleBuffer.java b/src/main/java/rs117/hd/scene/particles/core/buffer/ParticleBuffer.java new file mode 100644 index 0000000000..975e8d5711 --- /dev/null +++ b/src/main/java/rs117/hd/scene/particles/core/buffer/ParticleBuffer.java @@ -0,0 +1,388 @@ +/* + * Copyright (c) 2025, Mark7625 (https://github.com/Mark7625/) + * All rights reserved. + */ +package rs117.hd.scene.particles.core.buffer; + +import rs117.hd.scene.particles.core.Particle; +import rs117.hd.scene.particles.emitter.ParticleEmitter; + +/** + * SoA particle buffer. Fixed capacity, pre-allocated at construction. No runtime resize. + */ +public final class ParticleBuffer { + + public static final int FLAG_COLOUR_INCREMENT = 1; + public static final int FLAG_HAS_TARGET_COLOR = 2; + + public int count; + public int capacity; + + public float[] posX; + public float[] posY; + public float[] posZ; + public float[] velX; + public float[] velY; + public float[] velZ; + public float[] life; + public float[] maxLife; + public float[] size; + public int[] plane; + + public float[] colorR; + public float[] colorG; + public float[] colorB; + public float[] colorA; + + public float[] initialColorR; + public float[] initialColorG; + public float[] initialColorB; + public float[] initialColorA; + public float[] targetColorR; + public float[] targetColorG; + public float[] targetColorB; + public float[] targetColorA; + public float[] colorTransitionPct; + public float[] alphaTransitionPct; + + public float[] colourIncR; + public float[] colourIncG; + public float[] colourIncB; + public float[] colourIncA; + public float[] colourTransitionEndLife; + public float[] scaleTransitionEndLife; + public float[] speedTransitionEndLife; + public float[] scaleIncPerSec; + public float[] speedIncPerSec; + + public float[] emitterOriginX; + public float[] emitterOriginY; + public float[] emitterOriginZ; + public int[] distanceFalloffType; + public int[] distanceFalloffStrength; + public boolean[] hasLevelBounds; + public int[] upperBoundLevel; + public int[] lowerBoundLevel; + + public int[] flags; + public ParticleEmitter[] emitter; + + public long[] xFixed; + public long[] yFixed; + public long[] zFixed; + public short[] velocityX; + public short[] velocityY; + public short[] velocityZ; + public int[] speedRef; + public int[] lifetimeTicks; + public int[] remainingTicks; + public int[] colourArgbRef; + public int[] colourRgbLowRef; + public int[] scaleRef; + + public float[] flipbookFrame; + public float[] yaw; + + public ParticleBuffer(int capacity) { + this.capacity = capacity; + posX = new float[capacity]; + posY = new float[capacity]; + posZ = new float[capacity]; + velX = new float[capacity]; + velY = new float[capacity]; + velZ = new float[capacity]; + life = new float[capacity]; + maxLife = new float[capacity]; + size = new float[capacity]; + plane = new int[capacity]; + colorR = new float[capacity]; + colorG = new float[capacity]; + colorB = new float[capacity]; + colorA = new float[capacity]; + initialColorR = new float[capacity]; + initialColorG = new float[capacity]; + initialColorB = new float[capacity]; + initialColorA = new float[capacity]; + targetColorR = new float[capacity]; + targetColorG = new float[capacity]; + targetColorB = new float[capacity]; + targetColorA = new float[capacity]; + colorTransitionPct = new float[capacity]; + alphaTransitionPct = new float[capacity]; + colourIncR = new float[capacity]; + colourIncG = new float[capacity]; + colourIncB = new float[capacity]; + colourIncA = new float[capacity]; + colourTransitionEndLife = new float[capacity]; + scaleTransitionEndLife = new float[capacity]; + speedTransitionEndLife = new float[capacity]; + scaleIncPerSec = new float[capacity]; + speedIncPerSec = new float[capacity]; + emitterOriginX = new float[capacity]; + emitterOriginY = new float[capacity]; + emitterOriginZ = new float[capacity]; + distanceFalloffType = new int[capacity]; + distanceFalloffStrength = new int[capacity]; + hasLevelBounds = new boolean[capacity]; + upperBoundLevel = new int[capacity]; + lowerBoundLevel = new int[capacity]; + flags = new int[capacity]; + emitter = new ParticleEmitter[capacity]; + xFixed = new long[capacity]; + yFixed = new long[capacity]; + zFixed = new long[capacity]; + velocityX = new short[capacity]; + velocityY = new short[capacity]; + velocityZ = new short[capacity]; + speedRef = new int[capacity]; + lifetimeTicks = new int[capacity]; + remainingTicks = new int[capacity]; + colourArgbRef = new int[capacity]; + colourRgbLowRef = new int[capacity]; + scaleRef = new int[capacity]; + flipbookFrame = new float[capacity]; + yaw = new float[capacity]; + } + + + public void ensureCapacity(int min) {} + + public void addFrom(Particle p) { + if (count >= capacity) return; + int i = count++; + posX[i] = p.position[0]; + posY[i] = p.position[1]; + posZ[i] = p.position[2]; + velX[i] = p.velocity[0]; + velY[i] = p.velocity[1]; + velZ[i] = p.velocity[2]; + life[i] = p.life; + maxLife[i] = p.maxLife; + size[i] = p.size; + plane[i] = p.plane; + colorR[i] = p.color[0]; + colorG[i] = p.color[1]; + colorB[i] = p.color[2]; + colorA[i] = p.color[3]; + initialColorR[i] = p.initialColor[0]; + initialColorG[i] = p.initialColor[1]; + initialColorB[i] = p.initialColor[2]; + initialColorA[i] = p.initialColor[3]; + colorTransitionPct[i] = p.colorTransitionPct; + alphaTransitionPct[i] = p.alphaTransitionPct; + emitterOriginX[i] = p.emitterOriginX; + emitterOriginY[i] = p.emitterOriginY; + emitterOriginZ[i] = p.emitterOriginZ; + distanceFalloffType[i] = p.distanceFalloffType; + distanceFalloffStrength[i] = p.distanceFalloffStrength; + hasLevelBounds[i] = p.hasLevelBounds; + upperBoundLevel[i] = p.upperBoundLevel; + lowerBoundLevel[i] = p.lowerBoundLevel; + emitter[i] = p.emitter; + + int f = 0; + if (p.colourIncrementPerSecond != null) { + f |= FLAG_COLOUR_INCREMENT; + colourIncR[i] = p.colourIncrementPerSecond[0]; + colourIncG[i] = p.colourIncrementPerSecond[1]; + colourIncB[i] = p.colourIncrementPerSecond[2]; + colourIncA[i] = p.colourIncrementPerSecond[3]; + colourTransitionEndLife[i] = p.colourTransitionEndLife; + } else { + colourTransitionEndLife[i] = -1f; + } + if (p.targetColor != null) { + f |= FLAG_HAS_TARGET_COLOR; + targetColorR[i] = p.targetColor[0]; + targetColorG[i] = p.targetColor[1]; + targetColorB[i] = p.targetColor[2]; + targetColorA[i] = p.targetColor[3]; + } + flags[i] = f; + scaleIncPerSec[i] = p.scaleIncrementPerSecond; + speedIncPerSec[i] = p.speedIncrementPerSecond; + scaleTransitionEndLife[i] = p.scaleTransitionEndLife; + speedTransitionEndLife[i] = p.speedTransitionEndLife; + // Ref state from float (so MovingParticle.tick 1:1 can run) + xFixed[i] = (long) (p.position[0] * 4096); + yFixed[i] = (long) (p.position[1] * 4096); + zFixed[i] = (long) (p.position[2] * 4096); + float vx = p.velocity[0], vy = p.velocity[1], vz = p.velocity[2]; + float mag = (float) Math.sqrt((double) (vx * vx + vy * vy + vz * vz)); + if (mag > 1e-6f) { + velocityX[i] = (short) Math.max(-32768, Math.min(32767, (int) (vx / mag * 32767))); + velocityY[i] = (short) Math.max(-32768, Math.min(32767, (int) (vy / mag * 32767))); + velocityZ[i] = (short) Math.max(-32768, Math.min(32767, (int) (vz / mag * 32767))); + speedRef[i] = (int) (mag * 16384); + } else { + velocityX[i] = 0; + velocityY[i] = 0; + velocityZ[i] = 0; + speedRef[i] = 0; + } + lifetimeTicks[i] = (int) (p.maxLife * 50f); + remainingTicks[i] = (int) (p.life * 50f); + int r = (int) (p.color[0] * 255f) & 0xff; + int g = (int) (p.color[1] * 255f) & 0xff; + int b = (int) (p.color[2] * 255f) & 0xff; + int a = (int) (p.color[3] * 255f) & 0xff; + colourArgbRef[i] = (a << 24) | (r << 16) | (g << 8) | b; + colourRgbLowRef[i] = 0; + scaleRef[i] = (int) (p.size / 4f * 16384); + flipbookFrame[i] = p.flipbookRandomFrame >= 0 ? (float) p.flipbookRandomFrame : -1f; + yaw[i] = 0f; + } + + /** + * Writes ref state (fixed-point) to float arrays for rendering. + */ + public void syncRefToFloat(int i) { + posX[i] = (float) (xFixed[i] >> 12); + posY[i] = (float) (yFixed[i] >> 12); + posZ[i] = (float) (zFixed[i] >> 12); + life[i] = (float) remainingTicks[i] / 50f; + maxLife[i] = (float) lifetimeTicks[i] / 50f; + size[i] = (float) scaleRef[i] / 16384f * 4f; + int argb = colourArgbRef[i]; + int low = colourRgbLowRef[i]; + int red16 = (argb >> 8 & 0xff00) + (low >> 16 & 0xff); + int green16 = (argb & 0xff00) + (low >> 8 & 0xff); + int blue16 = (argb << 8 & 0xff00) + (low & 0xff); + int alpha16 = (argb >> 16 & 0xff00) + (low >> 24 & 0xff); + colorR[i] = (red16 >> 8) / 255f; + colorG[i] = (green16 >> 8) / 255f; + colorB[i] = (blue16 >> 8) / 255f; + colorA[i] = (alpha16 >> 8) / 255f; + } + + public void swap(int i, int j) { + swapFloat(posX, i, j); + swapFloat(posY, i, j); + swapFloat(posZ, i, j); + swapFloat(velX, i, j); + swapFloat(velY, i, j); + swapFloat(velZ, i, j); + swapFloat(life, i, j); + swapFloat(maxLife, i, j); + swapFloat(size, i, j); + swapInt(plane, i, j); + swapFloat(colorR, i, j); + swapFloat(colorG, i, j); + swapFloat(colorB, i, j); + swapFloat(colorA, i, j); + swapFloat(initialColorR, i, j); + swapFloat(initialColorG, i, j); + swapFloat(initialColorB, i, j); + swapFloat(initialColorA, i, j); + swapFloat(targetColorR, i, j); + swapFloat(targetColorG, i, j); + swapFloat(targetColorB, i, j); + swapFloat(targetColorA, i, j); + swapFloat(colorTransitionPct, i, j); + swapFloat(alphaTransitionPct, i, j); + swapFloat(colourIncR, i, j); + swapFloat(colourIncG, i, j); + swapFloat(colourIncB, i, j); + swapFloat(colourIncA, i, j); + swapFloat(colourTransitionEndLife, i, j); + swapFloat(scaleTransitionEndLife, i, j); + swapFloat(speedTransitionEndLife, i, j); + swapFloat(scaleIncPerSec, i, j); + swapFloat(speedIncPerSec, i, j); + swapFloat(emitterOriginX, i, j); + swapFloat(emitterOriginY, i, j); + swapFloat(emitterOriginZ, i, j); + swapInt(distanceFalloffType, i, j); + swapInt(distanceFalloffStrength, i, j); + swapBool(hasLevelBounds, i, j); + swapInt(upperBoundLevel, i, j); + swapInt(lowerBoundLevel, i, j); + swapInt(flags, i, j); + ParticleEmitter e = emitter[i]; + emitter[i] = emitter[j]; + emitter[j] = e; + swapLong(xFixed, i, j); + swapLong(yFixed, i, j); + swapLong(zFixed, i, j); + swapShort(velocityX, i, j); + swapShort(velocityY, i, j); + swapShort(velocityZ, i, j); + swapInt(speedRef, i, j); + swapInt(lifetimeTicks, i, j); + swapInt(remainingTicks, i, j); + swapInt(colourArgbRef, i, j); + swapInt(colourRgbLowRef, i, j); + swapInt(scaleRef, i, j); + swapFloat(flipbookFrame, i, j); + swapFloat(yaw, i, j); + } + + private static void swapLong(long[] a, int i, int j) { + long t = a[i]; + a[i] = a[j]; + a[j] = t; + } + + private static void swapShort(short[] a, int i, int j) { + short t = a[i]; + a[i] = a[j]; + a[j] = t; + } + + private static void swapFloat(float[] a, int i, int j) { + float t = a[i]; + a[i] = a[j]; + a[j] = t; + } + + private static void swapInt(int[] a, int i, int j) { + int t = a[i]; + a[i] = a[j]; + a[j] = t; + } + + private static void swapBool(boolean[] a, int i, int j) { + boolean t = a[i]; + a[i] = a[j]; + a[j] = t; + } + + /** + * Writes current RGBA of particle at slot i into out[0..3]. + */ + public void getCurrentColor(int i, float[] out) { + if ((flags[i] & FLAG_COLOUR_INCREMENT) != 0) { + out[0] = colorR[i]; + out[1] = colorG[i]; + out[2] = colorB[i]; + out[3] = colorA[i]; + return; + } + float m = maxLife[i]; + if (m <= 0) { + out[0] = colorR[i]; + out[1] = colorG[i]; + out[2] = colorB[i]; + out[3] = Math.max(0f, Math.min(1f, colorA[i])); + return; + } + float t = 1f - life[i] / m; + if ((flags[i] & FLAG_HAS_TARGET_COLOR) == 0 || colorTransitionPct[i] <= 0) { + out[0] = colorR[i]; + out[1] = colorG[i]; + out[2] = colorB[i]; + out[3] = Math.max(0f, Math.min(1f, colorA[i])); + return; + } + float blend = Particle.transitionBlend(t, colorTransitionPct[i]); + float aBlend = Particle.transitionBlend(t, alphaTransitionPct[i]); + out[0] = initialColorR[i] + (targetColorR[i] - initialColorR[i]) * blend; + out[1] = initialColorG[i] + (targetColorG[i] - initialColorG[i]) * blend; + out[2] = initialColorB[i] + (targetColorB[i] - initialColorB[i]) * blend; + out[3] = initialColorA[i] + (targetColorA[i] - initialColorA[i]) * aBlend; + } + + public void clear() { + count = 0; + } +} diff --git a/src/main/java/rs117/hd/scene/particles/core/buffer/ParticlePool.java b/src/main/java/rs117/hd/scene/particles/core/buffer/ParticlePool.java new file mode 100644 index 0000000000..7eabf3e6fa --- /dev/null +++ b/src/main/java/rs117/hd/scene/particles/core/buffer/ParticlePool.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025, Mark7625 (https://github.com/Mark7625/) + * All rights reserved. + */ +package rs117.hd.scene.particles.core.buffer; + +import javax.annotation.Nullable; +import lombok.Getter; +import rs117.hd.scene.particles.core.Particle; + +/** + * Pre-allocated particle pool with index-based free list. No runtime allocation. + */ +public final class ParticlePool { + + private final Particle[] particles; + private final int[] freeIndices; + @Getter + private int freeCount; + @Getter + private final int capacity; + + public ParticlePool(int capacity) { + this.capacity = capacity; + this.particles = new Particle[capacity]; + this.freeIndices = new int[capacity]; + for (int i = 0; i < capacity; i++) { + particles[i] = new Particle(); + particles[i].poolIndex = i; + freeIndices[i] = i; + } + this.freeCount = capacity; + } + + @Nullable + public Particle obtain() { + if (freeCount <= 0) return null; + int idx = freeIndices[--freeCount]; + Particle p = particles[idx]; + p.resetForPool(); + return p; + } + + public void release(Particle p) { + if (p != null) { + p.resetForPool(); + freeIndices[freeCount++] = p.poolIndex; + } + } +} diff --git a/src/main/java/rs117/hd/scene/particles/debug/DirectionGizmoPanel.java b/src/main/java/rs117/hd/scene/particles/debug/DirectionGizmoPanel.java new file mode 100644 index 0000000000..9430331057 --- /dev/null +++ b/src/main/java/rs117/hd/scene/particles/debug/DirectionGizmoPanel.java @@ -0,0 +1,380 @@ +/* + * Copyright (c) 2025, Mark7625. + * Direction gizmo: compass for yaw (drag to point) + vertical pitch bar (drag up/down). + */ +package rs117.hd.scene.particles.debug; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.font.FontRenderContext; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Line2D; +import java.awt.geom.Path2D; +import java.awt.geom.RoundRectangle2D; +import java.util.function.Supplier; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import net.runelite.client.ui.ColorScheme; + +/** + * Two controls that fill the panel width: + * 1. Compass (left): drag to set yaw. N/E/S/W labels, arrow shows direction. + * 2. Pitch bar (right): vertical strip — drag to set pitch (Up / Side / Down). + */ +public class DirectionGizmoPanel extends JPanel { + + private static final int PITCH_BAR_WIDTH = 24; + private static final int GAP = 12; + private static final int PREF_HEIGHT = 140; + private static final int LABEL_OFFSET = 10; + private static final double YAW_FULL = 2048.0; + private static final double PITCH_FULL = 1024.0; + private static final int PITCH_CORNER = 12; + private static final int THUMB_CORNER = 4; + + private static final Color COMPASS_FILL = new Color(45, 45, 48); + private static final Color COMPASS_STROKE = ColorScheme.MEDIUM_GRAY_COLOR; + private static final Color CARDINAL_COLOR = ColorScheme.LIGHT_GRAY_COLOR; + + // Single accent color used for base arrow and base pitch. + private static final Color ACCENT = new Color(102, 187, 183); + + private static final Color ARROW_COLOR = ACCENT; + private static final Color ARROW_BASE = ACCENT; + private static final Color PITCH_TRACK = new Color(55, 58, 62); + private static final Color PITCH_FILL = ACCENT; + private static final Color PITCH_THUMB = ACCENT; + + // Spread visualization colors (yaw wedge + pitch band + legend). + // Use a distinct, darker color so spread stands out but isn't bright yellow. + private static final Color SPREAD_YAW_FILL = new Color(64, 120, 200, 170); // translucent dark blue wedge + private static final Color SPREAD_PITCH_FILL = new Color(64, 120, 200); // solid dark blue band over base + + private final JSpinner yawSpinner; + private final JSpinner pitchSpinner; + + private int dragMode; + private Supplier spreadSupplier; + + public DirectionGizmoPanel(JSpinner yawSpinner, JSpinner pitchSpinner) { + this.yawSpinner = yawSpinner; + this.pitchSpinner = pitchSpinner; + this.dragMode = 0; + setOpaque(false); + setPreferredSize(new Dimension(0, PREF_HEIGHT)); + setMinimumSize(new Dimension(120, PREF_HEIGHT)); + if (yawSpinner != null) yawSpinner.addChangeListener(e -> repaint()); + if (pitchSpinner != null) pitchSpinner.addChangeListener(e -> repaint()); + + MouseAdapter ma = new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + Layout layout = computeLayout(); + if (layout.compassContains(e.getX(), e.getY())) { + dragMode = 1; + applyYawFromPoint(e.getX(), e.getY(), layout); + } else if (layout.pitchBarContains(e.getX(), e.getY())) { + dragMode = 2; + applyPitchFromY(e.getY(), layout); + } else { + dragMode = 0; + } + } + + @Override + public void mouseReleased(MouseEvent e) { + dragMode = 0; + } + + @Override + public void mouseDragged(MouseEvent e) { + Layout layout = computeLayout(); + if (dragMode == 1) applyYawFromPoint(e.getX(), e.getY(), layout); + else if (dragMode == 2) applyPitchFromY(e.getY(), layout); + } + }; + addMouseListener(ma); + addMouseMotionListener(ma); + } + + public void setSpreadSupplier(Supplier spreadSupplier) { + this.spreadSupplier = spreadSupplier; + repaint(); + } + + public static final class SpreadValues { + public final int yawMinGame; + public final int yawMaxGame; + public final int pitchMinGame; + public final int pitchMaxGame; + + public SpreadValues(int yawMinGame, int yawMaxGame, int pitchMinGame, int pitchMaxGame) { + this.yawMinGame = yawMinGame; + this.yawMaxGame = yawMaxGame; + this.pitchMinGame = pitchMinGame; + this.pitchMaxGame = pitchMaxGame; + } + } + + private static final class Layout { + final double compassCx, compassCy, compassRadius; + final double pitchBarX, pitchBarY, pitchBarW, pitchBarH; + + Layout(double compassCx, double compassCy, double compassRadius, + double pitchBarX, double pitchBarY, double pitchBarW, double pitchBarH) { + this.compassCx = compassCx; + this.compassCy = compassCy; + this.compassRadius = compassRadius; + this.pitchBarX = pitchBarX; + this.pitchBarY = pitchBarY; + this.pitchBarW = pitchBarW; + this.pitchBarH = pitchBarH; + } + + boolean compassContains(int x, int y) { + double dx = x - compassCx; + double dy = y - compassCy; + return dx * dx + dy * dy <= compassRadius * compassRadius; + } + + boolean pitchBarContains(int x, int y) { + return x >= pitchBarX && x <= pitchBarX + pitchBarW && y >= pitchBarY && y <= pitchBarY + pitchBarH; + } + } + + private Layout computeLayout() { + int w = getWidth(); + int h = getHeight(); + if (w < 80) w = 80; + if (h < 60) h = 60; + + double compassAreaWidth = w - PITCH_BAR_WIDTH - GAP - 8; + double maxRadius = Math.min(compassAreaWidth * 0.5, (h - 20) * 0.5) - LABEL_OFFSET; + double compassRadius = Math.max(20, maxRadius); + double compassCx = compassRadius + LABEL_OFFSET + 4; + double compassCy = h * 0.5; + + double pitchBarX = w - PITCH_BAR_WIDTH - 6; + double pitchBarY = 10; + double pitchBarH = h - 20; + return new Layout(compassCx, compassCy, compassRadius, pitchBarX, pitchBarY, PITCH_BAR_WIDTH, pitchBarH); + } + + private void applyYawFromPoint(int mx, int my, Layout layout) { + if (yawSpinner == null) return; + double dx = mx - layout.compassCx; + double dy = my - layout.compassCy; + double angle = Math.atan2(dx, -dy); + if (angle < 0) angle += 2 * Math.PI; + int yaw = (int) Math.round((angle / (2 * Math.PI)) * YAW_FULL) % (int) YAW_FULL; + if (yaw < 0) yaw += (int) YAW_FULL; + SpinnerNumberModel m = (SpinnerNumberModel) yawSpinner.getModel(); + int min = m.getMinimum() != null ? ((Number) m.getMinimum()).intValue() : 0; + int max = m.getMaximum() != null ? ((Number) m.getMaximum()).intValue() : (int) YAW_FULL; + yaw = Math.max(min, Math.min(max, yaw)); + yawSpinner.setValue(yaw); + } + + private int pitchDisplay(int backendPitch) { + return (int) PITCH_FULL - backendPitch; + } + + private int gameToUiYaw(int yawGame) { + int full = (int) YAW_FULL; + int ui = full / 2 - yawGame; + ui %= full; + if (ui < 0) ui += full; + return ui; + } + + private void applyPitchFromY(int my, Layout layout) { + if (pitchSpinner == null) return; + double t = (my - layout.pitchBarY) / layout.pitchBarH; + t = Math.max(0, Math.min(1, t)); + int displayPitch = (int) Math.round((1 - t) * PITCH_FULL); + int backendPitch = (int) PITCH_FULL - displayPitch; + SpinnerNumberModel m = (SpinnerNumberModel) pitchSpinner.getModel(); + int min = m.getMinimum() != null ? ((Number) m.getMinimum()).intValue() : 0; + int max = m.getMaximum() != null ? ((Number) m.getMaximum()).intValue() : (int) PITCH_FULL; + backendPitch = Math.max(min, Math.min(max, backendPitch)); + pitchSpinner.setValue(backendPitch); + } + + private int intValue(JSpinner s, int def) { + if (s == null) return def; + Object v = s.getValue(); + if (v instanceof Number) return ((Number) v).intValue(); + return def; + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); + + Layout layout = computeLayout(); + double cx = layout.compassCx; + double cy = layout.compassCy; + double radius = layout.compassRadius; + + // Compass: fill + thin stroke + g2.setColor(COMPASS_FILL); + g2.fill(new Ellipse2D.Double(cx - radius, cy - radius, radius * 2, radius * 2)); + g2.setColor(COMPASS_STROKE); + g2.setStroke(new BasicStroke(1.2f)); + g2.draw(new Ellipse2D.Double(cx - radius, cy - radius, radius * 2, radius * 2)); + + // Cardinal labels + double labelRadius = radius + 8; + g2.setColor(CARDINAL_COLOR); + g2.setFont(g2.getFont().deriveFont(Font.PLAIN, 14f)); + FontRenderContext frc = g2.getFontRenderContext(); + String n = "N"; + g2.drawString(n, (float) (cx - g2.getFont().getStringBounds(n, frc).getWidth() / 2), (float) (cy - labelRadius)); + String s = "S"; + g2.drawString(s, (float) (cx - g2.getFont().getStringBounds(s, frc).getWidth() / 2), (float) (cy + labelRadius + 8)); + g2.drawString("E", (float) (cx + labelRadius - 4), (float) (cy + 4)); + String west = "W"; + g2.drawString(west, (float) (cx - labelRadius - g2.getFont().getStringBounds(west, frc).getWidth() + 4), (float) (cy + 4)); + + // Spread yaw wedge (under base arrow), if provided. + // Spread yaw values are provided in UI yaw space (0 = North, 512 = East, etc.). + SpreadValues spreadForYaw = spreadSupplier != null ? spreadSupplier.get() : null; + if (spreadForYaw != null) { + int full = (int) YAW_FULL; + int uiMin = spreadForYaw.yawMinGame; + int uiMax = spreadForYaw.yawMaxGame; + double aMin = (uiMin / (double) full) * 2 * Math.PI; + double aMax = (uiMax / (double) full) * 2 * Math.PI; + if (aMax < aMin) { + aMax += 2 * Math.PI; + } + + double rSpread = radius * 0.8; + Path2D wedge = new Path2D.Double(); + wedge.moveTo(cx, cy); + int steps = 24; + for (int i = 0; i <= steps; i++) { + double t = aMin + (aMax - aMin) * (i / (double) steps); + double px = cx + Math.sin(t) * rSpread; + double py = cy - Math.cos(t) * rSpread; + wedge.lineTo(px, py); + } + wedge.closePath(); + g2.setColor(SPREAD_YAW_FILL); + g2.fill(wedge); + } + + // Direction arrow (drawn above yaw spread) + int yaw = intValue(yawSpinner, 0); + double yawRad = (yaw / YAW_FULL) * 2 * Math.PI; + double horizontalLength = radius * 0.68; + double tipX = cx + Math.sin(yawRad) * horizontalLength; + double tipY = cy - Math.cos(yawRad) * horizontalLength; + + g2.setColor(ARROW_COLOR); + g2.setStroke(new BasicStroke(2.2f)); + g2.draw(new Line2D.Double(cx, cy, tipX, tipY)); + double dx = tipX - cx; + double dy = tipY - cy; + double len = Math.hypot(dx, dy); + if (len > 1e-6) { + double ux = dx / len; + double uy = dy / len; + double headLen = 10; + double perpX = -uy; + double perpY = ux; + Path2D head = new Path2D.Double(); + head.moveTo(tipX, tipY); + head.lineTo(tipX - ux * headLen + perpX * 4, tipY - uy * headLen + perpY * 4); + head.lineTo(tipX - ux * headLen - perpX * 4, tipY - uy * headLen - perpY * 4); + head.closePath(); + g2.fill(head); + } + g2.setColor(ARROW_BASE); + g2.fill(new Ellipse2D.Double(cx - 3, cy - 3, 6, 6)); + + // Pitch bar: rounded track + double bx = layout.pitchBarX; + double by = layout.pitchBarY; + double bw = layout.pitchBarW; + double bh = layout.pitchBarH; + g2.setColor(PITCH_TRACK); + g2.fill(new RoundRectangle2D.Double(bx, by, bw, bh, PITCH_CORNER, PITCH_CORNER)); + + int backendPitch = intValue(pitchSpinner, 0); + int pitch1 = pitchDisplay(backendPitch); + double fillH = (pitch1 / PITCH_FULL) * bh; + g2.setColor(PITCH_FILL); + g2.fill(new RoundRectangle2D.Double(bx + 2, by + bh - fillH, bw - 4, Math.max(fillH, 4), 4, 4)); + + double thumbH = 8; + double thumbY = by + bh - fillH - thumbH / 2; + thumbY = Math.max(by + 2, Math.min(by + bh - thumbH - 2, thumbY)); + g2.setColor(PITCH_THUMB); + g2.fill(new RoundRectangle2D.Double(bx + 4, thumbY, bw - 8, thumbH, THUMB_CORNER, THUMB_CORNER)); + + // Spread visualization (pitch band), if provided + if (spreadSupplier != null) { + SpreadValues sv = spreadSupplier.get(); + if (sv != null) { + // Pitch spread band + int pitchMinDisp = pitchDisplay(sv.pitchMinGame); + int pitchMaxDisp = pitchDisplay(sv.pitchMaxGame); + double fMin = Math.max(0, Math.min(1, pitchMinDisp / PITCH_FULL)); + double fMax = Math.max(0, Math.min(1, pitchMaxDisp / PITCH_FULL)); + double fLo = Math.min(fMin, fMax); + double fHi = Math.max(fMin, fMax); + double yLo = by + bh - fLo * bh; + double yHi = by + bh - fHi * bh; + double bandY = Math.min(yLo, yHi); + double bandH = Math.max(4, Math.abs(yLo - yHi)); + + g2.setColor(SPREAD_PITCH_FILL); + g2.fill(new RoundRectangle2D.Double(bx + 3, bandY, bw - 6, bandH, 4, 4)); + } + } + + // Labels and legend + g2.setColor(CARDINAL_COLOR); + g2.setFont(g2.getFont().deriveFont(Font.PLAIN, 13f)); + g2.drawString("Up", (float) (bx - 22), (float) (by + 2)); + g2.drawString("Down", (float) (bx - 30), (float) (by + bh + 2)); + + // Legend (Base / Spread) – show above the gizmo, side by side + int legendX = (int) (cx - radius); + int legendY = (int) (cy - radius) - 18; + int box = 10; + int gap = 4; + + g2.setFont(g2.getFont().deriveFont(Font.PLAIN, 11f)); + + // Base legend item + int baseX = legendX; + int baseY = legendY; + g2.setColor(ARROW_COLOR); + g2.fill(new Ellipse2D.Double(baseX, baseY, box, box)); + g2.setColor(CARDINAL_COLOR); + g2.drawString("Base", baseX + box + gap, baseY + box - 1); + + // Spread legend item, laid out to the right of Base + int spreadX = legendX + 80; + int spreadY = legendY; + g2.setColor(SPREAD_PITCH_FILL); + g2.fill(new Ellipse2D.Double(spreadX, spreadY, box, box)); + g2.setColor(CARDINAL_COLOR); + g2.drawString("Spread", spreadX + box + gap, spreadY + box - 1); + + g2.dispose(); + } +} diff --git a/src/main/java/rs117/hd/scene/particles/debug/ParticleDebugOverlay.java b/src/main/java/rs117/hd/scene/particles/debug/ParticleDebugOverlay.java new file mode 100644 index 0000000000..89535264bc --- /dev/null +++ b/src/main/java/rs117/hd/scene/particles/debug/ParticleDebugOverlay.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2025, Mark7625. + * Overlay: green = emitter positions, white = particle positions (helps see culling). + */ +package rs117.hd.scene.particles.debug; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import javax.inject.Inject; +import javax.inject.Singleton; +import net.runelite.api.Client; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayLayer; +import net.runelite.client.ui.overlay.OverlayManager; +import net.runelite.client.ui.overlay.OverlayPosition; +import rs117.hd.HdPlugin; +import rs117.hd.scene.SceneContext; +import rs117.hd.scene.particles.ParticleManager; +import rs117.hd.scene.particles.core.buffer.ParticleBuffer; +import rs117.hd.utils.Mat4; + +import static rs117.hd.utils.MathUtils.round; + +@Singleton +public class ParticleDebugOverlay extends Overlay { + + private static final int EMITTER_DOT_R = 4; + private static final int PARTICLE_DOT_R = 2; + private static final Color EMITTER_COLOR = new Color(0, 255, 0, 200); + private static final Color PARTICLE_COLOR = new Color(255, 255, 255, 220); + + @Inject + private Client client; + + @Inject + private OverlayManager overlayManager; + + @Inject + private HdPlugin plugin; + + @Inject + private ParticleManager particleManager; + + public ParticleDebugOverlay() { + setLayer(OverlayLayer.ABOVE_SCENE); + setPosition(OverlayPosition.DYNAMIC); + } + + public void setActive(boolean active) { + if (active) { + overlayManager.add(this); + } else { + overlayManager.remove(this); + } + } + + @Override + public Dimension render(Graphics2D g) { + SceneContext ctx = plugin.getSceneContext(); + if (ctx == null || ctx.sceneBase == null) + return null; + + int currentPlane = client.getTopLevelWorldView() != null ? client.getTopLevelWorldView().getPlane() : 0; + float[] proj = Mat4.identity(); + int vw = client.getViewportWidth(); + int vh = client.getViewportHeight(); + Mat4.mul(proj, Mat4.translate(client.getViewportXOffset(), client.getViewportYOffset(), 0)); + Mat4.mul(proj, Mat4.scale(vw, vh, 1)); + Mat4.mul(proj, Mat4.translate(.5f, .5f, .5f)); + Mat4.mul(proj, Mat4.scale(.5f, -.5f, .5f)); + Mat4.mul(proj, plugin.viewProjMatrix); + + float[] pos = new float[3]; + int[] planeOut = new int[1]; + float[] point = new float[4]; + + g.setColor(EMITTER_COLOR); + for (var em : particleManager.getSceneEmitters()) { + if (!particleManager.getEmitterSpawnPosition(ctx, em, pos, planeOut)) + continue; + if (planeOut[0] != currentPlane) + continue; + point[0] = pos[0]; + point[1] = pos[1]; + point[2] = pos[2]; + point[3] = 1f; + Mat4.projectVec(point, proj, point); + if (point[3] <= 0) continue; + int sx = round(point[0]); + int sy = round(point[1]); + g.fillOval(sx - EMITTER_DOT_R, sy - EMITTER_DOT_R, EMITTER_DOT_R * 2, EMITTER_DOT_R * 2); + } + + ParticleBuffer buf = particleManager.getParticleBuffer(); + int[] cameraShift = plugin.cameraShift; + g.setColor(PARTICLE_COLOR); + for (int i = 0; i < buf.count; i++) { + if (buf.plane[i] != currentPlane) + continue; + point[0] = buf.posX[i] + cameraShift[0]; + point[1] = buf.posY[i]; + point[2] = buf.posZ[i] + cameraShift[1]; + point[3] = 1f; + Mat4.projectVec(point, proj, point); + if (point[3] <= 0) continue; + int sx = round(point[0]); + int sy = round(point[1]); + g.fillOval(sx - PARTICLE_DOT_R, sy - PARTICLE_DOT_R, PARTICLE_DOT_R * 2, PARTICLE_DOT_R * 2); + } + + return null; + } +} diff --git a/src/main/java/rs117/hd/scene/particles/debug/ParticleGizmoOverlay.java b/src/main/java/rs117/hd/scene/particles/debug/ParticleGizmoOverlay.java new file mode 100644 index 0000000000..2ac52d98d1 --- /dev/null +++ b/src/main/java/rs117/hd/scene/particles/debug/ParticleGizmoOverlay.java @@ -0,0 +1,730 @@ +/* + * Copyright (c) 2025, Hooder + * All rights reserved. + */ +package rs117.hd.scene.particles.debug; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.Polygon; +import java.awt.Stroke; +import java.awt.event.MouseEvent; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.inject.Inject; +import javax.inject.Singleton; +import net.runelite.api.Client; +import net.runelite.api.MenuAction; +import net.runelite.api.Point; +import net.runelite.api.Menu; +import net.runelite.api.MenuEntry; +import net.runelite.api.Tile; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.MenuEntryAdded; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.input.MouseListener; +import net.runelite.client.input.MouseManager; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayLayer; +import net.runelite.client.ui.overlay.OverlayManager; +import net.runelite.client.ui.overlay.OverlayPosition; +import rs117.hd.HdPlugin; +import rs117.hd.overlays.TileInfoOverlay; +import rs117.hd.scene.SceneContext; +import rs117.hd.scene.particles.ParticleManager; +import rs117.hd.scene.particles.core.buffer.ParticleBuffer; +import rs117.hd.scene.particles.emitter.EmitterDefinitionManager; +import rs117.hd.scene.particles.emitter.ParticleEmitter; +import rs117.hd.scene.particles.emitter.WeatherAreaConfig; +import rs117.hd.utils.Mat4; + +import static rs117.hd.utils.MathUtils.cos; +import static rs117.hd.utils.MathUtils.round; +import static rs117.hd.utils.MathUtils.sin; + +import static net.runelite.api.Constants.EXTENDED_SCENE_SIZE; + +@Singleton +public class ParticleGizmoOverlay extends Overlay implements MouseListener +{ + private static final int INNER_HANDLE_RING = 19; + private static final int OUTER_HANDLE_RING = 25; + private static final int INNER_DOT = 6; + private static final int GIZMO_HOVER_RADIUS = OUTER_HANDLE_RING / 2 + 5; + private static final Color ACTIVE_COLOR = Color.WHITE; + private static final Color INACTIVE_COLOR = Color.RED; + private static final int EMITTER_DOT_R = 4; + private static final int PARTICLE_DOT_R = 4; + private static final Color EMITTER_DEBUG_COLOR = new Color(0, 255, 0, 200); + private static final Color PARTICLE_DEBUG_COLOR = new Color(255, 105, 180, 240); + private static final Color PARTICLE_DEBUG_OUTLINE = new Color(180, 50, 120, 255); + private static final Color TRAJECTORY_LINE_COLOR = new Color(100, 200, 255, 160); + private static final Color BOUNDS_SPREAD_COLOR = new Color(150, 220, 255, 180); + private static final float BOUNDS_SPEED_SCALE = 3.125f; + private static final Color PLACE_MODE_TILE_OUTLINE = new Color(100, 255, 150, 220); + private static final Color WEATHER_AREA_FILL = new Color(100, 200, 255, 100); + + /** When non-null, show debug dots only for emitters/particles with this particle def id. */ + private String debugParticleId; + private boolean overlayActive; + + /** Place mode: when true, hovering shows tile outline and click places emitter. */ + private boolean placeModeActive; + private String placeModeParticleId; + private boolean placeModeMouseRegistered; + private Runnable onPlaceModeChanged; + + @Inject + private TileInfoOverlay tileInfoOverlay; + + @Inject + private MouseManager mouseManager; + + @Inject + private ClientThread clientThread; + + @Inject + private Client client; + + @Inject + private OverlayManager overlayManager; + + @Inject + private HdPlugin plugin; + + @Inject + private ParticleManager particleManager; + + @Inject + private EmitterDefinitionManager emitterDefinitionManager; + + @Inject + private EventBus eventBus; + + private boolean menuRegistered; + + public ParticleGizmoOverlay() + { + setLayer(OverlayLayer.ABOVE_SCENE); + setPosition(OverlayPosition.DYNAMIC); + } + + public boolean isOverlayActive() + { + return overlayActive; + } + + public boolean isDebugForParticleId(String pid) + { + return matchesParticleId(debugParticleId, pid); + } + + public void toggleDebugForParticleId(String pid) + { + if (pid == null || pid.isEmpty()) + return; + if (matchesParticleId(debugParticleId, pid)) + debugParticleId = null; + else + debugParticleId = pid; + } + + public void setActive(boolean activate) + { + overlayActive = activate; + if (activate) + { + overlayManager.add(this); + if (!menuRegistered) { + eventBus.register(this); + menuRegistered = true; + } + } + else + { + overlayManager.remove(this); + debugParticleId = null; + setPlaceMode(false, null); + if (menuRegistered) { + eventBus.unregister(this); + menuRegistered = false; + } + } + } + + /** Enable place mode: tile outline on hover, click to place selected particle and exit. */ + public void setPlaceMode(boolean active, String particleId) + { + boolean wasActive = placeModeActive; + placeModeActive = active; + placeModeParticleId = particleId; + if (active) + { + if (!overlayActive) + setActive(true); + if (!placeModeMouseRegistered) + { + mouseManager.registerMouseListener(0, this); + placeModeMouseRegistered = true; + } + } + else + { + if (placeModeMouseRegistered) + { + mouseManager.unregisterMouseListener(this); + placeModeMouseRegistered = false; + } + placeModeParticleId = null; + } + if (wasActive != placeModeActive && onPlaceModeChanged != null) + onPlaceModeChanged.run(); + } + + public void setOnPlaceModeChanged(Runnable r) + { + onPlaceModeChanged = r; + } + + public boolean isPlaceModeActive() + { + return placeModeActive; + } + + @Subscribe + public void onMenuEntryAdded(MenuEntryAdded event) + { + if (!overlayActive) + return; + + int type = event.getType(); + if (type != MenuAction.WALK.getId() && type != MenuAction.SET_HEADING.getId()) + return; + + Point mouse = client.getMouseCanvasPosition(); + if (mouse == null) + return; + + SceneContext ctx = plugin.getSceneContext(); + if (ctx == null || ctx.sceneBase == null) + return; + + float[] projectionMatrix = buildProjectionMatrix(); + int currentPlane = client.getTopLevelWorldView() != null + ? client.getTopLevelWorldView().getPlane() + : 0; + + // Check all scene emitters - project each to screen and see if mouse is over gizmo. + // This avoids tile-selection issues when emitters have large height offsets. + List emittersUnderMouse = new java.util.ArrayList<>(); + float[] pos = new float[3]; + int[] planeOut = new int[1]; + float[] point = new float[4]; + for (ParticleEmitter em : particleManager.getSceneEmitters()) + { + if (!particleManager.getEmitterSpawnPosition(ctx, em, pos, planeOut)) + continue; + if (planeOut[0] != currentPlane) + continue; + point[0] = pos[0]; + point[1] = pos[1]; + point[2] = pos[2]; + point[3] = 1f; + Mat4.projectVec(point, projectionMatrix, point); + if (point[3] <= 0) + continue; + int sx = round(point[0]); + int sy = round(point[1]); + double dist = Math.hypot(mouse.getX() - sx, mouse.getY() - sy); + if (dist <= GIZMO_HOVER_RADIUS) + emittersUnderMouse.add(em); + } + + if (emittersUnderMouse.isEmpty()) + return; + + MenuEntry parent = client.createMenuEntry(-1).setOption("Particle").setType(MenuAction.RUNELITE); + Menu submenu = parent.createSubMenu(); + + Set addedDebugIds = new HashSet<>(); + for (ParticleEmitter em : emittersUnderMouse) + { + String pid = em.getParticleId(); + if (pid == null || pid.isEmpty()) + continue; + + if (addedDebugIds.add(pid)) + { + boolean showing = isDebugForParticleId(pid); + String option = showing ? "Hide debug" : "Show debug"; + String label = emittersUnderMouse.size() > 1 ? option + " (" + pid + ")" : option; + String pidCapture = pid; + submenu.createMenuEntry(-1) + .setOption(label) + .setTarget("Tile") + .setType(MenuAction.RUNELITE) + .onClick(e -> toggleDebugForParticleId(pidCapture)); + } + } + + for (ParticleEmitter em : emittersUnderMouse) + { + String pid = em.getParticleId(); + if (pid == null) + pid = "?"; + boolean active = em.isActive(); + String option = active ? "Hide particle" : "Show particle"; + String label = emittersUnderMouse.size() > 1 ? option + " (" + pid + ")" : option; + ParticleEmitter emCapture = em; + submenu.createMenuEntry(-1) + .setOption(label) + .setTarget("Tile") + .setType(MenuAction.RUNELITE) + .onClick(e -> emCapture.active(!emCapture.isActive())); + } + + Set addedEditIds = new HashSet<>(); + for (ParticleEmitter em : emittersUnderMouse) + { + String pid = em.getParticleId(); + if (pid == null || pid.isEmpty() || !addedEditIds.add(pid)) + continue; + String option = "Edit config"; + String label = emittersUnderMouse.size() > 1 ? option + " (" + pid + ")" : option; + String pidCapture = pid; + submenu.createMenuEntry(-1) + .setOption(label) + .setTarget("Tile") + .setType(MenuAction.RUNELITE) + .onClick(e -> plugin.openParticleConfig(pidCapture)); + } + + List toRemove = new java.util.ArrayList<>(emittersUnderMouse); + String removeLabel = toRemove.size() > 1 ? "Remove (" + toRemove.size() + ")" : "Remove"; + submenu.createMenuEntry(-1) + .setOption(removeLabel) + .setTarget("Tile") + .setType(MenuAction.RUNELITE) + .onClick(e -> clientThread.invokeLater(() -> { + for (ParticleEmitter em : toRemove) + particleManager.removeEmitter(em); + })); + } + + @Override + public Dimension render(Graphics2D g) + { + SceneContext ctx = plugin.getSceneContext(); + if (ctx == null || ctx.sceneBase == null) + return null; + + int currentPlane = client.getTopLevelWorldView() != null + ? client.getTopLevelWorldView().getPlane() + : 0; + + // Debug: draw filled weather areas from emitters.json weatherAreas + for (WeatherAreaConfig wac : emitterDefinitionManager.getWeatherAreaConfigs()) { + for (var aabb : wac.getAabbs()) { + tileInfoOverlay.drawFilledWorldAabb(g, ctx, aabb, currentPlane, WEATHER_AREA_FILL); + } + } + + // Place mode: draw tile outline under mouse + if (placeModeActive) + { + Point mousePos = client.getMouseCanvasPosition(); + if (mousePos != null && mousePos.getX() >= 0 && mousePos.getY() >= 0) + { + Tile[][][] tiles = ctx.scene.getExtendedTiles(); + float mx = mousePos.getX(); + float my = mousePos.getY(); + tileSearch: + for (int z = currentPlane; z >= 0; z--) + { + for (int x = 0; x < EXTENDED_SCENE_SIZE; x++) + { + for (int y = 0; y < EXTENDED_SCENE_SIZE; y++) + { + Tile tile = tiles[z][x][y]; + if (tile == null) continue; + Polygon poly = tileInfoOverlay.getCanvasTilePoly(client, ctx, tile); + if (poly != null && poly.contains(mx, my)) + { + g.setColor(PLACE_MODE_TILE_OUTLINE); + g.setStroke(new BasicStroke(2)); + g.drawPolygon(poly); + break tileSearch; + } + } + } + } + } + } + + float[] projectionMatrix = buildProjectionMatrix(); + + Stroke thinDashed = new BasicStroke( + 1, + BasicStroke.CAP_BUTT, + BasicStroke.JOIN_BEVEL, + 0, + new float[]{3}, + 0 + ); + Stroke thinLine = new BasicStroke(1); + + float[] pos = new float[3]; + int[] planeOut = new int[1]; + float[] point = new float[4]; + + // When debug mode is on (toggled by clicking a gizmo), draw emitter and particle dots for that particle def only + if (debugParticleId != null) + { + String pid = debugParticleId; + g.setColor(EMITTER_DEBUG_COLOR); + for (ParticleEmitter em : particleManager.getSceneEmitters()) + { + if (!matchesParticleId(em.getParticleId(), pid)) + continue; + if (!particleManager.getEmitterSpawnPosition(ctx, em, pos, planeOut)) + continue; + if (planeOut[0] != currentPlane) + continue; + point[0] = pos[0]; + point[1] = pos[1]; + point[2] = pos[2]; + point[3] = 1f; + Mat4.projectVec(point, projectionMatrix, point); + if (point[3] <= 0) continue; + int sx = round(point[0]); + int sy = round(point[1]); + g.fillOval(sx - EMITTER_DOT_R, sy - EMITTER_DOT_R, EMITTER_DOT_R * 2, EMITTER_DOT_R * 2); + } + ParticleBuffer buf = particleManager.getParticleBuffer(); + g.setColor(PARTICLE_DEBUG_COLOR); + for (int i = 0; i < buf.count; i++) + { + ParticleEmitter em = buf.emitter[i]; + if (em == null || !matchesParticleId(em.getParticleId(), pid)) + continue; + if (buf.plane[i] != currentPlane) + continue; + float px = buf.posX[i]; + float py = buf.posY[i]; + float pz = buf.posZ[i]; + point[0] = px; + point[1] = py; + point[2] = pz; + point[3] = 1f; + Mat4.projectVec(point, projectionMatrix, point); + if (point[3] <= 0) continue; + int sx = round(point[0]); + int sy = round(point[1]); + int d = PARTICLE_DOT_R * 2; + g.setColor(PARTICLE_DEBUG_COLOR); + g.fillOval(sx - PARTICLE_DOT_R, sy - PARTICLE_DOT_R, d, d); + g.setColor(PARTICLE_DEBUG_OUTLINE); + g.setStroke(new BasicStroke(2)); + g.drawOval(sx - PARTICLE_DOT_R, sy - PARTICLE_DOT_R, d, d); + + // Draw trajectory line from particle to predicted end point + float[] endPos = predictEndPosition(buf, i); + if (endPos != null) { + float[] startScreen = new float[4]; + float[] endScreen = new float[4]; + startScreen[0] = px; + startScreen[1] = py; + startScreen[2] = pz; + startScreen[3] = 1f; + endScreen[0] = endPos[0]; + endScreen[1] = endPos[1]; + endScreen[2] = endPos[2]; + endScreen[3] = 1f; + Mat4.projectVec(startScreen, projectionMatrix, startScreen); + Mat4.projectVec(endScreen, projectionMatrix, endScreen); + if (startScreen[3] > 0 && endScreen[3] > 0) { + int x1 = round(startScreen[0]); + int y1 = round(startScreen[1]); + int x2 = round(endScreen[0]); + int y2 = round(endScreen[1]); + g.setColor(TRAJECTORY_LINE_COLOR); + g.setStroke(thinLine); + g.drawLine(x1, y1, x2, y2); + int endDotR = 2; + g.fillOval(x2 - endDotR, y2 - endDotR, endDotR * 2, endDotR * 2); + g.setColor(PARTICLE_DEBUG_COLOR); + } + } + } + } + + for (ParticleEmitter em : particleManager.getSceneEmitters()) + { + if (!particleManager.getEmitterSpawnPosition(ctx, em, pos, planeOut)) + continue; + + if (planeOut[0] != currentPlane) + continue; + + point[0] = pos[0]; + point[1] = pos[1]; + point[2] = pos[2]; + point[3] = 1; + + Mat4.projectVec(point, projectionMatrix, point); + + if (point[3] <= 0) + continue; + + int sx = round(point[0]); + int sy = round(point[1]); + + Color ringColor = em.isActive() ? ACTIVE_COLOR : INACTIVE_COLOR; + g.setColor(ringColor); + + g.setStroke(thinDashed); + drawRing(g, sx, sy, INNER_HANDLE_RING); + drawRing(g, sx, sy, OUTER_HANDLE_RING); + + g.setStroke(thinLine); + drawCircleOutline(g, sx, sy, INNER_DOT); + + if (debugParticleId != null && matchesParticleId(em.getParticleId(), debugParticleId)) { + drawEmitterBounds(g, em, pos, projectionMatrix, thinLine); + } + + String name = em.getParticleId(); + if (name != null && !name.isEmpty()) + { + g.setColor(Color.WHITE); + g.setFont(g.getFont().deriveFont(Font.PLAIN, 11f)); + + FontMetrics fm = g.getFontMetrics(); + int tw = fm.stringWidth(name); + + g.drawString( + name, + sx - tw / 2, + sy + OUTER_HANDLE_RING / 2 + fm.getAscent() + 2 + ); + } + } + + return null; + } + + private float[] buildProjectionMatrix() + { + float[] m = Mat4.identity(); + int vw, vh, vx, vy; + if (plugin.sceneViewport != null) { + vx = plugin.sceneViewport[0]; + vy = plugin.sceneViewport[1]; + vw = plugin.sceneViewport[2]; + vh = plugin.sceneViewport[3]; + } else { + vx = client.getViewportXOffset(); + vy = client.getViewportYOffset(); + vw = client.getViewportWidth(); + vh = client.getViewportHeight(); + } + Mat4.mul(m, Mat4.translate(vx, vy, 0)); + Mat4.mul(m, Mat4.scale(vw, vh, 1)); + Mat4.mul(m, Mat4.translate(.5f, .5f, .5f)); + Mat4.mul(m, Mat4.scale(.5f, -.5f, .5f)); + Mat4.mul(m, plugin.viewProjMatrix); + return m; + } + + private static boolean matchesParticleId(String a, String b) + { + if (a == null || a.isEmpty()) return b == null || b.isEmpty(); + if (b == null || b.isEmpty()) return false; + return a.equalsIgnoreCase(b); + } + + /** + * Predicts where a particle will end up based on current velocity and remaining lifetime. + * Uses the same movement formula as MovingParticle.tick (ignoring falloff/speed transitions). + */ + private float[] predictEndPosition(ParticleBuffer buf, int i) { + int remaining = buf.remainingTicks[i]; + if (remaining <= 0) + return null; + int vx = buf.velocityX[i]; + int vy = buf.velocityY[i]; + int vz = buf.velocityZ[i]; + int speedRef = buf.speedRef[i]; + if (speedRef <= 0) + return null; + long dxFixed = (long) vx * (long) (speedRef << 2) >> 23; + long dyFixed = (long) vy * (long) (speedRef << 2) >> 23; + long dzFixed = (long) vz * (long) (speedRef << 2) >> 23; + float dx = (float) (dxFixed * remaining) / 4096f; + float dy = (float) (dyFixed * remaining) / 4096f; + float dz = (float) (dzFixed * remaining) / 4096f; + return new float[] { + buf.posX[i] + dx, + buf.posY[i] + dy, + buf.posZ[i] + dz + }; + } + + private void drawEmitterBounds(Graphics2D g, ParticleEmitter em, float[] center, float[] proj, Stroke stroke) { + float maxRadius = (em.getSpeedMax() / 16384f) * BOUNDS_SPEED_SCALE * em.getParticleLifeMax(); + if (maxRadius < 2f) maxRadius = 8f; + + float[] p = new float[4]; + + g.setColor(BOUNDS_SPREAD_COLOR); + g.setStroke(stroke); + float syMin = em.getSpreadYawMin(); + float syMax = em.getSpreadYawMax(); + float spMin = em.getSpreadPitchMin(); + float spMax = em.getSpreadPitchMax(); + float baseYaw = em.getDirectionYaw(); + float basePitch = em.getDirectionPitch(); + float[] corners = { + baseYaw + syMin, basePitch + spMin, + baseYaw + syMin, basePitch + spMax, + baseYaw + syMax, basePitch + spMin, + baseYaw + syMax, basePitch + spMax + }; + int[][] cornerScreens = new int[4][2]; + boolean[] cornerVisible = new boolean[4]; + for (int c = 0; c < 4; c++) { + float yaw = corners[c * 2]; + float pitch = corners[c * 2 + 1]; + float cp = cos(pitch); + float dirX = sin(yaw) * cp; + float dirY = -sin(pitch); + float dirZ = -cos(yaw) * cp; + p[0] = center[0] + dirX * maxRadius; + p[1] = center[1] + dirY * maxRadius; + p[2] = center[2] + dirZ * maxRadius; + p[3] = 1f; + Mat4.projectVec(p, proj, p); + if (p[3] > 0) { + cornerScreens[c][0] = round(p[0]); + cornerScreens[c][1] = round(p[1]); + cornerVisible[c] = true; + } else { + cornerVisible[c] = false; + } + } + int[] centerScreen = null; + p[0] = center[0]; + p[1] = center[1]; + p[2] = center[2]; + p[3] = 1f; + Mat4.projectVec(p, proj, p); + if (p[3] > 0) + centerScreen = new int[] { round(p[0]), round(p[1]) }; + if (centerScreen != null) { + for (int c = 0; c < 4; c++) { + if (cornerVisible[c]) + g.drawLine(centerScreen[0], centerScreen[1], cornerScreens[c][0], cornerScreens[c][1]); + } + for (int c = 0; c < 4; c++) { + int next = (c + 1) % 4; + if (cornerVisible[c] && cornerVisible[next]) + g.drawLine(cornerScreens[c][0], cornerScreens[c][1], cornerScreens[next][0], cornerScreens[next][1]); + } + } + } + + private void drawRing(Graphics2D g, int centerX, int centerY, int diameter) + { + int d = (int) (Math.ceil(diameter / 2.0) * 2 - 1); + int r = (int) Math.ceil(d / 2.0); + g.drawOval(centerX - r, centerY - r, d, d); + } + + private void drawCircleOutline(Graphics2D g, int centerX, int centerY, int diameter) + { + int r = (int) Math.ceil(diameter / 2.0); + int s = diameter - 1; + g.drawRoundRect(centerX - r, centerY - r, s, s, s - 1, s - 1); + } + + @Override + public MouseEvent mouseClicked(MouseEvent event) + { + if (!placeModeActive || placeModeParticleId == null || event.getButton() != MouseEvent.BUTTON1) + return event; + + event.consume(); + + SceneContext ctx = plugin.getSceneContext(); + if (ctx == null || ctx.sceneBase == null) + return event; + + Point mousePos = client.getMouseCanvasPosition(); + if (mousePos == null || mousePos.getX() < 0 || mousePos.getY() < 0) + return event; + + int currentPlane = client.getTopLevelWorldView() != null + ? client.getTopLevelWorldView().getPlane() + : 0; + + Tile[][][] tiles = ctx.scene.getExtendedTiles(); + float mx = mousePos.getX(); + float my = mousePos.getY(); + for (int z = currentPlane; z >= 0; z--) + { + for (int x = 0; x < EXTENDED_SCENE_SIZE; x++) + { + for (int y = 0; y < EXTENDED_SCENE_SIZE; y++) + { + Tile tile = tiles[z][x][y]; + if (tile == null) continue; + Polygon poly = tileInfoOverlay.getCanvasTilePoly(client, ctx, tile); + if (poly != null && poly.contains(mx, my)) + { + int[] worldPos = ctx.extendedSceneToWorld(x, y, tile.getRenderLevel()); + WorldPoint wp = new WorldPoint(worldPos[0], worldPos[1], worldPos[2]); + String pid = placeModeParticleId; + clientThread.invokeLater(() -> particleManager.spawnEmitterFromDefinition(pid, wp)); + return event; + } + } + } + } + return event; + } + + @Override + public MouseEvent mousePressed(MouseEvent event) + { + if (placeModeActive && event.getButton() == MouseEvent.BUTTON1) + event.consume(); + return event; + } + + @Override + public MouseEvent mouseReleased(MouseEvent event) + { + if (placeModeActive && event.getButton() == MouseEvent.BUTTON1) + event.consume(); + return event; + } + + @Override + public MouseEvent mouseEntered(MouseEvent event) { return event; } + + @Override + public MouseEvent mouseExited(MouseEvent event) { return event; } + + @Override + public MouseEvent mouseMoved(MouseEvent event) { return event; } + + @Override + public MouseEvent mouseDragged(MouseEvent event) { return event; } +} \ No newline at end of file diff --git a/src/main/java/rs117/hd/scene/particles/debug/ParticleSidebarPanel.java b/src/main/java/rs117/hd/scene/particles/debug/ParticleSidebarPanel.java new file mode 100644 index 0000000000..43b7404426 --- /dev/null +++ b/src/main/java/rs117/hd/scene/particles/debug/ParticleSidebarPanel.java @@ -0,0 +1,2488 @@ +/* + * Copyright (c) 2025, Mark7625. + * Sidebar panel with Emitters, Particles, and Debug tabs. Debug has overlay toggles and test particles. + */ +package rs117.hd.scene.particles.debug; + +import java.awt.BorderLayout; +import java.awt.CardLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.Font; +import java.awt.Insets; +import java.awt.Rectangle; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import javax.annotation.Nullable; +import javax.inject.Singleton; +import javax.swing.AbstractSpinnerModel; +import javax.swing.DefaultComboBoxModel; +import javax.swing.DefaultListCellRenderer; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTabbedPane; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.Scrollable; +import javax.swing.BoxLayout; +import javax.swing.JTextField; +import javax.swing.ImageIcon; +import javax.swing.SwingUtilities; +import javax.swing.event.ChangeListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; +import javax.swing.border.TitledBorder; +import javax.swing.border.MatteBorder; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.PluginPanel; +import net.runelite.client.ui.components.colorpicker.ColorPickerManager; +import net.runelite.client.ui.components.colorpicker.RuneliteColorPicker; +import net.runelite.client.ui.components.materialtabs.MaterialTab; +import net.runelite.client.ui.components.materialtabs.MaterialTabGroup; +import rs117.hd.HdPlugin; +import rs117.hd.scene.particles.ParticleManager; +import rs117.hd.scene.particles.definition.ParticleDefinition; +import rs117.hd.scene.particles.core.ParticleTextureLoader; +import rs117.hd.utils.ResourcePath; + +@Slf4j +@Singleton +public class ParticleSidebarPanel extends PluginPanel { + + private static final Color ACTIVE_BUTTON_GREEN = new Color(76, 175, 80); + private static final Color ACTIVE_BUTTON_BORDER = new Color(56, 142, 60); + /** Yaw units used by the game (0–2048, full circle). */ + private static final int YAW_UNITS = 2048; + /** Game ticks per second (particle code uses delay/64 for lifetime in seconds). */ + private static final int TICKS_PER_SECOND = 64; + + /** Convert backend ticks to seconds (delay/64). */ + private static double emissionTicksToSeconds(int ticks) { + return ticks < 0 ? 0 : ticks / (double) TICKS_PER_SECOND; + } + + /** Convert seconds to backend ticks. */ + private static int emissionSecondsToTicks(double seconds) { + return (int) Math.round(seconds * TICKS_PER_SECOND); + } + + /** Convert ticks to hours, minutes, seconds (for display). */ + private static int[] emissionTicksToHMS(int ticks) { + if (ticks < 0) return new int[]{ 0, 0, 0 }; + int totalSec = (int) Math.round(emissionTicksToSeconds(ticks)); + int h = totalSec / 3600; + int m = (totalSec % 3600) / 60; + int s = totalSec % 60; + return new int[]{ h, m, s }; + } + + /** Convert hours, minutes, seconds to ticks. */ + private static int emissionHMSToTicks(int h, int m, int s) { + long totalSec = (long) h * 3600 + (long) m * 60 + (long) s; + return emissionSecondsToTicks(totalSec); + } + + /** Format ticks as "HH:MM:SS" for display. */ + private static String emissionTicksToTimeString(int ticks) { + int[] hms = emissionTicksToHMS(ticks); + return String.format("%02d:%02d:%02d", hms[0], hms[1], hms[2]); + } + + /** Parse "H:MM:SS" or "HH:MM:SS" to ticks; invalid input returns 0. */ + private static int emissionTimeStringToTicks(String s) { + if (s == null || s.isEmpty()) return 0; + String[] parts = s.trim().split(":"); + if (parts.length != 3) return 0; + try { + int h = Math.max(0, Math.min(99, Integer.parseInt(parts[0].trim()))); + int m = Math.max(0, Math.min(59, Integer.parseInt(parts[1].trim()))); + int sec = Math.max(0, Math.min(59, Integer.parseInt(parts[2].trim()))); + return emissionHMSToTicks(h, m, sec); + } catch (NumberFormatException e) { + return 0; + } + } + + // --- Min/Max delay: minutes, seconds, milliseconds (no hours) --- + + /** Convert ticks to minutes, seconds, milliseconds. */ + private static int[] emissionTicksToMinSecMs(int ticks) { + if (ticks < 0) return new int[]{ 0, 0, 0 }; + double totalSec = emissionTicksToSeconds(ticks); + int min = (int) (totalSec / 60); + int sec = (int) (totalSec % 60); + int ms = (int) Math.round((totalSec - min * 60 - sec) * 1000); + if (ms >= 1000) { ms = 0; sec++; if (sec >= 60) { sec = 0; min++; } } + return new int[]{ min, sec, ms }; + } + + /** Convert minutes, seconds, milliseconds to ticks. */ + private static int emissionMinSecMsToTicks(int min, int sec, int ms) { + double totalSec = min * 60 + sec + ms / 1000.0; + return emissionSecondsToTicks(totalSec); + } + + /** Format delay as "MM:SS.mmm" for display. */ + private static String emissionTicksToDelayString(int ticks) { + int[] msm = emissionTicksToMinSecMs(ticks); + return String.format("%02d:%02d.%03d", msm[0], msm[1], msm[2]); + } + + /** Parse "M:SS.mmm" or "MM:SS.mmm" to ticks; invalid input returns 0. */ + private static int emissionDelayStringToTicks(String s) { + if (s == null || s.isEmpty()) return 0; + String[] colonParts = s.trim().split(":"); + if (colonParts.length != 2) return 0; + String secMs = colonParts[1].trim(); + int dot = secMs.indexOf('.'); + int sec; + int ms = 0; + try { + if (dot < 0) { + sec = Math.max(0, Math.min(59, Integer.parseInt(secMs))); + } else { + sec = Math.max(0, Math.min(59, Integer.parseInt(secMs.substring(0, dot).trim()))); + String m = secMs.substring(dot + 1).replaceAll("\\D", ""); + if (m.length() > 3) m = m.substring(0, 3); + if (!m.isEmpty()) { + ms = Math.max(0, Math.min(999, Integer.parseInt(m))); + if (m.length() == 1) ms *= 100; + else if (m.length() == 2) ms *= 10; + } + } + int min = Math.max(0, Math.min(99, Integer.parseInt(colonParts[0].trim()))); + return emissionMinSecMsToTicks(min, sec, ms); + } catch (NumberFormatException e) { + return 0; + } + } + + /** Spinner model for delay: minutes, seconds, ms (MM:SS.mmm); step 1 second. */ + private static final class DelaySpinnerModel extends AbstractSpinnerModel { + private static final int MAX_TICKS = 99 * 60 * TICKS_PER_SECOND; + private int ticks; + + DelaySpinnerModel(int initialTicks) { + this.ticks = Math.max(0, Math.min(MAX_TICKS, initialTicks)); + } + + int getTicks() { + return ticks; + } + + void setTicks(int t) { + t = Math.max(0, Math.min(MAX_TICKS, t)); + if (t != ticks) { + ticks = t; + fireStateChanged(); + } + } + + @Override + public Object getValue() { + return emissionTicksToDelayString(ticks); + } + + @Override + public void setValue(Object value) { + if (value instanceof String) { + setTicks(emissionDelayStringToTicks((String) value)); + } else if (value instanceof Number) { + setTicks(((Number) value).intValue()); + } + } + + @Override + public Object getNextValue() { + return emissionTicksToDelayString(Math.min(MAX_TICKS, ticks + TICKS_PER_SECOND)); + } + + @Override + public Object getPreviousValue() { + return emissionTicksToDelayString(Math.max(0, ticks - TICKS_PER_SECOND)); + } + } + + /** Spinner model that holds time as ticks and displays/edits as HH:MM:SS; step is 1 second (64 ticks). When allowNegative, -1 is allowed and displays as -1. */ + private static final class TimeSpinnerModel extends AbstractSpinnerModel { + private static final int MAX_TICKS = 99 * 3600 * TICKS_PER_SECOND; + private int ticks; + private final boolean allowNegative; + + TimeSpinnerModel(int initialTicks) { + this(initialTicks, false); + } + + TimeSpinnerModel(int initialTicks, boolean allowNegative) { + this.allowNegative = allowNegative; + this.ticks = allowNegative && initialTicks < 0 + ? -1 + : Math.max(0, Math.min(MAX_TICKS, initialTicks)); + } + + int getTicks() { + return ticks; + } + + void setTicks(int t) { + if (allowNegative && t < 0) { + t = -1; + } else { + t = Math.max(0, Math.min(MAX_TICKS, t)); + } + if (t != ticks) { + ticks = t; + fireStateChanged(); + } + } + + @Override + public Object getValue() { + if (allowNegative && ticks < 0) return -1; + return emissionTicksToTimeString(ticks); + } + + @Override + public void setValue(Object value) { + if (value instanceof String) { + String s = ((String) value).trim(); + if (allowNegative && ("-1".equals(s) || "−1".equals(s))) { + setTicks(-1); + return; + } + setTicks(emissionTimeStringToTicks((String) value)); + } else if (value instanceof Number) { + setTicks(((Number) value).intValue()); + } + } + + @Override + public Object getNextValue() { + if (allowNegative && ticks < 0) return emissionTicksToTimeString(0); + return emissionTicksToTimeString(Math.min(MAX_TICKS, ticks + TICKS_PER_SECOND)); + } + + @Override + public Object getPreviousValue() { + if (allowNegative && ticks <= 0) return allowNegative ? -1 : emissionTicksToTimeString(0); + return emissionTicksToTimeString(Math.max(0, ticks - TICKS_PER_SECOND)); + } + } + + /** Make spinner text editable and commit when focus is lost so typing is applied. */ + private static void makeSpinnerEditable(JSpinner spinner) { + if (spinner.getEditor() instanceof JSpinner.DefaultEditor) { + JTextField tf = ((JSpinner.DefaultEditor) spinner.getEditor()).getTextField(); + tf.setEditable(true); + tf.addFocusListener(new java.awt.event.FocusAdapter() { + @Override + public void focusLost(java.awt.event.FocusEvent e) { + try { + spinner.commitEdit(); + } catch (Exception ignored) { + // ignore parse errors on commit + } + } + }); + } + } + + /** Create a delay spinner (min, sec, ms) showing 00:00.000; format in tooltip. */ + private static JPanel createDelaySpinnerWithLabel(Dimension size, JSpinner[] outSpinner) { + JPanel p = new JPanel(new FlowLayout(FlowLayout.LEADING, 0, 0)); + p.setOpaque(false); + DelaySpinnerModel model = new DelaySpinnerModel(0); + JSpinner spinner = new JSpinner(model); + spinner.setPreferredSize(size); + spinner.setMinimumSize(size); + spinner.setToolTipText("Minutes:Seconds.Milliseconds (e.g. 01:30.500)"); + makeSpinnerEditable(spinner); + p.add(spinner); + outSpinner[0] = spinner; + return p; + } + + /** Create a time spinner showing 00:00:00; format in tooltip. Use allowNegative true for cycle/threshold so -1 disables. */ + private static JPanel createTimeSpinnerWithLabel(Dimension size, JSpinner[] outSpinner) { + return createTimeSpinnerWithLabel(size, outSpinner, false); + } + + private static JPanel createTimeSpinnerWithLabel(Dimension size, JSpinner[] outSpinner, boolean allowNegative) { + JPanel p = new JPanel(new FlowLayout(FlowLayout.LEADING, 0, 0)); + p.setOpaque(false); + TimeSpinnerModel model = new TimeSpinnerModel(0, allowNegative); + JSpinner spinner = new JSpinner(model); + spinner.setPreferredSize(size); + spinner.setMinimumSize(size); + spinner.setToolTipText(allowNegative + ? "Hours:Minutes:Seconds or -1 to disable (e.g. 00:01:30 or -1)" + : "Hours:Minutes:Seconds (e.g. 00:01:30)"); + makeSpinnerEditable(spinner); + p.add(spinner); + outSpinner[0] = spinner; + return p; + } + + /** + * Map stored game yaw to UI compass yaw. + * Game yaw uses: + * 0 = south, 512 = east, 1024 = north, 1536 = west + * Gizmo yaw uses: + * 0 = north, 512 = east, 1024 = south, 1536 = west + * This transform lines up all four cardinals: N/E/S/W. + */ + private static int gameToUiYaw(int yaw) { + return (YAW_UNITS / 2 - yaw + YAW_UNITS) % YAW_UNITS; + } + + /** Inverse of gameToUiYaw (same formula). */ + private static int uiToGameYaw(int yaw) { + return (YAW_UNITS / 2 - yaw + YAW_UNITS) % YAW_UNITS; + } + + private final HdPlugin plugin; + private final ParticleManager particleManager; + private final ParticleGizmoOverlay particleGizmoOverlay; + private final Client client; + private final ColorPickerManager colorPickerManager; + private final ClientThread clientThread; + + private boolean gizmoOverlayActive; + + /** Dropdown in Particles tab; used to refresh and load default on activate. */ + private JComboBox particleDropdownRef; + private JButton placeBtnRef; + private MaterialTabGroup tabGroupRef; + private MaterialTab particlesTabRef; + + public ParticleSidebarPanel( + HdPlugin plugin, + ParticleManager particleManager, + ClientThread clientThread, + Client client, + ColorPickerManager colorPickerManager, + ParticleGizmoOverlay particleGizmoOverlay + ) { + super(false); + this.plugin = plugin; + this.particleManager = particleManager; + this.clientThread = clientThread; + this.client = client; + this.colorPickerManager = colorPickerManager; + this.particleGizmoOverlay = particleGizmoOverlay; + + setLayout(new BorderLayout()); + + JPanel display = new JPanel(); + display.setLayout(new BorderLayout()); + display.setLayout(new CardLayout()); + display.setBorder(new EmptyBorder(0, 0, 0, 0)); + + MaterialTabGroup tabGroup = new MaterialTabGroup(display); + tabGroup.setLayout(new GridLayout(1, 0, 8, 4)); + tabGroup.setBorder(new EmptyBorder(0, 10, 10, 10)); + + MaterialTab emittersTab = new MaterialTab("Emitters", tabGroup, buildEmittersPanel()); + MaterialTab particlesTab = new MaterialTab("Particles", tabGroup, buildParticlesPanel()); + MaterialTab debugTab = new MaterialTab("Debug", tabGroup, buildDebugPanel()); + + tabGroupRef = tabGroup; + particlesTabRef = particlesTab; + + tabGroup.setBorder(new EmptyBorder(5, 0, 0, 0)); + tabGroup.addTab(emittersTab); + tabGroup.addTab(particlesTab); + tabGroup.addTab(debugTab); + tabGroup.select(debugTab); + + add(tabGroup, BorderLayout.NORTH); + add(display, BorderLayout.CENTER); + } + + @Override + public void onActivate() { + refreshParticleDropdownAndLoadDefault(); + } + + /** Switch to Particles tab and select the given particle definition. Used by right-click "Edit config". */ + public void openToParticleConfig(String particleId) { + if (tabGroupRef != null && particlesTabRef != null) { + tabGroupRef.select(particlesTabRef); + } + if (particleDropdownRef != null && particleId != null) { + particleDropdownRef.setSelectedItem(particleId); + } + } + + /** Refresh dropdown from definitions and select default particle so General section loads its values. */ + private void refreshParticleDropdownAndLoadDefault() { + if (particleDropdownRef == null) return; + java.util.List orderedIds = particleManager.getDefinitionIdsOrdered(); + // Same ordering as initial build: text IDs first, then purely numeric IDs + java.util.List textIds = new java.util.ArrayList<>(); + java.util.List numericIds = new java.util.ArrayList<>(); + for (String id : orderedIds) { + if (id != null && id.matches("\\d+")) { + numericIds.add(id); + } else { + textIds.add(id); + } + } + textIds.addAll(numericIds); + particleDropdownRef.setModel(new DefaultComboBoxModel<>(textIds.toArray(new String[0]))); + selectDefaultParticle(particleDropdownRef); + } + + private static final String DEFAULT_PARTICLE_ID = "7"; + + private static void selectDefaultParticle(JComboBox dropdown) { + for (int i = 0; i < dropdown.getItemCount(); i++) { + if (DEFAULT_PARTICLE_ID.equals(dropdown.getItemAt(i))) { + dropdown.setSelectedIndex(i); + return; + } + } + if (dropdown.getItemCount() > 0) + dropdown.setSelectedIndex(0); + } + + private JPanel buildEmittersPanel() { + JPanel p = new JPanel(); + p.setBackground(ColorScheme.DARK_GRAY_COLOR); + p.add(new JLabel("Emitters (coming soon)")); + return p; + } + + private JPanel buildParticlesPanel() { + JPanel p = new JPanel(); + p.setLayout(new BorderLayout()); + p.setBackground(ColorScheme.DARK_GRAY_COLOR); + p.setBorder(new EmptyBorder(5, 5, 5, 5)); + + // Top: dropdown + Load + New in one row (default to test emitter "7" from emitters.json) + java.util.List orderedIds = particleManager.getDefinitionIdsOrdered(); + // Show human-labeled IDs first (not just numeric), while preserving original relative order + java.util.List textIds = new java.util.ArrayList<>(); + java.util.List numericIds = new java.util.ArrayList<>(); + for (String id : orderedIds) { + if (id != null && id.matches("\\d+")) { + numericIds.add(id); + } else { + textIds.add(id); + } + } + textIds.addAll(numericIds); + String[] particleIds = textIds.toArray(new String[0]); + JComboBox particleDropdown = new JComboBox<>(particleIds); + particleDropdownRef = particleDropdown; + selectDefaultParticle(particleDropdown); + + JButton loadBtn = new JButton("Load"); + styleButton(loadBtn); + + JButton newBtn = new JButton("New"); + styleButton(newBtn); + + JPanel topBar = new JPanel(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + c.gridy = 0; + c.weighty = 0; + c.fill = GridBagConstraints.HORIZONTAL; + c.insets = new Insets(0, 0, 0, 4); + c.gridx = 0; + c.weightx = 0.95; + topBar.add(particleDropdown, c); + c.gridx = 1; + c.weightx = 0.025; + topBar.add(loadBtn, c); + c.gridx = 2; + c.weightx = 0.025; + c.insets = new Insets(0, 0, 0, 0); + topBar.add(newBtn, c); + p.add(topBar, BorderLayout.NORTH); + + // Center: scrollable content — one section per particle def type + // ScrollablePanel tracks viewport width so content does not expand scroll horizontally + JPanel scrollContent = new ScrollablePanel(); + scrollContent.setLayout(new BoxLayout(scrollContent, BoxLayout.Y_AXIS)); + scrollContent.setBackground(ColorScheme.DARK_GRAY_COLOR); + // Texture section first so texture / flipbook settings sit above General + scrollContent.add(buildTextureSection(particleDropdown)); + scrollContent.add(buildGeneralSection(particleDropdown)); + scrollContent.add(buildSpreadSection(particleDropdown)); + scrollContent.add(buildSpeedSection(particleDropdown)); + scrollContent.add(buildScaleSection(particleDropdown)); + scrollContent.add(buildColoursSection(particleDropdown)); + scrollContent.add(buildEmissionSection(particleDropdown)); + scrollContent.add(buildPhysicsSection(particleDropdown)); + JScrollPane scroll = new JScrollPane(scrollContent); + scroll.setBorder(new EmptyBorder(5, 0, 5, 5)); + scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + // Prefer height 0 so BorderLayout gives the scroll pane all remaining vertical space + scroll.setPreferredSize(new Dimension(0, 0)); + p.add(scroll, BorderLayout.CENTER); + + // Footer: Place, Export — full width, equal size + JPanel footer = new JPanel(new GridLayout(1, 2, 4, 0)); + + JButton placeBtn = new JButton("Place"); + placeBtnRef = placeBtn; + styleButton(placeBtn); + particleGizmoOverlay.setOnPlaceModeChanged(() -> + SwingUtilities.invokeLater(() -> setButtonActive(placeBtnRef, particleGizmoOverlay.isPlaceModeActive()))); + placeBtn.addActionListener(e -> { + String pid = particleDropdownRef != null ? (String) particleDropdownRef.getSelectedItem() : null; + if (pid == null || pid.isEmpty()) return; + boolean entering = !particleGizmoOverlay.isPlaceModeActive(); + particleGizmoOverlay.setPlaceMode(entering, pid); + setButtonActive(placeBtn, entering); + }); + footer.add(placeBtn); + + JButton exportBtn = new JButton("Export"); + styleButton(exportBtn); + footer.add(exportBtn); + + p.add(footer, BorderLayout.SOUTH); + return p; + } + + private static final Insets ROW_INSETS_LABEL = new Insets(2, 0, 2, 8); + private static final Insets ROW_INSETS_CONTROL = new Insets(2, 0, 2, 0); + + /** Label left, control right; control column has weightx=1 so it stays right-aligned within section width. */ + private static GridBagConstraints labelConstraints(int row) { + GridBagConstraints c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = row; + c.anchor = GridBagConstraints.WEST; + c.fill = GridBagConstraints.NONE; + c.weightx = 0; + c.insets = ROW_INSETS_LABEL; + return c; + } + + private static GridBagConstraints controlConstraints(int row) { + GridBagConstraints c = new GridBagConstraints(); + c.gridx = 1; + c.gridy = row; + c.anchor = GridBagConstraints.EAST; + c.fill = GridBagConstraints.NONE; + c.weightx = 1; + c.insets = ROW_INSETS_CONTROL; + return c; + } + + /** Control column left-aligned (so wide controls like time spinners stay visible). */ + private static GridBagConstraints controlConstraintsWest(int row) { + GridBagConstraints c = new GridBagConstraints(); + c.gridx = 1; + c.gridy = row; + c.anchor = GridBagConstraints.WEST; + c.fill = GridBagConstraints.NONE; + c.weightx = 1; + c.insets = ROW_INSETS_CONTROL; + return c; + } + + /** Control column left-aligned, no horizontal grow (keeps section compact, doesn't push other content). */ + private static GridBagConstraints controlConstraintsWestNoGrow(int row) { + GridBagConstraints c = new GridBagConstraints(); + c.gridx = 1; + c.gridy = row; + c.anchor = GridBagConstraints.WEST; + c.fill = GridBagConstraints.NONE; + c.weightx = 0; + c.insets = ROW_INSETS_CONTROL; + return c; + } + + /** Min/Max delay only: minimal gap so spinner sits right next to label and doesn't overflow. */ + private static final Insets DELAY_ROW_INSETS_LABEL = new Insets(2, 0, 2, 0); + private static GridBagConstraints delayLabelConstraints(int row) { + GridBagConstraints c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = row; + c.anchor = GridBagConstraints.WEST; + c.fill = GridBagConstraints.NONE; + c.weightx = 0; + c.insets = DELAY_ROW_INSETS_LABEL; + return c; + } + private static GridBagConstraints delayControlConstraints(int row) { + GridBagConstraints c = new GridBagConstraints(); + c.gridx = 1; + c.gridy = row; + c.anchor = GridBagConstraints.WEST; + c.fill = GridBagConstraints.NONE; + c.weightx = 0; + c.insets = new Insets(2, 0, 2, 0); + return c; + } + + private boolean generalLoadingFromDefinition; + private boolean textureLoadingFromDefinition; + private boolean spreadLoadingFromDefinition; + private boolean speedLoadingFromDefinition; + private boolean scaleLoadingFromDefinition; + private boolean coloursLoadingFromDefinition; + private boolean emissionLoadingFromDefinition; + private boolean physicsLoadingFromDefinition; + private DirectionGizmoPanel directionGizmo; + + private JPanel buildGeneralSection(JComboBox particleDropdown) { + generalLoadingFromDefinition = false; + return buildGeneralSectionInner(particleDropdown); + } + + private JPanel buildSpreadSection(JComboBox particleDropdown) { + spreadLoadingFromDefinition = false; + + JPanel section = buildTitledSection("Spread"); + JPanel content = new JPanel(new GridBagLayout()); + // Match margins used by Emission/Physics so labels and controls line up + content.setBorder(new EmptyBorder(6, 6, 6, 10)); + + int row = 0; + + JLabel yawMinLabel = new JLabel("Yaw Min"); + yawMinLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(yawMinLabel, labelConstraints(row)); + JSpinner yawMinSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 2048, 10)); + content.add(yawMinSpinner, controlConstraints(row)); + row++; + + JLabel yawMaxLabel = new JLabel("Yaw Max"); + yawMaxLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(yawMaxLabel, labelConstraints(row)); + JSpinner yawMaxSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 2048, 10)); + content.add(yawMaxSpinner, controlConstraints(row)); + row++; + + JLabel pitchMinLabel = new JLabel("Pitch Min"); + pitchMinLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(pitchMinLabel, labelConstraints(row)); + JSpinner pitchMinSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 1024, 10)); + content.add(pitchMinSpinner, controlConstraints(row)); + row++; + + JLabel pitchMaxLabel = new JLabel("Pitch Max"); + pitchMaxLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(pitchMaxLabel, labelConstraints(row)); + JSpinner pitchMaxSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 1024, 10)); + content.add(pitchMaxSpinner, controlConstraints(row)); + + ItemListener loadFromDef = e -> { + if (e.getStateChange() != ItemEvent.SELECTED) return; + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + spreadLoadingFromDefinition = true; + try { + ParticleDefinition.Spread s = def.spread; + // Convert stored game yaw to UI yaw so 0 = North, 512 = East, etc. + int yawMinGame = (int) s.yawMin; + int yawMaxGame = (int) s.yawMax; + int yawMinUi = Math.max(0, Math.min(2048, gameToUiYaw(yawMinGame))); + int yawMaxUi = Math.max(0, Math.min(2048, gameToUiYaw(yawMaxGame))); + int pitchMin = Math.max(0, Math.min(1024, (int) s.pitchMin)); + int pitchMax = Math.max(0, Math.min(1024, (int) s.pitchMax)); + yawMinSpinner.setValue(yawMinUi); + yawMaxSpinner.setValue(yawMaxUi); + pitchMinSpinner.setValue(pitchMin); + pitchMaxSpinner.setValue(pitchMax); + } finally { + spreadLoadingFromDefinition = false; + } + // Refresh gizmo spread immediately when switching particles + if (directionGizmo != null) { + directionGizmo.repaint(); + } + }; + particleDropdown.addItemListener(loadFromDef); + + Runnable pushToDef = () -> { + if (spreadLoadingFromDefinition) return; + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + // Convert UI yaw back to game yaw before storing on the definition + int yawMinUi = ((Number) yawMinSpinner.getValue()).intValue(); + int yawMaxUi = ((Number) yawMaxSpinner.getValue()).intValue(); + def.spread.yawMin = uiToGameYaw(yawMinUi); + def.spread.yawMax = uiToGameYaw(yawMaxUi); + def.spread.pitchMin = ((Number) pitchMinSpinner.getValue()).floatValue(); + def.spread.pitchMax = ((Number) pitchMaxSpinner.getValue()).floatValue(); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(id)); + if (directionGizmo != null) { + directionGizmo.repaint(); + } + }; + ChangeListener spreadChange = e -> pushToDef.run(); + yawMinSpinner.addChangeListener(spreadChange); + yawMaxSpinner.addChangeListener(spreadChange); + pitchMinSpinner.addChangeListener(spreadChange); + pitchMaxSpinner.addChangeListener(spreadChange); + + // Initial load for default selection + loadFromDef.itemStateChanged(new ItemEvent(particleDropdown, ItemEvent.ITEM_STATE_CHANGED, particleDropdown.getSelectedItem(), ItemEvent.SELECTED)); + + // Hook spread values into gizmo so it can visualize them + if (directionGizmo != null) { + directionGizmo.setSpreadSupplier(() -> { + int yawMin = ((Number) yawMinSpinner.getValue()).intValue(); + int yawMax = ((Number) yawMaxSpinner.getValue()).intValue(); + int pitchMin = ((Number) pitchMinSpinner.getValue()).intValue(); + int pitchMax = ((Number) pitchMaxSpinner.getValue()).intValue(); + return new DirectionGizmoPanel.SpreadValues(yawMin, yawMax, pitchMin, pitchMax); + }); + } + + section.add(content, BorderLayout.CENTER); + return section; + } + + private JPanel buildSpeedSection(JComboBox particleDropdown) { + speedLoadingFromDefinition = false; + + JPanel section = buildTitledSection("Speed"); + JPanel content = new JPanel(new GridBagLayout()); + content.setBorder(new EmptyBorder(6, 6, 6, 10)); + + int row = 0; + Dimension spinnerSize = new Dimension(72, 24); + Dimension speedDelaySize = new Dimension(96, 24); // Same as min/max delay + + // Speed values displayed at 1/100 scale, integer only (e.g. 786 = 78643 internal) + final int SPEED_UI_SCALE = 100; + + JLabel minSpeedLabel = new JLabel("Min speed"); + minSpeedLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + minSpeedLabel.setToolTipText("Lower bound of random initial speed when the particle spawns. Displayed at 1/100 scale (integer)."); + content.add(minSpeedLabel, labelConstraints(row)); + JSpinner minSpeedSpinner = new JSpinner(new SpinnerNumberModel(2, 0, 999999, 1)); + minSpeedSpinner.setPreferredSize(speedDelaySize); + minSpeedSpinner.setMinimumSize(speedDelaySize); + content.add(minSpeedSpinner, controlConstraints(row)); + row++; + + JLabel maxSpeedLabel = new JLabel("Max speed"); + maxSpeedLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + maxSpeedLabel.setToolTipText("Upper bound of random initial speed when the particle spawns. Displayed at 1/100 scale (integer)."); + content.add(maxSpeedLabel, labelConstraints(row)); + JSpinner maxSpeedSpinner = new JSpinner(new SpinnerNumberModel(6, 0, 999999, 1)); + maxSpeedSpinner.setPreferredSize(speedDelaySize); + maxSpeedSpinner.setMinimumSize(speedDelaySize); + content.add(maxSpeedSpinner, controlConstraints(row)); + row++; + + // Target speed: no target (-1) shows Enable button; otherwise spinner + Disable (like cycle duration) + JLabel targetSpeedLabel = new JLabel("Target speed"); + targetSpeedLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + targetSpeedLabel.setToolTipText("Speed to transition toward over the particle's lifetime. Displayed at 1/100 scale (integer). Enable to set a target; Disable for no target."); + content.add(targetSpeedLabel, labelConstraints(row)); + JPanel targetSpeedCardPanel = new JPanel(new CardLayout()); + targetSpeedCardPanel.setOpaque(false); + JButton targetSpeedEnableBtn = new JButton("Enable"); + targetSpeedEnableBtn.setToolTipText("Enable target speed"); + targetSpeedCardPanel.add(targetSpeedEnableBtn, "enable"); + JPanel targetSpeedSpinnerRow = new JPanel(new FlowLayout(FlowLayout.TRAILING, 0, 0)); + targetSpeedSpinnerRow.setOpaque(false); + JSpinner targetSpeedSpinner = new JSpinner(new SpinnerNumberModel(6, -1, 999999, 1)); + targetSpeedSpinner.setPreferredSize(spinnerSize); + targetSpeedSpinner.setMinimumSize(spinnerSize); + targetSpeedSpinnerRow.add(targetSpeedSpinner); + JButton targetSpeedDisableBtn = new JButton("Disable"); + targetSpeedDisableBtn.setToolTipText("No target speed (particle speed stays constant)"); + targetSpeedSpinnerRow.add(targetSpeedDisableBtn); + targetSpeedCardPanel.add(targetSpeedSpinnerRow, "spinner"); + content.add(targetSpeedCardPanel, controlConstraints(row)); + final boolean[] targetSpeedEnabled = { false }; + row++; + + JLabel speedTransitionLabel = new JLabel("Speed transition %"); + speedTransitionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + speedTransitionLabel.setToolTipText("Percentage of the particle's lifetime over which speed changes from initial to target. 100 = transition over full lifetime."); + content.add(speedTransitionLabel, labelConstraints(row)); + JSpinner speedTransitionSpinner = new JSpinner(new SpinnerNumberModel(100, 0, 100, 1)); + speedTransitionSpinner.setPreferredSize(spinnerSize); + speedTransitionSpinner.setMinimumSize(spinnerSize); + content.add(speedTransitionSpinner, controlConstraints(row)); + + targetSpeedEnableBtn.addActionListener(e -> { + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + def.speed.targetSpeed = 60f; + targetSpeedSpinner.setValue(6); + targetSpeedEnabled[0] = true; + ((CardLayout) targetSpeedCardPanel.getLayout()).show(targetSpeedCardPanel, "spinner"); + def.postDecode(); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(id)); + }); + targetSpeedDisableBtn.addActionListener(e -> { + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + def.speed.targetSpeed = -1f; + targetSpeedEnabled[0] = false; + ((CardLayout) targetSpeedCardPanel.getLayout()).show(targetSpeedCardPanel, "enable"); + def.postDecode(); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(id)); + }); + + ItemListener loadFromDef = e -> { + if (e.getStateChange() != ItemEvent.SELECTED) return; + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + speedLoadingFromDefinition = true; + try { + ParticleDefinition.Speed s = def.speed; + minSpeedSpinner.setValue((int) Math.round(s.getMinSpeed() / SPEED_UI_SCALE)); + maxSpeedSpinner.setValue((int) Math.round(s.getMaxSpeed() / SPEED_UI_SCALE)); + boolean hasTarget = s.getTargetSpeed() >= 0; + targetSpeedEnabled[0] = hasTarget; + targetSpeedSpinner.setValue(hasTarget ? (int) Math.round(s.getTargetSpeed() / SPEED_UI_SCALE) : 6); + ((CardLayout) targetSpeedCardPanel.getLayout()).show(targetSpeedCardPanel, hasTarget ? "spinner" : "enable"); + speedTransitionSpinner.setValue(s.getSpeedTransitionPercent()); + } finally { + speedLoadingFromDefinition = false; + } + SwingUtilities.invokeLater(() -> { + targetSpeedCardPanel.revalidate(); + targetSpeedCardPanel.repaint(); + }); + }; + particleDropdown.addItemListener(loadFromDef); + + Runnable pushToDef = () -> { + if (speedLoadingFromDefinition) return; + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + ParticleDefinition.Speed s = def.speed; + s.minSpeed = ((Number) minSpeedSpinner.getValue()).intValue() * SPEED_UI_SCALE; + s.maxSpeed = ((Number) maxSpeedSpinner.getValue()).intValue() * SPEED_UI_SCALE; + s.targetSpeed = targetSpeedEnabled[0] ? ((Number) targetSpeedSpinner.getValue()).intValue() * SPEED_UI_SCALE : -1f; + s.speedTransitionPercent = ((Number) speedTransitionSpinner.getValue()).intValue(); + def.postDecode(); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(id)); + }; + ChangeListener speedChanged = e -> pushToDef.run(); + minSpeedSpinner.addChangeListener(speedChanged); + maxSpeedSpinner.addChangeListener(speedChanged); + targetSpeedSpinner.addChangeListener(e -> { + if (((Number) targetSpeedSpinner.getValue()).intValue() < 0) { // -1 = disabled + targetSpeedEnabled[0] = false; + ((CardLayout) targetSpeedCardPanel.getLayout()).show(targetSpeedCardPanel, "enable"); + } + pushToDef.run(); + }); + speedTransitionSpinner.addChangeListener(speedChanged); + + loadFromDef.itemStateChanged(new ItemEvent(particleDropdown, ItemEvent.ITEM_STATE_CHANGED, particleDropdown.getSelectedItem(), ItemEvent.SELECTED)); + + section.add(content, BorderLayout.CENTER); + return section; + } + + private JPanel buildScaleSection(JComboBox particleDropdown) { + scaleLoadingFromDefinition = false; + + JPanel section = buildTitledSection("Scale"); + JPanel content = new JPanel(new GridBagLayout()); + content.setBorder(new EmptyBorder(6, 6, 6, 10)); + + int row = 0; + Dimension spinnerSize = new Dimension(72, 24); + + JLabel minScaleLabel = new JLabel("Min scale"); + minScaleLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + minScaleLabel.setToolTipText("Lower bound of initial particle size. Higher values make particles spawn larger."); + content.add(minScaleLabel, labelConstraints(row)); + JSpinner minScaleSpinner = new JSpinner(new SpinnerNumberModel(2f, 0f, 10000f, 0.5f)); + minScaleSpinner.setPreferredSize(spinnerSize); + minScaleSpinner.setMinimumSize(spinnerSize); + content.add(minScaleSpinner, controlConstraints(row)); + row++; + + JLabel maxScaleLabel = new JLabel("Max scale"); + maxScaleLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + maxScaleLabel.setToolTipText("Upper bound of initial particle size. Each particle gets a random size between min and max."); + content.add(maxScaleLabel, labelConstraints(row)); + JSpinner maxScaleSpinner = new JSpinner(new SpinnerNumberModel(4f, 0f, 10000f, 0.5f)); + maxScaleSpinner.setPreferredSize(spinnerSize); + maxScaleSpinner.setMinimumSize(spinnerSize); + content.add(maxScaleSpinner, controlConstraints(row)); + row++; + + // Target scale: no target (-1) shows Enable button; otherwise spinner + Disable (like target speed) + JLabel targetScaleLabel = new JLabel("Target scale"); + targetScaleLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + targetScaleLabel.setToolTipText("Size to transition toward over the particle's lifetime. Enable to set a target; Disable or -1 for no target."); + content.add(targetScaleLabel, labelConstraints(row)); + JPanel targetScaleCardPanel = new JPanel(new CardLayout()); + targetScaleCardPanel.setOpaque(false); + JButton targetScaleEnableBtn = new JButton("Enable"); + targetScaleEnableBtn.setToolTipText("Enable target scale"); + targetScaleCardPanel.add(targetScaleEnableBtn, "enable"); + JPanel targetScaleSpinnerRow = new JPanel(new FlowLayout(FlowLayout.TRAILING, 0, 0)); + targetScaleSpinnerRow.setOpaque(false); + JSpinner targetScaleSpinner = new JSpinner(new SpinnerNumberModel(4f, -1f, 10000f, 0.5f)); + targetScaleSpinner.setPreferredSize(spinnerSize); + targetScaleSpinner.setMinimumSize(spinnerSize); + targetScaleSpinnerRow.add(targetScaleSpinner); + JButton targetScaleDisableBtn = new JButton("Disable"); + targetScaleDisableBtn.setToolTipText("No target scale (particle size stays between min and max)"); + targetScaleSpinnerRow.add(targetScaleDisableBtn); + targetScaleCardPanel.add(targetScaleSpinnerRow, "spinner"); + content.add(targetScaleCardPanel, controlConstraints(row)); + final boolean[] targetScaleEnabled = { false }; + row++; + + JLabel scaleTransitionLabel = new JLabel("Scale transition %"); + scaleTransitionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + scaleTransitionLabel.setToolTipText("Percentage of the particle's lifetime over which scale changes from initial to target. 100 = transition over full lifetime."); + content.add(scaleTransitionLabel, labelConstraints(row)); + JSpinner scaleTransitionSpinner = new JSpinner(new SpinnerNumberModel(100, 0, 100, 1)); + scaleTransitionSpinner.setPreferredSize(spinnerSize); + scaleTransitionSpinner.setMinimumSize(spinnerSize); + content.add(scaleTransitionSpinner, controlConstraints(row)); + + targetScaleEnableBtn.addActionListener(e -> { + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + def.scale.targetScale = 4f; + targetScaleSpinner.setValue(4f); + targetScaleEnabled[0] = true; + ((CardLayout) targetScaleCardPanel.getLayout()).show(targetScaleCardPanel, "spinner"); + def.postDecode(); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(id)); + }); + targetScaleDisableBtn.addActionListener(e -> { + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + def.scale.targetScale = ParticleDefinition.NO_TARGET; + targetScaleEnabled[0] = false; + ((CardLayout) targetScaleCardPanel.getLayout()).show(targetScaleCardPanel, "enable"); + def.postDecode(); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(id)); + }); + + ItemListener loadFromDef = e -> { + if (e.getStateChange() != ItemEvent.SELECTED) return; + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + scaleLoadingFromDefinition = true; + try { + ParticleDefinition.Scale s = def.scale; + minScaleSpinner.setValue((float) s.getMinScale()); + maxScaleSpinner.setValue((float) s.getMaxScale()); + boolean hasTarget = s.getTargetScale() >= 0; + targetScaleEnabled[0] = hasTarget; + targetScaleSpinner.setValue(hasTarget ? s.getTargetScale() : 4f); + ((CardLayout) targetScaleCardPanel.getLayout()).show(targetScaleCardPanel, hasTarget ? "spinner" : "enable"); + scaleTransitionSpinner.setValue(s.getScaleTransitionPercent()); + } finally { + scaleLoadingFromDefinition = false; + } + SwingUtilities.invokeLater(() -> { + targetScaleCardPanel.revalidate(); + targetScaleCardPanel.repaint(); + }); + }; + particleDropdown.addItemListener(loadFromDef); + + Runnable pushToDef = () -> { + if (scaleLoadingFromDefinition) return; + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + ParticleDefinition.Scale s = def.scale; + s.minScale = ((Number) minScaleSpinner.getValue()).floatValue(); + s.maxScale = ((Number) maxScaleSpinner.getValue()).floatValue(); + s.targetScale = targetScaleEnabled[0] ? ((Number) targetScaleSpinner.getValue()).floatValue() : ParticleDefinition.NO_TARGET; + s.scaleTransitionPercent = ((Number) scaleTransitionSpinner.getValue()).intValue(); + def.postDecode(); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(id)); + }; + ChangeListener scaleChanged = e -> pushToDef.run(); + minScaleSpinner.addChangeListener(scaleChanged); + maxScaleSpinner.addChangeListener(scaleChanged); + targetScaleSpinner.addChangeListener(e -> { + if (((Number) targetScaleSpinner.getValue()).floatValue() < 0) { + targetScaleEnabled[0] = false; + ((CardLayout) targetScaleCardPanel.getLayout()).show(targetScaleCardPanel, "enable"); + } + pushToDef.run(); + }); + scaleTransitionSpinner.addChangeListener(scaleChanged); + + loadFromDef.itemStateChanged(new ItemEvent(particleDropdown, ItemEvent.ITEM_STATE_CHANGED, particleDropdown.getSelectedItem(), ItemEvent.SELECTED)); + + section.add(content, BorderLayout.CENTER); + return section; + } + + private JPanel buildColoursSection(JComboBox particleDropdown) { + coloursLoadingFromDefinition = false; + + JPanel section = new JPanel(); + section.setLayout(new BorderLayout(0, 0)); + TitledBorder coloursTitleBorder = new TitledBorder( + new MatteBorder(1, 1, 1, 1, ColorScheme.MEDIUM_GRAY_COLOR), + "Colours", + TitledBorder.LEFT, + TitledBorder.TOP); + coloursTitleBorder.setTitleColor(ColorScheme.LIGHT_GRAY_COLOR); + coloursTitleBorder.setTitleFont(coloursTitleBorder.getTitleFont().deriveFont(Font.BOLD, 14f)); + section.setBorder(coloursTitleBorder); + + JPanel content = new JPanel(new GridBagLayout()); + content.setBorder(new EmptyBorder(6, 6, 0, 6)); + + int row = 0; + + // Min colour + JLabel minLabel = new JLabel("Min Colour"); + minLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(minLabel, labelConstraints(row)); + JButton minButton = new JButton(); + minButton.setOpaque(true); + minButton.setBorder(new LineBorder(ColorScheme.MEDIUM_GRAY_COLOR)); + minButton.setPreferredSize(new Dimension(140, 24)); + minButton.setMinimumSize(new Dimension(100, 24)); + updateColorButton(minButton, null); + content.add(minButton, controlConstraints(row)); + row++; + + // Max colour + JLabel maxLabel = new JLabel("Max Colour"); + maxLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(maxLabel, labelConstraints(row)); + JButton maxButton = new JButton(); + maxButton.setOpaque(true); + maxButton.setBorder(new LineBorder(ColorScheme.MEDIUM_GRAY_COLOR)); + maxButton.setPreferredSize(new Dimension(140, 24)); + maxButton.setMinimumSize(new Dimension(100, 24)); + updateColorButton(maxButton, null); + content.add(maxButton, controlConstraints(row)); + row++; + + // Target colour: no target (0/-1 argb) shows Enable button; otherwise colour button + Disable (like cycle duration) + JLabel targetLabel = new JLabel("Target Colour"); + targetLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + targetLabel.setToolTipText("Optional end colour to transition to. Enable to set a target; Disable for no target."); + content.add(targetLabel, labelConstraints(row)); + JPanel targetColourCardPanel = new JPanel(new CardLayout()); + targetColourCardPanel.setOpaque(false); + JButton targetColourEnableBtn = new JButton("Enable"); + targetColourEnableBtn.setToolTipText("Enable target colour"); + targetColourCardPanel.add(targetColourEnableBtn, "enable"); + JPanel targetColourControlRow = new JPanel(new FlowLayout(FlowLayout.TRAILING, 0, 0)); + targetColourControlRow.setOpaque(false); + JButton targetButton = new JButton(); + targetButton.setOpaque(true); + targetButton.setBorder(new LineBorder(ColorScheme.MEDIUM_GRAY_COLOR)); + targetButton.setToolTipText("Target end colour"); + // Compact swatch so the colour box + Disable button fit comfortably + targetButton.setPreferredSize(new Dimension(84, 24)); + targetButton.setMinimumSize(new Dimension(64, 24)); + updateColorButton(targetButton, null); + targetColourControlRow.add(targetButton); + JButton targetColourDisableBtn = new JButton("Disable"); + targetColourDisableBtn.setToolTipText("No target colour (particle colour stays between min and max only)"); + targetColourControlRow.add(targetColourDisableBtn); + targetColourCardPanel.add(targetColourControlRow, "control"); + content.add(targetColourCardPanel, controlConstraints(row)); + final boolean[] targetColourEnabled = { false }; + row++; + + // Colour transition % + JLabel colourTransLabel = new JLabel("Color Transition %"); + colourTransLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(colourTransLabel, labelConstraints(row)); + JSpinner colourTransSpinner = new JSpinner(new SpinnerNumberModel(100, 0, 100, 5)); + content.add(colourTransSpinner, controlConstraints(row)); + row++; + + // Alpha transition % + JLabel alphaTransLabel = new JLabel("Alpha Transition %"); + alphaTransLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(alphaTransLabel, labelConstraints(row)); + JSpinner alphaTransSpinner = new JSpinner(new SpinnerNumberModel(100, 0, 100, 5)); + content.add(alphaTransSpinner, controlConstraints(row)); + row++; + + // Uniform colour variation + JLabel uniformLabel = new JLabel("Uniform variation"); + uniformLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(uniformLabel, labelConstraints(row)); + JCheckBox uniformCheck = new JCheckBox("", false); + uniformCheck.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(uniformCheck, controlConstraints(row)); + row++; + + JLabel useSceneAmbientLabel = new JLabel("Use scene ambient light"); + useSceneAmbientLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + useSceneAmbientLabel.setToolTipText("Apply scene ambient lighting to this particle. Disable for self-lit effects like fire."); + content.add(useSceneAmbientLabel, labelConstraints(row)); + JCheckBox useSceneAmbientCheck = new JCheckBox("", true); + useSceneAmbientCheck.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(useSceneAmbientCheck, controlConstraints(row)); + + section.add(content, BorderLayout.NORTH); + + JLabel paletteLabel = new JLabel("Color palette"); + paletteLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + paletteLabel.setBorder(new EmptyBorder(4, 0, 2, 0)); + + // Single colour bar: content swaps based on Uniform variation toggle + JPanel preview = new JPanel() { + @Override + protected void paintComponent(java.awt.Graphics g) { + super.paintComponent(g); + java.awt.Graphics2D g2 = (java.awt.Graphics2D) g; + int w = getWidth(); + int h = getHeight(); + if (w <= 0 || h <= 0) { + return; + } + + Color minPicked = (Color) minButton.getClientProperty("pickedColor"); + Color maxPicked = (Color) maxButton.getClientProperty("pickedColor"); + Color tgtPicked = (Color) targetButton.getClientProperty("pickedColor"); + Color min = minPicked != null ? minPicked : Color.WHITE; + Color max = maxPicked != null ? maxPicked : Color.WHITE; + boolean hasTarget = tgtPicked != null; + Color tgt = tgtPicked; + + int topGap = 8; + int bottomGap = 8; + int barHeight = Math.max(20, h - topGap - bottomGap); + int barY = topGap; + int steps = 24; + int sw = Math.max(2, w / steps); + + java.util.function.Function gradient = t -> { + double clamped = Math.max(0.0, Math.min(1.0, t)); + if (!hasTarget) { + return lerpColor(min, max, clamped); + } + if (clamped < 0.5) { + return lerpColor(min, tgt, clamped * 2.0); + } else { + return lerpColor(tgt, max, (clamped - 0.5) * 2.0); + } + }; + + boolean uniform = uniformCheck.isSelected(); + if (uniform) { + // Uniform: smooth left-to-right gradient + for (int i = 0; i < steps; i++) { + double t = steps == 1 ? 0.0 : (double) i / (steps - 1); + Color c = gradient.apply(t); + int x = i * sw; + g2.setColor(c); + g2.fillRect(x, barY, sw + 1, barHeight); + } + } else { + // Normal: random samples across gradient + java.util.Random rng = new java.util.Random(0); + for (int i = 0; i < steps; i++) { + double t = rng.nextDouble(); + Color c = gradient.apply(t); + int x = i * sw; + g2.setColor(c); + g2.fillRect(x, barY, sw + 1, barHeight); + } + } + + g2.setColor(ColorScheme.MEDIUM_GRAY_COLOR); + g2.drawRect(0, barY, w - 1, barHeight); + } + }; + preview.setPreferredSize(new Dimension(0, 44)); + preview.setBorder(new EmptyBorder(0, 0, 0, 0)); + + JPanel palettePanel = new JPanel(new BorderLayout(0, 0)); + palettePanel.setBorder(new EmptyBorder(0, 6, 8, 6)); + palettePanel.add(paletteLabel, BorderLayout.NORTH); + palettePanel.add(preview, BorderLayout.CENTER); + section.add(palettePanel, BorderLayout.CENTER); + + // Load from selected definition + ItemListener loadFromDef = e -> { + if (e.getStateChange() != ItemEvent.SELECTED) return; + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + coloursLoadingFromDefinition = true; + try { + ParticleDefinition.Colours c = def.colours; + updateColorButton(minButton, isUnsetColourArgb(c.minColourArgb) ? null : argbToColor(c.minColourArgb)); + updateColorButton(maxButton, isUnsetColourArgb(c.maxColourArgb) ? null : argbToColor(c.maxColourArgb)); + boolean hasTargetColour = !isUnsetColourArgb(c.targetColourArgb); + targetColourEnabled[0] = hasTargetColour; + updateColorButton(targetButton, hasTargetColour ? argbToColor(c.targetColourArgb) : null); + ((CardLayout) targetColourCardPanel.getLayout()).show(targetColourCardPanel, hasTargetColour ? "control" : "enable"); + int colPct = Math.max(0, Math.min(100, c.colourTransitionPercent)); + int alphaPct = Math.max(0, Math.min(100, c.alphaTransitionPercent)); + colourTransSpinner.setValue(colPct); + alphaTransSpinner.setValue(alphaPct); + uniformCheck.setSelected(c.uniformColourVariation); + useSceneAmbientCheck.setSelected(c.useSceneAmbientLight); + preview.repaint(); + } finally { + coloursLoadingFromDefinition = false; + } + SwingUtilities.invokeLater(() -> { + targetColourCardPanel.revalidate(); + targetColourCardPanel.repaint(); + }); + }; + particleDropdown.addItemListener(loadFromDef); + + targetColourEnableBtn.addActionListener(e -> { + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + Color gray = Color.GRAY; + def.colours.targetColourArgb = colorToArgb(gray); + updateColorButton(targetButton, gray); + targetColourEnabled[0] = true; + ((CardLayout) targetColourCardPanel.getLayout()).show(targetColourCardPanel, "control"); + preview.repaint(); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(id)); + }); + targetColourDisableBtn.addActionListener(e -> { + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + def.colours.targetColourArgb = 0; + updateColorButton(targetButton, null); + targetColourEnabled[0] = false; + ((CardLayout) targetColourCardPanel.getLayout()).show(targetColourCardPanel, "enable"); + preview.repaint(); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(id)); + }); + + // Push non-colour settings (percentages + uniform checkbox) back into definition + ChangeListener percentagesChanged = e -> { + if (coloursLoadingFromDefinition) return; + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + def.colours.colourTransitionPercent = ((Number) colourTransSpinner.getValue()).intValue(); + def.colours.alphaTransitionPercent = ((Number) alphaTransSpinner.getValue()).intValue(); + def.colours.uniformColourVariation = uniformCheck.isSelected(); + def.colours.useSceneAmbientLight = useSceneAmbientCheck.isSelected(); + preview.repaint(); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(id)); + }; + colourTransSpinner.addChangeListener(percentagesChanged); + alphaTransSpinner.addChangeListener(percentagesChanged); + uniformCheck.addItemListener(e -> percentagesChanged.stateChanged(null)); + useSceneAmbientCheck.addItemListener(e -> percentagesChanged.stateChanged(null)); + + // Colour pickers using RuneLite's RuneliteColorPicker + minButton.addActionListener(e -> { + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + final String defId = id; + Color initial = (Color) minButton.getClientProperty("pickedColor"); + openColorPicker("Min Colour", initial != null ? initial : Color.WHITE, picked -> { + ParticleDefinition def = particleManager.getDefinition(defId); + if (def == null) return; + def.colours.minColourArgb = colorToArgb(picked); + updateColorButton(minButton, picked); + preview.repaint(); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(defId)); + }); + }); + + maxButton.addActionListener(e -> { + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + final String defId = id; + Color initial = (Color) maxButton.getClientProperty("pickedColor"); + openColorPicker("Max Colour", initial != null ? initial : Color.WHITE, picked -> { + ParticleDefinition def = particleManager.getDefinition(defId); + if (def == null) return; + def.colours.maxColourArgb = colorToArgb(picked); + updateColorButton(maxButton, picked); + preview.repaint(); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(defId)); + }); + }); + + targetButton.addActionListener(e -> { + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + final String defId = id; + Color initial = (Color) targetButton.getClientProperty("pickedColor"); + openColorPicker("Target Colour", initial != null ? initial : Color.GRAY, picked -> { + ParticleDefinition def = particleManager.getDefinition(defId); + if (def == null) return; + def.colours.targetColourArgb = colorToArgb(picked); + updateColorButton(targetButton, picked); + preview.repaint(); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(defId)); + }); + }); + + // Initial load for current selection + loadFromDef.itemStateChanged(new ItemEvent(particleDropdown, ItemEvent.ITEM_STATE_CHANGED, particleDropdown.getSelectedItem(), ItemEvent.SELECTED)); + + return section; + } + + private JPanel buildPhysicsSection(JComboBox particleDropdown) { + physicsLoadingFromDefinition = false; + + JPanel section = buildTitledSection("Physics"); + JPanel content = new JPanel(new GridBagLayout()); + content.setBorder(new EmptyBorder(6, 6, 6, 6)); + + int row = 0; + + JLabel clipLabel = new JLabel("Clip to terrain"); + clipLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(clipLabel, labelConstraints(row)); + JCheckBox clipCheck = new JCheckBox("", true); + clipCheck.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(clipCheck, controlConstraints(row)); + row++; + + JLabel collidesLabel = new JLabel("Collides with objects"); + collidesLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(collidesLabel, labelConstraints(row)); + JCheckBox collidesCheck = new JCheckBox("", false); + collidesCheck.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(collidesCheck, controlConstraints(row)); + row++; + + Dimension spinnerSize = new Dimension(72, 24); + + JLabel upperLabel = new JLabel("Upper bound level"); + upperLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + upperLabel.setToolTipText("-2 = no bound, -1 = current plane, 0-3 = level"); + content.add(upperLabel, labelConstraints(row)); + JSpinner upperSpinner = new JSpinner(new SpinnerNumberModel(-2, -2, 3, 1)); + upperSpinner.setPreferredSize(spinnerSize); + upperSpinner.setMinimumSize(spinnerSize); + content.add(upperSpinner, controlConstraints(row)); + row++; + + JLabel lowerLabel = new JLabel("Lower bound level"); + lowerLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + lowerLabel.setToolTipText("-2 = no bound, -1 = current plane, 0-3 = level"); + content.add(lowerLabel, labelConstraints(row)); + JSpinner lowerSpinner = new JSpinner(new SpinnerNumberModel(-2, -2, 3, 1)); + lowerSpinner.setPreferredSize(spinnerSize); + lowerSpinner.setMinimumSize(spinnerSize); + content.add(lowerSpinner, controlConstraints(row)); + row++; + + JLabel falloffTypeLabel = new JLabel("Distance falloff type"); + falloffTypeLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + falloffTypeLabel.setToolTipText("0 = none, 1 = linear, 2 = squared"); + content.add(falloffTypeLabel, labelConstraints(row)); + JSpinner falloffTypeSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 2, 1)); + falloffTypeSpinner.setPreferredSize(spinnerSize); + falloffTypeSpinner.setMinimumSize(spinnerSize); + content.add(falloffTypeSpinner, controlConstraints(row)); + row++; + + JLabel falloffStrengthLabel = new JLabel("Distance falloff strength"); + falloffStrengthLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(falloffStrengthLabel, labelConstraints(row)); + JSpinner falloffStrengthSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 10000, 1)); + falloffStrengthSpinner.setPreferredSize(new Dimension(80, 24)); + falloffStrengthSpinner.setMinimumSize(new Dimension(80, 24)); + content.add(falloffStrengthSpinner, controlConstraints(row)); + + ItemListener loadFromDef = e -> { + if (e.getStateChange() != ItemEvent.SELECTED) return; + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + physicsLoadingFromDefinition = true; + try { + ParticleDefinition.Physics ph = def.physics; + clipCheck.setSelected(ph.clipToTerrain); + collidesCheck.setSelected(ph.collidesWithObjects); + upperSpinner.setValue(ph.upperBoundLevel); + lowerSpinner.setValue(ph.lowerBoundLevel); + falloffTypeSpinner.setValue(ph.distanceFalloffType); + falloffStrengthSpinner.setValue(ph.distanceFalloffStrength); + } finally { + physicsLoadingFromDefinition = false; + } + }; + particleDropdown.addItemListener(loadFromDef); + + Runnable pushPhysics = () -> { + if (physicsLoadingFromDefinition) return; + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + ParticleDefinition.Physics ph = def.physics; + ph.clipToTerrain = clipCheck.isSelected(); + ph.collidesWithObjects = collidesCheck.isSelected(); + ph.upperBoundLevel = ((Number) upperSpinner.getValue()).intValue(); + ph.lowerBoundLevel = ((Number) lowerSpinner.getValue()).intValue(); + ph.distanceFalloffType = ((Number) falloffTypeSpinner.getValue()).intValue(); + ph.distanceFalloffStrength = ((Number) falloffStrengthSpinner.getValue()).intValue(); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(id)); + }; + ChangeListener physicsChanged = e -> pushPhysics.run(); + clipCheck.addItemListener(e -> pushPhysics.run()); + collidesCheck.addItemListener(e -> pushPhysics.run()); + upperSpinner.addChangeListener(physicsChanged); + lowerSpinner.addChangeListener(physicsChanged); + falloffTypeSpinner.addChangeListener(physicsChanged); + falloffStrengthSpinner.addChangeListener(physicsChanged); + + loadFromDef.itemStateChanged(new ItemEvent(particleDropdown, ItemEvent.ITEM_STATE_CHANGED, particleDropdown.getSelectedItem(), ItemEvent.SELECTED)); + + section.add(content, BorderLayout.CENTER); + return section; + } + + private JPanel buildEmissionSection(JComboBox particleDropdown) { + emissionLoadingFromDefinition = false; + + JPanel section = buildTitledSection("Emission"); + JPanel content = new JPanel(new GridBagLayout()); + content.setBorder(new EmptyBorder(6, 6, 6, 10)); + + Dimension smallSpinnerSize = new Dimension(72, 24); + Dimension timeSpinnerSize = new Dimension(88, 24); + Dimension delaySpinnerSize = new Dimension(96, 24); // slightly wider than time for M:S.mmm, still fits on panel + + int row = 0; + final int emissionLabelMaxWidth = 85; + + // Min delay + JLabel minDelayLabel = new JLabel("Min delay"); + minDelayLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + minDelayLabel.setToolTipText("Particle lifetime min (minutes:seconds.milliseconds)"); + minDelayLabel.setMaximumSize(new Dimension(emissionLabelMaxWidth, 36)); + content.add(minDelayLabel, labelConstraints(row)); + JSpinner[] minDelaySpinnerRef = new JSpinner[1]; + content.add(createDelaySpinnerWithLabel(delaySpinnerSize, minDelaySpinnerRef), controlConstraints(row)); + JSpinner minDelayTimeSpinner = minDelaySpinnerRef[0]; + row++; + + // Max delay + JLabel maxDelayLabel = new JLabel("Max delay"); + maxDelayLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + maxDelayLabel.setToolTipText("Particle lifetime max (minutes:seconds.milliseconds)"); + maxDelayLabel.setMaximumSize(new Dimension(emissionLabelMaxWidth, 36)); + content.add(maxDelayLabel, labelConstraints(row)); + JSpinner[] maxDelaySpinnerRef = new JSpinner[1]; + content.add(createDelaySpinnerWithLabel(delaySpinnerSize, maxDelaySpinnerRef), controlConstraints(row)); + JSpinner maxDelayTimeSpinner = maxDelaySpinnerRef[0]; + row++; + + JLabel minSpawnLabel = new JLabel("Min spawn"); + minSpawnLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + minSpawnLabel.setMaximumSize(new Dimension(emissionLabelMaxWidth, 24)); + content.add(minSpawnLabel, labelConstraints(row)); + JSpinner minSpawnSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 4096, 1)); + minSpawnSpinner.setPreferredSize(smallSpinnerSize); + minSpawnSpinner.setMinimumSize(smallSpinnerSize); + content.add(minSpawnSpinner, controlConstraints(row)); + row++; + + JLabel maxSpawnLabel = new JLabel("Max spawn"); + maxSpawnLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + maxSpawnLabel.setMaximumSize(new Dimension(emissionLabelMaxWidth, 24)); + content.add(maxSpawnLabel, labelConstraints(row)); + JSpinner maxSpawnSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 4096, 1)); + maxSpawnSpinner.setPreferredSize(smallSpinnerSize); + maxSpawnSpinner.setMinimumSize(smallSpinnerSize); + content.add(maxSpawnSpinner, controlConstraints(row)); + row++; + + JLabel initialSpawnLabel = new JLabel("Initial spawn"); + initialSpawnLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + initialSpawnLabel.setMaximumSize(new Dimension(emissionLabelMaxWidth, 24)); + content.add(initialSpawnLabel, labelConstraints(row)); + JSpinner initialSpawnSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 4096, 1)); + initialSpawnSpinner.setPreferredSize(smallSpinnerSize); + initialSpawnSpinner.setMinimumSize(smallSpinnerSize); + content.add(initialSpawnSpinner, controlConstraints(row)); + row++; + + // Cycle duration: when -1 show "Enable" button; when >= 0 show spinner + "Disable" button + JLabel cycleDurationLabel = new JLabel("Cycle duration"); + cycleDurationLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + cycleDurationLabel.setToolTipText("When enabled, emission repeats every this duration (-1 = always). Format: Hours:Minutes:Seconds"); + cycleDurationLabel.setMaximumSize(new Dimension(emissionLabelMaxWidth, 36)); + content.add(cycleDurationLabel, labelConstraints(row)); + JPanel cycleCardPanel = new JPanel(new CardLayout()); + cycleCardPanel.setOpaque(false); + JButton cycleEnableBtn = new JButton("Enable"); + cycleEnableBtn.setToolTipText("Enable cycle duration"); + cycleCardPanel.add(cycleEnableBtn, "enable"); + JSpinner[] cycleSpinnerRef = new JSpinner[1]; + JPanel cycleSpinnerRow = new JPanel(new FlowLayout(FlowLayout.LEADING, 4, 0)); + cycleSpinnerRow.setOpaque(false); + cycleSpinnerRow.add(createTimeSpinnerWithLabel(timeSpinnerSize, cycleSpinnerRef, true)); + JButton cycleDisableBtn = new JButton("Disable"); + cycleDisableBtn.setToolTipText("Set cycle duration to -1 (disabled)"); + cycleSpinnerRow.add(cycleDisableBtn); + cycleCardPanel.add(cycleSpinnerRow, "spinner"); + content.add(cycleCardPanel, controlConstraints(row)); + JSpinner cycleTimeSpinner = cycleSpinnerRef[0]; + final boolean[] cycleDurationEnabled = { false }; + row++; + + // Time threshold: when -1 show "Enable" button; when >= 0 show spinner + "Disable" button + JLabel thresholdLabel = new JLabel("Time threshold"); + thresholdLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + thresholdLabel.setToolTipText("When enabled, emit only before/after this time in the cycle (-1 = disabled). Format: Hours:Minutes:Seconds"); + thresholdLabel.setMaximumSize(new Dimension(emissionLabelMaxWidth, 36)); + content.add(thresholdLabel, labelConstraints(row)); + JPanel thresholdCardPanel = new JPanel(new CardLayout()); + thresholdCardPanel.setOpaque(false); + JButton thresholdEnableBtn = new JButton("Enable"); + thresholdEnableBtn.setToolTipText("Enable time threshold"); + thresholdCardPanel.add(thresholdEnableBtn, "enable"); + JSpinner[] thresholdSpinnerRef = new JSpinner[1]; + JPanel thresholdSpinnerRow = new JPanel(new FlowLayout(FlowLayout.LEADING, 4, 0)); + thresholdSpinnerRow.setOpaque(false); + thresholdSpinnerRow.add(createTimeSpinnerWithLabel(timeSpinnerSize, thresholdSpinnerRef, true)); + JButton thresholdDisableBtn = new JButton("Disable"); + thresholdDisableBtn.setToolTipText("Set time threshold to -1 (disabled)"); + thresholdSpinnerRow.add(thresholdDisableBtn); + thresholdCardPanel.add(thresholdSpinnerRow, "spinner"); + content.add(thresholdCardPanel, controlConstraints(row)); + JSpinner thresholdTimeSpinner = thresholdSpinnerRef[0]; + final boolean[] thresholdEnabled = { false }; + row++; + + JLabel emitOnlyBeforeLabel = new JLabel("Emit only before"); + emitOnlyBeforeLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + emitOnlyBeforeLabel.setMaximumSize(new Dimension(emissionLabelMaxWidth, 36)); + content.add(emitOnlyBeforeLabel, labelConstraints(row)); + JCheckBox emitOnlyBeforeCheck = new JCheckBox("", true); + emitOnlyBeforeCheck.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(emitOnlyBeforeCheck, controlConstraints(row)); + row++; + + JLabel loopLabel = new JLabel("Loop emission"); + loopLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + loopLabel.setMaximumSize(new Dimension(emissionLabelMaxWidth, 36)); + content.add(loopLabel, labelConstraints(row)); + JCheckBox loopCheck = new JCheckBox("", true); + loopCheck.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(loopCheck, controlConstraints(row)); + + // Cycle duration: Enable -> set 1 sec and show spinner; Disable / 00:00:00 -> show Enable + cycleEnableBtn.addActionListener(e -> { + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + int oneSec = TICKS_PER_SECOND; + def.emission.emissionCycleDuration = oneSec; + ((TimeSpinnerModel) cycleTimeSpinner.getModel()).setTicks(oneSec); + cycleDurationEnabled[0] = true; + ((CardLayout) cycleCardPanel.getLayout()).show(cycleCardPanel, "spinner"); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(id)); + }); + cycleDisableBtn.addActionListener(e -> { + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + def.emission.emissionCycleDuration = -1; + cycleDurationEnabled[0] = false; + ((CardLayout) cycleCardPanel.getLayout()).show(cycleCardPanel, "enable"); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(id)); + }); + + // Time threshold: Enable -> set 1 sec and show spinner; Disable / 00:00:00 -> show Enable + thresholdEnableBtn.addActionListener(e -> { + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + int oneSec = TICKS_PER_SECOND; + def.emission.emissionTimeThreshold = oneSec; + ((TimeSpinnerModel) thresholdTimeSpinner.getModel()).setTicks(oneSec); + thresholdEnabled[0] = true; + ((CardLayout) thresholdCardPanel.getLayout()).show(thresholdCardPanel, "spinner"); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(id)); + }); + thresholdDisableBtn.addActionListener(e -> { + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + def.emission.emissionTimeThreshold = -1; + thresholdEnabled[0] = false; + ((CardLayout) thresholdCardPanel.getLayout()).show(thresholdCardPanel, "enable"); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(id)); + }); + + ItemListener loadFromDef = e -> { + if (e.getStateChange() != ItemEvent.SELECTED) return; + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + ParticleDefinition.Emission em = def.emission; + emissionLoadingFromDefinition = true; + try { + ((DelaySpinnerModel) minDelayTimeSpinner.getModel()).setTicks(em.minDelay); + ((DelaySpinnerModel) maxDelayTimeSpinner.getModel()).setTicks(em.maxDelay); + minSpawnSpinner.setValue(em.minSpawn); + maxSpawnSpinner.setValue(em.maxSpawn); + initialSpawnSpinner.setValue(em.initialSpawn); + boolean cycleEn = em.emissionCycleDuration > 0; + boolean threshEn = em.emissionTimeThreshold > 0; + cycleDurationEnabled[0] = cycleEn; + thresholdEnabled[0] = threshEn; + ((TimeSpinnerModel) cycleTimeSpinner.getModel()).setTicks(cycleEn ? em.emissionCycleDuration : 0); + ((TimeSpinnerModel) thresholdTimeSpinner.getModel()).setTicks(threshEn ? em.emissionTimeThreshold : 0); + ((CardLayout) cycleCardPanel.getLayout()).show(cycleCardPanel, cycleEn ? "spinner" : "enable"); + ((CardLayout) thresholdCardPanel.getLayout()).show(thresholdCardPanel, threshEn ? "spinner" : "enable"); + emitOnlyBeforeCheck.setSelected(em.emitOnlyBeforeTime); + loopCheck.setSelected(em.loopEmission); + } finally { + emissionLoadingFromDefinition = false; + } + // Defer UI refresh so cycle/threshold cards and spinners update after other listeners + SwingUtilities.invokeLater(() -> { + cycleCardPanel.revalidate(); + cycleCardPanel.repaint(); + thresholdCardPanel.revalidate(); + thresholdCardPanel.repaint(); + }); + }; + particleDropdown.addItemListener(loadFromDef); + + Runnable pushEmission = () -> { + if (emissionLoadingFromDefinition) return; + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + ParticleDefinition.Emission em = def.emission; + em.minDelay = ((DelaySpinnerModel) minDelayTimeSpinner.getModel()).getTicks(); + em.maxDelay = ((DelaySpinnerModel) maxDelayTimeSpinner.getModel()).getTicks(); + em.minSpawn = ((Number) minSpawnSpinner.getValue()).intValue(); + em.maxSpawn = ((Number) maxSpawnSpinner.getValue()).intValue(); + em.initialSpawn = ((Number) initialSpawnSpinner.getValue()).intValue(); + em.emissionCycleDuration = cycleDurationEnabled[0] ? ((TimeSpinnerModel) cycleTimeSpinner.getModel()).getTicks() : -1; + em.emissionTimeThreshold = thresholdEnabled[0] ? ((TimeSpinnerModel) thresholdTimeSpinner.getModel()).getTicks() : -1; + em.emitOnlyBeforeTime = emitOnlyBeforeCheck.isSelected(); + em.loopEmission = loopCheck.isSelected(); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(id)); + }; + + ChangeListener emissionChanged = e -> pushEmission.run(); + minDelayTimeSpinner.addChangeListener(emissionChanged); + maxDelayTimeSpinner.addChangeListener(emissionChanged); + // When spinner is set to -1 or 00:00:00 (0), switch back to Enable button + cycleTimeSpinner.addChangeListener(e -> { + if (((TimeSpinnerModel) cycleTimeSpinner.getModel()).getTicks() <= 0) { + cycleDurationEnabled[0] = false; + ((CardLayout) cycleCardPanel.getLayout()).show(cycleCardPanel, "enable"); + } + pushEmission.run(); + }); + thresholdTimeSpinner.addChangeListener(e -> { + if (((TimeSpinnerModel) thresholdTimeSpinner.getModel()).getTicks() <= 0) { + thresholdEnabled[0] = false; + ((CardLayout) thresholdCardPanel.getLayout()).show(thresholdCardPanel, "enable"); + } + pushEmission.run(); + }); + minSpawnSpinner.addChangeListener(emissionChanged); + maxSpawnSpinner.addChangeListener(emissionChanged); + initialSpawnSpinner.addChangeListener(emissionChanged); + emitOnlyBeforeCheck.addItemListener(e -> pushEmission.run()); + loopCheck.addItemListener(e -> pushEmission.run()); + + loadFromDef.itemStateChanged(new ItemEvent(particleDropdown, ItemEvent.ITEM_STATE_CHANGED, particleDropdown.getSelectedItem(), ItemEvent.SELECTED)); + + section.add(content, BorderLayout.CENTER); + return section; + } + + private JPanel buildTextureSection(JComboBox particleDropdown) { + textureLoadingFromDefinition = false; + + JPanel section = buildTitledSection("Texture"); + JTabbedPane tabs = new JTabbedPane() { + @Override + public Dimension getPreferredSize() { + Dimension superPref = super.getPreferredSize(); + int count = getTabCount(); + if (count == 0) + return superPref; + + int maxChildHeight = 0; + int selectedChildHeight = 0; + java.awt.Component selected = getSelectedComponent(); + for (int i = 0; i < count; i++) { + java.awt.Component c = getComponentAt(i); + if (c == null) + continue; + Dimension cd = c.getPreferredSize(); + int h = cd != null ? cd.height : 0; + if (h > maxChildHeight) + maxChildHeight = h; + if (c == selected) + selectedChildHeight = h; + } + + if (maxChildHeight <= 0) + return superPref; + + int headerHeight = superPref.height - maxChildHeight; + if (headerHeight < 0) + headerHeight = 0; + + int newHeight = headerHeight + selectedChildHeight; + if (newHeight > 0 && newHeight < superPref.height) { + return new Dimension(superPref.width, newHeight); + } + return superPref; + } + }; + + // --- Texture tab: file + preview --- + JPanel textureTab = new JPanel(new GridBagLayout()); + textureTab.setBorder(new EmptyBorder(1, 1, 1, 1)); + int row = 0; + + // Row 0: Texture dropdown + JLabel texLabel = new JLabel("Texture"); + texLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + textureTab.add(texLabel, labelConstraints(row)); + + java.util.List textureNames = particleManager.getAvailableTextureNames(); + // Build model: use "None" as display for empty; store "" in model for no texture + String[] names = textureNames.toArray(new String[0]); + JComboBox textureCombo = new JComboBox<>(names); + textureCombo.setEditable(false); + textureCombo.setRenderer(new DefaultListCellRenderer() { + private static final int DROP_PREVIEW_SIZE = 24; + @Override + public java.awt.Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + String file = value == null ? "" : value.toString(); + if (file.isEmpty()) { + setText("None"); + setIcon(null); + } else { + setText(file); + ImageIcon icon = loadTexturePreview(file, DROP_PREVIEW_SIZE); + setIcon(icon); + } + return this; + } + }); + GridBagConstraints texC = controlConstraints(row); + textureTab.add(textureCombo, texC); + row++; + + // Row 1: Preview area (label + panel) used by Preview tab + JLabel previewLabel = new JLabel(null, null, JLabel.CENTER); + previewLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + JPanel previewPanel = new JPanel(new BorderLayout()); + previewPanel.setPreferredSize(new Dimension(72, 72)); + previewPanel.setMinimumSize(new Dimension(72, 72)); + previewPanel.setBorder(new MatteBorder(1, 1, 1, 1, ColorScheme.MEDIUM_GRAY_COLOR)); + previewPanel.add(previewLabel, BorderLayout.CENTER); + + // --- Flipbook tab: Enable + options --- + JPanel flipbookTab = new JPanel(new BorderLayout()); + flipbookTab.setBorder(new EmptyBorder(4, 4, 4, 4)); + + CardLayout flipCards = new CardLayout(); + JPanel flipCardPanel = new JPanel(flipCards); + flipCardPanel.setBorder(new EmptyBorder(0, 0, 0, 0)); + + // Disabled card: centered Enable button + JPanel disabledCard = new JPanel(new BorderLayout()); + JButton enableFlipbookBtn = new JButton("Enable"); + styleButton(enableFlipbookBtn); + disabledCard.add(enableFlipbookBtn, BorderLayout.CENTER); + + // Enabled card: Cols, Rows, Mode + Disable button + JPanel enabledCard = new JPanel(new GridBagLayout()); + GridBagConstraints fc = new GridBagConstraints(); + fc.insets = new Insets(0, 0, 0, 4); + fc.anchor = GridBagConstraints.WEST; + + JLabel colsLbl = new JLabel("Cols"); + colsLbl.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + fc.gridx = 0; fc.gridy = 0; + enabledCard.add(colsLbl, fc); + + JSpinner colsSpinner = new JSpinner(new SpinnerNumberModel(1, 1, 64, 1)); + fc.gridx = 1; + enabledCard.add(colsSpinner, fc); + + JLabel rowsLbl = new JLabel("Rows"); + rowsLbl.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + fc.gridx = 2; + enabledCard.add(rowsLbl, fc); + + JSpinner rowsSpinner = new JSpinner(new SpinnerNumberModel(1, 1, 64, 1)); + fc.gridx = 3; + enabledCard.add(rowsSpinner, fc); + + JLabel modeLbl = new JLabel("Mode"); + modeLbl.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + fc.gridx = 0; fc.gridy = 1; fc.gridwidth = 4; + fc.insets = new Insets(6, 0, 0, 0); + fc.fill = GridBagConstraints.NONE; + fc.weightx = 0; + enabledCard.add(modeLbl, fc); + + JComboBox modeCombo = new JComboBox<>(new String[] { "Order", "Random" }); + modeCombo.setEditable(false); + fc.gridx = 0; fc.gridy = 2; fc.gridwidth = 4; + fc.insets = new Insets(2, 0, 0, 0); + fc.fill = GridBagConstraints.HORIZONTAL; + fc.weightx = 1; + enabledCard.add(modeCombo, fc); + + JButton disableFlipbookBtn = new JButton("Disable"); + styleButton(disableFlipbookBtn); + fc.gridx = 0; fc.gridy = 3; fc.gridwidth = 4; + fc.insets = new Insets(8, 0, 0, 0); + fc.fill = GridBagConstraints.HORIZONTAL; + fc.weightx = 1; + enabledCard.add(disableFlipbookBtn, fc); + + flipCardPanel.add(disabledCard, "disabled"); + flipCardPanel.add(enabledCard, "enabled"); + + // Keep flipbook content hugging the left + flipbookTab.add(flipCardPanel, BorderLayout.WEST); + + // Helpers to load current def into UI + Runnable reloadFromDefinition = () -> { + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + + textureLoadingFromDefinition = true; + try { + // Texture file + String file = def.texture.file; + String display = (file == null || file.isEmpty()) ? "(no texture)" : file; + textureCombo.setSelectedItem(file != null ? file : ""); + ImageIcon icon = loadTexturePreview(file); + if (icon != null) { + previewLabel.setIcon(icon); + previewLabel.setText(null); + } else { + previewLabel.setIcon(null); + previewLabel.setText(display); + } + + // Flipbook + var fb = def.texture.flipbook; + boolean fbEnabled = fb != null && + (fb.flipbookColumns > 0 || fb.flipbookRows > 0 || + (fb.flipbookMode != null && !fb.flipbookMode.isEmpty())); + if (fbEnabled) { + colsSpinner.setValue(Math.max(1, fb.flipbookColumns)); + rowsSpinner.setValue(Math.max(1, fb.flipbookRows)); + String m = fb.flipbookMode; + if (m != null && "random".equalsIgnoreCase(m)) { + modeCombo.setSelectedItem("Random"); + } else { + modeCombo.setSelectedItem("Order"); + } + flipCards.show(flipCardPanel, "enabled"); + } else { + flipCards.show(flipCardPanel, "disabled"); + } + } finally { + textureLoadingFromDefinition = false; + } + }; + + // When the selected particle changes, reload texture/flipbook UI + particleDropdown.addItemListener(e -> { + if (e.getStateChange() == ItemEvent.SELECTED) { + reloadFromDefinition.run(); + } + }); + + // Push texture changes back into definition + emitters + Runnable pushTextureToDefinition = () -> { + if (textureLoadingFromDefinition) return; + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + + // File + Object sel = textureCombo.getSelectedItem(); + String file = sel != null ? sel.toString().trim() : ""; + def.texture.file = file.isEmpty() ? null : file; + ImageIcon icon = loadTexturePreview(def.texture.file); + if (icon != null) { + previewLabel.setIcon(icon); + previewLabel.setText(null); + } else { + previewLabel.setIcon(null); + previewLabel.setText(def.texture.file != null ? def.texture.file : "(no texture)"); + } + + // Flipbook + var fb = def.texture.flipbook; + if (((CardLayout) flipCardPanel.getLayout()) == flipCards) { + // if disabled, clear flipbook; if enabled, store values + boolean enabled = ((Number) colsSpinner.getValue()).intValue() > 0 || + ((Number) rowsSpinner.getValue()).intValue() > 0; + if (!enabled) { + fb.flipbookColumns = 0; + fb.flipbookRows = 0; + fb.flipbookMode = null; + } else { + fb.flipbookColumns = ((Number) colsSpinner.getValue()).intValue(); + fb.flipbookRows = ((Number) rowsSpinner.getValue()).intValue(); + Object selMode = modeCombo.getSelectedItem(); + if (selMode == null) { + fb.flipbookMode = null; + } else if ("Random".equals(selMode.toString())) { + fb.flipbookMode = "random"; + } else { + fb.flipbookMode = "order"; + } + } + } + + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(id)); + }; + + textureCombo.addItemListener(e -> { + if (e.getStateChange() == ItemEvent.SELECTED) { + pushTextureToDefinition.run(); + } + }); + + enableFlipbookBtn.addActionListener(e -> { + if (textureLoadingFromDefinition) return; + flipCards.show(flipCardPanel, "enabled"); + // Sensible defaults + if (((Number) colsSpinner.getValue()).intValue() <= 0) colsSpinner.setValue(4); + if (((Number) rowsSpinner.getValue()).intValue() <= 0) rowsSpinner.setValue(4); + modeCombo.setSelectedItem("Order"); + pushTextureToDefinition.run(); + }); + + disableFlipbookBtn.addActionListener(e -> { + if (textureLoadingFromDefinition) return; + String id = (String) particleDropdown.getSelectedItem(); + if (id != null) { + ParticleDefinition def = particleManager.getDefinition(id); + if (def != null) { + var fb = def.texture.flipbook; + fb.flipbookColumns = 0; + fb.flipbookRows = 0; + fb.flipbookMode = null; + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(id)); + } + } + flipCards.show(flipCardPanel, "disabled"); + colsSpinner.setValue(1); + rowsSpinner.setValue(1); + modeCombo.setSelectedItem("Order"); + }); + + colsSpinner.addChangeListener(e -> pushTextureToDefinition.run()); + rowsSpinner.addChangeListener(e -> pushTextureToDefinition.run()); + modeCombo.addItemListener(e -> { + if (e.getStateChange() == ItemEvent.SELECTED) { + pushTextureToDefinition.run(); + } + }); + + // --- Big image preview tab --- + JPanel previewTab = new JPanel(new BorderLayout()); + previewTab.setBorder(new EmptyBorder(4, 4, 4, 4)); + previewTab.add(previewPanel, BorderLayout.CENTER); + + // Initial load for current selection + reloadFromDefinition.run(); + + // Tabs: Texture / Flipbook + tabs.addTab("Texture", textureTab); + tabs.addTab("Flipbook", flipbookTab); + tabs.addTab("Preview", previewTab); + + section.add(tabs, BorderLayout.CENTER); + return section; + } + + private static final int MAIN_PREVIEW_SIZE = 64; + + @Nullable + private ImageIcon loadTexturePreview(@Nullable String file) { + return loadTexturePreview(file, MAIN_PREVIEW_SIZE); + } + + @Nullable + private ImageIcon loadTexturePreview(@Nullable String file, int maxSize) { + if (file == null || file.isEmpty()) + return null; + try { + ResourcePath base = ParticleTextureLoader.getParticleTexturesPath(); + ResourcePath res = base.resolve(file); + try (java.io.InputStream is = res.toInputStream()) { + BufferedImage img = javax.imageio.ImageIO.read(is); + if (img == null) + return null; + int w = img.getWidth(); + int h = img.getHeight(); + if (w > maxSize || h > maxSize) { + float scale = Math.min((float) maxSize / w, (float) maxSize / h); + int nw = Math.max(1, Math.round(w * scale)); + int nh = Math.max(1, Math.round(h * scale)); + BufferedImage scaled = new BufferedImage(nw, nh, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = scaled.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2.drawImage(img, 0, 0, nw, nh, null); + g2.dispose(); + img = scaled; + } + return new ImageIcon(img); + } + } catch (Exception ex) { + log.warn("[Particles] Failed to load texture preview for {}", file, ex); + return null; + } + } + + private static JPanel buildTitledSection(String title) { + JPanel section = new JPanel(); + section.setLayout(new BorderLayout(0, 0)); + TitledBorder titledBorder = new TitledBorder( + new MatteBorder(1, 1, 1, 1, ColorScheme.MEDIUM_GRAY_COLOR), + title, + TitledBorder.LEFT, + TitledBorder.TOP); + titledBorder.setTitleColor(ColorScheme.LIGHT_GRAY_COLOR); + titledBorder.setTitleFont(titledBorder.getTitleFont().deriveFont(Font.BOLD)); + section.setBorder(titledBorder); + return section; + } + + private static Color lerpColor(Color a, Color b, double t) { + double clamped = Math.max(0.0, Math.min(1.0, t)); + int r = (int) Math.round(a.getRed() + (b.getRed() - a.getRed()) * clamped); + int g = (int) Math.round(a.getGreen() + (b.getGreen() - a.getGreen()) * clamped); + int bl = (int) Math.round(a.getBlue() + (b.getBlue() - a.getBlue()) * clamped); + return new Color(r, g, bl); + } + + private static Color safeColor(Object c, Color fallback) { + return c instanceof Color ? (Color) c : fallback; + } + + private static String colorToHex(Color c) { + if (c == null) return "#000000"; + return String.format("#%02X%02X%02X", c.getRed(), c.getGreen(), c.getBlue()); + } + + /** Treat 0 or 0xFFFFFFFF as "colour not set". */ + private static boolean isUnsetColourArgb(int argb) { + return argb == 0 || argb == -1; + } + + /** Set button to swatch + hex, or normal "Set color" when no colour is set (null). */ + private static void updateColorButton(JButton btn, Color c) { + btn.putClientProperty("pickedColor", c); + if (c == null) { + btn.setBackground(ColorScheme.DARKER_GRAY_COLOR); + btn.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + btn.setText("Set color"); + } else { + btn.setBackground(c); + btn.setText(colorToHex(c)); + double luminance = (0.299 * c.getRed() + 0.587 * c.getGreen() + 0.114 * c.getBlue()) / 255.0; + btn.setForeground(luminance < 0.5 ? Color.WHITE : Color.BLACK); + } + } + + private static int colorToArgb(Color c) { + if (c == null) { + return 0; + } + return (c.getAlpha() << 24) | (c.getRed() << 16) | (c.getGreen() << 8) | c.getBlue(); + } + + private static Color argbToColor(int argb) { + if (argb == 0) { + return Color.WHITE; + } + int a = (argb >>> 24) & 0xFF; + int r = (argb >>> 16) & 0xFF; + int g = (argb >>> 8) & 0xFF; + int b = argb & 0xFF; + return new Color(r, g, b, a); + } + + private void openColorPicker(String title, Color initial, java.util.function.Consumer onPicked) { + if (client == null || colorPickerManager == null) { + log.warn("Color picker not available: client or colorPickerManager not injected"); + return; + } + SwingUtilities.invokeLater(() -> { + RuneliteColorPicker picker = colorPickerManager.create( + client, + initial, + title, + true + ); + picker.setOnClose(col -> { + if (col != null) { + onPicked.accept(col); + } + }); + picker.setVisible(true); + }); + } + + private JPanel buildGeneralSectionInner(JComboBox particleDropdown) { + JPanel section = buildTitledSection("General"); + JPanel content = new JPanel(new GridBagLayout()); + // Match margins used by other sections so everything lines up visually + content.setBorder(new EmptyBorder(6, 6, 6, 10)); + + ParticleDefinition.General defaults = new ParticleDefinition.General(); + int row = 0; + + JSpinner yawSpinner = new JSpinner(new SpinnerNumberModel(defaults.getDirectionYaw(), 0, 2048, 10)); + JSpinner pitchSpinner = new JSpinner(new SpinnerNumberModel(defaults.getDirectionPitch(), 0, 1024, 10)); + + GridBagConstraints gizmoC = new GridBagConstraints(); + gizmoC.gridx = 0; + gizmoC.gridy = row; + gizmoC.gridwidth = 2; + gizmoC.fill = GridBagConstraints.HORIZONTAL; + gizmoC.weightx = 1; + gizmoC.anchor = GridBagConstraints.CENTER; + gizmoC.insets = new Insets(4, 0, 4, 0); + directionGizmo = new DirectionGizmoPanel(yawSpinner, pitchSpinner); + content.add(directionGizmo, gizmoC); + row++; + + // Presets: Yaw and Pitch combo boxes on one row + int[] yawPresets = { 0, 256, 512, 768, 1024, 1280, 1536, 1792 }; + String[] yawLabels = { "N", "NE", "E", "SE", "S", "SW", "W", "NW" }; + int[] pitchPresets = { 0, 512, 1024 }; + String[] pitchLabels = { "Up", "Side", "Down" }; + + JPanel presetsRow = new JPanel(new FlowLayout(FlowLayout.CENTER, 4, 0)); + JLabel yawPresetLbl = new JLabel("Yaw:"); + yawPresetLbl.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + presetsRow.add(yawPresetLbl); + JComboBox yawPresetCombo = new JComboBox<>(yawLabels); + yawPresetCombo.setMaximumRowCount(yawLabels.length); + // Keep width minimal so Pitch dropdown fits on same row + int comboH = yawPresetCombo.getPreferredSize().height; + yawPresetCombo.setPreferredSize(new Dimension(48, comboH)); + yawPresetCombo.setMaximumSize(new Dimension(48, comboH)); + yawPresetCombo.addItemListener(e -> { + if (e.getStateChange() == ItemEvent.SELECTED) { + int idx = yawPresetCombo.getSelectedIndex(); + if (idx >= 0 && idx < yawPresets.length) + yawSpinner.setValue(yawPresets[idx]); + } + }); + presetsRow.add(yawPresetCombo); + JLabel pitchPresetLbl = new JLabel("Pitch:"); + pitchPresetLbl.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + presetsRow.add(pitchPresetLbl); + JComboBox pitchPresetCombo = new JComboBox<>(pitchLabels); + pitchPresetCombo.setMaximumRowCount(pitchLabels.length); + pitchPresetCombo.addItemListener(e -> { + if (e.getStateChange() == ItemEvent.SELECTED) { + int idx = pitchPresetCombo.getSelectedIndex(); + if (idx >= 0 && idx < pitchPresets.length) + pitchSpinner.setValue(pitchPresets[idx]); + } + }); + presetsRow.add(pitchPresetCombo); + + GridBagConstraints presetsC = new GridBagConstraints(); + presetsC.gridx = 0; + presetsC.gridy = row; + presetsC.gridwidth = 2; + presetsC.anchor = GridBagConstraints.CENTER; + presetsC.insets = new Insets(2, 0, 4, 0); + content.add(presetsRow, presetsC); + row++; + + GridBagConstraints sepC = new GridBagConstraints(); + sepC.gridx = 0; + sepC.gridy = row; + sepC.gridwidth = 2; + sepC.fill = GridBagConstraints.HORIZONTAL; + sepC.weightx = 1; + sepC.insets = new Insets(4, 0, 6, 0); + JPanel sepPanel = new JPanel(); + sepPanel.setPreferredSize(new Dimension(0, 1)); + sepPanel.setMinimumSize(new Dimension(0, 1)); + sepPanel.setBorder(new MatteBorder(1, 0, 0, 0, ColorScheme.MEDIUM_GRAY_COLOR)); + content.add(sepPanel, sepC); + row++; + + JLabel yawLbl = new JLabel("Base Yaw"); + yawLbl.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(yawLbl, labelConstraints(row)); + content.add(yawSpinner, controlConstraints(row)); + row++; + + JLabel pitchLbl = new JLabel("Base Pitch"); + pitchLbl.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(pitchLbl, labelConstraints(row)); + content.add(pitchSpinner, controlConstraints(row)); + row++; + + JLabel heightLbl = new JLabel("Height Offset"); + heightLbl.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(heightLbl, labelConstraints(row)); + JSpinner heightSpinner = new JSpinner(new SpinnerNumberModel(defaults.getHeightOffset(), Integer.MIN_VALUE, Integer.MAX_VALUE, 1)); + content.add(heightSpinner, controlConstraints(row)); + row++; + + JLabel culledLbl = new JLabel("Display When Culled"); + culledLbl.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(culledLbl, labelConstraints(row)); + JCheckBox culledCheck = new JCheckBox("", defaults.isDisplayWhenCulled()); + culledCheck.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + content.add(culledCheck, controlConstraints(row)); + + // Load from selected definition when dropdown changes + ItemListener loadFromDef = e -> { + if (e.getStateChange() != ItemEvent.SELECTED) return; + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + generalLoadingFromDefinition = true; + try { + ParticleDefinition.General g = def.general; + heightSpinner.setValue(g.getHeightOffset()); + int uiYaw = gameToUiYaw(g.getDirectionYaw()); + int pitch = Math.min(1024, Math.max(0, g.getDirectionPitch())); + yawSpinner.setValue(uiYaw); + pitchSpinner.setValue(pitch); + yawPresetCombo.setSelectedIndex(Math.min(7, (uiYaw + 128) / 256 % 8)); + pitchPresetCombo.setSelectedIndex(pitch <= 256 ? 0 : (pitch <= 768 ? 1 : 2)); + culledCheck.setSelected(g.isDisplayWhenCulled()); + } finally { + generalLoadingFromDefinition = false; + } + }; + particleDropdown.addItemListener(loadFromDef); + + // Push General to definition and apply to game (test emitter) when controls change + Runnable pushToGame = () -> { + if (generalLoadingFromDefinition) return; + String id = (String) particleDropdown.getSelectedItem(); + if (id == null) return; + ParticleDefinition def = particleManager.getDefinition(id); + if (def == null) return; + def.general.heightOffset = (Integer) heightSpinner.getValue(); + def.general.directionYaw = uiToGameYaw((Integer) yawSpinner.getValue()); + def.general.directionPitch = (Integer) pitchSpinner.getValue(); + def.general.displayWhenCulled = culledCheck.isSelected(); + clientThread.invoke(() -> particleManager.applyDefinitionToEmittersWithId(id)); + }; + ChangeListener changePush = e -> pushToGame.run(); + heightSpinner.addChangeListener(changePush); + yawSpinner.addChangeListener(changePush); + pitchSpinner.addChangeListener(changePush); + culledCheck.addItemListener(e -> pushToGame.run()); + + // Initial load from default selection ("7") + loadFromDef.itemStateChanged(new ItemEvent(particleDropdown, ItemEvent.ITEM_STATE_CHANGED, particleDropdown.getSelectedItem(), ItemEvent.SELECTED)); + + section.add(content, BorderLayout.CENTER); + return section; + } + + private static JPanel buildDefinitionSection(String title, String[] subHeadings, String[][] contentLines) { + JPanel section = new JPanel(); + section.setLayout(new BorderLayout(0, 0)); + TitledBorder titledBorder = new TitledBorder( + new MatteBorder(1, 1, 1, 1, ColorScheme.MEDIUM_GRAY_COLOR), + title, + TitledBorder.LEFT, + TitledBorder.TOP); + titledBorder.setTitleColor(ColorScheme.LIGHT_GRAY_COLOR); + titledBorder.setTitleFont(titledBorder.getTitleFont().deriveFont(Font.BOLD)); + section.setBorder(titledBorder); + + // Content: sub-headings with indented content lines + JPanel content = new JPanel(); + content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS)); + content.setBorder(new EmptyBorder(1, 1, 1, 1)); + int lineIndent = 20; + for (int i = 0; i < subHeadings.length; i++) { + JLabel subLabel = new JLabel(subHeadings[i]); + subLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + subLabel.setBorder(new EmptyBorder(6, 0, 2, 0)); + content.add(subLabel); + String[] lines = i < contentLines.length ? contentLines[i] : new String[0]; + for (String line : lines) { + JLabel lineLabel = new JLabel(line); + lineLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + lineLabel.setBorder(new EmptyBorder(1, lineIndent, 1, 0)); + content.add(lineLabel); + } + } + section.add(content, BorderLayout.CENTER); + return section; + } + + private static void styleButton(JButton btn) { + btn.setFocusPainted(false); + btn.setBorderPainted(true); + btn.setContentAreaFilled(true); + btn.setOpaque(true); + } + + private static void setButtonActive(JButton btn, boolean active) { + if (active) { + btn.setBackground(ACTIVE_BUTTON_GREEN); + btn.setForeground(Color.WHITE); + } else { + btn.setBackground(ColorScheme.DARKER_GRAY_COLOR); + btn.setForeground(Color.WHITE); + } + } + + private JPanel buildDebugPanel() { + JPanel p = new JPanel(); + p.setLayout(new BorderLayout()); + p.setBackground(ColorScheme.DARK_GRAY_COLOR); + p.setBorder(new EmptyBorder(15, 15, 15, 15)); + + JPanel buttons = new JPanel(); + buttons.setLayout(new GridLayout(0, 1, 0, 4)); + + JButton gizmoOverlayBtn = new JButton("Particle gizmo overlay"); + gizmoOverlayBtn.setToolTipText("Show emitter gizmos in-game. Right-click a tile with emitters for menu options."); + styleButton(gizmoOverlayBtn); + gizmoOverlayBtn.addActionListener(e -> { + gizmoOverlayActive = !gizmoOverlayActive; + particleGizmoOverlay.setActive(gizmoOverlayActive); + setButtonActive(gizmoOverlayBtn, gizmoOverlayActive); + }); + buttons.add(gizmoOverlayBtn); + + JButton testParticlesBtn = new JButton("Test particles"); + styleButton(testParticlesBtn); + testParticlesBtn.addActionListener(e -> { + clientThread.invoke(() -> { + if (particleManager.hasPerformanceTestEmitters()) { + particleManager.despawnPerformanceTestEmitters(); + setButtonActive(testParticlesBtn, false); + testParticlesBtn.setText("Test particles"); + } else { + particleManager.spawnPerformanceTestEmitters(); + setButtonActive(testParticlesBtn, true); + testParticlesBtn.setText("Despawn Test Particles"); + } + }); + }); + buttons.add(testParticlesBtn); + + JButton spawn4096Btn = new JButton("Spawn 4096 particles"); + spawn4096Btn.setToolTipText("Toggle continuous spawning of particles around the player (maintains ~4096 until turned off)"); + styleButton(spawn4096Btn); + boolean initialSpawn = particleManager.isContinuousRandomSpawn(); + setButtonActive(spawn4096Btn, initialSpawn); + spawn4096Btn.setText(initialSpawn ? "Stop spawning" : "Spawn 4096 particles"); + spawn4096Btn.addActionListener(e -> { + clientThread.invoke(() -> { + boolean on = !particleManager.isContinuousRandomSpawn(); + particleManager.setContinuousRandomSpawn(on); + setButtonActive(spawn4096Btn, on); + spawn4096Btn.setText(on ? "Stop spawning" : "Spawn 4096 particles"); + }); + }); + buttons.add(spawn4096Btn); + + p.add(buttons, BorderLayout.NORTH); + return p; + } + + /** JPanel that implements Scrollable so the view tracks viewport width and does not expand horizontally. */ + private static class ScrollablePanel extends JPanel implements Scrollable { + @Override + public boolean getScrollableTracksViewportWidth() { + return true; + } + @Override + public boolean getScrollableTracksViewportHeight() { + return false; + } + @Override + public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { + return 16; + } + @Override + public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { + return orientation == javax.swing.SwingConstants.VERTICAL ? visibleRect.height : visibleRect.width; + } + @Override + public Dimension getPreferredScrollableViewportSize() { + return getPreferredSize(); + } + } +} diff --git a/src/main/java/rs117/hd/scene/particles/definition/ParticleDefinition.java b/src/main/java/rs117/hd/scene/particles/definition/ParticleDefinition.java new file mode 100644 index 0000000000..76addd33b9 --- /dev/null +++ b/src/main/java/rs117/hd/scene/particles/definition/ParticleDefinition.java @@ -0,0 +1,368 @@ +/* + * Copyright (c) 2025, Mark7625 (https://github.com/Mark7625/) + * All rights reserved. + */ +package rs117.hd.scene.particles.definition; + +import com.google.gson.Gson; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nullable; +import javax.inject.Inject; +import javax.inject.Singleton; +import lombok.Data; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.callback.ClientThread; +import rs117.hd.HdPlugin; +import rs117.hd.utils.FileWatcher; +import rs117.hd.utils.Props; +import rs117.hd.utils.ResourcePath; + +import static rs117.hd.utils.ResourcePath.path; + +/** + * Particle definition (nested sections) and loader for particles.json. + * One singleton instance holds the definitions map and loader methods; map values are definition instances. + */ +@Slf4j +@Singleton +public class ParticleDefinition { + + public static final float NO_TARGET = -1f; + + public static final ResourcePath PARTICLES_CONFIG_PATH = Props.getFile( + "rlhd.particles-config-path", + () -> path(ParticleDefinition.class, "..", "particles.json") + ); + + @Inject + transient HdPlugin plugin; + @Inject + transient ClientThread clientThread; + + @Getter + private final transient Map definitions = new LinkedHashMap<>(); + private transient FileWatcher.UnregisterCallback watcher; + @Getter + private transient int lastDefinitionCount; + @Getter + private transient long lastLoadTimeMs; + + public String id; + @Nullable + public String description; + + public General general = new General(); + public Texture texture = new Texture(); + public Spread spread = new Spread(); + public Speed speed = new Speed(); + public Scale scale = new Scale(); + public Colours colours = new Colours(); + public Emission emission = new Emission(); + public Physics physics = new Physics(); + + public boolean hasLevelBounds; + public int startRed, endRed, deltaRed; + public int startGreen, endGreen, deltaGreen; + public int startBlue, endBlue, deltaBlue; + public int startAlpha, endAlpha, deltaAlpha; + public int scaleTransitionTicks, scaleIncrementPerTick; + public int targetScaleRef; + public int speedTransitionTicks, speedIncrementPerTick; + public int alphaTransitionTicks, colourTransitionTicks; + public int redIncrementPerTick, greenIncrementPerTick, blueIncrementPerTick, alphaIncrementPerTick; + + private static final float TICKS_TO_SEC = 64f; + private static final float U8 = 256f * 256f; + @Nullable + public float[] colourIncrementPerSecond; + public float colourTransitionSecondsConstant; + public float scaleIncrementPerSecondCached; + public float scaleTransitionSecondsConstant; + public float speedIncrementPerSecondCached; + public float speedTransitionSecondsConstant; + + @Data + public static class General { + public int heightOffset = 10; + public int directionPitch = 30; + public int directionYaw = 1024; + public boolean displayWhenCulled; + /** Max random yaw rotation (radians/sec) applied during lifecycle. 0 = no rotation. */ + public float randomYawRotation; + } + + @Data + public static class Texture { + @Nullable + public String file; + public Flipbook flipbook = new Flipbook(); + } + + @Data + public static class Flipbook { + public int flipbookColumns; + public int flipbookRows; + @Nullable + public String flipbookMode; + } + + @Data + public static class Spread { + public float yawMin; + public float yawMax; + public float pitchMin; + public float pitchMax; + } + + @Data + public static class Speed { + public float minSpeed; + public float maxSpeed; + public float targetSpeed = NO_TARGET; + public int speedTransitionPercent = 100; + } + + @Data + public static class Scale { + public float minScale; + public float maxScale; + public float targetScale = NO_TARGET; + public int scaleTransitionPercent = 100; + } + + @Data + public static class Colours { + public int minColourArgb; + public int maxColourArgb; + public int targetColourArgb; + @Nullable + public String minColour; + @Nullable + public String maxColour; + @Nullable + public String targetColour; + public int colourTransitionPercent = 100; + public int alphaTransitionPercent = 100; + public boolean uniformColourVariation = true; + public boolean useSceneAmbientLight = true; + } + + @Data + public static class Emission { + public int minDelay; + public int maxDelay; + public int minSpawn; + public int maxSpawn; + public int initialSpawn; + public int emissionTimeThreshold = -1; + public int emissionCycleDuration = -1; + public boolean emitOnlyBeforeTime = true; + public boolean loopEmission = true; + } + + @Data + public static class Physics { + public int upperBoundLevel = -2; + public int lowerBoundLevel = -2; + public boolean clipToTerrain = true; + public boolean collidesWithObjects; + public int distanceFalloffType; + public int distanceFalloffStrength; + } + + public void postDecode() { + if (physics.upperBoundLevel > -2 || physics.lowerBoundLevel > -2) { + hasLevelBounds = true; + } + startRed = (colours.minColourArgb >> 16) & 0xff; + endRed = (colours.maxColourArgb >> 16) & 0xff; + deltaRed = endRed - startRed; + startGreen = (colours.minColourArgb >> 8) & 0xff; + endGreen = (colours.maxColourArgb >> 8) & 0xff; + deltaGreen = endGreen - startGreen; + startBlue = colours.minColourArgb & 0xff; + endBlue = colours.maxColourArgb & 0xff; + deltaBlue = endBlue - startBlue; + startAlpha = (colours.minColourArgb >> 24) & 0xff; + endAlpha = (colours.maxColourArgb >> 24) & 0xff; + deltaAlpha = endAlpha - startAlpha; + + float targetScaleVal = scale.targetScale >= 0 ? (float) scale.targetScale : NO_TARGET; + float targetSpeedVal = speed.targetSpeed >= 0 ? (float) speed.targetSpeed : NO_TARGET; + int maxDelay = emission.maxDelay; + + if (targetScaleVal >= 0f) { + scaleTransitionTicks = scale.scaleTransitionPercent * maxDelay / 100; + if (scaleTransitionTicks == 0) scaleTransitionTicks = 1; + targetScaleRef = (int) Math.round(targetScaleVal / 4f * 16384f); + float midScaleRef = (scale.minScale + scale.maxScale) / 2f / 4f * 16384f; + scaleIncrementPerTick = (int) Math.round((targetScaleRef - midScaleRef) / scaleTransitionTicks); + } + if (targetSpeedVal >= 0f) { + speedTransitionTicks = maxDelay * speed.speedTransitionPercent / 100; + if (speedTransitionTicks == 0) speedTransitionTicks = 1; + speedIncrementPerTick = (int) Math.round((targetSpeedVal - (speed.minSpeed + speed.maxSpeed) / 2f) / speedTransitionTicks); + } + if (colours.targetColourArgb != 0) { + alphaTransitionTicks = colours.alphaTransitionPercent * maxDelay / 100; + colourTransitionTicks = maxDelay * colours.colourTransitionPercent / 100; + if (colourTransitionTicks == 0) colourTransitionTicks = 1; + if (alphaTransitionTicks == 0) alphaTransitionTicks = 1; + int targetRed = (colours.targetColourArgb >> 16) & 0xff; + int targetGreen = (colours.targetColourArgb >> 8) & 0xff; + int targetBlue = colours.targetColourArgb & 0xff; + int targetAlpha = (colours.targetColourArgb >> 24) & 0xff; + redIncrementPerTick = ((targetRed - (startRed + deltaRed / 2)) << 8) / colourTransitionTicks; + greenIncrementPerTick = ((targetGreen - (startGreen + deltaGreen / 2)) << 8) / colourTransitionTicks; + blueIncrementPerTick = ((targetBlue - (startBlue + deltaBlue / 2)) << 8) / colourTransitionTicks; + alphaIncrementPerTick = ((targetAlpha - (startAlpha + deltaAlpha / 2)) << 8) / alphaTransitionTicks; + redIncrementPerTick += (redIncrementPerTick <= 0 ? 4 : -4); + greenIncrementPerTick += (greenIncrementPerTick <= 0 ? 4 : -4); + blueIncrementPerTick += (blueIncrementPerTick <= 0 ? 4 : -4); + alphaIncrementPerTick += (alphaIncrementPerTick <= 0 ? 4 : -4); + colourIncrementPerSecond = new float[] { + redIncrementPerTick * TICKS_TO_SEC / U8, + greenIncrementPerTick * TICKS_TO_SEC / U8, + blueIncrementPerTick * TICKS_TO_SEC / U8, + alphaIncrementPerTick * TICKS_TO_SEC / U8 + }; + colourTransitionSecondsConstant = colourTransitionTicks / TICKS_TO_SEC; + } + if (targetScaleVal >= 0f) { + scaleIncrementPerSecondCached = scaleIncrementPerTick * TICKS_TO_SEC / 16384f * 4f; + scaleTransitionSecondsConstant = scaleTransitionTicks / TICKS_TO_SEC; + } + if (targetSpeedVal >= 0f) { + speedIncrementPerSecondCached = speedIncrementPerTick * TICKS_TO_SEC / 16384f; + speedTransitionSecondsConstant = speedTransitionTicks / TICKS_TO_SEC; + } + } + + public static int hexToArgb(String hex) { + if (hex == null || hex.isEmpty()) return 0; + String s = hex.startsWith("#") ? hex.substring(1) : hex; + if (s.length() != 6 && s.length() != 8) return 0; + try { + int r = Integer.parseInt(s.substring(0, 2), 16); + int g = Integer.parseInt(s.substring(2, 4), 16); + int b = Integer.parseInt(s.substring(4, 6), 16); + int a = s.length() == 8 ? Integer.parseInt(s.substring(6, 8), 16) : 255; + return (a << 24) | (r << 16) | (g << 8) | b; + } catch (NumberFormatException e) { + return 0; + } + } + + public void parseHexColours() { + if (colours.minColour != null) { + int argb = hexToArgb(colours.minColour); + if (argb != 0) colours.minColourArgb = argb; + } + if (colours.maxColour != null) { + int argb = hexToArgb(colours.maxColour); + if (argb != 0) colours.maxColourArgb = argb; + } + if (colours.targetColour != null) { + colours.targetColourArgb = hexToArgb(colours.targetColour); + } + } + + public static String argbToHex(int argb) { + int r = (argb >> 16) & 0xff; + int g = (argb >> 8) & 0xff; + int b = argb & 0xff; + int a = (argb >> 24) & 0xff; + return String.format("#%02x%02x%02x%02x", r, g, b, a); + } + + public static float[] argbToFloat(int argb) { + return new float[] { + ((argb >> 16) & 0xff) / 255f, + ((argb >> 8) & 0xff) / 255f, + (argb & 0xff) / 255f, + ((argb >> 24) & 0xff) / 255f + }; + } + + public void startup(Runnable onReload) { + watcher = PARTICLES_CONFIG_PATH.watch((p, first) -> { + loadConfig(); + clientThread.invoke(onReload); + }); + } + + public void shutdown() { + if (watcher != null) { + watcher.unregister(); + watcher = null; + } + } + + public void loadConfig() { + long start = System.nanoTime(); + Gson gson = plugin.getGson(); + ParticleDefinition[] defs; + try { + defs = PARTICLES_CONFIG_PATH.loadJson(gson, ParticleDefinition[].class); + } catch (IOException ex) { + log.error("[Particles] Failed to load particles.json from {}", PARTICLES_CONFIG_PATH, ex); + return; + } + definitions.clear(); + List ordered = new ArrayList<>(); + if (defs != null) { + for (ParticleDefinition def : defs) { + if (def.id != null && !def.id.isEmpty()) + def.id = def.id.toUpperCase(); + def.parseHexColours(); + def.postDecode(); + if (def.id == null || def.id.isEmpty()) { + log.warn("[Particles] Skipping definition with missing id"); + continue; + } + if (definitions.put(def.id, def) != null) + log.warn("[Particles] Duplicate particle id: {}", def.id); + ordered.add(def); + } + } + lastDefinitionCount = definitions.size(); + lastLoadTimeMs = (System.nanoTime() - start) / 1_000_000; + } + + @Nullable + public ParticleDefinition getDefinition(String id) { + return definitions.get(id); + } + + public List getDefinitionIdsOrdered() { + return new ArrayList<>(definitions.keySet()); + } + + public List getAvailableTextureNames() { + Set names = new LinkedHashSet<>(); + names.add(""); + for (ParticleDefinition def : definitions.values()) { + String file = def.texture.file; + if (file != null && !file.isEmpty()) + names.add(file); + } + return new ArrayList<>(names); + } + + @Nullable + public String getDefaultTexturePath() { + for (ParticleDefinition def : definitions.values()) { + String file = def.texture.file; + if (file != null && !file.isEmpty()) + return file; + } + return null; + } +} diff --git a/src/main/java/rs117/hd/scene/particles/emitter/EmitterConfigEntry.java b/src/main/java/rs117/hd/scene/particles/emitter/EmitterConfigEntry.java new file mode 100644 index 0000000000..62539c561d --- /dev/null +++ b/src/main/java/rs117/hd/scene/particles/emitter/EmitterConfigEntry.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025, Mark7625 (https://github.com/Mark7625/) + * All rights reserved. + */ +package rs117.hd.scene.particles.emitter; + +import java.util.List; +import javax.annotation.Nullable; +import lombok.Data; +import lombok.Getter; +import rs117.hd.scene.lights.Alignment; + +@Data +public class EmitterConfigEntry { + @Nullable + public int[][] placements; + @Nullable + public List objectEmitters; + @Nullable + public String description; + public String particleId; + + /** Multiple particle IDs (overrides particleId when set). */ + @Nullable + public List particleIds; + + /** Weather area names from areas.json. Resolved to AABBs for each particleId. */ + @Nullable + public List weatherAreas; + + /** Tiles from edge (inside) to fade particles to 0 alpha. 0 = no inside fade. */ + public int edgeFadeInside; + + /** Tiles from edge (outside) for margin fade. 0 = no outside margin. Default 2. */ + public int edgeFadeOutside = 2; + + /** Emitter every N tiles. E.g. 10 = 1 emitter per 10 tiles. 0 = no cap (use full placement). */ + public int weatherEveryNTiles; + + /** Grid spacing for weather placements (tiles between emitters). Default 3. */ + public int weatherSpacing = 3; + + /** Object-specific emitter config. JSON: { "object": "...", "offsetX": 0, "offsetY": 0, "offsetZ": 0, "alignment": "CUSTOM" } */ + @Data + public static class ObjectBinding { + @Nullable + public String object; + public int offsetX; + public int offsetY; + public int offsetZ; + @Nullable + @Getter(lombok.AccessLevel.NONE) + public Alignment alignment; + /** Set when loading from parent entry (entry.particleId). */ + @Nullable + public String particleId; + + public Alignment getAlignment() { + return alignment != null ? alignment : Alignment.CUSTOM; + } + } +} diff --git a/src/main/java/rs117/hd/scene/particles/emitter/EmitterDefinitionManager.java b/src/main/java/rs117/hd/scene/particles/emitter/EmitterDefinitionManager.java new file mode 100644 index 0000000000..5b6396e0a2 --- /dev/null +++ b/src/main/java/rs117/hd/scene/particles/emitter/EmitterDefinitionManager.java @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2025, Mark7625 (https://github.com/Mark7625/) + * All rights reserved. + */ +package rs117.hd.scene.particles.emitter; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; +import com.google.gson.Gson; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import javax.inject.Inject; +import javax.inject.Singleton; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.callback.ClientThread; +import rs117.hd.HdPlugin; +import rs117.hd.scene.AreaManager; +import rs117.hd.scene.GamevalManager; +import rs117.hd.scene.areas.AABB; +import rs117.hd.scene.particles.definition.ParticleDefinition; +import rs117.hd.utils.FileWatcher; +import rs117.hd.utils.Props; +import rs117.hd.utils.ResourcePath; + +import static rs117.hd.utils.ResourcePath.path; + +@Slf4j +@Singleton +public class EmitterDefinitionManager { + + private static final ResourcePath EMITTERS_CONFIG_PATH = Props.getFile( + "rlhd.emitters-config-path", + () -> path(ParticleDefinition.class, "..", "emitters.json") + ); + + @Inject + private GamevalManager gamevalManager; + + @Inject + private AreaManager areaManager; + + @Inject + private HdPlugin plugin; + + @Inject + private ClientThread clientThread; + + @Getter + private final List placements = new ArrayList<>(); + + @Getter + private final ListMultimap objectBindingsByType = ArrayListMultimap.create(); + + @Getter + private final List definitionEmitters = new ArrayList<>(); + + @Getter + private final List weatherAreaConfigs = new ArrayList<>(); + + /** Placements at center tile of each weather AABB, for spawning like regular tile emitters. */ + @Getter + private final List weatherPlacements = new ArrayList<>(); + + private FileWatcher.UnregisterCallback watcher; + + @Getter + private int lastPlacements; + @Getter + private int lastObjectBindings; + @Getter + private long lastLoadTimeMs; + + /** + * Load config and register file watcher for hot-reload. When config changes, reloads then runs {@code onReload} on the client thread. + */ + public void startup(Runnable onReload) { + watcher = EMITTERS_CONFIG_PATH.watch((path, first) -> { + loadConfig(); + clientThread.invoke(onReload); + }); + } + + public void shutdown() { + if (watcher != null) { + watcher.unregister(); + watcher = null; + } + } + + /** + * Load from the given path (e.g. for tests or custom paths). + */ + public void loadConfig() { + long start = System.nanoTime(); + try { + EmitterConfigEntry[] entries = EMITTERS_CONFIG_PATH.loadJson(plugin.getGson(), EmitterConfigEntry[].class); + placements.clear(); + objectBindingsByType.clear(); + weatherAreaConfigs.clear(); + weatherPlacements.clear(); + if (entries != null) { + var objects = gamevalManager.getObjects(); + for (EmitterConfigEntry entry : entries) { + List pids = getParticleIds(entry); + if (pids.isEmpty()) continue; + String pid = pids.get(0); + if (entry.placements != null) { + for (int[] p : entry.placements) { + if (p != null && p.length >= 3) { + for (String pid2 : pids) { + placements.add(new EmitterPlacement(p[0], p[1], p[2], pid2, 1f)); + } + } + } + } + if (objects != null && entry.objectEmitters != null) { + for (EmitterConfigEntry.ObjectBinding b : entry.objectEmitters) { + if (b == null || b.object == null || b.object.isEmpty()) continue; + Integer id = objects.get(b.object); + if (id != null) { + for (String pid2 : pids) { + var binding = new EmitterConfigEntry.ObjectBinding(); + binding.object = b.object; + binding.offsetX = b.offsetX; + binding.offsetY = b.offsetY; + binding.offsetZ = b.offsetZ; + binding.alignment = b.alignment; + binding.particleId = pid2; + objectBindingsByType.put(id, binding); + } + } else { + log.warn("[Particles] Unknown object gameval in emitters.json: {}", b.object); + } + } + } + if (entry.weatherAreas != null && !entry.weatherAreas.isEmpty()) { + List aabbs = new ArrayList<>(); + for (String areaName : entry.weatherAreas) { + var area = areaManager.getArea(areaName); + if (area != null && !"NONE".equals(area.name) && area.aabbs != null) { + Collections.addAll(aabbs, area.aabbs); + } else { + log.warn("[Particles] Unknown area in weatherAreas: {}", areaName); + } + } + if (!aabbs.isEmpty()) { + int padding = Math.max(0, entry.edgeFadeInside); + int paddingOutside = Math.max(0, entry.edgeFadeOutside); + int everyNTiles = Math.max(0, entry.weatherEveryNTiles); + int spacing = Math.max(1, entry.weatherSpacing); + weatherAreaConfigs.add(new WeatherAreaConfig(aabbs, new ArrayList<>(pids), padding, paddingOutside)); + addWeatherPlacementsStandard(aabbs, pids, padding, paddingOutside, everyNTiles, spacing, ThreadLocalRandom.current()); + } + } + } + } + lastPlacements = placements.size(); + lastObjectBindings = objectBindingsByType.size(); + lastLoadTimeMs = (System.nanoTime() - start) / 1_000_000; + } catch (IOException ex) { + log.error("[Particles] Failed to load emitters.json from {}", EMITTERS_CONFIG_PATH, ex); + placements.clear(); + objectBindingsByType.clear(); + weatherAreaConfigs.clear(); + weatherPlacements.clear(); + } + } + + private void addWeatherPlacementsStandard(List aabbs, List pids, int padding, int paddingOutside, int everyNTiles, int spacing, ThreadLocalRandom rng) { + for (AABB aabb : aabbs) { + List aabbPlacements = new ArrayList<>(); + int plane = aabbPlane(aabb); + int width = aabb.maxX - aabb.minX + 1; + int height = aabb.maxY - aabb.minY + 1; + int tileCount = width * height; + + for (int x = aabb.minX; x <= aabb.maxX; x += spacing) { + for (int y = aabb.minY; y <= aabb.maxY; y += spacing) { + int jitterX = rng.nextInt(-spacing, spacing + 1); + int jitterY = rng.nextInt(-spacing, spacing + 1); + int wx = Math.max(aabb.minX, Math.min(aabb.maxX, x + jitterX)); + int wy = Math.max(aabb.minY, Math.min(aabb.maxY, y + jitterY)); + String pid2 = pids.get(rng.nextInt(pids.size())); + aabbPlacements.add(new EmitterPlacement(wx, wy, plane, pid2, 1f)); + } + } + int extraCount = rng.nextInt(width * height / (spacing * spacing) + 1); + for (int i = 0; i < extraCount; i++) { + int wx = rng.nextInt(aabb.minX, aabb.maxX + 1); + int wy = rng.nextInt(aabb.minY, aabb.maxY + 1); + String pid2 = pids.get(rng.nextInt(pids.size())); + aabbPlacements.add(new EmitterPlacement(wx, wy, plane, pid2, 1f)); + } + if (padding > 0) { + int edgeZoneTiles = Math.max(4, (width + height) * padding); + for (int i = 0; i < edgeZoneTiles; i++) { + int wx = rng.nextInt(aabb.minX, aabb.maxX + 1); + int wy = rng.nextInt(aabb.minY, aabb.maxY + 1); + int distToEdge = distToEdge(wx, wy, aabb); + if (distToEdge >= padding) continue; + float t = (float) distToEdge / padding; + if (rng.nextFloat() > t) continue; + String pid2 = pids.get(rng.nextInt(pids.size())); + aabbPlacements.add(new EmitterPlacement(wx, wy, plane, pid2, t)); + } + if (paddingOutside > 0) { + int outsideTiles = Math.max(4, (width + height) * paddingOutside); + for (int i = 0; i < outsideTiles; i++) { + int wx = rng.nextInt(aabb.minX - paddingOutside, aabb.maxX + paddingOutside + 1); + int wy = rng.nextInt(aabb.minY - paddingOutside, aabb.maxY + paddingOutside + 1); + if (wx >= aabb.minX && wx <= aabb.maxX && wy >= aabb.minY && wy <= aabb.maxY) continue; + int distOutside = distOutside(wx, wy, aabb); + if (distOutside > paddingOutside) continue; + float t = 1f - (float) distOutside / paddingOutside; + if (rng.nextFloat() > t * t) continue; + String pid2 = pids.get(rng.nextInt(pids.size())); + aabbPlacements.add(new EmitterPlacement(wx, wy, plane, pid2, t)); + } + } + } + + if (everyNTiles > 0 && tileCount > 0 && aabbPlacements.size() > 0) { + int maxPlacements = Math.max(1, tileCount / everyNTiles); + if (aabbPlacements.size() > maxPlacements) { + // Stratified sampling: divide area into cells, pick one per cell to avoid gaps + int n = Math.max(1, (int) Math.sqrt(maxPlacements)); + int numCellsX = Math.max(1, Math.min(n, width)); + int numCellsY = Math.max(1, Math.min(n, height)); + List> cells = new ArrayList<>(numCellsX * numCellsY); + for (int c = 0; c < numCellsX * numCellsY; c++) + cells.add(new ArrayList<>()); + for (EmitterPlacement p : aabbPlacements) { + int cx = (p.getWorldX() - aabb.minX) * numCellsX / width; + int cy = (p.getWorldY() - aabb.minY) * numCellsY / height; + cx = Math.max(0, Math.min(numCellsX - 1, cx)); + cy = Math.max(0, Math.min(numCellsY - 1, cy)); + cells.get(cy * numCellsX + cx).add(p); + } + aabbPlacements = new ArrayList<>(); + for (List cell : cells) { + if (!cell.isEmpty()) + aabbPlacements.add(cell.get(rng.nextInt(cell.size()))); + } + if (aabbPlacements.size() > maxPlacements) { + Collections.shuffle(aabbPlacements, rng); + aabbPlacements = aabbPlacements.subList(0, maxPlacements); + } + } + } + weatherPlacements.addAll(aabbPlacements); + } + } + + private static int distToEdge(int wx, int wy, AABB aabb) { + int distX = Math.min(wx - aabb.minX, aabb.maxX - wx); + int distY = Math.min(wy - aabb.minY, aabb.maxY - wy); + return Math.min(distX, distY); + } + + private static int distOutside(int wx, int wy, AABB aabb) { + int d = Integer.MAX_VALUE; + if (wx < aabb.minX) d = Math.min(d, aabb.minX - wx); + if (wx > aabb.maxX) d = Math.min(d, wx - aabb.maxX); + if (wy < aabb.minY) d = Math.min(d, aabb.minY - wy); + if (wy > aabb.maxY) d = Math.min(d, wy - aabb.maxY); + return d == Integer.MAX_VALUE ? 0 : d; + } + + private static int aabbPlane(AABB aabb) { + if (aabb.hasZ()) return Math.max(0, Math.min(2, aabb.minZ)); + return 0; + } + + private static List getParticleIds(EmitterConfigEntry entry) { + if (entry.particleIds != null && !entry.particleIds.isEmpty()) { + return entry.particleIds.stream().map(String::toUpperCase).toList(); + } + if (entry.particleId != null && !entry.particleId.isEmpty()) { + return List.of(entry.particleId.toUpperCase()); + } + return List.of(); + } + +} diff --git a/src/main/java/rs117/hd/scene/particles/emitter/EmitterPlacement.java b/src/main/java/rs117/hd/scene/particles/emitter/EmitterPlacement.java new file mode 100644 index 0000000000..1747a35d3b --- /dev/null +++ b/src/main/java/rs117/hd/scene/particles/emitter/EmitterPlacement.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2025, Mark7625 (https://github.com/Mark7625/) + * All rights reserved. + */ +package rs117.hd.scene.particles.emitter; + +import javax.annotation.Nullable; +import lombok.Value; + +/** A single world placement for a particle emitter (worldX, worldY, plane + particleId). */ +@Value +public class EmitterPlacement { + int worldX; + int worldY; + int plane; + @Nullable + String particleId; + /** Alpha scale for edge fade (1 = full, 0 = invisible). Used by weather. */ + float edgeFadeFactor; +} diff --git a/src/main/java/rs117/hd/scene/particles/emitter/ParticleEmitter.java b/src/main/java/rs117/hd/scene/particles/emitter/ParticleEmitter.java new file mode 100644 index 0000000000..c07a09f4f1 --- /dev/null +++ b/src/main/java/rs117/hd/scene/particles/emitter/ParticleEmitter.java @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2025, Mark7625 (https://github.com/Mark7625/) + * All rights reserved. + */ +package rs117.hd.scene.particles.emitter; + +import java.util.concurrent.ThreadLocalRandom; +import javax.annotation.Nullable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import net.runelite.api.TileObject; +import net.runelite.api.coords.WorldPoint; +import rs117.hd.scene.lights.Alignment; +import rs117.hd.scene.particles.core.buffer.ParticleBuffer; +import rs117.hd.scene.particles.core.Particle; +import rs117.hd.scene.particles.definition.ParticleDefinition; + +import static rs117.hd.utils.MathUtils.*; + +@Data +@NoArgsConstructor +@AllArgsConstructor(access = lombok.AccessLevel.PACKAGE) +@Builder +public class ParticleEmitter { + private static final float[] TMP_DIR = new float[3]; + + @Nullable + private WorldPoint worldPoint; + + @Nullable + private TileObject tileObject; + + @Nullable + private String particleId; + + @Builder.Default + private float heightOffset = 50f; + @Builder.Default + private float offsetX = 0f; + @Builder.Default + private float offsetY = 0f; + @Builder.Default + private float offsetZ = 0f; + @Builder.Default + private Alignment alignment = Alignment.CUSTOM; + private int orientation; + @Builder.Default + private int sizeX = 1; + @Builder.Default + private int sizeY = 1; + @Builder.Default + private float directionYaw = 0f; + @Builder.Default + private float directionPitch = 0f; + private float spreadYawMin; + private float spreadYawMax; + private float spreadPitchMin; + private float spreadPitchMax; + @Builder.Default + private float speedMin = 20f; + @Builder.Default + private float speedMax = 60f; + @Builder.Default + private float targetSpeed = -1f; + @Builder.Default + private float speedTransition = 2f; + private float emissionAccum; + @Builder.Default + private float particleLifeMin = 0.5f; + @Builder.Default + private float particleLifeMax = 1.5f; + @Builder.Default + private float sizeMin = 2f; + @Builder.Default + private float sizeMax = 6f; + private float targetScale; + @Builder.Default + private float scaleTransition = 1f; + @Builder.Default + private float[] colorMin = new float[] { 1f, 0.9f, 0.5f, 0.9f }; + @Builder.Default + private float[] colorMax = new float[] { 1f, 0.9f, 0.5f, 0.9f }; + private float[] targetColor; + @Builder.Default + private float colorTransitionPct = 100f; + @Builder.Default + private float alphaTransitionPct = 100f; + private boolean uniformColorVariation; + @Builder.Default + private float emissionSpawnMin = 1f; + @Builder.Default + private float emissionSpawnMax = 1f; + @Builder.Default + private float alphaScale = 1f; + /** When true, spawn at fixed top-of-world height (for weather). Ignores tile height. */ + @Builder.Default + private boolean spawnAtTopOfWorld = false; + private int initialSpawn; + private boolean initialSpawnDone; + @Builder.Default + private boolean active = true; + @Nullable + private ParticleDefinition definition; + + private long cycleStartCycle; + @Builder.Default + private int emissionCycleDurationTicks = 65535; + @Builder.Default + private int emissionTimeThresholdTicks = 65535; + @Builder.Default + private boolean emitOnlyBeforeTime = true; + @Builder.Default + private boolean loopEmission = true; + + public ParticleEmitter at(WorldPoint worldPoint) { + this.worldPoint = worldPoint; + return this; + } + + public ParticleEmitter particleId(String id) { + this.particleId = id; + return this; + } + + public ParticleEmitter heightOffset(float aboveGround) { + this.heightOffset = aboveGround; + return this; + } + + public ParticleEmitter positionOffset(float x, float z, float height) { + this.offsetX = x; + this.offsetY = z; + this.offsetZ = height; + return this; + } + + public ParticleEmitter direction(float yawRad, float pitchRad) { + this.directionYaw = yawRad; + this.directionPitch = pitchRad; + return this; + } + + public ParticleEmitter spreadYaw(float minRad, float maxRad) { + this.spreadYawMin = minRad; + this.spreadYawMax = maxRad; + return this; + } + + public ParticleEmitter spreadPitch(float minRad, float maxRad) { + this.spreadPitchMin = minRad; + this.spreadPitchMax = maxRad; + return this; + } + + public ParticleEmitter speed(float min, float max) { + this.speedMin = min; + this.speedMax = max; + return this; + } + + public ParticleEmitter targetSpeed(float speed) { + return targetSpeed(speed, 2f); + } + + public ParticleEmitter targetSpeed(float speed, float transitionPerSecond) { + this.targetSpeed = speed; + this.speedTransition = transitionPerSecond; + return this; + } + + public ParticleEmitter particleLifetime(float minSeconds, float maxSeconds) { + this.particleLifeMin = minSeconds; + this.particleLifeMax = maxSeconds; + return this; + } + + public ParticleEmitter size(float minPixels, float maxPixels) { + this.sizeMin = minPixels; + this.sizeMax = maxPixels; + return this; + } + + public ParticleEmitter targetScale(float target, float transitionPerSecond) { + this.targetScale = target; + this.scaleTransition = transitionPerSecond; + return this; + } + + public ParticleEmitter color(float r, float g, float b, float a) { + this.colorMin[0] = this.colorMax[0] = r; + this.colorMin[1] = this.colorMax[1] = g; + this.colorMin[2] = this.colorMax[2] = b; + this.colorMin[3] = this.colorMax[3] = a; + return this; + } + + public ParticleEmitter colorRange(float[] min, float[] max) { + if (min != null && min.length >= 4) System.arraycopy(min, 0, colorMin, 0, 4); + if (max != null && max.length >= 4) System.arraycopy(max, 0, colorMax, 0, 4); + return this; + } + + public ParticleEmitter targetColor(float[] target, float colorTransitionPct, float alphaTransitionPct) { + this.targetColor = target != null && target.length >= 4 ? target : null; + this.colorTransitionPct = colorTransitionPct; + this.alphaTransitionPct = alphaTransitionPct; + return this; + } + + public ParticleEmitter uniformColorVariation(boolean use) { + this.uniformColorVariation = use; + return this; + } + + public ParticleEmitter emissionBurst(int minSpawnCount, int maxSpawnCount, int initialSpawnCount) { + this.emissionSpawnMin = minSpawnCount; + this.emissionSpawnMax = Math.max(minSpawnCount, maxSpawnCount); + this.initialSpawn = initialSpawnCount; + return this; + } + + public void setDefinition(@Nullable ParticleDefinition def) { + this.definition = def; + } + + public void setEmissionTime(long cycleStartCycle, int cycleDurationTicks, int thresholdTicks, boolean emitOnlyBeforeTime, boolean loopEmission) { + this.cycleStartCycle = cycleStartCycle; + this.emissionCycleDurationTicks = cycleDurationTicks; + this.emissionTimeThresholdTicks = thresholdTicks; + this.emitOnlyBeforeTime = emitOnlyBeforeTime; + this.loopEmission = loopEmission; + } + + public void setEmissionAccum(float accum) { + this.emissionAccum = Math.max(0f, Math.min(1f, accum)); + } + + public boolean isEmissionAllowedAtCycle(long currentGameCycle) { + if (emissionCycleDurationTicks < 0) return true; + long elapsed = Math.max(0, currentGameCycle - cycleStartCycle); + int elapsedTicks = (int) Math.min(elapsed, Integer.MAX_VALUE); + if (loopEmission || elapsedTicks < emissionCycleDurationTicks) { + elapsedTicks = elapsedTicks % Math.max(1, emissionCycleDurationTicks); + } else { + return false; + } + if (emissionTimeThresholdTicks < 0) return true; + if (emitOnlyBeforeTime && elapsedTicks >= emissionTimeThresholdTicks) return false; + if (!emitOnlyBeforeTime && elapsedTicks < emissionTimeThresholdTicks) return false; + return true; + } + + public ParticleEmitter active(boolean active) { + this.active = active; + return this; + } + + public boolean isActive() { + return active && worldPoint != null; + } + + private void randomDirectionFromRanges(float[] out, ThreadLocalRandom rng) { + float yaw = directionYaw + (spreadYawMin == spreadYawMax ? spreadYawMin : spreadYawMin + (spreadYawMax - spreadYawMin) * rng.nextFloat()); + float pitch = directionPitch + (spreadPitchMin == spreadPitchMax ? spreadPitchMin : spreadPitchMin + (spreadPitchMax - spreadPitchMin) * rng.nextFloat()); + float cp = cos(pitch); + out[0] = sin(yaw) * cp; + out[1] = -sin(pitch); + out[2] = -cos(yaw) * cp; + } + + @Nullable + public Particle spawn(float originLocalX, float originLocalY, float originLocalZ, int plane) { + if (!isActive()) return null; + Particle into = new Particle(); + return spawnInto(into, originLocalX, originLocalY, originLocalZ, plane) ? into : null; + } + + public boolean spawnInto(Particle into, float originLocalX, float originLocalY, float originLocalZ, int plane) { + if (!isActive()) return false; + + ThreadLocalRandom rng = ThreadLocalRandom.current(); + randomDirectionFromRanges(TMP_DIR, rng); + float speed = speedMin + (speedMax - speedMin) * rng.nextFloat(); + float speedWorld = speed / 16384f; + float vx = TMP_DIR[0] * speedWorld; + float vy = TMP_DIR[1] * speedWorld; + float vz = TMP_DIR[2] * speedWorld; + + float life = particleLifeMin + (particleLifeMax - particleLifeMin) * rng.nextFloat(); + float size = sizeMin + (sizeMax - sizeMin) * rng.nextFloat(); + + into.setPosition(originLocalX, originLocalY, originLocalZ); + into.setVelocity(vx, vy, vz); + into.life = life; + into.maxLife = life; + into.size = size; + into.targetScale = targetScale; + into.scaleTransition = scaleTransition; + into.targetSpeed = targetSpeed; + into.speedTransition = speedTransition; + into.plane = plane; + if (uniformColorVariation) { + float u = rng.nextFloat(); + for (int i = 0; i < 4; i++) + into.initialColor[i] = colorMin[i] + (colorMax[i] - colorMin[i]) * u; + } else { + for (int i = 0; i < 4; i++) + into.initialColor[i] = colorMin[i] + (colorMax[i] - colorMin[i]) * rng.nextFloat(); + } + into.initialColor[3] *= alphaScale; + into.color[0] = into.initialColor[0]; + into.color[1] = into.initialColor[1]; + into.color[2] = into.initialColor[2]; + into.color[3] = into.initialColor[3]; + into.targetColor = targetColor; + into.colorTransitionPct = colorTransitionPct; + into.alphaTransitionPct = alphaTransitionPct; + ParticleDefinition def = getDefinition(); + if (def != null && def.texture.flipbook.flipbookColumns > 0 && def.texture.flipbook.flipbookRows > 0 && "random".equalsIgnoreCase(def.texture.flipbook.flipbookMode)) { + int totalFrames = def.texture.flipbook.flipbookColumns * def.texture.flipbook.flipbookRows; + into.flipbookRandomFrame = rng.nextInt(totalFrames); + } + return true; + } + + public int advanceEmission(float dt) { + if (!active) return 0; + if (!initialSpawnDone && initialSpawn > 0) { + initialSpawnDone = true; + return initialSpawn; + } + float rate = emissionSpawnMin + (emissionSpawnMax - emissionSpawnMin) * ThreadLocalRandom.current().nextFloat(); + emissionAccum += rate * dt; + if (emissionAccum < 1f) return 0; + int n = (int) emissionAccum; + emissionAccum -= n; + return n; + } + + /** + * Emitter tick (reference ParticleEmitter.tick): spawn particles. Call once per frame per emitter. + * Manager handles culling and buffer; this runs advanceEmission and spawnInto + add to buffer. + */ + public void tick(float dt, long gameCycle, float originX, float originY, float originZ, int plane, rs117.hd.scene.particles.ParticleManager manager) { + if (!isEmissionAllowedAtCycle(gameCycle)) return; + ParticleBuffer buf = manager.getParticleBuffer(); + int maxParticles = manager.getMaxParticles(); + int toSpawn = advanceEmission(dt); + for (int i = 0; i < toSpawn && buf.count < maxParticles; i++) { + Particle p = manager.obtainParticle(); + if (p == null) continue; + if (!spawnInto(p, originX, originY, originZ, plane)) { + manager.releaseParticle(p); + continue; + } + manager.addSpawnedParticleToBuffer(p, originX, originY, originZ, this); + } + } +} diff --git a/src/main/java/rs117/hd/scene/particles/emitter/WeatherAreaConfig.java b/src/main/java/rs117/hd/scene/particles/emitter/WeatherAreaConfig.java new file mode 100644 index 0000000000..102a109c7b --- /dev/null +++ b/src/main/java/rs117/hd/scene/particles/emitter/WeatherAreaConfig.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2025 + * All rights reserved. + */ +package rs117.hd.scene.particles.emitter; + +import java.util.List; +import lombok.Value; +import rs117.hd.scene.areas.AABB; + +/** Resolved weather area config: AABBs from area names + particle IDs. Used for debug overlay. */ +@Value +public class WeatherAreaConfig { + List aabbs; + List particleIds; + /** Tiles from edge (inside) to fade to 0. 0 = no fade. */ + int edgeFadeInside; + /** Tiles from edge (outside) for margin. 0 = no outside. */ + int edgeFadeOutside; +} diff --git a/src/main/java/rs117/hd/utils/DeveloperTools.java b/src/main/java/rs117/hd/utils/DeveloperTools.java index 36d42b6171..4dea9daecf 100644 --- a/src/main/java/rs117/hd/utils/DeveloperTools.java +++ b/src/main/java/rs117/hd/utils/DeveloperTools.java @@ -2,8 +2,10 @@ import java.awt.event.KeyEvent; import javax.inject.Inject; +import javax.swing.JFrame; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import net.runelite.api.ChatMessageType; import net.runelite.api.events.*; import net.runelite.client.callback.ClientThread; import net.runelite.client.config.Keybind; @@ -14,6 +16,7 @@ import rs117.hd.HdPlugin; import rs117.hd.overlays.FrameTimerOverlay; import rs117.hd.overlays.LightGizmoOverlay; +import rs117.hd.scene.particles.debug.ParticleDebugOverlay; import rs117.hd.overlays.ShadowMapOverlay; import rs117.hd.overlays.TileInfoOverlay; import rs117.hd.overlays.TiledLightingOverlay; @@ -37,6 +40,7 @@ public class DeveloperTools implements KeyListener { private static final Keybind KEY_TOGGLE_ORTHOGRAPHIC = new Keybind(KeyEvent.VK_TAB, SHIFT_DOWN_MASK); private static final Keybind KEY_TOGGLE_HIDE_UI = new Keybind(KeyEvent.VK_H, CTRL_DOWN_MASK); private static final Keybind KEY_RELOAD_SCENE = new Keybind(KeyEvent.VK_R, CTRL_DOWN_MASK); + private static final Keybind KEY_OPEN_PARTICLE_DEV = new Keybind(KeyEvent.VK_P, CTRL_DOWN_MASK); @Inject private ClientThread clientThread; @@ -68,6 +72,9 @@ public class DeveloperTools implements KeyListener { @Inject private TiledLightingOverlay tiledLightingOverlay; + @Inject + private ParticleDebugOverlay particleDebugOverlay; + private boolean keyBindingsEnabled; private boolean tileInfoOverlayEnabled; @Getter @@ -78,6 +85,8 @@ public class DeveloperTools implements KeyListener { private boolean hideUiEnabled; private boolean tiledLightingOverlayEnabled; + private JFrame particleDevFrame; + public void activate() { // Listen for commands eventBus.register(this); @@ -115,9 +124,15 @@ public void activate() { public void deactivate() { eventBus.unregister(this); keyManager.unregisterKeyListener(this); + if (particleDevFrame != null) { + particleDevFrame.setVisible(false); + particleDevFrame.dispose(); + particleDevFrame = null; + } tileInfoOverlay.setActive(false); frameTimerOverlay.setActive(false); shadowMapOverlay.setActive(false); + particleDebugOverlay.setActive(false); lightGizmoOverlay.setActive(false); tiledLightingOverlay.setActive(false); hideUiEnabled = false; @@ -169,6 +184,13 @@ public void onCommandExecuted(CommandExecuted commandExecuted) { case "culling": plugin.freezeCulling = !plugin.freezeCulling; break; + case "pt": + clientThread.invoke(() -> { + int n = plugin.getParticleManager().spawnPerformanceTestEmitters(); + plugin.client.addChatMessage(ChatMessageType.GAMEMESSAGE, "117 HD", + "[117 HD] Spawned " + n + " particle test emitters.", "117 HD"); + }); + break; } } diff --git a/src/main/resources/rs117/hd/icon.png b/src/main/resources/rs117/hd/icon.png new file mode 100644 index 0000000000..12aba9f5a1 Binary files /dev/null and b/src/main/resources/rs117/hd/icon.png differ diff --git a/src/main/resources/rs117/hd/particle_frag.glsl b/src/main/resources/rs117/hd/particle_frag.glsl new file mode 100644 index 0000000000..2b73ed866f --- /dev/null +++ b/src/main/resources/rs117/hd/particle_frag.glsl @@ -0,0 +1,42 @@ +/* Copyright (c) 2025, Hooder. Particle fragment: texture * color, shape from texture alpha. */ +#version 330 + +#include +#include GLOBAL_PARTICLE_AMBIENT_LIGHT + +uniform sampler2DArray uParticleTexture; + +in vec4 vColor; +in vec2 vUV; +in float vLayer; +in vec3 vFlipbook; +in float vUseSceneAmbientLight; + +layout (location = 0) out vec4 outColor; + +void main() { + vec2 uv = vUV; + float cols = vFlipbook.x; + float rows = vFlipbook.y; + float frameVal = vFlipbook.z; + if (cols > 0.0 && rows > 0.0) { + float numFrames = cols * rows; + float frame = frameVal >= 1.0 ? floor(frameVal - 1.0) : floor(frameVal * numFrames); + frame = clamp(frame, 0.0, numFrames - 1.0); + float col = mod(frame, cols); + float row = floor(frame / cols); + uv = (uv + vec2(col, row)) / vec2(cols, rows); + } + vec4 tex = texture(uParticleTexture, vec3(uv, vLayer)); + vec4 particleColor = tex * vColor; + if (particleColor.a <= 0.0) + discard; + + #if GLOBAL_PARTICLE_AMBIENT_LIGHT + if (vUseSceneAmbientLight != 0.0) { + vec3 ambientLight = ambientColor * ambientStrength; + particleColor.rgb *= mix(vec3(1.0), ambientLight, 0.35); + } + #endif + outColor = particleColor; +} diff --git a/src/main/resources/rs117/hd/particle_vert.glsl b/src/main/resources/rs117/hd/particle_vert.glsl new file mode 100644 index 0000000000..76a1fb5dd1 --- /dev/null +++ b/src/main/resources/rs117/hd/particle_vert.glsl @@ -0,0 +1,42 @@ +/* Copyright (c) 2025, Mark7625. Instanced billboard particles: one quad mesh, per-instance center/size/color. */ +#version 330 + +#include + +layout (location = 0) in vec2 aCorner; +layout (location = 1) in vec3 aCenter; +layout (location = 2) in vec4 aColor; +layout (location = 3) in float aSize; +layout (location = 4) in float aLayer; +layout (location = 5) in float aFlipbookCols; +layout (location = 6) in float aFlipbookRows; +layout (location = 7) in float aFlipbookFrame; +layout (location = 8) in float aUseSceneAmbientLight; +layout (location = 9) in float aYaw; + +out vec4 vColor; +out vec2 vUV; +out float vLayer; +out vec3 vFlipbook; +out float vUseSceneAmbientLight; + +void main() { + vec3 toCamera = cameraPos - aCenter; + toCamera *= inversesqrt(max(dot(toCamera, toCamera), 1e-12)); + vec3 worldUp = vec3(0, 1, 0); + vec3 right = cross(worldUp, toCamera); + float lenSq = dot(right, right); + right = mix(vec3(1, 0, 0), right * inversesqrt(lenSq), step(1e-6, lenSq)); + vec3 up = cross(toCamera, right); + float c = cos(aYaw); + float s = sin(aYaw); + vec3 rightR = right * c + up * s; + vec3 upR = -right * s + up * c; + vec3 worldPos = aCenter + (rightR * aCorner.x + upR * aCorner.y) * aSize; + gl_Position = projectionMatrix * vec4(worldPos, 1.0); + vColor = aColor; + vUV = aCorner * 0.5 + 0.5; + vLayer = aLayer; + vFlipbook = vec3(aFlipbookCols, aFlipbookRows, aFlipbookFrame); + vUseSceneAmbientLight = aUseSceneAmbientLight; +} diff --git a/src/main/resources/rs117/hd/scene/areas.json b/src/main/resources/rs117/hd/scene/areas.json index 8331151278..7e02097a63 100644 --- a/src/main/resources/rs117/hd/scene/areas.json +++ b/src/main/resources/rs117/hd/scene/areas.json @@ -1191,6 +1191,12 @@ [ 3456, 4736, 3527, 4783 ] ] }, + { + "name": "FALADOR_MIDDLE", + "aabbs": [ + [ 2961, 3385, 2969, 3377 ] + ] + }, { "name": "FALADOR", "areas": [ diff --git a/src/main/resources/rs117/hd/scene/particles/emitters.json b/src/main/resources/rs117/hd/scene/particles/emitters.json new file mode 100644 index 0000000000..b17355dfbf --- /dev/null +++ b/src/main/resources/rs117/hd/scene/particles/emitters.json @@ -0,0 +1,23 @@ +[ + { + "objectEmitters": [ + { + "object": "FAI_FALADOR_HOUSE_TORCH", + "offsetX": -55, + "offsetY": 0, + "offsetZ": 205 + } + ], + "description": "WALL_TORCH_MID_SMALL", + "particleId": "TORCH_SMOKE" + }, + { + "particleIds": [ "3", "4" ], + "description": "Particles 3 and 4 emitters", + "weatherAreas": [ "ICE_MOUNTAIN" ], + "edgeFadeInside": 5, + "edgeFadeOutside": 2, + "weatherSpacing": 4, + "weatherEveryNTiles": 8 + } +] diff --git a/src/main/resources/rs117/hd/scene/particles/particles.json b/src/main/resources/rs117/hd/scene/particles/particles.json new file mode 100644 index 0000000000..0cf9eb74d7 --- /dev/null +++ b/src/main/resources/rs117/hd/scene/particles/particles.json @@ -0,0 +1,13730 @@ +[ { + "id" : "0", + "texture" : { + "file" : "816.png" + }, + "spread" : { + "yawMax" : 2047, + "pitchMin" : 659, + "pitchMax" : 372 + }, + "speed" : { + "minSpeed" : 7864320, + "maxSpeed" : 10485760, + "targetSpeed" : 31457280 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 20, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : -13421824, + "maxColourArgb" : -16764109, + "targetColourArgb" : 13107, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 70, + "minSpawn" : 384, + "maxSpawn" : 512, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + } +}, { + "id" : "1", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 584, + "pitchMax" : 662 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 655360, + "targetSpeed" : 2097152 + }, + "scale" : { + "minScale" : 19, + "maxScale" : 19 + }, + "colours" : { + "minColourArgb" : 2030016768, + "maxColourArgb" : 1694498662, + "targetColourArgb" : 1264975872, + "alphaTransitionPercent" : 25 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 128, + "maxSpawn" : 192, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "718.png" + } +}, { + "id" : "2", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 584, + "pitchMax" : 662 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144, + "targetSpeed" : 1572864, + "speedTransitionPercent" : 80 + }, + "scale" : { + "minScale" : 16, + "maxScale" : 16 + }, + "colours" : { + "minColourArgb" : 674444083, + "maxColourArgb" : 1677721600, + "targetColourArgb" : 10066329, + "colourTransitionPercent" : 80, + "alphaTransitionPercent" : 90 + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 120, + "minSpawn" : 64, + "maxSpawn" : 96, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "717.png" + } +}, { + "id" : "3", + "general" : { + "randomYawRotation" : 2.0 + }, + "spread" : { + "yawMin" : 947, + "yawMax" : 1061, + "pitchMin" : 451, + "pitchMax" : 511 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288, + "targetSpeed" : 2097152 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 8 + }, + "colours" : { + "minColourArgb" : -1191182337, + "maxColourArgb" : -3342337, + "targetColourArgb" : -1, + "colourTransitionPercent" : 25, + "useSceneAmbientLight" : false, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 1000, + "maxDelay" : 1200, + "minSpawn" : 2, + "maxSpawn" : 4, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "720.png" + } +}, { + "id" : "4", + "general" : { + "randomYawRotation" : 2.0 + }, + "spread" : { + "yawMin" : 947, + "yawMax" : 1061, + "pitchMin" : 451, + "pitchMax" : 511 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288, + "targetSpeed" : 2097152 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 8 + }, + "colours" : { + "minColourArgb" : -1191182337, + "maxColourArgb" : -3342337, + "targetColourArgb" : -1, + "colourTransitionPercent" : 25, + "useSceneAmbientLight" : false, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 1000, + "maxDelay" : 1200, + "minSpawn" : 2, + "maxSpawn" : 4, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "723.png" + } +}, { + "id" : "5", + "spread" : { + "yawMin" : 947, + "yawMax" : 1061, + "pitchMin" : 451, + "pitchMax" : 511 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288, + "targetSpeed" : 2097152 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 8 + }, + "colours" : { + "minColourArgb" : -1191182337, + "maxColourArgb" : -3342337, + "targetColourArgb" : -1, + "colourTransitionPercent" : 25, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 800, + "maxDelay" : 1000, + "minSpawn" : 1, + "maxSpawn" : 1, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "395.png" + } +}, { + "id" : "6", + "spread" : { + "yawMin" : 947, + "yawMax" : 1061, + "pitchMin" : 451, + "pitchMax" : 511 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288, + "targetSpeed" : 2097152 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 8 + }, + "colours" : { + "minColourArgb" : -1191182337, + "maxColourArgb" : -3342337, + "targetColourArgb" : -1, + "colourTransitionPercent" : 25, + "useSceneAmbientLight" : false, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 800, + "maxDelay" : 1000, + "minSpawn" : 1, + "maxSpawn" : 1, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "723.png" + } +}, { + "id" : "7", + "spread" : { + "yawMin" : 947, + "yawMax" : 1061, + "pitchMin" : 499, + "pitchMax" : 542 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1310720 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 2 + }, + "colours" : { + "minColourArgb" : -1191182337, + "maxColourArgb" : -3342337, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 10, + "alphaTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 50, + "minSpawn" : 128, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "720.png" + } +}, { + "id" : "8", + "spread" : { + "yawMin" : 992, + "yawMax" : 1279, + "pitchMin" : 474, + "pitchMax" : 695 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1048576 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 2 + }, + "colours" : { + "minColourArgb" : -1191182337, + "maxColourArgb" : -3342337, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 10, + "alphaTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 80, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "720.png" + } +}, { + "id" : "9", + "spread" : { + "yawMin" : 2047, + "yawMax" : 2047, + "pitchMin" : 474, + "pitchMax" : 695 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 4 + }, + "colours" : { + "minColourArgb" : -1191182337, + "maxColourArgb" : -3342337, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 10, + "alphaTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 80, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "720.png" + } +}, { + "id" : "10", + "spread" : { + "yawMin" : 947, + "yawMax" : 1061, + "pitchMin" : 499, + "pitchMax" : 542 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1310720 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 2 + }, + "colours" : { + "minColourArgb" : -1191182337, + "maxColourArgb" : -3342337, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 10, + "alphaTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 50, + "minSpawn" : 128, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "720.png" + } +}, { + "id" : "11", + "spread" : { + "yawMin" : 992, + "yawMax" : 1279, + "pitchMin" : 474, + "pitchMax" : 695 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1048576 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 2 + }, + "colours" : { + "minColourArgb" : -1191182337, + "maxColourArgb" : -3342337, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 10, + "alphaTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 80, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "720.png" + } +}, { + "id" : "12", + "spread" : { + "yawMin" : 2047, + "yawMax" : 2047, + "pitchMin" : 474, + "pitchMax" : 695 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 4 + }, + "colours" : { + "minColourArgb" : -1191182337, + "maxColourArgb" : -3342337, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 10, + "alphaTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 80, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "720.png" + } +}, { + "id" : "13", + "spread" : { + "yawMin" : 947, + "yawMax" : 1061, + "pitchMin" : 499, + "pitchMax" : 542 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1310720 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 2 + }, + "colours" : { + "minColourArgb" : -1191182337, + "maxColourArgb" : -3342337, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 10, + "alphaTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 50, + "minSpawn" : 128, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "720.png" + } +}, { + "id" : "14", + "spread" : { + "yawMin" : 992, + "yawMax" : 1279, + "pitchMin" : 474, + "pitchMax" : 695 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1048576 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 2 + }, + "colours" : { + "minColourArgb" : -1191182337, + "maxColourArgb" : -3342337, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 10, + "alphaTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 80, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "720.png" + } +}, { + "id" : "15", + "spread" : { + "yawMin" : 2047, + "yawMax" : 2047, + "pitchMin" : 474, + "pitchMax" : 695 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 4 + }, + "colours" : { + "minColourArgb" : -1191182337, + "maxColourArgb" : -3342337, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 10, + "alphaTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 80, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "720.png" + } +}, { + "id" : "16", + "spread" : { + "yawMin" : 992, + "yawMax" : 1279, + "pitchMin" : 474, + "pitchMax" : 695 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1048576 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 2 + }, + "colours" : { + "minColourArgb" : 16777215, + "maxColourArgb" : 550305791, + "targetColourArgb" : -1258291201, + "colourTransitionPercent" : 10, + "alphaTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 40, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "720.png" + } +}, { + "id" : "17", + "spread" : { + "yawMin" : 2047, + "yawMax" : 2047, + "pitchMin" : 474, + "pitchMax" : 695 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 4 + }, + "colours" : { + "minColourArgb" : 16777215, + "maxColourArgb" : 550305791, + "targetColourArgb" : -1258291201, + "colourTransitionPercent" : 10, + "alphaTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 40, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "720.png" + } +}, { + "id" : "18", + "spread" : { + "yawMin" : 947, + "yawMax" : 1061, + "pitchMin" : 451, + "pitchMax" : 511 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1572864, + "targetSpeed" : 6291456 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 10 + }, + "colours" : { + "minColourArgb" : -1191182337, + "maxColourArgb" : -3342337, + "targetColourArgb" : -1, + "colourTransitionPercent" : 25, + "useSceneAmbientLight" : false, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 300, + "maxDelay" : 500, + "minSpawn" : 10, + "maxSpawn" : 14, + "initialSpawn" : 1000, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "720.png" + } +}, { + "id" : "19", + "spread" : { + "yawMin" : 1649, + "yawMax" : 2038, + "pitchMin" : 924, + "pitchMax" : 1003 + }, + "speed" : { + "minSpeed" : 65536, + "maxSpeed" : 65536, + "targetSpeed" : 262144 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 3, + "targetScale" : 15 + }, + "colours" : { + "minColourArgb" : -930582723, + "maxColourArgb" : -596089564, + "targetColourArgb" : 12759404, + "colourTransitionPercent" : 60, + "alphaTransitionPercent" : 95 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 64, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "649.png" + } +}, { + "id" : "20", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 811, + "pitchMax" : 268 + }, + "speed" : { + "minSpeed" : 1572864, + "maxSpeed" : 1835008, + "targetSpeed" : 4194304 + }, + "scale" : { + "minScale" : 16, + "maxScale" : 24, + "targetScale" : 48 + }, + "colours" : { + "minColourArgb" : -930582723, + "maxColourArgb" : -596089564, + "targetColourArgb" : 12759404, + "colourTransitionPercent" : 60, + "alphaTransitionPercent" : 95 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 192, + "maxSpawn" : 256, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "649.png" + } +}, { + "id" : "21", + "spread" : { + "yawMin" : 1649, + "yawMax" : 2038, + "pitchMin" : 924, + "pitchMax" : 1003 + }, + "speed" : { + "minSpeed" : 65536, + "maxSpeed" : 65536, + "targetSpeed" : 262144 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 3, + "targetScale" : 15 + }, + "colours" : { + "minColourArgb" : -927864079, + "maxColourArgb" : -591733249, + "targetColourArgb" : 14023931, + "colourTransitionPercent" : 60, + "alphaTransitionPercent" : 95 + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 80, + "minSpawn" : 64, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "649.png" + } +}, { + "id" : "22", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 811, + "pitchMax" : 268 + }, + "speed" : { + "minSpeed" : 1572864, + "maxSpeed" : 1835008, + "targetSpeed" : 4194304 + }, + "scale" : { + "minScale" : 16, + "maxScale" : 24, + "targetScale" : 48 + }, + "colours" : { + "minColourArgb" : -927864079, + "maxColourArgb" : -591733249, + "targetColourArgb" : 14023931, + "colourTransitionPercent" : 60, + "alphaTransitionPercent" : 95 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 192, + "maxSpawn" : 256, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "649.png" + } +}, { + "id" : "23", + "spread" : { + "yawMin" : 1649, + "yawMax" : 2038, + "pitchMin" : 924, + "pitchMax" : 1003 + }, + "speed" : { + "minSpeed" : 65536, + "maxSpeed" : 65536, + "targetSpeed" : 262144 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 3, + "targetScale" : 15 + }, + "colours" : { + "minColourArgb" : -935643611, + "maxColourArgb" : -599182047, + "targetColourArgb" : 5521688, + "colourTransitionPercent" : 60, + "alphaTransitionPercent" : 95 + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 80, + "minSpawn" : 64, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "649.png" + } +}, { + "id" : "24", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 811, + "pitchMax" : 268 + }, + "speed" : { + "minSpeed" : 1572864, + "maxSpeed" : 1835008, + "targetSpeed" : 4194304 + }, + "scale" : { + "minScale" : 16, + "maxScale" : 24, + "targetScale" : 48 + }, + "colours" : { + "minColourArgb" : -935643611, + "maxColourArgb" : -599182047, + "targetColourArgb" : 5521688, + "colourTransitionPercent" : 60, + "alphaTransitionPercent" : 95 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 192, + "maxSpawn" : 256, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "649.png" + } +}, { + "id" : "25", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 867, + "pitchMax" : 511 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 131072, + "targetSpeed" : 524288 + }, + "scale" : { + "minScale" : 7, + "maxScale" : 10, + "targetScale" : 64 + }, + "colours" : { + "minColourArgb" : -930580651, + "maxColourArgb" : -596153531, + "targetColourArgb" : 11511172, + "colourTransitionPercent" : 60, + "alphaTransitionPercent" : 95 + }, + "emission" : { + "minDelay" : 75, + "maxDelay" : 100, + "minSpawn" : 64, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "649.png" + } +}, { + "id" : "26", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 396, + "pitchMax" : 950 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 131072, + "targetSpeed" : 524288 + }, + "scale" : { + "minScale" : 7, + "maxScale" : 10, + "targetScale" : 64 + }, + "colours" : { + "minColourArgb" : 1348862054, + "maxColourArgb" : 1684406425, + "targetColourArgb" : 6684825, + "colourTransitionPercent" : 95, + "alphaTransitionPercent" : 95 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 75, + "minSpawn" : 32, + "maxSpawn" : 32, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "649.png" + } +}, { + "id" : "27", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 396, + "pitchMax" : 950 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 131072, + "targetSpeed" : 524288 + }, + "scale" : { + "minScale" : 7, + "maxScale" : 10, + "targetScale" : 64 + }, + "colours" : { + "minColourArgb" : -932839322, + "maxColourArgb" : -597294951, + "targetColourArgb" : 6684825, + "colourTransitionPercent" : 95, + "alphaTransitionPercent" : 95 + }, + "emission" : { + "minDelay" : 75, + "maxDelay" : 100, + "minSpawn" : 64, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "649.png" + } +}, { + "id" : "28", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 396, + "pitchMax" : 950 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 131072, + "targetSpeed" : 524288 + }, + "scale" : { + "minScale" : 7, + "maxScale" : 10, + "targetScale" : 64 + }, + "colours" : { + "minColourArgb" : -926089217, + "maxColourArgb" : -590557953, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 95, + "alphaTransitionPercent" : 95 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 75, + "minSpawn" : 32, + "maxSpawn" : 32, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "649.png" + } +}, { + "id" : "29", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 396, + "pitchMax" : 950 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 131072, + "targetSpeed" : 524288 + }, + "scale" : { + "minScale" : 7, + "maxScale" : 10, + "targetScale" : 64 + }, + "colours" : { + "minColourArgb" : -926089217, + "maxColourArgb" : -590557953, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 95, + "alphaTransitionPercent" : 95 + }, + "emission" : { + "minDelay" : 75, + "maxDelay" : 100, + "minSpawn" : 64, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "649.png" + } +}, { + "id" : "30", + "spread" : { }, + "speed" : { + "minSpeed" : 157286, + "maxSpeed" : 209715 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1012185310, + "maxColourArgb" : 677884355, + "targetColourArgb" : 8046043, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 250, + "maxDelay" : 350, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "31", + "spread" : { }, + "speed" : { + "minSpeed" : 157286, + "maxSpeed" : 209715 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1012185310, + "maxColourArgb" : 677884355, + "targetColourArgb" : 8046043, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 150, + "maxDelay" : 200, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "32", + "spread" : { }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 183500 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1012185310, + "maxColourArgb" : 677884355, + "targetColourArgb" : 8046043, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 150, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "33", + "spread" : { }, + "speed" : { + "minSpeed" : 78643, + "maxSpeed" : 131072 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1012185310, + "maxColourArgb" : 677884355, + "targetColourArgb" : 8046043, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 100, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "34", + "spread" : { }, + "speed" : { + "minSpeed" : 157286, + "maxSpeed" : 209715 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1012642141, + "maxColourArgb" : 681502379, + "targetColourArgb" : 9754286, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 250, + "maxDelay" : 350, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "35", + "spread" : { }, + "speed" : { + "minSpeed" : 157286, + "maxSpeed" : 209715 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1012642141, + "maxColourArgb" : 681502379, + "targetColourArgb" : 9754286, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 150, + "maxDelay" : 200, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "36", + "spread" : { }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 183500 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1012642141, + "maxColourArgb" : 681502379, + "targetColourArgb" : 9754286, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 150, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "37", + "spread" : { }, + "speed" : { + "minSpeed" : 78643, + "maxSpeed" : 131072 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1012642141, + "maxColourArgb" : 681502379, + "targetColourArgb" : 9754286, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 100, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "38", + "spread" : { }, + "speed" : { + "minSpeed" : 157286, + "maxSpeed" : 209715 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1021235290, + "maxColourArgb" : 686024371, + "targetColourArgb" : 14931298, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 250, + "maxDelay" : 350, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "39", + "spread" : { }, + "speed" : { + "minSpeed" : 157286, + "maxSpeed" : 209715 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1021235290, + "maxColourArgb" : 686024371, + "targetColourArgb" : 14931298, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 150, + "maxDelay" : 200, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "40", + "spread" : { }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 183500 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1021235290, + "maxColourArgb" : 686024371, + "targetColourArgb" : 14931298, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 150, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "41", + "spread" : { }, + "speed" : { + "minSpeed" : 78643, + "maxSpeed" : 131072 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1021235290, + "maxColourArgb" : 686024371, + "targetColourArgb" : 14931298, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 100, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "42", + "spread" : { }, + "speed" : { + "minSpeed" : 157286, + "maxSpeed" : 209715 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1021633753, + "maxColourArgb" : 687471345, + "targetColourArgb" : 15921368, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 250, + "maxDelay" : 350, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "43", + "spread" : { }, + "speed" : { + "minSpeed" : 157286, + "maxSpeed" : 209715 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1021633753, + "maxColourArgb" : 687471345, + "targetColourArgb" : 15921368, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 150, + "maxDelay" : 200, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "44", + "spread" : { }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 183500 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1021633753, + "maxColourArgb" : 687471345, + "targetColourArgb" : 15921368, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 150, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "45", + "spread" : { }, + "speed" : { + "minSpeed" : 78643, + "maxSpeed" : 131072 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1021633753, + "maxColourArgb" : 687471345, + "targetColourArgb" : 15921368, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 100, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "46", + "spread" : { + "yawMax" : 1024, + "pitchMin" : 979, + "pitchMax" : 990 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 1, + "targetScale" : 2, + "scaleTransitionPercent" : 1 + }, + "colours" : { + "minColourArgb" : 1683273950, + "maxColourArgb" : 1852289475, + "targetColourArgb" : 1937425883 + }, + "emission" : { + "minDelay" : 90, + "maxDelay" : 90, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "47", + "spread" : { + "yawMax" : 1024, + "pitchMin" : 979, + "pitchMax" : 990 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 1, + "targetScale" : 2, + "scaleTransitionPercent" : 1 + }, + "colours" : { + "minColourArgb" : 1694498560, + "maxColourArgb" : 1862244659, + "targetColourArgb" : 1946157055 + }, + "emission" : { + "minDelay" : 90, + "maxDelay" : 90, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "777.png" + } +}, { + "id" : "48", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 885, + "pitchMax" : 571 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 786432, + "targetSpeed" : 1048576, + "speedTransitionPercent" : 60 + }, + "scale" : { + "minScale" : 12, + "maxScale" : 24, + "targetScale" : 48, + "scaleTransitionPercent" : 80 + }, + "colours" : { + "minColourArgb" : -1607257293, + "maxColourArgb" : -939524096, + "targetColourArgb" : 6710886, + "colourTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 50, + "minSpawn" : 192, + "maxSpawn" : 256, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "766.png" + } +}, { + "id" : "49", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 885, + "pitchMax" : 571 + }, + "speed" : { + "minSpeed" : 655360, + "maxSpeed" : 655360, + "targetSpeed" : 786432, + "speedTransitionPercent" : 60 + }, + "scale" : { + "minScale" : 12, + "maxScale" : 24, + "targetScale" : 48, + "scaleTransitionPercent" : 80 + }, + "colours" : { + "minColourArgb" : -1607257293, + "maxColourArgb" : -939524096, + "targetColourArgb" : 6710886, + "colourTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 50, + "minSpawn" : 192, + "maxSpawn" : 256, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "766.png" + } +}, { + "id" : "50", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 885, + "pitchMax" : 571 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 524288, + "targetSpeed" : 524288, + "speedTransitionPercent" : 60 + }, + "scale" : { + "minScale" : 12, + "maxScale" : 24, + "targetScale" : 48, + "scaleTransitionPercent" : 80 + }, + "colours" : { + "minColourArgb" : -1607257293, + "maxColourArgb" : -939524096, + "targetColourArgb" : 6710886, + "colourTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 50, + "minSpawn" : 192, + "maxSpawn" : 256, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "766.png" + } +}, { + "id" : "51", + "spread" : { + "yawMax" : 2047, + "pitchMax" : 491 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 524288 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 8 + }, + "colours" : { + "minColourArgb" : 2025835486, + "maxColourArgb" : 1021044973, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 40, + "alphaTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 500, + "maxDelay" : 600, + "minSpawn" : 1, + "maxSpawn" : 2, + "initialSpawn" : 10000, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "717.png" + } +}, { + "id" : "52", + "spread" : { + "yawMax" : 2047, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144 + }, + "scale" : { + "minScale" : 11, + "maxScale" : 11 + }, + "colours" : { + "minColourArgb" : 687852799, + "maxColourArgb" : 1023397068, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 40, + "alphaTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 60, + "minSpawn" : 1280, + "maxSpawn" : 1600, + "initialSpawn" : 10000, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "717.png" + } +}, { + "id" : "53", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 473, + "pitchMax" : 473 + }, + "speed" : { + "minSpeed" : 52428, + "maxSpeed" : 131072 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 6 + }, + "colours" : { + "minColourArgb" : 1691143423, + "maxColourArgb" : 2026700799, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 40, + "alphaTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 150, + "maxDelay" : 250, + "minSpawn" : 12, + "maxSpawn" : 32, + "initialSpawn" : 10000, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "717.png" + } +}, { + "id" : "54", + "spread" : { + "pitchMin" : 1023, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 52428, + "maxSpeed" : 78643 + }, + "scale" : { + "minScale" : 11, + "maxScale" : 11 + }, + "colours" : { + "minColourArgb" : 180341726, + "maxColourArgb" : 98298093, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 40, + "alphaTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 60, + "minSpawn" : 320, + "maxSpawn" : 384, + "initialSpawn" : 10000, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "717.png" + } +}, { + "id" : "55", + "spread" : { + "pitchMin" : 1023, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 52428, + "maxSpeed" : 78643 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 3 + }, + "colours" : { + "minColourArgb" : 1355612159, + "maxColourArgb" : 1691156428, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 40, + "alphaTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 200, + "maxDelay" : 220, + "minSpawn" : 32, + "maxSpawn" : 64, + "initialSpawn" : 10000, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "808.png" + } +}, { + "id" : "56", + "spread" : { + "yawMax" : 1024, + "pitchMin" : 979, + "pitchMax" : 990 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 1, + "targetScale" : 2, + "scaleTransitionPercent" : 1 + }, + "colours" : { + "minColourArgb" : 1683273950, + "maxColourArgb" : 1860434687, + "targetColourArgb" : 1937425883 + }, + "emission" : { + "minDelay" : 90, + "maxDelay" : 90, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "57", + "spread" : { + "yawMax" : 1024, + "pitchMin" : 979, + "pitchMax" : 990 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 1, + "targetScale" : 2, + "scaleTransitionPercent" : 1 + }, + "colours" : { + "minColourArgb" : 1692323930, + "maxColourArgb" : 1862007252, + "targetColourArgb" : 1944311138 + }, + "emission" : { + "minDelay" : 90, + "maxDelay" : 90, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "58", + "spread" : { + "yawMax" : 1024, + "pitchMin" : 979, + "pitchMax" : 990 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 1, + "targetScale" : 2, + "scaleTransitionPercent" : 1 + }, + "colours" : { + "minColourArgb" : 1683730781, + "maxColourArgb" : 1860042723, + "targetColourArgb" : 1939134126 + }, + "emission" : { + "minDelay" : 90, + "maxDelay" : 90, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "59", + "spread" : { + "yawMin" : 1024, + "yawMax" : 1024 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144 + }, + "scale" : { + "minScale" : 24, + "maxScale" : 26, + "targetScale" : 6 + }, + "colours" : { + "minColourArgb" : 687865855, + "maxColourArgb" : 855638015, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 20 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 160, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "813.png" + } +}, { + "id" : "60", + "spread" : { + "yawMax" : 2047, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1048576 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 8 + }, + "colours" : { + "minColourArgb" : 352321535, + "maxColourArgb" : 687865855, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 40, + "alphaTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 5, + "maxDelay" : 10, + "minSpawn" : 1600, + "maxSpawn" : 1920, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "813.png" + } +}, { + "id" : "61", + "spread" : { + "yawMax" : 2047, + "pitchMax" : 79 + }, + "speed" : { + "minSpeed" : 393216, + "maxSpeed" : 655360 + }, + "scale" : { + "minScale" : 12, + "maxScale" : 12 + }, + "colours" : { + "minColourArgb" : 675953711, + "maxColourArgb" : 1012024890, + "targetColourArgb" : 7363913, + "colourTransitionPercent" : 40, + "alphaTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 960, + "maxSpawn" : 1280, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "810.png" + } +}, { + "id" : "62", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 838, + "pitchMax" : 195 + }, + "speed" : { + "minSpeed" : 393216, + "maxSpeed" : 655360 + }, + "scale" : { + "minScale" : 12, + "maxScale" : 12 + }, + "colours" : { + "minColourArgb" : 341067062, + "maxColourArgb" : 676217915, + "targetColourArgb" : 5852481, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 70, + "minSpawn" : 960, + "maxSpawn" : 1280, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "810.png" + } +}, { + "id" : "63", + "spread" : { + "yawMin" : 8, + "yawMax" : 316, + "pitchMin" : 805, + "pitchMax" : 1011 + }, + "speed" : { + "minSpeed" : 1310720, + "maxSpeed" : 1572864, + "targetSpeed" : 6291456 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 4, + "targetScale" : 4 + }, + "colours" : { + "minColourArgb" : -936106487, + "maxColourArgb" : -601482234, + "targetColourArgb" : 1774080, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 256, + "maxSpawn" : 320, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "816.png" + } +}, { + "id" : "64", + "spread" : { + "yawMax" : 751, + "pitchMin" : 66, + "pitchMax" : 271 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144, + "targetSpeed" : 4194304 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 2, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : -1270138605, + "maxColourArgb" : -936237814, + "targetColourArgb" : 2431232, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 60, + "minSpawn" : 256, + "maxSpawn" : 320, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "816.png" + } +}, { + "id" : "65", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 536, + "pitchMax" : 662 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432, + "targetSpeed" : 4194304 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 2, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : -1602789844, + "maxColourArgb" : -1268560091, + "targetColourArgb" : 3942400, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 140, + "maxDelay" : 160, + "minSpawn" : 1280, + "maxSpawn" : 1600, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "816.png" + } +}, { + "id" : "66", + "spread" : { + "yawMin" : 525, + "yawMax" : 525, + "pitchMin" : 530, + "pitchMax" : 530 + }, + "speed" : { + "minSpeed" : -183501, + "maxSpeed" : -262144 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 10, + "targetScale" : 3 + }, + "colours" : { + "minColourArgb" : 1023410175, + "maxColourArgb" : 1694498764, + "targetColourArgb" : 16777215, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 200, + "maxDelay" : 250, + "minSpawn" : 6, + "maxSpawn" : 19, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "67", + "spread" : { + "yawMin" : 525, + "yawMax" : 525, + "pitchMin" : 530, + "pitchMax" : 530 + }, + "speed" : { + "minSpeed" : -183501, + "maxSpeed" : -262144 + }, + "scale" : { + "minScale" : 14, + "maxScale" : 14, + "targetScale" : 8 + }, + "colours" : { + "minColourArgb" : 1023410175, + "maxColourArgb" : 1694498764, + "targetColourArgb" : 16777215, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 300, + "maxDelay" : 400, + "minSpawn" : 6, + "maxSpawn" : 19, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "68", + "spread" : { }, + "speed" : { + "targetSpeed" : 0, + "speedTransitionPercent" : 0 + }, + "scale" : { + "minScale" : 14, + "maxScale" : 7, + "targetScale" : 8 + }, + "colours" : { + "minColourArgb" : 1023410175, + "maxColourArgb" : 1694498764, + "targetColourArgb" : 16777215, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 128, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "69", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 527, + "pitchMax" : 552 + }, + "speed" : { + "minSpeed" : 104857, + "maxSpeed" : 157286 + }, + "scale" : { + "minScale" : 15, + "maxScale" : 25, + "targetScale" : 80 + }, + "colours" : { + "minColourArgb" : 506671923, + "maxColourArgb" : 345610649, + "targetColourArgb" : 6710886, + "colourTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 300, + "maxDelay" : 600, + "minSpawn" : 32, + "maxSpawn" : 64, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "70", + "spread" : { + "yawMax" : 225, + "pitchMin" : 494, + "pitchMax" : 914 + }, + "speed" : { + "minSpeed" : 1835008, + "maxSpeed" : 2359296 + }, + "scale" : { + "minScale" : 32, + "maxScale" : 32, + "targetScale" : 32 + }, + "colours" : { + "minColourArgb" : 172968775, + "maxColourArgb" : 508710217, + "targetColourArgb" : 7366751, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 15, + "maxDelay" : 20, + "minSpawn" : 256, + "maxSpawn" : 320, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "810.png" + } +}, { + "id" : "71", + "spread" : { }, + "speed" : { + "minSpeed" : -78643, + "maxSpeed" : -131072, + "targetSpeed" : -524288, + "speedTransitionPercent" : 0 + }, + "scale" : { + "minScale" : 14, + "maxScale" : 7, + "targetScale" : 8 + }, + "colours" : { + "minColourArgb" : 1023410175, + "maxColourArgb" : 1694498764, + "targetColourArgb" : 16777215, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 640, + "maxSpawn" : 704, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "72", + "spread" : { }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 52428, + "targetSpeed" : 209715 + }, + "scale" : { + "minScale" : 12, + "maxScale" : 3 + }, + "colours" : { + "minColourArgb" : 687865855, + "maxColourArgb" : 1023410124, + "targetColourArgb" : 16777215, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 320, + "maxSpawn" : 448, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "73", + "spread" : { + "yawMax" : 64, + "pitchMin" : 924, + "pitchMax" : 963 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144, + "targetSpeed" : 2097152 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 10 + }, + "colours" : { + "minColourArgb" : 2027876862, + "maxColourArgb" : 1683856603, + "targetColourArgb" : 1264946076, + "colourTransitionPercent" : 40, + "alphaTransitionPercent" : 25 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 384, + "maxSpawn" : 640, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "815.png" + } +}, { + "id" : "74", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 767, + "pitchMax" : 881 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288, + "targetSpeed" : 2097152 + }, + "scale" : { + "minScale" : 12, + "maxScale" : 14, + "targetScale" : 6 + }, + "colours" : { + "minColourArgb" : 2026950819, + "maxColourArgb" : 1690156438, + "targetColourArgb" : 1684432384, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 128, + "maxSpawn" : 192, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "815.png" + } +}, { + "id" : "75", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 767, + "pitchMax" : 881 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432, + "targetSpeed" : 3145728 + }, + "scale" : { + "minScale" : 12, + "maxScale" : 14, + "targetScale" : 6 + }, + "colours" : { + "minColourArgb" : 2027876862, + "maxColourArgb" : 1683856603, + "targetColourArgb" : 1684376476, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 256, + "maxSpawn" : 384, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "815.png" + } +}, { + "id" : "76", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 578, + "pitchMax" : 703 + }, + "speed" : { + "minSpeed" : 2097152, + "maxSpeed" : 2621440, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 2, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : 2030043135, + "maxColourArgb" : 1687801087, + "targetColourArgb" : 1271726079, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 30, + "minSpawn" : 128, + "maxSpawn" : 192, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + } +}, { + "id" : "77", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 502, + "pitchMax" : 517 + }, + "speed" : { + "minSpeed" : 1572864, + "maxSpeed" : 1835008, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 7, + "maxScale" : 10, + "targetScale" : 16 + }, + "colours" : { + "minColourArgb" : 2027876862, + "maxColourArgb" : 1683856603, + "targetColourArgb" : 1264946076, + "colourTransitionPercent" : 40, + "alphaTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 70, + "minSpawn" : 448, + "maxSpawn" : 640, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "814.png" + } +}, { + "id" : "78", + "spread" : { + "yawMin" : 525, + "yawMax" : 525, + "pitchMin" : 530, + "pitchMax" : 530 + }, + "speed" : { + "minSpeed" : -183501, + "maxSpeed" : -262144 + }, + "scale" : { + "minScale" : 14, + "maxScale" : 14, + "targetScale" : 8 + }, + "colours" : { + "minColourArgb" : 1023410175, + "maxColourArgb" : 1694498764, + "targetColourArgb" : 16777215, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 300, + "maxDelay" : 400, + "minSpawn" : 6, + "maxSpawn" : 19, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "79", + "spread" : { + "yawMin" : 525, + "yawMax" : 525, + "pitchMin" : 530, + "pitchMax" : 530 + }, + "speed" : { + "minSpeed" : -183501, + "maxSpeed" : -262144 + }, + "scale" : { + "minScale" : 14, + "maxScale" : 14, + "targetScale" : 8 + }, + "colours" : { + "minColourArgb" : 1023410175, + "maxColourArgb" : 1694498764, + "targetColourArgb" : 16777215, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 300, + "maxDelay" : 400, + "minSpawn" : 6, + "maxSpawn" : 19, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "80", + "spread" : { + "yawMin" : 525, + "yawMax" : 525, + "pitchMin" : 530, + "pitchMax" : 530 + }, + "speed" : { + "minSpeed" : -183501, + "maxSpeed" : -262144 + }, + "scale" : { + "minScale" : 14, + "maxScale" : 14, + "targetScale" : 8 + }, + "colours" : { + "minColourArgb" : 1023410175, + "maxColourArgb" : 1694498764, + "targetColourArgb" : 16777215, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 300, + "maxDelay" : 400, + "minSpawn" : 6, + "maxSpawn" : 19, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "81", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 674, + "pitchMax" : 781 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288, + "targetSpeed" : 2097152 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 12, + "targetScale" : 4 + }, + "colours" : { + "minColourArgb" : 1342177280, + "maxColourArgb" : 1694498815, + "targetColourArgb" : 16777215, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 25, + "maxDelay" : 30, + "minSpawn" : 640, + "maxSpawn" : 960, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "82", + "spread" : { + "yawMin" : 1023, + "yawMax" : 1128, + "pitchMin" : 428, + "pitchMax" : 498 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 524288, + "targetSpeed" : 2097152 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 10, + "targetScale" : 4 + }, + "colours" : { + "minColourArgb" : 1342177280, + "maxColourArgb" : 1694498815, + "targetColourArgb" : 1694498815, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 11, + "maxDelay" : 11, + "minSpawn" : 640, + "maxSpawn" : 960, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "83", + "spread" : { }, + "speed" : { + "targetSpeed" : 0, + "speedTransitionPercent" : 0 + }, + "scale" : { + "minScale" : 14, + "maxScale" : 10, + "targetScale" : 4, + "scaleTransitionPercent" : 80 + }, + "colours" : { + "minColourArgb" : -2130706433, + "maxColourArgb" : -1056964660, + "targetColourArgb" : 16777215, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 1280, + "maxSpawn" : 1600, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "84", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 576, + "pitchMax" : 683 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1310720, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 12, + "targetScale" : 4 + }, + "colours" : { + "minColourArgb" : -2147483648, + "maxColourArgb" : -1056964609, + "targetColourArgb" : 16777215, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 70, + "maxDelay" : 80, + "minSpawn" : 256, + "maxSpawn" : 384, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "85", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 584, + "pitchMax" : 662 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 655360, + "targetSpeed" : 2097152 + }, + "scale" : { + "minScale" : 28, + "maxScale" : 26, + "targetScale" : 4 + }, + "colours" : { + "minColourArgb" : 2030016768, + "maxColourArgb" : 1694498662, + "targetColourArgb" : 1264975872, + "alphaTransitionPercent" : 25 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 64, + "maxSpawn" : 128, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "813.png" + } +}, { + "id" : "86", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 584, + "pitchMax" : 662 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 157286, + "targetSpeed" : 1572864, + "speedTransitionPercent" : 80 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 8, + "targetScale" : 32, + "scaleTransitionPercent" : 80 + }, + "colours" : { + "minColourArgb" : 674444083, + "maxColourArgb" : 1006632960, + "targetColourArgb" : 6710886, + "colourTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 80, + "minSpawn" : 48, + "maxSpawn" : 64, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "810.png" + } +}, { + "id" : "87", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 555, + "pitchMax" : 620 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288, + "targetSpeed" : 9437184 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 2, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1694498764, + "maxColourArgb" : 2030042982, + "targetColourArgb" : 1358915072, + "colourTransitionPercent" : 80, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 60, + "minSpawn" : 192, + "maxSpawn" : 320, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "811.png" + } +}, { + "id" : "88", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 780, + "pitchMax" : 858 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1048576, + "targetSpeed" : 4194304 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 2, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1694498764, + "maxColourArgb" : 2030042982, + "targetColourArgb" : 1358915072, + "colourTransitionPercent" : 80, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 22, + "maxDelay" : 25, + "minSpawn" : 192, + "maxSpawn" : 320, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "811.png" + } +}, { + "id" : "89", + "spread" : { + "yawMax" : 2047, + "pitchMax" : 312 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288 + }, + "scale" : { + "minScale" : 12, + "maxScale" : 12 + }, + "colours" : { + "minColourArgb" : 677138242, + "maxColourArgb" : 1013406542, + "targetColourArgb" : 7036496, + "colourTransitionPercent" : 40, + "alphaTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 960, + "maxSpawn" : 1280, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "810.png" + } +}, { + "id" : "90", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 720, + "pitchMax" : 662 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432 + }, + "scale" : { + "minScale" : 12, + "maxScale" : 12 + }, + "colours" : { + "minColourArgb" : 341329976, + "maxColourArgb" : 677204552, + "targetColourArgb" : 7891549, + "colourTransitionPercent" : 40, + "alphaTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 70, + "maxDelay" : 80, + "minSpawn" : 640, + "maxSpawn" : 768, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "810.png" + } +}, { + "id" : "91", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 598, + "pitchMax" : 767 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432, + "targetSpeed" : 9437184 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 2, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : 1694498764, + "maxColourArgb" : 2030042982, + "targetColourArgb" : 1358915072, + "colourTransitionPercent" : 80, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 20, + "minSpawn" : 640, + "maxSpawn" : 768, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "811.png" + } +}, { + "id" : "92", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 775, + "pitchMax" : 264 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 393216 + }, + "scale" : { + "minScale" : 48, + "maxScale" : 64, + "targetScale" : 16 + }, + "colours" : { + "minColourArgb" : 687865855, + "maxColourArgb" : 855638015, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 20, + "alphaTransitionPercent" : 95 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 60, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "813.png" + } +}, { + "id" : "93", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 767, + "pitchMax" : 950 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144 + }, + "scale" : { + "minScale" : 32, + "maxScale" : 64, + "targetScale" : 92 + }, + "colours" : { + "minColourArgb" : 1342177280, + "maxColourArgb" : 1681077043, + "targetColourArgb" : 10066329, + "colourTransitionPercent" : 80, + "alphaTransitionPercent" : 90 + }, + "emission" : { + "minDelay" : 110, + "maxDelay" : 120, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "94", + "spread" : { + "yawMax" : 2047 + }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 52428 + }, + "scale" : { + "minScale" : 24, + "maxScale" : 26, + "targetScale" : 12 + }, + "colours" : { + "minColourArgb" : 687865855, + "maxColourArgb" : 855638015, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 20, + "alphaTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 30, + "minSpawn" : 160, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "813.png" + } +}, { + "id" : "95", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 581, + "pitchMax" : 669 + }, + "speed" : { + "minSpeed" : 1572864, + "maxSpeed" : 1835008 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 2, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : 1694498764, + "maxColourArgb" : 2030042982, + "targetColourArgb" : 16777215, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 55, + "maxDelay" : 60, + "minSpawn" : 320, + "maxSpawn" : 384, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "811.png" + } +}, { + "id" : "96", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 555, + "pitchMax" : 620 + }, + "speed" : { + "minSpeed" : 13107, + "maxSpeed" : 26214, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 2, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1694498764, + "maxColourArgb" : 2030042982, + "targetColourArgb" : 1358915072, + "colourTransitionPercent" : 80, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 50, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "811.png" + } +}, { + "id" : "97", + "spread" : { + "yawMin" : 525, + "yawMax" : 525, + "pitchMin" : 530, + "pitchMax" : 530 + }, + "speed" : { + "minSpeed" : -183501, + "maxSpeed" : -262144 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 10, + "targetScale" : 3 + }, + "colours" : { + "minColourArgb" : -1258291201, + "maxColourArgb" : -922746932, + "targetColourArgb" : 16777215, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 75, + "maxDelay" : 100, + "minSpawn" : 6, + "maxSpawn" : 19, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "98", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 410, + "pitchMax" : 511 + }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 52428 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 15, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : 1342203596, + "maxColourArgb" : 2016634623, + "targetColourArgb" : 13209 + }, + "emission" : { + "minDelay" : 75, + "maxDelay" : 80, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "99", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 410, + "pitchMax" : 511 + }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 52428 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 15, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : 1358888960, + "maxColourArgb" : 2029990707, + "targetColourArgb" : 10027008 + }, + "emission" : { + "minDelay" : 75, + "maxDelay" : 80, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "100", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 410, + "pitchMax" : 511 + }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 52428 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 15, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : 1342216448, + "maxColourArgb" : 2013318144, + "targetColourArgb" : 26112 + }, + "emission" : { + "minDelay" : 75, + "maxDelay" : 80, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "101", + "spread" : { + "yawMin" : 16, + "yawMax" : 2020, + "pitchMin" : 560, + "pitchMax" : 642 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 1048576 + }, + "scale" : { + "minScale" : 32, + "maxScale" : 32, + "targetScale" : 32 + }, + "colours" : { + "minColourArgb" : 340409391, + "maxColourArgb" : 674636289, + "targetColourArgb" : 2757633, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 80, + "maxDelay" : 80, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "717.png" + } +}, { + "id" : "102", + "spread" : { + "yawMin" : 525, + "yawMax" : 525, + "pitchMin" : 530, + "pitchMax" : 530 + }, + "speed" : { + "minSpeed" : -524288, + "maxSpeed" : -786432 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 10, + "targetScale" : 3 + }, + "colours" : { + "minColourArgb" : -1258291201, + "maxColourArgb" : -922746932, + "targetColourArgb" : 16777215, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 100, + "minSpawn" : 6, + "maxSpawn" : 19, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "103", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 694, + "pitchMax" : 906 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1310720 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 1 + }, + "colours" : { + "minColourArgb" : 1694498764, + "maxColourArgb" : 2030042982, + "targetColourArgb" : 16777215, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 70, + "maxDelay" : 80, + "minSpawn" : 768, + "maxSpawn" : 960, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "811.png" + } +}, { + "id" : "104", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 540, + "pitchMax" : 497 + }, + "speed" : { + "minSpeed" : 2097152, + "maxSpeed" : 2621440 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 1 + }, + "colours" : { + "minColourArgb" : 1694498764, + "maxColourArgb" : 2030042982, + "targetColourArgb" : 16777215, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 60, + "minSpawn" : 768, + "maxSpawn" : 960, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "811.png" + } +}, { + "id" : "105", + "spread" : { + "pitchMin" : 1023, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 78643, + "maxSpeed" : 131072 + }, + "scale" : { + "minScale" : 15, + "maxScale" : 15, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : 686620671, + "maxColourArgb" : 852295679, + "targetColourArgb" : 16777215, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 80, + "maxDelay" : 100, + "minSpawn" : 160, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "106", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 626, + "pitchMax" : 498 + }, + "speed" : { + "minSpeed" : 1179648, + "maxSpeed" : 1572864 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 15, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : 1342177280, + "maxColourArgb" : 1694498815, + "targetColourArgb" : 16777215, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 80, + "minSpawn" : 384, + "maxSpawn" : 512, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "107", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 626, + "pitchMax" : 498 + }, + "speed" : { + "minSpeed" : 1048576, + "maxSpeed" : 1310720 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 15, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : 1348901375, + "maxColourArgb" : 2020003071, + "targetColourArgb" : 16777215 + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 80, + "minSpawn" : 192, + "maxSpawn" : 256, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "108", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 626, + "pitchMax" : 498 + }, + "speed" : { + "minSpeed" : 1048576, + "maxSpeed" : 1310720 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 15, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : 1348927334, + "maxColourArgb" : 2023358361, + "targetColourArgb" : 16777215 + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 80, + "minSpawn" : 192, + "maxSpawn" : 256, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "109", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 626, + "pitchMax" : 498 + }, + "speed" : { + "minSpeed" : 1048576, + "maxSpeed" : 1310720 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 15, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : 1355572991, + "maxColourArgb" : 2030017023, + "targetColourArgb" : 16777215 + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 80, + "minSpawn" : 192, + "maxSpawn" : 256, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "110", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 529, + "pitchMax" : 466 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 1048576, + "targetSpeed" : 1048576 + }, + "scale" : { + "minScale" : 20, + "maxScale" : 10, + "targetScale" : 64 + }, + "colours" : { + "minColourArgb" : -6737152, + "maxColourArgb" : -6741760, + "targetColourArgb" : 3355443, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 150, + "maxDelay" : 200, + "minSpawn" : 3, + "maxSpawn" : 32, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "111", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 1011, + "pitchMax" : 410 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 786432 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 1, + "targetScale" : 2, + "scaleTransitionPercent" : 40 + }, + "colours" : { + "minColourArgb" : -256, + "maxColourArgb" : -986896, + "targetColourArgb" : 15790320, + "colourTransitionPercent" : 10, + "alphaTransitionPercent" : 15, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 100, + "minSpawn" : 64, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "735.png" + } +}, { + "id" : "112", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 557, + "pitchMax" : 619 + }, + "speed" : { + "minSpeed" : 1310720, + "maxSpeed" : 2097152, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 1, + "targetScale" : 2, + "scaleTransitionPercent" : 40 + }, + "colours" : { + "minColourArgb" : -13261, + "maxColourArgb" : -986896, + "targetColourArgb" : 16777164, + "colourTransitionPercent" : 10, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 50, + "minSpawn" : 2, + "maxSpawn" : 5, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "735.png" + } +}, { + "id" : "113", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 529, + "pitchMax" : 466 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 1048576, + "targetSpeed" : 1048576 + }, + "scale" : { + "minScale" : 20, + "maxScale" : 10, + "targetScale" : 64 + }, + "colours" : { + "minColourArgb" : -16763905, + "maxColourArgb" : -16763956, + "targetColourArgb" : 3355443, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 150, + "maxDelay" : 200, + "minSpawn" : 3, + "maxSpawn" : 32, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "114", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 1011, + "pitchMax" : 410 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 786432 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 1, + "targetScale" : 2, + "scaleTransitionPercent" : 40 + }, + "colours" : { + "minColourArgb" : -256, + "maxColourArgb" : -986896, + "targetColourArgb" : 15790320, + "colourTransitionPercent" : 10, + "alphaTransitionPercent" : 15, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 100, + "minSpawn" : 64, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "735.png" + } +}, { + "id" : "115", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 557, + "pitchMax" : 619 + }, + "speed" : { + "minSpeed" : 1310720, + "maxSpeed" : 2097152, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 1, + "targetScale" : 2, + "scaleTransitionPercent" : 40 + }, + "colours" : { + "minColourArgb" : -13261, + "maxColourArgb" : -986896, + "targetColourArgb" : 16777164, + "colourTransitionPercent" : 10, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 50, + "minSpawn" : 2, + "maxSpawn" : 5, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "735.png" + } +}, { + "id" : "116", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 540, + "pitchMax" : 431 + }, + "speed" : { + "minSpeed" : 26, + "maxSpeed" : 131072, + "targetSpeed" : 104857 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 4, + "targetScale" : 2, + "scaleTransitionPercent" : 80 + }, + "colours" : { + "minColourArgb" : 1691104000, + "maxColourArgb" : -1765002496, + "targetColourArgb" : 16737792, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 20, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "813.png" + } +}, { + "id" : "117", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 441, + "pitchMax" : 403 + }, + "speed" : { + "minSpeed" : 26, + "maxSpeed" : 26, + "targetSpeed" : 104 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 4, + "targetScale" : 2, + "scaleTransitionPercent" : 80 + }, + "colours" : { + "minColourArgb" : 1691090944, + "maxColourArgb" : -1761620992, + "targetColourArgb" : 16737792, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 8, + "maxDelay" : 18, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "813.png" + } +}, { + "id" : "118", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 450, + "pitchMax" : 416 + }, + "speed" : { + "minSpeed" : 26, + "maxSpeed" : 26, + "targetSpeed" : 104 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1691104000, + "maxColourArgb" : -1765015552, + "targetColourArgb" : 13421772, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 8, + "maxDelay" : 16, + "minSpawn" : 128, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "813.png" + } +}, { + "id" : "119", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 503, + "pitchMax" : 349 + }, + "speed" : { + "minSpeed" : 52428, + "maxSpeed" : 52428, + "targetSpeed" : 209715 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 2, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : 1687813990, + "maxColourArgb" : -1771634944, + "targetColourArgb" : 13434828, + "alphaTransitionPercent" : 70, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 50, + "minSpawn" : 6, + "maxSpawn" : 25, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "719.png" + } +}, { + "id" : "120", + "spread" : { + "yawMin" : 15, + "yawMax" : 2038, + "pitchMin" : 991 + }, + "speed" : { + "minSpeed" : 52428, + "maxSpeed" : 52428, + "targetSpeed" : 209715 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 2, + "targetScale" : 2, + "scaleTransitionPercent" : 20 + }, + "colours" : { + "minColourArgb" : 1677747967, + "maxColourArgb" : 1677747967, + "targetColourArgb" : 39423, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 50, + "minSpawn" : 128, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "818.png" + } +}, { + "id" : "121", + "spread" : { + "yawMin" : 15, + "yawMax" : 2038, + "pitchMin" : 991 + }, + "speed" : { + "minSpeed" : 262, + "maxSpeed" : 2621, + "targetSpeed" : 1048 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 5, + "targetScale" : 12 + }, + "colours" : { + "minColourArgb" : 1684406476, + "maxColourArgb" : -1771700071, + "targetColourArgb" : 6684825, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 25, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "818.png" + } +}, { + "id" : "122", + "spread" : { + "yawMin" : 15, + "yawMax" : 2038, + "pitchMin" : 565, + "pitchMax" : 503 + }, + "speed" : { + "minSpeed" : 3145728, + "maxSpeed" : 3932160, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 4, + "maxScale" : 6, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : -931827615, + "maxColourArgb" : -7370380, + "targetColourArgb" : 1689365917, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 80, + "minSpawn" : 128, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "810.png" + } +}, { + "id" : "123", + "spread" : { + "yawMin" : 15, + "yawMax" : 2038, + "pitchMin" : 636, + "pitchMax" : 520 + }, + "speed" : { + "minSpeed" : 3932160, + "maxSpeed" : 3932160, + "targetSpeed" : 12582912 + }, + "scale" : { + "minScale" : 7, + "maxScale" : 5, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : -10001583, + "maxColourArgb" : -9476016, + "targetColourArgb" : -8948900, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 25, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "810.png" + } +}, { + "id" : "124", + "spread" : { + "yawMax" : 64, + "pitchMin" : 584, + "pitchMax" : 662 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144, + "targetSpeed" : 2097152 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 1, + "targetScale" : 8 + }, + "colours" : { + "minColourArgb" : 2027876862, + "maxColourArgb" : 1683856603, + "targetColourArgb" : 1684376476, + "colourTransitionPercent" : 60, + "alphaTransitionPercent" : 25 + }, + "emission" : { + "minDelay" : 28, + "maxDelay" : 28, + "minSpawn" : 96, + "maxSpawn" : 128, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "820.png" + } +}, { + "id" : "125", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 58, + "pitchMax" : 372 + }, + "speed" : { + "minSpeed" : 52428, + "maxSpeed" : 78643, + "targetSpeed" : 524288 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 10, + "targetScale" : 10 + }, + "colours" : { + "minColourArgb" : 184549375, + "maxColourArgb" : 1021110774, + "targetColourArgb" : 513127618, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 22, + "minSpawn" : 32, + "maxSpawn" : 64, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "820.png" + } +}, { + "id" : "126", + "spread" : { + "yawMin" : 1014, + "yawMax" : 815, + "pitchMin" : 899, + "pitchMax" : 652 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 393216, + "targetSpeed" : 1887436 + }, + "scale" : { + "minScale" : 7, + "maxScale" : 10, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : 2030016768, + "maxColourArgb" : 1694498662, + "targetColourArgb" : 1264975872, + "alphaTransitionPercent" : 25 + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 15, + "minSpawn" : 256, + "maxSpawn" : 384, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "718.png" + } +}, { + "id" : "127", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 584, + "pitchMax" : 662 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144, + "targetSpeed" : 1572864, + "speedTransitionPercent" : 80 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 12, + "targetScale" : 32 + }, + "colours" : { + "minColourArgb" : 674444083, + "maxColourArgb" : 1013330688, + "targetColourArgb" : 10066329, + "colourTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 50, + "minSpawn" : 25, + "maxSpawn" : 38, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "717.png" + } +}, { + "id" : "128", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 662, + "pitchMax" : 949 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1310720, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 1, + "targetScale" : 7 + }, + "colours" : { + "minColourArgb" : -1776019450, + "maxColourArgb" : -96786688, + "targetColourArgb" : 6710886 + }, + "emission" : { + "minDelay" : 15, + "maxDelay" : 30, + "minSpawn" : 32, + "maxSpawn" : 51, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "717.png" + } +}, { + "id" : "129", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 767, + "pitchMax" : 881 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432, + "targetSpeed" : 3145728 + }, + "scale" : { + "minScale" : 12, + "maxScale" : 14, + "targetScale" : 6 + }, + "colours" : { + "minColourArgb" : 2030043135, + "maxColourArgb" : 1692332542, + "targetColourArgb" : 1684376476, + "colourTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 30, + "minSpawn" : 128, + "maxSpawn" : 192, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "815.png" + } +}, { + "id" : "130", + "spread" : { + "yawMin" : 1535, + "yawMax" : 2029, + "pitchMin" : 457, + "pitchMax" : 940 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144, + "targetSpeed" : 1572864, + "speedTransitionPercent" : 80 + }, + "scale" : { + "minScale" : 35, + "maxScale" : 15, + "targetScale" : 38 + }, + "colours" : { + "minColourArgb" : 520093695, + "maxColourArgb" : 1020054732, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 45, + "maxDelay" : 65, + "minSpawn" : 25, + "maxSpawn" : 38, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "717.png" + } +}, { + "id" : "131", + "spread" : { + "yawMax" : 104, + "pitchMin" : 503, + "pitchMax" : 552 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 314572 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 2, + "targetScale" : 24 + }, + "colours" : { + "minColourArgb" : 852282572, + "maxColourArgb" : 1184471449, + "targetColourArgb" : 14934754, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 70, + "minSpawn" : 32, + "maxSpawn" : 64, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "850.png" + } +}, { + "id" : "132", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 527, + "pitchMax" : 552 + }, + "speed" : { + "minSpeed" : 157286, + "maxSpeed" : 209715 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 4, + "targetScale" : 64 + }, + "colours" : { + "minColourArgb" : 674444083, + "maxColourArgb" : 838860800, + "targetColourArgb" : 6710886, + "colourTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 200, + "maxDelay" : 250, + "minSpawn" : 3, + "maxSpawn" : 9, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "850.png" + } +}, { + "id" : "133", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 527, + "pitchMax" : 552 + }, + "speed" : { + "minSpeed" : 157286, + "maxSpeed" : 209715 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 4, + "targetScale" : 64 + }, + "colours" : { + "minColourArgb" : 1177760563, + "maxColourArgb" : 1509949440, + "targetColourArgb" : 6710886, + "colourTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 200, + "maxDelay" : 250, + "minSpawn" : 6, + "maxSpawn" : 12, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "850.png" + } +}, { + "id" : "TORCH_SMOKE", + "general" : { + "heightOffset" : 250 + }, + "spread" : { + "yawMax" : 2047, + "pitchMin" : 527, + "pitchMax" : 552 + }, + "speed" : { + "minSpeed" : 157286, + "maxSpeed" : 209715 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 4, + "targetScale" : 64 + }, + "colours" : { + "minColourArgb" : 1177760563, + "maxColourArgb" : 1509949440, + "targetColourArgb" : 6710886, + "colourTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 200, + "maxDelay" : 250, + "minSpawn" : 6, + "maxSpawn" : 12, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "850.png" + } +}, { + "id" : "134", + "spread" : { + "yawMax" : 2034, + "pitchMin" : 591, + "pitchMax" : 662 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 1, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : 855637862, + "maxColourArgb" : 1191182284, + "targetColourArgb" : 16737792, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 70, + "minSpawn" : 12, + "maxSpawn" : 32, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "849.png" + } +}, { + "id" : "135", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 527, + "pitchMax" : 552 + }, + "speed" : { + "minSpeed" : 157286, + "maxSpeed" : 209715 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 4, + "targetScale" : 48 + }, + "colours" : { + "minColourArgb" : 1177760563, + "maxColourArgb" : 1509949440, + "targetColourArgb" : 6710886, + "colourTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 200, + "maxDelay" : 250, + "minSpawn" : 3, + "maxSpawn" : 9, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "850.png" + } +}, { + "id" : "136", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 309, + "pitchMax" : 717 + }, + "speed" : { + "minSpeed" : 393216, + "maxSpeed" : 524288 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 16, + "targetScale" : 128 + }, + "colours" : { + "minColourArgb" : 1687787929, + "maxColourArgb" : 2013265920, + "targetColourArgb" : 3355443, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 60, + "minSpawn" : 192, + "maxSpawn" : 320, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "850.png" + } +}, { + "id" : "137", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 186, + "pitchMax" : 490 + }, + "speed" : { + "minSpeed" : 2097152, + "maxSpeed" : 2621440 + }, + "scale" : { + "minScale" : 4, + "maxScale" : 6, + "targetScale" : 1, + "scaleTransitionPercent" : 60 + }, + "colours" : { + "minColourArgb" : 855637862, + "maxColourArgb" : 1191182284, + "targetColourArgb" : 16737792, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 90, + "minSpawn" : 320, + "maxSpawn" : 448, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "849.png" + } +}, { + "id" : "138", + "spread" : { + "yawMax" : 104, + "pitchMin" : 503, + "pitchMax" : 552 + }, + "speed" : { + "minSpeed" : 78643, + "maxSpeed" : 104857 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 2, + "targetScale" : 12 + }, + "colours" : { + "minColourArgb" : 852282572, + "maxColourArgb" : 1184471449, + "targetColourArgb" : 14934754, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 70, + "maxDelay" : 90, + "minSpawn" : 12, + "maxSpawn" : 19, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "850.png" + } +}, { + "id" : "139", + "spread" : { + "yawMax" : 809, + "pitchMin" : 503, + "pitchMax" : 172 + }, + "speed" : { + "minSpeed" : 78643, + "maxSpeed" : 131072 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 2, + "targetScale" : 12 + }, + "colours" : { + "minColourArgb" : 852282572, + "maxColourArgb" : 1184471449, + "targetColourArgb" : 14934754, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 70, + "maxDelay" : 90, + "minSpawn" : 32, + "maxSpawn" : 659, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "850.png" + } +}, { + "id" : "140", + "spread" : { }, + "speed" : { + "minSpeed" : 157286, + "maxSpeed" : 209715 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1020002508, + "maxColourArgb" : 687800575, + "targetColourArgb" : 16724991, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 250, + "maxDelay" : 350, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "141", + "spread" : { }, + "speed" : { + "minSpeed" : 157286, + "maxSpeed" : 209715 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1020002508, + "maxColourArgb" : 687800575, + "targetColourArgb" : 16724991, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 150, + "maxDelay" : 200, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "142", + "spread" : { }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 183500 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1020002508, + "maxColourArgb" : 687800575, + "targetColourArgb" : 16724991, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 150, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "143", + "spread" : { }, + "speed" : { + "minSpeed" : 78643, + "maxSpeed" : 131072 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1020002508, + "maxColourArgb" : 687800575, + "targetColourArgb" : 16724991, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 100, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "144", + "spread" : { + "yawMax" : 64, + "pitchMin" : 584, + "pitchMax" : 662 + }, + "speed" : { + "minSpeed" : 52428, + "maxSpeed" : 131072, + "targetSpeed" : 1048576 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 1, + "targetScale" : 8 + }, + "colours" : { + "minColourArgb" : 2030017023, + "maxColourArgb" : 1689682129, + "targetColourArgb" : 1682911842, + "colourTransitionPercent" : 60, + "alphaTransitionPercent" : 25 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 50, + "minSpawn" : 96, + "maxSpawn" : 128, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "820.png" + } +}, { + "id" : "145", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 628, + "pitchMax" : 228 + }, + "speed" : { + "minSpeed" : 7864320, + "maxSpeed" : 10485760, + "targetSpeed" : 31457280 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 7, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : -10066432, + "maxColourArgb" : -6750208, + "targetColourArgb" : 10027008, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 70, + "minSpawn" : 384, + "maxSpawn" : 512, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "860.png" + } +}, { + "id" : "146", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 15, + "pitchMax" : 10 + }, + "speed" : { + "minSpeed" : 5242880, + "maxSpeed" : 7864320, + "targetSpeed" : 20971520 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 7, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : -10066432, + "maxColourArgb" : -6750208, + "targetColourArgb" : 10027008, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 30, + "minSpawn" : 320, + "maxSpawn" : 448, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "860.png" + } +}, { + "id" : "147", + "spread" : { + "yawMax" : 1230, + "pitchMin" : 609, + "pitchMax" : 389 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 393216, + "targetSpeed" : 2097152 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 2, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : -1, + "maxColourArgb" : -1, + "targetColourArgb" : 16777215, + "alphaTransitionPercent" : 75 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 70, + "minSpawn" : 192, + "maxSpawn" : 256, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "859.png" + } +}, { + "id" : "148", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 835, + "pitchMax" : 211 + }, + "speed" : { + "minSpeed" : 2621440, + "maxSpeed" : 5242880, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 10, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : -16763905, + "maxColourArgb" : -16763905, + "targetColourArgb" : 52479 + }, + "emission" : { + "minDelay" : 15, + "maxDelay" : 20, + "minSpawn" : 192, + "maxSpawn" : 256, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "858.png" + } +}, { + "id" : "149", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 15, + "pitchMax" : 10 + }, + "speed" : { + "minSpeed" : 7864, + "maxSpeed" : 13107, + "targetSpeed" : 10485 + }, + "scale" : { + "minScale" : 100, + "maxScale" : 150, + "targetScale" : 10 + }, + "colours" : { + "minColourArgb" : -6684673, + "maxColourArgb" : -10027009, + "targetColourArgb" : 6750207 + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 30, + "minSpawn" : 64, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "858.png" + } +}, { + "id" : "150", + "spread" : { + "yawMin" : 1938, + "yawMax" : 1368, + "pitchMin" : 781, + "pitchMax" : 603 + }, + "speed" : { + "minSpeed" : 7864320, + "maxSpeed" : 13107200, + "targetSpeed" : 10485760 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 5, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : -10066432, + "maxColourArgb" : -6711040, + "targetColourArgb" : -1768357888, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 25, + "minSpawn" : 512, + "maxSpawn" : 640, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "879.png" + } +}, { + "id" : "151", + "spread" : { + "yawMin" : 1864, + "yawMax" : 1297, + "pitchMin" : 1012, + "pitchMax" : 176 + }, + "speed" : { + "minSpeed" : 3932160, + "maxSpeed" : 5242880, + "targetSpeed" : 31457280 + }, + "scale" : { + "minScale" : 4, + "maxScale" : 7, + "targetScale" : 3 + }, + "colours" : { + "minColourArgb" : -10066432, + "maxColourArgb" : -10066432, + "targetColourArgb" : -6711040, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 15, + "minSpawn" : 64, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "879.png" + } +}, { + "id" : "152", + "spread" : { + "yawMin" : 79, + "yawMax" : 99, + "pitchMin" : 738, + "pitchMax" : 703 + }, + "speed" : { + "minSpeed" : 3932160, + "maxSpeed" : 5242880, + "targetSpeed" : 10485760 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 7, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : -10066432, + "maxColourArgb" : -10066432, + "targetColourArgb" : 848887808, + "colourTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 15, + "maxDelay" : 30, + "minSpawn" : 320, + "maxSpawn" : 512, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "879.png" + } +}, { + "id" : "153", + "spread" : { + "yawMax" : 1993, + "pitchMax" : 999 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 1572864, + "targetSpeed" : 3145728 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 15, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : 1687814143, + "maxColourArgb" : 1681116415, + "targetColourArgb" : 39321 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 50, + "minSpawn" : 256, + "maxSpawn" : 320, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "859.png" + } +}, { + "id" : "154", + "spread" : { + "yawMax" : 8, + "pitchMin" : 511, + "pitchMax" : 493 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144, + "targetSpeed" : 104857 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 15, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : 1687814143, + "maxColourArgb" : 1681116415, + "targetColourArgb" : 39321 + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 15, + "minSpawn" : 64, + "maxSpawn" : 96, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "859.png" + } +}, { + "id" : "155", + "spread" : { + "yawMax" : 8, + "pitchMin" : 511, + "pitchMax" : 493 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144, + "targetSpeed" : 104857 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 15, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : 1687814143, + "maxColourArgb" : 1681116415, + "targetColourArgb" : 39321 + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 15, + "minSpawn" : 64, + "maxSpawn" : 96, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "859.png" + } +}, { + "id" : "156", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 742, + "pitchMax" : 994 + }, + "speed" : { + "minSpeed" : 52428, + "maxSpeed" : 78643, + "targetSpeed" : 524288 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 10, + "targetScale" : 10 + }, + "colours" : { + "minColourArgb" : 1694163711, + "maxColourArgb" : 2028455154, + "targetColourArgb" : 13408996, + "colourTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 22, + "minSpawn" : 19, + "maxSpawn" : 32, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "820.png" + } +}, { + "id" : "157", + "spread" : { + "yawMax" : 8, + "pitchMin" : 511, + "pitchMax" : 493 + }, + "speed" : { + "minSpeed" : 52428, + "maxSpeed" : 78643, + "targetSpeed" : 734003 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 9, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : -1778384896, + "maxColourArgb" : -939524096, + "targetColourArgb" : 1118481 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 60, + "minSpawn" : 19, + "maxSpawn" : 32, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "158", + "spread" : { + "yawMax" : 8, + "pitchMin" : 511, + "pitchMax" : 493 + }, + "speed" : { + "minSpeed" : 52428, + "maxSpeed" : 78643, + "targetSpeed" : 734003 + }, + "scale" : { + "minScale" : 7, + "maxScale" : 8, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : -1777266415, + "maxColourArgb" : -938405615, + "targetColourArgb" : 2236962 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 60, + "minSpawn" : 19, + "maxSpawn" : 32, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "159", + "spread" : { + "yawMax" : 8, + "pitchMin" : 511, + "pitchMax" : 493 + }, + "speed" : { + "minSpeed" : 52428, + "maxSpeed" : 78643, + "targetSpeed" : 734003 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 7, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : -1776147934, + "maxColourArgb" : -937287134, + "targetColourArgb" : 3355443 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 60, + "minSpawn" : 19, + "maxSpawn" : 32, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "160", + "spread" : { + "yawMin" : 1034, + "yawMax" : 1489, + "pitchMin" : 355, + "pitchMax" : 591 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1048576, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 2, + "targetScale" : 8 + }, + "colours" : { + "minColourArgb" : -936142592, + "maxColourArgb" : -3342592, + "targetColourArgb" : 10066176 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 30, + "minSpawn" : 320, + "maxSpawn" : 448, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "161", + "spread" : { + "yawMin" : 1014, + "yawMax" : 838, + "pitchMin" : 429, + "pitchMax" : 527 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432, + "targetSpeed" : 4194304 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 3, + "targetScale" : 32 + }, + "colours" : { + "minColourArgb" : -1778384896, + "maxColourArgb" : -939524096, + "targetColourArgb" : 1118481 + }, + "emission" : { + "minDelay" : 25, + "maxDelay" : 35, + "minSpawn" : 128, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "162", + "spread" : { + "yawMax" : 2034, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 104857, + "maxSpeed" : 209715, + "targetSpeed" : 838860 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 5, + "targetScale" : 3, + "scaleTransitionPercent" : 50 + }, + "colours" : { + "minColourArgb" : -1, + "maxColourArgb" : -13395457, + "targetColourArgb" : 3355647, + "alphaTransitionPercent" : 45 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 130, + "minSpawn" : 64, + "maxSpawn" : 640, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "865.png" + } +}, { + "id" : "163", + "spread" : { + "yawMin" : 1174, + "yawMax" : 1938, + "pitchMin" : 419, + "pitchMax" : 355 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288, + "targetSpeed" : 524288 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 8, + "targetScale" : 1, + "scaleTransitionPercent" : 50 + }, + "colours" : { + "minColourArgb" : -13369345, + "maxColourArgb" : -13261, + "targetColourArgb" : 16777215, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 20, + "minSpawn" : 192, + "maxSpawn" : 320, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "859.png" + } +}, { + "id" : "164", + "spread" : { + "yawMin" : 2042, + "yawMax" : 1351, + "pitchMin" : 501, + "pitchMax" : 557 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288, + "targetSpeed" : 524288 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 11, + "targetScale" : 1, + "scaleTransitionPercent" : 50 + }, + "colours" : { + "minColourArgb" : -16737895, + "maxColourArgb" : -3342337, + "targetColourArgb" : 16777215, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 50, + "minSpawn" : 320, + "maxSpawn" : 640, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "859.png" + } +}, { + "id" : "165", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 698, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 393216, + "targetSpeed" : 524288 + }, + "scale" : { + "minScale" : 20, + "maxScale" : 30, + "targetScale" : 50, + "scaleTransitionPercent" : 50 + }, + "colours" : { + "minColourArgb" : -3342337, + "maxColourArgb" : -13369345, + "targetColourArgb" : 13434879, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 120, + "minSpawn" : 192, + "maxSpawn" : 320, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "166", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 574, + "pitchMax" : 414 + }, + "speed" : { + "minSpeed" : 1048576, + "maxSpeed" : 1835008, + "targetSpeed" : 4194304 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 5, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : -52, + "maxColourArgb" : -13312, + "targetColourArgb" : 16777164, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 75, + "minSpawn" : 128, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "859.png" + } +}, { + "id" : "167", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 607, + "pitchMax" : 720 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 16, + "targetScale" : 30 + }, + "colours" : { + "minColourArgb" : 852769748, + "maxColourArgb" : 1177747507, + "targetColourArgb" : 10254499 + }, + "emission" : { + "minDelay" : 80, + "maxDelay" : 100, + "minSpawn" : 64, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "868.png" + } +}, { + "id" : "168", + "spread" : { + "yawMin" : 98, + "yawMax" : 1977, + "pitchMin" : 528, + "pitchMax" : 511 + }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 262144 + }, + "scale" : { + "minScale" : 16, + "maxScale" : 15, + "targetScale" : 18 + }, + "colours" : { + "minColourArgb" : 681141760, + "maxColourArgb" : 1013343846, + "targetColourArgb" : 3355443 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 50, + "minSpawn" : 12, + "maxSpawn" : 32, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "868.png" + } +}, { + "id" : "169", + "spread" : { + "yawMin" : 98, + "yawMax" : 1977, + "pitchMin" : 528, + "pitchMax" : 511 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144 + }, + "scale" : { + "minScale" : 12, + "maxScale" : 15, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : 1358943529, + "maxColourArgb" : 1526704422, + "targetColourArgb" : 16771448, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 60, + "minSpawn" : 6, + "maxSpawn" : 32, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "867.png" + } +}, { + "id" : "170", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 1023, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 786432 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 10, + "targetScale" : 1, + "scaleTransitionPercent" : 90 + }, + "colours" : { + "minColourArgb" : 409377279, + "maxColourArgb" : 1885785343, + "targetColourArgb" : 1207985919, + "colourTransitionPercent" : 62, + "alphaTransitionPercent" : 88 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 35, + "minSpawn" : 64, + "maxSpawn" : 192, + "initialSpawn" : 10000, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "859.png" + } +}, { + "id" : "171", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 169, + "pitchMax" : 56 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 505937 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 5, + "scaleTransitionPercent" : 80 + }, + "colours" : { + "minColourArgb" : 335518054, + "maxColourArgb" : 1409246720, + "targetColourArgb" : 3342336, + "colourTransitionPercent" : 66, + "alphaTransitionPercent" : 89 + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 25, + "minSpawn" : 64, + "maxSpawn" : 192, + "initialSpawn" : 10000, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "718.png" + } +}, { + "id" : "172", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 604, + "pitchMax" : 691 + }, + "speed" : { + "minSpeed" : 1572864, + "maxSpeed" : 3145728, + "targetSpeed" : 4194304 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 2, + "targetScale" : 8 + }, + "colours" : { + "minColourArgb" : 1677721600, + "maxColourArgb" : -1258291201, + "targetColourArgb" : 5671065, + "colourTransitionPercent" : 50, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 80, + "minSpawn" : 320, + "maxSpawn" : 384, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "173", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 953, + "pitchMax" : 959 + }, + "speed" : { + "minSpeed" : 393216, + "maxSpeed" : 393216, + "targetSpeed" : 1572864 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 1, + "targetScale" : 20 + }, + "colours" : { + "minColourArgb" : 181593083, + "maxColourArgb" : 352321535, + "targetColourArgb" : -1269397351, + "colourTransitionPercent" : 50 + }, + "emission" : { + "minDelay" : 63, + "maxDelay" : 64, + "minSpawn" : 192, + "maxSpawn" : 256, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "820.png" + } +}, { + "id" : "174", + "spread" : { + "yawMax" : 64, + "pitchMin" : 584, + "pitchMax" : 662 + }, + "speed" : { + "minSpeed" : 78643, + "maxSpeed" : 131072, + "targetSpeed" : 1048576 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 3, + "targetScale" : 8 + }, + "colours" : { + "minColourArgb" : 350155262, + "maxColourArgb" : 683464174, + "targetColourArgb" : -1602115142, + "colourTransitionPercent" : 60, + "alphaTransitionPercent" : 20 + }, + "emission" : { + "minDelay" : 38, + "maxDelay" : 38, + "minSpawn" : 64, + "maxSpawn" : 96, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "820.png" + } +}, { + "id" : "175", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 58, + "pitchMax" : 372 + }, + "speed" : { + "minSpeed" : 52428, + "maxSpeed" : 78643, + "targetSpeed" : 524288 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 10, + "targetScale" : 10 + }, + "colours" : { + "minColourArgb" : 184549375, + "maxColourArgb" : 1021110774, + "targetColourArgb" : 9811138, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 22, + "minSpawn" : 64, + "maxSpawn" : 70, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "814.png" + } +}, { + "id" : "176", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 527, + "pitchMax" : 552 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 183500 + }, + "scale" : { + "minScale" : 15, + "maxScale" : 25, + "targetScale" : 64 + }, + "colours" : { + "minColourArgb" : 842216243, + "maxColourArgb" : 1013343846, + "targetColourArgb" : 14934754, + "colourTransitionPercent" : 40, + "alphaTransitionPercent" : 70 + }, + "emission" : { + "minDelay" : 320, + "maxDelay" : 350, + "minSpawn" : 2, + "maxSpawn" : 7, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "872.png" + } +}, { + "id" : "177", + "spread" : { + "yawMax" : 67, + "pitchMin" : 498, + "pitchMax" : 510 + }, + "speed" : { + "minSpeed" : 393216, + "maxSpeed" : 524288 + }, + "scale" : { + "minScale" : 25, + "maxScale" : 35, + "targetScale" : 128 + }, + "colours" : { + "minColourArgb" : 1191182335, + "maxColourArgb" : 1523967724, + "targetColourArgb" : 10857141, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 120, + "minSpawn" : 9, + "maxSpawn" : 12, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "871.png" + } +}, { + "id" : "178", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 527, + "pitchMax" : 552 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 314572 + }, + "scale" : { + "minScale" : 25, + "maxScale" : 35, + "targetScale" : 128 + }, + "colours" : { + "minColourArgb" : 1177760563, + "maxColourArgb" : 1516660326, + "targetColourArgb" : 14934754, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 350, + "maxDelay" : 400, + "minSpawn" : 2, + "maxSpawn" : 7, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "850.png" + } +}, { + "id" : "179", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 527, + "pitchMax" : 552 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 314572 + }, + "scale" : { + "minScale" : 25, + "maxScale" : 35, + "targetScale" : 128 + }, + "colours" : { + "minColourArgb" : 1177760563, + "maxColourArgb" : 1509949440, + "targetColourArgb" : 3355443, + "colourTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 350, + "maxDelay" : 400, + "minSpawn" : 2, + "maxSpawn" : 7, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "850.png" + } +}, { + "id" : "180", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 537, + "pitchMax" : 751 + }, + "speed" : { + "minSpeed" : 5242880, + "maxSpeed" : 7864320, + "targetSpeed" : 20971520 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 20, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : -10066432, + "maxColourArgb" : -13421824, + "targetColourArgb" : 26214, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 30, + "minSpawn" : 320, + "maxSpawn" : 448, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "816.png" + } +}, { + "id" : "181", + "spread" : { }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288, + "targetSpeed" : 20971520 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 10, + "targetScale" : 10, + "scaleTransitionPercent" : 5 + }, + "colours" : { + "minColourArgb" : -10066432, + "maxColourArgb" : -13421824, + "targetColourArgb" : 26163, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 70, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "816.png" + } +}, { + "id" : "182", + "spread" : { }, + "speed" : { + "minSpeed" : 5242880, + "maxSpeed" : 2621440, + "targetSpeed" : 10485760 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 40, + "targetScale" : 40, + "scaleTransitionPercent" : 5 + }, + "colours" : { + "minColourArgb" : -3355444, + "maxColourArgb" : -1, + "targetColourArgb" : 16777215 + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 20, + "minSpawn" : 128, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "717.png" + } +}, { + "id" : "183", + "spread" : { + "yawMin" : 8, + "yawMax" : 2015, + "pitchMin" : 820, + "pitchMax" : 706 + }, + "speed" : { + "minSpeed" : 1048576, + "maxSpeed" : 1310720, + "targetSpeed" : 5242880, + "speedTransitionPercent" : 4 + }, + "scale" : { + "minScale" : 20, + "maxScale" : 20, + "targetScale" : 20, + "scaleTransitionPercent" : 5 + }, + "colours" : { + "minColourArgb" : 1691091148, + "maxColourArgb" : 1687748761, + "targetColourArgb" : 16777215 + }, + "emission" : { + "minDelay" : 13, + "maxDelay" : 20, + "minSpawn" : 192, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "819.png" + } +}, { + "id" : "184", + "spread" : { + "yawMin" : 462, + "yawMax" : 650, + "pitchMin" : 841, + "pitchMax" : 79 + }, + "speed" : { + "minSpeed" : 1310720, + "maxSpeed" : 2097152, + "targetSpeed" : 8388608 + }, + "scale" : { + "minScale" : 4, + "maxScale" : 3, + "targetScale" : 4, + "scaleTransitionPercent" : 4 + }, + "colours" : { + "minColourArgb" : -6724096, + "maxColourArgb" : -13395712, + "targetColourArgb" : 3394560 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 128, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "816.png" + } +}, { + "id" : "185", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 624, + "pitchMax" : 795 + }, + "speed" : { + "minSpeed" : 5242880, + "maxSpeed" : 2097152, + "targetSpeed" : 20971520, + "speedTransitionPercent" : 50 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 12, + "targetScale" : 12 + }, + "colours" : { + "minColourArgb" : -7064830, + "maxColourArgb" : -12311010, + "targetColourArgb" : -932826368 + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 30, + "minSpawn" : 160, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "873.png" + } +}, { + "id" : "186", + "spread" : { + "yawMax" : 2047, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 2621440, + "maxSpeed" : 3932160, + "targetSpeed" : 1048576, + "speedTransitionPercent" : 30 + }, + "scale" : { + "minScale" : 60, + "maxScale" : 80, + "targetScale" : 1, + "scaleTransitionPercent" : 80 + }, + "colours" : { + "minColourArgb" : -6737152, + "maxColourArgb" : -13312, + "targetColourArgb" : 8881274 + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 30, + "minSpawn" : 96, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "874.png" + } +}, { + "id" : "187", + "spread" : { + "yawMin" : 986, + "yawMax" : 1131, + "pitchMin" : 428, + "pitchMax" : 459 + }, + "speed" : { + "minSpeed" : 1310720, + "maxSpeed" : 2621440, + "targetSpeed" : 10485760 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 4, + "targetScale" : 8, + "scaleTransitionPercent" : 4 + }, + "colours" : { + "minColourArgb" : -3355444, + "maxColourArgb" : -1, + "targetColourArgb" : 16777215 + }, + "emission" : { + "minDelay" : 3, + "maxDelay" : 10, + "minSpawn" : 256, + "maxSpawn" : 256, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "736.png" + } +}, { + "id" : "188", + "spread" : { + "yawMin" : 1499, + "yawMax" : 798, + "pitchMin" : 683, + "pitchMax" : 411 + }, + "speed" : { + "minSpeed" : 1310720, + "maxSpeed" : 2621440, + "targetSpeed" : 10485760 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 4, + "targetScale" : 8, + "scaleTransitionPercent" : 8 + }, + "colours" : { + "minColourArgb" : -16763905, + "maxColourArgb" : -13395457, + "targetColourArgb" : 16777215 + }, + "emission" : { + "minDelay" : 4, + "maxDelay" : 6, + "minSpawn" : 192, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "858.png" + } +}, { + "id" : "189", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 624, + "pitchMax" : 795 + }, + "speed" : { + "minSpeed" : 5242880, + "maxSpeed" : 2097152, + "targetSpeed" : 20971520, + "speedTransitionPercent" : 50 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 12, + "targetScale" : 12 + }, + "colours" : { + "minColourArgb" : -7064830, + "maxColourArgb" : -12311010, + "targetColourArgb" : -932826368 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 60, + "minSpawn" : 320, + "maxSpawn" : 384, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "873.png" + } +}, { + "id" : "190", + "spread" : { + "yawMax" : 2047, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 2621440, + "maxSpeed" : 3932160, + "targetSpeed" : 1048576, + "speedTransitionPercent" : 30 + }, + "scale" : { + "minScale" : 60, + "maxScale" : 80, + "targetScale" : 1, + "scaleTransitionPercent" : 80 + }, + "colours" : { + "minColourArgb" : -6737152, + "maxColourArgb" : -13312, + "targetColourArgb" : 8881274 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 60, + "minSpawn" : 192, + "maxSpawn" : 256, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "874.png" + } +}, { + "id" : "191", + "spread" : { + "yawMin" : 441, + "yawMax" : 605, + "pitchMin" : 83, + "pitchMax" : 177 + }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 131072, + "targetSpeed" : 1048576, + "speedTransitionPercent" : 12 + }, + "scale" : { + "minScale" : 50, + "maxScale" : 30, + "targetScale" : 10, + "scaleTransitionPercent" : 70 + }, + "colours" : { + "minColourArgb" : 1694485708, + "maxColourArgb" : 1694433280, + "targetColourArgb" : 6684672 + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 80, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "859.png" + } +}, { + "id" : "192", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 273, + "pitchMax" : 754 + }, + "speed" : { + "minSpeed" : 1572864, + "maxSpeed" : 2621440, + "targetSpeed" : 5242880, + "speedTransitionPercent" : 10 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 15, + "targetScale" : 8, + "scaleTransitionPercent" : 8 + }, + "colours" : { + "minColourArgb" : 1694485708, + "maxColourArgb" : 1694433280, + "targetColourArgb" : 6684672 + }, + "emission" : { + "minDelay" : 5, + "maxDelay" : 50, + "minSpawn" : 256, + "maxSpawn" : 640, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "859.png" + } +}, { + "id" : "193", + "spread" : { + "yawMin" : 100, + "yawMax" : 343, + "pitchMin" : 750, + "pitchMax" : 945 + }, + "speed" : { + "minSpeed" : 1572864, + "maxSpeed" : 2621440, + "targetSpeed" : 5242880, + "speedTransitionPercent" : 10 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 15, + "targetScale" : 8, + "scaleTransitionPercent" : 8 + }, + "colours" : { + "minColourArgb" : 1694485708, + "maxColourArgb" : 1694433280, + "targetColourArgb" : 6684672 + }, + "emission" : { + "minDelay" : 5, + "maxDelay" : 29, + "minSpawn" : 64, + "maxSpawn" : 320, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "873.png" + } +}, { + "id" : "194", + "spread" : { + "yawMin" : 2037, + "yawMax" : 17, + "pitchMin" : 79, + "pitchMax" : 154 + }, + "speed" : { + "minSpeed" : 2621, + "maxSpeed" : 13107 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 4, + "targetScale" : 7, + "scaleTransitionPercent" : 10 + }, + "colours" : { + "minColourArgb" : 1526726604, + "maxColourArgb" : 1691130112, + "targetColourArgb" : 16750848 + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 30, + "minSpawn" : 256, + "maxSpawn" : 320, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "859.png" + } +}, { + "id" : "195", + "spread" : { + "yawMin" : 2037, + "yawMax" : 17, + "pitchMin" : 79, + "pitchMax" : 154 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432, + "targetSpeed" : 524288, + "speedTransitionPercent" : 5 + }, + "scale" : { + "minScale" : 27, + "maxScale" : 25, + "targetScale" : 10, + "scaleTransitionPercent" : 40 + }, + "colours" : { + "minColourArgb" : 1513331199, + "maxColourArgb" : 1687814143, + "targetColourArgb" : 1194375, + "colourTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 50, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "819.png" + } +}, { + "id" : "196", + "spread" : { + "yawMin" : 2037, + "yawMax" : 17, + "pitchMin" : 79, + "pitchMax" : 154 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432, + "targetSpeed" : 524288, + "speedTransitionPercent" : 5 + }, + "scale" : { + "minScale" : 27, + "maxScale" : 25, + "targetScale" : 10, + "scaleTransitionPercent" : 40 + }, + "colours" : { + "minColourArgb" : 1526713344, + "maxColourArgb" : 1694498713, + "targetColourArgb" : 10646803, + "colourTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 50, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "819.png" + } +}, { + "id" : "197", + "spread" : { + "yawMin" : 2037, + "yawMax" : 17, + "pitchMin" : 79, + "pitchMax" : 154 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432, + "targetSpeed" : 524288, + "speedTransitionPercent" : 5 + }, + "scale" : { + "minScale" : 27, + "maxScale" : 25, + "targetScale" : 10, + "scaleTransitionPercent" : 40 + }, + "colours" : { + "minColourArgb" : 1511840039, + "maxColourArgb" : 1687027557, + "targetColourArgb" : 19200, + "colourTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 50, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "819.png" + } +}, { + "id" : "198", + "spread" : { + "yawMin" : 2037, + "yawMax" : 17, + "pitchMin" : 79, + "pitchMax" : 154 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432, + "targetSpeed" : 524288, + "speedTransitionPercent" : 5 + }, + "scale" : { + "minScale" : 27, + "maxScale" : 25, + "targetScale" : 10, + "scaleTransitionPercent" : 40 + }, + "colours" : { + "minColourArgb" : 1526661120, + "maxColourArgb" : 1694459494, + "targetColourArgb" : 6684672, + "colourTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 50, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "819.png" + } +}, { + "id" : "199", + "spread" : { + "yawMin" : 12, + "yawMax" : 2047, + "pitchMin" : 7, + "pitchMax" : 1011 + }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 131072, + "targetSpeed" : 1048576, + "speedTransitionPercent" : 5 + }, + "scale" : { + "minScale" : 4, + "maxScale" : 12, + "targetScale" : 4, + "scaleTransitionPercent" : 5 + }, + "colours" : { + "minColourArgb" : 1694498815, + "maxColourArgb" : 1694498560, + "targetColourArgb" : 16777113 + }, + "emission" : { + "minDelay" : 5, + "maxDelay" : 35, + "minSpawn" : 256, + "maxSpawn" : 320, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "744.png" + } +}, { + "id" : "200", + "spread" : { + "yawMin" : 12, + "yawMax" : 2047, + "pitchMin" : 7, + "pitchMax" : 1011 + }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 131072, + "targetSpeed" : 1048576, + "speedTransitionPercent" : 5 + }, + "scale" : { + "minScale" : 4, + "maxScale" : 12, + "targetScale" : 4, + "scaleTransitionPercent" : 5 + }, + "colours" : { + "minColourArgb" : 1694485606, + "maxColourArgb" : 1694459392, + "targetColourArgb" : 13382400 + }, + "emission" : { + "minDelay" : 5, + "maxDelay" : 35, + "minSpawn" : 256, + "maxSpawn" : 320, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "744.png" + } +}, { + "id" : "201", + "spread" : { + "yawMin" : 12, + "yawMax" : 2047, + "pitchMin" : 7, + "pitchMax" : 1011 + }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 131072, + "targetSpeed" : 1048576, + "speedTransitionPercent" : 5 + }, + "scale" : { + "minScale" : 4, + "maxScale" : 12, + "targetScale" : 4, + "scaleTransitionPercent" : 5 + }, + "colours" : { + "minColourArgb" : 1694433535, + "maxColourArgb" : 1687748761, + "targetColourArgb" : 6684876 + }, + "emission" : { + "minDelay" : 5, + "maxDelay" : 35, + "minSpawn" : 256, + "maxSpawn" : 320, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "744.png" + } +}, { + "id" : "202", + "spread" : { + "yawMin" : 8, + "yawMax" : 2015, + "pitchMin" : 820, + "pitchMax" : 706 + }, + "speed" : { + "minSpeed" : 1048576, + "maxSpeed" : 1310720, + "targetSpeed" : 5242880, + "speedTransitionPercent" : 4 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 4, + "targetScale" : 6, + "scaleTransitionPercent" : 5 + }, + "colours" : { + "minColourArgb" : 1687814143, + "maxColourArgb" : 1681103359, + "targetColourArgb" : 16777215 + }, + "emission" : { + "minDelay" : 13, + "maxDelay" : 20, + "minSpawn" : 192, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "819.png" + } +}, { + "id" : "203", + "spread" : { }, + "speed" : { + "minSpeed" : 157286, + "maxSpeed" : 209715 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1016659968, + "maxColourArgb" : 684457984, + "targetColourArgb" : 16711680, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 250, + "maxDelay" : 350, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "204", + "spread" : { }, + "speed" : { + "minSpeed" : 157286, + "maxSpeed" : 209715 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1016659968, + "maxColourArgb" : 684457984, + "targetColourArgb" : 16711680, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 150, + "maxDelay" : 200, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "205", + "spread" : { }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 183500 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1016659968, + "maxColourArgb" : 684457984, + "targetColourArgb" : 16711680, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 150, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "206", + "spread" : { }, + "speed" : { + "minSpeed" : 78643, + "maxSpeed" : 131072 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 3, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1016659968, + "maxColourArgb" : 684457984, + "targetColourArgb" : 16711680, + "colourTransitionPercent" : 10 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 100, + "minSpawn" : 4, + "maxSpawn" : 6, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "759.png" + } +}, { + "id" : "207", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 742, + "pitchMax" : 994 + }, + "speed" : { + "minSpeed" : 52428, + "maxSpeed" : 78643, + "targetSpeed" : 524288 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 10, + "targetScale" : 10 + }, + "colours" : { + "minColourArgb" : 1694485708, + "maxColourArgb" : 2030016921, + "targetColourArgb" : 16737894, + "colourTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 22, + "minSpawn" : 19, + "maxSpawn" : 32, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "820.png" + } +}, { + "id" : "208", + "spread" : { + "yawMax" : 8, + "pitchMin" : 511, + "pitchMax" : 493 + }, + "speed" : { + "minSpeed" : 52428, + "maxSpeed" : 78643, + "targetSpeed" : 734003 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 9, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : -1778384896, + "maxColourArgb" : -939524096, + "targetColourArgb" : 1118481 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 60, + "minSpawn" : 19, + "maxSpawn" : 32, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "209", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 520, + "pitchMax" : 533 + }, + "speed" : { + "minSpeed" : 1310720, + "maxSpeed" : 1572864, + "targetSpeed" : 4194304 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 7, + "targetScale" : 15 + }, + "colours" : { + "minColourArgb" : 1020453883, + "maxColourArgb" : -1761607681, + "targetColourArgb" : 515891694, + "colourTransitionPercent" : 50 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 50, + "minSpawn" : 32, + "maxSpawn" : 51, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "820.png" + } +}, { + "id" : "210", + "spread" : { + "yawMax" : 2015, + "pitchMax" : 979 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432 + }, + "scale" : { + "minScale" : 15, + "maxScale" : 20, + "targetScale" : 50 + }, + "colours" : { + "minColourArgb" : 184549375, + "maxColourArgb" : 181193932, + "targetColourArgb" : 10092543, + "colourTransitionPercent" : 70 + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 60, + "minSpawn" : 128, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "868.png" + } +}, { + "id" : "211", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 1023, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 2097152, + "targetSpeed" : 3145728 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 5, + "targetScale" : 90 + }, + "colours" : { + "minColourArgb" : 1684432486, + "maxColourArgb" : 2016621363, + "targetColourArgb" : 6710886 + }, + "emission" : { + "minDelay" : 25, + "maxDelay" : 50, + "minSpawn" : 960, + "maxSpawn" : 2560, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "766.png" + } +}, { + "id" : "212", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 1023, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 2097152, + "targetSpeed" : 3145728 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 5, + "targetScale" : 90 + }, + "colours" : { + "minColourArgb" : 1681129471, + "maxColourArgb" : 2023332249, + "targetColourArgb" : 26214 + }, + "emission" : { + "minDelay" : 25, + "maxDelay" : 50, + "minSpawn" : 960, + "maxSpawn" : 2560, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "766.png" + } +}, { + "id" : "213", + "spread" : { }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 262144, + "targetSpeed" : 1048576 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 6, + "targetScale" : 7 + }, + "colours" : { + "minColourArgb" : 13395456, + "maxColourArgb" : 6684672, + "targetColourArgb" : -1 + }, + "emission" : { + "minDelay" : 80, + "maxDelay" : 80, + "maxSpawn" : 12, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "878.png" + } +}, { + "id" : "214", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 520, + "pitchMax" : 533 + }, + "speed" : { + "minSpeed" : 1310720, + "maxSpeed" : 1572864, + "targetSpeed" : 4194304 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 7, + "targetScale" : 15 + }, + "colours" : { + "minColourArgb" : 1020453883, + "maxColourArgb" : -1761607681, + "targetColourArgb" : 515891694, + "colourTransitionPercent" : 50 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 50, + "minSpawn" : 32, + "maxSpawn" : 51, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "820.png" + } +}, { + "id" : "215", + "spread" : { + "yawMin" : 441, + "yawMax" : 605, + "pitchMin" : 83, + "pitchMax" : 177 + }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 131072, + "targetSpeed" : 1048576, + "speedTransitionPercent" : 5 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 15, + "targetScale" : 8, + "scaleTransitionPercent" : 8 + }, + "colours" : { + "minColourArgb" : 1694498815, + "maxColourArgb" : 1677774079, + "targetColourArgb" : 65535 + }, + "emission" : { + "minDelay" : 5, + "maxDelay" : 20, + "minSpawn" : 256, + "maxSpawn" : 320, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "859.png" + } +}, { + "id" : "216", + "spread" : { + "yawMin" : 441, + "yawMax" : 605, + "pitchMin" : 83, + "pitchMax" : 177 + }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 131072, + "targetSpeed" : 1048576, + "speedTransitionPercent" : 5 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 15, + "targetScale" : 8, + "scaleTransitionPercent" : 8 + }, + "colours" : { + "minColourArgb" : 1694498815, + "maxColourArgb" : 1677774079, + "targetColourArgb" : 65535 + }, + "emission" : { + "minDelay" : 5, + "maxDelay" : 20, + "minSpawn" : 256, + "maxSpawn" : 320, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "859.png" + } +}, { + "id" : "217", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 706, + "pitchMax" : 658 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 1048576, + "targetSpeed" : 1048576 + }, + "scale" : { + "minScale" : 20, + "maxScale" : 30, + "targetScale" : 60 + }, + "colours" : { + "minColourArgb" : 1677721600, + "maxColourArgb" : 2016621363, + "targetColourArgb" : 3355443 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 100, + "minSpawn" : 64, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "736.png" + } +}, { + "id" : "218", + "spread" : { }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 2621440 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 1, + "targetScale" : 30 + }, + "colours" : { + "minColourArgb" : 1694498713, + "maxColourArgb" : 2030016819, + "targetColourArgb" : 16737792 + }, + "emission" : { + "minDelay" : 1, + "maxDelay" : 20, + "minSpawn" : 320, + "maxSpawn" : 448, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "889.png" + } +}, { + "id" : "219", + "spread" : { + "yawMax" : 2047, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 13107200 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 5, + "targetScale" : 90 + }, + "colours" : { + "minColourArgb" : 1694498560, + "maxColourArgb" : 2029977600, + "targetColourArgb" : 16764057 + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 30, + "minSpawn" : 256, + "maxSpawn" : 384, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "858.png" + } +}, { + "id" : "220", + "spread" : { }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 5242880 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 1, + "targetScale" : 30 + }, + "colours" : { + "minColourArgb" : 1694498815, + "maxColourArgb" : 2013266175, + "targetColourArgb" : 6750207 + }, + "emission" : { + "minDelay" : 1, + "maxDelay" : 10, + "minSpawn" : 320, + "maxSpawn" : 448, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "889.png" + } +}, { + "id" : "221", + "spread" : { + "yawMax" : 2047, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 13107200 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 5, + "targetScale" : 90 + }, + "colours" : { + "minColourArgb" : 1694498815, + "maxColourArgb" : 2013279231, + "targetColourArgb" : 13434879 + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 30, + "minSpawn" : 256, + "maxSpawn" : 384, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "858.png" + } +}, { + "id" : "222", + "spread" : { }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 2621440 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 1, + "targetScale" : 30 + }, + "colours" : { + "minColourArgb" : 1694498713, + "maxColourArgb" : 2030016819, + "targetColourArgb" : 16737792 + }, + "emission" : { + "minDelay" : 1, + "maxDelay" : 20, + "minSpawn" : 320, + "maxSpawn" : 448, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "889.png" + } +}, { + "id" : "223", + "spread" : { }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 2621440 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 1, + "targetScale" : 30 + }, + "colours" : { + "minColourArgb" : 1694498713, + "maxColourArgb" : 2030016819, + "targetColourArgb" : 16737792 + }, + "emission" : { + "minDelay" : 1, + "maxDelay" : 20, + "minSpawn" : 320, + "maxSpawn" : 448, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "889.png" + } +}, { + "id" : "224", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 607, + "pitchMax" : 720 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 16, + "targetScale" : 30 + }, + "colours" : { + "minColourArgb" : 845545472, + "maxColourArgb" : 1177747456, + "targetColourArgb" : 1, + "colourTransitionPercent" : 70 + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 70, + "minSpawn" : 96, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "868.png" + } +}, { + "id" : "225", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 288, + "pitchMax" : 720 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 16, + "targetScale" : 30 + }, + "colours" : { + "minColourArgb" : 845545472, + "maxColourArgb" : 1177747456, + "targetColourArgb" : 1, + "colourTransitionPercent" : 70 + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 70, + "minSpawn" : 96, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "868.png" + } +}, { + "id" : "226", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 607, + "pitchMax" : 720 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 16, + "targetScale" : 48 + }, + "colours" : { + "minColourArgb" : 845545472, + "maxColourArgb" : 1177747456, + "targetColourArgb" : 1, + "colourTransitionPercent" : 70 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 50, + "minSpawn" : 96, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "868.png" + } +}, { + "id" : "227", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 320, + "pitchMax" : 894 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1048576, + "targetSpeed" : 9437184 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 2, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : 1694498764, + "maxColourArgb" : 2030042982, + "targetColourArgb" : 1358915072, + "colourTransitionPercent" : 80, + "alphaTransitionPercent" : 60, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 40, + "minSpawn" : 256, + "maxSpawn" : 320, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "811.png" + } +}, { + "id" : "228", + "spread" : { + "yawMin" : 969, + "yawMax" : 1837, + "pitchMin" : 459, + "pitchMax" : 561 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1048576 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 16, + "targetScale" : 48 + }, + "colours" : { + "minColourArgb" : 852295475, + "maxColourArgb" : 1187839744, + "targetColourArgb" : 10079232, + "colourTransitionPercent" : 70 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 50, + "minSpawn" : 64, + "maxSpawn" : 96, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "868.png" + } +}, { + "id" : "229", + "spread" : { + "yawMin" : 969, + "yawMax" : 1837, + "pitchMin" : 459, + "pitchMax" : 561 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1048576 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 16, + "targetScale" : 48 + }, + "colours" : { + "minColourArgb" : 845584895, + "maxColourArgb" : 1177786879, + "targetColourArgb" : 13311, + "colourTransitionPercent" : 70 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 50, + "minSpawn" : 64, + "maxSpawn" : 96, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "868.png" + } +}, { + "id" : "230", + "spread" : { + "yawMin" : 969, + "yawMax" : 1837, + "pitchMin" : 459, + "pitchMax" : 561 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1048576 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 16, + "targetScale" : 48 + }, + "colours" : { + "minColourArgb" : 851669343, + "maxColourArgb" : 1185436472, + "targetColourArgb" : 8405554, + "colourTransitionPercent" : 70 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 50, + "minSpawn" : 64, + "maxSpawn" : 96, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "868.png" + } +}, { + "id" : "231", + "spread" : { + "yawMin" : 8, + "yawMax" : 2015, + "pitchMin" : 820, + "pitchMax" : 706 + }, + "speed" : { + "minSpeed" : 1048576, + "maxSpeed" : 1310720, + "targetSpeed" : 5242880, + "speedTransitionPercent" : 4 + }, + "scale" : { + "minScale" : 15, + "maxScale" : 15, + "targetScale" : 15, + "scaleTransitionPercent" : 5 + }, + "colours" : { + "minColourArgb" : 1691091148, + "maxColourArgb" : 1687748761, + "targetColourArgb" : 16777215 + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 20, + "minSpawn" : 128, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "859.png" + } +}, { + "id" : "232", + "spread" : { + "yawMin" : 8, + "yawMax" : 2015, + "pitchMin" : 820, + "pitchMax" : 706 + }, + "speed" : { + "minSpeed" : 1048576, + "maxSpeed" : 1310720, + "targetSpeed" : 5242880, + "speedTransitionPercent" : 4 + }, + "scale" : { + "minScale" : 15, + "maxScale" : 15, + "targetScale" : 15, + "scaleTransitionPercent" : 5 + }, + "colours" : { + "minColourArgb" : 1681077247, + "maxColourArgb" : 1677747967, + "targetColourArgb" : 16777215 + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 20, + "minSpawn" : 192, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "859.png" + } +}, { + "id" : "233", + "spread" : { + "yawMin" : 8, + "yawMax" : 2015, + "pitchMin" : 820, + "pitchMax" : 706 + }, + "speed" : { + "minSpeed" : 1048576, + "maxSpeed" : 1310720, + "targetSpeed" : 5242880, + "speedTransitionPercent" : 4 + }, + "scale" : { + "minScale" : 15, + "maxScale" : 15, + "targetScale" : 15, + "scaleTransitionPercent" : 5 + }, + "colours" : { + "minColourArgb" : 1694498611, + "maxColourArgb" : 1694485504, + "targetColourArgb" : 16777215 + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 20, + "minSpawn" : 192, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "859.png" + } +}, { + "id" : "234", + "spread" : { }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 393216, + "targetSpeed" : 1048576, + "speedTransitionPercent" : 80 + }, + "scale" : { + "minScale" : 35, + "maxScale" : 20, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : 687852646, + "maxColourArgb" : 1023383808, + "targetColourArgb" : 16777062, + "colourTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 50, + "minSpawn" : 25, + "maxSpawn" : 38, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "813.png" + } +}, { + "id" : "235", + "spread" : { + "yawMin" : 1132, + "yawMax" : 1299, + "pitchMin" : 441, + "pitchMax" : 511 + }, + "speed" : { + "minSpeed" : 996147, + "maxSpeed" : 1101004, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 15, + "maxScale" : 8, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : -1258291201, + "maxColourArgb" : 855638015, + "targetColourArgb" : -1761607681, + "colourTransitionPercent" : 25, + "alphaTransitionPercent" : 10, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 180, + "maxDelay" : 250, + "minSpawn" : 6, + "maxSpawn" : 38, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "236", + "spread" : { + "yawMin" : 209, + "yawMax" : 2028, + "pitchMin" : 419, + "pitchMax" : 225 + }, + "speed" : { + "minSpeed" : 1048576, + "maxSpeed" : 1310720, + "targetSpeed" : 5242880, + "speedTransitionPercent" : 4 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 4, + "targetScale" : 8, + "scaleTransitionPercent" : 1 + }, + "colours" : { + "minColourArgb" : -922746881, + "maxColourArgb" : -932786945, + "targetColourArgb" : 13260 + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 50, + "minSpawn" : 64, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "859.png" + } +}, { + "id" : "237", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 973, + "pitchMax" : 248 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 786432 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 3, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : 352321484, + "maxColourArgb" : 922746879, + "targetColourArgb" : 16777215, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 45, + "maxDelay" : 41, + "minSpawn" : 128, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "collidesWithObjects" : true, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "723.png" + } +}, { + "id" : "238", + "spread" : { }, + "speed" : { + "minSpeed" : 3932160, + "maxSpeed" : 5242880, + "targetSpeed" : 10485760, + "speedTransitionPercent" : 20 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 5, + "targetScale" : 15, + "scaleTransitionPercent" : 20 + }, + "colours" : { + "minColourArgb" : -1, + "maxColourArgb" : -1, + "targetColourArgb" : 16777215, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 15, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "902.png" + } +}, { + "id" : "239", + "spread" : { }, + "speed" : { + "minSpeed" : 3932160, + "maxSpeed" : 5242880, + "targetSpeed" : 10485760, + "speedTransitionPercent" : 20 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 5, + "targetScale" : 15, + "scaleTransitionPercent" : 20 + }, + "colours" : { + "minColourArgb" : -1, + "maxColourArgb" : -1, + "targetColourArgb" : 16777215, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 15, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "902.png" + } +}, { + "id" : "240", + "spread" : { + "yawMax" : 70 + }, + "speed" : { + "minSpeed" : 3932160, + "maxSpeed" : 5242880, + "targetSpeed" : 10485760, + "speedTransitionPercent" : 20 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 5, + "targetScale" : 15, + "scaleTransitionPercent" : 20 + }, + "colours" : { + "minColourArgb" : -1, + "maxColourArgb" : -1, + "targetColourArgb" : 16777215, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 15, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "902.png" + } +}, { + "id" : "241", + "spread" : { }, + "speed" : { + "minSpeed" : 3932160, + "maxSpeed" : 5242880, + "targetSpeed" : 10485760, + "speedTransitionPercent" : 20 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 5, + "targetScale" : 15, + "scaleTransitionPercent" : 20 + }, + "colours" : { + "minColourArgb" : -1, + "maxColourArgb" : -1, + "targetColourArgb" : 16777215, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 15, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "902.png" + } +}, { + "id" : "242", + "spread" : { + "yawMin" : 2021, + "yawMax" : 1304, + "pitchMin" : 835, + "pitchMax" : 970 + }, + "speed" : { + "minSpeed" : 78643, + "maxSpeed" : 157286, + "targetSpeed" : 209715 + }, + "scale" : { + "minScale" : 20, + "maxScale" : 10, + "targetScale" : 60 + }, + "colours" : { + "minColourArgb" : -15265276, + "maxColourArgb" : -10066330, + "targetColourArgb" : 9209979 + }, + "emission" : { + "minDelay" : 300, + "maxDelay" : 350, + "minSpawn" : 12, + "maxSpawn" : 25, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "902.png" + } +}, { + "id" : "243", + "spread" : { + "yawMin" : 1263, + "yawMax" : 447, + "pitchMin" : 773, + "pitchMax" : 652 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 209715, + "targetSpeed" : 838860 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 7, + "targetScale" : 30 + }, + "colours" : { + "minColourArgb" : -14344168, + "maxColourArgb" : -9673391, + "targetColourArgb" : 9210497 + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 200, + "minSpawn" : 12, + "maxSpawn" : 32, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "902.png" + } +}, { + "id" : "244", + "spread" : { + "yawMin" : 1535, + "yawMax" : 2003, + "pitchMin" : 960, + "pitchMax" : 720 + }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 157286, + "targetSpeed" : 1048576 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 7, + "targetScale" : 40 + }, + "colours" : { + "minColourArgb" : -13421773, + "maxColourArgb" : -9542060, + "targetColourArgb" : 8881274 + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 200, + "minSpawn" : 12, + "maxSpawn" : 38, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "902.png" + } +}, { + "id" : "245", + "spread" : { + "yawMax" : 2047, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 78643, + "maxSpeed" : 157286, + "targetSpeed" : 209715 + }, + "scale" : { + "minScale" : 30, + "maxScale" : 20, + "targetScale" : 10, + "scaleTransitionPercent" : 30 + }, + "colours" : { + "minColourArgb" : -7064830, + "maxColourArgb" : -12311010, + "targetColourArgb" : 6697728 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 50, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "902.png" + } +}, { + "id" : "246", + "spread" : { }, + "speed" : { + "minSpeed" : 3932160, + "maxSpeed" : 5242880, + "targetSpeed" : 1048576, + "speedTransitionPercent" : 30 + }, + "scale" : { + "minScale" : 40, + "maxScale" : 25, + "targetScale" : 10, + "scaleTransitionPercent" : 80 + }, + "colours" : { + "minColourArgb" : -6737152, + "maxColourArgb" : -13312, + "targetColourArgb" : 8881274 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 50, + "minSpawn" : 192, + "maxSpawn" : 256, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "874.png" + } +}, { + "id" : "247", + "spread" : { }, + "speed" : { + "minSpeed" : 5242880, + "maxSpeed" : 7864320, + "targetSpeed" : 2097152, + "speedTransitionPercent" : 30 + }, + "scale" : { + "minScale" : 40, + "maxScale" : 25, + "targetScale" : 10, + "scaleTransitionPercent" : 80 + }, + "colours" : { + "minColourArgb" : -6737152, + "maxColourArgb" : -26368, + "targetColourArgb" : 8881274 + }, + "emission" : { + "minDelay" : 35, + "maxDelay" : 40, + "minSpawn" : 192, + "maxSpawn" : 256, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "874.png" + } +}, { + "id" : "248", + "spread" : { + "yawMin" : 511, + "yawMax" : 999, + "pitchMin" : 928, + "pitchMax" : 437 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 2359296 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 4, + "targetScale" : 40 + }, + "colours" : { + "minColourArgb" : 335544575, + "maxColourArgb" : 905969817, + "targetColourArgb" : 39423, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 31, + "minSpawn" : 384, + "maxSpawn" : 512, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "collidesWithObjects" : true, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "814.png" + } +}, { + "id" : "249", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 1007, + "pitchMax" : 892 + }, + "speed" : { + "minSpeed" : 1310720, + "maxSpeed" : 5242880 + }, + "scale" : { + "minScale" : 50, + "maxScale" : 52, + "targetScale" : 75 + }, + "colours" : { + "minColourArgb" : 342268415, + "maxColourArgb" : 909325311, + "targetColourArgb" : 520093798, + "alphaTransitionPercent" : 92, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 18, + "maxDelay" : 31, + "minSpawn" : 762, + "maxSpawn" : 1004, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "collidesWithObjects" : true, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "907.png" + } +}, { + "id" : "250", + "spread" : { + "yawMin" : 2039, + "yawMax" : 7, + "pitchMin" : 798, + "pitchMax" : 159 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 1048576 + }, + "scale" : { + "minScale" : 60, + "maxScale" : 1, + "targetScale" : 40 + }, + "colours" : { + "minColourArgb" : 335557427, + "maxColourArgb" : 912680550, + "targetColourArgb" : 3355443, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 31, + "minSpawn" : 192, + "maxSpawn" : 512, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "collidesWithObjects" : true, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "904.png" + } +}, { + "id" : "251", + "spread" : { + "yawMin" : 393, + "yawMax" : 626, + "pitchMin" : 23, + "pitchMax" : 365 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 2621440 + }, + "scale" : { + "minScale" : 35, + "maxScale" : 30, + "targetScale" : 15 + }, + "colours" : { + "minColourArgb" : 335557427, + "maxColourArgb" : 912680550, + "targetColourArgb" : 3355443, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 10, + "minSpawn" : 128, + "maxSpawn" : 320, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "collidesWithObjects" : true, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "892.png" + } +}, { + "id" : "252", + "spread" : { + "yawMin" : 2039, + "yawMax" : 7, + "pitchMin" : 798, + "pitchMax" : 159 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 1048576 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 20, + "targetScale" : 15 + }, + "colours" : { + "minColourArgb" : 677799679, + "maxColourArgb" : 909351423, + "targetColourArgb" : 26316, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 31, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "collidesWithObjects" : true, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "892.png" + } +}, { + "id" : "253", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 568, + "pitchMax" : 662 + }, + "speed" : { + "minSpeed" : 655360, + "maxSpeed" : 1310720, + "targetSpeed" : 2097152 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 12, + "targetScale" : 40 + }, + "colours" : { + "minColourArgb" : 516738048, + "maxColourArgb" : 1100585216, + "targetColourArgb" : 10079232, + "colourTransitionPercent" : 70 + }, + "emission" : { + "minDelay" : 58, + "maxDelay" : 60, + "minSpawn" : 64, + "maxSpawn" : 83, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "clipToTerrain" : false + }, + "texture" : { + "file" : "736.png" + } +}, { + "id" : "254", + "spread" : { }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 540016 + }, + "scale" : { + "minScale" : 43, + "maxScale" : 20, + "targetScale" : 10 + }, + "colours" : { + "minColourArgb" : -194615706, + "maxColourArgb" : -30198989, + "targetColourArgb" : 3355443, + "alphaTransitionPercent" : 75, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 145, + "minSpawn" : 25, + "maxSpawn" : 61, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "721.png" + } +}, { + "id" : "255", + "spread" : { }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 540016 + }, + "scale" : { + "minScale" : 72, + "maxScale" : 20, + "targetScale" : 10 + }, + "colours" : { + "minColourArgb" : -187904769, + "maxColourArgb" : -20132609, + "targetColourArgb" : 654245888, + "alphaTransitionPercent" : 78, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 185, + "minSpawn" : 13, + "maxSpawn" : 61, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "721.png" + } +}, { + "id" : "256", + "spread" : { + "yawMin" : 511, + "yawMax" : 512 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 540016 + }, + "scale" : { + "minScale" : 43, + "maxScale" : 20, + "targetScale" : 1, + "scaleTransitionPercent" : 59 + }, + "colours" : { + "minColourArgb" : 16724787, + "maxColourArgb" : 16737894, + "targetColourArgb" : 1681077043, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 78, + "minSpawn" : 46, + "maxSpawn" : 46, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "849.png" + } +}, { + "id" : "257", + "spread" : { + "yawMin" : 511, + "yawMax" : 511 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 262144 + }, + "scale" : { + "minScale" : 15, + "maxScale" : 10, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : -201287373, + "maxColourArgb" : -33489101, + "targetColourArgb" : 1221394380, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 14, + "maxDelay" : 50, + "minSpawn" : 72, + "maxSpawn" : 72, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "910.png" + } +}, { + "id" : "258", + "spread" : { + "yawMin" : 511, + "yawMax" : 511 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 262144 + }, + "scale" : { + "minScale" : 15, + "maxScale" : 10, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : 922747084, + "maxColourArgb" : -1701209601, + "targetColourArgb" : 1221381375, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 14, + "maxDelay" : 22, + "minSpawn" : 72, + "maxSpawn" : 72, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "910.png" + } +}, { + "id" : "259", + "spread" : { + "yawMin" : 511, + "yawMax" : 511 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 262144 + }, + "scale" : { + "minScale" : 15, + "maxScale" : 10, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : 936116224, + "maxColourArgb" : -1694538138, + "targetColourArgb" : 1224723660, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 14, + "maxDelay" : 22, + "minSpawn" : 72, + "maxSpawn" : 72, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "910.png" + } +}, { + "id" : "260", + "spread" : { + "yawMin" : 511, + "yawMax" : 511 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 262144 + }, + "scale" : { + "minScale" : 15, + "maxScale" : 10, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : 922772992, + "maxColourArgb" : -1711223757, + "targetColourArgb" : 1221394380, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 14, + "maxDelay" : 22, + "minSpawn" : 72, + "maxSpawn" : 72, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "910.png" + } +}, { + "id" : "261", + "spread" : { }, + "speed" : { + "minSpeed" : 2621440, + "maxSpeed" : 2621440 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 3, + "targetScale" : 3 + }, + "colours" : { + "minColourArgb" : 600624332, + "maxColourArgb" : 402653183, + "targetColourArgb" : 16777215 + }, + "emission" : { + "minDelay" : 4, + "maxDelay" : 8, + "minSpawn" : 384, + "maxSpawn" : 384, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "811.png" + } +}, { + "id" : "262", + "spread" : { + "yawMin" : 959, + "yawMax" : 1581, + "pitchMin" : 194, + "pitchMax" : 916 + }, + "speed" : { + "minSpeed" : 996147, + "maxSpeed" : 1101004, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 30, + "maxScale" : 20, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : 1691143372, + "maxColourArgb" : 1352243609, + "targetColourArgb" : -1768318567, + "colourTransitionPercent" : 25, + "alphaTransitionPercent" : 10, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 20, + "minSpawn" : 64, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "913.png" + } +}, { + "id" : "263", + "spread" : { + "yawMin" : 959, + "yawMax" : 1472, + "pitchMin" : 194, + "pitchMax" : 916 + }, + "speed" : { + "minSpeed" : 996147, + "maxSpeed" : 1101004, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 30, + "maxScale" : 20, + "targetScale" : 15 + }, + "colours" : { + "minColourArgb" : -1768318567, + "maxColourArgb" : 1684432486, + "targetColourArgb" : 674430976, + "colourTransitionPercent" : 45, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 20, + "minSpawn" : 64, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "913.png" + } +}, { + "id" : "264", + "spread" : { + "yawMin" : 959, + "yawMax" : 1472, + "pitchMin" : 194, + "pitchMax" : 916 + }, + "speed" : { + "minSpeed" : 996147, + "maxSpeed" : 1101004, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 30, + "maxScale" : 20, + "targetScale" : 15 + }, + "colours" : { + "minColourArgb" : 1687813990, + "maxColourArgb" : -1768305664, + "targetColourArgb" : 677812480, + "colourTransitionPercent" : 45, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 20, + "minSpawn" : 64, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "913.png" + } +}, { + "id" : "265", + "spread" : { + "yawMin" : 959, + "yawMax" : 1472, + "pitchMin" : 194, + "pitchMax" : 916 + }, + "speed" : { + "minSpeed" : 996147, + "maxSpeed" : 1101004, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 30, + "maxScale" : 20, + "targetScale" : 15 + }, + "colours" : { + "minColourArgb" : 1694498662, + "maxColourArgb" : -1761620890, + "targetColourArgb" : 687852544, + "colourTransitionPercent" : 45, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 20, + "minSpawn" : 64, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "913.png" + } +}, { + "id" : "266", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 677, + "pitchMax" : 410 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1572864 + }, + "scale" : { + "minScale" : 50, + "maxScale" : 40, + "targetScale" : 35 + }, + "colours" : { + "minColourArgb" : 352308275, + "maxColourArgb" : 922720563, + "targetColourArgb" : 16367150, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 15, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "collidesWithObjects" : true, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "867.png" + } +}, { + "id" : "267", + "spread" : { + "yawMin" : 1649, + "yawMax" : 2038, + "pitchMin" : 924, + "pitchMax" : 1003 + }, + "speed" : { + "minSpeed" : 65536, + "maxSpeed" : 65536, + "targetSpeed" : 262144 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 7, + "targetScale" : 30 + }, + "colours" : { + "minColourArgb" : -935643611, + "maxColourArgb" : -599182047, + "targetColourArgb" : 5521688, + "colourTransitionPercent" : 60, + "alphaTransitionPercent" : 95 + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 80, + "minSpawn" : 64, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "649.png" + } +}, { + "id" : "268", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 584, + "pitchMax" : 662 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144, + "targetSpeed" : 1572864, + "speedTransitionPercent" : 80 + }, + "scale" : { + "minScale" : 16, + "maxScale" : 16 + }, + "colours" : { + "minColourArgb" : 674444083, + "maxColourArgb" : 1677721600, + "targetColourArgb" : 10066329, + "colourTransitionPercent" : 80, + "alphaTransitionPercent" : 90 + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 120, + "minSpawn" : 64, + "maxSpawn" : 96, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "717.png" + } +}, { + "id" : "269", + "spread" : { + "yawMin" : 2037, + "pitchMin" : 449, + "pitchMax" : 41 + }, + "speed" : { + "minSpeed" : 1048576, + "maxSpeed" : 262144, + "targetSpeed" : 4194304 + }, + "scale" : { + "minScale" : 70, + "maxScale" : 60, + "targetScale" : 100 + }, + "colours" : { + "minColourArgb" : 1694472448, + "maxColourArgb" : 2030043084, + "targetColourArgb" : 16763904 + }, + "emission" : { + "minDelay" : 5, + "maxDelay" : 45, + "minSpawn" : 128, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "916.png" + } +}, { + "id" : "270", + "spread" : { }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 1310720, + "targetSpeed" : 3145728 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 10, + "targetScale" : 30 + }, + "colours" : { + "minColourArgb" : 1694498815, + "maxColourArgb" : 2026687692, + "targetColourArgb" : 16777215 + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 40, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "916.png" + } +}, { + "id" : "271", + "spread" : { + "yawMin" : 2027, + "yawMax" : 2033, + "pitchMin" : 993, + "pitchMax" : 34 + }, + "speed" : { + "minSpeed" : 65536, + "maxSpeed" : 131072, + "targetSpeed" : 157286, + "speedTransitionPercent" : 90 + }, + "scale" : { + "minScale" : 12, + "maxScale" : 25, + "targetScale" : 5, + "scaleTransitionPercent" : 60 + }, + "colours" : { + "minColourArgb" : -1, + "maxColourArgb" : 1348174736, + "targetColourArgb" : 10066329 + }, + "emission" : { + "minDelay" : 8, + "maxDelay" : 15, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "744.png" + } +}, { + "id" : "272", + "spread" : { + "yawMax" : 2033, + "pitchMin" : 112, + "pitchMax" : 34 + }, + "speed" : { + "minSpeed" : 65536, + "maxSpeed" : 131072, + "targetSpeed" : 524288 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 20, + "targetScale" : 10, + "scaleTransitionPercent" : 50 + }, + "colours" : { + "minColourArgb" : -1, + "maxColourArgb" : 1349029789, + "targetColourArgb" : 10066329 + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 20, + "minSpawn" : 32, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "744.png" + } +}, { + "id" : "273", + "spread" : { + "yawMax" : 2033, + "pitchMin" : 112, + "pitchMax" : 34 + }, + "speed" : { + "minSpeed" : 65536, + "maxSpeed" : 131072, + "targetSpeed" : 157286, + "speedTransitionPercent" : 90 + }, + "scale" : { + "minScale" : 14, + "maxScale" : 10, + "targetScale" : 8, + "scaleTransitionPercent" : 50 + }, + "colours" : { + "minColourArgb" : -1, + "maxColourArgb" : 1348174736, + "targetColourArgb" : 10066329 + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 20, + "minSpawn" : 32, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "744.png" + } +}, { + "id" : "274", + "spread" : { + "yawMax" : 2033, + "pitchMin" : 112, + "pitchMax" : 34 + }, + "speed" : { + "minSpeed" : 65536, + "maxSpeed" : 131072, + "targetSpeed" : 157286, + "speedTransitionPercent" : 90 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 7, + "targetScale" : 8, + "scaleTransitionPercent" : 50 + }, + "colours" : { + "minColourArgb" : -1, + "maxColourArgb" : 1348174736, + "targetColourArgb" : 10066329 + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 20, + "minSpawn" : 32, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "744.png" + } +}, { + "id" : "275", + "spread" : { }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144, + "targetSpeed" : 2621440 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 1, + "targetScale" : 12 + }, + "colours" : { + "minColourArgb" : 1355599052, + "maxColourArgb" : 1516660326, + "targetColourArgb" : 16777215 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 60, + "minSpawn" : 1, + "maxSpawn" : 32, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "909.png" + } +}, { + "id" : "276", + "spread" : { }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1572864, + "targetSpeed" : 6815744 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 12, + "targetScale" : 60 + }, + "colours" : { + "minColourArgb" : 1358954495, + "maxColourArgb" : 1523371212, + "targetColourArgb" : 16777215 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 192, + "maxSpawn" : 320, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "909.png" + } +}, { + "id" : "277", + "spread" : { }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432, + "targetSpeed" : 4194304 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 12, + "targetScale" : 45 + }, + "colours" : { + "minColourArgb" : -1936165536, + "maxColourArgb" : -1270798560, + "targetColourArgb" : 10130057 + }, + "emission" : { + "minDelay" : 15, + "maxDelay" : 20, + "minSpawn" : 1, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "902.png" + } +}, { + "id" : "278", + "spread" : { + "yawMin" : 7, + "yawMax" : 2041, + "pitchMin" : 858, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 183500, + "targetSpeed" : 838860, + "speedTransitionPercent" : 1 + }, + "scale" : { + "minScale" : 23, + "maxScale" : 25, + "targetScale" : 18, + "scaleTransitionPercent" : 10 + }, + "colours" : { + "minColourArgb" : 1358954495, + "maxColourArgb" : 1691156479, + "targetColourArgb" : 177851647, + "colourTransitionPercent" : 70 + }, + "emission" : { + "minDelay" : 13, + "maxDelay" : 12, + "minSpawn" : 128, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "857.png" + } +}, { + "id" : "279", + "spread" : { + "yawMax" : 2026, + "pitchMin" : 649, + "pitchMax" : 896 + }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 104857, + "targetSpeed" : 314572, + "speedTransitionPercent" : 1 + }, + "scale" : { + "minScale" : 38, + "maxScale" : 36, + "targetScale" : 48, + "scaleTransitionPercent" : 15 + }, + "colours" : { + "minColourArgb" : 687865855, + "maxColourArgb" : 352321535, + "targetColourArgb" : 16777215 + }, + "emission" : { + "minDelay" : 25, + "maxDelay" : 48, + "minSpawn" : 57, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "902.png" + } +}, { + "id" : "280", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 568, + "pitchMax" : 662 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 1310720, + "targetSpeed" : 2097152 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 12, + "targetScale" : 40 + }, + "colours" : { + "minColourArgb" : 520093695, + "maxColourArgb" : 1103953919, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 70 + }, + "emission" : { + "minDelay" : 58, + "maxDelay" : 60, + "minSpawn" : 64, + "maxSpawn" : 83, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "clipToTerrain" : false, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "736.png" + } +}, { + "id" : "281", + "spread" : { + "yawMin" : 1132, + "yawMax" : 1299, + "pitchMin" : 441, + "pitchMax" : 511 + }, + "speed" : { + "minSpeed" : 996147, + "maxSpeed" : 1101004, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 2 + }, + "colours" : { + "minColourArgb" : 16777215, + "maxColourArgb" : 16777215, + "targetColourArgb" : -1, + "colourTransitionPercent" : 25, + "alphaTransitionPercent" : 10, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 200, + "maxDelay" : 300, + "minSpawn" : 3, + "maxSpawn" : 6, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "282", + "spread" : { + "yawMin" : 1132, + "yawMax" : 1299, + "pitchMin" : 441, + "pitchMax" : 511 + }, + "speed" : { + "minSpeed" : 996147, + "maxSpeed" : 1101004, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 2 + }, + "colours" : { + "minColourArgb" : 16777215, + "maxColourArgb" : 16777215, + "targetColourArgb" : -1, + "colourTransitionPercent" : 25, + "alphaTransitionPercent" : 10, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 200, + "maxDelay" : 300, + "minSpawn" : 10, + "maxSpawn" : 12, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "283", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 350, + "pitchMax" : 511 + }, + "speed" : { + "minSpeed" : 1441792, + "maxSpeed" : 1835008, + "targetSpeed" : 12582912 + }, + "scale" : { + "minScale" : 4, + "maxScale" : 10 + }, + "colours" : { + "minColourArgb" : 16777215, + "maxColourArgb" : 16777215, + "targetColourArgb" : -1, + "colourTransitionPercent" : 25, + "alphaTransitionPercent" : 10, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 80, + "maxDelay" : 90, + "minSpawn" : 12, + "maxSpawn" : 20, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "284", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 607, + "pitchMax" : 720 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 524288 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 16, + "targetScale" : 30 + }, + "colours" : { + "minColourArgb" : 852769748, + "maxColourArgb" : 1177747507, + "targetColourArgb" : 10254499 + }, + "emission" : { + "minDelay" : 80, + "maxDelay" : 100, + "minSpawn" : 64, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "868.png" + } +}, { + "id" : "285", + "spread" : { + "yawMin" : 82, + "yawMax" : 39, + "pitchMax" : 921 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 131072, + "targetSpeed" : 1048576 + }, + "scale" : { + "minScale" : 4, + "maxScale" : 10, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : -1764950068, + "maxColourArgb" : -1764963073, + "targetColourArgb" : 184536319, + "colourTransitionPercent" : 30, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 150, + "minSpawn" : 12, + "maxSpawn" : 20, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "1166.png" + } +}, { + "id" : "286", + "spread" : { + "yawMin" : 31, + "yawMax" : 2019, + "pitchMin" : 370, + "pitchMax" : 148 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 262144, + "targetSpeed" : 1048576 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 3 + }, + "colours" : { + "minColourArgb" : -16763905, + "maxColourArgb" : -13261, + "targetColourArgb" : -65332, + "alphaTransitionPercent" : 10, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 100, + "minSpawn" : 32, + "maxSpawn" : 224, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "1164.png" + } +}, { + "id" : "287", + "spread" : { + "yawMin" : 31, + "yawMax" : 30, + "pitchMin" : 9, + "pitchMax" : 9 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 131072, + "targetSpeed" : 1048576 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 3, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : -26368, + "maxColourArgb" : -3407770, + "targetColourArgb" : -16750849, + "colourTransitionPercent" : 80, + "alphaTransitionPercent" : 10, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 50, + "minSpawn" : 12, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "1164.png" + } +}, { + "id" : "288", + "spread" : { + "yawMin" : 62, + "yawMax" : 2034, + "pitchMin" : 194, + "pitchMax" : 646 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 78643, + "targetSpeed" : 104857 + }, + "scale" : { + "minScale" : 20, + "maxScale" : 30, + "targetScale" : 20 + }, + "colours" : { + "minColourArgb" : 1694498713, + "maxColourArgb" : 1358941235, + "targetColourArgb" : -1764950068, + "colourTransitionPercent" : 30, + "alphaTransitionPercent" : 10, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 5, + "maxDelay" : 10, + "minSpawn" : 64, + "maxSpawn" : 6, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "913.png" + } +}, { + "id" : "289", + "spread" : { + "yawMin" : 234, + "yawMax" : 1844, + "pitchMin" : 147, + "pitchMax" : 982 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144, + "targetSpeed" : 524288 + }, + "scale" : { + "minScale" : 20, + "maxScale" : 15, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : -1265001985, + "maxColourArgb" : 855638015, + "targetColourArgb" : -1764950017, + "colourTransitionPercent" : 25, + "alphaTransitionPercent" : 10, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 20, + "minSpawn" : 12, + "maxSpawn" : 19, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "290", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 350, + "pitchMax" : 511 + }, + "speed" : { + "minSpeed" : 1441792, + "maxSpeed" : 1835008, + "targetSpeed" : 1048576 + }, + "scale" : { + "minScale" : 4, + "maxScale" : 10 + }, + "colours" : { + "minColourArgb" : 16777215, + "maxColourArgb" : 16777215, + "targetColourArgb" : -1, + "colourTransitionPercent" : 25, + "alphaTransitionPercent" : 10, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 80, + "maxDelay" : 90, + "minSpawn" : 12, + "maxSpawn" : 20, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "720.png" + } +}, { + "id" : "291", + "spread" : { + "yawMax" : 2037, + "pitchMin" : 503, + "pitchMax" : 542 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 262144, + "targetSpeed" : 2097152 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 12, + "targetScale" : 8 + }, + "colours" : { + "minColourArgb" : -1265001985, + "maxColourArgb" : 855638015, + "targetColourArgb" : -1764950017, + "colourTransitionPercent" : 25, + "alphaTransitionPercent" : 10, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 35, + "minSpawn" : 38, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "1165.png" + } +}, { + "id" : "292", + "spread" : { + "yawMin" : 10, + "yawMax" : 9, + "pitchMin" : 536, + "pitchMax" : 541 + }, + "speed" : { + "minSpeed" : 3145728, + "maxSpeed" : 3670016, + "targetSpeed" : 15728640 + }, + "scale" : { + "minScale" : 90, + "maxScale" : 80, + "targetScale" : 120, + "scaleTransitionPercent" : 20 + }, + "colours" : { + "minColourArgb" : 768396492, + "maxColourArgb" : 929457766, + "targetColourArgb" : 14934754, + "colourTransitionPercent" : 40, + "alphaTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 60, + "minSpawn" : 115, + "maxSpawn" : 160, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "293", + "spread" : { + "yawMin" : 10, + "yawMax" : 9, + "pitchMin" : 536, + "pitchMax" : 541 + }, + "speed" : { + "minSpeed" : 3145728, + "maxSpeed" : 3670016, + "targetSpeed" : 15728640 + }, + "scale" : { + "minScale" : 90, + "maxScale" : 80, + "targetScale" : 120, + "scaleTransitionPercent" : 20 + }, + "colours" : { + "minColourArgb" : 768396492, + "maxColourArgb" : 929457766, + "targetColourArgb" : 14934754, + "colourTransitionPercent" : 40, + "alphaTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 15, + "minSpawn" : 115, + "maxSpawn" : 160, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "294", + "spread" : { + "yawMax" : 287, + "pitchMin" : 511, + "pitchMax" : 634 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432 + }, + "scale" : { + "minScale" : 32, + "maxScale" : 32 + }, + "colours" : { + "minColourArgb" : 520093695, + "maxColourArgb" : 687865855, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 60, + "minSpawn" : 64, + "maxSpawn" : 128, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "clipToTerrain" : false + }, + "texture" : { + "file" : "1175.png" + } +}, { + "id" : "295", + "spread" : { + "yawMax" : 991, + "pitchMin" : 6 + }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 52428 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 50, + "targetScale" : 20 + }, + "colours" : { + "minColourArgb" : 1187826892, + "maxColourArgb" : 1694498815, + "targetColourArgb" : 13421772 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 100, + "minSpawn" : 6, + "maxSpawn" : 19, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "913.png" + } +}, { + "id" : "296", + "spread" : { + "yawMin" : 9, + "yawMax" : 2035, + "pitchMin" : 662, + "pitchMax" : 847 + }, + "speed" : { + "minSpeed" : 1048576, + "maxSpeed" : 786432, + "targetSpeed" : 4194304, + "speedTransitionPercent" : 95 + }, + "scale" : { + "minScale" : 50, + "maxScale" : 16, + "targetScale" : 40, + "scaleTransitionPercent" : 99 + }, + "colours" : { + "minColourArgb" : 687865855, + "maxColourArgb" : 1020054783, + "targetColourArgb" : 590597375 + }, + "emission" : { + "minDelay" : 25, + "maxDelay" : 45, + "minSpawn" : 32, + "maxSpawn" : 51, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "897.png" + } +}, { + "id" : "297", + "spread" : { + "yawMin" : 10, + "yawMax" : 16, + "pitchMin" : 553, + "pitchMax" : 612 + }, + "speed" : { + "minSpeed" : 1048576, + "maxSpeed" : 2097152, + "targetSpeed" : 6291456 + }, + "scale" : { + "minScale" : 90, + "maxScale" : 80, + "targetScale" : 100, + "scaleTransitionPercent" : 20 + }, + "colours" : { + "minColourArgb" : 771751935, + "maxColourArgb" : 936181759, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 40, + "alphaTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 30, + "minSpawn" : 44, + "maxSpawn" : 64, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "298", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 520, + "pitchMax" : 533 + }, + "speed" : { + "minSpeed" : 1310720, + "maxSpeed" : 1572864, + "targetSpeed" : 4194304 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 7, + "targetScale" : 15 + }, + "colours" : { + "minColourArgb" : 1020453883, + "maxColourArgb" : -1761607681, + "targetColourArgb" : 515891694, + "colourTransitionPercent" : 50 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 50, + "minSpawn" : 32, + "maxSpawn" : 51, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "820.png" + } +}, { + "id" : "299", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 520, + "pitchMax" : 555 + }, + "speed" : { + "minSpeed" : 1835008, + "maxSpeed" : 2359296, + "targetSpeed" : 6291456 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 1, + "targetScale" : 3 + }, + "colours" : { + "minColourArgb" : 1020453883, + "maxColourArgb" : -1761607681, + "targetColourArgb" : 515891694, + "colourTransitionPercent" : 50 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 60, + "minSpawn" : 6, + "maxSpawn" : 9, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "820.png" + } +}, { + "id" : "300", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 1005, + "pitchMax" : 8 + }, + "speed" : { + "minSpeed" : 13107, + "maxSpeed" : 26214, + "targetSpeed" : 314572 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 4, + "targetScale" : 24 + }, + "colours" : { + "minColourArgb" : 684909563, + "maxColourArgb" : 1023410175, + "targetColourArgb" : 515891694, + "colourTransitionPercent" : 50 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 32, + "maxSpawn" : 38, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "820.png" + } +}, { + "id" : "301", + "spread" : { + "yawMax" : 2047, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 2621440, + "maxSpeed" : 3932160, + "targetSpeed" : 1048576, + "speedTransitionPercent" : 30 + }, + "scale" : { + "minScale" : 15, + "maxScale" : 10, + "targetScale" : 1, + "scaleTransitionPercent" : 80 + }, + "colours" : { + "minColourArgb" : -6737152, + "maxColourArgb" : -13312, + "targetColourArgb" : 8881274 + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 30, + "minSpawn" : 96, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "874.png" + } +}, { + "id" : "302", + "spread" : { + "yawMax" : 2047, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 2621440, + "maxSpeed" : 3932160, + "targetSpeed" : 1048576, + "speedTransitionPercent" : 30 + }, + "scale" : { + "minScale" : 15, + "maxScale" : 40, + "targetScale" : 1, + "scaleTransitionPercent" : 80 + }, + "colours" : { + "minColourArgb" : -6737152, + "maxColourArgb" : -13312, + "targetColourArgb" : 8881274 + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 30, + "minSpawn" : 96, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "874.png" + } +}, { + "id" : "303", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 1007, + "pitchMax" : 892 + }, + "speed" : { + "minSpeed" : 1310720, + "maxSpeed" : 5242880 + }, + "scale" : { + "minScale" : 50, + "maxScale" : 52, + "targetScale" : 75 + }, + "colours" : { + "minColourArgb" : 342268415, + "maxColourArgb" : 909325311, + "targetColourArgb" : 520093798, + "alphaTransitionPercent" : 92, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 18, + "maxDelay" : 31, + "minSpawn" : 762, + "maxSpawn" : 1004, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "collidesWithObjects" : true, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "907.png" + } +}, { + "id" : "304", + "spread" : { + "yawMax" : 2037, + "pitchMin" : 721, + "pitchMax" : 939 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 1048576, + "targetSpeed" : 1048576 + }, + "scale" : { + "minScale" : 20, + "maxScale" : 30, + "targetScale" : 60 + }, + "colours" : { + "minColourArgb" : 1684432486, + "maxColourArgb" : 2013265920, + "targetColourArgb" : 6710886 + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 100, + "minSpawn" : 64, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "717.png" + } +}, { + "id" : "305", + "spread" : { + "yawMin" : 965, + "yawMax" : 1770, + "pitchMin" : 677, + "pitchMax" : 410 + }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 78643, + "targetSpeed" : 209715, + "speedTransitionPercent" : 20 + }, + "scale" : { + "minScale" : 15, + "maxScale" : 1, + "targetScale" : 6 + }, + "colours" : { + "minColourArgb" : 520080435, + "maxColourArgb" : 1526687232, + "targetColourArgb" : 16367150, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 2, + "maxDelay" : 10, + "minSpawn" : 128, + "maxSpawn" : 256, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "collidesWithObjects" : true, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "744.png" + } +}, { + "id" : "306", + "spread" : { + "yawMin" : 965, + "yawMax" : 1770, + "pitchMin" : 677, + "pitchMax" : 410 + }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 78643, + "targetSpeed" : 209715, + "speedTransitionPercent" : 20 + }, + "scale" : { + "minScale" : 15, + "maxScale" : 1, + "targetScale" : 6 + }, + "colours" : { + "minColourArgb" : 520093695, + "maxColourArgb" : 1523371212, + "targetColourArgb" : 16777215, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 2, + "maxDelay" : 10, + "minSpawn" : 128, + "maxSpawn" : 256, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "collidesWithObjects" : true, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "744.png" + } +}, { + "id" : "307", + "spread" : { }, + "speed" : { + "minSpeed" : 2621440, + "maxSpeed" : 3932160 + }, + "scale" : { + "minScale" : 13, + "maxScale" : 50, + "targetScale" : 30, + "scaleTransitionPercent" : 20 + }, + "colours" : { + "minColourArgb" : 352308275, + "maxColourArgb" : 922720563, + "targetColourArgb" : 13369344, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 2, + "maxDelay" : 9, + "minSpawn" : 640, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "collidesWithObjects" : true, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "867.png" + } +}, { + "id" : "308", + "spread" : { + "yawMin" : 1556, + "yawMax" : 363, + "pitchMin" : 511, + "pitchMax" : 924 + }, + "speed" : { + "minSpeed" : 52428, + "maxSpeed" : 262144 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 12, + "targetScale" : 2, + "scaleTransitionPercent" : 0 + }, + "colours" : { + "minColourArgb" : 352308275, + "maxColourArgb" : 922720563, + "targetColourArgb" : 13369344, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 12, + "minSpawn" : 640, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "collidesWithObjects" : true, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "867.png" + } +}, { + "id" : "309", + "spread" : { + "yawMin" : 64, + "yawMax" : 581, + "pitchMin" : 767, + "pitchMax" : 333 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144, + "targetSpeed" : 1572864, + "speedTransitionPercent" : 80 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 20, + "targetScale" : 15, + "scaleTransitionPercent" : 20 + }, + "colours" : { + "minColourArgb" : 177838489, + "maxColourArgb" : 1103940812, + "targetColourArgb" : 1268357529, + "colourTransitionPercent" : 39, + "alphaTransitionPercent" : 90 + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 30, + "minSpawn" : 64, + "maxSpawn" : 96, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "902.png" + } +}, { + "id" : "310", + "spread" : { + "yawMin" : 1375, + "yawMax" : 342, + "pitchMin" : 40, + "pitchMax" : 707 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144, + "targetSpeed" : 1572864, + "speedTransitionPercent" : 80 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 20, + "targetScale" : 15, + "scaleTransitionPercent" : 20 + }, + "colours" : { + "minColourArgb" : 681154969, + "maxColourArgb" : 1103940812, + "targetColourArgb" : 1268357529, + "colourTransitionPercent" : 46, + "alphaTransitionPercent" : 90 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 70, + "minSpawn" : 64, + "maxSpawn" : 96, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "902.png" + } +}, { + "id" : "311", + "spread" : { + "yawMax" : 2037, + "pitchMin" : 721, + "pitchMax" : 939 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 1048576, + "targetSpeed" : 1048576 + }, + "scale" : { + "minScale" : 20, + "maxScale" : 20, + "targetScale" : 30 + }, + "colours" : { + "minColourArgb" : 1691143372, + "maxColourArgb" : 2026687692, + "targetColourArgb" : 16777215 + }, + "emission" : { + "minDelay" : 25, + "maxDelay" : 50, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "717.png" + } +}, { + "id" : "312", + "spread" : { }, + "speed" : { + "minSpeed" : 445644, + "maxSpeed" : 576716, + "targetSpeed" : 2097152, + "speedTransitionPercent" : 95 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 14, + "targetScale" : 8 + }, + "colours" : { + "minColourArgb" : 1694498815, + "maxColourArgb" : 1687814143, + "targetColourArgb" : 13434879 + }, + "emission" : { + "minDelay" : 5, + "maxDelay" : 25, + "minSpawn" : 192, + "maxSpawn" : 224, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "744.png" + } +}, { + "id" : "313", + "spread" : { + "yawMin" : 1925, + "yawMax" : 387, + "pitchMin" : 671, + "pitchMax" : 131 + }, + "speed" : { + "minSpeed" : 445644, + "maxSpeed" : 576716, + "targetSpeed" : 2097152, + "speedTransitionPercent" : 95 + }, + "scale" : { + "minScale" : 12, + "maxScale" : 10, + "targetScale" : 13 + }, + "colours" : { + "minColourArgb" : 1694498815, + "maxColourArgb" : 1687814143, + "targetColourArgb" : 13434879 + }, + "emission" : { + "minDelay" : 18, + "maxDelay" : 20, + "minSpawn" : 64, + "maxSpawn" : 96, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "744.png" + } +}, { + "id" : "314", + "spread" : { + "pitchMin" : 238, + "pitchMax" : 616 + }, + "speed" : { + "minSpeed" : 445644, + "maxSpeed" : 576716, + "targetSpeed" : 2097152, + "speedTransitionPercent" : 95 + }, + "scale" : { + "minScale" : 7, + "maxScale" : 5, + "targetScale" : 8 + }, + "colours" : { + "minColourArgb" : 1694498815, + "maxColourArgb" : 1694472703, + "targetColourArgb" : 16777215 + }, + "emission" : { + "minDelay" : 25, + "maxDelay" : 26, + "minSpawn" : 96, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "744.png" + } +}, { + "id" : "315", + "spread" : { }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 262144 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 5, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : -3368449, + "maxColourArgb" : -2251777, + "targetColourArgb" : -925341953, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 1, + "maxDelay" : 1, + "minSpawn" : 320, + "maxSpawn" : 320, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "858.png" + } +}, { + "id" : "316", + "spread" : { + "yawMin" : 1948, + "yawMax" : 643, + "pitchMin" : 690, + "pitchMax" : 703 + }, + "speed" : { + "minSpeed" : 5242880, + "maxSpeed" : 7864320, + "targetSpeed" : 0, + "speedTransitionPercent" : 30 + }, + "scale" : { + "minScale" : 40, + "maxScale" : 10, + "targetScale" : 50 + }, + "colours" : { + "minColourArgb" : 1684432486, + "maxColourArgb" : 2016621363, + "targetColourArgb" : 13421772 + }, + "emission" : { + "minDelay" : 5, + "maxDelay" : 20, + "minSpawn" : 640, + "maxSpawn" : 1280, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "736.png" + } +}, { + "id" : "317", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 607, + "pitchMax" : 720 + }, + "speed" : { + "minSpeed" : 393216, + "maxSpeed" : 498073 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 16, + "targetScale" : 20 + }, + "colours" : { + "minColourArgb" : 842255615, + "maxColourArgb" : 1174418278, + "targetColourArgb" : 13434879 + }, + "emission" : { + "minDelay" : 90, + "maxDelay" : 115, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : 0 + }, + "texture" : { + "file" : "868.png" + } +}, { + "id" : "318", + "spread" : { + "yawMin" : 13, + "yawMax" : 2047, + "pitchMin" : 467, + "pitchMax" : 494 + }, + "speed" : { + "minSpeed" : 78643, + "maxSpeed" : 131072, + "targetSpeed" : 2621440, + "speedTransitionPercent" : 75 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 4, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : 1694485657, + "maxColourArgb" : -1761607834, + "targetColourArgb" : 6684672, + "colourTransitionPercent" : 95 + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 40, + "minSpawn" : 22, + "maxSpawn" : 32, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "850.png" + } +}, { + "id" : "319", + "spread" : { + "yawMin" : 12, + "yawMax" : 2047, + "pitchMin" : 483, + "pitchMax" : 535 + }, + "speed" : { + "minSpeed" : 78643, + "maxSpeed" : 131072, + "targetSpeed" : 2097152, + "speedTransitionPercent" : 30 + }, + "scale" : { + "minScale" : 4, + "maxScale" : 3, + "targetScale" : 2, + "scaleTransitionPercent" : 80 + }, + "colours" : { + "minColourArgb" : 1691090944, + "maxColourArgb" : 1687748608, + "targetColourArgb" : 6684672, + "colourTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 15, + "maxDelay" : 35, + "minSpawn" : 128, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "1181.png" + } +}, { + "id" : "320", + "spread" : { + "yawMin" : 1811, + "yawMax" : 727, + "pitchMin" : 883, + "pitchMax" : 674 + }, + "speed" : { + "minSpeed" : 1310720, + "maxSpeed" : 1572864, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 7, + "maxScale" : 8, + "targetScale" : 5, + "scaleTransitionPercent" : 75 + }, + "colours" : { + "minColourArgb" : 2030042880, + "maxColourArgb" : -1761620941, + "targetColourArgb" : 13421568, + "colourTransitionPercent" : 68 + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 25, + "minSpawn" : 64, + "maxSpawn" : 160, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "817.png" + } +}, { + "id" : "321", + "spread" : { + "yawMax" : 1999, + "pitchMin" : 1023, + "pitchMax" : 411 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 786432, + "targetSpeed" : 1048576, + "speedTransitionPercent" : 2 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 20, + "targetScale" : 20, + "scaleTransitionPercent" : 20 + }, + "colours" : { + "minColourArgb" : 201326591, + "maxColourArgb" : 1103953919, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 50, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 64, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "collidesWithObjects" : true, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "850.png" + } +}, { + "id" : "322", + "spread" : { + "yawMax" : 1999, + "pitchMin" : 1023, + "pitchMax" : 411 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 786432, + "targetSpeed" : 1048576, + "speedTransitionPercent" : 2 + }, + "scale" : { + "minScale" : 50, + "maxScale" : 35, + "targetScale" : 50, + "scaleTransitionPercent" : 60 + }, + "colours" : { + "minColourArgb" : 1694498815, + "maxColourArgb" : 1355612159, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 50, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 30, + "minSpawn" : 64, + "maxSpawn" : 96, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "850.png" + } +}, { + "id" : "323", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 584, + "pitchMax" : 662 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144, + "targetSpeed" : 1572864, + "speedTransitionPercent" : 80 + }, + "scale" : { + "minScale" : 16, + "maxScale" : 16 + }, + "colours" : { + "minColourArgb" : 674444083, + "maxColourArgb" : 1677721600, + "targetColourArgb" : 10066329, + "colourTransitionPercent" : 80, + "alphaTransitionPercent" : 90 + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 120, + "minSpawn" : 64, + "maxSpawn" : 96, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "717.png" + } +}, { + "id" : "324", + "spread" : { + "yawMin" : 555, + "yawMax" : 872, + "pitchMin" : 8, + "pitchMax" : 92 + }, + "speed" : { + "minSpeed" : 3932160, + "maxSpeed" : 5242880 + }, + "scale" : { + "minScale" : 69, + "maxScale" : 63, + "targetScale" : 70, + "scaleTransitionPercent" : 39 + }, + "colours" : { + "minColourArgb" : 352308275, + "maxColourArgb" : 922720563, + "targetColourArgb" : 13369344, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 120, + "minSpawn" : 128, + "maxSpawn" : 256, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "collidesWithObjects" : true, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "867.png" + } +}, { + "id" : "325", + "spread" : { + "yawMin" : 1023, + "yawMax" : 811, + "pitchMin" : 727, + "pitchMax" : 401 + }, + "speed" : { + "minSpeed" : 52428, + "maxSpeed" : 786432 + }, + "scale" : { + "minScale" : 23, + "maxScale" : 16, + "targetScale" : 14, + "scaleTransitionPercent" : 39 + }, + "colours" : { + "minColourArgb" : 419430400, + "maxColourArgb" : 1375731712, + "targetColourArgb" : 1912602625, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 20, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "collidesWithObjects" : true, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "872.png" + } +}, { + "id" : "326", + "spread" : { + "pitchMin" : 1023, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 26214 + }, + "scale" : { + "minScale" : 4, + "maxScale" : 4 + }, + "colours" : { + "minColourArgb" : 687800575, + "maxColourArgb" : 1006698495, + "targetColourArgb" : 16776960, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 60, + "minSpawn" : 640, + "maxSpawn" : 768, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "777.png" + } +}, { + "id" : "327", + "spread" : { + "yawMin" : 947, + "yawMax" : 1061, + "pitchMin" : 451, + "pitchMax" : 511 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 524288, + "targetSpeed" : 2097152 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 2 + }, + "colours" : { + "minColourArgb" : 6710784, + "maxColourArgb" : 3355392, + "targetColourArgb" : 1077097216, + "colourTransitionPercent" : 25, + "alphaTransitionPercent" : 10, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 800, + "maxDelay" : 1000, + "minSpawn" : 1, + "maxSpawn" : 2, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "328", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 350, + "pitchMax" : 511 + }, + "speed" : { + "minSpeed" : 1441792, + "maxSpeed" : 1835008, + "targetSpeed" : 1048576 + }, + "scale" : { + "minScale" : 4, + "maxScale" : 10 + }, + "colours" : { + "minColourArgb" : 16777215, + "maxColourArgb" : 16777215, + "targetColourArgb" : -1, + "colourTransitionPercent" : 25, + "alphaTransitionPercent" : 10, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 80, + "maxDelay" : 90, + "minSpawn" : 12, + "maxSpawn" : 20, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "812.png" + } +}, { + "id" : "329", + "spread" : { }, + "speed" : { + "minSpeed" : 1048576, + "maxSpeed" : 1310720, + "targetSpeed" : 10485760, + "speedTransitionPercent" : 90 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 4, + "targetScale" : 4, + "scaleTransitionPercent" : 50 + }, + "colours" : { + "minColourArgb" : 2702951, + "maxColourArgb" : 4609900, + "targetColourArgb" : -933403527, + "colourTransitionPercent" : 35, + "alphaTransitionPercent" : 25 + }, + "emission" : { + "minDelay" : 12, + "maxDelay" : 23, + "minSpawn" : 51, + "maxSpawn" : 64, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "1181.png" + } +}, { + "id" : "330", + "spread" : { + "yawMin" : 320, + "yawMax" : 2047, + "pitchMin" : 779, + "pitchMax" : 573 + }, + "speed" : { + "minSpeed" : 1835008, + "maxSpeed" : 2097152, + "targetSpeed" : 838860 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 4, + "targetScale" : 2 + }, + "colours" : { + "minColourArgb" : -3342337, + "maxColourArgb" : -419430401, + "targetColourArgb" : 16777215, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 15, + "minSpawn" : 51, + "maxSpawn" : 32, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "collidesWithObjects" : true, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "898.png" + } +}, { + "id" : "331", + "spread" : { + "yawMin" : 12, + "yawMax" : 2047, + "pitchMin" : 128, + "pitchMax" : 689 + }, + "speed" : { + "minSpeed" : 393216, + "maxSpeed" : 786432, + "targetSpeed" : 1048576, + "speedTransitionPercent" : 50 + }, + "scale" : { + "minScale" : 4, + "maxScale" : 6, + "targetScale" : 4, + "scaleTransitionPercent" : 75 + }, + "colours" : { + "minColourArgb" : 1694498815, + "maxColourArgb" : 1694498815, + "targetColourArgb" : 16724787 + }, + "emission" : { + "minDelay" : 5, + "maxDelay" : 20, + "minSpawn" : 128, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "907.png" + } +}, { + "id" : "332", + "spread" : { + "yawMin" : 12, + "yawMax" : 2047, + "pitchMin" : 128, + "pitchMax" : 689 + }, + "speed" : { + "minSpeed" : 393216, + "maxSpeed" : 786432, + "targetSpeed" : 1048576, + "speedTransitionPercent" : 50 + }, + "scale" : { + "minScale" : 4, + "maxScale" : 6, + "targetScale" : 4, + "scaleTransitionPercent" : 75 + }, + "colours" : { + "minColourArgb" : 1694498815, + "maxColourArgb" : 1694498815, + "targetColourArgb" : 16776960 + }, + "emission" : { + "minDelay" : 5, + "maxDelay" : 20, + "minSpawn" : 128, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "907.png" + } +}, { + "id" : "333", + "spread" : { + "yawMin" : 12, + "yawMax" : 2047, + "pitchMin" : 128, + "pitchMax" : 689 + }, + "speed" : { + "minSpeed" : 393216, + "maxSpeed" : 786432, + "targetSpeed" : 1048576, + "speedTransitionPercent" : 50 + }, + "scale" : { + "minScale" : 4, + "maxScale" : 6, + "targetScale" : 4, + "scaleTransitionPercent" : 75 + }, + "colours" : { + "minColourArgb" : 1694498815, + "maxColourArgb" : 1694498815, + "targetColourArgb" : 3407871 + }, + "emission" : { + "minDelay" : 5, + "maxDelay" : 20, + "minSpawn" : 128, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "907.png" + } +}, { + "id" : "334", + "spread" : { + "yawMin" : 12, + "yawMax" : 2047, + "pitchMin" : 128, + "pitchMax" : 689 + }, + "speed" : { + "minSpeed" : 393216, + "maxSpeed" : 786432, + "targetSpeed" : 1048576, + "speedTransitionPercent" : 50 + }, + "scale" : { + "minScale" : 4, + "maxScale" : 6, + "targetScale" : 4, + "scaleTransitionPercent" : 75 + }, + "colours" : { + "minColourArgb" : 1694498815, + "maxColourArgb" : 1694498815, + "targetColourArgb" : 16776960 + }, + "emission" : { + "minDelay" : 5, + "maxDelay" : 20, + "minSpawn" : 128, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "907.png" + } +}, { + "id" : "335", + "spread" : { + "yawMax" : 2047, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 4, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : -1, + "maxColourArgb" : -1, + "targetColourArgb" : -922759988, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 35, + "maxDelay" : 40, + "minSpawn" : 6, + "maxSpawn" : 12, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "858.png" + } +}, { + "id" : "336", + "spread" : { }, + "speed" : { + "minSpeed" : 3932160, + "maxSpeed" : 6553600, + "targetSpeed" : 26214400 + }, + "scale" : { + "minScale" : 25, + "maxScale" : 10, + "targetScale" : 12, + "scaleTransitionPercent" : 30 + }, + "colours" : { + "minColourArgb" : -13623015, + "maxColourArgb" : -12311010, + "targetColourArgb" : 3417882 + }, + "emission" : { + "minDelay" : 75, + "maxDelay" : 75, + "minSpawn" : 6, + "maxSpawn" : 6, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "902.png" + } +}, { + "id" : "337", + "spread" : { + "yawMin" : 985, + "yawMax" : 2023, + "pitchMin" : 961, + "pitchMax" : 452 + }, + "speed" : { + "minSpeed" : 4194304, + "maxSpeed" : 1572864, + "targetSpeed" : 16777216, + "speedTransitionPercent" : 50 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 12, + "targetScale" : 12 + }, + "colours" : { + "minColourArgb" : -7064830, + "maxColourArgb" : -12311010, + "targetColourArgb" : -932826368 + }, + "emission" : { + "minDelay" : 18, + "maxDelay" : 35, + "minSpawn" : 160, + "maxSpawn" : 192, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "1212.png" + } +}, { + "id" : "338", + "spread" : { }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432, + "targetSpeed" : 1048576 + }, + "scale" : { + "minScale" : 8, + "maxScale" : 4, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : 1442840320, + "maxColourArgb" : -1714657792, + "targetColourArgb" : 3355392, + "colourTransitionPercent" : 90 + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 40, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 65535, + "emissionCycleDuration" : 65535 + }, + "texture" : { + "file" : "858.png" + } +}, { + "id" : "339", + "spread" : { + "yawMin" : 1535, + "yawMax" : 2029, + "pitchMin" : 457, + "pitchMax" : 940 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144, + "targetSpeed" : 1572864, + "speedTransitionPercent" : 80 + }, + "scale" : { + "minScale" : 20, + "maxScale" : 10, + "targetScale" : 25 + }, + "colours" : { + "minColourArgb" : 520093695, + "maxColourArgb" : 1020054732, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 45, + "maxDelay" : 65, + "minSpawn" : 25, + "maxSpawn" : 38, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "717.png" + } +}, { + "id" : "340", + "spread" : { + "yawMax" : 2037, + "pitchMin" : 541, + "pitchMax" : 939 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 1048576, + "targetSpeed" : 1048576 + }, + "scale" : { + "minScale" : 50, + "maxScale" : 30, + "targetScale" : 60 + }, + "colours" : { + "minColourArgb" : 1691143372, + "maxColourArgb" : 1677721600, + "targetColourArgb" : 167772160 + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 50, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "902.png" + } +}, { + "id" : "341", + "spread" : { + "yawMax" : 2037, + "pitchMin" : 731, + "pitchMax" : 939 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 1048576, + "targetSpeed" : 1048576 + }, + "scale" : { + "minScale" : 30, + "maxScale" : 30, + "targetScale" : 25, + "scaleTransitionPercent" : 80 + }, + "colours" : { + "minColourArgb" : 1691143372, + "maxColourArgb" : 1681077043, + "targetColourArgb" : 174483046 + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 30, + "minSpawn" : 19, + "maxSpawn" : 32, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "902.png" + } +}, { + "id" : "342", + "spread" : { + "yawMin" : 1013, + "yawMax" : 735, + "pitchMin" : 439, + "pitchMax" : 755 + }, + "speed" : { + "minSpeed" : 1310720, + "maxSpeed" : 2097152, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 3, + "targetScale" : 5, + "scaleTransitionPercent" : 10 + }, + "colours" : { + "minColourArgb" : -13261, + "maxColourArgb" : -986896, + "targetColourArgb" : 16777164, + "colourTransitionPercent" : 10, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 10, + "maxDelay" : 15, + "minSpawn" : 64, + "maxSpawn" : 32, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "735.png" + } +}, { + "id" : "343", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 555, + "pitchMax" : 637 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 1048576, + "targetSpeed" : 20971 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 10, + "targetScale" : 50 + }, + "colours" : { + "minColourArgb" : 1006632960, + "maxColourArgb" : 1345532723, + "targetColourArgb" : 4013110 + }, + "emission" : { + "minDelay" : 120, + "maxDelay" : 120, + "minSpawn" : 16, + "maxSpawn" : 32, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "766.png" + } +}, { + "id" : "344", + "spread" : { + "yawMax" : 2047, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 393216, + "maxSpeed" : 655360 + }, + "scale" : { + "minScale" : 12, + "maxScale" : 12 + }, + "colours" : { + "minColourArgb" : 676681045, + "maxColourArgb" : 1006632960, + "targetColourArgb" : 6710886, + "colourTransitionPercent" : 40, + "alphaTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 51, + "maxSpawn" : 83, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "810.png" + } +}, { + "id" : "345", + "spread" : { + "yawMax" : 2047, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 235929 + }, + "scale" : { + "minScale" : 12, + "maxScale" : 12 + }, + "colours" : { + "minColourArgb" : 676681045, + "maxColourArgb" : 1006632960, + "targetColourArgb" : 6710886, + "colourTransitionPercent" : 40, + "alphaTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 40, + "minSpawn" : 19, + "maxSpawn" : 44, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "810.png" + } +}, { + "id" : "346", + "spread" : { }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1048576 + }, + "scale" : { + "minScale" : 12, + "maxScale" : 8, + "targetScale" : 35 + }, + "colours" : { + "minColourArgb" : 167772159, + "maxColourArgb" : 265080012, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 80 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 45, + "minSpawn" : 192, + "maxSpawn" : 320, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "913.png" + } +}, { + "id" : "347", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 557, + "pitchMax" : 619 + }, + "speed" : { + "minSpeed" : 1310720, + "maxSpeed" : 2097152, + "targetSpeed" : 5242880 + }, + "scale" : { + "minScale" : 1, + "maxScale" : 1, + "targetScale" : 2, + "scaleTransitionPercent" : 40 + }, + "colours" : { + "minColourArgb" : -13261, + "maxColourArgb" : -986896, + "targetColourArgb" : 16777164, + "colourTransitionPercent" : 10, + "uniformColourVariation" : false + }, + "emission" : { + "minDelay" : 50, + "maxDelay" : 50, + "minSpawn" : 2, + "maxSpawn" : 5, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "735.png" + } +}, { + "id" : "348", + "spread" : { + "yawMin" : 596, + "yawMax" : 659, + "pitchMax" : 177 + }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 786432 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 6, + "targetScale" : 10, + "scaleTransitionPercent" : 10 + }, + "colours" : { + "minColourArgb" : 1090519039, + "maxColourArgb" : 1107296153, + "targetColourArgb" : 16777215, + "colourTransitionPercent" : 70 + }, + "emission" : { + "minDelay" : 20, + "maxDelay" : 60, + "minSpawn" : 128, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "913.png" + } +}, { + "id" : "349", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 791, + "pitchMax" : 297 + }, + "speed" : { + "minSpeed" : 2621440, + "maxSpeed" : 2097152 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 10, + "targetScale" : 6 + }, + "colours" : { + "minColourArgb" : -12599083, + "maxColourArgb" : -13974576, + "targetColourArgb" : -936524335, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 6, + "maxDelay" : 8, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "1255.png" + } +}, { + "id" : "350", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 791, + "pitchMax" : 297 + }, + "speed" : { + "minSpeed" : 2621440, + "maxSpeed" : 2097152 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 10, + "targetScale" : 6 + }, + "colours" : { + "minColourArgb" : -6750004, + "maxColourArgb" : -6750004, + "targetColourArgb" : -929496884, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 6, + "maxDelay" : 8, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "1255.png" + } +}, { + "id" : "351", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 791, + "pitchMax" : 297 + }, + "speed" : { + "minSpeed" : 2621440, + "maxSpeed" : 2097152 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 10, + "targetScale" : 6 + }, + "colours" : { + "minColourArgb" : -16711936, + "maxColourArgb" : -13369600, + "targetColourArgb" : -936116429, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 6, + "maxDelay" : 8, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "1255.png" + } +}, { + "id" : "352", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 791, + "pitchMax" : 297 + }, + "speed" : { + "minSpeed" : 2621440, + "maxSpeed" : 2097152 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 10, + "targetScale" : 6 + }, + "colours" : { + "minColourArgb" : -13421569, + "maxColourArgb" : -13421569, + "targetColourArgb" : -936168449, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 6, + "maxDelay" : 8, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "1255.png" + } +}, { + "id" : "353", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 791, + "pitchMax" : 297 + }, + "speed" : { + "minSpeed" : 2621440, + "maxSpeed" : 2097152 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 10, + "targetScale" : 6 + }, + "colours" : { + "minColourArgb" : -52225, + "maxColourArgb" : -52225, + "targetColourArgb" : -922799105, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 6, + "maxDelay" : 8, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "1255.png" + } +}, { + "id" : "354", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 791, + "pitchMax" : 297 + }, + "speed" : { + "minSpeed" : 2621440, + "maxSpeed" : 2097152 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 10, + "targetScale" : 6 + }, + "colours" : { + "minColourArgb" : -52429, + "maxColourArgb" : -52429, + "targetColourArgb" : -922799309, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 6, + "maxDelay" : 8, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "1255.png" + } +}, { + "id" : "355", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 791, + "pitchMax" : 297 + }, + "speed" : { + "minSpeed" : 2621440, + "maxSpeed" : 2097152 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 10, + "targetScale" : 6 + }, + "colours" : { + "minColourArgb" : -256, + "maxColourArgb" : -205, + "targetColourArgb" : -922747136, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 6, + "maxDelay" : 8, + "minSpawn" : 32, + "maxSpawn" : 64, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "1255.png" + } +}, { + "id" : "356", + "spread" : { }, + "speed" : { + "minSpeed" : 131072, + "maxSpeed" : 262144 + }, + "scale" : { + "minScale" : 14, + "maxScale" : 12, + "targetScale" : 8 + }, + "colours" : { + "minColourArgb" : 1355307781, + "maxColourArgb" : 1523079946, + "targetColourArgb" : 16367150, + "useSceneAmbientLight" : false + }, + "emission" : { + "minDelay" : 90, + "maxDelay" : 110, + "minSpawn" : 6, + "maxSpawn" : 32, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "858.png" + } +}, { + "id" : "357", + "spread" : { }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1048576 + }, + "scale" : { + "minScale" : 21, + "maxScale" : 15, + "targetScale" : 50 + }, + "colours" : { + "minColourArgb" : 93947783, + "maxColourArgb" : 158952556, + "targetColourArgb" : 6710886 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 45, + "minSpawn" : 96, + "maxSpawn" : 192, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "913.png" + } +}, { + "id" : "358", + "spread" : { + "yawMax" : 287, + "pitchMin" : 511, + "pitchMax" : 634 + }, + "speed" : { + "minSpeed" : 235929, + "maxSpeed" : 235929 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 20, + "targetScale" : 120 + }, + "colours" : { + "minColourArgb" : 1694498815, + "maxColourArgb" : 1694498815, + "targetColourArgb" : 1358954495, + "colourTransitionPercent" : 40, + "alphaTransitionPercent" : 0 + }, + "emission" : { + "minDelay" : 150, + "maxDelay" : 150, + "minSpawn" : 9, + "maxSpawn" : 12, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "clipToTerrain" : false + }, + "texture" : { + "file" : "815.png" + } +}, { + "id" : "359", + "spread" : { + "yawMin" : 56, + "yawMax" : 1023, + "pitchMin" : 941, + "pitchMax" : 591 + }, + "speed" : { + "minSpeed" : 393216, + "maxSpeed" : 393216 + }, + "scale" : { + "minScale" : 40, + "maxScale" : 30, + "targetScale" : 80 + }, + "colours" : { + "minColourArgb" : 1023410175, + "maxColourArgb" : 2026687692, + "targetColourArgb" : 10792627, + "colourTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 60, + "minSpawn" : 32, + "maxSpawn" : 38, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "clipToTerrain" : false, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "909.png" + } +}, { + "id" : "360", + "spread" : { + "yawMin" : 56, + "yawMax" : 1023, + "pitchMin" : 941, + "pitchMax" : 591 + }, + "speed" : { + "minSpeed" : 256901, + "maxSpeed" : 256901 + }, + "scale" : { + "minScale" : 5, + "maxScale" : 3, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : 1023410175, + "maxColourArgb" : 2026687692, + "targetColourArgb" : 10792627, + "colourTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 15, + "maxDelay" : 15, + "minSpawn" : 32, + "maxSpawn" : 51, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "clipToTerrain" : false, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "1175.png" + } +}, { + "id" : "361", + "spread" : { + "yawMin" : 1492, + "yawMax" : 450, + "pitchMin" : 883, + "pitchMax" : 537 + }, + "speed" : { + "minSpeed" : 264765, + "maxSpeed" : 264765 + }, + "scale" : { + "minScale" : 3, + "maxScale" : 3, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : -1761607681, + "maxColourArgb" : -1764963124, + "targetColourArgb" : 10792627, + "colourTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 5, + "maxDelay" : 15, + "minSpawn" : 19, + "maxSpawn" : 32, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "clipToTerrain" : false, + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "1260.png" + } +}, { + "id" : "362", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 1005, + "pitchMax" : 8 + }, + "speed" : { + "minSpeed" : 13107, + "maxSpeed" : 26214, + "targetSpeed" : 314572 + }, + "scale" : { + "minScale" : 6, + "maxScale" : 4, + "targetScale" : 24 + }, + "colours" : { + "minColourArgb" : 684909563, + "maxColourArgb" : 1023410175, + "targetColourArgb" : 515891694, + "colourTransitionPercent" : 50 + }, + "emission" : { + "minDelay" : 15, + "maxDelay" : 25, + "minSpawn" : 25, + "maxSpawn" : 32, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "820.png" + } +}, { + "id" : "363", + "spread" : { }, + "speed" : { + "minSpeed" : 13107, + "maxSpeed" : 26214, + "targetSpeed" : 314572 + }, + "scale" : { + "minScale" : 12, + "maxScale" : 25, + "targetScale" : 4 + }, + "colours" : { + "minColourArgb" : 684909563, + "maxColourArgb" : 1023410175, + "targetColourArgb" : 515891694, + "colourTransitionPercent" : 50 + }, + "emission" : { + "minDelay" : 5, + "maxDelay" : 15, + "minSpawn" : 12, + "maxSpawn" : 19, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "1175.png" + } +}, { + "id" : "364", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 520, + "pitchMax" : 533 + }, + "speed" : { + "minSpeed" : 2621440, + "maxSpeed" : 3932160, + "targetSpeed" : 3145728 + }, + "scale" : { + "minScale" : 25, + "maxScale" : 30, + "targetScale" : 50 + }, + "colours" : { + "minColourArgb" : 1020453883, + "maxColourArgb" : -1761607681, + "targetColourArgb" : 1354752494, + "colourTransitionPercent" : 50 + }, + "emission" : { + "minDelay" : 60, + "maxDelay" : 60, + "minSpawn" : 26, + "maxSpawn" : 46, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "1246.png" + } +}, { + "id" : "365", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 520, + "pitchMax" : 555 + }, + "speed" : { + "minSpeed" : 1835008, + "maxSpeed" : 2359296, + "targetSpeed" : 6291456 + }, + "scale" : { + "minScale" : 2, + "maxScale" : 1, + "targetScale" : 3 + }, + "colours" : { + "minColourArgb" : 1020453883, + "maxColourArgb" : -1761607681, + "targetColourArgb" : 515891694, + "colourTransitionPercent" : 50 + }, + "emission" : { + "minDelay" : 40, + "maxDelay" : 50, + "minSpawn" : 5, + "maxSpawn" : 7, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "820.png" + } +}, { + "id" : "366", + "spread" : { + "yawMin" : 1310, + "yawMax" : 598, + "pitchMin" : 713, + "pitchMax" : 43 + }, + "speed" : { + "minSpeed" : 13107, + "maxSpeed" : 26214, + "targetSpeed" : 314572 + }, + "scale" : { + "minScale" : 15, + "maxScale" : 45, + "targetScale" : 24 + }, + "colours" : { + "minColourArgb" : 433251323, + "maxColourArgb" : 687865855, + "targetColourArgb" : 515891694, + "colourTransitionPercent" : 50 + }, + "emission" : { + "minDelay" : 15, + "maxDelay" : 25, + "minSpawn" : 25, + "maxSpawn" : 32, + "initialSpawn" : 100, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "1175.png" + } +}, { + "id" : "367", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 209, + "pitchMax" : 543 + }, + "speed" : { + "minSpeed" : 52428, + "maxSpeed" : 209715 + }, + "scale" : { + "minScale" : 25, + "maxScale" : 15, + "targetScale" : 30 + }, + "colours" : { + "minColourArgb" : -1, + "maxColourArgb" : -1, + "targetColourArgb" : 16842751, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 120, + "minSpawn" : 6, + "maxSpawn" : 12, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "858.png" + } +}, { + "id" : "368", + "spread" : { + "yawMax" : 2047, + "pitchMax" : 1023 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 393216 + }, + "scale" : { + "minScale" : 12, + "maxScale" : 12, + "targetScale" : 1 + }, + "colours" : { + "minColourArgb" : 676681045, + "maxColourArgb" : 1006632960, + "targetColourArgb" : 6710886, + "colourTransitionPercent" : 40, + "alphaTransitionPercent" : 60 + }, + "emission" : { + "minDelay" : 110, + "maxDelay" : 120, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1, + "lowerBoundLevel" : -1 + }, + "texture" : { + "file" : "810.png" + } +}, { + "id" : "369", + "spread" : { + "yawMin" : 9, + "yawMax" : 2035, + "pitchMin" : 662, + "pitchMax" : 847 + }, + "speed" : { + "minSpeed" : 10485, + "maxSpeed" : 301465, + "targetSpeed" : 136314, + "speedTransitionPercent" : 10 + }, + "scale" : { + "minScale" : 40, + "maxScale" : 15, + "targetScale" : 35, + "scaleTransitionPercent" : 99 + }, + "colours" : { + "minColourArgb" : 822109696, + "maxColourArgb" : 1358993715, + "targetColourArgb" : 587228672 + }, + "emission" : { + "minDelay" : 30, + "maxDelay" : 30, + "minSpawn" : 6, + "maxSpawn" : 12, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "897.png" + } +}, { + "id" : "370", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 527, + "pitchMax" : 552 + }, + "speed" : { + "minSpeed" : 262144, + "maxSpeed" : 314572 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 20, + "targetScale" : 80 + }, + "colours" : { + "minColourArgb" : 1127428915, + "maxColourArgb" : 1063675494, + "targetColourArgb" : 14934754, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 250, + "maxDelay" : 300, + "minSpawn" : 2, + "maxSpawn" : 7, + "initialSpawn" : 500, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "texture" : { + "file" : "850.png" + } +}, { + "id" : "371", + "spread" : { + "yawMax" : 2047, + "pitchMin" : 962, + "pitchMax" : 905 + }, + "speed" : { + "minSpeed" : 26214, + "maxSpeed" : 131072, + "targetSpeed" : 1048576 + }, + "scale" : { + "minScale" : 20, + "maxScale" : 25, + "targetScale" : 5 + }, + "colours" : { + "minColourArgb" : -1, + "maxColourArgb" : -3407668, + "targetColourArgb" : -932839322, + "colourTransitionPercent" : 40 + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 110, + "minSpawn" : 64, + "maxSpawn" : 12, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "858.png" + } +}, { + "id" : "372", + "spread" : { + "yawMin" : 7, + "yawMax" : 2036, + "pitchMin" : 731, + "pitchMax" : 975 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 1048576, + "targetSpeed" : 2097152, + "speedTransitionPercent" : 95 + }, + "scale" : { + "minScale" : 11, + "maxScale" : 13, + "targetScale" : 9 + }, + "colours" : { + "minColourArgb" : 1694433280, + "maxColourArgb" : 1687748608, + "targetColourArgb" : 13369344 + }, + "emission" : { + "minDelay" : 15, + "maxDelay" : 40, + "minSpawn" : 224, + "maxSpawn" : 256, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "850.png" + } +}, { + "id" : "373", + "spread" : { }, + "speed" : { + "minSpeed" : 524288, + "maxSpeed" : 1048576, + "targetSpeed" : 1048576 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 30, + "targetScale" : 10 + }, + "colours" : { + "minColourArgb" : 1684432486, + "maxColourArgb" : 2013265920, + "targetColourArgb" : 1 + }, + "emission" : { + "minDelay" : 100, + "maxDelay" : 10, + "minSpawn" : 64, + "maxSpawn" : 128, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "1383.png" + } +}, { + "id" : "374", + "spread" : { + "yawMin" : 44, + "yawMax" : 2043, + "pitchMin" : 711, + "pitchMax" : 343 + }, + "speed" : { + "minSpeed" : 786432, + "maxSpeed" : 524288, + "targetSpeed" : 262144, + "speedTransitionPercent" : 95 + }, + "scale" : { + "minScale" : 10, + "maxScale" : 20, + "targetScale" : 50, + "scaleTransitionPercent" : 90 + }, + "colours" : { + "minColourArgb" : 1012679491, + "maxColourArgb" : 844050994, + "targetColourArgb" : 6710886, + "colourTransitionPercent" : 75 + }, + "emission" : { + "minDelay" : 35, + "maxDelay" : 120, + "minSpawn" : 38, + "maxSpawn" : 76, + "emissionTimeThreshold" : 32767, + "emissionCycleDuration" : 32767 + }, + "physics" : { + "upperBoundLevel" : -1 + }, + "texture" : { + "file" : "868.png" + } +} ] \ No newline at end of file diff --git a/src/main/resources/rs117/hd/scene/textures/particles/1164.png b/src/main/resources/rs117/hd/scene/textures/particles/1164.png new file mode 100644 index 0000000000..d05506319b Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/1164.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/1165.png b/src/main/resources/rs117/hd/scene/textures/particles/1165.png new file mode 100644 index 0000000000..0aa205caa1 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/1165.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/1166.png b/src/main/resources/rs117/hd/scene/textures/particles/1166.png new file mode 100644 index 0000000000..b519bb03b3 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/1166.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/1175.png b/src/main/resources/rs117/hd/scene/textures/particles/1175.png new file mode 100644 index 0000000000..bb0fd15950 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/1175.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/1181.png b/src/main/resources/rs117/hd/scene/textures/particles/1181.png new file mode 100644 index 0000000000..ede23f76f5 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/1181.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/1212.png b/src/main/resources/rs117/hd/scene/textures/particles/1212.png new file mode 100644 index 0000000000..35e0a82c54 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/1212.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/1246.png b/src/main/resources/rs117/hd/scene/textures/particles/1246.png new file mode 100644 index 0000000000..64baccf9ec Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/1246.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/1255.png b/src/main/resources/rs117/hd/scene/textures/particles/1255.png new file mode 100644 index 0000000000..06a49ebf6e Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/1255.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/1260.png b/src/main/resources/rs117/hd/scene/textures/particles/1260.png new file mode 100644 index 0000000000..ec95b783db Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/1260.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/1383.png b/src/main/resources/rs117/hd/scene/textures/particles/1383.png new file mode 100644 index 0000000000..bda8bc1a60 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/1383.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/395.png b/src/main/resources/rs117/hd/scene/textures/particles/395.png new file mode 100644 index 0000000000..097a2c02a8 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/395.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/649.png b/src/main/resources/rs117/hd/scene/textures/particles/649.png new file mode 100644 index 0000000000..b70fe1d9e2 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/649.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/717.png b/src/main/resources/rs117/hd/scene/textures/particles/717.png new file mode 100644 index 0000000000..c90542e32e Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/717.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/718.png b/src/main/resources/rs117/hd/scene/textures/particles/718.png new file mode 100644 index 0000000000..0f39b87e79 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/718.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/719.png b/src/main/resources/rs117/hd/scene/textures/particles/719.png new file mode 100644 index 0000000000..0811c3b1d9 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/719.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/720.png b/src/main/resources/rs117/hd/scene/textures/particles/720.png new file mode 100644 index 0000000000..a2aff9c12e Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/720.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/721.png b/src/main/resources/rs117/hd/scene/textures/particles/721.png new file mode 100644 index 0000000000..9881f20116 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/721.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/723.png b/src/main/resources/rs117/hd/scene/textures/particles/723.png new file mode 100644 index 0000000000..c8a63044a6 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/723.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/735.png b/src/main/resources/rs117/hd/scene/textures/particles/735.png new file mode 100644 index 0000000000..8f99a8228c Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/735.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/736.png b/src/main/resources/rs117/hd/scene/textures/particles/736.png new file mode 100644 index 0000000000..8f99a8228c Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/736.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/744.png b/src/main/resources/rs117/hd/scene/textures/particles/744.png new file mode 100644 index 0000000000..4d406b1ede Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/744.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/759.png b/src/main/resources/rs117/hd/scene/textures/particles/759.png new file mode 100644 index 0000000000..223aa878f9 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/759.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/766.png b/src/main/resources/rs117/hd/scene/textures/particles/766.png new file mode 100644 index 0000000000..c90542e32e Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/766.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/777.png b/src/main/resources/rs117/hd/scene/textures/particles/777.png new file mode 100644 index 0000000000..223aa878f9 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/777.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/808.png b/src/main/resources/rs117/hd/scene/textures/particles/808.png new file mode 100644 index 0000000000..1af9270a49 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/808.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/810.png b/src/main/resources/rs117/hd/scene/textures/particles/810.png new file mode 100644 index 0000000000..c90542e32e Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/810.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/811.png b/src/main/resources/rs117/hd/scene/textures/particles/811.png new file mode 100644 index 0000000000..a686588b0b Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/811.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/812.png b/src/main/resources/rs117/hd/scene/textures/particles/812.png new file mode 100644 index 0000000000..0aa4ec7e87 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/812.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/813.png b/src/main/resources/rs117/hd/scene/textures/particles/813.png new file mode 100644 index 0000000000..c423f41841 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/813.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/814.png b/src/main/resources/rs117/hd/scene/textures/particles/814.png new file mode 100644 index 0000000000..8190ff88a4 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/814.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/815.png b/src/main/resources/rs117/hd/scene/textures/particles/815.png new file mode 100644 index 0000000000..ec95b783db Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/815.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/816.png b/src/main/resources/rs117/hd/scene/textures/particles/816.png new file mode 100644 index 0000000000..abff0c4b10 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/816.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/817.png b/src/main/resources/rs117/hd/scene/textures/particles/817.png new file mode 100644 index 0000000000..e98d738e1d Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/817.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/818.png b/src/main/resources/rs117/hd/scene/textures/particles/818.png new file mode 100644 index 0000000000..ca83a713fc Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/818.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/819.png b/src/main/resources/rs117/hd/scene/textures/particles/819.png new file mode 100644 index 0000000000..0793824eab Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/819.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/820.png b/src/main/resources/rs117/hd/scene/textures/particles/820.png new file mode 100644 index 0000000000..0ea05b7b99 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/820.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/849.png b/src/main/resources/rs117/hd/scene/textures/particles/849.png new file mode 100644 index 0000000000..1954bf3492 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/849.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/850.png b/src/main/resources/rs117/hd/scene/textures/particles/850.png new file mode 100644 index 0000000000..af7b1b4b24 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/850.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/857.png b/src/main/resources/rs117/hd/scene/textures/particles/857.png new file mode 100644 index 0000000000..0dd8b7e3ca Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/857.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/858.png b/src/main/resources/rs117/hd/scene/textures/particles/858.png new file mode 100644 index 0000000000..1de77327ec Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/858.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/859.png b/src/main/resources/rs117/hd/scene/textures/particles/859.png new file mode 100644 index 0000000000..2ba839aff5 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/859.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/860.png b/src/main/resources/rs117/hd/scene/textures/particles/860.png new file mode 100644 index 0000000000..56f7ca0beb Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/860.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/865.png b/src/main/resources/rs117/hd/scene/textures/particles/865.png new file mode 100644 index 0000000000..918352532f Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/865.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/867.png b/src/main/resources/rs117/hd/scene/textures/particles/867.png new file mode 100644 index 0000000000..5352f29ee3 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/867.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/868.png b/src/main/resources/rs117/hd/scene/textures/particles/868.png new file mode 100644 index 0000000000..c90542e32e Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/868.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/871.png b/src/main/resources/rs117/hd/scene/textures/particles/871.png new file mode 100644 index 0000000000..8190ff88a4 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/871.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/872.png b/src/main/resources/rs117/hd/scene/textures/particles/872.png new file mode 100644 index 0000000000..af7b1b4b24 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/872.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/873.png b/src/main/resources/rs117/hd/scene/textures/particles/873.png new file mode 100644 index 0000000000..ba3fa7caa3 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/873.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/874.png b/src/main/resources/rs117/hd/scene/textures/particles/874.png new file mode 100644 index 0000000000..b7d9e20fb2 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/874.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/878.png b/src/main/resources/rs117/hd/scene/textures/particles/878.png new file mode 100644 index 0000000000..e4f45dc1a4 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/878.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/879.png b/src/main/resources/rs117/hd/scene/textures/particles/879.png new file mode 100644 index 0000000000..86d4d6977a Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/879.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/889.png b/src/main/resources/rs117/hd/scene/textures/particles/889.png new file mode 100644 index 0000000000..905f2d412d Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/889.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/892.png b/src/main/resources/rs117/hd/scene/textures/particles/892.png new file mode 100644 index 0000000000..3b3a9cc711 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/892.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/897.png b/src/main/resources/rs117/hd/scene/textures/particles/897.png new file mode 100644 index 0000000000..b519bb03b3 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/897.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/898.png b/src/main/resources/rs117/hd/scene/textures/particles/898.png new file mode 100644 index 0000000000..b89fe8efb1 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/898.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/902.png b/src/main/resources/rs117/hd/scene/textures/particles/902.png new file mode 100644 index 0000000000..0aa4ec7e87 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/902.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/904.png b/src/main/resources/rs117/hd/scene/textures/particles/904.png new file mode 100644 index 0000000000..22b9853d6e Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/904.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/907.png b/src/main/resources/rs117/hd/scene/textures/particles/907.png new file mode 100644 index 0000000000..a4291e2dbc Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/907.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/909.png b/src/main/resources/rs117/hd/scene/textures/particles/909.png new file mode 100644 index 0000000000..135b99fe56 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/909.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/910.png b/src/main/resources/rs117/hd/scene/textures/particles/910.png new file mode 100644 index 0000000000..c0276d0e4a Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/910.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/913.png b/src/main/resources/rs117/hd/scene/textures/particles/913.png new file mode 100644 index 0000000000..3b8ee01225 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/913.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/916.png b/src/main/resources/rs117/hd/scene/textures/particles/916.png new file mode 100644 index 0000000000..3ca688c70d Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/916.png differ diff --git a/src/main/resources/rs117/hd/scene/textures/particles/fire_explosion.png b/src/main/resources/rs117/hd/scene/textures/particles/fire_explosion.png new file mode 100644 index 0000000000..391223c823 Binary files /dev/null and b/src/main/resources/rs117/hd/scene/textures/particles/fire_explosion.png differ