package openblocks.common.tileentity;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import net.minecraft.block.Block;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.inventory.IInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.ChatComponentTranslation;
import net.minecraft.util.Vec3;
import net.minecraftforge.common.util.ForgeDirection;
import openblocks.api.IPointable;
import openblocks.common.entity.EntityItemProjectile;
import openblocks.rpc.ITriggerable;
import openmods.Log;
import openmods.api.ISurfaceAttachment;
import openmods.inventory.legacy.ItemDistribution;
import openmods.sync.SyncableDouble;
import openmods.tileentity.SyncedTileEntity;
import openmods.utils.InventoryUtils;
import openmods.utils.render.GeometryUtils;
public class TileEntityCannon extends SyncedTileEntity implements IPointable, ISurfaceAttachment, ITriggerable {
/*
* Blocks and Entities have a right-angle offset
*/
private static final int YAW_OFFSET_DEGREES = -90;
private static final int KNOB_YAW_CHANGE_SPEED = 3;
private static final int KNOB_PITCH_CHANGE_SPEED = 20;
private static final int KNOB_VEL_CHANGE_SPEED = 20;
private static final int KNOB_LOB_MINIMUM_VALUE = 20;
private static final int KNOB_LOB_MAXIMUM_VALUE = 75;
private static final int KNOB_LOB_VERTICAL_MUL = 4;
private static final int KNOB_LOB_HORIZONTAL_MUL = 1;
private static final int KNOB_LOB_BONUS = 5;
public SyncableDouble targetPitch;
public SyncableDouble targetYaw;
public SyncableDouble targetSpeed;
public double currentPitch = 45;
public double currentYaw = 0;
private double currentSpeed = 1.4;
public Vec3 motion;
public boolean renderLine = true;
private int ticksSinceLastFire = Integer.MAX_VALUE;
private Vec3 projectileOrigin = null;
@Override
protected void createSyncedFields() {
targetPitch = new SyncableDouble();
targetYaw = new SyncableDouble();
targetSpeed = new SyncableDouble(1.4);
}
@Override
@SideOnly(Side.CLIENT)
public void prepareForInventoryRender(Block block, int metadata) {
super.prepareForInventoryRender(block, metadata);
renderLine = false;
}
@Override
public void updateEntity() {
checkOrigin();
if (Double.isNaN(currentPitch)) {
Log.warn("Pitch was NaN");
currentPitch = 45;
targetPitch.set(currentPitch);
}
if (Double.isNaN(currentYaw)) {
Log.warn("Yaw was NaN");
currentYaw = 0;
}
super.updateEntity();
// ugly, need to clean
currentPitch = currentPitch - ((currentPitch - targetPitch.get()) / KNOB_PITCH_CHANGE_SPEED);
currentYaw = GeometryUtils.normalizeAngle(currentYaw);
final double targetYaw = GeometryUtils.normalizeAngle(this.targetYaw.get());
if (Math.abs(currentYaw - targetYaw) < KNOB_YAW_CHANGE_SPEED) currentYaw = targetYaw;
else {
double dist = GeometryUtils.getAngleDistance(currentYaw, targetYaw);
currentYaw += KNOB_YAW_CHANGE_SPEED * Math.signum(dist);
}
currentSpeed = currentSpeed - ((currentSpeed - targetSpeed.get()) / KNOB_VEL_CHANGE_SPEED);
invalidateMotion();
if (!worldObj.isRemote) {
if (worldObj.getTotalWorldTime() % 20 == 0) {
if (worldObj.isBlockIndirectlyGettingPowered(xCoord, yCoord, zCoord)) {
ItemStack stack = findStack();
if (stack != null) fireStack(stack);
}
}
} else {
if (ticksSinceLastFire < 100) {
ticksSinceLastFire++;
}
}
}
private ItemStack findStack() {
for (ForgeDirection direction : ForgeDirection.VALID_DIRECTIONS) {
IInventory inventory = InventoryUtils.getInventory(worldObj, xCoord, yCoord, zCoord, direction);
if (inventory != null) {
ItemStack stack = ItemDistribution.removeFromFirstNonEmptySlot(inventory);
if (stack != null) return stack;
}
}
return null;
}
private void fireStack(ItemStack stack) {
final ITriggerable rpc = createServerRpcProxy(ITriggerable.class);
rpc.trigger();
// projectileOrigin is not used here, it's used for the calculations below.
EntityItem item = new EntityItemProjectile(worldObj, xCoord + 0.5, yCoord + 0.5, zCoord + 0.5, stack);
item.delayBeforeCanPickup = 20;
// Now that we generate vectors instead of eular angles, this should be revised.
Vec3 motion = getMotion();
item.motionX = motion.xCoord;
item.motionY = motion.yCoord;
item.motionZ = motion.zCoord;
worldObj.spawnEntityInWorld(item);
worldObj.playSoundEffect(xCoord + 0.5, yCoord + 0.5, zCoord + 0.5, "openblocks:cannon.activate", 0.2f, 1.0f);
}
@Override
public void trigger() {
ticksSinceLastFire = 0;
double pitchRad = Math.toRadians(currentYaw - 90);
double x = -0.5 * Math.cos(pitchRad);
double z = -0.5 * Math.sin(pitchRad);
for (int i = 0; i < 20; i++) {
worldObj.spawnParticle((i < 4? "large" : "") + "smoke", x + xCoord + 0.3 + (worldObj.rand.nextDouble() * 0.4), yCoord + 0.7, z + zCoord + 0.3 + (worldObj.rand.nextDouble() * 0.4), 0.0D, 0.0D, 0.0D);
}
}
public int getTicksSinceLastFire() {
return ticksSinceLastFire;
}
@Override
@SideOnly(Side.CLIENT)
public AxisAlignedBB getRenderBoundingBox() {
AxisAlignedBB box = super.getRenderBoundingBox();
return box.expand(32.0, 32.0, 32.0);
}
private Vec3 calcMotionFromAngles() {
double p = Math.toRadians(currentPitch);
double y = Math.toRadians(180 - currentYaw);
double sinPitch = Math.sin(p);
double cosPitch = Math.cos(p);
double sinYaw = Math.sin(y);
double cosYaw = Math.cos(y);
return Vec3.createVectorHelper(-cosPitch * sinYaw * currentSpeed,
sinPitch * currentSpeed,
-cosPitch * cosYaw * currentSpeed);
}
private void invalidateMotion() {
motion = null;
}
public Vec3 getMotion() {
if (motion == null) motion = calcMotionFromAngles();
return motion;
}
private void checkOrigin() {
if (projectileOrigin == null) {
projectileOrigin = Vec3.createVectorHelper(xCoord + 0.5, yCoord, zCoord + 0.5);
}
}
public void setTarget(int x, int y, int z) {
checkOrigin();
// We target the middle of the block, at the very top.
final Vec3 target = Vec3.createVectorHelper(x + 0.5, y + 1, z + 0.5);
// Horizontal distance between the origin and target
final double distHorizontal = KNOB_LOB_HORIZONTAL_MUL * Math.sqrt(
Math.pow(target.xCoord - projectileOrigin.xCoord, 2)
+ Math.pow(target.zCoord - projectileOrigin.zCoord, 2));
// No vertical multiplier is applied for decline slopes.
final double distVertical = Math.max((target.yCoord - projectileOrigin.yCoord) * KNOB_LOB_VERTICAL_MUL, 0);
// Calculate the arc of the trajectory
final float lobScale = (float)Math.min(KNOB_LOB_MAXIMUM_VALUE,
Math.max(KNOB_LOB_MINIMUM_VALUE,
KNOB_LOB_BONUS + distHorizontal + distVertical));
// Calculate the velocity of the projectile
final Vec3 velocity = TileEntityCannonLogic.calculateTrajectory(projectileOrigin, target, lobScale);
// m/s applied to item.
final double speed = velocity.lengthVector();
targetSpeed.set(speed);
// reverse the vector to angles for cannon model
final Vec3 direction = velocity.normalize();
final double pitch = Math.asin(direction.yCoord);
final double yaw = Math.atan2(direction.zCoord, direction.xCoord);
// Set yaw and pitch
targetYaw.set(Math.toDegrees(yaw) + YAW_OFFSET_DEGREES);
targetPitch.set(Math.toDegrees(pitch));
currentYaw = targetYaw.get();
currentPitch = targetPitch.get();
// Sync targets
sync();
}
public void disableLineRender() {
renderLine = false;
}
@Override
public void onPointingStart(ItemStack itemStack, EntityPlayer player) {
player.addChatComponentMessage(new ChatComponentTranslation("openblocks.misc.selected_cannon"));
}
@Override
public void onPointingEnd(ItemStack itemStack, EntityPlayer player, int x, int y, int z) {
player.addChatMessage(new ChatComponentTranslation("openblocks.misc.pointed_cannon", x, y, z));
setTarget(x, y, z);
}
public void setSpeed(double speed) {
targetSpeed.set(speed);
sync();
}
public void setPitch(double pitch2) {
targetPitch.set(pitch2);
sync();
}
public void setYaw(double yaw2) {
targetYaw.set(yaw2);
sync();
}
static class TileEntityCannonLogic {
/*
* Hello, If you think you can improve the code below to work better,
* all power to you! But please, if you give up and revert your changes.
* Increment the counter below as an increasing warning to the next
* sorry soul that thinks they can make this work better.
* Regards -NC
*/
public static final int HOURS_WASTED_ON_CANNON_LOGIC = 14;
/**
* 20 physics ticks per second (on a good day)
*/
private static final double PHYS_STEPS_PER_SECOND = 20D;
/**
* Physics calculation time in partial seconds
*/
private static final double PHYS_PARTIAL_TIME = 1D / PHYS_STEPS_PER_SECOND;
/**
* Minecraft known gravity in meters/second/second
*/
private static final double PHYS_WORLD_GRAVITY = 0.8D;
/**
* Amount of gravity acceleration per physics tick.
*/
private static final double PHYS_PARTIAL_WORLD_GRAVITY = PHYS_WORLD_GRAVITY * PHYS_PARTIAL_TIME;
/**
* Squared timestep for acceleration
*/
private static final double PHYS_PARTIAL_TIME_SQUARE = PHYS_PARTIAL_TIME * PHYS_PARTIAL_TIME;
/**
* Physics gravity vector in partial time squared for acceleration calculation
*/
private static final Vec3 PHYS_GRAVITY_VECTOR_SQUARE_PARTIAL = Vec3.createVectorHelper(0, PHYS_PARTIAL_TIME_SQUARE * -PHYS_PARTIAL_WORLD_GRAVITY, 0);
/**
* The actual work for calculating trajectory. Which is much simpler now.
*
* @param start
* The origin of the projectile to be fired
* @param target
* The target location of the projectile
* @param scale
* The arcing size of the trajectory
* @return Vector to achieve trajectory
*/
public static Vec3 calculateTrajectory(Vec3 start, Vec3 target, float scale) {
final double n = scale * PHYS_STEPS_PER_SECOND;
final double accelerationMultiplier = 0.5 * n * n + n; // (n^2+n)/2
final Vec3 scaledAcceleration = Vec3.createVectorHelper(
PHYS_GRAVITY_VECTOR_SQUARE_PARTIAL.xCoord * accelerationMultiplier,
PHYS_GRAVITY_VECTOR_SQUARE_PARTIAL.yCoord * accelerationMultiplier,
PHYS_GRAVITY_VECTOR_SQUARE_PARTIAL.zCoord * accelerationMultiplier);
// -1 /n * Phys = -Phys / n
final double velocityMultiplier = -PHYS_STEPS_PER_SECOND / n;
final Vec3 velocity = Vec3.createVectorHelper(
(start.xCoord + scaledAcceleration.xCoord - target.xCoord) * velocityMultiplier,
(start.yCoord + scaledAcceleration.yCoord - target.yCoord) * velocityMultiplier,
(start.zCoord + scaledAcceleration.zCoord - target.zCoord) * velocityMultiplier);
return velocity;
}
}
@Override
public ForgeDirection getSurfaceDirection() {
return ForgeDirection.DOWN;
}
}