diff --git a/src/main/java/net/tfminecraft/magic/artifact/ArtifactCareStore.java b/src/main/java/net/tfminecraft/magic/artifact/ArtifactCareStore.java index 3637924..d8fe2c1 100644 --- a/src/main/java/net/tfminecraft/magic/artifact/ArtifactCareStore.java +++ b/src/main/java/net/tfminecraft/magic/artifact/ArtifactCareStore.java @@ -27,6 +27,20 @@ public static long readLastTickMs(ItemStack stack) { return readLong(stack, ArtifactKeys.careLastTick()); } + /** Sets an artifact's muffle level and resets its care clock. */ + public static boolean setMuffle(ItemStack stack, double muffle, long nowMs) { + if (Artifact.fromItem(stack) == null) { + return false; + } + ItemMeta meta = metaOf(stack); + PersistentDataContainer root = meta.getPersistentDataContainer(); + root.set(ArtifactKeys.careMuffle(), PersistentDataType.DOUBLE, clamp01(muffle)); + root.set(ArtifactKeys.careLastTick(), PersistentDataType.LONG, nowMs); + stack.setItemMeta(meta); + ArtifactLore.apply(stack); + return true; + } + public static Map readUsers(ItemStack stack) { ItemMeta meta = metaOf(stack); if (meta == null) { diff --git a/src/main/java/net/tfminecraft/magic/command/MagicCommand.java b/src/main/java/net/tfminecraft/magic/command/MagicCommand.java index 15fcee6..ff3a3d4 100644 --- a/src/main/java/net/tfminecraft/magic/command/MagicCommand.java +++ b/src/main/java/net/tfminecraft/magic/command/MagicCommand.java @@ -19,9 +19,11 @@ import net.tfminecraft.magic.Cache; import net.tfminecraft.magic.Magic; import net.tfminecraft.magic.Messages; +import net.tfminecraft.magic.artifact.Artifact; +import net.tfminecraft.magic.artifact.ArtifactCareStore; +import net.tfminecraft.magic.artifact.ArtifactIds; import net.tfminecraft.magic.artifact.aura.AuraVessel; import net.tfminecraft.magic.artifact.aura.AuraVessels; -import net.tfminecraft.magic.artifact.ArtifactIds; import net.tfminecraft.magic.artifact.fillchest.ArtifactFillChestService; import net.tfminecraft.magic.artifact.config.ArtifactRarityDef; import net.tfminecraft.magic.artifact.config.ArtifactRarityRegistry; @@ -183,6 +185,12 @@ private static boolean handleArtifact(CommandSender sender, String[] args) { if ("setfill".equalsIgnoreCase(args[1])) { return handleArtifactSetFill(sender, args); } + if ("setmuffle".equalsIgnoreCase(args[1])) { + return handleArtifactSetMuffle(sender, args, false); + } + if ("unmuffle".equalsIgnoreCase(args[1])) { + return handleArtifactSetMuffle(sender, args, true); + } sender.sendMessage(Messages.get("artifact.usage")); return true; } @@ -457,6 +465,64 @@ private static boolean handleArtifactSetFill(CommandSender sender, String[] args return true; } + private static boolean handleArtifactSetMuffle(CommandSender sender, String[] args, boolean clear) { + if (clear ? args.length != 2 : args.length != 3) { + sender.sendMessage(Messages.get(clear + ? "artifact.unmuffle.usage" + : "artifact.setmuffle.usage")); + return true; + } + if (!(sender instanceof Player player)) { + sender.sendMessage(Messages.get(clear + ? "artifact.unmuffle.players_only" + : "artifact.setmuffle.players_only")); + return true; + } + ItemStack item = heldArtifactItem(player); + if (item == null || Artifact.fromItem(item) == null) { + sender.sendMessage(Messages.get(clear + ? "artifact.unmuffle.no_item" + : "artifact.setmuffle.no_item")); + return true; + } + UUID id = ArtifactIds.read(item); + if (id == null) { + sender.sendMessage(Messages.get(clear + ? "artifact.unmuffle.no_id" + : "artifact.setmuffle.no_id")); + return true; + } + double requested; + if (clear) { + requested = 0.0; + } else { + Double percent = parseAmount(args[2]); + if (percent == null || percent < 0.0 || percent > 100.0) { + sender.sendMessage(Messages.get("artifact.setmuffle.invalid_amount")); + return true; + } + requested = percent / 100.0; + } + double before = ArtifactCareStore.readMuffle(item); + if (!ArtifactCareStore.setMuffle(item, requested, System.currentTimeMillis())) { + sender.sendMessage(Messages.get(clear + ? "artifact.unmuffle.no_item" + : "artifact.setmuffle.no_item")); + return true; + } + player.getInventory().setItemInMainHand(item); + AuraLog.append( + "setmuffle player=%s artifact=%s muffle=%s->%s", + player.getName(), + id.toString(), + AuraLog.n(before), + AuraLog.n(requested)); + sender.sendMessage(Messages.get( + clear ? "artifact.unmuffle.ok" : "artifact.setmuffle.ok", + "muffle", MagicNumbers.format(requested * 100.0))); + return true; + } + private static ItemStack heldArtifactItem(Player player) { ItemStack item = player.getInventory().getItemInMainHand(); if (item.getType().isAir()) { @@ -673,7 +739,9 @@ public List onTabComplete(CommandSender sender, Command command, String return Collections.emptyList(); } if (args.length == 2) { - return filterPrefix(List.of("roll", "give", "create", "path", "setfill"), args[1]); + return filterPrefix( + List.of("roll", "give", "create", "path", "setfill", "setmuffle", "unmuffle"), + args[1]); } if ("setfill".equalsIgnoreCase(args[1])) { if (args.length == 3) { diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 4307361..01f2962 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -20,7 +20,7 @@ admin: no_permission: "#ff5555You do not have permission to use this command." - usage: "#aaaaaaUsage: /magic rune keybind #555555| #aaaaaa/magic reload #555555| #aaaaaa/magic open #555555| #aaaaaa/magic resonance #555555| #aaaaaa/magic artifact roll #555555| #aaaaaa/magic artifact give #555555| #aaaaaa/magic artifact create #555555| #aaaaaa/magic artifact setfill #555555| #aaaaaa/magic fillchest #555555| #aaaaaa/magic shrine fill" + usage: "#aaaaaaUsage: /magic rune keybind #555555| #aaaaaa/magic reload #555555| #aaaaaa/magic open #555555| #aaaaaa/magic resonance #555555| #aaaaaa/magic artifact roll #555555| #aaaaaa/magic artifact give #555555| #aaaaaa/magic artifact create #555555| #aaaaaa/magic artifact setfill #555555| #aaaaaa/magic artifact setmuffle #555555| #aaaaaa/magic artifact unmuffle #555555| #aaaaaa/magic fillchest #555555| #aaaaaa/magic shrine fill" rune: @@ -117,7 +117,7 @@ resonance: artifact: - usage: "#aaaaaaUsage: /magic artifact roll #555555| #aaaaaa/magic artifact give #555555| #aaaaaa/magic artifact create #555555| #aaaaaa/magic artifact path #555555| #aaaaaa/magic artifact setfill" + usage: "#aaaaaaUsage: /magic artifact roll #555555| #aaaaaa/magic artifact give #555555| #aaaaaa/magic artifact create #555555| #aaaaaa/magic artifact path #555555| #aaaaaa/magic artifact setfill #555555| #aaaaaa/magic artifact setmuffle #555555| #aaaaaa/magic artifact unmuffle" path: @@ -187,6 +187,32 @@ artifact: ok: "#55ff55Set {element} fill to {fill}." + setmuffle: + + usage: "#aaaaaaUsage: /magic artifact setmuffle " + + players_only: "#ff5555Only players can set artifact muffle." + + no_item: "#ff5555Hold an artifact in your main hand." + + no_id: "#ff5555That item has no artifact id." + + invalid_amount: "#ff5555Muffle must be a number from 0 to 100." + + ok: "#55ff55Set artifact muffle to {muffle}%." + + unmuffle: + + usage: "#aaaaaaUsage: /magic artifact unmuffle" + + players_only: "#ff5555Only players can unmuffle artifacts." + + no_item: "#ff5555Hold an artifact in your main hand." + + no_id: "#ff5555That item has no artifact id." + + ok: "#55ff55Cleared artifact muffle ({muffle}%)." + meditation: no_artifacts: "#aaaaaa§oCan't meditate here, no artifacts" @@ -344,4 +370,3 @@ gear: failed: "#ff5555Could not refresh that weapon. Check console." ok: "#55ff55Weapon refreshed against the live gear config." - diff --git a/src/test/java/net/tfminecraft/magic/CommandTest.java b/src/test/java/net/tfminecraft/magic/CommandTest.java index becbe26..9b32e9b 100644 --- a/src/test/java/net/tfminecraft/magic/CommandTest.java +++ b/src/test/java/net/tfminecraft/magic/CommandTest.java @@ -240,6 +240,53 @@ void artifactFillValidatesIdentityCapAndAmount() { 9, AuraVessels.fromItem(player.getInventory().getItemInMainHand()).getFill("fire")); } + @Test + void artifactMuffleCommandsValidateAndUpdateHeldArtifact() { + expect("artifact.setmuffle.usage", "artifact", "setmuffle"); + expect("artifact.unmuffle.usage", "artifact", "unmuffle", "extra"); + var console = mock(CommandSender.class); + when(console.hasPermission(anyString())).thenReturn(true); + run(console, "artifact", "setmuffle", "25"); + run(console, "artifact", "unmuffle"); + verify(console).sendMessage("artifact.setmuffle.players_only"); + verify(console).sendMessage("artifact.unmuffle.players_only"); + + expect("artifact.setmuffle.no_item", "artifact", "setmuffle", "25"); + expect("artifact.unmuffle.no_item", "artifact", "unmuffle"); + player.getInventory().setItemInMainHand(new ItemStack(Material.STONE)); + expect("artifact.setmuffle.no_item", "artifact", "setmuffle", "25"); + expect("artifact.unmuffle.no_item", "artifact", "unmuffle"); + + var noId = new ItemStack(Material.STONE); + var noIdArtifact = Artifact.create(); + noIdArtifact.setCap("fire", 10); + noIdArtifact.persistPdc(noId); + player.getInventory().setItemInMainHand(noId); + expect("artifact.setmuffle.no_id", "artifact", "setmuffle", "25"); + expect("artifact.unmuffle.no_id", "artifact", "unmuffle"); + + var item = artifact(); + player.getInventory().setItemInMainHand(item); + expect("artifact.setmuffle.invalid_amount", "artifact", "setmuffle", "bad"); + expect("artifact.setmuffle.invalid_amount", "artifact", "setmuffle", "-1"); + expect("artifact.setmuffle.invalid_amount", "artifact", "setmuffle", "101"); + expect("artifact.setmuffle.ok", "artifact", "setmuffle", "25"); + ItemStack held = player.getInventory().getItemInMainHand(); + assertEquals(.25, ArtifactCareStore.readMuffle(held), 1e-10); + assertTrue(held.getItemMeta().getLore().stream().anyMatch(line -> line.contains("Muffled: 25%"))); + + try (var care = mockStatic(ArtifactCareStore.class)) { + expect("artifact.setmuffle.no_item", "artifact", "setmuffle", "50"); + expect("artifact.unmuffle.no_item", "artifact", "unmuffle"); + } + + expect("artifact.unmuffle.ok", "artifact", "unmuffle"); + held = player.getInventory().getItemInMainHand(); + assertEquals(0, ArtifactCareStore.readMuffle(held)); + assertTrue(ArtifactCareStore.readLastTickMs(held) > 0); + assertTrue(held.getItemMeta().getLore().stream().noneMatch(line -> line.contains("Muffled:"))); + } + @Test void nonFiniteAmountsAreRejectedWithoutChangingArtifactOrResonance() { player.getInventory().setItemInMainHand(artifact()); @@ -278,7 +325,7 @@ void chestModesAndTabSuggestionsRespectPermissionsEnabledTypesAndPositions() { assertEquals(List.of("fill"), tab("shrine", "")); assertEquals(2, tab("shrine", "fill", "").size()); assertTrue(tab("shrine", "other", "").isEmpty()); - assertEquals(5, tab("artifact", "").size()); + assertEquals(7, tab("artifact", "").size()); assertEquals(2, tab("artifact", "setfill", "").size()); assertTrue(tab("artifact", "setfill", "fire", "").isEmpty()); assertEquals(List.of("random", "fire"), tab("artifact", "roll", "")); diff --git a/src/test/java/net/tfminecraft/magic/artifact/ArtifactCareTest.java b/src/test/java/net/tfminecraft/magic/artifact/ArtifactCareTest.java index 60ee16c..0dd46fc 100644 --- a/src/test/java/net/tfminecraft/magic/artifact/ArtifactCareTest.java +++ b/src/test/java/net/tfminecraft/magic/artifact/ArtifactCareTest.java @@ -115,6 +115,22 @@ void housedArtifactsRecoverAndUnhousedArtifactsMuffleOnlyAfterFirstTick() { assertEquals(0, ArtifactCareStore.usableMax(10, 2)); } + @Test + void setMuffleClampsTheValueAndResetsTheCareClock() { + var item = artifact(); + try (var lore = mockStatic(ArtifactLore.class)) { + assertTrue(ArtifactCareStore.setMuffle(item, -1, 2000)); + assertEquals(0, ArtifactCareStore.readMuffle(item)); + assertEquals(2000, ArtifactCareStore.readLastTickMs(item)); + lore.verify(() -> ArtifactLore.apply(item)); + + assertTrue(ArtifactCareStore.setMuffle(item, 2, 4000)); + assertEquals(1, ArtifactCareStore.readMuffle(item)); + assertEquals(4000, ArtifactCareStore.readLastTickMs(item)); + assertFalse(ArtifactCareStore.setMuffle(new ItemStack(Material.STONE), .5, 5000)); + } + } + @Test void chargeItemsNeverAcquireArtifactCareState() { assertFalse(ArtifactCareStore.tick(null, false, 1000));