blob: 7776cec2cb44dd99419c5b3e89f2e5c8f8c0ce1c [file] [log] [blame] [raw]
package net.glowstone;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import net.glowstone.block.GlowBlock;
import net.glowstone.block.blocktype.BlockTnt;
import net.glowstone.entity.GlowEntity;
import net.glowstone.entity.GlowPlayer;
import net.glowstone.net.message.play.game.ExplosionMessage;
import net.glowstone.net.message.play.game.ExplosionMessage.Record;
import net.glowstone.util.RayUtil;
import org.bukkit.Effect;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
import org.bukkit.event.block.BlockExplodeEvent;
import org.bukkit.event.block.BlockIgniteEvent;
import org.bukkit.event.block.BlockIgniteEvent.IgniteCause;
import org.bukkit.event.entity.EntityDamageEvent.DamageCause;
import org.bukkit.event.entity.EntityExplodeEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.BlockVector;
import org.bukkit.util.Vector;
public final class Explosion {
public static final int POWER_TNT = 4;
public static final int POWER_BED = 5;
public static final int POWER_CREEPER = 3;
public static final int POWER_CHARGED_CREEPER = 6;
public static final int POWER_GHAST = 1;
public static final int POWER_WITHER_SKULL = 1;
public static final int POWER_WITHER_CREATION = 7;
public static final int POWER_ENDER_CRYSTAL = 6;
public static final int EXPLOSION_VISIBILITY_RADIUS = 64;
private static final List<Vector> RAY_DIRECTIONS = new ArrayList<>();
static {
int value = 16;
for (int x = 0; x < value; x++) {
for (int y = 0; y < value; y++) {
for (int z = 0; z < value; z++) {
if (x != 0 && x != value - 1
&& y != 0 && y != value - 1
&& z != 0 && z != value - 1) {
continue;
}
double vx = x / 7.5 - 1;
double vy = y / 7.5 - 1;
double vz = z / 7.5 - 1;
Vector direction = new Vector(vx, vy, vz);
direction.normalize();
direction.multiply(0.3f); // 0.3 blocks away with each step
RAY_DIRECTIONS.add(direction);
}
}
}
}
@Nullable
private final Entity source;
private final Location location;
private final boolean incendiary;
private final boolean breakBlocks;
private final GlowWorld world;
private float power;
private float yield = 0.3f;
/**
* Creates a new explosion.
*
* @param source The entity causing this explosion
* @param world The world this explosion is in
* @param x The X location of the explosion
* @param y The Y location of the explosion
* @param z The Z location of the explosion
* @param power The power of the explosion
* @param incendiary Whether or not blocks should be set on fire
* @param breakBlocks Whether blocks should break through this explosion
*/
public Explosion(Entity source, GlowWorld world, double x, double y, double z, float power,
boolean incendiary, boolean breakBlocks) {
this(source, new Location(world, x, y, z), power, incendiary, breakBlocks);
}
/**
* Creates a new explosion.
*
* @param source The entity causing this explosion
* @param location The location this explosion is occuring at. Must be in a GlowWorld.
* @param power The power of the explosion
* @param incendiary Whether or not blocks should be set on fire
* @param breakBlocks Whether blocks should break through this explosion
*/
public Explosion(@Nullable Entity source, Location location, float power,
boolean incendiary, boolean breakBlocks) {
if (!(location.getWorld() instanceof GlowWorld)) {
throw new IllegalArgumentException("Supplied location does not have a valid GlowWorld");
}
this.source = source;
this.location = location.clone();
this.power = power;
this.incendiary = incendiary;
this.breakBlocks = breakBlocks;
world = (GlowWorld) location.getWorld();
}
/**
* Explodes, firing a {@link EntityExplodeEvent}, as long as its power is non-negligible.
* @return false if the explosion was canceled by a plugin; true otherwise
*/
public boolean explodeWithEvent() {
if (power < 0.1f) {
return true;
}
Set<BlockVector> droppedBlocks = calculateBlocks();
// The 'blocks' list should mutable for event calls.
List<Block> blocks = toBlockList(droppedBlocks);
if (source != null) {
EntityExplodeEvent event = EventFactory.getInstance().callEvent(
new EntityExplodeEvent(source, location, blocks, yield));
if (event.isCancelled()) {
return false;
}
yield = event.getYield();
} else {
BlockExplodeEvent event = EventFactory.getInstance().callEvent(
new BlockExplodeEvent(location.getBlock(), blocks, yield));
if (event.isCancelled()) {
return false;
}
yield = event.getYield();
}
playOutSoundAndParticles();
for (Block block : blocks) {
handleBlockExplosion((GlowBlock) block);
}
if (incendiary) {
for (Block block : blocks) {
setBlockOnFire((GlowBlock) block);
}
}
damageEntities();
Collection<GlowPlayer> affectedPlayers
= collectPlayersInRadius(EXPLOSION_VISIBILITY_RADIUS);
for (GlowPlayer player : affectedPlayers) {
playOutExplosion(player, blocks);
}
return true;
}
///////////////////////////////////////////////////
// Calculate all the dropping blocks
private Set<BlockVector> calculateBlocks() {
if (!breakBlocks) {
return new HashSet<>();
}
Set<BlockVector> blocks = new HashSet<>();
RAY_DIRECTIONS.forEach(v -> calculateRay(v, blocks));
return blocks;
}
private void calculateRay(Vector direction, Collection<BlockVector> result) {
Location current = location.clone();
float currentPower = calculateStartPower();
while (currentPower > 0) {
GlowBlock block = world.getBlockAt(current);
if (block.getType() != Material.AIR) {
double blastDurability = getBlastDurability(block) / 5d;
blastDurability += 0.3F;
blastDurability *= 0.3F;
currentPower -= blastDurability;
if (currentPower > 0) {
result.add(new BlockVector(block.getX(), block.getY(), block.getZ()));
}
}
current.add(direction);
currentPower -= 0.225f;
}
}
private void handleBlockExplosion(GlowBlock block) {
if (block.getType() == Material.AIR
|| block.getType() == Material.BARRIER
|| block.getType() == Material.BEDROCK) {
return;
} else if (block.getType() == Material.TNT) {
BlockTnt.igniteBlock(block, true);
return;
}
block.breakNaturally(yield);
}
private float calculateStartPower() {
float rand = ThreadLocalRandom.current().nextFloat();
rand *= 0.6F; // (max - 0.7)
rand += 0.7; // min
return rand * power;
}
private double getBlastDurability(GlowBlock block) {
return block.getMaterialValues().getBlastResistance();
}
private List<Block> toBlockList(Collection<BlockVector> locations) {
List<Block> blocks = new ArrayList<>(locations.size());
blocks.addAll(locations.stream()
.map(location -> world.getBlockAt(
location.getBlockX(), location.getBlockY(), location.getBlockZ()))
.collect(Collectors.toList()));
return blocks;
}
private void setBlockOnFire(GlowBlock block) {
if (ThreadLocalRandom.current().nextInt(3) != 0) {
return;
}
Block below = block.getRelative(BlockFace.DOWN);
Material belowType = below.getType();
if (belowType == Material.AIR || belowType == Material.FIRE || !belowType.isFlammable()) {
return;
}
BlockIgniteEvent event = EventFactory.getInstance().callEvent(
new BlockIgniteEvent(block, IgniteCause.EXPLOSION, source));
if (event.isCancelled()) {
return;
}
block.setType(Material.FIRE);
}
/////////////////////////////////////////
// Damage entities
private void damageEntities() {
float power = this.power;
this.power *= 2;
Collection<Entity> entities = getNearbyEntities();
for (Entity entity : entities) {
if (!(entity instanceof GlowEntity)) {
continue;
}
// refine area to sphere, instead of box
if (distanceToSquared(entity) > power * power) {
continue;
}
double exposure = world.rayTrace(location, (GlowEntity) entity);
double impact = (1 - (distanceTo(entity) / power / 2)) * exposure;
double damage = (impact * impact + impact) * 8 * power + 1;
int epf = getProtectionFactor(entity);
double reduction = calculateEnchantedReduction(epf);
damage = damage * reduction;
exposure -= exposure * epf * 0.15;
DamageCause damageCause;
if (source == null || source.getType() == EntityType.PRIMED_TNT) {
damageCause = DamageCause.BLOCK_EXPLOSION;
} else {
damageCause = DamageCause.ENTITY_EXPLOSION;
}
((GlowEntity) entity).damage(damage, source, damageCause);
if (entity instanceof GlowPlayer && ((GlowPlayer) entity).isFlying()) {
continue;
}
if (entity instanceof LivingEntity) {
LivingEntity livingEntity = (LivingEntity) entity;
Vector rayLength = RayUtil.getVelocityRay(distanceToHead(livingEntity));
rayLength.multiply(exposure);
Vector currentVelocity = entity.getVelocity();
currentVelocity.add(rayLength);
entity.setVelocity(currentVelocity);
}
}
}
private double calculateEnchantedReduction(int epf) {
// TODO: move this to damage main (in entity)
double reduction = 1;
if (epf > 0) {
reduction = (1 - epf / 25);
}
return reduction;
}
private int getProtectionFactor(Entity entity) {
if (!(entity instanceof LivingEntity)) {
return 0;
}
LivingEntity livingEntity = (LivingEntity) entity;
int level = 0;
if (livingEntity.getEquipment() != null) {
for (ItemStack stack : livingEntity.getEquipment().getArmorContents()) {
if (stack != null) {
int stackLevel = stack.getEnchantmentLevel(Enchantment.PROTECTION_EXPLOSIONS);
if (stackLevel > level) {
level = stackLevel;
}
}
}
}
return level << 1;
}
private Collection<Entity> getNearbyEntities() {
return location.getWorld().getNearbyEntities(location, power, power, power);
}
private double distanceTo(Entity entity) {
return RayUtil.getRayBetween(location, entity.getLocation()).length();
}
private double distanceToSquared(Entity entity) {
return RayUtil.getRayBetween(location, entity.getLocation()).lengthSquared();
}
private Vector distanceToHead(LivingEntity entity) {
return RayUtil.getRayBetween(entity.getEyeLocation(), location);
}
///////////////////////////////////////
// Visualize
private void playOutSoundAndParticles() {
ThreadLocalRandom random = ThreadLocalRandom.current();
world.playSound(location, Sound.ENTITY_GENERIC_EXPLODE, 4,
(1.0F + (random.nextFloat() - random.nextFloat()) * 0.2F) * 0.7F);
if (power >= 2.0F && breakBlocks) {
// send huge explosion
world.spigot().playEffect(location, Effect.EXPLOSION_HUGE);
} else {
// send large explosion
world.spigot().playEffect(location, Effect.EXPLOSION_LARGE);
}
}
private void playOutExplosion(GlowPlayer player, Iterable<Block> blocks) {
Collection<Record> records = new ArrayList<>();
for (Block block : blocks) {
Location blockLocation = block.getLocation();
byte x = (byte) (blockLocation.getBlockX() - (int) this.location.getX());
byte y = (byte) (blockLocation.getBlockY() - (int) this.location.getY());
byte z = (byte) (blockLocation.getBlockZ() - (int) this.location.getZ());
records.add(new Record(x, y, z));
}
Vector velocity = player.getVelocity();
ExplosionMessage message = new ExplosionMessage(
(float) location.getX(), (float) location.getY(), (float) location.getZ(), power,
(float) velocity.getX(), (float) velocity.getY(), (float) velocity.getZ(), records);
player.getSession().send(message);
}
private Collection<GlowPlayer> collectPlayersInRadius(int radius) {
int radiusSquared = radius * radius;
return world.getRawPlayers().stream()
.filter(player -> player.getLocation().distanceSquared(location) <= radiusSquared)
.collect(Collectors.toList());
}
}