├── .github ├── demo.gif └── icon.png ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── main └── java │ └── net │ └── worldseed │ └── particleemitter │ ├── curves │ ├── Curve.java │ └── CurveNode.java │ ├── emitters │ ├── EmitterLifetime.java │ ├── EmitterRate.java │ ├── EmitterShape.java │ ├── init │ │ ├── EmitterInitialization.java │ │ └── EmitterLocalSpace.java │ ├── lifetime │ │ ├── EmitterLifetimeExpression.java │ │ ├── EmitterLifetimeLooping.java │ │ └── EmitterLifetimeOnce.java │ ├── rate │ │ ├── EmitterRateInstant.java │ │ └── EmitterRateSteady.java │ └── shape │ │ ├── EmitterShapeBox.java │ │ ├── EmitterShapeDisc.java │ │ ├── EmitterShapeEntityAABB.java │ │ ├── EmitterShapePoint.java │ │ └── EmitterShapeSphere.java │ ├── generator │ └── ParticleGenerator.java │ ├── misc │ ├── Colour.java │ ├── EmitterDirectionType.java │ └── EmitterPlaneNormalType.java │ ├── particle │ ├── ParticleAppearanceTinting.java │ ├── ParticleInitialSpeed.java │ ├── ParticleLifetime.java │ └── ParticleLifetimeExpression.java │ └── runtime │ ├── Particle.java │ ├── ParticleEmitter.java │ ├── ParticleEmitterScript.java │ ├── ParticleInterface.java │ └── ParticleParser.java └── test ├── java ├── Demo.java ├── ParticleManagerDemo.java └── ParticleParserTest.java └── resources └── particles ├── colour_expression.particle.json ├── cool.particle.json ├── disc.particle.json ├── error.particle.json ├── flame.particle.json ├── loading.particle.json ├── looping.particle.json ├── magic.particle.json ├── rect.particle.json ├── rgb.particle.json ├── rgb_transition.particle.json ├── sphere.particle.json └── ternary.particle.json /.github/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AtlasEngineCa/ParticleEmitter/9a374fe4574f00617ce0854a1bcb9f43bed0737f/.github/demo.gif -------------------------------------------------------------------------------- /.github/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AtlasEngineCa/ParticleEmitter/9a374fe4574f00617ce0854a1bcb9f43bed0737f/.github/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Java template 3 | # Compiled class file 4 | *.class 5 | 6 | # Log file 7 | *.log 8 | 9 | # BlueJ files 10 | *.ctxt 11 | 12 | # Mobile Tools for Java (J2ME) 13 | .mtj.tmp/ 14 | 15 | # Package Files # 16 | *.jar 17 | *.war 18 | *.nar 19 | *.ear 20 | *.zip 21 | *.tar.gz 22 | *.rar 23 | 24 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 25 | hs_err_pid* 26 | 27 | ### Gradle template 28 | **/.gradle 29 | **/build/ 30 | 31 | # Ignore Gradle GUI config 32 | gradle-app.setting 33 | 34 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 35 | !gradle-wrapper.jar 36 | 37 | # Cache of project 38 | .gradletasknamecache 39 | 40 | # IDEA files 41 | .idea/ 42 | **/out/ 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2022] [Alexander Parent] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 11 | [![Contributors][contributors-shield]][contributors-url] 12 | [![Forks][forks-shield]][forks-url] 13 | [![Stargazers][stars-shield]][stars-url] 14 | [![Issues][issues-shield]][issues-url] 15 | [![APACHE-2.0 License][license-shield]][license-url] 16 | 17 | 18 |
19 |
20 | 21 | Logo 22 | 23 | 24 |

ParticleEmitter

25 |

26 | Report Bug 27 | · 28 | Request Feature 29 |

