| package net.glowstone.entity.passive; |
| |
| import static org.bukkit.event.player.PlayerFishEvent.State.CAUGHT_FISH; |
| |
| import com.flowpowered.network.Message; |
| import com.google.common.base.Objects; |
| import java.util.Collections; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.concurrent.ThreadLocalRandom; |
| import net.glowstone.EventFactory; |
| import net.glowstone.GlowWorld; |
| import net.glowstone.constants.GlowBiomeClimate; |
| import net.glowstone.entity.EntityNetworkUtil; |
| import net.glowstone.entity.FishingRewardManager.RewardCategory; |
| import net.glowstone.entity.FishingRewardManager.RewardItem; |
| import net.glowstone.entity.GlowPlayer; |
| import net.glowstone.entity.meta.MetadataIndex; |
| import net.glowstone.entity.projectile.GlowProjectile; |
| import net.glowstone.net.GlowSession; |
| import net.glowstone.net.message.play.entity.DestroyEntitiesMessage; |
| import net.glowstone.net.message.play.entity.SpawnObjectMessage; |
| import net.glowstone.util.InventoryUtil; |
| import net.glowstone.util.Position; |
| import org.bukkit.Location; |
| import org.bukkit.Material; |
| import org.bukkit.World; |
| import org.bukkit.block.Block; |
| import org.bukkit.enchantments.Enchantment; |
| import org.bukkit.entity.Entity; |
| import org.bukkit.entity.EntityType; |
| import org.bukkit.entity.FishHook; |
| import org.bukkit.entity.LivingEntity; |
| import org.bukkit.entity.Player; |
| import org.bukkit.event.player.PlayerFishEvent; |
| import org.bukkit.inventory.ItemStack; |
| import org.bukkit.projectiles.ProjectileSource; |
| import org.bukkit.util.Vector; |
| |
| public class GlowFishingHook extends GlowProjectile implements FishHook { |
| public static final Message[] EMPTY_MESSAGE_ARRAY = new Message[0]; |
| |
| @Override |
| public void setShooter(ProjectileSource shooter) { |
| ProjectileSource oldShooter = getShooter(); |
| if (oldShooter == shooter) { |
| return; |
| } |
| // Shooter is immutable client-side (a situation peculiar to fishing hooks), so if it |
| // changes then all clients who can see this fishing hook must be told that this hook has |
| // despawned and a new one has spawned. |
| super.setShooter(shooter); |
| World world = location.getWorld(); |
| if (world instanceof GlowWorld) { |
| List<Message> respawnMessages = new LinkedList<>(); |
| DestroyEntitiesMessage destroyOldCopy = new DestroyEntitiesMessage( |
| Collections.singletonList(getObjectId())); |
| respawnMessages.add(destroyOldCopy); |
| respawnMessages.addAll(createSpawnMessage(getShooterId())); |
| ((GlowWorld) world).getRawPlayers() |
| .stream() |
| .filter(player -> !Objects.equal(player, shooter)) |
| .filter(player -> player.canSeeEntity(this)) |
| .forEach(player -> player.getSession().sendAll( |
| respawnMessages.toArray(EMPTY_MESSAGE_ARRAY))); |
| // Each player believes her own entity ID is 0, so the update is sent separately to the |
| // shooter |
| if (shooter instanceof GlowPlayer) { |
| GlowSession session = ((GlowPlayer) shooter).getSession(); |
| session.send(destroyOldCopy); |
| session.sendAll( |
| createSpawnMessage(GlowPlayer.SELF_ID).toArray(EMPTY_MESSAGE_ARRAY)); |
| } |
| } |
| } |
| |
| /** |
| * The minimum time, in seconds, to make the player wait for a bite when using an unenchanted |
| * fishing pole. |
| */ |
| private static final int MINIMUM_BASE_WAIT = 5; |
| /** |
| * The maximum time, in seconds, to make the player wait for a bite. |
| */ |
| private static final int MAXIMUM_WAIT = 45; |
| /** |
| * Waiting time saved per level of lure (down to a minimum of zero). |
| */ |
| private static final int SECONDS_SAVED_PER_LURE_LEVEL = 5; |
| /** |
| * Waiting time in ticks after a bite, before it is considered missed if the player hasn't |
| * clicked. |
| */ |
| private static final int CLICK_TIMEOUT_TICKS = 10; |
| private int lived; |
| private int lifeTime; |
| private final ItemStack itemStack; |
| |
| /** |
| * Creates a fishing bob. |
| * |
| * @param location the location |
| * @param itemStack the fishing rod (used to handle enchantments) or null (equivalent to |
| * @param angler the player who is casting this fish hook (must be set at spawn time) |
| */ |
| public GlowFishingHook(Location location, ItemStack itemStack, Player angler) { |
| super(location); |
| setSize(0.25f, 0.25f); |
| lifeTime = calculateLifeTime(); |
| |
| this.itemStack = InventoryUtil.itemOrEmpty(itemStack).clone(); |
| |
| // TODO: velocity does not match vanilla |
| Vector direction = location.getDirection(); |
| setVelocity(direction.multiply(1.5)); |
| super.setShooter(angler); |
| } |
| |
| private int calculateLifeTime() { |
| // Waiting time is 5-45 seconds |
| int lifeTime = ThreadLocalRandom.current().nextInt(MINIMUM_BASE_WAIT, MAXIMUM_WAIT + 1); |
| |
| int level = getEnchantmentLevel(Enchantment.LURE); |
| lifeTime -= level * SECONDS_SAVED_PER_LURE_LEVEL; |
| lifeTime = Math.max(lifeTime, 0); |
| lifeTime *= 20; |
| return lifeTime; |
| } |
| |
| @Override |
| public List<Message> createSpawnMessage() { |
| return createSpawnMessage(getShooterId()); |
| } |
| |
| /** |
| * Creates the spawn messages given the shooter ID on the receiving end (which is different for |
| * the shooter than for everyone else). |
| * |
| * @param shooterId the shooter's ID, according to the receiving client |
| * @return the spawn messages |
| */ |
| private List<Message> createSpawnMessage(int shooterId) { |
| List<Message> spawnMessage = super.createSpawnMessage(); |
| |
| double x = location.getX(); |
| double y = location.getY(); |
| double z = location.getZ(); |
| int intPitch = Position.getIntPitch(location); |
| int intHeadYaw = Position.getIntHeadYaw(location.getYaw()); |
| |
| spawnMessage.set(0, new SpawnObjectMessage(getEntityId(), getUniqueId(), |
| EntityNetworkUtil.getObjectId(EntityType.FISHING_HOOK), |
| x, y, z, intPitch, intHeadYaw, shooterId, velocity)); |
| return spawnMessage; |
| } |
| |
| private int getShooterId() { |
| return getShooter() instanceof Entity ? ((Entity) getShooter()).getEntityId() |
| : ENTITY_ID_NOBODY; |
| } |
| |
| @Override |
| public void collide(Block block) { |
| // TODO |
| } |
| |
| @Override |
| public void collide(LivingEntity entity) { |
| // No effect. |
| } |
| |
| @Override |
| protected int getObjectId() { |
| return EntityNetworkUtil.getObjectId(EntityType.FISHING_HOOK); |
| } |
| |
| @Override |
| public boolean shouldSave() { |
| return false; |
| } |
| |
| @Deprecated |
| @Override |
| public double getBiteChance() { |
| // Not supported in newer mc versions anymore |
| return 0; |
| } |
| |
| @Deprecated |
| @Override |
| public void setBiteChance(double v) throws IllegalArgumentException { |
| // Not supported in newer mc versions anymore |
| } |
| |
| private Entity getHookedEntity() { |
| return world.getEntityManager().getEntity( |
| metadata.getInt(MetadataIndex.FISHING_HOOK_HOOKED_ENTITY) - 1); |
| } |
| |
| private void setHookedEntity(Entity entity) { |
| metadata.set(MetadataIndex.FISHING_HOOK_HOOKED_ENTITY, |
| entity == null ? 0 : entity.getEntityId() + 1); |
| } |
| |
| @Override |
| public void pulse() { |
| super.pulse(); |
| // TODO: Particles |
| // TODO: Bopper movement |
| if (location.getBlock().getType() == Material.WATER) { |
| increaseTimeLived(); |
| } |
| } |
| |
| private void increaseTimeLived() { |
| // "The window for reeling in when a fish bites is about half a second. |
| // If a bite is missed, the line can be left in the water to wait for another bite." |
| // TODO: Option to give high-latency players more time? Not much abuse potential! |
| if (lived - lifeTime > CLICK_TIMEOUT_TICKS) { |
| lifeTime = calculateLifeTime(); |
| lived = 0; |
| } |
| |
| // "If the bobber is not directly exposed to sun or moonlight, the wait time will be |
| // approximately doubled." |
| Block highestBlockAt = world.getHighestBlockAt(location); |
| if (location.getY() < highestBlockAt.getLocation().getY()) { |
| if (ThreadLocalRandom.current().nextDouble(100) < 50) { |
| return; |
| } |
| } |
| |
| if (GlowBiomeClimate.isRainy(location.getBlock()) && lived < lifeTime) { |
| if (ThreadLocalRandom.current().nextDouble(100) < 20) { |
| lived++; |
| } |
| } |
| |
| lived++; |
| } |
| |
| /** |
| * Removes this fishing hook. Drops loot and xp if a player is fishing. |
| */ |
| public void reelIn() { |
| if (location.getBlock().getType() == Material.WATER) { |
| ProjectileSource shooter = getShooter(); |
| if (shooter instanceof Player) { |
| PlayerFishEvent fishEvent |
| = new PlayerFishEvent((Player) shooter, this, null, CAUGHT_FISH); |
| fishEvent.setExpToDrop(ThreadLocalRandom.current().nextInt(1, 7)); |
| fishEvent = EventFactory.getInstance().callEvent(fishEvent); |
| if (!fishEvent.isCancelled()) { |
| // TODO: Item should "fly" towards player |
| world.dropItemNaturally(((Player) getShooter()).getLocation(), getRewardItem()); |
| ((Player) getShooter()).giveExp(fishEvent.getExpToDrop()); |
| } |
| } |
| } |
| remove(); |
| } |
| |
| private ItemStack getRewardItem() { |
| RewardCategory rewardCategory = getRewardCategory(); |
| int level = getEnchantmentLevel(Enchantment.LUCK); |
| |
| if (rewardCategory == null || world.getServer().getFishingRewardManager() |
| .getCategoryItems(rewardCategory).isEmpty()) { |
| return InventoryUtil.createEmptyStack(); |
| } |
| double rewardCategoryChance = rewardCategory.getChance() |
| + rewardCategory.getModifier() * level; |
| double random; |
| // This loop is needed because rounding errors make the probabilities add up to less than |
| // 100%. It will rarely iterate more than once. |
| do { |
| random = ThreadLocalRandom.current().nextDouble(100); |
| |
| for (RewardItem rewardItem |
| : world.getServer().getFishingRewardManager() |
| .getCategoryItems(rewardCategory)) { |
| random -= rewardItem.getChance() * rewardCategoryChance / 100.0; |
| if (random < 0) { |
| ItemStack reward = rewardItem.getItem().clone(); |
| int enchantLevel = rewardItem.getMinEnchantmentLevel(); |
| int maxEnchantLevel = rewardItem.getMaxEnchantmentLevel(); |
| if (maxEnchantLevel > enchantLevel) { |
| enchantLevel = ThreadLocalRandom.current().nextInt( |
| enchantLevel, maxEnchantLevel + 1); |
| } |
| if (enchantLevel > 0) { |
| enchant(reward, enchantLevel); |
| } |
| return reward; |
| } |
| } |
| } while (random >= 0); |
| |
| return InventoryUtil.createEmptyStack(); |
| } |
| |
| /** |
| * Adds a random set of enchantments, which may include treasure enchantments, to an item. |
| * |
| * @param reward the item to enchant |
| * @param enchantLevel the level of enchantment to use |
| */ |
| private static void enchant(ItemStack reward, int enchantLevel) { |
| // TODO |
| } |
| |
| private int getEnchantmentLevel(Enchantment enchantment) { |
| return !InventoryUtil.isEmpty(itemStack) && itemStack.getType() == Material.FISHING_ROD |
| ? itemStack.getEnchantmentLevel(enchantment) |
| : 0; |
| } |
| |
| private RewardCategory getRewardCategory() { |
| int level = getEnchantmentLevel(Enchantment.LUCK); |
| double random = ThreadLocalRandom.current().nextDouble(100); |
| |
| for (RewardCategory rewardCategory : RewardCategory.values()) { |
| random -= rewardCategory.getChance() + rewardCategory.getModifier() * level; |
| if (random <= 0) { |
| return rewardCategory; |
| } |
| } |
| |
| return null; |
| } |
| } |