package tc.oc.pgm.points; import java.util.List; import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkNotNull; import org.bukkit.Location; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.entity.Entity; import org.bukkit.util.Vector; import tc.oc.commons.bukkit.util.Materials; import tc.oc.commons.bukkit.util.WorldBorderUtils; import tc.oc.commons.core.inspect.Inspectable; import tc.oc.commons.core.stream.Collectors; import tc.oc.commons.core.util.Lazy; import tc.oc.commons.core.random.RandomUtils; import tc.oc.pgm.match.Match; import tc.oc.pgm.regions.Region; import tc.oc.pgm.regions.Union; public class RegionPointProvider extends Inspectable.Impl implements PointProvider { private final @Inspect Region region; private final @Inspect PointProviderAttributes attributes; /** * For convenience, union point-providers are treated as multi-region point providers, * i.e. a single sub-region is chosen at random. We can't expand unions until after * parsing though, so we have to do it lazily here instead of in the parser. */ private final Lazy<List<Region>> expandedRegions = Lazy.from( () -> Union.expand(getRegion()).collect(Collectors.toImmutableList()) ); public RegionPointProvider(Region region, PointProviderAttributes attributes) { this.attributes = attributes; this.region = checkNotNull(region, "region");; } @Override public Region getRegion() { return region; } @Override public int getWeight() { return expandedRegions.get().size(); } @Override public boolean canFail() { return attributes.isSafe(); } @Override public Location getPoint(Match match, @Nullable Entity entity) { final Region region = RandomUtils.element(match.getRandom(), expandedRegions.get()); final Vector pos = region.getRandom(match.getRandom()); PointProviderLocation location = new PointProviderLocation(match.getWorld(), pos); if(attributes.getYawProvider() != null) { location.setYaw(attributes.getYawProvider().getAngle(pos)); } if(attributes.getPitchProvider() != null) { location.setPitch(attributes.getPitchProvider().getAngle(pos)); } location = makeSafe(location); return location; } private PointProviderLocation makeSafe(PointProviderLocation location) { if(location == null) return null; // If the initial point is safe, just return it if(isSpawnable(location)) return location; // Try centering the point in its block location = location.clone(); location.setX(location.getBlockX() + 0.5); location.setY(location.getBlockY()); location.setZ(location.getBlockZ() + 0.5); if(isSpawnable(location)) return location; int scanDirection; if(attributes.isOutdoors()) { location.setY(Math.max(location.getY(), location.getWorld().getHighestBlockYAt(location))); scanDirection = 1; } else { scanDirection = -1; } // Scan downward, then upward, for a safe point in the region. If spawn is outdoors, just scan upward. for(; scanDirection <= 1; scanDirection += 2) { for(PointProviderLocation safe = location.clone(); safe.getBlockY() >= 0 && safe.getBlockY() < 256 && region.contains(safe); safe.setY(safe.getBlockY() + scanDirection)) { if(isSpawnable(safe)) return safe; } } // Give up return null; } private boolean isSpawnable(Location location) { if(attributes.isSafe() && !isSafe(location)) return false; if(attributes.isOutdoors() && !isOutdoors(location)) return false; return true; } /** * Indicates whether or not this spawn is safe. * * @param location Location to check for. * @return True or false depending on whether this is a safe spawn point. */ private boolean isSafe(Location location) { if(!WorldBorderUtils.isInsideBorder(location)) return false; Block block = location.getBlock(); Block above = block.getRelative(BlockFace.UP); Block below = block.getRelative(BlockFace.DOWN); return block.isEmpty() && above.isEmpty() && Materials.isColliding(below.getType()); } private boolean isOutdoors(Location location) { return location.getWorld().getHighestBlockYAt(location) <= location.getBlockY(); } }