/**
Copyright (C) <2017> <coolAlias>
This file is part of coolAlias' Zelda Sword Skills Minecraft Mod; as such,
you can redistribute it and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package zeldaswordskills.entity.projectile;
import net.minecraft.block.Block;
import net.minecraft.block.BlockFence;
import net.minecraft.block.BlockHugeMushroom;
import net.minecraft.block.BlockLadder;
import net.minecraft.block.BlockLever;
import net.minecraft.block.BlockLog;
import net.minecraft.block.BlockSandStone;
import net.minecraft.block.BlockSign;
import net.minecraft.block.BlockTorch;
import net.minecraft.block.material.Material;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.entity.projectile.EntityThrowable;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.BlockPos;
import net.minecraft.util.DamageSource;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.MathHelper;
import net.minecraft.util.MovingObjectPosition;
import net.minecraft.util.MovingObjectPosition.MovingObjectType;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import net.minecraftforge.fml.common.eventhandler.Event.Result;
import zeldaswordskills.ZSSAchievements;
import zeldaswordskills.api.block.IWhipBlock;
import zeldaswordskills.api.block.IWhipBlock.WhipType;
import zeldaswordskills.api.damage.DamageUtils.DamageSourceBaseIndirect;
import zeldaswordskills.api.entity.IEntityLootable;
import zeldaswordskills.api.entity.LootableEntityRegistry;
import zeldaswordskills.api.item.ArmorIndex;
import zeldaswordskills.entity.player.ZSSPlayerSkills;
import zeldaswordskills.item.ItemWhip;
import zeldaswordskills.network.PacketDispatcher;
import zeldaswordskills.network.client.UnpressKeyPacket;
import zeldaswordskills.network.server.FallDistancePacket;
import zeldaswordskills.ref.Config;
import zeldaswordskills.ref.Sounds;
import zeldaswordskills.skills.SkillBase;
import zeldaswordskills.skills.sword.Parry;
import zeldaswordskills.util.TargetUtils;
import zeldaswordskills.util.WorldUtils;
/**
*
* A whip: let's the player swing from certain objects and
* deals slight damage to entities struck.
*
*/
public class EntityWhip extends EntityThrowable
{
/** Watchable object index for thrower entity's id, since EntityThrowable#thrower is both private and not synced to client */
protected static final int THROWER_INDEX = 22;
/** Watchable object index for whip's type */
protected static final int WHIP_TYPE_INDEX = 23;
/** Watchable object for if the player has reached the hookshot */
protected static final int IN_GROUND_INDEX = 24;
/** Watchable objects for whip's impact position, set to center of side of block hit;
* used for determining swing point and also for rendering */
public static final int HIT_POS_X = 25, HIT_POS_Y = 26, HIT_POS_Z = 27;
/** Impact position, used for retrieving block; needed on both sides (requires DataWatcher as of 1.8) */
public static final int HIT_X = 28, HIT_Y = 29, HIT_Z = 30;
/** Needed since 1.8 to adjust the pivot position for the swinging entity's eye height */
public static final int SIDE_HIT = 31;
/** Number of ticks since whip has latched onto a block, since EntityThrowable#ticksInGround field is private */
private int ticksInGround = 0;
/** Number of ticks player has been swinging */
private int swingTicks = 0;
/** Swing vector heading */
private Vec3 swingVec = null;
/** The y-motion factor, based on distance from rotation point */
private double dy;
public EntityWhip(World world) {
super(world);
}
public EntityWhip(World world, EntityLivingBase entity) {
super(world, entity);
}
public EntityWhip(World world, double x, double y, double z) {
super(world, x, y, z);
}
@Override
protected void entityInit() {
super.entityInit();
setSize(0.25F, 0.25F);
dataWatcher.addObject(THROWER_INDEX, "");
dataWatcher.addObject(WHIP_TYPE_INDEX, WhipType.WHIP_SHORT.ordinal());
dataWatcher.addObject(IN_GROUND_INDEX, (byte) 0);
dataWatcher.addObject(HIT_POS_X, 0.0F);
dataWatcher.addObject(HIT_POS_Y, 0.0F);
dataWatcher.addObject(HIT_POS_Z, 0.0F);
dataWatcher.addObject(HIT_X, 0);
dataWatcher.addObject(HIT_Y, 0);
dataWatcher.addObject(HIT_Z, 0);
dataWatcher.addObject(SIDE_HIT, 0);
}
/**
* Return's this entity's hookshot Type
*/
public WhipType getType() {
return WhipType.values()[dataWatcher.getWatchableObjectInt(WHIP_TYPE_INDEX) % WhipType.values().length];
}
/**
* Sets the whip's type; returns itself for convenience
*/
public EntityWhip setType(WhipType type) {
dataWatcher.updateObject(WHIP_TYPE_INDEX, type.ordinal());
return this;
}
public float getMaxDistance() {
return (float) Config.getWhipRange() * (getType().isExtended() ? 1.5F : 1.0F);
}
public void setThrower(EntityPlayer player) {
dataWatcher.updateObject(THROWER_INDEX, player != null ? player.getName() : "");
}
@Override
public EntityLivingBase getThrower() {
String name = dataWatcher.getWatchableObjectString(THROWER_INDEX);
return (name.equals("") ? null : worldObj.getPlayerEntityByName(name));
}
/**
* Returns the block position of the actual block struck, or null if no block was struck
*/
protected BlockPos getHitBlockPosition() {
if (isInGround()) {
return new BlockPos(dataWatcher.getWatchableObjectInt(HIT_X), dataWatcher.getWatchableObjectInt(HIT_Y), dataWatcher.getWatchableObjectInt(HIT_Z));
}
return null;
}
/**
* Set the position of the actual block struck in onImpact
*/
protected void setHitBlockPosition(BlockPos pos, EnumFacing face) {
dataWatcher.updateObject(HIT_X, pos.getX());
dataWatcher.updateObject(HIT_Y, pos.getY());
dataWatcher.updateObject(HIT_Z, pos.getZ());
dataWatcher.updateObject(SIDE_HIT, face.getIndex());
}
public boolean isInGround() {
return (dataWatcher.getWatchableObjectByte(IN_GROUND_INDEX) & 1) == 1;
}
protected void setInGround(boolean isInGround) {
dataWatcher.updateObject(IN_GROUND_INDEX, isInGround ? (byte) 1 : (byte) 0);
inGround = isInGround;
}
protected float getDamage() {
switch(getType()) {
case WHIP_SHORT: return 1.0F;
case WHIP_LONG: return 2.0F;
case WHIP_MAGIC: return 4.0F;
}
return 1.0F;
}
/** Returns a whip damage source */
protected DamageSource getDamageSource() {
return new DamageSourceBaseIndirect("whip", this, getThrower()).setStunDamage(40, 10, true);
}
/**
* Returns true if the whip can destroy the material type
*/
protected boolean canBreakBlock(Block block, Material m, BlockPos pos, EnumFacing side) {
EntityLivingBase thrower = getThrower();
if (block instanceof IWhipBlock) {
return ((IWhipBlock) block).canBreakBlock(getType(), thrower, worldObj, pos, side);
}
boolean isBreakable = block.getBlockHardness(worldObj, pos) >= 0.0F;
boolean canPlayerEdit = false;
if (thrower instanceof EntityPlayer) {
canPlayerEdit = ((EntityPlayer) thrower).capabilities.allowEdit && Config.canHookshotBreakBlocks();
}
// can dislodge blocks such as torches, leaves, flowers, etc.
return (isBreakable && canPlayerEdit && (block instanceof BlockTorch || m == Material.leaves || m == Material.plants));
}
/**
* Returns true if the whip can grapple the block at the position
*/
protected boolean canGrabBlock(Block block, BlockPos pos, EnumFacing face) {
if (block instanceof IWhipBlock) {
return ((IWhipBlock) block).canGrabBlock(getType(), getThrower(), worldObj, pos, face);
}
switch (getType()) {
case WHIP_MAGIC:
// this excludes things like dirt, most plants, etc.
if (block instanceof BlockSandStone || block instanceof BlockHugeMushroom ||
(block.getMaterial().blocksMovement() && block.getBlockHardness(worldObj, pos) > 1.0F)) {
return true;
} // otherwise, fall through to standard case:
case WHIP_SHORT:
case WHIP_LONG:
int clear = 0;
if (isSideClear(pos.east()) && isSideClear(pos.west())) {
++clear;
}
if (isSideClear(pos.up()) && isSideClear(pos.down())) {
++clear;
}
if (isSideClear(pos.south()) && isSideClear(pos.north())) {
++clear;
}
return (clear > 1 && (block instanceof BlockFence || block instanceof BlockLog ||
block instanceof BlockLever || block instanceof BlockSign ||
block instanceof BlockLadder));
}
return false;
}
/**
* Returns true if the position given is clear of obstacles, such that
* the whip would be able to freely move through the space and latch on
*/
protected boolean isSideClear(BlockPos pos) {
Material m = worldObj.getBlockState(pos).getBlock().getMaterial();
return (!m.blocksMovement() || m == Material.leaves);
}
@Override
protected float getVelocity() {
return 1.25F;
}
@Override
protected float getGravityVelocity() {
return 0.0F;
}
@Override
protected void onImpact(MovingObjectPosition mop) {
if (mop.typeOfHit == MovingObjectType.BLOCK) {
BlockPos pos = mop.getBlockPos();
IBlockState state = worldObj.getBlockState(pos);
Block block = state.getBlock();
if (!isInGround() && ticksExisted < getMaxDistance()) {
WorldUtils.playSoundAtEntity(this, Sounds.WHIP_CRACK, 1.0F, 0.2F);
motionX = motionY = motionZ = 0.0D;
if (canGrabBlock(block, pos, mop.sideHit)) {
setInGround(true);
AxisAlignedBB box = block.getCollisionBoundingBox(worldObj, pos, state);
// bounding box may be null, depending on the block
if (box != null) {
posX = box.minX + ((box.maxX - box.minX) / 2.0D);
posY = box.minY + ((box.maxY - box.minY) / 2.0D);
posZ = box.minZ + ((box.maxZ - box.minZ) / 2.0D);
switch(mop.sideHit) {
case EAST: posX = box.maxX; break;
case WEST: posX = box.minX - 0.015D; break; // a little extra to compensate for block border, otherwise renders black
case SOUTH: posZ = box.maxZ; break;
case NORTH: posZ = box.minZ - 0.015D; break; // a little extra to compensate for block border, otherwise renders black
case UP: posY = box.maxY; break;
case DOWN: posY = box.minY - 0.015D; break;
}
} else {
// adjusting posX/Y/Z here seems to make no difference to the rendering, even when client side makes same changes
posX = (double) pos.getX() + 0.5D;
posY = (double) pos.getY() + 0.5D;
posZ = (double) pos.getZ() + 0.5D;
switch(mop.sideHit) {
//case 5: posX += 0.5D; break; // EAST
//case 4: posX -= 0.515D; break; // WEST (a little extra to compensate for block border, otherwise renders black)
//case 3: posZ += 0.5D; break; // SOUTH
//case 2: posZ -= 0.515D; break; // NORTH (a little extra to compensate for block border, otherwise renders black)
case UP: posY = pos.getY() + 1.0D; break;
case DOWN: posY = pos.getY() - 0.015D; break;
default:
}
}
// however, setting position as watched values and using these on the client works... weird
dataWatcher.updateObject(HIT_POS_X, (float) posX);
dataWatcher.updateObject(HIT_POS_Y, (float) posY);
dataWatcher.updateObject(HIT_POS_Z, (float) posZ);
// unfortunately, this means the datawatcher values are no longer usable for getting the block,
// so need to store hit position separately for updating
setHitBlockPosition(pos, mop.sideHit);
} else if (canBreakBlock(block, block.getMaterial(), pos, mop.sideHit)) {
if (!worldObj.isRemote) {
// don't drop items for players in creative mode
boolean drop = (getThrower() instanceof EntityPlayer ? !(((EntityPlayer) getThrower()).capabilities.isCreativeMode) : true);
worldObj.destroyBlock(pos, drop);
setDead();
}
} else if (block.getMaterial().blocksMovement()) {
// Only call onEntityCollidedWithBlock if the whip didn't already grab or break the block
block.onEntityCollidedWithBlock(worldObj, mop.getBlockPos(), this);
} else {
block.onEntityCollidedWithBlock(worldObj, mop.getBlockPos(), this);
return; // continue onward
}
}
} else if (mop.entityHit != null) {
worldObj.playSoundAtEntity(mop.entityHit, Sounds.WHIP_CRACK, 1.0F, 1.0F);
boolean inflictDamage = true; // set to false if held item disarmed
if (mop.entityHit instanceof EntityLivingBase) {
EntityLivingBase target = (EntityLivingBase) mop.entityHit;
if (getThrower() instanceof EntityPlayer) {
EntityPlayer player = (EntityPlayer) getThrower();
if (lootTarget(player, target)) {
inflictDamage = (target instanceof IEntityLootable ? ((IEntityLootable) target).isHurtOnTheft(player, getType()) : Config.getHurtOnSteal());
} else if (target.getHeldItem() != null && ZSSPlayerSkills.get(player).hasSkill(SkillBase.parry)) {
float chance = Parry.getDisarmModifier(player, target);
float yaw = (target.rotationYaw - player.rotationYaw);
while (yaw >= 360.0F) { yaw -= 360.0F; }
while (yaw < 0.0F) { yaw += 360.0F; }
yaw = Math.abs(Math.abs(yaw) - 180.0F);
//LogHelper.info("Target Yaw: " + target.rotationYaw + " | Player Yaw: " + player.rotationYaw + " | Difference: " + yaw);
// should be impossible to disarm from more than 90 degrees to either side
// however, rotationYaw does not seem to be the most reliable, but it's close enough
float mod = 0.5F - (0.25F * (yaw / 45.0F));
chance = 0.05F + (chance * mod); // max chance is 0.3F, min is 0.05F
//LogHelper.info("Modifier to disarm chance: " + mod + " | Total chance: " + chance);
if (worldObj.rand.nextFloat() < chance) {
WorldUtils.dropHeldItem(target);
inflictDamage = false;
}
}
}
if (inflictDamage && target.getEquipmentInSlot(ArmorIndex.EQUIPPED_CHEST) != null) {
inflictDamage = false; // cannot damage armor-wearing opponents
}
}
if (inflictDamage) {
mop.entityHit.attackEntityFrom(getDamageSource(), getDamage());
}
setDead();
}
}
private boolean lootTarget(EntityPlayer player, EntityLivingBase target) {
if (target.getEntityData().getBoolean("LootableEntityFlag")) {
return false;
}
IEntityLootable lootable = (target instanceof IEntityLootable ? (IEntityLootable) target : null);
float lootChance = (lootable != null ? lootable.getLootableChance(player, getType())
: LootableEntityRegistry.getEntityLootChance(target.getClass()));
lootChance *= Config.getWhipLootMultiplier();
boolean wasItemStolen = false;
if (rand.nextFloat() < lootChance) {
ItemStack loot = (lootable != null ? lootable.getEntityLoot(player, getType())
: LootableEntityRegistry.getEntityLoot(target.getClass()));
if (loot != null) {
EntityItem item = new EntityItem(worldObj, posX, posY + 1, posZ, loot);
double dx = player.posX - posX;
double dy = player.posY - posY;
double dz = player.posZ - posZ;
TargetUtils.setEntityHeading(item, dx, dy, dz, 1.0F, 1.0F, true);
if (!worldObj.isRemote) {
worldObj.spawnEntityInWorld(item);
}
player.triggerAchievement(ZSSAchievements.orcaThief);
wasItemStolen = true;
}
}
if (lootable == null || lootable.onLootStolen(player, wasItemStolen)) {
if (!worldObj.isRemote) {
target.getEntityData().setBoolean("LootableEntityFlag", true);
}
}
return wasItemStolen;
}
@Override
public void onUpdate() {
// Copied from Hookshot: avoiding entityThrowable's update while in ground
if (isInGround()) {
lastTickPosX = posX;
lastTickPosY = posY;
lastTickPosZ = posZ;
super.onEntityUpdate();
} else {
super.onUpdate();
}
if (canUpdate()) {
// can hold onto whip for five minutes
if (isInGround() && ticksExisted < 6000) {
++ticksInGround;
if (shouldSwing()) {
swingThrower();
}
} else if (ticksExisted > getMaxDistance()) {
WorldUtils.playSoundAtEntity(this, Sounds.WHIP_CRACK, 0.5F, 0.2F);
setDead();
}
} else {
setDead();
}
}
/**
* Returns true if player should swing (player far enough below whip, whip not attached to a lever, etc.)
*/
private boolean shouldSwing() {
BlockPos pos = getHitBlockPosition();
if (pos == null) {
return false;
}
IBlockState state = worldObj.getBlockState(pos);
Block block = state.getBlock();
if (block.getMaterial() == Material.air) {
setDead();
return false;
}
if (block instanceof IWhipBlock) {
Result result = ((IWhipBlock) block).shouldSwing(this, worldObj, pos, ticksInGround);
switch (result) {
case ALLOW: return true;
case DENY: return false;
default: // continue on to rest of processing
}
}
if (isDead) { // in case IWhipBlock killed the whip entity
return false;
} else if (block instanceof BlockLever) {
if (ticksInGround > 10 && !worldObj.isRemote) {
WorldUtils.activateButton(worldObj, state, pos);
setDead();
}
return false;
} else if (worldObj.isRemote && swingVec == null && getThrower() != null) {
// make sure thrower's y position is below impact position before starting swing
return (getThrower().getEntityBoundingBox().maxY) < dataWatcher.getWatchableObjectFloat(HIT_POS_Y);
}
return true;
}
/**
* Use method instead of 'this.dead = true' to clear the player's item in use.
*/
@Override
public void setDead() {
super.setDead();
if (getThrower() instanceof EntityPlayer) {
if (getThrower() instanceof EntityPlayerMP && !worldObj.isRemote) {
PacketDispatcher.sendTo(new UnpressKeyPacket(UnpressKeyPacket.RMB), (EntityPlayerMP) getThrower());
}
}
}
/**
* Returns true if the whip is allowed to update (i.e. thrower is holding correct item, etc.)
*/
protected boolean canUpdate() {
EntityLivingBase thrower = getThrower();
if (!isDead && thrower instanceof EntityPlayer && ((EntityPlayer) thrower).isUsingItem()) {
return thrower.getHeldItem() != null && thrower.getHeldItem().getItem() instanceof ItemWhip;
} else {
return false;
}
}
protected void swingThrower() {
EntityLivingBase thrower = getThrower();
if (thrower != null && !thrower.onGround && isInGround()) {
if (thrower.worldObj.isRemote) {
// Determine the swing variables on first swing tick:
float x = dataWatcher.getWatchableObjectFloat(HIT_POS_X);
float y = dataWatcher.getWatchableObjectFloat(HIT_POS_Y);
float z = dataWatcher.getWatchableObjectFloat(HIT_POS_Z);
switch (EnumFacing.getFront(dataWatcher.getWatchableObjectInt(SIDE_HIT))) {
case UP: break; // do nothing
case DOWN: y -= thrower.getEyeHeight(); break; // subtract thrower's eye height
default: y -= (thrower.getEyeHeight() / 1.5F); break; // subtract 1/3 of the thrower's eye height
}
if (swingTicks == 0 && swingVec == null && thrower.motionY < 0) {
swingVec = new Vec3((x - thrower.posX), y - thrower.posY, (z - thrower.posZ)).normalize();
dy = (thrower.getDistance(x, y, z) / 7.0D); // lower divisor gives bigger change in y
// calculate horizontal distance to find initial swing tick position
// as distance approaches zero, swing ticks should approach ticks required / 2
// as distance approaches maxDistance, swing ticks should approach zero
// this makes sure player's arc is even around pivot point
double d = Math.min(thrower.getDistance(x, thrower.posY, z), getMaxDistance());
swingTicks = MathHelper.floor_double(((getMaxDistance() - d) / getMaxDistance()) * 8);
}
if (swingVec != null) {
double sin = Math.sin(10.0D * swingTicks * Math.PI / 180.0D);
double f = 0.8D; // arbitrary horizontal motion factor
thrower.motionX = (sin * swingVec.xCoord * f);
thrower.motionZ = (sin * swingVec.zCoord * f);
// y motion needs to oscillate twice as quickly, so it goes up on the other side of the swing
thrower.motionY = dy * -Math.sin(20.0D * swingTicks * Math.PI / 180.0D);
// check for horizontal collisions that should stop swinging motion
MovingObjectPosition mop = TargetUtils.checkForImpact(worldObj, thrower, this, -(thrower.width / 4.0F), false);
if (mop != null && mop.typeOfHit != MovingObjectType.MISS) {
thrower.motionX = -thrower.motionX * 0.15D;
thrower.motionY = -thrower.motionY * 0.15D;
thrower.motionZ = -thrower.motionZ * 0.15D;
swingVec = null;
}
++swingTicks; // increment at end
if (thrower.fallDistance > 0 && thrower.motionY < 0) {
// 0.466885F seems to be roughly the amount added each tick while swinging; round for a little extra server-side padding
PacketDispatcher.sendToServer(new FallDistancePacket(thrower, -0.467F));
thrower.fallDistance -= 0.467F;
}
} else if (swingTicks > 0) {
// still let player hang there after colliding, but move towards center
if (thrower.getDistanceSq(x, thrower.posY, z) > 1.0D) {
double dx = x - thrower.posX;
double dz = z - thrower.posZ;
thrower.motionX = 0.15D * dx;
thrower.motionZ = 0.15D * dz;
}
if (thrower.posY < (y - (getMaxDistance() / 2.0D))) {
thrower.motionY = 0;
}
++swingTicks; // increment at end
PacketDispatcher.sendToServer(new FallDistancePacket(thrower, 0.0F));
thrower.fallDistance = 0.0F;
}
}
}
}
@Override
public void writeEntityToNBT(NBTTagCompound compound) {
super.writeEntityToNBT(compound);
compound.setFloat("hitPosX", dataWatcher.getWatchableObjectFloat(HIT_POS_X));
compound.setFloat("hitPosY", dataWatcher.getWatchableObjectFloat(HIT_POS_Y));
compound.setFloat("hitPosZ", dataWatcher.getWatchableObjectFloat(HIT_POS_Z));
if (getHitBlockPosition() != null) {
compound.setLong("hitPos", getHitBlockPosition().toLong());
compound.setInteger("sideHit", dataWatcher.getWatchableObjectInt(SIDE_HIT));
}
compound.setByte("customInGround", (byte)(isInGround() ? 1 : 0));
compound.setInteger("whipType", getType().ordinal());
}
@Override
public void readEntityFromNBT(NBTTagCompound compound) {
super.readEntityFromNBT(compound);
dataWatcher.updateObject(HIT_POS_X, compound.getFloat("hitPosX"));
dataWatcher.updateObject(HIT_POS_Y, compound.getFloat("hitPosY"));
dataWatcher.updateObject(HIT_POS_Z, compound.getFloat("hitPosZ"));
if (compound.hasKey("hitPos")) {
setHitBlockPosition(BlockPos.fromLong(compound.getLong("hitPos")), EnumFacing.getFront(compound.getInteger("sideHit")));
}
// retrieving owner name saved by super-class EntityThrowable
dataWatcher.updateObject(THROWER_INDEX, compound.getString("ownerName"));
dataWatcher.updateObject(WHIP_TYPE_INDEX, WhipType.values()[compound.getInteger("whipType") % WhipType.values().length]);
dataWatcher.updateObject(IN_GROUND_INDEX, compound.getByte("customInGround"));
}
}