From 47a7adfac8e1f7539bf48f0b7a72e6bf5f0e1324 Mon Sep 17 00:00:00 2001 From: BlackYps Date: Fri, 8 Mar 2024 23:46:13 +0100 Subject: [PATCH] Decouple rating and division displaying --- .../client/domain/api/GamePlayerStats.java | 12 ++- .../client/game/PlayerCardController.java | 39 +++++++++- .../client/game/TeamCardController.java | 77 ++++++++++++++++++- .../client/mapstruct/ReplayMapper.java | 1 + .../client/replay/ReplayDetailController.java | 46 ++++++++++- src/main/resources/i18n/messages.properties | 6 +- src/main/resources/theme/player_card.fxml | 5 +- src/main/resources/theme/team_card.fxml | 5 +- 8 files changed, 172 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/faforever/client/domain/api/GamePlayerStats.java b/src/main/java/com/faforever/client/domain/api/GamePlayerStats.java index 75070cdd08..ab045447d8 100644 --- a/src/main/java/com/faforever/client/domain/api/GamePlayerStats.java +++ b/src/main/java/com/faforever/client/domain/api/GamePlayerStats.java @@ -7,8 +7,16 @@ import java.util.List; public record GamePlayerStats( - PlayerInfo player, byte score, byte team, - Faction faction, OffsetDateTime scoreTime, List leaderboardRatingJournals + boolean ai, + Faction faction, + byte color, + byte team, + byte startSpot, + byte score, + OffsetDateTime scoreTime, + GameOutcome outcome, + PlayerInfo player, + List leaderboardRatingJournals ) { public GamePlayerStats { diff --git a/src/main/java/com/faforever/client/game/PlayerCardController.java b/src/main/java/com/faforever/client/game/PlayerCardController.java index 632e5d6545..c907ce9aae 100644 --- a/src/main/java/com/faforever/client/game/PlayerCardController.java +++ b/src/main/java/com/faforever/client/game/PlayerCardController.java @@ -4,6 +4,7 @@ import com.faforever.client.avatar.AvatarService; import com.faforever.client.domain.api.GamePlayerStats; import com.faforever.client.domain.api.LeaderboardRatingJournal; +import com.faforever.client.domain.api.Subdivision; import com.faforever.client.domain.server.PlayerInfo; import com.faforever.client.fx.JavaFxUtil; import com.faforever.client.fx.NodeController; @@ -21,6 +22,7 @@ import com.faforever.client.fx.contextmenu.ShowPlayerInfoMenuItem; import com.faforever.client.fx.contextmenu.ViewReplaysMenuItem; import com.faforever.client.i18n.I18n; +import com.faforever.client.leaderboard.LeaderboardService; import com.faforever.client.player.CountryFlagService; import com.faforever.client.player.SocialStatus; import com.faforever.client.theme.ThemeService; @@ -60,6 +62,7 @@ public class PlayerCardController extends NodeController { private final UiService uiService; private final CountryFlagService countryFlagService; private final AvatarService avatarService; + private final LeaderboardService leaderboardService; private final ContextMenuBuilder contextMenuBuilder; private final I18n i18n; @@ -72,21 +75,29 @@ public class PlayerCardController extends NodeController { public Label friendIconText; public Region factionIcon; public ImageView factionImage; + public ImageView divisionImageView; public Label noteIcon; + public Label ratingLabel; public Label ratingChange; private final ObjectProperty player = new SimpleObjectProperty<>(); private final ObjectProperty playerStats = new SimpleObjectProperty<>(); private final ObjectProperty rating = new SimpleObjectProperty<>(); + private final ObjectProperty division = new SimpleObjectProperty<>(); private final ObjectProperty faction = new SimpleObjectProperty<>(); private final Tooltip noteTooltip = new Tooltip(); private final Tooltip avatarTooltip = new Tooltip(); + private final Tooltip divisionTooltip = new Tooltip(); @Override protected void onInitialize() { - JavaFxUtil.bindManagedToVisible(avatarStackPane, factionIcon, foeIconText, factionImage, friendIconText, countryImageView, noteIcon); + JavaFxUtil.bindManagedToVisible(avatarStackPane, factionIcon, foeIconText, factionImage, friendIconText, + countryImageView, divisionImageView, ratingLabel, ratingChange, noteIcon); countryImageView.visibleProperty().bind(countryImageView.imageProperty().isNotNull()); avatarImageView.visibleProperty().bind(avatarImageView.imageProperty().isNotNull()); + divisionImageView.visibleProperty().bind(divisionImageView.imageProperty().isNotNull()); + ratingLabel.visibleProperty().bind(rating.isNotNull().and(divisionImageView.imageProperty().isNull())); + ratingChange.visibleProperty().bind(playerStats.isNotNull().and(divisionImageView.imageProperty().isNull())); factionImage.setImage(uiService.getImage(ThemeService.RANDOM_FACTION_IMAGE)); factionImage.visibleProperty().bind(faction.map(value -> value == Faction.RANDOM)); @@ -97,10 +108,12 @@ protected void onInitialize() { .when(showing)); avatarImageView.imageProperty() .bind(player.flatMap(PlayerInfo::avatarProperty).map(avatarService::loadAvatar).when(showing)); + divisionImageView.imageProperty() + .bind(division.map(Subdivision::smallImageUrl).map(leaderboardService::loadDivisionImage).when(showing)); playerInfo.textProperty().bind(player.flatMap(PlayerInfo::usernameProperty) - .flatMap(username -> rating.map(value -> i18n.get("userInfo.tooltipFormat.withRating", username, value)) - .orElse(i18n.get("userInfo.tooltipFormat.noRating", username))) .when(showing)); + ratingLabel.textProperty().bind(rating.map(value -> i18n.get("game.tooltip.ratingFormat", value)) + .when(showing)); foeIconText.visibleProperty().bind(player.flatMap(PlayerInfo::socialStatusProperty) .map(socialStatus -> socialStatus == SocialStatus.FOE) .when(showing)); @@ -111,7 +124,6 @@ protected void onInitialize() { .when(showing) .addListener((SimpleChangeListener) this::onNoteChanged); - ratingChange.visibleProperty().bind(playerStats.isNotNull()); ObservableValue ratingChangeObservable = playerStats.map(GamePlayerStats::leaderboardRatingJournals) .map( journals -> journals.isEmpty() ? null : journals.getFirst()) @@ -131,6 +143,13 @@ protected void onInitialize() { avatarTooltip.setShowDelay(Duration.ZERO); avatarTooltip.setShowDuration(Duration.seconds(30)); Tooltip.install(avatarImageView, avatarTooltip); + + divisionTooltip.textProperty().bind( + division.map(value -> i18n.get("leaderboard.divisionName", i18n.get("leagues.divisionName.%s".formatted(value.division().nameKey())), value.nameKey())) + .when(showing)); + divisionTooltip.setShowDelay(Duration.ZERO); + divisionTooltip.setShowDuration(Duration.seconds(30)); + Tooltip.install(divisionImageView, divisionTooltip); } private void onNoteChanged(String newValue) { @@ -241,6 +260,18 @@ public void setRating(Integer rating) { this.rating.set(rating); } + public Subdivision getDivision() { + return division.get(); + } + + public ObjectProperty divisionProperty() { + return division; + } + + public void setDivision(Subdivision subdivision) { + this.division.set(subdivision); + } + public Faction getFaction() { return faction.get(); } diff --git a/src/main/java/com/faforever/client/game/TeamCardController.java b/src/main/java/com/faforever/client/game/TeamCardController.java index 11d5c63073..7ab8adf7d3 100644 --- a/src/main/java/com/faforever/client/game/TeamCardController.java +++ b/src/main/java/com/faforever/client/game/TeamCardController.java @@ -2,9 +2,12 @@ import com.faforever.client.domain.api.GamePlayerStats; +import com.faforever.client.domain.api.Subdivision; import com.faforever.client.domain.server.GameInfo; +import com.faforever.client.domain.api.GameOutcome; import com.faforever.client.domain.server.PlayerInfo; import com.faforever.client.fx.FxApplicationThreadExecutor; +import com.faforever.client.fx.JavaFxUtil; import com.faforever.client.fx.NodeController; import com.faforever.client.fx.SimpleChangeListener; import com.faforever.client.i18n.I18n; @@ -12,12 +15,14 @@ import com.faforever.client.theme.UiService; import com.faforever.client.util.RatingUtil; import com.faforever.commons.api.dto.Faction; +import javafx.beans.binding.Bindings; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; +import javafx.css.PseudoClass; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.layout.Pane; @@ -43,6 +48,12 @@ @Slf4j @RequiredArgsConstructor public class TeamCardController extends NodeController { + + private static final PseudoClass VICTORY = PseudoClass.getPseudoClass("victory"); + private static final PseudoClass DEFEAT = PseudoClass.getPseudoClass("defeat"); + private static final PseudoClass DRAW = PseudoClass.getPseudoClass("draw"); + private static final PseudoClass UNKNOWN = PseudoClass.getPseudoClass("unknown"); + private final I18n i18n; private final PlayerService playerService; private final FxApplicationThreadExecutor fxApplicationThreadExecutor; @@ -51,12 +62,16 @@ public class TeamCardController extends NodeController { public Pane teamPaneRoot; public VBox teamPane; public Label teamNameLabel; + public Label teamRatingLabel; + public Label gameResultLabel; private final ObjectProperty> playerIds = new SimpleObjectProperty<>(List.of()); private final ObjectProperty> players = new SimpleObjectProperty<>(List.of()); private final ObjectProperty> ratingProvider = new SimpleObjectProperty<>(); + private final ObjectProperty> divisionProvider = new SimpleObjectProperty<>(); private final ObjectProperty> factionProvider = new SimpleObjectProperty<>(); private final ObjectProperty ratingPrecision = new SimpleObjectProperty<>(); + private final ObjectProperty teamOutcome = new SimpleObjectProperty<>(); private final IntegerProperty teamId = new SimpleIntegerProperty(); private final SimpleChangeListener> playersListener = this::populateTeamContainer; private final ObservableValue teamRating = ratingProvider.flatMap(provider -> ratingPrecision.flatMap(precision -> players.map(playerBeans -> playerBeans.stream() @@ -70,18 +85,21 @@ public class TeamCardController extends NodeController { @Override protected void onInitialize() { teamNameLabel.textProperty() - .bind(teamRating.flatMap(teamRating -> teamId.map(id -> switch (id.intValue()) { + .bind(teamId.map(id -> switch (id.intValue()) { case 0, GameInfo.NO_TEAM -> i18n.get("game.tooltip.teamTitleNoTeam"); case GameInfo.OBSERVERS_TEAM -> i18n.get("game.tooltip.observers"); default -> { try { - yield i18n.get("game.tooltip.teamTitle", id.intValue() - 1, teamRating); + yield i18n.get("game.tooltip.teamTitle", id.intValue() - 1); } catch (NumberFormatException e) { yield ""; } } - }))); - + })); + JavaFxUtil.bindManagedToVisible(teamRatingLabel, gameResultLabel); + teamRatingLabel.textProperty().bind(teamRating.map(value -> i18n.get("game.tooltip.ratingFormat", value))); + teamRatingLabel.visibleProperty().bind(Bindings.createBooleanBinding(() -> teamRating.getValue() != null && teamRating.getValue() != 0, teamRating)); + gameResultLabel.visibleProperty().bind(gameResultLabel.textProperty().isEmpty().not()); players.addListener(playersListener); } @@ -99,6 +117,8 @@ private List createPlayerCardControllers(List controller.ratingProperty() .bind(ratingProvider.map(ratingFunction -> ratingFunction.apply(player)) .flatMap(rating -> ratingPrecision.map(precision -> precision == RatingPrecision.ROUNDED ? RatingUtil.getRoundedRating(rating) : rating))); + controller.divisionProperty() + .bind(divisionProvider.map(divisionFunction -> divisionFunction.apply(player))); controller.factionProperty() .bind(factionProvider.map(factionFunction -> factionFunction.apply(player))); controller.setPlayer(player); @@ -109,6 +129,39 @@ private List createPlayerCardControllers(List }).toList(); } + public void showGameResult() { + switch (teamOutcome.get()) { + case VICTORY -> { + gameResultLabel.setText(i18n.get("game.resultVictory")); + gameResultLabel.pseudoClassStateChanged(VICTORY, true); + gameResultLabel.pseudoClassStateChanged(DEFEAT, false); + gameResultLabel.pseudoClassStateChanged(DRAW, false); + gameResultLabel.pseudoClassStateChanged(UNKNOWN, false); + } + case DEFEAT -> { + gameResultLabel.setText(i18n.get("game.resultDefeat")); + gameResultLabel.pseudoClassStateChanged(VICTORY, false); + gameResultLabel.pseudoClassStateChanged(DEFEAT, true); + gameResultLabel.pseudoClassStateChanged(DRAW, false); + gameResultLabel.pseudoClassStateChanged(UNKNOWN, false); + } + case DRAW, MUTUAL_DRAW -> { + gameResultLabel.setText(i18n.get("game.resultDraw")); + gameResultLabel.pseudoClassStateChanged(VICTORY, false); + gameResultLabel.pseudoClassStateChanged(DEFEAT, false); + gameResultLabel.pseudoClassStateChanged(DRAW, true); + gameResultLabel.pseudoClassStateChanged(UNKNOWN, false); + } + default -> { + gameResultLabel.setText(i18n.get("game.resultUnknown")); + gameResultLabel.pseudoClassStateChanged(VICTORY, false); + gameResultLabel.pseudoClassStateChanged(DEFEAT, false); + gameResultLabel.pseudoClassStateChanged(DRAW, false); + gameResultLabel.pseudoClassStateChanged(UNKNOWN, true); + } + } + } + public void bindPlayersToPlayerIds() { players.bind(playerIds.map(ids -> ids.stream() .map(playerService::getPlayerByIdIfOnline) @@ -120,6 +173,10 @@ public void setRatingProvider(Function ratingProvider) { this.ratingProvider.set(ratingProvider); } + public void setDivisionProvider(Function divisionProvider) { + this.divisionProvider.set(divisionProvider); + } + public void setFactionProvider(Function factionProvider) { this.factionProvider.set(factionProvider); } @@ -156,6 +213,18 @@ public ObjectProperty> ratingProviderProperty() { return ratingProvider; } + public void setTeamOutcome(GameOutcome teamOutcome) { + this.teamOutcome.set(teamOutcome); + } + + public GameOutcome getTeamOutcome() { + return teamOutcome.get(); + } + + public ObjectProperty teamResult() { + return teamOutcome; + } + public void setStats(List teamPlayerStats) { for (GamePlayerStats playerStats : teamPlayerStats) { PlayerCardController controller = playerCardControllersMap.get(playerStats.player()); diff --git a/src/main/java/com/faforever/client/mapstruct/ReplayMapper.java b/src/main/java/com/faforever/client/mapstruct/ReplayMapper.java index 784f259091..f70c72c1f3 100644 --- a/src/main/java/com/faforever/client/mapstruct/ReplayMapper.java +++ b/src/main/java/com/faforever/client/mapstruct/ReplayMapper.java @@ -161,6 +161,7 @@ default List mapToTeamPlayerStats(Map { + teamCardControllers.forEach(teamCardController -> teamCardController.setDivisionProvider(player -> getPlayerDivision(player, scores))); + }); + reviewsController.setCanWriteReview(true); reviewService.getReplayReviews(newValue) @@ -406,6 +417,10 @@ private CompletableFuture enrichReplayLater(Path path, Replay replay) { private void populateTeamsContainer(java.util.Map> newValue) { CompletableFuture.supplyAsync(() -> createTeamCardControllers(newValue)).thenAcceptAsync(controllers -> { teamCardControllers.clear(); + if (controllers.stream() + .map(TeamCardController::getTeamOutcome).noneMatch(gameOutcome -> gameOutcome != GameOutcome.DEFEAT)) { + controllers.forEach(teamCardController -> teamCardController.setTeamOutcome(GameOutcome.DRAW)); + } teamCardControllers.addAll(controllers); teamsContainer.getChildren().setAll(teamCardControllers.stream().map(TeamCardController::getRoot).toList()); }, fxApplicationThreadExecutor); @@ -423,6 +438,7 @@ private List createTeamCardControllers(java.util.Map getPlayerRating(player, statsByPlayer)); controller.setFactionProvider(player -> getPlayerFaction(player, statsByPlayer)); @@ -433,6 +449,25 @@ private List createTeamCardControllers(java.util.Map statsByPlayer) { + // Game outcomes are saved since 2020, so this should suffice for the + // vast majority of replays that people will realistically look up. + java.util.Map outcomeCounts = statsByPlayer.stream() + .map(GamePlayerStats::outcome) + .filter(Objects::nonNull) + .map(gameOutcome -> (gameOutcome == GameOutcome.CONFLICTING) ? GameOutcome.UNKNOWN : gameOutcome) + .map(gameOutcome -> (gameOutcome == GameOutcome.MUTUAL_DRAW) ? GameOutcome.DRAW : gameOutcome) + .collect(Collectors.groupingBy(gameOutcome -> gameOutcome, Collectors.counting())); + + if (outcomeCounts.containsKey(GameOutcome.VICTORY)) { + return GameOutcome.VICTORY; + } + + return outcomeCounts.entrySet() + .stream() + .max(Entry.comparingByValue()).map(Entry::getKey).orElse(GameOutcome.UNKNOWN); + } + private Faction getPlayerFaction(PlayerInfo player, java.util.Map statsByPlayerId) { GamePlayerStats playerStats = statsByPlayerId.get(player); return playerStats == null ? null : playerStats.faction(); @@ -449,6 +484,15 @@ private Integer getPlayerRating(PlayerInfo player, java.util.Map journals) { + return journals + .stream() + .filter(journal -> journal.loginId() == player.getId()) + .findFirst() + .map(LeagueScoreJournal::divisionBefore) + .orElse(null); + } + public void onReport() { ReportDialogController reportDialogController = uiService.loadFxml("theme/reporting/report_dialog.fxml"); reportDialogController.setReplay(replay.get()); @@ -506,8 +550,8 @@ public void copyLink() { } public void showRatingChange() { + teamCardControllers.forEach(TeamCardController::showGameResult); java.util.Map> teamsValue = teams.get(); - teamCardControllers.forEach(teamCardController -> teamCardController.setStats( teamsValue.get(String.valueOf(teamCardController.getTeamId())))); } diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index a8f57e814b..f289e95bf1 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -187,9 +187,9 @@ game.detail.players.format = {0,number,#}/{1,number,#} players game.join = Join game.cancel = Cancel game.host = Host -game.tooltip.teamTitle = Team {0} ({1,number,#}) +game.tooltip.teamTitle = Team {0} +game.tooltip.ratingFormat = ({0,number,#}) game.tooltip.teamTitleNoTeam = No Team -game.tooltip.teamTitleNoRating = Team {0} game.tooltip.observers = Observers game.join.passwordPrompt = Enter game password game.join.passwordWrong = Incorrect password @@ -1013,8 +1013,6 @@ leaderboard.global.name = Global leaderboard.gamesPlayed = {0} Games Played\: {1,number,#} leaderboard.rating = {0} Rating leaderboard.displayName = Leaderboard -userInfo.tooltipFormat.withRating = {0} ({1,number,#}) -userInfo.tooltipFormat.noRating = {0} map.updater.search = Search for a new version of the map teammatchmaking.chat.topic = Find teammates and get gameplay advice on our official discord\: getHelp = Get Help diff --git a/src/main/resources/theme/player_card.fxml b/src/main/resources/theme/player_card.fxml index 5616ababf6..7f4e4a45b2 100644 --- a/src/main/resources/theme/player_card.fxml +++ b/src/main/resources/theme/player_card.fxml @@ -51,9 +51,8 @@ -