From 9dfa161e6fcdf1396708154c971da9fc1cc7b1af Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Wed, 30 Sep 2026 21:46:21 +0200 Subject: [PATCH] fix: leave MMOCore class skill casts alone Subclass spells are cast from the MMOCore skill bar, not from runes. Skip them in the resonance gate (refusal, overload and rift whiffs, weapon wear) and in cast drift, so class casts behave the same with or without Magic. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../magic/integration/SkillIdResolver.java | 8 ++ .../magic/listener/CastDriftListener.java | 2 +- .../magic/listener/ResonanceCastListener.java | 2 +- .../tfminecraft/magic/ClassCastSkipTest.java | 83 +++++++++++++++++++ 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 src/test/java/net/tfminecraft/magic/ClassCastSkipTest.java diff --git a/src/main/java/net/tfminecraft/magic/integration/SkillIdResolver.java b/src/main/java/net/tfminecraft/magic/integration/SkillIdResolver.java index 50c9e9e..03ed7b8 100644 --- a/src/main/java/net/tfminecraft/magic/integration/SkillIdResolver.java +++ b/src/main/java/net/tfminecraft/magic/integration/SkillIdResolver.java @@ -37,6 +37,14 @@ public static boolean isActiveCast(Skill cast) { return lower != null && Cache.castTriggers.contains(lower.toUpperCase(Locale.ROOT)); } + /** + * True for a skill cast from an MMOCore class skill bar. Class skills are not runes: + * Magic leaves them alone (no refusal, whiff, weapon wear or cast drift). + */ + public static boolean isClassCast(Skill cast) { + return cast instanceof CastableSkill; + } + /** * Spells come from MMOItems key combinations, whose {@code AbilityData} is a plain * MythicLib {@link Skill} and never an MMOCore {@link CastableSkill}. The handler id diff --git a/src/main/java/net/tfminecraft/magic/listener/CastDriftListener.java b/src/main/java/net/tfminecraft/magic/listener/CastDriftListener.java index c55eb37..6d2bf83 100644 --- a/src/main/java/net/tfminecraft/magic/listener/CastDriftListener.java +++ b/src/main/java/net/tfminecraft/magic/listener/CastDriftListener.java @@ -23,7 +23,7 @@ public void onSkillCast(SkillCastEvent event) { return; } Skill cast = event.getCast(); - if (!SkillIdResolver.isActiveCast(cast)) { + if (!SkillIdResolver.isActiveCast(cast) || SkillIdResolver.isClassCast(cast)) { return; } Player player = event.getPlayer(); diff --git a/src/main/java/net/tfminecraft/magic/listener/ResonanceCastListener.java b/src/main/java/net/tfminecraft/magic/listener/ResonanceCastListener.java index 576c04e..b9f2717 100644 --- a/src/main/java/net/tfminecraft/magic/listener/ResonanceCastListener.java +++ b/src/main/java/net/tfminecraft/magic/listener/ResonanceCastListener.java @@ -65,7 +65,7 @@ public void onPlayerCastSkill(PlayerCastSkillEvent event) { return; } Skill cast = event.getCast(); - if (!SkillIdResolver.isActiveCast(cast)) { + if (!SkillIdResolver.isActiveCast(cast) || SkillIdResolver.isClassCast(cast)) { return; } String skillId = SkillIdResolver.resolveSkillId(cast); diff --git a/src/test/java/net/tfminecraft/magic/ClassCastSkipTest.java b/src/test/java/net/tfminecraft/magic/ClassCastSkipTest.java new file mode 100644 index 0000000..72ee483 --- /dev/null +++ b/src/test/java/net/tfminecraft/magic/ClassCastSkipTest.java @@ -0,0 +1,83 @@ +package net.tfminecraft.magic; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import io.lumine.mythic.lib.api.event.skill.PlayerCastSkillEvent; +import io.lumine.mythic.lib.api.event.skill.SkillCastEvent; +import io.lumine.mythic.lib.skill.Skill; +import io.lumine.mythic.lib.skill.trigger.TriggerType; +import net.Indyuce.mmocore.skill.CastableSkill; +import net.tfminecraft.magic.gear.GearHand; +import net.tfminecraft.magic.integration.SkillIdResolver; +import net.tfminecraft.magic.listener.CastDriftListener; +import net.tfminecraft.magic.listener.ResonanceCastListener; +import net.tfminecraft.magic.registry.SkillElementRegistry; +import net.tfminecraft.magic.session.ResonanceSessionManager; +import org.junit.jupiter.api.*; +import org.mockbukkit.mockbukkit.*; + +/** MMOCore class skills are cast from the skill bar, not from runes: Magic must not touch them. */ +class ClassCastSkipTest { + ServerMock server; + + @BeforeEach + void setup() { + server = MockBukkit.mock(); + Magic.plugin = mock(Magic.class, RETURNS_DEEP_STUBS); + SkillElementRegistry.clear(); + } + + @AfterEach + void cleanup() { + Magic.plugin = null; + SkillElementRegistry.clear(); + MockBukkit.unmock(); + } + + @Test + void onlyMmocoreCastableSkillsCountAsClassCasts() { + assertTrue(SkillIdResolver.isClassCast(mock(CastableSkill.class))); + assertFalse(SkillIdResolver.isClassCast(mock(Skill.class))); + assertFalse(SkillIdResolver.isClassCast(null)); + } + + @Test + void resonanceGateIgnoresClassCastsEvenForBoundSpells() { + var listener = new ResonanceCastListener(); + var event = mock(PlayerCastSkillEvent.class); + var cast = mock(CastableSkill.class); + when(event.getCast()).thenReturn(cast); + when(event.getPlayer()).thenReturn(server.addPlayer()); + SkillElementRegistry.register("fire_shard", "oseni", 1); + try (var ids = mockStatic(SkillIdResolver.class); + var hand = mockStatic(GearHand.class)) { + ids.when(() -> SkillIdResolver.isActiveCast(cast)).thenReturn(true); + ids.when(() -> SkillIdResolver.isClassCast(cast)).thenReturn(true); + ids.when(() -> SkillIdResolver.resolveSkillId(cast)).thenReturn("fire_shard"); + listener.onPlayerCastSkill(event); + ids.verify(() -> SkillIdResolver.resolveSkillId(cast), never()); + hand.verifyNoInteractions(); + } + verify(event, never()).setCancelled(anyBoolean()); + verify(cast, never()).whenCast(any()); + } + + @Test + void castDriftIgnoresClassCasts() { + var listener = new CastDriftListener(); + var event = mock(SkillCastEvent.class); + var cast = mock(CastableSkill.class); + when(cast.getTrigger()).thenReturn(TriggerType.CAST); + when(cast.getParameter("mana")).thenReturn(40.); + when(event.getCast()).thenReturn(cast); + var player = server.addPlayer(); + when(event.getPlayer()).thenReturn(player); + var sessions = new ResonanceSessionManager(); + when(Magic.plugin.getResonanceGuiManager().getSessionManager()).thenReturn(sessions); + var session = sessions.getOrCreate(player); + listener.onSkillCast(event); + assertEquals(0., session.getEquilibrium()); + verify(Magic.plugin, never()).syncSpellModifiers(player, session); + } +}