diff --git a/README.md b/README.md index f7c98ea..875e803 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,21 @@ Cobblemon Challenge is an extremely simple plugin for Cobblemon that makes challenging your friends and rivals to flat-level Pokemon battles easier! This is a server-side plugin that only needs to be installed on the Server. -#### Commands - +#### Command Parameters ```/challenge ``` - Challenges specified player to a lvl 50 pokemon battle. They may accept or deny this challenge. -```/challenge level ``` - Challenges specified player to a lvl X battle where X can be 1-100. +```/challenge level ``` - Challenges specified player to a lvl `````` battle where `````` can be **1 min** & **100 max**. + +```/challenge levelRange ``` - Challenges specified player to a lvl where the Pokemon's levels are clamped between the ``````&`````` where ``````&`````` can be **1 min** & **100 max**. + +```/challenge handicap ``` - Challenges specified player to a battle where `````` (the challenger) & `````` (the challenged) Pokemon level is offset by the input level. ``````&`````` can be from **-99 min** & **99 max**. + +```/challenge showPreview ``` - Challenges specified player to a default battle with no preview of the rivals pokemon during starter selection. + +##### Notes About Command Parameters: +1. The order of parameter input must be ```username``` -> ```level/levelRange``` -> ```handicap``` -> ```showPreview```. +2. If the challenge property is optional & not specified in the command. The default values from the config file will be used. -Example: ```/challenge TurtleHoarder level 100``` challenges TurtleHoarder to a level 100 battle. #### Configurations There are numerous options that will allow you to customize the Challenge experience to your server's needs. These settings can be found in the Cobblemon Challenge config file: @@ -18,14 +26,49 @@ settings can be found in the Cobblemon Challenge config file: ```challengeDistanceRestriction``` - The value that determines if challenges are restricted by distance. Set to **false** if you would want no restrictions on distance. This value is set to **true** by default. -```maxChallengeDistance``` - If challengeDistanceRestriction is set to **true**, then this value defines the max distance +```maxChallengeDistance``` - If challengeDistanceRestriction is set to **true**, then this value defines the max distance that a challenge can be sent. This is set to 50 blocks by default. -```defaultChallengeLevel``` - The value that determines the level of a challenge if there is not level specified by the challenger. +```defaultChallengeLevel``` - The value that determines the level of a challenge if there is no level specified by the challenger. This is set to 50 by default for lvl 50 battles. +```defaultHandicap``` - The value that determines each player's final level of each Pokemon if a handicap is not specified by the challenger. +This is set to 0 by default. + +```defaultShowPreview``` - If defaultShowPreview is set to **true** both players will see which pokemon the opponent is bringing to battle. If defaultShowPreview is set to **false** both players parties will be hidden from their opponent. +(Note: Even when defaultShowPreview is true the player will never see the opponents chosen lead pokemon) +This is set to true by default. + ```challengeExpirationTime``` - The value that determines how long a challenge should be pending before it expires. This is set to 60000 milliseconds / 1 minute by default. -```challengeCooldownTime``` - The value that determines how long a player must wait before sending a consecutive request. This value is +```challengeCooldownTime``` - The value that determines how long a player must wait before sending a consecutive request. This value is set to 5000 milliseconds / 5 seconds by default, though players will need to wait until their existing challenge expires before sending another one. + +### All Possible Commands: + +```/challenge ``` + +```/challenge handicap ``` + +```/challenge showPreview ``` + +```/challenge handicap showPreview ``` + + +```/challenge level ``` + +```/challenge level handicap ``` + +```/challenge level showPreview ``` + +```/challenge level handicap showPreview ``` + + +```/challenge levelRange ``` + +```/challenge levelRange handicap ``` + +```/challenge levelRange showPreview ``` + +```/challenge levelRange handicap showPreview ``` \ No newline at end of file diff --git a/build.gradle b/build.gradle index 0cec26b..ff51ebb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,18 +1,13 @@ plugins { - id 'fabric-loom' version '1.2-SNAPSHOT' + id 'fabric-loom' version '1.6-SNAPSHOT' id 'maven-publish' - id 'org.jetbrains.kotlin.jvm' version "1.7.10" + id 'org.jetbrains.kotlin.jvm' version "2.0.0" } version = project.mod_version group = project.maven_group repositories { - // Add repositories to retrieve artifacts from in here. - // You should only use this when depending on other mods because - // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. - // See https://docs.gradle.org/current/userguide/declaring_repositories.html - // for more information about repositories. maven {url = "https://maven.parchmentmc.org"} maven { url "https://cursemaven.com" @@ -46,6 +41,7 @@ dependencies { //modRuntimeOnly"curse.maven:cobblemon-687131:4468330" modImplementation"curse.maven:architectury-419699:4663010" + modImplementation("net.fabricmc:fabric-language-kotlin:1.11.0+kotlin.2.0.0") } base { diff --git a/gradle.properties b/gradle.properties index e78e6ff..e7ecc3d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.parallel=true # check these on https://fabricmc.net/develop minecraft_version=1.20.1 yarn_mappings=1.20.1+build.10 -loader_version=0.14.22 +loader_version=0.15.11 # Mod Properties mod_version = 1.1.7 @@ -14,5 +14,5 @@ maven_group = com.turtlehoarder.cobblemonchallenge archives_base_name = cobblemonchallenge # Dependencies -fabric_version=0.89.3+1.20.1 -cloth_config_version=11.1.106 \ No newline at end of file +fabric_version=0.92.2+1.20.1 +cloth_config_version=11.1.118 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index db9a6b8..a8382d7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/src/main/java/com/cobblemontournament/CobblemonTournament.java b/src/main/java/com/cobblemontournament/CobblemonTournament.java new file mode 100644 index 0000000..3ad8f70 --- /dev/null +++ b/src/main/java/com/cobblemontournament/CobblemonTournament.java @@ -0,0 +1,24 @@ +package com.cobblemontournament; + +import com.cobblemontournament.config.TournamentConfig; +import com.cobblemontournament.testing.command.TournamentCommandTest; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CobblemonTournament implements ModInitializer +{ + public static String MODID = "cobblemontournament"; + public static final Logger LOGGER = LoggerFactory.getLogger("cobblemontournament"); + + @Override + public void onInitialize() + { + TournamentConfig.registerConfigs(); + + // for testing + CommandRegistrationCallback.EVENT.register((commandDispatcher, commandBuildContext, commandSelection) -> + TournamentCommandTest.register(commandDispatcher)); + } +} diff --git a/src/main/java/com/cobblemontournament/api/builder/TournamentBuilder.java b/src/main/java/com/cobblemontournament/api/builder/TournamentBuilder.java new file mode 100644 index 0000000..6c93347 --- /dev/null +++ b/src/main/java/com/cobblemontournament/api/builder/TournamentBuilder.java @@ -0,0 +1,301 @@ +package com.cobblemontournament.api.builder; + +import com.cobblemontournament.api.round.TournamentRoundType; +import com.cobblemontournament.api.tournament.Tournament; +import com.cobblemontournament.api.match.TournamentMatch; +import com.cobblemontournament.api.player.SeededPlayer; +import com.cobblemontournament.api.round.TournamentRound; +import com.cobblemontournament.util.IndexedSeedArray; +import com.cobblemontournament.util.IndexedSeedSortType; +import com.cobblemontournament.util.SeedUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.function.Predicate; + +public final class TournamentBuilder +{ + public TournamentBuilder(TournamentPropertiesBuilder builder) + { + propertiesBuilder = builder != null ? builder : new TournamentPropertiesBuilder(); + } + + @NotNull public final TournamentPropertiesBuilder propertiesBuilder; + @NotNull private final ArrayList seededPlayers = new ArrayList<>(); + @NotNull private final ArrayList unseededPlayers = new ArrayList<>(); + + // player registration & management + public int getPlayerCount() + { + return seededPlayers.size() + unseededPlayers.size(); + } + public boolean addPlayer(UUID playerID, Integer seed) + { + Predicate predicate = sp -> sp.id() == playerID; + if (containsPlayerWith(seededPlayers,predicate) || containsPlayerWith(unseededPlayers,predicate)) { + return false; + } + return seededPlayers.add(new SeededPlayer(playerID, seed)); + } + public boolean updateSeededPlayer(UUID playerID, Integer seed) + { + int notNullSeed = (seed != null && seed > 0) ? seed : -1; + Predicate removePredicate = sp -> sp.id() == playerID && sp.seed() != notNullSeed; + removePlayerIf(seededPlayers,removePredicate); + removePlayerIf(unseededPlayers,removePredicate); + return addPlayer(playerID, seed); + } + public boolean removePlayer(@NotNull UUID playerID) + { + boolean removed = removePlayerIf(seededPlayers, sp -> sp.id() == playerID); + return removePlayerIf(unseededPlayers, sp -> sp.id() == playerID) || removed; + } + private boolean containsPlayerWith(ArrayList collection,Predicate predicate) + { + return collection.stream().anyMatch(predicate); + } + private boolean removePlayerIf(ArrayList collection,Predicate predicate) + { + return collection.removeIf(predicate); + } + + public int getRoundCount() + { + return switch (propertiesBuilder.getTournamentType()) { + case SingleElimination -> getRoundCountSingleElimination(); + case DoubleElimination -> getRoundCountDoubleElimination(); + case RoundRobin -> getRoundCountRoundRobin(); + case VGC -> getRoundCountVGC(); + }; + } + private int getRoundCountSingleElimination() + { + int playerCount = getPlayerCount(); + int bracketSlots = SeedUtil.ceilToPowerOfTwo(playerCount); + int rounds = 0; + while (bracketSlots > 0) { + bracketSlots = bracketSlots >> 1; + rounds++; + } + return rounds; + } + private int getRoundCountDoubleElimination() + { + double playersRemaining = getPlayerCount(); + int rounds = 0; + while (playersRemaining > 1) { + playersRemaining = Math.ceil(playersRemaining * 0.5f); + } + + // TODO lower bracket + + return rounds; + } + private int getRoundCountRoundRobin() + { + var playersRemaining = getPlayerCount(); + int rounds = 0; + + // TODO + + return rounds; + } + private int getRoundCountVGC() + { + double playersRemaining = getPlayerCount(); + int rounds = 0; + + // TODO + + return rounds; + } + + /** Construct a finalized tournament */ + public Tournament toTournament() + { + var playerCount = getPlayerCount(); + if (playerCount < 2) { + // TODO log not enough players for tournament + return null; + } + + return switch (propertiesBuilder.getTournamentType()){ + case SingleElimination -> handleSingleElimination(UUID.randomUUID(),playerCount); + case DoubleElimination -> handleDoubleElimination(UUID.randomUUID(),playerCount); + case RoundRobin -> handleRoundRobin(UUID.randomUUID(),playerCount); + case VGC -> handleVGC(UUID.randomUUID(),playerCount); + }; + } + + private Tournament handleSingleElimination(UUID tournamentID,int playerCount) + { + var properties = propertiesBuilder.toTournamentProperties(); + var roundCount = getRoundCount(); + + // get total matches in first -> always power of 2 for single & double elimination brackets + var matchCount = 1 << (roundCount - 1); // remove championship b/c... fml + + ArrayList rounds = new ArrayList<>(roundCount); + rounds.add(getFirstEliminationRound(tournamentID)); + + int totalMatches = matchCount; + for(int i = 1; i < roundCount; i++) { + // size of indexed seeds should always be power of 2 -> this cuts it in half 'safely' + matchCount = matchCount >> 1; + rounds.add(getInitializedRound(tournamentID,TournamentRoundType.Primary,i,totalMatches - 1,matchCount)); + totalMatches += matchCount; + } + + return new Tournament(properties,tournamentID,rounds); + } + private Tournament handleDoubleElimination(UUID tournamentID,int playerCount) + { + // TODO log || implement + return null; + } + private Tournament handleRoundRobin(UUID tournamentID,int playerCount) + { + // TODO log || implement + return null; + } + private Tournament handleVGC(UUID tournamentID,int playerCount) + { + // TODO log || implement + return null; + } + + + private TournamentRound getFirstEliminationRound(UUID tournamentID) + { + var roundID = UUID.randomUUID(); + var orderedPlayers = sortAndSyncSeededPlayers(seededPlayers); + var indexedSeeds = SeedUtil.getIndexedSeedArray(getPlayerCount(), IndexedSeedSortType.INDEX_ASCENDING); + ArrayList matches = getSeedOrderedMatches(tournamentID,roundID,orderedPlayers,indexedSeeds); + fillWithUnseededPlayers(matches,indexedSeeds); + return new TournamentRound(tournamentID,roundID,0,TournamentRoundType.Primary,matches); + } + + private ArrayList sortAndSyncSeededPlayers(ArrayList seededPlayers) + { + var orderedPlayers = new ArrayList<>(seededPlayers.stream().toList()); + orderedPlayers.sort(Comparator.comparing(SeededPlayer::seed)); // ascending + + Random random = new Random(); + var sameSeededPlayers = new ArrayList(); + int size = orderedPlayers.size(); + for (int i = 0; i < size; i++) { + SeededPlayer nextPlayer; + if (!sameSeededPlayers.isEmpty()) { + var index = random.ints(0, sameSeededPlayers.size()) + .findFirst() + .orElse(0); + nextPlayer = sameSeededPlayers.remove(index); + } else if (i + 1 != size && Objects.equals(orderedPlayers.get(i).seed(), orderedPlayers.get(i + 1).seed())) { // 'i + 1 != size' to catch out of bounds error on last iteration + // multiple players with same seed -> create collection to pull players from at random + var lastIndex = i; + sameSeededPlayers.add(orderedPlayers.get(lastIndex)); + while (Objects.equals(orderedPlayers.get(lastIndex).seed(), orderedPlayers.get(lastIndex + 1).seed())) + { + sameSeededPlayers.add(orderedPlayers.get(++lastIndex)); + } + var index = random.ints(0, sameSeededPlayers.size()) + .findFirst() + .orElse(0); + nextPlayer = sameSeededPlayers.remove(index); + } else { + // just add the next player in order with new instance containing synced seed + nextPlayer = orderedPlayers.get(i); + } + + orderedPlayers.remove(i); + orderedPlayers.add(i,new SeededPlayer(nextPlayer.id(), i + 1)); + } + return orderedPlayers; + } + + private ArrayList getSeedOrderedMatches( + UUID tournamentID, + UUID roundID, + ArrayList players, + @NotNull IndexedSeedArray indexedSeeds + ) { + if (indexedSeeds.sortStatus() != IndexedSeedSortType.INDEX_ASCENDING) { + indexedSeeds.sortBySeedAscending(); + } + var size = indexedSeeds.size(); + int matchCount = size >> 1; // size of indexed seeds should always be power of 2 -> cuts in half 'safely'... + var matches = new ArrayList(matchCount); + int seedIndex = 0; + for (int i = 0; i < matchCount; i++) { + var seed1 = indexedSeeds.collection.get(seedIndex++).seed(); + var seed2 = indexedSeeds.collection.get(seedIndex++).seed(); + var player1 = players.stream() + .filter(p -> p.seed().equals(seed1)) + .findFirst() + .orElse(null); + var player2 = players.stream() + .filter(p -> p.seed().equals(seed2)) + .findFirst() + .orElse(null); + UUID player1ID = player1 != null ? player1.id() : null; + UUID player2ID = player2 != null ? player2.id() : null; + matches.add(new TournamentMatch(tournamentID,roundID,UUID.randomUUID(),i,i,player1ID,player2ID)); + } + return matches; + } + + private void fillWithUnseededPlayers( + @NotNull ArrayList matches, + @NotNull IndexedSeedArray indexedSeeds + ) { + indexedSeeds.sortBySeedAscending(); + var unseededCount = unseededPlayers.size(); + var unseededIndex = 0; + for (int i = 0; i < indexedSeeds.size();i++) { + var seedIndex = indexedSeeds.get(i).index(); + var matchIndex = seedIndex/2; + var remainder = seedIndex%2; // used to place player correctly into player1 or player2 -> always -> 1 | 0 + var match = matches.get(matchIndex); + + if (remainder == 0) { + // is player 1 slot + if (match.player1() == null){ + // available slot + var player = unseededPlayers.get(unseededIndex); + if (match.trySetPlayer1(player.id())){ + unseededIndex++; + } //else { // TODO log if fail } + } + } else { + // is player 2 slot + if (match.player1() == null){ + // available slot + var player = unseededPlayers.get(unseededIndex); + if (match.trySetPlayer2(player.id())) { + unseededIndex++; + } //else { // TODO log if fail } + } + } + + if (unseededIndex == unseededCount) { + break; + } + } + } + + private TournamentRound getInitializedRound( + UUID tournamentID, + TournamentRoundType type, // ignore warning -> will go away when other types implemented + int roundIndex, + int lastMatchIndex, + int matchCount + ) { + UUID roundID = UUID.randomUUID(); + var matches = new ArrayList(matchCount); + for (var i = 0; i < matchCount; i++) { + matches.add(new TournamentMatch(tournamentID,roundID,UUID.randomUUID(),lastMatchIndex++,i)); + } + return new TournamentRound(tournamentID,roundID,roundIndex,type,matches); + } + +} diff --git a/src/main/java/com/cobblemontournament/api/builder/TournamentPropertiesBuilder.java b/src/main/java/com/cobblemontournament/api/builder/TournamentPropertiesBuilder.java new file mode 100644 index 0000000..2edd095 --- /dev/null +++ b/src/main/java/com/cobblemontournament/api/builder/TournamentPropertiesBuilder.java @@ -0,0 +1,114 @@ +package com.cobblemontournament.api.builder; + +import com.cobblemontournament.api.tournament.TournamentProperties; +import com.cobblemontournament.api.tournament.TournamentType; +import com.turtlehoarder.cobblemonchallenge.battle.ChallengeFormat; +import org.jetbrains.annotations.NotNull; + +public final class TournamentPropertiesBuilder +{ + public TournamentPropertiesBuilder() { + tournamentType = TournamentProperties.DEFAULT_TOURNAMENT_TYPE; + groupSize = TournamentProperties.DEFAULT_GROUP_SIZE; + maxPlayerCount = TournamentProperties.DEFAULT_MAX_PLAYER_COUNT; + challengeFormat = TournamentProperties.DEFAULT_CHALLENGE_FORMAT; + minLevel = TournamentProperties.DEFAULT_CHALLENGE_MIN_LEVEL; + maxLevel = TournamentProperties.DEFAULT_CHALLENGE_MAX_LEVEL; + showPreview = TournamentProperties.DEFAULT_SHOW_PREVIEW; + } + + public TournamentPropertiesBuilder( + TournamentType tournamentType, + Integer groupSize, + Integer maxPlayerCount, + ChallengeFormat challengeFormat, + Integer minLevel, + Integer maxLevel, + Boolean showPreview + ) { + this.tournamentType = tournamentType != null ? tournamentType : TournamentProperties.DEFAULT_TOURNAMENT_TYPE; + this.groupSize = groupSize != null ? groupSize : TournamentProperties.DEFAULT_GROUP_SIZE; + this.maxPlayerCount = maxPlayerCount != null ? maxPlayerCount : TournamentProperties.DEFAULT_MAX_PLAYER_COUNT; + this.challengeFormat = challengeFormat != null ? challengeFormat : TournamentProperties.DEFAULT_CHALLENGE_FORMAT; + this.minLevel = minLevel != null ? minLevel : TournamentProperties.DEFAULT_CHALLENGE_MIN_LEVEL; + this.maxLevel = maxLevel != null ? maxLevel : TournamentProperties.DEFAULT_CHALLENGE_MAX_LEVEL; + this.showPreview = showPreview != null ? showPreview : TournamentProperties.DEFAULT_SHOW_PREVIEW; + } + + @NotNull private TournamentType tournamentType; + private int groupSize; + private int maxPlayerCount; + @NotNull ChallengeFormat challengeFormat; + private int minLevel; + private int maxLevel; + private boolean showPreview; + + @NotNull public TournamentType getTournamentType() { return tournamentType; } + public boolean setTournamentType(TournamentType type) { + if (type != null && tournamentType != type) { + tournamentType = type; + return true; + } + return false; + } + public int getGroupSize() { return groupSize; } + public boolean setGroupSize(int groupSize) { + if (this.groupSize != groupSize) { + this.groupSize = groupSize; + return true; + } + return false; + } + public int getMaxPlayerCount() { return maxPlayerCount; } + public boolean setMaxPlayerCount(int maxPlayerCount) { + if (this.maxPlayerCount != maxPlayerCount) { + this.maxPlayerCount = maxPlayerCount; + return true; + } + return false; + } + @NotNull public ChallengeFormat getChallengeFormat() { return challengeFormat; } + public boolean setChallengeFormat(ChallengeFormat format) { + if (format != null && challengeFormat != format) { + challengeFormat = format; + return true; + } + return false; + } + public int getMinLevel() { return minLevel; } + public boolean setMinLevel(int min) { + if (minLevel != min) { + minLevel = min; + return true; + } + return false; + } + public int getMaxLevel() { return maxLevel; } + public boolean setMaxLevel(int max) { + if (maxLevel != max) { + maxLevel = max; + return true; + } + return false; + } + public boolean getShowPreview() { return showPreview; } + public boolean setShowPreview(boolean showPreview) { + if (this.showPreview != showPreview) { + this.showPreview = showPreview; + return true; + } + return false; + } + + public TournamentProperties toTournamentProperties() { + return new TournamentProperties( + getTournamentType(), + getGroupSize(), + getMaxPlayerCount(), + getChallengeFormat(), + getMinLevel(), + getMaxLevel(), + getShowPreview() + ); + } +} diff --git a/src/main/java/com/cobblemontournament/api/match/MatchStatus.java b/src/main/java/com/cobblemontournament/api/match/MatchStatus.java new file mode 100644 index 0000000..5ae5708 --- /dev/null +++ b/src/main/java/com/cobblemontournament/api/match/MatchStatus.java @@ -0,0 +1,12 @@ +package com.cobblemontournament.api.match; + +public enum MatchStatus +{ + Error, + Empty, + NotReady, + Ready, + InProgress, + Complete, + Finalized, +} diff --git a/src/main/java/com/cobblemontournament/api/match/TournamentMatch.java b/src/main/java/com/cobblemontournament/api/match/TournamentMatch.java new file mode 100644 index 0000000..c80f5d5 --- /dev/null +++ b/src/main/java/com/cobblemontournament/api/match/TournamentMatch.java @@ -0,0 +1,171 @@ +package com.cobblemontournament.api.match; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +public final class TournamentMatch +{ + public TournamentMatch( + @NotNull UUID tournamentID, + @NotNull UUID roundID, + UUID matchID, + int tournamentMatchIndex, + int roundMatchIndex + ) { + this.tournamentID = tournamentID; + this.roundID = roundID; + this.matchID = matchID != null ? matchID : UUID.randomUUID(); + this.tournamentMatchIndex = tournamentMatchIndex; + this.roundMatchIndex = roundMatchIndex; + } + + public TournamentMatch( + @NotNull UUID tournamentID, + @NotNull UUID roundID, + UUID matchUUID, + int tournamentMatchIndex, + int roundMatchIndex, + @Nullable UUID player1id, + @Nullable UUID player2ID + ) { + this.tournamentID = tournamentID; + this.roundID = roundID; + this.matchID = matchUUID != null ? matchUUID : UUID.randomUUID(); + this.tournamentMatchIndex = tournamentMatchIndex; + this.roundMatchIndex = roundMatchIndex; + this.player1ID = player1id; + this.player2ID = player2ID; + updateStatus(); + } + + public final UUID tournamentID; + public final UUID roundID; + public final UUID matchID; + public final int tournamentMatchIndex; + public final int roundMatchIndex; + private MatchStatus status = MatchStatus.Empty; + + @Nullable private UUID player1ID; + @Nullable private UUID player2ID; + @Nullable private UUID victorID; + + @Nullable public UUID player1() + { + return player1ID; + } + @Nullable public UUID player2() + { + return player2ID; + } + @Nullable public UUID victorID() + { + return victorID; + } + + // TODO: create data class to hold & serialize finalized match details + // > possibly just use matchUUID to search a database + + public boolean trySetPlayer1(@NotNull UUID id) + { + if (player1ID != null) { + return false; + } + player1ID = id; + return true; + } + public boolean trySetPlayer2(@NotNull UUID id) + { + if (player2ID != null) { + return false; + } + player2ID = id; + return true; + } + + public boolean updatePlayer(@NotNull UUID id) + { + if (player1ID != null && player2ID != null) { + return false; + } + if (player1ID == id || player2ID == id) { + return false; + } + if (player1ID == null) { + player1ID = id; + return true; + } + player2ID = id; + return true; + } + public boolean updateResult(@NotNull UUID victorID) + { + if (player1ID != null && player2ID != null) { + return false; + } + if (player1ID != victorID && player2ID != victorID) { + return false; + } + this.victorID = victorID; + return true; + } + + public MatchStatus getStatus() + { + return updateStatus(); + } + public MatchStatus updateStatus() + { + return switch (status) { + case Error,Empty -> updateEmptyStatus(); // recycle error status for now to reset & possibly resolve issue + case NotReady -> updateNotReadyStatus(); + case Ready -> updateReadyStatus(); + case InProgress -> updateInProgressStatus(); + case Complete -> updateCompleteStatus(); + case Finalized -> status; + }; + } + private MatchStatus updateEmptyStatus() + { + if (player1ID == null && player2ID == null) { + if (status != MatchStatus.Empty) { + status = MatchStatus.Empty; + } + return status; + } + return updateNotReadyStatus(); + } + private MatchStatus updateNotReadyStatus() + { + if (player1ID == null || player2ID == null) { + if (status != MatchStatus.NotReady) { + status = MatchStatus.NotReady; + } + return status; + } + return updateReadyStatus(); + } + private MatchStatus updateReadyStatus() + { + if (player1ID != null && player2ID != null) { + if (status != MatchStatus.Ready) { + status = MatchStatus.Ready; + } + return status; + } + // TODO: implement check for matches in progress on the server -> progress to inProgress + return MatchStatus.Ready; + } + private MatchStatus updateInProgressStatus() + { + // TODO: implement check for matches in progress on the server -> if not progress to Complete + return MatchStatus.InProgress; + } + private MatchStatus updateCompleteStatus() + { + // TODO: implement check for matches in progress on the server -> + return MatchStatus.Complete; + } + +} diff --git a/src/main/java/com/cobblemontournament/api/player/SeededPlayer.java b/src/main/java/com/cobblemontournament/api/player/SeededPlayer.java new file mode 100644 index 0000000..4243a3f --- /dev/null +++ b/src/main/java/com/cobblemontournament/api/player/SeededPlayer.java @@ -0,0 +1,5 @@ +package com.cobblemontournament.api.player; + +import java.util.UUID; + +public record SeededPlayer (UUID id, Integer seed) { } diff --git a/src/main/java/com/cobblemontournament/api/player/TournamentPlayer.java b/src/main/java/com/cobblemontournament/api/player/TournamentPlayer.java new file mode 100644 index 0000000..89fdc0f --- /dev/null +++ b/src/main/java/com/cobblemontournament/api/player/TournamentPlayer.java @@ -0,0 +1,50 @@ +package com.cobblemontournament.api.player; + +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +@SuppressWarnings("unused") +public final class TournamentPlayer +{ + public TournamentPlayer( + UUID player, + int seed + ) { + this.player = player; + this.seed = seed; + } + public TournamentPlayer( + UUID player, + int seed, + @Nullable UUID pokemonSideUUID + ) { + this.player = player; + this.seed = seed; + this.pokemonSideUUID = pokemonSideUUID; + } + + @Nullable private UUID pokemonSideUUID; + private Integer finalPlacement; + + public final UUID player; + public final int seed; + + @Nullable public UUID getPokemonSideUUID() { return pokemonSideUUID; } + public boolean updatePokemonSideUUID(UUID pokemonSideUUID, boolean override) { + if (this.pokemonSideUUID == pokemonSideUUID || (!override && this.pokemonSideUUID != null)){ + return false; + } + this.pokemonSideUUID = pokemonSideUUID; + return true; + } + public int getFinalPlacement() { return finalPlacement != null ? finalPlacement : -1; } + public boolean updateFinalPlacement(int finalPlacement) { + if (this.finalPlacement == finalPlacement) { + return false; + } + this.finalPlacement = finalPlacement; + return true; + } + +} diff --git a/src/main/java/com/cobblemontournament/api/pokemon/PokemonEntry.java b/src/main/java/com/cobblemontournament/api/pokemon/PokemonEntry.java new file mode 100644 index 0000000..891f1f8 --- /dev/null +++ b/src/main/java/com/cobblemontournament/api/pokemon/PokemonEntry.java @@ -0,0 +1,13 @@ +package com.cobblemontournament.api.pokemon; + +import com.cobblemon.mod.common.pokemon.Pokemon; + +import java.util.UUID; +import java.util.Vector; + +public record PokemonEntry( + UUID id, + Vector pokemon +) { + +} diff --git a/src/main/java/com/cobblemontournament/api/pokemon/PokemonTeam.java b/src/main/java/com/cobblemontournament/api/pokemon/PokemonTeam.java new file mode 100644 index 0000000..3ebbd9c --- /dev/null +++ b/src/main/java/com/cobblemontournament/api/pokemon/PokemonTeam.java @@ -0,0 +1,6 @@ +package com.cobblemontournament.api.pokemon; + +import java.util.UUID; +import java.util.Vector; + +public record PokemonTeam (UUID id,Vector pokemon) { } diff --git a/src/main/java/com/cobblemontournament/api/round/TournamentRound.java b/src/main/java/com/cobblemontournament/api/round/TournamentRound.java new file mode 100644 index 0000000..ef2bcc5 --- /dev/null +++ b/src/main/java/com/cobblemontournament/api/round/TournamentRound.java @@ -0,0 +1,35 @@ +package com.cobblemontournament.api.round; + +import com.cobblemontournament.api.match.TournamentMatch; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; +import java.util.ArrayList; + +public final class TournamentRound +{ + public TournamentRound( + @NotNull UUID tournamentID, + UUID roundUUID, + int roundIndex, + @NotNull TournamentRoundType roundType, + ArrayList matches + ) { + this.tournamentID = tournamentID; + this.roundUUID = roundUUID != null ? roundUUID : UUID.randomUUID(); + this.roundIndex = roundIndex; + this.roundType = roundType; + this.matches = matches; + } + + public final UUID tournamentID; + public final UUID roundUUID; + public final int roundIndex; + public final TournamentRoundType roundType; + public final ArrayList matches; + + public void updateMatch(TournamentMatch match) + { + matches.add(match); + } +} diff --git a/src/main/java/com/cobblemontournament/api/round/TournamentRoundType.java b/src/main/java/com/cobblemontournament/api/round/TournamentRoundType.java new file mode 100644 index 0000000..0cbeedc --- /dev/null +++ b/src/main/java/com/cobblemontournament/api/round/TournamentRoundType.java @@ -0,0 +1,10 @@ +package com.cobblemontournament.api.round; + +public enum TournamentRoundType +{ + Preliminary, + Primary, + Secondary, + RoundRobin, + TieBreaker, +} diff --git a/src/main/java/com/cobblemontournament/api/tournament/Tournament.java b/src/main/java/com/cobblemontournament/api/tournament/Tournament.java new file mode 100644 index 0000000..e82f5fd --- /dev/null +++ b/src/main/java/com/cobblemontournament/api/tournament/Tournament.java @@ -0,0 +1,50 @@ +package com.cobblemontournament.api.tournament; + +import com.cobblemontournament.api.round.TournamentRound; +import com.cobblemontournament.api.pokemon.PokemonEntry; +import com.cobblemontournament.api.pokemon.PokemonTeam; +import com.cobblemontournament.api.match.TournamentMatch; +import com.cobblemontournament.api.player.TournamentPlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.UUID; + +public final class Tournament +{ + public Tournament( + @NotNull TournamentProperties properties, + @NotNull UUID tournamentID, + @NotNull ArrayList rounds + ) { + this.tournamentID = tournamentID; + this.tournamentProperties = properties; + // TODO register rounds to a HashMap + this.rounds = rounds; + // TODO register matches, players, pokemon teams, pokemon to hashmaps + // -> pokemon teams & pokemon temporary until a server wide database is implemented + // -> !! still confirm pokemon & teams are in database when implemented !! + } + + @NotNull public final UUID tournamentID; + @NotNull public final TournamentProperties tournamentProperties; + @NotNull public final ArrayList rounds; + private final HashMap matches = new HashMap<>(); + private final HashMap players = new HashMap<>(); + private final HashMap pokemonTeams = new HashMap<>(); // TODO: temporary until rental team functionality implemented + private final HashMap pokemonEntries = new HashMap<>(); // TODO: temporary until rental team functionality implemented + + @SuppressWarnings("unused") + public void safeCloneOf(boolean reset){ + // TODO: make a deep copy of tournament + // if reset -> clone with clean start + } + + @SuppressWarnings("unused") + public void safeBuilderCloneOf(boolean reset){ + // TODO: make a deep copy of tournament back into tournament builder + // if reset -> clone with clean start + } +} diff --git a/src/main/java/com/cobblemontournament/api/tournament/TournamentProperties.java b/src/main/java/com/cobblemontournament/api/tournament/TournamentProperties.java new file mode 100644 index 0000000..8a8525e --- /dev/null +++ b/src/main/java/com/cobblemontournament/api/tournament/TournamentProperties.java @@ -0,0 +1,60 @@ +package com.cobblemontournament.api.tournament; + +import com.cobblemontournament.config.TournamentConfig; +import com.turtlehoarder.cobblemonchallenge.battle.ChallengeFormat; +import org.jetbrains.annotations.NotNull; + +@NotNull +public final class TournamentProperties +{ + public static final TournamentType DEFAULT_TOURNAMENT_TYPE = TournamentConfig.DEFAULT_TOURNAMENT_TYPE; + public static final int DEFAULT_GROUP_SIZE = TournamentConfig.DEFAULT_GROUP_SIZE; + public static final int DEFAULT_MAX_PLAYER_COUNT = TournamentConfig.DEFAULT_MAX_PLAYER_COUNT; + public static final ChallengeFormat DEFAULT_CHALLENGE_FORMAT = TournamentConfig.DEFAULT_CHALLENGE_FORMAT; + public static final int DEFAULT_CHALLENGE_MIN_LEVEL = TournamentConfig.DEFAULT_CHALLENGE_MIN_LEVEL; + public static final int DEFAULT_CHALLENGE_MAX_LEVEL = TournamentConfig.DEFAULT_CHALLENGE_MAX_LEVEL; + public static final boolean DEFAULT_SHOW_PREVIEW = TournamentConfig.DEFAULT_SHOW_PREVIEW; + + public TournamentProperties() { + tournamentType = DEFAULT_TOURNAMENT_TYPE; + groupSize = DEFAULT_GROUP_SIZE; + maxPlayerCount = DEFAULT_MAX_PLAYER_COUNT; + challengeFormat = DEFAULT_CHALLENGE_FORMAT; + minLevel = DEFAULT_CHALLENGE_MIN_LEVEL; + maxLevel = DEFAULT_CHALLENGE_MAX_LEVEL; + showPreview = DEFAULT_SHOW_PREVIEW; + } + public TournamentProperties ( + TournamentType type, + Integer groupSize, + Integer maxPlayerCount, + ChallengeFormat format, + Integer minLevel, + Integer maxLevel, + Boolean showPreview + ) { + this.tournamentType = (type != null) ? type : DEFAULT_TOURNAMENT_TYPE; + this.groupSize = groupSize != null ? groupSize : DEFAULT_GROUP_SIZE; + this.maxPlayerCount = maxPlayerCount != null ? maxPlayerCount : DEFAULT_MAX_PLAYER_COUNT; + this.challengeFormat = format != null ? format : DEFAULT_CHALLENGE_FORMAT; + this.minLevel = minLevel != null ? minLevel : DEFAULT_CHALLENGE_MIN_LEVEL; + this.maxLevel = maxLevel != null ? maxLevel : DEFAULT_CHALLENGE_MAX_LEVEL; + this.showPreview = showPreview != null ? showPreview : DEFAULT_SHOW_PREVIEW; + } + + private final TournamentType tournamentType; + private final Integer groupSize; + private final Integer maxPlayerCount; + private final ChallengeFormat challengeFormat; + private final int minLevel; + private final int maxLevel; + private final boolean showPreview; + + @NotNull public TournamentType getTournamentType(){ return tournamentType; } + public int getGroupSize(){ return groupSize; } + public int getMaxPlayerCount(){ return maxPlayerCount; } + @NotNull public ChallengeFormat getChallengeFormat(){ return challengeFormat; } + public int getMinLevel(){ return minLevel; } + public int getMaxLevel(){ return maxLevel; } + public boolean getShowPreview(){ return showPreview; } +} diff --git a/src/main/java/com/cobblemontournament/api/tournament/TournamentType.java b/src/main/java/com/cobblemontournament/api/tournament/TournamentType.java new file mode 100644 index 0000000..ff9d862 --- /dev/null +++ b/src/main/java/com/cobblemontournament/api/tournament/TournamentType.java @@ -0,0 +1,10 @@ +package com.cobblemontournament.api.tournament; + +@SuppressWarnings("unused") +public enum TournamentType +{ + RoundRobin, + SingleElimination, + DoubleElimination, + VGC, +} diff --git a/src/main/java/com/cobblemontournament/command/TournamentCommand.java b/src/main/java/com/cobblemontournament/command/TournamentCommand.java new file mode 100644 index 0000000..76da890 --- /dev/null +++ b/src/main/java/com/cobblemontournament/command/TournamentCommand.java @@ -0,0 +1,6 @@ +package com.cobblemontournament.command; + +public class TournamentCommand +{ + +} diff --git a/src/main/java/com/cobblemontournament/config/TournamentConfig.java b/src/main/java/com/cobblemontournament/config/TournamentConfig.java new file mode 100644 index 0000000..b2a5cfb --- /dev/null +++ b/src/main/java/com/cobblemontournament/config/TournamentConfig.java @@ -0,0 +1,51 @@ +package com.cobblemontournament.config; + +import com.cobblemontournament.CobblemonTournament; +import com.cobblemontournament.api.tournament.TournamentType; +import com.mojang.datafixers.util.Pair; +import com.turtlehoarder.cobblemonchallenge.battle.ChallengeFormat; +import com.turtlehoarder.cobblemonchallenge.config.ConfigProvider; +import com.turtlehoarder.cobblemonchallenge.config.SimpleConfig; + +@SuppressWarnings("unused") +public final class TournamentConfig +{ + public static SimpleConfig CONFIG; + private static ConfigProvider configs; + + public static TournamentType DEFAULT_TOURNAMENT_TYPE; + public static int DEFAULT_GROUP_SIZE; + public static int DEFAULT_MAX_PLAYER_COUNT; + // challenge properties + public static ChallengeFormat DEFAULT_CHALLENGE_FORMAT; + public static int DEFAULT_CHALLENGE_MIN_LEVEL; + public static int DEFAULT_CHALLENGE_MAX_LEVEL; + public static boolean DEFAULT_SHOW_PREVIEW; + + public static void registerConfigs() { + CobblemonTournament.LOGGER.info("Loading Tournament Configs"); + configs = new ConfigProvider(); + createConfigs(); + CONFIG = SimpleConfig.of(CobblemonTournament.MODID + "-config").provider(configs).request(); + assignConfigs(); + } + private static void createConfigs() { + configs.addKeyValuePair(new Pair<>("defaultTournamentType", TournamentType.SingleElimination)); + configs.addKeyValuePair(new Pair<>("defaultGroupSize", 4)); + configs.addKeyValuePair(new Pair<>("defaultMaxPlayerCount", 32)); + configs.addKeyValuePair(new Pair<>("defaultChallengeFormat", ChallengeFormat.STANDARD_6V6)); + configs.addKeyValuePair(new Pair<>("defaultMinLevel", 50)); + configs.addKeyValuePair(new Pair<>("defaultMaxLevel", 50)); + configs.addKeyValuePair(new Pair<>("defaultShowPreview", true)); + } + + private static void assignConfigs() { + DEFAULT_TOURNAMENT_TYPE = CONFIG.getOrDefault("defaultTournamentType", TournamentType.SingleElimination); + DEFAULT_GROUP_SIZE = CONFIG.getOrDefault("defaultGroupSize", 4); + DEFAULT_MAX_PLAYER_COUNT = CONFIG.getOrDefault("defaultMaxPlayerCount", 32); + DEFAULT_CHALLENGE_FORMAT = CONFIG.getOrDefault("defaultChallengeFormat", ChallengeFormat.STANDARD_6V6); + DEFAULT_CHALLENGE_MIN_LEVEL = CONFIG.getOrDefault("defaultChallengeMinLevel", 50); + DEFAULT_CHALLENGE_MAX_LEVEL = CONFIG.getOrDefault("defaultChallengeMaxLevel", 50); + DEFAULT_SHOW_PREVIEW = CONFIG.getOrDefault("", true); + } +} diff --git a/src/main/java/com/cobblemontournament/testing/TournamentBuilderTest.java b/src/main/java/com/cobblemontournament/testing/TournamentBuilderTest.java new file mode 100644 index 0000000..f221887 --- /dev/null +++ b/src/main/java/com/cobblemontournament/testing/TournamentBuilderTest.java @@ -0,0 +1,93 @@ +package com.cobblemontournament.testing; + +import com.cobblemontournament.api.builder.TournamentBuilder; +import com.cobblemontournament.api.builder.TournamentPropertiesBuilder; +import com.cobblemontournament.api.match.TournamentMatch; +import com.cobblemontournament.api.round.TournamentRound; +import com.cobblemontournament.api.tournament.Tournament; +import com.cobblemontournament.api.tournament.TournamentType; +import com.turtlehoarder.cobblemonchallenge.battle.ChallengeFormat; +import org.slf4j.helpers.Util; // Util.report(""); + +import java.util.UUID; + +public class TournamentBuilderTest +{ + public static void buildTournamentDebug(int maxPlayers,boolean doPrint) + { + var propertiesBuilder = new TournamentPropertiesBuilder( + TournamentType.SingleElimination, + null, + maxPlayers, + ChallengeFormat.STANDARD_6V6, + null, + null, + null + ); + var tournamentBuilder = new TournamentBuilder(propertiesBuilder); + + for (int i = 0; i < maxPlayers; i++) { + tournamentBuilder.addPlayer(UUID.randomUUID(),i); + } + + var tournament = tournamentBuilder.toTournament(); + + if (doPrint && tournament != null){ + printTournamentDebug(tournament); + } + } + + public static void printTournamentDebug(Tournament tournament) + { + // print Properties + Util.report("Tournament Debug - ID: " + tournament.tournamentID); + Util.report(" Properties:"); + Util.report(" - Tournament Type: " + tournament.tournamentProperties.getTournamentType()); + Util.report(" - Group Size: " + tournament.tournamentProperties.getGroupSize()); + Util.report(" - Max Players: " + tournament.tournamentProperties.getMaxPlayerCount()); + Util.report(" - Challenge Format: " + tournament.tournamentProperties.getChallengeFormat()); + Util.report(" - Min Level: " + tournament.tournamentProperties.getMinLevel()); + Util.report(" - Max Level: " + tournament.tournamentProperties.getMaxLevel()); + Util.report(" - Show Preview: " + tournament.tournamentProperties.getShowPreview()); + + int roundCount = tournament.rounds.size(); + int matchCount = 0; + for (int i = 0; i < roundCount; i++) { + matchCount += tournament.rounds.get(i).matches.size(); + } + + Util.report(" Details:"); + Util.report(" - Rounds: " + roundCount); + Util.report(" - Matches: " + matchCount); + + // print round details + for (int i = 0; i < roundCount;i++) { + printRoundDetails(tournament.rounds.get(i),true); + } + } + + public static void printRoundDetails(TournamentRound round,boolean includeMatches) + { + Util.report("Round Details - ID:" + round.roundUUID); + Util.report("- Round Type: " + round.roundType); + Util.report("- Round Index: " + round.roundIndex); + Util.report("- Matches: " + round.matches.size()); + if (!includeMatches) { + return; + } + var size = round.matches.size(); + for (int i = 0; i < size;i++) { + printMatchDetails(round.matches.get(i)); + } + } + public static void printMatchDetails(TournamentMatch match) + { + Util.report("Match Details - ID:" + match.matchID); + Util.report("- Tournament Match Index: " + match.tournamentMatchIndex); + Util.report("- Round Match Index: " + match.roundMatchIndex); + Util.report("- Player 1: " + match.player1()); + Util.report("- Player 2: " + match.player2()); + Util.report("- Victor: " + match.victorID()); + Util.report("- Status: " + match.getStatus()); + } +} diff --git a/src/main/java/com/cobblemontournament/testing/command/TournamentCommandTest.java b/src/main/java/com/cobblemontournament/testing/command/TournamentCommandTest.java new file mode 100644 index 0000000..c12f95b --- /dev/null +++ b/src/main/java/com/cobblemontournament/testing/command/TournamentCommandTest.java @@ -0,0 +1,49 @@ +package com.cobblemontournament.testing.command; + +import com.cobblemontournament.testing.TournamentBuilderTest; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; + +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.commands.arguments.selector.EntitySelector; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; +import net.minecraft.server.level.ServerPlayer; + +import java.util.UUID; + +public class TournamentCommandTest +{ + public static void register(CommandDispatcher dispatcher) + { + LiteralArgumentBuilder buildTournamentDebugCommand = Commands.literal("tournament") + .then(Commands.literal("testBuild") + .then(Commands.argument("maxPlayers", IntegerArgumentType.integer(0,64)) + .then(Commands.literal("print") + .then(Commands.argument("doPrint", BoolArgumentType.bool()) + .executes(c -> buildTournamentDebug(IntegerArgumentType.getInteger(c,"maxPlayers"),BoolArgumentType.getBool(c,"doPrint")) + ) + ) + ) + ) + ); + + dispatcher.register(buildTournamentDebugCommand); + } + + public static int buildTournamentDebug(int players,boolean doPrint) + { + TournamentBuilderTest.buildTournamentDebug(players,doPrint); + return Command.SINGLE_SUCCESS; + } +} diff --git a/src/main/java/com/cobblemontournament/util/IndexedSeed.java b/src/main/java/com/cobblemontournament/util/IndexedSeed.java new file mode 100644 index 0000000..1771d9e --- /dev/null +++ b/src/main/java/com/cobblemontournament/util/IndexedSeed.java @@ -0,0 +1,3 @@ +package com.cobblemontournament.util; + +public record IndexedSeed(Integer index, Integer seed) { } diff --git a/src/main/java/com/cobblemontournament/util/IndexedSeedArray.java b/src/main/java/com/cobblemontournament/util/IndexedSeedArray.java new file mode 100644 index 0000000..ff6fa3c --- /dev/null +++ b/src/main/java/com/cobblemontournament/util/IndexedSeedArray.java @@ -0,0 +1,62 @@ +package com.cobblemontournament.util; + +import org.jetbrains.annotations.Nullable; +import java.util.Comparator; +import java.util.Vector; + +public class IndexedSeedArray +{ + public IndexedSeedArray( + Vector collection, + @Nullable IndexedSeedSortType sortingType + ) { + this.collection = collection; + if (sortingType == null) { + return; + } + switch (sortingType) { + case SEED_ASCENDING -> sortBySeedAscending(); + case SEED_DESCENDING -> sortBySeedDescending(); + case INDEX_ASCENDING -> sortByIndexAscending(); + case INDEX_DESCENDING -> sortByIndexDescending(); + } + } + + public Vector collection; + private IndexedSeedSortType indexedSeedStatus = IndexedSeedSortType.UNKNOWN; + public IndexedSeedSortType sortStatus() + { + return indexedSeedStatus; + } + + public int size() + { + return collection.size(); + } + public IndexedSeed get(int index) + { + return collection.get(index); + } + + public void sortBySeedAscending() + { + collection.sort(Comparator.comparing(IndexedSeed::seed)); + indexedSeedStatus = IndexedSeedSortType.SEED_ASCENDING; + } + public void sortBySeedDescending() + { + collection.sort(Comparator.comparing(IndexedSeed::seed).reversed()); + indexedSeedStatus = IndexedSeedSortType.SEED_DESCENDING; + } + public void sortByIndexAscending() + { + collection.sort(Comparator.comparing(IndexedSeed::index)); + indexedSeedStatus = IndexedSeedSortType.INDEX_ASCENDING; + } + public void sortByIndexDescending() + { + collection.sort(Comparator.comparing(IndexedSeed::index).reversed()); + indexedSeedStatus = IndexedSeedSortType.INDEX_DESCENDING; + } + +} diff --git a/src/main/java/com/cobblemontournament/util/IndexedSeedSortType.java b/src/main/java/com/cobblemontournament/util/IndexedSeedSortType.java new file mode 100644 index 0000000..7f4e441 --- /dev/null +++ b/src/main/java/com/cobblemontournament/util/IndexedSeedSortType.java @@ -0,0 +1,10 @@ +package com.cobblemontournament.util; + +public enum IndexedSeedSortType +{ + UNKNOWN, + SEED_ASCENDING, + SEED_DESCENDING, + INDEX_ASCENDING, + INDEX_DESCENDING, +} diff --git a/src/main/java/com/cobblemontournament/util/SeedUtil.java b/src/main/java/com/cobblemontournament/util/SeedUtil.java new file mode 100644 index 0000000..6be1c77 --- /dev/null +++ b/src/main/java/com/cobblemontournament/util/SeedUtil.java @@ -0,0 +1,239 @@ +package com.cobblemontournament.util; + +import java.util.ArrayList; +import java.util.Vector; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class SeedUtil +{ + /** + * IndexedSeeds( int index, int seed ) + * index: order index of seed in tournament structure + * seed: value of seed + */ + public static IndexedSeedArray getIndexedSeedArray(int seedCount, IndexedSeedSortType sortStatus) + { + var seeds = getSeedArray(seedCount); + var size = seeds.size(); + var placeHolder = new IndexedSeed(-1,-1); + var indexedSeeds = new Vector<>(Stream.generate(() -> placeHolder) + .limit(size) + .collect(Collectors.toList()) + ); + for (int i = 0; i < size; i++) { + indexedSeeds.set(i, new IndexedSeed(i,seeds.get(i))); + } + return new IndexedSeedArray(indexedSeeds,sortStatus); + } + + /** @return Vector with sorted seeds for tournament bracket
+ * -> the array size will be the nearest ceil to power of 2 int for (param)seedCount
+ * ex 1: (param)seedCount = 5 -> nearest power of 2 -> 8
+ * ex 2: (param)seedCount = 33 -> nearest power of 2 -> 64 (ceil not round b/c return val needs to include ALL seeds) + */ + public static Vector getSeedArray(int seedCount) + { + var testVector = new Vector() { { add(0); add(0); } }; + var testValue = testVector.get(0); + + int finalSize = ceilToPowerOfTwo(seedCount); + //System.out.println("finalSize " + finalSize); + ArrayList seeds = new ArrayList<>(2) { { add(1); add(2); } }; + //System.out.println("seeds " + seeds); + + // minSeed = the lowest seed (highest int values) 'processed' into the ArrayList<> seeds + int previousSeed = 2; + // empty ~ Pair(x,-1) -> x is always > 0 & represents a 'processed' seed + int emptySeeds = 0; + while (previousSeed < finalSize) { + // step 1 -> add filler entry (-1) to collection if all current seed values in seeds are processed + if (emptySeeds < 1) { + emptySeeds = seeds.size(); + int newSize = seeds.size() * 2; + // .collect & new ArrayList<> necessary b/c ArrayList is mutable & List is not + var newSeeds = new ArrayList<>(Stream.generate(() -> -1) + .limit(newSize) + .collect(Collectors.toList()) + ); + for (int i = 0; i * 2 < newSize; i++) { + newSeeds.add(i * 2,seeds.get(i)); // insert last processed value + newSeeds.remove(newSize); // trim offset filler entry from tail of collection + } + seeds = newSeeds; + //System.out.println("seeds " + seeds); + } + + // step 2 -> find next + // Pair + // value 1 -> the 'processed' seed value + // value 2 -> value 1's pair index (value 1 index + 1) + var seedIndex1 = new IndexedSeed(-1,-1); // empty ~ Pair(x,-1) + var seedIndex2 = new IndexedSeed(-1,-1); // empty ~ Pair(x,-1) + // +2 b/c always checking index that has a guaranteed processed seed value + // > basically -> value != -1 + for (int i = 0; i < seeds.size(); i = i + 2) { + if (seeds.get(i + 1) != -1) { + continue; + } + int seed = seeds.get(i); + if (seedIndex1.seed() >= seed && seedIndex2.seed() >= seed){ + continue; + } + // replace lower value seed (aka higher seed) + if (seedIndex1.seed() < seedIndex2.seed()){ + seedIndex1 = new IndexedSeed(i + 1,seed); // + 1, b/c looking for empty (-1) seed pairing + continue; + } + seedIndex2 = new IndexedSeed(i + 1,seed); + } + + // step 3 -> apply next seeds to array + var seed1 = ++previousSeed; + var seed2 = ++previousSeed; + emptySeeds = emptySeeds - 2; + + // !! 'Higher' -> 1 is 'Higher' seed than 2, but lower value + var isSeed1Higher = seedIndex1.seed() < seedIndex2.seed(); + int index1 = isSeed1Higher ? seedIndex2.index() : seedIndex1.index(); + int index2 = isSeed1Higher ? seedIndex1.index() : seedIndex2.index(); + seeds.add(index1,seed1); + seeds.remove(index1 + 1); + seeds.add(index2,seed2); + seeds.remove(index2 + 1); + } + + return doFinalSeedSort(seeds); + } + + private static Vector doFinalSeedSort(ArrayList seeds) + { + // all seed collections will be power of 2 -> safe to bit shift for half + int halfSize = seeds.size() >> 1; + var filler = new ArrayList(0); + // size = half size of seeds, b/c we want all arrays to have 2 seeds each + var seedArrays = new ArrayList<>(Stream.generate(() -> filler) + .limit(halfSize) + .collect(Collectors.toList()) + ); + seedArrays.add(0,seeds); + seedArrays.remove(1); + + // int used to make bit shifting safe + // -> should be 0 on even iterations & 1 on odd iterations + int iterationOffset = 0; + // iterations = (seeds.size/4) + var iterations = seeds.size() >> 2; + for (int i = 0; i < iterations; i++) { + + int size = firstFillerIndex(seedArrays); + for (int ii = 0; ii < size; ii++) { + + var index = ii * 2; + var array = seedArrays.get(index); + var arrays = split(array); + var front = arrays.get(0); + var back = arrays.get(1); + + //var frontMaxSeed = front.stream().min(Integer::compareTo).orElseThrow(); + //var backMaxSeed = back.stream().min(Integer::compareTo).orElseThrow(); + var frontMaxSeed = front.stream() + .min(Integer::compareTo) + .orElse(seeds.size()); + var backMaxSeed = back.stream() + .min(Integer::compareTo) + .orElse(seeds.size()); + + if (frontMaxSeed < backMaxSeed) { + // front has high seed -> reverse back order + reverse(back); + } else { + // back has high seed -> reverse front order + reverse(front); + } + + seedArrays.remove(index); // original array + seedArrays.add(index,front); + seedArrays.remove(seedArrays.size() - 1); // trim off tail + seedArrays.add(index + 1,back); + } + } + + + var finalSeeds = new Vector(Stream.generate(() -> -1) + .limit(seeds.size()) + .collect(Collectors.toList()) + ); + var index = 0; + var lastIndex = finalSeeds.size() - 1; + for (int i = 0; i < seedArrays.size(); i++) { + var nestedSeeds = seedArrays.get(i); + for (int ii = 0; ii < seedArrays.size(); ii++) { + if (nestedSeeds.size() <= ii) { + break; + } + finalSeeds.remove(lastIndex); + finalSeeds.add(index++,nestedSeeds.get(ii)); + } + } + return finalSeeds; + } + + public static Integer ceilToPowerOfTwo(int value) + { + int maxBitInt = Integer.highestOneBit(value); + return (!(value == 0) && (value ^ maxBitInt) == 0) ? value : maxBitInt << 1; + } + + private static int firstFillerIndex(ArrayList> arrayLists) + { + var firstEmptyIndex = -1; + for (int i = 0; i < arrayLists.size(); i++) { + if (arrayLists.get(i).isEmpty()){ + firstEmptyIndex = i; + break; + } + } + return firstEmptyIndex; + } + + // !! size must be pow of 2 b/c bit shift to middle used !! + private static ArrayList> split(ArrayList array) + { + int middle = array.size() >> 1; + var front = getNewFilled(middle); + var back = getNewFilled(middle); + for (int i = 0; i < middle; i++) { + front.add(i,array.get(i)); + front.remove(middle); + } + int index = middle; + for (int i = 0; i < middle; i++) { + back.add(i,array.get(index++)); + back.remove(middle); + } + return new ArrayList<>() { { add(front); add(back); } }; + } + + private static ArrayList getNewFilled(int length) + { + return new ArrayList<>(Stream.generate(() -> -1) + .limit(length) + .collect(Collectors.toList()) + ); + } + + private static void reverse(ArrayList arrayList) + { + int size = arrayList.size(); + for (int i = 0; i < size; i++) { + arrayList.add(i,arrayList.remove(size - 1)); + } + } + + public static boolean isPowerOfTwo(int value) + { + return !(value == 0) && (value ^ Integer.highestOneBit(value)) == 0; + } + +} diff --git a/src/main/java/com/turtlehoarder/cobblemonchallenge/CobblemonChallenge.java b/src/main/java/com/turtlehoarder/cobblemonchallenge/CobblemonChallenge.java index e5f3708..8667847 100644 --- a/src/main/java/com/turtlehoarder/cobblemonchallenge/CobblemonChallenge.java +++ b/src/main/java/com/turtlehoarder/cobblemonchallenge/CobblemonChallenge.java @@ -2,6 +2,7 @@ import com.turtlehoarder.cobblemonchallenge.command.ChallengeCommand; import com.turtlehoarder.cobblemonchallenge.config.ChallengeConfig; +import com.turtlehoarder.cobblemonchallenge.config.UniversalDifficultyConfig; import com.turtlehoarder.cobblemonchallenge.event.ChallengeEventHandler; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; @@ -17,6 +18,7 @@ public class CobblemonChallenge implements ModInitializer { @Override public void onInitialize() { ChallengeConfig.registerConfigs(); + UniversalDifficultyConfig.registerConfigs(); ChallengeEventHandler.registerEvents(); CommandRegistrationCallback.EVENT.register((commandDispatcher, commandBuildContext, commandSelection) -> ChallengeCommand.register(commandDispatcher)); } diff --git a/src/main/java/com/turtlehoarder/cobblemonchallenge/api/ChallengeProperties.java b/src/main/java/com/turtlehoarder/cobblemonchallenge/api/ChallengeProperties.java new file mode 100644 index 0000000..291e072 --- /dev/null +++ b/src/main/java/com/turtlehoarder/cobblemonchallenge/api/ChallengeProperties.java @@ -0,0 +1,47 @@ +package com.turtlehoarder.cobblemonchallenge.api; + +import com.turtlehoarder.cobblemonchallenge.battle.ChallengeFormat; +import com.turtlehoarder.cobblemonchallenge.config.ChallengeConfig; +import org.jetbrains.annotations.Nullable; + +public final class ChallengeProperties +{ + private static final ChallengeFormat DEFAULT_CHALLENGE_FORMAT = ChallengeConfig.DEFAULT_CHALLENGE_FORMAT; + private static final int DEFAULT_CHALLENGE_LEVEL = ChallengeConfig.DEFAULT_CHALLENGE_LEVEL; + private static final int DEFAULT_HANDICAP = ChallengeConfig.DEFAULT_HANDICAP; + private static final Boolean DEFAULT_SHOW_PREVIEW = ChallengeConfig.DEFAULT_SHOW_PREVIEW; + + public ChallengeProperties(@Nullable ChallengeFormat format, Integer minLvl, Integer maxLvl, Integer handicapPlayer1, Integer handicapPlayer2, Boolean preview) { + _challengeFormat = format != null ? format : DEFAULT_CHALLENGE_FORMAT; + _maxLevel = clampIntNotNull(maxLvl, DEFAULT_CHALLENGE_LEVEL,0,100); + _minLevel = clampIntNotNull(maxLvl,Math.min(DEFAULT_CHALLENGE_LEVEL,_maxLevel),0,Math.min(100,_maxLevel)); + _handicapP1 = handicapPlayer1 != null ? handicapPlayer1 : DEFAULT_HANDICAP; + _handicapP2 = handicapPlayer2 != null ? handicapPlayer2 : DEFAULT_HANDICAP; + _showPreview = preview != null ? preview : DEFAULT_SHOW_PREVIEW; + } + + private final ChallengeFormat _challengeFormat; + private final int _minLevel; + private final int _maxLevel; + private final int _handicapP1; + private final int _handicapP2; + private final boolean _showPreview; + + public ChallengeFormat getChallengeFormat() { return _challengeFormat; } + public int getMinLevel() { return _minLevel; } + public int getMaxLevel() { return _maxLevel; } + public int getHandicapP1() { return _handicapP1; } + public int getHandicapP2() { return _handicapP2; } + public boolean getShowPreview() { return _showPreview; } + + private int clampIntNotNull(Integer value,int defaultValue,int min, int max) { + if (value == null) { + return defaultValue; + } else if (value > max){ + return max; + } else if(value < min){ + return min; + } + return value; + } +} diff --git a/src/main/java/com/turtlehoarder/cobblemonchallenge/api/ChallengeRequest.java b/src/main/java/com/turtlehoarder/cobblemonchallenge/api/ChallengeRequest.java new file mode 100644 index 0000000..b348e82 --- /dev/null +++ b/src/main/java/com/turtlehoarder/cobblemonchallenge/api/ChallengeRequest.java @@ -0,0 +1,13 @@ +package com.turtlehoarder.cobblemonchallenge.api; + +import net.minecraft.server.level.ServerPlayer; + +public record ChallengeRequest ( + String id, + ServerPlayer challengerPlayer, + ServerPlayer challengedPlayer, + ChallengeProperties properties, + long createdTime +) { + +} diff --git a/src/main/java/com/turtlehoarder/cobblemonchallenge/api/LeadPokemonSelection.java b/src/main/java/com/turtlehoarder/cobblemonchallenge/api/LeadPokemonSelection.java new file mode 100644 index 0000000..dc715a4 --- /dev/null +++ b/src/main/java/com/turtlehoarder/cobblemonchallenge/api/LeadPokemonSelection.java @@ -0,0 +1,10 @@ +package com.turtlehoarder.cobblemonchallenge.api; + +import com.turtlehoarder.cobblemonchallenge.gui.LeadPokemonSelectionSession; + +public record LeadPokemonSelection( + LeadPokemonSelectionSession selectionWrapper, + long createdTime +) { + +} diff --git a/src/main/java/com/turtlehoarder/cobblemonchallenge/battle/ChallengeBattleBuilder.java b/src/main/java/com/turtlehoarder/cobblemonchallenge/battle/ChallengeBattleBuilder.java index e01421d..ccd548f 100644 --- a/src/main/java/com/turtlehoarder/cobblemonchallenge/battle/ChallengeBattleBuilder.java +++ b/src/main/java/com/turtlehoarder/cobblemonchallenge/battle/ChallengeBattleBuilder.java @@ -1,5 +1,9 @@ package com.turtlehoarder.cobblemonchallenge.battle; +import com.turtlehoarder.cobblemonchallenge.CobblemonChallenge; +import com.turtlehoarder.cobblemonchallenge.api.ChallengeProperties; +import com.turtlehoarder.cobblemonchallenge.battle.pokemon.ChallengeBattlePokemon; + import com.cobblemon.mod.common.Cobblemon; import com.cobblemon.mod.common.api.battles.model.PokemonBattle; import com.cobblemon.mod.common.api.storage.party.PartyStore; @@ -10,28 +14,27 @@ import com.cobblemon.mod.common.battles.pokemon.BattlePokemon; import com.cobblemon.mod.common.entity.pokemon.PokemonEntity; import com.cobblemon.mod.common.pokemon.Pokemon; -import com.turtlehoarder.cobblemonchallenge.CobblemonChallenge; -import com.turtlehoarder.cobblemonchallenge.util.ChallengeUtil; -import kotlin.Unit; + import net.minecraft.server.level.ServerPlayer; import java.util.ArrayList; import java.util.List; import java.util.Vector; +import kotlin.Unit; public class ChallengeBattleBuilder { public static Vector clonedPokemonList = new Vector<>(); public static Vector challengeBattles = new Vector<>(); - private ChallengeFormat format = ChallengeFormat.STANDARD_6V6; - public void lvlxpvp(ServerPlayer player1, ServerPlayer player2, BattleFormat battleFormat, int minLevel, int maxLevel, int handicapP1, int handicapP2, List player1Selection, List player2Selection) throws ChallengeBuilderException { + private final ChallengeFormat format = ChallengeFormat.STANDARD_6V6; + public void lvlxpvp(ServerPlayer player1, ServerPlayer player2, BattleFormat battleFormat, ChallengeProperties properties, List player1Selection, List player2Selection) throws ChallengeBuilderException { PartyStore p1Party = Cobblemon.INSTANCE.getStorage().getParty(player1); PartyStore p2Party = Cobblemon.INSTANCE.getStorage().getParty(player2); // Clone parties so original is not effected - List player1Team = createBattleTeamFromParty(p1Party, player1Selection, minLevel, maxLevel, handicapP1); - List player2Team = createBattleTeamFromParty(p2Party, player2Selection, minLevel, maxLevel, handicapP2); + List player1Team = createBattleTeamFromParty(p1Party, player1Selection, properties, properties.getHandicapP1()); + List player2Team = createBattleTeamFromParty(p2Party, player2Selection, properties, properties.getHandicapP2()); PlayerBattleActor player1Actor = new PlayerBattleActor(player1.getUUID(), player1Team); PlayerBattleActor player2Actor = new PlayerBattleActor(player2.getUUID(), player2Team); @@ -44,9 +47,9 @@ public void lvlxpvp(ServerPlayer player1, ServerPlayer player2, BattleFormat bat } // Method to create our own clones according to the format - private List createBattleTeamFromParty(PartyStore party, List selectedSlots, int minLevel, int maxLevel, int handicap) throws ChallengeBuilderException { + private List createBattleTeamFromParty(PartyStore party, List selectedSlots, ChallengeProperties properties, int handicap) throws ChallengeBuilderException { - List battlePokemonList = new ArrayList(); + List battlePokemonList = new ArrayList<>(); if (format == ChallengeFormat.STANDARD_6V6) { int leadSlot = selectedSlots.get(0); Pokemon leadPokemon = party.get(leadSlot); @@ -55,15 +58,15 @@ private List createBattleTeamFromParty(PartyStore party, List CHALLENGE_REQUESTS = new HashMap<>(); public static final HashMap ACTIVE_SELECTIONS = new HashMap<>(); @@ -43,7 +47,7 @@ public static void register(CommandDispatcher dispatcher) { // Overview: // > always player name with level or min/maxLevel are mutually exclusive // > 12 command trees - // > further additions may need a UI implementation to refine/make more user friendly + // > further additions may need a UI implementation to refine/make more user-friendly // (default everything) // handicap @@ -63,19 +67,16 @@ public static void register(CommandDispatcher dispatcher) { // (default everything) LiteralArgumentBuilder defaultChallengeProperties = Commands.literal("challenge") .then(Commands.argument("player", EntityArgument.player()) - .executes(c -> challengePlayer(c, DEFAULT_LEVEL, DEFAULT_LEVEL, DEFAULT_HANDICAP, DEFAULT_HANDICAP, true)) + .executes(c -> challengePlayer(c, DEFAULT_LEVEL, DEFAULT_LEVEL, DEFAULT_HANDICAP, DEFAULT_HANDICAP, DEFAULT_SHOW_PREVIEW)) ); // handicap LiteralArgumentBuilder handicapChallengeProperties = Commands.literal("challenge") .then(Commands.argument("player", EntityArgument.player()) - .then(Commands.literal("handicapP1") - .then(Commands.argument("setP1HandicapTo", IntegerArgumentType.integer(-99,99)) - .then(Commands.literal("handicapP2") - .then(Commands.argument("setP2HandicapTo", IntegerArgumentType.integer(-99,99)) - .executes(c -> challengePlayer(c, DEFAULT_LEVEL, DEFAULT_LEVEL, IntegerArgumentType.getInteger(c, "setP1HandicapTo"), IntegerArgumentType.getInteger(c, "setP2HandicapTo"), true)) - ) - + .then(Commands.literal("handicap") + .then(Commands.argument("self", IntegerArgumentType.integer(-99,99)) + .then(Commands.argument("rival", IntegerArgumentType.integer(-99,99)) + .executes(c -> challengePlayer(c, DEFAULT_LEVEL, DEFAULT_LEVEL, IntegerArgumentType.getInteger(c, "self"), IntegerArgumentType.getInteger(c, "rival"), DEFAULT_SHOW_PREVIEW)) ) ) ) @@ -84,20 +85,22 @@ public static void register(CommandDispatcher dispatcher) { // no preview LiteralArgumentBuilder noPreviewChallengeProperties = Commands.literal("challenge") .then(Commands.argument("player", EntityArgument.player()) - .then(Commands.literal("nopreview") - .executes(c -> challengePlayer(c, DEFAULT_LEVEL, DEFAULT_LEVEL, DEFAULT_HANDICAP, DEFAULT_HANDICAP, false)) + .then(Commands.literal("showPreview") + .then(Commands.argument("show",BoolArgumentType.bool()) + .executes(c -> challengePlayer(c, DEFAULT_LEVEL, DEFAULT_LEVEL, DEFAULT_HANDICAP, DEFAULT_HANDICAP, BoolArgumentType.getBool(c, "show"))) + ) ) ); // handicap + no preview LiteralArgumentBuilder handicapNoPreviewChallengeProperties = Commands.literal("challenge") .then(Commands.argument("player", EntityArgument.player()) - .then(Commands.literal("handicapP1") - .then(Commands.argument("setP1HandicapTo", IntegerArgumentType.integer(-99,99)) - .then(Commands.literal("handicapP2") - .then(Commands.argument("setP2HandicapTo", IntegerArgumentType.integer(-99,99)) - .then(Commands.literal("nopreview") - .executes(c -> challengePlayer(c, DEFAULT_LEVEL, DEFAULT_LEVEL, IntegerArgumentType.getInteger(c, "setP1HandicapTo"), IntegerArgumentType.getInteger(c, "setP2HandicapTo"), false)) + .then(Commands.literal("handicap") + .then(Commands.argument("self", IntegerArgumentType.integer(-99,99)) + .then(Commands.argument("rival", IntegerArgumentType.integer(-99,99)) + .then(Commands.literal("showPreview") + .then(Commands.argument("show",BoolArgumentType.bool()) + .executes(c -> challengePlayer(c, DEFAULT_LEVEL, DEFAULT_LEVEL, IntegerArgumentType.getInteger(c, "self"), IntegerArgumentType.getInteger(c, "rival"), BoolArgumentType.getBool(c, "show"))) ) ) ) @@ -110,7 +113,7 @@ public static void register(CommandDispatcher dispatcher) { .then(Commands.argument("player", EntityArgument.player()) .then(Commands.literal("level") .then(Commands.argument("setLevelTo", IntegerArgumentType.integer(1,100)) - .executes(c -> challengePlayer(c, IntegerArgumentType.getInteger(c, "setLevelTo"), IntegerArgumentType.getInteger(c, "setLevelTo"), DEFAULT_HANDICAP, DEFAULT_HANDICAP, true)) + .executes(c -> challengePlayer(c, IntegerArgumentType.getInteger(c, "setLevelTo"), IntegerArgumentType.getInteger(c, "setLevelTo"), DEFAULT_HANDICAP, DEFAULT_HANDICAP, DEFAULT_SHOW_PREVIEW)) ) ) @@ -121,16 +124,13 @@ public static void register(CommandDispatcher dispatcher) { .then(Commands.argument("player", EntityArgument.player()) .then(Commands.literal("level") .then(Commands.argument("setLevelTo", IntegerArgumentType.integer(1,100)) - .then(Commands.literal("handicapP1") - .then(Commands.argument("setP1HandicapTo", IntegerArgumentType.integer(-99,99)) - .then(Commands.literal("handicapP2") - .then(Commands.argument("setP2HandicapTo", IntegerArgumentType.integer(-99,99)) - .executes(c -> challengePlayer(c, IntegerArgumentType.getInteger(c, "setLevelTo"), IntegerArgumentType.getInteger(c, "setLevelTo"), IntegerArgumentType.getInteger(c, "setP1HandicapTo"), IntegerArgumentType.getInteger(c, "setP2HandicapTo"), true)) - ) + .then(Commands.literal("handicap") + .then(Commands.argument("self", IntegerArgumentType.integer(-99,99)) + .then(Commands.argument("rival", IntegerArgumentType.integer(-99,99)) + .executes(c -> challengePlayer(c, IntegerArgumentType.getInteger(c, "setLevelTo"), IntegerArgumentType.getInteger(c, "setLevelTo"), IntegerArgumentType.getInteger(c, "self"), IntegerArgumentType.getInteger(c, "rival"), DEFAULT_SHOW_PREVIEW)) ) ) ) - ) ) ); @@ -140,10 +140,11 @@ public static void register(CommandDispatcher dispatcher) { .then(Commands.argument("player", EntityArgument.player()) .then(Commands.literal("level") .then(Commands.argument("setLevelTo", IntegerArgumentType.integer(1,100)) - .then(Commands.literal("nopreview") - .executes(c -> challengePlayer(c, IntegerArgumentType.getInteger(c, "setLevelTo"), IntegerArgumentType.getInteger(c, "setLevelTo"), DEFAULT_HANDICAP, DEFAULT_HANDICAP, false)) + .then(Commands.literal("showPreview") + .then(Commands.argument("show",BoolArgumentType.bool()) + .executes(c -> challengePlayer(c, IntegerArgumentType.getInteger(c, "setLevelTo"), IntegerArgumentType.getInteger(c, "setLevelTo"), DEFAULT_HANDICAP, DEFAULT_HANDICAP, BoolArgumentType.getBool(c, "show"))) + ) ) - ) ) ); @@ -153,18 +154,17 @@ public static void register(CommandDispatcher dispatcher) { .then(Commands.argument("player", EntityArgument.player()) .then(Commands.literal("level") .then(Commands.argument("setLevelTo", IntegerArgumentType.integer(1,100)) - .then(Commands.literal("handicapP1") - .then(Commands.argument("setP1HandicapTo", IntegerArgumentType.integer(-99,99)) - .then(Commands.literal("handicapP2") - .then(Commands.argument("setP2HandicapTo", IntegerArgumentType.integer(-99,99)) - .then(Commands.literal("nopreview") - .executes(c -> challengePlayer(c, IntegerArgumentType.getInteger(c, "setLevelTo"), IntegerArgumentType.getInteger(c, "setLevelTo"), IntegerArgumentType.getInteger(c, "setP1HandicapTo"), IntegerArgumentType.getInteger(c, "setP2HandicapTo"), false)) + .then(Commands.literal("handicap") + .then(Commands.argument("self", IntegerArgumentType.integer(-99,99)) + .then(Commands.argument("rival", IntegerArgumentType.integer(-99,99)) + .then(Commands.literal("showPreview") + .then(Commands.argument("show",BoolArgumentType.bool()) + .executes(c -> challengePlayer(c, IntegerArgumentType.getInteger(c, "setLevelTo"), IntegerArgumentType.getInteger(c, "setLevelTo"), IntegerArgumentType.getInteger(c, "self"), IntegerArgumentType.getInteger(c, "rival"), BoolArgumentType.getBool(c, "show"))) ) ) ) ) ) - ) ) ); @@ -172,14 +172,11 @@ public static void register(CommandDispatcher dispatcher) { // min/max LiteralArgumentBuilder minMaxLevelChallengeProperties = Commands.literal("challenge") .then(Commands.argument("player", EntityArgument.player()) - .then(Commands.literal("minLevel") - .then(Commands.argument("setMinLevelTo", IntegerArgumentType.integer(1,100)) - .then(Commands.literal("maxLevel") - .then(Commands.argument("setMaxLevelTo", IntegerArgumentType.integer(1,100)) - .executes(c -> challengePlayer(c, IntegerArgumentType.getInteger(c, "setMinLevelTo"), IntegerArgumentType.getInteger(c, "setMaxLevelTo"), DEFAULT_HANDICAP, DEFAULT_HANDICAP, true)) - ) + .then(Commands.literal("levelRange") + .then(Commands.argument("minLevel", IntegerArgumentType.integer(1,100)) + .then(Commands.argument("maxLevel", IntegerArgumentType.integer(1,100)) + .executes(c -> challengePlayer(c, IntegerArgumentType.getInteger(c, "minLevel"), IntegerArgumentType.getInteger(c, "maxLevel"), DEFAULT_HANDICAP, DEFAULT_HANDICAP, DEFAULT_SHOW_PREVIEW)) ) - ) ) ); @@ -187,22 +184,17 @@ public static void register(CommandDispatcher dispatcher) { // min/max + handicap LiteralArgumentBuilder minMaxLevelHandicapChallengeProperties = Commands.literal("challenge") .then(Commands.argument("player", EntityArgument.player()) - .then(Commands.literal("minLevel") - .then(Commands.argument("setMinLevelTo", IntegerArgumentType.integer(1,100)) - .then(Commands.literal("maxLevel") - .then(Commands.argument("setMaxLevelTo", IntegerArgumentType.integer(1,100)) - .then(Commands.literal("handicapP1") - .then(Commands.argument("setP1HandicapTo", IntegerArgumentType.integer(-99,99)) - .then(Commands.literal("handicapP2") - .then(Commands.argument("setP2HandicapTo", IntegerArgumentType.integer(-99,99)) - .executes(c -> challengePlayer(c, IntegerArgumentType.getInteger(c, "setMinLevelTo"), IntegerArgumentType.getInteger(c, "setMaxLevelTo"), IntegerArgumentType.getInteger(c, "setP1HandicapTo"), IntegerArgumentType.getInteger(c, "setP2HandicapTo"), true)) - ) - ) + .then(Commands.literal("levelRange") + .then(Commands.argument("minLevel", IntegerArgumentType.integer(1,100)) + .then(Commands.argument("maxLevel", IntegerArgumentType.integer(1,100)) + .then(Commands.literal("handicap") + .then(Commands.argument("self", IntegerArgumentType.integer(-99,99)) + .then(Commands.argument("rival", IntegerArgumentType.integer(-99,99)) + .executes(c -> challengePlayer(c, IntegerArgumentType.getInteger(c, "minLevel"), IntegerArgumentType.getInteger(c, "maxLevel"), IntegerArgumentType.getInteger(c, "self"), IntegerArgumentType.getInteger(c, "rival"), DEFAULT_SHOW_PREVIEW)) ) ) ) ) - ) ) ); @@ -210,16 +202,15 @@ public static void register(CommandDispatcher dispatcher) { // min/max + no preview LiteralArgumentBuilder minMaxLevelNoPreviewChallengeProperties = Commands.literal("challenge") .then(Commands.argument("player", EntityArgument.player()) - .then(Commands.literal("minLevel") - .then(Commands.argument("setMinLevelTo", IntegerArgumentType.integer(1,100)) - .then(Commands.literal("maxLevel") - .then(Commands.argument("setMaxLevelTo", IntegerArgumentType.integer(1,100)) - .then(Commands.literal("nopreview") - .executes(c -> challengePlayer(c, IntegerArgumentType.getInteger(c, "setMinLevelTo"), IntegerArgumentType.getInteger(c, "setMaxLevelTo"), DEFAULT_HANDICAP, DEFAULT_HANDICAP, false)) + .then(Commands.literal("levelRange") + .then(Commands.argument("minLevel", IntegerArgumentType.integer(1,100)) + .then(Commands.argument("maxLevel", IntegerArgumentType.integer(1,100)) + .then(Commands.literal("showPreview") + .then(Commands.argument("show",BoolArgumentType.bool()) + .executes(c -> challengePlayer(c, IntegerArgumentType.getInteger(c, "minLevel"), IntegerArgumentType.getInteger(c, "maxLevel"), DEFAULT_HANDICAP, DEFAULT_HANDICAP, BoolArgumentType.getBool(c, "show"))) ) ) ) - ) ) ); @@ -227,33 +218,30 @@ public static void register(CommandDispatcher dispatcher) { // min/max + handicap + no preview LiteralArgumentBuilder minMaxLevelHandicapNoPreviewChallengeProperties = Commands.literal("challenge") .then(Commands.argument("player", EntityArgument.player()) - .then(Commands.literal("minLevel") - .then(Commands.argument("setMinLevelTo", IntegerArgumentType.integer(1,100)) - .then(Commands.literal("maxLevel") - .then(Commands.argument("setMaxLevelTo", IntegerArgumentType.integer(1,100)) - .then(Commands.literal("handicapP1") - .then(Commands.argument("setP1HandicapTo", IntegerArgumentType.integer(-99,99)) - .then(Commands.literal("handicapP2") - .then(Commands.argument("setP2HandicapTo", IntegerArgumentType.integer(-99,99)) - .then(Commands.literal("nopreview") - .executes(c -> challengePlayer(c, IntegerArgumentType.getInteger(c, "setMinLevelTo"), IntegerArgumentType.getInteger(c, "setMaxLevelTo"), IntegerArgumentType.getInteger(c, "setP1HandicapTo"), IntegerArgumentType.getInteger(c, "setP2HandicapTo"), false)) - ) + .then(Commands.literal("levelRange") + .then(Commands.argument("minLevel", IntegerArgumentType.integer(1,100)) + .then(Commands.argument("maxLevel", IntegerArgumentType.integer(1,100)) + .then(Commands.literal("handicap") + .then(Commands.argument("self", IntegerArgumentType.integer(-99,99)) + .then(Commands.argument("rival", IntegerArgumentType.integer(-99,99)) + .then(Commands.literal("showPreview") + .then(Commands.argument("show",BoolArgumentType.bool()) + .executes(c -> challengePlayer(c, IntegerArgumentType.getInteger(c, "minLevel"), IntegerArgumentType.getInteger(c, "maxLevel"), IntegerArgumentType.getInteger(c, "self"), IntegerArgumentType.getInteger(c, "rival"), BoolArgumentType.getBool(c, "show"))) ) ) ) ) ) ) - ) ) ); // Command called to accept challenges - LiteralArgumentBuilder acceptChallengeAndProperties = Commands.literal("acceptchallenge") + LiteralArgumentBuilder acceptChallengeAndProperties = Commands.literal("accept-challenge") .then(Commands.argument("id", StringArgumentType.string()).executes(c -> acceptChallenge(c, StringArgumentType.getString(c, "id")))); // Command called to deny challenges - LiteralArgumentBuilder rejectChallengeAndProperties = Commands.literal("rejectchallenge") + LiteralArgumentBuilder rejectChallengeAndProperties = Commands.literal("reject-challenge") .then(Commands.argument("id", StringArgumentType.string()).executes(c -> rejectChallenge(c, StringArgumentType.getString(c, "id")))); @@ -281,18 +269,23 @@ public static void register(CommandDispatcher dispatcher) { public static int challengePlayer(CommandContext c, int minLevel, int maxLevel, int handicapP1, int handicapP2, boolean preview) { try { ServerPlayer challengerPlayer = c.getSource().getPlayer(); - ServerPlayer challengedPlayer = c.getArgument("player", EntitySelector.class).findSinglePlayer(c.getSource()); + if (challengerPlayer == null){ + c.getSource().sendFailure(Component.literal("Cannot send challenge, because ChallengerPlayer is null!")); + return 0; + } - if (LAST_SENT_CHALLENGE.containsKey(challengerPlayer.getUUID())) { - if (System.currentTimeMillis() - LAST_SENT_CHALLENGE.get(challengerPlayer.getUUID()) < CHALLENGE_COOLDOWN) { + ServerPlayer challengedPlayer = c.getArgument("player", EntitySelector.class).findSinglePlayer(c.getSource()); + UUID challengerUUID = challengerPlayer.getUUID(); + if (LAST_SENT_CHALLENGE.containsKey(challengerUUID)) { + if (System.currentTimeMillis() - LAST_SENT_CHALLENGE.get(challengerUUID) < CHALLENGE_COOLDOWN) { c.getSource().sendFailure(Component.literal(String.format("You must wait at least %d second(s) before sending another challenge", (int)Math.ceil(CHALLENGE_COOLDOWN / 1000f)))); return 0; } } for (ChallengeRequest request : CHALLENGE_REQUESTS.values()) { - if (request.challengerPlayer.getUUID().equals(challengerPlayer.getUUID())) { - c.getSource().sendFailure(Component.literal(String.format("You already have a pending challenge to %s", request.challengedPlayer.getDisplayName().getString()))); + if (request.challengerPlayer().getUUID().equals(challengerUUID)) { + c.getSource().sendFailure(Component.literal(String.format("You already have a pending challenge to %s", request.challengedPlayer().getDisplayName().getString()))); return 0; } } @@ -302,19 +295,18 @@ public static int challengePlayer(CommandContext c, int minL c.getSource().sendFailure(Component.literal("Cannot send challenge while in-battle")); return 0; } - if (Cobblemon.INSTANCE.getStorage().getParty(challengerPlayer).occupied() == 0) { c.getSource().sendFailure(Component.literal("Cannot send challenge while you have no pokemon!")); return 0; } - float distance = challengedPlayer.distanceTo(challengerPlayer); if (USE_DISTANCE_RESTRICTION && (distance > MAX_DISTANCE || challengedPlayer.level() != challengerPlayer.level())) { - c.getSource().sendFailure(Component.literal(String.format("Target must be less than %d blocks away to initiate a challenge", (int)MAX_DISTANCE))); + c.getSource().sendFailure(Component.literal(String.format("Target must be less than %d blocks away to initiate a challenge", (int) MAX_DISTANCE))); return 0; } + if (challengerPlayer == challengedPlayer) { c.getSource().sendFailure(Component.literal("You may not challenge yourself")); return 0; @@ -327,18 +319,18 @@ public static int challengePlayer(CommandContext c, int minL } ChallengeRequest request = ChallengeUtil.createChallengeRequest(challengerPlayer, challengedPlayer, minLevel, maxLevel, handicapP1, handicapP2, preview); - CHALLENGE_REQUESTS.put(request.id, request); + CHALLENGE_REQUESTS.put(request.id(), request); String levelComponent = (minLevel == maxLevel) ? ChatFormatting.YELLOW + String.format("You have been challenged to a " + ChatFormatting.BOLD + "level %d Pokemon battle", maxLevel) : ChatFormatting.YELLOW + String.format("You have been challenged to a " + ChatFormatting.BOLD + "level %d - %d Pokemon battle", minLevel, maxLevel); String challengerComponent = ChatFormatting.YELLOW + " by " + challengerPlayer.getDisplayName().getString() + "!"; - String optionsComponent = request.preview() ? "" : ChatFormatting.RED + " [NoTeamPreview]"; - String handicapComponent = (handicapP1 == 0 && handicapP2 == 0) ? "" : ChatFormatting.BLUE + " [" + challengerPlayer.getDisplayName().getString() + " handicap of " + handicapP1 + "] [" + challengedPlayer.getDisplayName().getString() + " handicap of " + handicapP2 + "]"; + String optionsComponent = request.properties().getShowPreview() ? "" : ChatFormatting.GOLD + " [NoTeamPreview]"; + String handicapComponent = (handicapP1 == 0 && handicapP2 == 0) ? "" : ChatFormatting.GREEN + " [" + challengerPlayer.getDisplayName().getString() + " handicap of " + handicapP1 + "] [" + challengedPlayer.getDisplayName().getString() + " handicap of " + handicapP2 + "]"; MutableComponent notificationComponent = Component.literal(levelComponent + challengerComponent + optionsComponent + handicapComponent); MutableComponent interactiveComponent = Component.literal("Click to accept or deny: "); - interactiveComponent.append(Component.literal(ChatFormatting.GREEN + "Battle!").setStyle(Style.EMPTY.withBold(true).withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, String.format("/acceptchallenge %s", request.id))))); + interactiveComponent.append(Component.literal(ChatFormatting.GREEN + "Battle!").setStyle(Style.EMPTY.withBold(true).withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, String.format("/accept-challenge %s", request.id()))))); interactiveComponent.append(Component.literal(" or ")); - interactiveComponent.append(Component.literal(ChatFormatting.RED + "Reject").setStyle(Style.EMPTY.withBold(true).withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, String.format("/rejectchallenge %s", request.id))))); + interactiveComponent.append(Component.literal(ChatFormatting.RED + "Reject").setStyle(Style.EMPTY.withBold(true).withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, String.format("/reject-challenge %s", request.id()))))); challengedPlayer.displayClientMessage(notificationComponent, false); challengedPlayer.displayClientMessage(interactiveComponent, false); challengerPlayer.displayClientMessage(Component.literal(ChatFormatting.YELLOW + String.format("Challenge has been sent to %s", challengedPlayer.getDisplayName().getString())), false); @@ -346,6 +338,7 @@ public static int challengePlayer(CommandContext c, int minL return Command.SINGLE_SUCCESS; } catch (Exception e) { c.getSource().sendFailure(Component.literal("An unexpected error has occurred: " + e.getMessage())); + //noinspection CallToPrintStackTrace e.printStackTrace(); return 0; } @@ -358,16 +351,17 @@ public static int rejectChallenge(CommandContext c, String c c.getSource().sendFailure(Component.literal("Challenge request is not valid")); return 0; } - CHALLENGE_REQUESTS.remove(request.id); - request.challengedPlayer.displayClientMessage(Component.literal(ChatFormatting.RED + "Challenge has been rejected"), false); + CHALLENGE_REQUESTS.remove(request.id()); + request.challengedPlayer().displayClientMessage(Component.literal(ChatFormatting.RED + "Challenge has been rejected"), false); - if (ChallengeUtil.isPlayerOnline(request.challengerPlayer)) { - request.challengerPlayer.displayClientMessage(Component.literal(ChatFormatting.RED + String.format("%s has rejected your challenge.", request.challengedPlayer.getDisplayName().getString())), false); + if (ChallengeUtil.isPlayerOnline(request.challengerPlayer())) { + request.challengerPlayer().displayClientMessage(Component.literal(ChatFormatting.RED + String.format("%s has rejected your challenge.", request.challengedPlayer().getDisplayName().getString())), false); } return Command.SINGLE_SUCCESS; } catch (Exception e) { c.getSource().sendFailure(Component.literal("An unexpected error has occurred: " + e.getMessage())); + //noinspection CallToPrintStackTrace e.printStackTrace(); return 0; } @@ -382,32 +376,32 @@ public static int acceptChallenge(CommandContext c, String c } BattleRegistry br = Cobblemon.INSTANCE.getBattleRegistry(); - if (br.getBattleByParticipatingPlayer(request.challengedPlayer) != null) { + if (br.getBattleByParticipatingPlayer(request.challengedPlayer()) != null) { c.getSource().sendFailure(Component.literal("Cannot accept challenge: you are already in a battle")); return 0; } - else if (br.getBattleByParticipatingPlayer(request.challengerPlayer) != null) { - c.getSource().sendFailure(Component.literal(String.format("Cannot accept challenge: %s is already in a battle", request.challengerPlayer.getDisplayName().getString()))); + else if (br.getBattleByParticipatingPlayer(request.challengerPlayer()) != null) { + c.getSource().sendFailure(Component.literal(String.format("Cannot accept challenge: %s is already in a battle", request.challengerPlayer().getDisplayName().getString()))); return 0; } - if (Cobblemon.INSTANCE.getStorage().getParty(request.challengedPlayer).occupied() == 0) { + if (Cobblemon.INSTANCE.getStorage().getParty(request.challengedPlayer()).occupied() == 0) { c.getSource().sendFailure(Component.literal("Cannot accept challenge: You have no pokemon!")); return 0; } - if (Cobblemon.INSTANCE.getStorage().getParty(request.challengerPlayer).occupied() == 0) { - c.getSource().sendFailure(Component.literal(String.format("Cannot accept challenge: %s has no pokemon... somehow!", request.challengerPlayer.getDisplayName().getString()))); + if (Cobblemon.INSTANCE.getStorage().getParty(request.challengerPlayer()).occupied() == 0) { + c.getSource().sendFailure(Component.literal(String.format("Cannot accept challenge: %s has no pokemon... somehow!", request.challengerPlayer().getDisplayName().getString()))); return 0; } - float distance = request.challengerPlayer.distanceTo(request.challengedPlayer); - if (USE_DISTANCE_RESTRICTION && (distance > MAX_DISTANCE || request.challengerPlayer.level() != request.challengedPlayer.level())) { + float distance = request.challengerPlayer().distanceTo(request.challengedPlayer()); + if (USE_DISTANCE_RESTRICTION && (distance > MAX_DISTANCE || request.challengerPlayer().level() != request.challengedPlayer().level())) { c.getSource().sendFailure(Component.literal(String.format("Target must be less than %d blocks away to accept a challenge", (int)MAX_DISTANCE))); return 0; } ChallengeRequest challengeRequestRemoved = CHALLENGE_REQUESTS.remove(challengeId); - ServerPlayer challengerPlayer = request.challengerPlayer; + ServerPlayer challengerPlayer = request.challengerPlayer(); if (!ChallengeUtil.isPlayerOnline(challengerPlayer)) { c.getSource().sendFailure(Component.literal(String.format("%s is no longer online", challengerPlayer.getDisplayName().getString()))); @@ -417,6 +411,7 @@ else if (br.getBattleByParticipatingPlayer(request.challengerPlayer) != null) { return Command.SINGLE_SUCCESS; } catch (Exception exc) { c.getSource().sendFailure(Component.literal("Unexpected exception when accepting challenge: " + exc.getMessage())); + //noinspection CallToPrintStackTrace exc.printStackTrace(); return 1; } diff --git a/src/main/java/com/turtlehoarder/cobblemonchallenge/config/ChallengeConfig.java b/src/main/java/com/turtlehoarder/cobblemonchallenge/config/ChallengeConfig.java index 62ba39a..7e901a0 100644 --- a/src/main/java/com/turtlehoarder/cobblemonchallenge/config/ChallengeConfig.java +++ b/src/main/java/com/turtlehoarder/cobblemonchallenge/config/ChallengeConfig.java @@ -1,21 +1,27 @@ package com.turtlehoarder.cobblemonchallenge.config; import com.turtlehoarder.cobblemonchallenge.CobblemonChallenge; +import com.turtlehoarder.cobblemonchallenge.battle.ChallengeFormat; + import com.mojang.datafixers.util.Pair; public class ChallengeConfig { + public static SimpleConfig CONFIG; - private static ChallengeConfigProvider configs; + private static ConfigProvider configs; public static Boolean CHALLENGE_DISTANCE_RESTRICTION; public static int MAX_CHALLENGE_DISTANCE; + public static ChallengeFormat DEFAULT_CHALLENGE_FORMAT; public static int DEFAULT_CHALLENGE_LEVEL; public static int DEFAULT_HANDICAP; + public static Boolean DEFAULT_SHOW_PREVIEW; public static int REQUEST_EXPIRATION_MILLIS; public static int CHALLENGE_COOLDOWN_MILLIS; + public static void registerConfigs() { CobblemonChallenge.LOGGER.info("Loading Challenge Configs"); - configs = new ChallengeConfigProvider(); + configs = new ConfigProvider(); createConfigs(); CONFIG = SimpleConfig.of(CobblemonChallenge.MODID + "-config").provider(configs).request(); assignConfigs(); @@ -23,8 +29,10 @@ public static void registerConfigs() { private static void createConfigs() { configs.addKeyValuePair(new Pair<>("challengeDistanceRestriction", true)); configs.addKeyValuePair(new Pair<>("maxChallengeDistance", 50)); + configs.addKeyValuePair(new Pair<>("defaultChallengeFormat", ChallengeFormat.STANDARD_6V6)); configs.addKeyValuePair(new Pair<>("defaultChallengeLevel", 50)); configs.addKeyValuePair(new Pair<>("defaultHandicap", 0)); + configs.addKeyValuePair(new Pair<>("defaultShowPreview", true)); configs.addKeyValuePair(new Pair<>("challengeExpirationTime", 60000)); configs.addKeyValuePair(new Pair<>("challengeCooldownTime", 5000)); } @@ -32,8 +40,10 @@ private static void createConfigs() { private static void assignConfigs() { CHALLENGE_COOLDOWN_MILLIS = CONFIG.getOrDefault("challengeCooldownTime", 5000); CHALLENGE_DISTANCE_RESTRICTION = CONFIG.getOrDefault("challengeDistanceRestriction", true); + DEFAULT_CHALLENGE_FORMAT = CONFIG.getOrDefault("defaultChallengeFormat",ChallengeFormat.STANDARD_6V6); DEFAULT_CHALLENGE_LEVEL = CONFIG.getOrDefault("defaultChallengeLevel", 50); DEFAULT_HANDICAP = CONFIG.getOrDefault("defaultHandicap", 0); + DEFAULT_SHOW_PREVIEW = CONFIG.getOrDefault("defaultShowPreview", true); MAX_CHALLENGE_DISTANCE = CONFIG.getOrDefault("maxChallengeDistance", 50); REQUEST_EXPIRATION_MILLIS = CONFIG.getOrDefault("challengeExpirationTime", 60000); } diff --git a/src/main/java/com/turtlehoarder/cobblemonchallenge/config/ChallengeConfigProvider.java b/src/main/java/com/turtlehoarder/cobblemonchallenge/config/ConfigProvider.java similarity index 87% rename from src/main/java/com/turtlehoarder/cobblemonchallenge/config/ChallengeConfigProvider.java rename to src/main/java/com/turtlehoarder/cobblemonchallenge/config/ConfigProvider.java index cd45669..b9536d1 100644 --- a/src/main/java/com/turtlehoarder/cobblemonchallenge/config/ChallengeConfigProvider.java +++ b/src/main/java/com/turtlehoarder/cobblemonchallenge/config/ConfigProvider.java @@ -4,7 +4,8 @@ import java.util.ArrayList; import java.util.List; -public class ChallengeConfigProvider implements SimpleConfig.DefaultConfig { + +public class ConfigProvider implements SimpleConfig.DefaultConfig { private String configContents = ""; private final List configsList = new ArrayList<>(); diff --git a/src/main/java/com/turtlehoarder/cobblemonchallenge/config/SimpleConfig.java b/src/main/java/com/turtlehoarder/cobblemonchallenge/config/SimpleConfig.java index dc52b94..ddc5e17 100644 --- a/src/main/java/com/turtlehoarder/cobblemonchallenge/config/SimpleConfig.java +++ b/src/main/java/com/turtlehoarder/cobblemonchallenge/config/SimpleConfig.java @@ -21,6 +21,9 @@ * THE SOFTWARE. */ +import com.cobblemontournament.api.tournament.TournamentType; +import com.turtlehoarder.cobblemonchallenge.battle.ChallengeFormat; + import net.fabricmc.loader.api.FabricLoader; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -226,6 +229,36 @@ public double getOrDefault( String key, double def ) { } } + /** + * Returns ChallengeFormat enum value from config corresponding to the given + * key, or the default ChallengeFormat enum if the key is missing or invalid. + * + * @return value corresponding to the given key, or the default value + */ + public ChallengeFormat getOrDefault( String key, ChallengeFormat def ) { + for (ChallengeFormat f : ChallengeFormat.values()) { + if (f.name().equals(key)) { + return f; + } + } + return def; + } + + /** + * Temporary while tournaments is integrated + * @param key + * @param def + * @return + */ + public TournamentType getOrDefault(String key, TournamentType def ) { + for (TournamentType f : TournamentType.values()) { + if (f.name().equals(key)) { + return f; + } + } + return def; + } + /** * If any error occurred during loading or reading from the config * a 'broken' flag is set, indicating that the config's state diff --git a/src/main/java/com/turtlehoarder/cobblemonchallenge/config/UniversalDifficultyConfig.java b/src/main/java/com/turtlehoarder/cobblemonchallenge/config/UniversalDifficultyConfig.java new file mode 100644 index 0000000..5d2f979 --- /dev/null +++ b/src/main/java/com/turtlehoarder/cobblemonchallenge/config/UniversalDifficultyConfig.java @@ -0,0 +1,48 @@ +package com.turtlehoarder.cobblemonchallenge.config; + +import com.turtlehoarder.cobblemonchallenge.CobblemonChallenge; +import com.mojang.datafixers.util.Pair; + +// temp class for possible configs while testing new features +public class UniversalDifficultyConfig { + + public static SimpleConfig CONFIG; + private static ConfigProvider configs; + public static Boolean USE_UNIVERSAL_LEVEL; + public static Boolean USE_UNIVERSAL_LEVEL_RANGE; + public static Boolean USE_UNIVERSAL_HANDICAP; + public static int DEFAULT_UNIVERSAL_LEVEL; + public static int DEFAULT_UNIVERSAL_MIN_LEVEL; + public static int DEFAULT_UNIVERSAL_MAX_LEVEL; + public static int DEFAULT_UNIVERSAL_HANDICAP; + public static Boolean DEFAULT_SHOW_PREVIEW; + + public static void registerConfigs() { + CobblemonChallenge.LOGGER.info("Loading Universal Difficulty Configs"); + configs = new ConfigProvider(); + createConfigs(); + CONFIG = SimpleConfig.of(CobblemonChallenge.MODID + "-universal-difficulty-config").provider(configs).request(); + assignConfigs(); + } + private static void createConfigs() { + configs.addKeyValuePair(new Pair<>("useUniversalLevel", false)); + configs.addKeyValuePair(new Pair<>("useUniversalLevelRange", true)); + configs.addKeyValuePair(new Pair<>("useUniversalHandicap", true)); + configs.addKeyValuePair(new Pair<>("defaultUniversalLevel", 50)); + configs.addKeyValuePair(new Pair<>("defaultUniversalMinLevel", 50)); + configs.addKeyValuePair(new Pair<>("defaultUniversalMinLevel", 0)); + configs.addKeyValuePair(new Pair<>("defaultUniversalHandicap", 0)); + configs.addKeyValuePair(new Pair<>("defaultShowPreview", true)); + } + + private static void assignConfigs() { + USE_UNIVERSAL_LEVEL = CONFIG.getOrDefault("useUniversalLevel", false); + USE_UNIVERSAL_LEVEL_RANGE = CONFIG.getOrDefault("useUniversalLevelRange", true); + USE_UNIVERSAL_HANDICAP = CONFIG.getOrDefault("useUniversalHandicap", true); + DEFAULT_UNIVERSAL_LEVEL = CONFIG.getOrDefault("defaultUniversalLevel", 50); + DEFAULT_UNIVERSAL_MIN_LEVEL = CONFIG.getOrDefault("defaultUniversalMinLevel", 50); + DEFAULT_UNIVERSAL_MAX_LEVEL = CONFIG.getOrDefault("defaultUniversalMinLevel", 0); + DEFAULT_UNIVERSAL_HANDICAP = CONFIG.getOrDefault("defaultUniversalHandicap", 0); + DEFAULT_SHOW_PREVIEW = CONFIG.getOrDefault("defaultShowPreview", true); + } +} diff --git a/src/main/java/com/turtlehoarder/cobblemonchallenge/event/ChallengeEventHandler.java b/src/main/java/com/turtlehoarder/cobblemonchallenge/event/ChallengeEventHandler.java index cf8db7e..3b8982c 100644 --- a/src/main/java/com/turtlehoarder/cobblemonchallenge/event/ChallengeEventHandler.java +++ b/src/main/java/com/turtlehoarder/cobblemonchallenge/event/ChallengeEventHandler.java @@ -1,5 +1,17 @@ package com.turtlehoarder.cobblemonchallenge.event; +import com.turtlehoarder.cobblemonchallenge.CobblemonChallenge; +import com.turtlehoarder.cobblemonchallenge.api.ChallengeRequest; +import com.turtlehoarder.cobblemonchallenge.api.LeadPokemonSelection; +import com.turtlehoarder.cobblemonchallenge.api.storage.FakePokemonStore; +import com.turtlehoarder.cobblemonchallenge.api.storage.party.FakePartyPosition; +import com.turtlehoarder.cobblemonchallenge.api.storage.party.FakePlayerPartyStore; +import com.turtlehoarder.cobblemonchallenge.battle.ChallengeBattleBuilder; +import com.turtlehoarder.cobblemonchallenge.command.ChallengeCommand; +import com.turtlehoarder.cobblemonchallenge.config.ChallengeConfig; +import com.turtlehoarder.cobblemonchallenge.gui.LeadPokemonSelectionSession; +import com.turtlehoarder.cobblemonchallenge.util.ChallengeUtil; + import com.cobblemon.mod.common.Cobblemon; import com.cobblemon.mod.common.CobblemonNetwork; import com.cobblemon.mod.common.api.Priority; @@ -9,16 +21,7 @@ import com.cobblemon.mod.common.api.storage.*; import com.cobblemon.mod.common.entity.pokemon.PokemonEntity; import com.cobblemon.mod.common.net.messages.client.storage.party.SetPartyReferencePacket; -import com.turtlehoarder.cobblemonchallenge.CobblemonChallenge; -import com.turtlehoarder.cobblemonchallenge.battle.ChallengeBattleBuilder; -import com.turtlehoarder.cobblemonchallenge.command.ChallengeCommand; -import com.turtlehoarder.cobblemonchallenge.config.ChallengeConfig; -import com.turtlehoarder.cobblemonchallenge.gui.LeadPokemonSelectionSession; -import com.turtlehoarder.cobblemonchallenge.util.ChallengeUtil; -import com.turtlehoarder.cobblemonchallenge.util.FakeStore; -import com.turtlehoarder.cobblemonchallenge.util.FakeStorePosition; -import kotlin.Unit; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; @@ -30,6 +33,7 @@ import net.minecraft.world.entity.Entity; import java.util.*; +import kotlin.Unit; public class ChallengeEventHandler { @@ -37,26 +41,17 @@ public static void registerEvents() { registerPostVictoryEvent(); registerChallengeLootPrevention(); registerCobblemonSavePrevention(); - ServerEntityEvents.ENTITY_LOAD.register((entity, server) -> { - checkSpawn(entity); - }); - ServerPlayConnectionEvents.DISCONNECT.register((event, server) -> { - onPlayerLoggedOut(event.getPlayer()); - }); - - ServerLifecycleEvents.SERVER_STOPPING.register((server) -> { - onServerShutdown(); - }); - + ServerEntityEvents.ENTITY_LOAD.register((entity, server) -> checkSpawn(entity)); + ServerPlayConnectionEvents.DISCONNECT.register((event, server) -> onPlayerLoggedOut(event.getPlayer())); + ServerLifecycleEvents.SERVER_STOPPING.register((server) -> onServerShutdown()); ServerTickEvents.END_SERVER_TICK.register(ChallengeEventHandler::onServerTick); - } /* Since this plugin uses cloned pokemon in its battles, there will be a *cloned* pokemon left behind after the battle is complete. These events ensure that these cloned entities are tracked and removed when a battle ends via Victory, disconnect, or server shutdown */ - public static boolean registerPostVictoryEvent() { + public static void registerPostVictoryEvent() { CobblemonEvents.BATTLE_VICTORY.subscribe(Priority.NORMAL, (battleVictoryEvent) -> { CobblemonChallenge.LOGGER.debug("Battle victory!"); UUID battleId = battleVictoryEvent.getBattle().getBattleId(); @@ -103,7 +98,6 @@ public static boolean registerPostVictoryEvent() { } return Unit.INSTANCE; }); - return true; } // Prevent Challenge-mons from being saved to the world to prevent odd scenarios where duplicates can be spawned and re-caught @@ -162,22 +156,15 @@ public static void checkSpawn(Entity entity) { CobblemonChallenge.LOGGER.debug(String.format("Entity Joined already in battle: %s | Battle id %s", entity.getDisplayName().getString(), pokemonEntity.getBattleId())); ChallengeBattleBuilder.clonedPokemonList.add(pokemonEntity); // Trick Cobblemon into thinking the clones are *not* wild pokemon. This will prevent duplicates being caught if something unexpected happens to the battle, like /stopbattle or a server crash - UUID foundplayerUUID = null; PokemonBattle pb = ChallengeUtil.getAssociatedBattle(pokemonEntity); if (pb != null) { - foundplayerUUID = ChallengeUtil.getOwnerUuidOfClonedPokemon(pb, pokemonEntity); - } - if (pb != null) { + UUID foundplayerUUID = ChallengeUtil.getOwnerUuidOfClonedPokemon(pb, pokemonEntity); UUID playerUUID = (foundplayerUUID != null ? foundplayerUUID : new UUID(0,0)); - FakeStore fakeStore = new FakeStore(playerUUID); - // World's worst casting. Don't do this at home. - PokemonStore fakePartyStore = (PokemonStore)(PokemonStore) fakeStore; - pokemonEntity.getPokemon().getStoreCoordinates().set(new StoreCoordinates<>(fakePartyStore, new FakeStorePosition())); + FakePlayerPartyStore fakePlayerStore = new FakePlayerPartyStore(playerUUID); + PokemonStore fakePokemonStore = new FakePokemonStore(fakePlayerStore,playerUUID); + pokemonEntity.getPokemon().getStoreCoordinates().set(new StoreCoordinates<>(fakePokemonStore, new FakePartyPosition())); pokemonEntity.getBusyLocks().add("Cloned_Pokemon"); // Busy lock prevents others from interacting with cloned pokemon } - - - } } } @@ -197,10 +184,10 @@ public static void onServerTick(MinecraftServer server) { int tickCount = server.getTickCount(); if (tickCount % 20 == 0) { long nowTime = System.currentTimeMillis(); - Iterator> requestIterator = ChallengeCommand.CHALLENGE_REQUESTS.entrySet().iterator(); + Iterator> requestIterator = ChallengeCommand.CHALLENGE_REQUESTS.entrySet().iterator(); while (requestIterator.hasNext()) { - Map.Entry requestMap = requestIterator.next(); - ChallengeCommand.ChallengeRequest request = requestMap.getValue(); + Map.Entry requestMap = requestIterator.next(); + ChallengeRequest request = requestMap.getValue(); if (request.createdTime() + ChallengeConfig.REQUEST_EXPIRATION_MILLIS < nowTime) { if (ChallengeUtil.isPlayerOnline(request.challengedPlayer())) { request.challengedPlayer().displayClientMessage(Component.literal(ChatFormatting.RED + String.format("Challenge from %s has expired", request.challengerPlayer().getDisplayName().getString())), false); @@ -211,7 +198,7 @@ public static void onServerTick(MinecraftServer server) { requestIterator.remove(); } } - Iterator> selectionIterator = ChallengeCommand.ACTIVE_SELECTIONS.entrySet().iterator(); + Iterator> selectionIterator = ChallengeCommand.ACTIVE_SELECTIONS.entrySet().iterator(); while (selectionIterator.hasNext()) { LeadPokemonSelectionSession selectionSession = selectionIterator.next().getValue().selectionWrapper(); selectionSession.doTick(); diff --git a/src/main/java/com/turtlehoarder/cobblemonchallenge/gui/LeadPokemonMenu.java b/src/main/java/com/turtlehoarder/cobblemonchallenge/gui/LeadPokemonMenu.java index 490a915..98d2d40 100644 --- a/src/main/java/com/turtlehoarder/cobblemonchallenge/gui/LeadPokemonMenu.java +++ b/src/main/java/com/turtlehoarder/cobblemonchallenge/gui/LeadPokemonMenu.java @@ -6,6 +6,7 @@ import net.minecraft.world.inventory.ChestMenu; import net.minecraft.world.inventory.ClickType; import net.minecraft.world.inventory.MenuType; + import org.jetbrains.annotations.NotNull; public class LeadPokemonMenu extends ChestMenu { diff --git a/src/main/java/com/turtlehoarder/cobblemonchallenge/gui/LeadPokemonMenuProvider.java b/src/main/java/com/turtlehoarder/cobblemonchallenge/gui/LeadPokemonMenuProvider.java index faf1348..6ea5398 100644 --- a/src/main/java/com/turtlehoarder/cobblemonchallenge/gui/LeadPokemonMenuProvider.java +++ b/src/main/java/com/turtlehoarder/cobblemonchallenge/gui/LeadPokemonMenuProvider.java @@ -1,15 +1,16 @@ package com.turtlehoarder.cobblemonchallenge.gui; +import com.turtlehoarder.cobblemonchallenge.api.ChallengeRequest; +import com.turtlehoarder.cobblemonchallenge.battle.ChallengeFormat; +import com.turtlehoarder.cobblemonchallenge.battle.pokemon.ChallengeBattlePokemon; +import com.turtlehoarder.cobblemonchallenge.util.ChallengeUtil; + import com.cobblemon.mod.common.Cobblemon; import com.cobblemon.mod.common.CobblemonItems; import com.cobblemon.mod.common.api.storage.party.PartyStore; -import com.cobblemon.mod.common.battles.pokemon.BattlePokemon; import com.cobblemon.mod.common.item.PokemonItem; import com.cobblemon.mod.common.pokemon.Pokemon; -import com.turtlehoarder.cobblemonchallenge.CobblemonChallenge; -import com.turtlehoarder.cobblemonchallenge.battle.ChallengeFormat; -import com.turtlehoarder.cobblemonchallenge.command.ChallengeCommand; -import com.turtlehoarder.cobblemonchallenge.util.ChallengeUtil; + import net.minecraft.ChatFormatting; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.StringTag; @@ -22,6 +23,7 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.level.block.Blocks; + import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -43,9 +45,9 @@ private enum MenuState {WAITING_FOR_BOTH, WAITING_FOR_RIVAL, WAITING_FOR_PLAYER} private LeadPokemonMenu openedMenu; public List selectedSlots = new ArrayList(); - private ChallengeCommand.ChallengeRequest request; + private final ChallengeRequest request; - public LeadPokemonMenuProvider(LeadPokemonSelectionSession wrapper, ServerPlayer selector, ServerPlayer rivalPlayer, ChallengeCommand.ChallengeRequest request) { + public LeadPokemonMenuProvider(LeadPokemonSelectionSession wrapper, ServerPlayer selector, ServerPlayer rivalPlayer, ChallengeRequest request) { this.selector = selector; this.rival = rivalPlayer; this.selectionSession = wrapper; @@ -70,34 +72,35 @@ private void setupPokemonRepresentation(LeadPokemonMenu leadPokemonMenu) { PartyStore p2Party = Cobblemon.INSTANCE.getStorage().getParty(rival); setupGlassFiller(leadPokemonMenu); - int handicapP1 = (this.selector == request.challengerPlayer()) ? request.handicapP1() : request.handicapP2(); - int handicapP2 = (this.selector == request.challengerPlayer()) ? request.handicapP2() : request.handicapP1(); - + // Cache handicap + int handicapP1 = (this.selector == request.challengerPlayer()) ? request.properties().getHandicapP1() : request.properties().getHandicapP2(); for (int x = 0; x < p1Party.size(); x ++) { - int itemSlot = x * 9; // Lefthand column of the menu + int itemSlot = x * 9; // Left hand column of the menu Pokemon pokemon = p1Party.get(x); if (pokemon == null) // Skip any empty slots in the pokemon team continue; - BattlePokemon copy = BattlePokemon.Companion.safeCopyOf(pokemon); - int adjustedLevelP1 = ChallengeUtil.getBattlePokemonAdjustedLevel(pokemon.getLevel(), request.minLevel(), request.maxLevel(), handicapP1); - pokemon = ChallengeUtil.applyFormatTransformations(ChallengeFormat.STANDARD_6V6, copy, adjustedLevelP1).getEffectedPokemon(); // Apply battle transformations to each pokemon + ChallengeBattlePokemon copy = ChallengeBattlePokemon.Companion.safeCopyOfChallenge(pokemon); + copy.applyChallengePropertiesToEffectedPokemon(request.properties().getMinLevel(), request.properties().getMaxLevel(), handicapP1,true); + pokemon = copy.getEffectedPokemon(); ItemStack pokemonItem = PokemonItem.from(pokemon, 1); - pokemonItem.setHoverName(Component.literal(ChatFormatting.AQUA + String.format("%s (lvl%d)", pokemon.getDisplayName().getString(), adjustedLevelP1))); + pokemonItem.setHoverName(Component.literal(ChatFormatting.AQUA + String.format("%s (lvl%d)", pokemon.getDisplayName().getString(), pokemon.getLevel()))); ListTag pokemonLoreTag = ChallengeUtil.generateLoreTagForPokemon(pokemon); pokemonItem.getOrCreateTagElement("display").put("Lore", pokemonLoreTag); leadPokemonMenu.setItem(itemSlot, leadPokemonMenu.getStateId(), pokemonItem); } + // Cache enemy handicap + int handicapP2 = (this.selector == request.challengerPlayer()) ? request.properties().getHandicapP2() : request.properties().getHandicapP1(); // Set enemy side: for (int x= 0; x < p2Party.size(); x++) { - int itemSlot = (x * 9) + 8; // Righthand column of the menu + int itemSlot = (x * 9) + 8; // Right hand column of the menu Pokemon pokemon = p2Party.get(x); if (pokemon == null) { continue; } - if (selectionSession.teamPreviewOn()) { + if (selectionSession.isShowTeamPreview()) { ItemStack pokemonItem = PokemonItem.from(pokemon, 1); - int adjustedLevelP2 = ChallengeUtil.getBattlePokemonAdjustedLevel(pokemon.getLevel(), request.minLevel(), request.maxLevel(), handicapP2); + int adjustedLevelP2 = ChallengeUtil.getBattlePokemonAdjustedLevel(pokemon.getLevel(), request.properties().getMinLevel(), request.properties().getMaxLevel(), handicapP2); pokemonItem.setHoverName(Component.literal(ChatFormatting.RED + String.format("%s's %s (lvl%d)", rival.getDisplayName().getString(), pokemon.getDisplayName().getString(), adjustedLevelP2))); leadPokemonMenu.setItem(itemSlot, leadPokemonMenu.getStateId(), pokemonItem); } else { diff --git a/src/main/java/com/turtlehoarder/cobblemonchallenge/gui/LeadPokemonSelectionSession.java b/src/main/java/com/turtlehoarder/cobblemonchallenge/gui/LeadPokemonSelectionSession.java index 59412df..cfb7b8c 100644 --- a/src/main/java/com/turtlehoarder/cobblemonchallenge/gui/LeadPokemonSelectionSession.java +++ b/src/main/java/com/turtlehoarder/cobblemonchallenge/gui/LeadPokemonSelectionSession.java @@ -1,11 +1,13 @@ package com.turtlehoarder.cobblemonchallenge.gui; -import com.cobblemon.mod.common.battles.BattleFormat; import com.turtlehoarder.cobblemonchallenge.CobblemonChallenge; +import com.turtlehoarder.cobblemonchallenge.api.ChallengeRequest; import com.turtlehoarder.cobblemonchallenge.battle.ChallengeBattleBuilder; import com.turtlehoarder.cobblemonchallenge.battle.ChallengeBuilderException; -import com.turtlehoarder.cobblemonchallenge.command.ChallengeCommand; import com.turtlehoarder.cobblemonchallenge.util.ChallengeUtil; + +import com.cobblemon.mod.common.battles.BattleFormat; + import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; @@ -16,7 +18,7 @@ public class LeadPokemonSelectionSession { private final LeadPokemonMenuProvider challengerMenuProvider; private final LeadPokemonMenuProvider challengedMenuProvider; - private final ChallengeCommand.ChallengeRequest originRequest; + private final ChallengeRequest originRequest; private final UUID uuid; public long creationTime; private int pokemonToSelect = 1; @@ -28,7 +30,7 @@ public class LeadPokemonSelectionSession { public static int LEAD_TIMEOUT_MILLIS = 90000; - public LeadPokemonSelectionSession(UUID uuid, long creationTime, ChallengeCommand.ChallengeRequest request) { + public LeadPokemonSelectionSession(UUID uuid, long creationTime, ChallengeRequest request) { this.originRequest = request; this.uuid = uuid; this.creationTime = creationTime; @@ -61,23 +63,19 @@ public void onPokemonSelected(LeadPokemonMenuProvider menuProvider) { } private void beginBattle() { - int minLevel = originRequest.minLevel(); - int maxLevel = originRequest.maxLevel(); - int handicapP1 = originRequest.handicapP1(); - int handicapP2 = originRequest.handicapP2(); SESSIONS_TO_CANCEL.add(this); challengerMenuProvider.forceCloseMenu(); challengedMenuProvider.forceCloseMenu(); ChallengeBattleBuilder challengeBuilder = new ChallengeBattleBuilder(); try { - challengeBuilder.lvlxpvp(originRequest.challengerPlayer(), originRequest.challengedPlayer(), BattleFormat.Companion.getGEN_9_SINGLES(), minLevel, maxLevel, handicapP1, handicapP2, challengerMenuProvider.selectedSlots, challengedMenuProvider.selectedSlots); + challengeBuilder.lvlxpvp(originRequest.challengerPlayer(), originRequest.challengedPlayer(), BattleFormat.Companion.getGEN_9_SINGLES(), originRequest.properties(), challengerMenuProvider.selectedSlots, challengedMenuProvider.selectedSlots); } catch (ChallengeBuilderException e) { e.printStackTrace(); } } - public boolean teamPreviewOn() { - return originRequest.preview(); + public boolean isShowTeamPreview() { + return originRequest.properties().getShowPreview(); } private boolean isBattleReady() { diff --git a/src/main/java/com/turtlehoarder/cobblemonchallenge/mixin/BattleBuilderMixin.java b/src/main/java/com/turtlehoarder/cobblemonchallenge/mixin/BattleBuilderMixin.java new file mode 100644 index 0000000..098d8f5 --- /dev/null +++ b/src/main/java/com/turtlehoarder/cobblemonchallenge/mixin/BattleBuilderMixin.java @@ -0,0 +1,77 @@ +package com.turtlehoarder.cobblemonchallenge.mixin; + +import com.cobblemon.mod.common.api.storage.party.PartyStore; +import com.cobblemon.mod.common.battles.BattleBuilder; +import com.cobblemon.mod.common.battles.pokemon.BattlePokemon; +import com.turtlehoarder.cobblemonchallenge.config.UniversalDifficultyConfig; + +import net.fabricmc.loader.impl.util.log.Log; +import net.fabricmc.loader.impl.util.log.LogCategory; + +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.List; +import java.util.UUID; + +@Mixin(BattleBuilder.class) +public class BattleBuilderMixin { + + @Unique private final static boolean useUniversalLevel = UniversalDifficultyConfig.USE_UNIVERSAL_LEVEL; + @Unique private final static boolean useUniversalLevelRange = UniversalDifficultyConfig.USE_UNIVERSAL_LEVEL_RANGE; + @Unique private final static boolean useUniversalHandicap = UniversalDifficultyConfig.USE_UNIVERSAL_HANDICAP; + + @Redirect( + method = "pvp1v1*", + at = @At( + value = "INVOKE", + target = "Lcom/cobblemon/mod/common/api/storage/party/PartyStore;toBattleTeam(ZZLjava/util/UUID;)Ljava/util/List;", + remap = false + )) + private List mixinPvpToBattleTeam( + PartyStore partyStore, + boolean cloneParties, + boolean healFirst, + @Nullable UUID leadingPokemon, + CallbackInfoReturnable> battleSideCI + ) { + Log.debug(LogCategory.LOG,"partyStore = '" + partyStore.toString() + "'"); + Log.debug(LogCategory.LOG,"mixin Redirect at BattleBuilder.pvp1v1 worked!"); + if (useUniversalLevel || useUniversalLevelRange) { // TODO expand on with future configs + // TODO Implement custom battle team here w/ ChallengeBattlePokemon + //return ; + } + // passing onto the default implementation + return partyStore.toBattleTeam(cloneParties, healFirst, leadingPokemon); + } + + + @Redirect( + method = "pve*", + at = @At( + value = "INVOKE", + target = "Lcom/cobblemon/mod/common/api/storage/party/PartyStore;toBattleTeam(ZZLjava/util/UUID;)Ljava/util/List;", + remap = false + )) + public List mixinPveToBattleTeam( + PartyStore partyStore, + boolean cloneParties, + boolean healFirst, + @Nullable UUID leadingPokemon + ) { + // TODO expand on with future configs + if ((useUniversalLevel || useUniversalLevelRange) || useUniversalHandicap) { + // TODO Implement custom battle team here w/ ChallengeBattlePokemon + // !! possible alternative -> + // capture parameters, or @Inject + HEAD to capture/test parameters then -> + // @Redirect + INVOKE .toBattleTeam w/ callbackInfo + //return ; + } + // passing onto the default implementation + return partyStore.toBattleTeam(cloneParties, healFirst, leadingPokemon); + } +} diff --git a/src/main/java/com/turtlehoarder/cobblemonchallenge/util/ChallengeUtil.java b/src/main/java/com/turtlehoarder/cobblemonchallenge/util/ChallengeUtil.java index f193958..2c1eaf4 100644 --- a/src/main/java/com/turtlehoarder/cobblemonchallenge/util/ChallengeUtil.java +++ b/src/main/java/com/turtlehoarder/cobblemonchallenge/util/ChallengeUtil.java @@ -7,10 +7,10 @@ import com.cobblemon.mod.common.entity.pokemon.PokemonEntity; import com.cobblemon.mod.common.pokemon.Pokemon; import com.cobblemon.mod.common.util.LocalizationUtilsKt; -import com.turtlehoarder.cobblemonchallenge.CobblemonChallenge; +import com.turtlehoarder.cobblemonchallenge.api.ChallengeProperties; +import com.turtlehoarder.cobblemonchallenge.api.ChallengeRequest; import com.turtlehoarder.cobblemonchallenge.battle.ChallengeBattleBuilder; import com.turtlehoarder.cobblemonchallenge.battle.ChallengeFormat; -import com.turtlehoarder.cobblemonchallenge.command.ChallengeCommand; import net.minecraft.ChatFormatting; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.StringTag; @@ -54,13 +54,13 @@ public static boolean isBattleChallenge(UUID battleId) { } public static boolean isPlayerOnline(ServerPlayer player) { + if (player.getServer() == null) { return false; } // prevent NullPointerException return player.getServer().getPlayerList().getPlayer(player.getUUID()) != null; } - public static ChallengeCommand.ChallengeRequest createChallengeRequest(ServerPlayer challengerPlayer, ServerPlayer challengedPlayer, int minLevel, int maxLevel, int handicapP1, int handicapP2, boolean preview) { + public static ChallengeRequest createChallengeRequest(ServerPlayer challengerPlayer, ServerPlayer challengedPlayer, int minLevel, int maxLevel, int handicapP1, int handicapP2, boolean showPreview) { String key = UUID.randomUUID().toString().replaceAll("-", ""); - ChallengeCommand.ChallengeRequest newRequest = new ChallengeCommand.ChallengeRequest(key, challengerPlayer, challengedPlayer, minLevel, maxLevel, handicapP1, handicapP2, preview, System.currentTimeMillis()); - return newRequest; + return new ChallengeRequest(key, challengerPlayer, challengedPlayer, new ChallengeProperties(null,minLevel, maxLevel, handicapP1, handicapP2, showPreview), System.currentTimeMillis()); } public static ItemLike getDisplayBlockForPokemon(Pokemon pokemon) { @@ -96,12 +96,12 @@ public static ListTag generateLoreTagForPokemon(Pokemon pokemon) { String statSeparator = ChatFormatting.GRAY + " / "; Component statsPartOne = Component.literal(String.format(ChatFormatting.RED + "HP: %d" + statSeparator + ChatFormatting.GOLD + "Atk: %d" + statSeparator + ChatFormatting.YELLOW + "Def: %d", pokemon.getHp(), pokemon.getAttack(), pokemon.getDefence())); Component statsPartTwo = Component.literal(String.format(ChatFormatting.AQUA + "SpA: %d" + statSeparator + ChatFormatting.GREEN + "SpD: %d" + statSeparator + ChatFormatting.LIGHT_PURPLE + "Spe: %d", pokemon.getSpecialAttack(), pokemon.getSpecialDefence(), pokemon.getSpeed())); - Component moveSeperator = Component.literal( "Moves:"); + Component moveSeparator = Component.literal( "Moves:"); loreTag.add(StringTag.valueOf(Component.Serializer.toJson(abilityComponent))); loreTag.add(StringTag.valueOf(Component.Serializer.toJson(natureComponent))); loreTag.add(StringTag.valueOf(Component.Serializer.toJson(statsPartOne))); loreTag.add(StringTag.valueOf(Component.Serializer.toJson(statsPartTwo))); - loreTag.add(StringTag.valueOf(Component.Serializer.toJson(moveSeperator))); + loreTag.add(StringTag.valueOf(Component.Serializer.toJson(moveSeparator))); pokemon.getMoveSet().getMoves().forEach(move -> { Component moveComponent = Component.literal(ChatFormatting.WHITE + String.format("%s - %d/%d", move.getDisplayName().getString() + ChatFormatting.GRAY, move.getMaxPp(), move.getMaxPp())); loreTag.add(StringTag.valueOf(Component.Serializer.toJson(moveComponent))); @@ -112,14 +112,15 @@ public static ListTag generateLoreTagForPokemon(Pokemon pokemon) { // Roundabout, but reliable way of getting the associated owner UUID of the cloned pokemon sent out in a challenge public static UUID getOwnerUuidOfClonedPokemon(PokemonBattle battle, PokemonEntity pokemonEntity) { for (ActiveBattlePokemon abp : battle.getActivePokemon()) { - if (abp.getBattlePokemon() != null && pokemonEntity.getPokemon().getUuid().equals(abp.getBattlePokemon().getEffectedPokemon().getUuid())) { - return abp.getBattlePokemon().getOriginalPokemon().getOwnerUUID(); + var battlePokemon = abp.getBattlePokemon(); + if (battlePokemon != null && pokemonEntity.getPokemon().getUuid().equals(battlePokemon.getEffectedPokemon().getUuid())) { + return battlePokemon.getOriginalPokemon().getOwnerUUID(); } } - return null; } + @SuppressWarnings("unused") public static BattlePokemon applyFormatTransformations(ChallengeFormat format, BattlePokemon pokemon, int level) { if (format == ChallengeFormat.STANDARD_6V6) { pokemon.getEffectedPokemon().setLevel(level); @@ -128,10 +129,6 @@ public static BattlePokemon applyFormatTransformations(ChallengeFormat format, B return pokemon; } - // Method for clamping Battle Pokemon to level range, between 1-100, & applying handicap - // > the handicap applied AFTER level clamp to range - // > A players level may be outside this range after the handicap is applied - // > But, the finalized handicap will be a hard clamped to (1,100) public static int getBattlePokemonAdjustedLevel(int actualLevel, int minLevel, int maxLevel, int handicap) { int adjustedLevel = (actualLevel < minLevel) ? minLevel + handicap : Math.min(actualLevel, maxLevel) + handicap; return (adjustedLevel < 1) ? 1 : Math.min(adjustedLevel, 100); diff --git a/src/main/kotlin/com/turtlehoarder/cobblemonchallenge/api/storage/FakePokemonStore.kt b/src/main/kotlin/com/turtlehoarder/cobblemonchallenge/api/storage/FakePokemonStore.kt new file mode 100644 index 0000000..95745d6 --- /dev/null +++ b/src/main/kotlin/com/turtlehoarder/cobblemonchallenge/api/storage/FakePokemonStore.kt @@ -0,0 +1,100 @@ +package com.turtlehoarder.cobblemonchallenge.api.storage + +import com.turtlehoarder.cobblemonchallenge.api.storage.party.FakePlayerPartyStore + +import com.cobblemon.mod.common.api.reactive.Observable +import com.cobblemon.mod.common.api.storage.PokemonStore +import com.cobblemon.mod.common.api.storage.StoreCoordinates +import com.cobblemon.mod.common.api.storage.StorePosition +import com.cobblemon.mod.common.api.storage.factory.PokemonStoreFactory +import com.cobblemon.mod.common.pokemon.Pokemon +import com.google.gson.JsonObject +import net.minecraft.server.level.ServerPlayer +import net.minecraft.nbt.CompoundTag +import java.util.* + +class FakePokemonStore( + private val playerStore : FakePlayerPartyStore, + /** The UUID of the store. The exact uniqueness requirements depend on the method used for saving. */ + override val uuid: UUID = playerStore.playerUUID +): PokemonStore() { + + /** Gets the [Pokemon] at the given position. */ + override operator fun get(position: StorePosition): Pokemon?{ + return null + } + + /** Gets the first empty position that a [Pokemon] might be put. */ + override fun getFirstAvailablePosition(): StorePosition? { + return null + } + + /** Gets an iterable of all [ServerPlayer]s that should be notified of any changes to the Pokémon in this store. */ + override fun getObservingPlayers(): Iterable { + return emptyList() + } + + /** Sends the contents of this store to a player as if they've never seen it before. This initializes the store then sends each contained Pokémon. */ + override fun sendTo(player: ServerPlayer) { } + + /** + * Runs initialization logic for this store, knowing that it has just been constructed in a [PokemonStoreFactory]. + * + * The minimum of what this function should do is iterate over all the Pokémon in this store and set their store + * coordinates. + * + * If this does not get called, or it does not do its job properly, serious de-sync issues may follow. + */ + override fun initialize() { } + + override fun iterator(): Iterator { + TODO("Not yet implemented") + } + + /** + * Sets the given position with the given [Pokemon], which can be null. This is for internal use only because + * other, more public methods will additionally send updates to the client, and for logical reasons this means + * there must be an internal and external set method. + */ + override fun setAtPosition(position: StorePosition, pokemon: Pokemon?) { + + } + + /** Returns true if the given position is pointing to a legitimate location in this store. */ + override fun isValidPosition(position: StorePosition): Boolean { + return true + } + + override fun saveToNBT(nbt: CompoundTag): CompoundTag { + return CompoundTag() + } + + override fun loadFromNBT(nbt: CompoundTag): PokemonStore { + return this + } + + override fun saveToJSON(json: JsonObject): JsonObject { + return JsonObject() + } + + override fun loadFromJSON(json: JsonObject): PokemonStore { + return this + } + + override fun savePositionToNBT(position: StorePosition, nbt: CompoundTag) { + + } + + override fun loadPositionFromNBT(nbt: CompoundTag): StoreCoordinates { + TODO("Not yet implemented") + } + + /** + * Returns an [Observable] that emits Unit whenever there is a change to this store. This includes any save-worthy + * change to a [Pokemon] contained in the store. You can access an [Observable] in each [Pokemon] that emits Unit for + * each change, accessed by [Pokemon.getChangeObservable]. + */ + override fun getAnyChangeObservable(): Observable { + return Observable.just(Unit) // @Eric: I think this should work but... I'll defer to you lol ;) - David + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/turtlehoarder/cobblemonchallenge/api/storage/party/FakePartyPosition.kt b/src/main/kotlin/com/turtlehoarder/cobblemonchallenge/api/storage/party/FakePartyPosition.kt new file mode 100644 index 0000000..cc92d83 --- /dev/null +++ b/src/main/kotlin/com/turtlehoarder/cobblemonchallenge/api/storage/party/FakePartyPosition.kt @@ -0,0 +1,8 @@ +package com.turtlehoarder.cobblemonchallenge.api.storage.party + +import com.cobblemon.mod.common.api.storage.StorePosition + +class FakePartyPosition(private var slot: Int = 0) : StorePosition { + fun set(newSlot: Int) { slot = newSlot } + fun get(): Int { return slot } +} \ No newline at end of file diff --git a/src/main/kotlin/com/turtlehoarder/cobblemonchallenge/api/storage/party/FakePlayerPartyStore.kt b/src/main/kotlin/com/turtlehoarder/cobblemonchallenge/api/storage/party/FakePlayerPartyStore.kt new file mode 100644 index 0000000..b85397c --- /dev/null +++ b/src/main/kotlin/com/turtlehoarder/cobblemonchallenge/api/storage/party/FakePlayerPartyStore.kt @@ -0,0 +1,15 @@ +package com.turtlehoarder.cobblemonchallenge.api.storage.party + +import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore +import com.cobblemon.mod.common.pokemon.Pokemon +import java.util.* + +class FakePlayerPartyStore( + private val fakeUUID: UUID = UUID(0, 0) +) : PlayerPartyStore(fakeUUID, fakeUUID) { + + // override function to serve the same purpose as the previous FakeStore + override fun add(pokemon: Pokemon): Boolean { + return true + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/turtlehoarder/cobblemonchallenge/battle/pokemon/ChallengeBattlePokemon.kt b/src/main/kotlin/com/turtlehoarder/cobblemonchallenge/battle/pokemon/ChallengeBattlePokemon.kt new file mode 100644 index 0000000..290e293 --- /dev/null +++ b/src/main/kotlin/com/turtlehoarder/cobblemonchallenge/battle/pokemon/ChallengeBattlePokemon.kt @@ -0,0 +1,86 @@ +package com.turtlehoarder.cobblemonchallenge.battle.pokemon + +import com.cobblemon.mod.common.Cobblemon +import com.cobblemon.mod.common.api.battles.model.actor.ActorType +import com.cobblemon.mod.common.api.tags.CobblemonItemTags +import com.cobblemon.mod.common.battles.pokemon.BattlePokemon +import com.cobblemon.mod.common.entity.pokemon.PokemonEntity +import com.cobblemon.mod.common.pokemon.Pokemon +import com.cobblemon.mod.common.pokemon.evolution.requirements.LevelRequirement + +import kotlin.math.min +import kotlin.math.pow +import kotlin.math.roundToInt + + +class ChallengeBattlePokemon( + originalPokemon: Pokemon, + effectedPokemon: Pokemon, + postBattleEntityOperation: (PokemonEntity) -> Unit +) : BattlePokemon(originalPokemon) { + + companion object { + fun safeCopyOfChallenge(pokemon: Pokemon): ChallengeBattlePokemon = ChallengeBattlePokemon( + originalPokemon = pokemon, + effectedPokemon = pokemon.clone(), + postBattleEntityOperation = { entity -> entity.discard() } + ) + } + + /** + * Method for clamping Battle Pokemon to level range & applying a handicap + * + * - [handicap] is applied AFTER level clamp to range + * - The [effectedPokemon] level may be outside the levelRange after the handicap is applied, but will be a hard clamped to 1-100 inclusive + */ + fun applyChallengePropertiesToEffectedPokemon(minLevel: Int, maxLevel: Int, handicap: Int,heal: Boolean) : BattlePokemon { + val adjustedLevel = if ((originalPokemon.level < minLevel)) minLevel + handicap else (min(originalPokemon.level, maxLevel) + handicap) + effectedPokemon.level = if ((adjustedLevel < 1)) 1 else min(adjustedLevel,100) + if (heal) effectedPokemon.heal() + return this + } + + /* + * TODO: call from mixin -> if BattlePokemon is ChallengeBattlePokemon in PokemonBattle.end() -> + * redirect here else -> + * call ExperienceCalculator.Calculate as normally done + * original call -> + * com.cobblemon.mod.common.api.pokemon.experience.ExperienceCalculator.Calculate + */ + fun calculateExperience(battlePokemon: BattlePokemon, opponentPokemon: BattlePokemon, participationMultiplier: Double): Int { + // This is meant to be a division but this is due to the intended behavior of handling the 2.0 sent over from Exp. All in modern Pokémon + + // Tweaked method to get exp gain that reflects that original pokemon's unaltered levels + val term2 = 1 * participationMultiplier + val victorPokemon = opponentPokemon.effectedPokemon + val victorLevel = victorPokemon.level + val baseExp = opponentPokemon.originalPokemon.form.baseExperienceYield + val opponentLevel = victorLevel + (opponentPokemon.effectedPokemon.level - battlePokemon.effectedPokemon.level) + val term1 = (baseExp * opponentLevel) / 5.0 + val term3 = (((2.0 * opponentLevel) + 10) / (opponentLevel + victorLevel + 10)).pow(2.5) + + // eight addition not yet in base mod -> can use ternary, but it is super wide, so I tried to keep it readable + val validOriginalTrainer = victorPokemon.originalTrainer != null && battlePokemon.actor.type == ActorType.PLAYER + val differentUuid = victorPokemon.originalTrainer != battlePokemon.actor.uuid.toString() + val validName = victorPokemon.originalTrainerName != null + val differentName = victorPokemon.originalTrainerName != battlePokemon.actor.getName().toString() + val nonOtBonus = when { + validOriginalTrainer && differentUuid && (!validName || differentName) -> 1.7 + validName && differentName && !validOriginalTrainer -> 1.7 + else -> 1.0 + } + + val luckyEggMultiplier = if (battlePokemon.effectedPokemon.heldItem().tags.anyMatch { tag -> + tag == CobblemonItemTags.LUCKY_EGG + }) Cobblemon.config.luckyEggMultiplier else 1.0 + val evolutionMultiplier = if (battlePokemon.effectedPokemon.evolutionProxy.server().any { evolution -> + val requirements = evolution.requirements.asSequence() + requirements.any { it is LevelRequirement } && requirements.all { it.check(battlePokemon.effectedPokemon) } + }) 1.2 else 1.0 + val affectionMultiplier = if (battlePokemon.effectedPokemon.friendship >= 220) 1.2 else 1.0 + val gimmickBoost = Cobblemon.config.experienceMultiplier + + val term4 = term1 * term2 * term3 + 1 + return (term4 * nonOtBonus * luckyEggMultiplier * evolutionMultiplier * affectionMultiplier * gimmickBoost).roundToInt() + } +} \ No newline at end of file diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 5ea5a4d..1b66f0f 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -19,7 +19,8 @@ "environment": "*", "entrypoints": { "main": [ - "com.turtlehoarder.cobblemonchallenge.CobblemonChallenge" + "com.turtlehoarder.cobblemonchallenge.CobblemonChallenge", + "com.cobblemontournament.CobblemonTournament" ] }, "depends": { diff --git a/src/main/resources/mixins.common.json b/src/main/resources/mixins.common.json new file mode 100644 index 0000000..a4947ed --- /dev/null +++ b/src/main/resources/mixins.common.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.turtlehoarder.cobblemonchallenge.mixin", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "BattleBuilderMixin" + ], + "client": [ + ], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file