diff --git a/core/src/main/java/tc/oc/pgm/api/Modules.java b/core/src/main/java/tc/oc/pgm/api/Modules.java index ff2b80cb5b..e5826e1e6a 100644 --- a/core/src/main/java/tc/oc/pgm/api/Modules.java +++ b/core/src/main/java/tc/oc/pgm/api/Modules.java @@ -20,6 +20,8 @@ import tc.oc.pgm.broadcast.BroadcastModule; import tc.oc.pgm.classes.ClassMatchModule; import tc.oc.pgm.classes.ClassModule; +import tc.oc.pgm.compass.CompassMatchModule; +import tc.oc.pgm.compass.CompassModule; import tc.oc.pgm.consumable.ConsumableMatchModule; import tc.oc.pgm.consumable.ConsumableModule; import tc.oc.pgm.controlpoint.ControlPointMatchModule; @@ -247,6 +249,7 @@ void registerAll() { register(ScoreModule.class, ScoreMatchModule.class, new ScoreModule.Factory()); register(KitModule.class, KitMatchModule.class, new KitModule.Factory()); register(ActionModule.class, ActionMatchModule.class, new ActionModule.Factory()); + register(CompassModule.class, CompassMatchModule.class, new CompassModule.Factory()); register( ItemDestroyModule.class, ItemDestroyMatchModule.class, new ItemDestroyModule.Factory()); register(ToolRepairModule.class, ToolRepairMatchModule.class, new ToolRepairModule.Factory()); diff --git a/core/src/main/java/tc/oc/pgm/compass/CompassMatchModule.java b/core/src/main/java/tc/oc/pgm/compass/CompassMatchModule.java new file mode 100644 index 0000000000..454d131e3f --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/compass/CompassMatchModule.java @@ -0,0 +1,177 @@ +package tc.oc.pgm.compass; + +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.Component.translatable; +import static net.kyori.adventure.text.format.Style.style; +import static tc.oc.pgm.util.text.TextTranslations.translateLegacy; + +import com.google.common.collect.ImmutableList; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.Material; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.ItemSpawnEvent; +import org.bukkit.event.inventory.InventoryMoveItemEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.inventory.meta.ItemMeta; +import tc.oc.pgm.api.match.Match; +import tc.oc.pgm.api.match.MatchModule; +import tc.oc.pgm.api.match.Tickable; +import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.api.time.Tick; +import tc.oc.pgm.events.PlayerResetEvent; +import tc.oc.pgm.spawns.events.ParticipantKitApplyEvent; + +public class CompassMatchModule implements MatchModule, Tickable, Listener { + + private static final long REFRESH_TICKS = 20 * 2; + private final Match match; + private final Map lastRefresh; + private final ImmutableList compassTargets; + private final OrderStrategy orderStrategy; + private final boolean showDistance; + + public CompassMatchModule( + Match match, + ImmutableList compassTargets, + OrderStrategy orderStrategy, + boolean showDistance) { + this.match = match; + this.lastRefresh = new ConcurrentHashMap<>(); + this.compassTargets = compassTargets; + this.orderStrategy = orderStrategy; + this.showDistance = showDistance; + } + + @Override + public synchronized void tick(Match match, Tick tick) { + for (Map.Entry lastRefreshEntry : lastRefresh.entrySet()) { + if (tick.tick - lastRefreshEntry.getValue() >= REFRESH_TICKS) { + refreshPlayer(lastRefreshEntry.getKey(), tick.tick); + } + } + } + + private void refreshPlayer(UUID uuid, long tick) { + MatchPlayer player = match.getPlayer(uuid); + if (player == null || !player.isAlive()) { + lastRefresh.remove(uuid); + return; + } else { + lastRefresh.put(uuid, tick); + } + + updatePlayerCompass(player, chooseCompassTarget(player)); + } + + private void updatePlayerCompass( + MatchPlayer player, Optional compassResult) { + compassResult.ifPresent( + compassTargetResult -> + player.getBukkit().setCompassTarget(compassTargetResult.getLocation())); + + PlayerInventory inventory = player.getInventory(); + if (inventory == null) { + return; + } + ItemStack[] contents = inventory.getContents(); + if (contents == null) { + return; + } + + for (ItemStack content : contents) { + if (content != null && Material.COMPASS.equals(content.getType())) { + ItemMeta itemMeta = content.getItemMeta(); + + Component itemNameComponent; + if (compassResult.isPresent()) { + Component resultComponent = compassResult.get().getComponent(); + + TextComponent.Builder builder = + text() + .append( + translatable( + "compass.tracking", style(NamedTextColor.GRAY, TextDecoration.BOLD))) + .append(text(": ", style(NamedTextColor.WHITE, TextDecoration.BOLD))) + .append(resultComponent); + + if (showDistance) { + builder + .append(text(" ")) + .append( + translatable( + "compass.tracking.distance", + style(NamedTextColor.AQUA, TextDecoration.BOLD), + text((int) compassResult.get().getDistance()))); + } + + itemNameComponent = builder.build(); + } else { + itemNameComponent = + translatable( + "compass.tracking.unknown", style(NamedTextColor.WHITE, TextDecoration.BOLD)); + } + + itemMeta.setDisplayName(translateLegacy(itemNameComponent, player)); + content.setItemMeta(itemMeta); + } + } + inventory.setContents(contents); + } + + private Optional chooseCompassTarget(MatchPlayer player) { + Optional result = Optional.empty(); + Stream targetStream = + compassTargets.stream() + .map((compassTarget -> compassTarget.getResult(match, player))) + .filter(Optional::isPresent) + .map(Optional::get); + switch (orderStrategy) { + case FIRST_DEFINED: + result = targetStream.findFirst(); + break; + case CLOSEST: + result = targetStream.min(CompassTargetResult::compareTo); + break; + } + return result; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerReset(PlayerResetEvent event) { + this.lastRefresh.put(event.getPlayer().getId(), -REFRESH_TICKS); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onItemDrop(ItemSpawnEvent event) { + ItemStack itemStack = event.getEntity().getItemStack(); + if (Material.COMPASS.equals(itemStack.getType())) { + event.getEntity().setItemStack(new ItemStack(Material.COMPASS, itemStack.getAmount())); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onItemMove(InventoryMoveItemEvent event) { + if (!(event.getDestination() instanceof PlayerInventory)) { + ItemStack itemStack = event.getItem(); + if (Material.COMPASS.equals(itemStack.getType())) { + event.setItem(new ItemStack(Material.COMPASS, itemStack.getAmount())); + } + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onSpawn(ParticipantKitApplyEvent event) { + this.lastRefresh.put(event.getPlayer().getId(), -REFRESH_TICKS); + } +} diff --git a/core/src/main/java/tc/oc/pgm/compass/CompassModule.java b/core/src/main/java/tc/oc/pgm/compass/CompassModule.java new file mode 100644 index 0000000000..a59d07baa3 --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/compass/CompassModule.java @@ -0,0 +1,71 @@ +package tc.oc.pgm.compass; + +import com.google.common.collect.ImmutableList; +import java.util.Collection; +import java.util.logging.Logger; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.api.map.MapModule; +import tc.oc.pgm.api.map.factory.MapFactory; +import tc.oc.pgm.api.map.factory.MapModuleFactory; +import tc.oc.pgm.api.match.Match; +import tc.oc.pgm.api.match.MatchModule; +import tc.oc.pgm.api.module.exception.ModuleLoadException; +import tc.oc.pgm.filters.FilterMatchModule; +import tc.oc.pgm.util.xml.InvalidXMLException; +import tc.oc.pgm.util.xml.Node; +import tc.oc.pgm.util.xml.XMLUtils; + +public class CompassModule implements MapModule { + + private final ImmutableList compassTargets; + private final OrderStrategy orderStrategy; + private final boolean showDistance; + + public CompassModule( + ImmutableList compassTargets, + OrderStrategy orderStrategy, + boolean showDistance) { + this.compassTargets = compassTargets; + this.orderStrategy = orderStrategy; + this.showDistance = showDistance; + } + + @Nullable + @Override + public Collection> getHardDependencies() { + return ImmutableList.of(FilterMatchModule.class); + } + + @Override + public @Nullable CompassMatchModule createMatchModule(Match match) throws ModuleLoadException { + return new CompassMatchModule(match, compassTargets, orderStrategy, showDistance); + } + + public static class Factory implements MapModuleFactory { + @Override + public @Nullable CompassModule parse(MapFactory factory, Logger logger, Document doc) + throws InvalidXMLException { + CompassParser parser = new CompassParser(factory); + + ImmutableList.Builder compassTargets = ImmutableList.builder(); + OrderStrategy orderStrategy = OrderStrategy.FIRST_DEFINED; + boolean showDistance = false; + for (Element compassRoot : doc.getRootElement().getChildren("compass")) { + orderStrategy = + XMLUtils.parseEnum( + Node.fromAttr(compassRoot, "order"), OrderStrategy.class, orderStrategy); + showDistance = + XMLUtils.parseBoolean(Node.fromAttr(compassRoot, "show-distance"), showDistance); + for (Element compassTarget : compassRoot.getChildren()) { + if (parser.isTarget(compassTarget)) { + compassTargets.add(parser.parseCompassTarget(compassTarget)); + } + } + } + + return new CompassModule(compassTargets.build(), orderStrategy, showDistance); + } + } +} diff --git a/core/src/main/java/tc/oc/pgm/compass/CompassParser.java b/core/src/main/java/tc/oc/pgm/compass/CompassParser.java new file mode 100644 index 0000000000..2de6ae73f8 --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/compass/CompassParser.java @@ -0,0 +1,61 @@ +package tc.oc.pgm.compass; + +import java.lang.reflect.Method; +import java.util.Map; +import net.kyori.adventure.text.Component; +import org.jdom2.Element; +import tc.oc.pgm.api.filter.Filter; +import tc.oc.pgm.api.map.factory.MapFactory; +import tc.oc.pgm.compass.targets.PlayerCompassTarget; +import tc.oc.pgm.features.FeatureDefinitionContext; +import tc.oc.pgm.filters.parse.FilterParser; +import tc.oc.pgm.util.MethodParser; +import tc.oc.pgm.util.MethodParsers; +import tc.oc.pgm.util.xml.InvalidXMLException; +import tc.oc.pgm.util.xml.Node; +import tc.oc.pgm.util.xml.XMLUtils; + +public class CompassParser { + + private final MapFactory factory; + private final FeatureDefinitionContext features; + private final FilterParser filters; + private final Map methodParsers; + + public CompassParser(MapFactory factory) { + this.factory = factory; + this.features = factory.getFeatures(); + this.filters = factory.getFilters(); + this.methodParsers = MethodParsers.getMethodParsersForClass(getClass()); + } + + public boolean isTarget(Element el) { + return getParserFor(el) != null; + } + + protected Method getParserFor(Element el) { + return methodParsers.get(el.getName().toLowerCase()); + } + + public CompassTarget parseCompassTarget(Element el) throws InvalidXMLException { + Method parser = getParserFor(el); + if (parser != null) { + try { + return (CompassTarget) parser.invoke(this, el); + } catch (Exception e) { + throw InvalidXMLException.coerce(e, new Node(el)); + } + } else { + throw new InvalidXMLException("Unknown compass tracker type: " + el.getName(), el); + } + } + + @MethodParser("player") + public PlayerCompassTarget parsePlayerTarget(Element el) throws InvalidXMLException { + Component name = XMLUtils.parseFormattedText(Node.fromChildOrAttr(el, "name")); + Filter filter = filters.parseProperty(el, "filter"); + boolean showPlayerName = XMLUtils.parseBoolean(Node.fromAttr(el, "show-player"), false); + + return new PlayerCompassTarget(filter, name, showPlayerName); + } +} diff --git a/core/src/main/java/tc/oc/pgm/compass/CompassTarget.java b/core/src/main/java/tc/oc/pgm/compass/CompassTarget.java new file mode 100644 index 0000000000..be666b2d32 --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/compass/CompassTarget.java @@ -0,0 +1,18 @@ +package tc.oc.pgm.compass; + +import java.util.Optional; +import net.kyori.adventure.text.Component; +import org.bukkit.Location; +import tc.oc.pgm.api.filter.Filter; +import tc.oc.pgm.api.match.Match; +import tc.oc.pgm.api.player.MatchPlayer; + +public interface CompassTarget { + Filter getFilter(); + + Component getName(Match match, MatchPlayer player); + + Optional getLocation(Match match, MatchPlayer player); + + Optional getResult(Match match, MatchPlayer player); +} diff --git a/core/src/main/java/tc/oc/pgm/compass/CompassTargetResult.java b/core/src/main/java/tc/oc/pgm/compass/CompassTargetResult.java new file mode 100644 index 0000000000..e310cc8f38 --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/compass/CompassTargetResult.java @@ -0,0 +1,32 @@ +package tc.oc.pgm.compass; + +import net.kyori.adventure.text.Component; +import org.bukkit.Location; + +public class CompassTargetResult { + private final Location location; + private final double distance; + private final Component component; + + public CompassTargetResult(Location location, double distance, Component component) { + this.location = location; + this.distance = distance; + this.component = component; + } + + public Location getLocation() { + return location; + } + + public double getDistance() { + return distance; + } + + public Component getComponent() { + return component; + } + + public int compareTo(CompassTargetResult other) { + return (int) (distance - other.distance); + } +} diff --git a/core/src/main/java/tc/oc/pgm/compass/OrderStrategy.java b/core/src/main/java/tc/oc/pgm/compass/OrderStrategy.java new file mode 100644 index 0000000000..15513219a8 --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/compass/OrderStrategy.java @@ -0,0 +1,6 @@ +package tc.oc.pgm.compass; + +public enum OrderStrategy { + FIRST_DEFINED, + CLOSEST, +} diff --git a/core/src/main/java/tc/oc/pgm/compass/targets/PlayerCompassTarget.java b/core/src/main/java/tc/oc/pgm/compass/targets/PlayerCompassTarget.java new file mode 100644 index 0000000000..c27dade67b --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/compass/targets/PlayerCompassTarget.java @@ -0,0 +1,68 @@ +package tc.oc.pgm.compass.targets; + +import static tc.oc.pgm.util.player.PlayerComponent.player; + +import java.util.Optional; +import net.kyori.adventure.text.Component; +import org.bukkit.Location; +import tc.oc.pgm.api.filter.Filter; +import tc.oc.pgm.api.match.Match; +import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.compass.CompassTarget; +import tc.oc.pgm.compass.CompassTargetResult; +import tc.oc.pgm.filters.query.PlayerQuery; +import tc.oc.pgm.util.named.NameStyle; + +public class PlayerCompassTarget implements CompassTarget { + + private final Filter filter; + private final Component name; + private final boolean showPlayerName; + + public PlayerCompassTarget(Filter filter, Component name, boolean showPlayerName) { + this.filter = filter; + this.name = name == null ? Component.translatable("compass.tracking.player") : name; + this.showPlayerName = showPlayerName; + } + + @Override + public Filter getFilter() { + return filter; + } + + @Override + public Component getName(Match match, MatchPlayer player) { + return showPlayerName ? player(player, NameStyle.FANCY) : name; + } + + @Override + public Optional getLocation(Match match, MatchPlayer player) { + return getClosestPlayer(match, player).map(MatchPlayer::getLocation); + } + + @Override + public Optional getResult(Match match, MatchPlayer player) { + Optional playerOptional = getClosestPlayer(match, player); + if (playerOptional.isPresent()) { + MatchPlayer matchPlayer = playerOptional.get(); + return Optional.of( + new CompassTargetResult( + matchPlayer.getLocation(), + matchPlayer.getLocation().distance(player.getLocation()), + getName(match, matchPlayer))); + } else { + return Optional.empty(); + } + } + + private Optional getClosestPlayer(Match match, MatchPlayer player) { + return match.getParticipants().stream() + .filter((otherPlayer) -> !otherPlayer.equals(player)) + .filter((otherPlayer) -> filter.response(new PlayerQuery(null, otherPlayer))) + .min( + (firstPlayer, secondPlayer) -> + (int) + (firstPlayer.getLocation().distance(player.getLocation()) + - secondPlayer.getLocation().distance(player.getLocation()))); + } +} diff --git a/util/src/main/i18n/templates/ui.properties b/util/src/main/i18n/templates/ui.properties index 45be28d63f..817fb68d86 100644 --- a/util/src/main/i18n/templates/ui.properties +++ b/util/src/main/i18n/templates/ui.properties @@ -231,3 +231,10 @@ shop.item.empty = No items for sale at the moment shop.category.empty = This shop has no items for sale at the moment +compass.tracking = Tracking + +compass.tracking.player = Player + +compass.tracking.distance = ({0}m) + +compass.tracking.unknown = No Targets Found!