diff --git a/src/main/java/net/asodev/islandutils/mixins/ItemBarMixin.java b/src/main/java/net/asodev/islandutils/mixins/ItemBarMixin.java new file mode 100644 index 00000000..534ff5b9 --- /dev/null +++ b/src/main/java/net/asodev/islandutils/mixins/ItemBarMixin.java @@ -0,0 +1,175 @@ +package net.asodev.islandutils.mixins; + +import net.asodev.islandutils.modules.cosmetics.CosmeticChroma; +import net.asodev.islandutils.options.IslandOptions; +import net.asodev.islandutils.util.BarInfo; +import net.asodev.islandutils.util.Utils; +import net.minecraft.network.chat.Component; +import net.minecraft.util.ARGB; +import net.minecraft.world.item.ItemStack; +import org.apache.commons.lang3.math.Fraction; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.ArrayList; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Mixin(ItemStack.class) +public class ItemBarMixin { + @Unique + private static final Pattern PROGRESS_REGEX = Pattern.compile(".*\\n[\\uE001\\uE269\\uE26C\\uE266]* (\\d+)%.*"); + @Unique + private static final String PROGRESS_COSMETIC_LABEL = "Left-Click to Equip\n"; + @Unique + private static final String PROGRESS_COSMETIC_LABEL_2 = "Left-Click to Unequip\n"; + @Unique + private static final String PROGRESS_BADGE_PINNED_LABEL = "Shift-Click to Unpin"; + @Unique + private static final String PROGRESS_BADGE_LOCKED_LABEL = "\nUnlock this slot by reaching the below\namount of Skill Trophies.\n"; + @Unique + private static final String PROGRESS_QUEST_LABEL = "\nComplete any of the below tasks to\nfinish the quest.\n"; + @Unique + private static final String PROGRESS_TOOL_USES_LABEL = "\nUses Remaining: "; + @Unique + private static final String PROGRESS_TOOL_REPAIRABLE_LABEL = "\nThis item is out of uses! You can Repair\nit to restore all its uses, you can only\ndo this once per item.\n"; + @Unique + private static final String PROGRESS_TOOL_BROKEN_LABEL = "\nThis item is out of uses! You've already\nrepaired it once, so cannot do so\nagain."; + + @Unique + private static final Pattern REPUTATION_REGEX = Pattern.compile(".*\\nRoyal Donations: (\\d+)/(\\d+)\\n.*"); + + @Unique + private static final String CHROMA_LABEL = "\nChromas Unlocked:\n"; + @Unique + private static final Pattern CHROMA_REGEX = Pattern.compile(".*\\n([\\uE02A\\uE02E\\uE02F\\uE02C\\uE02D\\uE02B]{5}) - (\\d+)\\uE328\\n.*"); + + @Unique + private static final int PROGRESS_BAR_COLOR = ARGB.colorFromFloat(1.0F, 0.33F, 1.0F, 0.33F); + @Unique + private static final int REPAIRABLE_BAR_COLOR = ARGB.colorFromFloat(1.0F, 1.0F, 0.33F, 0.33F); + + @Unique + private static final int REPUTATION_BAR_COLOR = ARGB.colorFromFloat(1.0F, 0.66F, 0.33F, 1.0F); + + @Unique + private static final int CHROMA_BAR_COLOR_SPEED_MS = 2000; + + @Unique + private Optional getProgressBar(String lore) { + if (!IslandOptions.getMisc().isShowProgressBar()) return Optional.empty(); + + var isCosmetic = lore.contains(PROGRESS_COSMETIC_LABEL) || lore.contains(PROGRESS_COSMETIC_LABEL_2); + var isProfileBadge = lore.contains(PROGRESS_BADGE_PINNED_LABEL) || lore.contains(PROGRESS_BADGE_LOCKED_LABEL); + if (isCosmetic || isProfileBadge) return Optional.empty(); + + var isBrokenTool = lore.contains(PROGRESS_TOOL_BROKEN_LABEL); + if (isBrokenTool) return Optional.of(new BarInfo(Fraction.ZERO, PROGRESS_BAR_COLOR)); + + var isRepairableTool = lore.contains(PROGRESS_TOOL_REPAIRABLE_LABEL); + if (isRepairableTool) return Optional.of(new BarInfo(Fraction.getFraction(1, 100), REPAIRABLE_BAR_COLOR)); + + var matcher = PROGRESS_REGEX.matcher(lore); + var progresses = new ArrayList(); + while (matcher.find()) { + var percentage = Integer.parseInt(matcher.group(1)); + var fraction = Fraction.getFraction(percentage, 100); + progresses.add(fraction); + } + if (progresses.isEmpty()) return Optional.empty(); + + var isQuest = lore.contains(PROGRESS_QUEST_LABEL); + var fraction = isQuest ? progresses.stream().max(Fraction::compareTo) : progresses.stream().findFirst(); + + var isNonBrokenTool = lore.contains(PROGRESS_TOOL_USES_LABEL); + + return fraction.map(f -> new BarInfo(f, PROGRESS_BAR_COLOR)); + } + + @Unique + private Optional getReputationBar(String lore) { + if (!IslandOptions.getCosmetics().isShowReputationBar()) return Optional.empty(); + + var matcher = REPUTATION_REGEX.matcher(lore); + if (!matcher.find()) return Optional.empty(); + + var level = Integer.parseInt(matcher.group(1)); + var max = Integer.parseInt(matcher.group(2)); + + try { + var fraction = Fraction.getFraction(level, max); + return Optional.of(new BarInfo(fraction, REPUTATION_BAR_COLOR)); + } catch (ArithmeticException e) { + return Optional.empty(); + } + } + + @Unique + private Optional getChromaBar(String lore) { + if (!IslandOptions.getCosmetics().isShowChromaBar()) return Optional.empty(); + + if (!lore.contains(CHROMA_LABEL)) return Optional.empty(); + + var matcher = CHROMA_REGEX.matcher(lore); + if (!matcher.find()) return Optional.empty(); + var chromaChars = Optional.of(matcher.group(1).chars().mapToObj(c -> (char) c).toList()); + + return chromaChars.map(charList -> + charList.stream() + .map(CosmeticChroma::fromChar) + .filter(Optional::isPresent) + .map(Optional::get) + .toList() + ).map(chromas -> { + var fraction = CosmeticChroma.toFraction(chromas); + + var color = 0; + if (!chromas.isEmpty()) { + var currentChromaIdx = (int) ((System.currentTimeMillis() / CHROMA_BAR_COLOR_SPEED_MS) % chromas.size()); + var nextChromaIdx = (currentChromaIdx + 1) % chromas.size(); + + var currentChromaColor = chromas.get(currentChromaIdx).toColor(); + var nextChromaColor = chromas.get(nextChromaIdx).toColor(); + + var interpolatedProgress = (float) (System.currentTimeMillis() % CHROMA_BAR_COLOR_SPEED_MS) / CHROMA_BAR_COLOR_SPEED_MS; + color = ARGB.lerp(interpolatedProgress, currentChromaColor, nextChromaColor); + } + + return new BarInfo(fraction, color); + }); + } + + @Unique + private Optional getItemBar() { + var itemStack = (ItemStack)(Object)this; + var lore = Utils.getLores(itemStack); + if (lore == null) return Optional.empty(); + + var loreStr = lore.stream().map(Component::getString).collect(Collectors.joining("\n")); + + return this.getChromaBar(loreStr) + .or(() -> this.getReputationBar(loreStr)) + .or(() -> this.getProgressBar(loreStr)); + } + + @Inject(method = "isBarVisible", at = @At("RETURN"), cancellable = true) + private void injectBarVisibility(CallbackInfoReturnable cir) { + if (this.getItemBar().isPresent()) { + cir.setReturnValue(true); + } + } + + @Inject(method = "getBarWidth", at = @At("RETURN"), cancellable = true) + private void injectBarWidth(CallbackInfoReturnable cir) { + this.getItemBar().ifPresent(barInfo -> cir.setReturnValue(barInfo.getBarWidth())); + } + + @Inject(method = "getBarColor", at = @At("RETURN"), cancellable = true) + private void injectBarColor(CallbackInfoReturnable cir) { + this.getItemBar().ifPresent(barInfo -> cir.setReturnValue(barInfo.color())); + } +} diff --git a/src/main/java/net/asodev/islandutils/modules/cosmetics/CosmeticChroma.java b/src/main/java/net/asodev/islandutils/modules/cosmetics/CosmeticChroma.java new file mode 100644 index 00000000..9a8887b0 --- /dev/null +++ b/src/main/java/net/asodev/islandutils/modules/cosmetics/CosmeticChroma.java @@ -0,0 +1,42 @@ +package net.asodev.islandutils.modules.cosmetics; + +import net.minecraft.util.ARGB; +import org.apache.commons.lang3.math.Fraction; + +import java.util.List; +import java.util.Optional; + +public enum CosmeticChroma { + MECHANICAL, + NATURAL, + OCEANIC, + MAGICAL, + GRAYSCALE; + + public static final int COUNT = 5; + + public static Optional fromChar(char c) { + return Optional.ofNullable(switch (c) { + case '\uE02E' -> MECHANICAL; + case '\uE02F' -> NATURAL; + case '\uE02C' -> OCEANIC; + case '\uE02D' -> MAGICAL; + case '\uE02B' -> GRAYSCALE; + default -> null; + }); + } + + public int toColor() { + return switch (this) { + case MECHANICAL -> ARGB.colorFromFloat(1.0f, 1.0f, 0.33f, 0.0f); + case NATURAL -> ARGB.colorFromFloat(1.0f, 0.0f, 0.66f, 0.33f); + case OCEANIC -> ARGB.colorFromFloat(1.0f, 0.0f, 0.5f, 1.0f); + case MAGICAL -> ARGB.colorFromFloat(1.0f, 0.5f, 0.0f, 1.0f); + case GRAYSCALE -> ARGB.colorFromFloat(1.0f, 0.5f, 0.5f, 0.5f); + }; + } + + public static Fraction toFraction(List chromas) { + return Fraction.getFraction(chromas.size(), COUNT); + } +} diff --git a/src/main/java/net/asodev/islandutils/options/categories/CosmeticsOptions.java b/src/main/java/net/asodev/islandutils/options/categories/CosmeticsOptions.java index bf9747aa..21dbfd15 100644 --- a/src/main/java/net/asodev/islandutils/options/categories/CosmeticsOptions.java +++ b/src/main/java/net/asodev/islandutils/options/categories/CosmeticsOptions.java @@ -13,6 +13,8 @@ public class CosmeticsOptions implements OptionsCategory { boolean showPlayerPreview = true; boolean showOnHover = true; boolean showOnOnlyCosmeticMenus = true; + boolean showReputationBar = true; + boolean showChromaBar = true; public boolean isShowPlayerPreview() { return showPlayerPreview; @@ -26,6 +28,14 @@ public boolean isShowOnOnlyCosmeticMenus() { return showOnOnlyCosmeticMenus; } + public boolean isShowReputationBar() { + return showReputationBar; + } + + public boolean isShowChromaBar() { + return showChromaBar; + } + @Override public ConfigCategory getCategory() { Option showPreviewOption = Option.createBuilder() @@ -38,16 +48,28 @@ public ConfigCategory getCategory() { .controller(TickBoxControllerBuilder::create) .binding(defaults.showOnHover, () -> showOnHover, value -> this.showOnHover = value) .build(); - Option showInOnlyCosmeticMenu = Option.createBuilder() + Option showOnlyCosmeticMenusOption = Option.createBuilder() .name(Component.translatable("text.autoconfig.islandutils.option.showOnOnlyCosmeticMenus")) .controller(TickBoxControllerBuilder::create) .binding(defaults.showOnOnlyCosmeticMenus, () -> showOnOnlyCosmeticMenus, value -> this.showOnOnlyCosmeticMenus = value) .build(); + Option showReputationBarOption = Option.createBuilder() + .name(Component.translatable("text.autoconfig.islandutils.option.showReputationBar")) + .controller(TickBoxControllerBuilder::create) + .binding(defaults.showReputationBar, () -> showReputationBar, value -> this.showReputationBar = value) + .build(); + Option showChromaBarOption = Option.createBuilder() + .name(Component.translatable("text.autoconfig.islandutils.option.showChromaBar")) + .controller(TickBoxControllerBuilder::create) + .binding(defaults.showChromaBar, () -> showChromaBar, value -> this.showChromaBar = value) + .build(); return ConfigCategory.createBuilder() .name(Component.literal("Cosmetics")) .option(showPreviewOption) .option(showHoverOption) - .option(showInOnlyCosmeticMenu) + .option(showOnlyCosmeticMenusOption) + .option(showReputationBarOption) + .option(showChromaBarOption) .build(); } } diff --git a/src/main/java/net/asodev/islandutils/options/categories/MiscOptions.java b/src/main/java/net/asodev/islandutils/options/categories/MiscOptions.java index d3f698b9..9132ea79 100644 --- a/src/main/java/net/asodev/islandutils/options/categories/MiscOptions.java +++ b/src/main/java/net/asodev/islandutils/options/categories/MiscOptions.java @@ -19,6 +19,7 @@ public class MiscOptions implements OptionsCategory { boolean silverPreview = true; boolean channelSwitchers = true; boolean showFishingUpgradeIcon = true; + boolean showProgressBar = true; boolean enableConfigButton = true; boolean debugMode = false; @@ -44,6 +45,9 @@ public boolean showChannelSwitchers() { public boolean isShowFishingUpgradeIcon() { return showFishingUpgradeIcon; } + public boolean isShowProgressBar() { + return showProgressBar; + } public boolean isDebugMode() { return debugMode && !Utils.isLunarClient(); @@ -83,7 +87,6 @@ public ConfigCategory getCategory() { .build(); Option fishingUpgradeOption = Option.createBuilder() .name(Component.translatable("text.autoconfig.islandutils.option.showFishingUpgradeIcon")) - .description(OptionDescription.of(Component.translatable("text.autoconfig.islandutils.option.showFishingUpgradeIcon.@Tooltip"))) .controller(TickBoxControllerBuilder::create) .binding(defaults.showFishingUpgradeIcon, () -> showFishingUpgradeIcon, value -> this.showFishingUpgradeIcon = value) .build(); @@ -93,6 +96,12 @@ public ConfigCategory getCategory() { .controller(TickBoxControllerBuilder::create) .binding(defaults.enableConfigButton, () -> enableConfigButton, value -> this.enableConfigButton = value) .build(); + Option progressBarOption = Option.createBuilder() + .name(Component.translatable("text.autoconfig.islandutils.option.showProgressBar")) + .description(OptionDescription.of(Component.translatable("text.autoconfig.islandutils.option.showProgressBar.@Tooltip"))) + .controller(TickBoxControllerBuilder::create) + .binding(defaults.showProgressBar, () -> showProgressBar, value -> this.showProgressBar = value) + .build(); Option debugOption = Option.createBuilder() .name(Component.translatable("text.autoconfig.islandutils.option.debugMode")) .description(OptionDescription.of(Component.translatable("text.autoconfig.islandutils.option.debugMode.@Tooltip"))) @@ -114,6 +123,7 @@ public ConfigCategory getCategory() { .option(channelsOption) .option(fishingUpgradeOption) .option(buttonOption) + .option(progressBarOption) .group(OptionGroup.createBuilder() .name(Component.literal("Debug Options")) .collapsed(true) diff --git a/src/main/java/net/asodev/islandutils/util/BarInfo.java b/src/main/java/net/asodev/islandutils/util/BarInfo.java new file mode 100644 index 00000000..5f5d41a6 --- /dev/null +++ b/src/main/java/net/asodev/islandutils/util/BarInfo.java @@ -0,0 +1,14 @@ +package net.asodev.islandutils.util; + +import net.minecraft.util.Mth; +import org.apache.commons.lang3.math.Fraction; + +public record BarInfo(Fraction fraction, int color) { + public int getBarWidth() { + if (fraction.compareTo(Fraction.ZERO) > 0) { + return Math.min(1 + Mth.mulAndTruncate(fraction, 12), 13); + } else { + return Math.min(Mth.mulAndTruncate(fraction, 13), 13); + } + } +} diff --git a/src/main/resources/assets/island/lang/en_us.json b/src/main/resources/assets/island/lang/en_us.json index 3314a5dc..5e162bd9 100644 --- a/src/main/resources/assets/island/lang/en_us.json +++ b/src/main/resources/assets/island/lang/en_us.json @@ -23,6 +23,8 @@ "text.autoconfig.islandutils.option.showPlayerPreview": "Show Cosmetic Previews", "text.autoconfig.islandutils.option.showOnHover": "Preview cosmetics on hover", "text.autoconfig.islandutils.option.showOnOnlyCosmeticMenus": "Show cosmetic preview only in Cosmetic Menus", + "text.autoconfig.islandutils.option.showReputationBar": "Show reputation bar", + "text.autoconfig.islandutils.option.showChromaBar": "Show chroma bar", "text.autoconfig.islandutils.category.discord": "Discord Presence", "text.autoconfig.islandutils.option.discordPresence": "Enable Discord Presence", @@ -74,6 +76,8 @@ "text.autoconfig.islandutils.option.silverPreview.@Tooltip": "Shows all the items you'll earn in the Scavenging menu", "text.autoconfig.islandutils.option.channelSwitchers": "Show chat channel buttons", "text.autoconfig.islandutils.option.channelSwitchers.@Tooltip": "Shows the chat channel switcher buttons above the chat box", + "text.autoconfig.islandutils.option.showProgressBar": "Show progress bars", + "text.autoconfig.islandutils.option.showProgressBar.@Tooltip": "Adds an progress bar to inventories for menu items with progress in their tooltip, such as quests or fishing islands", "text.autoconfig.islandutils.option.hideSliders": "Hide Sound Sliders if not Online", "text.autoconfig.islandutils.option.hideSliders.@Tooltip": "Hides the Music Sliders if you're not on MCC Island", "text.autoconfig.islandutils.option.showFishingUpgradeIcon": "Show fishing upgrade icons", diff --git a/src/main/resources/islandutils.mixins.json b/src/main/resources/islandutils.mixins.json index 9ca0dd57..0746c6eb 100644 --- a/src/main/resources/islandutils.mixins.json +++ b/src/main/resources/islandutils.mixins.json @@ -10,6 +10,7 @@ "cosmetics.ChestScreenMixin", "cosmetics.FontLoaderMixin", "cosmetics.PreviewTutorialMixin", + "ItemBarMixin", "discord.ConnectionMixin", "discord.ScoreDisplayMixin", "resources.PackRepositoryMixin",