package net.minestom.server.collision; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.*; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; /** * See https://wiki.vg/Entity_metadata#Mobs_2 */ public final class BoundingBox implements Shape { private final double width, height, depth; private final Point offset; private Map faces; BoundingBox(double width, double height, double depth, Point offset) { this.width = width; this.height = height; this.depth = depth; this.offset = offset; } public BoundingBox(double width, double height, double depth) { this(width, height, depth, new Vec(-width / 2, 0, -depth / 2)); } @Override @ApiStatus.Experimental public boolean intersectBox(@NotNull Point positionRelative, @NotNull BoundingBox boundingBox) { return (minX() + positionRelative.x() <= boundingBox.maxX() && maxX() + positionRelative.x() >= boundingBox.minX()) && (minY() + positionRelative.y() <= boundingBox.maxY() && maxY() + positionRelative.y() >= boundingBox.minY()) && (minZ() + positionRelative.z() <= boundingBox.maxZ() && maxZ() + positionRelative.z() >= boundingBox.minZ()); } @Override @ApiStatus.Experimental public boolean intersectBoxSwept(@NotNull Point rayStart, @NotNull Point rayDirection, @NotNull Point shapePos, @NotNull BoundingBox moving, @NotNull SweepResult finalResult) { final boolean isHit = RayUtils.BoundingBoxIntersectionCheck( moving, rayStart, rayDirection, this, shapePos ); if (!isHit) return false; SweepResult tempResult = new SweepResult(1, 0, 0, 0, null); // Longer check to get result of collision RayUtils.SweptAABB(moving, rayStart, rayDirection, this, shapePos, tempResult); // Update final result if the temp result collision is sooner than the current final result if (tempResult.res < finalResult.res) { finalResult.res = tempResult.res; finalResult.normalX = tempResult.normalX; finalResult.normalY = tempResult.normalY; finalResult.normalZ = tempResult.normalZ; finalResult.collidedShapePosition = shapePos; finalResult.collidedShape = this; finalResult.blockType = null; } return true; } /** * Used to know if this {@link BoundingBox} intersects with the bounding box of an entity. * * @param entity the entity to check the bounding box * @return true if this bounding box intersects with the entity, false otherwise */ @ApiStatus.Experimental public boolean intersectEntity(@NotNull Point src, @NotNull Entity entity) { return intersectBox(src.sub(entity.getPosition()), entity.getBoundingBox()); } @ApiStatus.Experimental public boolean boundingBoxRayIntersectionCheck(Vec start, Vec direction, Pos position) { return RayUtils.BoundingBoxRayIntersectionCheck(start, direction, this, position); } @Override public @NotNull Point relativeStart() { return offset; } @Override public @NotNull Point relativeEnd() { return offset.add(width, height, depth); } @Override public String toString() { String result = "BoundingBox"; result += "\n"; result += "[" + minX() + " : " + maxX() + "]"; result += "\n"; result += "[" + minY() + " : " + maxY() + "]"; result += "\n"; result += "[" + minZ() + " : " + maxZ() + "]"; return result; } /** * Creates a new {@link BoundingBox} linked to the same {@link Entity} with expanded size. * * @param x the X offset * @param y the Y offset * @param z the Z offset * @return a new {@link BoundingBox} expanded */ public @NotNull BoundingBox expand(double x, double y, double z) { return new BoundingBox(this.width + x, this.height + y, this.depth + z); } /** * Creates a new {@link BoundingBox} linked to the same {@link Entity} with contracted size. * * @param x the X offset * @param y the Y offset * @param z the Z offset * @return a new bounding box contracted */ public @NotNull BoundingBox contract(double x, double y, double z) { return new BoundingBox(this.width - x, this.height - y, this.depth - z); } public double width() { return width; } public double height() { return height; } public double depth() { return depth; } @NotNull Map faces() { Map faces = this.faces; if (faces == null) { this.faces = faces = retrieveFaces(); } return faces; } public double minX() { return relativeStart().x(); } public double maxX() { return relativeEnd().x(); } public double minY() { return relativeStart().y(); } public double maxY() { return relativeEnd().y(); } public double minZ() { return relativeStart().z(); } public double maxZ() { return relativeEnd().z(); } private Vec[] buildSet(Collection a) { return a.toArray(Vec[]::new); } private Vec[] buildSet(Collection a, Collection b) { Set allFaces = new HashSet<>(); Stream.of(a, b).forEach(allFaces::addAll); return allFaces.toArray(Vec[]::new); } private Vec[] buildSet(Collection a, Collection b, Collection c) { Set allFaces = new HashSet<>(); Stream.of(a, b, c).forEach(allFaces::addAll); return allFaces.toArray(Vec[]::new); } private Map retrieveFaces() { double minX = minX(); double maxX = maxX(); double minY = minY(); double maxY = maxY(); double minZ = minZ(); double maxZ = maxZ(); // Calculate steppings for each axis // Start at minimum, increase by step size until we reach maximum // This is done to catch all blocks that are part of that axis // Since this stops before max point is reached, we add the max point after final List stepsX = IntStream.rangeClosed(0, (int) ((maxX - minX))).mapToDouble(x -> x + minX).boxed().collect(Collectors.toCollection(ArrayList::new)); final List stepsY = IntStream.rangeClosed(0, (int) ((maxY - minY))).mapToDouble(x -> x + minY).boxed().collect(Collectors.toCollection(ArrayList::new)); final List stepsZ = IntStream.rangeClosed(0, (int) ((maxZ - minZ))).mapToDouble(x -> x + minZ).boxed().collect(Collectors.toCollection(ArrayList::new)); stepsX.add(maxX); stepsY.add(maxY); stepsZ.add(maxZ); final Set bottom = new HashSet<>(); final Set top = new HashSet<>(); final Set left = new HashSet<>(); final Set right = new HashSet<>(); final Set front = new HashSet<>(); final Set back = new HashSet<>(); CartesianProduct.product(stepsX, stepsY).forEach(cross -> { double i = (double) ((List) cross).get(0); double j = (double) ((List) cross).get(1); front.add(new Vec(i, j, minZ)); back.add(new Vec(i, j, maxZ)); }); CartesianProduct.product(stepsY, stepsZ).forEach(cross -> { double j = (double) ((List) cross).get(0); double k = (double) ((List) cross).get(1); left.add(new Vec(minX, j, k)); right.add(new Vec(maxX, j, k)); }); CartesianProduct.product(stepsX, stepsZ).forEach(cross -> { double i = (double) ((List) cross).get(0); double k = (double) ((List) cross).get(1); bottom.add(new Vec(i, minY, k)); top.add(new Vec(i, maxY, k)); }); // X -1 left | 1 right // Y -1 bottom | 1 top // Z -1 front | 1 back var query = new HashMap(); query.put(new Vec(0, 0, 0), new Vec[0]); query.put(new Vec(-1, 0, 0), buildSet(left)); query.put(new Vec(1, 0, 0), buildSet(right)); query.put(new Vec(0, -1, 0), buildSet(bottom)); query.put(new Vec(0, 1, 0), buildSet(top)); query.put(new Vec(0, 0, -1), buildSet(front)); query.put(new Vec(0, 0, 1), buildSet(back)); query.put(new Vec(0, -1, -1), buildSet(bottom, front)); query.put(new Vec(0, -1, 1), buildSet(bottom, back)); query.put(new Vec(0, 1, -1), buildSet(top, front)); query.put(new Vec(0, 1, 1), buildSet(top, back)); query.put(new Vec(-1, -1, 0), buildSet(left, bottom)); query.put(new Vec(-1, 1, 0), buildSet(left, top)); query.put(new Vec(1, -1, 0), buildSet(right, bottom)); query.put(new Vec(1, 1, 0), buildSet(right, top)); query.put(new Vec(-1, 0, -1), buildSet(left, front)); query.put(new Vec(-1, 0, 1), buildSet(left, back)); query.put(new Vec(1, 0, -1), buildSet(right, front)); query.put(new Vec(1, 0, 1), buildSet(right, back)); query.put(new Vec(1, 1, 1), buildSet(right, top, back)); query.put(new Vec(1, 1, -1), buildSet(right, top, front)); query.put(new Vec(1, -1, 1), buildSet(right, bottom, back)); query.put(new Vec(1, -1, -1), buildSet(right, bottom, front)); query.put(new Vec(-1, 1, 1), buildSet(left, top, back)); query.put(new Vec(-1, 1, -1), buildSet(left, top, front)); query.put(new Vec(-1, -1, 1), buildSet(left, bottom, back)); query.put(new Vec(-1, -1, -1), buildSet(left, bottom, front)); return query; } }