diff --git a/src/main/java/io/github/pylonmc/pylon/base/BaseBlocks.java b/src/main/java/io/github/pylonmc/pylon/base/BaseBlocks.java index 8e519b646..fb171d114 100644 --- a/src/main/java/io/github/pylonmc/pylon/base/BaseBlocks.java +++ b/src/main/java/io/github/pylonmc/pylon/base/BaseBlocks.java @@ -46,6 +46,7 @@ public static void initialize() { PylonBlock.register(BaseKeys.GRINDSTONE_HANDLE, Material.OAK_FENCE, GrindstoneHandle.class); PylonBlock.register(BaseKeys.ENRICHED_SOUL_SOIL, Material.SOUL_SOIL, EnrichedSoulSoil.class); PylonBlock.register(BaseKeys.MIXING_POT, Material.CAULDRON, MixingPot.class); + PylonBlock.register(BaseKeys.CRUCIBLE, Material.CAULDRON, Crucible.class); PylonBlock.register(BaseKeys.IGNEOUS_COMPOSITE, Material.OBSIDIAN, PylonBlock.class); PylonBlock.register(BaseKeys.PORTABLE_FLUID_TANK_WOOD, Material.BROWN_STAINED_GLASS, PortableFluidTank.class); PylonBlock.register(BaseKeys.PORTABLE_FLUID_TANK_COPPER, Material.ORANGE_STAINED_GLASS, PortableFluidTank.class); diff --git a/src/main/java/io/github/pylonmc/pylon/base/BaseItems.java b/src/main/java/io/github/pylonmc/pylon/base/BaseItems.java index 104fc1dce..b49cfc83e 100644 --- a/src/main/java/io/github/pylonmc/pylon/base/BaseItems.java +++ b/src/main/java/io/github/pylonmc/pylon/base/BaseItems.java @@ -766,8 +766,14 @@ private BaseItems() { BasePages.SIMPLE_MACHINES.addItem(MIXING_POT); } + public static final ItemStack CRUCIBLE = ItemStackBuilder.pylon(Material.CAULDRON, BaseKeys.CRUCIBLE) + .build(); + static { + PylonItem.register(PylonItem.class, CRUCIBLE, BaseKeys.CRUCIBLE); + BasePages.SIMPLE_MACHINES.addItem(CRUCIBLE); + } + public static final ItemStack ENRICHED_SOUL_SOIL = ItemStackBuilder.pylon(Material.SOUL_SOIL, BaseKeys.ENRICHED_SOUL_SOIL) - .set(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, true) .build(); static { PylonItem.register(PylonItem.class, ENRICHED_SOUL_SOIL, BaseKeys.ENRICHED_SOUL_SOIL); diff --git a/src/main/java/io/github/pylonmc/pylon/base/BaseKeys.java b/src/main/java/io/github/pylonmc/pylon/base/BaseKeys.java index 5479ece5d..e762836ac 100644 --- a/src/main/java/io/github/pylonmc/pylon/base/BaseKeys.java +++ b/src/main/java/io/github/pylonmc/pylon/base/BaseKeys.java @@ -157,6 +157,7 @@ public class BaseKeys { public static final NamespacedKey GRINDSTONE_HANDLE = baseKey("grindstone_handle"); public static final NamespacedKey MIXING_POT = baseKey("mixing_pot"); + public static final NamespacedKey CRUCIBLE = baseKey("crucible"); public static final NamespacedKey PRESS = baseKey("press"); diff --git a/src/main/java/io/github/pylonmc/pylon/base/BaseRecipes.java b/src/main/java/io/github/pylonmc/pylon/base/BaseRecipes.java index 251ab5782..4444cee29 100644 --- a/src/main/java/io/github/pylonmc/pylon/base/BaseRecipes.java +++ b/src/main/java/io/github/pylonmc/pylon/base/BaseRecipes.java @@ -18,6 +18,7 @@ public static void initialize() { MagicAltarRecipe.RECIPE_TYPE.register(); MeltingRecipe.RECIPE_TYPE.register(); MixingPotRecipe.RECIPE_TYPE.register(); + CrucibleRecipe.RECIPE_TYPE.register(); MoldingRecipe.RECIPE_TYPE.register(); PipeBendingRecipe.RECIPE_TYPE.register(); PressRecipe.RECIPE_TYPE.register(); diff --git a/src/main/java/io/github/pylonmc/pylon/base/content/machines/simple/Crucible.java b/src/main/java/io/github/pylonmc/pylon/base/content/machines/simple/Crucible.java new file mode 100644 index 000000000..c15774969 --- /dev/null +++ b/src/main/java/io/github/pylonmc/pylon/base/content/machines/simple/Crucible.java @@ -0,0 +1,290 @@ +package io.github.pylonmc.pylon.base.content.machines.simple; + +import com.destroystokyo.paper.ParticleBuilder; +import io.github.pylonmc.pylon.base.BaseKeys; +import io.github.pylonmc.pylon.base.recipes.CrucibleRecipe; +import io.github.pylonmc.pylon.base.util.BaseUtils; +import io.github.pylonmc.pylon.core.block.BlockStorage; +import io.github.pylonmc.pylon.core.block.PylonBlock; +import io.github.pylonmc.pylon.core.block.base.*; +import io.github.pylonmc.pylon.core.block.context.BlockBreakContext; +import io.github.pylonmc.pylon.core.block.context.BlockCreateContext; +import io.github.pylonmc.pylon.core.config.Settings; +import io.github.pylonmc.pylon.core.config.adapter.ConfigAdapter; +import io.github.pylonmc.pylon.core.datatypes.PylonSerializers; +import io.github.pylonmc.pylon.core.fluid.FluidPointType; +import io.github.pylonmc.pylon.core.fluid.PylonFluid; +import io.github.pylonmc.pylon.core.i18n.PylonArgument; +import io.github.pylonmc.pylon.core.recipe.FluidOrItem; +import io.github.pylonmc.pylon.core.util.PylonUtils; +import io.github.pylonmc.pylon.core.util.gui.unit.UnitFormat; +import io.github.pylonmc.pylon.core.waila.WailaDisplay; +import io.papermc.paper.datacomponent.DataComponentTypes; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import org.bukkit.Keyed; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Particle; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.data.Levelled; +import org.bukkit.entity.Player; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.CauldronLevelChangeEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataContainer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +import static io.github.pylonmc.pylon.base.util.BaseUtils.baseKey; + +public final class Crucible extends PylonBlock implements PylonInteractBlock, PylonFluidTank, PylonCauldron, PylonBreakHandler, PylonTickingBlock { + public final int capacity = getSettings().getOrThrow("capacity", ConfigAdapter.INT); + public final int smeltTime = getSettings().getOrThrow("smelt-time", ConfigAdapter.INT); + + private ItemStack processingType = null; + private int amount = 0; + + private static final NamespacedKey PROCESSING_KEY = baseKey("processing"); + private static final NamespacedKey AMOUNT_KEY = baseKey("amount"); + + public static final Map VANILLA_BLOCK_HEAT_MAP = Settings.get(BaseKeys.CRUCIBLE).getOrThrow("vanilla-block-heat-map", ConfigAdapter.MAP.from(ConfigAdapter.MATERIAL, ConfigAdapter.INT)); + public static final Set ITEM_BLACKLIST = Set.of(Material.BUCKET, Material.WATER_BUCKET, Material.LAVA_BUCKET, Material.GLASS_BOTTLE); + + @SuppressWarnings("unused") + public Crucible(@NotNull Block block, @NotNull BlockCreateContext context) { + super(block); + createFluidPoint(FluidPointType.OUTPUT, BlockFace.NORTH, context, false); + setCapacity(1000.0); + } + + @SuppressWarnings("unused") + public Crucible(@NotNull Block block, @NotNull PersistentDataContainer pdc) { + super(block); + processingType = pdc.get(PROCESSING_KEY, PylonSerializers.ITEM_STACK); + amount = pdc.get(AMOUNT_KEY, PylonSerializers.INTEGER); + } + + //region Inventory handling + public int spaceAvailable() { + return capacity - amount; + } + + @Override + public void onBreak(@NotNull List drops, @NotNull BlockBreakContext context) { + PylonFluidTank.super.onBreak(drops, context); + if (processingType == null || amount == 0) return; + + int maxStack = processingType.getMaxStackSize(); + int cycles = amount / maxStack; + int spare = amount % maxStack; + for (int i = 0; i < cycles; i++) { + drops.add(processingType.asQuantity(maxStack)); + } + + drops.add(processingType.asQuantity(spare)); + } + //endregion + + @Override + public void write(@NotNull PersistentDataContainer pdc) { + PylonUtils.setNullable(pdc, PROCESSING_KEY, PylonSerializers.ITEM_STACK, processingType); + pdc.set(AMOUNT_KEY, PylonSerializers.INTEGER, amount); + } + + @Override + public boolean isAllowedFluid(@NotNull PylonFluid fluid) { + return true; + } + + @Override + public void onInteract(@NotNull PlayerInteractEvent event) { + // Don't allow fluid to be manually inserted/removed + if (event.getItem() != null && ITEM_BLACKLIST.contains(event.getMaterial())) { + event.setCancelled(true); + return; + } + + if (event.getPlayer().isSneaking() + || event.getHand() != EquipmentSlot.HAND + || event.getAction() != Action.RIGHT_CLICK_BLOCK + ) { + return; + } + + event.setCancelled(true); + + ItemStack item = event.getPlayer().getInventory().getItemInMainHand(); + + if (!CrucibleRecipe.isValid(item)) { + return; + } + + int amount = Math.min(item.getAmount(), spaceAvailable()); + if (amount == 0) { + return; + } + + if (processingType == null) { + this.processingType = item.asOne(); + this.amount = amount; + item.subtract(amount); + } else if (processingType.isSimilar(item)) { + this.amount += amount; + item.subtract(amount); + } + } + + public void clearInventory() { + this.processingType = null; + this.amount = 0; + } + + public boolean tryDoRecipe() { + if (processingType == null) return false; + + for (CrucibleRecipe recipe : CrucibleRecipe.RECIPE_TYPE.getRecipes()) { + if (recipe.matches(processingType)) { + + doRecipe(recipe); + return true; + } + } + + return false; + } + + private void doRecipe(@NotNull CrucibleRecipe recipe) { + if (recipe.output().fluid().equals(getFluidType())) { + if (getFluidSpaceRemaining() < 1.0e-6) return; // no need to waste stuff + } + + FluidOrItem.Fluid fluid = recipe.output(); + + setFluidType(fluid.fluid()); + addFluid(fluid.amountMillibuckets()); + + new ParticleBuilder(Particle.SMOKE) + .count(20) + .location(getBlock().getLocation().toCenterLocation().add(0, 0.5, 0)) + .offset(0.3, 0, 0.3) + .spawn(); + + new ParticleBuilder(Particle.ASH) + .count(30) + .location(getBlock().getLocation().toCenterLocation()) + .extra(0.05) + .spawn(); + + this.amount--; + if (this.amount == 0) { + clearInventory(); + } + } + //region Cauldron logic + + @Override + public void onLevelChange(@NotNull CauldronLevelChangeEvent event) { + event.setCancelled(true); + } + + @Override + public void onFluidAdded(@NotNull PylonFluid fluid, double amount) { + PylonFluidTank.super.onFluidAdded(fluid, amount); + updateCauldron(); + } + + @Override + public void onFluidRemoved(@NotNull PylonFluid fluid, double amount) { + PylonFluidTank.super.onFluidRemoved(fluid, amount); + updateCauldron(); + } + + private void updateCauldron() { + int level = (int) getFluidAmount() / 333; + if (level > 0 && getBlock().getType() == Material.CAULDRON) { + getBlock().setType(Material.WATER_CAULDRON); + } else if (level == 0) { + getBlock().setType(Material.CAULDRON); + } + if (getBlock().getBlockData() instanceof Levelled levelled) { + levelled.setLevel(level); + getBlock().setBlockData(levelled); + } + } + + //endregion + + @Override + public @Nullable WailaDisplay getWaila(@NotNull Player player) { + return new WailaDisplay(getDefaultWailaTranslationKey().arguments( + PylonArgument.of("item_info", this.processingType == null ? + Component.translatable("pylon.pylonbase.waila.crucible.item.empty") : + Component.translatable("pylon.pylonbase.waila.crucible.item.stored", + PylonArgument.of("type", this.processingType.getData(DataComponentTypes.ITEM_NAME)), + PylonArgument.of("amount", this.amount) + )), + + PylonArgument.of("liquid_info", getFluidType() == null ? + Component.translatable("pylon.pylonbase.waila.crucible.liquid.empty") : + Component.translatable("pylon.pylonbase.waila.crucible.liquid.filled", + PylonArgument.of("fluid", getFluidType().getName()), + PylonArgument.of("bar", BaseUtils.createFluidAmountBar( + getFluidAmount(), + getFluidCapacity(), + 10, + TextColor.color(200, 255, 255) + )) + )) + )); + } + + //region Tick handling + @Override + public void tick() { + tryDoRecipe(); + updateCauldron(); + } + + @Override + public int getTickInterval() { + if (processingType == null) { + return smeltTime; // minimize ticking when empty + } + + Integer heatFactor = getHeatFactor(); + if (heatFactor == null) { + return smeltTime; // probably won't even work be operating in such scenario + } + + // stronger heat factor -> faster smelting + return smeltTime / heatFactor; + } + + //endregion + + //region Heat handling + public interface HeatedBlock extends Keyed { + int heatGenerated(); + } + + public Integer getHeatFactor() { + Block below = getBlock().getRelative(BlockFace.DOWN); + var pylonBlock = BlockStorage.get(below); + if (pylonBlock != null) { + if (pylonBlock instanceof HeatedBlock heatedBlock) { + return heatedBlock.heatGenerated(); + } + + return null; + } + + return VANILLA_BLOCK_HEAT_MAP.get(below.getType()); + } + //endregion +} diff --git a/src/main/java/io/github/pylonmc/pylon/base/recipes/CrucibleRecipe.java b/src/main/java/io/github/pylonmc/pylon/base/recipes/CrucibleRecipe.java new file mode 100644 index 000000000..ea0bb94cd --- /dev/null +++ b/src/main/java/io/github/pylonmc/pylon/base/recipes/CrucibleRecipe.java @@ -0,0 +1,132 @@ +package io.github.pylonmc.pylon.base.recipes; + +import io.github.pylonmc.pylon.base.BaseItems; +import io.github.pylonmc.pylon.base.content.machines.simple.Crucible; +import io.github.pylonmc.pylon.base.util.BaseUtils; +import io.github.pylonmc.pylon.core.block.PylonBlockSchema; +import io.github.pylonmc.pylon.core.config.ConfigSection; +import io.github.pylonmc.pylon.core.config.adapter.ConfigAdapter; +import io.github.pylonmc.pylon.core.guide.button.FluidButton; +import io.github.pylonmc.pylon.core.guide.button.ItemButton; +import io.github.pylonmc.pylon.core.recipe.ConfigurableRecipeType; +import io.github.pylonmc.pylon.core.recipe.FluidOrItem; +import io.github.pylonmc.pylon.core.recipe.PylonRecipe; +import io.github.pylonmc.pylon.core.recipe.RecipeInput; +import io.github.pylonmc.pylon.core.recipe.RecipeType; +import io.github.pylonmc.pylon.core.registry.PylonRegistry; +import io.github.pylonmc.pylon.core.util.PylonUtils; +import io.github.pylonmc.pylon.core.util.gui.GuiItems; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import xyz.xenondevs.invui.gui.Gui; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static io.github.pylonmc.pylon.base.util.BaseUtils.baseKey; + +public record CrucibleRecipe( + @NotNull NamespacedKey key, + @NotNull RecipeInput.Item input, + @NotNull FluidOrItem.Fluid output +) implements PylonRecipe { + + private static Set HEATED_BLOCKS = null; + private static List HEAT_SOURCES = null; + + public static final RecipeType RECIPE_TYPE = new ConfigurableRecipeType<>(baseKey("crucible")) { + @Override + protected @NotNull CrucibleRecipe loadRecipe(@NotNull NamespacedKey key, @NotNull ConfigSection section) { + FluidOrItem output = section.getOrThrow("output", ConfigAdapter.FLUID_OR_ITEM); + if (!(output instanceof FluidOrItem.Fluid fluidOutput)) { + throw new IllegalArgumentException(key + ": In crucible recipe output must be a fluid."); + } + + return new CrucibleRecipe( + key, + new RecipeInput.Item(section.getOrThrow("input-item", ConfigAdapter.RECIPE_INPUT_ITEM).getItems(), 1), + fluidOutput + ); + } + }; + + @Override + public @NotNull List<@NotNull RecipeInput> getInputs() { + return List.of(input); + } + + @Override + public @NotNull List<@NotNull FluidOrItem> getResults() { + return List.of(output); + } + + public static Set getHeatedBlocks() { + if (HEATED_BLOCKS == null) { + HEATED_BLOCKS = new HashSet<>(); + for (Material material : Crucible.VANILLA_BLOCK_HEAT_MAP.keySet()) { + HEATED_BLOCKS.add(material.getKey()); + } + + for (PylonBlockSchema schema : PylonRegistry.BLOCKS) { + if (Crucible.HeatedBlock.class.isAssignableFrom(schema.getBlockClass())) { + HEATED_BLOCKS.add(schema.getKey()); + } + } + } + + return HEATED_BLOCKS; + } + + public static List getHeatSources() { + if (HEAT_SOURCES == null) { + HEAT_SOURCES = new ArrayList<>(); + for (NamespacedKey key : getHeatedBlocks()) { + HEAT_SOURCES.add(PylonUtils.itemFromKey(key)); + } + } + + return HEAT_SOURCES; + } + + public static boolean isValid(ItemStack item) { + for(var entry : CrucibleRecipe.RECIPE_TYPE.getRecipes()) { + if (entry.matches(item)) { + return true; + } + } + + return false; + } + + @Override + public @NotNull Gui display() { + + return Gui.normal() + .setStructure( + "# # # # # # # # #", + "# # # # i # # # #", + "# # # # m # # o #", + "# # # # h # # # #", + "# # # # # # # # #" + ) + .addIngredient('#', GuiItems.backgroundBlack()) + .addIngredient('i', ItemButton.from(input)) + .addIngredient('m', ItemButton.from(BaseItems.CRUCIBLE)) + .addIngredient('h', new ItemButton(getHeatSources())) + .addIngredient('o', new FluidButton(output.amountMillibuckets(), output.fluid()) + ).build(); + } + + public boolean matches(ItemStack inputItem) { + return input.matches(inputItem); + } + + @Override + public @NotNull NamespacedKey getKey() { + return key; + } +} diff --git a/src/main/resources/lang/en.yml b/src/main/resources/lang/en.yml index 88b7e6d12..32fa11776 100644 --- a/src/main/resources/lang/en.yml +++ b/src/main/resources/lang/en.yml @@ -573,6 +573,15 @@ item: Capacity: %capacity% waila: "Mixing Pot | %bar% (%fluid%)" + crucible: + name: "Crucible" + lore: |- + Place items in the crucible using right click + Can hold only one item type at a time + The items will melt depending on the heat produced by the block below the cauldron + Requires a heat source underneath to work + waila: "Crucible %item_info% %liquid_info%" + monster_jerky: name: "Monster Jerky" lore: |- @@ -1477,6 +1486,13 @@ waila: fluid_tank: empty: "(empty)" filled: "| %bar%/%capacity% of %fluid%" + crucible: + item: + empty: "| No items |" + stored: "| x%amount% %type% |" + liquid: + empty: "No fluid" + filled: "%bar% (%fluid%)" fluid_strainer: straining: " | Straining %item% | %bars%" pit_kiln: "Pit Kiln | %time% left" diff --git a/src/main/resources/recipes/minecraft/crafting_shaped.yml b/src/main/resources/recipes/minecraft/crafting_shaped.yml index c154809cb..b702e3159 100644 --- a/src/main/resources/recipes/minecraft/crafting_shaped.yml +++ b/src/main/resources/recipes/minecraft/crafting_shaped.yml @@ -346,6 +346,16 @@ pylonbase:mixing_pot: result: pylonbase:mixing_pot category: building +pylonbase:crucible: + pattern: + - "C C" + - "C C" + - "CCC" + key: + C: pylonbase:refractory_brick + result: pylonbase:crucible + category: building + pylonbase:enriched_soul_soil: pattern: - " S " @@ -1519,6 +1529,7 @@ pylonbase:hydraulic_refueling_station: result: pylonbase:hydraulic_refueling_station category: building + pylonbase:reactivated_wither_skull: pattern: - " N " diff --git a/src/main/resources/recipes/pylonbase/crucible.yml b/src/main/resources/recipes/pylonbase/crucible.yml new file mode 100644 index 000000000..6dca686ca --- /dev/null +++ b/src/main/resources/recipes/pylonbase/crucible.yml @@ -0,0 +1,29 @@ +pylonbase:cobblestone_to_lava: + input-item: + minecraft:cobblestone: 1 + output: + pylonbase:lava: 100 + +pylonbase:cobbled_deepslate_to_lava: + input-item: + minecraft:cobbled_deepslate: 1 + output: + pylonbase:lava: 200 + +pylonbase:nether_blocks_to_lava: + input-item: + "#minecraft:base_stone_nether": 1 + output: + pylonbase:lava: 250 + +pylonbase:obsidian_to_lava: + input-item: + minecraft:obsidian: 1 + output: + pylonbase:lava: 1000 + +pylonbase:leaves_to_water: + input-item: + "#minecraft:leaves": 1 + output: + pylonbase:water: 50 \ No newline at end of file diff --git a/src/main/resources/settings/crucible.yml b/src/main/resources/settings/crucible.yml new file mode 100644 index 000000000..856100815 --- /dev/null +++ b/src/main/resources/settings/crucible.yml @@ -0,0 +1,11 @@ +capacity: 64 +smelt-time: 60 +vanilla-block-heat-map: + torch: 1 + redstone_torch: 1 + copper_torch: 1 + soul_torch: 2 + magma_block: 10 + fire: 10 + soul_fire: 15 + lava: 20 \ No newline at end of file