package com.faforever.client.rankedmatch; import com.faforever.client.api.Ranked1v1Stats; import com.faforever.client.chat.PlayerInfoBean; import com.faforever.client.game.Faction; import com.faforever.client.game.GameService; import com.faforever.client.i18n.I18n; import com.faforever.client.leaderboard.LeaderboardService; import com.faforever.client.player.PlayerService; import com.faforever.client.preferences.PreferencesService; import com.faforever.client.util.RatingUtil; import com.google.common.annotations.VisibleForTesting; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.WeakInvalidationListener; import javafx.collections.ObservableList; import javafx.css.PseudoClass; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.chart.BarChart; import javafx.scene.chart.CategoryAxis; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.ScrollPane; import javafx.scene.control.ToggleButton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.lang.invoke.MethodHandles; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Random; import java.util.stream.Collectors; import static java.lang.Integer.parseInt; @SuppressWarnings("FieldCanBeLocal") public class Ranked1v1Controller { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final PseudoClass NOTIFICATION_HIGHLIGHTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("highlighted-bar"); private final Random random; @FXML CategoryAxis ratingDistributionXAxis; @FXML NumberAxis ratingDistributionYAxis; @FXML BarChart<String, Integer> ratingDistributionChart; @FXML Label ratingHintLabel; @FXML Label searchingForOpponentLabel; @FXML Label ratingLabel; @FXML ProgressIndicator searchProgressIndicator; @FXML ProgressIndicator ratingProgressIndicator; @FXML ToggleButton aeonButton; @FXML ToggleButton uefButton; @FXML ToggleButton cybranButton; @FXML ToggleButton seraphimButton; @FXML Button cancelButton; @FXML Button playButton; @FXML ScrollPane ranked1v1Root; @FXML Label gamesPlayedLabel; @FXML Label rankingLabel; @FXML Label winLossRationLabel; @FXML Label rankingOutOfLabel; @Resource GameService gameService; @Resource PreferencesService preferencesService; @Resource PlayerService playerService; @Resource Environment environment; @Resource LeaderboardService leaderboardService; @Resource I18n i18n; @Resource Locale locale; @VisibleForTesting HashMap<Faction, ToggleButton> factionsToButtons; private InvalidationListener playerRatingListener; private boolean initialized; public Ranked1v1Controller() { random = new Random(); } @FXML void initialize() { cancelButton.managedProperty().bind(cancelButton.visibleProperty()); playButton.managedProperty().bind(playButton.visibleProperty()); ratingLabel.managedProperty().bind(ratingLabel.visibleProperty()); ratingProgressIndicator.managedProperty().bind(ratingProgressIndicator.visibleProperty()); factionsToButtons = new HashMap<>(); factionsToButtons.put(Faction.AEON, aeonButton); factionsToButtons.put(Faction.UEF, uefButton); factionsToButtons.put(Faction.CYBRAN, cybranButton); factionsToButtons.put(Faction.SERAPHIM, seraphimButton); setSearching(false); } private void setSearching(boolean searching) { cancelButton.setVisible(searching); playButton.setVisible(!searching); searchProgressIndicator.setVisible(searching); searchingForOpponentLabel.setVisible(searching); setFactionButtonsDisabled(searching); } private void setFactionButtonsDisabled(boolean disabled) { factionsToButtons.values().forEach(button -> button.setDisable(disabled)); } @PostConstruct void postConstruct() { gameService.searching1v1Property().addListener((observable, oldValue, newValue) -> { setSearching(newValue); }); ObservableList<Faction> factions = preferencesService.getPreferences().getRanked1v1().getFactions(); for (Faction faction : factions) { factionsToButtons.get(faction).setSelected(true); } playButton.setDisable(factions.isEmpty()); preferencesService.addUpdateListener(preferences -> { if (preferencesService.getPreferences().getForgedAlliance().getPath() == null) { onCancelButtonClicked(); } }); } @FXML void onCancelButtonClicked() { gameService.stopSearchRanked1v1(); setSearching(false); } public Node getRoot() { return ranked1v1Root; } @FXML void onPlayButtonClicked() { if (preferencesService.getPreferences().getForgedAlliance().getPath() == null) { // FIXME implement user notification return; } setSearching(true); setFactionButtonsDisabled(true); ObservableList<Faction> factions = preferencesService.getPreferences().getRanked1v1().getFactions(); Faction randomFaction = factions.get(random.nextInt(factions.size())); gameService.startSearchRanked1v1(randomFaction); } @FXML void onFactionButtonClicked() { List<Faction> factions = factionsToButtons.entrySet().stream() .filter(entry -> entry.getValue().isSelected()) .map(Map.Entry::getKey) .collect(Collectors.toList()); preferencesService.getPreferences().getRanked1v1().getFactions().setAll(factions); preferencesService.storeInBackground(); playButton.setDisable(factions.isEmpty()); } public void setUpIfNecessary() { if (initialized) { return; } playerService.currentPlayerProperty().addListener((observable, oldValue, newValue) -> { setCurrentPlayer(newValue); }); setCurrentPlayer(playerService.getCurrentPlayer()); initialized = true; } private void setCurrentPlayer(PlayerInfoBean player) { playerRatingListener = ratingObservable -> Platform.runLater(() -> updateRating(player)); player.leaderboardRatingDeviationProperty().addListener(new WeakInvalidationListener(playerRatingListener)); player.leaderboardRatingMeanProperty().addListener(new WeakInvalidationListener(playerRatingListener)); updateRating(player); updateOtherValues(player); } private void updateRating(PlayerInfoBean player) { int rating = RatingUtil.getLeaderboardRating(player); int beta = environment.getProperty("rating.beta", int.class); float deviation = player.getLeaderboardRatingDeviation(); if (deviation > beta) { int initialStandardDeviation = environment.getProperty("rating.initialStandardDeviation", int.class); ratingProgressIndicator.setProgress((initialStandardDeviation - deviation) / beta); ratingProgressIndicator.setVisible(true); ratingLabel.setVisible(false); ratingHintLabel.setText(i18n.get("ranked1v1.ratingProgress.stillLearning")); } else { ratingProgressIndicator.setVisible(false); ratingLabel.setVisible(true); ratingLabel.setText(String.format(locale, "%d", rating)); updateRatingHint(rating); } leaderboardService.getRanked1v1Stats() .thenAccept(ranked1v1Stats -> { int totalPlayers = 0; for (Map.Entry<String, Integer> entry : ranked1v1Stats.getRatingDistribution().entrySet()) { totalPlayers += entry.getValue(); } plotRatingDistributions(ranked1v1Stats, player); String rankingOutOfText = i18n.get("ranked1v1.rankingOutOf", totalPlayers); Platform.runLater(() -> rankingOutOfLabel.setText(rankingOutOfText)); }) .exceptionally(throwable -> { logger.warn("Could not plot rating distribution", throwable); return null; }); } private void updateOtherValues(PlayerInfoBean currentPlayer) { leaderboardService.getEntryForPlayer(currentPlayer.getId()).thenAccept(leaderboardEntryBean -> Platform.runLater(() -> { rankingLabel.setText(i18n.get("ranked1v1.rankingFormat", leaderboardEntryBean.getRank())); gamesPlayedLabel.setText(String.format("%d", leaderboardEntryBean.getGamesPlayed())); winLossRationLabel.setText(i18n.get("percentage", leaderboardEntryBean.getWinLossRatio() * 100)); })).exceptionally(throwable -> { logger.warn("Leaderboard entry could not be read for current player: " + currentPlayer.getUsername()); return null; }); } private void updateRatingHint(int rating) { if (rating < environment.getProperty("rating.low", int.class)) { ratingHintLabel.setText(i18n.get("ranked1v1.ratingHint.low")); } else if (rating < environment.getProperty("rating.moderate", int.class)) { ratingHintLabel.setText(i18n.get("ranked1v1.ratingHint.moderate")); } else if (rating < environment.getProperty("rating.good", int.class)) { ratingHintLabel.setText(i18n.get("ranked1v1.ratingHint.good")); } else if (rating < environment.getProperty("rating.high", int.class)) { ratingHintLabel.setText(i18n.get("ranked1v1.ratingHint.high")); } else if (rating < environment.getProperty("rating.top", int.class)) { ratingHintLabel.setText(i18n.get("ranked1v1.ratingHint.top")); } } @SuppressWarnings("unchecked") private void plotRatingDistributions(Ranked1v1Stats ranked1v1Stats, PlayerInfoBean player) { XYChart.Series<String, Integer> series = new XYChart.Series<>(); series.setName(i18n.get("ranked1v1.players")); series.getData().addAll(ranked1v1Stats.getRatingDistribution().entrySet().stream() .sorted((o1, o2) -> Integer.compare(parseInt(o1.getKey()), parseInt(o2.getKey()))) .map(item -> { int rating = parseInt(item.getKey()); XYChart.Data<String, Integer> data = new XYChart.Data<>(String.format(locale, "%d", rating), item.getValue()); int currentPlayerRating = RatingUtil.getLeaderboardRating(player); if (rating == currentPlayerRating) { data.nodeProperty().addListener((observable, oldValue, newValue) -> { newValue.pseudoClassStateChanged(NOTIFICATION_HIGHLIGHTED_PSEUDO_CLASS, true); }); } return data; }) .collect(Collectors.toList())); Platform.runLater(() -> ratingDistributionChart.getData().setAll(series)); } }