blob: 001cdf36106637ec643975e48a767307f3d544df [file] [log] [blame] [raw]
package net.glowstone;
import net.glowstone.block.GlowBlock;
import net.glowstone.block.blocktype.BlockTNT;
import net.glowstone.entity.GlowEntity;
import net.glowstone.entity.GlowLivingEntity;
import net.glowstone.entity.GlowPlayer;
import net.glowstone.net.message.play.game.ExplosionMessage;
import net.glowstone.net.message.play.game.ExplosionMessage.Record;
import org.bukkit.*;
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.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;
import java.util.*;
import java.util.stream.Collectors;
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;
private static final Random random = new Random();
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 contain 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(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();
}
public boolean explodeWithEvent() {
if (power < 0.1f)
return true;
Set<BlockVector> droppedBlocks = calculateBlocks();
EntityExplodeEvent event = EventFactory.callEvent(new EntityExplodeEvent(source, location, toBlockList(droppedBlocks), yield));
if (event.isCancelled()) return false;
yield = event.getYield();
playOutSoundAndParticles();
List<Block> blocks = toBlockList(droppedBlocks);
for (Block block : blocks) {
handleBlockExplosion((GlowBlock) block);
}
if (incendiary) {
for (Block block : blocks) {
setBlockOnFire((GlowBlock) block);
}
}
Collection<GlowPlayer> affectedPlayers = damageEntities();
for (GlowPlayer player : affectedPlayers) {
playOutExplosion(player, droppedBlocks);
}
return true;
}
///////////////////////////////////////////////////
// Calculate all the dropping blocks
private Set<BlockVector> calculateBlocks() {
if (!breakBlocks)
return new HashSet<>();
Set<BlockVector> blocks = new HashSet<>();
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;
}
calculateRay(x, y, z, blocks);
}
}
}
return blocks;
}
private void calculateRay(int ox, int oy, int oz, Collection<BlockVector> result) {
double x = ox / 7.5 - 1;
double y = oy / 7.5 - 1;
double z = oz / 7.5 - 1;
Vector direction = new Vector(x, y, z);
direction.normalize();
direction.multiply(0.3f); // 0.3 blocks away with each step
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 = random.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> locs) {
List<Block> blocks = new ArrayList<>(locs.size());
blocks.addAll(locs.stream().map(location -> world.getBlockAt(location.getBlockX(), location.getBlockY(), location.getBlockZ())).collect(Collectors.toList()));
return blocks;
}
private void setBlockOnFire(GlowBlock block) {
if (random.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.callEvent(new BlockIgniteEvent(block, IgniteCause.EXPLOSION, source));
if (event.isCancelled()) {
return;
}
block.setType(Material.FIRE);
}
/////////////////////////////////////////
// Damage entities
private Collection<GlowPlayer> damageEntities() {
float power = this.power;
this.power *= 2f;
Collection<GlowPlayer> affectedPlayers = new ArrayList<>();
Collection<GlowLivingEntity> entities = getNearbyEntities();
for (GlowLivingEntity entity : entities) {
if (entity instanceof GlowPlayer) {
affectedPlayers.add((GlowPlayer) entity);
continue;
}
double disDivPower = distanceTo(entity) / this.power;
if (disDivPower > 1.0D) continue;
Vector vecDistance = distanceToHead(entity);
if (vecDistance.length() == 0.0) continue;
vecDistance.normalize();
double basicDamage = calculateDamage(entity, disDivPower);
double explosionDamage = calculateEnchantedDamage((int) ((basicDamage * basicDamage + basicDamage) * 4 * power + 1.0D), entity);
DamageCause damageCause;
if (source == null || source.getType() == EntityType.PRIMED_TNT) {
damageCause = DamageCause.BLOCK_EXPLOSION;
} else {
damageCause = DamageCause.ENTITY_EXPLOSION;
}
entity.damage(explosionDamage, source, damageCause);
vecDistance.multiply(explosionDamage).multiply(0.25);
Vector currentVelocity = entity.getVelocity();
currentVelocity.add(vecDistance);
entity.setVelocity(currentVelocity);
}
return affectedPlayers;
}
private double calculateEnchantedDamage(double explosionDamage, GlowLivingEntity entity) {
int level = 0;
if (entity.getEquipment() != null) {
for (ItemStack stack : entity.getEquipment().getArmorContents()) {
if (stack != null) {
level += stack.getEnchantmentLevel(Enchantment.PROTECTION_EXPLOSIONS);
}
}
}
if (level > 0) {
float sub = level * 0.15f;
double damage = explosionDamage * sub;
damage = Math.floor(damage);
return explosionDamage - damage;
}
return explosionDamage;
}
private double calculateDamage(GlowEntity entity, double disDivPower) {
double damage = world.rayTrace(location, entity);
return damage * (1D - disDivPower);
}
private Collection<GlowLivingEntity> getNearbyEntities() {
ArrayList<Chunk> chunks = new ArrayList<>();
chunks.add(location.getChunk());
/* TODO: select near chunks based on explosion's distance to chunk edge
int closestChunkEdgeX = ((location.getBlockX() - 1) | 15) + 1;
int closestChunkEdgeZ = ((location.getBlockZ() - 1) | 15) + 1;
int distanceFromClosestChunkEdgeX = Math.abs(location.getBlockX() - closestChunkEdgeX);
int distanceFromClosestChunkEdgeZ = Math.abs(location.getBlockX() - closestChunkEdgeZ);
if (distanceFromClosestChunkEdgeX <= power || distanceFromClosestChunkEdgeZ <= power) {
*/
int chunkX = location.getChunk().getX();
int chunkZ = location.getChunk().getZ();
chunks.add(location.getWorld().getChunkAt(chunkX + 1, chunkZ + 1));
chunks.add(location.getWorld().getChunkAt(chunkX - 1, chunkZ - 1));
chunks.add(location.getWorld().getChunkAt(chunkX - 1, chunkZ + 1));
chunks.add(location.getWorld().getChunkAt(chunkX + 1, chunkZ - 1));
chunks.add(location.getWorld().getChunkAt(chunkX + 1, chunkZ));
chunks.add(location.getWorld().getChunkAt(chunkX, chunkZ + 1));
chunks.add(location.getWorld().getChunkAt(chunkX - 1, chunkZ));
chunks.add(location.getWorld().getChunkAt(chunkX, chunkZ - 1));
// }
ArrayList<Entity> entities = new ArrayList<>();
for (Chunk chunk : chunks) {
entities.addAll(Arrays.asList(chunk.getEntities()));
}
return entities.stream().filter(entity -> entity instanceof LivingEntity && distanceTo((LivingEntity) entity) / power < 1).map(entity -> (GlowLivingEntity) entity).collect(Collectors.toList());
}
private double distanceTo(LivingEntity entity) {
return location.clone().subtract(entity.getLocation()).length();
}
private Vector distanceToHead(LivingEntity entity) {
return entity.getLocation().clone().subtract(location.clone().subtract(0, entity.getEyeHeight(), 0)).toVector();
}
///////////////////////////////////////
// Visualize
private void playOutSoundAndParticles() {
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<BlockVector> blocks) {
Collection<Record> records = new ArrayList<>();
Location clientLoc = location.clone();
clientLoc.setX((int) clientLoc.getX());
clientLoc.setY((int) clientLoc.getY());
clientLoc.setZ((int) clientLoc.getZ());
for (BlockVector block : blocks) {
byte x = (byte) (block.getBlockX() - clientLoc.getBlockX());
byte y = (byte) (block.getBlockY() - clientLoc.getBlockY());
byte z = (byte) (block.getBlockZ() - clientLoc.getBlockZ());
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);
}
}