feat(combat): opt out attacking players
This commit is contained in:
@@ -42,3 +42,10 @@
|
|||||||
- Name cleanup restores only values Leaf installed; third-party changes are preserved.
|
- Name cleanup restores only values Leaf installed; third-party changes are preserved.
|
||||||
- Documented scoreboard, custom chat, tab-list, and per-viewer scoreboard limitations.
|
- Documented scoreboard, custom chat, tab-list, and per-viewer scoreboard limitations.
|
||||||
- US-003 remains in progress until combat and global administration paths are verified.
|
- US-003 remains in progress until combat and global administration paths are verified.
|
||||||
|
|
||||||
|
### US-002 combat opt-out completed
|
||||||
|
|
||||||
|
- Added modern Spigot damage-source attribution for direct attacks, projectiles, thrown potions, area clouds, tamed animals, TNT, Thorns, and other reliably attributed sources.
|
||||||
|
- Ambiguous, cancelled, zero-damage, environmental, and self damage do not opt out a player.
|
||||||
|
- Combat opt-out bypasses administrative locks, immediately reconciles effect and prefix state, persists once, and sends one explanatory message.
|
||||||
|
- Verified attribution, victim safety, lock bypass, duplicate suppression, and the full build with `./gradlew clean check jar`.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
type: User Story
|
type: User Story
|
||||||
title: "US-002: Relinquish protection when attacking"
|
title: "US-002: Relinquish protection when attacking"
|
||||||
description: Remove Leaf protection when an opted-in player attacks another player.
|
description: Remove Leaf protection when an opted-in player attacks another player.
|
||||||
status: backlog
|
status: done
|
||||||
---
|
---
|
||||||
|
|
||||||
# US-002: Relinquish protection when attacking
|
# US-002: Relinquish protection when attacking
|
||||||
@@ -11,17 +11,17 @@ As a **player facing PvP**, I want Leaf protection to belong only to non-aggress
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] When an opted-in player damages another player, Leaf automatically changes the attacker's saved choice to opted out.
|
- [x] When an opted-in player damages another player, Leaf automatically changes the attacker's saved choice to opted out.
|
||||||
- [ ] Automatic opt-out immediately removes Leaf-managed Resistance and the attacker's leaf prefix.
|
- [x] Automatic opt-out immediately removes Leaf-managed Resistance and the attacker's leaf prefix.
|
||||||
- [ ] The attacker receives a clear chat message explaining that attacking another player disabled Leaf and that they may opt in again when permitted.
|
- [x] The attacker receives a clear chat message explaining that attacking another player disabled Leaf and that they may opt in again when permitted.
|
||||||
- [ ] Receiving player-caused damage without retaliating does not change the protected player's choice or Resistance.
|
- [x] Receiving player-caused damage without retaliating does not change the protected player's choice or Resistance.
|
||||||
- [ ] Retaliatory damage counts as attacking, including Thorns damage attributable to the protected player.
|
- [x] Retaliatory damage counts as attacking, including Thorns damage attributable to the protected player.
|
||||||
- [ ] Direct melee attacks and player-fired projectiles are attributed to the attacking player.
|
- [x] Direct melee attacks and player-fired projectiles are attributed to the attacking player.
|
||||||
- [ ] Harmful splash or lingering potion damage is attributed to the player who threw the potion.
|
- [x] Harmful splash or lingering potion damage is attributed to the player who threw the potion.
|
||||||
- [ ] Damage caused by a tamed animal is attributed to its player owner when Spigot exposes that ownership.
|
- [x] Damage caused by a tamed animal is attributed to its player owner when Spigot exposes that ownership.
|
||||||
- [ ] TNT, fire, and other indirect damage trigger opt-out only when Spigot exposes a reliable responsible player; ambiguous environmental damage does not opt out an innocent player.
|
- [x] TNT, fire, and other indirect damage trigger opt-out only when Spigot exposes a reliable responsible player; ambiguous environmental damage does not opt out an innocent player.
|
||||||
- [ ] PvP-triggered opt-out applies even when an administrator has locked the player's preference.
|
- [x] PvP-triggered opt-out applies even when an administrator has locked the player's preference.
|
||||||
- [ ] A single attack produces no duplicate state changes or duplicate notifications.
|
- [x] A single attack produces no duplicate state changes or duplicate notifications.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ public final class LeafCommand implements TabExecutor {
|
|||||||
|
|
||||||
private void reportChoice(Player player, boolean enabled, LeafRuntime.Change change) {
|
private void reportChoice(Player player, boolean enabled, LeafRuntime.Change change) {
|
||||||
if (change == LeafRuntime.Change.LOCKED) {
|
if (change == LeafRuntime.Change.LOCKED) {
|
||||||
player.sendMessage(runtime.settings().lockedMessage());
|
player.sendMessage(LeafText.color(runtime.settings().lockedMessage()));
|
||||||
} else if (change == LeafRuntime.Change.UNCHANGED) {
|
} else if (change == LeafRuntime.Change.UNCHANGED) {
|
||||||
player.sendMessage("Leaf was already " + (enabled ? "enabled" : "disabled") + ".");
|
player.sendMessage("Leaf was already " + (enabled ? "enabled" : "disabled") + ".");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ package games.dmg.leaf;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.entity.EntityDamageEvent;
|
||||||
import org.bukkit.event.player.PlayerJoinEvent;
|
import org.bukkit.event.player.PlayerJoinEvent;
|
||||||
|
|
||||||
/** Handles player lifecycle events that affect Leaf protection. */
|
/** Handles player lifecycle events that affect Leaf protection. */
|
||||||
@@ -13,11 +18,13 @@ public final class LeafListener implements Listener {
|
|||||||
private final LeafRuntime runtime;
|
private final LeafRuntime runtime;
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
private final Logger logger;
|
private final Logger logger;
|
||||||
|
private final PlayerDamageAttributor damageAttributor;
|
||||||
|
|
||||||
public LeafListener(LeafRuntime runtime, Clock clock, Logger logger) {
|
public LeafListener(LeafRuntime runtime, Clock clock, Logger logger) {
|
||||||
this.runtime = runtime;
|
this.runtime = runtime;
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
|
this.damageAttributor = new PlayerDamageAttributor();
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
@@ -30,4 +37,21 @@ public final class LeafListener implements Listener {
|
|||||||
event.getPlayer().sendMessage("Leaf protection is unavailable because state could not be saved.");
|
event.getPlayer().sendMessage("Leaf protection is unavailable because state could not be saved.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onDamage(EntityDamageEvent event) {
|
||||||
|
if (event.isCancelled() || event.getFinalDamage() <= 0.0
|
||||||
|
|| !(event.getEntity() instanceof Player victim)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Optional<UUID> responsible = damageAttributor.responsiblePlayer(event);
|
||||||
|
if (responsible.isEmpty() || responsible.get().equals(victim.getUniqueId())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
runtime.combatOptOut(responsible.get());
|
||||||
|
} catch (IOException exception) {
|
||||||
|
logger.log(Level.SEVERE, "Could not persist Leaf combat opt-out", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,19 @@ public final class LeafRuntime {
|
|||||||
return Change.CHANGED;
|
return Change.CHANGED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Change combatOptOut(UUID playerId) throws IOException {
|
||||||
|
PlayerLeafState state = stateManager.find(playerId).orElse(null);
|
||||||
|
if (state == null || !state.optedIn()) {
|
||||||
|
return Change.UNCHANGED;
|
||||||
|
}
|
||||||
|
Change changed = setChoice(playerId, false);
|
||||||
|
Player player = server.getPlayer(playerId);
|
||||||
|
if (changed == Change.CHANGED && player != null) {
|
||||||
|
player.sendMessage(LeafText.color(settingsProvider.current().combatDisabledMessage()));
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
public Change setLocked(UUID playerId, boolean locked) throws IOException {
|
public Change setLocked(UUID playerId, boolean locked) throws IOException {
|
||||||
PlayerLeafState state = requiredState(playerId);
|
PlayerLeafState state = requiredState(playerId);
|
||||||
if (state.locked() == locked) {
|
if (state.locked() == locked) {
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ public record LeafSettings(
|
|||||||
string(
|
string(
|
||||||
values,
|
values,
|
||||||
"combat-disabled-message",
|
"combat-disabled-message",
|
||||||
"&cLeaf protection was disabled because you attacked another player."
|
"&cLeaf protection was disabled because you attacked another player. "
|
||||||
|
+ "You may use /leaf on again when permitted."
|
||||||
),
|
),
|
||||||
string(values, "locked-message", "&cAn administrator locked your Leaf setting.")
|
string(values, "locked-message", "&cAn administrator locked your Leaf setting.")
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package games.dmg.leaf;
|
||||||
|
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
|
||||||
|
final class LeafText {
|
||||||
|
private LeafText() { }
|
||||||
|
|
||||||
|
static String color(String value) {
|
||||||
|
return ChatColor.translateAlternateColorCodes('&', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package games.dmg.leaf;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.bukkit.damage.DamageSource;
|
||||||
|
import org.bukkit.entity.AreaEffectCloud;
|
||||||
|
import org.bukkit.entity.Entity;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.entity.Projectile;
|
||||||
|
import org.bukkit.entity.TNTPrimed;
|
||||||
|
import org.bukkit.entity.Tameable;
|
||||||
|
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||||
|
import org.bukkit.event.entity.EntityDamageEvent;
|
||||||
|
import org.bukkit.projectiles.ProjectileSource;
|
||||||
|
|
||||||
|
/** Resolves only player attribution that Spigot exposes reliably. */
|
||||||
|
public final class PlayerDamageAttributor {
|
||||||
|
public Optional<UUID> responsiblePlayer(EntityDamageEvent event) {
|
||||||
|
DamageSource source = event.getDamageSource();
|
||||||
|
Optional<UUID> causing = source == null
|
||||||
|
? Optional.empty()
|
||||||
|
: fromEntity(source.getCausingEntity(), 0);
|
||||||
|
if (causing.isPresent()) {
|
||||||
|
return causing;
|
||||||
|
}
|
||||||
|
Optional<UUID> direct = source == null
|
||||||
|
? Optional.empty()
|
||||||
|
: fromEntity(source.getDirectEntity(), 0);
|
||||||
|
if (direct.isPresent()) {
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
|
if (event instanceof EntityDamageByEntityEvent byEntity) {
|
||||||
|
return fromEntity(byEntity.getDamager(), 0);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<UUID> fromEntity(Entity entity, int depth) {
|
||||||
|
if (entity == null || depth > 4) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
if (entity instanceof Player player) {
|
||||||
|
return Optional.of(player.getUniqueId());
|
||||||
|
}
|
||||||
|
if (entity instanceof Tameable tameable && tameable.getOwner() != null) {
|
||||||
|
return Optional.of(tameable.getOwner().getUniqueId());
|
||||||
|
}
|
||||||
|
if (entity instanceof TNTPrimed tnt) {
|
||||||
|
return fromEntity(tnt.getSource(), depth + 1);
|
||||||
|
}
|
||||||
|
if (entity instanceof Projectile projectile) {
|
||||||
|
return fromSource(projectile.getShooter(), depth + 1);
|
||||||
|
}
|
||||||
|
if (entity instanceof AreaEffectCloud cloud) {
|
||||||
|
return fromSource(cloud.getSource(), depth + 1);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<UUID> fromSource(ProjectileSource source, int depth) {
|
||||||
|
if (source instanceof Entity entity) {
|
||||||
|
return fromEntity(entity, depth);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,5 +11,5 @@ prefix: "&a🍃 "
|
|||||||
onboarding-days: 7
|
onboarding-days: 7
|
||||||
|
|
||||||
welcome-message: "&aLeaf protection is available: /leaf on, /leaf off, or /leaf status. Attacking another player opts you out."
|
welcome-message: "&aLeaf protection is available: /leaf on, /leaf off, or /leaf status. Attacking another player opts you out."
|
||||||
combat-disabled-message: "&cLeaf protection was disabled because you attacked another player."
|
combat-disabled-message: "&cLeaf protection was disabled because you attacked another player. You may use /leaf on again when permitted."
|
||||||
locked-message: "&cAn administrator locked your Leaf setting."
|
locked-message: "&cAn administrator locked your Leaf setting."
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package games.dmg.leaf;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import org.bukkit.damage.DamageSource;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.entity.EntityDamageEvent;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
final class LeafListenerCombatTest {
|
||||||
|
@Test
|
||||||
|
void optsOutTheAttackerButNotTheVictim() throws Exception {
|
||||||
|
UUID attackerId = UUID.randomUUID();
|
||||||
|
UUID victimId = UUID.randomUUID();
|
||||||
|
Player attacker = mock(Player.class);
|
||||||
|
Player victim = mock(Player.class);
|
||||||
|
when(attacker.getUniqueId()).thenReturn(attackerId);
|
||||||
|
when(victim.getUniqueId()).thenReturn(victimId);
|
||||||
|
DamageSource source = mock(DamageSource.class);
|
||||||
|
when(source.getCausingEntity()).thenReturn(attacker);
|
||||||
|
EntityDamageEvent event = mock(EntityDamageEvent.class);
|
||||||
|
when(event.getEntity()).thenReturn(victim);
|
||||||
|
when(event.getFinalDamage()).thenReturn(2.0);
|
||||||
|
when(event.getDamageSource()).thenReturn(source);
|
||||||
|
LeafRuntime runtime = mock(LeafRuntime.class);
|
||||||
|
|
||||||
|
new LeafListener(runtime, Clock.systemUTC(), Logger.getAnonymousLogger()).onDamage(event);
|
||||||
|
|
||||||
|
verify(runtime).combatOptOut(attackerId);
|
||||||
|
verify(runtime, never()).combatOptOut(victimId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ignoresCancelledAndUnattributedDamage() throws Exception {
|
||||||
|
Player victim = mock(Player.class);
|
||||||
|
when(victim.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||||
|
EntityDamageEvent event = mock(EntityDamageEvent.class);
|
||||||
|
when(event.getEntity()).thenReturn(victim);
|
||||||
|
when(event.getFinalDamage()).thenReturn(2.0);
|
||||||
|
when(event.isCancelled()).thenReturn(true);
|
||||||
|
LeafRuntime runtime = mock(LeafRuntime.class);
|
||||||
|
|
||||||
|
new LeafListener(runtime, Clock.systemUTC(), Logger.getAnonymousLogger()).onDamage(event);
|
||||||
|
|
||||||
|
verify(runtime, never()).combatOptOut(org.mockito.ArgumentMatchers.any());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package games.dmg.leaf;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.contains;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
@@ -62,6 +63,28 @@ final class LeafRuntimeTest {
|
|||||||
verify(protection, never()).apply(player, 1);
|
verify(protection, never()).apply(player, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void combatOptOutBypassesLockAndNotifiesExactlyOnce() throws Exception {
|
||||||
|
UUID playerId = UUID.randomUUID();
|
||||||
|
Player player = player(playerId, "Alex");
|
||||||
|
Server server = mock(Server.class);
|
||||||
|
when(server.getPlayer(playerId)).thenReturn(player);
|
||||||
|
LeafRuntime runtime = runtime(
|
||||||
|
server,
|
||||||
|
mock(LeafProtection.class),
|
||||||
|
temporaryDirectory.resolve("combat.yml")
|
||||||
|
);
|
||||||
|
runtime.observe(player, Instant.parse("2026-08-10T00:00:00Z"));
|
||||||
|
runtime.setChoice(playerId, true);
|
||||||
|
runtime.setLocked(playerId, true);
|
||||||
|
|
||||||
|
assertEquals(LeafRuntime.Change.CHANGED, runtime.combatOptOut(playerId));
|
||||||
|
assertEquals(LeafRuntime.Change.UNCHANGED, runtime.combatOptOut(playerId));
|
||||||
|
|
||||||
|
assertFalse(runtime.status(playerId).savedChoice());
|
||||||
|
verify(player).sendMessage(contains("attacked another player"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void optingOutRemovesLeafProtectionAndRepeatingIsANoOp() throws Exception {
|
void optingOutRemovesLeafProtectionAndRepeatingIsANoOp() throws Exception {
|
||||||
UUID playerId = UUID.randomUUID();
|
UUID playerId = UUID.randomUUID();
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package games.dmg.leaf;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.bukkit.OfflinePlayer;
|
||||||
|
import org.bukkit.damage.DamageSource;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.entity.Projectile;
|
||||||
|
import org.bukkit.entity.Tameable;
|
||||||
|
import org.bukkit.event.entity.EntityDamageEvent;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
final class PlayerDamageAttributorTest {
|
||||||
|
@Test
|
||||||
|
void attributesDirectAndThornsDamageToTheCausingPlayer() {
|
||||||
|
Player attacker = player();
|
||||||
|
EntityDamageEvent event = eventWith(attacker, null);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
Optional.of(attacker.getUniqueId()),
|
||||||
|
new PlayerDamageAttributor().responsiblePlayer(event)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void attributesProjectileDamageToItsPlayerShooter() {
|
||||||
|
Player attacker = player();
|
||||||
|
Projectile projectile = mock(Projectile.class);
|
||||||
|
when(projectile.getShooter()).thenReturn(attacker);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
Optional.of(attacker.getUniqueId()),
|
||||||
|
new PlayerDamageAttributor().responsiblePlayer(eventWith(null, projectile))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void attributesTamedAnimalDamageToItsOwner() {
|
||||||
|
OfflinePlayer owner = mock(OfflinePlayer.class);
|
||||||
|
UUID ownerId = UUID.randomUUID();
|
||||||
|
when(owner.getUniqueId()).thenReturn(ownerId);
|
||||||
|
Tameable animal = mock(Tameable.class);
|
||||||
|
when(animal.getOwner()).thenReturn(owner);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
Optional.of(ownerId),
|
||||||
|
new PlayerDamageAttributor().responsiblePlayer(eventWith(animal, animal))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void leavesAmbiguousEnvironmentalDamageUnattributed() {
|
||||||
|
assertTrue(
|
||||||
|
new PlayerDamageAttributor().responsiblePlayer(eventWith(null, null)).isEmpty()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EntityDamageEvent eventWith(
|
||||||
|
org.bukkit.entity.Entity causing,
|
||||||
|
org.bukkit.entity.Entity direct
|
||||||
|
) {
|
||||||
|
DamageSource source = mock(DamageSource.class);
|
||||||
|
when(source.getCausingEntity()).thenReturn(causing);
|
||||||
|
when(source.getDirectEntity()).thenReturn(direct);
|
||||||
|
EntityDamageEvent event = mock(EntityDamageEvent.class);
|
||||||
|
when(event.getDamageSource()).thenReturn(source);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Player player() {
|
||||||
|
Player player = mock(Player.class);
|
||||||
|
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user