Melee inflicts regional body damage

Discussion in 'Plugin Requests' started by BenSmart112, Aug 10, 2016.

Thread Status:
Not open for further replies.
  1. Offline

    BenSmart112

    My idea is that when using any kind of tool/weapon or enchanted non-weapon item it could inflict bonus damage and effects depending on the body part hit.

    For instance, stabbing someone in the head could do 10 additional damage, stabbing in the leg could slow them, stabbing in the chest could have normal effect.

    However, armor being worn that's either iron or diamond would negate the ability to hit that person in that body part (No effect but still damage). Like, if you had leggings and boots you wouldn't be slowed by being stabbed there, and wearing a helmet negates the additional damage to 1-4 damage points (half-2 hearts).
     
  2. Offline

    Lordloss

    Im guessing this is not doable. At least not properly.
     
  3. @Lordloss
    Well technically it is, if you calculated which part of the body was hit using the player's Direction, although it might be quite hard and math-intensive to get it accurate.
     
  4. Offline

    I Al Istannen

    @BenSmart112 @Lordloss @AlvinB
    I am no math genious, but I think this is good enough, isn't it? Don't know how heavy it is on the server, if you optimize it. And I have no one to test, so I haven't tried a moving target.
    body-part-detection.gif

    The numbers in brackets are the clicked height. They are fairly accurate.


    Crude sample code (not java syntax, as the forum seems to hate code blocks at the moment):
    Code:
        @EventHandler
        public void onInteract(PlayerInteractAtEntityEvent event) {
            if (!(event.getRightClicked() instanceof Villager) && !(event.getRightClicked() instanceof Player)) {
                event.getPlayer().sendMessage("Nope, no villager or player");
                return;
            } else {
                event.getPlayer().sendMessage("Clicked");
            }
    
            Location playerEyeLocation = event.getPlayer().getEyeLocation();
    
            SphericalCoords sphericalCoords = new SphericalCoords(0,
                    Math.toRadians(event.getPlayer().getLocation().getYaw() + 90)
                    , Math.toRadians(event.getPlayer().getLocation().getPitch() + 90));
    
            Location newLoc = playerEyeLocation.clone();
            Location otherLoc = event.getRightClicked().getLocation();
    
            AxisAlignedBB boundingBox = ((CraftEntity) event.getRightClicked()).getHandle().getBoundingBox();
    
            for (double i = 0; i < 10; i += 0.05) {
                sphericalCoords.setRho(i);
                newLoc = newLoc.add(sphericalCoords.toBukkitVector());
    
                Vec3D vec3D = new Vec3D(newLoc.getX(), newLoc.getY(), newLoc.getZ());
    
                if(boundingBox.a(vec3D)) {
                    double hitHeight = newLoc.getY() - otherLoc.getY();
                    String message;
                    if (hitHeight <= 0.3) {
                        message = "shoes";
                    }
                    else if (hitHeight <= 0.75) {
                        message = "legs";
                    } else if (hitHeight <= 1.4) {
                        message = "torso";
                    } else {
                        message = "head";
                    }
                    event.getPlayer().sendMessage("I am there now! "
                            + message
                            + " / "
                            + getCoveringArmorParts(hitHeight).stream().map(Enum::name).collect(Collectors.joining(", "))
                            + " ("
                            + DecimalFormat.getInstance().format(newLoc.getY() - otherLoc.getY())
                            + ")");
                    break;
    
                }
    
                newLoc = playerEyeLocation.clone();
            }
        }
    
        private List<ArmorParts> getCoveringArmorParts(double height) {
            List<ArmorParts> armorParts = new ArrayList<>();
            if(height <= 0.35) {
                armorParts.add(ArmorParts.SHOES);
            }
            if(height >= 0.19 && height <= 1) {
                armorParts.add(ArmorParts.LEGGINS);
            }
            if(height >= 0.75 && height <= 1.47) {
                armorParts.add(ArmorParts.CHESTPLATE);
            }
            if(height >= 1.4) {
                armorParts.add(ArmorParts.HELMET);
            }
            return armorParts;
        }
    
        private enum ArmorParts {
            HELMET,
            CHESTPLATE,
            LEGGINS,
            SHOES;
        }

    SphericalCoords is a class of mine, which just provides a semi-nice implementation of spherical coordinates. If you really want it, you can find it here. But it is probably not so well done, so be careful.

    EDIT:
    I have toyed around with it and made it a bit more accurate (shoes) and also recognize the arms... But there are a few new problems, which make it slightly less accurate. I think you might be able do some AABB magic with the player hitbox to make it really accurate (like stop the ray as soon as it touches the player hitbox). But I won't make this plugin and I am not sure if I want to toy around more with it.

    EDIT 2:
    Edited the code to use an AABB, which makes it a whole lot more precise. I think it should make it correct in every case.

    EDIT 3:
    What the code does:
    It shoots a ray in the location the attacking player is looking. As soon as the endpoint of that ray is INSIDE the AABB (bounding box) of the target player, it will stop the ray and get the distance to the ground. This distance is the point of the other player the attacker has hit. From the height you can experiement a bit and get nicer values for when which armor part protects.

    EDIT 4:
    For anybody who wants to do this: I made a Util class which tells you at which height the player was hit. I tested it for 1.8.8, hope it works for other versions too:
    Code:
    package me.ialistannen.bukkittoyaround;
    
    import org.bukkit.Bukkit;
    import org.bukkit.Location;
    import org.bukkit.entity.Entity;
    import org.bukkit.entity.Player;
    import org.bukkit.util.Vector;
    
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import java.util.Arrays;
    import java.util.Optional;
    import java.util.OptionalDouble;
    
    /**
    * Finds the height an entity was hit at / the player is looking at
    */
    public class HitHeightUtil {
    
        /**
         * Returns the height of the other entities bounding box a player looks at
         *
         * @param player      The player
         * @param entity      THe entity he looks at
         * @param maxDistance The maximum distance to scan
         * @param granularity The distance between each step. Smaller ==> More accurate, but more laggy
         *
         * @return The looking height, if any found
         */
        public static OptionalDouble getLookingHeight(Player player, Entity entity, int maxDistance, double granularity) {
    
            Object nmsEntity = ReflectionUtil.invokeMethod(entity, "getHandle", new Class[0]);
    
            Object boundingBox = ReflectionUtil.invokeMethod(nmsEntity, "getBoundingBox", new Class[0]);
    
            Class<?> vec3DClass = ReflectionUtil.getNMSClass("Vec3D");
    
            Optional<Constructor> vec3DConstructorOpt = ReflectionUtil.getConstructor(vec3DClass,
                    double.class, double.class, double.class);
    
            if (!vec3DConstructorOpt.isPresent()) {
                return OptionalDouble.empty();
            }
    
            Constructor<?> vec3DConstructor = vec3DConstructorOpt.get();
    
            Optional<Method> isFullyInsideMethodOpt = Arrays.stream(boundingBox.getClass().getMethods())
                    .filter(method -> method.getParameterTypes().length == 1
                            && method.getParameterTypes()[0].equals(vec3DClass))
                    .findAny();
    
            if (!isFullyInsideMethodOpt.isPresent()) {
                return OptionalDouble.empty();
            }
    
            Method isInsideMethod = isFullyInsideMethodOpt.get();
    
            Location playerEyeLocation = player.getEyeLocation();
    
            SphericalCoords sphericalCoords = new SphericalCoords(0,
                    Math.toRadians(player.getLocation().getYaw() + 90)
                    , Math.toRadians(player.getLocation().getPitch() + 90));
    
    
            for (double i = 0; i < maxDistance; i += granularity) {
                sphericalCoords.setRho(i);
                Location newLoc = playerEyeLocation.clone().add(sphericalCoords.toBukkitVector());
    
                Object vec3D;
                try {
                    vec3D = vec3DConstructor.newInstance(newLoc.getX(), newLoc.getY(), newLoc.getZ());
    
                } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
                    return OptionalDouble.empty();
                }
    
                if ((boolean) ReflectionUtil
                        .invokeMethod(boundingBox,
                                isInsideMethod.getName(),
                                isInsideMethod.getParameterTypes(),
                                vec3D)) {
                    double hitHeight = newLoc.getY() - entity.getLocation().getY();
                    return OptionalDouble.of(hitHeight);
                }
    
            }
    
            return OptionalDouble.empty();
        }
    
        /**
         * A small help with reflection
         */
        public static class ReflectionUtil {
    
            private static final String SERVER_VERSION;
    
            static {
                String name = Bukkit.getServer().getClass().getName();
                name = name.substring(name.indexOf("craftbukkit.") + "craftbukkit.".length());
                name = name.substring(0, name.indexOf("."));
    
                SERVER_VERSION = name;
            }
    
            /**
             * Returns the NMS class.
             *
             * @param name The name of the class
             *
             * @return The NMS class or null if an error occurred
             */
            public static Class<?> getNMSClass(String name) {
                try {
                    return Class.forName("net.minecraft.server." + SERVER_VERSION + "." + name);
                } catch (ClassNotFoundException e) {
                    return null;
                }
            }
    
            /**
             * Invokes the method
             *
             * @param handle           The handle to invoke it on
             * @param methodName       The name of the method
             * @param parameterClasses The parameter types
             * @param args             The arguments
             *
             * @return The resulting object or null if an error occurred / the method didn't return a thing
             */
            public static Object invokeMethod(Object handle, String methodName, Class[] parameterClasses, Object... args) {
                Class<?> clazz = handle.getClass();
    
                Optional<Method> methodOptional = getMethod(clazz, methodName, parameterClasses);
    
                if (!methodOptional.isPresent()) {
                    return null;
                }
    
                Method method = methodOptional.get();
    
                try {
                    return method.invoke(handle, args);
                } catch (IllegalAccessException | InvocationTargetException e) {
                    e.printStackTrace();
                }
                return null;
            }
    
    
            private static Optional<Method> getMethod(Class<?> clazz, String name, Class<?>... params) {
                try {
                    return Optional.of(clazz.getMethod(name, params));
                } catch (NoSuchMethodException ignored) {
                }
    
                try {
                    return Optional.of(clazz.getDeclaredMethod(name, params));
                } catch (NoSuchMethodException ignored) {
                }
    
                return Optional.empty();
            }
    
            /**
             * Returns the constructor
             *
             * @param clazz  The class
             * @param params The Constructor parameters
             *
             * @return The Constructor or an empty Optional if there is none with these parameters
             */
            public static Optional<Constructor> getConstructor(Class<?> clazz, Class<?>... params) {
                try {
                    return Optional.of(clazz.getConstructor(params));
                } catch (NoSuchMethodException e) {
                    return Optional.empty();
                }
            }
    
        }
    
    
        /**
         * Spherical coordinates
         */
        @SuppressWarnings({"SpellCheckingInspection", "unused", "WeakerAccess"})
        public static class SphericalCoords implements Cloneable {
    
            private double theta, phi, rho;
    
            /**
             * @param rho   The length of the vector
             * @param theta The angle on the x - y plane
             * @param phi   The angle between the z and the direct line
             */
            public SphericalCoords(double rho, double theta, double phi) {
                this.rho = rho;
                this.theta = theta;
                this.phi = phi;
            }
    
            /**
             * @return Rho
             */
            public double getRho() {
                return rho;
            }
    
            /**
             * @param rho The new rho
             */
            public void setRho(double rho) {
                this.rho = rho;
            }
    
            /**
             * @return Theta in radian
             */
            public double getTheta() {
                return theta;
            }
    
            /**
             * @return Theta in degrees
             */
            public double getThetaDegrees() {
                return Math.toDegrees(theta);
            }
    
            /**
             * @param theta Theta in radians
             */
            public void setTheta(double theta) {
                this.theta = theta;
            }
    
            /**
             * @return Phi in radian
             */
            public double getPhi() {
                return phi;
            }
    
            /**
             * @return Phi in degrees
             */
            public double getPhiDegree() {
                return Math.toDegrees(phi);
            }
    
            /**
             * @param phi Phi in radian
             */
            public void setPhi(double phi) {
                this.phi = phi;
            }
    
            /**
             * @param x The cartesian x
             * @param y The cartesian y
             * @param z The cartesian z
             *
             * @return The Spherical coords
             */
            public static SphericalCoords fromCartesian(double x, double y, double z) {
                double rho = Math.sqrt(x * x + y * y + z * z);
                double phi = Math.acos(z / rho);
                double theta = Math.atan2(y, x);
    
                return new SphericalCoords(rho, theta, phi);
            }
    
            /**
             * @param rho   The rho
             * @param theta The theta
             * @param phi   The phi
             *
             * @return The cartesian coords
             */
            public static double[] toCartesian(double rho, double theta, double phi) {
                double x = Math.cos(theta) * Math.sin(phi) * rho;
                double y = Math.sin(theta) * Math.sin(phi) * rho;
                double z = Math.cos(phi) * rho;
    
                return new double[]{x, y, z};
            }
    
            /**
             * @param coords The Spherical coordinates
             *
             * @return The cartesian coords
             */
            public static double[] toCartesian(SphericalCoords coords) {
                return toCartesian(coords.getRho(), coords.getTheta(), coords.getPhi());
            }
    
            /**
             * @param rho      rho
             * @param thetaDeg theta in degrees
             * @param phiDeg   phi in degrees
             *
             * @return The cartesian coords
             */
            public static double[] toCartesianDegree(double rho, double thetaDeg, double phiDeg) {
                double theta = Math.toRadians(thetaDeg);
                double phi = Math.toRadians(phiDeg);
                double x = Math.cos(theta) * Math.sin(phi) * rho;
                double y = Math.sin(theta) * Math.sin(phi) * rho;
                double z = Math.cos(phi) * rho;
    
                return new double[]{x, y, z};
            }
    
            /**
             * @return The vector. Does account for the x-y swap.
             */
            public Vector toBukkitVector() {
                double[] values = toCartesian(this);
                return new Vector(values[0], values[2], values[1]);
            }
    
            @Override
            public SphericalCoords clone() {
                return new SphericalCoords(getRho(), getTheta(), getPhi());
            }
    
    
            /* (non-Javadoc)
             * @see java.lang.Object#hashCode()
             */
            @Override
            public int hashCode() {
                final int prime = 31;
                int result = 1;
                long temp;
                temp = Double.doubleToLongBits(phi);
                result = prime * result + (int) (temp ^ (temp >>> 32));
                temp = Double.doubleToLongBits(rho);
                result = prime * result + (int) (temp ^ (temp >>> 32));
                temp = Double.doubleToLongBits(theta);
                result = prime * result + (int) (temp ^ (temp >>> 32));
                return result;
            }
    
            /* (non-Javadoc)
             * @see java.lang.Object#equals(java.lang.Object)
             */
            @Override
            public boolean equals(Object obj) {
                if (this == obj) {
                    return true;
                }
                if (obj == null) {
                    return false;
                }
                if (getClass() != obj.getClass()) {
                    return false;
                }
                SphericalCoords other = (SphericalCoords) obj;
                return Double.doubleToLongBits(phi) == Double.doubleToLongBits(other.phi) && Double.doubleToLongBits(rho)
                        == Double.doubleToLongBits(other.rho) && Double.doubleToLongBits(theta) == Double.doubleToLongBits
                        (other.theta);
            }
           
        }
    
    }
     
    Last edited: Aug 11, 2016
  5. Offline

    Lordloss

    @I Al Istannen
    Wow thats how i know you, you never hesitate to put huge effort in stuff, even if you dont really want to make the plugin. This is a crazy, math heavy approach which i never could handle to create.

    But i wouldnt even try because of the complications which crossed my mind: May i ask, wont it cause problems if the player is jumping mid-air, crouching, swimming, standing on half slabs or standing in non-solid blocks?
     
  6. Offline

    I Al Istannen

    @Lordloss
    Thanks ;)
    Just like a challenge sometimes, but have no motivation to do the more boring stuff a plugin needs :D

    It may look quite complicated, but the idea behind isn't. Implementing the spherical coordinates was a pain though, and that minecraft decides to handle yaw differently than what is the standard in maths (afaik) doesn't make it easier...

    But what it does in the end is sending a beam out of the players eyes, directly where his crosshair is pointing. This beam then travels on and on and on, until it reaches the max distance or is inside the target's boundingbox (just press F3 + B to see them ingame). You can see what this beam would look like, it is the blue line coming out the players head if you activate the bounding boxes.
    If it is inside the bounding box, you know at which part of the target the player is looking.

    I can't test much of what you asked, but I could test a few and the other should be true too. Don't quote me on that though :p

    • It uses the Entity#getLocation() as the base value for the y coordinate, so jumping shouldn't make a difference
    • I belive swimming doesn't change the hitbox, so I think it would work too.
    • I actually tested it while the other player or I stood on half slabs, and it worked. So that should be fine too.
    • Standing in non solid blocks should also not affect the hitbox. Tested it, works.
    • From the code crouching should change the hitbox size, but it seems it doesn't. Wouldn't matter anyways.
      It changes the visual size though, so you would maybe need to change the height ranges, which define the body parts.
    Summary:
    You may need a special handling for crouching, as the visual relations change (head lower, chest height smaller, ...)
     
  7. Offline

    Lordloss

    @I Al Istannen Okay i thought the other things i mentioned would matter because you said it calculates the distance to the floor.
     
  8. Offline

    I Al Istannen

    @Lordloss
    Oh, I see. Yea, that wasn't quite clear :/ Sorry :)

    Wonder if anybody will pick this up :p Would be interesting ;)

    Have a nice day!
     
Thread Status:
Not open for further replies.

Share This Page