31 Commits
Author SHA1 Message Date
ByteDice 2423efbd92 ready for 1.0! Idc that it may be buggy, I want 1.0 now! 2024-12-29 15:21:06 +01:00
ByteDice 46498d25f4 added emitter debug tools. More testing required before 1.0! 2024-12-26 16:34:19 +01:00
ByteDice c416432f7b updated particle debug tools (emitter debug coming soon) 2024-12-25 16:29:32 +01:00
ByteDice a7212dbe42 removed TODO 2024-12-23 22:46:30 +01:00
ByteDice fdea77092b added new parameters 2024-12-23 16:03:13 +01:00
ByteDice 22027610ea updated the BDE to handle extra properties 2024-12-23 15:13:41 +01:00
ByteDiceandGitHub 4c051ec292 Update Bde_particles.kt TODOs 2024-12-22 23:24:53 +01:00
ByteDiceandGitHub adfa1b7bf2 Update README.md 2024-12-22 22:29:31 +01:00
ByteDice 45ff607f6a fixed some bugs. Testing time! 2024-12-22 17:49:04 +01:00
ByteDice 3ff032db1d if it works then im not touching it anymore 2024-12-22 17:06:16 +01:00
ByteDice 3b47ad870f fixed bug when compiling JAR file 2024-12-15 18:53:52 +01:00
ByteDice ddd78dcef4 fixed bug where creating a particle with a minecraft: prefix crashes server 2024-12-15 15:29:39 +01:00
ByteDice b1832825a8 fixed all bugs on ConfigArgs.kt 2024-12-15 14:31:59 +01:00
ByteDice 3e08bd4d81 tested & fixed some bugs for ConfigArgs.kt 2024-12-15 10:57:59 +01:00
ByteDice 7716ba30f8 added force field arg. Testing required for ALL args 2024-12-14 19:17:42 +01:00
ByteDice 5c1b1b07ce added more args 2024-12-14 13:11:23 +01:00
ByteDice 46cc808889 added args 2024-12-13 20:21:43 +01:00
ByteDice 0b17f8688c remade ManageEmitters command (except config). Added SpawnEmitter command. 2024-12-07 14:31:34 +01:00
ByteDice ad9ef87874 uhhhhh 2024-12-05 12:14:06 +01:00
ByteDice e2049fd204 i forgor 2024-12-02 20:27:07 +01:00
ByteDice 6eea92033d i forgor 2024-12-02 19:11:12 +01:00
ByteDice 13113e4c18 updated TODOs, canceled cone & cylinder SDF :( 2024-12-01 15:24:00 +01:00
ByteDice c5cc910be3 added async particle ticking and made new params fully working. 2024-12-01 15:13:41 +01:00
ByteDice 2edb23e9ec bugfixes 2024-11-30 19:52:25 +01:00
ByteDice 828297aef2 moved to ItemDisplays and added support for CustomModelData 2024-11-30 17:22:20 +01:00
ByteDice 8ad6cb9ffb made some newly added params function correctly 2024-11-30 13:08:35 +01:00
ByteDice be00598efe added some new params to Particle 2024-11-30 00:05:31 +01:00
ByteDice 939867934b significantly changed emitter params. STILL A LOT OF ERRORS. 2024-11-29 19:38:43 +01:00
ByteDice 6728445280 made particles compatible with updated param data. Though not the new ones. STILL FILLED WITH ERRORS 2024-11-28 20:29:37 +01:00
ByteDice 5b43c12c15 reworked a bunch of stuff. Contains errors 2024-11-28 19:53:49 +01:00
ByteDiceandGitHub af7d29ce7d Update issue templates 2024-11-25 20:08:26 +01:00
24 changed files with 2535 additions and 1404 deletions
+5 -13
View File
@@ -1,8 +1,8 @@
---
name: Bug report
about: Create a report to help us improve
about: Create a report to help us squash pests
title: ''
labels: ''
labels: bug
assignees: ''
---
@@ -10,6 +10,9 @@ assignees: ''
**Describe the bug**
A clear and concise description of what the bug is.
**Minecraft version**
Provide your Minecraft version, even if the mod is only for a single version.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
@@ -23,16 +26,5 @@ A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
+4 -4
View File
@@ -2,19 +2,19 @@
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
**Is your feature request related to a problem?**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
**Describe the result you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
A clear and concise description of any alternative features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+9 -2
View File
@@ -3,8 +3,15 @@
BDE_ParticleSys is a highly customizable **server-side** particle system that uses `Block Display Entities` (BDEs) as particles. It is meant to be used as a library for other mods, but can be used as a standalone mod too.
<!-- change GiveParticleEmitter to GiveEmitterTool -->
If BDE_ParticleSys is used as a standalone mod, you will only be able to use it with the custom in-game commands `/GiveEmitterTool`, `/KillAllEmitters` and `/ManageEmitters`. Any particles made with those commands are **temporary** and get removed once the server shuts down, as they are meant for quick testing.
If BDE_ParticleSys is used as a standalone mod, you will only be able to use it with the custom in-game commands `/GiveEmitterTool`, `/KillAllEmitters`, and `/ManageEmitters`. Any particles made, removed, or changed with those commands are **temporary** and get reverted once the server shuts down, as they are meant for quick testing.
### In-game commands (does not save):
* `/GiveEmitterTool`
* `/SpawnEmitter`
* `/KillAllEmitters`
* `/ManageEmitters`
* `GlobalMaxParticles` (GameRule)
* `ShowParticleDebug` (GameRule)
# Open-source - Copyright
@@ -3,6 +3,8 @@ package com.bytedice.bde_particles.client
import com.bytedice.bde_particles.commands.GiveEmitterTool
import com.bytedice.bde_particles.commands.KillAllEmitters
import com.bytedice.bde_particles.commands.ManageEmitters
import com.bytedice.bde_particles.commands.SpawnEmitter
//import com.bytedice.bde_particles.commands.ManageEmitters
import com.bytedice.bde_particles.init
import com.bytedice.bde_particles.onRightClick
import com.bytedice.bde_particles.tick
@@ -38,8 +40,9 @@ class Bde_particlesClient : ClientModInitializer {
CommandRegistrationCallback.EVENT.register { dispatcher, registryAccess, _ ->
GiveEmitterTool.register(dispatcher, registryAccess)
ManageEmitters .register(dispatcher)
ManageEmitters .register(dispatcher, registryAccess)
KillAllEmitters.register(dispatcher)
SpawnEmitter .register(dispatcher)
}
UseItemCallback.EVENT.register(UseItemCallback { player: PlayerEntity, world: World, hand: Hand ->
@@ -3,35 +3,80 @@ package com.bytedice.bde_particles
import com.bytedice.bde_particles.commands.GiveEmitterTool
import com.bytedice.bde_particles.commands.KillAllEmitters
import com.bytedice.bde_particles.commands.ManageEmitters
import com.bytedice.bde_particles.commands.SpawnEmitter
import com.bytedice.bde_particles.items.ParticleEmitterTool
import com.bytedice.bde_particles.particles.*
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.builder.RequiredArgumentBuilder
import com.mojang.brigadier.context.CommandContext
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import net.fabricmc.api.EnvType
import net.fabricmc.api.ModInitializer
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents
import net.fabricmc.fabric.api.event.player.UseItemCallback
import net.fabricmc.fabric.api.gamerule.v1.GameRuleFactory
import net.fabricmc.fabric.api.gamerule.v1.GameRuleRegistry
import net.fabricmc.loader.api.FabricLoader
import net.minecraft.entity.EntityType
import net.minecraft.entity.decoration.DisplayEntity.BlockDisplayEntity
import net.minecraft.entity.decoration.DisplayEntity.ItemDisplayEntity
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound
import net.minecraft.server.MinecraftServer
import net.minecraft.server.command.CommandManager
import net.minecraft.server.command.ServerCommandSource
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.server.world.ServerWorld
import net.minecraft.text.Style
import net.minecraft.text.Text
import net.minecraft.util.ActionResult
import net.minecraft.util.Hand
import net.minecraft.util.TypedActionResult
import net.minecraft.world.GameRules
import net.minecraft.world.World
import org.joml.Vector2f
import java.awt.Color
import java.util.*
import java.util.concurrent.CompletableFuture
import kotlin.reflect.full.companionObject
import kotlin.reflect.full.companionObjectInstance
import kotlin.reflect.full.declaredMemberProperties
// TODO: (future additions)
// Cylinder and cone force field shape.
// Copying particle params in-game.
// Custom curve equation command args (parse from strings).
// Force field "config" option.
// add rotation offset to transformWithVel
// TODO: (now)
// nothing lmao
var ALL_PARTICLE_EMITTERS: Array<ParticleEmitter> = emptyArray()
val sessionUuid: UUID = UUID.randomUUID()
var LIVING_PARTICLE_COUNT: Int = 0
val SESSION_UUID: UUID = UUID.randomUUID()
class Bde_particles : ModInitializer {
companion object {
val GLOBAL_MAX_PARTICLES: GameRules.Key<GameRules.IntRule> = GameRuleRegistry.register(
"GlobalMaxParticles",
GameRules.Category.UPDATES,
GameRuleFactory.createIntRule(3000, 0)
)
val SHOW_PARTICLE_DEBUG: GameRules.Key<GameRules.BooleanRule> = GameRuleRegistry.register(
"ShowParticleDebug",
GameRules.Category.UPDATES,
GameRuleFactory.createBooleanRule(false)
)
}
override fun onInitialize() {
if (FabricLoader.getInstance().environmentType != EnvType.SERVER) {
return
@@ -39,18 +84,15 @@ class Bde_particles : ModInitializer {
println("BPS - Initializing on ${FabricLoader.getInstance().environmentType}")
ServerLifecycleEvents.SERVER_STARTED.register { _ ->
init()
}
ServerTickEvents.START_SERVER_TICK.register { server ->
tick(server)
}
CommandRegistrationCallback.EVENT.register { dispatcher, registryAccess, _ ->
GiveEmitterTool.register(dispatcher, registryAccess)
ManageEmitters .register(dispatcher)
ManageEmitters .register(dispatcher, registryAccess)
KillAllEmitters.register(dispatcher)
SpawnEmitter .register(dispatcher)
}
UseItemCallback.EVENT.register(UseItemCallback { player: PlayerEntity, world: World, hand: Hand ->
@@ -61,6 +103,8 @@ class Bde_particles : ModInitializer {
})
ServerLifecycleEvents.SERVER_STARTED.register(ServerLifecycleEvents.ServerStarted { _ ->
init()
println("BPS - Progress bar filled :3")
println(
"\n____________ _____ _____ \n" +
@@ -76,31 +120,41 @@ class Bde_particles : ModInitializer {
fun init() {
addToEmitterRegister("DEFAULT", EmitterParams.DEFAULT)
addToEmitterRegister("FIRE_GEYSER", EmitterParams.FIRE_GEYSER)
addToEmitterRegister("RING_EXPLOSION", EmitterParams.RING_EXPLOSION)
addToRegister("DEFAULT", EmitterParams.DEFAULT)
addToRegister("DEBUG", EmitterParams.DEBUG)
addToRegister("STRESS_TEST", EmitterParams.STRESS_TEST)
}
fun tick(server: MinecraftServer) {
for (world in server.worlds) {
for (entity in world.iterateEntities()) {
if (entity.type == EntityType.BLOCK_DISPLAY) {
if (displayEntityContainsTag(entity as BlockDisplayEntity, "BPS_UUID")
&& !displayEntityContainsTag(entity, sessionUuid.toString())) {
runBlocking {
server.worlds.map { world ->
async {
val displayEntities = world.iterateEntities()
.filter { it.type == EntityType.ITEM_DISPLAY }
for (entity in displayEntities) {
if (displayEntityContainsTag(entity as ItemDisplayEntity, "BPS_UUID")
&& !displayEntityContainsTag(entity, SESSION_UUID.toString())) {
entity.kill()
}
}
}
}.awaitAll()
}
for (emitter in ALL_PARTICLE_EMITTERS) {
runBlocking {
ALL_PARTICLE_EMITTERS.map { emitter ->
async {
if (emitter.isDead) {
synchronized(ALL_PARTICLE_EMITTERS) {
ALL_PARTICLE_EMITTERS = ALL_PARTICLE_EMITTERS.toMutableList().apply { remove(emitter) }.toTypedArray()
}
}
else { emitter.tick() }
}
}.awaitAll()
}
}
@@ -108,7 +162,7 @@ fun onRightClick(player: PlayerEntity, world: ServerWorld, hand: Hand) : TypedAc
val handItem = player.getStackInHand(hand)
val (isEmitterTool, emitterId) = ParticleEmitterTool.getToolDetails(handItem)
val hitResult = raycastFromPlayer(player as ServerPlayerEntity, 200.0)
val emitterParams = getEmitterParams(emitterId)
val emitterParams = getEmitterDataById(emitterId)
if (
!isEmitterTool
@@ -118,53 +172,77 @@ fun onRightClick(player: PlayerEntity, world: ServerWorld, hand: Hand) : TypedAc
return TypedActionResult(ActionResult.PASS, handItem)
}
val emitter = ParticleEmitter(hitResult.pos, world, emitterParams)
ALL_PARTICLE_EMITTERS += emitter
val debug = world.gameRules.getBoolean(Bde_particles.SHOW_PARTICLE_DEBUG)
val emitter = ParticleEmitter(
hitResult.pos,
Vector2f(0.0f, 0.0f),
world,
emitterParams,
debug
)
spawnEmitter(emitter)
return TypedActionResult(ActionResult.PASS, handItem)
}
fun emitterParamsToJson(params: EmitterParams) : Map<String, Any> {
val allParticleParamsJSON: MutableList<Map<String, Any?>> = mutableListOf()
val particle = params.particle
val particleParamsJSON = mapOf(
"shape" to particle.shape,
"blockCurve" to particle.blockCurve,
"rotRandom" to particle.rotRandom,
"rotVelRandom" to particle.rotVelRandom,
"rotVelCurve" to particle.rotVelCurve,
"sizeRandom" to particle.sizeRandom,
"uniformSize" to particle.uniformSize,
"sizeCurve" to particle.sizeCurve,
"velRandom" to particle.velRandom,
"forceFields" to particle.forceFields,
"gravity" to particle.gravity,
"drag" to particle.drag,
"minVel" to particle.minVel,
"lifeTime" to particle.lifeTime
)
allParticleParamsJSON.add(particleParamsJSON)
val emitterParamsJSON = mapOf(
"maxCount" to params.maxCount,
"spawnsPerTick" to params.spawnsPerTick,
"loopDur" to params.loopDur,
"loopDelay" to params.loopDelay,
"loopCount" to params.loopCount,
"particleTypes" to allParticleParamsJSON
)
return emitterParamsJSON
}
fun displayEntityContainsTag(entity: BlockDisplayEntity, tag: String) : Boolean {
fun displayEntityContainsTag(entity: ItemDisplayEntity, tag: String) : Boolean {
val nbt = NbtCompound()
entity.writeNbt(nbt)
val tagsNbtList = nbt.getList("Tags", 8)
return tagsNbtList?.any { it.asString() == tag } == true
}
fun emitterListArg(argName: String) : RequiredArgumentBuilder<ServerCommandSource, String> {
return CommandManager.argument(argName, StringArgumentType.string())
.suggests { _, builder ->
idRegister.keys.forEach { key ->
builder.suggest(key)
}
CompletableFuture.completedFuture(builder.build())
}
}
fun curveListArg(argName: String): RequiredArgumentBuilder<ServerCommandSource, String> {
return CommandManager.argument(argName, StringArgumentType.string())
.suggests { _, builder ->
LerpCurves.Companion::class.members
.filterIsInstance<kotlin.reflect.KProperty1<LerpCurves.Companion, *>>()
.filter { it.returnType.classifier == LerpCurves::class }
.forEach { property ->
builder.suggest(property.name)
}
CompletableFuture.completedFuture(builder.build())
}
}
fun stringToCurve(curveName: String): LerpCurves? {
val companion = LerpCurves::class.companionObjectInstance
val properties = LerpCurves::class.companionObject?.declaredMemberProperties
return properties?.firstOrNull { it.name == curveName }?.getter?.call(companion) as? LerpCurves
}
fun spawnEmitter(emitter: ParticleEmitter) {
ALL_PARTICLE_EMITTERS += emitter
}
fun positiveFeedback(text: String, context: CommandContext<ServerCommandSource>) {
val feedback = Text.literal("BPS - $text")
.setStyle(Style.EMPTY.withColor(Color(0, 200, 0).rgb))
context.source.sendFeedback({ feedback }, false)
}
fun negativeFeedback(text: String, context: CommandContext<ServerCommandSource>) {
val feedback = Text.literal("BPS - $text")
.setStyle(Style.EMPTY.withColor(Color(200, 0, 0).rgb))
context.source.sendFeedback({ feedback }, false)
}
@@ -1,21 +1,21 @@
package com.bytedice.bde_particles
import net.minecraft.entity.EntityType
import net.minecraft.entity.decoration.DisplayEntity.BlockDisplayEntity
import net.minecraft.entity.decoration.DisplayEntity.ItemDisplayEntity
import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtFloat
import net.minecraft.nbt.NbtList
import net.minecraft.nbt.NbtString
import net.minecraft.server.world.ServerWorld
class DisplayEntity(private var properties: DisplayEntityProperties?) {
class DisplayEntity(var properties: DisplayEntityProperties?) {
private var hasSpawned = false
private var entity: BlockDisplayEntity? = null
private var entity: ItemDisplayEntity? = null
fun spawn(world: ServerWorld) {
if (hasSpawned || properties == null) { return }
entity = BlockDisplayEntity(EntityType.BLOCK_DISPLAY, world)
entity = ItemDisplayEntity(EntityType.ITEM_DISPLAY, world)
updateProperties(properties!!)
world.spawnEntity(entity)
@@ -34,7 +34,13 @@ class DisplayEntity(private var properties: DisplayEntityProperties?) {
put("Tags", tagList)
put("block_state", NbtCompound().apply { putString("Name", properties.blockType) })
if (properties.model != "none") {
put("item", NbtCompound().apply {
putString("id", properties.model)
putInt("count", 1)
put("components", NbtCompound().apply { putInt("minecraft:custom_model_data", properties.customModel) })
})
}
put("transformation", NbtCompound().apply {
val offsetList = NbtList().apply {
@@ -65,6 +71,22 @@ class DisplayEntity(private var properties: DisplayEntityProperties?) {
put("scale", scaleList)
put("right_rotation", rightRotList)
})
putFloat("view_range", properties.viewRange)
putString("billboard", properties.billboard)
if (properties.customName != null) {
putString("CustomName", "{\"text\":\"${properties.customName}\"}")
putByte("CustomNameVisible", 1)
}
if (properties.brightnessOverride != null) {
put("brightness", NbtCompound().apply {
putInt("block", properties.brightnessOverride!!)
putInt("sky", properties.brightnessOverride!!)
})
}
}
entity?.readNbt(nbt)
@@ -75,7 +97,7 @@ class DisplayEntity(private var properties: DisplayEntityProperties?) {
fun kill() {
if (entity == null) { println("BPS - Particle Emitter entity is null!"); return }
if (entity == null) { println("BPS - Failed to kill BDE. Entity is null!"); return }
entity?.kill()
}
}
@@ -6,14 +6,28 @@ import org.joml.Vector3f
import org.joml.Vector4f
data class DisplayEntityProperties (
val pos: Vec3d = Vec3d(0.0, 0.0, 0.0),
val rot: Vector2f = Vector2f(0.0f, 0.0f),
var pos: Vec3d = Vec3d(0.0, 0.0, 0.0),
var rot: Vector2f = Vector2f(0.0f, 0.0f),
val blockType: String = "minecraft:purple_concrete",
val tags: Array<String> = emptyArray(),
var model: String = "minecraft:purple_concrete",
var customModel: Int = 0,
var tags: Array<String> = emptyArray(),
var viewRange: Float = 2.0f,
var customName: String? = null,
var brightnessOverride: Int? = null,
var billboard: String = Billboard.FIXED,
val translation: Vector3f = Vector3f(-0.5f, -0.5f, -0.5f),
val leftRotation: Vector4f = Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
val scale: Vector3f = Vector3f(1.0f, 1.0f, 1.0f),
val rightRotation: Vector4f = Vector4f(0.0f, 0.0f, 0.0f, 1.0f)
var translation: Vector3f = Vector3f(0.0f, 0.0f, 0.0f),
var leftRotation: Vector4f = Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
var scale: Vector3f = Vector3f(1.0f, 1.0f, 1.0f),
var rightRotation: Vector4f = Vector4f(0.0f, 0.0f, 0.0f, 1.0f)
)
class Billboard () {
companion object {
const val FIXED: String = "fixed"
const val VERTICAL: String = "vertical"
const val HORIZONTAL: String = "horizontal"
const val CENTER: String = "center"
}
}
@@ -5,25 +5,24 @@ import net.minecraft.util.hit.HitResult
import net.minecraft.util.math.Vec3d
import net.minecraft.world.RaycastContext
import net.minecraft.world.World
import org.joml.Vector2f
import org.joml.Vector3f
import org.joml.Vector4f
import kotlin.math.*
import kotlin.random.Random
class InterpolationCurves(val function: (Float) -> Float) {
class LerpCurves(val function: (Float) -> Float) {
companion object {
val LINEAR = InterpolationCurves { y -> y }
val SQRT = InterpolationCurves { y -> sqrt(y) }
val EXPONENT = InterpolationCurves { y -> y.pow(2.0f) }
val CUBIC = InterpolationCurves { y -> y.pow(3.0f) }
val SINE = InterpolationCurves { y -> sin(y * PI.toFloat() / 2) }
val COSINE = InterpolationCurves { y -> 1 - cos(y * PI.toFloat() / 2) }
val INVERSE = InterpolationCurves { y -> 1 - y }
val LOG = InterpolationCurves { y -> if (y > 0) ln(y + 1) else 0f }
val EXP = InterpolationCurves { y -> exp(y) - 1 }
val BOUNCE = InterpolationCurves { y ->
val Linear = LerpCurves { y -> y }
val Sqrt = LerpCurves { y -> sqrt(y) }
val Exponent = LerpCurves { y -> y.pow(2.0f) }
val Cubic = LerpCurves { y -> y.pow(3.0f) }
val Sine = LerpCurves { y -> sin(y * PI.toFloat() / 2) }
val Cosine = LerpCurves { y -> 1 - cos(y * PI.toFloat() / 2) }
val Inverse = LerpCurves { y -> 1 - y }
val Log = LerpCurves { y -> if (y > 0) ln(y + 1) else 0f }
val Exp = LerpCurves { y -> exp(y) - 1 }
val Bounce = LerpCurves { y ->
val n1 = 7.5625f
val d1 = 2.75f
when {
@@ -43,7 +42,11 @@ class InterpolationCurves(val function: (Float) -> Float) {
}
}
fun custom(equation: (Float) -> Float) = InterpolationCurves(equation)
fun custom(equation: (Float) -> Float) = LerpCurves(equation)
}
fun deepCopy() : LerpCurves {
return LerpCurves(this.function)
}
}
@@ -73,28 +76,23 @@ fun raycastFromPlayer(player: ServerPlayerEntity, maxDistance: Double): HitResul
fun randomFloatBetween(min: Float, max: Float) : Float {
return Random.nextFloat() * (max - min) + min
return if (min == max) { max }
else if (min > max) { Random.nextFloat() * (min - max) + max }
else { Random.nextFloat() * (max - min) + min }
}
fun randomIntBetween(min: Int, max: Int) : Int {
return Random.nextInt(min, max)
}
fun randomBetweenVector3f(min: Vector3f, max: Vector3f) : Vector3f {
return Vector3f(
randomFloatBetween(min.x, max.x),
randomFloatBetween(min.y, max.y),
randomFloatBetween(min.z, max.z)
)
return if (min == max) { max }
else if (min > max) { Random.nextInt(min - max) + max }
else { Random.nextInt(max - min) + min }
}
fun eulerToQuat(euler: Vector3f): Vector4f {
val roll = euler.x
val pitch = euler.y
val yaw = euler.z
val roll = euler.x * (Math.PI / 180).toFloat()
val pitch = euler.y * (Math.PI / 180).toFloat()
val yaw = euler.z * (Math.PI / 180).toFloat()
val cy = cos(yaw * 0.5)
val sy = sin(yaw * 0.5)
@@ -137,45 +135,6 @@ fun quatToEuler(quat: Vector4f): Vector3f {
}
fun randomInCircle(r: Float) : Vector2f {
val randRadius = r * sqrt(randomFloatBetween(0.0f, 1.0f))
val randAngle = randomFloatBetween(0.0f, Math.PI.toFloat() * 2)
val randomPos = Vector2f(randRadius * cos(randAngle), randRadius * sin(randAngle))
return randomPos
}
fun randomInSphere(r: Float) : Vector3f {
val randRadius = r * Random.nextDouble().pow(1.0 / 3.0)
val phi = Random.nextDouble(0.0, 2 * Math.PI)
val cosTheta = Random.nextDouble(-1.0, 1.0)
val theta = acos(cosTheta)
val x = randRadius * sin(theta) * cos(phi)
val y = randRadius * sin(theta) * sin(phi)
val z = randRadius * cos(theta)
return Vector3f(x.toFloat(), y.toFloat(), z.toFloat())
}
fun randomInRect(size: Vector2f) : Vector2f {
return Vector2f(randomFloatBetween(0.0f, size.x), randomFloatBetween(0.0f, size.y))
}
fun randomInCube(size: Vector3f) : Vector3f {
return Vector3f(
randomFloatBetween(0.0f, size.x),
randomFloatBetween(0.0f, size.y),
randomFloatBetween(0.0f, size.z)
)
}
fun transformOffsetByQuat(offset: Vector3f, rotation: Vector4f): Vector3f {
val x = offset.x
val y = offset.y
@@ -205,22 +164,14 @@ fun transformOffsetByScale(offset: Vector3f, scale: Vector3f): Vector3f {
fun lerp(x: Float, y: Float, t: Float) : Float {
return x + (y - x) * t
val clampedT = t.coerceIn(0f, 1f)
return x + (y - x) * clampedT
}
fun interpolateCurve(start: Vector3f, end: Vector3f, t: Float, curve: InterpolationCurves): Vector3f {
val clampedT = t.coerceIn(0.0f, 1.0f)
val curvedT = curve.function(clampedT)
val interpolatedVec = Vector3f(
lerp(start.x, end.x, curvedT),
lerp(start.y, end.y, curvedT),
lerp(start.z, end.z, curvedT)
)
return interpolatedVec
fun lerpArray(array: Array<Any>, t: Float, curve: LerpCurves = LerpCurves.Linear) : Any {
val idx = round(lerp(0.0f, array.lastIndex.toFloat(), t) * curve.function(t)).toInt()
return array[idx.coerceIn(0, array.lastIndex)]
}
@@ -234,7 +185,6 @@ fun sdfSphere(pos: Vector3f, radius: Float, objectPos: Vector3f): Float {
}
fun sdfCube(pos: Vector3f, size: Vector3f, objectPos: Vector3f): Float {
val px = objectPos.x - pos.x
val py = objectPos.y - pos.y
@@ -0,0 +1,914 @@
@file:Suppress("UNCHECKED_CAST")
package com.bytedice.bde_particles.commands
import com.bytedice.bde_particles.curveListArg
import com.bytedice.bde_particles.negativeFeedback
import com.bytedice.bde_particles.particles.*
import com.bytedice.bde_particles.positiveFeedback
import com.bytedice.bde_particles.stringToCurve
import com.mojang.brigadier.Command
import com.mojang.brigadier.arguments.BoolArgumentType
import com.mojang.brigadier.arguments.FloatArgumentType
import com.mojang.brigadier.arguments.IntegerArgumentType
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import com.mojang.brigadier.builder.RequiredArgumentBuilder
import com.mojang.brigadier.context.CommandContext
import net.minecraft.command.CommandRegistryAccess
import net.minecraft.command.argument.ItemStackArgumentType
import net.minecraft.registry.Registries
import net.minecraft.server.command.CommandManager
import net.minecraft.server.command.ServerCommandSource
import org.joml.Vector2f
import org.joml.Vector3f
import kotlin.reflect.*
import kotlin.reflect.full.memberProperties
typealias CommandArgumentBuilder = (
access: KProperty1<EmitterParams, *>,
config: LiteralArgumentBuilder<ServerCommandSource>,
reg: CommandRegistryAccess
) -> LiteralArgumentBuilder<ServerCommandSource>?
fun listConfigArgs(configArgs: Array<String>,
rootArg: RequiredArgumentBuilder<ServerCommandSource, *>,
registryAccess: CommandRegistryAccess
): RequiredArgumentBuilder<ServerCommandSource, *>
{
configArgs.forEach { arg ->
var configArg = CommandManager.literal(arg)
val argAccess = parseArgNameToAccess(arg)!!
val func = parseArgTypeToFunc(argAccess.returnType, configArg)
if (func != null) {
configArg = func(argAccess, configArg, registryAccess)
}
rootArg.then(configArg)
}
return rootArg
}
fun dataClassToArray(dataClass: KClass<*>): Array<String> {
return dataClass.memberProperties.map { it.name }.toTypedArray()
}
fun parseArgNameToAccess(name: String) : KProperty1<EmitterParams, *>? {
val access = EmitterParams::class.memberProperties
.firstOrNull { it.name == name }
return access
}
fun parseArgTypeToFunc(type: KType, configArg: LiteralArgumentBuilder<ServerCommandSource>): CommandArgumentBuilder? {
return when (type.classifier) {
Int::class -> { access, _, _ ->
configArg.then(intArg(access as KProperty1<EmitterParams, Int>))
}
Float::class -> { access, _, _ ->
configArg.then(floatArg(access as KProperty1<EmitterParams, Float>))
}
String::class -> { access, _, _ ->
configArg.then(stringArg(access as KProperty1<EmitterParams, String>))
}
ParamClasses.Duration::class -> { access, config, _ ->
durArg(access as KProperty1<EmitterParams, ParamClasses.Duration>, config)
}
Vector3f::class -> { access, _, _ ->
configArg.then(vec3fArg(access as KProperty1<EmitterParams, Vector3f>))
}
ParamClasses.PairInt::class -> { access, _, _ ->
configArg.then(pairIntArg(access as KProperty1<EmitterParams, ParamClasses.PairInt>))
}
SpawningShape::class -> { access, config, _ ->
shapeArg(access as KProperty1<EmitterParams, SpawningShape>, config)
}
ParamClasses.PairVec3f::class -> { access, config, _ ->
pairVec3fArg(access as KProperty1<EmitterParams, ParamClasses.PairVec3f>, config)
}
ParamClasses.PairFloat::class -> { access, _, _ ->
configArg.then(pairFloatArg(access as KProperty1<EmitterParams, ParamClasses.PairFloat>))
}
ParamClasses.TransformWithVel::class -> { access, config, _ ->
transformWithVelArg(access as KProperty1<EmitterParams, ParamClasses.TransformWithVel>, config)
}
ParamClasses.StringCurve::class -> { access, config, reg ->
stringCurveArg(access as KProperty1<EmitterParams, ParamClasses.StringCurve>, config, reg)
}
ParamClasses.LerpValVec3f::class -> { access, config, _ ->
lerpValVec3fArg(access as KProperty1<EmitterParams, ParamClasses.LerpValVec3f>, config)
}
ParamClasses.ForceFieldArray::class -> { access, config, _ ->
forceFieldArg(access as KProperty1<EmitterParams, ParamClasses.ForceFieldArray>, config)
}
ParamClasses.LerpValInt::class -> { access, config, _ ->
lerpValIntArg(access as KProperty1<EmitterParams, ParamClasses.LerpValInt>, config)
}
else -> null
}
}
fun successText(access: KProperty1<EmitterParams, *>, value: String, id: String, context: CommandContext<ServerCommandSource>) {
positiveFeedback("Successfully updated parameter \"${access.name}\" to \"$value\" of Emitter ID \"$id\"", context)
}
fun intArg(access: KProperty1<EmitterParams, Int>) : RequiredArgumentBuilder<ServerCommandSource, Int> {
return CommandManager.argument("Int", IntegerArgumentType.integer())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val value = IntegerArgumentType.getInteger(context, "Int")
updateParam(id, access, value)
successText(access, value.toString(), id, context)
Command.SINGLE_SUCCESS
}
}
fun floatArg(access: KProperty1<EmitterParams, Float>) : RequiredArgumentBuilder<ServerCommandSource, Float> {
return CommandManager.argument("Float", FloatArgumentType.floatArg())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val value = FloatArgumentType.getFloat(context, "Float")
updateParam(id, access, value)
successText(access, value.toString(), id, context)
Command.SINGLE_SUCCESS
}
}
fun stringArg(access: KProperty1<EmitterParams, String>) : RequiredArgumentBuilder<ServerCommandSource, String> {
return CommandManager.argument("String", StringArgumentType.string())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val value = StringArgumentType.getString(context, "String")
updateParam(id, access, value)
successText(access, value.toString(), id, context)
Command.SINGLE_SUCCESS
}
}
fun durArg(access: KProperty1<EmitterParams, ParamClasses.Duration>,
configArg: LiteralArgumentBuilder<ServerCommandSource>
) : LiteralArgumentBuilder<ServerCommandSource>
{
return configArg
.then(CommandManager.literal("SingleBurst")
.then(CommandManager.argument("loopDur", IntegerArgumentType.integer())
.executes { context -> durArgExec("SingleBurst", access, context); Command.SINGLE_SUCCESS }
)
)
.then(CommandManager.literal("MultiBurst")
.then(CommandManager.argument("loopDur", IntegerArgumentType.integer())
.then(CommandManager.argument("loopDelay", IntegerArgumentType.integer())
.then(CommandManager.argument("loopCount", IntegerArgumentType.integer())
.executes { context -> durArgExec("MultiBurst", access, context); Command.SINGLE_SUCCESS }
)
)
)
)
.then(CommandManager.literal("InfiniteLoop")
.then(CommandManager.argument("loopDur", IntegerArgumentType.integer())
.then(CommandManager.argument("loopDelay", IntegerArgumentType.integer())
.executes { context -> durArgExec("InfiniteLoop", access, context); Command.SINGLE_SUCCESS }
)
)
)
}
fun durArgExec(paramName: String, access: KProperty1<EmitterParams, ParamClasses.Duration>, context: CommandContext<ServerCommandSource>) {
val id = StringArgumentType.getString(context, "Emitter ID")
val loopDur = IntegerArgumentType.getInteger(context, "loopDur")
when (paramName) {
"SingleBurst" -> {
updateParam(id, access, ParamClasses.Duration.SingleBurst(loopDur))
}
"MultiBurst" -> {
val loopDelay = IntegerArgumentType.getInteger(context, "loopDelay")
val loopCount = IntegerArgumentType.getInteger(context, "loopCount")
updateParam(id, access, ParamClasses.Duration.MultiBurst(loopDur, loopDelay, loopCount))
}
"InfiniteLoop" -> {
val loopDelay = IntegerArgumentType.getInteger(context, "loopDelay")
updateParam(id, access, ParamClasses.Duration.InfiniteLoop(loopDur, loopDelay))
}
}
successText(access, access::class.simpleName ?: "UNKNOWN", id, context)
}
fun vec3fArg(access: KProperty1<EmitterParams, Vector3f>) : RequiredArgumentBuilder<ServerCommandSource, Float> {
return CommandManager.argument("X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z", FloatArgumentType.floatArg())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val valueX = FloatArgumentType.getFloat(context, "X")
val valueY = FloatArgumentType.getFloat(context, "Y")
val valueZ = FloatArgumentType.getFloat(context, "Z")
updateParam(id, access, Vector3f(valueX, valueY, valueZ))
successText(access, "[$valueX, $valueY, $valueZ]", id, context)
Command.SINGLE_SUCCESS
}
)
)
}
fun pairIntArg(access: KProperty1<EmitterParams, ParamClasses.PairInt>) : RequiredArgumentBuilder<ServerCommandSource, Int> {
return CommandManager.argument("Min Int", IntegerArgumentType.integer())
.then(CommandManager.argument("Max Int", IntegerArgumentType.integer())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val valueX = IntegerArgumentType.getInteger(context, "Min Int")
val valueY = IntegerArgumentType.getInteger(context, "Max Int")
updateParam(id, access, ParamClasses.PairInt(valueX, valueY))
successText(access, "[$valueX, $valueY]", id, context)
Command.SINGLE_SUCCESS
}
)
}
fun shapeArg(access: KProperty1<EmitterParams, SpawningShape>,
configArg: LiteralArgumentBuilder<ServerCommandSource>
) : LiteralArgumentBuilder<ServerCommandSource>
{
return configArg
.then(CommandManager.literal("Circle")
.then(CommandManager.argument("Radius", FloatArgumentType.floatArg())
.then(CommandManager.argument("Spawn On Edge", BoolArgumentType.bool())
.executes { context -> shapeArgExec("Circle", access, context); Command.SINGLE_SUCCESS }
)
)
)
.then(CommandManager.literal("Rect")
.then(CommandManager.argument("X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Spawn On Edge", BoolArgumentType.bool())
.executes { context -> shapeArgExec("Rect", access, context); Command.SINGLE_SUCCESS }
)
)
)
)
.then(CommandManager.literal("Cube")
.then(CommandManager.argument("X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Spawn On Edge", BoolArgumentType.bool())
.executes { context -> shapeArgExec("Cube", access, context); Command.SINGLE_SUCCESS }
)
)
)
)
)
.then(CommandManager.literal("Sphere")
.then(CommandManager.argument("Radius", FloatArgumentType.floatArg())
.then(CommandManager.argument("Spawn On Edge", BoolArgumentType.bool())
.executes { context -> shapeArgExec("Sphere", access, context); Command.SINGLE_SUCCESS }
)
)
)
.then(CommandManager.literal("Point")
.executes { context -> shapeArgExec("Point", access, context); Command.SINGLE_SUCCESS }
)
}
fun shapeArgExec(paramName: String, access: KProperty1<EmitterParams, SpawningShape>, context: CommandContext<ServerCommandSource>) {
val id = StringArgumentType.getString(context, "Emitter ID")
when (paramName) {
"Circle" -> {
val radius = FloatArgumentType.getFloat(context, "Radius")
val onEdge = BoolArgumentType.getBool(context, "Spawn On Edge")
updateParam(id, access, SpawningShape.Circle(radius, onEdge))
successText(access, "Circle", id, context)
}
"Rect" -> {
val onEdge = BoolArgumentType.getBool(context, "Spawn On Edge")
val x = FloatArgumentType.getFloat(context, "X")
val y = FloatArgumentType.getFloat(context, "Y")
updateParam(id, access, SpawningShape.Rect(Vector2f(x, y), onEdge))
successText(access, "Rect", id, context)
}
"Cube" -> {
val onEdge = BoolArgumentType.getBool(context, "Spawn On Edge")
val x = FloatArgumentType.getFloat(context, "X")
val y = FloatArgumentType.getFloat(context, "Y")
val z = FloatArgumentType.getFloat(context, "Z")
updateParam(id, access, SpawningShape.Cube(Vector3f(x, y, z), onEdge))
successText(access, "Cube", id, context)
}
"Sphere" -> {
val radius = FloatArgumentType.getFloat(context, "Radius")
val onEdge = BoolArgumentType.getBool(context, "Spawn On Edge")
updateParam(id, access, SpawningShape.Sphere(radius, onEdge))
successText(access, "Sphere", id, context)
}
"Point" -> {
updateParam(id, access, SpawningShape.Point)
successText(access, "Point", id, context)
}
}
}
fun pairVec3fArg(access: KProperty1<EmitterParams,ParamClasses.PairVec3f>,
configArg: LiteralArgumentBuilder<ServerCommandSource>
) : LiteralArgumentBuilder<ServerCommandSource>
{
return configArg
.then(CommandManager.literal("NonUniform")
.then(CommandManager.argument("Min X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Z", FloatArgumentType.floatArg())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val minX = FloatArgumentType.getFloat(context, "Min X")
val minY = FloatArgumentType.getFloat(context, "Min Y")
val minZ = FloatArgumentType.getFloat(context, "Min Z")
val maxX = FloatArgumentType.getFloat(context, "Max X")
val maxY = FloatArgumentType.getFloat(context, "Max Y")
val maxZ = FloatArgumentType.getFloat(context, "Max Z")
updateParam(id, access, ParamClasses.PairVec3f.NonUniform(minX, minY, minZ, maxX, maxY, maxZ))
successText(access, "NonUniform", id, context)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
)
.then(CommandManager.literal("Uniform")
.then(CommandManager.argument("X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y", FloatArgumentType.floatArg())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val min = FloatArgumentType.getFloat(context, "X")
val max = FloatArgumentType.getFloat(context, "Y")
updateParam(id, access, ParamClasses.PairVec3f.Uniform(min, max))
successText(access, "Uniform", id, context)
Command.SINGLE_SUCCESS
}
)
)
)
.then(CommandManager.literal("Constant")
.then(CommandManager.argument("X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z", FloatArgumentType.floatArg())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val x = FloatArgumentType.getFloat(context, "X")
val y = FloatArgumentType.getFloat(context, "Y")
val z = FloatArgumentType.getFloat(context, "Z")
updateParam(id, access, ParamClasses.PairVec3f.Constant(Vector3f(x, y, z)))
successText(access, "Constant", id, context)
Command.SINGLE_SUCCESS
}
)
)
)
)
.then(CommandManager.literal("Null")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
updateParam(id, access, ParamClasses.PairVec3f.Null)
successText(access, "Null", id, context)
Command.SINGLE_SUCCESS
}
)
}
fun pairFloatArg(access: KProperty1<EmitterParams, ParamClasses.PairFloat>) : RequiredArgumentBuilder<ServerCommandSource, Float> {
return CommandManager.argument("Min Float", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Float", FloatArgumentType.floatArg())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val valueX = FloatArgumentType.getFloat(context, "Min Float")
val valueY = FloatArgumentType.getFloat(context, "Max Float")
updateParam(id, access, ParamClasses.PairFloat(valueX, valueY))
successText(access, "[$valueX, $valueY]", id, context)
Command.SINGLE_SUCCESS
}
)
}
fun transformWithVelArg(access: KProperty1<EmitterParams, ParamClasses.TransformWithVel>,
configArg: LiteralArgumentBuilder<ServerCommandSource>
) : LiteralArgumentBuilder<ServerCommandSource>
{
return configArg
.then(CommandManager.literal("RotOnly")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
updateParam(id, access, ParamClasses.TransformWithVel.RotOnly)
successText(access, "RotOnly", id, context)
Command.SINGLE_SUCCESS
}
)
.then(CommandManager.literal("ScaleAndRot")
.then(CommandManager.argument("Scale Multiplier", FloatArgumentType.floatArg())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val scaleMul = FloatArgumentType.getFloat(context, "Scale Multiplier")
updateParam(id, access, ParamClasses.TransformWithVel.ScaleAndRot(scaleMul))
successText(access, "ScaleAndRot", id, context)
Command.SINGLE_SUCCESS
}
)
)
.then(CommandManager.literal("Null")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
updateParam(id, access, ParamClasses.TransformWithVel.Null)
successText(access, "Null", id, context)
Command.SINGLE_SUCCESS
}
)
}
fun stringCurveArg(access: KProperty1<EmitterParams, ParamClasses.StringCurve>,
configArg: LiteralArgumentBuilder<ServerCommandSource>,
registryAccess: CommandRegistryAccess
) : LiteralArgumentBuilder<ServerCommandSource>
{
return configArg
.then(CommandManager.literal("Add")
.then(CommandManager.argument("Item", ItemStackArgumentType.itemStack(registryAccess))
.then(CommandManager.argument("Index", IntegerArgumentType.integer())
.executes { context ->
val index = IntegerArgumentType.getInteger(context, "Index")
stringCurveAddArg(context, access, index)
Command.SINGLE_SUCCESS
}
)
.executes { context ->
stringCurveAddArg(context, access, null)
Command.SINGLE_SUCCESS
}
)
)
.then(CommandManager.literal("Remove")
.then(CommandManager.argument("Index", IntegerArgumentType.integer())
.executes { context ->
val index = IntegerArgumentType.getInteger(context, "Index")
stringCurveRemoveArg(context, access, index)
Command.SINGLE_SUCCESS
}
)
.executes { context ->
stringCurveRemoveArg(context, access, null)
Command.SINGLE_SUCCESS
}
)
.then(CommandManager.literal("Curve")
.then(curveListArg("Curve")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val curveName = StringArgumentType.getString(context, "Curve")
val curve = stringToCurve(curveName)
val modelCurve = getEmitterDataById(id)?.modelCurve
if (curve != null && modelCurve != null) {
modelCurve.curve = curve
updateParam(id, access, modelCurve)
successText(access, curveName, id, context)
Command.SINGLE_SUCCESS
}
else {
negativeFeedback("Failed to update param \"modelCurve.curve\"! The curve is invalid!", context)
Command.SINGLE_SUCCESS
}
}
)
)
}
fun stringCurveAddArg(context: CommandContext<ServerCommandSource>,
access: KProperty1<EmitterParams, ParamClasses.StringCurve>,
index: Int?
) {
val id = StringArgumentType.getString(context, "Emitter ID")
val item = ItemStackArgumentType.getItemStackArgument(context, "Item").item
val itemId = Registries.ITEM.getId(item).toString()
val modelCurve = getEmitterDataById(id)?.modelCurve
if (index == null && modelCurve != null) {
modelCurve.array = modelCurve.array.toMutableList().apply { add(itemId) }.toTypedArray()
updateParam(id, access, modelCurve)
}
else if (modelCurve != null) {
modelCurve.array = modelCurve.array.toMutableList().apply { add(index!!.coerceIn(0, modelCurve.array.lastIndex), itemId) }.toTypedArray()
updateParam(id, access, modelCurve)
}
successText(access, "++$item", id, context)
}
fun stringCurveRemoveArg(context: CommandContext<ServerCommandSource>,
access: KProperty1<EmitterParams, ParamClasses.StringCurve>,
index: Int?
) {
val id = StringArgumentType.getString(context, "Emitter ID")
val modelCurve = getEmitterDataById(id)?.modelCurve
val item: String
if (index == null && modelCurve != null && modelCurve.array.isNotEmpty()) {
item = modelCurve.array.last()
modelCurve.array = modelCurve.array.toMutableList().apply { removeLast() }.toTypedArray()
updateParam(id, access, modelCurve)
}
else if (modelCurve != null && modelCurve.array.isNotEmpty()) {
item = modelCurve.array[index!!.coerceIn(0, modelCurve.array.lastIndex)]
modelCurve.array = modelCurve.array.toMutableList().apply { removeAt(index.coerceIn(0, modelCurve.array.lastIndex)) }.toTypedArray()
updateParam(id, access, modelCurve)
}
else {
negativeFeedback("Couldn't remove model. There are no models in the Emitter.", context)
return
}
successText(access, "--$item", id, context)
}
fun lerpValVec3fArg(access: KProperty1<EmitterParams,ParamClasses.LerpValVec3f>,
configArg: LiteralArgumentBuilder<ServerCommandSource>
) : LiteralArgumentBuilder<ServerCommandSource>
{
return configArg
.then(CommandManager.literal("LerpVec3f")
.then(CommandManager.argument("Min X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Z", FloatArgumentType.floatArg())
.then(curveListArg("Curve")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val minX = FloatArgumentType.getFloat(context, "Min X")
val minY = FloatArgumentType.getFloat(context, "Min Y")
val minZ = FloatArgumentType.getFloat(context, "Min Z")
val maxX = FloatArgumentType.getFloat(context, "Max X")
val maxY = FloatArgumentType.getFloat(context, "Max Y")
val maxZ = FloatArgumentType.getFloat(context, "Max Z")
val curveName = StringArgumentType.getString(context, "Curve")
val curve = stringToCurve(curveName)
updateParam(id, access, ParamClasses.LerpValVec3f.LerpVec3f(minX, minY, minZ, maxX, maxY, maxZ, curve!!))
successText(access, "LerpVec3f", id, context)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
)
)
.then(CommandManager.literal("MultiLerpVec3f")
.then(CommandManager.argument("Min X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Z", FloatArgumentType.floatArg())
.then(curveListArg("CurveX")
.then(curveListArg("CurveY")
.then(curveListArg("CurveZ")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val minX = FloatArgumentType.getFloat(context, "Min X")
val minY = FloatArgumentType.getFloat(context, "Min Y")
val minZ = FloatArgumentType.getFloat(context, "Min Z")
val maxX = FloatArgumentType.getFloat(context, "Max X")
val maxY = FloatArgumentType.getFloat(context, "Max Y")
val maxZ = FloatArgumentType.getFloat(context, "Max Z")
val curveNameX = StringArgumentType.getString(context, "CurveX")
val curveNameY = StringArgumentType.getString(context, "CurveY")
val curveNameZ = StringArgumentType.getString(context, "CurveZ")
val curveX = stringToCurve(curveNameX)
val curveY = stringToCurve(curveNameY)
val curveZ = stringToCurve(curveNameZ)
updateParam(id, access, ParamClasses.LerpValVec3f.MultiLerpVec3f(minX, minY, minZ, maxX, maxY, maxZ, curveX!!, curveY!!, curveZ!!))
successText(access, "MultiLerpVec3f", id, context)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
)
)
)
)
.then(CommandManager.literal("LerpUniform")
.then(CommandManager.argument("Min", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max", FloatArgumentType.floatArg())
.then(curveListArg("Curve")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val min = FloatArgumentType.getFloat(context, "Min")
val max = FloatArgumentType.getFloat(context, "Max")
val curveName = StringArgumentType.getString(context, "Curve")
val curve = stringToCurve(curveName)
updateParam(id, access, ParamClasses.LerpValVec3f.LerpUniform(min, max, curve!!))
successText(access, "LerpUniform", id, context)
Command.SINGLE_SUCCESS
}
)
)
)
)
.then(CommandManager.literal("Constant")
.then(CommandManager.argument("X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z", FloatArgumentType.floatArg())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val x = FloatArgumentType.getFloat(context, "X")
val y = FloatArgumentType.getFloat(context, "Y")
val z = FloatArgumentType.getFloat(context, "Z")
updateParam(id, access, ParamClasses.LerpValVec3f.Constant(Vector3f(x, y, z)))
successText(access, "Constant", id, context)
Command.SINGLE_SUCCESS
}
)
)
)
)
.then(CommandManager.literal("Null")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
updateParam(id, access, ParamClasses.LerpValVec3f.Null)
successText(access, "Null", id, context)
Command.SINGLE_SUCCESS
}
)
}
fun forceFieldArg(access: KProperty1<EmitterParams,ParamClasses.ForceFieldArray>,
configArg: LiteralArgumentBuilder<ServerCommandSource>
) : LiteralArgumentBuilder<ServerCommandSource>
{
return configArg
.then(CommandManager.literal("Add")
.then(CommandManager.argument("X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z", FloatArgumentType.floatArg())
.then(CommandManager.literal("Sphere")
.then(CommandManager.argument("Radius", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Force", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Force", FloatArgumentType.floatArg())
.then(CommandManager.argument("Index", IntegerArgumentType.integer())
.executes { context ->
val index = IntegerArgumentType.getInteger(context, "Index")
forceFieldAddArg(context, access, "Sphere", index)
Command.SINGLE_SUCCESS
}
)
.executes { context ->
forceFieldAddArg(context, access, "Sphere", null)
Command.SINGLE_SUCCESS
}
)
)
)
)
.then(CommandManager.literal("Cube")
.then(CommandManager.argument("Size X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Size Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Size Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Force X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Force Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Force Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Index", IntegerArgumentType.integer())
.executes { context ->
val index = IntegerArgumentType.getInteger(context, "Index")
forceFieldAddArg(context, access, "Cube", index)
Command.SINGLE_SUCCESS
}
)
.executes { context ->
forceFieldAddArg(context, access, "Cube", null)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
)
)
)
)
)
.then(CommandManager.literal("Remove")
.then(CommandManager.argument("Index", IntegerArgumentType.integer())
.executes { context ->
val index = IntegerArgumentType.getInteger(context, "Index")
forceFieldRemoveArg(context, access, index)
Command.SINGLE_SUCCESS
}
)
.executes { context ->
forceFieldRemoveArg(context, access, null)
Command.SINGLE_SUCCESS
}
)
}
fun forceFieldAddArg(context: CommandContext<ServerCommandSource>,
access: KProperty1<EmitterParams, ParamClasses.ForceFieldArray>,
shape: String,
index: Int?
) {
val id = StringArgumentType.getString(context, "Emitter ID")
val x = FloatArgumentType.getFloat(context, "X")
val y = FloatArgumentType.getFloat(context, "Y")
val z = FloatArgumentType.getFloat(context, "Z")
val forceFields = getEmitterDataById(id)?.forceFields
var forceField: ForceField? = null
if (shape == "Sphere") {
val radius = FloatArgumentType.getFloat(context, "Radius")
val minForce = FloatArgumentType.getFloat(context, "Min Force")
val maxForce = FloatArgumentType.getFloat(context, "Max Force")
forceField = ForceField(
Vector3f(x, y, z),
ForceFieldShape.Sphere(radius, Pair(minForce, maxForce))
)
}
else if (shape == "Cube") {
val sizeX = FloatArgumentType.getFloat(context, "Size X")
val sizeY = FloatArgumentType.getFloat(context, "Size Y")
val sizeZ = FloatArgumentType.getFloat(context, "Size Z")
val forceX = FloatArgumentType.getFloat(context, "Force X")
val forceY = FloatArgumentType.getFloat(context, "Force Y")
val forceZ = FloatArgumentType.getFloat(context, "Force Z")
forceField = ForceField(
Vector3f(x, y, z),
ForceFieldShape.Cube(Vector3f(sizeX, sizeY, sizeZ), Vector3f(forceX, forceY, forceZ))
)
}
if (forceFields != null && forceField != null) {
if (index == null) {
forceFields.array = forceFields.array.toMutableList().apply { add(forceField) }.toTypedArray()
}
else {
val newIndex = index.coerceIn(0, forceFields.array.size)
forceFields.array = forceFields.array.toMutableList().apply { add(newIndex, forceField) }.toTypedArray()
}
val calcIndex = index?.coerceIn(0, forceFields.array.size)
updateParam(id, access, forceFields)
successText(access, "++ForceField at $calcIndex", id, context)
}
else {
negativeFeedback("Failed to update param \"forceFields\"! The Emitter ID is most likely invalid!", context)
}
}
fun forceFieldRemoveArg(context: CommandContext<ServerCommandSource>,
access: KProperty1<EmitterParams, ParamClasses.ForceFieldArray>,
index: Int?
) {
val id = StringArgumentType.getString(context, "Emitter ID")
val forceFields = getEmitterDataById(id)?.forceFields
if (index == null && forceFields != null && forceFields.array.isNotEmpty()) {
forceFields.array = forceFields.array.toMutableList().apply { removeLast() }.toTypedArray()
updateParam(id, access, forceFields)
}
else if (forceFields != null && forceFields.array.isNotEmpty()) {
val newIndex = index?.coerceIn(0, forceFields.array.lastIndex) ?: forceFields.array.lastIndex
forceFields.array = forceFields.array.toMutableList().apply { removeAt(newIndex) }.toTypedArray()
updateParam(id, access, forceFields)
}
else {
negativeFeedback("Couldn't remove ForceField. There are no ForceFields in the Emitter.", context)
return
}
val calcIndex = index?.coerceIn(0, forceFields.array.size) ?: forceFields.array.size
successText(access, "--ForceField at $calcIndex", id, context)
}
fun lerpValIntArg(access: KProperty1<EmitterParams, ParamClasses.LerpValInt>,
configArg: LiteralArgumentBuilder<ServerCommandSource>
) : LiteralArgumentBuilder<ServerCommandSource>
{
return configArg
.then(CommandManager.literal("LerpInt")
.then(CommandManager.argument("From", IntegerArgumentType.integer())
.then(CommandManager.argument("To", IntegerArgumentType.integer())
.then(curveListArg("Curve")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val from = IntegerArgumentType.getInteger(context, "From")
val to = IntegerArgumentType.getInteger(context, "To")
val curveName = StringArgumentType.getString(context, "Curve")
val curve = stringToCurve(curveName)
updateParam(id, access, ParamClasses.LerpValInt.LerpInt(from, to, curve!!))
successText(access, "LerpInt", id, context)
Command.SINGLE_SUCCESS
}
)
)
)
)
.then(CommandManager.literal("Constant")
.then(CommandManager.argument("Value", IntegerArgumentType.integer())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val value = IntegerArgumentType.getInteger(context, "Value")
updateParam(id, access, ParamClasses.LerpValInt.Constant(value))
successText(access, "Constant", id, context)
Command.SINGLE_SUCCESS
}
)
)
.then(CommandManager.literal("Null")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
updateParam(id, access, ParamClasses.LerpValInt.Null)
successText(access, "Null", id, context)
Command.SINGLE_SUCCESS
}
)
}
@@ -1,12 +1,16 @@
package com.bytedice.bde_particles.commands
import com.bytedice.bde_particles.items.ParticleEmitterTool
import com.bytedice.bde_particles.particles.emitterIdRegister
import com.bytedice.bde_particles.particles.idRegister
import com.mojang.brigadier.Command
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.context.CommandContext
import net.minecraft.command.CommandRegistryAccess
import net.minecraft.command.argument.ItemStackArgumentType
import net.minecraft.item.Item
import net.minecraft.item.ItemStack
import net.minecraft.item.Items
import net.minecraft.server.command.CommandManager
import net.minecraft.server.command.ServerCommandSource
import net.minecraft.text.Style
@@ -18,9 +22,9 @@ import java.util.concurrent.CompletableFuture
object GiveEmitterTool {
fun register(dispatcher: CommandDispatcher<ServerCommandSource>, registryAccess: CommandRegistryAccess) {
val command = CommandManager.literal("GiveEmitterTool")
.requires { source -> source.hasPermissionLevel(4) }
.requires { source -> source.hasPermissionLevel(2) }
val allEmitterIds = emitterIdRegister.keys
val allEmitterIds = idRegister.keys
val itemNameArg = CommandManager.argument("Item Name", StringArgumentType.string())
val itemTypeArg = CommandManager.argument("Item Type", ItemStackArgumentType.itemStack(registryAccess))
@@ -33,26 +37,42 @@ object GiveEmitterTool {
CompletableFuture.completedFuture(builder.build())
}
dispatcher.register(
command
.then(itemNameArg
.then(itemTypeArg
dispatcher.register(command
.then(emitterIdSuggestion
.then(itemTypeArg
.then(itemNameArg
.executes { context ->
val name = StringArgumentType.getString(context, "Item Name")
val item = ItemStackArgumentType.getItemStackArgument(context, "Item Type")
val emitterId = StringArgumentType.getString(context, "Emitter ID")
val feedback = Text.literal("BPS - You like fancy particles. Don't you?\nBPS - gave particle emitter, bound to ID: \"${emitterId}\".")
.setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(0, 200, 0).rgb)))
exec(context, emitterId, item.item, name)
Command.SINGLE_SUCCESS
}
)
.executes { context ->
val item = ItemStackArgumentType.getItemStackArgument(context, "Item Type")
val emitterId = StringArgumentType.getString(context, "Emitter ID")
context.source.player?.inventory?.insertStack(ParticleEmitterTool.makeData(item.item, name, emitterId))
context.source.sendFeedback({ feedback }, false)
exec(context, emitterId, item.item, emitterId)
Command.SINGLE_SUCCESS
}
)
.executes { context ->
val emitterId = StringArgumentType.getString(context, "Emitter ID")
exec(context, emitterId, Items.AMETHYST_SHARD, emitterId)
Command.SINGLE_SUCCESS
}
)
)
)
)
}
private fun exec(context: CommandContext<ServerCommandSource>, emitterId: String, itemType: Item, itemName: String) {
val feedback = Text.literal("BPS - You like fancy particles. Don't you?\nBPS - gave particle emitter, bound to ID: \"${emitterId}\".")
.setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(0, 200, 0).rgb)))
context.source.player?.inventory?.insertStack(ParticleEmitterTool.makeData(itemType, itemName, emitterId))
context.source.sendFeedback({ feedback }, false)
}
}
@@ -16,11 +16,13 @@ object KillAllEmitters {
dispatcher.register(
command.executes { context ->
var amountEmitters = 0
for (emitter in ALL_PARTICLE_EMITTERS) {
emitter.kill()
amountEmitters += 1
}
val feedback = Text.literal("BPS - Killed all living Emitters.")
val feedback = Text.literal("BPS - Killed all living Emitters. [$amountEmitters]")
.setStyle(Style.EMPTY.withColor(Color(0, 200, 0).rgb))
context.source.sendFeedback({ feedback }, false)
@@ -1,602 +0,0 @@
package com.bytedice.bde_particles.commands
import com.bytedice.bde_particles.InterpolationCurves
import com.bytedice.bde_particles.particles.*
import com.mojang.brigadier.Command
import com.mojang.brigadier.arguments.BoolArgumentType
import com.mojang.brigadier.arguments.FloatArgumentType
import com.mojang.brigadier.arguments.IntegerArgumentType
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import com.mojang.brigadier.context.CommandContext
import net.minecraft.server.command.CommandManager
import net.minecraft.server.command.ServerCommandSource
import net.minecraft.text.MutableText
import net.minecraft.text.Style
import net.minecraft.text.Text
import org.joml.Vector2f
import org.joml.Vector3f
import java.awt.Color
import java.util.concurrent.CompletableFuture
// CLOSE THIS FILE IMMEDIATELY, YOU ARE ENTERING EGYPTIAN (pyramid) AND ITALIAN (spaghetti) TERRITORY
val curves = arrayOf("LINEAR", "SQRT", "EXPONENT", "CUBIC", "SINE", "COSINE", "INVERSE", "LOG", "EXP", "BOUNCE")
fun updateEmitterParam(context: CommandContext<ServerCommandSource>, paramName: String, typeValue: Any) : MutableText {
val emitterIdVal = StringArgumentType.getString(context, "Registered Emitter ID")
val success = updateSingleEmitterParam(emitterIdVal, paramName, typeValue)
val feedback = if (success) {
Text.literal("BPS - Successfully updated Emitter ID \"$emitterIdVal\": Parameter \"$paramName\", value \"$typeValue\"")
.setStyle(Style.EMPTY.withColor(Color(0, 200, 0).rgb))
}
else {
Text.literal("BPS - Failed to update Emitter ID \"$emitterIdVal\": Parameter \"$paramName\", value \"$typeValue\": Unknown Error\n" +
"Try checking if the emitter and parameter exist, and the value is the correct type.")
.setStyle(Style.EMPTY.withColor(Color(200, 0, 0).rgb))
}
return feedback
}
fun updateParticleParam(context: CommandContext<ServerCommandSource>, paramName: String, typeValue: Any) : MutableText {
val emitterIdVal = StringArgumentType.getString(context, "Registered Emitter ID")
val success = updateSingleParticleParam(emitterIdVal, paramName, typeValue)
val feedback = if (success) {
Text.literal("BPS - Successfully updated Particle of Emitter ID \"$emitterIdVal\": parameter \"$paramName\", value \"$typeValue\"")
.setStyle(Style.EMPTY.withColor(Color(0, 200, 0).rgb))
}
else {
Text.literal("BPS - Failed to update Emitter ID \"$emitterIdVal\": parameter \"$paramName\", value \"$typeValue\": Unknown Error\n" +
"Try checking if the Emitter ID and parameter exist, and the value is the correct type.")
.setStyle(Style.EMPTY.withColor(Color(200, 0, 0).rgb))
}
return feedback
}
class ArgConfigEmitterKeys {
fun maxCount() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("maxCount")
.then(CommandManager.argument("Int", IntegerArgumentType.integer())
.executes { context ->
val typeValue = IntegerArgumentType.getInteger(context, "Int")
val feedback = updateEmitterParam(context, "maxCount", typeValue)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
fun spawnsPerTick() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("spawnsPerTick")
.then(CommandManager.argument("Int", IntegerArgumentType.integer())
.executes { context ->
val typeValue = IntegerArgumentType.getInteger(context, "Int")
val feedback = updateEmitterParam(context, "spawnsPerTick", typeValue)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
fun loopDur() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("loopDur")
.then(CommandManager.argument("Int", IntegerArgumentType.integer())
.executes { context ->
val typeValue = IntegerArgumentType.getInteger(context, "Int")
val feedback = updateEmitterParam(context, "loopDur", typeValue)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
fun loopDelay() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("loopDelay")
.then(CommandManager.argument("Int", IntegerArgumentType.integer())
.executes { context ->
val typeValue = IntegerArgumentType.getInteger(context, "Int")
val feedback = updateEmitterParam(context, "loopDelay", typeValue)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
fun loopCount() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("loopCount")
.then(CommandManager.argument("Int", IntegerArgumentType.integer())
.executes { context ->
val typeValue = IntegerArgumentType.getInteger(context, "Int")
val feedback = updateEmitterParam(context, "loopCount", typeValue)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
}
class ArgConfigParticleKeys {
fun shape() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("shape")
.then(CommandManager.literal("circle")
.then(CommandManager.argument("Radius", FloatArgumentType.floatArg())
.executes { context ->
val radius = FloatArgumentType.getFloat(context, "Radius")
val feedback = updateParticleParam(context, "shape", SpawningShape.CIRCLE(radius))
context.source.sendFeedback( { feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
.then(CommandManager.literal("rect")
.then(CommandManager.argument("Width", FloatArgumentType.floatArg())
.then(CommandManager.argument("Height", FloatArgumentType.floatArg())
.executes { context ->
val w = FloatArgumentType.getFloat(context, "Width")
val h = FloatArgumentType.getFloat(context, "Height")
val feedback = updateParticleParam(context, "shape", SpawningShape.RECT(Vector2f(w, h)))
context.source.sendFeedback( { feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
)
.then(CommandManager.literal("cube")
.then(CommandManager.argument("X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z", FloatArgumentType.floatArg())
.executes { context ->
val x = FloatArgumentType.getFloat(context, "X")
val y = FloatArgumentType.getFloat(context, "Y")
val z = FloatArgumentType.getFloat(context, "Z")
val feedback = updateParticleParam(context, "shape", SpawningShape.CUBE(Vector3f(x, y, z)))
context.source.sendFeedback( { feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
)
)
.then(CommandManager.literal("sphere")
.then(CommandManager.argument("Radius", FloatArgumentType.floatArg())
.executes { context ->
val radius = FloatArgumentType.getFloat(context, "Radius")
val feedback = updateParticleParam(context, "shape", SpawningShape.SPHERE(radius))
context.source.sendFeedback( { feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
.then(CommandManager.literal("point")
.executes { context ->
val feedback = updateParticleParam(context, "shape", SpawningShape.POINT)
context.source.sendFeedback( { feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
fun blockCurve() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("blockCurve")
.then(CommandManager.argument("Array (separate by comma)", StringArgumentType.string())
.executes { context ->
val typeValue = StringArgumentType.getString(context, "Array (separate by comma)")
val typeArray = typeValue.split(",").toTypedArray()
val feedback = updateParticleParam(context, "blockCurve", typeArray)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
fun rotRandom() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("rotRandom")
.then(CommandManager.argument("Min X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Z", FloatArgumentType.floatArg())
.executes { context ->
val vectors = x1x2ToPairVector3f(context)
val feedback = updateParticleParam(context, "rotRandom", vectors)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
}
fun rotVelRandom() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("rotVelRandom")
.then(CommandManager.argument("Min X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Z", FloatArgumentType.floatArg())
.executes { context ->
val vectors = x1x2ToPairVector3f(context)
val feedback = updateParticleParam(context, "rotVelRandom", vectors)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
}
fun sizeRandom() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("sizeRandom")
.then(CommandManager.argument("Min X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Z", FloatArgumentType.floatArg())
.executes { context ->
val vectors = x1x2ToPairVector3f(context)
val feedback = updateParticleParam(context, "sizeRandom", vectors)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
}
fun uniformSize() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("uniformSize")
.then(CommandManager.argument("Bool", BoolArgumentType.bool())
.executes { context ->
val uniform = BoolArgumentType.getBool(context, "Bool")
val feedback = updateParticleParam(context, "uniformSize", uniform)
context.source.sendFeedback( { feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
fun velRandom() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("velRandom")
.then(CommandManager.argument("Min X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Z", FloatArgumentType.floatArg())
.executes { context ->
val vectors = x1x2ToPairVector3f(context)
val feedback = updateParticleParam(context, "velRandom", vectors)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
}
fun forceFields() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("forceFields")
.then(argAddForceField())
.then(argRemoveForceField())
}
fun gravity() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("gravity")
.then(CommandManager.argument("X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z", FloatArgumentType.floatArg())
.executes { context ->
val x1 = FloatArgumentType.getFloat(context, "X")
val y1 = FloatArgumentType.getFloat(context, "Y")
val z1 = FloatArgumentType.getFloat(context, "Z")
val feedback = updateParticleParam(context, "gravity", Vector3f(x1, y1, z1))
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
)
}
fun drag() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("drag")
.then(CommandManager.argument("Float", FloatArgumentType.floatArg())
.executes { context ->
val value = FloatArgumentType.getFloat(context, "Float")
val feedback = updateParticleParam(context, "drag", value)
context.source.sendFeedback( { feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
fun minVel() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("minVel")
.then(CommandManager.argument("Float", FloatArgumentType.floatArg())
.executes { context ->
val value = FloatArgumentType.getFloat(context, "Float")
val feedback = updateParticleParam(context, "minVel", value)
context.source.sendFeedback( { feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
fun lifeTime() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("lifeTime")
.then(CommandManager.argument("min", IntegerArgumentType.integer())
.then(CommandManager.argument("max", IntegerArgumentType.integer())
.executes { context ->
val min = IntegerArgumentType.getInteger(context, "min")
val max = IntegerArgumentType.getInteger(context, "max")
val feedback = updateParticleParam(context, "lifeTime", Pair(min, max))
context.source.sendFeedback( { feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
}
fun rotVelCurve() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("rotVelCurve")
.then(CommandManager.argument("Min X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Curve", StringArgumentType.string())
.suggests { _, builder ->
curves.forEach { builder.suggest(it) }
CompletableFuture.completedFuture(builder.build())
}
.executes { context ->
val vectors = x1x2ToPairVector3f(context)
val curveString = StringArgumentType.getString(context, "Curve")
val feedback = updateParticleParam(
context,
"rotVelCurve",
Triple(vectors.first, vectors.second, getCurveByString(curveString)))
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
)
}
fun sizeCurve() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("sizeCurve")
.then(CommandManager.argument("Min X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Curve", StringArgumentType.string())
.suggests { _, builder ->
curves.forEach { builder.suggest(it) }
CompletableFuture.completedFuture(builder.build())
}
.executes { context ->
val vectors = x1x2ToPairVector3f(context)
val curveString = StringArgumentType.getString(context, "Curve")
val feedback = updateParticleParam(
context,
"sizeCurve",
Triple(vectors.first, vectors.second, getCurveByString(curveString)))
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
)
}
}
fun getCurveByString(str: String) : InterpolationCurves {
return when (str) {
"LINEAR" -> InterpolationCurves.LINEAR
"SQRT" -> InterpolationCurves.SQRT
"EXPONENT" -> InterpolationCurves.EXPONENT
"CUBIC" -> InterpolationCurves.CUBIC
"SINE" -> InterpolationCurves.SINE
"COSINE" -> InterpolationCurves.COSINE
"INVERSE" -> InterpolationCurves.INVERSE
"LOG" -> InterpolationCurves.LOG
"EXP" -> InterpolationCurves.EXP
"BOUNCE" -> InterpolationCurves.BOUNCE
else -> InterpolationCurves.LINEAR
}
}
fun x1x2ToPairVector3f(context: CommandContext<ServerCommandSource>) : Pair<Vector3f, Vector3f> {
val x1 = FloatArgumentType.getFloat(context, "Min X")
val y1 = FloatArgumentType.getFloat(context, "Min Y")
val z1 = FloatArgumentType.getFloat(context, "Min Z")
val x2 = FloatArgumentType.getFloat(context, "Max X")
val y2 = FloatArgumentType.getFloat(context, "Max Y")
val z2 = FloatArgumentType.getFloat(context, "Max Z")
return Pair(Vector3f(x1, y1, z1), Vector3f(x2, y2, z2))
}
fun argAddForceField() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("add")
.then(CommandManager.argument("Name", StringArgumentType.string())
.then(CommandManager.argument("X Pos", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y Pos", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z Pos", FloatArgumentType.floatArg())
.then(CommandManager.literal("sphere")
.then(CommandManager.argument("Radius", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Force", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Force", FloatArgumentType.floatArg())
.executes { context -> forceFieldAddSphereExec(context) }
)
)
)
)
.then(CommandManager.literal("cube")
.then(CommandManager.argument("X Size", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y Size", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z Size", FloatArgumentType.floatArg())
.then(CommandManager.argument("X Force", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y Force", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z Force", FloatArgumentType.floatArg())
.executes { context -> forceFieldAddCubeExec(context) }
)
)
)
)
)
)
)
)
)
)
)
}
fun argRemoveForceField() : LiteralArgumentBuilder<ServerCommandSource> { // TODO fix this error somehow
return CommandManager.literal("remove")
.then(CommandManager.argument("Force Field Name", StringArgumentType.string())
.suggests { context, builder ->
val emitterIdVal = StringArgumentType.getString(context, "Registered Emitter ID")
val params = getEmitterParams(emitterIdVal)
val forceFields = params!!.particle.forceFields
for (forceField in forceFields) {
builder.suggest(forceField.name)
}
CompletableFuture.completedFuture(builder.build())
}
.executes { context ->
val ffName = StringArgumentType.getString(context, "Force Field Name")
val emitterIdVal = StringArgumentType.getString(context, "Registered Emitter ID")
val params = getEmitterParams(emitterIdVal)
val forceFields = params!!.particle.forceFields
val newForceFields: MutableList<ForceField> = forceFields.toMutableList()
if (forceFields.isEmpty()) {
val feedback = Text.literal("BPS - There are no force fields for Emitter ID \"$emitterIdVal\".")
.setStyle(Style.EMPTY.withColor(Color(200, 0, 0).rgb))
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
else {
for (forceField in forceFields) {
if (forceField.name == ffName) {
newForceFields.apply { remove(forceField) }
}
}
val feedback = updateParticleParam(
context,
"forceFields",
newForceFields.toTypedArray()
)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
}
)
}
fun forceFieldAddSphereExec(context: CommandContext<ServerCommandSource>) : Int {
val emitterIdVal = StringArgumentType.getString(context, "Registered Emitter ID")
val radius = FloatArgumentType.getFloat(context, "Radius")
val minForce = FloatArgumentType.getFloat(context, "Min Force")
val maxForce = FloatArgumentType.getFloat(context, "Max Force")
val shape = ForceFieldShape.SPHERE(radius, Pair(minForce, maxForce))
val forceField = forceFieldGenericParams(context, shape)
val currentParams = getEmitterParams(emitterIdVal)
val forceFields = currentParams!!.particle.forceFields
val addedArray = forceFields.toMutableList().apply { add(forceField) }.toTypedArray()
val feedback = updateParticleParam(
context,
"forceFields",
addedArray
)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
return Command.SINGLE_SUCCESS
}
fun forceFieldAddCubeExec(context: CommandContext<ServerCommandSource>) : Int {
val xSize = FloatArgumentType.getFloat(context, "X Size")
val ySize = FloatArgumentType.getFloat(context, "Y Size")
val zSize = FloatArgumentType.getFloat(context, "Z Size")
val xForce = FloatArgumentType.getFloat(context, "X Force")
val yForce = FloatArgumentType.getFloat(context, "Y Force")
val zForce = FloatArgumentType.getFloat(context, "Z Force")
val emitterIdVal = StringArgumentType.getString(context, "Registered Emitter ID")
val shape = ForceFieldShape.CUBE(Vector3f(xSize, ySize, zSize), Vector3f(xForce, yForce, zForce))
val forceField = forceFieldGenericParams(context, shape)
val currentParams = getEmitterParams(emitterIdVal)
val forceFields = currentParams!!.particle.forceFields
val addedArray = forceFields.toMutableList().apply { add(forceField) }.toTypedArray()
val feedback = updateParticleParam(
context,
"forceFields",
addedArray
)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
return Command.SINGLE_SUCCESS
}
fun forceFieldGenericParams(context: CommandContext<ServerCommandSource>, shape: ForceFieldShape) : ForceField {
val name = StringArgumentType.getString(context, "Name")
val posX = FloatArgumentType.getFloat(context, "X Pos")
val posY = FloatArgumentType.getFloat(context, "Y Pos")
val posZ = FloatArgumentType.getFloat(context, "Z Pos")
return ForceField(
name.replace(" ", "_"),
Vector3f(posX, posY, posZ),
shape
)
}
@@ -1,30 +1,58 @@
package com.bytedice.bde_particles.commands
import com.bytedice.bde_particles.emitterParamsToJson
import com.bytedice.bde_particles.emitterListArg
import com.bytedice.bde_particles.negativeFeedback
import com.bytedice.bde_particles.particles.*
import com.bytedice.bde_particles.positiveFeedback
import com.mojang.brigadier.Command
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.IntegerArgumentType
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import com.mojang.brigadier.builder.RequiredArgumentBuilder
import net.minecraft.component.DataComponentTypes
import net.minecraft.component.type.NbtComponent
import net.minecraft.item.ItemStack
import net.minecraft.item.Items
import net.minecraft.nbt.NbtCompound
import com.mojang.brigadier.context.CommandContext
import net.minecraft.command.CommandRegistryAccess
import net.minecraft.server.command.CommandManager
import net.minecraft.server.command.ServerCommandSource
import net.minecraft.text.MutableText
import net.minecraft.text.Style
import net.minecraft.text.Text
import net.minecraft.text.TextColor
import java.awt.Color
import java.util.concurrent.CompletableFuture
// TODO: completely redesign
/*
// spawning
maxCount: Int,
spawnRate: Int,
spawnChance: Float,
spawnDuration: ParamClasses.Duration,
spawnPosOffset: Vector3f,
lifeTime: ParamClasses.PairInt,
shape: SpawningShape,
// init transforms
offset: ParamClasses.PairVec3f,
initRot: ParamClasses.PairVec3f,
rotVel: ParamClasses.PairVec3f,
rotWithVel: Boolean,
initVel: ParamClasses.PairVec3f,
initCenterVel: ParamClasses.PairFloat,
initScale: ParamClasses.PairVec3f,
scaleWithVel: Boolean,
// velocity
forceFields: Array<ForceField>,
constVel: Vector3f,
drag: Float,
minVel: Float,
// curves
offsetCurve: ParamClasses.LerpVal,
rotVelCurve: ParamClasses.LerpVal,
scaleCurve: ParamClasses.LerpVal,
blockCurve: Pair<Array<String>, LerpCurves>,
*/
// ManageEmitters
// <create / remove / config / list>
// <create / remove / config / list / copy>
// create
// <emitter id>
// <preset emitter id>
@@ -36,9 +64,8 @@ import java.util.concurrent.CompletableFuture
// config
// <emitter id>
// <PARTICLE / EMITTER>
// <param key> // HARDCODED KEYS, if EMITTER use emitter params.
// <new param value> // HARDCODED TYPES
// <param key>
// <new param value>
// output -> update the selected parameter of the selected emitter id to the new value
// list
@@ -50,219 +77,121 @@ import java.util.concurrent.CompletableFuture
object ManageEmitters {
fun register(dispatcher: CommandDispatcher<ServerCommandSource>) {
fun register(dispatcher: CommandDispatcher<ServerCommandSource>, registryAccess: CommandRegistryAccess) {
val command = CommandManager.literal("ManageEmitters")
.requires { source -> source.hasPermissionLevel(4) }
.requires { source -> source.hasPermissionLevel(2) }
dispatcher.register(
command
// <create / remove / config / list / copy>
.then(argCreate())
.then(argRemove())
.then(argConfig())
.then(argList())
.then(argCopy())
)
}
}
val allEmitterIds = emitterIdRegister.keys
// TODO: perhaps make this a Command.literal() if possible
val emitterIdSuggestion: RequiredArgumentBuilder<ServerCommandSource, String> = CommandManager.argument("Registered Emitter ID", StringArgumentType.string())
.suggests { _, builder ->
allEmitterIds.forEach { key ->
builder.suggest(key)
}
CompletableFuture.completedFuture(builder.build())
}
val emitterConfigKeys = ArgConfigEmitterKeys()
val particleConfigKeys = ArgConfigParticleKeys()
// create
// <emitter id>
// <preset emitter id>
// output -> register new emitter with a preset
fun argCreate() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("create")
.then(CommandManager.argument("Emitter ID", StringArgumentType.string())
.then(emitterIdSuggestion
.then(CommandManager.literal("create").then(create()))
.then(CommandManager.literal("remove").then(remove()))
.then(CommandManager.literal("config").then(config(registryAccess)))
.then(CommandManager.literal("list")
.executes { context ->
val emitterIdVal = StringArgumentType.getString(context, "Emitter ID")
val emitterPresetVal = StringArgumentType.getString(context, "Registered Emitter ID") ?: "DEFAULT"
val emitterPresetParams = getEmitterParams(emitterPresetVal)
val emitterIdList = idRegister.keys.sorted().joinToString(" ")
var newEmitterId = "NULL"
val emitterRegisterData: Pair<String, Boolean>
if (emitterPresetParams != null) {
emitterRegisterData = addToEmitterRegister(emitterIdVal, emitterPresetParams)
newEmitterId = emitterRegisterData.first
}
else { emitterRegisterData = Pair("NULL", false) }
val feedback = if (emitterRegisterData.second) {
Text.literal("BPS - Created particle emitter with ID \"$newEmitterId\".\n" +
"BPS - This emitter will be removed on server restart! Use \"/ManageEmitters copy\" to save the data to your clipboard!")
.setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(0, 200, 0).rgb)))
}
else if (newEmitterId !in allEmitterIds) {
Text.literal("BPS - Preset Emitter ID \"$emitterPresetVal\" doesn't exist!\n" +
"BPS - Use \"/ManageEmitters list\" to view all Emitter ID's.")
.setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(200, 0, 0).rgb)))
}
else {
Text.literal("BPS - Emitter ID \"$newEmitterId\" already exists\n" +
"BPS - Use \"/ManageEmitters list\" to view all Emitter ID's.")
.setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(200, 0, 0).rgb)))
}
context.source.sendFeedback( { feedback }, false )
Command.SINGLE_SUCCESS
}
)
)
}
// remove
// <emitter id>
// output -> removes the particle with that id
fun argRemove() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("remove")
.then(emitterIdSuggestion
.executes { context ->
val emitterIdVal = StringArgumentType.getString(context, "Registered Emitter ID") ?: "NULL"
val isRemoved = removeFromEmitterRegister(emitterIdVal)
val feedback = if (isRemoved) {
Text.literal("BPS - Removed particle emitter with ID \"$emitterIdVal\".")
.setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(0, 200, 0).rgb)))
}
else {
Text.literal("BPS - Emitter ID \"$emitterIdVal\" doesn't exist or is reserved!\n" +
"BPS - Use \"/ManageEmitters list\" to view all Emitter ID's.")
.setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(200, 0, 0).rgb)))
}
context.source.sendFeedback( { feedback }, false )
Command.SINGLE_SUCCESS
}
)
}
// list
// output -> all registered emitters
fun argList() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("list")
.executes { context ->
val allEmittersString = allEmitterIds.joinToString(", ")
val feedbackStatic = Text.literal("BPS - List of all available Emitter IDs:\n")
.setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(0, 200, 0).rgb)))
val feedbackVar = Text.literal(allEmittersString)
.setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(255, 255, 255).rgb)))
val feedback = feedbackStatic.append(feedbackVar)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
}
// copy
// <emitter id>
// output -> give player a command block with the particle params (should be paste-able in kotlin)
fun argCopy() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("copy")
.then(emitterIdSuggestion
.executes { context ->
val emitterIdVal = StringArgumentType.getString(context, "Registered Emitter ID")
val commandBlock = makeCommandBlock(
emitterParamsToJson(
getEmitterParams(
emitterIdVal
)!!
)
)
val feedback: MutableText
if (context.source.server.areCommandBlocksEnabled()) {
feedback = Text.literal("BPS - Copied Emitter data as a JSON. Place down the command block and copy it to your clipboard to use the value.")
val feedback = Text.literal("List of all registered Emitter IDs:\n$emitterIdList")
.setStyle(Style.EMPTY.withColor(Color(0, 200, 0).rgb))
}
else {
feedback = Text.literal("BPS - Could not copy Emitter data: Command blocks aren't enabled. Go to server.properties and enable command blocks before retrying.")
.setStyle(Style.EMPTY.withColor(Color(200, 0, 0).rgb))
Command.SINGLE_SUCCESS
}
context.source.player?.inventory?.insertStack(commandBlock)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
// config
// <emitter id>
// <PARTICLE / EMITTER>
// <param key> // HARDCODED KEYS, if EMITTER use emitter params.
// <new param value> // HARDCODED TYPES
// output -> update the selected parameter of the selected emitter id to the new value
fun argConfig() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("config")
.then(emitterIdSuggestion
.then(CommandManager.literal("emitter")
.then(emitterConfigKeys.maxCount())
.then(emitterConfigKeys.spawnsPerTick())
.then(emitterConfigKeys.loopDur())
.then(emitterConfigKeys.loopDelay())
.then(emitterConfigKeys.loopCount())
)
.then(CommandManager.literal("particle")
.then(particleConfigKeys.shape())
.then(particleConfigKeys.blockCurve())
.then(particleConfigKeys.rotRandom())
.then(particleConfigKeys.rotVelRandom())
.then(particleConfigKeys.sizeRandom())
.then(particleConfigKeys.uniformSize())
.then(particleConfigKeys.velRandom())
.then(particleConfigKeys.forceFields())
.then(particleConfigKeys.gravity())
.then(particleConfigKeys.drag())
.then(particleConfigKeys.minVel())
.then(particleConfigKeys.lifeTime())
.then(particleConfigKeys.rotVelCurve())
.then(particleConfigKeys.sizeCurve())
)
//.then(CommandManager.literal("copy"))
)
}
private fun create() : RequiredArgumentBuilder<ServerCommandSource, String> {
return CommandManager.argument("New Emitter ID", StringArgumentType.string())
.then(emitterListArg("Preset Emitter ID")
.executes { context ->
val newEmitterId = StringArgumentType.getString(context, "New Emitter ID")
val presetEmitterId = StringArgumentType.getString(context, "Preset Emitter ID")
fun makeCommandBlock(emitterData: Map<String, Any>): ItemStack {
val i = ItemStack(Items.COMMAND_BLOCK)
val nbt = NbtCompound()
nbt.putString("Command", emitterData.toString())
nbt.putString("id", "minecraft:command_block")
nbt.putByte("powered", 0)
nbt.putByte("auto", 0)
nbt.putByte("UpdateLastExecution", 1)
nbt.putByte("conditionMet", 0)
nbt.putByte("TrackOutput", 1)
nbt.putInt("SuccessCount", 0)
val component: NbtComponent = NbtComponent.of(nbt)
i.set(DataComponentTypes.BLOCK_ENTITY_DATA, component)
return i
createExec(context, newEmitterId, presetEmitterId)
Command.SINGLE_SUCCESS
}
)
.executes { context ->
val newEmitterId = StringArgumentType.getString(context, "New Emitter ID")
createExec(context, newEmitterId, "")
Command.SINGLE_SUCCESS
}
}
private fun createExec(context: CommandContext<ServerCommandSource>, newEmitterId: String, presetEmitterId: String) {
val params = getEmitterDataById(presetEmitterId)
val usedDefault = params == null
val result = addToRegister(
newEmitterId,
(params ?: EmitterParams.DEFAULT).deepCopy(),
)
if (usedDefault && result.second) {
positiveFeedback(
"Successfully added new Emitter with ID \"${result.first}\" to register.\n" +
"(DEFAULT preset was used because the specified Preset Emitter ID wasn't valid.)",
context
)
}
else if (!usedDefault && result.second) {
positiveFeedback(
"Successfully added new Emitter with ID \"${result.first}\" and Preset Emitter ID \"$presetEmitterId\" to register.",
context
)
}
else {
negativeFeedback(
"Failed to add new Emitter with ID \"${result.first}\" to register.\n" +
"The provided Emitter ID is either reserved for system use or already in use.",
context
)
}
}
private fun remove() : RequiredArgumentBuilder<ServerCommandSource, String> {
return emitterListArg("Emitter ID")
.executes { context ->
val emitterId = StringArgumentType.getString(context, "Emitter ID")
val result = removeFromRegister(emitterId)
if (result) {
positiveFeedback(
"Successfully removed Emitter ID \"$emitterId\" from register.",
context
)
}
else {
negativeFeedback(
"Failed to remove Emitter ID \"$emitterId\" from register.\nThe provided Emitter ID is either reserved for system use or already in use.",
context
)
}
Command.SINGLE_SUCCESS
}
}
private fun config(registryAccess: CommandRegistryAccess) : RequiredArgumentBuilder<ServerCommandSource, String> {
val emitterList = emitterListArg("Emitter ID")
val configArg = listConfigArgs(dataClassToArray(EmitterParams::class), emitterList, registryAccess) as RequiredArgumentBuilder<ServerCommandSource, String>
return configArg
}
// I am not going to torture myself by converting this to a json.
/*
private fun copy() : RequiredArgumentBuilder<ServerCommandSource, String> {
return emitterList("Emitter ID").executes { context ->
val emitterId = StringArgumentType.getString(context, "Emitter ID")
Command.SINGLE_SUCCESS
}
}
*/
}
@@ -0,0 +1,61 @@
package com.bytedice.bde_particles.commands
import com.bytedice.bde_particles.*
import com.bytedice.bde_particles.particles.ParticleEmitter
import com.bytedice.bde_particles.particles.getEmitterDataById
import com.mojang.brigadier.Command
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.context.CommandContext
import net.minecraft.command.argument.Vec3ArgumentType
import net.minecraft.server.command.CommandManager
import net.minecraft.server.command.ServerCommandSource
import net.minecraft.util.math.Vec3d
import org.joml.Vector2f
object SpawnEmitter {
fun register(dispatcher: CommandDispatcher<ServerCommandSource>) {
val command = CommandManager.literal("SpawnEmitter")
.requires { source -> source.hasPermissionLevel(2) }
dispatcher.register(
command.then(emitterListArg("Emitter ID")
.executes { context ->
execute(context, context.source.position)
Command.SINGLE_SUCCESS
}
.then(CommandManager.argument("Pos", Vec3ArgumentType.vec3())
.executes { context ->
val pos = Vec3ArgumentType.getVec3(context, "Pos")
execute(context, pos)
Command.SINGLE_SUCCESS
}
)
)
)
}
private fun execute(context: CommandContext<ServerCommandSource>, pos: Vec3d) {
val emitterId = StringArgumentType.getString(context, "Emitter ID")
val emitterParams = getEmitterDataById(emitterId)
val world = context.source.world
if (emitterParams != null) {
val emitter = ParticleEmitter(
pos,
Vector2f(0.0f, 0.0f),
world,
emitterParams,
world.gameRules.getBoolean(Bde_particles.SHOW_PARTICLE_DEBUG)
)
spawnEmitter(emitter)
positiveFeedback("Successfully spawned Emitter ID \"$emitterId\"", context)
}
else {
negativeFeedback("Failed to spawn Emitter ID \"$emitterId\". Emitter ID doesn't exist!", context)
}
}
}
@@ -0,0 +1,167 @@
package com.bytedice.bde_particles.particles
import com.bytedice.bde_particles.Billboard
import com.bytedice.bde_particles.DisplayEntity
import com.bytedice.bde_particles.DisplayEntityProperties
import com.bytedice.bde_particles.SESSION_UUID
import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.Vec3d
import org.joml.Vector2f
import org.joml.Vector3f
import org.joml.Vector4f
class EmitterDebug(
rot: Vector2f,
eOrigin: Vec3d,
spawnPosOffset: Vector3f,
shape: SpawningShape,
) {
private val eOriginDebug = EOrigin(rot, eOrigin)
private val shapeDebug = ShapeDebug(eOrigin, spawnPosOffset, shape)
private val forceFieldDebug = ForceFieldDebug()
fun spawn(world: ServerWorld, eOrigin: Vec3d, forceFields: ParamClasses.ForceFieldArray, spawnPosOffset: Vector3f) {
eOriginDebug.spawn(world)
shapeDebug.spawn(world)
forceFieldDebug.makeArray(eOrigin, forceFields, spawnPosOffset)
forceFieldDebug.spawn(world)
}
fun update(
rot: Vector2f,
eOrigin: Vec3d,
spawnPosOffset: Vector3f
) {
eOriginDebug.update(rot, eOrigin)
shapeDebug.update(rot, eOrigin, spawnPosOffset)
}
fun kill() {
eOriginDebug.kill()
shapeDebug.kill()
forceFieldDebug.kill()
}
}
class EOrigin(rot: Vector2f, eOrigin: Vec3d) {
val eOriginDE = DisplayEntity(
DisplayEntityProperties(
pos = eOrigin,
rot = rot,
model = "minecraft:red_concrete",
tags = arrayOf("DEBUG", "E_DEBUG", "eOrigin_DEBUG", "BPS_UUID", SESSION_UUID.toString()),
brightnessOverride = 15,
scale = Vector3f(0.25f, 0.25f, 0.25f)
))
fun spawn(world: ServerWorld) {
eOriginDE.spawn(world)
}
fun update(rot: Vector2f, eOrigin: Vec3d) {
val newProperties = eOriginDE.properties
newProperties?.rot = rot
newProperties?.pos = eOrigin
eOriginDE.updateProperties(newProperties!!)
}
fun kill() {
eOriginDE.kill()
}
}
class ShapeDebug(eOrigin: Vec3d, spawnPosOffset: Vector3f, shape: SpawningShape) {
val shapeScale: Vector3f? = when (shape) {
is SpawningShape.Circle -> Vector3f(shape.radius * 2 * 2, shape.radius * 2 * 2, 0.0001f)
is SpawningShape.Sphere -> Vector3f(shape.radius * 2 * 2, shape.radius * 2 * 2, 0.0001f)
is SpawningShape.Rect -> Vector3f(shape.size.x, 0.0001f, shape.size.y)
is SpawningShape.Cube -> Vector3f(shape.size.x, shape.size.y, shape.size.z)
else -> null
}
val shapeRot = when (shape) {
is SpawningShape.Circle -> Vector4f(-0.707f, 0.0f, 0.0f, 0.707f)
else -> Vector4f(0.0f, 0.0f, 0.0f, 1.0f)
}
private fun determineModel(shape: SpawningShape): String =
when (shape) {
is SpawningShape.Circle, is SpawningShape.Sphere -> "minecraft:sunflower"
else -> "minecraft:yellow_stained_glass"
}
private fun determineBillboard(shape: SpawningShape): String =
if (shape !is SpawningShape.Sphere) Billboard.FIXED else Billboard.CENTER
val shapeDE: DisplayEntity? = if (shape !is SpawningShape.Point) {
val position = eOrigin.add(
spawnPosOffset.x.toDouble(),
spawnPosOffset.y.toDouble(),
spawnPosOffset.z.toDouble()
)
DisplayEntity(DisplayEntityProperties(
pos = position.add(spawnPosOffset.x.toDouble(), spawnPosOffset.y.toDouble(), spawnPosOffset.z.toDouble()),
model = determineModel(shape),
tags = arrayOf("DEBUG", "P_DEBUG", "shape_DEBUG", "BPS_UUID", SESSION_UUID.toString()),
brightnessOverride = 15,
billboard = determineBillboard(shape),
leftRotation = shapeRot,
scale = shapeScale ?: Vector3f(1f, 1f, 1f)
))
} else null
fun spawn(world: ServerWorld) {
shapeDE?.spawn(world)
}
fun update(rot: Vector2f, eOrigin: Vec3d, spawnPosOffset: Vector3f) {
val newProperties = shapeDE?.properties
newProperties?.rot = rot
newProperties?.pos = eOrigin
newProperties?.translation = spawnPosOffset
shapeDE?.updateProperties(newProperties!!)
}
fun kill() {
shapeDE?.kill()
}
}
class ForceFieldDebug {
var FFDEArray: Array<DisplayEntity> = emptyArray()
fun makeArray(eOrigin: Vec3d, forceFieldArray: ParamClasses.ForceFieldArray, spawnPosOffset: Vector3f) {
forceFieldArray.array.forEachIndexed { index, it ->
val shapeScale = when (it.shape) {
is ForceFieldShape.Sphere -> Vector3f(1.333f * it.shape.radius * 2, 1.333f * it.shape.radius * 2, 0.0001f)
is ForceFieldShape.Cube -> it.shape.size
}
val newOffset = Vector3f(spawnPosOffset).add(it.pos)
val displayEntity = DisplayEntity(DisplayEntityProperties(
pos = eOrigin.add(newOffset.x.toDouble(), newOffset.y.toDouble(), newOffset.z.toDouble()),
model = if (it.shape is ForceFieldShape.Sphere) { "minecraft:snowball" } else { "minecraft:white_stained_glass" },
tags = arrayOf("DEBUG", "E_DEBUG", "eOrigin_DEBUG", "BPS_UUID", SESSION_UUID.toString()),
customName = "forceField index: $index",
brightnessOverride = 15,
billboard = if (it.shape is ForceFieldShape.Sphere) { Billboard.CENTER } else { Billboard.FIXED },
scale = shapeScale
))
FFDEArray += displayEntity
}
}
fun spawn(world: ServerWorld) {
FFDEArray.forEach { it.spawn(world) }
}
fun kill() {
FFDEArray.forEach { it.kill() }
}
}
@@ -1,42 +1,159 @@
package com.bytedice.bde_particles.particles
/**
* EmitterParams defines the parameters for controlling how particles are emitted,
* including settings for looping, spawn frequency, and maximum particle count.
*
* @param maxCount The maximum number of particles that can exist. If set to 0 or below, no particles will spawn.
* @param spawnsPerTick The number of particles spawned per tick. If set to 0 or below, no particles will spawn.
* @param loopDur The duration for which the loop runs, in ticks. A value 0 or below means the loop is infinite, and `loopDelay` and `loopCount` will be ignored.
* @param loopDelay The delay between each loop cycle, in ticks. Negative values are treated as 0, meaning no delay.
* @param loopCount The number of times the loop will repeat. 0 means it will shoot a single burst of particles. A value below 0 means it will repeat indefinitely.
* @param particle The particle params of the particle that will spawn.
*/
import com.bytedice.bde_particles.LerpCurves
import org.joml.Vector3f
data class EmitterParams (
var maxCount: Int = 200,
var spawnsPerTick: Int = 1,
var loopDur: Int = 25,
var loopDelay: Int = 0,
var loopCount: Int = 0,
var particle: ParticleParams = ParticleParams.DEFAULT,
// spawning
var maxCount: Int,
var spawnRate: Int,
var spawnChance: Float,
var spawnDuration: ParamClasses.Duration,
var spawnPosOffset: Vector3f,
var lifeTime: ParamClasses.PairInt,
var shape: SpawningShape,
// init transforms
var offset: ParamClasses.PairVec3f,
var initRot: ParamClasses.PairVec3f,
var rotVel: ParamClasses.PairVec3f,
var initVel: ParamClasses.PairVec3f,
var initCenterVel: ParamClasses.PairFloat,
var initScale: ParamClasses.PairVec3f,
var transformWithVel: ParamClasses.TransformWithVel,
// velocity
var forceFields: ParamClasses.ForceFieldArray,
var constVel: Vector3f,
var drag: Float,
var minVel: Float,
// curves
var offsetCurve: ParamClasses.LerpValVec3f,
var rotVelCurve: ParamClasses.LerpValVec3f,
var scaleCurve: ParamClasses.LerpValVec3f,
var modelCurve: ParamClasses.StringCurve,
var brightnessCurve: ParamClasses.LerpValInt,
)
{
fun deepCopy() : EmitterParams {
val cModelCurveArray = this.modelCurve.deepCopy()
val cForceFieldArray = this.forceFields.deepCopy()
val cParams = this.copy()
cParams.modelCurve = cModelCurveArray
cParams.forceFields = cForceFieldArray
return cParams
}
companion object Presets {
val DEFAULT = EmitterParams()
val FIRE_GEYSER = EmitterParams(
200,
1,
25,
0,
0,
ParticleParams.FIRE_GEYSER
val DEFAULT = EmitterParams(
maxCount = 200,
spawnRate = 50,
spawnChance = 1.0f,
spawnDuration = ParamClasses.Duration.SingleBurst(4),
spawnPosOffset = Vector3f(0.0f, 0.0f, 0.0f),
lifeTime = ParamClasses.PairInt(25, 40),
shape = SpawningShape.Sphere(1.0f, true),
offset = ParamClasses.PairVec3f.Uniform(0.0f, 0.0f),
initRot = ParamClasses.PairVec3f.NonUniform(0.0f, 0.0f, 0.0f, 360.0f, 360.0f, 360.0f),
rotVel = ParamClasses.PairVec3f.NonUniform(-0.2f, -0.2f, -0.2f, 0.2f, 0.2f, 0.2f),
initVel = ParamClasses.PairVec3f.Null,
initCenterVel = ParamClasses.PairFloat(0.4f, 0.5f),
initScale = ParamClasses.PairVec3f.Uniform(0.5f, 1.5f),
transformWithVel = ParamClasses.TransformWithVel.Null,
forceFields = ParamClasses.ForceFieldArray(emptyArray()),
constVel = Vector3f(0.0f, 0.0f, 0.0f),
drag = 0.075f,
minVel = 0.0f,
offsetCurve = ParamClasses.LerpValVec3f.Null,
rotVelCurve = ParamClasses.LerpValVec3f.LerpUniform(1.0f, 0.0f, LerpCurves.Sqrt),
scaleCurve = ParamClasses.LerpValVec3f.LerpUniform(1.0f, 2.5f, LerpCurves.Linear),
modelCurve = ParamClasses.StringCurve(
arrayOf(
"shroomlight",
"orange_concrete",
"orange_stained_glass",
"gray_wool",
"gray_stained_glass",
"light_gray_stained_glass"
),
LerpCurves.Sqrt
),
brightnessCurve = ParamClasses.LerpValInt.LerpInt(15, 5, LerpCurves.Sqrt)
)
val RING_EXPLOSION = EmitterParams(
200,
150,
1,
0,
0,
ParticleParams.RING_EXPLOSION
val DEBUG = EmitterParams(
maxCount = 50,
spawnRate = 10,
spawnChance = 1.0f,
spawnDuration = ParamClasses.Duration.SingleBurst(5),
spawnPosOffset = Vector3f(0.0f, 2.0f, 0.0f),
lifeTime = ParamClasses.PairInt(25, 40),
shape = SpawningShape.Circle(1.0f, true),
offset = ParamClasses.PairVec3f.Uniform(0.0f, 0.0f),
initRot = ParamClasses.PairVec3f.Null,
rotVel = ParamClasses.PairVec3f.Null,
initVel = ParamClasses.PairVec3f.NonUniform(0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f),
initCenterVel = ParamClasses.PairFloat(1.5f, 1.5f),
initScale = ParamClasses.PairVec3f.Uniform(2.0f, 2.0f),
transformWithVel = ParamClasses.TransformWithVel.ScaleAndRot(0.75f),
forceFields = ParamClasses.ForceFieldArray(emptyArray()),
constVel = Vector3f(0.0f, -0.05f, 0.0f),
drag = 0.075f,
minVel = 0.0f,
offsetCurve = ParamClasses.LerpValVec3f.Null,
rotVelCurve = ParamClasses.LerpValVec3f.LerpUniform(1.0f, 0.0f, LerpCurves.Linear),
scaleCurve = ParamClasses.LerpValVec3f.LerpUniform(1.0f, 1.0f, LerpCurves.Linear),
modelCurve = ParamClasses.StringCurve(
arrayOf(
"shroomlight",
"orange_concrete",
"orange_stained_glass",
"gray_wool",
"gray_stained_glass",
"light_gray_stained_glass"
),
LerpCurves.Linear
),
brightnessCurve = ParamClasses.LerpValInt.LerpInt(15, 5, LerpCurves.Sqrt)
)
val STRESS_TEST = EmitterParams(
maxCount = 10000,
spawnRate = 50,
spawnChance = 1.0f,
spawnDuration = ParamClasses.Duration.SingleBurst(100),
spawnPosOffset = Vector3f(0.0f, 0.0f, 0.0f),
lifeTime = ParamClasses.PairInt(75, 100),
shape = SpawningShape.Circle(10.0f, true),
offset = ParamClasses.PairVec3f.Uniform(0.0f, 0.0f),
initRot = ParamClasses.PairVec3f.NonUniform(0.0f, 0.0f, 0.0f, 360.0f, 360.0f, 360.0f),
rotVel = ParamClasses.PairVec3f.NonUniform(-0.2f, -0.2f, -0.2f, 0.2f, 0.2f, 0.2f),
initVel = ParamClasses.PairVec3f.NonUniform(0.0f, 4.0f, 0.0f, 0.0f, 8.0f, 0.0f),
initCenterVel = ParamClasses.PairFloat(-1.5f, -1.5f),
initScale = ParamClasses.PairVec3f.Uniform(10.0f, 15.0f),
transformWithVel = ParamClasses.TransformWithVel.Null,
forceFields = ParamClasses.ForceFieldArray(
arrayOf(ForceField(
Vector3f(0.0f, 75.0f, 0.0f),
ForceFieldShape.Sphere(15.0f, Pair(0.1f, 0.2f))
))
),
constVel = Vector3f(0.0f, 0.00001f, 0.0f),
drag = 0.075f,
minVel = 0.0f,
offsetCurve = ParamClasses.LerpValVec3f.Null,
rotVelCurve = ParamClasses.LerpValVec3f.LerpUniform(1.0f, 0.0f, LerpCurves.Linear),
scaleCurve = ParamClasses.LerpValVec3f.LerpUniform(1.0f, 1.5f, LerpCurves.Linear),
modelCurve = ParamClasses.StringCurve(
arrayOf(
"shroomlight",
"orange_concrete",
"orange_stained_glass",
"gray_wool",
"gray_stained_glass",
"light_gray_stained_glass"
),
LerpCurves.Linear
),
brightnessCurve = ParamClasses.LerpValInt.Null
)
}
}
@@ -3,7 +3,13 @@ package com.bytedice.bde_particles.particles
import org.joml.Vector3f
data class ForceField (
val name: String = "DEFAULT",
val pos: Vector3f = Vector3f(0.0f, 5.0f, 0.0f),
val shape: ForceFieldShape = ForceFieldShape.SPHERE()
val pos: Vector3f,
val shape: ForceFieldShape
) {
fun deepCopy(): ForceField {
return ForceField(
Vector3f(pos),
shape.deepCopy()
)
}
}
@@ -0,0 +1,208 @@
package com.bytedice.bde_particles.particles
import com.bytedice.bde_particles.LerpCurves
import com.bytedice.bde_particles.lerpArray
import com.bytedice.bde_particles.randomFloatBetween
import com.bytedice.bde_particles.randomIntBetween
import org.joml.Vector3f
import kotlin.math.atan2
import kotlin.math.pow
import kotlin.math.sqrt
class ParamClasses {
sealed class PairVec3f {
class Uniform (
private val min: Float,
private val max: Float,
) : PairVec3f() {
fun randomize() : Vector3f {
val x = randomFloatBetween(min, max)
return Vector3f(x, x, x)
}
}
class NonUniform (
private val minX: Float,
private val minY: Float,
private val minZ: Float,
private val maxX: Float,
private val maxY: Float,
private val maxZ: Float
) : PairVec3f() {
fun randomize(): Vector3f {
return Vector3f(
randomFloatBetween(minX, maxX),
randomFloatBetween(minY, maxY),
randomFloatBetween(minZ, maxZ),
)
}
}
data class Constant(val value: Vector3f) : PairVec3f()
data object Null : PairVec3f()
}
class PairInt (
val min: Int,
val max: Int,
) {
fun randomize() : Int { return randomIntBetween(min, max) }
}
class PairFloat(
val min: Float,
val max: Float,
) {
fun randomize() : Float { return randomFloatBetween(min, max) }
}
sealed class LerpValVec3f {
class LerpVec3f(
private val fromX: Float,
private val fromY: Float,
private val fromZ: Float,
private val toX: Float,
private val toY: Float,
private val toZ: Float,
private val curve: LerpCurves,
) : LerpValVec3f() {
fun lerp(t: Float): Vector3f {
val x = com.bytedice.bde_particles.lerp(fromX, toX, curve.function(t))
val y = com.bytedice.bde_particles.lerp(fromY, toY, curve.function(t))
val z = com.bytedice.bde_particles.lerp(fromZ, toZ, curve.function(t))
return Vector3f(x, y, z)
}
}
class MultiLerpVec3f(
private val fromX: Float,
private val fromY: Float,
private val fromZ: Float,
private val toX: Float,
private val toY: Float,
private val toZ: Float,
private val curveX: LerpCurves,
private val curveY: LerpCurves,
private val curveZ: LerpCurves
) : LerpValVec3f() {
fun lerp(t: Float): Vector3f {
val x = com.bytedice.bde_particles.lerp(fromX, toX, curveX.function(t))
val y = com.bytedice.bde_particles.lerp(fromY, toY, curveY.function(t))
val z = com.bytedice.bde_particles.lerp(fromZ, toZ, curveZ.function(t))
return Vector3f(x, y, z)
}
}
class LerpUniform(
private val from: Float,
private val to: Float,
private val curve: LerpCurves,
) : LerpValVec3f() {
fun lerp(t: Float) : Float {
return com.bytedice.bde_particles.lerp(from, to, curve.function(t))
}
}
data class Constant(val value: Vector3f) : LerpValVec3f()
data object Null : LerpValVec3f()
fun lerpToVector3f(t: Float) : Vector3f {
when (this) {
is LerpVec3f -> return this.lerp(t)
is MultiLerpVec3f -> return this.lerp(t)
is LerpUniform -> {
val x = this.lerp(t)
return Vector3f(x, x, x)
}
is Constant -> return this.value
is Null -> return Vector3f(1.0f, 1.0f, 1.0f)
}
}
}
sealed class LerpValInt {
class LerpInt(
private val from: Int,
private val to: Int,
private val curve: LerpCurves,
) : LerpValInt() {
fun lerp(t: Float): Int {
return Math.round(com.bytedice.bde_particles.lerp(from.toFloat(), to.toFloat(), curve.function(t)))
}
}
data class Constant(val value: Int) : LerpValInt()
data object Null: LerpValInt()
fun lerpToInt(t: Float) : Int? {
return when (this) {
is LerpInt -> this.lerp(t)
is Constant -> this.value
is Null -> null
}
}
}
sealed class Duration {
data class SingleBurst(
val loopDur: Int
) : Duration()
data class MultiBurst(
val loopDur: Int,
val loopDelay: Int,
val loopCount: Int,
) : Duration()
data class InfiniteLoop(
val loopDur: Int,
val loopDelay: Int
) : Duration()
}
sealed class TransformWithVel {
data object RotOnly : TransformWithVel() {
fun velToRot(velocity: Vector3f): Vector3f {
val normalizedVelocity = Vector3f(velocity).normalize()
val yaw = Math.toDegrees(atan2(normalizedVelocity.x.toDouble(), normalizedVelocity.z.toDouble())).toFloat()
val horizontalMag = sqrt(normalizedVelocity.x.toDouble().pow(2) + normalizedVelocity.z.toDouble().pow(2))
val pitch = Math.toDegrees(atan2(-normalizedVelocity.y.toDouble(), horizontalMag)).toFloat()
return Vector3f(pitch, yaw, 0f)
}
}
class ScaleAndRot(private val scaleMul: Float) : TransformWithVel() {
fun velToRot(velocity: Vector3f) : Vector3f {
val normalizedVelocity = Vector3f(velocity).normalize()
val yaw = Math.toDegrees(atan2(normalizedVelocity.x.toDouble(), normalizedVelocity.z.toDouble())).toFloat()
val horizontalMag = sqrt(normalizedVelocity.x.toDouble().pow(2) + normalizedVelocity.z.toDouble().pow(2))
val pitch = Math.toDegrees(atan2(-normalizedVelocity.y.toDouble(), horizontalMag)).toFloat()
return Vector3f(pitch, yaw, 0f)
}
fun velToScale(velocity: Vector3f): Vector3f {
val velocityMagnitude = velocity.length()
val stretchFactor = velocityMagnitude * scaleMul + 1.0f
val contractFactor = 1.0f / stretchFactor
return Vector3f(contractFactor, contractFactor, stretchFactor)
}
}
data object Null : TransformWithVel()
}
class StringCurve (
var array: Array<String>,
var curve: LerpCurves
) {
fun lerp(t: Float) : String { return lerpArray(array as Array<Any>, t, curve) as String }
fun deepCopy(): StringCurve {
return StringCurve(array.copyOf(), curve.deepCopy())
}
}
data class ForceFieldArray (
var array: Array<ForceField>
) {
fun deepCopy(): ForceFieldArray {
return ForceFieldArray(array.map { it.deepCopy() }.toTypedArray())
}
}
}
@@ -1,77 +1,110 @@
package com.bytedice.bde_particles.particles
import com.bytedice.bde_particles.*
import net.minecraft.particle.DustParticleEffect
import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.Vec3d
import org.joml.Vector2f
import org.joml.Vector3f
import org.joml.Vector4f
import kotlin.math.*
// TODO: consider moving most params to emitter (such as shape, and forceFields) to store less data
class Particle(private val particleParams: ParticleParams) {
private val initOffset = Vector3f(-0.5f, -0.5f, -0.5f)
private var offset = Vector3f(0.0f, 0.0f, 0.0f)
private var pos = Vec3d(0.0, 0.0, 0.0)
private var rot = randomBetweenVector3f(particleParams.rotRandom.first, particleParams.rotRandom.second)
private val rotVel = randomBetweenVector3f(particleParams.rotVelRandom.first, particleParams.rotVelRandom.second)
private var vel = randomBetweenVector3f(particleParams.velRandom.first, particleParams.velRandom.second)
private var scale = randomBetweenVector3f(particleParams.sizeRandom.first, particleParams.sizeRandom.second)
private var blockDisplay: DisplayEntity? = null
private var lifeTime = randomIntBetween(particleParams.lifeTime.first, particleParams.lifeTime.second)
var isDead = false
class Particle(
private val emitterParams: EmitterParams,
private var entityPos: Vec3d,
private var entityRot: Vector2f,
private val debug: Boolean = false
) {
private var pos = Vector3f(0.0f, 0.0f, 0.0f)
private var timeAlive = 0
private var isInit = false
var isDead = false
fun init(pos: Vec3d) {
this.pos = pos
if ( particleParams.uniformSize ) {
val randomUniform = randomFloatBetween(particleParams.sizeRandom.first.x, particleParams.sizeRandom.second.x)
scale.x = randomUniform
scale.y = randomUniform
scale.z = randomUniform
// params
private val originOffset = when (val offset = emitterParams.offset) {
is ParamClasses.PairVec3f.Uniform -> offset.randomize()
is ParamClasses.PairVec3f.NonUniform -> offset.randomize()
is ParamClasses.PairVec3f.Constant -> offset.value
is ParamClasses.PairVec3f.Null -> Vector3f(0.0f, 0.0f, 0.0f)
}
val shape = particleParams.shape
val newSpawnPos: Vector3f
when (shape) {
is SpawningShape.CIRCLE -> { val posInCircle = randomInCircle(shape.radius); newSpawnPos = Vector3f(posInCircle.x, 0.0f, posInCircle.y) }
is SpawningShape.SPHERE -> { newSpawnPos = randomInSphere(shape.radius) }
is SpawningShape.RECT -> { val posInRect = randomInRect(shape.size); newSpawnPos = Vector3f(posInRect.x, 0.0f, posInRect.y) }
is SpawningShape.CUBE -> { newSpawnPos = randomInCube(shape.size) }
is SpawningShape.POINT -> { newSpawnPos = Vector3f(0.0f, 0.0f, 0.0f) }
private var rot = when (val initRot = emitterParams.initRot) {
is ParamClasses.PairVec3f.Uniform -> initRot.randomize()
is ParamClasses.PairVec3f.NonUniform -> initRot.randomize()
is ParamClasses.PairVec3f.Constant -> initRot.value
is ParamClasses.PairVec3f.Null -> Vector3f(0.0f, 0.0f, 0.0f)
}
offset = newSpawnPos
private var rotVel = when (val initRotVel = emitterParams.rotVel) {
is ParamClasses.PairVec3f.Uniform -> initRotVel.randomize()
is ParamClasses.PairVec3f.NonUniform -> initRotVel.randomize()
is ParamClasses.PairVec3f.Constant -> initRotVel.value
is ParamClasses.PairVec3f.Null -> Vector3f(0.0f, 0.0f, 0.0f)
}
val (quatRot, newOffset) = calcTransformOffset(rot, initOffset, scale)
private var vel = when (val initVel = emitterParams.initVel) {
is ParamClasses.PairVec3f.Uniform -> initVel.randomize()
is ParamClasses.PairVec3f.NonUniform -> initVel.randomize()
is ParamClasses.PairVec3f.Constant -> initVel.value
is ParamClasses.PairVec3f.Null -> Vector3f(0.0f, 0.0f, 0.0f)
}
private var scale = when (val initScale = emitterParams.initScale) {
is ParamClasses.PairVec3f.Uniform -> initScale.randomize()
is ParamClasses.PairVec3f.NonUniform -> initScale.randomize()
is ParamClasses.PairVec3f.Constant -> initScale.value
is ParamClasses.PairVec3f.Null -> Vector3f(0.0f, 0.0f, 0.0f)
}
private var lifeTime = emitterParams.lifeTime.randomize()
private lateinit var particleEntity: DisplayEntity
private val debugTool = ParticleDebug(entityRot, entityPos, pos, vel, scale)
fun init() {
val so = Vector3f(emitterParams.spawnPosOffset)
if (so.x + so.y + so.z != 0.0f) {
entityPos = entityPos.add(so.x.toDouble(), so.y.toDouble(), so.z.toDouble())
}
val shape = emitterParams.shape
val newSpawnPos = shape.random(shape)
pos = newSpawnPos
val posDouble = Vec3d(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble())
val dirFromCenter = posDouble.normalize()
val forceFromCenter = dirFromCenter.multiply(emitterParams.initCenterVel.randomize().toDouble())
vel.add(forceFromCenter.x.toFloat(), forceFromCenter.y.toFloat(), forceFromCenter.z.toFloat())
val (quatRot, newPos) = calcTransformOffset(this.rot, originOffset, scale)
val properties = DisplayEntityProperties(
pos = this.pos,
blockType = particleParams.blockCurve[0],
translation = newOffset.add(offset),
pos = entityPos,
rot = entityRot,
model = if (emitterParams.modelCurve.array.isNotEmpty()) { emitterParams.modelCurve.lerp(0.0f) } else "none",
translation = newPos.add(pos),
leftRotation = quatRot,
scale = scale,
tags = arrayOf("BPS_Particle", "BPS_UUID", sessionUuid.toString())
scale = Vector3f(scale).mul(emitterParams.scaleCurve.lerpToVector3f(0.0f)),
tags = arrayOf("BPS_Particle", "BPS_UUID", SESSION_UUID.toString()),
brightnessOverride = emitterParams.brightnessCurve.lerpToInt(0.0f)
)
blockDisplay = DisplayEntity(properties)
particleEntity = DisplayEntity(properties)
isInit = true
}
fun spawn(world: ServerWorld) {
if (isInit) { blockDisplay?.spawn(world) }
if (isInit) {
particleEntity.spawn(world)
if (debug) { debugTool.spawn(world) }
LIVING_PARTICLE_COUNT += 1
}
else { error("Particle is not initialized before being spawned. Please use Particle.init() to initialize it.") }
}
@@ -81,9 +114,9 @@ class Particle(private val particleParams: ParticleParams) {
val newProperties = calcNewProperties()
blockDisplay?.updateProperties(newProperties)
particleEntity.updateProperties(newProperties)
if (timeAlive > lifeTime) { isDead = true; blockDisplay?.kill() }
if (timeAlive > lifeTime) { kill() }
timeAlive += 1
}
@@ -103,100 +136,130 @@ class Particle(private val particleParams: ParticleParams) {
private fun calcNewProperties() : DisplayEntityProperties {
val timeAliveClamped = (timeAlive.toFloat() / lifeTime.toFloat()).coerceIn(0.0f, 1.0f)
fun calcBlockCurve(timeAliveClamped: Float) : String {
val newBlockIdx = round(lerp(0.0f, particleParams.blockCurve.lastIndex.toFloat(), timeAliveClamped)).toInt()
return particleParams.blockCurve[newBlockIdx]
val newModel = if (emitterParams.modelCurve.array.isNotEmpty()) { emitterParams.modelCurve.lerp(timeAliveClamped) } else "none"
val (newVel, newPos) = calcPos()
val newOriginOffset = Vector3f(originOffset.mul(emitterParams.offsetCurve.lerpToVector3f(timeAliveClamped)))
val newBrightness = emitterParams.brightnessCurve.lerpToInt(timeAliveClamped)
val TWVScale: Vector3f
//val TWVRot: Vector3f
val newRot: Vector3f
when (emitterParams.transformWithVel) {
is ParamClasses.TransformWithVel.RotOnly -> {
TWVScale = Vector3f(1.0f, 1.0f, 1.0f)
newRot = (emitterParams.transformWithVel as ParamClasses.TransformWithVel.RotOnly).velToRot(vel)
}
is ParamClasses.TransformWithVel.ScaleAndRot -> {
TWVScale = (emitterParams.transformWithVel as ParamClasses.TransformWithVel.ScaleAndRot).velToScale(vel)
newRot = (emitterParams.transformWithVel as ParamClasses.TransformWithVel.ScaleAndRot).velToRot(vel)
}
else -> {
TWVScale = Vector3f(1.0f, 1.0f, 1.0f)
newRot = calcRot(timeAliveClamped)
}
}
fun sphereSdfVel(forceField: ForceField) : Vector3f {
if (forceField.shape !is ForceFieldShape.SPHERE) { return Vector3f(0.0f, 0.0f, 0.0f) }
val newScale = TWVScale.mul(calcScale(timeAliveClamped))
//val newRot = TWVRot.add(calcRot(timeAliveClamped))
rot = newRot
vel = newVel
pos = newPos
for (forceField in emitterParams.forceFields.array) {
if (forceField.shape is ForceFieldShape.Sphere) { vel.add(sphereSdfVel(forceField)) }
else if (forceField.shape is ForceFieldShape.Cube) { vel.add(cubeSdfVel(forceField)) }
}
val (quatRot, transformOffset) = calcTransformOffset(newRot, originOffset, newScale)
val combinedOffset = transformOffset.add(newPos)
val newTranslate = combinedOffset.add(newOriginOffset)
val newProperties = DisplayEntityProperties(
pos = entityPos,
rot = entityRot,
model = newModel,
translation = newTranslate,
leftRotation = quatRot,
scale = newScale,
tags = arrayOf("BPS_Particle", "BPS_UUID", SESSION_UUID.toString()),
brightnessOverride = newBrightness
)
if (debug) { debugTool.update(entityRot, entityPos, newTranslate, vel, newScale) }
return newProperties
}
private fun sphereSdfVel(forceField: ForceField) : Vector3f {
if (forceField.shape !is ForceFieldShape.Sphere) { return Vector3f(0.0f, 0.0f, 0.0f) }
val shape = forceField.shape
val sdfVal = sdfSphere(forceField.pos, shape.radius, offset)
val sdfVal = sdfSphere(forceField.pos, shape.radius, pos)
val normalizedSdf = normalizeSdf(sdfVal, shape.radius)
val velDir = Vector3f(offset).sub(forceField.pos).normalize()
val velDir = Vector3f(pos).sub(forceField.pos).normalize()
val velMul = if (sdfVal > 0) { 0.0f }
else { lerp(shape.force.second, shape.force.second, 1 - normalizedSdf) }
return velDir.mul(velMul)
}
fun cubeSdfVel(forceField: ForceField) : Vector3f {
if (forceField.shape !is ForceFieldShape.CUBE) { return Vector3f(0.0f, 0.0f, 0.0f) }
private fun cubeSdfVel(forceField: ForceField) : Vector3f {
if (forceField.shape !is ForceFieldShape.Cube) { return Vector3f(0.0f, 0.0f, 0.0f) }
val shape = forceField.shape
val sdfVal = sdfCube(forceField.pos, shape.size, offset)
val velDir = if (sdfVal < 0) { shape.forceDir }
val sdfVal = sdfCube(forceField.pos, shape.size, pos)
val velDir = if (sdfVal < 0) { shape.force }
else { Vector3f(0.0f, 0.0f, 0.0f) }
return velDir
}
fun calcRot(timeAliveClamped: Float) : Vector3f {
private fun calcRot(t: Float) : Vector3f {
val newRot = Vector3f(rot)
val newVel = Vector3f(rotVel)
val rotCurve = particleParams.rotVelCurve
newVel.mul(interpolateCurve(rotCurve.first, rotCurve.second, timeAliveClamped, rotCurve.third))
newRot.add(newVel)
return newRot
newVel.mul(emitterParams.rotVelCurve.lerpToVector3f(t))
return newRot.add(newVel)
}
fun calcOffset() : Pair<Vector3f, Vector3f> {
private fun calcPos() : Pair<Vector3f, Vector3f> {
var vel = Vector3f(vel)
vel = vel.add(particleParams.gravity)
vel = vel.mul(1.0f - particleParams.drag)
val constVel = emitterParams.constVel
if (constVel.x + constVel.y + constVel.z != 0.0f) {
vel = vel.add(emitterParams.constVel)
}
vel = vel.mul(1.0f - emitterParams.drag)
if (abs(vel.x) < particleParams.minVel) { vel.x = 0.0f }
if (abs(vel.y) < particleParams.minVel) { vel.y = 0.0f }
if (abs(vel.z) < particleParams.minVel) { vel.z = 0.0f }
if (abs(vel.x) < emitterParams.minVel) { vel.x = 0.0f }
if (abs(vel.y) < emitterParams.minVel) { vel.y = 0.0f }
if (abs(vel.z) < emitterParams.minVel) { vel.z = 0.0f }
var newOffset = Vector3f(offset)
newOffset = newOffset.add(vel)
return Pair(vel, newOffset)
var newPos = Vector3f(pos)
newPos = newPos.add(vel)
return Pair(vel, newPos)
}
fun calcSize(timeAliveClamped: Float) : Vector3f {
val scale = Vector3f(scale)
val sizeCurve = particleParams.sizeCurve
scale.mul(interpolateCurve(sizeCurve.first, sizeCurve.second, timeAliveClamped, sizeCurve.third))
return scale
}
val timeAliveClamped = (timeAlive.toFloat() / lifeTime.toFloat()).coerceIn(0.0f, 1.0f)
val newBlock = calcBlockCurve(timeAliveClamped)
val (newVel, newOffset) = calcOffset()
val newRot = calcRot(timeAliveClamped)
val newScale = calcSize(timeAliveClamped)
vel = newVel
offset = newOffset
rot = newRot
for (forceField in particleParams.forceFields) {
if (forceField.shape is ForceFieldShape.SPHERE) { vel.add(sphereSdfVel(forceField)) }
else if (forceField.shape is ForceFieldShape.CUBE) { vel.add(cubeSdfVel(forceField)) }
}
val (quatRot, transformOffset) = calcTransformOffset(newRot, initOffset, newScale)
val combinedOffset = transformOffset.add(newOffset)
val newProperties = DisplayEntityProperties(
pos = pos,
blockType = newBlock,
translation = combinedOffset,
leftRotation = quatRot,
scale = newScale,
tags = arrayOf("BPS_Particle", "BPS_UUID", sessionUuid.toString())
)
return newProperties
private fun calcScale(t: Float) : Vector3f {
val newScale = Vector3f(scale)
return newScale.mul(emitterParams.scaleCurve.lerpToVector3f(t))
}
fun kill() {
blockDisplay?.kill()
if (!isDead) {
particleEntity.kill()
if (debug) { debugTool.kill() }
LIVING_PARTICLE_COUNT -= 1
isDead = true
}
}
}
@@ -0,0 +1,120 @@
package com.bytedice.bde_particles.particles
import com.bytedice.bde_particles.*
import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.Vec3d
import org.joml.Vector2f
import org.joml.Vector3f
import kotlin.math.atan2
import kotlin.math.max
import kotlin.math.pow
import kotlin.math.sqrt
class ParticleDebug(
rot: Vector2f,
eOrigin: Vec3d,
pOrigin: Vector3f,
pVel: Vector3f,
pScale: Vector3f
) {
private val pOriginDebug = POrigin(rot, eOrigin, pOrigin)
private val pVelDebug = PVel(rot, eOrigin, pOrigin, pVel, pScale)
fun spawn(world: ServerWorld) {
pOriginDebug.spawn(world)
pVelDebug.spawn(world)
}
fun update(
rot: Vector2f,
eOrigin: Vec3d,
pOrigin: Vector3f,
pVel: Vector3f,
pScale: Vector3f
) {
pOriginDebug.update(rot, eOrigin, pOrigin)
pVelDebug.update(rot, eOrigin, pOrigin, pVel, pScale)
}
fun kill() {
pOriginDebug.kill()
pVelDebug.kill()
}
}
class POrigin(rot: Vector2f, eOrigin: Vec3d, pOrigin: Vector3f) {
val pOriginDE = DisplayEntity(DisplayEntityProperties(
pos = eOrigin,
rot = rot,
model = "minecraft:lime_concrete",
tags = arrayOf("DEBUG", "P_DEBUG", "pOrigin_DEBUG", "BPS_UUID", SESSION_UUID.toString()),
brightnessOverride = 15,
translation = Vector3f(pOrigin),
scale = Vector3f(0.15f, 0.15f, 0.15f)
))
fun spawn(world: ServerWorld) {
pOriginDE.spawn(world)
}
fun update(rot: Vector2f, eOrigin: Vec3d, pOrigin: Vector3f) {
val newProperties = pOriginDE.properties
newProperties?.rot = rot
newProperties?.pos = eOrigin
newProperties?.translation = Vector3f(pOrigin)
pOriginDE.updateProperties(newProperties!!)
}
fun kill() {
pOriginDE.kill()
}
}
class PVel(rot: Vector2f, eOrigin: Vec3d, pOrigin: Vector3f, pVel: Vector3f, pScale: Vector3f) {
val pVelDE = DisplayEntity(DisplayEntityProperties(
pos = eOrigin,
rot = rot,
model = "minecraft:red_nether_brick_slab", //"minecraft:red_stained_glass",
tags = arrayOf("DEBUG", "P_DEBUG", "pVel_DEBUG", "BPS_UUID", SESSION_UUID.toString()),
brightnessOverride = 15,
translation = Vector3f(pOrigin),
scale = Vector3f(0.05f, max(max(pScale.x, pScale.y), pScale.z) + (pVel.x + pVel.y + pVel.z), 0.05f)
))
fun spawn(world: ServerWorld) {
pVelDE.spawn(world)
}
fun update(rot: Vector2f, eOrigin: Vec3d, pOrigin: Vector3f, pVel: Vector3f, pScale: Vector3f) {
val newProperties = pVelDE.properties
val scale = (max(max(pScale.x, pScale.y), pScale.z) + (pVel.x + pVel.y + pVel.z)) * 2.0f
val scaleVec3f = Vector3f(0.05f, scale, 0.05f)
val leftRotEuler = velToRot(pVel)
newProperties?.rot = rot
newProperties?.pos = eOrigin
newProperties?.translation = Vector3f(pOrigin)
newProperties?.leftRotation = eulerToQuat(leftRotEuler)
newProperties?.scale = scaleVec3f
pVelDE.updateProperties(newProperties!!)
}
fun kill() {
pVelDE.kill()
}
fun velToRot(velocity: Vector3f): Vector3f {
val normalizedVelocity = Vector3f(velocity).normalize()
val yaw = Math.toDegrees(atan2(normalizedVelocity.x.toDouble(), normalizedVelocity.z.toDouble())).toFloat()
val horizontalMag = sqrt(normalizedVelocity.x.toDouble().pow(2) + normalizedVelocity.z.toDouble().pow(2))
val pitch = Math.toDegrees(atan2(-normalizedVelocity.y.toDouble(), horizontalMag)).toFloat() + -90.0f
return Vector3f(pitch, yaw, 0.0f)
}
}
@@ -1,44 +1,91 @@
package com.bytedice.bde_particles.particles
import com.bytedice.bde_particles.Bde_particles
import com.bytedice.bde_particles.LIVING_PARTICLE_COUNT
import com.bytedice.bde_particles.randomFloatBetween
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.Vec3d
import org.joml.Vector2f
class ParticleEmitter(private val emitterPos: Vec3d, private val emitterWorld: ServerWorld, private val emitterParams: EmitterParams) {
class ParticleEmitter(
var pos: Vec3d,
var rot: Vector2f,
val world: ServerWorld,
val params: EmitterParams,
private val debug: Boolean = false
) {
private var allParticles: Array<Particle> = emptyArray()
private var timeAlive = 0
private var loopCount = 0
private var loopDelay = 0
private var loopDur = 0
private var loopDelayActive = false
private var timeEmitted = 0
private var timePaused = 0
private var timesLooped = 0
private var isPaused = false
private var isTicking = true
var isDead = false
private var debugTool = EmitterDebug(rot, pos, params.spawnPosOffset, params.shape)
fun tick() {
suspend fun tick() {
if (isTicking) { count() }
if (isTicking) { addParticle() }
if (!isTicking && allParticles.isEmpty()) { isDead = true }
if (!isTicking && allParticles.isEmpty()) { isDead = true; kill() }
for (particle in allParticles) {
if (particle.isDead) { allParticles = allParticles.toMutableList().apply { remove(particle) }.toTypedArray() }
else { particle.tick() }
processParticlesInBatches(100)
if (timeAlive == 0 && debug) { debugTool.spawn(world, pos, params.forceFields, params.spawnPosOffset) }
if (debug) { debugTool.update(rot, pos, params.spawnPosOffset) }
timeAlive += 1
}
private suspend fun processParticlesInBatches(
batchSize: Int
) = coroutineScope {
val batches = allParticles.toMutableList().chunked(batchSize)
val jobs = batches.map { batch ->
async {
val toRemove = mutableListOf<Particle>()
batch.forEach { particle ->
if (particle.isDead) {
toRemove.add(particle)
} else {
particle.tick()
}
}
synchronized(allParticles) {
allParticles = allParticles.toMutableList().apply { removeAll(toRemove) }.toTypedArray()
}
}
}
jobs.awaitAll()
}
private fun addParticle() {
if (loopDelayActive) { return }
if (isPaused) { return }
repeat(this.emitterParams.spawnsPerTick) {
if (allParticles.size >= emitterParams.maxCount) { return }
repeat(params.spawnRate) {
if (allParticles.size >= params.maxCount) { return }
val particle = Particle(emitterParams.particle)
particle.init(this.emitterPos)
particle.spawn(emitterWorld)
val maxParticles = world.gameRules.getInt(Bde_particles.GLOBAL_MAX_PARTICLES)
if (LIVING_PARTICLE_COUNT >= maxParticles) { return }
val randomSpawnChance = randomFloatBetween(0.0f, params.spawnChance.coerceIn(0.0f, 1.0f))
if (randomSpawnChance > params.spawnChance) { return }
val particle = Particle(params, pos, rot, debug)
particle.init()
particle.spawn(world)
allParticles += particle
}
@@ -46,44 +93,50 @@ class ParticleEmitter(private val emitterPos: Vec3d, private val emitterWorld: S
private fun count() {
// this is the only section I have comments
// because im so damn good at cooking spaghetti
if (params.spawnDuration is ParamClasses.Duration.SingleBurst) {
val loopDur = (params.spawnDuration as ParamClasses.Duration.SingleBurst).loopDur
if (timeEmitted >= loopDur) { isTicking = false; return }
// if max duration is 0 or less, skip counting, its infinite
if (emitterParams.loopDur <= 0) { return }
// if the duration is greater than max duration, add 1 loopCount
// return if max loopCount is 0
// if the delay is greater than 0 then enable it
if (loopDur >= emitterParams.loopDur) {
if (loopCount == 0) {
isTicking = false
return
}
loopCount += 1
loopDur = 0
if (emitterParams.loopDelay > 0) { loopDelayActive = true }
timeEmitted += 1
}
// if the delay is greater than max delay, disable the delay
else if (loopDelay >= emitterParams.loopDelay) {
loopDelay = 0
loopDelayActive = false
else if (params.spawnDuration is ParamClasses.Duration.MultiBurst) {
val loopDur = (params.spawnDuration as ParamClasses.Duration.MultiBurst).loopDur
val loopDelay = (params.spawnDuration as ParamClasses.Duration.MultiBurst).loopDelay
val loopCount = (params.spawnDuration as ParamClasses.Duration.MultiBurst).loopCount
if (timeEmitted >= loopDur && timesLooped + 1 >= loopCount) { isTicking = false; return }
if (timePaused >= loopDelay) {
isPaused = false
timePaused = 0
timesLooped += 1
}
if (timeEmitted >= loopDur && loopDelay > 0) {
isPaused = true
timeEmitted = 0
}
// if loopCount is greater than max loopCount then stop immediately
// if max loopCount is below 0 then don't do anything, loop infinitely
if (loopCount > 0 && loopCount >= emitterParams.loopCount) {
isTicking = false
return
if (!isPaused) { timeEmitted += 1 }
else { timePaused += 1 }
}
// if delay isn't active, add 1 to duration
// if delay is active, add 1 to delay
if (!loopDelayActive) { loopDur += 1 }
else { loopDelay += 1 }
else if (params.spawnDuration is ParamClasses.Duration.InfiniteLoop) {
val loopDur = (params.spawnDuration as ParamClasses.Duration.InfiniteLoop).loopDur
val loopDelay = (params.spawnDuration as ParamClasses.Duration.InfiniteLoop).loopDelay
timeAlive += 1
if (timePaused >= loopDelay) {
isPaused = false
timePaused = 0
}
if (timeEmitted >= loopDur && loopDelay > 0) {
isPaused = true
timeEmitted = 0
}
if (!isPaused) { timeEmitted += 1 }
else { timePaused += 1 }
}
}
@@ -92,7 +145,13 @@ class ParticleEmitter(private val emitterPos: Vec3d, private val emitterWorld: S
particle.kill()
allParticles = allParticles.toMutableList().apply { remove(particle) }.toTypedArray()
}
if (debug) { debugTool.kill() }
isTicking = false
isDead = true
}
fun stopTicking() {
isTicking = false
}
}
@@ -1,134 +1,97 @@
@file:Suppress("UNCHECKED_CAST")
package com.bytedice.bde_particles.particles
import com.bytedice.bde_particles.InterpolationCurves
import org.joml.Vector3f
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KProperty1
import kotlin.reflect.full.isSubclassOf
val emitterIdRegister: MutableMap<String, EmitterParams> = mutableMapOf()
val forbiddenIds = arrayOf("", "DEFAULT", "NULL")
val idRegister: MutableMap<String, EmitterParams> = mutableMapOf()
val forbiddenIds = arrayOf("", "NULL")
fun addToEmitterRegister(id: String, params: EmitterParams) : Pair<String, Boolean> {
val newId = id.replace(" ", "_")
fun addToRegister(id: String, params: EmitterParams) : Pair<String, Boolean> {
val newId = replaceForbiddenChars(id.replace(" ", "_"))
if (newId in forbiddenIds && newId != "DEFAULT") { return Pair(newId, false) }
if (emitterIdRegister.containsKey(newId)) { return Pair(newId, false) }
if (idRegister.containsKey(newId)) {
println("BPS - Failed to register Emitter ID \"$newId\" as it already exists!")
return Pair(newId, false)
}
emitterIdRegister[id] = params
if (newId in forbiddenIds) {
println("BPS - Failed to register Emitter ID \"$newId\" as it's been reserved for system use!")
return Pair(newId, false)
}
val newModels: MutableList<String> = mutableListOf()
for (model in params.modelCurve.array) {
val modelModIdRegex = ".*:".toRegex()
if (modelModIdRegex.containsMatchIn(model)) { newModels.add(model) }
else { newModels.add("minecraft:$model") }
}
params.modelCurve.array = newModels.toTypedArray()
idRegister[newId] = params
println("BPS - Registered Emitter ID \"$newId\".")
return Pair(newId, true)
}
fun removeFromEmitterRegister(id: String) : Boolean {
if (id in forbiddenIds) { return false }
if (!emitterIdRegister.containsKey(id)) { return false }
fun removeFromRegister(id: String) : Boolean {
if (!idRegister.containsKey(id)) {
println("BPS - Failed to remove Emitter ID \"$id\" as it doesn't exist!")
return false
}
if (id in forbiddenIds) {
println("BPS - Failed to remove Emitter ID \"$id\" as it's been reserved for system use!")
return false
}
emitterIdRegister.remove(id)
idRegister.remove(id)
println("BPS - Removed Emitter ID \"$id\" from register.")
return true
}
fun getEmitterParams(id: String) : EmitterParams? {
return emitterIdRegister[id]
fun getEmitterDataById(id: String) : EmitterParams? {
return idRegister[id]
}
fun updateEmitterParams(id: String, newParams: EmitterParams) {
emitterIdRegister[id] = newParams
fun updateRegistered(id: String, newParams: EmitterParams) {
idRegister[id] = newParams
}
fun updateSingleEmitterParam(id: String, paramName: String, paramValue: Any) : Boolean {
val emitter = getEmitterParams(id) ?: return false
val updatedValues = updateSingleEmitterParam(emitter, paramName, paramValue)
updateEmitterParams(id, updatedValues.first)
fun updateParam(id: String, paramAccess: KProperty1<EmitterParams, *>, paramValue: Any) : Boolean {
val emitter = getEmitterDataById(id) ?: return false
val updatedValues = updateParams(emitter, paramAccess as KProperty1<EmitterParams, Any>, paramValue)
updateRegistered(id, updatedValues.first)
return updatedValues.second
}
fun updateSingleParticleParam(emitterId: String, paramName: String, paramValue: Any) : Boolean {
val emitter = getEmitterParams(emitterId) ?: return false
val particle = emitter.particle
val updatedValues = updateSingleParticleParam(particle, paramName, paramValue)
emitter.particle = updatedValues.first
updateEmitterParams(emitterId, emitter)
return updatedValues.second
}
fun updateSingleEmitterParam(params: EmitterParams, paramName: String, paramValue: Any) : Pair<EmitterParams, Boolean> {
val value: Any? = when (paramName) {
"maxCount" -> paramValue as Int
"spawnsPerTick" -> paramValue as Int
"loopDur" -> paramValue as Int
"loopDelay" -> paramValue as Int
"loopCount" -> paramValue as Int
else -> null
}
if (value == null) { return Pair(params, false) }
fun updateParams(params: EmitterParams, paramAccess: KProperty1<EmitterParams, Any>, paramValue: Any): Pair<EmitterParams, Boolean> {
val newParams = params.copy()
when (paramName) {
"maxCount" -> newParams.maxCount = value as Int
"spawnsPerTick" -> newParams.spawnsPerTick = value as Int
"loopDur" -> newParams.loopDur = value as Int
"loopDelay" -> newParams.loopDelay = value as Int
"loopCount" -> newParams.loopCount = value as Int
else -> return Pair(params, false)
}
if (paramAccess.returnType.classifier == paramValue::class
|| (paramAccess.returnType.classifier as? KClass<*>)?.let { paramValue::class.isSubclassOf(it) } == true
)
{
paramAccess as KMutableProperty1<EmitterParams, Any>
paramAccess.set(newParams, paramValue)
return Pair(newParams, true)
}
fun updateSingleParticleParam(params: ParticleParams, paramName: String, paramValue: Any) : Pair<ParticleParams, Boolean> {
val value: Any? = when (paramName) {
"shape" -> paramValue as SpawningShape?
"blockCurve" -> paramValue as Array<String>
"rotRandom" -> paramValue as Pair<Vector3f, Vector3f>
"rotVelRandom" -> paramValue as Pair<Vector3f, Vector3f>
"sizeRandom" -> paramValue as Pair<Vector3f, Vector3f>
"uniformSize" -> paramValue as Boolean
"velRandom" -> paramValue as Pair<Vector3f, Vector3f>
"forceFields" -> paramValue as Array<ForceField>
"gravity" -> paramValue as Vector3f
"drag" -> paramValue as Float
"minVel" -> paramValue as Float
"lifeTime" -> paramValue as Pair<Int, Int>
"rotVelCurve" -> paramValue as Triple<Vector3f, Vector3f, InterpolationCurves>
"sizeCurve" -> paramValue as Triple<Vector3f, Vector3f, InterpolationCurves>
else -> null
return Pair(params, false)
}
if (value == null) { return Pair(params, false) }
val newParams = params.copy()
when (paramName) {
"shape" -> newParams.shape = value as SpawningShape
"blockCurve" -> newParams.blockCurve = value as Array<String>
"rotRandom" -> newParams.rotRandom = value as Pair<Vector3f, Vector3f>
"rotVelRandom" -> newParams.rotVelRandom = value as Pair<Vector3f, Vector3f>
"sizeRandom" -> newParams.sizeRandom = value as Pair<Vector3f, Vector3f>
"uniformSize" -> newParams.uniformSize = value as Boolean
"velRandom" -> newParams.velRandom = value as Pair<Vector3f, Vector3f>
"forceFields" -> newParams.forceFields = value as Array<ForceField>
"gravity" -> newParams.gravity = value as Vector3f
"drag" -> newParams.drag = value as Float
"minVel" -> newParams.minVel = value as Float
"lifeTime" -> newParams.lifeTime = value as Pair<Int, Int>
"rotVelCurve" -> newParams.rotVelCurve = value as Triple<Vector3f, Vector3f, InterpolationCurves>
"sizeCurve" -> newParams.sizeCurve = value as Triple<Vector3f, Vector3f, InterpolationCurves>
else -> return Pair(params, false)
}
return Pair(newParams, true)
fun replaceForbiddenChars(input: String): String {
val regex = "[^a-zA-Z0-9_-]".toRegex()
return input.replace(regex, "-")
}
@@ -1,75 +0,0 @@
package com.bytedice.bde_particles.particles
import com.bytedice.bde_particles.InterpolationCurves
import org.joml.Vector3f
/**
* ParticleParams defines the parameters for particle behavior, including shape, size, velocity, and other physics properties.
*
* @param shape The shape of the particle's spawning area, `null` is a single point.
* @param blockCurve An array of block names that gets cycled through during the particle's lifetime. Empty defaults to "minecraft:air".
* @param rotRandom The random ***initial*** rotation range for the particle's rotation in X, Y, and Z axes.
* @param rotVelRandom The random velocity for particle rotation in the X, Y, and Z axes.
* @param sizeRandom The random ***initial*** size range for the particle. If `uniformSize` is true, this will be uniform (cubic).
* @param uniformSize If true, particles will have a uniform (cubic) size. Otherwise, their sizes on different axis can vary.
* @param velRandom The random ***initial*** velocity range for the particle in the X, Y, and Z axes.
* @param forceFields An array of force fields applied to the particle.
* @param gravity A constant force applied to the particle every tick. It's often used as gravity.
* @param drag A drag force that affects the speed overtime. The formula is (velocity * (1.0 - drag))
* @param minVel If particles travel slower than this their speed is automatically 0. Values less than or equal to 0 mean the particle can travel at any speed.
* @param lifeTime A range for the particle's lifetime in ticks. A value below 1 will result in the particle not spawning.
* @param rotVelCurve A curve which the rotation velocity gets multiplied by during the particle's lifetime.
* @param sizeCurve A curve which the size gets multiplied by during the particle's lifetime.
*/
data class ParticleParams (
var shape: SpawningShape = SpawningShape.CIRCLE(3.0f),
var blockCurve: Array<String> = arrayOf("minecraft:purple_concrete", "minecraft:purple_stained_glass"),
var rotRandom: Pair<Vector3f, Vector3f> = Pair(Vector3f(0.0f, 0.0f, 0.0f), Vector3f(360.0f, 360.0f, 360.0f)),
var rotVelRandom: Pair<Vector3f, Vector3f> = Pair(Vector3f(-0.2f, -0.2f, -0.2f), Vector3f(0.2f, 0.2f, 0.2f)),
var sizeRandom: Pair<Vector3f, Vector3f> = Pair(Vector3f(0.5f, 0.5f, 0.5f), Vector3f(1.0f, 1.0f, 1.0f)),
var uniformSize: Boolean = true,
var velRandom: Pair<Vector3f, Vector3f> = Pair(Vector3f(-0.1f, 0.3f, -0.1f), Vector3f(0.1f, 0.6f, 0.1f)),
var forceFields: Array<ForceField> = emptyArray(),
var gravity: Vector3f = Vector3f(0.0f, -0.01f, 0.0f),
var drag: Float = 0.075f,
var minVel: Float = 0.0f,
var lifeTime: Pair<Int, Int> = Pair(15, 45),
var rotVelCurve: Triple<Vector3f, Vector3f, InterpolationCurves> = Triple(Vector3f(1.0f, 1.0f, 1.0f), Vector3f(0.0f, 0.0f, 0.0f), InterpolationCurves.SQRT),
var sizeCurve: Triple<Vector3f, Vector3f, InterpolationCurves> = Triple(Vector3f(1.0f, 1.0f, 1.0f), Vector3f(0.5f, 0.5f, 0.5f), InterpolationCurves.LINEAR),
) {
companion object Presets {
val DEFAULT = ParticleParams()
val FIRE_GEYSER = ParticleParams(
SpawningShape.POINT,
arrayOf("minecraft:shroomlight", "minecraft:orange_concrete", "minecraft:orange_stained_glass", "minecraft:gray_stained_glass", "minecraft:light_gray_stained_glass"),
Pair(Vector3f(0.0f, 0.0f, 0.0f), Vector3f(360.0f, 360.0f, 360.0f)),
Pair(Vector3f(-0.2f, -0.2f, -0.2f), Vector3f(0.2f, 0.2f, 0.2f)),
Pair(Vector3f(0.5f, 0.5f, 0.5f), Vector3f(1.0f, 1.0f, 1.0f)),
true,
Pair(Vector3f(-0.1f, 0.4f, -0.1f), Vector3f(0.1f, 0.8f, 0.1f)),
emptyArray(),
Vector3f(0.0f, -0.01f, 0.0f),
0.075f,
0.0f,
Pair(15, 45),
Triple(Vector3f(1.0f, 1.0f, 1.0f), Vector3f(0.0f, 0.0f, 0.0f), InterpolationCurves.SQRT),
Triple(Vector3f(1.0f, 1.0f, 1.0f), Vector3f(0.5f, 0.5f, 0.5f), InterpolationCurves.SQRT)
)
val RING_EXPLOSION = ParticleParams(
SpawningShape.CIRCLE(0.1f),
arrayOf("minecraft:shroomlight", "minecraft:orange_concrete", "minecraft:orange_stained_glass", "minecraft:gray_stained_glass", "minecraft:light_gray_stained_glass"),
Pair(Vector3f(0.0f, 0.0f, 0.0f), Vector3f(360.0f, 360.0f, 360.0f)),
Pair(Vector3f(-0.2f, -0.2f, -0.2f), Vector3f(0.2f, 0.2f, 0.2f)),
Pair(Vector3f(10.0f, 10.0f, 10.0f), Vector3f(15.0f, 15.0f, 15.0f)),
true,
Pair(Vector3f(0.0f, 0.0f, 0.0f), Vector3f(0.0f, 0.0f, 0.0f)),
arrayOf(ForceField("init", Vector3f(0.0f, -0.0001f, 0.0f), ForceFieldShape.SPHERE(0.1f, Pair(4.0f, 4.0f)))),
Vector3f(0.0f, 0.0f, 0.0f),
0.0075f,
0.0f,
Pair(50, 100),
Triple(Vector3f(1.0f, 1.0f, 1.0f), Vector3f(0.0f, 0.0f, 0.0f), InterpolationCurves.SQRT),
Triple(Vector3f(0.0f, 0.0f, 0.0f), Vector3f(3.0f, 3.0f, 3.0f), InterpolationCurves.SQRT)
)
}
}
@@ -1,23 +1,136 @@
package com.bytedice.bde_particles.particles
import com.bytedice.bde_particles.randomFloatBetween
import com.bytedice.bde_particles.randomIntBetween
import org.joml.Vector2f
import org.joml.Vector3f
import kotlin.math.*
import kotlin.random.Random
sealed class SpawningShape {
data class CIRCLE(val radius: Float = 1.0f) : SpawningShape()
data class RECT (val size: Vector2f = Vector2f(1.0f, 1.0f)) : SpawningShape()
data class CUBE (val size: Vector3f = Vector3f(1.0f, 1.0f, 1.0f)) : SpawningShape()
data class SPHERE(val radius: Float = 1.0f) : SpawningShape()
data object POINT : SpawningShape()
class Circle(val radius: Float, val spawnOnEdge: Boolean) : SpawningShape() {
fun randomWithin(): Vector3f {
val randRadius = radius * sqrt(randomFloatBetween(0.0f, 1.0f))
val randAngle = randomFloatBetween(0.0f, Math.PI.toFloat() * 2)
val randomPos = Vector3f(randRadius * cos(randAngle), 0.0f, randRadius * sin(randAngle))
return randomPos
}
sealed class ForceFieldShape {
data class SPHERE(
val radius: Float = 3.0f,
val force: Pair<Float, Float> = Pair(0.0f, 0.02f),
) : ForceFieldShape()
data class CUBE(
val size: Vector3f = Vector3f(1.0f, 1.0f, 1.0f),
val forceDir: Vector3f = Vector3f(0.0f, 0.0f, 0.0f)
) : ForceFieldShape()
fun randomOnEdge(): Vector3f {
val randAngle = randomFloatBetween(0.0f, Math.PI.toFloat() * 2)
return Vector3f(radius * cos(randAngle), 0.0f, radius * sin(randAngle))
}
}
class Rect(val size: Vector2f, val spawnOnEdge: Boolean) : SpawningShape() {
fun randomWithin(): Vector3f {
return Vector3f(randomFloatBetween(0.0f, size.x), 0.0f, randomFloatBetween(0.0f, size.y))
}
fun randomOnEdge(): Vector3f {
val perimeter = 2 * (size.x + size.y)
val randPos = randomFloatBetween(0.0f, perimeter)
return when {
randPos <= size.x -> Vector3f(randPos, 0.0f, 0.0f)
randPos <= size.x + size.y -> Vector3f(size.x, 0.0f, randPos - size.x)
randPos <= 2 * size.x + size.y -> Vector3f(size.x - (randPos - size.x - size.y), 0.0f, size.y)
else -> Vector3f(0.0f, 0.0f, size.y - (randPos - 2 * size.x - size.y))
}
}
}
class Cube(val size: Vector3f, val spawnOnEdge: Boolean) : SpawningShape() {
fun randomWithin(): Vector3f {
return Vector3f(
randomFloatBetween(0.0f, size.x),
randomFloatBetween(0.0f, size.y),
randomFloatBetween(0.0f, size.z)
)
}
fun randomOnEdge(): Vector3f {
val edgeIndex = randomIntBetween(0, 12) // 12 edges
val randPos = randomFloatBetween(0.0f, 1.0f)
return when (edgeIndex) {
0 -> Vector3f(randPos * size.x, 0.0f, 0.0f)
1 -> Vector3f(size.x, 0.0f, randPos * size.z)
2 -> Vector3f(size.x - randPos * size.x, 0.0f, size.z)
3 -> Vector3f(0.0f, 0.0f, size.z - randPos * size.z)
4 -> Vector3f(randPos * size.x, size.y, 0.0f)
5 -> Vector3f(size.x, size.y, randPos * size.z)
6 -> Vector3f(size.x - randPos * size.x, size.y, size.z)
7 -> Vector3f(0.0f, size.y, size.z - randPos * size.z)
8 -> Vector3f(0.0f, randPos * size.y, 0.0f)
9 -> Vector3f(size.x, randPos * size.y, 0.0f)
10 -> Vector3f(size.x, randPos * size.y, size.z)
11 -> Vector3f(0.0f, randPos * size.y, size.z)
else -> Vector3f(0.0f, 0.0f, 0.0f)
}
}
}
class Sphere(val radius: Float, val spawnOnEdge: Boolean) : SpawningShape() {
fun randomWithin(): Vector3f {
val randRadius = radius * Random.nextDouble().pow(1.0 / 3.0)
val phi = Random.nextDouble(0.0, 2 * Math.PI)
val cosTheta = Random.nextDouble(-1.0, 1.0)
val theta = acos(cosTheta)
val x = randRadius * sin(theta) * cos(phi)
val y = randRadius * sin(theta) * sin(phi)
val z = randRadius * cos(theta)
return Vector3f(x.toFloat(), y.toFloat(), z.toFloat())
}
fun randomOnEdge(): Vector3f {
val phi = randomFloatBetween(0.0f, Math.PI.toFloat() * 2)
val cosTheta = randomFloatBetween(-1.0f, 1.0f)
val sinTheta = sqrt(1 - cosTheta * cosTheta)
val x = radius * sinTheta * cos(phi)
val y = radius * sinTheta * sin(phi)
val z = radius * cosTheta
return Vector3f(x, y, z)
}
}
data object Point : SpawningShape()
fun random(shape: SpawningShape) : Vector3f {
return when (shape) {
is Circle -> if (!shape.spawnOnEdge) { shape.randomWithin() } else { shape.randomOnEdge() }
is Sphere -> if (!shape.spawnOnEdge) { shape.randomWithin() } else { shape.randomOnEdge() }
is Rect -> if (!shape.spawnOnEdge) { shape.randomWithin() } else { shape.randomOnEdge() }.sub(Vector3f(shape.size.x, 0.0f, shape.size.y).div(2.0f))
is Cube -> if (!shape.spawnOnEdge) { shape.randomWithin() } else { shape.randomOnEdge() }.sub(Vector3f(shape.size.x, shape.size.y, shape.size.z).div(2.0f))
is Point -> Vector3f(0.0f, 0.0f, 0.0f)
}
}
}
sealed class ForceFieldShape {
data class Sphere(
val radius: Float,
val force: Pair<Float, Float>,
) : ForceFieldShape()
data class Cube(
val size: Vector3f,
val force: Vector3f
) : ForceFieldShape()
fun deepCopy() : ForceFieldShape {
return when (this) {
is Sphere -> Sphere(this.radius, this.force)
is Cube -> Cube(Vector3f(this.size), Vector3f(this.force))
}
}
}