diff --git a/src/main/java/io/github/pylonmc/pylon/PylonBlocks.java b/src/main/java/io/github/pylonmc/pylon/PylonBlocks.java index 628cf83ec..98b6f81f9 100644 --- a/src/main/java/io/github/pylonmc/pylon/PylonBlocks.java +++ b/src/main/java/io/github/pylonmc/pylon/PylonBlocks.java @@ -175,6 +175,8 @@ public static void initialize() { RebarBlock.register(PylonKeys.ASSEMBLY_TABLE, Material.ANVIL, AssemblyTable.class); RebarBlock.register(PylonKeys.CREATIVE_ITEM_SOURCE, Material.STRUCTURE_VOID, CreativeItemSource.class); RebarBlock.register(PylonKeys.CREATIVE_ITEM_VOIDER, Material.STRUCTURE_VOID, CreativeItemVoider.class); + RebarBlock.register(PylonKeys.POTION_PEDESTAL, Material.END_STONE_BRICK_WALL, PotionPedestal.class); + RebarBlock.register(PylonKeys.POTION_ALTAR, Material.STONE_BRICK_SLAB, PotionAltar.class); RebarBlock.register(PylonKeys.COLLIMATOR, Material.OBSIDIAN, Collimator.class); RebarBlock.register(PylonKeys.COLLIMATOR_PILLAR, Material.DEEPSLATE_TILE_WALL, CollimatorPillar.class); RebarBlock.register(PylonKeys.WOODEN_SILO, Material.BROWN_TERRACOTTA, Silo.class); diff --git a/src/main/java/io/github/pylonmc/pylon/PylonItems.java b/src/main/java/io/github/pylonmc/pylon/PylonItems.java index 287690238..3b741f101 100644 --- a/src/main/java/io/github/pylonmc/pylon/PylonItems.java +++ b/src/main/java/io/github/pylonmc/pylon/PylonItems.java @@ -3244,6 +3244,42 @@ private PylonItems() { // + public static final ItemStack POTION_PEDESTAL = ItemStackBuilder.rebar(Material.END_STONE_BRICK_WALL, PylonKeys.POTION_PEDESTAL) + .build(); + static { + RebarItem.register(RebarItem.class, POTION_PEDESTAL, PylonKeys.POTION_PEDESTAL); + PylonPages.SIMPLE_MACHINES.addItem(POTION_PEDESTAL); + } + + public static final ItemStack POTION_ALTAR = ItemStackBuilder.rebar(Material.STONE_BRICK_SLAB, PylonKeys.POTION_ALTAR) + .build(); + static { + RebarItem.register(PotionAltar.Item.class, POTION_ALTAR, PylonKeys.POTION_ALTAR); + PylonPages.SIMPLE_MACHINES.addItem(POTION_ALTAR); + } + + public static final ItemStack ASCENDANT_EMBER = ItemStackBuilder.rebar(Material.BLAZE_POWDER, PylonKeys.ASCENDANT_EMBER) + .build(); + static { + RebarItem.register(AscendantEmber.class, ASCENDANT_EMBER, PylonKeys.ASCENDANT_EMBER); + PylonPages.MAGIC.addItem(ASCENDANT_EMBER); + } + + public static final ItemStack CHRONICLE_RESIN = ItemStackBuilder.rebar(Material.RESIN_CLUMP, PylonKeys.CHRONICLE_RESIN) + .build(); + static { + RebarItem.register(ChronicleResin.class, CHRONICLE_RESIN, PylonKeys.CHRONICLE_RESIN); + PylonPages.MAGIC.addItem(CHRONICLE_RESIN); + } + + public static final ItemStack EON_WEAVE_CRYSTAL = ItemStackBuilder.rebar(Material.CLAY_BALL, PylonKeys.EON_WEAVE_CRYSTAL) + .set(DataComponentTypes.ITEM_MODEL, Material.END_CRYSTAL.getKey()) + .build(); + static { + RebarItem.register(EonWeaveCrystal.class, EON_WEAVE_CRYSTAL, PylonKeys.EON_WEAVE_CRYSTAL); + PylonPages.MAGIC.addItem(EON_WEAVE_CRYSTAL); + } + public static final ItemStack CLEANSING_POTION = ItemStackBuilder.rebar(Material.SPLASH_POTION, PylonKeys.CLEANSING_POTION) .set(DataComponentTypes.POTION_CONTENTS, PotionContents.potionContents() .customColor(Color.FUCHSIA) diff --git a/src/main/java/io/github/pylonmc/pylon/PylonKeys.java b/src/main/java/io/github/pylonmc/pylon/PylonKeys.java index 138b05bce..a10d12adf 100644 --- a/src/main/java/io/github/pylonmc/pylon/PylonKeys.java +++ b/src/main/java/io/github/pylonmc/pylon/PylonKeys.java @@ -463,6 +463,11 @@ public class PylonKeys { public static final NamespacedKey FLUID_TANK_CASING_PALLADIUM = pylonKey("fluid_tank_casing_palladium"); public static final NamespacedKey PORTABLE_FLUID_TANK_PALLADIUM = pylonKey("portable_fluid_tank_palladium"); + public static final NamespacedKey POTION_PEDESTAL = pylonKey("potion_pedestal"); + public static final NamespacedKey POTION_ALTAR = pylonKey("potion_altar"); + public static final NamespacedKey ASCENDANT_EMBER = pylonKey("ascendant_ember"); + public static final NamespacedKey CHRONICLE_RESIN = pylonKey("chronicle_resin"); + public static final NamespacedKey EON_WEAVE_CRYSTAL = pylonKey("eon_weave_crystal"); public static final NamespacedKey SILO_CONVERTER = pylonKey("silo_converter"); public static final NamespacedKey WOODEN_SILO = pylonKey("wooden_silo"); public static final NamespacedKey COPPER_SILO = pylonKey("copper_silo"); diff --git a/src/main/java/io/github/pylonmc/pylon/content/building/Pedestal.java b/src/main/java/io/github/pylonmc/pylon/content/building/Pedestal.java index 3ce6dfe34..8dd51fab6 100644 --- a/src/main/java/io/github/pylonmc/pylon/content/building/Pedestal.java +++ b/src/main/java/io/github/pylonmc/pylon/content/building/Pedestal.java @@ -103,6 +103,7 @@ public void onInteract(@NotNull PlayerInteractEvent event, @NotNull EventPriorit // drop old item ItemStack oldStack = display.getItemStack(); ItemStack newStack = event.getItem(); + if (!oldStack.getType().isAir()) { player.give(oldStack); display.setItemStack(null); @@ -111,12 +112,26 @@ public void onInteract(@NotNull PlayerInteractEvent event, @NotNull EventPriorit // insert new item if (newStack != null) { + if (isIllegalItem(event.getPlayer(), newStack)) { + return; + } + ItemStack stackToInsert = newStack.asQuantity(1); display.setItemStack(stackToInsert); newStack.subtract(); } } + /** + * This method is called when an item is inserted into the pedestal. It can be used to check if the item is + * allowed to be inserted. + * + * @return true if the item can be inserted into the pedestal, false otherwise + */ + public boolean isIllegalItem(Player player, ItemStack stack) { + return false; + } + @Override public void onBreak(@NotNull List drops, @NotNull BlockBreakContext context) { drops.add(getItemDisplay().getItemStack()); diff --git a/src/main/java/io/github/pylonmc/pylon/content/machines/simple/PotionAltar.java b/src/main/java/io/github/pylonmc/pylon/content/machines/simple/PotionAltar.java new file mode 100644 index 000000000..f3d0f5225 --- /dev/null +++ b/src/main/java/io/github/pylonmc/pylon/content/machines/simple/PotionAltar.java @@ -0,0 +1,579 @@ +package io.github.pylonmc.pylon.content.machines.simple; + +import com.destroystokyo.paper.ParticleBuilder; +import io.github.pylonmc.pylon.Pylon; +import io.github.pylonmc.pylon.PylonKeys; +import io.github.pylonmc.pylon.content.building.Pedestal; +import io.github.pylonmc.pylon.content.tools.base.PotionCatalyst; +import io.github.pylonmc.pylon.util.HslColor; +import io.github.pylonmc.pylon.util.PylonUtils; +import io.github.pylonmc.rebar.block.BlockStorage; +import io.github.pylonmc.rebar.block.RebarBlock; +import io.github.pylonmc.rebar.block.base.RebarBreakHandler; +import io.github.pylonmc.rebar.block.base.RebarDirectionalBlock; +import io.github.pylonmc.rebar.block.base.RebarInteractBlock; +import io.github.pylonmc.rebar.block.base.RebarRecipeProcessor; +import io.github.pylonmc.rebar.block.base.RebarSimpleMultiblock; +import io.github.pylonmc.rebar.block.base.RebarTickingBlock; +import io.github.pylonmc.rebar.block.context.BlockBreakContext; +import io.github.pylonmc.rebar.block.context.BlockCreateContext; +import io.github.pylonmc.rebar.config.adapter.ConfigAdapter; +import io.github.pylonmc.rebar.datatypes.RebarSerializers; +import io.github.pylonmc.rebar.entity.display.BlockDisplayBuilder; +import io.github.pylonmc.rebar.entity.display.transform.TransformBuilder; +import io.github.pylonmc.rebar.event.api.annotation.MultiHandler; +import io.github.pylonmc.rebar.i18n.RebarArgument; +import io.github.pylonmc.rebar.item.RebarItem; +import io.github.pylonmc.rebar.waila.WailaDisplay; +import io.papermc.paper.datacomponent.DataComponentTypes; +import io.papermc.paper.datacomponent.item.PotionContents; +import lombok.Getter; +import net.kyori.adventure.sound.Sound; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import org.bukkit.Bukkit; +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Particle; +import org.bukkit.Vibration; +import org.bukkit.Vibration.Destination.BlockDestination; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.joml.Vector3i; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; + +/** + * @author balugaq + */ +public class PotionAltar extends RebarBlock + implements RebarSimpleMultiblock, RebarInteractBlock, RebarTickingBlock, RebarDirectionalBlock, RebarBreakHandler { + + private static final NamespacedKey RECIPE_TICKS_REMAINING_KEY = PylonUtils.pylonKey("potion_altar_recipe_ticks_remaining"); + private static final MultiblockComponent SHIMMER_PEDESTAL_COMPONENT = new RebarMultiblockComponent(PylonKeys.SHIMMER_PEDESTAL); + private static final MultiblockComponent POTION_PEDESTAL_COMPONENT = new RebarMultiblockComponent(PylonKeys.POTION_PEDESTAL); + private static final MultiblockComponent LIT_ORANGE_CANDLE_COMPONENT = new VanillaBlockdataMultiblockComponent(Material.ORANGE_CANDLE.createBlockData("[lit=true]")); + private final Sound START_SOUND = getSettings().getOrThrow("sound.start", ConfigAdapter.SOUND); + private final Sound FINISH_SOUND = getSettings().getOrThrow("sound.finish", ConfigAdapter.SOUND); + private final Sound CANCEL_SOUND = getSettings().getOrThrow("sound.cancel", ConfigAdapter.SOUND); + private final Sound PROCESSING_SOUND = getSettings().getOrThrow("sound.processing", ConfigAdapter.SOUND); + private final Sound CANNOT_APPLY_CATALYST_SOUND = getSettings().getOrThrow("sound.cannot_apply_catalyst", ConfigAdapter.SOUND); + private final Sound FAILED_APPLY_CATALYST_SOUND = getSettings().getOrThrow("sound.failed_apply_catalyst", ConfigAdapter.SOUND); + + private final int tickInterval = getSettings().getOrThrow("tick-interval", ConfigAdapter.INTEGER); + private final int recipeTimeTicks = getSettings().getOrThrow("recipe-time-ticks", ConfigAdapter.INTEGER); + private final int maxEffectTypes = getSettings().getOrThrow("max-effect-types", ConfigAdapter.INTEGER); + private int ticked = 0; + private @Nullable Player interactor; + private @Nullable AltarProgress altarProgress; + + /** + * @author balugaq + */ + public static class Item extends RebarItem { + private final int maxEffectTypes = getSettings().getOrThrow("max-effect-types", ConfigAdapter.INTEGER); + + public Item(@NotNull ItemStack stack) { + super(stack); + } + + @Override + public @NotNull List getPlaceholders() { + return List.of( + RebarArgument.of("max-effect-types", maxEffectTypes) + ); + } + } + + @SuppressWarnings("unused") + public PotionAltar(Block block, BlockCreateContext context) { + super(block, context); + + setTickInterval(tickInterval); + setFacing(context.getFacing()); + setMultiblockDirection(getFacing()); + + addEntity("brewing_stand", new BlockDisplayBuilder() + .transformation(new TransformBuilder() + .translate(0, 0.5, 0) + .scale(0.9) + .buildForBlockDisplay() + ) + .blockData(Material.BREWING_STAND.createBlockData()) + .build(getBlock().getLocation().toCenterLocation()) + ); + } + + @SuppressWarnings("unused") + public PotionAltar(Block block, PersistentDataContainer pdc) { + super(block, pdc); + // recover the recipe + int lastRecipeTicks = pdc.getOrDefault(RECIPE_TICKS_REMAINING_KEY, PersistentDataType.INTEGER, -1); + if (lastRecipeTicks > 0) { + Bukkit.getScheduler().runTaskLater(Pylon.getInstance(), () -> { + altarProgress = tryStartProgress(null); + if (altarProgress != null) { + altarProgress.ticksRemaining = lastRecipeTicks; + } + pdc.remove(RECIPE_TICKS_REMAINING_KEY); + }, 20L); + } + } + + @Override + public @NotNull Map getComponents() { + Map map = new LinkedHashMap<>(); + map.put(new Vector3i(1, 0, 0), LIT_ORANGE_CANDLE_COMPONENT); + map.put(new Vector3i(2, 0, 0), POTION_PEDESTAL_COMPONENT); + map.put(new Vector3i(-1, 0, 0), LIT_ORANGE_CANDLE_COMPONENT); + map.put(new Vector3i(-2, 0, 0), POTION_PEDESTAL_COMPONENT); + map.put(new Vector3i(0, 0, 2), SHIMMER_PEDESTAL_COMPONENT); + return map; + } + + private boolean isInvalidRecipe(@NotNull ItemStack potion1, @NotNull ItemStack potion2, @Nullable PotionCatalyst catalyst, @Nullable Player interactor) { + if (catalyst == null) { + // 2 potions + if (isInvalidPotion(potion1) || isInvalidPotion(potion2)) { + sendMessage("invalid-potion"); + return true; + } + + if (potion1.getType() != potion2.getType()) { + sendMessage("not-same-type"); + return true; + } + } else { + if (isInvalidPotion(potion1) && isInvalidPotion(potion2)) { + // 1 potion + catalyst + // both potions are invalid + sendMessage("invalid-potion"); + return true; + } + } + return false; + } + + @NotNull + private Color mixColor(@Nullable PotionContents contents1, @Nullable PotionContents contents2) { + Color mixedColor; + if (contents1 == null || contents2 == null) { + mixedColor = contents1 != null ? contents1.computeEffectiveColor() : contents2.computeEffectiveColor(); + } else { + + HslColor color1 = HslColor.fromRgb(contents1.computeEffectiveColor()); + HslColor color2 = HslColor.fromRgb(contents2.computeEffectiveColor()); + + mixedColor = new HslColor( + (color1.hue() + color2.hue()) / 2, + (color1.saturation() + color2.saturation()) / 2, + (color1.lightness() + color2.lightness()) / 2 + ).toRgb(); + } + return mixedColor; + } + + private boolean isProcessingRecipe() { + return altarProgress != null; + } + + @Override @MultiHandler(priorities = { EventPriority.NORMAL, EventPriority.MONITOR }) + public void onInteract(@NotNull PlayerInteractEvent event, @NotNull EventPriority priority) { + if (event.getPlayer().isSneaking() + || event.getHand() != EquipmentSlot.HAND + || event.getAction() != Action.RIGHT_CLICK_BLOCK + || event.useInteractedBlock() == Event.Result.DENY + ) { + return; + } + + if (priority == EventPriority.NORMAL) { + event.setUseItemInHand(Event.Result.DENY); + return; + } + + if (!isFormedAndFullyLoaded() || isProcessingRecipe()) { + return; + } + + // start recipe + interactor = event.getPlayer(); + AltarProgress recipe = tryStartProgress(event.getPlayer()); + if (recipe == null) { + return; + } + altarProgress = recipe; + playSound(START_SOUND); + } + + private @Nullable AltarProgress tryStartProgress(@Nullable Player interactor) { + if (getPotionPedestal1() == null || getPotionPedestal2() == null || getCatalystPedestal() == null) { + return null; + } + + ItemStack potion1 = getPotionPedestal1().getItemDisplay().getItemStack(); + ItemStack potion2 = getPotionPedestal2().getItemDisplay().getItemStack(); + ItemStack catalystItem = getCatalystPedestal().getItemDisplay().getItemStack(); + RebarItem rebar = RebarItem.fromStack(catalystItem); + @Nullable PotionCatalyst catalyst = null; + if (!catalystItem.getType().isAir()) { + if (rebar instanceof PotionCatalyst clyst) { + catalyst = clyst; + } else { + // invalid catalyst + sendMessage("invalid-catalyst"); + return null; + } + } + + // Player could use the altar with: + // 1 potion + catalyst, + // 2 potions, + // 2 potions + catalyst + // since catalyst is not required. + if (isInvalidRecipe(potion1, potion2, catalyst, interactor)) { + return null; + } + + PotionContents contents1 = potion1.getData(DataComponentTypes.POTION_CONTENTS); + PotionContents contents2 = potion2.getData(DataComponentTypes.POTION_CONTENTS); + if (contents1 == null && contents2 == null) { + sendMessage("invalid-potion"); + return null; + } + + // attempt to start recipe + for (Pedestal pedestal : getAllPedestals()) { + if (pedestal != null) { + pedestal.setLocked(true); + } + } + + Map effects = new HashMap<>(); + if (contents1 != null) fuseEffects(effects, contents1.allEffects()); + if (contents2 != null) fuseEffects(effects, contents2.allEffects()); + if (effects.size() > maxEffectTypes) { + sendMessage("too-many-effects", RebarArgument.of("max_effect_types", maxEffectTypes)); + return null; + } + boolean catalystApplied = true; + if (catalyst != null) { + if (!catalyst.apply(effects)) { + catalystApplied = false; + } + } + + Color mixedColor = mixColor(contents1, contents2); + + PotionContents contents = PotionContents.potionContents().addCustomEffects(effects.values().stream().toList()).customColor(mixedColor).build(); + ItemStack fusedPotion = potion1.clone(); + fusedPotion.setData(DataComponentTypes.ITEM_NAME, Component.translatable("pylon.message.potion_altar.fused-potion-name")); + fusedPotion.setData(DataComponentTypes.POTION_CONTENTS, contents); + + return new AltarProgress(catalyst, fusedPotion, recipeTimeTicks, catalystApplied); + } + + private boolean isInvalidPotion(@NotNull ItemStack potion) { + return RebarItem.isRebarItem(potion) + || !potion.hasData(DataComponentTypes.POTION_CONTENTS) + || potion.getData(DataComponentTypes.POTION_CONTENTS).allEffects().isEmpty(); + } + + private void fuseEffects(@NotNull Map origin, @NotNull List effects) { + for (PotionEffect effect : effects) { + if (origin.containsKey(effect.getType())) { + origin.compute(effect.getType(), (k, originEffect) -> new PotionEffect( + effect.getType(), + originEffect.getDuration() + effect.getDuration(), + Math.max(originEffect.getAmplifier(), effect.getAmplifier()), + originEffect.isAmbient() || effect.isAmbient(), + originEffect.hasParticles() || effect.hasParticles(), + originEffect.hasIcon() || effect.hasIcon() + )); + } else { + origin.put(effect.getType(), effect); + } + } + } + + private void progressRecipe(int ticks) { + if (altarProgress != null) { + altarProgress.ticksRemaining -= ticks; + if (altarProgress.ticksRemaining <= 0) { + onRecipeFinished(altarProgress); + altarProgress = null; + } + } + } + + @Override + public void tick() { + progressRecipe(tickInterval); + + if (isProcessingRecipe()) { + if (!isFormedAndFullyLoaded()) { + cancelRecipe(); + return; + } + + // flames on the candle + for (Block candle : getCandles()) { + new ParticleBuilder(Particle.FLAME) + .count(10) + .extra(0.02) + .location(candle.getLocation().toCenterLocation()) + .spawn(); + } + + new ParticleBuilder(Particle.WAX_OFF) + .location(getBlock().getLocation().toCenterLocation().add(2*Math.sin(30*Math.toRadians(ticked)), 0.25+0.25*Math.sin(30*Math.toRadians(ticked)), 2*Math.cos(30*Math.toRadians(ticked)))) + .count(50) + .extra(0.5) + .spawn(); + + new ParticleBuilder(Particle.WAX_ON) + .location(getBlock().getLocation().toCenterLocation().add(2*Math.sin(30*Math.toRadians(ticked)+Math.PI), 0.25+0.25*Math.sin(30*Math.toRadians(ticked)), 2*Math.cos(30*Math.toRadians(ticked)+Math.PI))) + .count(50) + .extra(0.5) + .spawn(); + + // sound + if (ticked % 2 == 0) { + playSound(PROCESSING_SOUND); + } + } + + ticked += 1; + + int speed = isProcessingRecipe() ? 40 : 10; + new ParticleBuilder(Particle.DRAGON_BREATH) + .count(10) + .extra(0.02) + .location(getBlock().getLocation().toCenterLocation().add(Math.sin(speed*Math.toRadians(ticked)), 0, Math.cos(speed*Math.toRadians(ticked)))) + .data(1f) + .spawn(); + + new ParticleBuilder(Particle.DUST) + .count(20) + .extra(0.02) + .location(getBlock().getLocation().toCenterLocation()) + .data(new Particle.DustOptions(Color.fromRGB(0x0055AAAA), 1)) + .spawn(); + } + + @NotNull + private List<@Nullable Pedestal> getAllPedestals() { + List pedestals = new ArrayList<>(); + pedestals.add(getPotionPedestal1()); + pedestals.add(getPotionPedestal2()); + pedestals.add(getCatalystPedestal()); + return pedestals; + } + + @NotNull + private List<@Nullable Pedestal> getPotionPedestals() { + List pedestals = new ArrayList<>(); + pedestals.add(getPotionPedestal1()); + pedestals.add(getPotionPedestal2()); + return pedestals; + } + + @Nullable + private Pedestal getPotionPedestal1() { + Vector3i offset = getBlockOffsets(POTION_PEDESTAL_COMPONENT).get(0); + return BlockStorage.getAs(Pedestal.class, getBlock().getRelative(offset.x(), offset.y(), offset.z())); + } + + @Nullable + private Pedestal getPotionPedestal2() { + Vector3i offset = getBlockOffsets(POTION_PEDESTAL_COMPONENT).get(1); + return BlockStorage.getAs(Pedestal.class, getBlock().getRelative(offset.x(), offset.y(), offset.z())); + } + + @Nullable + private Pedestal getCatalystPedestal() { + Vector3i offset = getBlockOffsets(SHIMMER_PEDESTAL_COMPONENT).getFirst(); + return BlockStorage.getAs(Pedestal.class, getBlock().getRelative(offset.x(), offset.y(), offset.z())); + } + + @NotNull + private List getBlockOffsets(@NotNull MultiblockComponent component) { + return validStructures().getFirst().entrySet().stream().filter(entry -> entry.getValue() == component).map(Map.Entry::getKey).toList(); + } + + @NotNull + private List getCandles() { + List candles = new ArrayList<>(); + for (Vector3i offset : getBlockOffsets(LIT_ORANGE_CANDLE_COMPONENT)) { + candles.add(getBlock().getRelative(offset.x(), offset.y(), offset.z())); + } + return candles; + } + + private void onRecipeFinished(@NotNull final AltarProgress recipe) { + for (Pedestal pedestal : getPotionPedestals()) { + if (pedestal != null) { + pedestal.getItemDisplay().setItemStack(null); + } + } + for (Pedestal pedestal : getAllPedestals()) { + if (pedestal != null) { + pedestal.setLocked(false); + } + } + + Location location = getBlock().getLocation().toCenterLocation(); + getBlock().getWorld().strikeLightningEffect(location); + getBlock().getWorld().dropItemNaturally(location, recipe.result); + + new ParticleBuilder(Particle.VIBRATION) + .count(40) + .extra(0.02) + .location(getBlock().getLocation().toCenterLocation()) + .data(new Vibration(new BlockDestination(getBlock().getLocation().toCenterLocation().add(0, 3, 0)), 10)) + .spawn(); + + if (recipe.catalyst == null || getCatalystPedestal() == null) { + playSound(FINISH_SOUND); + return; + } + + // catalysts + if (ThreadLocalRandom.current().nextDouble() >= recipe.catalyst.getApplicationSuccessRate()) { + sendMessage("failed-apply-catalyst"); + playSound(FAILED_APPLY_CATALYST_SOUND); + getCatalystPedestal().getItemDisplay().setItemStack(null); + return; + } + + if (!recipe.catalystApplied) { + sendMessage("cannot-apply-catalyst"); + playSound(CANNOT_APPLY_CATALYST_SOUND); + return; + } + + // succeed applying catalyst + getCatalystPedestal().getItemDisplay().setItemStack(null); + playSound(FINISH_SOUND); + } + + private void cancelRecipe() { + for (Pedestal pedestal : getAllPedestals()) { + if (pedestal != null) { + pedestal.setLocked(false); + } + } + + for (Block candle : getCandles()) { + new ParticleBuilder(Particle.CAMPFIRE_COSY_SMOKE) + .count(20) + .extra(0.05) + .location(candle.getLocation().toCenterLocation()) + .spawn(); + } + + playSound(CANCEL_SOUND); + } + + private void playSound(@NotNull Sound sound) { + getBlock().getWorld().playSound(sound, getBlock().getX() + 0.5, getBlock().getY() + 0.5, getBlock().getZ() + 0.5); + } + + private void sendMessage(@NotNull String key, RebarArgument... arguments) { + if (interactor != null) { + interactor.sendMessage(Component.translatable("pylon.message.potion_altar." + key, arguments)); + } + } + + @Override + public @Nullable WailaDisplay getWaila(@NotNull Player player) { + return new WailaDisplay(getDefaultWailaTranslationKey().arguments( + RebarArgument.of( + "processing", + altarProgress == null + ? Component.translatable("pylon.waila.potion_altar.idle") + : Component.translatable("pylon.waila.potion_altar.processing") + .arguments( + RebarArgument.of( + "bars", PylonUtils.createProgressBar( + altarProgress.timeTicks - altarProgress.ticksRemaining, + altarProgress.timeTicks, + 20, + TextColor.color(100, 255, 100) + ) + ) + ) + ) + )); + } + + /** + * Represents a variable recipe, for internal use only + * + * @author balugaq + */ + @Getter + private static class AltarProgress { + private final @Nullable PotionCatalyst catalyst; + private final @NotNull ItemStack result; + private final int timeTicks; + private final boolean catalystApplied; + private int ticksRemaining; + + /** + * Creates a new altar progress + * + * @param result + * the output (respects amount) + */ + protected AltarProgress(@Nullable PotionCatalyst catalyst, @NotNull ItemStack result, int timeTicks, boolean catalystApplied) { + this.catalyst = catalyst; + this.result = result; + this.timeTicks = timeTicks; + this.catalystApplied = catalystApplied; + this.ticksRemaining = timeTicks; + } + } + + /** + * {@link RebarRecipeProcessor} requires a unique recipe key to recover recipe progress after restarting server, + * while this altar doesn't have any static recipe to be loaded or be recovered, which produces tons of + * error logs for "Couldn't find recipe". So we have to recover recipe progress manually. + * + * @see AltarProgress + */ + @Override + public void write(@NotNull PersistentDataContainer pdc) { + if (altarProgress != null) { + pdc.set(RECIPE_TICKS_REMAINING_KEY, RebarSerializers.INTEGER, altarProgress.ticksRemaining); + } + } + + @Override + public void onBreak(@NotNull List drops, @NotNull BlockBreakContext context) { + for (Pedestal pedestal : getAllPedestals()) { + if (pedestal != null) { + pedestal.setLocked(false); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/io/github/pylonmc/pylon/content/machines/simple/PotionPedestal.java b/src/main/java/io/github/pylonmc/pylon/content/machines/simple/PotionPedestal.java new file mode 100644 index 000000000..5230f1d01 --- /dev/null +++ b/src/main/java/io/github/pylonmc/pylon/content/machines/simple/PotionPedestal.java @@ -0,0 +1,35 @@ +package io.github.pylonmc.pylon.content.machines.simple; + +import io.github.pylonmc.pylon.content.building.Pedestal; +import io.github.pylonmc.pylon.util.PylonUtils; +import io.github.pylonmc.rebar.block.context.BlockCreateContext; +import net.kyori.adventure.text.Component; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataContainer; +import org.jetbrains.annotations.NotNull; + + +/** + * @author balugaq + */ +public class PotionPedestal extends Pedestal { + public PotionPedestal(@NotNull final Block block, @NotNull final BlockCreateContext context) { + super(block, context); + } + + public PotionPedestal(@NotNull final Block block, @NotNull final PersistentDataContainer pdc) { + super(block, pdc); + } + + @Override + public boolean isIllegalItem(@NotNull final Player player, @NotNull final ItemStack stack) { + if (!PylonUtils.isPotion(stack.getType())) { + player.sendMessage(Component.translatable("pylon.message.potion_pedestal.not-potion")); + return true; + } + + return false; + } +} diff --git a/src/main/java/io/github/pylonmc/pylon/content/tools/AscendantEmber.java b/src/main/java/io/github/pylonmc/pylon/content/tools/AscendantEmber.java new file mode 100644 index 000000000..1045f2a68 --- /dev/null +++ b/src/main/java/io/github/pylonmc/pylon/content/tools/AscendantEmber.java @@ -0,0 +1,50 @@ +package io.github.pylonmc.pylon.content.tools; + +import io.github.pylonmc.pylon.content.tools.base.PotionCatalyst; +import io.github.pylonmc.rebar.config.adapter.ConfigAdapter; +import io.github.pylonmc.rebar.i18n.RebarArgument; +import io.github.pylonmc.rebar.item.RebarItem; +import io.github.pylonmc.rebar.util.gui.unit.UnitFormat; +import org.bukkit.inventory.ItemStack; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; + +/** + * @author balugaq + */ +public class AscendantEmber extends RebarItem implements PotionCatalyst { + private final int maxAmplifier = getSettings().getOrThrow("max-amplifier", ConfigAdapter.INTEGER); + private final double durationShortenRate = getSettings().getOrThrow("duration-shorten-rate", ConfigAdapter.DOUBLE); + private final double applicationSuccessRate = getSettings().getOrThrow("application-success-rate", ConfigAdapter.DOUBLE); + + public AscendantEmber(final @NotNull ItemStack stack) { + super(stack); + } + + @Override + public boolean apply(final @NotNull Map effects) { + // randomly choose one type + PotionEffectType type = effects.keySet().stream().toList().get(ThreadLocalRandom.current().nextInt(effects.size())); + PotionEffect effect = effects.get(type); + if (effect.getAmplifier() > maxAmplifier) { + return false; + } + + effects.put(type, effect.withAmplifier(effect.getAmplifier() + 1).withDuration((int) (effect.getDuration() * durationShortenRate))); + return true; + } + + @Override + public @NotNull List<@NotNull RebarArgument> getPlaceholders() { + return List.of( + RebarArgument.of("max-amplifier", maxAmplifier), + RebarArgument.of("duration-shorten-rate", UnitFormat.PERCENT.format(durationShortenRate * 100).decimalPlaces(2)), + RebarArgument.of("application-success-rate", UnitFormat.PERCENT.format(applicationSuccessRate * 100).decimalPlaces(2)) + ); + } +} diff --git a/src/main/java/io/github/pylonmc/pylon/content/tools/ChronicleResin.java b/src/main/java/io/github/pylonmc/pylon/content/tools/ChronicleResin.java new file mode 100644 index 000000000..402841d46 --- /dev/null +++ b/src/main/java/io/github/pylonmc/pylon/content/tools/ChronicleResin.java @@ -0,0 +1,48 @@ +package io.github.pylonmc.pylon.content.tools; + +import io.github.pylonmc.pylon.content.tools.base.PotionCatalyst; +import io.github.pylonmc.rebar.config.adapter.ConfigAdapter; +import io.github.pylonmc.rebar.i18n.RebarArgument; +import io.github.pylonmc.rebar.item.RebarItem; +import io.github.pylonmc.rebar.util.gui.unit.UnitFormat; +import org.bukkit.inventory.ItemStack; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; + +/** + * @author balugaq + */ +public class ChronicleResin extends RebarItem implements PotionCatalyst { + private final int durationBoostSeconds = getSettings().getOrThrow("duration-boost-seconds", ConfigAdapter.INTEGER); + private final double applicationSuccessRate = getSettings().getOrThrow("application-success-rate", ConfigAdapter.DOUBLE); + + public ChronicleResin(final @NotNull ItemStack stack) { + super(stack); + } + + @Override + public boolean apply(final @NotNull Map effects) { + // randomly choose one type + PotionEffectType type = effects.keySet().stream().toList().get(ThreadLocalRandom.current().nextInt(effects.size())); + PotionEffect effect = effects.get(type); + if (effect.isInfinite()) { + return false; + } + + effects.put(type, effect.withDuration(effect.getDuration() + durationBoostSeconds * 20)); + return true; + } + + @Override + public @NotNull List<@NotNull RebarArgument> getPlaceholders() { + return List.of( + RebarArgument.of("duration-boost-seconds", UnitFormat.SECONDS.format(durationBoostSeconds)), + RebarArgument.of("application-success-rate", UnitFormat.PERCENT.format(applicationSuccessRate * 100).decimalPlaces(2)) + ); + } +} diff --git a/src/main/java/io/github/pylonmc/pylon/content/tools/EonWeaveCrystal.java b/src/main/java/io/github/pylonmc/pylon/content/tools/EonWeaveCrystal.java new file mode 100644 index 000000000..16260c2a0 --- /dev/null +++ b/src/main/java/io/github/pylonmc/pylon/content/tools/EonWeaveCrystal.java @@ -0,0 +1,48 @@ +package io.github.pylonmc.pylon.content.tools; + +import io.github.pylonmc.pylon.content.tools.base.PotionCatalyst; +import io.github.pylonmc.rebar.config.adapter.ConfigAdapter; +import io.github.pylonmc.rebar.i18n.RebarArgument; +import io.github.pylonmc.rebar.item.RebarItem; +import io.github.pylonmc.rebar.util.gui.unit.UnitFormat; +import org.bukkit.inventory.ItemStack; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; + +/** + * @author balugaq + */ +public class EonWeaveCrystal extends RebarItem implements PotionCatalyst { + private final double multipleRate = getSettings().getOrThrow("multiple-rate", ConfigAdapter.DOUBLE); + private final double applicationSuccessRate = getSettings().getOrThrow("application-success-rate", ConfigAdapter.DOUBLE); + + public EonWeaveCrystal(final @NotNull ItemStack stack) { + super(stack); + } + + @Override + public boolean apply(final @NotNull Map effects) { + // randomly choose one type + PotionEffectType type = effects.keySet().stream().toList().get(ThreadLocalRandom.current().nextInt(effects.size())); + PotionEffect effect = effects.get(type); + if (effect.isInfinite()) { + return false; + } + + effects.put(type, effect.withDuration((int) (effect.getDuration() * multipleRate))); + return true; + } + + @Override + public @NotNull List<@NotNull RebarArgument> getPlaceholders() { + return List.of( + RebarArgument.of("multiple-rate", multipleRate), + RebarArgument.of("application-success-rate", UnitFormat.PERCENT.format(applicationSuccessRate * 100).decimalPlaces(2)) + ); + } +} diff --git a/src/main/java/io/github/pylonmc/pylon/content/tools/base/PotionCatalyst.java b/src/main/java/io/github/pylonmc/pylon/content/tools/base/PotionCatalyst.java new file mode 100644 index 000000000..f4f97f9e3 --- /dev/null +++ b/src/main/java/io/github/pylonmc/pylon/content/tools/base/PotionCatalyst.java @@ -0,0 +1,38 @@ +package io.github.pylonmc.pylon.content.tools.base; + +import io.github.pylonmc.pylon.content.machines.simple.PotionAltar; +import io.github.pylonmc.pylon.content.tools.AscendantEmber; +import io.github.pylonmc.rebar.config.Config; +import io.github.pylonmc.rebar.config.adapter.ConfigAdapter; +import io.github.pylonmc.rebar.item.RebarItem; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +/** + * A catalyst for modifying potion effects in {@link PotionAltar} result potion. + * + * @author balugaq + * @see AscendantEmber + */ +public interface PotionCatalyst { + /** + * Applies special features to the potion effects. + * @param effects the potion effects to be handled, linked to the result potion directly. + * @return whether the effects were applied successfully. + */ + boolean apply(@NotNull Map effects); + + @NotNull + Config getSettings(); + + /** + * `application-success-rate` (double type) is required to check the success rate of the catalyst. + * @see RebarItem#getSettings() + */ + default double getApplicationSuccessRate() { + return getSettings().get("application-success-rate", ConfigAdapter.DOUBLE, 0.0D); + } +} diff --git a/src/main/java/io/github/pylonmc/pylon/util/PylonUtils.java b/src/main/java/io/github/pylonmc/pylon/util/PylonUtils.java index e92e4e3f3..11d72958f 100644 --- a/src/main/java/io/github/pylonmc/pylon/util/PylonUtils.java +++ b/src/main/java/io/github/pylonmc/pylon/util/PylonUtils.java @@ -232,6 +232,10 @@ public static ItemStack itemFromKey(NamespacedKey key) { return stack; } + public boolean isPotion(Material material) { + return material == Material.POTION || material == Material.SPLASH_POTION || material == Material.LINGERING_POTION; + } + /** * Handles players right clicking with bottles, water buckets, etc * Returns true if the function attempted to process the item used (i.e. if it's a water bucket, bottle, etc) diff --git a/src/main/resources/lang/en.yml b/src/main/resources/lang/en.yml index 450c8a44c..61dfd2546 100644 --- a/src/main/resources/lang/en.yml +++ b/src/main/resources/lang/en.yml @@ -588,7 +588,7 @@ item: lore: |- Multiblock A self-contained structure for performing shimmer rituals - Add items on the pedestals, then right click the altar with the catalyst to begin the ritual + Add items on the pedestals, then right click the altar with the catalyst to begin the ritual Components 1x @@ -601,6 +601,53 @@ item: Right click to set the displayed item Shift right click to rotate the item + potion_altar: + name: "Potion Altar" + lore: |- + Multiblock + Allows potions to be fused and upgraded + Add potions on the potion pedestals or add a more potion catalyst on the shimmer pedestal, then right click the altar to begin the ritual + Max effect types: %max-effect-types% + + Components + 1x + 2x Lit Orange Candle + 2x + 2x + waila: "Potion Altar%processing%" + + potion_pedestal: + name: "Potion Pedestal" + lore: |- + Potion Altar multiblock component + Right click to set the displayed item + Shift right click to rotate the item + + ascendant_ember: + name: "Ascendant Ember" + lore: |- + Potion catalyst + Upgrades 1 potion effect + Max amplifier: %max-amplifier% + Duration shorten rate: %duration-shorten-rate% + Success rate: %application-success-rate% + + chronicle_resin: + name: "Chronicle Resin" + lore: |- + Potion catalyst + Boosts the duration of 1 potion effect + Duration boost: %duration-boost-seconds% + Success rate: %application-success-rate% + + eon_weave_crystal: + name: "Eon Weave Crystal" + lore: |- + Potion catalyst + Multiples the duration of 1 potion effect + Multiple rate: %multiple-rate% + Success rate: %application-success-rate% + medkit: name: "Medkit" lore: |- @@ -2540,6 +2587,9 @@ waila: not-empty: " | %item% x%amount%" idle: "" processing: " | %bars%" + potion_altar: + idle: "" + processing: " | %bars%" cargo_splitter: left: "<#efae15>Left" right: "<#2386c4>Right" @@ -2626,6 +2676,7 @@ research: silos_1: "<#ea9335>Silos I" silos_2: "<#ea9335>Silos II" silos_3: "<#ea9335>Silos III" + potion_magic: "<#e5c4ef>Potion magic" guide: pressable_items: " %plant-oil%" @@ -2795,6 +2846,16 @@ message: soulbound_rune: tooltip: "Soulbound" soulbind-message: "Your soul has been linked with your item" + potion_altar: + invalid-potion: "You have to put 2 potions to fuse them" + invalid-catalyst: "You have to put a valid catalyst" + not-same-type: "You have to put 2 potions with the same bottle type" + too-many-effects: "You have to put 2 potions with less than %max_effect_types% different effects" + fused-potion-name: "Fused Potion" + failed-apply-catalyst: "Failed to apply catalyst" + cannot-apply-catalyst: "Cannot apply catalyst" + potion_pedestal: + not-potion: "You have to put a potion" hammer: too-low-tier: "Hammer too low tier to craft %item_name% (%tier_needed% required)" tier: diff --git a/src/main/resources/recipes/minecraft/crafting_shaped.yml b/src/main/resources/recipes/minecraft/crafting_shaped.yml index 0722fecde..fe4320477 100644 --- a/src/main/resources/recipes/minecraft/crafting_shaped.yml +++ b/src/main/resources/recipes/minecraft/crafting_shaped.yml @@ -2663,6 +2663,32 @@ pylon:steel_support_beam: pylon:steel_support_beam: 1 category: building +pylon:potion_pedestal: + pattern: + - " E " + - "BPB" + - " E " + key: + P: pylon:pedestal + E: minecraft:end_stone + B: pylon:bronze_ingot + result: pylon:potion_pedestal + category: building + +pylon:potion_altar: + pattern: + - " B " + - "DSD" + - "ROR" + key: + O: minecraft:obsidian + S: minecraft:brewing_stand + D: minecraft:diamond + R: pylon:bronze_block + B: minecraft:blaze_rod + result: pylon:potion_altar + category: building + pylon:palladium_condenser: pattern: - "SNS" diff --git a/src/main/resources/recipes/pylon/shimmer_altar.yml b/src/main/resources/recipes/pylon/shimmer_altar.yml index 04e0d210e..2242fccfa 100644 --- a/src/main/resources/recipes/pylon/shimmer_altar.yml +++ b/src/main/resources/recipes/pylon/shimmer_altar.yml @@ -120,3 +120,39 @@ pylon:liselette_collector: S: pylon:shimmer_bronze result: pylon:liselette_collector time-seconds: 15 + +pylon:chronicle_resin: + shape: + - "SGS" + - "GAG" + - "SGS" + key: + S: pylon:shimmer_dust_1 + A: pylon:covalent_binder + G: minecraft:glowstone + result: pylon:chronicle_resin + time-seconds: 20 + +pylon:eon_weave_crystal: + shape: + - "RSR" + - "SAS" + - "RSR" + key: + S: pylon:shimmer_dust_2 + A: pylon:covalent_binder + R: minecraft:redstone_block + result: pylon:eon_weave_crystal + time-seconds: 30 + +pylon:ascendant_ember: + shape: + - "R E" + - " A " + - "E R" + key: + A: pylon:covalent_binder + E: pylon:eon_weave_crystal + R: pylon:chronicle_resin + result: pylon:ascendant_ember + time-seconds: 100 \ No newline at end of file diff --git a/src/main/resources/researches.yml b/src/main/resources/researches.yml index d30ed0e00..d2c469395 100644 --- a/src/main/resources/researches.yml +++ b/src/main/resources/researches.yml @@ -434,6 +434,16 @@ shimmer_field_manipulation: - pylon:liselette_anode - pylon:liselette_collector +potion_magic: + item: pylon:potion_altar + cost: 20 + unlocks: + - pylon:potion_pedestal + - pylon:potion_altar + - pylon:ascendant_ember + - pylon:chronicle_resin + - pylon:eon_weave_crystal + assembling: item: pylon:assembly_table cost: 5 diff --git a/src/main/resources/settings/ascendant_ember.yml b/src/main/resources/settings/ascendant_ember.yml new file mode 100644 index 000000000..eaf7de3b0 --- /dev/null +++ b/src/main/resources/settings/ascendant_ember.yml @@ -0,0 +1,3 @@ +application-success-rate: 0.5 +duration-shorten-rate: 0.75 +max-amplifier: 5 \ No newline at end of file diff --git a/src/main/resources/settings/chronicle_resin.yml b/src/main/resources/settings/chronicle_resin.yml new file mode 100644 index 000000000..db60b6cc8 --- /dev/null +++ b/src/main/resources/settings/chronicle_resin.yml @@ -0,0 +1,2 @@ +application-success-rate: 1 +duration-boost-seconds: 240 \ No newline at end of file diff --git a/src/main/resources/settings/eon_weave_crystal.yml b/src/main/resources/settings/eon_weave_crystal.yml new file mode 100644 index 000000000..3386b3c41 --- /dev/null +++ b/src/main/resources/settings/eon_weave_crystal.yml @@ -0,0 +1,2 @@ +application-success-rate: 0.7 +multiple-rate: 1.5 \ No newline at end of file diff --git a/src/main/resources/settings/potion_altar.yml b/src/main/resources/settings/potion_altar.yml new file mode 100644 index 000000000..41f7fa142 --- /dev/null +++ b/src/main/resources/settings/potion_altar.yml @@ -0,0 +1,35 @@ +tick-interval: 10 +max-effect-types: 10 # The max types the fused potion can contain +recipe-time-ticks: 400 # The time in ticks the fused potion takes to finish + +sound: + start: + name: minecraft:entity.ender_eye.launch + source: player + pitch: 1 + volume: 1 + finish: + name: minecraft:block.brewing_stand.brew + source: player + pitch: 1 + volume: 1 + cancel: + name: minecraft:entity.ender_eye.death + source: player + pitch: 1 + volume: 1 + processing: + name: minecraft:entity.enderman.teleport + source: player + pitch: 1 + volume: 1 + cannot_apply_catalyst: + name: minecraft:block.chorus_flower.death + source: player + pitch: 1 + volume: 1 + failed_apply_catalyst: + name: minecraft:entity.iron_golem.death + source: player + pitch: 1 + volume: 1