diff --git a/src/main/java/electroblob/wizardry/entity/projectile/EntityMagicProjectile.java b/src/main/java/electroblob/wizardry/entity/projectile/EntityMagicProjectile.java index 3246d818..fa94a473 100644 --- a/src/main/java/electroblob/wizardry/entity/projectile/EntityMagicProjectile.java +++ b/src/main/java/electroblob/wizardry/entity/projectile/EntityMagicProjectile.java @@ -71,7 +71,7 @@ public abstract class EntityMagicProjectile extends EntityThrowable implements I double dy = !this.hasNoGravity() ? target.getEntityBoundingBox().minY + (double)(target.height / 3.0f) - this.posY : target.getEntityBoundingBox().minY + (double)(target.height / 2.0f) - this.posY; double dz = target.posZ - caster.posZ; - double horizontalDistance = (double)MathHelper.sqrt(dx * dx + dz * dz); + double horizontalDistance = MathHelper.sqrt(dx * dx + dz * dz); if(horizontalDistance >= 1.0E-7D){ diff --git a/src/main/java/electroblob/wizardry/registry/Spells.java b/src/main/java/electroblob/wizardry/registry/Spells.java index dd577d8d..85ee05a1 100644 --- a/src/main/java/electroblob/wizardry/registry/Spells.java +++ b/src/main/java/electroblob/wizardry/registry/Spells.java @@ -5,6 +5,7 @@ import electroblob.wizardry.entity.construct.*; import electroblob.wizardry.entity.living.*; import electroblob.wizardry.entity.projectile.*; import electroblob.wizardry.spell.*; +import net.minecraft.entity.projectile.EntitySnowball; import net.minecraft.init.MobEffects; import net.minecraft.item.EnumAction; import net.minecraft.util.ResourceLocation; @@ -251,7 +252,7 @@ public final class Spells { }.addProperties(Spell.DAMAGE).soundValues(1, 1.4f, 0.4f)); registry.register(new Ignite()); registry.register(new Freeze()); - registry.register(new Snowball()); + registry.register(new SpellThrowable<>("snowball", EntitySnowball::new).npcSelector((e, o) -> o).soundValues(0.5f, 0.4f, 0.2f)); // Let's spare wizards the pain of the snowball spell registry.register(new Arc()); registry.register(new SpellProjectile<>("thunderbolt", EntityThunderbolt::new).addProperties(Spell.DAMAGE, EntityThunderbolt.KNOCKBACK_STRENGTH).soundValues(0.8f, 0.9f, 0.2f)); registry.register(new SummonZombie()); diff --git a/src/main/java/electroblob/wizardry/spell/Snowball.java b/src/main/java/electroblob/wizardry/spell/Snowball.java deleted file mode 100644 index 500488fd..00000000 --- a/src/main/java/electroblob/wizardry/spell/Snowball.java +++ /dev/null @@ -1,45 +0,0 @@ -package electroblob.wizardry.spell; - -import electroblob.wizardry.registry.WizardryItems; -import electroblob.wizardry.util.SpellModifiers; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.projectile.EntitySnowball; -import net.minecraft.item.EnumAction; -import net.minecraft.util.EnumHand; -import net.minecraft.util.math.MathHelper; -import net.minecraft.world.World; - -public class Snowball extends Spell { - - public Snowball(){ - super("snowball", EnumAction.NONE, false); - addProperties(RANGE); - soundValues(0.5f, 0.4f, 0.2f); - } - - @Override - public boolean requiresPacket(){ - return false; - } - - @Override - public boolean cast(World world, EntityPlayer caster, EnumHand hand, int ticksInUse, SpellModifiers modifiers){ - - if(!world.isRemote){ - // Trajectory calculation - see SpellProjectile for a more detailed explanation - float g = 0.03f; - float launchHeight = caster.getEyeHeight(); - float range = getProperty(RANGE).floatValue() * modifiers.get(WizardryItems.range_upgrade); - float velocity = MathHelper.sqrt(MathHelper.sqrt(g*g * (launchHeight*launchHeight + range*range)) - g*launchHeight); - - EntitySnowball snowball = new EntitySnowball(world, caster); - snowball.shoot(caster, caster.rotationPitch, caster.rotationYaw, 0.0f, velocity, 1.0f); - world.spawnEntity(snowball); - } - - this.playSound(world, caster, ticksInUse, -1, modifiers); - caster.swingArm(hand); - return true; - } - -} diff --git a/src/main/java/electroblob/wizardry/spell/SpellThrowable.java b/src/main/java/electroblob/wizardry/spell/SpellThrowable.java new file mode 100644 index 00000000..d3145dd6 --- /dev/null +++ b/src/main/java/electroblob/wizardry/spell/SpellThrowable.java @@ -0,0 +1,142 @@ +package electroblob.wizardry.spell; + +import electroblob.wizardry.Wizardry; +import electroblob.wizardry.entity.living.ISpellCaster; +import electroblob.wizardry.registry.WizardryItems; +import electroblob.wizardry.util.SpellModifiers; +import electroblob.wizardry.util.WizardryUtilities; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLiving; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.projectile.EntityThrowable; +import net.minecraft.item.EnumAction; +import net.minecraft.tileentity.TileEntityDispenser; +import net.minecraft.util.EnumHand; +import net.minecraft.util.math.MathHelper; +import net.minecraft.world.World; + +import java.util.function.BiFunction; + +/** + * Similar to {@link electroblob.wizardry.spell.SpellProjectile}, but for any {@link EntityThrowable}. + * This allows all the relevant code to be centralised, since these spells all work in the same way. Usually, a simple + * instantiation of this class is sufficient to create a projectile spell; if something extra needs to be done, such as + * particle spawning, then methods can be overridden (perhaps using an anonymous class) to add the required functionality. + *

