diff --git a/src/main/java/net/tfminecraft/companionpets/care/CareSettings.java b/src/main/java/net/tfminecraft/companionpets/care/CareSettings.java index 332837d..2862b38 100644 --- a/src/main/java/net/tfminecraft/companionpets/care/CareSettings.java +++ b/src/main/java/net/tfminecraft/companionpets/care/CareSettings.java @@ -10,6 +10,7 @@ public record CareSettings( double minutesUntilSick, boolean decayWhileStored, boolean deathOnNeglect, + boolean invulnerable, double playSeconds, double playMoodGain, double playEnergyCost, @@ -40,6 +41,7 @@ public static CareSettings defaults() { 3, false, false, + false, 6, 22, 3, diff --git a/src/main/java/net/tfminecraft/companionpets/config/CompanionConfig.java b/src/main/java/net/tfminecraft/companionpets/config/CompanionConfig.java index 6277ad8..30e1b3c 100644 --- a/src/main/java/net/tfminecraft/companionpets/config/CompanionConfig.java +++ b/src/main/java/net/tfminecraft/companionpets/config/CompanionConfig.java @@ -128,6 +128,7 @@ public static CompanionConfig load(JavaPlugin plugin, FileConfiguration config) num(logger, care, "minutes-until-sick", defaults.minutesUntilSick()), care != null && care.contains("decay-while-stored") ? care.getBoolean("decay-while-stored") : defaults.decayWhileStored(), care != null && care.contains("death-on-neglect") ? care.getBoolean("death-on-neglect") : defaults.deathOnNeglect(), + care != null && care.contains("invulnerable") ? care.getBoolean("invulnerable") : defaults.invulnerable(), num(logger, care, "play-seconds", defaults.playSeconds()), num(logger, care, "play-mood-gain", defaults.playMoodGain()), num(logger, care, "play-energy-cost", defaults.playEnergyCost()), diff --git a/src/main/java/net/tfminecraft/companionpets/listen/PetListener.java b/src/main/java/net/tfminecraft/companionpets/listen/PetListener.java index 9815358..50d43f6 100644 --- a/src/main/java/net/tfminecraft/companionpets/listen/PetListener.java +++ b/src/main/java/net/tfminecraft/companionpets/listen/PetListener.java @@ -215,11 +215,18 @@ 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. */ + /** + * Owners cannot hurt their own pets: following pets end up inside area skills such as dashes. + * With care.invulnerable nothing can, since a pet's death deletes it. + */ @EventHandler(priority = EventPriority.LOWEST) - public void onOwnerDamage(EntityDamageByEntityEvent event) { + public void onProtectedDamage(EntityDamageEvent event) { Pet pet = runtime.byEntity(event.getEntity()); - if (pet != null && pet.ownerId().equals(responsiblePlayer(event))) event.setCancelled(true); + if (pet == null) return; + if (runtime.config().care().invulnerable() + || event instanceof EntityDamageByEntityEvent attack && pet.ownerId().equals(responsiblePlayer(attack))) { + event.setCancelled(true); + } } private static UUID responsiblePlayer(EntityDamageByEntityEvent event) { diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index a6f374a..099c500 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -258,6 +258,7 @@ care: minutes-until-sick: 60 # Additional minutes with critical needs after the unwell threshold before sickness. decay-while-stored: false # true advances care in the Pet House while the owner is online. death-on-neglect: false # true kills at zero health from illness; false leaves the pet weakened. + invulnerable: false # true cancels all damage to pets; false only blocks damage from their owner. play-seconds: 6 # Loaded duration in seconds; currently unused by runtime play. play-mood-gain: 22 # Mood points gained on play completion or when comforting an ill pet. play-energy-cost: 1 # Energy points spent on play completion. diff --git a/src/test/java/net/tfminecraft/companionpets/care/NeedClockTest.java b/src/test/java/net/tfminecraft/companionpets/care/NeedClockTest.java index 901be6e..1bbaabf 100644 --- a/src/test/java/net/tfminecraft/companionpets/care/NeedClockTest.java +++ b/src/test/java/net/tfminecraft/companionpets/care/NeedClockTest.java @@ -154,6 +154,7 @@ void neglectCanWeakenOrKill() { care.minutesUntilSick(), care.decayWhileStored(), true, + care.invulnerable(), care.playSeconds(), care.playMoodGain(), care.playEnergyCost(), diff --git a/src/test/java/net/tfminecraft/companionpets/listen/PetListenerCoverageTest.java b/src/test/java/net/tfminecraft/companionpets/listen/PetListenerCoverageTest.java index 1361c22..f36c603 100644 --- a/src/test/java/net/tfminecraft/companionpets/listen/PetListenerCoverageTest.java +++ b/src/test/java/net/tfminecraft/companionpets/listen/PetListenerCoverageTest.java @@ -385,6 +385,36 @@ void drowningAndSuffocationAreCancelledBeforeOtherPluginsAndVisualDamage(EntityD assertFalse(fire(attack(owner, wolf(owner.getLocation()), 2)).isCancelled()); } + @Test void invulnerablePetsTakeNoDamageFromAnySourceBeforeOtherPlugins() throws Exception { + var yaml = new YamlConfiguration(); + yaml.loadFromString(""" + items: {kennel: BARREL, kennel-block: BARREL, toys: [STICK], treats: [COD]} + care: {invulnerable: true} + pets: + wolf: {entity: WOLF, egg: WOLF_SPAWN_EGG, default-tricks: []} + """); + runtime.config(CompanionConfig.load(runtime.plugin(), yaml)); + var observed = new ArrayList(); + server.getPluginManager().registerEvent(EntityDamageEvent.class, new Listener() { }, EventPriority.LOW, + (unused, event) -> observed.add(((EntityDamageEvent) event).isCancelled()), runtime.plugin()); + var stranger = server.addPlayer(); + Snowball shot = world.spawn(stranger.getEyeLocation(), Snowball.class); + shot.setShooter(stranger); + List events = new ArrayList<>(List.of(attack(stranger, body, 1_000), attack(shot, body, 1_000), + attack(wolf(owner.getLocation()), body, 1_000))); + for (var cause : List.of(EntityDamageEvent.DamageCause.FALL, EntityDamageEvent.DamageCause.LAVA, + EntityDamageEvent.DamageCause.FIRE_TICK, EntityDamageEvent.DamageCause.VOID, EntityDamageEvent.DamageCause.KILL)) { + events.add(new EntityDamageEvent(body, cause, 1_000)); + } + for (var event : events) assertTrue(fire(event).isCancelled(), event.getCause().name()); + assertTrue(observed.stream().allMatch(Boolean::booleanValue)); + assertEquals(events.size(), observed.size()); + assertEquals(80, pet.need(Need.MOOD)); + assertTrue(animations.isEmpty()); + assertFalse(fire(attack(stranger, wolf(owner.getLocation()), 2)).isCancelled()); + assertFalse(fire(new EntityDamageEvent(wolf(owner.getLocation()), EntityDamageEvent.DamageCause.FALL, 2)).isCancelled()); + } + @Test void toyProjectileDamageIsCancelledBeforeMoodOrVisualEffects() { Snowball toy = world.spawn(owner.getEyeLocation(), Snowball.class); toy.getPersistentDataContainer().set(runtime.toyKey(), PersistentDataType.STRING, UUID.randomUUID().toString());