Browse Source

Roll rework / refactor Fixes #5023

nossr50 10 months ago
parent
commit
5cc97383fa

+ 10 - 0
Changelog.txt

@@ -1,3 +1,13 @@
+Version 2.2.020
+    (Codebase) Reworked Roll implementation (See notes)
+    (Codebase) Added unit test coverage for Roll
+    Fixed a bug where Roll was modifying damage unnecessarily
+
+    NOTES:
+    The code for Roll was a bit of a mess, I've rewritten a good chunk of it and added some unit test coverage.
+    I will likely put out another update for Acrobatics in general, as the code for Acrobatics is whack.
+    This would be a good time to suggest changes to Acrobatics on discord.
+
 Version 2.2.019
 Version 2.2.019
     Optimized Alchemy code (thanks MrPowerGamerBR)
     Optimized Alchemy code (thanks MrPowerGamerBR)
     Fixed an exception that could occur when shooting entities through worlds (thanks Wariorrrr)
     Fixed an exception that could occur when shooting entities through worlds (thanks Wariorrrr)

+ 1 - 1
pom.xml

@@ -2,7 +2,7 @@
     <modelVersion>4.0.0</modelVersion>
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.gmail.nossr50.mcMMO</groupId>
     <groupId>com.gmail.nossr50.mcMMO</groupId>
     <artifactId>mcMMO</artifactId>
     <artifactId>mcMMO</artifactId>
-    <version>2.2.019</version>
+    <version>2.2.020-SNAPSHOT</version>
     <name>mcMMO</name>
     <name>mcMMO</name>
     <url>https://github.com/mcMMO-Dev/mcMMO</url>
     <url>https://github.com/mcMMO-Dev/mcMMO</url>
     <scm>
     <scm>

+ 79 - 51
src/main/java/com/gmail/nossr50/datatypes/skills/subskills/acrobatics/Roll.java

@@ -11,13 +11,9 @@ import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.util.EventUtils;
 import com.gmail.nossr50.util.EventUtils;
 import com.gmail.nossr50.util.ItemUtils;
 import com.gmail.nossr50.util.ItemUtils;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
-import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.random.Probability;
 import com.gmail.nossr50.util.random.Probability;
 import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.random.ProbabilityUtil;
-import com.gmail.nossr50.util.skills.PerksUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillUtils;
-import com.gmail.nossr50.util.sounds.SoundManager;
 import com.gmail.nossr50.util.sounds.SoundType;
 import com.gmail.nossr50.util.sounds.SoundType;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.TextComponent;
 import net.kyori.adventure.text.TextComponent;
@@ -34,9 +30,17 @@ import org.jetbrains.annotations.VisibleForTesting;
 
 
 import java.util.Locale;
 import java.util.Locale;
 
 
