package tc.oc.pgm.goals; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map.Entry; import com.google.common.cache.LoadingCache; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import com.google.common.reflect.TypeToken; import org.bukkit.Sound; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import tc.oc.commons.bukkit.chat.BukkitSound; import tc.oc.commons.core.util.CacheUtils; import tc.oc.pgm.events.CompetitorAddEvent; import tc.oc.pgm.events.CompetitorRemoveEvent; import tc.oc.pgm.events.ListenerScope; import tc.oc.pgm.goals.events.GoalCompleteEvent; import tc.oc.pgm.goals.events.GoalProximityChangeEvent; import tc.oc.pgm.goals.events.GoalStatusChangeEvent; import tc.oc.pgm.goals.events.GoalTouchEvent; import tc.oc.pgm.match.Competitor; import tc.oc.pgm.match.MatchModule; import tc.oc.pgm.match.MatchPlayer; import tc.oc.pgm.match.MatchScope; import tc.oc.pgm.victory.VictoryMatchModule; @ListenerScope(MatchScope.LOADED) public class GoalMatchModule extends MatchModule implements Listener { protected static final BukkitSound GOOD_SOUND = new BukkitSound(Sound.BLOCK_PORTAL_TRAVEL, 0.7f, 2f); protected static final BukkitSound BAD_SOUND = new BukkitSound(Sound.ENTITY_BLAZE_DEATH, 0.8f, 0.8f); protected final List<Goal> goals = new ArrayList<>(); protected final Multimap<Competitor, Goal> goalsByCompetitor = ArrayListMultimap.create(); protected final Multimap<Goal, Competitor> competitorsByGoal = HashMultimap.create(); protected final LoadingCache<Competitor, GoalProgress> progressCache = CacheUtils.newCache(GoalProgress::new); @Override public void load() { super.load(); match.featureDefinitions() .all(new TypeToken<GoalDefinition<?>>(){}) .map(match.features()::get) .forEach(this::addGoal); } private void addGoal(Goal<?> goal) { logger.fine("Adding goal " + goal); if(!goal.isVisible()) return; if(goals.isEmpty()) { logger.fine("First goal added, appending " + GoalsVictoryCondition.class.getSimpleName()); match.needMatchModule(VictoryMatchModule.class).setVictoryCondition(new GoalsVictoryCondition(goalsByCompetitor)); } goals.add(goal); for(Competitor competitor : match.getCompetitors()) { addCompetitorGoal(competitor, goal); } } public Collection<Goal> getGoals() { return goals; } public Collection<Goal> getGoals(Competitor competitor) { return goalsByCompetitor.get(competitor); } public Collection<Competitor> getCompetitors(Goal goal) { return competitorsByGoal.get(goal); } public Multimap<Competitor, Goal> getGoalsByCompetitor() { return goalsByCompetitor; } public Multimap<Goal, Competitor> getCompetitorsByGoal() { return competitorsByGoal; } private void addCompetitorGoal(Competitor competitor, Goal<?> goal) { if(goal.canComplete(competitor)) { logger.fine("Competitor " + competitor + " can complete goal " + goal); goalsByCompetitor.put(competitor, goal); competitorsByGoal.put(goal, competitor); } } @EventHandler public void onCompetitorAdd(CompetitorAddEvent event) { logger.fine("Competitor added " + event.getCompetitor()); for(Goal goal : goals) { addCompetitorGoal(event.getCompetitor(), goal); } } @EventHandler public void onCompetitorRemove(CompetitorRemoveEvent event) { progressCache.invalidate(event.getCompetitor()); goalsByCompetitor.removeAll(event.getCompetitor()); competitorsByGoal.entries().removeIf(entry -> entry.getValue().equals(event.getCompetitor())); } @SuppressWarnings("unchecked") public <T extends Goal> Multimap<Competitor, T> getGoals(Class<T> filterClass) { Multimap<Competitor, T> filteredGoals = ArrayListMultimap.create(); for(Entry<Competitor, Goal> entry : this.goalsByCompetitor.entries()) { if(filterClass.isInstance(entry.getValue())) { filteredGoals.put(entry.getKey(), (T) entry.getValue()); } } return filteredGoals; } public int compareProgress(Competitor a, Competitor b) { return progressCache.getUnchecked(a).compareTo(progressCache.getUnchecked(b)); } protected void updateProgress(Goal goal) { competitorsByGoal.get(goal).forEach(progressCache::invalidate); match.needMatchModule(VictoryMatchModule.class).invalidateCompetitorRanking(); } // TODO: These events will often be fired together.. debounce them somehow? @EventHandler public void onComplete(GoalCompleteEvent event) { updateProgress(event.getGoal()); // Don't play the objective sound if the match is over, because the win/lose sound will play instead if(!match.needMatchModule(VictoryMatchModule.class).checkMatchEnd() && event.getGoal().isVisible()) { for(MatchPlayer player : event.getMatch().getPlayers()) { if(!(player.getParty() instanceof Competitor)) { player.playSound(GOOD_SOUND); } else { final Competitor competitor = (Competitor) player.getParty(); player.playSound(event.wasCompletedFor(competitor) && !event.isCompletedFor(competitor) ? BAD_SOUND : GOOD_SOUND); } } } } @EventHandler public void onStatusChange(GoalStatusChangeEvent event) { updateProgress(event.getGoal()); } @EventHandler public void onProximityChange(GoalProximityChangeEvent event) { updateProgress(event.getGoal()); } @EventHandler public void onTouch(GoalTouchEvent event) { updateProgress(event.getGoal()); } }