+ * N.B. It is advised that this class is only used where the projectile to be launched belongs to vanilla Minecraft + * or another mod; no guarantees are made as to the behaviour of such projectiles! + *

+ * Properties added by this type of spell: {@link Spell#RANGE} + *

+ * By default, this type of spell can be cast by NPCs. {@link Spell#canBeCastBy(EntityLiving, boolean)} + *

+ * By default, this type of spell cannot be cast by dispensers. {@link Spell#canBeCastBy(TileEntityDispenser)} + *

+ * By default, this type of spell does not require a packet to be sent. {@link Spell#requiresPacket()} + * + * @author Electroblob + * @since Wizardry 4.2.8 + */ +// TODO: Use events to make these projectiles seek targets when the caster is wearing a ring of attraction (is this possible?) +public class SpellThrowable extends Spell { + + private static final float LAUNCH_Y_OFFSET = 0.1f; + + protected final BiFunction projectileFactory; + + public SpellThrowable(String name, BiFunction projectileFactory){ + this(Wizardry.MODID, name, projectileFactory); + } + + public SpellThrowable(String modID, String name, BiFunction projectileFactory){ + super(modID, name, EnumAction.NONE, false); + this.projectileFactory = projectileFactory; + addProperties(RANGE); + this.npcSelector((e, o) -> true); + } + + @Override + public boolean requiresPacket(){ + return false; + } + + /** Trajectory calculation - see {@link SpellProjectile} for a more detailed explanation */ + protected float calculateVelocity(SpellModifiers modifiers, float launchHeight){ + float g = 0.03f; + float range = getProperty(RANGE).floatValue() * modifiers.get(WizardryItems.range_upgrade); + return range / MathHelper.sqrt(2 * launchHeight/g); + } + + @Override + public boolean cast(World world, EntityPlayer caster, EnumHand hand, int ticksInUse, SpellModifiers modifiers){ + + if(!world.isRemote){ + float velocity = calculateVelocity(modifiers, caster.getEyeHeight() - LAUNCH_Y_OFFSET); + T projectile = projectileFactory.apply(world, caster); + projectile.shoot(caster, caster.rotationPitch, caster.rotationYaw, 0.0f, velocity, 1.0f); + addProjectileExtras(projectile, caster, modifiers); + world.spawnEntity(projectile); + } + + this.playSound(world, caster, ticksInUse, -1, modifiers); + caster.swingArm(hand); + return true; + } + + @Override + public boolean cast(World world, EntityLiving caster, EnumHand hand, int ticksInUse, EntityLivingBase target, SpellModifiers modifiers){ + + if(target != null){ + + if(!world.isRemote){ + float velocity = calculateVelocity(modifiers, caster.getEyeHeight() - LAUNCH_Y_OFFSET); + T projectile = projectileFactory.apply(world, caster); + int aimingError = caster instanceof ISpellCaster ? ((ISpellCaster)caster).getAimingError(world.getDifficulty()) + : WizardryUtilities.getDefaultAimingError(world.getDifficulty()); + aim(projectile, caster, target, velocity, aimingError); + addProjectileExtras(projectile, caster, modifiers); + world.spawnEntity(projectile); + } + + this.playSound(world, caster, ticksInUse, -1, modifiers); + caster.swingArm(hand); + return true; + } + + return false; + } + + // Copied from EntityMagicProjectile (ugh what a mess) + private void aim(T throwable, EntityLivingBase caster, Entity target, float speed, float aimingError){ + + throwable.ignoreEntity = caster; + + throwable.posY = caster.getEntityBoundingBox().minY + (double)caster.getEyeHeight() - LAUNCH_Y_OFFSET; + double dx = target.posX - caster.posX; + double dy = !throwable.hasNoGravity() ? target.getEntityBoundingBox().minY + (double)(target.height / 3.0f) - throwable.posY + : target.getEntityBoundingBox().minY + (double)(target.height / 2.0f) - throwable.posY; + double dz = target.posZ - caster.posZ; + double horizontalDistance = MathHelper.sqrt(dx * dx + dz * dz); + + if(horizontalDistance >= 1.0E-7D){ + + double dxNormalised = dx / horizontalDistance; + double dzNormalised = dz / horizontalDistance; + throwable.setPosition(caster.posX + dxNormalised, throwable.posY, caster.posZ + dzNormalised); + + // Depends on the horizontal distance between the two entities and accounts for bullet drop, + // but of course if gravity is ignored throwable should be 0 since there is no bullet drop. + float bulletDropCompensation = !throwable.hasNoGravity() ? (float)horizontalDistance * 0.2f : 0; + // It turns out that throwable method normalises the input (x, y, z) anyway + throwable.shoot(dx, dy + (double)bulletDropCompensation, dz, speed, aimingError); + } + } + + /** + * Does nothing by default, but can be overridden to call extra methods or set additional fields on the launched + * projectile. + */ + protected void addProjectileExtras(T projectile, EntityLivingBase caster, SpellModifiers modifiers){} + +} \ No newline at end of file