package net.glowstone; 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.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> 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 (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 *= 2; Collection<GlowPlayer> affectedPlayers = new ArrayList<>(); LivingEntity[] entities = getNearbyEntities(); for (LivingEntity entity : entities) { // 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; } entity.damage(damage, source, damageCause); if (entity instanceof GlowPlayer) { affectedPlayers.add((GlowPlayer) entity); if (((GlowPlayer) entity).isFlying()) { continue; } } Vector rayLength = RayUtil.getVelocityRay(distanceToHead(entity)); rayLength.multiply(exposure); Vector currentVelocity = entity.getVelocity(); currentVelocity.add(rayLength); entity.setVelocity(currentVelocity); } return affectedPlayers; } 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(LivingEntity entity) { int level = 0; if (entity.getEquipment() != null) { for (ItemStack stack : entity.getEquipment().getArmorContents()) { if (stack != null) { int stackLevel = stack.getEnchantmentLevel(Enchantment.PROTECTION_EXPLOSIONS); if (stackLevel > level) level = stackLevel; } } } return level * 2; } private LivingEntity[] getNearbyEntities() { Collection<Entity> entities = location.getWorld().getNearbyEntities(location, power, power, power); return entities.stream().filter(entity -> entity instanceof LivingEntity).toArray(LivingEntity[]::new); } private double distanceTo(LivingEntity entity) { return RayUtil.getRayBetween(location, entity.getLocation()).length(); } private double distanceToSquared(LivingEntity entity) { return RayUtil.getRayBetween(location, entity.getLocation()).lengthSquared(); } private Vector distanceToHead(LivingEntity entity) { return RayUtil.getRayBetween(entity.getEyeLocation(), location); } /////////////////////////////////////// // 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<>(); for (BlockVector block : blocks) { byte x = (byte) (block.getBlockX() - location.getBlockX()); byte y = (byte) (block.getBlockY() - location.getBlockY()); byte z = (byte) (block.getBlockZ() - location.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); } }