30 |
31 | 32 | 33 | ## About The Project 34 |
35 | 36 | Logo 37 | 38 |
39 | 40 | With this library, you can design particle animations with [snowstorm](https://snowstorm.app/) and draw them in a Minecraft world. 41 | 42 | Currently only [Minestom](https://github.com/Minestom/Minestom) is supported. 43 | 44 | 45 | ## Getting Started 46 | 47 | A full, runnable example server can be found in [here](https://github.com/WorldSeedGames/ParticleEmitter/blob/master/src/test/java/Demo.java) 48 | 49 | Particle examples can be found [here](https://github.com/WorldSeedGames/ParticleEmitter/tree/master/src/test/resources/particles) 50 | 51 | ### Adding as a dependency 52 | 53 | Add the following to your `build.gradle.kts` file: 54 | 55 | ``` 56 | repositories { 57 | maven("https://reposilite.atlasengine.ca/public") 58 | } 59 | ``` 60 | 61 | Add the library as a dependency 62 | ``` 63 | dependencies { 64 | implementation("net.worldseed.particleemitter:ParticleEmitter:") 65 | } 66 | ``` 67 | 68 | The lastest version number can be found [here](https://reposilite.atlasengine.ca/#/public/net/worldseed/particleemitter/ParticleEmitter) 69 | 70 | ### JAVA FLAGS 71 | 72 | To execute the code you must add the java VM flag `--add-opens java.base/java.lang=ALL-UNNAMED`. 73 | 74 | ## Features 75 | 76 | ParticleEmitter supports the following features 77 | - Emitter lifetime expression, loop and once 78 | - Emitter rate instant and steady 79 | - Emitter shape box, disc, point and sphere 80 | - Particle tinting (colour) 81 | 82 | ## Limitations 83 | 84 | [Minecraft Query Language](https://github.com/hollow-cube/common/tree/main/modules/mql) (MQL) does not fully support Molang 85 | - Variables do not work 86 | - Functions do not work 87 | 88 | Particle Restrictions 89 | - Velocity only works on certain particles and acceleration don't work on any particles 90 | - Custom particle textures don't work 91 | - Curves have not been implemented 92 | - Particle lifetimes have not been implemented 93 | 94 | ## Usage notes 95 | 96 | ### Particle amounts 97 | You may notice in practice that only one particle gets spawned per `emitter.tick()`. 98 | In some cases, this may be a problem as you may want to have more control over the amount of particles spawned for the same animation as demonstrated in the following video where the same animation is being played in 4 different positions with differing particle amounts: 99 | https://github.com/user-attachments/assets/8fd4f277-eb8d-4a3e-b068-18533f72346a 100 | 101 | This can be achieved by using a for loop around the `emitter.tick()` like so: 102 | ```java 103 | Collection packets = new ArrayList<>(); 104 | for (int i = 0; i < amount; i++) { 105 | packets.addAll(emitter.tick()); 106 | } 107 | if (emitter.status() != EmitterLifetime.LifetimeState.DEAD) { 108 | packets.forEach(packet -> { 109 | instance.getPlayers().forEach(p -> p.sendPackets(packet)); 110 | }); 111 | } 112 | ``` 113 | However, this method will require each emitter to have a different `updatesPerSecond` parameter equal to `x*amount` for the animations to be synchronised and play at the same time with differing particle counts, otherwise the animation may speed up or slow down. 114 | ```java 115 | List emitters = new ArrayList<>(); 116 | { 117 | var emitter = ParticleParser.parse(Particle.DUST_COLOR_TRANSITION, 1000*amount, map); 118 | emitters.add(emitter); 119 | } 120 | ``` 121 | Sample code for how this can be done in practice can be found in the `src/test/java/ParticleManagerDemo.java` file, where the video demonstration above was created with these calls: 122 | ```java 123 | ParticleManager.playParticle("rect.particle.json", new Vec(0, 45, 0), 1, instanceContainer, false); 124 | ParticleManager.playParticle("rect.particle.json", new Vec(3, 45, 0), 2, instanceContainer, false); 125 | ParticleManager.playParticle("rect.particle.json", new Vec(6, 45, 0), 3, instanceContainer, false); 126 | ParticleManager.playParticle("rect.particle.json", new Vec(9, 45, 0), 4, instanceContainer, false); 127 | ``` 128 | 129 | ### Playing a particle animation "once" over its lifetime 130 | If you want to have an animation play just once over its lifetime, you will need to use a Timer that gets cancelled once the emitter state is "DEAD" (meaning the animation completed successfully). Here is how that would be done in practice: 131 | 132 | ```java 133 | new Timer().schedule(new TimerTask() { 134 | public void run() { 135 | try { 136 | for (var emitter : emitters) { 137 | for (int i = 0; i < amount; i++) { 138 | Collection packets = emitter.tick(); 139 | if (emitter.status() != EmitterLifetime.LifetimeState.DEAD) { 140 | packets.forEach(packet -> { 141 | instance.getPlayers().forEach(p -> p.sendPackets(packet)); 142 | }); 143 | } else { 144 | emitter.reset(); 145 | // Cancel the timer so it doesn't keep looping 146 | this.cancel(); 147 | } 148 | } 149 | } 150 | } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | 151 | IllegalAccessException e) { 152 | throw new RuntimeException(e); 153 | } 154 | } 155 | }, 1, 1); 156 | ``` 157 | 158 | 159 | 160 |

(back to top)

161 | 162 | 163 | 164 | [contributors-shield]: https://img.shields.io/github/contributors/WorldSeedGames/ParticleEmitter.svg?style=for-the-badge 165 | [contributors-url]: https://github.com/WorldSeedGames/ParticleEmitter/graphs/contributors 166 | [forks-shield]: https://img.shields.io/github/forks/WorldSeedGames/ParticleEmitter.svg?style=for-the-badge 167 | [forks-url]: https://github.com/othneildrew/Best-README-Template/network/members 168 | [stars-shield]: https://img.shields.io/github/stars/WorldSeedGames/ParticleEmitter.svg?style=for-the-badge 169 | [stars-url]: https://github.com/WorldSeedGames/ParticleEmitter/stargazers 170 | [issues-shield]: https://img.shields.io/github/issues/WorldSeedGames/ParticleEmitter.svg?style=for-the-badge 171 | [issues-url]: https://github.com/WorldSeedGames/ParticleEmitter/issues 172 | [license-shield]: https://img.shields.io/github/license/WorldSeedGames/ParticleEmitter?style=for-the-badge 173 | [license-url]: https://github.com/WorldSeedGames/ParticleEmitter/blob/master/LICENSE 174 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java") 3 | `maven-publish` 4 | signing 5 | } 6 | 7 | sourceSets { 8 | main { 9 | java { 10 | srcDir("src/main") 11 | } 12 | } 13 | } 14 | 15 | repositories { 16 | mavenCentral() 17 | } 18 | 19 | description = "Particle Effects for WorldSeed" 20 | java.sourceCompatibility = JavaVersion.VERSION_21 21 | 22 | tasks.jar { 23 | manifest { 24 | archiveBaseName.set("ParticleEmitter") 25 | } 26 | } 27 | 28 | publishing { 29 | publications.create("maven") { 30 | groupId = "net.worldseed.particleemitter" 31 | artifactId = "ParticleEmitter" 32 | version = "1.4.1" 33 | 34 | from(components["java"]) 35 | } 36 | 37 | repositories { 38 | maven { 39 | name = "WorldSeed" 40 | url = uri("https://reposilite.worldseed.online/public") 41 | credentials(PasswordCredentials::class) 42 | authentication { 43 | create("basic") 44 | } 45 | } 46 | } 47 | } 48 | 49 | dependencies { 50 | testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") 51 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") 52 | 53 | compileOnly("net.minestom:minestom-snapshots:1f34e60ea6") 54 | testImplementation("net.minestom:minestom-snapshots:1f34e60ea6") 55 | 56 | implementation("dev.hollowcube:mql:1.0.1") 57 | } 58 | 59 | tasks.getByName("test") { 60 | useJUnitPlatform() 61 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AtlasEngineCa/ParticleEmitter/9a374fe4574f00617ce0854a1bcb9f43bed0737f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ParticleEffect" 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/curves/Curve.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.curves; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class Curve { 7 | final String curve_name; 8 | final String input; 9 | 10 | public enum CurveType { 11 | LINEAR, BEZIER, BEZIER_CHAIN, CATMULL_ROM 12 | } 13 | 14 | List nodes = new ArrayList<>(); 15 | 16 | public Curve(String curve_name, String input) { 17 | this.curve_name = curve_name; 18 | this.input = input; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/curves/CurveNode.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.curves; 2 | 3 | public class CurveNode { 4 | final String molang; 5 | final String input; 6 | 7 | public CurveNode(String molang, String input) { 8 | this.molang = molang; 9 | this.input = input; 10 | } 11 | 12 | double evaluate() { 13 | return 0; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/emitters/EmitterLifetime.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.emitters; 2 | 3 | import net.worldseed.particleemitter.runtime.ParticleInterface; 4 | 5 | public interface EmitterLifetime { 6 | enum LifetimeState { 7 | ALIVE, INACTIVE, DEAD 8 | } 9 | 10 | LifetimeState getState(ParticleInterface i); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/emitters/EmitterRate.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.emitters; 2 | 3 | import net.worldseed.particleemitter.runtime.ParticleInterface; 4 | 5 | public interface EmitterRate { 6 | boolean canEmit(ParticleInterface emitter); 7 | } -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/emitters/EmitterShape.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.emitters; 2 | 3 | import net.minestom.server.coordinate.Vec; 4 | import net.worldseed.particleemitter.runtime.ParticleInterface; 5 | 6 | public interface EmitterShape { 7 | Vec emitPosition(ParticleInterface particleEmitter); 8 | Vec emitDirection(Vec origin, ParticleInterface particleEmitter); 9 | boolean canRotate(); 10 | } -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/emitters/init/EmitterInitialization.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.emitters.init; 2 | 3 | import com.google.gson.JsonElement; 4 | import net.worldseed.particleemitter.runtime.ParticleEmitterScript; 5 | import net.worldseed.particleemitter.runtime.ParticleEmitter; 6 | 7 | import java.lang.reflect.InvocationTargetException; 8 | 9 | public record EmitterInitialization(ParticleEmitterScript creationExpression, ParticleEmitterScript perUpdateExpression) { 10 | public static EmitterInitialization parse(JsonElement emitterInitialization) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 11 | if (emitterInitialization == null) return new EmitterInitialization(null, null); 12 | 13 | JsonElement creation_expression = emitterInitialization.getAsJsonObject().get("creation_expression"); 14 | JsonElement per_update_expression = emitterInitialization.getAsJsonObject().get("per_update_expression"); 15 | 16 | String creationExpression = creation_expression == null ? null : creation_expression.getAsString(); 17 | String perUpdateExpression = per_update_expression == null ? null : per_update_expression.getAsString(); 18 | 19 | return new EmitterInitialization(ParticleEmitterScript.fromString(creationExpression), ParticleEmitterScript.fromString(perUpdateExpression)); 20 | } 21 | 22 | public void initialize(ParticleEmitter particleEmitter) { 23 | if (creationExpression != null) creationExpression.evaluate(particleEmitter); 24 | } 25 | 26 | public void update(ParticleEmitter particleEmitter) { 27 | if (perUpdateExpression != null) perUpdateExpression.evaluate(particleEmitter); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/emitters/init/EmitterLocalSpace.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.emitters.init; 2 | 3 | import com.google.gson.JsonElement; 4 | 5 | public record EmitterLocalSpace(boolean position, boolean velocity, boolean rotation) { 6 | public static EmitterLocalSpace parse(JsonElement emitterLocalSpace) { 7 | if (emitterLocalSpace == null) return new EmitterLocalSpace(false, false, false); 8 | 9 | JsonElement positionEl = emitterLocalSpace.getAsJsonObject().get("position"); 10 | JsonElement velocityEl = emitterLocalSpace.getAsJsonObject().get("velocity"); 11 | JsonElement rotationEl = emitterLocalSpace.getAsJsonObject().get("rotation"); 12 | 13 | boolean position = positionEl != null && positionEl.getAsBoolean(); 14 | boolean velocity = velocityEl != null && velocityEl.getAsBoolean(); 15 | boolean rotation = rotationEl != null && rotationEl.getAsBoolean(); 16 | 17 | return new EmitterLocalSpace(position, velocity, rotation); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/emitters/lifetime/EmitterLifetimeExpression.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.emitters.lifetime; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonObject; 5 | import net.worldseed.particleemitter.emitters.EmitterLifetime; 6 | import net.worldseed.particleemitter.runtime.ParticleEmitterScript; 7 | import net.worldseed.particleemitter.runtime.ParticleInterface; 8 | 9 | import java.lang.reflect.InvocationTargetException; 10 | import java.util.Objects; 11 | 12 | public final class EmitterLifetimeExpression implements EmitterLifetime { 13 | private final ParticleEmitterScript activeExpression; 14 | private final ParticleEmitterScript expirationExpression; 15 | private boolean expired = false; 16 | 17 | public EmitterLifetimeExpression(ParticleEmitterScript activeExpression, ParticleEmitterScript expirationExpression) { 18 | this.activeExpression = activeExpression; 19 | this.expirationExpression = expirationExpression; 20 | } 21 | 22 | public static EmitterLifetime parse(JsonObject asJsonObject) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 23 | if (asJsonObject == null) 24 | return new EmitterLifetimeExpression(ParticleEmitterScript.fromDouble(1), ParticleEmitterScript.fromDouble(0)); 25 | JsonElement active_expression = asJsonObject.get("activation_expression"); 26 | JsonElement expiration_expression = asJsonObject.get("expiration_expression"); 27 | String activeExpression = active_expression == null ? "1" : active_expression.getAsString(); 28 | String expirationExpression = expiration_expression == null ? "0" : expiration_expression.getAsString(); 29 | 30 | return new EmitterLifetimeExpression(ParticleEmitterScript.fromString(activeExpression), ParticleEmitterScript.fromString(expirationExpression)); 31 | } 32 | 33 | @Override 34 | public LifetimeState getState(ParticleInterface i) { 35 | if (expired) return LifetimeState.DEAD; 36 | if (expirationExpression.evaluate(i) != 0) { 37 | expired = true; 38 | return LifetimeState.DEAD; 39 | } 40 | 41 | if (activeExpression.evaluate(i) != 0) return LifetimeState.ALIVE; 42 | return LifetimeState.INACTIVE; 43 | } 44 | 45 | @Override 46 | public boolean equals(Object obj) { 47 | if (obj == this) return true; 48 | if (obj == null || obj.getClass() != this.getClass()) return false; 49 | var that = (EmitterLifetimeExpression) obj; 50 | return Objects.equals(this.activeExpression, that.activeExpression) && 51 | Objects.equals(this.expirationExpression, that.expirationExpression); 52 | } 53 | 54 | @Override 55 | public int hashCode() { 56 | return Objects.hash(activeExpression, expirationExpression); 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return "EmitterLifetimeExpression[" + 62 | "activeExpression=" + activeExpression + ", " + 63 | "expirationExpression=" + expirationExpression + ']'; 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/emitters/lifetime/EmitterLifetimeLooping.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.emitters.lifetime; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonObject; 5 | import net.worldseed.particleemitter.emitters.EmitterLifetime; 6 | import net.worldseed.particleemitter.runtime.ParticleEmitterScript; 7 | import net.worldseed.particleemitter.runtime.ParticleInterface; 8 | 9 | import java.lang.reflect.InvocationTargetException; 10 | import java.util.Objects; 11 | 12 | public final class EmitterLifetimeLooping implements EmitterLifetime { 13 | private final ParticleEmitterScript activeTime; 14 | private final ParticleEmitterScript sleepTime; 15 | private int waitTicks = 0; 16 | boolean active = true; 17 | 18 | public EmitterLifetimeLooping(ParticleEmitterScript activeTime, ParticleEmitterScript sleepTime) { 19 | this.activeTime = activeTime; 20 | this.sleepTime = sleepTime; 21 | } 22 | 23 | public static EmitterLifetime parse(JsonObject asJsonObject) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 24 | if (asJsonObject == null) 25 | return new EmitterLifetimeLooping(ParticleEmitterScript.fromDouble(10), ParticleEmitterScript.fromDouble(0)); 26 | JsonElement active_time = asJsonObject.get("active_time"); 27 | JsonElement sleep_time = asJsonObject.get("sleep_time"); 28 | var activeTime = active_time == null ? ParticleEmitterScript.fromDouble(10) : ParticleEmitterScript.fromString(active_time.getAsString()); 29 | var sleepTime = sleep_time == null ? ParticleEmitterScript.fromDouble(0) : ParticleEmitterScript.fromString(sleep_time.getAsString()); 30 | 31 | return new EmitterLifetimeLooping(activeTime, sleepTime); 32 | } 33 | 34 | @Override 35 | public LifetimeState getState(ParticleInterface i) { 36 | if (waitTicks <= 0) { 37 | if (active) { 38 | waitTicks = (int) (i.updatesPerSecond() * sleepTime.evaluate(i)); 39 | active = false; 40 | return LifetimeState.INACTIVE; 41 | } else { 42 | waitTicks = (int) (i.updatesPerSecond() * activeTime.evaluate(i)); 43 | active = true; 44 | i.reset(); 45 | return LifetimeState.ALIVE; 46 | } 47 | } else { 48 | waitTicks--; 49 | return active ? LifetimeState.ALIVE : LifetimeState.INACTIVE; 50 | } 51 | } 52 | 53 | @Override 54 | public boolean equals(Object obj) { 55 | if (obj == this) return true; 56 | if (obj == null || obj.getClass() != this.getClass()) return false; 57 | var that = (EmitterLifetimeLooping) obj; 58 | return Objects.equals(this.activeTime, that.activeTime) && 59 | Objects.equals(this.sleepTime, that.sleepTime); 60 | } 61 | 62 | @Override 63 | public int hashCode() { 64 | return Objects.hash(activeTime, sleepTime); 65 | } 66 | 67 | @Override 68 | public String toString() { 69 | return "EmitterLifetimeLooping[" + 70 | "activeTime=" + activeTime + ", " + 71 | "sleepTime=" + sleepTime + ']'; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/emitters/lifetime/EmitterLifetimeOnce.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.emitters.lifetime; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonObject; 5 | import net.worldseed.particleemitter.emitters.EmitterLifetime; 6 | import net.worldseed.particleemitter.runtime.ParticleEmitterScript; 7 | import net.worldseed.particleemitter.runtime.ParticleInterface; 8 | 9 | import java.lang.reflect.InvocationTargetException; 10 | 11 | public record EmitterLifetimeOnce(ParticleEmitterScript activeTime) implements EmitterLifetime { 12 | public static EmitterLifetime parse(JsonObject asJsonObject) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 13 | if (asJsonObject == null) return new EmitterLifetimeOnce(ParticleEmitterScript.fromDouble(10)); 14 | JsonElement active_time = asJsonObject.get("active_time"); 15 | var activeTime = active_time == null ? ParticleEmitterScript.fromDouble(0) : ParticleEmitterScript.fromString(active_time.getAsString()); 16 | 17 | return new EmitterLifetimeOnce(activeTime); 18 | } 19 | 20 | @Override 21 | public LifetimeState getState(ParticleInterface i) { 22 | if (activeTime.evaluate(i) < i.emitter_age()) return LifetimeState.DEAD; 23 | return LifetimeState.ALIVE; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/emitters/rate/EmitterRateInstant.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.emitters.rate; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonObject; 5 | import net.worldseed.particleemitter.emitters.EmitterRate; 6 | import net.worldseed.particleemitter.runtime.ParticleEmitterScript; 7 | import net.worldseed.particleemitter.runtime.ParticleInterface; 8 | 9 | import java.lang.reflect.InvocationTargetException; 10 | 11 | public record EmitterRateInstant(ParticleEmitterScript particleNumber) implements EmitterRate { 12 | public static EmitterRate parse(JsonObject asJsonObject) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 13 | if (asJsonObject == null) return new EmitterRateInstant(ParticleEmitterScript.fromDouble(10)); 14 | JsonElement particle_number = asJsonObject.get("num_particles"); 15 | var particleNumber = particle_number == null ? ParticleEmitterScript.fromDouble(10) : ParticleEmitterScript.fromString(particle_number.getAsString()); 16 | return new EmitterRateInstant(particleNumber); 17 | } 18 | 19 | @Override 20 | public boolean canEmit(ParticleInterface emitter) { 21 | return particleNumber.evaluate(emitter) > emitter.particle_count(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/emitters/rate/EmitterRateSteady.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.emitters.rate; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonObject; 5 | import net.worldseed.particleemitter.emitters.EmitterRate; 6 | import net.worldseed.particleemitter.runtime.ParticleEmitterScript; 7 | import net.worldseed.particleemitter.runtime.ParticleInterface; 8 | 9 | import java.lang.reflect.InvocationTargetException; 10 | import java.util.Objects; 11 | 12 | public final class EmitterRateSteady implements EmitterRate { 13 | private final ParticleEmitterScript spawnRate; 14 | private final ParticleEmitterScript maxParticles; 15 | private int waitTicks = 0; 16 | 17 | public EmitterRateSteady(ParticleEmitterScript spawnRate, ParticleEmitterScript maxParticles) { 18 | this.spawnRate = spawnRate; 19 | this.maxParticles = maxParticles; 20 | } 21 | 22 | public static EmitterRate parse(JsonObject asJsonObject) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 23 | if (asJsonObject == null) 24 | return new EmitterRateSteady(ParticleEmitterScript.fromDouble(1), ParticleEmitterScript.fromDouble(50)); 25 | JsonElement spawn_rate = asJsonObject.get("spawn_rate"); 26 | JsonElement max_particles = asJsonObject.get("max_particles"); 27 | var spawnRate = spawn_rate == null ? ParticleEmitterScript.fromDouble(1) : ParticleEmitterScript.fromString(spawn_rate.getAsString()); 28 | var maxParticles = max_particles == null ? ParticleEmitterScript.fromDouble(50) : ParticleEmitterScript.fromString(max_particles.getAsString()); 29 | 30 | return new EmitterRateSteady(spawnRate, maxParticles); 31 | } 32 | 33 | @Override 34 | public boolean canEmit(ParticleInterface emitter) { 35 | if (waitTicks <= 0) { 36 | waitTicks = (int) (emitter.updatesPerSecond() / spawnRate.evaluate(emitter)); 37 | return emitter.particle_count() < maxParticles.evaluate(emitter); 38 | } else { 39 | waitTicks--; 40 | return false; 41 | } 42 | } 43 | 44 | @Override 45 | public boolean equals(Object obj) { 46 | if (obj == this) return true; 47 | if (obj == null || obj.getClass() != this.getClass()) return false; 48 | var that = (EmitterRateSteady) obj; 49 | return Objects.equals(this.spawnRate, that.spawnRate) && 50 | Objects.equals(this.maxParticles, that.maxParticles); 51 | } 52 | 53 | @Override 54 | public int hashCode() { 55 | return Objects.hash(spawnRate, maxParticles); 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return "EmitterRateSteady[" + 61 | "spawnRate=" + spawnRate + ", " + 62 | "maxParticles=" + maxParticles + ']'; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/emitters/shape/EmitterShapeBox.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.emitters.shape; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonElement; 5 | import com.google.gson.JsonObject; 6 | import com.google.gson.JsonPrimitive; 7 | import net.worldseed.particleemitter.emitters.EmitterShape; 8 | import net.worldseed.particleemitter.misc.EmitterDirectionType; 9 | import net.minestom.server.coordinate.Vec; 10 | import net.worldseed.particleemitter.runtime.ParticleEmitterScript; 11 | import net.worldseed.particleemitter.runtime.ParticleInterface; 12 | 13 | import java.lang.reflect.InvocationTargetException; 14 | import java.util.Locale; 15 | 16 | public record EmitterShapeBox(ParticleEmitterScript offsetX, ParticleEmitterScript offsetY, ParticleEmitterScript offsetZ, 17 | ParticleEmitterScript halfDimensionX, ParticleEmitterScript halfDimensionY, ParticleEmitterScript halfDimensionZ, 18 | boolean surfaceOnly, 19 | EmitterDirectionType type, 20 | ParticleEmitterScript directionX, ParticleEmitterScript directionY, ParticleEmitterScript directionZ) implements EmitterShape { 21 | private static final JsonArray defaultOffset; 22 | 23 | static { 24 | defaultOffset = new JsonArray(); 25 | defaultOffset.add("0"); 26 | defaultOffset.add("0"); 27 | defaultOffset.add("0"); 28 | } 29 | 30 | public static EmitterShape parse(JsonObject asJsonObject) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 31 | JsonElement offsetEl = asJsonObject.get("offset"); 32 | JsonArray offset = offsetEl != null ? offsetEl.getAsJsonArray() : defaultOffset; 33 | 34 | JsonArray half_dimension = asJsonObject.get("half_dimensions").getAsJsonArray(); 35 | boolean surface_only = asJsonObject.has("surface_only") && asJsonObject.get("surface_only").getAsBoolean(); 36 | 37 | JsonElement direction = asJsonObject.get("direction"); 38 | if (direction == null) direction = new JsonPrimitive("outwards"); 39 | 40 | var offsetX = ParticleEmitterScript.fromString(offset.get(0).getAsString()); 41 | var offsetY = ParticleEmitterScript.fromString(offset.get(1).getAsString()); 42 | var offsetZ = ParticleEmitterScript.fromString(offset.get(2).getAsString()); 43 | 44 | var halfDimensionX = ParticleEmitterScript.fromString(half_dimension.get(0).getAsString()); 45 | var halfDimensionY = ParticleEmitterScript.fromString(half_dimension.get(1).getAsString()); 46 | var halfDimensionZ = ParticleEmitterScript.fromString(half_dimension.get(2).getAsString()); 47 | 48 | if (direction.isJsonPrimitive()) { 49 | EmitterDirectionType type = EmitterDirectionType.valueOf(direction.getAsString().toUpperCase(Locale.ROOT)); 50 | return new EmitterShapeBox(offsetX, offsetY, offsetZ, halfDimensionX, halfDimensionY, halfDimensionZ, surface_only, type, null, null, null); 51 | } else { 52 | JsonArray directionArray = direction.getAsJsonArray(); 53 | var directionX = ParticleEmitterScript.fromString(directionArray.get(0).getAsString()); 54 | var directionY = ParticleEmitterScript.fromString(directionArray.get(1).getAsString()); 55 | var directionZ = ParticleEmitterScript.fromString(directionArray.get(2).getAsString()); 56 | 57 | return new EmitterShapeBox(offsetX, offsetY, offsetZ, halfDimensionX, halfDimensionY, halfDimensionZ, surface_only, EmitterDirectionType.VELOCITY, directionX, directionY, directionZ); 58 | } 59 | } 60 | 61 | @Override 62 | public Vec emitPosition(ParticleInterface particleEmitter) { 63 | double x = offsetX.evaluate(particleEmitter); 64 | double y = offsetY.evaluate(particleEmitter); 65 | double z = offsetZ.evaluate(particleEmitter); 66 | 67 | double halfDimensionX = this.halfDimensionX.evaluate(particleEmitter); 68 | double halfDimensionY = this.halfDimensionY.evaluate(particleEmitter); 69 | double halfDimensionZ = this.halfDimensionZ.evaluate(particleEmitter); 70 | 71 | double randomX = Math.random() * halfDimensionX * 2 - halfDimensionX; 72 | double randomY = Math.random() * halfDimensionY * 2 - halfDimensionY; 73 | double randomZ = Math.random() * halfDimensionZ * 2 - halfDimensionZ; 74 | 75 | // Force to surface 76 | if (surfaceOnly) { 77 | double random = Math.random() * 6; 78 | if (random < 1) { 79 | randomX = -halfDimensionX; 80 | } else if (random < 2) { 81 | randomX = halfDimensionX; 82 | } else if (random < 3) { 83 | randomY = -halfDimensionY; 84 | } else if (random < 4) { 85 | randomY = halfDimensionY; 86 | } else if (random < 5) { 87 | randomZ = -halfDimensionZ; 88 | } else { 89 | randomZ = halfDimensionZ; 90 | } 91 | } 92 | 93 | return new Vec(x + randomX, y + randomY, z + randomZ); 94 | } 95 | 96 | @Override 97 | public Vec emitDirection(Vec origin, ParticleInterface particleEmitter) { 98 | return switch (type) { 99 | case INWARDS -> origin.sub(emitPosition(particleEmitter)).normalize(); 100 | case OUTWARDS -> emitPosition(particleEmitter).sub(origin).normalize(); 101 | case VELOCITY -> 102 | new Vec(directionX.evaluate(particleEmitter), directionY.evaluate(particleEmitter), directionZ.evaluate(particleEmitter)).normalize(); 103 | }; 104 | } 105 | 106 | @Override 107 | public boolean canRotate() { 108 | return type == EmitterDirectionType.VELOCITY; 109 | } 110 | } -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/emitters/shape/EmitterShapeDisc.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.emitters.shape; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonElement; 5 | import com.google.gson.JsonObject; 6 | import com.google.gson.JsonPrimitive; 7 | import net.worldseed.particleemitter.emitters.EmitterShape; 8 | import net.worldseed.particleemitter.misc.EmitterDirectionType; 9 | import net.worldseed.particleemitter.misc.EmitterPlaneNormalType; 10 | import net.minestom.server.coordinate.Vec; 11 | import net.worldseed.particleemitter.runtime.ParticleEmitterScript; 12 | import net.worldseed.particleemitter.runtime.ParticleInterface; 13 | 14 | import java.lang.reflect.InvocationTargetException; 15 | 16 | public record EmitterShapeDisc(EmitterPlaneNormalType planeNormalType, 17 | ParticleEmitterScript planeX, ParticleEmitterScript planeY, ParticleEmitterScript planeZ, 18 | ParticleEmitterScript offsetX, ParticleEmitterScript offsetY, ParticleEmitterScript offsetZ, 19 | ParticleEmitterScript radius, boolean surfaceOnly, 20 | EmitterDirectionType type, 21 | ParticleEmitterScript directionX, ParticleEmitterScript directionY, ParticleEmitterScript directionZ) implements EmitterShape { 22 | private static final JsonArray defaultPlaneNormal; 23 | private static final JsonArray defaultOffset; 24 | 25 | static { 26 | defaultPlaneNormal = new JsonArray(); 27 | defaultPlaneNormal.add("0"); 28 | defaultPlaneNormal.add("1"); 29 | defaultPlaneNormal.add("0"); 30 | 31 | defaultOffset = new JsonArray(); 32 | defaultOffset.add("0"); 33 | defaultOffset.add("0"); 34 | defaultOffset.add("0"); 35 | } 36 | 37 | public static EmitterShape parse(JsonObject asJsonObject) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 38 | JsonElement plane = asJsonObject.get("plane_normal"); 39 | if (plane == null) plane = defaultPlaneNormal; 40 | 41 | JsonElement offsetEl = asJsonObject.get("offset"); 42 | JsonArray offset = offsetEl == null ? defaultOffset : offsetEl.getAsJsonArray(); 43 | 44 | JsonElement radiusEl = asJsonObject.get("radius"); 45 | ParticleEmitterScript radius = radiusEl == null ? ParticleEmitterScript.fromDouble(1) : ParticleEmitterScript.fromString(radiusEl.getAsString()); 46 | 47 | boolean surface_only = asJsonObject.has("surface_only") && asJsonObject.get("surface_only").getAsBoolean(); 48 | JsonElement direction = asJsonObject.get("direction"); 49 | if (direction == null) direction = new JsonPrimitive("outwards"); 50 | 51 | var offsetX = ParticleEmitterScript.fromString(offset.get(0).getAsString()); 52 | var offsetY = ParticleEmitterScript.fromString(offset.get(1).getAsString()); 53 | var offsetZ = ParticleEmitterScript.fromString(offset.get(2).getAsString()); 54 | 55 | if (plane.isJsonPrimitive()) { 56 | EmitterPlaneNormalType planeNormalType = EmitterPlaneNormalType.valueOf(plane.getAsString().toUpperCase()); 57 | if (direction.isJsonPrimitive()) { 58 | EmitterDirectionType type = EmitterDirectionType.valueOf(direction.getAsString().toUpperCase()); 59 | return new EmitterShapeDisc(planeNormalType, 60 | null, null, null, 61 | offsetX, offsetY, offsetZ, 62 | radius, surface_only, type, 63 | null, null, null); 64 | } else { 65 | JsonArray directionArray = direction.getAsJsonArray(); 66 | var directionX = ParticleEmitterScript.fromString(directionArray.get(0).getAsString()); 67 | var directionY = ParticleEmitterScript.fromString(directionArray.get(1).getAsString()); 68 | var directionZ = ParticleEmitterScript.fromString(directionArray.get(2).getAsString()); 69 | 70 | return new EmitterShapeDisc(planeNormalType, null, null, null, 71 | offsetX, offsetY, offsetZ, 72 | radius, surface_only, EmitterDirectionType.VELOCITY, 73 | directionX, directionY, directionZ); 74 | } 75 | } else { 76 | JsonArray planeArray = plane.getAsJsonArray(); 77 | var planeX = ParticleEmitterScript.fromString(planeArray.get(0).getAsString()); 78 | var planeY = ParticleEmitterScript.fromString(planeArray.get(1).getAsString()); 79 | var planeZ = ParticleEmitterScript.fromString(planeArray.get(2).getAsString()); 80 | 81 | if (direction.isJsonPrimitive()) { 82 | EmitterDirectionType type = EmitterDirectionType.valueOf(direction.getAsString().toUpperCase()); 83 | return new EmitterShapeDisc(EmitterPlaneNormalType.CUSTOM, 84 | planeX, planeY, planeZ, 85 | offsetX, offsetY, offsetZ, 86 | radius, surface_only, type, 87 | null, null, null); 88 | } else { 89 | JsonArray directionArray = direction.getAsJsonArray(); 90 | var directionX = ParticleEmitterScript.fromString(directionArray.get(0).getAsString()); 91 | var directionY = ParticleEmitterScript.fromString(directionArray.get(1).getAsString()); 92 | var directionZ = ParticleEmitterScript.fromString(directionArray.get(2).getAsString()); 93 | 94 | return new EmitterShapeDisc(EmitterPlaneNormalType.CUSTOM, 95 | planeX, planeY, planeZ, 96 | offsetX, offsetY, offsetZ, 97 | radius, surface_only, EmitterDirectionType.VELOCITY, 98 | directionX, directionY, directionZ); 99 | } 100 | } 101 | } 102 | 103 | @Override 104 | public Vec emitPosition(ParticleInterface particleEmitter) { 105 | double normalX = 0; 106 | double normalY = 0; 107 | double normalZ = 0; 108 | 109 | if (planeNormalType == EmitterPlaneNormalType.CUSTOM) { 110 | normalX = planeX.evaluate(particleEmitter); 111 | normalY = planeY.evaluate(particleEmitter); 112 | normalZ = planeZ.evaluate(particleEmitter); 113 | } else { 114 | switch (planeNormalType) { 115 | case X -> normalX = 1; 116 | case Y -> normalY = 1; 117 | case Z -> normalZ = 1; 118 | } 119 | } 120 | 121 | if (normalX == 0 && normalY == 0 && normalZ == 0) { 122 | normalY = 1; 123 | } 124 | 125 | double radius = this.radius.evaluate(particleEmitter); 126 | double angle = Math.random() * 2 * Math.PI; 127 | 128 | double offsetX = this.offsetX.evaluate(particleEmitter); 129 | double offsetY = this.offsetY.evaluate(particleEmitter); 130 | double offsetZ = this.offsetZ.evaluate(particleEmitter); 131 | 132 | if (!surfaceOnly) { 133 | radius *= Math.random(); 134 | } 135 | 136 | Vec normal = new Vec(normalX, normalY, normalZ); 137 | Vec tangent = (normalX == 0 && normalY == 0 ? new Vec(0, -normalZ, normalY) : new Vec(-normalY, normalX, 0)).normalize(); 138 | Vec w = normal.cross(tangent).normalize(); 139 | 140 | double x = Math.cos(angle) * radius; 141 | double z = Math.sin(angle) * radius; 142 | 143 | return w.mul(x).add(tangent.mul(z)).add(offsetX, offsetY, offsetZ); 144 | } 145 | 146 | @Override 147 | public Vec emitDirection(Vec origin, ParticleInterface particleEmitter) { 148 | return switch (type) { 149 | case INWARDS -> origin.sub(emitPosition(particleEmitter)).normalize(); 150 | case OUTWARDS -> emitPosition(particleEmitter).sub(origin).normalize(); 151 | case VELOCITY -> 152 | new Vec(directionX.evaluate(particleEmitter), directionY.evaluate(particleEmitter), directionZ.evaluate(particleEmitter)).normalize(); 153 | }; 154 | } 155 | 156 | @Override 157 | public boolean canRotate() { 158 | return type == EmitterDirectionType.VELOCITY; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/emitters/shape/EmitterShapeEntityAABB.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.emitters.shape; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonElement; 5 | import com.google.gson.JsonObject; 6 | import net.worldseed.particleemitter.emitters.EmitterShape; 7 | import net.worldseed.particleemitter.misc.EmitterDirectionType; 8 | import net.minestom.server.coordinate.Vec; 9 | import net.worldseed.particleemitter.runtime.ParticleEmitterScript; 10 | import net.worldseed.particleemitter.runtime.ParticleInterface; 11 | 12 | import java.lang.reflect.InvocationTargetException; 13 | 14 | public record EmitterShapeEntityAABB(boolean surfaceOnly, 15 | EmitterDirectionType type, 16 | ParticleEmitterScript directionX, ParticleEmitterScript directionY, ParticleEmitterScript directionZ) implements EmitterShape { 17 | private static final JsonArray defaultDirection; 18 | 19 | static { 20 | defaultDirection = new JsonArray(); 21 | defaultDirection.add("0"); 22 | defaultDirection.add("0"); 23 | defaultDirection.add("0"); 24 | } 25 | 26 | public static EmitterShape parse(JsonObject asJsonObject) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 27 | boolean surface_only = asJsonObject.has("surface_only") && asJsonObject.get("surface_only").getAsBoolean(); 28 | 29 | JsonElement direction = asJsonObject.get("direction"); 30 | if (direction == null) direction = defaultDirection; 31 | 32 | if (direction.isJsonPrimitive()) { 33 | EmitterDirectionType type = EmitterDirectionType.valueOf(direction.getAsString().toUpperCase()); 34 | return new EmitterShapeEntityAABB(surface_only, type, null, null, null); 35 | } else { 36 | JsonArray directionArray = direction.getAsJsonArray(); 37 | 38 | var directionX = ParticleEmitterScript.fromString(directionArray.get(0).getAsString()); 39 | var directionY = ParticleEmitterScript.fromString(directionArray.get(1).getAsString()); 40 | var directionZ = ParticleEmitterScript.fromString(directionArray.get(2).getAsString()); 41 | 42 | return new EmitterShapeEntityAABB(surface_only, EmitterDirectionType.VELOCITY, 43 | directionX, directionY, directionZ); 44 | } 45 | } 46 | 47 | @Override 48 | public Vec emitPosition(ParticleInterface particleEmitter) { 49 | return null; 50 | } 51 | 52 | @Override 53 | public Vec emitDirection(Vec origin, ParticleInterface particleEmitter) { 54 | return null; 55 | } 56 | 57 | @Override 58 | public boolean canRotate() { 59 | return false; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/emitters/shape/EmitterShapePoint.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.emitters.shape; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonElement; 5 | import com.google.gson.JsonObject; 6 | import net.worldseed.particleemitter.emitters.EmitterShape; 7 | import net.minestom.server.coordinate.Vec; 8 | import net.worldseed.particleemitter.runtime.ParticleEmitterScript; 9 | import net.worldseed.particleemitter.runtime.ParticleInterface; 10 | 11 | import java.lang.reflect.InvocationTargetException; 12 | 13 | public record EmitterShapePoint(ParticleEmitterScript offsetX, ParticleEmitterScript offsetY, ParticleEmitterScript offsetZ, 14 | ParticleEmitterScript directionX, ParticleEmitterScript directionY, ParticleEmitterScript directionZ) implements EmitterShape { 15 | private static final JsonArray defaultOffset; 16 | 17 | static { 18 | defaultOffset = new JsonArray(); 19 | defaultOffset.add("0"); 20 | defaultOffset.add("0"); 21 | defaultOffset.add("0"); 22 | } 23 | 24 | public EmitterShapePoint() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 25 | this(ParticleEmitterScript.fromDouble(0), ParticleEmitterScript.fromDouble(0), ParticleEmitterScript.fromDouble(0), 26 | ParticleEmitterScript.fromDouble(0), ParticleEmitterScript.fromDouble(0), ParticleEmitterScript.fromDouble(0)); 27 | } 28 | 29 | public static EmitterShape parse(JsonObject asJsonObject) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 30 | if (asJsonObject == null) return new EmitterShapePoint( 31 | ParticleEmitterScript.fromDouble(0), 32 | ParticleEmitterScript.fromDouble(0), 33 | ParticleEmitterScript.fromDouble(0), 34 | ParticleEmitterScript.fromDouble(0), 35 | ParticleEmitterScript.fromDouble(0), 36 | ParticleEmitterScript.fromDouble(0) 37 | ); 38 | 39 | JsonElement offsetEl = asJsonObject.get("offset"); 40 | JsonArray offset = offsetEl == null ? defaultOffset : offsetEl.getAsJsonArray(); 41 | 42 | JsonElement directionEl = asJsonObject.get("direction"); 43 | 44 | JsonArray direction = directionEl == null ? null : directionEl.getAsJsonArray(); 45 | 46 | ParticleEmitterScript offsetX = ParticleEmitterScript.fromString(offset.get(0).getAsString()); 47 | ParticleEmitterScript offsetY = ParticleEmitterScript.fromString(offset.get(1).getAsString()); 48 | ParticleEmitterScript offsetZ = ParticleEmitterScript.fromString(offset.get(2).getAsString()); 49 | 50 | ParticleEmitterScript directionX = direction == null ? null : ParticleEmitterScript.fromString(direction.get(0).getAsString()); 51 | ParticleEmitterScript directionY = direction == null ? null : ParticleEmitterScript.fromString(direction.get(1).getAsString()); 52 | ParticleEmitterScript directionZ = direction == null ? null : ParticleEmitterScript.fromString(direction.get(2).getAsString()); 53 | 54 | return new EmitterShapePoint(offsetX, offsetY, offsetZ, directionX, directionY, directionZ); 55 | } 56 | 57 | @Override 58 | public Vec emitPosition(ParticleInterface particleEmitter) { 59 | return new Vec(offsetX.evaluate(particleEmitter), offsetY.evaluate(particleEmitter), offsetZ.evaluate(particleEmitter)); 60 | } 61 | 62 | @Override 63 | public Vec emitDirection(Vec origin, ParticleInterface particleEmitter) { 64 | if (directionX == null) { 65 | return new Vec(Math.random(), Math.random(), Math.random()).normalize(); 66 | } else { 67 | return new Vec(directionX.evaluate(particleEmitter), directionY.evaluate(particleEmitter), directionZ.evaluate(particleEmitter)).normalize(); 68 | } 69 | } 70 | 71 | @Override 72 | public boolean canRotate() { 73 | return true; 74 | } 75 | } -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/emitters/shape/EmitterShapeSphere.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.emitters.shape; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonElement; 5 | import com.google.gson.JsonObject; 6 | import com.google.gson.JsonPrimitive; 7 | import net.worldseed.particleemitter.emitters.EmitterShape; 8 | import net.worldseed.particleemitter.misc.EmitterDirectionType; 9 | import net.minestom.server.coordinate.Vec; 10 | import net.worldseed.particleemitter.runtime.ParticleEmitterScript; 11 | import net.worldseed.particleemitter.runtime.ParticleInterface; 12 | 13 | import java.lang.reflect.InvocationTargetException; 14 | import java.util.Locale; 15 | 16 | public record EmitterShapeSphere(ParticleEmitterScript offsetX, ParticleEmitterScript offsetY, ParticleEmitterScript offsetZ, 17 | ParticleEmitterScript radius, boolean surfaceOnly, 18 | EmitterDirectionType type, 19 | ParticleEmitterScript directionX, ParticleEmitterScript directionY, ParticleEmitterScript directionZ) implements EmitterShape { 20 | private static final JsonArray defaultOffset; 21 | 22 | static { 23 | defaultOffset = new JsonArray(); 24 | defaultOffset.add("0"); 25 | defaultOffset.add("0"); 26 | defaultOffset.add("0"); 27 | } 28 | 29 | public static EmitterShape parse(JsonObject asJsonObject) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 30 | JsonElement offsetEl = asJsonObject.get("offset"); 31 | JsonArray offset = offsetEl != null ? offsetEl.getAsJsonArray() : defaultOffset; 32 | 33 | JsonElement radiusEl = asJsonObject.get("radius"); 34 | ParticleEmitterScript radius = radiusEl != null ? ParticleEmitterScript.fromString(radiusEl.getAsString()) : ParticleEmitterScript.fromDouble(1); 35 | 36 | JsonElement surfaceOnlyEl = asJsonObject.get("surface_only"); 37 | boolean surfaceOnly = surfaceOnlyEl != null && surfaceOnlyEl.getAsBoolean(); 38 | 39 | JsonElement direction = asJsonObject.get("direction"); 40 | if (direction == null) direction = new JsonPrimitive("outwards"); 41 | 42 | var offsetX = ParticleEmitterScript.fromString(offset.get(0).getAsString()); 43 | var offsetY = ParticleEmitterScript.fromString(offset.get(1).getAsString()); 44 | var offsetZ = ParticleEmitterScript.fromString(offset.get(2).getAsString()); 45 | 46 | if (direction.isJsonPrimitive()) { 47 | EmitterDirectionType type = EmitterDirectionType.valueOf(direction.getAsString().toUpperCase(Locale.ROOT)); 48 | 49 | return new EmitterShapeSphere(offsetX, offsetY, offsetZ, 50 | radius, surfaceOnly, type, null, null, null); 51 | } else { 52 | JsonArray directionArray = direction.getAsJsonArray(); 53 | 54 | var directionX = ParticleEmitterScript.fromString(directionArray.get(0).getAsString()); 55 | var directionY = ParticleEmitterScript.fromString(directionArray.get(1).getAsString()); 56 | var directionZ = ParticleEmitterScript.fromString(directionArray.get(2).getAsString()); 57 | 58 | return new EmitterShapeSphere(offsetX, offsetY, offsetZ, 59 | radius, surfaceOnly, EmitterDirectionType.VELOCITY, 60 | directionX, directionY, directionZ); 61 | } 62 | 63 | } 64 | 65 | @Override 66 | public Vec emitPosition(ParticleInterface particleEmitter) { 67 | double radius = this.radius.evaluate(particleEmitter); 68 | double x = offsetX.evaluate(particleEmitter); 69 | double y = offsetY.evaluate(particleEmitter); 70 | double z = offsetZ.evaluate(particleEmitter); 71 | 72 | if (surfaceOnly) { 73 | var theta = Math.random() * Math.PI * 2; 74 | var v = Math.random(); 75 | var phi = Math.acos((2*v)-1); 76 | x += radius * Math.sin(phi) * Math.cos(theta); 77 | y += radius * Math.sin(phi) * Math.sin(theta); 78 | z += radius * Math.cos(phi); 79 | } else { 80 | var theta = Math.random() * Math.PI * 2; 81 | var v = Math.random(); 82 | var phi = Math.acos((2*v)-1); 83 | var r = Math.pow(Math.random(), 1.0/3) * radius; 84 | x += r * Math.sin(phi) * Math.cos(theta); 85 | y += r * Math.sin(phi) * Math.sin(theta); 86 | z += r * Math.cos(phi); 87 | } 88 | 89 | return new Vec(x, y, z); 90 | } 91 | 92 | @Override 93 | public Vec emitDirection(Vec origin, ParticleInterface particleEmitter) { 94 | return switch (type) { 95 | case INWARDS -> origin.sub(emitPosition(particleEmitter)).normalize(); 96 | case OUTWARDS -> emitPosition(particleEmitter).sub(origin).normalize(); 97 | case VELOCITY -> 98 | new Vec(directionX.evaluate(particleEmitter), directionY.evaluate(particleEmitter), directionZ.evaluate(particleEmitter)).normalize(); 99 | }; 100 | } 101 | 102 | @Override 103 | public boolean canRotate() { 104 | return type == EmitterDirectionType.VELOCITY; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/generator/ParticleGenerator.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.generator; 2 | 3 | import net.minestom.server.color.Color; 4 | import net.minestom.server.coordinate.Vec; 5 | import net.minestom.server.network.packet.server.play.ParticlePacket; 6 | import net.minestom.server.particle.Particle; 7 | 8 | import java.util.Map; 9 | 10 | public class ParticleGenerator { 11 | public static ParticlePacket buildParticle(Particle particleType, double x, double y, double z, double size, double velocityX, double velocityY, double velocityZ, double r, double g, double b) { 12 | return buildParticle(particleType, x, y, z, size, velocityX, velocityY, velocityZ, r, g, b, 0, 0,0); 13 | } 14 | 15 | public static ParticlePacket buildParticle(Particle particleType, double x, double y, double z, double size, double velocityX, double velocityY, double velocityZ, double r, double g, double b, double r2, double g2, double b2) { 16 | if (particleType == Particle.DUST) return buildDust(x, y, z, size, r, g, b); 17 | else if (particleType == Particle.DUST_COLOR_TRANSITION) return buildDustTransition(x, y, z, size, r, g, b, r2, g2, b2); 18 | else if (particleType == Particle.NOTE) return buildNote(x, y, z, r, g, b); 19 | else if (particleType == Particle.ENTITY_EFFECT) return buildEffect(x, y, z, r, g, b); 20 | else if (particleType == Particle.FLAME 21 | || particleType == Particle.SMOKE 22 | || particleType == Particle.FIREWORK 23 | || particleType == Particle.SOUL_FIRE_FLAME 24 | ) return buildDirectional(particleType, x, y, z, velocityX, velocityY, velocityZ); 25 | else return buildGeneric(particleType, x, y, z); 26 | } 27 | 28 | private static ParticlePacket buildDustTransition(double x, double y, double z, double size, double r, double g, double b, double r2, double g2, double b2) { 29 | Particle.DustColorTransition data = Particle.DUST_COLOR_TRANSITION.withProperties(new Color((int) (r * 255), (int) (g * 255), (int) (b * 255)), new Color((int) (r2 * 255), (int) (g2 * 255), (int) (b2 * 255)), (float) size); 30 | return new ParticlePacket(data, true, x, y, z, 0, 0, 0, 0, 0); 31 | } 32 | 33 | private static ParticlePacket buildGeneric(Particle p, double x, double y, double z) { 34 | return new ParticlePacket(p, true, x, y, z, 0, 0, 0, 0, 0); 35 | } 36 | 37 | private static ParticlePacket buildDirectional(Particle p, double x, double y, double z, double velocityX, double velocityY, double velocityZ) { 38 | Vec vec = new Vec(velocityX, velocityY, velocityZ); 39 | double size = vec.length(); 40 | vec = vec.normalize(); 41 | 42 | return new ParticlePacket(p, true, x, y, z, (float) vec.x(), (float) vec.y(), (float) vec.z(), (float) size, 0); 43 | } 44 | 45 | private static ParticlePacket buildDust(double x, double y, double z, double size, double r, double g, double b) { 46 | Particle.Dust data = Particle.DUST.withProperties(new Color((int) (r * 255), (int) (g * 255), (int) (b * 255)), (float) size); 47 | return new ParticlePacket(data, true, x, y, z, 0, 0, 0, 0, 0); 48 | } 49 | 50 | private static ParticlePacket buildEffect(double x, double y, double z, double r, double g, double b) { 51 | return new ParticlePacket(Particle.ENTITY_EFFECT, true, x, y, z, (float) r, (float) g, (float) b, 1, 0); 52 | } 53 | 54 | private static final Map noteColours = Map.ofEntries( 55 | Map.entry(new Vec(0.35, 0.9129164779574352, 0.0), 0.0),Map.entry(new Vec(0.5195469841701994, 0.8086557231547051, 0.0), 0.042),Map.entry(new Vec(0.6738203274905119, 0.6761781872346715, 0.0), 0.083),Map.entry(new Vec(0.8096194188492563, 0.5182322975301983, 0.0), 0.125),Map.entry(new Vec(0.9135959661842593, 0.3486385542433554, 0.0), 0.167),Map.entry(new Vec(0.9774980720415881, 0.18308286651870628, 0.0), 0.208),Map.entry(new Vec(0.9999999999999992, 0.024999913104564397, 0.025000217393631285), 0.25),Map.entry(new Vec(0.9774980556955284, 0.0, 0.18308320609876777), 0.292),Map.entry(new Vec(0.9135959349646745, 0.0, 0.3486389056053107), 0.333),Map.entry(new Vec(0.8096193745372532, 0.0, 0.5182326369205168), 0.375),Map.entry(new Vec(0.6738202731541083, 0.0, 0.6761784911550788), 0.417),Map.entry(new Vec(0.5195469236729805, 0.0, 0.8086559721254418), 0.458),Map.entry(new Vec(0.34999993733336554, 0.0, 0.9129166536387483), 0.5),Map.entry(new Vec(0.18045295533258326, 0.0, 0.9782028287289668), 0.542),Map.entry(new Vec(0.02617961817308767, 0.023821866970487715, 0.9999985750040863), 0.583),Map.entry(new Vec(0.0, 0.18176776300112518, 0.9778517099534143), 0.625),Map.entry(new Vec(0.0, 0.3513615084231421, 0.9122344477415489), 0.667),Map.entry(new Vec(0.0, 0.5169171940464481, 0.8105808060125694), 0.708),Map.entry(new Vec(0.0, 0.6750001411663266, 0.6749997283354576), 0.75),Map.entry(new Vec(0.0, 0.8105811423808698, 0.5169167333360676), 0.792),Map.entry(new Vec(0.0, 0.9122346869536953, 0.35136103172819166), 0.833),Map.entry(new Vec(0.0, 0.9778518333314586, 0.18176730254816992), 0.875),Map.entry(new Vec(0.026179781182298356, 0.9999985740057371, 0.023821454639781303), 0.917),Map.entry(new Vec(0.18045313682423997, 0.9782027063156398, 0.0), 0.958),Map.entry(new Vec(0.35000012533326885, 0.9129164152907789, 0.0), 1.0) 56 | ); 57 | 58 | private static double calculateMinDiff(double r, double g, double b) { 59 | if (r < 0.1 && g < 0.1 && b < 0.1) return 32768; 60 | double val = 0; 61 | double minDistance = 10; 62 | 63 | for (var entry : noteColours.entrySet()) { 64 | double distance = Math.sqrt(Math.pow(entry.getKey().x() - r, 2) + Math.pow(entry.getKey().y() - g, 2) + Math.pow(entry.getKey().z() - b, 2)); 65 | if (distance < minDistance) { 66 | minDistance = distance; 67 | val = entry.getValue(); 68 | } 69 | } 70 | 71 | return val; 72 | } 73 | 74 | private static ParticlePacket buildNote(double x, double y, double z, double r, double g, double b) { 75 | return new ParticlePacket(Particle.NOTE, true, x, y, z, (float) (calculateMinDiff(r, g, b)), 0, 0, 1, 0); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/misc/Colour.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.misc; 2 | 3 | import com.google.gson.JsonElement; 4 | import net.worldseed.particleemitter.particle.ParticleAppearanceTinting; 5 | import net.worldseed.particleemitter.runtime.ParticleEmitterScript; 6 | import net.worldseed.particleemitter.runtime.ParticleInterface; 7 | 8 | import java.awt.*; 9 | import java.lang.reflect.InvocationTargetException; 10 | 11 | public record Colour(ParticleEmitterScript r, ParticleEmitterScript g, ParticleEmitterScript b) { 12 | public Colour(String r, String g, String b) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 13 | this(ParticleEmitterScript.fromString(r), ParticleEmitterScript.fromString(g), ParticleEmitterScript.fromString(b)); 14 | } 15 | 16 | public ParticleAppearanceTinting.ColourEvaluated toColourEvaluated(ParticleInterface particle) { 17 | return new ParticleAppearanceTinting.ColourEvaluated(r.evaluate(particle), g.evaluate(particle), b.evaluate(particle)); 18 | } 19 | 20 | public Colour(double v, double v1, double v2) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 21 | this(ParticleEmitterScript.fromDouble(v), ParticleEmitterScript.fromDouble(v1), ParticleEmitterScript.fromDouble(v2)); 22 | } 23 | 24 | public static Colour fromJson(JsonElement color) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 25 | if (color.isJsonPrimitive()) { 26 | var s = color.getAsString(); 27 | 28 | if (s.length() == 9 && s.startsWith("#")) s = s.substring(1); 29 | if (s.length() == 8) s = s.substring(2); 30 | if (s.length() == 6) s = "#" + s; 31 | 32 | var c = Color.decode(s); 33 | return new Colour(c.getRed() / 255.0, c.getGreen() / 255.0, c.getBlue() / 255.0); 34 | } else { 35 | String r = color.getAsJsonArray().get(0).getAsString(); 36 | String g = color.getAsJsonArray().get(1).getAsString(); 37 | String b = color.getAsJsonArray().get(2).getAsString(); 38 | return new Colour(r, g, b); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/misc/EmitterDirectionType.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.misc; 2 | 3 | public enum EmitterDirectionType { 4 | INWARDS, OUTWARDS, VELOCITY 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/misc/EmitterPlaneNormalType.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.misc; 2 | 3 | public enum EmitterPlaneNormalType { 4 | X, Y, Z, CUSTOM 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/particle/ParticleAppearanceTinting.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.particle; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonElement; 5 | import com.google.gson.JsonObject; 6 | import net.worldseed.particleemitter.misc.Colour; 7 | import net.worldseed.particleemitter.runtime.ParticleEmitterScript; 8 | import net.worldseed.particleemitter.runtime.Particle; 9 | 10 | import java.awt.*; 11 | import java.lang.reflect.InvocationTargetException; 12 | import java.util.Map; 13 | import java.util.TreeMap; 14 | 15 | public record ParticleAppearanceTinting(Map color, ParticleEmitterScript interpolant) { 16 | public static final ParticleAppearanceTinting DEFAULT; 17 | 18 | static { 19 | Map map = new TreeMap<>(); 20 | try { 21 | map.put(0.0, new Colour(255, 255, 255)); 22 | } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) { 23 | throw new RuntimeException(e); 24 | } 25 | DEFAULT = new ParticleAppearanceTinting(map, null); 26 | } 27 | 28 | public static ParticleAppearanceTinting parse(JsonObject particleAppearanceTinting) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 29 | JsonElement colors = particleAppearanceTinting.get("color"); 30 | 31 | ParticleEmitterScript interpolant = null; 32 | Map map = new TreeMap<>(); 33 | 34 | if (colors.isJsonArray()) { 35 | String r = colors.getAsJsonArray().get(0).getAsString(); 36 | String g = colors.getAsJsonArray().get(1).getAsString(); 37 | String b = colors.getAsJsonArray().get(2).getAsString(); 38 | 39 | map.put(0.0, new Colour(r, g, b)); 40 | } else if (colors.isJsonPrimitive()) { 41 | String color = colors.getAsString(); 42 | var c = Color.decode(color); 43 | map.put(0.0, new Colour(c.getRed(), c.getGreen(), c.getBlue())); 44 | } else if (colors.isJsonObject()) { 45 | interpolant = ParticleEmitterScript.fromString(colors.getAsJsonObject().get("interpolant").getAsString()); 46 | 47 | if (colors.getAsJsonObject().has("gradient")) { 48 | JsonElement gradient = colors.getAsJsonObject().get("gradient"); 49 | 50 | if (gradient.isJsonObject()) { 51 | for (Map.Entry entry : colors.getAsJsonObject().get("gradient").getAsJsonObject().entrySet()) { 52 | var c = Colour.fromJson(entry.getValue()); 53 | map.put(Double.parseDouble(entry.getKey()), c); 54 | } 55 | } else { 56 | JsonArray gradientArray = gradient.getAsJsonArray(); 57 | for (int i = 0; i < gradientArray.size(); i++) { 58 | Colour c = Colour.fromJson(gradientArray.get(i)); 59 | map.put((double) i / (gradientArray.size() - 1), c); 60 | } 61 | } 62 | } else { 63 | for (Map.Entry entry : colors.getAsJsonObject().entrySet()) { 64 | String key = entry.getKey(); 65 | JsonElement value = entry.getValue(); 66 | 67 | var c = Colour.fromJson(value); 68 | map.put(Double.parseDouble(key), c); 69 | } 70 | } 71 | } 72 | 73 | return new ParticleAppearanceTinting(map, interpolant); 74 | } 75 | 76 | public ColourEvaluated evaluate(Particle particle) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 77 | if (interpolant == null && color.size() == 0) return ColourEvaluated.white(); 78 | if (interpolant == null) return color.values().iterator().next().toColourEvaluated(particle); 79 | 80 | double t = interpolant.evaluate(particle); 81 | if (color.size() == 1) return color.values().iterator().next().toColourEvaluated(particle); 82 | 83 | Colour c1 = null; 84 | Colour c2 = null; 85 | double t1 = 0; 86 | double t2 = 0; 87 | 88 | for (Map.Entry entry : color.entrySet()) { 89 | if (entry.getKey() <= t) { 90 | c1 = entry.getValue(); 91 | t1 = entry.getKey(); 92 | } else { 93 | c2 = entry.getValue(); 94 | t2 = entry.getKey(); 95 | break; 96 | } 97 | } 98 | 99 | if (c1 == null) return c2.toColourEvaluated(particle); 100 | if (c2 == null) return c1.toColourEvaluated(particle); 101 | 102 | double t3 = (t - t1) / (t2 - t1); 103 | return interpolateColour(c1, c2, t3, particle); 104 | } 105 | 106 | public record ColourEvaluated(double r, double g, double b) { 107 | public static ColourEvaluated white() { 108 | return new ColourEvaluated(1, 1, 1); 109 | } 110 | } 111 | 112 | public static ColourEvaluated interpolateColour(Colour c1, Colour c2, double t, Particle particle) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 113 | return new ColourEvaluated( 114 | (c1.r().evaluate(particle) * (1 - t) + c2.r().evaluate(particle) * t), 115 | (c1.g().evaluate(particle) * (1 - t) + c2.g().evaluate(particle) * t), 116 | (c1.b().evaluate(particle) * (1 - t) + c2.b().evaluate(particle) * t) 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/particle/ParticleInitialSpeed.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.particle; 2 | 3 | import com.google.gson.JsonElement; 4 | import net.worldseed.particleemitter.runtime.ParticleEmitterScript; 5 | 6 | import java.lang.reflect.InvocationTargetException; 7 | 8 | public record ParticleInitialSpeed(ParticleEmitterScript speedX, ParticleEmitterScript speedY, ParticleEmitterScript speedZ) { 9 | public static ParticleInitialSpeed parse(JsonElement particleInitialSpeed) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 10 | if (particleInitialSpeed == null) return new ParticleInitialSpeed(ParticleEmitterScript.fromDouble(0), ParticleEmitterScript.fromDouble(0), ParticleEmitterScript.fromDouble(0)); 11 | 12 | if (particleInitialSpeed.isJsonArray()) { 13 | var asArray = particleInitialSpeed.getAsJsonArray(); 14 | 15 | ParticleEmitterScript speedX = ParticleEmitterScript.fromString(asArray.get(0).getAsString()); 16 | ParticleEmitterScript speedY = ParticleEmitterScript.fromString(asArray.get(1).getAsString()); 17 | ParticleEmitterScript speedZ = ParticleEmitterScript.fromString(asArray.get(2).getAsString()); 18 | 19 | return new ParticleInitialSpeed(speedX, speedY, speedZ); 20 | } 21 | 22 | ParticleEmitterScript speed = ParticleEmitterScript.fromString(particleInitialSpeed.getAsString()); 23 | return new ParticleInitialSpeed(speed, speed, speed); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/particle/ParticleLifetime.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.particle; 2 | 3 | import net.worldseed.particleemitter.runtime.ParticleInterface; 4 | 5 | public interface ParticleLifetime { 6 | boolean isAlive(ParticleInterface i); 7 | } -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/particle/ParticleLifetimeExpression.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.particle; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonObject; 5 | import net.worldseed.particleemitter.runtime.ParticleEmitterScript; 6 | import net.worldseed.particleemitter.runtime.ParticleInterface; 7 | 8 | import java.lang.reflect.InvocationTargetException; 9 | 10 | public final class ParticleLifetimeExpression implements ParticleLifetime { 11 | public static final ParticleLifetime DEFAULT; 12 | private final ParticleEmitterScript expiration_expression; 13 | private final ParticleEmitterScript max_lifetime; 14 | 15 | static { 16 | try { 17 | DEFAULT = new ParticleLifetimeExpression(ParticleEmitterScript.fromDouble(0), null); 18 | } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) { 19 | throw new RuntimeException(e); 20 | } 21 | } 22 | 23 | public ParticleLifetimeExpression(ParticleEmitterScript expiration_expression, ParticleEmitterScript max_lifetime) { 24 | this.expiration_expression = expiration_expression; 25 | this.max_lifetime = max_lifetime; 26 | } 27 | 28 | public static ParticleLifetimeExpression parse(JsonObject asJsonObject) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 29 | if (asJsonObject == null) 30 | return new ParticleLifetimeExpression(ParticleEmitterScript.fromDouble(0), null); 31 | 32 | ParticleEmitterScript expiration_expression = null; 33 | ParticleEmitterScript max_lifetime = null; 34 | 35 | JsonElement expirationExpressionEl = asJsonObject.get("expiration_expression"); 36 | JsonElement maxLifetimeEl = asJsonObject.get("max_lifetime"); 37 | 38 | if (expirationExpressionEl == null && maxLifetimeEl == null) 39 | return new ParticleLifetimeExpression(ParticleEmitterScript.fromDouble(0), null); 40 | 41 | if (expirationExpressionEl != null) 42 | expiration_expression = ParticleEmitterScript.fromString(expirationExpressionEl.getAsString()); 43 | else max_lifetime = ParticleEmitterScript.fromString(maxLifetimeEl.getAsString()); 44 | 45 | return new ParticleLifetimeExpression(expiration_expression, max_lifetime); 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | return "ParticleLifetimeExpression{" + 51 | "expiration_expression=" + expiration_expression + 52 | ", max_lifetime=" + max_lifetime + 53 | '}'; 54 | } 55 | 56 | @Override 57 | public boolean isAlive(ParticleInterface i) { 58 | if (expiration_expression != null) { 59 | return expiration_expression.evaluate(i) != 0; 60 | } else { 61 | return i.particle_age() < max_lifetime.evaluate(i); 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/runtime/Particle.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.runtime; 2 | 3 | import net.minestom.server.coordinate.Point; 4 | import net.worldseed.particleemitter.emitters.EmitterShape; 5 | import net.worldseed.particleemitter.generator.ParticleGenerator; 6 | import net.hollowcube.mql.foreign.Query; 7 | import net.minestom.server.coordinate.Vec; 8 | import net.minestom.server.network.packet.server.play.ParticlePacket; 9 | import net.worldseed.particleemitter.particle.ParticleAppearanceTinting; 10 | import net.worldseed.particleemitter.particle.ParticleInitialSpeed; 11 | import net.worldseed.particleemitter.particle.ParticleLifetime; 12 | 13 | import java.lang.reflect.InvocationTargetException; 14 | import java.util.concurrent.ThreadLocalRandom; 15 | 16 | public class Particle extends ParticleInterface { 17 | private final ParticleEmitter emitter; 18 | private final ParticleAppearanceTinting particleColour; 19 | private final ParticleLifetime particleLifetime; 20 | private final ParticlePacket packet; 21 | private final net.minestom.server.particle.Particle type; 22 | private final ParticleInitialSpeed speed; 23 | 24 | double particle_age; 25 | 26 | final double particle_random_1; 27 | final double particle_random_2; 28 | final double particle_random_3; 29 | final double particle_random_4; 30 | 31 | private double emitter_age_offset = 0; 32 | 33 | @Query 34 | public double particle_age() { 35 | return particle_age; 36 | } 37 | 38 | @Query 39 | public double particle_random_1() { 40 | return particle_random_1; 41 | } 42 | 43 | @Query 44 | public double particle_random_2() { 45 | return particle_random_2; 46 | } 47 | 48 | @Query 49 | public double particle_random_3() { 50 | return particle_random_3; 51 | } 52 | 53 | @Query 54 | public double particle_random_4() { 55 | return particle_random_4; 56 | } 57 | 58 | @Override 59 | public int particle_count() { 60 | return emitter.particle_count(); 61 | } 62 | 63 | @Override 64 | public void reset() { 65 | emitter.reset(); 66 | particle_age = 0; 67 | } 68 | 69 | @Override 70 | public int updatesPerSecond() { 71 | return emitter.updatesPerSecond(); 72 | } 73 | 74 | @Query 75 | public double emitter_age() { 76 | return emitter.emitter_age() + emitter_age_offset; 77 | } 78 | 79 | @Query 80 | public double emitter_random_1() { 81 | return emitter.emitter_random_1(); 82 | } 83 | 84 | @Query 85 | public double emitter_random_2() { 86 | return emitter.emitter_random_2(); 87 | } 88 | 89 | @Query 90 | public double emitter_random_3() { 91 | return emitter.emitter_random_3(); 92 | } 93 | 94 | @Query 95 | public double emitter_random_4() { 96 | return emitter.emitter_random_4(); 97 | } 98 | 99 | public ParticlePacket getPacket() { 100 | return packet; 101 | } 102 | 103 | public Particle(net.minestom.server.particle.Particle type, EmitterShape shape, float yaw, Point offset, ParticleEmitter emitter, ParticleAppearanceTinting particleColour, ParticleLifetime particleLifetime, ParticleInitialSpeed particleSpeed) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 104 | this.particle_age = 0; 105 | 106 | this.particle_random_1 = Math.random(); 107 | this.particle_random_2 = Math.random(); 108 | this.particle_random_3 = Math.random(); 109 | this.particle_random_4 = Math.random(); 110 | 111 | this.emitter = emitter; 112 | this.type = type; 113 | 114 | this.particleColour = particleColour; 115 | this.particleLifetime = particleLifetime; 116 | this.speed = particleSpeed; 117 | 118 | Vec origin = rotateAroundOrigin(yaw, shape.emitPosition(this)); 119 | Vec position = origin.add(offset); 120 | 121 | if (type == net.minestom.server.particle.Particle.SOUL_FIRE_FLAME 122 | || type == net.minestom.server.particle.Particle.FLAME 123 | || type == net.minestom.server.particle.Particle.SMOKE 124 | || type == net.minestom.server.particle.Particle.FIREWORK) { 125 | Vec s = new Vec(speed.speedX().evaluate(this), speed.speedY().evaluate(this), speed.speedZ().evaluate(this)); 126 | Vec direction = shape.emitDirection(origin, this).mul(s); 127 | 128 | if (shape.canRotate()) { 129 | direction = rotateAroundOrigin(yaw, direction); 130 | } 131 | this.packet = draw(position, direction); 132 | } else { 133 | this.packet = draw(position, Vec.ZERO); 134 | } 135 | } 136 | 137 | private Vec rotateAroundOrigin(float yaw, Vec emitPosition) { 138 | return emitPosition.rotateAroundY(Math.toRadians(yaw)); 139 | } 140 | 141 | public ParticlePacket draw(Vec start, Vec direction) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 142 | ParticleAppearanceTinting.ColourEvaluated colour = particleColour.evaluate(this); 143 | 144 | if (type == net.minestom.server.particle.Particle.DUST_COLOR_TRANSITION) { 145 | int var17 = (int)(8.0D / (ThreadLocalRandom.current().nextDouble() * 0.8D + 0.2D)); 146 | double lifetimeSeconds = ((int)Math.max((float)var17, 1.0F)) * 1.0 / 20; 147 | 148 | double currentAge = this.particle_age; 149 | this.particle_age += lifetimeSeconds; 150 | this.emitter_age_offset = lifetimeSeconds; 151 | ParticleAppearanceTinting.ColourEvaluated colourAfter = particleColour.evaluate(this); 152 | this.particle_age = currentAge; 153 | this.emitter_age_offset = 0; 154 | 155 | return ParticleGenerator.buildParticle( 156 | type, 157 | start.x(), 158 | start.y(), 159 | start.z(), 160 | 1, 161 | direction.x(), 162 | direction.y(), 163 | direction.z(), 164 | colour.r(), 165 | colour.g(), 166 | colour.b(), 167 | colourAfter.r(), 168 | colourAfter.g(), 169 | colourAfter.b() 170 | ); 171 | } else { 172 | return ParticleGenerator.buildParticle( 173 | type, 174 | start.x(), 175 | start.y(), 176 | start.z(), 177 | 1, 178 | direction.x(), 179 | direction.y(), 180 | direction.z(), 181 | colour.r(), 182 | colour.g(), 183 | colour.b()); 184 | } 185 | } 186 | 187 | public void tick() { 188 | particle_age += 1.0 / updatesPerSecond(); 189 | } 190 | 191 | public boolean isAlive() { 192 | return particleLifetime.isAlive(this); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/runtime/ParticleEmitter.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.runtime; 2 | 3 | import net.worldseed.particleemitter.emitters.EmitterLifetime; 4 | import net.worldseed.particleemitter.emitters.EmitterRate; 5 | import net.worldseed.particleemitter.emitters.EmitterShape; 6 | import net.worldseed.particleemitter.emitters.init.EmitterInitialization; 7 | import net.worldseed.particleemitter.emitters.init.EmitterLocalSpace; 8 | import net.hollowcube.mql.foreign.Query; 9 | import net.minestom.server.coordinate.Point; 10 | import net.minestom.server.coordinate.Vec; 11 | import net.minestom.server.network.packet.server.play.ParticlePacket; 12 | import net.worldseed.particleemitter.particle.ParticleAppearanceTinting; 13 | import net.worldseed.particleemitter.particle.ParticleLifetime; 14 | import net.worldseed.particleemitter.particle.ParticleInitialSpeed; 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | import java.lang.reflect.InvocationTargetException; 18 | import java.util.Collection; 19 | import java.util.HashSet; 20 | import java.util.List; 21 | import java.util.Set; 22 | 23 | public class ParticleEmitter extends ParticleInterface { 24 | private final net.minestom.server.particle.Particle type; 25 | 26 | private Set particles = new HashSet<>(); 27 | private final int updatesPerSecond; 28 | 29 | private double emitter_age; 30 | private double emitter_random_1; 31 | private double emitter_random_2; 32 | private double emitter_random_3; 33 | private double emitter_random_4; 34 | 35 | private final EmitterLocalSpace local_space; 36 | private final EmitterInitialization initialization; 37 | 38 | private final EmitterLifetime lifetime; 39 | private final EmitterRate rate; 40 | private final EmitterShape shape; 41 | 42 | private final ParticleAppearanceTinting particleColour; 43 | private final ParticleInitialSpeed particleSpeed; 44 | private final ParticleLifetime particleLifetime; 45 | 46 | private EmitterLifetime.LifetimeState state = EmitterLifetime.LifetimeState.ALIVE; 47 | 48 | private Point offset = Vec.ZERO; 49 | private float yaw; 50 | 51 | @Query 52 | public int particle_count() { 53 | return particles.size(); 54 | } 55 | @Query 56 | public double emitter_age() { 57 | return emitter_age; 58 | } 59 | @Query 60 | public double emitter_random_1() { 61 | return emitter_random_1; 62 | } 63 | @Query 64 | public double emitter_random_2() { 65 | return emitter_random_2; 66 | } 67 | @Query 68 | public double emitter_random_3() { 69 | return emitter_random_3; 70 | } 71 | @Query 72 | public double emitter_random_4() { 73 | return emitter_random_4; 74 | } 75 | @Query 76 | public double particle_age() { 77 | return 0; 78 | } 79 | @Query 80 | public double particle_random_1() { 81 | return 0; 82 | } 83 | @Query 84 | public double particle_random_2() { 85 | return 0; 86 | } 87 | @Query 88 | public double particle_random_3() { 89 | return 0; 90 | } 91 | @Query 92 | public double particle_random_4() { 93 | return 0; 94 | } 95 | 96 | public void setPosition(Point offset) { 97 | this.offset = offset; 98 | } 99 | 100 | public void setRotation(float yaw) { 101 | this.yaw = yaw; 102 | } 103 | 104 | ParticleEmitter(net.minestom.server.particle.Particle type, int updatesPerSecond, EmitterInitialization initialization, EmitterLocalSpace local_space, 105 | EmitterLifetime lifetime, EmitterRate rate, EmitterShape shape, 106 | ParticleInitialSpeed particleSpeed, ParticleAppearanceTinting particleColour, ParticleLifetime particleLifetime) { 107 | this.emitter_age = 0; 108 | 109 | this.emitter_random_1 = Math.random(); 110 | this.emitter_random_2 = Math.random(); 111 | this.emitter_random_3 = Math.random(); 112 | this.emitter_random_4 = Math.random(); 113 | 114 | this.initialization = initialization; 115 | this.local_space = local_space; 116 | 117 | this.lifetime = lifetime; 118 | this.rate = rate; 119 | this.shape = shape; 120 | this.particleSpeed = particleSpeed; 121 | this.particleColour = particleColour; 122 | this.particleLifetime = particleLifetime; 123 | 124 | this.updatesPerSecond = updatesPerSecond; 125 | 126 | this.type = type; 127 | 128 | initialization.initialize(this); 129 | } 130 | 131 | public void reset() { 132 | emitter_age = 0; 133 | particles.clear(); 134 | initialization.initialize(this); 135 | 136 | emitter_random_1 = Math.random(); 137 | emitter_random_2 = Math.random(); 138 | emitter_random_3 = Math.random(); 139 | emitter_random_4 = Math.random(); 140 | } 141 | 142 | @Override 143 | public int updatesPerSecond() { 144 | return updatesPerSecond; 145 | } 146 | 147 | public @NotNull Collection tick() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 148 | emitter_age += 1.0/updatesPerSecond; 149 | 150 | initialization.update(this); 151 | // particles.forEach(Particle::tick); 152 | // particles = particles.stream().filter(particle -> particle.isAlive()).collect(Collectors.toSet()); 153 | 154 | this.state = lifetime.getState(this); 155 | if (state == EmitterLifetime.LifetimeState.DEAD || state == EmitterLifetime.LifetimeState.INACTIVE) { 156 | return List.of(); 157 | } 158 | 159 | boolean canCreateParticle = rate.canEmit(this); 160 | 161 | if (canCreateParticle) { 162 | Particle particle = new Particle(type, shape, yaw, offset, this, particleColour, particleLifetime, particleSpeed); 163 | // particles.add(particle); 164 | return List.of(particle.getPacket()); 165 | } 166 | 167 | return List.of(); 168 | } 169 | 170 | @Override 171 | public String toString() { 172 | return "ParticleEmitter{" + 173 | "emitter_age=" + emitter_age + 174 | ", emitter_random1=" + emitter_random_1 + 175 | ", emitter_random2=" + emitter_random_2 + 176 | ", emitter_random3=" + emitter_random_3 + 177 | ", emitter_random4=" + emitter_random_4 + 178 | ", local_space=" + local_space + 179 | ", initialization=" + initialization + 180 | ", lifetime=" + lifetime + 181 | ", rate=" + rate + 182 | ", shape=" + shape + 183 | ", particleColour=" + particleColour + 184 | ", particleSpeed=" + particleSpeed + 185 | '}'; 186 | } 187 | 188 | public EmitterLifetime.LifetimeState status() { 189 | return state; 190 | } 191 | 192 | @Override 193 | public ParticleEmitter clone() { 194 | return new ParticleEmitter(type, updatesPerSecond, initialization, local_space, lifetime, rate, shape, particleSpeed, particleColour, particleLifetime); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/runtime/ParticleEmitterScript.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.runtime; 2 | 3 | import net.hollowcube.mql.jit.MqlCompiler; 4 | import net.hollowcube.mql.jit.MqlEnv; 5 | 6 | import java.lang.reflect.InvocationTargetException; 7 | 8 | public interface ParticleEmitterScript { 9 | double evaluate(@MqlEnv({"variable", "v"}) ParticleInterface particle); 10 | 11 | static ParticleEmitterScript fromDouble(double value) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 12 | return fromString(Double.toString(value)); 13 | } 14 | 15 | static ParticleEmitterScript fromString(String s) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { 16 | if (s == null) return fromDouble(0); 17 | MqlCompiler compiler = new MqlCompiler<>(ParticleEmitterScript.class); 18 | Class scriptClass = compiler.compile(s.replace("Math", "math")); 19 | return scriptClass.getDeclaredConstructor().newInstance(); 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/runtime/ParticleInterface.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.runtime; 2 | 3 | import net.hollowcube.mql.foreign.Query; 4 | 5 | public abstract class ParticleInterface { 6 | @Query 7 | abstract public double emitter_age(); 8 | @Query 9 | abstract public double emitter_random_1(); 10 | @Query 11 | abstract public double emitter_random_2(); 12 | @Query 13 | abstract public double emitter_random_3(); 14 | @Query 15 | abstract public double emitter_random_4(); 16 | @Query 17 | abstract public double particle_age(); 18 | @Query 19 | abstract public double particle_random_1(); 20 | @Query 21 | abstract public double particle_random_2(); 22 | @Query 23 | abstract public double particle_random_3(); 24 | @Query 25 | abstract public double particle_random_4(); 26 | @Query 27 | abstract public int particle_count(); 28 | 29 | public abstract void reset(); 30 | public abstract int updatesPerSecond(); 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/net/worldseed/particleemitter/runtime/ParticleParser.java: -------------------------------------------------------------------------------- 1 | package net.worldseed.particleemitter.runtime; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonObject; 5 | import net.hollowcube.mql.parser.MqlParseError; 6 | import net.worldseed.particleemitter.emitters.EmitterLifetime; 7 | import net.worldseed.particleemitter.emitters.EmitterRate; 8 | import net.worldseed.particleemitter.emitters.EmitterShape; 9 | import net.worldseed.particleemitter.emitters.init.EmitterInitialization; 10 | import net.worldseed.particleemitter.emitters.init.EmitterLocalSpace; 11 | import net.worldseed.particleemitter.emitters.lifetime.EmitterLifetimeExpression; 12 | import net.worldseed.particleemitter.emitters.lifetime.EmitterLifetimeLooping; 13 | import net.worldseed.particleemitter.emitters.lifetime.EmitterLifetimeOnce; 14 | import net.worldseed.particleemitter.emitters.rate.EmitterRateInstant; 15 | import net.worldseed.particleemitter.emitters.rate.EmitterRateSteady; 16 | import net.worldseed.particleemitter.emitters.shape.*; 17 | import net.worldseed.particleemitter.particle.ParticleAppearanceTinting; 18 | import net.worldseed.particleemitter.particle.ParticleInitialSpeed; 19 | import net.worldseed.particleemitter.particle.ParticleLifetime; 20 | import net.worldseed.particleemitter.particle.ParticleLifetimeExpression; 21 | 22 | import java.lang.reflect.InvocationTargetException; 23 | 24 | public class ParticleParser { 25 | /** 26 | * Parses a particle emitter from a JSON object. 27 | * @param updatesPerSecond The number of times per second you will call the tick function. 28 | * @param description The JSON object describing the particle emitter. 29 | * @return The particle emitter. 30 | */ 31 | public static ParticleEmitter parse(net.minestom.server.particle.Particle type, int updatesPerSecond, JsonObject description) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 32 | String version = description.get("format_version").getAsString(); 33 | JsonObject particleEffect = description.get("particle_effect").getAsJsonObject(); 34 | JsonObject components = particleEffect.get("components").getAsJsonObject(); 35 | 36 | JsonElement emitterLocalSpace = components.get("minecraft:emitter_local_space"); 37 | JsonElement emitterInitialization = components.get("minecraft:emitter_initialization"); 38 | 39 | JsonElement emitterRateInstant = components.get("minecraft:emitter_rate_instant"); 40 | JsonElement emitterRateSteady = components.get("minecraft:emitter_rate_steady"); 41 | 42 | EmitterRate rate; 43 | if (emitterRateInstant != null) { 44 | try { 45 | rate = EmitterRateInstant.parse(emitterRateInstant.getAsJsonObject()); 46 | } catch (MqlParseError mqlParseError) { 47 | throw new MqlParseError("Failed to parse emitter_rate_instant: " + mqlParseError.getMessage()); 48 | } 49 | } else if (emitterRateSteady != null) { 50 | try { 51 | rate = EmitterRateSteady.parse(emitterRateSteady.getAsJsonObject()); 52 | } catch (MqlParseError mqlParseError) { 53 | throw new MqlParseError("Failed to parse emitter_rate_steady: " + mqlParseError.getMessage()); 54 | } 55 | } else { 56 | rate = new EmitterRateInstant(ParticleEmitterScript.fromDouble(100)); 57 | } 58 | 59 | JsonElement emitterLifetimeLooping = components.get("minecraft:emitter_lifetime_looping"); 60 | JsonElement emitterLifetimeOnce = components.get("minecraft:emitter_lifetime_once"); 61 | JsonElement emitterLifetimeExpression = components.get("minecraft:emitter_lifetime_expression"); 62 | 63 | EmitterLifetime lifetime; 64 | if (emitterLifetimeLooping != null) { 65 | try { 66 | lifetime = EmitterLifetimeLooping.parse(emitterLifetimeLooping.getAsJsonObject()); 67 | } catch (MqlParseError mqlParseError) { 68 | throw new MqlParseError("Failed to parse emitter_lifetime_looping: " + mqlParseError.getMessage()); 69 | } 70 | } else if (emitterLifetimeOnce != null) { 71 | try { 72 | lifetime = EmitterLifetimeOnce.parse(emitterLifetimeOnce.getAsJsonObject()); 73 | } catch (MqlParseError mqlParseError) { 74 | throw new MqlParseError("Failed to parse emitter_lifetime_once: " + mqlParseError.getMessage()); 75 | } 76 | } else if (emitterLifetimeExpression != null) { 77 | try { 78 | lifetime = EmitterLifetimeExpression.parse(emitterLifetimeExpression.getAsJsonObject()); 79 | } catch (MqlParseError mqlParseError) { 80 | throw new MqlParseError("Failed to parse emitter_lifetime_expression: " + mqlParseError.getMessage()); 81 | } 82 | } else { 83 | try { 84 | lifetime = new EmitterLifetimeOnce(ParticleEmitterScript.fromDouble(100)); 85 | } catch (MqlParseError mqlParseError) { 86 | throw new MqlParseError("Failed to parse emitter_lifetime_once: " + mqlParseError.getMessage()); 87 | } 88 | } 89 | 90 | JsonElement emitterShapePoint = components.get("minecraft:emitter_shape_point"); 91 | JsonElement emitterShapeSphere = components.get("minecraft:emitter_shape_sphere"); 92 | JsonElement emitterShapeBox = components.get("minecraft:emitter_shape_box"); 93 | JsonElement emitterShapeCustom = components.get("minecraft:emitter_shape_custom"); 94 | JsonElement emitterShapeEntityAABB = components.get("minecraft:emitter_shape_entity_aabb"); 95 | JsonElement emitterShapeDisc = components.get("minecraft:emitter_shape_disc"); 96 | 97 | EmitterShape shape; 98 | if (emitterShapePoint != null) { 99 | try { 100 | shape = EmitterShapePoint.parse(emitterShapePoint.getAsJsonObject()); 101 | } catch (MqlParseError mqlParseError) { 102 | throw new MqlParseError("Failed to parse emitter_shape_point: " + mqlParseError.getMessage()); 103 | } 104 | } else if (emitterShapeSphere != null) { 105 | try { 106 | shape = EmitterShapeSphere.parse(emitterShapeSphere.getAsJsonObject()); 107 | } catch (MqlParseError mqlParseError) { 108 | throw new MqlParseError("Failed to parse emitter_shape_sphere: " + mqlParseError.getMessage()); 109 | } 110 | } else if (emitterShapeBox != null) { 111 | try { 112 | shape = EmitterShapeBox.parse(emitterShapeBox.getAsJsonObject()); 113 | } catch (MqlParseError mqlParseError) { 114 | throw new MqlParseError("Failed to parse emitter_shape_box: " + mqlParseError.getMessage()); 115 | } 116 | } else if (emitterShapeCustom != null) { 117 | try { 118 | shape = EmitterShapePoint.parse(emitterShapeCustom.getAsJsonObject()); 119 | } catch (MqlParseError mqlParseError) { 120 | throw new MqlParseError("Failed to parse emitter_shape_custom: " + mqlParseError.getMessage()); 121 | } 122 | } else if (emitterShapeEntityAABB != null) { 123 | try { 124 | shape = EmitterShapeEntityAABB.parse(emitterShapeEntityAABB.getAsJsonObject()); 125 | } catch (MqlParseError mqlParseError) { 126 | throw new MqlParseError("Failed to parse emitter_shape_entity_aabb: " + mqlParseError.getMessage()); 127 | } 128 | } else if (emitterShapeDisc != null) { 129 | try { 130 | shape = EmitterShapeDisc.parse(emitterShapeDisc.getAsJsonObject()); 131 | } catch (MqlParseError mqlParseError) { 132 | throw new MqlParseError("Failed to parse emitter_shape_disc: " + mqlParseError.getMessage()); 133 | } 134 | } else { 135 | shape = new EmitterShapePoint(); 136 | } 137 | 138 | JsonElement particleInitialSpeed = components.get("minecraft:particle_initial_speed"); 139 | JsonObject particleAppearanceTinting = components.getAsJsonObject("minecraft:particle_appearance_tinting"); 140 | JsonObject particleLifetimeExpression = components.getAsJsonObject("minecraft:particle_lifetime_expression"); 141 | 142 | ParticleLifetime parsedParticleLifetimeExpression; 143 | ParticleAppearanceTinting parsedParticleAppearanceTinting; 144 | EmitterInitialization parsedEmitterInitialization; 145 | EmitterLocalSpace parsedEmitterLocalSpace; 146 | ParticleInitialSpeed parsedParticleInitialSpeed; 147 | 148 | try { 149 | parsedParticleLifetimeExpression = 150 | particleLifetimeExpression != null 151 | ? ParticleLifetimeExpression.parse(particleLifetimeExpression) 152 | : ParticleLifetimeExpression.DEFAULT; 153 | } catch (Exception e) { 154 | throw new MqlParseError("Failed to parse particle_lifetime_expression: " + e.getMessage()); 155 | } 156 | 157 | try { 158 | parsedParticleAppearanceTinting = 159 | particleAppearanceTinting != null 160 | ? ParticleAppearanceTinting.parse(particleAppearanceTinting) 161 | : ParticleAppearanceTinting.DEFAULT; 162 | } catch (Exception e) { 163 | throw new MqlParseError("Failed to parse particle_appearance_tinting: " + e.getMessage()); 164 | } 165 | 166 | try { 167 | parsedEmitterInitialization = EmitterInitialization.parse(emitterInitialization); 168 | } catch (Exception e) { 169 | throw new MqlParseError("Failed to parse emitter_initialization: " + e.getMessage()); 170 | } 171 | 172 | try { 173 | parsedEmitterLocalSpace = EmitterLocalSpace.parse(emitterLocalSpace); 174 | } catch (Exception e) { 175 | throw new MqlParseError("Failed to parse emitter_local_space: " + e.getMessage()); 176 | } 177 | 178 | try { 179 | parsedParticleInitialSpeed = ParticleInitialSpeed.parse(particleInitialSpeed); 180 | } catch (Exception e) { 181 | throw new MqlParseError("Failed to parse particle_initial_speed: " + e.getMessage()); 182 | } 183 | 184 | return new ParticleEmitter( 185 | type, 186 | updatesPerSecond, 187 | parsedEmitterInitialization, 188 | parsedEmitterLocalSpace, 189 | lifetime, rate, shape, 190 | parsedParticleInitialSpeed, 191 | parsedParticleAppearanceTinting, 192 | parsedParticleLifetimeExpression 193 | ); 194 | } 195 | 196 | public static ParticleEmitter parse(int updatesPerSecond, JsonObject description) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 197 | return parse(net.minestom.server.particle.Particle.DUST, updatesPerSecond, description); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/test/java/Demo.java: -------------------------------------------------------------------------------- 1 | import com.google.gson.Gson; 2 | import com.google.gson.GsonBuilder; 3 | import com.google.gson.JsonObject; 4 | import com.google.gson.stream.JsonReader; 5 | import net.minestom.server.event.player.AsyncPlayerConfigurationEvent; 6 | import net.minestom.server.instance.LightingChunk; 7 | import net.minestom.server.particle.Particle; 8 | import net.worldseed.particleemitter.emitters.EmitterLifetime; 9 | import net.minestom.server.MinecraftServer; 10 | import net.minestom.server.coordinate.Pos; 11 | import net.minestom.server.coordinate.Vec; 12 | import net.minestom.server.entity.GameMode; 13 | import net.minestom.server.entity.Player; 14 | import net.minestom.server.event.GlobalEventHandler; 15 | import net.minestom.server.instance.InstanceContainer; 16 | import net.minestom.server.instance.InstanceManager; 17 | import net.minestom.server.instance.block.Block; 18 | import net.minestom.server.network.packet.server.play.ParticlePacket; 19 | import net.minestom.server.timer.ExecutionType; 20 | import net.minestom.server.timer.TaskSchedule; 21 | import net.worldseed.particleemitter.runtime.ParticleEmitter; 22 | import net.worldseed.particleemitter.runtime.ParticleParser; 23 | 24 | import java.io.*; 25 | import java.lang.reflect.InvocationTargetException; 26 | import java.util.*; 27 | 28 | public class Demo { 29 | static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); 30 | 31 | public static void main(String[] args) throws FileNotFoundException, UnsupportedEncodingException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 32 | // Initialization 33 | MinecraftServer minecraftServer = MinecraftServer.init(); 34 | 35 | InstanceManager instanceManager = MinecraftServer.getInstanceManager(); 36 | InstanceContainer instanceContainer = instanceManager.createInstanceContainer(); 37 | instanceContainer.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE)); 38 | instanceContainer.setChunkSupplier(LightingChunk::new); 39 | 40 | File file = new File("./src/test/resources/particles/rgb.particle.json"); 41 | FileInputStream fis = new FileInputStream(file); 42 | JsonReader reader = new JsonReader(new InputStreamReader(fis, "UTF-8")); 43 | JsonObject map = GSON.fromJson(reader, JsonObject.class); 44 | 45 | List emitters = new ArrayList<>(); 46 | 47 | { 48 | var emitter = ParticleParser.parse(Particle.DUST_COLOR_TRANSITION, 1000, map); 49 | emitters.add(emitter); 50 | } 51 | 52 | // Add an event callback to specify the spawning instance (and the spawn position) 53 | GlobalEventHandler globalEventHandler = MinecraftServer.getGlobalEventHandler(); 54 | globalEventHandler.addListener(AsyncPlayerConfigurationEvent.class, event -> { 55 | final Player player = event.getPlayer(); 56 | player.setPermissionLevel(2); 57 | player.setGameMode(GameMode.CREATIVE); 58 | event.setSpawningInstance(instanceContainer); 59 | player.setRespawnPoint(new Pos(0, 42, 0)); 60 | 61 | for (var emitter : emitters) { 62 | emitter.setPosition(new Vec(0, 60, 0)); 63 | } 64 | }); 65 | 66 | new Timer().schedule(new TimerTask() { 67 | public void run() { 68 | try { 69 | for (var emitter : emitters) { 70 | Collection packets = emitter.tick(); 71 | 72 | if (emitter.status() != EmitterLifetime.LifetimeState.DEAD) { 73 | packets.forEach(packet -> { 74 | instanceContainer.getPlayers().forEach(p -> p.sendPackets(packet)); 75 | }); 76 | } else { 77 | emitter.reset(); 78 | } 79 | } 80 | } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | 81 | IllegalAccessException e) { 82 | throw new RuntimeException(e); 83 | } 84 | } 85 | }, 1, 1); 86 | 87 | // Start the server on port 25565 88 | minecraftServer.start("0.0.0.0", 25565); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/ParticleManagerDemo.java: -------------------------------------------------------------------------------- 1 | import com.google.gson.Gson; 2 | import com.google.gson.GsonBuilder; 3 | import com.google.gson.JsonObject; 4 | import com.google.gson.stream.JsonReader; 5 | import net.minestom.server.coordinate.Vec; 6 | import net.minestom.server.instance.InstanceContainer; 7 | import net.minestom.server.network.packet.server.play.ParticlePacket; 8 | import net.minestom.server.particle.Particle; 9 | import net.worldseed.particleemitter.emitters.EmitterLifetime; 10 | import net.worldseed.particleemitter.runtime.ParticleEmitter; 11 | import net.worldseed.particleemitter.runtime.ParticleParser; 12 | 13 | import java.io.*; 14 | import java.lang.reflect.InvocationTargetException; 15 | import java.util.*; 16 | 17 | public class ParticleManager { 18 | static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); 19 | 20 | public static List getParticleEmitters(String particleName,int amount) throws FileNotFoundException, UnsupportedEncodingException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException{ 21 | File file = new File("particles/"+particleName); 22 | FileInputStream fis = new FileInputStream(file); 23 | JsonReader reader = new JsonReader(new InputStreamReader(fis, "UTF-8")); 24 | JsonObject map = GSON.fromJson(reader, JsonObject.class); 25 | 26 | List emitters = new ArrayList<>(); 27 | { 28 | var emitter = ParticleParser.parse(Particle.DUST_COLOR_TRANSITION, 1000*amount, map); 29 | emitters.add(emitter); 30 | } 31 | 32 | return emitters; 33 | } 34 | 35 | public static void playParticle(String particleName, Vec position, int amount, InstanceContainer instance, boolean playInstantlyOnce) throws FileNotFoundException, UnsupportedEncodingException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 36 | List emitters = getParticleEmitters(particleName,amount); 37 | for (var emitter : emitters) { 38 | emitter.setPosition(position); 39 | } 40 | 41 | if (playInstantlyOnce){ 42 | try { 43 | for (var emitter : emitters) { 44 | emitter.reset(); 45 | Collection packets = new ArrayList<>(); 46 | for (int i = 0; i < amount; i++) { 47 | packets.addAll(emitter.tick()); 48 | } 49 | if (emitter.status() != EmitterLifetime.LifetimeState.DEAD) { 50 | packets.forEach(packet -> { 51 | instance.getPlayers().forEach(p -> p.sendPackets(packet)); 52 | }); 53 | } else { 54 | emitter.reset(); 55 | } 56 | emitter.reset(); 57 | } 58 | } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | 59 | IllegalAccessException e) { 60 | throw new RuntimeException(e); 61 | } 62 | } else { 63 | new Timer().schedule(new TimerTask() { 64 | public void run() { 65 | try { 66 | for (var emitter : emitters) { 67 | for (int i = 0; i < amount; i++) { 68 | Collection packets = emitter.tick(); 69 | if (emitter.status() != EmitterLifetime.LifetimeState.DEAD) { 70 | packets.forEach(packet -> { 71 | instance.getPlayers().forEach(p -> p.sendPackets(packet)); 72 | }); 73 | } else { 74 | emitter.reset(); 75 | this.cancel(); 76 | } 77 | } 78 | } 79 | } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | 80 | IllegalAccessException e) { 81 | throw new RuntimeException(e); 82 | } 83 | } 84 | }, 1, 1); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/ParticleParserTest.java: -------------------------------------------------------------------------------- 1 | import com.google.gson.Gson; 2 | import com.google.gson.GsonBuilder; 3 | import com.google.gson.JsonObject; 4 | import com.google.gson.stream.JsonReader; 5 | import net.minestom.server.particle.Particle; 6 | import org.junit.jupiter.api.Test; 7 | import net.worldseed.particleemitter.runtime.ParticleParser; 8 | import net.worldseed.particleemitter.runtime.ParticleEmitter; 9 | 10 | import java.io.File; 11 | import java.io.FileInputStream; 12 | import java.io.IOException; 13 | import java.io.InputStreamReader; 14 | import java.lang.reflect.InvocationTargetException; 15 | 16 | public class ParticleParserTest { 17 | static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); 18 | static final String root = "./src/test/resources/particles/"; 19 | 20 | @Test 21 | public void parseMagic() throws IOException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 22 | File file = new File(root + "magic.particle.json"); 23 | FileInputStream fis = new FileInputStream(file); 24 | JsonReader reader = new JsonReader(new InputStreamReader(fis, "UTF-8")); 25 | JsonObject map = GSON.fromJson(reader, JsonObject.class); 26 | ParticleEmitter emitter = ParticleParser.parse(Particle.DUST, 1000, map); 27 | } 28 | 29 | @Test 30 | public void parseLoading() throws IOException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 31 | File file = new File(root + "loading.particle.json"); 32 | FileInputStream fis = new FileInputStream(file); 33 | JsonReader reader = new JsonReader(new InputStreamReader(fis, "UTF-8")); 34 | JsonObject map = GSON.fromJson(reader, JsonObject.class); 35 | ParticleEmitter emitter = ParticleParser.parse(Particle.DUST, 1000, map); 36 | } 37 | 38 | @Test 39 | public void parseError() throws IOException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 40 | File file = new File(root + "error.particle.json"); 41 | FileInputStream fis = new FileInputStream(file); 42 | JsonReader reader = new JsonReader(new InputStreamReader(fis, "UTF-8")); 43 | JsonObject map = GSON.fromJson(reader, JsonObject.class); 44 | ParticleEmitter emitter = ParticleParser.parse(Particle.DUST, 1000, map); 45 | } 46 | } -------------------------------------------------------------------------------- /src/test/resources/particles/colour_expression.particle.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.10.0", 3 | "particle_effect": { 4 | "description": { 5 | "identifier": "snowstorm:loading", 6 | "basic_render_parameters": { 7 | "material": "particles_alpha", 8 | "texture": "textures/particle/particles" 9 | } 10 | }, 11 | "components": { 12 | "minecraft:emitter_local_space": { 13 | "position": true, 14 | "rotation": true 15 | }, 16 | "minecraft:emitter_rate_steady": { 17 | "spawn_rate": 300, 18 | "max_particles": 60 19 | }, 20 | "minecraft:emitter_lifetime_looping": { 21 | "active_time": 6 22 | }, 23 | "minecraft:emitter_shape_point": { 24 | "offset": ["(math.sin(variable.emitter_age * 60) < 0) ? 3*math.sin(variable.emitter_age*60) : 0", "(math.cos(variable.emitter_age * 60) > 0) ? 3*math.cos(variable.emitter_age*60) : 0", "(math.sin(variable.emitter_age * 60) < 0) ? 3*math.sin(variable.emitter_age*60) : 0"] 25 | }, 26 | "minecraft:particle_lifetime_expression": { 27 | "max_lifetime": 1 28 | }, 29 | "minecraft:particle_initial_speed": 0, 30 | "minecraft:particle_motion_dynamic": {}, 31 | "minecraft:particle_appearance_billboard": { 32 | "size": ["variable.size*(1-variable.particle_age)", "variable.size*(1-variable.particle_age)"], 33 | "facing_camera_mode": "rotate_xyz", 34 | "uv": { 35 | "texture_width": 128, 36 | "texture_height": 128, 37 | "uv": [32, 88], 38 | "uv_size": [8, 8] 39 | } 40 | }, 41 | "minecraft:particle_appearance_tinting": { 42 | "color": ["Math.clamp((Math.cos(variable.emitter_age * 60) + 1) / 2, 0, 1)", "Math.clamp((1-Math.sin(variable.emitter_age * 60) + 1) / 2, 0, 1)", "Math.clamp((Math.sin(variable.emitter_age * 60) + 1) / 2, 0, 1)", 1] 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/test/resources/particles/cool.particle.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.10.0", 3 | "particle_effect": { 4 | "description": { 5 | "identifier": "snowstorm:magic", 6 | "basic_render_parameters": { 7 | "material": "particles_alpha", 8 | "texture": "textures/particle/particles" 9 | } 10 | }, 11 | "components": { 12 | "minecraft:emitter_local_space": { 13 | "position": true, 14 | "rotation": true 15 | }, 16 | "minecraft:emitter_rate_steady": { 17 | "spawn_rate": 2000, 18 | "max_particles": 10000 19 | }, 20 | "minecraft:emitter_lifetime_expression": { 21 | "activation_expression": 1 22 | }, 23 | "minecraft:emitter_shape_point": { 24 | "offset": [ 25 | "math.cos(variable.emitter_age * 360) * 5 * math.random(1, 2)", 26 | "math.cos(variable.emitter_age * 180) * 3 * math.sin(variable.emitter_age * 360)", 27 | "math.sin(variable.emitter_age * 360) * 5 * math.random(1, 1.3)" 28 | ] 29 | }, 30 | "minecraft:particle_lifetime_expression": { 31 | "max_lifetime": "math.random(0.5, 0.7)" 32 | }, 33 | "minecraft:particle_motion_parametric": {}, 34 | "minecraft:particle_appearance_billboard": { 35 | "size": [0.2, 0.2], 36 | "facing_camera_mode": "rotate_xyz", 37 | "uv": { 38 | "texture_width": 128, 39 | "texture_height": 128, 40 | "flipbook": { 41 | "base_UV": [64, 96], 42 | "size_UV": [8, 8], 43 | "step_UV": [-8, 0], 44 | "max_frame": 8, 45 | "stretch_to_lifetime": true 46 | } 47 | } 48 | }, 49 | "minecraft:particle_appearance_tinting": { 50 | "color": ["Math.clamp(0.3 + variable.particle_random_4/7 + (variable.particle_random_3>0.2 ? 0.4 : 0), 0, 1)", "Math.clamp(0.2+variable.particle_random_4/5, 0, 1)", "Math.clamp(0.88 + variable.particle_random_4/8, 0, 1)", 1] 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/test/resources/particles/disc.particle.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.10.0", 3 | "particle_effect": { 4 | "description": { 5 | "identifier": "snowstorm:magic", 6 | "basic_render_parameters": { 7 | "material": "particles_alpha", 8 | "texture": "textures/particle/particles" 9 | } 10 | }, 11 | "components": { 12 | "minecraft:emitter_local_space": { 13 | "position": true, 14 | "rotation": true 15 | }, 16 | "minecraft:emitter_rate_steady": { 17 | "spawn_rate": 500, 18 | "max_particles": 1000 19 | }, 20 | "minecraft:emitter_lifetime_expression": { 21 | "activation_expression": 1 22 | }, 23 | "minecraft:emitter_shape_disc": { 24 | "offset": ["math.sin(variable.emitter_age*200)*6", "math.cos(variable.emitter_age*260)*6", 5], 25 | "radius": 1.6, 26 | "plane_normal": ["math.sin(variable.emitter_age*200)*6", 0, 1], 27 | "surface_only": true, 28 | "direction": "outwards" 29 | }, 30 | "minecraft:particle_lifetime_expression": { 31 | "max_lifetime": "math.random(3, 4)" 32 | }, 33 | "minecraft:particle_initial_spin": { 34 | "rotation_rate": "Math.random(-100, 100)" 35 | }, 36 | "minecraft:particle_initial_speed": 0, 37 | "minecraft:particle_motion_dynamic": { 38 | "linear_acceleration": ["math.random(0, 4)", "math.random(0, 8)", "variable.particle_random_3>0.2 ? -10 : -4"] 39 | }, 40 | "minecraft:particle_appearance_billboard": { 41 | "size": ["0.04+variable.particle_random_2/5", "0.04+variable.particle_random_2/5"], 42 | "facing_camera_mode": "rotate_xyz", 43 | "uv": { 44 | "texture_width": 128, 45 | "texture_height": 128, 46 | "flipbook": { 47 | "base_UV": [64, 96], 48 | "size_UV": [8, 8], 49 | "step_UV": [-8, 0], 50 | "max_frame": 8, 51 | "stretch_to_lifetime": true 52 | } 53 | } 54 | }, 55 | "minecraft:particle_appearance_tinting": { 56 | "color": ["Math.clamp(0.3 + variable.particle_random_4/7 + (variable.particle_random_3>0.2 ? 0.4 : 0), 0, 1)", "Math.clamp(0.2+variable.particle_random_4/5, 0, 1)", "Math.clamp(0.88 + variable.particle_random_4/8, 0, 1)", 1] 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/test/resources/particles/error.particle.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.10.0", 3 | "particle_effect": { 4 | "description": { 5 | "identifier": "snowstorm:fire", 6 | "basic_render_parameters": { 7 | "material": "particles_alpha", 8 | "texture": "textures/particle/flame_atlas" 9 | } 10 | }, 11 | "components": { 12 | "minecraft:emitter_rate_steady": { 13 | "spawn_rate": 20, 14 | "max_particles": 1000 15 | }, 16 | "minecraft:emitter_lifetime_looping": { 17 | "active_time": 1 18 | }, 19 | "minecraft:emitter_shape_disc": { 20 | "offset": [0, 0.4, 0], 21 | "radius": 1.2, 22 | "direction": "outwards" 23 | }, 24 | "minecraft:particle_lifetime_expression": { 25 | "max_lifetime": "Mth.random(1, 1.4)" 26 | }, 27 | "minecraft:particle_initial_speed": 1, 28 | "minecraft:particle_motion_dynamic": { 29 | "linear_acceleration": ["(variable.particle_random_1-0.5)", 1.2, "(variable.particle_random_1-0.5)"] 30 | }, 31 | "minecraft:particle_appearance_billboard": { 32 | "size": [0.4, 0.4], 33 | "facing_camera_mode": "lookat_xyz", 34 | "uv": { 35 | "texture_width": 16, 36 | "texture_height": 512, 37 | "flipbook": { 38 | "base_UV": [0, 0], 39 | "size_UV": [16, 16], 40 | "step_UV": [0, 16], 41 | "max_frame": 32, 42 | "stretch_to_lifetime": true 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/test/resources/particles/flame.particle.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.10.0", 3 | "particle_effect": { 4 | "description": { 5 | "identifier": "snowstorm:loading", 6 | "basic_render_parameters": { 7 | "material": "particles_alpha", 8 | "texture": "textures/particle/particles" 9 | } 10 | }, 11 | "components": { 12 | "minecraft:emitter_local_space": { 13 | "position": true, 14 | "rotation": true 15 | }, 16 | "minecraft:emitter_rate_steady": { 17 | "spawn_rate": 1000, 18 | "max_particles": 60000 19 | }, 20 | "minecraft:emitter_lifetime_expression": { 21 | "activation_expression": 1 22 | }, 23 | "minecraft:emitter_shape_point": { 24 | "offset": [0, "math.sin(variable.particle_random_1 * 360) * 5", "math.cos(variable.particle_random_1 * 360) * 5"], 25 | "direction": [0, "-math.cos(variable.particle_random_1 * 360) * 5", "math.sin(variable.particle_random_1 * 360) * 5"] 26 | }, 27 | "minecraft:particle_lifetime_expression": { 28 | "max_lifetime": 1 29 | }, 30 | "minecraft:particle_initial_speed": 0.3, 31 | "minecraft:particle_motion_dynamic": {}, 32 | "minecraft:particle_appearance_billboard": { 33 | "size": ["variable.size*(1-variable.particle_age)", "variable.size*(1-variable.particle_age)"], 34 | "facing_camera_mode": "rotate_xyz", 35 | "uv": { 36 | "texture_width": 128, 37 | "texture_height": 128, 38 | "uv": [32, 88], 39 | "uv_size": [8, 8] 40 | } 41 | }, 42 | "minecraft:particle_appearance_tinting": { 43 | "color": [1, 0, 0, 1] 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/test/resources/particles/loading.particle.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.10.0", 3 | "particle_effect": { 4 | "description": { 5 | "identifier": "snowstorm:loading", 6 | "basic_render_parameters": { 7 | "material": "particles_alpha", 8 | "texture": "textures/particle/particles" 9 | } 10 | }, 11 | "components": { 12 | "minecraft:emitter_initialization": { 13 | }, 14 | "minecraft:emitter_local_space": { 15 | "position": true, 16 | "rotation": true 17 | }, 18 | "minecraft:emitter_rate_steady": { 19 | "spawn_rate": 31, 20 | "max_particles": 60 21 | }, 22 | "minecraft:emitter_lifetime_looping": { 23 | "active_time": 1 24 | }, 25 | "minecraft:emitter_shape_point": { 26 | "offset": ["3*-math.sin(variable.emitter_age*360)", "3*math.cos(variable.emitter_age*360)", 0] 27 | }, 28 | "minecraft:particle_lifetime_expression": { 29 | "max_lifetime": 1 30 | }, 31 | "minecraft:particle_initial_speed": 0, 32 | "minecraft:particle_motion_dynamic": {}, 33 | "minecraft:particle_appearance_billboard": { 34 | "size": ["0.8*(1-variable.particle_age)", "0.8*(1-variable.particle_age)"], 35 | "facing_camera_mode": "rotate_xyz", 36 | "uv": { 37 | "texture_width": 128, 38 | "texture_height": 128, 39 | "uv": [32, 88], 40 | "uv_size": [8, 8] 41 | } 42 | }, 43 | "minecraft:particle_appearance_tinting": { 44 | "color": [0.41961, 1, 0.57647, 1] 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/test/resources/particles/looping.particle.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.10.0", 3 | "particle_effect": { 4 | "description": { 5 | "identifier": "snowstorm:loading", 6 | "basic_render_parameters": { 7 | "material": "particles_alpha", 8 | "texture": "textures/particle/particles" 9 | } 10 | }, 11 | "components": { 12 | "minecraft:emitter_local_space": { 13 | "position": true, 14 | "rotation": true 15 | }, 16 | "minecraft:emitter_rate_steady": { 17 | "spawn_rate": 100, 18 | "max_particles": 1000 19 | }, 20 | "minecraft:emitter_lifetime_looping": { 21 | "active_time": 2, 22 | "sleep_time": 1 23 | }, 24 | "minecraft:emitter_shape_point": { 25 | "offset": ["math.cos(variable.emitter_age * 80) * 3", "math.sin(variable.emitter_age * 80) * 3", 0] 26 | }, 27 | "minecraft:particle_lifetime_expression": { 28 | "max_lifetime": 1 29 | }, 30 | "minecraft:particle_initial_speed": 0, 31 | "minecraft:particle_motion_dynamic": {}, 32 | "minecraft:particle_appearance_billboard": { 33 | "size": ["0.08*(1-variable.particle_age)", "0.08*(1-variable.particle_age)"], 34 | "facing_camera_mode": "rotate_xyz", 35 | "uv": { 36 | "texture_width": 128, 37 | "texture_height": 128, 38 | "uv": [32, 88], 39 | "uv_size": [8, 8] 40 | } 41 | }, 42 | "minecraft:particle_appearance_tinting": { 43 | "color": [0.41961, 1, 0.57647, 1] 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/test/resources/particles/magic.particle.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.10.0", 3 | "particle_effect": { 4 | "description": { 5 | "identifier": "snowstorm:magic", 6 | "basic_render_parameters": { 7 | "material": "particles_alpha", 8 | "texture": "textures/particle/particles" 9 | } 10 | }, 11 | "components": { 12 | "minecraft:emitter_local_space": { 13 | "position": true, 14 | "rotation": true 15 | }, 16 | "minecraft:emitter_rate_steady": { 17 | "spawn_rate": 200, 18 | "max_particles": 1000 19 | }, 20 | "minecraft:emitter_lifetime_expression": { 21 | "activation_expression": 1 22 | }, 23 | "minecraft:emitter_shape_point": { 24 | "offset": ["((1.5 * Math.sin(variable.emitter_age * 360))) * Math.cos(variable.emitter_age * 30)", "((1.5 * Math.sin( variable.emitter_age * 360))) * Math.sin(variable.emitter_age * 30)", "1.5 * Math.cos(variable.emitter_age * 360)"] 25 | }, 26 | "minecraft:particle_lifetime_expression": { 27 | "max_lifetime": 0.7 28 | }, 29 | "minecraft:particle_motion_parametric": {}, 30 | "minecraft:particle_appearance_billboard": { 31 | "size": [1, 1], 32 | "facing_camera_mode": "rotate_xyz", 33 | "uv": { 34 | "texture_width": 128, 35 | "texture_height": 128, 36 | "flipbook": { 37 | "base_UV": [64, 96], 38 | "size_UV": [8, 8], 39 | "step_UV": [-8, 0], 40 | "max_frame": 8, 41 | "stretch_to_lifetime": true 42 | } 43 | } 44 | }, 45 | "minecraft:particle_appearance_tinting": { 46 | "color": ["Math.clamp(0.3 + variable.particle_random_4/7 + (variable.particle_random_3>0.2 ? 0.4 : 0), 0, 1)", "Math.clamp(0.2+variable.particle_random_4/5, 0, 1)", "Math.clamp(0.88 + variable.particle_random_4/8, 0, 1)", 1] 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/test/resources/particles/rect.particle.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.10.0", 3 | "particle_effect": { 4 | "description": { 5 | "identifier": "snowstorm:loading", 6 | "basic_render_parameters": { 7 | "material": "particles_alpha", 8 | "texture": "textures/particle/particles" 9 | } 10 | }, 11 | "components": { 12 | "minecraft:emitter_local_space": { 13 | "position": true, 14 | "rotation": true 15 | }, 16 | "minecraft:emitter_rate_steady": { 17 | "spawn_rate": 500, 18 | "max_particles": 1000 19 | }, 20 | "minecraft:emitter_lifetime_looping": { 21 | "active_time": 1 22 | }, 23 | "minecraft:emitter_shape_box": { 24 | "half_dimensions": [1, 1, 1], 25 | "surface_only": true, 26 | "direction": "outwards" 27 | }, 28 | "minecraft:particle_lifetime_expression": { 29 | "max_lifetime": 1 30 | }, 31 | "minecraft:particle_initial_speed": 0, 32 | "minecraft:particle_motion_dynamic": {}, 33 | "minecraft:particle_appearance_billboard": { 34 | "size": ["0.08*(1-variable.particle_age)", "0.08*(1-variable.particle_age)"], 35 | "facing_camera_mode": "rotate_xyz", 36 | "uv": { 37 | "texture_width": 128, 38 | "texture_height": 128, 39 | "uv": [32, 88], 40 | "uv_size": [8, 8] 41 | } 42 | }, 43 | "minecraft:particle_appearance_tinting": { 44 | "color": [0.41961, 1, 0.57647, 1] 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/test/resources/particles/rgb.particle.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.10.0", 3 | "particle_effect": { 4 | "description": { 5 | "identifier": "snowstorm:loading", 6 | "basic_render_parameters": { 7 | "material": "particles_alpha", 8 | "texture": "textures/particle/particles" 9 | } 10 | }, 11 | "components": { 12 | "minecraft:emitter_local_space": { 13 | "position": true, 14 | "rotation": true 15 | }, 16 | "minecraft:emitter_rate_steady": { 17 | "spawn_rate": 2000, 18 | "max_particles": 60000 19 | }, 20 | "minecraft:emitter_lifetime_once": { 21 | "active_time": 2 22 | }, 23 | "minecraft:emitter_shape_point": { 24 | "offset": ["5 * variable.particle_random_1", "5 * variable.particle_random_2", "5 * variable.particle_random_3"] 25 | }, 26 | "minecraft:particle_lifetime_expression": { 27 | "max_lifetime": 1 28 | }, 29 | "minecraft:particle_initial_speed": 0, 30 | "minecraft:particle_motion_dynamic": {}, 31 | "minecraft:particle_appearance_billboard": { 32 | "size": ["variable.size*(1-variable.particle_age)", "variable.size*(1-variable.particle_age)"], 33 | "facing_camera_mode": "rotate_xyz", 34 | "uv": { 35 | "texture_width": 128, 36 | "texture_height": 128, 37 | "uv": [32, 88], 38 | "uv_size": [8, 8] 39 | } 40 | }, 41 | "minecraft:particle_appearance_tinting": { 42 | "color": ["math.clamp(variable.particle_random_1, 0, 1)", "math.clamp(variable.particle_random_2, 0, 1)", "math.clamp(variable.particle_random_3, 0, 1)", 1] 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/test/resources/particles/rgb_transition.particle.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.10.0", 3 | "particle_effect": { 4 | "description": { 5 | "identifier": "snowstorm:loading", 6 | "basic_render_parameters": { 7 | "material": "particles_alpha", 8 | "texture": "textures/particle/particles" 9 | } 10 | }, 11 | "components": { 12 | "minecraft:emitter_local_space": { 13 | "position": true, 14 | "rotation": true 15 | }, 16 | "minecraft:emitter_rate_steady": { 17 | "spawn_rate": 1000, 18 | "max_particles": 60000 19 | }, 20 | "minecraft:emitter_lifetime_once": { 21 | "active_time": 2 22 | }, 23 | "minecraft:emitter_shape_point": { 24 | "offset": ["5 * variable.particle_random_1", "5 * variable.particle_random_2", "5 * variable.particle_random_3"] 25 | }, 26 | "minecraft:particle_lifetime_expression": { 27 | "max_lifetime": 1 28 | }, 29 | "minecraft:particle_initial_speed": 0, 30 | "minecraft:particle_motion_dynamic": {}, 31 | "minecraft:particle_appearance_billboard": { 32 | "size": ["variable.size*(1-variable.particle_age)", "variable.size*(1-variable.particle_age)"], 33 | "facing_camera_mode": "rotate_xyz", 34 | "uv": { 35 | "texture_width": 128, 36 | "texture_height": 128, 37 | "uv": [32, 88], 38 | "uv_size": [8, 8] 39 | } 40 | }, 41 | "minecraft:particle_appearance_tinting": { 42 | "color": { 43 | "interpolant": "variable.emitter_age", 44 | "gradient": { 45 | "0.0": "#FF0002FF", 46 | "2.0": "#FFFF0000" 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/test/resources/particles/sphere.particle.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.10.0", 3 | "particle_effect": { 4 | "description": { 5 | "identifier": "snowstorm:loading", 6 | "basic_render_parameters": { 7 | "material": "particles_alpha", 8 | "texture": "textures/particle/particles" 9 | } 10 | }, 11 | "components": { 12 | "minecraft:emitter_local_space": { 13 | "position": true, 14 | "rotation": true 15 | }, 16 | "minecraft:emitter_rate_steady": { 17 | "spawn_rate": 500, 18 | "max_particles": 1000 19 | }, 20 | "minecraft:emitter_lifetime_looping": { 21 | "active_time": 1 22 | }, 23 | "minecraft:emitter_shape_sphere": { 24 | "radius": 2, 25 | "surface_only": true, 26 | "direction": "outwards" 27 | }, 28 | "minecraft:particle_lifetime_expression": { 29 | "max_lifetime": 1 30 | }, 31 | "minecraft:particle_initial_speed": 0, 32 | "minecraft:particle_motion_dynamic": {}, 33 | "minecraft:particle_appearance_billboard": { 34 | "size": ["2*(1-variable.particle_age)", "2*(1-variable.particle_age)"], 35 | "facing_camera_mode": "rotate_xyz", 36 | "uv": { 37 | "texture_width": 128, 38 | "texture_height": 128, 39 | "uv": [32, 88], 40 | "uv_size": [8, 8] 41 | } 42 | }, 43 | "minecraft:particle_appearance_tinting": { 44 | "color": [0.41961, 1, 0.57647, 1] 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/test/resources/particles/ternary.particle.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.10.0", 3 | "particle_effect": { 4 | "description": { 5 | "identifier": "snowstorm:loading", 6 | "basic_render_parameters": { 7 | "material": "particles_alpha", 8 | "texture": "textures/particle/particles" 9 | } 10 | }, 11 | "components": { 12 | "minecraft:emitter_local_space": { 13 | "position": true, 14 | "rotation": true 15 | }, 16 | "minecraft:emitter_rate_steady": { 17 | "spawn_rate": 31, 18 | "max_particles": 60 19 | }, 20 | "minecraft:emitter_lifetime_expression": { 21 | "activation_expression": 1 22 | }, 23 | "minecraft:emitter_shape_point": { 24 | "offset": ["(math.sin(variable.emitter_age * 60) < 0) ? 3*math.sin(variable.emitter_age*60) : 0", "(math.cos(variable.emitter_age * 60) > 0) ? 3*math.cos(variable.emitter_age*60) : 0", 0] 25 | }, 26 | "minecraft:particle_lifetime_expression": { 27 | "max_lifetime": 1 28 | }, 29 | "minecraft:particle_initial_speed": 0, 30 | "minecraft:particle_motion_dynamic": {}, 31 | "minecraft:particle_appearance_billboard": { 32 | "size": ["variable.size*(1-variable.particle_age)", "variable.size*(1-variable.particle_age)"], 33 | "facing_camera_mode": "rotate_xyz", 34 | "uv": { 35 | "texture_width": 128, 36 | "texture_height": 128, 37 | "uv": [32, 88], 38 | "uv_size": [8, 8] 39 | } 40 | }, 41 | "minecraft:particle_appearance_tinting": { 42 | "color": [0.41961, 1, 0.57647, 1] 43 | } 44 | } 45 | } 46 | } --------------------------------------------------------------------------------