Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4b12c07
Add crucible recipe and block
Intybyte Nov 14, 2025
f2110c1
Add names and fancy stuff
Intybyte Nov 14, 2025
53d9886
Use RecipeInput.Item instead
Intybyte Nov 14, 2025
b1211ab
Add recipes
Intybyte Nov 14, 2025
bd52e22
Add crucible's recipe
Intybyte Nov 14, 2025
1e9ed7a
Make Crucible a tickable that works with one item at a time
Intybyte Nov 20, 2025
eda0063
Add a bunch more torches
Intybyte Nov 20, 2025
a6a5212
cleanup
Intybyte Nov 21, 2025
2b18932
No need to recreate it everytime
Intybyte Nov 21, 2025
cbcf701
Cleanup constructor
Intybyte Nov 21, 2025
5e8744b
Extract method
Intybyte Nov 21, 2025
31db01d
Properly display heat sources
Intybyte Nov 21, 2025
64b93b2
Heatable registry and better handling
Intybyte Nov 30, 2025
56dab3e
Cleanup
Intybyte Dec 12, 2025
84089c5
Relocate utility methods
Intybyte Dec 12, 2025
eedb81e
Add fallback map
Intybyte Dec 12, 2025
c01b11b
Address review
Intybyte Dec 14, 2025
4cf13d4
Use PylonUtils#setNullable
Intybyte Dec 14, 2025
28108bc
Nuke old stack system
Intybyte Dec 20, 2025
a55b843
Show stored items
Intybyte Dec 20, 2025
62ab108
Optimize imports
Intybyte Dec 20, 2025
7ef488f
Nuke heat manager
Intybyte Dec 30, 2025
5b6c5ae
Naming convention
Intybyte Dec 30, 2025
e2e8b97
Move method
Intybyte Dec 30, 2025
dc595f5
Remove multiblock
Intybyte Dec 30, 2025
09582df
Inline getBelow
Intybyte Dec 30, 2025
2ccabfb
Update lore
Intybyte Dec 30, 2025
a813370
Use Refractory Brick
Intybyte Dec 30, 2025
97e8c1a
Swap particles
Intybyte Dec 30, 2025
d446068
Rename to itemFromKey
Intybyte Dec 30, 2025
09e8569
Floating point magic
Intybyte Dec 30, 2025
c5d9729
More error info
Intybyte Dec 30, 2025
eb020f3
Merge branch 'master' into vaan/feature/crucible
Intybyte Dec 30, 2025
079ee8c
Fix merge
Intybyte Dec 30, 2025
b9843ff
Address review
Intybyte Dec 30, 2025
87fbc00
Merge branch 'master' into vaan/feature/crucible
Intybyte Dec 30, 2025
dfb2fbd
Extract to PylonUtils
Intybyte Dec 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/java/io/github/pylonmc/pylon/base/BaseBlocks.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 7 additions & 1 deletion src/main/java/io/github/pylonmc/pylon/base/BaseItems.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/main/java/io/github/pylonmc/pylon/base/BaseKeys.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Material, Integer> VANILLA_BLOCK_HEAT_MAP = Settings.get(BaseKeys.CRUCIBLE).getOrThrow("vanilla-block-heat-map", ConfigAdapter.MAP.from(ConfigAdapter.MATERIAL, ConfigAdapter.INT));
public static final Set<Material> 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<ItemStack> 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
}
Loading
Loading