Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 3 additions & 4 deletions src/main/java/io/github/pylonmc/pylon/base/BaseItems.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<CommandSourceStack> ctx) {
Expand All @@ -52,4 +63,20 @@ private int fillFluid(CommandContext<CommandSourceStack> ctx) {

return Command.SINGLE_SUCCESS;
}

private int resetLoupe(CommandContext<CommandSourceStack> ctx, Player target) {
CommandSender sender = ctx.getSource().getSender();
if (target == null) {
if (!(sender instanceof Player player)) {
sender.sendRichMessage("<red>You must be a player to use this command");
return Command.SINGLE_SUCCESS;
}
target = player;
}

target.getPersistentDataContainer().remove(Loupe.CONSUMED_KEY);
sender.sendRichMessage("<green>Reset loupe data for <target>",
Placeholder.unparsed("target", target.getName()));
return Command.SINGLE_SUCCESS;
}
}
213 changes: 116 additions & 97 deletions src/main/java/io/github/pylonmc/pylon/base/content/science/Loupe.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<PersistentDataContainer, Map<Material, Integer>> CONSUMED_TYPE =
public static final NamespacedKey CONSUMED_KEY = new NamespacedKey(PylonBase.getInstance(), "consumed");
public static final PersistentDataType<PersistentDataContainer, Map<Material, Integer>> CONSUMED_TYPE =
PylonSerializers.MAP.mapTypeFrom(
PylonSerializers.KEYED.keyedTypeFrom(Material.class, Registry.MATERIAL::getOrThrow),
PylonSerializers.INTEGER
Expand All @@ -60,121 +69,141 @@ public class Loupe extends PylonItem implements PylonInteractor, PylonConsumable
}
}

private static final Map<UUID, RayTraceResult> scanning = new HashMap<>();

public Loupe(@NotNull ItemStack stack) {
super(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",
Expand All @@ -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<Material, Integer> items) {
Collection<org.bukkit.entity.Item> 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) {
Expand Down
Loading