+import static com.gmail.nossr50.util.player.NotificationManager.sendPlayerInformation;
+import static com.gmail.nossr50.util.random.ProbabilityUtil.getSubSkillProbability;
+import static com.gmail.nossr50.util.skills.SkillUtils.applyXpGain;
+import static com.gmail.nossr50.util.sounds.SoundManager.sendCategorizedSound;
+
 public class Roll extends AcrobaticsSubSkill {
 public class Roll extends AcrobaticsSubSkill {
 
 
 
 
+    public static final String GRACEFUL_ROLL_ACTIVATED_LOCALE_STR_KEY = "Acrobatics.Ability.Proc";
+    public static final String ROLL_ACTIVATED_LOCALE_KEY = "Acrobatics.Roll.Text";
+
     public Roll() {
     public Roll() {
         super("Roll", EventPriority.HIGHEST, SubSkillType.ACROBATICS_ROLL);
         super("Roll", EventPriority.HIGHEST, SubSkillType.ACROBATICS_ROLL);
     }
     }
@@ -49,23 +53,14 @@ public class Roll extends AcrobaticsSubSkill {
      */
      */
     @Override
     @Override
     public boolean doInteraction(Event event, mcMMO plugin) {
     public boolean doInteraction(Event event, mcMMO plugin) {
-        //TODO: Go through and API this up
-
-        /*
-         * Roll is a SubSkill which allows players to negate fall damage from certain heights with sufficient Acrobatics skill and luck
-         * Roll is activated when a player takes damage from a fall
-         * If a player holds shift, they double their odds at a successful roll and upon success are told they did a graceful roll.
-         */
-
-        //Casting
-        EntityDamageEvent entityDamageEvent = (EntityDamageEvent) event;
+        final EntityDamageEvent entityDamageEvent = (EntityDamageEvent) event;
 
 
         //Make sure a real player was damaged in this event
         //Make sure a real player was damaged in this event
         if (!EventUtils.isRealPlayerDamaged(entityDamageEvent))
         if (!EventUtils.isRealPlayerDamaged(entityDamageEvent))
             return false;
             return false;
 
 
-        if (entityDamageEvent.getCause() == EntityDamageEvent.DamageCause.FALL) {//Grab the player
-            McMMOPlayer mmoPlayer = EventUtils.getMcMMOPlayer(entityDamageEvent.getEntity());
+        if (entityDamageEvent.getCause() == EntityDamageEvent.DamageCause.FALL) {
+            final McMMOPlayer mmoPlayer = EventUtils.getMcMMOPlayer(entityDamageEvent.getEntity());
 
 
             if (mmoPlayer == null)
             if (mmoPlayer == null)
                 return false;
                 return false;
@@ -75,16 +70,38 @@ public class Roll extends AcrobaticsSubSkill {
              */
              */
             
             
             if (canRoll(mmoPlayer)) {
             if (canRoll(mmoPlayer)) {
-                entityDamageEvent.setDamage(
-                        rollCheck(mmoPlayer, entityDamageEvent.getFinalDamage(), mmoPlayer.getPlayer().isSneaking()));
+                final RollResult rollResult
+                        = rollCheck(mmoPlayer, entityDamageEvent);
+                if (rollResult == null) {
+                    // no-op - fall was fatal or otherwise did not get processed
+                    return false;
+                }
+                entityDamageEvent.setDamage(rollResult.getModifiedDamage());
 
 
                 if (entityDamageEvent.getFinalDamage() == 0) {
                 if (entityDamageEvent.getFinalDamage() == 0) {
                     entityDamageEvent.setCancelled(true);
                     entityDamageEvent.setCancelled(true);
-                    return true;
                 }
                 }
+
+                // Roll happened, send messages and XP
+                if (rollResult.isRollSuccess()) {
+                    final String key
+                            = rollResult.isGraceful() ? GRACEFUL_ROLL_ACTIVATED_LOCALE_STR_KEY : ROLL_ACTIVATED_LOCALE_KEY;
+                    sendPlayerInformation(mmoPlayer.getPlayer(), NotificationType.SUBSKILL_MESSAGE, key);
+                    sendCategorizedSound(mmoPlayer.getPlayer(), mmoPlayer.getPlayer().getLocation(),
+                            SoundType.ROLL_ACTIVATED, SoundCategory.PLAYERS,0.5F);
+                }
+
+                if (!rollResult.isExploiting() && rollResult.getXpGain() > 0) {
+                    applyXpGain(mmoPlayer, getPrimarySkill(), rollResult.getXpGain(), XPGainReason.PVE);
+                }
+
+                // Player didn't die, so add the location to the list
+                addFallLocation(mmoPlayer);
+                return true;
+            // We give Acrobatics XP for fall damage even if they haven't unlocked roll
             } else if (mcMMO.p.getSkillTools().doesPlayerHaveSkillPermission(mmoPlayer.getPlayer(), PrimarySkillType.ACROBATICS)) {
             } else if (mcMMO.p.getSkillTools().doesPlayerHaveSkillPermission(mmoPlayer.getPlayer(), PrimarySkillType.ACROBATICS)) {
-                //Give XP Anyways
-                SkillUtils.applyXpGain(mmoPlayer, getPrimarySkill(), calculateRollXP(mmoPlayer, ((EntityDamageEvent) event).getFinalDamage(), false), XPGainReason.PVE);
+                //Give XP
+                applyXpGain(mmoPlayer, getPrimarySkill(), calculateRollXP(mmoPlayer, ((EntityDamageEvent) event).getFinalDamage(), false), XPGainReason.PVE);
             }
             }
         }
         }
 
 
@@ -167,7 +184,7 @@ public class Roll extends AcrobaticsSubSkill {
 
 
     @NotNull
     @NotNull
     private Probability getRollProbability(McMMOPlayer mmoPlayer) {
     private Probability getRollProbability(McMMOPlayer mmoPlayer) {
-        return ProbabilityUtil.getSubSkillProbability(SubSkillType.ACROBATICS_ROLL, mmoPlayer);
+        return getSubSkillProbability(SubSkillType.ACROBATICS_ROLL, mmoPlayer);
     }
     }
 
 
     @Override
     @Override
@@ -185,54 +202,67 @@ public class Roll extends AcrobaticsSubSkill {
         return true;
         return true;
     }
     }
 
 
-    private boolean canRoll(McMMOPlayer mmoPlayer) {
+    @VisibleForTesting
+    public boolean canRoll(McMMOPlayer mmoPlayer) {
         return RankUtils.hasUnlockedSubskill(mmoPlayer.getPlayer(), SubSkillType.ACROBATICS_ROLL)
         return RankUtils.hasUnlockedSubskill(mmoPlayer.getPlayer(), SubSkillType.ACROBATICS_ROLL)
                 && Permissions.isSubSkillEnabled(mmoPlayer.getPlayer(), SubSkillType.ACROBATICS_ROLL);
                 && Permissions.isSubSkillEnabled(mmoPlayer.getPlayer(), SubSkillType.ACROBATICS_ROLL);
     }
     }
 
 
-    private int getActivationChance(McMMOPlayer mmoPlayer) {
-        return PerksUtils.handleLuckyPerks(mmoPlayer, getPrimarySkill());
-    }
-
     /**
     /**
      * Handle the damage reduction and XP gain from the Roll / Graceful Roll ability
      * Handle the damage reduction and XP gain from the Roll / Graceful Roll ability
      *
      *
-     * @param damage The amount of damage initially dealt by the event
+     * @param entityDamageEvent the event to modify in the event of roll success
      * @return the modified event damage if the ability was successful, the original event damage otherwise
      * @return the modified event damage if the ability was successful, the original event damage otherwise
      */
      */
-    private double rollCheck(McMMOPlayer mmoPlayer, double damage, boolean isGracefulRoll) {
+    @VisibleForTesting
+    public RollResult rollCheck(McMMOPlayer mmoPlayer, EntityDamageEvent entityDamageEvent) {
+        double baseDamage = entityDamageEvent.getDamage();
+        final boolean isGraceful = mmoPlayer.getPlayer().isSneaking();
+        final RollResult.Builder rollResultBuilder
+                = new RollResult.Builder(entityDamageEvent, isGraceful);
         final Probability probability
         final Probability probability
-                = isGracefulRoll ? getGracefulProbability(mmoPlayer) : getNonGracefulProbability(mmoPlayer);
-        double modifiedDamage = calculateModifiedRollDamage(damage,
+                = isGraceful ? getGracefulProbability(mmoPlayer) : getNonGracefulProbability(mmoPlayer);
+
+        double modifiedDamage = calculateModifiedRollDamage(baseDamage,
                 mcMMO.p.getAdvancedConfig().getRollDamageThreshold() * 2);
                 mcMMO.p.getAdvancedConfig().getRollDamageThreshold() * 2);
+        rollResultBuilder.modifiedDamage(modifiedDamage);
 
 
+        boolean isExploiting = isPlayerExploitingAcrobatics(mmoPlayer);
+        rollResultBuilder.exploiting(isExploiting);
+        // They Rolled
         if (!isFatal(mmoPlayer, modifiedDamage)
         if (!isFatal(mmoPlayer, modifiedDamage)
                 && ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.ACROBATICS, mmoPlayer, probability)) {
                 && ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.ACROBATICS, mmoPlayer, probability)) {
-            NotificationManager.sendPlayerInformation(mmoPlayer.getPlayer(), NotificationType.SUBSKILL_MESSAGE, "Acrobatics.Ability.Proc");
-            SoundManager.sendCategorizedSound(mmoPlayer.getPlayer(), mmoPlayer.getPlayer().getLocation(), SoundType.ROLL_ACTIVATED, SoundCategory.PLAYERS,0.5F);
-            if (!isExploiting(mmoPlayer) && mmoPlayer.getAcrobaticsManager().canGainRollXP())
-                SkillUtils.applyXpGain(mmoPlayer, getPrimarySkill(), calculateRollXP(mmoPlayer, damage, true), XPGainReason.PVE);
-
-            addFallLocation(mmoPlayer);
-            return modifiedDamage;
-        } else if (!isFatal(mmoPlayer, damage)) {
-            if (!isExploiting(mmoPlayer) && mmoPlayer.getAcrobaticsManager().canGainRollXP())
-                SkillUtils.applyXpGain(mmoPlayer, getPrimarySkill(), calculateRollXP(mmoPlayer, damage, false), XPGainReason.PVE);
-            
-            addFallLocation(mmoPlayer);
-        }
+            rollResultBuilder.rollSuccess(true);
+            rollResultBuilder.exploiting(isExploiting);
+            final boolean canGainXp = mmoPlayer.getAcrobaticsManager().canGainRollXP();
 
 
-        return damage;
+            if (!isExploiting && canGainXp) {
+                rollResultBuilder.xpGain((int) calculateRollXP(mmoPlayer, baseDamage, true));
+            }
+
+            return rollResultBuilder.build();
+        // They did not roll, but they also did not die so reward XP as appropriate
+        } else if (!isFatal(mmoPlayer, baseDamage)) {
+            rollResultBuilder.rollSuccess(false);
+            final boolean canGainXp = mmoPlayer.getAcrobaticsManager().canGainRollXP();
+            if (!isExploiting && canGainXp) {
+                rollResultBuilder.xpGain((int) calculateRollXP(mmoPlayer, baseDamage, false));
+            }
+
+            return rollResultBuilder.build();
+        }
+        // Fall was fatal return null
+        return null;
     }
     }
 
 
     @NotNull
     @NotNull
     public static Probability getGracefulProbability(McMMOPlayer mmoPlayer) {
     public static Probability getGracefulProbability(McMMOPlayer mmoPlayer) {
-        double gracefulOdds = ProbabilityUtil.getSubSkillProbability(SubSkillType.ACROBATICS_ROLL, mmoPlayer).getValue() * 2;
+        double gracefulOdds = getSubSkillProbability(SubSkillType.ACROBATICS_ROLL, mmoPlayer).getValue() * 2;
         return Probability.ofValue(gracefulOdds);
         return Probability.ofValue(gracefulOdds);
     }
     }
 
 
     public static Probability getNonGracefulProbability(McMMOPlayer mmoPlayer) {
     public static Probability getNonGracefulProbability(McMMOPlayer mmoPlayer) {
-        double gracefulOdds = ProbabilityUtil.getSubSkillProbability(SubSkillType.ACROBATICS_ROLL, mmoPlayer).getValue();
+        double gracefulOdds = getSubSkillProbability(SubSkillType.ACROBATICS_ROLL, mmoPlayer).getValue();
         return Probability.ofValue(gracefulOdds);
         return Probability.ofValue(gracefulOdds);
     }
     }
 
 
@@ -242,7 +272,7 @@ public class Roll extends AcrobaticsSubSkill {
      *
      *
      * @return true if exploits are detected, false otherwise
      * @return true if exploits are detected, false otherwise
      */
      */
-    private boolean isExploiting(McMMOPlayer mmoPlayer) {
+    private boolean isPlayerExploitingAcrobatics(McMMOPlayer mmoPlayer) {
         if (!ExperienceConfig.getInstance().isAcrobaticsExploitingPrevented()) {
         if (!ExperienceConfig.getInstance().isAcrobaticsExploitingPrevented()) {
             return false;
             return false;
         }
         }
@@ -331,11 +361,9 @@ public class Roll extends AcrobaticsSubSkill {
      */
      */
     @Override
     @Override
     public Double[] getStats(McMMOPlayer mmoPlayer) {
     public Double[] getStats(McMMOPlayer mmoPlayer) {
-        double playerChanceRoll = ProbabilityUtil.getSubSkillProbability(subSkillType, mmoPlayer).getValue();
+        double playerChanceRoll = getSubSkillProbability(subSkillType, mmoPlayer).getValue();
         double playerChanceGrace = playerChanceRoll * 2;
         double playerChanceGrace = playerChanceRoll * 2;
 
 
-        double gracefulOdds = ProbabilityUtil.getSubSkillProbability(subSkillType, mmoPlayer).getValue() * 2;
-
         return new Double[]{ playerChanceRoll, playerChanceGrace };
         return new Double[]{ playerChanceRoll, playerChanceGrace };
     }
     }
 
 

+ 115 - 0
src/main/java/com/gmail/nossr50/datatypes/skills/subskills/acrobatics/RollResult.java

@@ -0,0 +1,115 @@
+package com.gmail.nossr50.datatypes.skills.subskills.acrobatics;
+
+import org.bukkit.event.entity.EntityDamageEvent;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Immutable class representing the result of a roll action in acrobatics.
+ */
+public class RollResult {
+    private final boolean rollSuccess;
+    private final boolean isGraceful;
+    private final double eventDamage;
+    private final double modifiedDamage;
+    private final boolean isFatal;
+    private final boolean isExploiting;
+    private final float xpGain;
+
+    private RollResult(Builder builder) {
+        this.rollSuccess = builder.rollSuccess;
+        this.isGraceful = builder.isGraceful;
+        this.eventDamage = builder.eventDamage;
+        this.modifiedDamage = builder.modifiedDamage;
+        this.isFatal = builder.isFatal;
+        this.isExploiting = builder.isExploiting;
+        this.xpGain = builder.xpGain;
+    }
+
+    public boolean isRollSuccess() {
+        return rollSuccess;
+    }
+
+    public boolean isGraceful() {
+        return isGraceful;
+    }
+
+    public double getEventDamage() {
+        return eventDamage;
+    }
+
+    public double getModifiedDamage() {
+        return modifiedDamage;
+    }
+
+    public boolean isFatal() {
+        return isFatal;
+    }
+
+    public boolean isExploiting() {
+        return isExploiting;
+    }
+
+    public float getXpGain() {
+        return xpGain;
+    }
+
+    /**
+     * Builder class for constructing {@code RollResult} instances.
+     */
+    public static class Builder {
+        private final boolean isGraceful;
+        private final double eventDamage;
+        private double modifiedDamage;
+        private boolean isFatal;
+        private boolean rollSuccess;
+        private boolean isExploiting;
+        private float xpGain;
+
+        /**
+         * Constructs a new {@code Builder} with required parameters.
+         *
+         * @param entityDamageEvent the damage event, must not be null
+         * @param isGracefulRoll    whether the roll is graceful
+         */
+        public Builder(EntityDamageEvent entityDamageEvent, boolean isGracefulRoll) {
+            requireNonNull(entityDamageEvent, "EntityDamageEvent cannot be null");
+            this.eventDamage = entityDamageEvent.getDamage();
+            this.isGraceful = isGracefulRoll;
+        }
+
+        public Builder modifiedDamage(double modifiedDamage) {
+            this.modifiedDamage = modifiedDamage;
+            return this;
+        }
+
+        public Builder fatal(boolean isFatal) {
+            this.isFatal = isFatal;
+            return this;
+        }
+
+        public Builder rollSuccess(boolean rollSuccess) {
+            this.rollSuccess = rollSuccess;
+            return this;
+        }
+
+        public Builder exploiting(boolean isExploiting) {
+            this.isExploiting = isExploiting;
+            return this;
+        }
+
+        public Builder xpGain(float xpGain) {
+            this.xpGain = xpGain;
+            return this;
+        }
+
+        /**
+         * Builds and returns a {@code RollResult} instance.
+         *
+         * @return a new {@code RollResult}
+         */
+        public RollResult build() {
+            return new RollResult(this);
+        }
+    }
+}

+ 42 - 4
src/test/java/com/gmail/nossr50/MMOTestEnvironment.java

@@ -15,11 +15,16 @@ import com.gmail.nossr50.util.*;
 import com.gmail.nossr50.util.blockmeta.ChunkManager;
 import com.gmail.nossr50.util.blockmeta.ChunkManager;
 import com.gmail.nossr50.util.compat.CompatibilityManager;
 import com.gmail.nossr50.util.compat.CompatibilityManager;
 import com.gmail.nossr50.util.platform.MinecraftGameVersion;
 import com.gmail.nossr50.util.platform.MinecraftGameVersion;
+import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.SkillTools;
 import com.gmail.nossr50.util.skills.SkillTools;
+import com.gmail.nossr50.util.sounds.SoundManager;
 import org.bukkit.*;
 import org.bukkit.*;
+import org.bukkit.attribute.Attribute;
+import org.bukkit.block.Block;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;
+import org.bukkit.event.Event;
 import org.bukkit.inventory.ItemFactory;
 import org.bukkit.inventory.ItemFactory;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.PlayerInventory;
 import org.bukkit.inventory.PlayerInventory;
@@ -44,6 +49,8 @@ public abstract class MMOTestEnvironment {
     protected MockedStatic<Misc> mockedMisc;
     protected MockedStatic<Misc> mockedMisc;
     protected MockedStatic<SkillTools> mockedSkillTools;
     protected MockedStatic<SkillTools> mockedSkillTools;
     protected MockedStatic<EventUtils> mockedEventUtils;
     protected MockedStatic<EventUtils> mockedEventUtils;
+    protected MockedStatic<NotificationManager> notificationManager;
+    protected MockedStatic<SoundManager> mockedSoundManager;
     protected TransientEntityTracker transientEntityTracker;
     protected TransientEntityTracker transientEntityTracker;
     protected AdvancedConfig advancedConfig;
     protected AdvancedConfig advancedConfig;
     protected PartyConfig partyConfig;
     protected PartyConfig partyConfig;
@@ -63,7 +70,6 @@ public abstract class MMOTestEnvironment {
     protected PlayerInventory playerInventory;
     protected PlayerInventory playerInventory;
     protected PlayerProfile playerProfile;
     protected PlayerProfile playerProfile;
     protected McMMOPlayer mmoPlayer;
     protected McMMOPlayer mmoPlayer;
-    protected String playerName = "testPlayer";
     protected ItemFactory itemFactory;
     protected ItemFactory itemFactory;
 
 
     protected ChunkManager chunkManager;
     protected ChunkManager chunkManager;
@@ -124,14 +130,25 @@ public abstract class MMOTestEnvironment {
         this.server = mock(Server.class);
         this.server = mock(Server.class);
         when(mcMMO.p.getServer()).thenReturn(server);
         when(mcMMO.p.getServer()).thenReturn(server);
 
 
+        // wire Bukkit
         mockedBukkit = mockStatic(Bukkit.class);
         mockedBukkit = mockStatic(Bukkit.class);
         when(Bukkit.getItemFactory()).thenReturn(itemFactory);
         when(Bukkit.getItemFactory()).thenReturn(itemFactory);
         itemFactory = mock(ItemFactory.class);
         itemFactory = mock(ItemFactory.class);
-        // when(itemFactory.getItemMeta(any())).thenReturn(mock(ItemMeta.class));
+
+        // wire Bukkit call to get server
+        when(Bukkit.getServer()).thenReturn(server);
 
 
         // wire plugin manager
         // wire plugin manager
         this.pluginManager = mock(PluginManager.class);
         this.pluginManager = mock(PluginManager.class);
+        // wire server -> plugin manager
         when(server.getPluginManager()).thenReturn(pluginManager);
         when(server.getPluginManager()).thenReturn(pluginManager);
+        // wire Bukkit -> plugin manager
+        when(Bukkit.getPluginManager()).thenReturn(pluginManager);
+        // return the argument provided when call event is invoked on plugin manager mock
+        doAnswer(invocation -> {
+            Object[] args = invocation.getArguments();
+            return args[0];
+        }).when(pluginManager).callEvent(any(Event.class));
 
 
         // wire world
         // wire world
         this.world = mock(World.class);
         this.world = mock(World.class);
@@ -143,10 +160,19 @@ public abstract class MMOTestEnvironment {
         // setup player and player related mocks after everything else
         // setup player and player related mocks after everything else
         this.player = mock(Player.class);
         this.player = mock(Player.class);
         when(player.getUniqueId()).thenReturn(playerUUID);
         when(player.getUniqueId()).thenReturn(playerUUID);
-
+        when(player.isValid()).thenReturn(true);
+        when(player.isOnline()).thenReturn(true);
+        // health
+        when(player.getHealth()).thenReturn(20D);
         // wire inventory
         // wire inventory
         this.playerInventory = mock(PlayerInventory.class);
         this.playerInventory = mock(PlayerInventory.class);
         when(player.getInventory()).thenReturn(playerInventory);
         when(player.getInventory()).thenReturn(playerInventory);
+        // player location
+        Location playerLocation = mock(Location.class);
+        Block playerLocationBlock = mock(Block.class);
+        when(player.getLocation()).thenReturn(playerLocation);
+        when(playerLocation.getBlock()).thenReturn(playerLocationBlock);
+        // when(playerLocationBlock.getType()).thenReturn(Material.AIR);
 
 
         // PlayerProfile and McMMOPlayer are partially mocked
         // PlayerProfile and McMMOPlayer are partially mocked
         playerProfile = new PlayerProfile("testPlayer", player.getUniqueId(), 0);
         playerProfile = new PlayerProfile("testPlayer", player.getUniqueId(), 0);
@@ -158,6 +184,12 @@ public abstract class MMOTestEnvironment {
 
 
         this.materialMapStore = new MaterialMapStore();
         this.materialMapStore = new MaterialMapStore();
         when(mcMMO.getMaterialMapStore()).thenReturn(materialMapStore);
         when(mcMMO.getMaterialMapStore()).thenReturn(materialMapStore);
+
+        // wire notification manager
+        notificationManager = mockStatic(NotificationManager.class);
+
+        // wire sound manager
+        mockedSoundManager = mockStatic(SoundManager.class);
     }
     }
 
 
     private void mockPermissions() {
     private void mockPermissions() {
@@ -201,7 +233,7 @@ public abstract class MMOTestEnvironment {
         when(ExperienceConfig.getInstance().getCombatXP("Cow")).thenReturn(1D);
         when(ExperienceConfig.getInstance().getCombatXP("Cow")).thenReturn(1D);
     }
     }
 
 
-    protected void cleanupBaseEnvironment() {
+    protected void cleanUpStaticMocks() {
         // Clean up resources here if needed.
         // Clean up resources here if needed.
         if (mockedMcMMO != null) {
         if (mockedMcMMO != null) {
             mockedMcMMO.close();
             mockedMcMMO.close();
@@ -230,5 +262,11 @@ public abstract class MMOTestEnvironment {
         if (mockedBukkit != null) {
         if (mockedBukkit != null) {
             mockedBukkit.close();
             mockedBukkit.close();
         }
         }
+        if (notificationManager != null) {
+            notificationManager.close();
+        }
+        if (mockedSoundManager != null) {
+            mockedSoundManager.close();
+        }
     }
     }
 }
 }

+ 3 - 2
src/test/java/com/gmail/nossr50/party/PartyManagerTest.java

@@ -12,12 +12,13 @@ import org.mockito.Mockito;
 import java.util.UUID;
 import java.util.UUID;
 import java.util.logging.Logger;
 import java.util.logging.Logger;
 
 
+import static java.util.logging.Logger.getLogger;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.when;
 
 
 class PartyManagerTest extends MMOTestEnvironment {
 class PartyManagerTest extends MMOTestEnvironment {
-    private static final Logger logger = Logger.getLogger(PartyManagerTest.class.getName());
+    private static final Logger logger = getLogger(PartyManagerTest.class.getName());
 
 
     @BeforeEach
     @BeforeEach
     public void setUp() {
     public void setUp() {
@@ -29,7 +30,7 @@ class PartyManagerTest extends MMOTestEnvironment {
 
 
     @AfterEach
     @AfterEach
     public void tearDown() {
     public void tearDown() {
-        cleanupBaseEnvironment();
+        cleanUpStaticMocks();
 
 
         // disable parties in config for other tests
         // disable parties in config for other tests
         Mockito.when(partyConfig.isPartyEnabled()).thenReturn(false);
         Mockito.when(partyConfig.isPartyEnabled()).thenReturn(false);

+ 101 - 0
src/test/java/com/gmail/nossr50/skills/acrobatics/AcrobaticsTest.java

@@ -0,0 +1,101 @@
+package com.gmail.nossr50.skills.acrobatics;
+
+import com.gmail.nossr50.MMOTestEnvironment;
+import com.gmail.nossr50.api.exceptions.InvalidSkillException;
+import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
+import com.gmail.nossr50.datatypes.skills.SubSkillType;
+import com.gmail.nossr50.datatypes.skills.subskills.AbstractSubSkill;
+import com.gmail.nossr50.datatypes.skills.subskills.acrobatics.Roll;
+import com.gmail.nossr50.mcMMO;
+import com.gmail.nossr50.util.skills.RankUtils;
+import org.bukkit.entity.Player;
+import org.bukkit.event.entity.EntityDamageEvent;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.logging.Logger;
+
+import static java.util.logging.Logger.getLogger;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+
+class AcrobaticsTest extends MMOTestEnvironment {
+    private static final Logger logger = getLogger(AcrobaticsTest.class.getName());
+
+    @BeforeEach
+    void setUp() throws InvalidSkillException {
+        mockBaseEnvironment(logger);
+        when(rankConfig.getSubSkillUnlockLevel(SubSkillType.ACROBATICS_ROLL, 1)).thenReturn(1);
+        when(rankConfig.getSubSkillUnlockLevel(SubSkillType.ACROBATICS_DODGE, 1)).thenReturn(1);
+
+        // wire advanced config
+        when(advancedConfig.getMaximumProbability(SubSkillType.ACROBATICS_ROLL)).thenReturn(100D);
+        when(advancedConfig.getMaxBonusLevel(SubSkillType.ACROBATICS_ROLL)).thenReturn(1000);
+        when(advancedConfig.getRollDamageThreshold()).thenReturn(7D);
+
+        Mockito.when(RankUtils.getRankUnlockLevel(SubSkillType.ACROBATICS_ROLL, 1)).thenReturn(1); // needed?
+        Mockito.when(RankUtils.getRankUnlockLevel(SubSkillType.ACROBATICS_DODGE, 1)).thenReturn(1000); // needed?
+
+        when(RankUtils.getRankUnlockLevel(SubSkillType.ACROBATICS_ROLL, 1)).thenReturn(1); // needed?
+        when(RankUtils.hasReachedRank(eq(1), any(Player.class), eq(SubSkillType.ACROBATICS_ROLL))).thenReturn(true);
+        when(RankUtils.hasReachedRank(eq(1), any(Player.class), any(AbstractSubSkill.class))).thenReturn(true);
+    }
+
+    @AfterEach
+    void tearDown() {
+        cleanUpStaticMocks();
+    }
+
+    @SuppressWarnings("deprecation")
+    @Test
+    public void rollShouldLowerDamage() {
+        // Given
+        final Roll roll = new Roll();
+        final double damage = 2D;
+        final EntityDamageEvent mockEvent = mockEntityDamageEvent(damage);
+        mmoPlayer.modifySkill(PrimarySkillType.ACROBATICS, 1000);
+        when(roll.canRoll(mmoPlayer)).thenReturn(true);
+        assertThat(roll.canRoll(mmoPlayer)).isTrue();
+
+        // When
+        roll.doInteraction(mockEvent, mcMMO.p);
+
+        // Then
+        verify(mockEvent, atLeastOnce()).setDamage(0);
+    }
+
+    @SuppressWarnings("deprecation")
+    @Test
+    public void rollShouldNotLowerDamage() {
+        // Given
+        final Roll roll = new Roll();
+        final double damage = 100D;
+        final EntityDamageEvent mockEvent = mockEntityDamageEvent(damage);
+        mmoPlayer.modifySkill(PrimarySkillType.ACROBATICS, 0);
+        when(roll.canRoll(mmoPlayer)).thenReturn(true);
+        assertThat(roll.canRoll(mmoPlayer)).isTrue();
+
+        // When
+        roll.doInteraction(mockEvent, mcMMO.p);
+
+        // Then
+        assertThat(roll.canRoll(mmoPlayer)).isTrue();
+        verify(mockEvent, Mockito.never()).setDamage(any(Double.class));
+    }
+
+    private @NotNull EntityDamageEvent mockEntityDamageEvent(double damage) {
+        final EntityDamageEvent mockEvent = mock(EntityDamageEvent.class);
+        when(mockEvent.getCause()).thenReturn(EntityDamageEvent.DamageCause.FALL);
+        when(mockEvent.getFinalDamage()).thenReturn(damage);
+        when(mockEvent.getDamage(any(EntityDamageEvent.DamageModifier.class))).thenReturn(damage);
+        when(mockEvent.getDamage()).thenReturn(damage);
+        when(mockEvent.isCancelled()).thenReturn(false);
+        when(mockEvent.getEntity()).thenReturn(player);
+        return mockEvent;
+    }
+}

+ 1 - 8
src/test/java/com/gmail/nossr50/skills/excavation/ExcavationTest.java

@@ -13,7 +13,6 @@ import org.bukkit.block.BlockState;
 import org.bukkit.block.data.BlockData;
 import org.bukkit.block.data.BlockData;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.ItemStack;
-import org.bukkit.inventory.PlayerInventory;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
@@ -29,7 +28,6 @@ import static org.mockito.Mockito.*;
 class ExcavationTest extends MMOTestEnvironment {
 class ExcavationTest extends MMOTestEnvironment {
     private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(ExcavationTest.class.getName());
     private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(ExcavationTest.class.getName());
 
 
-
     @BeforeEach
     @BeforeEach
     void setUp() throws InvalidSkillException {
     void setUp() throws InvalidSkillException {
         mockBaseEnvironment(logger);
         mockBaseEnvironment(logger);
@@ -48,18 +46,13 @@ class ExcavationTest extends MMOTestEnvironment {
         when(player.getUniqueId()).thenReturn(playerUUID);
         when(player.getUniqueId()).thenReturn(playerUUID);
 
 
         // wire inventory
         // wire inventory
-        this.playerInventory = Mockito.mock(PlayerInventory.class);
         this.itemInMainHand = new ItemStack(Material.DIAMOND_SHOVEL);
         this.itemInMainHand = new ItemStack(Material.DIAMOND_SHOVEL);
-        when(player.getInventory()).thenReturn(playerInventory);
         when(playerInventory.getItemInMainHand()).thenReturn(itemInMainHand);
         when(playerInventory.getItemInMainHand()).thenReturn(itemInMainHand);
-
-        // Set up spy for Excavation Manager
-
     }
     }
 
 
     @AfterEach
     @AfterEach
     void tearDown() {
     void tearDown() {
-        cleanupBaseEnvironment();
+        cleanUpStaticMocks();
     }
     }
 
 
     @Test
     @Test

+ 6 - 2
src/test/java/com/gmail/nossr50/skills/tridents/TridentsTest.java

@@ -10,8 +10,12 @@ import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.mockito.Mockito;
 import org.mockito.Mockito;
 
 
+import java.util.logging.Logger;
+
+import static java.util.logging.Logger.getLogger;
+
 class TridentsTest extends MMOTestEnvironment {
 class TridentsTest extends MMOTestEnvironment {
-    private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(TridentsTest.class.getName());
+    private static final Logger logger = getLogger(TridentsTest.class.getName());
 
 
     TridentsManager tridentsManager;
     TridentsManager tridentsManager;
     ItemStack trident;
     ItemStack trident;
@@ -34,6 +38,6 @@ class TridentsTest extends MMOTestEnvironment {
 
 
     @AfterEach
     @AfterEach
     void tearDown() {
     void tearDown() {
-        cleanupBaseEnvironment();
+        cleanUpStaticMocks();
     }
     }
 }
 }

+ 6 - 9
src/test/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingTest.java

@@ -11,21 +11,23 @@ import org.bukkit.block.Block;
 import org.bukkit.block.BlockState;
 import org.bukkit.block.BlockState;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.ItemStack;
-import org.bukkit.inventory.PlayerInventory;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 import org.mockito.Mockito;
 
 
 import java.util.Collections;
 import java.util.Collections;
+import java.util.logging.Logger;
 
 
+import static java.util.logging.Logger.getLogger;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.eq;
 
 
 class WoodcuttingTest extends MMOTestEnvironment {
 class WoodcuttingTest extends MMOTestEnvironment {
-    private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(WoodcuttingTest.class.getName());
+    private static final Logger logger = getLogger(WoodcuttingTest.class.getName());
+
+    private WoodcuttingManager woodcuttingManager;
 
 
-    WoodcuttingManager woodcuttingManager;
     @BeforeEach
     @BeforeEach
     void setUp() throws InvalidSkillException {
     void setUp() throws InvalidSkillException {
         mockBaseEnvironment(logger);
         mockBaseEnvironment(logger);
@@ -42,12 +44,7 @@ class WoodcuttingTest extends MMOTestEnvironment {
         Mockito.when(RankUtils.hasReachedRank(eq(1), any(Player.class), eq(SubSkillType.WOODCUTTING_HARVEST_LUMBER))).thenReturn(true);
         Mockito.when(RankUtils.hasReachedRank(eq(1), any(Player.class), eq(SubSkillType.WOODCUTTING_HARVEST_LUMBER))).thenReturn(true);
         Mockito.when(RankUtils.hasReachedRank(eq(1), any(Player.class), eq(SubSkillType.WOODCUTTING_CLEAN_CUTS))).thenReturn(true);
         Mockito.when(RankUtils.hasReachedRank(eq(1), any(Player.class), eq(SubSkillType.WOODCUTTING_CLEAN_CUTS))).thenReturn(true);
 
 
-        // setup player and player related mocks after everything else
-        this.player = Mockito.mock(Player.class);
-        Mockito.when(player.getUniqueId()).thenReturn(playerUUID);
-
         // wire inventory
         // wire inventory
-        this.playerInventory = Mockito.mock(PlayerInventory.class);
         this.itemInMainHand = new ItemStack(Material.DIAMOND_AXE);
         this.itemInMainHand = new ItemStack(Material.DIAMOND_AXE);
         Mockito.when(player.getInventory()).thenReturn(playerInventory);
         Mockito.when(player.getInventory()).thenReturn(playerInventory);
         Mockito.when(playerInventory.getItemInMainHand()).thenReturn(itemInMainHand);
         Mockito.when(playerInventory.getItemInMainHand()).thenReturn(itemInMainHand);
@@ -58,7 +55,7 @@ class WoodcuttingTest extends MMOTestEnvironment {
 
 
     @AfterEach
     @AfterEach
     void tearDown() {
     void tearDown() {
-        cleanupBaseEnvironment();
+        cleanUpStaticMocks();
     }
     }
 
 
     @Test
     @Test

+ 1 - 1
src/test/java/com/gmail/nossr50/util/random/ProbabilityUtilTest.java

@@ -38,7 +38,7 @@ class ProbabilityUtilTest extends MMOTestEnvironment {
 
 
     @AfterEach
     @AfterEach
     public void tearDown() {
     public void tearDown() {
-        cleanupBaseEnvironment();
+        cleanUpStaticMocks();
     }
     }
 
 
     private static Stream<Arguments> staticChanceSkills() {
     private static Stream<Arguments> staticChanceSkills() {