package tc.oc.pgm.blockdrops;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.ExperienceOrb;
import org.bukkit.entity.FallingBlock;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.entity.EntityChangeBlockEvent;
import org.bukkit.event.entity.EntityDespawnInVoidEvent;
import org.bukkit.event.entity.EntityExplodeEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.material.MaterialData;
import org.bukkit.util.RayBlockIntersection;
import org.bukkit.util.Vector;
import tc.oc.commons.bukkit.util.BlockUtils;
import tc.oc.commons.bukkit.util.NMSHacks;
import tc.oc.commons.core.util.Pair;
import tc.oc.pgm.kits.KitPlayerFacet;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.match.MatchScope;
import tc.oc.commons.bukkit.event.BlockPunchEvent;
import tc.oc.commons.bukkit.event.BlockTrampleEvent;
import tc.oc.pgm.events.BlockTransformEvent;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.ParticipantBlockTransformEvent;
import tc.oc.pgm.match.MatchModule;
import tc.oc.commons.bukkit.util.Materials;
import javax.annotation.Nullable;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
@ListenerScope(MatchScope.RUNNING)
public class BlockDropsMatchModule extends MatchModule implements Listener {
private static final double BASE_FALL_SPEED = 3d;
private final BlockDropsRuleSet ruleSet;
// Tracks FallingBlocks created by explosions that have been randomly chosen
// to not form a block when they land. We need to track them from the time
// they are created because we don't want this to affect FallingBlocks created
// in the normal vanilla way.
//
// This WILL leak a few entities now and then, because there are ways they can
// die that do not fire an event e.g. the tick age limit, but this should be
// rare and they will only leak until the end of the match.
private final Set<FallingBlock> fallingBlocksThatWillNotLand = new HashSet<>();
public BlockDropsMatchModule(Match match, BlockDropsRuleSet ruleSet) {
super(match);
this.ruleSet = ruleSet;
}
public BlockDropsRuleSet getRuleSet() {
return ruleSet;
}
public static boolean causesDrops(final Event event) {
return event instanceof BlockBreakEvent || event instanceof EntityExplodeEvent;
}
@EventHandler(priority = EventPriority.LOW)
public void initializeDrops(BlockTransformEvent event) {
if(!causesDrops(event.getCause())) {
return;
}
BlockDrops drops = this.ruleSet.getDrops(event, event.getOldState(), ParticipantBlockTransformEvent.getPlayerState(event));
if(drops != null) {
event.setDrops(drops);
}
}
private void dropItems(BlockDrops drops, @Nullable MatchPlayer player, Location location, double yield) {
if(player == null || player.getBukkit().getGameMode() != GameMode.CREATIVE) {
Random random = getMatch().getRandom();
for (Pair<Double, ItemStack> entry : drops.items) {
if (random.nextFloat() < yield * entry.first) {
location.getWorld().dropItemNaturally(BlockUtils.center(location), entry.second);
}
}
}
}
private void dropExperience(BlockDrops drops, Location location) {
if(drops.experience != 0) {
ExperienceOrb expOrb = (ExperienceOrb) location.getWorld().spawnEntity(BlockUtils.center(location), EntityType.EXPERIENCE_ORB);
if(expOrb != null) {
expOrb.setExperience(drops.experience);
}
}
}
private void giveKit(BlockDrops drops, MatchPlayer player) {
if(player != null && player.isParticipating() && player.isSpawned() && drops.kit != null) {
player.facet(KitPlayerFacet.class).applyKit(drops.kit, false);
}
}
private void dropObjects(BlockDrops drops, @Nullable MatchPlayer player, Location location, double yield, boolean explosion) {
giveKit(drops, player);
if(explosion) {
match.getScheduler(MatchScope.RUNNING).createTask(() -> dropItems(drops, player, location, yield));
} else {
dropItems(drops, player, location, yield);
dropExperience(drops, location);
}
}
private void replaceBlock(BlockDrops drops, Block block, MatchPlayer player) {
if(drops.replacement != null) {
EntityChangeBlockEvent event = new EntityChangeBlockEvent(player.getBukkit(), block, drops.replacement);
getMatch().callEvent(event);
if(!event.isCancelled()) {
BlockState state = block.getState();
state.setType(drops.replacement.getItemType());
state.setData(drops.replacement);
state.update(true, true);
}
}
}
/**
* This is not an event handler. It is called explicitly by BlockTransformListener
* after all event handlers have been called.
*/
@SuppressWarnings("deprecation")
public void doBlockDrops(final BlockTransformEvent event) {
if(!causesDrops(event.getCause())) {
return;
}
final BlockDrops drops = event.getDrops();
if(drops != null) {
event.setCancelled(true);
final BlockState oldState = event.getOldState();
final BlockState newState = event.getNewState();
final Block block = event.getOldState().getBlock();
final int newTypeId = newState.getTypeId();
final byte newData = newState.getRawData();
block.setTypeIdAndData(newTypeId, newData, true);
boolean explosion = false;
MatchPlayer player = ParticipantBlockTransformEvent.getParticipant(event);
if(event.getCause() instanceof EntityExplodeEvent) {
EntityExplodeEvent explodeEvent = (EntityExplodeEvent) event.getCause();
explosion = true;
if(drops.fallChance != null &&
oldState.getType().isBlock() &&
oldState.getType() != Material.AIR &&
this.getMatch().getRandom().nextFloat() < drops.fallChance) {
FallingBlock fallingBlock = event.getOldState().spawnFallingBlock();
fallingBlock.setDropItem(false);
if(drops.landChance != null && this.getMatch().getRandom().nextFloat() >= drops.landChance) {
this.fallingBlocksThatWillNotLand.add(fallingBlock);
}
Vector v = fallingBlock.getLocation().subtract(explodeEvent.getLocation()).toVector();
double distance = v.length();
v.normalize().multiply(BASE_FALL_SPEED * drops.fallSpeed / Math.max(1d, distance));
// A very simple deflection model. Check for a solid
// neighbor block and "bounce" the velocity off of it.
Block west = block.getRelative(BlockFace.WEST);
Block east = block.getRelative(BlockFace.EAST);
Block down = block.getRelative(BlockFace.DOWN);
Block up = block.getRelative(BlockFace.UP);
Block north = block.getRelative(BlockFace.NORTH);
Block south = block.getRelative(BlockFace.SOUTH);
if((v.getX() < 0 && west != null && Materials.isColliding(west.getType())) ||
v.getX() > 0 && east != null && Materials.isColliding(east.getType())) {
v.setX(-v.getX());
}
if((v.getY() < 0 && down != null && Materials.isColliding(down.getType())) ||
v.getY() > 0 && up != null && Materials.isColliding(up.getType())) {
v.setY(-v.getY());
}
if((v.getZ() < 0 && north != null && Materials.isColliding(north.getType())) ||
v.getZ() > 0 && south != null && Materials.isColliding(south.getType())) {
v.setZ(-v.getZ());
}
fallingBlock.setVelocity(v);
}
}
dropObjects(drops, player, newState.getLocation(), 1d, explosion);
}
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onFallingBlockLand(BlockTransformEvent event) {
if(event.getCause() instanceof EntityChangeBlockEvent) {
Entity entity = ((EntityChangeBlockEvent) event.getCause()).getEntity();
if(entity instanceof FallingBlock && this.fallingBlocksThatWillNotLand.remove(entity)) {
event.setCancelled(true);
}
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onBlockFallInVoid(EntityDespawnInVoidEvent event) {
if(event.getEntity() instanceof FallingBlock) {
this.fallingBlocksThatWillNotLand.remove(event.getEntity());
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onBlockPunch(BlockPunchEvent event) {
final MatchPlayer player = getMatch().getPlayer(event.getPlayer());
if(player == null) return;
RayBlockIntersection hit = event.getIntersection();
BlockDrops drops = getRuleSet().getDrops(event, hit.getBlock().getState(), player.getParticipantState());
if(drops == null) return;
MaterialData oldMaterial = hit.getBlock().getState().getData();
replaceBlock(drops, hit.getBlock(), player);
// Play a fake punching effect if the block is punchable. Use raw particles instead of
// playBlockBreakEffect so the position is precise rather than in the block center.
Object packet = NMSHacks.blockCrackParticlesPacket(oldMaterial, false, hit.getPosition(), new Vector(), 0, 5);
for(MatchPlayer viewer : getMatch().getPlayers()) {
if(viewer.getBukkit().getEyeLocation().toVector().distanceSquared(hit.getPosition()) < 16 * 16) {
NMSHacks.sendPacket(viewer.getBukkit(), packet);
}
}
NMSHacks.playBlockPlaceSound(hit.getBlock().getWorld(), hit.getPosition(), oldMaterial.getItemType(), 1);
dropObjects(drops, player, hit.getPosition().toLocation(hit.getBlock().getWorld()), 1d, false);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onBlockTrample(BlockTrampleEvent event) {
final MatchPlayer player = getMatch().getPlayer(event.getPlayer());
if(player == null) return;
BlockDrops drops = getRuleSet().getDrops(event, event.getBlock().getState(), player.getParticipantState());
if(drops == null) return;
replaceBlock(drops, event.getBlock(), player);
dropObjects(drops, player, player.getBukkit().getLocation(), 1d, false);
}
}