From 39fc60ad6e6220fa9973f4d9bda9ebfa212c9310 Mon Sep 17 00:00:00 2001 From: TheRealWormbo Date: Mon, 29 May 2023 10:02:54 -0700 Subject: [PATCH] Add conversion chance to Orechid recipe displays - Applies to Orechid, Orechid Ignem, and Marimorphosis. For the latter it also shows biome-specific chance, if different from base chance. - Implemented for JEI (XPlat), REI (Fabric), and EMI (Fabric), ensuring the displays look similar across mods and supporting REI's dark theme. --- .../integration/emi/BotaniaEmiPlugin.java | 5 +- .../emi/MarimorphosisEmiRecipe.java | 28 +++ .../integration/emi/OrechidEmiRecipe.java | 43 ++++- .../integration/emi/PureDaisyEmiRecipe.java | 10 +- .../integration/rei/BotaniaREIPlugin.java | 2 +- .../rei/MarimorphosisREICategory.java | 25 +++ .../rei/OrechidBaseREIDisplay.java | 6 + .../integration/rei/OrechidREICategory.java | 47 +++-- .../integration/rei/PureDaisyREICategory.java | 15 +- .../client/core/proxy/ClientProxy.java | 5 +- .../orechid/MarimorphosisRecipeCategory.java | 13 ++ .../orechid/OrechidRecipeCategoryBase.java | 38 ++++ .../integration/shared/LocaleHelper.java | 31 ++++ .../integration/shared/OrechidUIHelper.java | 169 ++++++++++++++++++ .../common/handler/OrechidManager.java | 47 +++-- .../resources/assets/botania/lang/de_de.json | 2 + .../resources/assets/botania/lang/en_us.json | 2 + web/changelog.md | 1 + 18 files changed, 451 insertions(+), 38 deletions(-) create mode 100644 Fabric/src/main/java/vazkii/botania/fabric/integration/emi/MarimorphosisEmiRecipe.java create mode 100644 Fabric/src/main/java/vazkii/botania/fabric/integration/rei/MarimorphosisREICategory.java create mode 100644 Xplat/src/main/java/vazkii/botania/client/integration/shared/LocaleHelper.java create mode 100644 Xplat/src/main/java/vazkii/botania/client/integration/shared/OrechidUIHelper.java diff --git a/Fabric/src/main/java/vazkii/botania/fabric/integration/emi/BotaniaEmiPlugin.java b/Fabric/src/main/java/vazkii/botania/fabric/integration/emi/BotaniaEmiPlugin.java index 552bb67bb3..a0854dca64 100644 --- a/Fabric/src/main/java/vazkii/botania/fabric/integration/emi/BotaniaEmiPlugin.java +++ b/Fabric/src/main/java/vazkii/botania/fabric/integration/emi/BotaniaEmiPlugin.java @@ -30,6 +30,7 @@ import vazkii.botania.common.block.BotaniaBlocks; import vazkii.botania.common.block.BotaniaFlowerBlocks; import vazkii.botania.common.crafting.BotaniaRecipeTypes; +import vazkii.botania.common.crafting.MarimorphosisRecipe; import vazkii.botania.common.item.BotaniaItems; import vazkii.botania.common.item.equipment.tool.terrasteel.TerraShattererItem; import vazkii.botania.common.item.lens.LensItem; @@ -200,8 +201,8 @@ public void register(EmiRegistry registry) { registry.addRecipe(new OrechidEmiRecipe(ORECHID_IGNEM, recipe, flower)); } flower = EmiStack.of(BotaniaFlowerBlocks.marimorphosis); - for (OrechidRecipe recipe : registry.getRecipeManager().getAllRecipesFor(BotaniaRecipeTypes.MARIMORPHOSIS_TYPE)) { - registry.addRecipe(new OrechidEmiRecipe(MARIMORPHOSIS, recipe, flower)); + for (MarimorphosisRecipe recipe : registry.getRecipeManager().getAllRecipesFor(BotaniaRecipeTypes.MARIMORPHOSIS_TYPE)) { + registry.addRecipe(new MarimorphosisEmiRecipe(recipe, flower)); } } diff --git a/Fabric/src/main/java/vazkii/botania/fabric/integration/emi/MarimorphosisEmiRecipe.java b/Fabric/src/main/java/vazkii/botania/fabric/integration/emi/MarimorphosisEmiRecipe.java new file mode 100644 index 0000000000..8022dfde7c --- /dev/null +++ b/Fabric/src/main/java/vazkii/botania/fabric/integration/emi/MarimorphosisEmiRecipe.java @@ -0,0 +1,28 @@ +package vazkii.botania.fabric.integration.emi; + +import dev.emi.emi.api.stack.EmiIngredient; + +import net.minecraft.network.chat.Component; + +import org.jetbrains.annotations.NotNull; + +import vazkii.botania.client.integration.shared.OrechidUIHelper; +import vazkii.botania.common.crafting.MarimorphosisRecipe; + +import java.util.stream.Stream; + +public class MarimorphosisEmiRecipe extends OrechidEmiRecipe { + public MarimorphosisEmiRecipe( + MarimorphosisRecipe recipe, + EmiIngredient orechid) { + super(BotaniaEmiPlugin.MARIMORPHOSIS, recipe, orechid); + } + + @NotNull + @Override + protected Stream getChanceTooltipComponents(double chance) { + Stream genericChanceTooltipComponents = super.getChanceTooltipComponents(chance); + Stream biomeChanceTooltipComponents = OrechidUIHelper.getBiomeChanceAndRatioTooltipComponents(chance, recipe); + return Stream.concat(genericChanceTooltipComponents, biomeChanceTooltipComponents); + } +} diff --git a/Fabric/src/main/java/vazkii/botania/fabric/integration/emi/OrechidEmiRecipe.java b/Fabric/src/main/java/vazkii/botania/fabric/integration/emi/OrechidEmiRecipe.java index 3e47c37b7c..9d0488a1fe 100644 --- a/Fabric/src/main/java/vazkii/botania/fabric/integration/emi/OrechidEmiRecipe.java +++ b/Fabric/src/main/java/vazkii/botania/fabric/integration/emi/OrechidEmiRecipe.java @@ -3,16 +3,26 @@ import dev.emi.emi.api.recipe.EmiRecipeCategory; import dev.emi.emi.api.stack.EmiIngredient; import dev.emi.emi.api.stack.EmiStack; +import dev.emi.emi.api.widget.TextWidget; import dev.emi.emi.api.widget.WidgetHolder; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent; +import net.minecraft.network.chat.Component; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Ingredient; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import vazkii.botania.api.recipe.OrechidRecipe; +import vazkii.botania.client.integration.shared.OrechidUIHelper; + +import java.util.List; +import java.util.stream.Stream; public class OrechidEmiRecipe extends BotaniaEmiRecipe { private final EmiIngredient orechid; - private final int weight; + protected final OrechidRecipe recipe; public OrechidEmiRecipe(EmiRecipeCategory category, OrechidRecipe recipe, EmiIngredient orechid) { super(category, recipe); @@ -25,7 +35,7 @@ public OrechidEmiRecipe(EmiRecipeCategory category, OrechidRecipe recipe, EmiIng throw new RuntimeException(e); } this.orechid = orechid; - this.weight = recipe.getWeight(); + this.recipe = recipe; } @Override @@ -35,7 +45,7 @@ public int getDisplayHeight() { @Override public int getDisplayWidth() { - return 76; + return 96; } @Override @@ -46,9 +56,34 @@ public boolean supportsRecipeTree() { @Override public void addWidgets(WidgetHolder widgets) { PureDaisyEmiRecipe.addPureDaisyWidgets(widgets, this, input.get(0), orechid, output.get(0)); + + final Double chance = getChance(recipe); + if (chance != null) { + final Component chanceComponent = OrechidUIHelper.getPercentageComponent(chance); + widgets.add(new TextWidget(chanceComponent.getVisualOrderText(), 90, 3, 0x555555, false) { + @Override + public List getTooltip(int mouseX, int mouseY) { + return getChanceTooltipComponents(chance) + .map(Component::getVisualOrderText) + .map(ClientTooltipComponent::create) + .toList(); + } + }.horizontalAlign(TextWidget.Alignment.END)); + } + } + + @NotNull + protected Stream getChanceTooltipComponents(double chance) { + final var ratio = OrechidUIHelper.getRatioForChance(chance); + return Stream.of(OrechidUIHelper.getRatioTooltipComponent(ratio)); + } + + @Nullable + protected Double getChance(@NotNull OrechidRecipe recipe) { + return OrechidUIHelper.getChance(recipe, null); } public int getWeight() { - return weight; + return recipe.getWeight(); } } diff --git a/Fabric/src/main/java/vazkii/botania/fabric/integration/emi/PureDaisyEmiRecipe.java b/Fabric/src/main/java/vazkii/botania/fabric/integration/emi/PureDaisyEmiRecipe.java index 61015ec5cf..86fc8b491a 100644 --- a/Fabric/src/main/java/vazkii/botania/fabric/integration/emi/PureDaisyEmiRecipe.java +++ b/Fabric/src/main/java/vazkii/botania/fabric/integration/emi/PureDaisyEmiRecipe.java @@ -38,7 +38,7 @@ public int getDisplayHeight() { @Override public int getDisplayWidth() { - return 76; + return 96; } @Override @@ -48,9 +48,9 @@ public void addWidgets(WidgetHolder widgets) { public static void addPureDaisyWidgets(WidgetHolder widgets, EmiRecipe recipe, EmiIngredient input, EmiIngredient flower, EmiStack output) { - widgets.add(new BlendTextureWidget(TEXTURE, 7, 0, 65, 44, 0, 0)); - widgets.addSlot(input, 0, 13).drawBack(false); - widgets.addSlot(flower, 29, 13).catalyst(true).drawBack(false); - widgets.addSlot(output, 58, 13).drawBack(false).recipeContext(recipe); + widgets.add(new BlendTextureWidget(TEXTURE, 17, 0, 65, 44, 0, 0)); + widgets.addSlot(input, 10, 13).drawBack(false); + widgets.addSlot(flower, 39, 13).catalyst(true).drawBack(false); + widgets.addSlot(output, 68, 13).drawBack(false).recipeContext(recipe); } } diff --git a/Fabric/src/main/java/vazkii/botania/fabric/integration/rei/BotaniaREIPlugin.java b/Fabric/src/main/java/vazkii/botania/fabric/integration/rei/BotaniaREIPlugin.java index 07f577190e..0f0eaff8c7 100644 --- a/Fabric/src/main/java/vazkii/botania/fabric/integration/rei/BotaniaREIPlugin.java +++ b/Fabric/src/main/java/vazkii/botania/fabric/integration/rei/BotaniaREIPlugin.java @@ -80,7 +80,7 @@ public void registerCategories(CategoryRegistry helper) { new TerrestrialAgglomerationREICategory(), new OrechidREICategory(BotaniaREICategoryIdentifiers.ORECHID, BotaniaFlowerBlocks.orechid), new OrechidREICategory(BotaniaREICategoryIdentifiers.ORECHID_IGNEM, BotaniaFlowerBlocks.orechidIgnem), - new OrechidREICategory(BotaniaREICategoryIdentifiers.MARIMORPHOSIS, BotaniaFlowerBlocks.marimorphosis) + new MarimorphosisREICategory() )); helper.addWorkstations(BuiltinPlugin.CRAFTING, EntryStacks.of(BotaniaItems.craftingHalo), EntryStacks.of(BotaniaItems.autocraftingHalo)); diff --git a/Fabric/src/main/java/vazkii/botania/fabric/integration/rei/MarimorphosisREICategory.java b/Fabric/src/main/java/vazkii/botania/fabric/integration/rei/MarimorphosisREICategory.java new file mode 100644 index 0000000000..55bc70f46b --- /dev/null +++ b/Fabric/src/main/java/vazkii/botania/fabric/integration/rei/MarimorphosisREICategory.java @@ -0,0 +1,25 @@ +package vazkii.botania.fabric.integration.rei; + +import net.minecraft.network.chat.Component; + +import org.jetbrains.annotations.NotNull; + +import vazkii.botania.api.recipe.OrechidRecipe; +import vazkii.botania.client.integration.shared.OrechidUIHelper; +import vazkii.botania.common.block.BotaniaFlowerBlocks; + +import java.util.stream.Stream; + +public class MarimorphosisREICategory extends OrechidREICategory { + public MarimorphosisREICategory() { + super(BotaniaREICategoryIdentifiers.MARIMORPHOSIS, BotaniaFlowerBlocks.marimorphosis); + } + + @NotNull + @Override + protected Stream getChanceTooltipComponents(double chance, OrechidRecipe recipe) { + Stream genericChanceTooltipComponents = super.getChanceTooltipComponents(chance, recipe); + Stream biomeChanceTooltipComponents = OrechidUIHelper.getBiomeChanceAndRatioTooltipComponents(chance, recipe); + return Stream.concat(genericChanceTooltipComponents, biomeChanceTooltipComponents); + } +} diff --git a/Fabric/src/main/java/vazkii/botania/fabric/integration/rei/OrechidBaseREIDisplay.java b/Fabric/src/main/java/vazkii/botania/fabric/integration/rei/OrechidBaseREIDisplay.java index bc824171bf..35db5c534f 100644 --- a/Fabric/src/main/java/vazkii/botania/fabric/integration/rei/OrechidBaseREIDisplay.java +++ b/Fabric/src/main/java/vazkii/botania/fabric/integration/rei/OrechidBaseREIDisplay.java @@ -23,10 +23,12 @@ public abstract class OrechidBaseREIDisplay implements Display { private final List stone; private final List ores; + private final T recipe; public OrechidBaseREIDisplay(T recipe) { stone = Collections.singletonList(EntryIngredient.of(recipe.getInput().getDisplayedStacks().stream().map(EntryStacks::of).collect(Collectors.toList()))); ores = Collections.singletonList(EntryIngredient.of(recipe.getOutput().getDisplayedStacks().stream().map(EntryStacks::of).collect(Collectors.toList()))); + this.recipe = recipe; } @Override @@ -38,4 +40,8 @@ public OrechidBaseREIDisplay(T recipe) { public @NotNull List getOutputEntries() { return ores; } + + public T getRecipe() { + return recipe; + } } diff --git a/Fabric/src/main/java/vazkii/botania/fabric/integration/rei/OrechidREICategory.java b/Fabric/src/main/java/vazkii/botania/fabric/integration/rei/OrechidREICategory.java index 4b8fa225ce..d403456c81 100644 --- a/Fabric/src/main/java/vazkii/botania/fabric/integration/rei/OrechidREICategory.java +++ b/Fabric/src/main/java/vazkii/botania/fabric/integration/rei/OrechidREICategory.java @@ -11,6 +11,7 @@ import me.shedaniel.math.Point; import me.shedaniel.math.Rectangle; import me.shedaniel.rei.api.client.gui.Renderer; +import me.shedaniel.rei.api.client.gui.widgets.Label; import me.shedaniel.rei.api.client.gui.widgets.Widget; import me.shedaniel.rei.api.client.gui.widgets.Widgets; import me.shedaniel.rei.api.client.registry.display.DisplayCategory; @@ -20,23 +21,25 @@ import net.minecraft.core.Registry; import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.Block; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import vazkii.botania.api.recipe.OrechidRecipe; +import vazkii.botania.client.integration.shared.OrechidUIHelper; import vazkii.botania.common.block.BotaniaFlowerBlocks; -import vazkii.botania.common.lib.ResourceLocationHelper; -import java.util.ArrayList; import java.util.List; +import java.util.stream.Stream; + +import static vazkii.botania.fabric.integration.rei.PureDaisyREICategory.setupPureDaisyDisplay; public class OrechidREICategory implements DisplayCategory> { private final EntryStack orechid; private final CategoryIdentifier> categoryId; private final String langKey; - private final ResourceLocation OVERLAY = ResourceLocationHelper.prefix("textures/gui/pure_daisy_overlay.png"); public OrechidREICategory(CategoryIdentifier> categoryId, Block orechid) { this.categoryId = categoryId; @@ -61,19 +64,39 @@ public OrechidREICategory(CategoryIdentifier> @Override public @NotNull List setupDisplay(OrechidBaseREIDisplay display, Rectangle bounds) { - List widgets = new ArrayList<>(); - Point center = new Point(bounds.getCenterX() - 8, bounds.getCenterY() - 9); - - widgets.add(Widgets.createRecipeBase(bounds)); - widgets.add(Widgets.createDrawableWidget(((helper, matrices, mouseX, mouseY, delta) -> CategoryUtils.drawOverlay(helper, matrices, OVERLAY, center.x - 23, center.y - 13, 0, 0, 65, 44)))); - widgets.add(Widgets.createSlot(center).entry(orechid).disableBackground()); - widgets.add(Widgets.createSlot(new Point(center.x - 30, center.y)).entries(display.getInputEntries().get(0)).disableBackground()); - widgets.add(Widgets.createSlot(new Point(center.x + 29, center.y)).entries(display.getOutputEntries().get(0)).disableBackground()); + List widgets = setupPureDaisyDisplay(display, bounds, orechid); + + final Double chance = getChance(display.getRecipe()); + if (chance != null) { + final Component chanceComponent = OrechidUIHelper.getPercentageComponent(chance); + final Point center = new Point(bounds.getCenterX() - 8, bounds.getCenterY() - 9); + final Label chanceLabel = Widgets.createLabel(new Point(center.x + 51, center.y - 11), chanceComponent) + .rightAligned().color(0x555555, 0xAAAAAA).noShadow(); + chanceLabel.tooltip(getChanceTooltipComponents(chance, display.getRecipe()).toArray(Component[]::new)); + widgets.add(chanceLabel); + } + return widgets; } + @NotNull + protected Stream getChanceTooltipComponents(double chance, OrechidRecipe recipe) { + final var ratio = OrechidUIHelper.getRatioForChance(chance); + return Stream.of(OrechidUIHelper.getRatioTooltipComponent(ratio)); + } + + @Nullable + protected Double getChance(@NotNull OrechidRecipe recipe) { + return OrechidUIHelper.getChance(recipe, null); + } + @Override public int getDisplayHeight() { return 54; } + + @Override + public int getDisplayWidth(OrechidBaseREIDisplay display) { + return 112; + } } diff --git a/Fabric/src/main/java/vazkii/botania/fabric/integration/rei/PureDaisyREICategory.java b/Fabric/src/main/java/vazkii/botania/fabric/integration/rei/PureDaisyREICategory.java index d6832d195c..648806b41d 100644 --- a/Fabric/src/main/java/vazkii/botania/fabric/integration/rei/PureDaisyREICategory.java +++ b/Fabric/src/main/java/vazkii/botania/fabric/integration/rei/PureDaisyREICategory.java @@ -15,6 +15,7 @@ import me.shedaniel.rei.api.client.gui.widgets.Widgets; import me.shedaniel.rei.api.client.registry.display.DisplayCategory; import me.shedaniel.rei.api.common.category.CategoryIdentifier; +import me.shedaniel.rei.api.common.display.Display; import me.shedaniel.rei.api.common.entry.EntryStack; import me.shedaniel.rei.api.common.util.EntryStacks; @@ -32,7 +33,7 @@ public class PureDaisyREICategory implements DisplayCategory { private final EntryStack daisy = EntryStacks.of(new ItemStack(BotaniaFlowerBlocks.pureDaisy)); - private final ResourceLocation OVERLAY = ResourceLocationHelper.prefix("textures/gui/pure_daisy_overlay.png"); + private static final ResourceLocation OVERLAY = ResourceLocationHelper.prefix("textures/gui/pure_daisy_overlay.png"); @Override public @NotNull CategoryIdentifier getCategoryIdentifier() { @@ -51,12 +52,17 @@ public class PureDaisyREICategory implements DisplayCategory setupDisplay(PureDaisyREIDisplay display, Rectangle bounds) { + return setupPureDaisyDisplay(display, bounds, daisy); + } + + @NotNull + public static List setupPureDaisyDisplay(Display display, Rectangle bounds, EntryStack entryStack) { List widgets = new ArrayList<>(); Point center = new Point(bounds.getCenterX() - 8, bounds.getCenterY() - 9); widgets.add(Widgets.createRecipeBase(bounds)); widgets.add(Widgets.createDrawableWidget(((helper, matrices, mouseX, mouseY, delta) -> CategoryUtils.drawOverlay(helper, matrices, OVERLAY, center.x - 23, center.y - 13, 0, 0, 65, 44)))); - widgets.add(Widgets.createSlot(center).entry(daisy).disableBackground()); + widgets.add(Widgets.createSlot(center).entry(entryStack).disableBackground()); widgets.add(Widgets.createSlot(new Point(center.x - 30, center.y)).entries(display.getInputEntries().get(0)).disableBackground()); widgets.add(Widgets.createSlot(new Point(center.x + 29, center.y)).entries(display.getOutputEntries().get(0)).disableBackground()); return widgets; @@ -66,4 +72,9 @@ public class PureDaisyREICategory implements DisplayCategory 2 + ? new Locale(parts[0], parts[1], parts[2]) + : parts.length == 2 ? new Locale(parts[0], parts[1]) : new Locale(languageCode); } } diff --git a/Xplat/src/main/java/vazkii/botania/client/integration/jei/orechid/MarimorphosisRecipeCategory.java b/Xplat/src/main/java/vazkii/botania/client/integration/jei/orechid/MarimorphosisRecipeCategory.java index ad1ff68ca7..a9cc0367a9 100644 --- a/Xplat/src/main/java/vazkii/botania/client/integration/jei/orechid/MarimorphosisRecipeCategory.java +++ b/Xplat/src/main/java/vazkii/botania/client/integration/jei/orechid/MarimorphosisRecipeCategory.java @@ -8,11 +8,15 @@ import org.jetbrains.annotations.NotNull; +import vazkii.botania.api.recipe.OrechidRecipe; +import vazkii.botania.client.integration.shared.OrechidUIHelper; import vazkii.botania.common.block.BotaniaFlowerBlocks; import vazkii.botania.common.crafting.BotaniaRecipeTypes; import vazkii.botania.common.crafting.MarimorphosisRecipe; import vazkii.botania.common.lib.LibMisc; +import java.util.stream.Stream; + public class MarimorphosisRecipeCategory extends OrechidRecipeCategoryBase { public static final mezz.jei.api.recipe.RecipeType TYPE = mezz.jei.api.recipe.RecipeType.create(LibMisc.MOD_ID, "marimorphosis", MarimorphosisRecipe.class); @@ -31,4 +35,13 @@ public mezz.jei.api.recipe.RecipeType getRecipeType() { protected RecipeType recipeType() { return BotaniaRecipeTypes.MARIMORPHOSIS_TYPE; } + + @NotNull + @Override + protected Stream getChanceTooltipComponents(double chance, @NotNull OrechidRecipe recipe) { + Stream genericChanceTooltipComponents = super.getChanceTooltipComponents(chance, recipe); + Stream biomeChanceTooltipComponents = OrechidUIHelper.getBiomeChanceAndRatioTooltipComponents(chance, recipe); + return Stream.concat(genericChanceTooltipComponents, biomeChanceTooltipComponents); + } + } diff --git a/Xplat/src/main/java/vazkii/botania/client/integration/jei/orechid/OrechidRecipeCategoryBase.java b/Xplat/src/main/java/vazkii/botania/client/integration/jei/orechid/OrechidRecipeCategoryBase.java index 77548b8640..5470b0b1a6 100644 --- a/Xplat/src/main/java/vazkii/botania/client/integration/jei/orechid/OrechidRecipeCategoryBase.java +++ b/Xplat/src/main/java/vazkii/botania/client/integration/jei/orechid/OrechidRecipeCategoryBase.java @@ -21,13 +21,21 @@ import mezz.jei.api.recipe.RecipeIngredientRole; import mezz.jei.api.recipe.category.IRecipeCategory; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; import net.minecraft.network.chat.Component; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.RecipeType; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import vazkii.botania.api.recipe.OrechidRecipe; +import vazkii.botania.client.integration.shared.OrechidUIHelper; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; import static vazkii.botania.common.lib.ResourceLocationHelper.prefix; @@ -70,6 +78,7 @@ public IDrawable getIcon() { @Override public void setRecipe(@NotNull IRecipeLayoutBuilder builder, @NotNull OrechidRecipe recipe, @NotNull IFocusGroup focusGroup) { + builder.addSlot(RecipeIngredientRole.INPUT, 9, 12) .addItemStacks(recipe.getInput().getDisplayedStacks()); builder.addSlot(RecipeIngredientRole.CATALYST, 39, 12).addItemStack(iconStack); @@ -81,9 +90,38 @@ public void setRecipe(@NotNull IRecipeLayoutBuilder builder, @NotNull OrechidRec @Override public void draw(@NotNull OrechidRecipe recipe, @NotNull IRecipeSlotsView view, @NotNull PoseStack ms, double mouseX, double mouseY) { + final Double chance = getChance(recipe); + if (chance != null) { + final Component chanceComponent = OrechidUIHelper.getPercentageComponent(chance); + Font font = Minecraft.getInstance().font; + int xOffset = 90 - font.width(chanceComponent); + font.draw(ms, chanceComponent, xOffset, 1, 0x888888); + } RenderSystem.enableBlend(); overlay.draw(ms, 17, 0); RenderSystem.disableBlend(); } + @NotNull + @Override + public List getTooltipStrings(@NotNull OrechidRecipe recipe, @NotNull IRecipeSlotsView recipeSlotsView, double mouseX, double mouseY) { + if (mouseX > 0.6 * background.getWidth() && mouseX < 90 && mouseY < 12) { + final Double chance = getChance(recipe); + if (chance != null) { + return getChanceTooltipComponents(chance, recipe).toList(); + } + } + return Collections.emptyList(); + } + + @NotNull + protected Stream getChanceTooltipComponents(double chance, @NotNull OrechidRecipe recipe) { + final var ratio = OrechidUIHelper.getRatioForChance(chance); + return Stream.of(OrechidUIHelper.getRatioTooltipComponent(ratio)); + } + + @Nullable + protected Double getChance(@NotNull OrechidRecipe recipe) { + return OrechidUIHelper.getChance(recipe, null); + } } diff --git a/Xplat/src/main/java/vazkii/botania/client/integration/shared/LocaleHelper.java b/Xplat/src/main/java/vazkii/botania/client/integration/shared/LocaleHelper.java new file mode 100644 index 0000000000..dc69ae2188 --- /dev/null +++ b/Xplat/src/main/java/vazkii/botania/client/integration/shared/LocaleHelper.java @@ -0,0 +1,31 @@ +package vazkii.botania.client.integration.shared; + +import org.jetbrains.annotations.NotNull; + +import vazkii.botania.common.proxy.Proxy; + +import java.math.RoundingMode; +import java.text.NumberFormat; + +public class LocaleHelper { + public static NumberFormat getIntegerFormat() { + return NumberFormat.getIntegerInstance(Proxy.INSTANCE.getLocale()); + } + + @NotNull + public static NumberFormat getPercentageFormat(int fractionDigits) { + final NumberFormat formatter = NumberFormat.getPercentInstance(Proxy.INSTANCE.getLocale()); + formatter.setMinimumFractionDigits(fractionDigits); + formatter.setMaximumFractionDigits(fractionDigits); + formatter.setRoundingMode(RoundingMode.HALF_UP); + return formatter; + } + + public static String formatAsPercentage(double value, int fractionDigits) { + final NumberFormat formatter = getPercentageFormat(fractionDigits); + final double minValue = Math.pow(10, -fractionDigits) / 100; + return (value < minValue + ? "< " + formatter.format(minValue) + : formatter.format(value)).replace('\u00a0', ' '); + } +} diff --git a/Xplat/src/main/java/vazkii/botania/client/integration/shared/OrechidUIHelper.java b/Xplat/src/main/java/vazkii/botania/client/integration/shared/OrechidUIHelper.java new file mode 100644 index 0000000000..fdb58b7194 --- /dev/null +++ b/Xplat/src/main/java/vazkii/botania/client/integration/shared/OrechidUIHelper.java @@ -0,0 +1,169 @@ +package vazkii.botania.client.integration.shared; + +import it.unimi.dsi.fastutil.ints.IntIntImmutablePair; +import it.unimi.dsi.fastutil.ints.IntIntPair; + +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import net.minecraft.world.item.crafting.RecipeType; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import vazkii.botania.api.recipe.OrechidRecipe; +import vazkii.botania.common.handler.OrechidManager; + +import java.text.NumberFormat; +import java.util.stream.Stream; + +/** + * Shared helper methods for the various recipe display mod plugins. + */ +public class OrechidUIHelper { + /** + * How far off from the actual chance an approximated outputs/inputs ratio calculated by + * {@link #getRatioForChance(double)} should be at most. + */ + private static final float MAX_ACCEPTABLE_RATIO_ERROR = 0.05f; + + /** + * How many input blocks an approximated outputs/inputs ratio calculated by + * {@link #getRatioForChance(double)} should have at most if the + * number of outputs in the ratio is greater than 1. + */ + private static final int MAX_NUM_INPUTS_FOR_RATIO = 16; + + @NotNull + public static Component getPercentageComponent(double chance) { + final String chanceText = LocaleHelper.formatAsPercentage(chance, 1); + return Component.literal(chanceText); + } + + @NotNull + public static Component getRatioTooltipComponent(@NotNull IntIntPair ratio) { + final NumberFormat formatter = LocaleHelper.getIntegerFormat(); + return Component.translatable("botaniamisc.conversionRatio", + formatter.format(ratio.secondInt()), + formatter.format(ratio.firstInt())); + } + + @NotNull + public static Component getBiomeChanceTooltipComponent(double chance, @NotNull String biomeTranslationKey) { + return Component.translatable("botaniamisc.conversionChanceBiome", + getPercentageComponent(chance), + Component.translatable(biomeTranslationKey).withStyle(ChatFormatting.ITALIC) + ).withStyle(ChatFormatting.GRAY); + } + + @NotNull + public static Stream getBiomeChanceAndRatioTooltipComponents(double chance, OrechidRecipe recipe) { + final var biomeTranslationKey = OrechidUIHelper.getPlayerBiomeTranslationKey(); + final var player = Minecraft.getInstance().player; + if (biomeTranslationKey == null || player == null) { + return Stream.empty(); + } + + final var biomeChance = OrechidUIHelper.getChance(recipe, player.blockPosition()); + if (biomeChance == null || Mth.equal(chance, biomeChance)) { + return Stream.empty(); + } + + final var biomeRatio = OrechidUIHelper.getRatioForChance(biomeChance); + return Stream.of( + OrechidUIHelper.getBiomeChanceTooltipComponent(biomeChance, biomeTranslationKey), + Component.literal("(") + .append(OrechidUIHelper.getRatioTooltipComponent(biomeRatio)) + .append(")") + .withStyle(ChatFormatting.GRAY) + ); + } + + @Nullable + public static Double getChance(T recipe, @Nullable BlockPos pos) { + final var level = Minecraft.getInstance().level; + if (level == null) { + return null; + } + @SuppressWarnings("unchecked") + final var type = (RecipeType) recipe.getType(); + final var state = recipe.getInput().getDisplayed().get(0); + final int totalWeight = OrechidManager.getTotalDisplayWeightAt(level, type, state, pos); + final int weight = pos != null + ? recipe.getWeight(level, pos) + : recipe.getWeight(); + return (double) weight / totalWeight; + } + + /** + * Determines a "visually pleasing" ratio to be expected between input and output that is not too far off the + * precise ratio. + * + * @param actualRatio The actual ratio. + * @return A pair of ints, first int being the number of input blocks, and second int being the expected number of + * output blocks. + */ + @NotNull + public static IntIntPair getRatioForChance(double actualRatio) { + // First shot: 1 desired output from N input blocks + int bestNumOutputs = 1; + int bestNumInputs = (int) Math.round(1 / actualRatio); + double bestError = calcError(actualRatio, bestNumOutputs, bestNumInputs); + + // Now try to bring the error below an acceptable margin, but only with relatively small integer ratios. + if (bestNumInputs < MAX_NUM_INPUTS_FOR_RATIO && bestError > MAX_ACCEPTABLE_RATIO_ERROR) { + // This calculates an approximation for outputs/inputs for the given chance using continued fractions. + // (also see https://en.wikipedia.org/wiki/Continued_fraction#Infinite_continued_fractions_and_convergents) + int numOutputsNminus1 = 1; + int numOutputsNminus2 = 0; + int numInputsNminus1 = 0; + int numInputsNminus2 = 1; + double remainderN = actualRatio; + do { + int coefficientN = (int) Math.floor(remainderN); + int numOutputsN = coefficientN * numOutputsNminus1 + numOutputsNminus2; + int numInputsN = coefficientN * numInputsNminus1 + numInputsNminus2; + + if (numInputsN > MAX_NUM_INPUTS_FOR_RATIO) { + // numbers are getting too big + break; + } + + final double errorN = calcError(actualRatio, numOutputsN, numInputsN); + if (errorN < bestError) { + bestNumOutputs = numOutputsN; + bestNumInputs = numInputsN; + bestError = errorN; + } + + // shift values for next iteration + numOutputsNminus2 = numOutputsNminus1; + numOutputsNminus1 = numOutputsN; + numInputsNminus2 = numInputsNminus1; + numInputsNminus1 = numInputsN; + remainderN = 1 / (remainderN - coefficientN); + + } while (numInputsNminus1 != 0 && bestError > MAX_ACCEPTABLE_RATIO_ERROR); + } + + return IntIntImmutablePair.of(bestNumInputs, bestNumOutputs); + } + + private static double calcError(double chance, int numOutputs, int numInputs) { + return Math.abs((double) numOutputs / numInputs - chance) / chance; + } + + public static String getPlayerBiomeTranslationKey() { + final var player = Minecraft.getInstance().player; + if (player == null) { + return null; + } + final var biomeKey = player.level.getBiome(player.blockPosition()).unwrapKey().orElse(null); + if (biomeKey == null) { + return "argument.id.invalid"; + } + return String.format("biome.%s.%s", biomeKey.location().getNamespace(), biomeKey.location().getPath()); + } +} diff --git a/Xplat/src/main/java/vazkii/botania/common/handler/OrechidManager.java b/Xplat/src/main/java/vazkii/botania/common/handler/OrechidManager.java index 7c539daee8..137c0a046c 100644 --- a/Xplat/src/main/java/vazkii/botania/common/handler/OrechidManager.java +++ b/Xplat/src/main/java/vazkii/botania/common/handler/OrechidManager.java @@ -10,24 +10,31 @@ import com.google.common.collect.ImmutableList; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; + +import net.minecraft.core.BlockPos; import net.minecraft.server.packs.PackType; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.server.packs.resources.ResourceManagerReloadListener; import net.minecraft.world.item.crafting.RecipeManager; import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import vazkii.botania.api.recipe.OrechidRecipe; import vazkii.botania.xplat.XplatAbstractions; import java.util.*; +import java.util.function.ToIntFunction; import static vazkii.botania.common.lib.ResourceLocationHelper.prefix; public class OrechidManager implements ResourceManagerReloadListener { private static final Map, Map>> BY_TYPE = new IdentityHashMap<>(); + private static final Map, Object2IntOpenHashMap> TOTAL_WEIGHTS_WITHOUT_POSITION = new IdentityHashMap<>(); public static void registerListener() { XplatAbstractions.INSTANCE.registerReloadListener(PackType.SERVER_DATA, prefix("orechid"), new OrechidManager()); @@ -36,32 +43,50 @@ public static void registerListener() { @Override public void onResourceManagerReload(@NotNull ResourceManager manager) { BY_TYPE.clear(); + TOTAL_WEIGHTS_WITHOUT_POSITION.clear(); } public static Collection getMatchingRecipes( RecipeManager manager, RecipeType type, BlockState state) { - var byState = BY_TYPE.get(type); - if (byState == null) { - byState = new IdentityHashMap<>(); - BY_TYPE.put(type, byState); - } - - var list = byState.get(state); - if (list == null) { + final var byState = BY_TYPE.computeIfAbsent(type, t -> new IdentityHashMap<>()); + final var list = byState.computeIfAbsent(state, s -> { var builder = ImmutableList.builder(); for (var recipe : manager.getAllRecipesFor(type)) { if (recipe.getInput().test(state)) { builder.add(recipe); } } - list = builder.build(); - byState.put(state, list); - } + return builder.build(); + }); @SuppressWarnings("unchecked") // we only add T's to this list in the above loop List result = (List) list; return result; } + + public static int getTotalDisplayWeightAt(Level level, RecipeType type, BlockState state, @Nullable BlockPos pos) { + return pos == null + ? getCachedTotalDisplayWeightWithoutPosition(level, type, state) + : calculateTotalDisplayWeightAtPosition(level, type, state, pos); + } + + private static int getCachedTotalDisplayWeightWithoutPosition(Level level, RecipeType type, BlockState state) { + final var byState = TOTAL_WEIGHTS_WITHOUT_POSITION.computeIfAbsent(type, t -> new Object2IntOpenHashMap<>()); + return byState.computeIfAbsent(state, s -> calculateTotalDisplayWeightAtPosition(level, type, state, null)); + } + + private static int calculateTotalDisplayWeightAtPosition(Level level, RecipeType type, BlockState state, @Nullable BlockPos pos) { + final var recipeList = getMatchingRecipes(level.getRecipeManager(), type, state); + if (recipeList.isEmpty()) { + return 0; + } + + ToIntFunction weightFunction = pos != null + ? r -> r.getWeight(level, pos) + : OrechidRecipe::getWeight; + return recipeList.stream().mapToInt(weightFunction).sum(); + } + } diff --git a/Xplat/src/main/resources/assets/botania/lang/de_de.json b/Xplat/src/main/resources/assets/botania/lang/de_de.json index b8eca7317c..69d33ab375 100644 --- a/Xplat/src/main/resources/assets/botania/lang/de_de.json +++ b/Xplat/src/main/resources/assets/botania/lang/de_de.json @@ -24,6 +24,8 @@ "botaniamisc.toolRank": "%s&7-Rang", "botaniamisc.sextantMode.circle": "Kreismodus", "botaniamisc.sextantMode.sphere": "Kugelmodus", + "botaniamisc.conversionRatio": "Etwa %s von %s", + "botaniamisc.conversionChanceBiome": "%s in %s-Biom", "botania.color.rainbow": "Regenbogenfarben", diff --git a/Xplat/src/main/resources/assets/botania/lang/en_us.json b/Xplat/src/main/resources/assets/botania/lang/en_us.json index ee5ed62624..17779f2c98 100644 --- a/Xplat/src/main/resources/assets/botania/lang/en_us.json +++ b/Xplat/src/main/resources/assets/botania/lang/en_us.json @@ -39,6 +39,8 @@ "botaniamisc.lexiconcover2": "A Book by Vazkii", "botaniamisc.shiftinfo": "Hold %s for more info", "botaniamisc.ratio": "%sx Zoom (hover to zoom out)", + "botaniamisc.conversionRatio": "About %s out of %s", + "botaniamisc.conversionChanceBiome": "%s in %s biome", "botaniamisc.bottleTooltip": "It has an acquired taste", "botaniamisc.shardLevel": "Shard Power %s", "botaniamisc.shardRange": "Radius: %s", diff --git a/web/changelog.md b/web/changelog.md index 2634a7c83a..62b5a0f94e 100644 --- a/web/changelog.md +++ b/web/changelog.md @@ -23,6 +23,7 @@ and start a new "Upcoming" section. * Add: Manufactory Halo's auto-crafting can be toggled by shift+right-clicking the crafting table segment or right-clicking the item in an inventory screen * Add: Mana lenses can now be merged using either honey bottles or slime balls +* Add: Conversion chances for Orechid, Orechid Ignem, and Marimorphosis (including biome-specific chances for player's location) are now displayed in the recipe listings of JEI, REI, and EMI (Wormbo) * Add: Reintroduce the useShaders config option to disable Botania's special shaders * Add: zh_cn updates (Dawnwalker666) * Change: The Worldshaper's Sextant provides more control over the exact shape of circles