From 5358b0ee6aa14d7e298a1602027c8f8b3c41418e Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Sat, 10 Oct 2026 18:03:41 +0200 Subject: [PATCH] fix: keep health and mana when the nutrition attribute updates MMOCore's AttributeInstance.setBase strips the attribute's stat buffs and adds them back, and its max-resource listener clamps health and mana to the stripped maximums in between. Every meal reset the nutrition attribute, so a player at full health lost the nutrition share of max health (0.1 per diet point) and of max mana on each bite, even when the diet did not change. Skip the update when the attribute already matches the diet score, and when it does change, give back what the clamp took, up to the new maximums. Co-Authored-By: Claude Opus 5.5 --- .../nutrition/NutritionAttributeBridge.java | 32 +++++++++++++++-- .../NutritionRuntimeCoverageTest.java | 35 +++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/tfminecraft/cooking/nutrition/NutritionAttributeBridge.java b/src/main/java/net/tfminecraft/cooking/nutrition/NutritionAttributeBridge.java index f86ca9c..d1c7f01 100644 --- a/src/main/java/net/tfminecraft/cooking/nutrition/NutritionAttributeBridge.java +++ b/src/main/java/net/tfminecraft/cooking/nutrition/NutritionAttributeBridge.java @@ -1,10 +1,13 @@ package net.tfminecraft.cooking.nutrition; import org.bukkit.Bukkit; +import org.bukkit.attribute.Attribute; import org.bukkit.entity.Player; +import io.lumine.mythic.lib.player.resource.ResourceUpdateReason; import net.Indyuce.mmocore.api.player.PlayerData; import net.Indyuce.mmocore.api.player.attribute.PlayerAttributes.AttributeInstance; +import net.Indyuce.mmocore.api.player.profess.resource.PlayerResource; import net.tfminecraft.rpcharacters.objects.RPCharacter; public final class NutritionAttributeBridge { @@ -20,11 +23,36 @@ public static void apply(Player player, RPCharacter character) { } String attrId = NutritionConfig.attributeName(); - AttributeInstance instance = PlayerData.get(player).getAttributes().getInstance(attrId); + PlayerData data = PlayerData.get(player); + AttributeInstance instance = data.getAttributes().getInstance(attrId); if (instance == null) { return; } - instance.setBase(character.getDietScore()); + int diet = character.getDietScore(); + if (instance.getBase() == diet) { + return; + } + + // setBase strips the attribute's stat buffs before adding them back, and MMOCore clamps + // health, mana, stamina and stellium to the stripped maximums in between. Put back what + // that clamp took, up to the new maximums. + PlayerResource[] resources = {PlayerResource.MANA, PlayerResource.STAMINA, PlayerResource.STELLIUM}; + double health = player.getHealth(); + double[] before = new double[resources.length]; + for (int i = 0; i < resources.length; i++) { + before[i] = resources[i].getCurrent(data); + } + instance.setBase(diet); + double healthCap = Math.min(health, player.getAttribute(Attribute.MAX_HEALTH).getValue()); + if (player.getHealth() < healthCap) { + player.setHealth(healthCap); + } + for (int i = 0; i < resources.length; i++) { + double cap = Math.min(before[i], resources[i].getMax(data)); + if (resources[i].getCurrent(data) < cap) { + resources[i].setCurrent(data, cap, ResourceUpdateReason.CLAMPING); + } + } } } diff --git a/src/test/java/net/tfminecraft/cooking/nutrition/NutritionRuntimeCoverageTest.java b/src/test/java/net/tfminecraft/cooking/nutrition/NutritionRuntimeCoverageTest.java index 0e173a0..4626c9c 100644 --- a/src/test/java/net/tfminecraft/cooking/nutrition/NutritionRuntimeCoverageTest.java +++ b/src/test/java/net/tfminecraft/cooking/nutrition/NutritionRuntimeCoverageTest.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; +import io.lumine.mythic.lib.player.resource.ResourceUpdateReason; import net.Indyuce.mmocore.api.player.PlayerData; import net.Indyuce.mmocore.api.player.attribute.PlayerAttributes.AttributeInstance; import net.tfminecraft.cooking.Cooking; @@ -19,6 +20,7 @@ import net.tfminecraft.rpcharacters.RPCharacters; import net.tfminecraft.rpcharacters.managers.PlayerManager; import net.tfminecraft.rpcharacters.objects.RPCharacter; +import org.bukkit.attribute.Attribute; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.Plugin; import org.junit.jupiter.api.AfterEach; @@ -203,6 +205,39 @@ class NutritionRuntimeCoverageTest { when(pd.getAttributes().getInstance(key)).thenReturn(attribute); character.setDietScore(12); NutritionAttributeBridge.apply(player,character); verify(attribute).setBase(12); + when(attribute.getBase()).thenReturn(12); + NutritionAttributeBridge.apply(player,character); + verify(attribute).setBase(12); + } + } + + @Test void attributeBridgeGivesBackHealthAndManaClampedWhileTheBuffsAreReplaced() { + MockBukkit.createMockPlugin("MMOCore"); + try (MockedStatic data = mockStatic(PlayerData.class)) { + PlayerData pd = mock(PlayerData.class,RETURNS_DEEP_STUBS); + data.when(() -> PlayerData.get(player)).thenReturn(pd); + AttributeInstance attribute = mock(AttributeInstance.class); + when(pd.getAttributes().getInstance(NutritionConfig.attributeName())).thenReturn(attribute); + when(pd.getStats().getStat("MAX_MANA")).thenReturn(30.0); + when(pd.getStats().getStat("MAX_STAMINA")).thenReturn(10.0); + when(pd.getStats().getStat("MAX_STELLIUM")).thenReturn(40.0); + double[] mana = {30}; + when(pd.getMana()).thenAnswer(call -> mana[0]); + when(pd.getStamina()).thenReturn(5.0); + when(pd.getStellium()).thenReturn(50.0, 40.0); + player.setHealth(20); + doAnswer(call -> { player.setHealth(16); mana[0] = 21; return null; }).when(attribute).setBase(36); + character.setDietScore(36); NutritionAttributeBridge.apply(player,character); + assertEquals(20, player.getHealth()); + verify(pd).setMana(30.0, ResourceUpdateReason.CLAMPING); + verify(pd,never()).setStamina(anyDouble(),any(ResourceUpdateReason.class)); + verify(pd,never()).setStellium(anyDouble(),any(ResourceUpdateReason.class)); + + doAnswer(call -> { + player.getAttribute(Attribute.MAX_HEALTH).setBaseValue(18); player.setHealth(14); return null; + }).when(attribute).setBase(10); + character.setDietScore(10); NutritionAttributeBridge.apply(player,character); + assertEquals(18, player.getHealth()); } }