From 4b13e1fd29baf4a3a03b1cf33438e080c949018b Mon Sep 17 00:00:00 2001 From: KnightMiner Date: Tue, 20 Sep 2022 00:31:36 -0400 Subject: [PATCH] Add ability to break down a tool in the part builder --- .../recipes/tables/tool_recycling.json | 6 + .../recipe/material/IMaterialValue.java | 65 ++++++ .../recipe/material/MaterialRecipe.java | 47 +--- .../recipe/material/MaterialValue.java | 21 ++ .../partbuilder/IPartBuilderContainer.java | 4 +- .../partbuilder/IPartBuilderRecipe.java | 41 +++- .../recipe/partbuilder/ItemPartRecipe.java | 6 +- .../recipe/partbuilder/PartRecipe.java | 15 +- .../library/tools/helper/ModifierUtil.java | 9 + .../tconstruct/tables/TinkerTables.java | 2 + .../PartBuilderContainerWrapper.java | 22 +- .../entity/table/PartBuilderBlockEntity.java | 50 ++-- .../client/inventory/PartBuilderScreen.java | 37 ++- .../tables/data/TableRecipeProvider.java | 4 + .../tables/recipe/PartBuilderToolRecycle.java | 219 ++++++++++++++++++ .../tconstruct/book/images/recycling.png | Bin 0 -> 1717 bytes .../en_us/intro/partbuilder.json | 2 +- .../en_us/intro/recycling.json | 14 ++ .../sections/introduction.json | 5 + .../assets/tconstruct/lang/en_us.json | 4 + 20 files changed, 484 insertions(+), 89 deletions(-) create mode 100644 src/generated/resources/data/tconstruct/recipes/tables/tool_recycling.json create mode 100644 src/main/java/slimeknights/tconstruct/library/recipe/material/IMaterialValue.java create mode 100644 src/main/java/slimeknights/tconstruct/library/recipe/material/MaterialValue.java create mode 100644 src/main/java/slimeknights/tconstruct/tables/recipe/PartBuilderToolRecycle.java create mode 100644 src/main/resources/assets/tconstruct/book/images/recycling.png create mode 100644 src/main/resources/assets/tconstruct/book/materials_and_you/en_us/intro/recycling.json diff --git a/src/generated/resources/data/tconstruct/recipes/tables/tool_recycling.json b/src/generated/resources/data/tconstruct/recipes/tables/tool_recycling.json new file mode 100644 index 00000000000..d165bdf9f41 --- /dev/null +++ b/src/generated/resources/data/tconstruct/recipes/tables/tool_recycling.json @@ -0,0 +1,6 @@ +{ + "type": "tconstruct:part_builder_tool_recycling", + "pattern": { + "tag": "tconstruct:patterns" + } +} \ No newline at end of file diff --git a/src/main/java/slimeknights/tconstruct/library/recipe/material/IMaterialValue.java b/src/main/java/slimeknights/tconstruct/library/recipe/material/IMaterialValue.java new file mode 100644 index 00000000000..3737245e94a --- /dev/null +++ b/src/main/java/slimeknights/tconstruct/library/recipe/material/IMaterialValue.java @@ -0,0 +1,65 @@ +package slimeknights.tconstruct.library.recipe.material; + +import net.minecraft.world.item.ItemStack; +import slimeknights.mantle.recipe.container.ISingleStackContainer; +import slimeknights.tconstruct.library.materials.definition.MaterialVariant; + +/** + * Represents a material with an amount and a cost + */ +public interface IMaterialValue { + /** Gets the material represented in this cost */ + MaterialVariant getMaterial(); + + /** Gets the number of items needed for a single craft */ + default int getNeeded() { + return 1; + } + + /** Gets the value of a single item of this material */ + int getValue(); + + /** + * Gets a copy of the leftover stack for this recipe + * @return Leftover stack + */ + default ItemStack getLeftover() { + return ItemStack.EMPTY; + } + + + /* Helpers */ + + /** + * Gets the amount of material present in the inventory as a float for display + * @param inv Inventory reference + * @return Number of material present as a float + */ + default float getMaterialValue(ISingleStackContainer inv) { + return inv.getStack().getCount() * this.getValue() / (float)this.getNeeded(); + } + + /** + * Gets the number of items in order to craft a material with the given cost + * @param itemCost Cost of the item being crafted + * @return Number of the input to consume + */ + default int getItemsUsed(int itemCost) { + int needed = itemCost * getNeeded(); + int value = getValue(); + int cost = needed / value; + if (needed % value != 0) { + cost++; + } + return cost; + } + + /** + * Gets the number of leftover material from crafting a part with this material + * @param itemCost Cost of the item being crafted + * @return Number of input to consume + */ + default int getRemainder(int itemCost) { + return itemCost * this.getNeeded() % this.getValue(); + } +} diff --git a/src/main/java/slimeknights/tconstruct/library/recipe/material/MaterialRecipe.java b/src/main/java/slimeknights/tconstruct/library/recipe/material/MaterialRecipe.java index cfac3a9a34f..1b5f3475e6b 100644 --- a/src/main/java/slimeknights/tconstruct/library/recipe/material/MaterialRecipe.java +++ b/src/main/java/slimeknights/tconstruct/library/recipe/material/MaterialRecipe.java @@ -33,7 +33,7 @@ /** * Recipe to get the material from an ingredient */ -public class MaterialRecipe implements ICustomOutputRecipe { +public class MaterialRecipe implements ICustomOutputRecipe, IMaterialValue { /** Vanilla requires 4 ingots for full repair, we drop it down to 3 to mesh better with nuggets and blocks and to fit small head costs better */ public static final float INGOTS_PER_REPAIR = 3f; @@ -90,6 +90,11 @@ public RecipeSerializer getSerializer() { return TinkerTables.materialRecipeSerializer.get(); } + @Override + public ItemStack getLeftover() { + return this.leftover.get().copy(); + } + /* Material methods */ @Override @@ -119,38 +124,6 @@ public List getDisplayItems() { return displayItems; } - /** - * Gets the amount of material present in the inventory as a float for display - * @param inv Inventory reference - * @return Number of material present as a float - */ - public float getMaterialValue(ISingleStackContainer inv) { - return inv.getStack().getCount() * this.value / (float)this.needed; - } - - /** - * Gets the number of items in order to craft a material with the given cost - * @param itemCost Cost of the item being crafted - * @return Number of the input to consume - */ - public int getItemsUsed(int itemCost) { - int needed = itemCost * this.needed; - int cost = needed / this.value; - if (needed % this.value != 0) { - cost++; - } - return cost; - } - - /** - * Gets the number of leftover material from crafting a part with this material - * @param itemCost Cost of the item being crafted - * @return Number of input to consume - */ - public int getRemainder(int itemCost) { - return itemCost * this.needed % this.value; - } - /** * Gets the amount to repair per item for tool repair * @param data Tool defintion data for fallback @@ -183,12 +156,4 @@ public static int getRepairDurability(ToolDefinitionData toolData, MaterialId ma } return optional.map(stats -> ((IRepairableMaterialStats)stats).getDurability()).orElseGet(() -> toolData.getBaseStat(ToolStats.DURABILITY).intValue()); } - - /** - * Gets a copy of the leftover stack for this recipe - * @return Leftover stack - */ - public ItemStack getLeftover() { - return this.leftover.get().copy(); - } } diff --git a/src/main/java/slimeknights/tconstruct/library/recipe/material/MaterialValue.java b/src/main/java/slimeknights/tconstruct/library/recipe/material/MaterialValue.java new file mode 100644 index 00000000000..be7757b0319 --- /dev/null +++ b/src/main/java/slimeknights/tconstruct/library/recipe/material/MaterialValue.java @@ -0,0 +1,21 @@ +package slimeknights.tconstruct.library.recipe.material; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import slimeknights.tconstruct.library.materials.definition.MaterialVariant; +import slimeknights.tconstruct.library.materials.definition.MaterialVariantId; + +/** + * Constant material value used for tool parts + */ +@RequiredArgsConstructor +public class MaterialValue implements IMaterialValue { + @Getter + private final MaterialVariant material; + @Getter + private final int value; + + public MaterialValue(MaterialVariantId material, int value) { + this(MaterialVariant.of(material), value); + } +} diff --git a/src/main/java/slimeknights/tconstruct/library/recipe/partbuilder/IPartBuilderContainer.java b/src/main/java/slimeknights/tconstruct/library/recipe/partbuilder/IPartBuilderContainer.java index 8249e25ee31..0539c298610 100644 --- a/src/main/java/slimeknights/tconstruct/library/recipe/partbuilder/IPartBuilderContainer.java +++ b/src/main/java/slimeknights/tconstruct/library/recipe/partbuilder/IPartBuilderContainer.java @@ -2,7 +2,7 @@ import net.minecraft.world.item.ItemStack; import slimeknights.mantle.recipe.container.ISingleStackContainer; -import slimeknights.tconstruct.library.recipe.material.MaterialRecipe; +import slimeknights.tconstruct.library.recipe.material.IMaterialValue; import javax.annotation.Nullable; @@ -15,7 +15,7 @@ public interface IPartBuilderContainer extends ISingleStackContainer { * @return Material recipe, or null if the slot contents are not a valid material */ @Nullable - MaterialRecipe getMaterial(); + IMaterialValue getMaterial(); /** * Gets the stack in the pattern slot diff --git a/src/main/java/slimeknights/tconstruct/library/recipe/partbuilder/IPartBuilderRecipe.java b/src/main/java/slimeknights/tconstruct/library/recipe/partbuilder/IPartBuilderRecipe.java index a7c835f6afd..01ebe4c7f78 100644 --- a/src/main/java/slimeknights/tconstruct/library/recipe/partbuilder/IPartBuilderRecipe.java +++ b/src/main/java/slimeknights/tconstruct/library/recipe/partbuilder/IPartBuilderRecipe.java @@ -1,20 +1,30 @@ package slimeknights.tconstruct.library.recipe.partbuilder; +import net.minecraft.network.chat.Component; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.RecipeType; import slimeknights.mantle.recipe.ICommonRecipe; import slimeknights.tconstruct.library.recipe.TinkerRecipeTypes; -import slimeknights.tconstruct.library.recipe.material.MaterialRecipe; +import slimeknights.tconstruct.library.recipe.material.IMaterialValue; import slimeknights.tconstruct.tables.TinkerTables; import slimeknights.tconstruct.tables.block.entity.inventory.PartBuilderContainerWrapper; +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.List; import java.util.Optional; +import java.util.stream.Stream; public interface IPartBuilderRecipe extends ICommonRecipe { /** Gets the pattern needed for this recipe, * if there are multiple recipes with the same pattern, they are effectively merged */ Pattern getPattern(); + /** Gets a stream of all patterns matching this recipe, allows one recipe to match on multiple patterns */ + default Stream getPatterns(IPartBuilderContainer inv) { + return Stream.of(getPattern()); + } + /** * Gets the number of material needed for this recipe * @return Material amount @@ -39,6 +49,11 @@ default int getItemsUsed(IPartBuilderContainer inv) { .orElse(1); } + /** Assembles the result with the given pattern */ + default ItemStack assemble(IPartBuilderContainer inv, Pattern pattern) { + return assemble(inv); + } + /* Recipe data */ @Override @@ -51,9 +66,10 @@ default ItemStack getToastSymbol() { return new ItemStack(TinkerTables.partBuilder); } - /** Gets the leftover from performing this recipe */ + /** @deprecated use {@link #getLeftover(PartBuilderContainerWrapper, Pattern)} */ + @Deprecated default ItemStack getLeftover(PartBuilderContainerWrapper inventoryWrapper) { - MaterialRecipe recipe = inventoryWrapper.getMaterial(); + IMaterialValue recipe = inventoryWrapper.getMaterial(); if (recipe != null) { int value = recipe.getValue(); if (value > 1) { @@ -70,4 +86,23 @@ default ItemStack getLeftover(PartBuilderContainerWrapper inventoryWrapper) { } return ItemStack.EMPTY; } + + /** + * Gets the leftover from performing this recipe + * TODO: switch to the interface for the parameter + */ + default ItemStack getLeftover(PartBuilderContainerWrapper inventoryWrapper, Pattern pattern) { + return getLeftover(inventoryWrapper); + } + + /** Gets the title to display on the part builder panel. If null, displays default info */ + @Nullable + default Component getTitle() { + return null; + } + + /** Gets the text to display on the part builder screen */ + default List getText(IPartBuilderContainer inv) { + return Collections.emptyList(); + } } diff --git a/src/main/java/slimeknights/tconstruct/library/recipe/partbuilder/ItemPartRecipe.java b/src/main/java/slimeknights/tconstruct/library/recipe/partbuilder/ItemPartRecipe.java index 9d7d716375f..20dbbcfb76c 100644 --- a/src/main/java/slimeknights/tconstruct/library/recipe/partbuilder/ItemPartRecipe.java +++ b/src/main/java/slimeknights/tconstruct/library/recipe/partbuilder/ItemPartRecipe.java @@ -15,7 +15,7 @@ import slimeknights.tconstruct.common.TinkerTags; import slimeknights.tconstruct.library.materials.definition.MaterialVariant; import slimeknights.tconstruct.library.materials.definition.MaterialVariantId; -import slimeknights.tconstruct.library.recipe.material.MaterialRecipe; +import slimeknights.tconstruct.library.recipe.material.IMaterialValue; import slimeknights.tconstruct.tables.TinkerTables; import javax.annotation.Nullable; @@ -54,7 +54,7 @@ public boolean partialMatch(IPartBuilderContainer inv) { } // if there is a material item, it must have a valid material and be craftable if (!inv.getStack().isEmpty()) { - MaterialRecipe materialRecipe = inv.getMaterial(); + IMaterialValue materialRecipe = inv.getMaterial(); return materialRecipe != null && material.matchesVariant(materialRecipe.getMaterial()); } // no material item? return match in case we get one later @@ -63,7 +63,7 @@ public boolean partialMatch(IPartBuilderContainer inv) { @Override public boolean matches(IPartBuilderContainer inv, Level worldIn) { - MaterialRecipe materialRecipe = inv.getMaterial(); + IMaterialValue materialRecipe = inv.getMaterial(); return materialRecipe != null && material.matchesVariant(materialRecipe.getMaterial()) && inv.getStack().getCount() >= materialRecipe.getItemsUsed(cost); } diff --git a/src/main/java/slimeknights/tconstruct/library/recipe/partbuilder/PartRecipe.java b/src/main/java/slimeknights/tconstruct/library/recipe/partbuilder/PartRecipe.java index 8e5ceef6f5d..d43460d2c1c 100644 --- a/src/main/java/slimeknights/tconstruct/library/recipe/partbuilder/PartRecipe.java +++ b/src/main/java/slimeknights/tconstruct/library/recipe/partbuilder/PartRecipe.java @@ -19,7 +19,7 @@ import slimeknights.tconstruct.library.materials.definition.MaterialId; import slimeknights.tconstruct.library.materials.definition.MaterialVariant; import slimeknights.tconstruct.library.materials.definition.MaterialVariantId; -import slimeknights.tconstruct.library.recipe.material.MaterialRecipe; +import slimeknights.tconstruct.library.recipe.material.IMaterialValue; import slimeknights.tconstruct.library.tools.part.IMaterialItem; import slimeknights.tconstruct.tables.TinkerTables; @@ -59,8 +59,13 @@ public boolean partialMatch(IPartBuilderContainer inv) { return false; } // if there is a material item, it must have a valid material and be craftable - if (!inv.getStack().isEmpty()) { - MaterialRecipe materialRecipe = inv.getMaterial(); + ItemStack stack = inv.getStack(); + if (!stack.isEmpty()) { + // no sense allowing if there is no change + if (stack.getItem() == output) { + return false; + } + IMaterialValue materialRecipe = inv.getMaterial(); if (materialRecipe == null) { return false; } @@ -80,7 +85,7 @@ public boolean partialMatch(IPartBuilderContainer inv) { @Override public boolean matches(IPartBuilderContainer inv, Level world) { // must have a material - MaterialRecipe materialRecipe = inv.getMaterial(); + IMaterialValue materialRecipe = inv.getMaterial(); if (materialRecipe != null) { // material must be craftable, usable in the item, and have a cost we can afford MaterialVariant material = materialRecipe.getMaterial(); @@ -112,7 +117,7 @@ public ItemStack getRecipeOutput(MaterialVariantId material) { @Override public ItemStack assemble(IPartBuilderContainer inv) { MaterialVariant material = MaterialVariant.UNKNOWN; - MaterialRecipe materialRecipe = inv.getMaterial(); + IMaterialValue materialRecipe = inv.getMaterial(); if (materialRecipe != null) { material = materialRecipe.getMaterial(); } diff --git a/src/main/java/slimeknights/tconstruct/library/tools/helper/ModifierUtil.java b/src/main/java/slimeknights/tconstruct/library/tools/helper/ModifierUtil.java index 2b285050ffb..dd6777940f8 100644 --- a/src/main/java/slimeknights/tconstruct/library/tools/helper/ModifierUtil.java +++ b/src/main/java/slimeknights/tconstruct/library/tools/helper/ModifierUtil.java @@ -188,6 +188,15 @@ public static int getModifierLevel(ItemStack stack, ModifierId modifier) { return 0; } + /** Checks if the given stack has upgrades */ + public static boolean hasUpgrades(ItemStack stack) { + if (!stack.isEmpty() && stack.is(TinkerTags.Items.MODIFIABLE)) { + CompoundTag nbt = stack.getTag(); + return nbt != null && !nbt.getList(ToolStack.TAG_UPGRADES, Tag.TAG_COMPOUND).isEmpty(); + } + return false; + } + /** * Adds levels to the given key in entity modifier data for an armor modifier * @param tool Tool instance diff --git a/src/main/java/slimeknights/tconstruct/tables/TinkerTables.java b/src/main/java/slimeknights/tconstruct/tables/TinkerTables.java index 607f9d40cec..dc2ece01e0f 100644 --- a/src/main/java/slimeknights/tconstruct/tables/TinkerTables.java +++ b/src/main/java/slimeknights/tconstruct/tables/TinkerTables.java @@ -49,6 +49,7 @@ import slimeknights.tconstruct.tables.menu.TinkerChestContainerMenu; import slimeknights.tconstruct.tables.menu.TinkerStationContainerMenu; import slimeknights.tconstruct.tables.recipe.CraftingTableRepairKitRecipe; +import slimeknights.tconstruct.tables.recipe.PartBuilderToolRecycle; import slimeknights.tconstruct.tables.recipe.TinkerStationDamagingRecipe; import slimeknights.tconstruct.tables.recipe.TinkerStationPartSwapping; import slimeknights.tconstruct.tables.recipe.TinkerStationRepairRecipe; @@ -119,6 +120,7 @@ public final class TinkerTables extends TinkerModule { public static final RegistryObject> specializedRepairKitSerializer = RECIPE_SERIALIZERS.register("specialized_repair_kit", () -> new SpecializedRepairRecipeSerializer<>(SpecializedRepairKitRecipe::new)); public static final RegistryObject> tinkerStationPartSwappingSerializer = RECIPE_SERIALIZERS.register("tinker_station_part_swapping", () -> new SimpleRecipeSerializer<>(TinkerStationPartSwapping::new)); public static final RegistryObject tinkerStationDamagingSerializer = RECIPE_SERIALIZERS.register("tinker_station_damaging", TinkerStationDamagingRecipe.Serializer::new); + public static final RegistryObject partBuilderToolRecycling = RECIPE_SERIALIZERS.register("part_builder_tool_recycling", PartBuilderToolRecycle.Serializer::new); @SubscribeEvent void commonSetup(final FMLCommonSetupEvent event) { diff --git a/src/main/java/slimeknights/tconstruct/tables/block/entity/inventory/PartBuilderContainerWrapper.java b/src/main/java/slimeknights/tconstruct/tables/block/entity/inventory/PartBuilderContainerWrapper.java index d9fb9dd1597..90f200de71c 100644 --- a/src/main/java/slimeknights/tconstruct/tables/block/entity/inventory/PartBuilderContainerWrapper.java +++ b/src/main/java/slimeknights/tconstruct/tables/block/entity/inventory/PartBuilderContainerWrapper.java @@ -2,9 +2,15 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; +import slimeknights.tconstruct.common.TinkerTags; +import slimeknights.tconstruct.library.materials.definition.IMaterial; +import slimeknights.tconstruct.library.materials.definition.MaterialVariantId; import slimeknights.tconstruct.library.recipe.TinkerRecipeTypes; -import slimeknights.tconstruct.library.recipe.material.MaterialRecipe; +import slimeknights.tconstruct.library.recipe.casting.material.MaterialCastingLookup; +import slimeknights.tconstruct.library.recipe.material.IMaterialValue; +import slimeknights.tconstruct.library.recipe.material.MaterialValue; import slimeknights.tconstruct.library.recipe.partbuilder.IPartBuilderContainer; +import slimeknights.tconstruct.library.tools.part.IMaterialItem; import slimeknights.tconstruct.tables.block.entity.table.PartBuilderBlockEntity; import javax.annotation.Nullable; @@ -16,7 +22,7 @@ public class PartBuilderContainerWrapper implements IPartBuilderContainer { private boolean materialNeedsUpdate = true; /** Cached material recipe, may be null if not a material item */ @Nullable - private MaterialRecipe material = null; + private IMaterialValue material = null; public PartBuilderContainerWrapper(PartBuilderBlockEntity builder) { this.builder = builder; @@ -45,11 +51,19 @@ public void refreshMaterial() { @Override @Nullable - public MaterialRecipe getMaterial() { + public IMaterialValue getMaterial() { if (this.materialNeedsUpdate) { this.materialNeedsUpdate = false; - if (getStack().isEmpty()) { + ItemStack stack = getStack(); + if (stack.isEmpty()) { this.material = null; + } else if (stack.is(TinkerTags.Items.TOOL_PARTS)) { + MaterialVariantId material = IMaterialItem.getMaterialFromStack(stack); + if (IMaterial.UNKNOWN_ID.matchesVariant(material)) { + this.material = null; + } else { + this.material = new MaterialValue(material, MaterialCastingLookup.getItemCost(stack.getItem())); + } } else { Level world = getWorld(); this.material = world.getRecipeManager().getRecipeFor(TinkerRecipeTypes.MATERIAL.get(), this, world).orElse(null); diff --git a/src/main/java/slimeknights/tconstruct/tables/block/entity/table/PartBuilderBlockEntity.java b/src/main/java/slimeknights/tconstruct/tables/block/entity/table/PartBuilderBlockEntity.java index 19d98308463..0537310bbf5 100644 --- a/src/main/java/slimeknights/tconstruct/tables/block/entity/table/PartBuilderBlockEntity.java +++ b/src/main/java/slimeknights/tconstruct/tables/block/entity/table/PartBuilderBlockEntity.java @@ -15,7 +15,7 @@ import slimeknights.tconstruct.TConstruct; import slimeknights.tconstruct.common.TinkerTags; import slimeknights.tconstruct.library.recipe.TinkerRecipeTypes; -import slimeknights.tconstruct.library.recipe.material.MaterialRecipe; +import slimeknights.tconstruct.library.recipe.material.IMaterialValue; import slimeknights.tconstruct.library.recipe.partbuilder.IPartBuilderRecipe; import slimeknights.tconstruct.library.recipe.partbuilder.Pattern; import slimeknights.tconstruct.shared.inventory.ConfigurableInvWrapperCapability; @@ -30,7 +30,7 @@ import java.util.Comparator; import java.util.List; import java.util.Map; -import java.util.function.Function; +import java.util.Map.Entry; import java.util.stream.Collectors; public class PartBuilderBlockEntity extends RetexturedTableBlockEntity implements ILazyCrafter { @@ -80,21 +80,19 @@ protected Map getCurrentRecipes() { recipes = Collections.emptyMap(); sortedButtons = Collections.emptyList(); } else { + record PatternRecipe(Pattern pattern, IPartBuilderRecipe recipe) {} // fetch all recipes that can match these inputs, the map ensures the patterns are unique recipes = level.getRecipeManager().byType(TinkerRecipeTypes.PART_BUILDER.get()).values().stream() - .filter(r -> r instanceof IPartBuilderRecipe) - .map(r -> (IPartBuilderRecipe)r) - .filter(r -> r.partialMatch(inventoryWrapper)) - .sorted(Comparator.comparing(Recipe::getId)) - .collect(Collectors.toMap(IPartBuilderRecipe::getPattern, Function.identity(), (a, b) -> a)); - sortedButtons = recipes.values().stream() - .sorted((a, b) -> { - if (a.getCost() != b.getCost()) { - return Integer.compare(a.getCost(), b.getCost()); - } - return a.getPattern().compareTo(b.getPattern()); - }) - .map(IPartBuilderRecipe::getPattern).collect(Collectors.toList()); + .filter(r -> r instanceof IPartBuilderRecipe) + .map(r -> (IPartBuilderRecipe)r) + .filter(r -> r.partialMatch(inventoryWrapper)) + .sorted(Comparator.comparing(Recipe::getId)) + .flatMap(r -> r.getPatterns(inventoryWrapper).map(p -> new PatternRecipe(p, r))) + .collect(Collectors.toMap(PatternRecipe::pattern, PatternRecipe::recipe, (a, b) -> a)); + sortedButtons = recipes.entrySet() + .stream() + .sorted(Comparator.>comparingInt(ent -> ent.getValue().getCost()).thenComparing(Entry::getKey)) + .map(Entry::getKey).collect(Collectors.toList()); } } return recipes; @@ -135,12 +133,22 @@ public IPartBuilderRecipe getPartRecipe() { return null; } + /** Gets the first available recipe */ + @Nullable + public IPartBuilderRecipe getFirstRecipe() { + List sortedButtons = getSortedButtons(); + if (sortedButtons.isEmpty()) { + return null; + } + return getCurrentRecipes().get(sortedButtons.get(0)); + } + /** * Gets the material recipe for the material slot * @return Material slot */ @Nullable - public MaterialRecipe getMaterialRecipe() { + public IMaterialValue getMaterialRecipe() { return inventoryWrapper.getMaterial(); } @@ -222,7 +230,7 @@ public ItemStack calcResult(@Nullable Player player) { if (level != null) { IPartBuilderRecipe recipe = getPartRecipe(); if (recipe != null && recipe.matches(inventoryWrapper, level)) { - return recipe.assemble(inventoryWrapper); + return recipe.assemble(inventoryWrapper, selectedPattern); } } return ItemStack.EMPTY; @@ -266,9 +274,11 @@ public void onCraft(Player player, ItemStack result, int amount) { this.playCraftSound(player); // give the player any leftovers - ItemStack leftover = recipe.getLeftover(inventoryWrapper); - if (!leftover.isEmpty()) { - ItemHandlerHelper.giveItemToPlayer(player, leftover); + if (level != null && !level.isClientSide) { + ItemStack leftover = recipe.getLeftover(inventoryWrapper, selectedPattern); + if (!leftover.isEmpty()) { + ItemHandlerHelper.giveItemToPlayer(player, leftover); + } } // shrink the inputs diff --git a/src/main/java/slimeknights/tconstruct/tables/client/inventory/PartBuilderScreen.java b/src/main/java/slimeknights/tconstruct/tables/client/inventory/PartBuilderScreen.java index 27163514800..ad636c63162 100644 --- a/src/main/java/slimeknights/tconstruct/tables/client/inventory/PartBuilderScreen.java +++ b/src/main/java/slimeknights/tconstruct/tables/client/inventory/PartBuilderScreen.java @@ -23,7 +23,7 @@ import slimeknights.tconstruct.library.materials.stats.IMaterialStats; import slimeknights.tconstruct.library.modifiers.Modifier; import slimeknights.tconstruct.library.modifiers.ModifierEntry; -import slimeknights.tconstruct.library.recipe.material.MaterialRecipe; +import slimeknights.tconstruct.library.recipe.material.IMaterialValue; import slimeknights.tconstruct.library.recipe.partbuilder.IPartBuilderRecipe; import slimeknights.tconstruct.library.recipe.partbuilder.Pattern; import slimeknights.tconstruct.library.utils.Util; @@ -165,16 +165,10 @@ public void updateDisplay() { this.recipeIndexOffset = 0; } - // update part recipe cost - IPartBuilderRecipe partRecipe = this.tile.getPartRecipe(); - if (partRecipe != null) { - this.infoPanelScreen.setPatternCost(partRecipe.getCost()); - } else { - this.infoPanelScreen.clearPatternCost(); - } + assert this.tile != null; // update material - MaterialRecipe materialRecipe = this.tile.getMaterialRecipe(); + IMaterialValue materialRecipe = this.tile.getMaterialRecipe(); if (materialRecipe != null) { this.setDisplayForMaterial(materialRecipe); } else { @@ -183,13 +177,36 @@ public void updateDisplay() { this.infoPanelScreen.setText(INFO_TEXT); this.infoPanelScreen.clearMaterialValue(); } + + // update part recipe cost + IPartBuilderRecipe partRecipe = this.tile.getPartRecipe(); + boolean skipCost = false; + if (partRecipe == null) { + partRecipe = this.tile.getFirstRecipe(); + skipCost = true; + } + if (partRecipe != null) { + int cost = partRecipe.getCost(); + if (cost > 0 && !skipCost) { + this.infoPanelScreen.setPatternCost(cost); + } else { + this.infoPanelScreen.clearPatternCost(); + } + Component title = partRecipe.getTitle(); + if (title != null) { + this.infoPanelScreen.setCaption(title); + this.infoPanelScreen.setText(partRecipe.getText(this.tile.getInventoryWrapper())); + } + } else { + this.infoPanelScreen.clearPatternCost(); + } } /** * Updates the data in the material display * @param materialRecipe New material recipe */ - private void setDisplayForMaterial(MaterialRecipe materialRecipe) { + private void setDisplayForMaterial(IMaterialValue materialRecipe) { MaterialVariant materialVariant = materialRecipe.getMaterial(); this.infoPanelScreen.setCaption(MaterialTooltipCache.getColoredDisplayName(materialVariant.getVariant())); diff --git a/src/main/java/slimeknights/tconstruct/tables/data/TableRecipeProvider.java b/src/main/java/slimeknights/tconstruct/tables/data/TableRecipeProvider.java index 62198a6d65a..fa7cc3581c5 100644 --- a/src/main/java/slimeknights/tconstruct/tables/data/TableRecipeProvider.java +++ b/src/main/java/slimeknights/tconstruct/tables/data/TableRecipeProvider.java @@ -20,6 +20,7 @@ import slimeknights.tconstruct.common.data.BaseRecipeProvider; import slimeknights.tconstruct.smeltery.TinkerSmeltery; import slimeknights.tconstruct.tables.TinkerTables; +import slimeknights.tconstruct.tables.recipe.PartBuilderToolRecycle; import slimeknights.tconstruct.tables.recipe.TinkerStationDamagingRecipe; import java.util.function.Consumer; @@ -185,6 +186,9 @@ protected void buildCraftingRecipes(Consumer consumer) { .setMatchAll() .build(consumer, modResource(folder + "scorched_forge")); + // recycling singleton + consumer.accept(new PartBuilderToolRecycle.Finished(modResource(folder + "tool_recycling"), Ingredient.of(TinkerTags.Items.PATTERNS))); + // tool repair recipe SpecialRecipeBuilder.special(TinkerTables.tinkerStationRepairSerializer.get()) .save(consumer, modPrefix(folder + "tinker_station_repair")); diff --git a/src/main/java/slimeknights/tconstruct/tables/recipe/PartBuilderToolRecycle.java b/src/main/java/slimeknights/tconstruct/tables/recipe/PartBuilderToolRecycle.java new file mode 100644 index 00000000000..bf530009058 --- /dev/null +++ b/src/main/java/slimeknights/tconstruct/tables/recipe/PartBuilderToolRecycle.java @@ -0,0 +1,219 @@ +package slimeknights.tconstruct.tables.recipe; + +import com.google.gson.JsonObject; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.minecraft.ChatFormatting; +import net.minecraft.data.recipes.FinishedRecipe; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; +import slimeknights.mantle.recipe.helper.LoggingRecipeSerializer; +import slimeknights.mantle.util.JsonHelper; +import slimeknights.tconstruct.TConstruct; +import slimeknights.tconstruct.common.TinkerTags; +import slimeknights.tconstruct.library.recipe.partbuilder.IPartBuilderContainer; +import slimeknights.tconstruct.library.recipe.partbuilder.IPartBuilderRecipe; +import slimeknights.tconstruct.library.recipe.partbuilder.Pattern; +import slimeknights.tconstruct.library.tools.definition.PartRequirement; +import slimeknights.tconstruct.library.tools.helper.ModifierUtil; +import slimeknights.tconstruct.library.tools.nbt.ToolStack; +import slimeknights.tconstruct.library.tools.part.IToolPart; +import slimeknights.tconstruct.library.tools.stat.ToolStats; +import slimeknights.tconstruct.tables.TinkerTables; +import slimeknights.tconstruct.tables.block.entity.inventory.PartBuilderContainerWrapper; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +/** Recipe to break a tool into tool parts */ +@RequiredArgsConstructor +public class PartBuilderToolRecycle implements IPartBuilderRecipe { + /** Title for the screen */ + private static final Component TOOL_RECYCLING = TConstruct.makeTranslation("recipe", "tool_recycling"); + /** General instructions for recycling */ + private static final List INSTRUCTIONS = Collections.singletonList(TConstruct.makeTranslation("recipe", "tool_recycling.info")); + /** Error for trying to recycle a tool that cannot be */ + private static final List NO_MODIFIERS = Collections.singletonList(TConstruct.makeTranslation("recipe", "tool_recycling.no_modifiers").withStyle(ChatFormatting.RED)); + + /** Should never be needed, but just in case better than null */ + private static final Pattern ERROR = new Pattern(TConstruct.MOD_ID, "missingno"); + @Getter + private final ResourceLocation id; + private final Ingredient pattern; + + @Override + public Pattern getPattern() { + return ERROR; + } + + @Override + public Stream getPatterns(IPartBuilderContainer inv) { + return ToolStack.from(inv.getStack()).getDefinition().getData().getParts().stream() + .map(PartRequirement::getPart) + .filter(Objects::nonNull) + .map(part -> part.asItem().getRegistryName()) + .filter(Objects::nonNull) + .distinct() + .map(Pattern::new); + } + + @Override + public int getCost() { + return 0; + } + + @Override + public int getItemsUsed(IPartBuilderContainer inv) { + return 1; + } + + @Override + public boolean partialMatch(IPartBuilderContainer inv) { + return pattern.test(inv.getPatternStack()) && inv.getStack().is(TinkerTags.Items.MULTIPART_TOOL); + } + + @Override + public boolean matches(IPartBuilderContainer inv, Level pLevel) { + return partialMatch(inv) && ToolStack.from(inv.getStack()).getUpgrades().isEmpty(); + } + + @Override + public ItemStack assemble(IPartBuilderContainer inv, Pattern pattern) { + ToolStack tool = ToolStack.from(inv.getStack()); + // first, try to find a matching part + IToolPart match = null; + int matchIndex = -1; + List requirements = tool.getDefinition().getData().getParts(); + for (int i = 0; i < requirements.size(); i++) { + IToolPart part = requirements.get(i).getPart(); + if (part != null && pattern.equals(part.asItem().getRegistryName())) { + matchIndex = i; + match = part; + break; + } + } + // failed to find part? should never happen but safety return + if (match == null) { + return ItemStack.EMPTY; + } + return match.withMaterial(tool.getMaterial(matchIndex).getVariant()); + } + + @Override + public ItemStack getLeftover(PartBuilderContainerWrapper inv, Pattern pattern) { + ToolStack tool = ToolStack.from(inv.getStack()); + + // if the tool is damaged, it we only have a chance of a second tool part + int damage = tool.getDamage(); + if (damage > 0) { + int max = tool.getStats().getInt(ToolStats.DURABILITY); + if (TConstruct.RANDOM.nextInt(max) < damage) { + return ItemStack.EMPTY; + } + } + + // find all parts that did not match the pattern + List parts = new ArrayList<>(); + IntList indices = new IntArrayList(); + boolean found = false; + List requirements = tool.getDefinition().getData().getParts(); + for (int i = 0; i < requirements.size(); i++) { + IToolPart part = requirements.get(i).getPart(); + if (part != null) { + if (found || !pattern.equals(part.asItem().getRegistryName())) { + parts.add(part); + indices.add(i); + } else { + found = true; + } + } + } + if (parts.isEmpty()) { + return ItemStack.EMPTY; + } + int index = TConstruct.RANDOM.nextInt(parts.size()); + return parts.get(index).withMaterial(tool.getMaterial(indices.getInt(index)).getVariant()); + } + + /** @deprecated use {@link #assemble(IPartBuilderContainer, Pattern)} */ + @Deprecated + @Override + public ItemStack getResultItem() { + return ItemStack.EMPTY; + } + + @Override + public RecipeSerializer getSerializer() { + return TinkerTables.partBuilderToolRecycling.get(); + } + + @Nullable + @Override + public Component getTitle() { + return TOOL_RECYCLING; + } + + @Override + public List getText(IPartBuilderContainer inv) { + return ModifierUtil.hasUpgrades(inv.getStack()) ? NO_MODIFIERS : INSTRUCTIONS; + } + + /** Serializer instance */ + public static class Serializer extends LoggingRecipeSerializer { + @Override + public PartBuilderToolRecycle fromJson(ResourceLocation id, JsonObject json) { + return new PartBuilderToolRecycle(id, Ingredient.fromJson(JsonHelper.getElement(json, "pattern"))); + } + + @Override + protected void toNetworkSafe(FriendlyByteBuf buffer, PartBuilderToolRecycle recipe) { + recipe.pattern.toNetwork(buffer); + } + + @Nullable + @Override + protected PartBuilderToolRecycle fromNetworkSafe(ResourceLocation id, FriendlyByteBuf buffer) { + return new PartBuilderToolRecycle(id, Ingredient.fromNetwork(buffer)); + } + } + + @RequiredArgsConstructor + public static class Finished implements FinishedRecipe { + @Getter + private final ResourceLocation id; + private final Ingredient pattern; + + @Override + public void serializeRecipeData(JsonObject json) { + json.add("pattern", pattern.toJson()); + } + + @Override + public RecipeSerializer getType() { + return TinkerTables.partBuilderToolRecycling.get(); + } + + @Nullable + @Override + public JsonObject serializeAdvancement() { + return null; + } + + @Nullable + @Override + public ResourceLocation getAdvancementId() { + return null; + } + } +} diff --git a/src/main/resources/assets/tconstruct/book/images/recycling.png b/src/main/resources/assets/tconstruct/book/images/recycling.png new file mode 100644 index 0000000000000000000000000000000000000000..7539b37f732cfabed33637b129689285cdf9de5c GIT binary patch literal 1717 zcmbtUdsNcd7XDF@CYjEpDdwZBEXzmHm`Xmt3UvUL1XIw|Ff_EZ(3vzrqw*12IWx+n zx0a))vZ{TgX6uLSLNcY`~Uspp0)Pbd#`iO{`U9nwN7DJ zNT8{SjR^!nrolmnk3tZPY>0D>R~vTvFYzk}FxJt)0O@9kdGL=Ua#-%?WIsCo}QkhU^I`%OAK=}8|yHlv*57wvpXJ=<2{%*EfFO8z z@L??Z{PE8kUWCRAIzq}D-acf$YUAtpq*H(DyS@Bm-ySG*4Y)yVnvN0`P8H;PmY&|_ z8+pu5N~np-{JWW3xWpM48RfJS3e_MA0i2YhZ?`t!lw)hPHHyEHEUjDwj44az2*2xH z=mw_Uj&{sCKB}`z02a*0R0}kg*Lyz$lVjyHE`j@t=i8k(76M3=-Dw9a3BKnk8dPNNUU)4PK)7sO4Km{uux;Ap1_jXZ=^gi&ZrCi>JIS%p%V%k+UA{Y|J2v;tphpo@PPfq67-Onk5L*?Iaz|EX?vg8J5*vXs3a$N~ltlyI#d zZ}~1{U9K^W+(`{J)huFgU`*ugNLdJcQsQcms*zpu(u|MP8S`+}GlZ#R#?@=XShe44 z8T;lCAG4xp3_oYyxBAB?;`PW3k^ANPimgfT-hvTFjJzp}5_C6l$;(M*#YMX<4;yZy zYR}?WF;b8Hr2lr}MiVh$szB{}Ps_?)#GNT{# z6q3|G#hY!zShHy4J(PX5UjL{vo2cCQ>L;Wc#Mn7JyCk+l)4%3T=!p*%27djZQMR2a zy+pLo|BM|Ws1|=*v;0o};W>aWD8XCF>q({?PWl>%inOQn&W+Q}UPSt;qlZf;i8 z+%%rgSL_&MgJX^N#lH<;@8n%-Ks-8tVul_G<0~pEPL`$>>duXJFN*JY5eR737)IF!8gGw2n@b`(16-P`VotUh&7LilE3~;dT@vNf)`E?f%yC-KwYpAN3^xkvFei6{VVhyC$u9Twl``Y-D8%?CNMHQvr))um zftw?6ECN1SxukeZGE2vKFh!1l_jIsoQM=~4ZrGW#bfU9xA)3-HzE;FkMZm*`x^Tpk zWI^lmqUQr`FY`5j4W|?-A>dqZkPr!~RY(aY)~Zx@VLvkwH!g}H%_)E#GDBQM-6Gi3 z2jD2?#s4$3fp#0|M!(mWC(J*Y>j$^5gk}$N5ZXSu3S^3i*ch?dhRIq*2?;?K$ZcB3 w^F{h{8;?|X(A43hE|=7i9!QRhhxM!fd2iQEa#zYh!`BT39|<{pKOm0#A3zI19{>OV literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/tconstruct/book/materials_and_you/en_us/intro/partbuilder.json b/src/main/resources/assets/tconstruct/book/materials_and_you/en_us/intro/partbuilder.json index f9e11fb7a27..42662fd72fc 100644 --- a/src/main/resources/assets/tconstruct/book/materials_and_you/en_us/intro/partbuilder.json +++ b/src/main/resources/assets/tconstruct/book/materials_and_you/en_us/intro/partbuilder.json @@ -7,7 +7,7 @@ }, "text": [ { - "text": "To make a part: simply grab a blank stencil, put it in the stencil slot. Select what tool part you are wanting to make, insert a soft material you can shape with your hands, and grab your part.", + "text": "To make a part: simply grab a blank stencil, put it in the stencil slot. Select what tool part you are wanting to make, insert a soft material you can shape with your hands, and grab your part. You can also reshape a soft part you previously made.", "paragraph": true } ] diff --git a/src/main/resources/assets/tconstruct/book/materials_and_you/en_us/intro/recycling.json b/src/main/resources/assets/tconstruct/book/materials_and_you/en_us/intro/recycling.json new file mode 100644 index 00000000000..a8187ad8211 --- /dev/null +++ b/src/main/resources/assets/tconstruct/book/materials_and_you/en_us/intro/recycling.json @@ -0,0 +1,14 @@ +{ + "title": "Recycling Tools", + "image": { + "file": "tconstruct:book/images/recycling.png", + "width": 170, + "height": 64 + }, + "text": [ + { + "text": "If you change your mind about a tool, you can always disassemble it in the part builder to salvage one of the tool parts. You can potentially salvage a second part, with a higher chance on repaired tools.", + "paragraph": true + } + ] +} diff --git a/src/main/resources/assets/tconstruct/book/materials_and_you/sections/introduction.json b/src/main/resources/assets/tconstruct/book/materials_and_you/sections/introduction.json index e047d5dcf20..34071f0756f 100644 --- a/src/main/resources/assets/tconstruct/book/materials_and_you/sections/introduction.json +++ b/src/main/resources/assets/tconstruct/book/materials_and_you/sections/introduction.json @@ -34,6 +34,11 @@ "type": "mantle:crafting", "data": "intro/tinkerstation_craft.json" }, + { + "name": "recycling", + "type": "mantle:image_text", + "data": "intro/recycling.json" + }, { "name": "storage", "type": "mantle:text", diff --git a/src/main/resources/assets/tconstruct/lang/en_us.json b/src/main/resources/assets/tconstruct/lang/en_us.json index 4fdc2b25d1e..96d47cb91c6 100644 --- a/src/main/resources/assets/tconstruct/lang/en_us.json +++ b/src/main/resources/assets/tconstruct/lang/en_us.json @@ -2428,6 +2428,10 @@ "recipe.tconstruct.modifier.max_level": "%s level cannot be above %s", "recipe.tconstruct.modifier.requirements_error": "Tool state invalid for one of the modifiers. Contact your modpack maker and request a more descriptive error.", + "recipe.tconstruct.tool_recycling": "Tool Recycling", + "recipe.tconstruct.tool_recycling.info": "Select which part to salvage from this tool. There is a chance based on the tool's remaining durability to salvage a second part.", + "recipe.tconstruct.tool_recycling.no_modifiers": "A tool cannot be recycled if it contains any upgrades or abilities, those must be removed first.", + "_comment": "GUIs Messages",