diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleBuff.java b/src/main/java/electroblob/wizardry/client/particle/ParticleBuff.java new file mode 100644 index 00000000..b7ea5432 --- /dev/null +++ b/src/main/java/electroblob/wizardry/client/particle/ParticleBuff.java @@ -0,0 +1,136 @@ +package electroblob.wizardry.client.particle; + +import org.lwjgl.opengl.GL11; + +import electroblob.wizardry.Wizardry; +import net.minecraft.client.Minecraft; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.renderer.BufferBuilder; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.GlStateManager.DestFactor; +import net.minecraft.client.renderer.GlStateManager.SourceFactor; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraft.entity.Entity; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.MathHelper; +import net.minecraft.world.World; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +@SideOnly(Side.CLIENT) +public class ParticleBuff extends ParticleEntityLinked { + + private static final ResourceLocation TEXTURE = new ResourceLocation(Wizardry.MODID, "textures/particle/buff.png"); + private final boolean mirror; + + public ParticleBuff(World world, Entity entity, float r, float g, float b, boolean mirror){ + super(world, entity); + this.setRBGColorF(r, g, b); + this.mirror = mirror; + } + + public ParticleBuff(World world, Entity entity, int maxAge, float r, float g, float b, boolean mirror){ + super(world, entity, maxAge); + this.setRBGColorF(r, g, b); + this.mirror = mirror; + } + + @Override + public void init(){ + this.particleGravity = 0; + this.posY += 1; + } + + @Override + public void onUpdate(){ + super.onUpdate(); + this.setPosition(this.entity.posX, this.entity.posY + 1 + 4f * this.particleAge/this.particleMaxAge, this.entity.posZ); + if(this.particleAge > this.particleMaxAge/2) this.particleAlpha = 2f - 2f*(float)this.particleAge/(float)this.particleMaxAge; + } + + @Override + public void renderParticle(BufferBuilder buffer, Entity viewer, float partialTicks, float rotationX, float rotationZ, + float rotationYZ, float rotationXY, float rotationXZ){ + + GlStateManager.pushMatrix(); + GlStateManager.pushAttrib(); + + float scale = 0.6f; + GlStateManager.scale(scale, scale, scale); + if(mirror) GlStateManager.scale(-1, 1, 1); + + GlStateManager.enableBlend(); + GlStateManager.disableAlpha(); + GlStateManager.disableCull(); + GlStateManager.disableLighting(); + GlStateManager.blendFunc(SourceFactor.SRC_ALPHA, DestFactor.ONE); + // Makes the particle colour add to the colour of the texture pixels, rather than the default multiplying + GlStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_ADD); + + OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f); + + GlStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT); + GlStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT); + + // Does the texture translation wrapping thing (the cool stuff) + GlStateManager.matrixMode(GL11.GL_TEXTURE); + GlStateManager.loadIdentity(); + + GlStateManager.translate((this.particleAge + partialTicks)/(float)this.particleMaxAge * -2, 0, 0); + + GlStateManager.matrixMode(GL11.GL_MODELVIEW); + + RenderHelper.disableStandardItemLighting(); + + Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE); + + buffer.begin(GL11.GL_TRIANGLE_STRIP, DefaultVertexFormats.POSITION_TEX_COLOR); + + // I'm pretty sure these were always static. + Particle.interpPosX = viewer.lastTickPosX + (viewer.posX - viewer.lastTickPosX) * (double)partialTicks; + Particle.interpPosY = viewer.lastTickPosY + (viewer.posY - viewer.lastTickPosY) * (double)partialTicks; + Particle.interpPosZ = viewer.lastTickPosZ + (viewer.posZ - viewer.lastTickPosZ) * (double)partialTicks; + + float x = (float)(this.prevPosX + (this.posX - this.prevPosX) * (double)partialTicks - interpPosX); + float y = (float)(this.prevPosY + (this.posY - this.prevPosY) * (double)partialTicks - interpPosY); + float z = (float)(this.prevPosZ + (this.posZ - this.prevPosZ) * (double)partialTicks - interpPosZ); + + // Increases from 0 to 1 in steps of 0.125 evenly throughout the particle's lifetime + float f = 0.875f - 0.125f * MathHelper.floor((float)this.particleAge/(float)this.particleMaxAge * 8 - 0.000001f); + float g = f + 0.125f; + float hrepeat = 1; + float yScale = 0.7f; + + buffer.pos(x-1, y-yScale, z-1).tex(0, g).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex(); + buffer.pos(x-1, y+yScale, z-1).tex(0, f).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex(); + buffer.pos(x+1, y-yScale, z-1).tex(0.25*hrepeat, g).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex(); + buffer.pos(x+1, y+yScale, z-1).tex(0.25*hrepeat, f).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex(); + buffer.pos(x+1, y-yScale, z+1).tex(0.5*hrepeat, g).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex(); + buffer.pos(x+1, y+yScale, z+1).tex(0.5*hrepeat, f).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex(); + buffer.pos(x-1, y-yScale, z+1).tex(0.75*hrepeat, g).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex(); + buffer.pos(x-1, y+yScale, z+1).tex(0.75*hrepeat, f).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex(); + buffer.pos(x-1, y-yScale, z-1).tex(hrepeat, g).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex(); + buffer.pos(x-1, y+yScale, z-1).tex(hrepeat, f).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex(); + + Tessellator.getInstance().draw(); + + // Undoes the texture transformations + GlStateManager.matrixMode(GL11.GL_TEXTURE); + GlStateManager.loadIdentity(); + GlStateManager.matrixMode(GL11.GL_MODELVIEW); + + GlStateManager.disableBlend(); + GlStateManager.enableAlpha(); + GlStateManager.enableCull(); + GlStateManager.enableLighting(); + // Reverses the colour addition change from before + GlStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE); + + GlStateManager.popAttrib(); + GlStateManager.popMatrix(); + + } +} diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleEntityLinked.java b/src/main/java/electroblob/wizardry/client/particle/ParticleEntityLinked.java new file mode 100644 index 00000000..76dfaefb --- /dev/null +++ b/src/main/java/electroblob/wizardry/client/particle/ParticleEntityLinked.java @@ -0,0 +1,61 @@ +package electroblob.wizardry.client.particle; + +import net.minecraft.client.particle.Particle; +import net.minecraft.entity.Entity; +import net.minecraft.world.World; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +/** + * Abstract superclass for particles that are linked to entities, i.e. move with a given entity. This is generally used + * for visual effects in spell casting. + * + * @author Electroblob + * @since Wizardry 4.2 + */ +@SideOnly(Side.CLIENT) +public abstract class ParticleEntityLinked extends Particle { + + /** True if the particle always renders at full brightness. Defaults to false. */ + protected boolean fullBrightness = false; + + /** The entity this particle is linked to. The particle will move with this entity. */ + protected Entity entity; + + public ParticleEntityLinked(World world, Entity entity){ + super(world, entity.posX, entity.posY, entity.posZ, entity.motionX, entity.motionY, entity.motionZ); + this.entity = entity; + this.init(); + } + + public ParticleEntityLinked(World world, Entity entity, int maxAge){ + this(world, entity); + this.particleMaxAge = maxAge; + } + + /** + * Called from both constructors to set constants, avoiding duplicate code. Common fields to set here include: + * particleScale, particleGravity, canCollide, fullBrightness and setting the texture index. + */ + public abstract void init(); + + /* There are 4 layers of particles, specified as 0-3 by the method below. - Layer 0 causes the normal particles.png + * to be bound to the render engine for normal particles. - Layer 1 causes the block textures to be bound to the + * render engine for digging fx and falling fx. - Layer 2 causes the item textures to be bound to the render engine + * for tool breaking fx, snowballpoofs, slime particles, etc. - Layer 3 is not used in vanilla minecraft and was + * presumably added by forge for exactly this reason. This means no texture is bound by vanilla minecraft, meaning + * you are free to do as you wish without possibly overwriting vanilla particles. Mod particles won't be overwritten + * anyway since they bind their own textures. It is of course important to bind the texture every time you render a + * custom particle, but I don't see how you could do it any other way, since you don't have access to + * EffectRenderer. */ + @Override + public int getFXLayer(){ + // This can only be 0-3 or it will cause an ArrayIndexOutOfBoundsException in EffectRenderer. + return 3; + } + + @Override + public int getBrightnessForRender(float partialTick){ + return fullBrightness ? 15728880 : super.getBrightnessForRender(partialTick); + } +} diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleFlash.java b/src/main/java/electroblob/wizardry/client/particle/ParticleFlash.java new file mode 100644 index 00000000..82927b46 --- /dev/null +++ b/src/main/java/electroblob/wizardry/client/particle/ParticleFlash.java @@ -0,0 +1,44 @@ +package electroblob.wizardry.client.particle; + +import net.minecraft.client.renderer.BufferBuilder; +import net.minecraft.entity.Entity; +import net.minecraft.util.math.MathHelper; +import net.minecraft.world.World; + +/** + * Copied from ParticleFirework.Overlay; for some reason that class has no public constructors, plus I want to change the + * scale and a few other things + * @author Electroblob + * @since Wizardry 4.2.0 + */ +public class ParticleFlash extends ParticleWizardry { + + public ParticleFlash(World world, double x, double y, double z){ + super(world, x, y, z); + this.setRBGColorF(1, 1, 1); + this.particleScale = 7.1f; // 7.1f is the value used in fireworks + this.particleMaxAge = 4; + } + + @Override + public void renderParticle(BufferBuilder buffer, Entity entityIn, float partialTicks, float rotationX, float rotationZ, float rotationYZ, float rotationXY, float rotationXZ){ + float f4 = particleScale * MathHelper.sin(((float)this.particleAge + partialTicks - 1.0F) * 0.25F * (float)Math.PI); + this.setAlphaF(0.6F - ((float)this.particleAge + partialTicks - 1.0F) * 0.25F * 0.5F); + float f5 = (float)(this.prevPosX + (this.posX - this.prevPosX) * (double)partialTicks - interpPosX); + float f6 = (float)(this.prevPosY + (this.posY - this.prevPosY) * (double)partialTicks - interpPosY); + float f7 = (float)(this.prevPosZ + (this.posZ - this.prevPosZ) * (double)partialTicks - interpPosZ); + int i = this.getBrightnessForRender(partialTicks); + int j = i >> 16 & 65535; + int k = i & 65535; + buffer.pos((double)(f5 - rotationX * f4 - rotationXY * f4), (double)(f6 - rotationZ * f4), (double)(f7 - rotationYZ * f4 - rotationXZ * f4)).tex(0.5D, 0.375D).color(this.particleRed, this.particleGreen, this.particleBlue, this.particleAlpha).lightmap(j, k).endVertex(); + buffer.pos((double)(f5 - rotationX * f4 + rotationXY * f4), (double)(f6 + rotationZ * f4), (double)(f7 - rotationYZ * f4 + rotationXZ * f4)).tex(0.5D, 0.125D).color(this.particleRed, this.particleGreen, this.particleBlue, this.particleAlpha).lightmap(j, k).endVertex(); + buffer.pos((double)(f5 + rotationX * f4 + rotationXY * f4), (double)(f6 + rotationZ * f4), (double)(f7 + rotationYZ * f4 + rotationXZ * f4)).tex(0.25D, 0.125D).color(this.particleRed, this.particleGreen, this.particleBlue, this.particleAlpha).lightmap(j, k).endVertex(); + buffer.pos((double)(f5 + rotationX * f4 - rotationXY * f4), (double)(f6 - rotationZ * f4), (double)(f7 + rotationYZ * f4 - rotationXZ * f4)).tex(0.25D, 0.375D).color(this.particleRed, this.particleGreen, this.particleBlue, this.particleAlpha).lightmap(j, k).endVertex(); + } + + @Override + public int getBrightnessForRender(float partialTicks){ + return 15728880; + } + +} diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleMagicBubble.java b/src/main/java/electroblob/wizardry/client/particle/ParticleMagicBubble.java new file mode 100644 index 00000000..59951ea5 --- /dev/null +++ b/src/main/java/electroblob/wizardry/client/particle/ParticleMagicBubble.java @@ -0,0 +1,39 @@ +package electroblob.wizardry.client.particle; + +import net.minecraft.world.World; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +@SideOnly(Side.CLIENT) +public class ParticleMagicBubble extends ParticleWizardry { + + public ParticleMagicBubble(World world, double x, double y, double z){ + super(world, x, y, z); + this.particleRed = 1.0F; + this.particleGreen = 1.0F; + this.particleBlue = 1.0F; + this.setParticleTextureIndex(32); + this.setSize(0.02F, 0.02F); + this.particleScale *= this.rand.nextFloat() * 0.6F + 0.2F; +// this.motionX = par8 * 0.20000000298023224D + (double)((float)(Math.random() * 2.0D - 1.0D) * 0.02F); +// this.motionY = par10 * 0.20000000298023224D + (double)((float)(Math.random() * 2.0D - 1.0D) * 0.02F); +// this.motionZ = par12 * 0.20000000298023224D + (double)((float)(Math.random() * 2.0D - 1.0D) * 0.02F); + this.particleMaxAge = (int)(8.0D / (Math.random() * 0.8D + 0.2D)); + } + + @Override + public void onUpdate(){ + this.prevPosX = this.posX; + this.prevPosY = this.posY; + this.prevPosZ = this.posZ; + this.motionY += 0.002D; + this.move(this.motionX, this.motionY, this.motionZ); + this.motionX *= 0.8500000238418579D; + this.motionY *= 0.8500000238418579D; + this.motionZ *= 0.8500000238418579D; + + if(this.particleMaxAge-- <= 0){ + this.setExpired(); + } + } +} diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleWizardry.java b/src/main/java/electroblob/wizardry/client/particle/ParticleWizardry.java new file mode 100644 index 00000000..607c4da5 --- /dev/null +++ b/src/main/java/electroblob/wizardry/client/particle/ParticleWizardry.java @@ -0,0 +1,94 @@ +package electroblob.wizardry.client.particle; + +import net.minecraft.client.particle.Particle; +import net.minecraft.world.World; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +/** + * Abstract superclass for all of wizardry's particles. This replaces {@code ParticleCustomTexture} (the functionality of + * which is no longer necessary since wizardry now uses {@code TextureAtlasSprite}s to do the rendering), and fits into + * {@code ParticleBuilder} by exposing all the necessary variables through getters, allowing them to be set on the fly + * rather than needing to be passed into the constructor. + *

+ * The new system is as follows: + *

+ * - All particle classes have a single constructor which takes a world and a position only.
+ * - Each particle class defines any relevant default values in its constructor, including velocity.
+ * - The particle builder then overwrites any other values that were set during building. + *

+ * This beauty of this system is that there are never any redundant parameters when spawning particles. For example, + * snow particles nearly always fall at the same speed, which can now be defined in the particle class and no longer + * needs to be defined when spawning the particle - but importantly, it can still be overridden if desired. + * + * @author Electroblob + * @since Wizardry 4.2.0 + * @see electroblob.wizardry.util.ParticleBuilder ParticleBuilder + */ +@SideOnly(Side.CLIENT) +public abstract class ParticleWizardry extends Particle { + + /** True if the particle is shaded, false if the particle always renders at full brightness. Defaults to false. */ + protected boolean shaded = false; + + protected float fadeRed = 1; + protected float fadeGreen = 1; + protected float fadeBlue = 0; + + public ParticleWizardry(World world, double x, double y, double z){ + super(world, x, y, z); + } + + /** Sets whether the particle should render at full brightness or not. True if the particle is shaded, false if + * the particle always renders at full brightness. Defaults to false.*/ + public void setShaded(boolean shaded){ + this.shaded = shaded; + } + + /** Sets this particle's gravity. True to enable gravity, false to disable. Defaults to false.*/ + public void setGravity(boolean gravity){ + this.particleGravity = gravity ? 1 : 0; + } + + /** Sets this particle's lifetime in ticks.*/ + public void setLifetime(int lifetime){ + this.particleMaxAge = lifetime; + } + + /** + * Sets the velocity of the particle. + * @param vx The x velocity + * @param vy The y velocity + * @param vz The z velocity + */ + public void setVelocity(double vx, double vy, double vz){ + this.motionX = vx; + this.motionY = vy; + this.motionZ = vz; + } + + /** + * Sets the fade colour of the particle. + * @param r The red colour component + * @param g The green colour component + * @param g The blue colour component + */ + public void setFadeColour(float r, float g, float b){ + this.fadeRed = r; + this.fadeGreen = g; + this.fadeBlue = b; + } + + @Override + public int getBrightnessForRender(float partialTick){ + return shaded ? super.getBrightnessForRender(partialTick) : 15728880; + } + + /** Simple particle factory interface which takes a world and a position and returns a particle. Used (via lambda + * expressions) in the client proxy to link particle enum types to actual particle classes. */ + @SideOnly(Side.CLIENT) + @FunctionalInterface + public interface IWizardryParticleFactory { + ParticleWizardry createParticle(World world, double x, double y, double z); + } +} diff --git a/src/main/java/electroblob/wizardry/util/ParticleBuilder.java b/src/main/java/electroblob/wizardry/util/ParticleBuilder.java new file mode 100644 index 00000000..c09fe325 --- /dev/null +++ b/src/main/java/electroblob/wizardry/util/ParticleBuilder.java @@ -0,0 +1,378 @@ +package electroblob.wizardry.util; + +import java.util.Random; + +import electroblob.wizardry.Wizardry; +import net.minecraft.entity.Entity; +import net.minecraft.util.math.MathHelper; +import net.minecraft.world.World; + +/** + * "Don't waste time spawning particles manually - let {@code ParticleBuilder} do the work for you!" + *

+ * Singleton class that builds wizardry particles. This is an alternative (and neater, I think) solution to using varargs. + * All building methods are chainable, so particles can be created using only one line of code (This is similar to the + * BufferBuilder system). The number of different combinations of parameters now required for the various particle + * types in wizardry made the method overloads in the proxies very cumbersome and inevitably resulted in redundant + * parameters, which made the code messy and hard to read. Those methods have now been removed. + *

+ * It also goes without saying that this class should only ever be used client-side. + *

+ * {@link ParticleBuilder#instance} retrieves the static instance of the particle builder. Use + * {@link ParticleBuilder#particle(Type)} to start building a particle, or alternatively use the static + * convenience version {@link ParticleBuilder#create(Type)}. Use {@link ParticleBuilder#spawn()} + * to finish building and spawn the particle. Between these two, a variety of parameters can be set using the various + * setter methods (see individual method descriptions for more details). These, along with {@code ParticleBuilder.particle(...)}, + * return the particle builder instance, allowing them to be chained together to spawn particles using a single line of code. + * If any parameters are unspecified these will default to certain values, which may or may not depend on the particle type. + * Not all parameters affect all particles. Again, see individual method descriptions for more details. + *

+ * For example, a typical call to the particle builder might look something like this: + *

+ * ParticleBuilder.create(Type.SPARKLE).pos(x, y, z).vel(vx, vy, vz).colour(r, g, b).spawn(world); + * @author Electroblob + * @since Wizardry 4.2.0 + */ +public final class ParticleBuilder { + + /** The static instance of the particle builder. */ + public static final ParticleBuilder instance = new ParticleBuilder(); + + /** Whether the particle builder is currently building or not. */ + private boolean building = false; + + // Builder variables + // We can't just store a particle and set its parameters in the builder methods, because the server won't like having + // a field of a client-only type + private Type type; + private double x, y, z; + private double vx, vy, vz; + private float r, g, b; + private float fr, fg, fb; + private double radius; + private int lifetime; + private boolean gravity; + private boolean shaded; + private float scale; + private Entity entity; + + /** Enum constants representing the different types of particle added by wizardry. As of 4.2.0, this has been moved + * from its own file {@code Type} to inside {@link ParticleBuilder}. This allowed its name to be + * shortened to simply {@code Type}, making most references more concise. References in classes where another + * {@code Type} is also used can simply refer to the full name, {@code ParticleBuilder.Type}, which is no more verbose + * than before. + *

+ * Individual constants have comments detailing their corresponding default parameters. A range of values indicates + * randomness. */ + public static enum Type { + @Deprecated BLIZZARD, + /** Spiral particle, like potions.

Defaults:

Lifetime: 8-40 ticks
Colour: white */ DARK_MAGIC, + /** Single pixel particle.

Defaults:

Lifetime: 16-80 ticks
Colour: white */ DUST, + /** Rapid flash, like fireworks.

Defaults:

Lifetime: 4 ticks
Colour: white */ FLASH, + /** Small shard of ice.

Defaults:

Lifetime: 8-40 ticks
Gravity: true */ ICE, + /** Single green/brown leaf.

Defaults:

Lifetime: 10-15 ticks
Velocity: (0, -0.03, 0) */ LEAF, + /** Bubble that doesn't burst in air.

Defaults:

Lifetime: 8-40 ticks */ MAGIC_BUBBLE, + /** Scaleable, moving flame.

Defaults:

Lifetime: 8-40 ticks
*/ MAGIC_FIRE, + /** Soft-edged round particle.

Defaults:

Lifetime: 8-40 ticks
Colour: white */ PATH, + /** Snowflake particle.

Defaults:

Lifetime: 40-50 ticks
Velocity: (0, -0.02, 0) */ SNOW, + /** Animated lightning particle.

Defaults:

Lifetime: 3 ticks */ SPARK, + /** Animated sparkle particle.

Defaults:

Lifetime: 48-60 ticks
Colour: white */ SPARKLE, + @Deprecated SPARKLE_ROTATING + } + + private ParticleBuilder(){ + reset(); + } + + // Convenience methods + + /** + * Starts building a particle of the given type. Static convenience version of + * {@link ParticleBuilder#particle(Type)}; makes code more concise. + * @param type The type of particle to build + * @return The particle builder instance, allowing other methods to be chained onto this one + * @throws IllegalStateException if the particle builder is already building. + */ + public static ParticleBuilder create(Type type){ + return ParticleBuilder.instance.particle(type); + } + + /** + * Starts building a particle of the given type and positions it randomly within the given entity's bounding box. + * Equivalent to calling {@code ParticleBuilder.create(type).pos(...)}; users should chain any additional builder + * methods onto this one and finish with {@code .spawn(world)} as normal. + * Used extensively with summoned creatures; makes code much neater and more concise. + * @param type The type of particle to build + * @param entity The entity to position the particle at + * @return The particle builder instance, allowing other methods to be chained onto this one + * @throws IllegalStateException if the particle builder is already building. + */ + public static ParticleBuilder create(Type type, Entity entity){ + + double x = entity.posX + (entity.world.rand.nextDouble() - 0.5D) * (double)entity.width; + double y = entity.posY + entity.world.rand.nextDouble() * (double)entity.height; + double z = entity.posZ + (entity.world.rand.nextDouble() - 0.5D) * (double)entity.width; + + return ParticleBuilder.instance.particle(type).pos(x, y, z); + } + + /** + * Starts building a particle of the given type and positions it randomly within the given radius of the given position, + * with velocity proportional to distance from the given position if move is true. Good for making explosion-type effects. + * Equivalent to calling {@code ParticleBuilder.create(type).pos(...).vel(...)}; users should chain any additional builder + * methods onto this one and finish with {@code .spawn(world)} as normal. + * @param type The type of particle to build + * @param random An RNG instance + * @param x The x coordinate of the centre of the region in which to position the particle + * @param y The y coordinate of the centre of the region in which to position the particle + * @param z The z coordinate of the centre of the region in which to position the particle + * @param radius The radius of the region in which to position the particle + * @param move Whether the particle should move outwards from the centre (note that if this is false, the particle's + * default velocity will apply) + * @return The particle builder instance, allowing other methods to be chained onto this one + * @throws IllegalStateException if the particle builder is already building. + */ + public static ParticleBuilder create(Type type, Random random, double x, double y, double z, double radius, boolean move){ + + double px = x + (random.nextDouble() - 0.5D) * radius; + double py = y + (random.nextDouble() - 0.5D) * radius; + double pz = z + (random.nextDouble() - 0.5D) * radius; + + if(move) return ParticleBuilder.instance.particle(type).pos(x, y, z).vel(px-x, py-y, pz-z); + + return ParticleBuilder.instance.particle(type).pos(x, y, z); + } + + // Core builder methods + + /** + * Starts building a particle of the given type. + * @param type The type of particle to build + * @return The particle builder instance, allowing other methods to be chained onto this one + * @throws IllegalStateException if the particle builder is already building. + */ + public ParticleBuilder particle(Type type){ + if(building) throw new IllegalStateException("Already building!"); + this.type = type; + this.building = true; + return this; + } + + /** + * Sets the position of the particle being built. If unspecified, this defaults to the origin (0, 0, 0). + *

+ * Affects: All particle types + * @param x The x coordinate to set + * @param y The y coordinate to set + * @param z The z coordinate to set + * @return The particle builder instance, allowing other methods to be chained onto this one + * @throws IllegalStateException if the particle builder is not yet building. + */ + public ParticleBuilder pos(double x, double y, double z){ + if(!building) throw new IllegalStateException("Not building yet!"); + this.x = x; + this.y = y; + this.z = z; + return this; + } + + /** + * Sets the velocity of the particle being built. If unspecified, this defaults to the particle's default velocity, + * specified within its constructor. + *

+ * Affects: All particle types except {@link Type#DUST DUST} + * @param x The x coordinate to set + * @param y The y coordinate to set + * @param z The z coordinate to set + * @return The particle builder instance, allowing other methods to be chained onto this one + * @throws IllegalStateException if the particle builder is not yet building. + */ + public ParticleBuilder vel(double vx, double vy, double vz){ + if(!building) throw new IllegalStateException("Not building yet!"); + this.vx = vx; + this.vy = vy; + this.vz = vz; + return this; + } + + /** + * Sets the colour of the particle being built. If unspecified, this defaults to the particle's default colour, + * specified within its constructor. + *

+ * Affects: {@link Type#DARK_MAGIC DARK_MAGIC}, {@link Type#DUST DUST}, {@link Type#FLASH FLASH}, + * {@link Type#PATH PATH}, {@link Type#SPARKLE SPARKLE} + * @param r The red colour component to set; will be clamped to between 0 and 1 + * @param g The green colour component to set; will be clamped to between 0 and 1 + * @param b The blue colour component to set; will be clamped to between 0 and 1 + * @return The particle builder instance, allowing other methods to be chained onto this one + * @throws IllegalStateException if the particle builder is not yet building. + */ + public ParticleBuilder colour(float r, float g, float b){ + if(!building) throw new IllegalStateException("Not building yet!"); + this.r = MathHelper.clamp(r, 0, 1); + this.g = MathHelper.clamp(g, 0, 1); + this.b = MathHelper.clamp(b, 0, 1); + return this; + } + + /** + * Sets the fade colour of the particle being built. If unspecified, this defaults to the whatever the particle's base + * colour is. + *

+ * Affects: {@link Type#DUST DUST}, {@link Type#PATH PATH}, {@link Type#SPARKLE SPARKLE} + * @param r The red colour component to set; will be clamped to between 0 and 1 + * @param g The green colour component to set; will be clamped to between 0 and 1 + * @param b The blue colour component to set; will be clamped to between 0 and 1 + * @return The particle builder instance, allowing other methods to be chained onto this one + * @throws IllegalStateException if the particle builder is not yet building. + */ + public ParticleBuilder fade(float r, float g, float b){ + if(!building) throw new IllegalStateException("Not building yet!"); + this.fr = MathHelper.clamp(r, 0, 1); + this.fg = MathHelper.clamp(g, 0, 1); + this.fb = MathHelper.clamp(b, 0, 1); + return this; + } + + /** + * Sets the scale of the particle being built. If unspecified, this defaults to 1. + *

+ * Affects: All particle types + * @param scale The scale to set, as a multiple of the particle's default scale + * @return The particle builder instance, allowing other methods to be chained onto this one + * @throws IllegalStateException if the particle builder is not yet building. + */ + public ParticleBuilder scale(float scale){ + if(!building) throw new IllegalStateException("Not building yet!"); + this.scale = scale; + return this; + } + + /** + * Sets the lifetime of the particle being built. If unspecified, this defaults to the particle's default lifetime, + * specified within its constructor. + *

+ * Affects: All particle types + * @param lifetime The lifetime to set in ticks + * @return The particle builder instance, allowing other methods to be chained onto this one + * @throws IllegalStateException if the particle builder is not yet building. + */ + public ParticleBuilder lifetime(int lifetime){ + if(!building) throw new IllegalStateException("Not building yet!"); + this.lifetime = lifetime; + return this; + } + + /** + * Sets the rotation radius of the particle being built. If unspecified, this defaults to 0. + *

+ * Affects: All particle types + * @param radius The rotation radius to set + * @return The particle builder instance, allowing other methods to be chained onto this one + * @throws IllegalStateException if the particle builder is not yet building. + */ + public ParticleBuilder radius(double radius){ + if(!building) throw new IllegalStateException("Not building yet!"); + this.radius = radius; + return this; + } + + /** + * Sets the gravity of the particle being built. If unspecified, this defaults to false. + *

+ * Affects: {@link Type#ICE ICE}, {@link Type#SPARKLE SPARKLE} + * @param gravity True to enable gravity for the particle, false to disable + * @return The particle builder instance, allowing other methods to be chained onto this one + * @throws IllegalStateException if the particle builder is not yet building. + */ + public ParticleBuilder gravity(boolean gravity){ + if(!building) throw new IllegalStateException("Not building yet!"); + this.gravity = gravity; + return this; + } + + /** + * Sets the shading of the particle being built. If unspecified, this defaults to false. + *

+ * Affects: All particle types + * @param shaded True to enable shading for the particle, false for full brightness + * @return The particle builder instance, allowing other methods to be chained onto this one + * @throws IllegalStateException if the particle builder is not yet building. + */ + public ParticleBuilder shaded(boolean shaded){ + if(!building) throw new IllegalStateException("Not building yet!"); + this.shaded = shaded; + return this; + } + + /** + * Sets the entity of the particle being built. This must be specified for entity-linked particles. + * @param entity The entity to set + * @return The particle builder instance, allowing other methods to be chained onto this one + * @throws IllegalStateException if the particle builder is not yet building. + */ + public ParticleBuilder entity(Entity entity){ + if(!building) throw new IllegalStateException("Not building yet!"); + this.entity = entity; + return this; + } + + /** + * Spawns the particle that has been built and resets the particle builder. + * @param world The world in which to spawn the particle + * @throws IllegalStateException if the particle builder is not yet building. + */ + public void spawn(World world){ + + if(!building) throw new IllegalStateException("Not building yet!"); + + if(y < 0 && entity == null) Wizardry.logger.warn("Spawning particle below y = 0 - are you sure the position/entity" + + "has been set correctly?"); + + electroblob.wizardry.client.particle.ParticleWizardry particle = Wizardry.proxy.createParticle(type, world, x, y, z); + + if(particle == null){ + reset(); + return; + } + + particle.multipleParticleScaleBy(scale); + if(!Double.isNaN(vx) && !Double.isNaN(vy) && !Double.isNaN(vz)) particle.setVelocity(vx, vy, vz); + if(r >= 0 && g >= 0 && b >= 0) particle.setRBGColorF(r, g, b); + if(fr >= 0 && fg >= 0 && fb >= 0) particle.setFadeColour(fr, fg, fb); + if(lifetime >= 0) particle.setLifetime(lifetime); + particle.setGravity(gravity); + particle.setShaded(shaded); + + net.minecraft.client.Minecraft.getMinecraft().effectRenderer.addEffect(particle); + + reset(); + } + + /** Resets the state of the particle builder and resets all the builder variables to their default values. */ + private void reset(){ + building = false; + type = null; + x = 0; + y = 0; + z = 0; + // NaN indicates the velocity was not set (can't use -1 since it could very reasonably be -1) + // For all other values -1 indicates the value was not set + vx = Double.NaN; + vy = Double.NaN; + vz = Double.NaN; + r = -1; + g = -1; + b = -1; + fr = -1; + fg = -1; + fb = -1; + radius = 0; + lifetime = -1; + gravity = false; + shaded = false; + scale = 1; + entity = null; + } + +} diff --git a/src/main/resources/assets/ebwizardry/textures/particle/buff.png b/src/main/resources/assets/ebwizardry/textures/particle/buff.png new file mode 100644 index 00000000..bdc069b0 Binary files /dev/null and b/src/main/resources/assets/ebwizardry/textures/particle/buff.png differ