diff --git a/src/main/java/net/tfminecraft/companionpets/listen/PetListener.java b/src/main/java/net/tfminecraft/companionpets/listen/PetListener.java index da7571b..9815358 100644 --- a/src/main/java/net/tfminecraft/companionpets/listen/PetListener.java +++ b/src/main/java/net/tfminecraft/companionpets/listen/PetListener.java @@ -215,6 +215,20 @@ public void onEnvironmentalDamage(EntityDamageEvent event) { && runtime.byEntity(event.getEntity()) != null) event.setCancelled(true); } + /** Owners cannot hurt their own pets: following pets end up inside area skills such as dashes. */ + @EventHandler(priority = EventPriority.LOWEST) + public void onOwnerDamage(EntityDamageByEntityEvent event) { + Pet pet = runtime.byEntity(event.getEntity()); + if (pet != null && pet.ownerId().equals(responsiblePlayer(event))) event.setCancelled(true); + } + + private static UUID responsiblePlayer(EntityDamageByEntityEvent event) { + Entity cause = event.getDamageSource().getCausingEntity(); + if (cause == null) cause = event.getDamager(); + if (cause instanceof Projectile projectile && projectile.getShooter() instanceof Entity shooter) cause = shooter; + return cause instanceof Player player ? player.getUniqueId() : null; + } + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onVisualDamage(EntityDamageEvent event) { if (event.getFinalDamage() <= 0) return; diff --git a/src/test/java/net/tfminecraft/companionpets/listen/PetListenerCoverageTest.java b/src/test/java/net/tfminecraft/companionpets/listen/PetListenerCoverageTest.java index 71bebaa..1361c22 100644 --- a/src/test/java/net/tfminecraft/companionpets/listen/PetListenerCoverageTest.java +++ b/src/test/java/net/tfminecraft/companionpets/listen/PetListenerCoverageTest.java @@ -253,7 +253,7 @@ private EntityDamageByEntityEvent attack(Entity attacker, Entity victim, double @Test void highPriorityDamageCancellationPreservesMoodAndAnimations() { atHigh(EntityDamageByEntityEvent.class, event -> event.setCancelled(true)); - var event = fire(attack(owner, body, 2)); + var event = fire(attack(server.addPlayer(), body, 2)); assertTrue(event.isCancelled()); assertEquals(80, pet.need(Need.MOOD)); assertTrue(animations.isEmpty()); @@ -261,7 +261,7 @@ private EntityDamageByEntityEvent attack(Entity attacker, Entity victim, double } @Test void zeroFinalDamageDoesNotCountAsBeingStruck() { - fire(attack(owner, body, 0)); + fire(attack(server.addPlayer(), body, 0)); assertEquals(80, pet.need(Need.MOOD)); assertTrue(animations.isEmpty()); } @@ -354,6 +354,37 @@ void drowningAndSuffocationAreCancelledBeforeOtherPluginsAndVisualDamage(EntityD assertFalse(ordinary.isCancelled()); assertFalse(observed.getLast()); } + @Test void ownerDamageIsCancelledBeforeOtherPluginsWhetherMeleeProjectileOrSkill() { + var observed = new ArrayList(); + server.getPluginManager().registerEvent(EntityDamageByEntityEvent.class, new Listener() { }, EventPriority.LOW, + (unused, event) -> observed.add(((EntityDamageEvent) event).isCancelled()), runtime.plugin()); + Snowball shot = world.spawn(owner.getEyeLocation(), Snowball.class); + shot.setShooter(owner); + // Skill effects may hit through another entity while naming the caster as the cause. + Snowball effect = world.spawn(owner.getEyeLocation(), Snowball.class); + var skill = DamageSource.builder(DamageType.MAGIC).withCausingEntity(owner).withDirectEntity(effect).build(); + for (var event : List.of(attack(owner, body, 10), attack(shot, body, 10), + new EntityDamageByEntityEvent(effect, body, EntityDamageEvent.DamageCause.MAGIC, skill, 10))) { + assertTrue(fire(event).isCancelled()); + } + assertEquals(List.of(true, true, true), observed); + assertEquals(80, pet.need(Need.MOOD)); + assertTrue(animations.isEmpty()); + assertTrue(cancelledAnimations.isEmpty()); + } + + @Test void ownerProtectionCoversOnlyTheirOwnPets() { + var stranger = server.addPlayer(); + assertFalse(fire(attack(stranger, body, 2)).isCancelled()); + assertEquals(80 - runtime.config().care().struckMoodPenalty(), pet.need(Need.MOOD)); + var otherBody = wolf(owner.getLocation().add(2, 0, 0)); + Pet other = net.tfminecraft.companionpets.testutil.OwnershipFixture.pet(UUID.randomUUID(), stranger.getUniqueId(), "wolf", "Luna", PetSex.FEMALE); + runtime.store().add(other); + runtime.remember(other, otherBody); + assertFalse(fire(attack(owner, otherBody, 2)).isCancelled()); + assertFalse(fire(attack(owner, wolf(owner.getLocation()), 2)).isCancelled()); + } + @Test void toyProjectileDamageIsCancelledBeforeMoodOrVisualEffects() { Snowball toy = world.spawn(owner.getEyeLocation(), Snowball.class); toy.getPersistentDataContainer().set(runtime.toyKey(), PersistentDataType.STRING, UUID.randomUUID().toString());