480 lines
19 KiB
Java
480 lines
19 KiB
Java
package me.rigamortis.seppuku.impl.module.render;
|
|
|
|
import me.rigamortis.seppuku.api.event.render.EventRender3D;
|
|
import me.rigamortis.seppuku.api.module.Module;
|
|
import me.rigamortis.seppuku.api.util.MathUtil;
|
|
import me.rigamortis.seppuku.api.util.RenderUtil;
|
|
import me.rigamortis.seppuku.api.value.Value;
|
|
import net.minecraft.block.material.Material;
|
|
import net.minecraft.block.state.IBlockState;
|
|
import net.minecraft.client.Minecraft;
|
|
import net.minecraft.client.entity.EntityPlayerSP;
|
|
import net.minecraft.client.renderer.BufferBuilder;
|
|
import net.minecraft.client.renderer.Tessellator;
|
|
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
|
|
import net.minecraft.entity.Entity;
|
|
import net.minecraft.item.Item;
|
|
import net.minecraft.item.ItemStack;
|
|
import net.minecraft.util.EnumHand;
|
|
import net.minecraft.util.math.*;
|
|
import org.lwjgl.opengl.GL11;
|
|
import org.lwjgl.opengl.GL32;
|
|
import team.stiff.pomelo.impl.annotated.handler.annotation.Listener;
|
|
|
|
import java.awt.*;
|
|
import java.util.List;
|
|
import java.util.Queue;
|
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
|
|
|
import static org.lwjgl.opengl.GL11.*;
|
|
|
|
/**
|
|
* A feature that projects the possible path of an entity that was fired.
|
|
* Throwables in this game use randomness to come up with a path that
|
|
* is <b>never</b> the same using {@link java.util.Random}.
|
|
* <p>
|
|
* This results in <b>perfect</b> predictions when disregarding
|
|
* the randomness in the trajectory.
|
|
* <p>
|
|
* todo; remove un-needed flightPoint collection
|
|
*
|
|
* @author Ddong
|
|
* @since Feb 18, 2017
|
|
*/
|
|
public final class ProjectilesModule extends Module {
|
|
|
|
private final Queue<Vec3d> flightPoint = new ConcurrentLinkedQueue<>();
|
|
|
|
public final Value<Float> width = new Value<Float>("Width", new String[]{"W", "Width"}, "Pixel width of the projectile path.", 1.0f, 0.1f, 5.0f, 0.1f);
|
|
public final Value<Color> color = new Value<Color>("PathColor", new String[]{"color", "c", "pc"}, "Change the color of the predicted path.", new Color(255, 255, 255));
|
|
public final Value<Integer> alpha = new Value<Integer>("PathAlpha", new String[]{"opacity", "a", "o", "pa", "po"}, "Alpha value for the predicted path.", 255, 1, 255, 1);
|
|
|
|
public ProjectilesModule() {
|
|
super("Projectiles", new String[]{"Proj"}, "Projects the possible path of an entity that was fired.", "NONE", -1, ModuleType.RENDER);
|
|
}
|
|
|
|
@Listener
|
|
public void onRender(EventRender3D event) {
|
|
final Minecraft mc = Minecraft.getMinecraft();
|
|
|
|
ThrowableType throwingType = this.getTypeFromCurrentItem(mc.player);
|
|
|
|
if (throwingType == ThrowableType.NONE) {
|
|
return;
|
|
}
|
|
|
|
FlightPath flightPath = new FlightPath(mc.player, throwingType);
|
|
|
|
while (!flightPath.isCollided()) {
|
|
flightPath.onUpdate();
|
|
|
|
flightPoint.offer(new Vec3d(flightPath.position.x - mc.getRenderManager().viewerPosX,
|
|
flightPath.position.y - mc.getRenderManager().viewerPosY,
|
|
flightPath.position.z - mc.getRenderManager().viewerPosZ));
|
|
}
|
|
|
|
final boolean bobbing = mc.gameSettings.viewBobbing;
|
|
mc.gameSettings.viewBobbing = false;
|
|
mc.entityRenderer.setupCameraTransform(event.getPartialTicks(), 0);
|
|
|
|
final Tessellator tessellator = Tessellator.getInstance();
|
|
final BufferBuilder bufferbuilder = tessellator.getBuffer();
|
|
|
|
RenderUtil.begin3D();
|
|
glLineWidth(width.getValue());
|
|
glEnable(GL32.GL_DEPTH_CLAMP);
|
|
while (!flightPoint.isEmpty()) {
|
|
bufferbuilder.begin(GL11.GL_LINE_STRIP, DefaultVertexFormats.POSITION_COLOR);
|
|
Vec3d head = flightPoint.poll();
|
|
if (head == null)
|
|
continue;
|
|
|
|
bufferbuilder.pos(head.x, head.y, head.z).color(this.color.getValue().getRed() / 255.0f, this.color.getValue().getGreen() / 255.0f, this.color.getValue().getBlue() / 255.0f, this.alpha.getValue() / 255.0f).endVertex();
|
|
|
|
if (flightPoint.peek() != null) {
|
|
Vec3d point = flightPoint.peek();
|
|
bufferbuilder.pos(point.x, point.y, point.z).color(this.color.getValue().getRed() / 255.0f, this.color.getValue().getGreen() / 255.0f, this.color.getValue().getBlue() / 255.0f, this.alpha.getValue() / 255.0f).endVertex();
|
|
}
|
|
|
|
tessellator.draw();
|
|
}
|
|
glDisable(GL32.GL_DEPTH_CLAMP);
|
|
|
|
mc.gameSettings.viewBobbing = bobbing;
|
|
mc.entityRenderer.setupCameraTransform(event.getPartialTicks(), 0);
|
|
|
|
if (flightPath.collided) {
|
|
final RayTraceResult hit = flightPath.target;
|
|
AxisAlignedBB bb = null;
|
|
|
|
if (hit.typeOfHit == RayTraceResult.Type.BLOCK) {
|
|
final BlockPos blockpos = hit.getBlockPos();
|
|
final IBlockState iblockstate = mc.world.getBlockState(blockpos);
|
|
|
|
if (iblockstate.getMaterial() != Material.AIR && mc.world.getWorldBorder().contains(blockpos)) {
|
|
final Vec3d interp = MathUtil.interpolateEntity(mc.player, mc.getRenderPartialTicks());
|
|
bb = iblockstate.getSelectedBoundingBox(mc.world, blockpos).grow(0.0020000000949949026D).offset(-interp.x, -interp.y, -interp.z);
|
|
}
|
|
} else if (hit.typeOfHit == RayTraceResult.Type.ENTITY && hit.entityHit != null) {
|
|
final AxisAlignedBB entityBB = hit.entityHit.getEntityBoundingBox();
|
|
bb = new AxisAlignedBB(entityBB.minX - mc.getRenderManager().viewerPosX, entityBB.minY - mc.getRenderManager().viewerPosY, entityBB.minZ - mc.getRenderManager().viewerPosZ, entityBB.maxX - mc.getRenderManager().viewerPosX, entityBB.maxY - mc.getRenderManager().viewerPosY, entityBB.maxZ - mc.getRenderManager().viewerPosZ);
|
|
}
|
|
|
|
if (bb != null) {
|
|
RenderUtil.drawBoundingBox(bb, width.getValue(), this.color.getValue().getRed() / 255.0f, this.color.getValue().getGreen() / 255.0f, this.color.getValue().getBlue() / 255.0f, this.alpha.getValue() / 255.0f);
|
|
}
|
|
}
|
|
RenderUtil.end3D();
|
|
}
|
|
|
|
private ThrowableType getTypeFromCurrentItem(EntityPlayerSP player) {
|
|
// Check if we're holding an item first
|
|
if (player.getHeldItemMainhand().isEmpty()) {
|
|
return ThrowableType.NONE;
|
|
}
|
|
|
|
final ItemStack itemStack = player.getHeldItem(EnumHand.MAIN_HAND);
|
|
// Check what type of item this is
|
|
switch (Item.getIdFromItem(itemStack.getItem())) {
|
|
case 261: // ItemBow
|
|
if (player.isHandActive())
|
|
return ThrowableType.ARROW;
|
|
break;
|
|
case 346: // ItemFishingRod
|
|
return ThrowableType.FISHING_ROD;
|
|
case 438: //splash potion
|
|
case 441: //splash potion linger
|
|
return ThrowableType.POTION;
|
|
case 384: // ItemExpBottle
|
|
return ThrowableType.EXPERIENCE;
|
|
case 332: // ItemSnowball
|
|
case 344: // ItemEgg
|
|
case 368: // ItemEnderPearl
|
|
return ThrowableType.NORMAL;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return ThrowableType.NONE;
|
|
}
|
|
|
|
enum ThrowableType {
|
|
/**
|
|
* Represents a non-throwable object.
|
|
*/
|
|
NONE(0.0f, 0.0f),
|
|
|
|
/**
|
|
* Arrows fired from a bow.
|
|
*/
|
|
ARROW(1.5f, 0.05f),
|
|
|
|
/**
|
|
* Splash potion entities
|
|
*/
|
|
POTION(0.5f, 0.05f),
|
|
|
|
/**
|
|
* Experience bottles.
|
|
*/
|
|
EXPERIENCE(0.7F, 0.07f),
|
|
|
|
/**
|
|
* The fishhook entity with a fishing rod.
|
|
*/
|
|
FISHING_ROD(1.5f, 0.04f),
|
|
|
|
/**
|
|
* Any throwable entity that doesn't have unique
|
|
* world velocity/gravity constants.
|
|
*/
|
|
NORMAL(1.5f, 0.03f);
|
|
|
|
private final float velocity;
|
|
private final float gravity;
|
|
|
|
ThrowableType(float velocity, float gravity) {
|
|
this.velocity = velocity;
|
|
this.gravity = gravity;
|
|
}
|
|
|
|
/**
|
|
* The initial velocity of the entity.
|
|
*
|
|
* @return entity velocity
|
|
*/
|
|
|
|
public float getVelocity() {
|
|
return velocity;
|
|
}
|
|
|
|
/**
|
|
* The constant gravity applied to the entity.
|
|
*
|
|
* @return constant world gravity
|
|
*/
|
|
public float getGravity() {
|
|
return gravity;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A class used to mimic the flight of an entity. Actual
|
|
* implementation resides in multiple classes but the parent of all
|
|
* of them is {@link net.minecraft.entity.projectile.EntityThrowable}
|
|
*/
|
|
private final class FlightPath {
|
|
private final EntityPlayerSP shooter;
|
|
private Vec3d position;
|
|
private Vec3d motion;
|
|
private float yaw;
|
|
private float pitch;
|
|
private AxisAlignedBB boundingBox;
|
|
private boolean collided;
|
|
private RayTraceResult target;
|
|
private final ThrowableType throwableType;
|
|
|
|
FlightPath(EntityPlayerSP player, ThrowableType throwableType) {
|
|
this.shooter = player;
|
|
this.throwableType = throwableType;
|
|
|
|
// Set the starting angles of the entity
|
|
this.setLocationAndAngles(this.shooter.posX, this.shooter.posY + this.shooter.getEyeHeight(), this.shooter.posZ,
|
|
this.shooter.rotationYaw, this.shooter.rotationPitch);
|
|
|
|
Vec3d startingOffset = new Vec3d(MathHelper.cos(this.yaw / 180.0F * (float) Math.PI) * 0.16F, 0.1d,
|
|
MathHelper.sin(this.yaw / 180.0F * (float) Math.PI) * 0.16F);
|
|
|
|
this.position = this.position.subtract(startingOffset);
|
|
// Update the entity's bounding box
|
|
this.setPosition(this.position);
|
|
|
|
// Set the entity's motion based on the shooter's rotations
|
|
this.motion = new Vec3d(-MathHelper.sin(this.yaw / 180.0F * (float) Math.PI) * MathHelper.cos(this.pitch / 180.0F * (float) Math.PI),
|
|
-MathHelper.sin(this.pitch / 180.0F * (float) Math.PI),
|
|
MathHelper.cos(this.yaw / 180.0F * (float) Math.PI) * MathHelper.cos(this.pitch / 180.0F * (float) Math.PI));
|
|
|
|
this.setThrowableHeading(this.motion, this.getInitialVelocity());
|
|
}
|
|
|
|
/**
|
|
* Update the entity's data in the world.
|
|
*/
|
|
public void onUpdate() {
|
|
// Get the predicted positions in the world
|
|
Vec3d prediction = this.position.add(this.motion);
|
|
// Check if we've collided with a block in the same time
|
|
RayTraceResult blockCollision = this.shooter.getEntityWorld().rayTraceBlocks(this.position, prediction,
|
|
this.throwableType == ThrowableType.FISHING_ROD, !this.collidesWithNoBoundingBox(), false);
|
|
// Check if we got a block collision
|
|
if (blockCollision != null) {
|
|
prediction = blockCollision.hitVec;
|
|
}
|
|
|
|
// Check entity collision
|
|
this.onCollideWithEntity(prediction, blockCollision);
|
|
|
|
// Check if we had a collision
|
|
if (this.target != null) {
|
|
this.collided = true;
|
|
// Update position
|
|
this.setPosition(this.target.hitVec);
|
|
return;
|
|
}
|
|
|
|
// Sanity check to see if we've gone below the world (if we have we will never collide)
|
|
if (this.position.y <= 0.0d) {
|
|
// Force this to true even though we haven't collided with anything
|
|
this.collided = true;
|
|
return;
|
|
}
|
|
|
|
// Update the entity's position based on velocity
|
|
this.position = this.position.add(this.motion);
|
|
float motionModifier = 0.99F;
|
|
// Check if our path will collide with water
|
|
if (this.shooter.getEntityWorld().isMaterialInBB(this.boundingBox, Material.WATER)) {
|
|
// Arrows move slower in water than normal throwables
|
|
motionModifier = this.throwableType == ThrowableType.ARROW ? 0.6F : 0.8F;
|
|
}
|
|
|
|
// Apply the fishing rod specific motion modifier
|
|
if (this.throwableType == ThrowableType.FISHING_ROD) {
|
|
motionModifier = 0.92f;
|
|
}
|
|
|
|
// Slowly decay the velocity of the path
|
|
this.motion = MathUtil.mult(this.motion, motionModifier);
|
|
// Drop the motionY by the constant gravity
|
|
this.motion = this.motion.subtract(0.0d, this.getGravityVelocity(), 0.0d);
|
|
// Update the position and bounding box
|
|
this.setPosition(this.position);
|
|
}
|
|
|
|
/**
|
|
* Checks if a specific item type will collide
|
|
* with a block that has no collision bounding box.
|
|
*
|
|
* @return true if type collides
|
|
*/
|
|
private boolean collidesWithNoBoundingBox() {
|
|
switch (this.throwableType) {
|
|
case FISHING_ROD:
|
|
case NORMAL:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if our path collides with an entity.
|
|
*
|
|
* @param prediction the predicted position
|
|
* @param blockCollision block collision if we had one
|
|
*/
|
|
private void onCollideWithEntity(Vec3d prediction, RayTraceResult blockCollision) {
|
|
Entity collidingEntity = null;
|
|
RayTraceResult collidingPosition = null;
|
|
|
|
double currentDistance = 0.0d;
|
|
// Get all possible collision entities disregarding the local player
|
|
List<Entity> collisionEntities = Minecraft.getMinecraft().world.getEntitiesWithinAABBExcludingEntity(this.shooter, this.boundingBox.expand(this.motion.x, this.motion.y, this.motion.z).grow(1.0D, 1.0D, 1.0D));
|
|
|
|
// Loop through every loaded entity in the world
|
|
for (Entity entity : collisionEntities) {
|
|
// Check if we can collide with the entity or it's ourself
|
|
if (!entity.canBeCollidedWith()) {
|
|
continue;
|
|
}
|
|
|
|
// Check if we collide with our bounding box
|
|
float collisionSize = entity.getCollisionBorderSize();
|
|
AxisAlignedBB expandedBox = entity.getEntityBoundingBox().expand(collisionSize, collisionSize, collisionSize);
|
|
RayTraceResult objectPosition = expandedBox.calculateIntercept(this.position, prediction);
|
|
|
|
// Check if we have a collision
|
|
if (objectPosition != null) {
|
|
double distanceTo = this.position.distanceTo(objectPosition.hitVec);
|
|
|
|
// Check if we've gotten a closer entity
|
|
if (distanceTo < currentDistance || currentDistance == 0.0D) {
|
|
collidingEntity = entity;
|
|
collidingPosition = objectPosition;
|
|
currentDistance = distanceTo;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if we had an entity
|
|
if (collidingEntity != null) {
|
|
// Set our target to the result
|
|
this.target = new RayTraceResult(collidingEntity, collidingPosition.hitVec);
|
|
} else {
|
|
// Fallback to the block collision
|
|
this.target = blockCollision;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the initial velocity of the entity at it's exact starting
|
|
* moment in flight.
|
|
*
|
|
* @return entity velocity in flight
|
|
*/
|
|
private float getInitialVelocity() {
|
|
switch (this.throwableType) {
|
|
// Arrows use the current use duration as a velocity multplier
|
|
case ARROW:
|
|
// Check how long we've been using the bow
|
|
int useDuration = this.shooter.getHeldItem(EnumHand.MAIN_HAND).getItem().getMaxItemUseDuration(this.shooter.getHeldItem(EnumHand.MAIN_HAND)) - this.shooter.getItemInUseCount();
|
|
float velocity = (float) useDuration / 20.0F;
|
|
velocity = (velocity * velocity + velocity * 2.0F) / 3.0F;
|
|
if (velocity > 1.0F) {
|
|
velocity = 1.0F;
|
|
}
|
|
|
|
// When the arrow is spawned inside of ItemBow, they multiply it by 2
|
|
return (velocity * 2.0f) * throwableType.getVelocity();
|
|
default:
|
|
return throwableType.getVelocity();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the constant gravity of the item in use.
|
|
*
|
|
* @return gravity relating to item
|
|
*/
|
|
private float getGravityVelocity() {
|
|
return throwableType.getGravity();
|
|
}
|
|
|
|
/**
|
|
* Set the position and rotation of the entity in the world.
|
|
*
|
|
* @param x x position in world
|
|
* @param y y position in world
|
|
* @param z z position in world
|
|
* @param yaw yaw rotation axis
|
|
* @param pitch pitch rotation axis
|
|
*/
|
|
private void setLocationAndAngles(double x, double y, double z, float yaw, float pitch) {
|
|
this.position = new Vec3d(x, y, z);
|
|
this.yaw = yaw;
|
|
this.pitch = pitch;
|
|
}
|
|
|
|
/**
|
|
* Sets the x,y,z of the entity from the given parameters. Also seems to set
|
|
* up a bounding box.
|
|
*
|
|
* @param position position in world
|
|
*/
|
|
private void setPosition(Vec3d position) {
|
|
this.position = new Vec3d(position.x, position.y, position.z);
|
|
// Usually this is this.width / 2.0f but throwables change
|
|
double entitySize = (this.throwableType == ThrowableType.ARROW ? 0.5d : 0.25d) / 2.0d;
|
|
// Update the path's current bounding box
|
|
this.boundingBox = new AxisAlignedBB(position.x - entitySize,
|
|
position.y - entitySize,
|
|
position.z - entitySize,
|
|
position.x + entitySize,
|
|
position.y + entitySize,
|
|
position.z + entitySize);
|
|
}
|
|
|
|
/**
|
|
* Set the entity's velocity and position in the world.
|
|
*
|
|
* @param motion velocity in world
|
|
* @param velocity starting velocity
|
|
*/
|
|
private void setThrowableHeading(Vec3d motion, float velocity) {
|
|
// Divide the current motion by the length of the vector
|
|
this.motion = MathUtil.div(motion, (float) motion.length());
|
|
// Multiply by the velocity
|
|
this.motion = MathUtil.mult(this.motion, velocity);
|
|
}
|
|
|
|
/**
|
|
* Check if the path has collided with an object.
|
|
*
|
|
* @return path collides with ground
|
|
*/
|
|
public boolean isCollided() {
|
|
return collided;
|
|
}
|
|
|
|
/**
|
|
* Get the target we've collided with if it exists.
|
|
*
|
|
* @return moving object target
|
|
*/
|
|
public RayTraceResult getCollidingTarget() {
|
|
return target;
|
|
}
|
|
}
|
|
|
|
}
|