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 f909947b..d81af43a 100644 --- a/src/main/java/io/github/pylonmc/pylon/base/BaseItems.java +++ b/src/main/java/io/github/pylonmc/pylon/base/BaseItems.java @@ -37,6 +37,7 @@ import io.papermc.paper.registry.keys.tags.BlockTypeTagKeys; import io.papermc.paper.registry.keys.tags.DamageTypeTagKeys; import io.papermc.paper.registry.set.RegistrySet; +import net.kyori.adventure.key.Key; import net.kyori.adventure.util.TriState; import org.bukkit.Color; import org.bukkit.FireworkEffect; @@ -1301,14 +1302,12 @@ private BaseItems() { public static final ItemStack LOUPE = ItemStackBuilder.pylonItem(Material.CLAY_BALL, BaseKeys.LOUPE) .set(DataComponentTypes.ITEM_MODEL, Material.GLASS_PANE.getKey()) - .set(DataComponentTypes.CONSUMABLE, io.papermc.paper.datacomponent.item.Consumable.consumable() + .set(DataComponentTypes.CONSUMABLE, Consumable.consumable() .animation(ItemUseAnimation.SPYGLASS) .hasConsumeParticles(false) .consumeSeconds(3) - .sound(Registry.SOUNDS.getKey(Sound.BLOCK_AMETHYST_CLUSTER_HIT)) + .sound(Key.key("silence")) ) - .set(DataComponentTypes.USE_COOLDOWN, UseCooldown.useCooldown(1) - .cooldownGroup(BaseKeys.LOUPE.key())) .build(); static { PylonItem.register(Loupe.class, LOUPE); diff --git a/src/main/java/io/github/pylonmc/pylon/base/command/PylonBaseCommand.java b/src/main/java/io/github/pylonmc/pylon/base/command/PylonBaseCommand.java index 5269b0ef..842e455a 100644 --- a/src/main/java/io/github/pylonmc/pylon/base/command/PylonBaseCommand.java +++ b/src/main/java/io/github/pylonmc/pylon/base/command/PylonBaseCommand.java @@ -4,13 +4,17 @@ import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.tree.LiteralCommandNode; import io.github.pylonmc.pylon.base.content.machines.fluid.PortableFluidTank; +import io.github.pylonmc.pylon.base.content.science.Loupe; import io.github.pylonmc.pylon.core.command.RegistryCommandArgument; import io.github.pylonmc.pylon.core.fluid.PylonFluid; import io.github.pylonmc.pylon.core.item.PylonItem; import io.github.pylonmc.pylon.core.registry.PylonRegistry; import io.papermc.paper.command.brigadier.CommandSourceStack; import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver; import lombok.experimental.UtilityClass; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -28,6 +32,13 @@ public class PylonBaseCommand { .executes(PylonBaseCommand::fillFluid) ) ) + .then(Commands.literal("resetloupedata") + .requires(source -> source.getSender().hasPermission("pylonbase.command.reset_loupe")) + .executes(ctx -> resetLoupe(ctx, null)) + .then(Commands.argument("player", ArgumentTypes.player()) + .executes(ctx -> resetLoupe(ctx, ctx.getArgument("player", PlayerSelectorArgumentResolver.class).resolve(ctx.getSource()).getFirst())) + ) + ) .build(); private int fillFluid(CommandContext ctx) { @@ -52,4 +63,20 @@ private int fillFluid(CommandContext ctx) { return Command.SINGLE_SUCCESS; } + + private int resetLoupe(CommandContext ctx, Player target) { + CommandSender sender = ctx.getSource().getSender(); + if (target == null) { + if (!(sender instanceof Player player)) { + sender.sendRichMessage("You must be a player to use this command"); + return Command.SINGLE_SUCCESS; + } + target = player; + } + + target.getPersistentDataContainer().remove(Loupe.CONSUMED_KEY); + sender.sendRichMessage("Reset loupe data for ", + Placeholder.unparsed("target", target.getName())); + return Command.SINGLE_SUCCESS; + } } diff --git a/src/main/java/io/github/pylonmc/pylon/base/content/science/Loupe.java b/src/main/java/io/github/pylonmc/pylon/base/content/science/Loupe.java index db42e756..d2d6f27d 100644 --- a/src/main/java/io/github/pylonmc/pylon/base/content/science/Loupe.java +++ b/src/main/java/io/github/pylonmc/pylon/base/content/science/Loupe.java @@ -1,5 +1,6 @@ package io.github.pylonmc.pylon.base.content.science; +import com.destroystokyo.paper.ParticleBuilder; import io.github.pylonmc.pylon.base.BaseKeys; import io.github.pylonmc.pylon.base.PylonBase; import io.github.pylonmc.pylon.core.block.BlockStorage; @@ -13,36 +14,44 @@ import io.github.pylonmc.pylon.core.item.base.PylonInteractor; import io.github.pylonmc.pylon.core.item.research.Research; import io.papermc.paper.datacomponent.DataComponentTypes; +import io.papermc.paper.registry.keys.SoundEventKeys; import lombok.Getter; +import net.kyori.adventure.sound.Sound; import net.kyori.adventure.text.Component; +import org.bukkit.Effect; import org.bukkit.FluidCollisionMode; import org.bukkit.Material; import org.bukkit.NamespacedKey; +import org.bukkit.Particle; import org.bukkit.Registry; import org.bukkit.block.Block; -import org.bukkit.block.BlockType; +import org.bukkit.entity.Item; +import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.player.PlayerAttemptPickupItemEvent; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerItemConsumeEvent; import org.bukkit.inventory.ItemRarity; import org.bukkit.inventory.ItemStack; import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.persistence.PersistentDataType; +import org.bukkit.util.RayTraceResult; import org.jetbrains.annotations.NotNull; -import java.util.Collection; import java.util.EnumMap; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.Objects; +import java.util.UUID; @SuppressWarnings("UnstableApiUsage") public class Loupe extends PylonItem implements PylonInteractor, PylonConsumable { - private static final NamespacedKey CONSUMED_KEY = new NamespacedKey(PylonBase.getInstance(), "consumed"); - private static final PersistentDataType> CONSUMED_TYPE = + public static final NamespacedKey CONSUMED_KEY = new NamespacedKey(PylonBase.getInstance(), "consumed"); + public static final PersistentDataType> CONSUMED_TYPE = PylonSerializers.MAP.mapTypeFrom( PylonSerializers.KEYED.keyedTypeFrom(Material.class, Registry.MATERIAL::getOrThrow), PylonSerializers.INTEGER @@ -60,6 +69,8 @@ public class Loupe extends PylonItem implements PylonInteractor, PylonConsumable } } + private static final Map scanning = new HashMap<>(); + public Loupe(@NotNull ItemStack stack) { super(stack); } @@ -67,114 +78,132 @@ public Loupe(@NotNull ItemStack stack) { @Override public void onUsedToRightClick(@NotNull PlayerInteractEvent event) { Player player = event.getPlayer(); - - var items = player.getPersistentDataContainer().getOrDefault(CONSUMED_KEY, CONSUMED_TYPE, Map.of()); - - // get scanned block - Block toScan = player.getTargetBlockExact(5, FluidCollisionMode.SOURCE_ONLY); - - // nothing found or scanning air - if (toScan == null || toScan.getType().isAir()) { - player.sendMessage(Component.translatable("pylon.pylonbase.message.loupe.nothing")); - event.setCancelled(true); + if (player.hasCooldown(getStack())) { return; } - // scan for entities first and process the first one found only - var entityItemType = hasValidItem(toScan, items); - if (entityItemType != null) { - ItemStack stack = entityItemType.getItemStack(); - if (PylonItem.fromStack(stack) != null) { - player.sendMessage(Component.translatable("pylon.pylonbase.message.loupe.is_pylon")); - event.setCancelled(true); - return; - } - - if (!stack.getPersistentDataContainer().isEmpty()) { - player.sendMessage(Component.translatable("pylon.pylonbase.message.loupe.is_other_plugin")); - event.setCancelled(true); - return; - } - - boolean invalid = processMaterial(stack.getType(), player); - event.setCancelled(invalid); + RayTraceResult scan = player.getWorld().rayTrace(player.getEyeLocation(), player.getEyeLocation().getDirection(), 5, + player.isUnderWater() ? FluidCollisionMode.NEVER : FluidCollisionMode.SOURCE_ONLY, false, 0.25, hit -> hit != player); + if (scan == null) { return; } - // process block aimed at - if (BlockStorage.get(toScan) != null) { - player.sendMessage(Component.translatable("pylon.pylonbase.message.loupe.is_pylon")); - event.setCancelled(true); + RayTraceResult initialScan = scanning.get(player.getUniqueId()); + if (initialScan != null && Objects.equals(scan.getHitBlock(), initialScan.getHitBlock()) && Objects.equals(scan.getHitEntity(), initialScan.getHitEntity())) { return; } - Material blockType = toScan.getType(); - event.setCancelled(processMaterial(blockType, player)); - } - - private static boolean processMaterial(Material type, Player player) { - var items = player.getPersistentDataContainer().getOrDefault(CONSUMED_KEY, CONSUMED_TYPE, Map.of()); - ItemRarity rarity = type.getDefaultData(DataComponentTypes.RARITY); - int maxUses = itemConfigs.get(rarity).uses; - - if (items.getOrDefault(type, 0) >= maxUses) { - player.sendMessage(Component.translatable("pylon.pylonbase.message.loupe.already_examined")); - return true; + if (scan.getHitEntity() instanceof Item hit) { + ItemStack stack = hit.getItemStack(); + if (PylonItem.fromStack(stack) != null) { + player.sendActionBar(Component.translatable("pylon.pylonbase.message.loupe.is_pylon")); + } else if (!stack.getPersistentDataContainer().isEmpty()) { + player.sendActionBar(Component.translatable("pylon.pylonbase.message.loupe.is_other_plugin")); + } else if (!canExamine(stack.getType(), player)) { + player.sendActionBar(Component.translatable("pylon.pylonbase.message.loupe.already_examined")); + } else { + player.playSound(Sound.sound(SoundEventKeys.BLOCK_BELL_RESONATE, Sound.Source.PLAYER, 1f, 0.7f)); + player.sendActionBar(Component.translatable("pylon.pylonbase.message.loupe.examining") + .arguments(PylonArgument.of("item", stack.effectiveName()))); + scanning.put(player.getUniqueId(), scan); + } + } else if (scan.getHitEntity() instanceof LivingEntity entity) { + // todo: scanning entities, allow scanning players? + } else if (scan.getHitBlock() != null) { + Block hit = scan.getHitBlock(); + Material type = hit.getType(); + if (BlockStorage.get(hit) != null) { + player.sendActionBar(Component.translatable("pylon.pylonbase.message.loupe.is_pylon")); + } else if (type.getHardness() < 0f) { + player.sendActionBar(Component.translatable("pylon.pylonbase.message.loupe.is_unbreakable")); + } else if (!canExamine(type, player)) { + player.sendActionBar(Component.translatable("pylon.pylonbase.message.loupe.already_examined")); + } else { + player.playSound(Sound.sound(SoundEventKeys.BLOCK_BELL_RESONATE, Sound.Source.PLAYER, 1f, 0.7f)); + player.sendActionBar(Component.translatable("pylon.pylonbase.message.loupe.examining") + .arguments(PylonArgument.of("item", Component.translatable(type)))); + scanning.put(player.getUniqueId(), scan); + } } - - return false; } @Override public void onConsumed(@NotNull PlayerItemConsumeEvent event) { event.setCancelled(true); - Player player = event.getPlayer(); - var items = new HashMap<>(player.getPersistentDataContainer().getOrDefault(CONSUMED_KEY, CONSUMED_TYPE, Map.of())); + Player player = event.getPlayer(); + RayTraceResult initialScan = scanning.remove(player.getUniqueId()); + if (initialScan == null) { + return; + } - Block toScan = player.getTargetBlockExact(5, FluidCollisionMode.SOURCE_ONLY); + RayTraceResult scan = player.getWorld().rayTrace(player.getEyeLocation(), player.getEyeLocation().getDirection(), 5, + player.isUnderWater() ? FluidCollisionMode.NEVER : FluidCollisionMode.SOURCE_ONLY, false, 0.25, hit -> hit != player); + if (scan == null || !Objects.equals(scan.getHitBlock(), initialScan.getHitBlock()) || !Objects.equals(scan.getHitEntity(), initialScan.getHitEntity())) { + return; + } - if (toScan == null || toScan.getType().isAir()) return; + if (scan.getHitEntity() instanceof Item hit) { + ItemStack stack = hit.getItemStack(); + if (PylonItem.fromStack(stack) != null || !stack.getPersistentDataContainer().isEmpty() || !canExamine(stack.getType(), player)) { + player.sendMessage(Component.translatable("pylon.pylonbase.message.loupe.examine_failed") + .arguments(PylonArgument.of("item", stack.effectiveName()))); + return; + } - // scan for entities first and process the first one found only - org.bukkit.entity.Item entityItem = hasValidItem(toScan, items); - if (entityItem != null) { - ItemStack stack = entityItem.getItemStack(); - if(addPoints(stack.getType(), stack.effectiveName(), player)) return; + PlayerAttemptPickupItemEvent pickupEvent = new PlayerAttemptPickupItemEvent(player, hit, stack.getAmount() - 1); + if (!pickupEvent.callEvent()) { + player.sendMessage(Component.translatable("pylon.pylonbase.message.loupe.examine_failed") + .arguments(PylonArgument.of("item", stack.effectiveName()))); + return; + } + new ParticleBuilder(Particle.ITEM).data(stack).extra(0.05).count(16).location(hit.getLocation().add(0, hit.getHeight() / 2, 0)).spawn(); + hit.getWorld().playSound(Sound.sound(SoundEventKeys.ENTITY_ITEM_BREAK, Sound.Source.PLAYER, 0.5f, 1f), hit.getX(), hit.getY(), hit.getZ()); if (stack.getAmount() == 1) { - entityItem.remove(); + hit.remove(); } else { - stack.setAmount(stack.getAmount() - 1); - entityItem.setItemStack(stack); + stack.subtract(); + hit.setItemStack(stack); + } + addPoints(stack.getType(), stack.effectiveName(), player); + player.setCooldown(getStack(), 60); + } else if (scan.getHitEntity() instanceof LivingEntity entity) { + // todo: scanning entities, allow scanning players? + } else if (scan.getHitBlock() != null) { + Block hit = scan.getHitBlock(); + Material type = hit.getType(); + if (type.getHardness() < 0f || BlockStorage.get(hit) != null || !canExamine(type, player)) { + player.sendMessage(Component.translatable("pylon.pylonbase.message.loupe.examine_failed") + .arguments(PylonArgument.of("item", Component.translatable(type)))); + return; } - return; - } - - // process block aimed at - Material blockType = toScan.getType(); - BlockType bt = blockType.asBlockType(); - if (bt == null) return; // shouldn't happen + BlockBreakEvent breakEvent = new BlockBreakEvent(hit, player); + breakEvent.setDropItems(false); + breakEvent.setExpToDrop(0); + if (!breakEvent.callEvent()) { + player.sendMessage(Component.translatable("pylon.pylonbase.message.loupe.examine_failed") + .arguments(PylonArgument.of("item", Component.translatable(type)))); + return; + } - if (addPoints(blockType, Component.translatable(bt.translationKey()), player)) return; - if (blockType.getHardness() > 0f) { // filter out unbreakable blocks - toScan.setType(Material.AIR); - new BlockBreakEvent(toScan, player).callEvent(); + hit.getWorld().playEffect(hit.getLocation(), Effect.STEP_SOUND, hit.getBlockData()); + hit.setType(Material.AIR, true); + addPoints(type, Component.translatable(type), player); + player.setCooldown(getStack(), 60); } } - private static boolean addPoints(Material type, Component name, Player player) { + private static void addPoints(Material type, Component name, Player player) { + ItemConfig config = itemConfigs.get(type.isItem() ? type.getDefaultData(DataComponentTypes.RARITY) : ItemRarity.COMMON); var items = new HashMap<>(player.getPersistentDataContainer().getOrDefault(CONSUMED_KEY, CONSUMED_TYPE, Map.of())); - ItemConfig config = itemConfigs.get(type.getDefaultData(DataComponentTypes.RARITY)); - - int currentUses = items.getOrDefault(type, 0); - if (currentUses >= config.uses) return true; // This should never happen - items.put(type, currentUses + 1); + items.put(type, items.getOrDefault(type, 0) + 1); player.getPersistentDataContainer().set(CONSUMED_KEY, CONSUMED_TYPE, items); - long points = Research.getResearchPoints(player); - Research.setResearchPoints(player, points + config.points); + + long points = Research.getResearchPoints(player) + config.points; + Research.setResearchPoints(player, points); player.sendMessage(Component.translatable( "pylon.pylonbase.message.loupe.examined", @@ -183,25 +212,15 @@ private static boolean addPoints(Material type, Component name, Player player) { player.sendMessage(Component.translatable( "pylon.pylonbase.message.gained_research_points", PylonArgument.of("points", config.points), - PylonArgument.of("total", Research.getResearchPoints(player)) + PylonArgument.of("total", points) )); - return false; } - private static org.bukkit.entity.Item hasValidItem(Block toScan, Map items) { - Collection entityItems = toScan.getLocation().getNearbyEntitiesByType(org.bukkit.entity.Item.class, 1.2); - for (var item : entityItems) { - ItemStack stack = item.getItemStack(); - ItemRarity rarity = stack.getType().getDefaultData(DataComponentTypes.RARITY); - int maxUses = itemConfigs.get(rarity).uses; - - // found valid item that hasn't been scanned yet - if (items.getOrDefault(stack.getType(), 0) < maxUses) { - return item; - } - } - - return null; + private static boolean canExamine(Material type, Player player) { + var items = player.getPersistentDataContainer().getOrDefault(CONSUMED_KEY, CONSUMED_TYPE, Map.of()); + ItemRarity rarity = type.isItem() ? type.getDefaultData(DataComponentTypes.RARITY) : ItemRarity.COMMON; + int maxUses = itemConfigs.get(rarity).uses; + return items.getOrDefault(type, 0) < maxUses; } public record ItemConfig(int uses, int points) { diff --git a/src/main/resources/lang/en.yml b/src/main/resources/lang/en.yml index fafb9e49..733070c5 100644 --- a/src/main/resources/lang/en.yml +++ b/src/main/resources/lang/en.yml @@ -1371,11 +1371,13 @@ message: sprinkler_too_close: "You cannot place sprinklers within %radius% blocks of each other" gained_research_points: "+%points% research points (%total% points total)" loupe: - nothing: "You look at your hand. It is not very interesting" - already_examined: "You cannot find any more interesting things about this item" is_pylon: "Having built this, you can't think of anything interesting you might find out about it" - examined: "As you examine the %item%, you get a sudden flash of insight" is_other_plugin: "You smug little knave, use vanilla items not plugin ones" + is_unbreakable: "As the block is unbreakable, you can't examine it placed down" + already_examined: "You cannot find any more interesting things about this item" + examining: "You examine the %item% closely...." + examine_failed: "Failed to examine the %item%. (Do you have permission?)" + examined: "After examining the %item%, you get a sudden flash of insight!" research_pack: message: "As you open the research pack, %happening%" happening: