package tc.oc.pgm.goals; import java.util.Map; import javax.annotation.Nullable; import com.google.common.collect.ImmutableSet; import net.md_5.bungee.api.ChatColor; import org.bukkit.Location; import org.bukkit.block.BlockState; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import tc.oc.commons.bukkit.util.BlockUtils; import tc.oc.commons.core.chat.ChatUtils; import tc.oc.commons.core.localization.Locales; import tc.oc.commons.core.util.DefaultMapAdapter; import tc.oc.pgm.events.ListenerScope; import tc.oc.pgm.match.Competitor; import tc.oc.pgm.Config; import tc.oc.pgm.match.Match; import tc.oc.pgm.match.MatchPlayer; import tc.oc.pgm.match.MatchScope; import tc.oc.pgm.match.ParticipantState; import tc.oc.pgm.match.Party; import tc.oc.commons.bukkit.event.CoarsePlayerMoveEvent; import tc.oc.pgm.events.CompetitorRemoveEvent; import tc.oc.pgm.events.MatchPlayerDeathEvent; import tc.oc.pgm.events.ParticipantBlockTransformEvent; import tc.oc.pgm.goals.events.GoalCompleteEvent; import tc.oc.pgm.goals.events.GoalProximityChangeEvent; import tc.oc.pgm.goals.events.GoalTouchEvent; @ListenerScope(MatchScope.RUNNING) public abstract class ProximityGoal<T extends ProximityGoalDefinition> extends OwnedGoal<T> implements Listener { private final Map<Competitor, Integer> proximity = new DefaultMapAdapter<>(Integer.MAX_VALUE); public ProximityGoal(T definition, Match match) { super(definition, match); match.registerEvents(this); } /** * Get the locations from which proximity can be measured relative to. * The shortest measurement will be used. */ public abstract Iterable<Location> getProximityLocations(ParticipantState player); public @Nullable ProximityMetric getProximityMetric(Competitor team) { return getDefinition().getPreTouchMetric(); } public @Nullable ProximityMetric.Type getProximityMetricType(Competitor team) { ProximityMetric metric = getProximityMetric(team); return metric == null ? null : metric.type; } /** * Is proximity relevant at the present moment for the given team? * That is, can it be measured and affect the outcome of te match? */ public boolean isProximityRelevant(Competitor team) { return canComplete(team) && !isCompleted() && getProximityMetric(team) != null; } protected boolean canPlayerUpdateProximity(ParticipantState player) { return canComplete(player.getParty()); } protected boolean canBlockUpdateProximity(BlockState oldState, BlockState newState) { return true; } private static double distanceFromDistanceSquared(int squared) { return squared == Integer.MAX_VALUE ? Double.POSITIVE_INFINITY : Math.sqrt(squared); } public int getProximity(Competitor team) { return this.proximity.get(team); } /** * Get the minimum distance the given team has been from the objective at * any time during the match (which is +Inf at the start of the match). * The given metric determines exactly how this is measured. */ public double getMinimumDistance(Competitor team) { return distanceFromDistanceSquared(this.getProximity(team)); } public void resetProximity(Competitor team) { Integer oldProximity = proximity.remove(team); if(oldProximity != null) { getMatch().callEvent(new GoalProximityChangeEvent(this, team, null, distanceFromDistanceSquared(oldProximity), Double.POSITIVE_INFINITY)); } } public void resetProximity() { for(Competitor team : ImmutableSet.copyOf(proximity.keySet())) { resetProximity(team); } } public int getProximityFrom(ParticipantState player, Location location) { if(Double.isInfinite(location.lengthSquared())) return Integer.MAX_VALUE; ProximityMetric metric = getProximityMetric(player.getParty()); if(metric == null) return Integer.MAX_VALUE; int minimumDistance = Integer.MAX_VALUE; for(Location v : getProximityLocations(player)) { // If either point is at infinity, the distance is infinite if(Double.isInfinite(v.lengthSquared())) continue; int dx = location.getBlockX() - v.getBlockX(); int dy = location.getBlockY() - v.getBlockY(); int dz = location.getBlockZ() - v.getBlockZ(); // Note: distances stay squared as long as possible int distance; if(metric.horizontal) { distance = dx*dx + dz*dz; } else { distance = dx*dx + dy*dy + dz*dz; } if(distance < minimumDistance) { minimumDistance = distance; } } return minimumDistance; } public boolean updateProximity(ParticipantState player, Location location) { if(isProximityRelevant(player.getParty()) && canPlayerUpdateProximity(player)) { int oldProximity = proximity.get(player.getParty()); int newProximity = getProximityFrom(player, location); if(newProximity < oldProximity) { proximity.put(player.getParty(), newProximity); getMatch().callEvent( new GoalProximityChangeEvent(this, player.getParty(), location, distanceFromDistanceSquared(oldProximity), distanceFromDistanceSquared(newProximity)) ); return true; } } return false; } public boolean shouldShowProximity(@Nullable Competitor team, Party viewer) { return team != null && Config.Scoreboard.showProximity() && isProximityRelevant(team) && (viewer == team || viewer.isObservingType()); } public ChatColor renderProximityColor(Competitor team, Party viewer) { return ChatColor.GRAY; } public String renderProximity(@Nullable Competitor team, Party viewer) { if(!shouldShowProximity(team, viewer)) return ""; String text; double distance = this.getMinimumDistance(team); if(distance == Double.POSITIVE_INFINITY) { text = "\u221e"; // ∞ } else { text = ChatUtils.tiny(String.format(Locales.DEFAULT_LOCALE, "%.1f", distance)); } return renderProximityColor(team, viewer) + text; } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) private void onPlayerMove(CoarsePlayerMoveEvent event) { MatchPlayer player = getMatch().getParticipant(event.getPlayer()); if(player != null && getProximityMetricType(player.getCompetitor()) == ProximityMetric.Type.CLOSEST_PLAYER) { updateProximity(player.getParticipantState(), event.getBlockTo()); } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) private void onPlayerPlaceBlock(ParticipantBlockTransformEvent event) { if(getProximityMetricType(event.getPlayerState().getParty()) == ProximityMetric.Type.CLOSEST_BLOCK && canBlockUpdateProximity(event.getOldState(), event.getNewState())) { updateProximity(event.getPlayerState(), BlockUtils.center(event.getNewState())); } } @EventHandler(priority = EventPriority.MONITOR) private void onPlayerKill(MatchPlayerDeathEvent event) { if(event.getKiller() != null && event.isChallengeKill() && getProximityMetricType(event.getKiller().getParty()) == ProximityMetric.Type.CLOSEST_KILL) { updateProximity(event.getKiller(), event.getKiller().getLocation()); } } @EventHandler(priority = EventPriority.MONITOR) private void onTouch(GoalTouchEvent event) { if(this == event.getGoal() && event.isFirstForCompetitor()) { resetProximity(event.getCompetitor()); } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) private void onComplete(GoalCompleteEvent event) { if(this == event.getGoal()) { resetProximity(); } } @EventHandler(priority = EventPriority.MONITOR) private void onCompetitorRemove(CompetitorRemoveEvent event) { resetProximity(event.getCompetitor()); } }